)]}'
{"version": 3, "sources": ["/web/static/src/module_loader.js", "/web/static/lib/luxon/luxon.js", "/web/static/lib/owl/owl.js", "/web/static/lib/owl/odoo_module.js", "/web/static/src/env.js", "/web/static/src/session.js", "/web/static/src/core/action_swiper/action_swiper.js", "/web/static/src/core/anchor_scroll_prevention.js", "/web/static/src/core/assets.js", "/web/static/src/core/autocomplete/autocomplete.js", "/web/static/src/core/barcode/ZXingBarcodeDetector.js", "/web/static/src/core/barcode/barcode_dialog.js", "/web/static/src/core/barcode/barcode_video_scanner.js", "/web/static/src/core/barcode/crop_overlay.js", "/web/static/src/core/bottom_sheet/bottom_sheet.js", "/web/static/src/core/bottom_sheet/bottom_sheet_service.js", "/web/static/src/core/browser/browser.js", "/web/static/src/core/browser/cookie.js", "/web/static/src/core/browser/feature_detection.js", "/web/static/src/core/browser/router.js", "/web/static/src/core/browser/title_service.js", "/web/static/src/core/checkbox/checkbox.js", "/web/static/src/core/code_editor/code_editor.js", "/web/static/src/core/color_picker/color_picker.js", "/web/static/src/core/color_picker/custom_color_picker/custom_color_picker.js", "/web/static/src/core/color_picker/tabs/color_picker_custom_tab.js", "/web/static/src/core/color_picker/tabs/color_picker_solid_tab.js", "/web/static/src/core/colorlist/colorlist.js", "/web/static/src/core/colors/colors.js", "/web/static/src/core/commands/command_category.js", "/web/static/src/core/commands/command_hook.js", "/web/static/src/core/commands/command_palette.js", "/web/static/src/core/commands/command_service.js", "/web/static/src/core/commands/default_providers.js", "/web/static/src/core/confirmation_dialog/confirmation_dialog.js", "/web/static/src/core/context.js", "/web/static/src/core/copy_button/copy_button.js", "/web/static/src/core/currency.js", "/web/static/src/core/datetime/datetime_input.js", "/web/static/src/core/datetime/datetime_picker.js", "/web/static/src/core/datetime/datetime_picker_hook.js", "/web/static/src/core/datetime/datetime_picker_popover.js", "/web/static/src/core/datetime/datetimepicker_service.js", "/web/static/src/core/debug/debug_context.js", "/web/static/src/core/debug/debug_menu.js", "/web/static/src/core/debug/debug_menu_basic.js", "/web/static/src/core/debug/debug_menu_items.js", "/web/static/src/core/debug/debug_providers.js", "/web/static/src/core/debug/debug_utils.js", "/web/static/src/core/dialog/dialog.js", "/web/static/src/core/dialog/dialog_service.js", "/web/static/src/core/domain.js", "/web/static/src/core/domain_selector/domain_selector.js", "/web/static/src/core/domain_selector/domain_selector_operator_editor.js", "/web/static/src/core/domain_selector/utils.js", "/web/static/src/core/domain_selector_dialog/domain_selector_dialog.js", "/web/static/src/core/dropdown/_behaviours/dropdown_group_hook.js", "/web/static/src/core/dropdown/_behaviours/dropdown_nesting.js", "/web/static/src/core/dropdown/_behaviours/dropdown_popover.js", "/web/static/src/core/dropdown/accordion_item.js", "/web/static/src/core/dropdown/checkbox_item.js", "/web/static/src/core/dropdown/dropdown.js", "/web/static/src/core/dropdown/dropdown_group.js", "/web/static/src/core/dropdown/dropdown_hooks.js", "/web/static/src/core/dropdown/dropdown_item.js", "/web/static/src/core/dropzone/dropzone.js", "/web/static/src/core/dropzone/dropzone_hook.js", "/web/static/src/core/effects/effect_service.js", "/web/static/src/core/effects/rainbow_man.js", "/web/static/src/core/emoji_picker/emoji_picker.js", "/web/static/src/core/emoji_picker/frequent_emoji_service.js", "/web/static/src/core/ensure_jquery.js", "/web/static/src/core/errors/error_dialogs.js", "/web/static/src/core/errors/error_handlers.js", "/web/static/src/core/errors/error_service.js", "/web/static/src/core/errors/error_utils.js", "/web/static/src/core/errors/scss_error_dialog.js", "/web/static/src/core/expression_editor/expression_editor.js", "/web/static/src/core/expression_editor/expression_editor_operator_editor.js", "/web/static/src/core/expression_editor_dialog/expression_editor_dialog.js", "/web/static/src/core/field_service.js", "/web/static/src/core/file_input/file_input.js", "/web/static/src/core/file_upload/file_upload_progress_bar.js", "/web/static/src/core/file_upload/file_upload_progress_container.js", "/web/static/src/core/file_upload/file_upload_progress_record.js", "/web/static/src/core/file_upload/file_upload_service.js", "/web/static/src/core/file_viewer/file_model.js", "/web/static/src/core/file_viewer/file_viewer.js", "/web/static/src/core/file_viewer/file_viewer_hook.js", "/web/static/src/core/hotkeys/hotkey_hook.js", "/web/static/src/core/hotkeys/hotkey_service.js", "/web/static/src/core/install_scoped_app/install_scoped_app.js", "/web/static/src/core/ir_ui_view_code_editor/code_editor.js", "/web/static/src/core/l10n/dates.js", "/web/static/src/core/l10n/localization.js", "/web/static/src/core/l10n/localization_service.js", "/web/static/src/core/l10n/time.js", "/web/static/src/core/l10n/translation.js", "/web/static/src/core/l10n/utils.js", "/web/static/src/core/l10n/utils/format_list.js", "/web/static/src/core/l10n/utils/locales.js", "/web/static/src/core/l10n/utils/normalize.js", "/web/static/src/core/macro.js", "/web/static/src/core/main_components_container.js", "/web/static/src/core/model_field_selector/model_field_selector.js", "/web/static/src/core/model_field_selector/model_field_selector_popover.js", "/web/static/src/core/model_selector/model_selector.js", "/web/static/src/core/name_service.js", "/web/static/src/core/navigation/navigation.js", "/web/static/src/core/network/download.js", "/web/static/src/core/network/http_service.js", "/web/static/src/core/network/rpc.js", "/web/static/src/core/network/rpc_cache.js", "/web/static/src/core/notebook/notebook.js", "/web/static/src/core/notifications/notification.js", "/web/static/src/core/notifications/notification_container.js", "/web/static/src/core/notifications/notification_service.js", "/web/static/src/core/orm_service.js", "/web/static/src/core/overlay/overlay_container.js", "/web/static/src/core/overlay/overlay_service.js", "/web/static/src/core/pager/pager.js", "/web/static/src/core/pager/pager_indicator.js", "/web/static/src/core/popover/popover.js", "/web/static/src/core/popover/popover_hook.js", "/web/static/src/core/popover/popover_service.js", "/web/static/src/core/position/position_hook.js", "/web/static/src/core/position/utils.js", "/web/static/src/core/pwa/install_prompt.js", "/web/static/src/core/pwa/pwa_service.js", "/web/static/src/core/py_js/py.js", "/web/static/src/core/py_js/py_builtin.js", "/web/static/src/core/py_js/py_date.js", "/web/static/src/core/py_js/py_interpreter.js", "/web/static/src/core/py_js/py_parser.js", "/web/static/src/core/py_js/py_tokenizer.js", "/web/static/src/core/py_js/py_utils.js", "/web/static/src/core/record_selectors/multi_record_selector.js", "/web/static/src/core/record_selectors/record_autocomplete.js", "/web/static/src/core/record_selectors/record_selector.js", "/web/static/src/core/record_selectors/tag_navigation_hook.js", "/web/static/src/core/registry.js", "/web/static/src/core/registry_hook.js", "/web/static/src/core/resizable_panel/resizable_panel.js", "/web/static/src/core/select_menu/select_menu.js", "/web/static/src/core/signature/name_and_signature.js", "/web/static/src/core/signature/signature_dialog.js", "/web/static/src/core/tags_list/tags_list.js", "/web/static/src/core/template_inheritance.js", "/web/static/src/core/templates.js", "/web/static/src/core/time_picker/time_picker.js", "/web/static/src/core/tooltip/tooltip.js", "/web/static/src/core/tooltip/tooltip_hook.js", "/web/static/src/core/tooltip/tooltip_service.js", "/web/static/src/core/transition.js", "/web/static/src/core/tree_editor/ast_utils.js", "/web/static/src/core/tree_editor/condition_tree.js", "/web/static/src/core/tree_editor/construct_domain_from_tree.js", "/web/static/src/core/tree_editor/construct_expression_from_tree.js", "/web/static/src/core/tree_editor/construct_tree_from_domain.js", "/web/static/src/core/tree_editor/construct_tree_from_expression.js", "/web/static/src/core/tree_editor/domain_contains_expressions.js", "/web/static/src/core/tree_editor/domain_from_tree.js", "/web/static/src/core/tree_editor/expression_from_tree.js", "/web/static/src/core/tree_editor/operators.js", "/web/static/src/core/tree_editor/tree_editor.js", "/web/static/src/core/tree_editor/tree_editor_autocomplete.js", "/web/static/src/core/tree_editor/tree_editor_components.js", "/web/static/src/core/tree_editor/tree_editor_operator_editor.js", "/web/static/src/core/tree_editor/tree_editor_value_editors.js", "/web/static/src/core/tree_editor/tree_from_domain.js", "/web/static/src/core/tree_editor/tree_from_expression.js", "/web/static/src/core/tree_editor/tree_processor.js", "/web/static/src/core/tree_editor/utils.js", "/web/static/src/core/tree_editor/virtual_operators.js", "/web/static/src/core/ui/block_ui.js", "/web/static/src/core/ui/ui_service.js", "/web/static/src/core/user.js", "/web/static/src/core/user_switch/user_switch.js", "/web/static/src/core/utils/arrays.js", "/web/static/src/core/utils/autoresize.js", "/web/static/src/core/utils/binary.js", "/web/static/src/core/utils/cache.js", "/web/static/src/core/utils/classname.js", "/web/static/src/core/utils/colors.js", "/web/static/src/core/utils/components.js", "/web/static/src/core/utils/concurrency.js", "/web/static/src/core/utils/draggable.js", "/web/static/src/core/utils/draggable_hook_builder.js", "/web/static/src/core/utils/draggable_hook_builder_owl.js", "/web/static/src/core/utils/dvu.js", "/web/static/src/core/utils/files.js", "/web/static/src/core/utils/functions.js", "/web/static/src/core/utils/hooks.js", "/web/static/src/core/utils/html.js", "/web/static/src/core/utils/indexed_db.js", "/web/static/src/core/utils/misc.js", "/web/static/src/core/utils/nested_sortable.js", "/web/static/src/core/utils/numbers.js", "/web/static/src/core/utils/objects.js", "/web/static/src/core/utils/patch.js", "/web/static/src/core/utils/pdfjs.js", "/web/static/src/core/utils/reactive.js", "/web/static/src/core/utils/render.js", "/web/static/src/core/utils/scrolling.js", "/web/static/src/core/utils/search.js", "/web/static/src/core/utils/sortable.js", "/web/static/src/core/utils/sortable_owl.js", "/web/static/src/core/utils/sortable_service.js", "/web/static/src/core/utils/strings.js", "/web/static/src/core/utils/timing.js", "/web/static/src/core/utils/ui.js", "/web/static/src/core/utils/urls.js", "/web/static/src/core/utils/xml.js", "/web/static/src/core/virtual_grid_hook.js", "/web/static/src/polyfills/clipboard.js", "/web/static/lib/popper/popper.js", "/web/static/lib/bootstrap/js/dist/util/index.js", "/web/static/lib/bootstrap/js/dist/dom/data.js", "/web/static/lib/bootstrap/js/dist/dom/event-handler.js", "/web/static/lib/bootstrap/js/dist/dom/manipulator.js", "/web/static/lib/bootstrap/js/dist/dom/selector-engine.js", "/web/static/lib/bootstrap/js/dist/util/config.js", "/web/static/lib/bootstrap/js/dist/util/component-functions.js", "/web/static/lib/bootstrap/js/dist/util/backdrop.js", "/web/static/lib/bootstrap/js/dist/util/focustrap.js", "/web/static/lib/bootstrap/js/dist/util/sanitizer.js", "/web/static/lib/bootstrap/js/dist/util/scrollbar.js", "/web/static/lib/bootstrap/js/dist/util/swipe.js", "/web/static/lib/bootstrap/js/dist/util/template-factory.js", "/web/static/lib/bootstrap/js/dist/base-component.js", "/web/static/lib/bootstrap/js/dist/alert.js", "/web/static/lib/bootstrap/js/dist/button.js", "/web/static/lib/bootstrap/js/dist/carousel.js", "/web/static/lib/bootstrap/js/dist/collapse.js", "/web/static/lib/bootstrap/js/dist/dropdown.js", "/web/static/lib/bootstrap/js/dist/modal.js", "/web/static/lib/bootstrap/js/dist/offcanvas.js", "/web/static/lib/bootstrap/js/dist/tooltip.js", "/web/static/lib/bootstrap/js/dist/popover.js", "/web/static/lib/bootstrap/js/dist/scrollspy.js", "/web/static/lib/bootstrap/js/dist/tab.js", "/web/static/lib/bootstrap/js/dist/toast.js", "/web/static/src/libs/bootstrap.js", "/web/static/lib/dompurify/DOMpurify.js", "/web/static/src/model/model.js", "/web/static/src/model/record.js", "/web/static/src/model/relational_model/datapoint.js", "/web/static/src/model/relational_model/dynamic_group_list.js", "/web/static/src/model/relational_model/dynamic_list.js", "/web/static/src/model/relational_model/dynamic_record_list.js", "/web/static/src/model/relational_model/errors.js", "/web/static/src/model/relational_model/group.js", "/web/static/src/model/relational_model/operation.js", "/web/static/src/model/relational_model/record.js", "/web/static/src/model/relational_model/relational_model.js", "/web/static/src/model/relational_model/static_list.js", "/web/static/src/model/relational_model/utils.js", "/web/static/src/model/sample_server.js", "/web/static/src/search/action_hook.js", "/web/static/src/search/action_menus/action_menus.js", "/web/static/src/search/breadcrumbs/breadcrumbs.js", "/web/static/src/search/cog_menu/cog_menu.js", "/web/static/src/search/control_panel/control_panel.js", "/web/static/src/search/custom_favorite_item/custom_favorite_item.js", "/web/static/src/search/custom_group_by_item/custom_group_by_item.js", "/web/static/src/search/layout.js", "/web/static/src/search/pager_hook.js", "/web/static/src/search/properties_group_by_item/properties_group_by_item.js", "/web/static/src/search/search_arch_parser.js", "/web/static/src/search/search_bar/search_bar.js", "/web/static/src/search/search_bar/search_bar_toggler.js", "/web/static/src/search/search_bar_menu/search_bar_menu.js", "/web/static/src/search/search_model.js", "/web/static/src/search/search_panel/search_panel.js", "/web/static/src/search/utils/dates.js", "/web/static/src/search/utils/group_by.js", "/web/static/src/search/utils/misc.js", "/web/static/src/search/utils/order_by.js", "/web/static/src/search/with_search/with_search.js", "/web/static/src/views/action_helper.js", "/web/static/src/views/calendar/calendar_arch_parser.js", "/web/static/src/views/calendar/calendar_common/calendar_common_popover.js", "/web/static/src/views/calendar/calendar_common/calendar_common_renderer.js", "/web/static/src/views/calendar/calendar_common/calendar_common_week_column.js", "/web/static/src/views/calendar/calendar_controller.js", "/web/static/src/views/calendar/calendar_filter_section/calendar_filter_section.js", "/web/static/src/views/calendar/calendar_model.js", "/web/static/src/views/calendar/calendar_renderer.js", "/web/static/src/views/calendar/calendar_side_panel/calendar_side_panel.js", "/web/static/src/views/calendar/calendar_view.js", "/web/static/src/views/calendar/calendar_year/calendar_year_popover.js", "/web/static/src/views/calendar/calendar_year/calendar_year_renderer.js", "/web/static/src/views/calendar/hooks/calendar_popover_hook.js", "/web/static/src/views/calendar/hooks/full_calendar_hook.js", "/web/static/src/views/calendar/hooks/square_selection_hook.js", "/web/static/src/views/calendar/mobile_filter_panel/calendar_mobile_filter_panel.js", "/web/static/src/views/calendar/quick_create/calendar_quick_create.js", "/web/static/src/views/calendar/utils.js", "/web/static/src/views/debug_items.js", "/web/static/src/views/fields/ace/ace_field.js", "/web/static/src/views/fields/attachment_image/attachment_image_field.js", "/web/static/src/views/fields/badge/badge_field.js", "/web/static/src/views/fields/badge_selection/badge_selection_field.js", "/web/static/src/views/fields/badge_selection/list_badge_selection_field.js", "/web/static/src/views/fields/badge_selection_with_filter/badge_selection_field_with_filter.js", "/web/static/src/views/fields/binary/binary_field.js", "/web/static/src/views/fields/boolean/boolean_field.js", "/web/static/src/views/fields/boolean_favorite/boolean_favorite_field.js", "/web/static/src/views/fields/boolean_icon/boolean_icon_field.js", "/web/static/src/views/fields/boolean_toggle/boolean_toggle_field.js", "/web/static/src/views/fields/boolean_toggle/list_boolean_toggle_field.js", "/web/static/src/views/fields/char/char_field.js", "/web/static/src/views/fields/color/color_field.js", "/web/static/src/views/fields/color_picker/color_picker_field.js", "/web/static/src/views/fields/contact_image/contact_image_field.js", "/web/static/src/views/fields/contact_statistics/contact_statistics.js", "/web/static/src/views/fields/copy_clipboard/copy_clipboard_field.js", "/web/static/src/views/fields/datetime/datetime_field.js", "/web/static/src/views/fields/datetime/list_datetime_field.js", "/web/static/src/views/fields/domain/domain_field.js", "/web/static/src/views/fields/dynamic_placeholder_hook.js", "/web/static/src/views/fields/dynamic_placeholder_popover.js", "/web/static/src/views/fields/email/email_field.js", "/web/static/src/views/fields/field.js", "/web/static/src/views/fields/field_selector/field_selector_field.js", "/web/static/src/views/fields/field_tooltip.js", "/web/static/src/views/fields/file_handler.js", "/web/static/src/views/fields/float/float_field.js", "/web/static/src/views/fields/float_factor/float_factor_field.js", "/web/static/src/views/fields/float_time/float_time_field.js", "/web/static/src/views/fields/float_toggle/float_toggle_field.js", "/web/static/src/views/fields/formatters.js", "/web/static/src/views/fields/gauge/gauge_field.js", "/web/static/src/views/fields/google_slide_viewer/google_slide_viewer.js", "/web/static/src/views/fields/handle/handle_field.js", "/web/static/src/views/fields/html/html_field.js", "/web/static/src/views/fields/iframe_wrapper/iframe_wrapper_field.js", "/web/static/src/views/fields/image/image_field.js", "/web/static/src/views/fields/image_url/image_url_field.js", "/web/static/src/views/fields/input_field_hook.js", "/web/static/src/views/fields/integer/integer_field.js", "/web/static/src/views/fields/ir_ui_view_ace/ace_field.js", "/web/static/src/views/fields/journal_dashboard_graph/journal_dashboard_graph_field.js", "/web/static/src/views/fields/json/json_field.js", "/web/static/src/views/fields/json_checkboxes/json_checkboxes_field.js", "/web/static/src/views/fields/kanban_color_picker/kanban_color_picker_field.js", "/web/static/src/views/fields/label_selection/label_selection_field.js", "/web/static/src/views/fields/many2many_binary/many2many_binary_field.js", "/web/static/src/views/fields/many2many_checkboxes/many2many_checkboxes_field.js", "/web/static/src/views/fields/many2many_tags/kanban_many2many_tags_field.js", "/web/static/src/views/fields/many2many_tags/many2many_tags_field.js", "/web/static/src/views/fields/many2many_tags_avatar/many2many_tags_avatar_field.js", "/web/static/src/views/fields/many2one/many2one.js", "/web/static/src/views/fields/many2one/many2one_field.js", "/web/static/src/views/fields/many2one_avatar/kanban_many2one_avatar_field.js", "/web/static/src/views/fields/many2one_avatar/many2one_avatar_field.js", "/web/static/src/views/fields/many2one_barcode/many2one_barcode_field.js", "/web/static/src/views/fields/many2one_reference/many2one_reference_field.js", "/web/static/src/views/fields/many2one_reference_integer/many2one_reference_integer_field.js", "/web/static/src/views/fields/monetary/monetary_field.js", "/web/static/src/views/fields/numpad_decimal_hook.js", "/web/static/src/views/fields/parsers.js", "/web/static/src/views/fields/pdf_viewer/pdf_viewer_field.js", "/web/static/src/views/fields/percent_pie/percent_pie_field.js", "/web/static/src/views/fields/percentage/percentage_field.js", "/web/static/src/views/fields/phone/phone_field.js", "/web/static/src/views/fields/priority/priority_field.js", "/web/static/src/views/fields/progress_bar/kanban_progress_bar_field.js", "/web/static/src/views/fields/progress_bar/progress_bar_field.js", "/web/static/src/views/fields/properties/calendar_properties_field.js", "/web/static/src/views/fields/properties/card_properties_field.js", "/web/static/src/views/fields/properties/properties_field.js", "/web/static/src/views/fields/properties/property_definition.js", "/web/static/src/views/fields/properties/property_definition_selection.js", "/web/static/src/views/fields/properties/property_tags.js", "/web/static/src/views/fields/properties/property_text.js", "/web/static/src/views/fields/properties/property_value.js", "/web/static/src/views/fields/radio/radio_field.js", "/web/static/src/views/fields/reference/reference_field.js", "/web/static/src/views/fields/relational_utils.js", "/web/static/src/views/fields/remaining_days/remaining_days_field.js", "/web/static/src/views/fields/selection/filterable_selection_field.js", "/web/static/src/views/fields/selection/selection_field.js", "/web/static/src/views/fields/signature/signature_field.js", "/web/static/src/views/fields/standard_field_props.js", "/web/static/src/views/fields/stat_info/stat_info_field.js", "/web/static/src/views/fields/state_selection/state_selection_field.js", "/web/static/src/views/fields/statusbar/statusbar_field.js", "/web/static/src/views/fields/text/text_field.js", "/web/static/src/views/fields/timezone_mismatch/timezone_mismatch_field.js", "/web/static/src/views/fields/translation_button.js", "/web/static/src/views/fields/translation_dialog.js", "/web/static/src/views/fields/url/url_field.js", "/web/static/src/views/fields/x2many/list_x2many_field.js", "/web/static/src/views/fields/x2many/x2many_field.js", "/web/static/src/views/form/button_box/button_box.js", "/web/static/src/views/form/form_arch_parser.js", "/web/static/src/views/form/form_cog_menu/form_cog_menu.js", "/web/static/src/views/form/form_compiler.js", "/web/static/src/views/form/form_controller.js", "/ai/static/src/web/form_controller_patch.js", "/knowledge/static/src/web/form_controller_patch.js", "/web/static/src/views/form/form_error_dialog/form_error_dialog.js", "/web/static/src/views/form/form_group/form_group.js", "/web/static/src/views/form/form_label.js", "/web/static/src/views/form/form_renderer.js", "/web/static/src/views/form/form_status_indicator/form_status_indicator.js", "/web/static/src/views/form/form_view.js", "/web/static/src/views/form/setting/setting.js", "/web/static/src/views/form/status_bar_buttons/status_bar_buttons.js", "/web/static/src/views/kanban/kanban_arch_parser.js", "/web/static/src/views/kanban/kanban_cog_menu.js", "/web/static/src/views/kanban/kanban_column_examples_dialog.js", "/web/static/src/views/kanban/kanban_column_quick_create.js", "/web/static/src/views/kanban/kanban_compiler.js", "/web/static/src/views/kanban/kanban_controller.js", "/web/static/src/views/kanban/kanban_cover_image_dialog.js", "/web/static/src/views/kanban/kanban_dropdown_menu_wrapper.js", "/web/static/src/views/kanban/kanban_header.js", "/web/static/src/views/kanban/kanban_record.js", "/web/static/src/views/kanban/kanban_record_quick_create.js", "/web/static/src/views/kanban/kanban_renderer.js", "/web/static/src/views/kanban/kanban_view.js", "/web/static/src/views/kanban/progress_bar_hook.js", "/web/static/src/views/list/column_width_hook.js", "/web/static/src/views/list/export_all/export_all.js", "/web/static/src/views/list/list_arch_parser.js", "/web/static/src/views/list/list_cog_menu.js", "/web/static/src/views/list/list_confirmation_dialog.js", "/web/static/src/views/list/list_controller.js", "/web/static/src/views/list/list_renderer.js", "/web/static/src/views/list/list_view.js", "/web/static/src/views/standard_view_props.js", "/web/static/src/views/utils.js", "/web/static/src/views/view.js", "/web/static/src/views/view_button/multi_record_view_button.js", "/web/static/src/views/view_button/view_button.js", "/web/static/src/views/view_button/view_button_hook.js", "/web/static/src/views/view_compiler.js", "/web/static/src/views/view_components/animated_number.js", "/web/static/src/views/view_components/column_progress.js", "/web/static/src/views/view_components/group_config_menu.js", "/web/static/src/views/view_components/multi_create_popover.js", "/web/static/src/views/view_components/multi_currency_popover.js", "/web/static/src/views/view_components/multi_selection_buttons.js", "/web/static/src/views/view_components/report_view_measures.js", "/web/static/src/views/view_components/selection_box.js", "/web/static/src/views/view_components/view_scale_selector.js", "/web/static/src/views/view_dialogs/export_data_dialog.js", "/web/static/src/views/view_dialogs/form_view_dialog.js", "/web/static/src/views/view_dialogs/select_create_dialog.js", "/web/static/src/views/view_hook.js", "/web/static/src/views/view_service.js", "/web/static/src/views/widgets/attach_document/attach_document.js", "/web/static/src/views/widgets/documentation_link/documentation_link.js", "/web/static/src/views/widgets/notification_alert/notification_alert.js", "/web/static/src/views/widgets/ribbon/ribbon.js", "/web/static/src/views/widgets/signature/signature.js", "/web/static/src/views/widgets/standard_widget_props.js", "/web/static/src/views/widgets/week_days/week_days.js", "/web/static/src/views/widgets/widget.js", "/web/static/src/webclient/actions/action_container.js", "/web/static/src/webclient/actions/action_dialog.js", "/web/static/src/webclient/actions/action_install_kiosk_pwa.js", "/web/static/src/webclient/actions/action_service.js", "/web/static/src/webclient/actions/client_actions.js", "/web/static/src/webclient/actions/debug_items.js", "/web/static/src/webclient/burger_menu/burger_menu.js", "/web/static/src/webclient/burger_menu/burger_user_menu/burger_user_menu.js", "/web/static/src/webclient/burger_menu/mobile_switch_company_menu/mobile_switch_company_menu.js", "/web/static/src/webclient/clickbot/clickbot_loader.js", "/web/static/src/webclient/currency_service.js", "/web/static/src/webclient/debug/debug_items.js", "/web/static/src/webclient/debug/profiling/profiling_item.js", "/web/static/src/webclient/debug/profiling/profiling_qweb.js", "/web/static/src/webclient/debug/profiling/profiling_service.js", "/web/static/src/webclient/debug/profiling/profiling_systray_item.js", "/web/static/src/webclient/errors/offline_fail_to_fetch_error_handler.js", "/web/static/src/webclient/loading_indicator/loading_indicator.js", "/web/static/src/webclient/menus/menu_helpers.js", "/web/static/src/webclient/menus/menu_providers.js", "/web/static/src/webclient/menus/menu_service.js", "/web/static/src/webclient/navbar/navbar.js", "/web/static/src/webclient/reload_company_service.js", "/web/static/src/webclient/res_user_group_ids_field/res_user_group_ids_field.js", "/web/static/src/webclient/res_user_group_ids_field/res_user_group_ids_popover.js", "/web/static/src/webclient/res_user_group_ids_field/res_user_group_ids_privilege_field.js", "/web/static/src/webclient/session_service.js", "/web/static/src/webclient/settings_form_view/fields/settings_binary_field/settings_binary_field.js", "/web/static/src/webclient/settings_form_view/fields/upgrade_boolean_field.js", "/web/static/src/webclient/settings_form_view/fields/upgrade_dialog.js", "/web/static/src/webclient/settings_form_view/highlight_text/form_label_highlight_text.js", "/web/static/src/webclient/settings_form_view/highlight_text/highlight_text.js", "/web/static/src/webclient/settings_form_view/highlight_text/settings_radio_field.js", "/web/static/src/webclient/settings_form_view/settings/searchable_setting.js", "/web/static/src/webclient/settings_form_view/settings/setting_header.js", "/web/static/src/webclient/settings_form_view/settings/settings_app.js", "/web/static/src/webclient/settings_form_view/settings/settings_block.js", "/web/static/src/webclient/settings_form_view/settings/settings_page.js", "/web/static/src/webclient/settings_form_view/settings_confirmation_dialog.js", "/web/static/src/webclient/settings_form_view/settings_form_compiler.js", "/web/static/src/webclient/settings_form_view/settings_form_controller.js", "/web/static/src/webclient/settings_form_view/settings_form_renderer.js", "/web/static/src/webclient/settings_form_view/settings_form_view.js", "/web/static/src/webclient/settings_form_view/widgets/demo_data_service.js", "/web/static/src/webclient/settings_form_view/widgets/res_config_dev_tool.js", "/web/static/src/webclient/settings_form_view/widgets/res_config_edition.js", "/web/static/src/webclient/settings_form_view/widgets/res_config_invite_users.js", "/web/static/src/webclient/settings_form_view/widgets/user_invite_service.js", "/web/static/src/webclient/share_target/share_target_service.js", "/web/static/src/webclient/switch_company_menu/switch_company_item.js", "/web/static/src/webclient/switch_company_menu/switch_company_menu.js", "/web/static/src/webclient/user_menu/user_menu.js", "/web/static/src/webclient/user_menu/user_menu_items.js", "/web/static/src/webclient/webclient.js", "/web/static/src/webclient/actions/reports/report_action.js", "/web/static/src/webclient/actions/reports/report_hook.js", "/web/static/src/webclient/actions/reports/utils.js", "/base_setup/static/src/views/module_views.js", "/bus/static/src/bus_parameters_service.js", "/bus/static/src/legacy_multi_tab_service.js", "/bus/static/src/misc.js", "/bus/static/src/multi_tab_fallback_service.js", "/bus/static/src/multi_tab_service.js", "/bus/static/src/multi_tab_shared_worker_service.js", "/bus/static/src/outdated_page_watcher_service.js", "/bus/static/src/simple_notification_service.js", "/bus/static/src/debug/bus_logs_menu_item.js", "/bus/static/src/debug/bus_logs_service.js", "/bus/static/src/services/assets_watchdog_service.js", "/bus/static/src/services/bus_monitoring_service.js", "/bus/static/src/services/bus_service.js", "/bus/static/src/services/presence_service.js", "/bus/static/src/services/worker_service.js", "/bus/static/src/workers/base_worker.js", "/bus/static/src/workers/bus_worker_utils.js", "/bus/static/src/workers/election_worker.js", "/bus/static/src/workers/websocket_worker.js", "/web_tour/static/src/js/tour_pointer/tour_pointer.js", "/web_tour/static/src/js/tour_pointer/tour_pointer_state.js", "/web_tour/static/src/js/utils/tour_utils.js", "/web_tour/static/src/js/tour_state.js", "/web_tour/static/src/js/tour_service.js", "/web_tour/static/src/js/tour_recorder/tour_recorder_state.js", "/web_tour/static/src/tour_utils.js", "/web_tour/static/src/views/tour_list.js", "/web_tour/static/src/widgets/tour_start.js", "/html_editor/static/src/components/switch/switch.js", "/html_editor/static/src/main/media/media_dialog/document_selector.js", "/html_editor/static/src/main/media/media_dialog/file_selector.js", "/html_editor/static/src/main/media/media_dialog/icon_selector.js", "/html_editor/static/src/main/media/media_dialog/image_selector.js", "/html_editor/static/src/main/media/media_dialog/media_dialog.js", "/html_editor/static/src/main/media/media_dialog/search_media.js", "/html_editor/static/src/main/media/media_dialog/upload_progress_toast/upload_progress_toast.js", "/html_editor/static/src/main/media/media_dialog/upload_progress_toast/upload_service.js", "/html_editor/static/src/main/media/media_dialog/video_selector.js", "/website/static/src/components/media_dialog/image_selector.js", "/web_unsplash/static/src/media_dialog/image_selector_patch.js", "/web_unsplash/static/src/media_dialog/media_dialog_patch.js", "/web_unsplash/static/src/unsplash_credentials/unsplash_credentials.js", "/web_unsplash/static/src/unsplash_error/unsplash_error.js", "/web_unsplash/static/src/unsplash_service.js", "/html_editor/static/src/components/html_viewer/html_viewer.js", "/html_editor/static/src/local_overlay_container.js", "/html_editor/static/src/position_hook.js", "/html_editor/static/src/html_migrations/html_migrations_utils.js", "/html_editor/static/src/html_migrations/html_upgrade_manager.js", "/html_editor/static/src/html_migrations/manifest.js", "/html_editor/static/src/html_migrations/migration-1.1.js", "/html_editor/static/src/html_migrations/migration-1.2.js", "/html_editor/static/src/others/embedded_component_utils.js", "/html_editor/static/src/others/embedded_components/core/embedded_component_toolbar/embedded_component_toolbar.js", "/html_editor/static/src/others/embedded_components/core/file/readonly_file.js", "/html_editor/static/src/others/embedded_components/core/file/state_file_model.js", "/html_editor/static/src/others/embedded_components/core/syntax_highlighting/readonly_syntax_highlighting.js", "/html_editor/static/src/others/embedded_components/core/syntax_highlighting/syntax_highlighting_utils.js", "/html_editor/static/src/others/embedded_components/core/table_of_content/table_of_content.js", "/html_editor/static/src/others/embedded_components/core/table_of_content/table_of_content_manager.js", "/html_editor/static/src/others/embedded_components/core/toggle_block/toggle_block.js", "/html_editor/static/src/others/embedded_components/core/video/readonly_video.js", "/html_editor/static/src/utils/base_container.js", "/html_editor/static/src/utils/blocks.js", "/html_editor/static/src/utils/clipboard.js", "/html_editor/static/src/utils/color.js", "/html_editor/static/src/utils/content_types.js", "/html_editor/static/src/utils/dom.js", "/html_editor/static/src/utils/dom_info.js", "/html_editor/static/src/utils/dom_state.js", "/html_editor/static/src/utils/dom_traversal.js", "/html_editor/static/src/utils/drag_and_drop.js", "/html_editor/static/src/utils/fonts.js", "/html_editor/static/src/utils/formatting.js", "/html_editor/static/src/utils/functions.js", "/html_editor/static/src/utils/html.js", "/html_editor/static/src/utils/image.js", "/html_editor/static/src/utils/image_processing.js", "/html_editor/static/src/utils/perspective_utils.js", "/html_editor/static/src/utils/position.js", "/html_editor/static/src/utils/regex.js", "/html_editor/static/src/utils/resource.js", "/html_editor/static/src/utils/sanitize.js", "/html_editor/static/src/utils/selection.js", "/html_editor/static/src/utils/table.js", "/html_editor/static/src/utils/tracking.js", "/html_editor/static/src/utils/url.js", "/html_editor/static/src/dropdown_autovisibility_hook.js", "/html_editor/static/src/editor.js", "/html_editor/static/src/plugin.js", "/html_editor/static/src/plugin_sets.js", "/html_editor/static/src/wysiwyg.js", "/html_editor/static/src/components/history_dialog/history_dialog.js", "/html_editor/static/src/core/base_container_plugin.js", "/html_editor/static/src/core/clipboard_plugin.js", "/html_editor/static/src/core/comment_plugin.js", "/html_editor/static/src/core/content_editable_plugin.js", "/html_editor/static/src/core/delete_plugin.js", "/html_editor/static/src/core/dialog_plugin.js", "/html_editor/static/src/core/dom_plugin.js", "/html_editor/static/src/core/editor_version_plugin.js", "/html_editor/static/src/core/format_plugin.js", "/html_editor/static/src/core/history_plugin.js", "/html_editor/static/src/core/input_plugin.js", "/html_editor/static/src/core/line_break_plugin.js", "/html_editor/static/src/core/no_inline_root_plugin.js", "/html_editor/static/src/core/overlay.js", "/html_editor/static/src/core/overlay_plugin.js", "/html_editor/static/src/core/protected_node_plugin.js", "/html_editor/static/src/core/sanitize_plugin.js", "/html_editor/static/src/core/selection_plugin.js", "/html_editor/static/src/core/shortcut_plugin.js", "/html_editor/static/src/core/split_plugin.js", "/html_editor/static/src/core/style_plugin.js", "/html_editor/static/src/core/user_command_plugin.js", "/html_editor/static/src/main/align/align_plugin.js", "/html_editor/static/src/main/align/align_selector.js", "/html_editor/static/src/main/banner_plugin.js", "/html_editor/static/src/main/chatgpt/chatgpt_dialog.js", "/html_editor/static/src/main/chatgpt/chatgpt_translate_dialog.js", "/html_editor/static/src/main/chatgpt/chatgpt_translate_plugin.js", "/html_editor/static/src/main/chatgpt/language_selector.js", "/html_editor/static/src/main/column_plugin.js", "/html_editor/static/src/main/emoji_plugin.js", "/html_editor/static/src/main/feff_plugin.js", "/html_editor/static/src/main/font/color_picker_gradient_tab.js", "/html_editor/static/src/main/font/color_plugin.js", "/html_editor/static/src/main/font/color_selector.js", "/html_editor/static/src/main/font/color_ui_plugin.js", "/html_editor/static/src/main/font/font_family_plugin.js", "/html_editor/static/src/main/font/font_family_selector.js", "/html_editor/static/src/main/font/font_plugin.js", "/html_editor/static/src/main/font/font_selector.js", "/html_editor/static/src/main/font/font_size_selector.js", "/html_editor/static/src/main/font/gradient_picker/gradient_picker.js", "/html_editor/static/src/main/hint_plugin.js", "/html_editor/static/src/main/inline_code.js", "/html_editor/static/src/main/link/command_category.js", "/html_editor/static/src/main/link/link_paste_plugin.js", "/html_editor/static/src/main/link/link_plugin.js", "/html_editor/static/src/main/link/link_popover.js", "/html_editor/static/src/main/link/link_selection_odoo_plugin.js", "/html_editor/static/src/main/link/link_selection_plugin.js", "/html_editor/static/src/main/link/powerbox_url_paste_plugin.js", "/html_editor/static/src/main/link/utils.js", "/html_editor/static/src/main/list/list_plugin.js", "/html_editor/static/src/main/list/list_selector.js", "/html_editor/static/src/main/list/utils.js", "/html_editor/static/src/main/local_overlay_plugin.js", "/html_editor/static/src/main/media/dblclick_image_preview_plugin.js", "/html_editor/static/src/main/media/file_plugin.js", "/html_editor/static/src/main/media/icon_color_plugin.js", "/html_editor/static/src/main/media/icon_plugin.js", "/html_editor/static/src/main/media/image_crop.js", "/html_editor/static/src/main/media/image_crop_plugin.js", "/html_editor/static/src/main/media/image_description.js", "/html_editor/static/src/main/media/image_plugin.js", "/html_editor/static/src/main/media/image_post_process_plugin.js", "/html_editor/static/src/main/media/image_save_plugin.js", "/html_editor/static/src/main/media/image_toolbar_dropdown.js", "/html_editor/static/src/main/media/image_transform_button.js", "/html_editor/static/src/main/media/image_transformation.js", "/html_editor/static/src/main/media/media_plugin.js", "/html_editor/static/src/main/media/video_plugin.js", "/html_editor/static/src/main/movenode_plugin.js", "/html_editor/static/src/main/placeholder_plugin.js", "/html_editor/static/src/main/position_plugin.js", "/html_editor/static/src/main/power_buttons_plugin.js", "/html_editor/static/src/main/powerbox/powerbox.js", "/html_editor/static/src/main/powerbox/powerbox_plugin.js", "/html_editor/static/src/main/powerbox/search_powerbox_plugin.js", "/html_editor/static/src/main/selection_placeholder_plugin.js", "/html_editor/static/src/main/separator_plugin.js", "/html_editor/static/src/main/star_plugin.js", "/html_editor/static/src/main/table/table_align_plugin.js", "/html_editor/static/src/main/table/table_align_selector.js", "/html_editor/static/src/main/table/table_menu.js", "/html_editor/static/src/main/table/table_picker.js", "/html_editor/static/src/main/table/table_plugin.js", "/html_editor/static/src/main/table/table_resize_plugin.js", "/html_editor/static/src/main/table/table_ui_plugin.js", "/html_editor/static/src/main/tabulation_plugin.js", "/html_editor/static/src/main/text_direction_plugin.js", "/html_editor/static/src/main/toolbar/mobile_toolbar.js", "/html_editor/static/src/main/toolbar/toolbar.js", "/html_editor/static/src/main/toolbar/toolbar_plugin.js", "/html_editor/static/src/main/youtube_plugin.js", "/html_editor/static/src/others/collaboration/PeerToPeer.js", "/html_editor/static/src/others/collaboration/collaboration_odoo_plugin.js", "/html_editor/static/src/others/collaboration/collaboration_plugin.js", "/html_editor/static/src/others/collaboration/collaboration_selection_avatar_plugin.js", "/html_editor/static/src/others/collaboration/collaboration_selection_plugin.js", "/html_editor/static/src/others/embedded_components/backend/caption/caption.js", "/html_editor/static/src/others/embedded_components/backend/file/file.js", "/html_editor/static/src/others/embedded_components/backend/syntax_highlighting/code_toolbar.js", "/html_editor/static/src/others/embedded_components/backend/syntax_highlighting/syntax_highlighting.js", "/html_editor/static/src/others/embedded_components/backend/video/video.js", "/html_editor/static/src/others/embedded_components/embedding_sets.js", "/html_editor/static/src/others/embedded_components/plugins/caption_plugin/caption_plugin.js", "/html_editor/static/src/others/embedded_components/plugins/embedded_file_plugin/embedded_file_documents_selector.js", "/html_editor/static/src/others/embedded_components/plugins/embedded_file_plugin/embedded_file_plugin.js", "/html_editor/static/src/others/embedded_components/plugins/syntax_highlighting_plugin/syntax_highlighting_plugin.js", "/html_editor/static/src/others/embedded_components/plugins/table_of_content_plugin/table_of_content_plugin.js", "/html_editor/static/src/others/embedded_components/plugins/toggle_block_plugin/toggle_block_plugin.js", "/html_editor/static/src/others/embedded_components/plugins/video_plugin/embedded_video_plugin.js", "/html_editor/static/src/others/embedded_components/plugins/video_plugin/embedded_youtube_plugin.js", "/html_editor/static/src/others/embedded_components/plugins/video_plugin/video_selector_dialog/embedded_video_selector.js", "/html_editor/static/src/others/embedded_component_plugin.js", "/html_editor/static/src/others/qweb_picker.js", "/html_editor/static/src/others/qweb_plugin.js", "/html_editor/static/src/services/upload_local_files_service.js", "/html_editor/static/src/others/dynamic_placeholder_plugin.js", "/html_editor/static/src/backend/plugin_sets.js", "/html_editor/static/src/fields/html_field.js", "/html_editor/static/src/fields/property_value.js", "/html_editor/static/src/fields/x2many_field/custom_media_dialog.js", "/html_editor/static/src/fields/x2many_field/x2many_image_field.js", "/html_editor/static/src/fields/x2many_field/x2many_media_viewer.js", "/html_editor/static/lib/vkbeautify/vkbeautify.0.99.00.beta.js", "/mail/static/lib/idb-keyval/idb-keyval.js", "/mail/static/lib/selfie_segmentation/selfie_segmentation.js", "/mail/static/src/js/emojis_mixin.js", "/mail/static/src/js/onchange_on_keydown.js", "/mail/static/src/js/rotting_mixin/rotting_column_progress.js", "/mail/static/src/js/rotting_mixin/rotting_kanban_controller.js", "/mail/static/src/js/rotting_mixin/rotting_kanban_header.js", "/mail/static/src/js/rotting_mixin/rotting_kanban_record.js", "/mail/static/src/js/rotting_mixin/rotting_kanban_renderer.js", "/mail/static/src/js/rotting_mixin/rotting_kanban_view.js", "/mail/static/src/js/rotting_mixin/rotting_progress_bar_hook.js", "/mail/static/src/js/rotting_mixin/rotting_statusbar.js", "/mail/static/src/js/rotting_mixin/rotting_widget.js", "/mail/static/src/js/tools/debug_manager.js", "/mail/static/src/js/tours/discuss_channel_tour.js", "/mail/static/src/model/export.js", "/mail/static/src/model/make_store.js", "/mail/static/src/model/misc.js", "/mail/static/src/model/model_internal.js", "/mail/static/src/model/record.js", "/mail/static/src/model/record_internal.js", "/mail/static/src/model/record_list.js", "/mail/static/src/model/record_uses.js", "/mail/static/src/model/store.js", "/mail/static/src/model/store_internal.js", "/mail/static/src/core/common/action.js", "/mail/static/src/core/common/action_list.js", "/mail/static/src/core/common/activity_model.js", "/mail/static/src/core/common/attachment_list.js", "/mail/static/src/core/common/attachment_model.js", "/mail/static/src/core/common/attachment_upload_service.js", "/mail/static/src/core/common/attachment_uploader_hook.js", "/mail/static/src/core/common/attachment_view.js", "/mail/static/src/core/common/autoresize_input.js", "/mail/static/src/core/common/canned_response_model.js", "/mail/static/src/core/common/chat_bubble.js", "/mail/static/src/core/common/chat_hub.js", "/mail/static/src/core/common/chat_hub_model.js", "/mail/static/src/core/common/chat_window.js", "/mail/static/src/core/common/chat_window_model.js", "/mail/static/src/core/common/composer.js", "/mail/static/src/core/common/composer_actions.js", "/mail/static/src/core/common/composer_model.js", "/mail/static/src/core/common/composer_service.js", "/mail/static/src/core/common/country_flag.js", "/mail/static/src/core/common/country_model.js", "/mail/static/src/core/common/data_response_model.js", "/mail/static/src/core/common/date_section.js", "/mail/static/src/core/common/discuss_call_history_model.js", "/mail/static/src/core/common/discuss_component_registry.js", "/mail/static/src/core/common/failure_model.js", "/mail/static/src/core/common/follower_model.js", "/mail/static/src/core/common/gif.js", "/mail/static/src/core/common/im_status.js", "/mail/static/src/core/common/im_status_dropdown.js", "/mail/static/src/core/common/im_status_service.js", "/mail/static/src/core/common/link_preview.js", "/mail/static/src/core/common/link_preview_confirm_delete.js", "/mail/static/src/core/common/link_preview_model.js", "/mail/static/src/core/common/mail_activity_type_model.js", "/mail/static/src/core/common/mail_attachment_dropzone.js", "/mail/static/src/core/common/mail_core_common_service.js", "/mail/static/src/core/common/mail_fullscreen.js", "/mail/static/src/core/common/mail_guest_model.js", "/mail/static/src/core/common/mail_message_subtype_model.js", "/mail/static/src/core/common/mail_popout_service.js", "/mail/static/src/core/common/mail_template_model.js", "/mail/static/src/core/common/message.js", "/mail/static/src/core/common/message_actions.js", "/mail/static/src/core/common/message_card_list.js", "/mail/static/src/core/common/message_confirm_dialog.js", "/mail/static/src/core/common/message_in_reply.js", "/mail/static/src/core/common/message_link_preview_list.js", "/mail/static/src/core/common/message_link_preview_model.js", "/mail/static/src/core/common/message_model.js", "/mail/static/src/core/common/message_notification_popover.js", "/mail/static/src/core/common/message_reaction_list.js", "/mail/static/src/core/common/message_reaction_menu.js", "/mail/static/src/core/common/message_reactions.js", "/mail/static/src/core/common/message_reactions_model.js", "/mail/static/src/core/common/message_search_hook.js", "/mail/static/src/core/common/navigable_list.js", "/mail/static/src/core/common/notification_message.js", "/mail/static/src/core/common/notification_model.js", "/mail/static/src/core/common/notification_permission_service.js", "/mail/static/src/core/common/out_of_focus_service.js", "/mail/static/src/core/common/partner_compare.js", "/mail/static/src/core/common/plugin/mail_composer_plugin.js", "/mail/static/src/core/common/plugin/mention_plugin.js", "/mail/static/src/core/common/plugin/plugin_sets.js", "/mail/static/src/core/common/quick_reaction_menu.js", "/mail/static/src/core/common/record.js", "/mail/static/src/core/common/relative_time.js", "/mail/static/src/core/common/res_company_model.js", "/mail/static/src/core/common/res_groups_model.js", "/mail/static/src/core/common/res_groups_privilege_model.js", "/mail/static/src/core/common/res_lang_model.js", "/mail/static/src/core/common/res_partner_model.js", "/mail/static/src/core/common/res_role_model.js", "/mail/static/src/core/common/res_users_model.js", "/mail/static/src/core/common/search_message_input.js", "/mail/static/src/core/common/search_message_result.js", "/mail/static/src/core/common/search_messages_panel.js", "/mail/static/src/core/common/settings_model.js", "/mail/static/src/core/common/sound_effects_service.js", "/mail/static/src/core/common/store_service.js", "/mail/static/src/core/common/suggestion_hook.js", "/mail/static/src/core/common/suggestion_service.js", "/mail/static/src/core/common/thread.js", "/mail/static/src/core/common/thread_actions.js", "/mail/static/src/core/common/thread_compare.js", "/mail/static/src/core/common/thread_icon.js", "/mail/static/src/core/common/thread_model.js", "/mail/static/src/core/common/volume_model.js", "/mail/static/src/core/public_web/discuss.js", "/mail/static/src/core/public_web/discuss_app_model.js", "/mail/static/src/core/public_web/discuss_client_action.js", "/mail/static/src/core/public_web/discuss_content.js", "/mail/static/src/core/public_web/discuss_search.js", "/mail/static/src/core/public_web/discuss_sidebar.js", "/mail/static/src/core/public_web/messaging_menu.js", "/mail/static/src/core/public_web/notification_item.js", "/mail/static/src/core/public_web/out_of_focus_service_patch.js", "/mail/static/src/core/public_web/store_service_patch.js", "/mail/static/src/core/public_web/thread_actions.js", "/mail/static/src/core/public_web/thread_model_patch.js", "/mail/static/src/core/web_portal/message_patch.js", "/mail/static/src/core/web/activity.js", "/mail/static/src/core/web/activity_button.js", "/mail/static/src/core/web/activity_list_popover.js", "/mail/static/src/core/web/activity_list_popover_item.js", "/mail/static/src/core/web/activity_mail_template.js", "/mail/static/src/core/web/activity_markasdone_popover.js", "/mail/static/src/core/web/activity_menu.js", "/mail/static/src/core/web/activity_model_patch.js", "/mail/static/src/core/web/chat_window_model_patch.js", "/mail/static/src/core/web/command_palette.js", "/mail/static/src/core/web/composer_patch.js", "/mail/static/src/core/web/dialog_patch.js", "/mail/static/src/core/web/discuss_patch.js", "/mail/static/src/core/web/discuss_sidebar_mailboxes.js", "/mail/static/src/core/web/follower.js", "/mail/static/src/core/web/follower_list.js", "/mail/static/src/core/web/follower_subtype_dialog.js", "/mail/static/src/core/web/mail_column_progress.js", "/mail/static/src/core/web/mail_composer_attachment_list.js", "/mail/static/src/core/web/mail_composer_attachment_selector.js", "/mail/static/src/core/web/mail_composer_bcc_list_popover.js", "/mail/static/src/core/web/mail_composer_template_selector.js", "/mail/static/src/core/web/mail_core_common_service_patch.js", "/mail/static/src/core/web/mail_core_web_service.js", "/mail/static/src/core/web/mention_list.js", "/mail/static/src/core/web/message_actions_patch.js", "/mail/static/src/core/web/message_model_patch.js", "/mail/static/src/core/web/message_patch.js", "/mail/static/src/core/web/messaging_menu_patch.js", "/mail/static/src/core/web/messaging_menu_quick_search.js", "/mail/static/src/core/web/open_chat_hook.js", "/mail/static/src/core/web/out_of_focus_service_patch.js", "/mail/static/src/core/web/recipients_input.js", "/mail/static/src/core/web/recipients_input_tags_list.js", "/mail/static/src/core/web/recipients_input_tags_list_popover.js", "/mail/static/src/core/web/recipients_popover.js", "/mail/static/src/core/web/store_service_patch.js", "/mail/static/src/core/web/thread_actions.js", "/mail/static/src/core/web/thread_model_patch.js", "/mail/static/src/utils/common/dates.js", "/mail/static/src/utils/common/format.js", "/mail/static/src/utils/common/hooks.js", "/mail/static/src/utils/common/media_monitoring.js", "/mail/static/src/utils/common/misc.js", "/mail/static/src/utils/common/pdf_thumbnail.js", "/mail/static/src/chatter/web_portal/chatter.js", "/mail/static/src/chatter/web_portal/composer_patch.js", "/mail/static/src/chatter/web_portal/thread_model_patch.js", "/mail/static/src/chatter/web/chatter_patch.js", "/mail/static/src/chatter/web/form_arch_parser.js", "/mail/static/src/chatter/web/form_compiler.js", "/mail/static/src/chatter/web/form_controller.js", "/mail/static/src/chatter/web/form_renderer.js", "/mail/static/src/chatter/web/mail_composer_form.js", "/mail/static/src/chatter/web/mail_composer_save_template_form.js", "/mail/static/src/chatter/web/scheduled_message.js", "/mail/static/src/chatter/web/scheduled_message_model.js", "/mail/static/src/chatter/web/thread_model_patch.js", "/mail/static/src/views/web/calendar/activity_calendar_renderer.js", "/mail/static/src/views/web/calendar/activity_calendar_view.js", "/mail/static/src/views/web/calendar/calendar_common/activity_calendar_common_popover.js", "/mail/static/src/views/web/calendar/calendar_common/activity_calendar_common_renderer.js", "/mail/static/src/views/web/calendar/calendar_year/activity_calendar_year_popover.js", "/mail/static/src/views/web/calendar/calendar_year/activity_calendar_year_renderer.js", "/mail/static/src/views/web/fields/activity_exception/activity_exception.js", "/mail/static/src/views/web/fields/assign_user_command_hook.js", "/mail/static/src/views/web/fields/avatar/avatar.js", "/mail/static/src/views/web/fields/avatar_autocomplete/avatar_many2x_autocomplete.js", "/mail/static/src/views/web/fields/emojis_char_field/emojis_char_field.js", "/mail/static/src/views/web/fields/emojis_field_common/emojis_field_common.js", "/mail/static/src/views/web/fields/emojis_text_field/emojis_text_field.js", "/mail/static/src/views/web/fields/html_composer_message_field/content_expandable_plugin.js", "/mail/static/src/views/web/fields/html_composer_message_field/html_composer_message_field.js", "/mail/static/src/views/web/fields/html_composer_message_field/mention_plugin.js", "/mail/static/src/views/web/fields/html_mail_field/convert_inline.js", "/mail/static/src/views/web/fields/html_mail_field/html_mail_field.js", "/mail/static/src/views/web/fields/kanban_activity/kanban_activity.js", "/mail/static/src/views/web/fields/list_activity/list_activity.js", "/mail/static/src/views/web/fields/many2many_avatar_user_field/many2many_avatar_user_field.js", "/mail/static/src/views/web/fields/many2many_tags_email/many2many_tags_email.js", "/mail/static/src/views/web/fields/many2one_avatar_user_field/kanban_many2one_avatar_user_field.js", "/mail/static/src/views/web/fields/many2one_avatar_user_field/many2one_avatar_user_field.js", "/mail/static/src/views/web/fields/properties_field/property_value.js", "/mail/static/src/views/web/fields/scheduled_date_field/scheduled_date_dialog.js", "/mail/static/src/views/web/fields/scheduled_date_field/scheduled_date_field.js", "/mail/static/src/views/web/fields/shortcut_char_field/shortcut_char_field.js", "/mail/static/src/views/web/kanban/mail_activity_my_kanban_controller.js", "/mail/static/src/views/web/kanban/mail_activity_my_kanban_view.js", "/mail/static/src/views/web/list/archive_disabled_list_controller.js", "/mail/static/src/views/web/list/archive_disabled_list_view.js", "/mail/static/src/views/web/list/mail_activity_list_reschedule.js", "/mail/static/src/views/web/list_renderer.js", "/mail/static/src/views/web/model/sample_server_patch.js", "/mail/static/src/views/web/view_dialog/avatar_user_form_view_dialog.js", "/mail/static/src/webclient/web/webclient.js", "/mail/static/src/discuss/core/common/action_panel.js", "/mail/static/src/discuss/core/common/attachment_model_patch.js", "/mail/static/src/discuss/core/common/attachment_panel.js", "/mail/static/src/discuss/core/common/avatar_stack.js", "/mail/static/src/discuss/core/common/channel_commands.js", "/mail/static/src/discuss/core/common/channel_invitation.js", "/mail/static/src/discuss/core/common/channel_member_list.js", "/mail/static/src/discuss/core/common/channel_member_model.js", "/mail/static/src/discuss/core/common/delete_thread_dialog.js", "/mail/static/src/discuss/core/common/discuss_core_common_service.js", "/mail/static/src/discuss/core/common/discuss_notification_settings.js", "/mail/static/src/discuss/core/common/discuss_notification_settings_client_action.js", "/mail/static/src/discuss/core/common/mail_guest_model_patch.js", "/mail/static/src/discuss/core/common/message_actions.js", "/mail/static/src/discuss/core/common/message_model_patch.js", "/mail/static/src/discuss/core/common/message_patch.js", "/mail/static/src/discuss/core/common/message_seen_indicator.js", "/mail/static/src/discuss/core/common/notification_settings.js", "/mail/static/src/discuss/core/common/partner_compare.js", "/mail/static/src/discuss/core/common/res_partner_model_patch.js", "/mail/static/src/discuss/core/common/store_service_patch.js", "/mail/static/src/discuss/core/common/suggestion_service_patch.js", "/mail/static/src/discuss/core/common/thread_actions.js", "/mail/static/src/discuss/core/common/thread_compare_patch.js", "/mail/static/src/discuss/core/common/thread_model_patch.js", "/mail/static/src/discuss/core/common/thread_patch.js", "/mail/static/src/discuss/core/public_web/action_panel_patch.js", "/mail/static/src/discuss/core/public_web/bus_connection_alert.js", "/mail/static/src/discuss/core/public_web/discuss_app_category_model.js", "/mail/static/src/discuss/core/public_web/discuss_app_model_patch.js", "/mail/static/src/discuss/core/public_web/discuss_client_action_patch.js", "/mail/static/src/discuss/core/public_web/discuss_command_palette.js", "/mail/static/src/discuss/core/public_web/discuss_content_patch.js", "/mail/static/src/discuss/core/public_web/discuss_core_public_web_service.js", "/mail/static/src/discuss/core/public_web/discuss_sidebar_categories.js", "/mail/static/src/discuss/core/public_web/discuss_sidebar_channel_actions.js", "/mail/static/src/discuss/core/public_web/message_actions.js", "/mail/static/src/discuss/core/public_web/message_model_patch.js", "/mail/static/src/discuss/core/public_web/message_patch.js", "/mail/static/src/discuss/core/public_web/messaging_menu_patch.js", "/mail/static/src/discuss/core/public_web/store_service_patch.js", "/mail/static/src/discuss/core/public_web/sub_channel_list.js", "/mail/static/src/discuss/core/public_web/sub_channel_preview.js", "/mail/static/src/discuss/core/public_web/thread_actions.js", "/mail/static/src/discuss/core/public_web/thread_model_patch.js", "/mail/static/src/discuss/core/public_web/thread_patch.js", "/mail/static/src/discuss/core/web/burger_menu_patch.js", "/mail/static/src/discuss/core/web/channel_member_list_patch.js", "/mail/static/src/discuss/core/web/discuss_command_palette_patch.js", "/mail/static/src/discuss/core/web/discuss_core_common_service_patch.js", "/mail/static/src/discuss/core/web/discuss_core_web_service.js", "/mail/static/src/discuss/core/web/discuss_sidebar_categories_patch.js", "/mail/static/src/discuss/core/web/messaging_menu_patch.js", "/mail/static/src/discuss/core/web/store_service_patch.js", "/mail/static/src/discuss/core/web/thread_actions.js", "/mail/static/src/discuss/core/web/thread_model_patch.js", "/mail/static/src/discuss/core/web/user_menu_patch.js", "/mail/static/src/discuss/call/common/blur_manager.js", "/mail/static/src/discuss/call/common/blur_performance_warning.js", "/mail/static/src/discuss/call/common/call.js", "/mail/static/src/discuss/call/common/call_action_list.js", "/mail/static/src/discuss/call/common/call_actions.js", "/mail/static/src/discuss/call/common/call_components.js", "/mail/static/src/discuss/call/common/call_context_menu.js", "/mail/static/src/discuss/call/common/call_dropdown.js", "/mail/static/src/discuss/call/common/call_infinite_mirroring_warning.js", "/mail/static/src/discuss/call/common/call_invitation.js", "/mail/static/src/discuss/call/common/call_invitations.js", "/mail/static/src/discuss/call/common/call_menu.js", "/mail/static/src/discuss/call/common/call_participant_card.js", "/mail/static/src/discuss/call/common/call_participant_video.js", "/mail/static/src/discuss/call/common/call_permission_dialog.js", "/mail/static/src/discuss/call/common/call_preview.js", "/mail/static/src/discuss/call/common/call_settings.js", "/mail/static/src/discuss/call/common/channel_member_patch.js", "/mail/static/src/discuss/call/common/chat_window_patch.js", "/mail/static/src/discuss/call/common/device_select.js", "/mail/static/src/discuss/call/common/discuss_call_settings_client_action.js", "/mail/static/src/discuss/call/common/discuss_p2p_service.js", "/mail/static/src/discuss/call/common/mail_guest_model_patch.js", "/mail/static/src/discuss/call/common/meeting.js", "/mail/static/src/discuss/call/common/meeting_chat.js", "/mail/static/src/discuss/call/common/meeting_side_actions.js", "/mail/static/src/discuss/call/common/peer_to_peer.js", "/mail/static/src/discuss/call/common/pip_banner.js", "/mail/static/src/discuss/call/common/pip_service.js", "/mail/static/src/discuss/call/common/ptt_ad_banner.js", "/mail/static/src/discuss/call/common/ptt_extension_service.js", "/mail/static/src/discuss/call/common/quick_video_settings.js", "/mail/static/src/discuss/call/common/quick_voice_settings.js", "/mail/static/src/discuss/call/common/res_partner_model_patch.js", "/mail/static/src/discuss/call/common/rtc_service.js", "/mail/static/src/discuss/call/common/rtc_session_model.js", "/mail/static/src/discuss/call/common/settings_model_patch.js", "/mail/static/src/discuss/call/common/store_service_patch.js", "/mail/static/src/discuss/call/common/thread_actions.js", "/mail/static/src/discuss/call/common/thread_model_patch.js", "/mail/static/src/discuss/call/common/tick_worker.js", "/mail/static/src/discuss/gif_picker/common/composer_actions_patch.js", "/mail/static/src/discuss/gif_picker/common/composer_patch.js", "/mail/static/src/discuss/gif_picker/common/gif_picker.js", "/mail/static/src/discuss/gif_picker/common/store_service_patch.js", "/mail/static/src/discuss/message_pin/common/message_actions.js", "/mail/static/src/discuss/message_pin/common/message_model_patch.js", "/mail/static/src/discuss/message_pin/common/message_patch.js", "/mail/static/src/discuss/message_pin/common/notification_message_patch.js", "/mail/static/src/discuss/message_pin/common/pinned_messages_panel.js", "/mail/static/src/discuss/message_pin/common/thread_actions.js", "/mail/static/src/discuss/message_pin/common/thread_model_patch.js", "/mail/static/src/discuss/typing/common/composer_patch.js", "/mail/static/src/discuss/typing/common/thread_icon_patch.js", "/mail/static/src/discuss/typing/common/typing.js", "/mail/static/src/discuss/voice_message/common/attachment_list_patch.js", "/mail/static/src/discuss/voice_message/common/attachment_model_patch.js", "/mail/static/src/discuss/voice_message/common/attachment_uploader_hook_patch.js", "/mail/static/src/discuss/voice_message/common/composer_actions_patch.js", "/mail/static/src/discuss/voice_message/common/composer_model_patch.js", "/mail/static/src/discuss/voice_message/common/composer_patch.js", "/mail/static/src/discuss/voice_message/common/mp3_encoder.js", "/mail/static/src/discuss/voice_message/common/voice_message_service.js", "/mail/static/src/discuss/voice_message/common/voice_metadata_model.js", "/mail/static/src/discuss/voice_message/common/voice_player.js", "/mail/static/src/discuss/voice_message/common/voice_recorder.js", "/mail/static/src/discuss/call/public_web/discuss_client_action_patch.js", "/mail/static/src/discuss/call/public_web/discuss_sidebar_call_indicator.js", "/mail/static/src/discuss/call/public_web/discuss_sidebar_call_participants.js", "/mail/static/src/discuss/call/public_web/discuss_sidebar_categories_patch.js", "/mail/static/src/discuss/call/public_web/thread_model_patch.js", "/mail/static/src/discuss/call/web/discuss_sidebar_call_participants_patch.js", "/mail/static/src/discuss/call/web/quick_video_settings_patch.js", "/mail/static/src/discuss/call/web/quick_voice_settings_patch.js", "/mail/static/src/discuss/web/avatar_card/avatar_card_popover.js", "/mail/static/src/discuss/web/bus_connection_alert_patch.js", "/mail/static/src/discuss/web/discuss_core_common_service_patch.js", "/mail/static/src/views/fields/activity_model_selector/activity_model_selector.js", "/mail/static/src/views/fields/badge_selection_icons/badge_selection_icons_field.js", "/mail/static/src/views/fields/mail_server_configurator_selection/mail_server_configurator_selection.js", "/mail/static/src/views/fields/statusbar_duration/statusbar_duration_field.js", "/sales_team/static/src/js/crm_team_form.js", "/onboarding/static/src/views/form/onboarding_step_form_controller.js", "/uom/static/src/components/many2one_uom/many2one_uom_field.js", "/uom/static/src/components/many2x_uom_tags/many2x_uom_tags.js", "/product/static/src/js/pricelist_report/product_pricelist_report.js", "/product/static/src/js/product_attribute_value_list.js", "/product/static/src/js/product_document_kanban/product_document_kanban_controller.js", "/product/static/src/js/product_document_kanban/product_document_kanban_record.js", "/product/static/src/js/product_document_kanban/product_document_kanban_renderer.js", "/product/static/src/js/product_document_kanban/product_document_kanban_view.js", "/product/static/src/js/product_document_kanban/upload_button/upload_button.js", "/product/static/src/product_catalog/kanban_controller.js", "/product/static/src/product_catalog/kanban_model.js", "/product/static/src/product_catalog/kanban_record.js", "/product/static/src/product_catalog/kanban_renderer.js", "/product/static/src/product_catalog/kanban_view.js", "/product/static/src/product_catalog/order_line/order_line.js", "/product/static/src/product_name_and_description/product_and_label_autoresize.js", "/product/static/src/product_name_and_description/product_name_and_description.js", "/analytic/static/src/components/analytic_distribution/analytic_distribution.js", "/analytic/static/src/services/batched_orm_service.js", "/analytic/static/src/views/analytic_search_model.js", "/analytic/static/src/views/kanban/kanban_view.js", "/analytic/static/src/views/list/list_view.js", "/portal/static/src/views/fields/portal_wizard_user_one2many.js", "/portal/static/src/views/list/portal_wizard_user_list_controller.js", "/resource/static/src/section_list_renderer.js", "/resource/static/src/section_one2many_field.js", "/resource/static/src/views/form_with_html_expander/form_controller_with_html_expander.js", "/resource/static/src/views/form_with_html_expander/form_renderer_with_html_expander.js", "/resource/static/src/views/form_with_html_expander/form_view_with_html_expander.js", "/account/static/src/components/account_batch_sending_summary/account_batch_sending_summary.js", "/account/static/src/components/account_file_uploader/account_file_uploader.js", "/account/static/src/components/account_merge_wizard_line_one2many/account_merge_wizard_line_one2many.js", "/account/static/src/components/account_move_form/account_move_form.js", "/account/static/src/components/account_payment_field/account_payment_field.js", "/account/static/src/components/account_payment_register_html/account_payment_register_html.js", "/account/static/src/components/account_payment_term_form/payment_term_line_ids.js", "/account/static/src/components/account_pick_currency_rate/account_pick_currency_rate.js", "/account/static/src/components/account_resequence/account_resequence_field.js", "/account/static/src/components/account_statusbar_secured/account_move_statusbar_secured.js", "/account/static/src/components/account_tax_repartition_line_factor_percent/account_tax_repartition_line_factor_percent.js", "/account/static/src/components/account_type_selection/account_type_selection.js", "/account/static/src/components/actionable_errors/actionable_errors.js", "/account/static/src/components/auto_save_res_partner_bank/auto_save_res_partner_bank.js", "/account/static/src/components/autosave_many2many_tax_tags/autosave_many2many_tax_tags.js", "/account/static/src/components/bill_guide/bill_guide.js", "/account/static/src/components/char_with_placeholder_field/char_with_placeholder_field.js", "/account/static/src/components/char_with_placeholder_field_to_check/char_with_placeholder_field_to_check_to_check.js", "/account/static/src/components/currency_form/form_controller.js", "/account/static/src/components/currency_form/open_decimal_precision_btn.js", "/account/static/src/components/document_file_uploader/document_file_uploader.js", "/account/static/src/components/document_state/document_state_field.js", "/account/static/src/components/dynamic_selection/dynamic_selection.js", "/account/static/src/components/fetch_einvoices/fetch_einvoices_cog.js", "/account/static/src/components/grouped_view_widget/grouped_view_widget.js", "/account/static/src/components/mail_attachments/mail_attachments.js", "/account/static/src/components/mail_attachments/mail_attachments_selector.js", "/account/static/src/components/many2many_tags_banks/many2many_tags_banks.js", "/account/static/src/components/many2many_tags_journals/many2many_tags_journals.js", "/account/static/src/components/many2x_tax_tags/many2x_tax_tags.js", "/account/static/src/components/onboarding/onboarding.js", "/account/static/src/components/open_move_line_move_widget/open_move_line_move_widget.js", "/account/static/src/components/open_move_widget/open_move_widget.js", "/account/static/src/components/product_catalog/account_move_line.js", "/account/static/src/components/product_catalog/kanban_controller.js", "/account/static/src/components/product_catalog/kanban_model.js", "/account/static/src/components/product_catalog/kanban_record.js", "/account/static/src/components/product_catalog/kanban_view.js", "/account/static/src/components/product_catalog/search/search_model.js", "/account/static/src/components/product_catalog/search/search_panel.js", "/account/static/src/components/product_label_section_and_note_field/product_label_section_and_note_field.js", "/account/static/src/components/product_label_section_and_note_field/product_label_section_and_note_field_o2m.js", "/account/static/src/components/receipt_selector/receipt_selector.js", "/account/static/src/components/section_and_note_fields_backend/section_and_note_fields_backend.js", "/account/static/src/components/tax_totals/tax_totals.js", "/account/static/src/components/tests_shared_js_python/tests_shared_js_python.js", "/account/static/src/components/upload_drop_zone/upload_drop_zone.js", "/account/static/src/components/x2many_buttons/x2many_buttons.js", "/account/static/src/services/account_move_service.js", "/account/static/src/services/account_notification_service.js", "/account/static/src/views/account_dashboard_kanban/account_dashboard_kanban_record.js", "/account/static/src/views/account_dashboard_kanban/account_dashboard_kanban_renderer.js", "/account/static/src/views/account_dashboard_kanban/account_dashboard_kanban_view.js", "/account/static/src/views/account_move_kanban/account_move_kanban_controller.js", "/account/static/src/views/account_move_kanban/account_move_kanban_view.js", "/account/static/src/views/account_move_list/account_move_list_controller.js", "/account/static/src/views/account_move_list/account_move_list_renderer.js", "/account/static/src/views/account_move_list/account_move_list_view.js", "/account/static/src/views/account_x2many_list_controller.js", "/account/static/src/views/file_upload_kanban/file_upload_kanban_controller.js", "/account/static/src/views/file_upload_kanban/file_upload_kanban_renderer.js", "/account/static/src/views/file_upload_kanban/file_upload_kanban_view.js", "/account/static/src/views/file_upload_list/file_upload_list_controller.js", "/account/static/src/views/file_upload_list/file_upload_list_renderer.js", "/account/static/src/views/file_upload_list/file_upload_list_view.js", "/account/static/src/views/upload_file_from_data_hook.js", "/account/static/src/js/tours/account.js", "/account/static/src/js/search/search_bar/search_bar.js", "/account/static/src/helpers/account_tax.js", "/payment/static/src/js/payment_wizard_copy_clipboard_field.js", "/utm/static/src/js/utm_campaign_kanban_examples.js", "/sale/static/src/js/badge_extra_price/badge_extra_price.js", "/sale/static/src/js/sale_action_helper/sale_action_helper.js", "/sale/static/src/js/sale_action_helper/sale_action_helper_dialog.js", "/sale/static/src/js/combo_configurator_dialog/combo_configurator_dialog.js", "/sale/static/src/js/models/product_combo.js", "/sale/static/src/js/models/product_combo_item.js", "/sale/static/src/js/models/product_product.js", "/sale/static/src/js/models/product_template_attribute_line.js", "/sale/static/src/js/models/product_template_attribute_value.js", "/sale/static/src/js/product/product.js", "/sale/static/src/js/product_card/product_card.js", "/sale/static/src/js/product_configurator_dialog/product_configurator_dialog.js", "/sale/static/src/js/product_list/product_list.js", "/sale/static/src/js/product_template_attribute_line/product_template_attribute_line.js", "/sale/static/src/js/quantity_buttons/quantity_buttons.js", "/sale/static/src/js/sale_order_line_field/sale_order_line_field.js", "/sale/static/src/js/sale_progressbar_field.js", "/sale/static/src/js/tours/sale.js", "/sale/static/src/js/upload_rfq_cog_menu/upload_rfq_cog_menu.js", "/sale/static/src/js/sale_product_field.js", "/sale/static/src/js/sale_utils.js", "/sale/static/src/views/sale_file_upload_kanban/sale_file_upload_kanban_controller.js", "/sale/static/src/views/sale_file_upload_kanban/sale_file_upload_kanban_renderer.js", "/sale/static/src/views/sale_file_upload_kanban/sale_file_upload_kanban_view.js", "/sale/static/src/views/sale_file_upload_list/sale_file_upload_list_controller.js", "/sale/static/src/views/sale_file_upload_list/sale_file_upload_list_renderer.js", "/sale/static/src/views/sale_file_upload_list/sale_file_upload_list_view.js", "/sale/static/src/views/sale_onboarding_kanban/sale_onboarding_kanban_renderer.js", "/sale/static/src/views/sale_onboarding_kanban/sale_onboarding_kanban_view.js", "/sale/static/src/views/sale_onboarding_list/sale_onboarding_list_renderer.js", "/sale/static/src/views/sale_onboarding_list/sale_onboarding_list_view.js", "/sale_management/static/src/fields/sale_order_line_field/sale_order_line_field.js", "/sale_management/static/src/fields/sale_order_template_line_field/sale_order_template_line_field.js", "/sale_management/static/src/fields/sale_product_field.js", "/website/static/src/components/resource_editor/resource_editor.js", "/website/static/src/components/resource_editor/resource_editor_warning.js", "/website/static/src/components/resource_editor/utils.js", "/website/static/src/components/edit_head_body_dialog/edit_head_body_dialog.js", "/website/static/src/utils/images.js", "/website/static/src/utils/misc.js", "/website/static/src/utils/videos.js", "/website/static/src/components/dialog/add_page_dialog.js", "/website/static/src/components/dialog/dialog.js", "/website/static/src/components/dialog/edit_menu.js", "/website/static/src/components/dialog/page_properties.js", "/website/static/src/components/dialog/seo.js", "/website/static/src/components/navbar/navbar.js", "/website/static/src/components/burger_menu/burger_menu.js", "/website/static/src/js/new_content_form.js", "/website/static/src/services/website_custom_menus.js", "/website/static/src/js/tours/homepage.js", "/website/static/src/js/backend/view_hierarchy/hierarchy_navbar.js", "/website/static/src/js/backend/view_hierarchy/view_hierarchy.js", "/website_sale/static/src/js/systray_items/new_content.js", "/ai_website/static/src/components/dialog/add_page_dialog.js", "/saas_website/static/src/js/dns_menu.js", "/saas_website/static/src/js/db_activation_reminder_backend.js", "/website_enterprise/static/src/js/systray_items/new_content.js", "/website_enterprise/static/src/services/color_scheme_service_patch.js", "/website_blog/static/src/js/systray_items/new_content.js", "/website_forum/static/src/js/systray_items/forum_forum_add_form.js", "/website_forum/static/src/js/systray_items/new_content.js", "/theme_nano/static/src/js/tour.js", "/website/static/src/js/editor/html_editor.js", "/website/static/src/js/tours/tour_utils.js", "/website/static/src/js/text_processing.js", "/website/static/src/js/highlight_utils.js", "/website/static/src/client_actions/configurator/configurator.js", "/website/static/src/client_actions/open_custom_menu/open_custom_menu.js", "/website/static/src/client_actions/website_dashboard/website_dashboard.js", "/website/static/src/client_actions/website_preview/create_page_message.js", "/website/static/src/client_actions/website_preview/edit_in_backend.js", "/website/static/src/client_actions/website_preview/edit_website_systray_item.js", "/website/static/src/client_actions/website_preview/install_module_dialog.js", "/website/static/src/client_actions/website_preview/mobile_preview_systray.js", "/website/static/src/client_actions/website_preview/new_content_systray_item.js", "/website/static/src/client_actions/website_preview/publish_website_systray_item.js", "/website/static/src/client_actions/website_preview/utils.js", "/website/static/src/client_actions/website_preview/website_builder_action.js", "/website/static/src/client_actions/website_preview/website_switcher_systray_item.js", "/website/static/src/client_actions/website_preview/website_systray_item.js", "/website/static/src/components/fields/fields.js", "/website/static/src/components/fields/publish_button.js", "/website/static/src/components/fields/redirect_field.js", "/website/static/src/components/fields/widget_iframe.js", "/website/static/src/components/fullscreen_indication/fullscreen_indication.js", "/website/static/src/components/website_loader/website_loader.js", "/website/static/src/components/views/page_kanban.js", "/website/static/src/components/views/page_list.js", "/website/static/src/components/views/page_manager_hook.js", "/website/static/src/components/views/page_search_model.js", "/website/static/src/components/views/theme_preview_form.js", "/website/static/src/components/views/theme_preview_kanban.js", "/website/static/src/services/website_service.js", "/website/static/src/js/utils.js", "/website/static/src/components/autocomplete_with_pages/autocomplete_with_pages.js", "/website/static/src/components/autocomplete_with_pages/url_autocomplete.js", "/website/static/src/common/website_model.js", "/website/static/src/common/website_visitor_model.js", "/website/static/src/js/send_mail_form.js", "/barcodes/static/src/barcode_handler_field.js", "/barcodes/static/src/barcode_handlers.js", "/barcodes/static/src/barcode_service.js", "/barcodes/static/src/components/barcode_scanner.js", "/barcodes/static/src/components/manual_barcode.js", "/barcodes/static/src/float_scannable_field.js", "/barcodes/static/src/js/barcode_parser.js", "/barcodes_gs1_nomenclature/static/src/js/barcode_parser.js", "/barcodes_gs1_nomenclature/static/src/js/barcode_service.js", "/stock/static/src/client_actions/multi_print.js", "/stock/static/src/client_actions/stock_traceability_report_backend.js", "/stock/static/src/components/reception_report_line/stock_reception_report_line.js", "/stock/static/src/components/reception_report_main/stock_reception_report_main.js", "/stock/static/src/components/reception_report_table/stock_reception_report_table.js", "/stock/static/src/components/stock_overview/stock_overview.js", "/stock/static/src/fields/stock_action_field.js", "/stock/static/src/fields/stock_move_line_x2_many_field.js", "/stock/static/src/picking_type_dashboard_graph/picking_type_dashboard_graph_field.js", "/stock/static/src/stock_forecasted/forecasted_buttons.js", "/stock/static/src/stock_forecasted/forecasted_details.js", "/stock/static/src/stock_forecasted/forecasted_header.js", "/stock/static/src/stock_forecasted/forecasted_warehouse_filter.js", "/stock/static/src/stock_forecasted/stock_forecasted.js", "/stock/static/src/stock_warehouse_service.js", "/stock/static/src/views/list/inventory_report_list_model.js", "/stock/static/src/views/list/inventory_report_list_view.js", "/stock/static/src/views/list/stock_add_package_list_view.js", "/stock/static/src/views/list/stock_report_list_view.js", "/stock/static/src/views/picking_form/stock_move_one2many.js", "/stock/static/src/views/picking_form/stock_move_product_label.js", "/stock/static/src/views/search/stock_orderpoint_search_model.js", "/stock/static/src/views/search/stock_orderpoint_search_panel.js", "/stock/static/src/views/search/stock_report_search_model.js", "/stock/static/src/views/search/stock_report_search_panel.js", "/stock/static/src/views/stock_empty_list_help.js", "/stock/static/src/views/stock_orderpoint_list_controller.js", "/stock/static/src/views/stock_orderpoint_list_view.js", "/stock/static/src/widgets/counted_quantity_widget.js", "/stock/static/src/widgets/forced_placeholder.js", "/stock/static/src/widgets/forecast_widget.js", "/stock/static/src/widgets/generate_serial.js", "/stock/static/src/widgets/json_widget.js", "/stock/static/src/widgets/many2many_barcode_tags.js", "/stock/static/src/widgets/popover_widget.js", "/stock/static/src/widgets/stock_package_m2m.js", "/stock/static/src/widgets/stock_package_m2o.js", "/stock/static/src/widgets/stock_pick_from.js", "/stock/static/src/widgets/stock_rescheduling_popover.js", "/rating/static/src/core/common/message_model_patch.js", "/rating/static/src/core/common/rating_model.js", "/rating/static/src/core/web/notification_item_patch.js", "/website_sale/static/src/js/client_actions/configurator/configurator.js", "/website_sale/static/src/js/client_actions/configurator/productPageSelectionScreen.js", "/website_sale/static/src/js/client_actions/configurator/shopPageSelectionScreen.js", "/website_sale/static/src/js/tours/tour_utils.js", "/website_sale/static/src/js/website_sale_video_field_preview.js", "/website_sale/static/src/js/tours/website_sale_shop.js", "/knowledge/static/src/components/article_annexe_picker_dialog/article_annexe_picker_dialog.js", "/knowledge/static/src/components/article_annexe_picker_dialog/article_annexe_picker_no_content_helper.js", "/knowledge/static/src/components/article_search_dialog/article_search_dialog.js", "/knowledge/static/src/components/article_template_picker_dialog/article_template_picker_dialog.js", "/knowledge/static/src/components/article_template_picker_dialog/article_template_picker_no_content_helper.js", "/knowledge/static/src/components/chatter_panel/chatter_panel.js", "/knowledge/static/src/components/custom_favorite_item/custom_favorite_item.js", "/knowledge/static/src/components/embedded_view_actions_menu/embedded_view_actions_menu.js", "/knowledge/static/src/components/external_embedded_view_favorite_menu/embedded_view_favorite_menu.js", "/knowledge/static/src/components/external_embedded_view_insertion/views_renderers_patches.js", "/knowledge/static/src/components/form_status_indicator/form_status_indicator.js", "/knowledge/static/src/components/hierarchy/hierarchy.js", "/knowledge/static/src/components/item_calendar_props_dialog/item_calendar_props_dialog.js", "/knowledge/static/src/components/knowledge_cover/knowledge_cover.js", "/knowledge/static/src/components/knowledge_cover/knowledge_cover_dialog.js", "/knowledge/static/src/components/knowledge_dropdown/knowledge_dropdown_patch.js", "/knowledge/static/src/components/knowledge_html_field/knowledge_html_field.js", "/knowledge/static/src/components/knowledge_html_viewer/knowledge_html_viewer.js", "/knowledge/static/src/components/knowledge_icon/knowledge_icon.js", "/knowledge/static/src/components/knowledge_wysiwyg/knowledge_wysiwyg.js", "/knowledge/static/src/components/move_article_dialog/move_article_dialog.js", "/knowledge/static/src/components/options_dropdown/options_dropdown.js", "/knowledge/static/src/components/permission_panel/permission_panel.js", "/knowledge/static/src/components/permission_panel_dialog/permission_panel_dialog.js", "/knowledge/static/src/components/prompt_embedded_view_name_dialog/prompt_embedded_view_name_dialog.js", "/knowledge/static/src/components/properties_panel/properties_panel.js", "/knowledge/static/src/components/sidebar/sidebar.js", "/knowledge/static/src/components/sidebar/sidebar_row.js", "/knowledge/static/src/components/sidebar/sidebar_section.js", "/knowledge/static/src/components/topbar/topbar.js", "/knowledge/static/src/components/with_lazy_loading/with_lazy_loading.js", "/knowledge/static/src/components/with_sub_env/with_sub_env.js", "/knowledge/static/src/components/wysiwyg_article_helper/wysiwyg_article_helper.js", "/knowledge/static/src/editor/embedded_components/backend/article_index/article_index.js", "/knowledge/static/src/editor/embedded_components/backend/article_index/article_index_list.js", "/knowledge/static/src/editor/embedded_components/backend/clipboard/macros_embedded_clipboard.js", "/knowledge/static/src/editor/embedded_components/backend/embedded_view_link/embedded_view_link.js", "/knowledge/static/src/editor/embedded_components/backend/embedded_view_link/embedded_view_link_edit_dialog.js", "/knowledge/static/src/editor/embedded_components/backend/embedded_view_link/embedded_view_link_popover.js", "/knowledge/static/src/editor/embedded_components/backend/embedded_view_link/readonly_embedded_view_link.js", "/knowledge/static/src/editor/embedded_components/backend/file/macros_file.js", "/knowledge/static/src/editor/embedded_components/backend/file/macros_file_mixin.js", "/knowledge/static/src/editor/embedded_components/backend/file/readonly_macros_file.js", "/knowledge/static/src/editor/embedded_components/backend/foldable_section/foldable_section.js", "/knowledge/static/src/editor/embedded_components/backend/view/embedded_view.js", "/knowledge/static/src/editor/embedded_components/backend/view/readonly_embedded_view.js", "/knowledge/static/src/editor/embedded_components/core/article_index/readonly_article_index.js", "/knowledge/static/src/editor/embedded_components/core/clipboard/embedded_clipboard.js", "/knowledge/static/src/editor/embedded_components/core/embedded_view_link/embedded_view_link_style.js", "/knowledge/static/src/editor/embedded_components/core/readonly_foldable_section/readonly_foldable_section.js", "/knowledge/static/src/editor/embedded_components/embedding_sets.js", "/knowledge/static/src/editor/embedded_components/plugins/article_index_plugin/article_index_plugin.js", "/knowledge/static/src/editor/embedded_components/plugins/embedded_clipboard_plugin/embedded_clipboard_plugin.js", "/knowledge/static/src/editor/embedded_components/plugins/embedded_view_link_plugin/embedded_view_link_plugin.js", "/knowledge/static/src/editor/embedded_components/plugins/embedded_view_plugin/embedded_view_plugin.js", "/knowledge/static/src/editor/embedded_components/plugins/foldable_section_plugin/foldable_section_plugin.js", "/knowledge/static/src/editor/html_migrations/manifest.js", "/knowledge/static/src/editor/html_migrations/migration-1.0.js", "/knowledge/static/src/editor/html_migrations/migration-2.0.js", "/knowledge/static/src/editor/html_migrations/utils.js", "/knowledge/static/src/editor/plugin_sets.js", "/knowledge/static/src/editor/plugins/article_plugin/article_plugin.js", "/knowledge/static/src/editor/plugins/autofocus_plugin/autofocus_plugin.js", "/knowledge/static/src/editor/plugins/comments_plugin/comments_plugin.js", "/knowledge/static/src/editor/plugins/delete_first_line_plugin/delete_first_line_plugin.js", "/knowledge/static/src/editor/plugins/heading_link_plugin/heading_link_plugin.js", "/knowledge/static/src/editor/plugins/insert_pending_element_plugin/insert_pending_element_plugin.js", "/knowledge/static/src/hooks/knowledge_article_selector.js", "/knowledge/static/src/comments/comment/comment.js", "/knowledge/static/src/comments/comment/comment_anchor_text.js", "/knowledge/static/src/comments/comment_beacon_manager.js", "/knowledge/static/src/comments/comments_handler/comments_handler.js", "/knowledge/static/src/comments/comments_panel/comments_panel.js", "/knowledge/static/src/comments/comments_popover/comments_popover.js", "/knowledge/static/src/comments/comments_service.js", "/knowledge/static/src/comments/editor_thread_info.js", "/knowledge/static/src/mail/attachment_uploader/attachment_uploader_hook_patch.js", "/knowledge/static/src/mail/composer/composer.js", "/knowledge/static/src/mail/composer/composer_actions_patch.js", "/knowledge/static/src/mail/composer/composer_patch.js", "/knowledge/static/src/mail/emoji_picker/emoji_picker_patch.js", "/knowledge/static/src/mail/message/knowledge_message.js", "/knowledge/static/src/mail/message/message_actions.js", "/knowledge/static/src/mail/message/message_model_patch.js", "/knowledge/static/src/mail/message/message_patch.js", "/knowledge/static/src/mail/thread/knowledge_thread.js", "/knowledge/static/src/mail/thread/thread_model_patch.js", "/knowledge/static/src/search_model/search_model.js", "/knowledge/static/src/web/chatter_patch.js", "/knowledge/static/src/js/knowledge_controller.js", "/knowledge/static/src/js/knowledge_utils.js", "/knowledge/static/src/js/knowledge_renderers.js", "/knowledge/static/src/js/knowledge_views.js", "/knowledge/static/src/webclient/commands/command_category.js", "/knowledge/static/src/webclient/commands/knowledge_providers.js", "/knowledge/static/src/views/embedded_controllers_patch.js", "/knowledge/static/src/views/embedded_kanban_view.js", "/knowledge/static/src/views/embedded_list_view.js", "/knowledge/static/src/views/embedded_view.js", "/knowledge/static/src/views/item_calendar/item_calendar_model.js", "/knowledge/static/src/views/item_calendar/item_calendar_view.js", "/knowledge/static/src/views/list_view.js", "/knowledge/static/src/search/search_bar_menu/search_bar_menu.js", "/knowledge/static/src/services/knowledge_commands_service.js", "/knowledge/static/src/services/knowledge_embedded_filters_service.js", "/knowledge/static/src/macros/abstract_macro.js", "/knowledge/static/src/macros/clipboard_macros.js", "/knowledge/static/src/macros/file_macros.js", "/knowledge/static/src/macros/utils.js", "/stock_account/static/src/fields/boolean_confirm.js", "/stock_account/static/src/stock_account_forecasted/forecasted_header.js", "/stock_account/static/src/stock_valuation/buttons_bar/buttons_bar.js", "/stock_account/static/src/stock_valuation/controller.js", "/stock_account/static/src/stock_valuation/filters/filters.js", "/stock_account/static/src/stock_valuation/line/line.js", "/stock_account/static/src/stock_valuation/line/toggle_line.js", "/stock_account/static/src/stock_valuation/stock_valuation_report.js", "/sale_stock/static/src/product_catalog/kanban_record.js", "/sale_stock/static/src/product_catalog/sale_order_line/sale_order_line.js", "/sale_stock/static/src/stock_valuation/stock_valuation_report.js", "/sale_stock/static/src/widgets/qty_at_date_widget.js", "/repair/static/src/components/product_catalog/kanban_controller.js", "/repair/static/src/components/product_catalog/order_line.js", "/web_enterprise/static/src/webclient/burger_menu/burger_menu.js", "/web_enterprise/static/src/webclient/color_scheme/color_scheme_service.js", "/web_enterprise/static/src/webclient/home_menu/enterprise_subscription_service.js", "/web_enterprise/static/src/webclient/home_menu/expiration_panel.js", "/web_enterprise/static/src/webclient/home_menu/home_menu.js", "/web_enterprise/static/src/webclient/home_menu/home_menu_service.js", "/web_enterprise/static/src/webclient/navbar/navbar.js", "/web_enterprise/static/src/webclient/promote_studio/promote_studio_dialog.js", "/web_enterprise/static/src/webclient/promote_studio/promote_studio_systray_item.js", "/web_enterprise/static/src/webclient/share_url/burger_menu.js", "/web_enterprise/static/src/webclient/share_url/share_url.js", "/web_enterprise/static/src/webclient/webclient.js", "/web_enterprise/static/src/views/list/list_renderer_desktop.js", "/web_enterprise/static/src/views/view_components/group_config_menu_patch.js", "/web_mobile/static/src/js/core/hooks.js", "/web_mobile/static/src/js/core/mixins.js", "/web_mobile/static/src/js/core/network/download.js", "/web_mobile/static/src/js/core/popover.js", "/web_mobile/static/src/js/crash_manager.js", "/web_mobile/static/src/js/hook_event_bus.js", "/web_mobile/static/src/js/mobile_service.js", "/web_mobile/static/src/js/services/core.js", "/web_mobile/static/src/js/user_menu_items.js", "/web_mobile/static/src/views/user_preferences_form_view.js", "/stock_barcode/static/src/barcode_object.js", "/stock_barcode/static/src/components/apply_quant_dialog.js", "/stock_barcode/static/src/components/backorder_dialog.js", "/stock_barcode/static/src/components/confirm_quant_dialog.js", "/stock_barcode/static/src/components/count_screen_rfid.js", "/stock_barcode/static/src/components/grouped_line.js", "/stock_barcode/static/src/components/line.js", "/stock_barcode/static/src/components/main.js", "/stock_barcode/static/src/components/package_line.js", "/stock_barcode/static/src/components/product_image_dialog.js", "/stock_barcode/static/src/js/stock_barcode_sml_form.js", "/stock_barcode/static/src/kanban/stock_barcode_kanban_controller.js", "/stock_barcode/static/src/kanban/stock_barcode_kanban_renderer.js", "/stock_barcode/static/src/kanban/stock_barcode_kanban_view.js", "/stock_barcode/static/src/lazy_barcode_cache.js", "/stock_barcode/static/src/main_menu/main_menu.js", "/stock_barcode/static/src/models/barcode_model.js", "/stock_barcode/static/src/models/barcode_picking_model.js", "/stock_barcode/static/src/models/barcode_quant_model.js", "/stock_barcode/static/src/widgets/digipad.js", "/stock_barcode/static/src/widgets/image_preview.js", "/stock_barcode/static/src/widgets/set_quant_stock_move_line.js", "/stock_barcode/static/src/widgets/set_reserved_qty_button.js", "/delivery_sendcloud/static/src/views/sendcloud_functionality_filter_widget.js", "/delivery_sendcloud/static/src/views/sendcloud_product_selection_widget.js", "/mail_enterprise/static/src/core/common/message_patch.js", "/mail_enterprise/static/src/attachments/attachment_viewer_patch.js", "/mail_enterprise/static/src/web/chat_window/chat_window_patch.js", "/mail_enterprise/static/src/web/messaging_menu/messaging_menu_patch.js", "/account_accountant/static/src/js/tours/account_accountant.js", "/account_accountant/static/src/components/attachment_preview_list_view/account_attachment_view.js", "/account_accountant/static/src/components/attachment_preview_list_view/attachment_preview_list_view.js", "/account_accountant/static/src/components/bank_reconciliation/apply_amount/apply_amount.js", "/account_accountant/static/src/components/bank_reconciliation/bank_reconciliation_service.js", "/account_accountant/static/src/components/bank_reconciliation/bankrec_form_dialog/bankrec_form_dialog.js", "/account_accountant/static/src/components/bank_reconciliation/button/button.js", "/account_accountant/static/src/components/bank_reconciliation/button_list/button_list.js", "/account_accountant/static/src/components/bank_reconciliation/chatter/chatter.js", "/account_accountant/static/src/components/bank_reconciliation/file_uploader/file_uploader.js", "/account_accountant/static/src/components/bank_reconciliation/kanban_controller.js", "/account_accountant/static/src/components/bank_reconciliation/kanban_renderer.js", "/account_accountant/static/src/components/bank_reconciliation/line_info_pop_over/line_info_pop_over.js", "/account_accountant/static/src/components/bank_reconciliation/line_to_reconcile/line_to_reconcile.js", "/account_accountant/static/src/components/bank_reconciliation/list_view/list.js", "/account_accountant/static/src/components/bank_reconciliation/list_view/list_view_many2one_multi_edit.js", "/account_accountant/static/src/components/bank_reconciliation/quick_create/quick_create.js", "/account_accountant/static/src/components/bank_reconciliation/reconciled_line_name/reconciled_line_name.js", "/account_accountant/static/src/components/bank_reconciliation/search_dialog/search_dialog.js", "/account_accountant/static/src/components/bank_reconciliation/search_dialog/search_dialog_list.js", "/account_accountant/static/src/components/bank_reconciliation/statement_line/statement_line.js", "/account_accountant/static/src/components/bank_reconciliation/statement_summary/statement_summary.js", "/account_accountant/static/src/components/export_data_dialog/export_data_dialog.js", "/account_accountant/static/src/components/journal_create_wizard/journal_create_wizard.js", "/account_accountant/static/src/components/matching_link_widget/matching_link_widget.js", "/account_accountant/static/src/components/move_line_list/move_line_list.js", "/account_accountant/static/src/components/move_line_list_reconcile/move_line_list_reconcile.js", "/iap/static/src/action_buttons_widget/action_buttons_widget.js", "/iap_mail/static/src/js/services/iap_notification_service.js", "/sms/static/src/components/phone_field/phone_field.js", "/sms/static/src/components/sms_button/sms_button.js", "/sms/static/src/components/sms_widget/fields_sms_widget.js", "/sms/static/src/core/failure_model_patch.js", "/sms/static/src/core/notification_model_patch.js", "/sms/static/src/messaging_menu/messaging_menu_patch.js", "/sms/static/src/thread/message_patch.js", "/account_accountant_batch_payment/static/src/components/bank_reconciliation/bank_reconciliation_service.js", "/account_accountant_batch_payment/static/src/components/bank_reconciliation/button_list/button_list.js", "/account_accountant_batch_payment/static/src/components/bank_reconciliation/kanban_controller.js", "/account_accountant_batch_payment/static/src/components/bank_reconciliation/kanban_renderer.js", "/account_accountant_batch_payment/static/src/components/bank_reconciliation/line_to_reconcile/line_to_reconcile.js", "/base_import/static/src/binary_file_manager.js", "/base_import/static/src/import_action/import_action.js", "/base_import/static/src/import_block_ui.js", "/base_import/static/src/import_data_column_error/import_data_column_error.js", "/base_import/static/src/import_data_content/import_data_content.js", "/base_import/static/src/import_data_options/import_data_options.js", "/base_import/static/src/import_data_progress/import_data_progress.js", "/base_import/static/src/import_data_sidepanel/import_data_sidepanel.js", "/base_import/static/src/import_model.js", "/base_import/static/src/import_records/import_records.js", "/account_bank_statement_import/static/src/account_bank_statement_import_model.js", "/account_bank_statement_import/static/src/bank_reconciliation/kanban.js", "/account_bank_statement_import/static/src/bank_reconciliation/list.js", "/account_bank_statement_import_csv/static/src/bank_statement_csv_import_action.js", "/account_bank_statement_import_csv/static/src/bank_statement_csv_import_model.js", "/iap_extract/static/src/components/manual_correction/box.js", "/iap_extract/static/src/components/manual_correction/box_layer.js", "/iap_extract/static/src/components/manual_correction/form_renderer.js", "/iap_extract/static/src/components/status_header/status.js", "/account_reports/static/src/components/account_report/account_report.js", "/account_reports/static/src/components/account_report/buttons_bar/buttons_bar.js", "/account_reports/static/src/components/account_report/cog_menu/cog_menu.js", "/account_reports/static/src/components/account_report/controller.js", "/account_reports/static/src/components/account_report/ellipsis/ellipsis.js", "/account_reports/static/src/components/account_report/ellipsis/popover/ellipsis_popover.js", "/account_reports/static/src/components/account_report/filters/filters.js", "/account_reports/static/src/components/account_report/header/header.js", "/account_reports/static/src/components/account_report/line/line.js", "/account_reports/static/src/components/account_report/line/popover/debug_popover.js", "/account_reports/static/src/components/account_report/line_cell/line_cell.js", "/account_reports/static/src/components/account_report/line_cell/popover/carryover_popover.js", "/account_reports/static/src/components/account_report/line_cell/popover/edit_popover.js", "/account_reports/static/src/components/account_report/line_cell_editable/line_cell_editable.js", "/account_reports/static/src/components/account_report/line_name/line_name.js", "/account_reports/static/src/components/account_report/search_bar/search_bar.js", "/account_reports/static/src/components/account_return/actions/client_check_completed_notification.js", "/account_reports/static/src/components/account_return/actions/close_wizard.js", "/account_reports/static/src/components/account_return/views/account_audit_balance_list_chatter_service.js", "/account_reports/static/src/components/account_return/views/account_audit_balance_list_controller.js", "/account_reports/static/src/components/account_return/views/account_audit_balance_list_renderer.js", "/account_reports/static/src/components/account_return/views/account_audit_balance_list_views.js", "/account_reports/static/src/components/account_return/views/account_return_audit_kanban_views.js", "/account_reports/static/src/components/account_return/views/account_return_base_kanban_renderer.js", "/account_reports/static/src/components/account_return/views/account_return_check_kanban_controller.js", "/account_reports/static/src/components/account_return/views/account_return_check_kanban_record.js", "/account_reports/static/src/components/account_return/views/account_return_check_kanban_renderer.js", "/account_reports/static/src/components/account_return/views/account_return_check_kanban_view.js", "/account_reports/static/src/components/account_return/views/account_return_kanban_record.js", "/account_reports/static/src/components/account_return/views/account_return_kanban_renderer.js", "/account_reports/static/src/components/account_return/views/account_return_kanban_view.js", "/account_reports/static/src/components/account_return/views/activity_menu_patch.js", "/account_reports/static/src/components/account_return/views/refresh_returns_cog_menu.js", "/account_reports/static/src/components/account_return/widgets/account_audit_balance_clickable_char_field.js", "/account_reports/static/src/components/account_return/widgets/account_audit_progressbas.js", "/account_reports/static/src/components/account_return/widgets/account_return_check_attachment_model.js", "/account_reports/static/src/components/account_return/widgets/account_return_check_attachment_viewer.js", "/account_reports/static/src/components/account_return/widgets/account_return_check_file_uploader.js", "/account_reports/static/src/components/account_return/widgets/account_return_dashboard_list.js", "/account_reports/static/src/components/account_return/widgets/account_return_name_badge.js", "/account_reports/static/src/components/account_return/widgets/account_return_selection_badge.js", "/account_reports/static/src/components/aged_partner_balance/filters.js", "/account_reports/static/src/components/journal_report/filters.js", "/account_reports/static/src/components/journal_report/line/line.js", "/account_reports/static/src/components/journal_report/move_line_list/move_line_list.js", "/account_reports/static/src/components/mail/chatter.js", "/account_reports/static/src/components/mail/composer.js", "/account_reports/static/src/components/mail/message.js", "/account_reports/static/src/components/mail/message_actions.js", "/account_reports/static/src/components/mail/message_patch.js", "/account_reports/static/src/components/mail/store_service_patch.js", "/account_reports/static/src/components/mail/thread.js", "/account_reports/static/src/components/multicurrency_revaluation_report/filters/filters.js", "/account_reports/static/src/components/partner_ledger/line_cell.js", "/account_reports/static/src/components/partner_ledger_followup/line/line.js", "/account_reports/static/src/components/partner_ledger_followup/line_cell/line_cell.js", "/account_reports/static/src/components/sales_report/filters/filters.js", "/account_reports/static/src/js/action_manager_account_report_dl.js", "/account_reports/static/src/js/util.js", "/account_reports/static/src/views/accrual_list_controller.js", "/account_reports/static/src/views/accrual_list_search_model.js", "/account_reports/static/src/views/accrual_list_view.js", "/account_reports/static/src/widgets/account_report_x2many/account_report_x2many.js", "/account_reports/static/src/widgets/many2many_approvers/many2many_approvers.js", "/account_followup/static/src/components/change_trust_widget/followup_trust_widget.js", "/account_followup/static/src/components/download_close_wizard_action/download_close_wizard_action.js", "/account_invoice_extract/static/src/js/invoice_extract_form.js", "/account_online_synchronization/static/src/components/account_duplicate_transaction/account_duplicate_transaction_form.js", "/account_online_synchronization/static/src/components/account_duplicate_transaction/account_duplicate_transaction_hook.js", "/account_online_synchronization/static/src/components/account_duplicate_transaction/account_duplicate_transaction_service.js", "/account_online_synchronization/static/src/components/account_duplicate_transaction/account_duplicate_transactions_x2many.js", "/account_online_synchronization/static/src/components/bank_configure/bank_configure.js", "/account_online_synchronization/static/src/components/bank_reconciliation/fetch_missing_transactions_cog_menu.js", "/account_online_synchronization/static/src/components/bank_reconciliation/find_duplicate_transactions_cog_menu.js", "/account_online_synchronization/static/src/components/bank_reconciliation/kanban_renderer.js", "/account_online_synchronization/static/src/components/bank_reconciliation/statement_summary/statement_summary.js", "/account_online_synchronization/static/src/components/connected_until_widget/connected_until_widget.js", "/account_online_synchronization/static/src/components/journal_create_wizard/journal_create_wizard.js", "/account_online_synchronization/static/src/components/online_account_radio/online_account_radio.js", "/account_online_synchronization/static/src/components/refresh_spin_journal_widget/refresh_spin_journal_widget.js", "/account_online_synchronization/static/src/components/transient_bank_statement_line_list_view/transient_bank_statement_line_list_view.js", "/account_online_synchronization/static/src/components/views/account_online_authorization_kanban.js", "/account_online_synchronization/static/src/hooks/bank_institutions_hook.js", "/account_online_synchronization/static/src/js/odoo_fin_connector.js", "/account_peppol/static/src/components/peppol_info/peppol_info.js", "/base_iban/static/src/components/iban_widget/iban_widget.js", "/ai/static/src/ai_chat_launcher_service.js", "/ai/static/src/ai_json_schema/ai_json_schema.js", "/ai/static/src/ai_model_field_selector/ai_model_field_selector_popover.js", "/ai/static/src/ai_natural_language_service.js", "/ai/static/src/ai_prompt/ai_field_selector_plugin.js", "/ai/static/src/ai_prompt/ai_prompt.js", "/ai/static/src/ai_prompt/ai_prompt_field.js", "/ai/static/src/ai_prompt/ai_records_selector_plugin.js", "/ai/static/src/components/agent_add_source_dialog/agent_add_source_dialog.js", "/ai/static/src/core/html_editor/banner_plugin.js", "/ai/static/src/core/html_editor/prompt_plugin.js", "/ai/static/src/core/html_editor/user_command_plugin.js", "/ai/static/src/core/web/ask_ai_button.js", "/ai/static/src/core/web/command_palette.js", "/ai/static/src/core/web/search_bar_menu_patch.js", "/ai/static/src/core/web/search_bar_patch.js", "/ai/static/src/core/web/search_model_patch.js", "/ai/static/src/core/web/view_patch.js", "/ai/static/src/core/web/with_search_patch.js", "/ai/static/src/discuss/ai_prompt_model.js", "/ai/static/src/discuss/chat_window_patch.js", "/ai/static/src/discuss/composer_actions_patch.js", "/ai/static/src/discuss/composer_patch.js", "/ai/static/src/discuss/core/common/chat_window_model_patch.js", "/ai/static/src/discuss/core/common/thread_model_patch.js", "/ai/static/src/discuss/core/common/view_details.js", "/ai/static/src/discuss/message_actions_patch.js", "/ai/static/src/discuss/message_patch.js", "/ai/static/src/discuss/suggestion_service_patch.js", "/ai/static/src/discuss/thread_actions_patch.js", "/ai/static/src/discuss/thread_model_patch.js", "/ai/static/src/discuss/thread_patch.js", "/ai/static/src/discuss/typing_patch.js", "/ai/static/src/editor/embedded_components/core/readonly_voice_transcription.js", "/ai/static/src/editor/embedded_components/core/voice_transcription.js", "/ai/static/src/editor/embedded_components/embedding_sets.js", "/ai/static/src/editor/embedded_components/plugins/voice_transcription_plugin.js", "/ai/static/src/editor/plugins/chatgpt_plugin.js", "/ai/static/src/js/open_chat_action.js", "/ai/static/src/mail_composer_chatgpt.js", "/ai/static/src/records_selector_popover/records_selector_popover.js", "/ai/static/src/vad_audio_recorder.js", "/ai/static/src/views/fields/agent_source_type_icon/agent_source_type_icon.js", "/ai/static/src/views/fields/source_size_field.js", "/ai/static/src/views/fields/source_view_link/source_view_link.js", "/ai/static/src/views/fields/status_badge_tooltip/status_badge_tooltip.js", "/ai/static/src/web/systray_action.js", "/ai_fields/static/src/model/relational_model/record.js", "/ai_fields/static/src/views/fields/ai_fields/ai_fields.js", "/ai_fields/static/src/views/fields/properties_field_patch/properties_field_patch.js", "/ai_knowledge/static/src/components/agent_add_source_dialog/agent_add_source_dialog.js", "/ai_knowledge/static/src/views/fields/agent_source_type_icon.js", "/ai_knowledge/static/src/wysiwyg_article_helper_patch.js", "/api_doc/static/src/api_action.js", "/auth_passkey/static/lib/simplewebauthn.js", "/auth_passkey/static/src/views/auth_passkey_identity_check_form_view.js", "/auth_passkey/static/src/views/auth_passkey_key_create_form_view.js", "/base_import_module/static/src/base_import_list_renderer.js", "/base_import_module/static/src/base_import_list_view.js", "/delivery_mondialrelay/static/src/components/mondialrelay_field.js", "/google_address_autocomplete/static/src/address_autocomplete/google_address_autocomplete.js", "/google_address_autocomplete/static/src/google_places_session.js", "/mail_mobile/static/src/js/mobile_device_register.js", "/partner_autocomplete/static/src/js/partner_autocomplete_component.js", "/partner_autocomplete/static/src/js/partner_autocomplete_core.js", "/partner_autocomplete/static/src/js/partner_autocomplete_fieldchar.js", "/partner_autocomplete/static/src/js/partner_autocomplete_many2one.js", "/partner_autocomplete/static/src/js/web_company_autocomplete.js", "/product_barcodelookup/static/src/widgets/barcode_scanner.js", "/resource_mail/static/src/components/avatar_card_resource/avatar_card_resource_popover.js", "/resource_mail/static/src/components/avatar_resource/avatar_resource.js", "/resource_mail/static/src/views/fields/many2many_avatar_resource/many2many_avatar_resource_field.js", "/resource_mail/static/src/views/fields/many2one_avatar_resource/kanban_many2one_avatar_resource_field.js", "/resource_mail/static/src/views/fields/many2one_avatar_resource/many2one_avatar_resource_field.js", "/saas_trial/static/js/saas_trial.js", "/saas_trial/static/src/dates.js", "/saas_trial/static/src/owl/confirm_modal/confirm_modal.js", "/saas_trial/static/src/owl/db_expiration_tag/db_expiration_tag.js", "/saas_trial/static/src/owl/expiration_panel/saas_expiration_panel.js", "/saas_trial/static/src/owl/invite_users/res_config_invite_users.js", "/saas_trial/static/src/owl/mail_limit_panel/mail_limit_panel.js", "/saas_trial/static/src/owl/my_database/my_database.js", "/saas_trial/static/src/owl/resend_activation_email_modal/resend_activation_email_modal.js", "/saas_trial/static/src/owl/rolling_release/rolling_release.js", "/saas_trial/static/src/owl/rolling_release/rolling_release_dialog.js", "/saas_trial/static/src/owl/saas_compat.js", "/saas_trial/static/src/owl/saas_display/saas_display.js", "/saas_trial/static/src/owl/saas_service.js", "/saas_trial/static/src/owl/saas_user_seats/saas_user_seats.js", "/saas_trial/static/src/owl/settings_form_renderer/saas_settings_form_compiler.js", "/saas_trial/static/src/owl/upsell_warning/upsell_warning.js", "/saas_website/static/src/js/saas_website_backend.js", "/saas_website/static/src/owl/dns_confirm_modal/dns_confirm_modal.js", "/saas_website/static/src/owl/dns_menu/dns_menu.js", "/saas_website/static/src/owl/website_configurator/feature_selection.js", "/sale_account_accountant/static/src/components/bank_reconciliation/button_list/button_list.js", "/sale_pdf_quote_builder/static/src/js/custom_content_kanban_like_widget/custom_content_kanban_like_widget.js", "/sale_pdf_quote_builder/static/src/js/custom_content_kanban_like_widget/custom_field_card/custom_field_card.js", "/sale_pdf_quote_builder/static/src/js/quotation_document_kanban/quotation_document_kanban_controller.js", "/sale_pdf_quote_builder/static/src/js/quotation_document_kanban/quotation_document_kanban_view.js", "/sale_pdf_quote_builder/static/src/js/quotation_document_kanban/quotation_document_kanban_widget.js", "/snailmail/static/src/core/failure_model_patch.js", "/snailmail/static/src/core/notification_model_patch.js", "/snailmail/static/src/core_ui/message_patch.js", "/snailmail/static/src/core_ui/snailmail_notification_popover.js", "/snailmail/static/src/messaging_menu/messaging_menu_patch.js", "/spreadsheet/static/src/assets_backend/spreadsheet_action_loader.js", "/spreadsheet/static/src/assets_backend/spreadsheet_binary_field/spreadsheet_binary_field.js", "/spreadsheet_dashboard/static/src/assets/dashboard_action_loader.js", "/spreadsheet_edition/static/src/assets/components/many2one_spreadsheet_field.js", "/spreadsheet_edition/static/src/assets/components/spreadsheet_selector_dialog/spreadsheet_selector_dialog.js", "/spreadsheet_edition/static/src/assets/components/spreadsheet_selector_dialog/spreadsheet_selector_panel.js", "/spreadsheet_edition/static/src/assets/components/spreadsheet_selector_grid/spreadsheet_selector_grid.js", "/spreadsheet_edition/static/src/assets/insert_action_link_menu/insert_action_link_menu.js", "/spreadsheet_edition/static/src/assets/kanban/kanban_controller.js", "/spreadsheet_edition/static/src/assets/list_view/insert_list_spreadsheet_menu_owl.js", "/spreadsheet_edition/static/src/assets/list_view/list_controller.js", "/spreadsheet_edition/static/src/assets/list_view/list_renderer.js", "/spreadsheet_edition/static/src/assets/message_patch.js", "/spreadsheet_edition/static/src/assets/spreadsheet_cog_menu/spreadsheet_cog_menu.js", "/spreadsheet_edition/static/src/assets/spreadsheet_history_action_loader.js", "/spreadsheet_edition/static/src/assets/view_hook.js", "/spreadsheet_dashboard_edition/static/src/assets/dashboard_edit_action_loader.js", "/spreadsheet_dashboard_edition/static/src/assets/spreadsheet_selector_panel_patch.js", "/spreadsheet_sale_management/static/src/assets/field_sync_action_loader.js", "/website_enterprise/static/src/client_actions/configurator/configurator.js", "/website_generator/static/src/client_actions/configurator/configurator.js", "/website_generator/static/src/client_actions/generator_wait/generator.js", "/website_generator/static/src/systray_items/generator_request.js", "/website_knowledge/static/src/backend/client_actions/website_preview/website_builder_action.js", "/website_knowledge/static/src/backend/components/permission_panel/permission_panel.js", "/website_knowledge/static/src/backend/components/sidebar/sidebar.js", "/website_blog/static/src/tours/website_blog.js", "/website_forum/static/src/js/tours/website_forum.js", "/web_enterprise/static/src/main.js", "/web/static/src/start.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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACzPA;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AC95QA;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AC7rMA;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;ACLA;;;;;;;;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;;;;ACzPA;;;;;;;;AAAA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;ACTA;;;;;;;;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;;;;ACtQA;;;;;;;;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;;;;AC/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;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACxNA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC7TA;;;;;;;;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;;;;AChHA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACvaA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;ACpLA;;;;;;;;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;;;;ACtWA;;;;;;;;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;;;;AC9qBA;;;;;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACzNA;;;;;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACpYA;;;;;;;;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;;;;AC7GA;;;;;;;;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;;;;ACrFA;;;;;;;;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;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACtoBA;;;;;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACviBA;;;;;;;;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;;;;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;;;;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;;;;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;;;;ACxDA;;;;;;;;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;;;;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;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC1aA;;;;;;;;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;;;;AC5JA;;;;;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC9GA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACnJA;;;;;;;;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;;;;ACtCA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACpYA;;;;;;;;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;;;;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;;;;AC1DA;;;;;;;;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;;;;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;;;;ACvFA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACtrBA;;;;;;;;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;;;;AChCA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC1LA;;;;;;;;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;;;;AC5LA;;;;;;;;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;;;;AC1LA;;;;;;;;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;;;;ACjHA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACjFA;;;;;;;;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;;;;ACxOA;;;;;;;;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;;;;ACxHA;;;;;;;;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;;;;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;;;;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;;;;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;;;;ACtIA;;;;;;;;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;;;;ACxQA;;;;;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACjeA;;;;;;;;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;;;;AC/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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACrsBA;;;;;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACvSA;;;;;;;;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;;;;ACHA;;;;;;;;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;;;;AC3EA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACvLA;;;;;;;;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;;;;AC9OA;;;;;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACnVA;;;;;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACvcA;;;;;;;;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;;;;AC9jBA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACpPA;;;;;;;;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;;;;AC9KA;;;;;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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/ZA;;;;;;;;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;;;;AC/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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AChNA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACnRA;;;;;;;;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;;;;AC5EA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACtUA;;;;;;;;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;;;;ACjLA;;;;;;;;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;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACn4BA;;;;;;;;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;;;;ACpeA;;;;;;;;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;;;;ACxYA;;;;;;;;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;;;;AC5TA;;;;;;;;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;;;;AChIA;;;;;;;;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;;;;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;;;;ACpEA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACpKA;;;;;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACrVA;;;;;;;;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;;;;ACrCA;;;;;;;;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;;;;AChXA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC9RA;;;;;;;;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;;;;ACZA;;;;;;;;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;;;;AC3QA;;;;;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC5VA;;;;;;;;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;;;;AC5EA;;;;;;;;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;;;;ACjCA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACPA;;;;;;;;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;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACvOA;;;;;;;;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;;;;AChFA;;;;;;;;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;;;;ACrFA;;;;;;;;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;;;;AClLA;;;;;;;;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;;;;ACzXA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACPA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACvaA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACrWA;;;;;;;;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;;;;ACxFA;;;;;;;;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;;;;AC3PA;;;;;;;;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;;;;AC3RA;;;;;;;;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;;;;ACnDA;;;;;;;;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;;;;AClRA;;;;;;;;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;;;;AChCA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;;;;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;;;;ACjDA;;;;;;;;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;;;;AC9jCA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC9GA;;;;;;;;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;;;;ACjCA;;;;;;;;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;;;;AC/RA;;;;;;;;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;;;;ACpSA;;;;;;;;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;;;;AChPA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACtZA;;;;;;;;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;;;;AClSA;;;;;;;;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;;;;ACxIA;;;;;;;;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;;;;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;;;;AClEA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACvMA;;;;;;;;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;;;;ACxKA;;;;;;;;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;;;;ACvVA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;ACpNA;;;;;;;;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;;;;AC3MA;;;;;;;;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;;;;ACzKA;;;;;;;;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;;;;AChKA;;;;;;;;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;;;;ACpLA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACjyDA;;;;;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;ACzRA;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AC7OA;;;;;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;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;ACpEA;;;;;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;AC3IA;;;;;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;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;ACvIA;;;;;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;ACpFA;;;;;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;AC1FA;;;;;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;AC/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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACpYA;;;;;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;ACzPA;;;;;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;ACnZA;;;;;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;AChUA;;;;;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;ACtPA;;;;;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;ACliBA;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACnRA;;;;;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;AC7RA;;;;;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;ACvMA;;;;;;;;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;;;;AChHA;;;;;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;AC1hDA;;;;;;;;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;;;;ACzPA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AChEA;;;;;;;;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;;;;ACjYA;;;;;;;;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;;;;AChgBA;;;;;;;;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;;;;ACrLA;;;;;;;;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;;;;ACxIA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACn3CA;;;;;;;;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;;;;AC32BA;;;;;;;;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;;;;AClqCA;;;;;;;;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;;;;ACz4BA;;;;;;;;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;;;;AC51BA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC1LA;;;;;;;;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;;;;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;;;;ACpsBA;;;;;;;;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;;;;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;;;;ACvCA;;;;;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC9ZA;;;;;;;;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;;;;ACzsBA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC9KA;;;;;;;;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;;;;ACryEA;;;;;;;;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;;;;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;;;;ACpVA;;;;;;;;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;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC9aA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACjLA;;;;;;;;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;;;;AClgCA;;;;;;;;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;;;;ACvDA;;;;;;;;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;;;;ACjCA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;;;;AClDA;;;;;;;;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;;;;AC/OA;;;;;;;;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;;;;ACrFA;;;;;;;;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;;;;ACnDA;;;;;;;;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;;;;AClaA;;;;;;;;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;;;;AClBA;;;;;;;;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;;;;AClEA;;;;;;;;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;;;;ACvHA;;;;;;;;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;;;;ACjDA;;;;;;;;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;;;;ACjCA;;;;;;;;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;;;;AC1GA;;;;;;;;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;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;;;;ACvCA;;;;;;;;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;;;;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;;;;AC9HA;;;;;;;;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;;;;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;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;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AChlBA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACrVA;;;;;;;;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;;;;ACtGA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACleA;;;;;;;;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;;;;AC1FA;;;;;;;;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;;;;AChCA;;;;;;;;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;;;;AC5GA;;;;;;;;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;;;;AChKA;;;;;;;;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;;;;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;AACA;AACA;AACA;AACA;;;;ACtGA;;;;;;;;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;;;;AC1dA;;;;;;;;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;;;;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;;;;AC3BA;;;;;;;;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;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACzTA;;;;;;;;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;;;;ACpEA;;;;;;;;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;;;;AC9LA;;;;;;;;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;AACA;AACA;;;;ACjBA;;;;;;;;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;;;;ACpLA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC1DA;;;;;;;;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;;;;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;;;;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;;;;AC1FA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACtYA;;;;;;;;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;;;;ACpKA;;;;;;;;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;;;;ACjWA;;;;;;;;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;;;;AC5HA;;;;;;;;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;AACA;AACA;;;;ACjCA;;;;;;;;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;;;;AChCA;;;;;;;;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;;;;ACjDA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACpIA;;;;;;;;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;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AChHA;;;;;;;;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;;;;ACjCA;;;;;;;;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;;;;ACnDA;;;;;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACtKA;;;;;;;;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;;;;AClBA;;;;;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AChSA;;;;;;;;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;;;;ACtUA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC1YA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACvPA;;;;;;;;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;;;;AC9/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;;;;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;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACrLA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AClEA;;;;;;;;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;;;;AC1GA;;;;;;;;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;;;;ACpXA;;;;;;;;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;AACA;AACA;AACA;;;;ACrGA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AChHA;;;;;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AChWA;;;;;;;;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;;;;ACjDA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;;;;ACLA;;;;;;;;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;;;;ACvqBA;;;;;;;;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;;;;ACnsBA;;;;;;;;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;;;;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;;;;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;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;;;;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;AACA;AACA;AACA;;;;ACrJA;;;;;;;;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;;;;ACrCA;;;;;;;;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;;;;AChEA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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/LA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;ACjFA;;;;;;;;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;;;;AC5LA;;;;;;;;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;;;;ACphBA;;;;;;;;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;;;;ACjBA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACvZA;;;;;;;;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;;;;AC/QA;;;;;;;;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;;;;ACvsBA;;;;;;;;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;;;;AC/XA;;;;;;;;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;;;;AC3jBA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC3PA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AClkBA;;;;;;;;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;;;;ACzwEA;;;;;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACxTA;;;;;;;;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;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACpKA;;;;;;;;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;;;;ACpJA;;;;;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACtFA;;;;;;;;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;;;;AC1GA;;;;;;;;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;;;;ACrFA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACraA;;;;;;;;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;;;;ACvIA;;;;;;;;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;;;;ACnIA;;;;;;;;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;;;;AC1NA;;;;;;;;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;;;;ACxFA;;;;;;;;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;;;;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;;;;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;;;;AC9EA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;;;;ACJA;;;;;;;;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;;;;AC3IA;;;;;;;;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;;;;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;;;;ACrCA;;;;;;;;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;;;;ACn1DA;;;;;;;;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;;;;AC5FA;;;;;;;;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;;;;ACnEA;;;;;;;;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;;;;ACjCA;;;;;;;;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;;;;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;;;;ACjCA;;;;;;;;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;;;;AChVA;;;;;;;;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;;;;ACtGA;;;;;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AClEA;;;;;;;;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;;;;ACrFA;;;;;;;;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;;;;AC/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;;;;AC9GA;;;;;;;;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;;;;AClPA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACzQA;;;;;;;;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;;;;ACjEA;;;;;;;;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;;;;AC1GA;;;;;;;;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;;;;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;;;;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;;;;ACzBA;;;;;;;;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;;;;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;;;;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;;;;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;;;;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;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;;;;ACnGA;;;;;;;;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;;;;AC/HA;;;;;;;;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;;;;ACpJA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;AClBA;;;;;;;;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;;;;AChCA;;;;;;;;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;;;;ACnJA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AChEA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC5WA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACtMA;;;;;;;;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;;;;ACjEA;;;;;;;;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;;;;ACtFA;;;;;;;;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;;;;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;;;;AChFA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC1LA;;;;;;;;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;;;;AC/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;;;;AC1FA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACjCA;;;;;;;;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;;;;ACjEA;;;;;;;;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;;;;AChEA;;;;;;;;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;;;;ACtPA;;;;;;;;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;;;;AClEA;;;;;;;;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;;;;ACxIA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACrlBA;;;;;;;;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;;;;AC/NA;;;;;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;ACjCA;;;;;;;;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;;;;ACxOA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;AC1FA;;;;;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACxUA;;;;;;;;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;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACxLA;;;;;;;;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;;;;ACvaA;;;;;;;;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;;;;ACpNA;;;;;;;;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;;;;ACvCA;;;;;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACtUA;;;;;;;;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;;;;AChCA;;;;;;;;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;;;;AClDA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;AChBA;;;;;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACpmBA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACpEA;;;;;;;;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;;;;ACvCA;;;;;;;;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;;;;AChFA;;;;;;;;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;;;;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;;;;ACzDA;;;;;;;;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;;;;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;;;;ACvFA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AChLA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AClXA;;;;;;;;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;;;;AC13BA;;;;;;;;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;;;;AC/kBA;;;;;;;;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;;;;ACnWA;;;;;;;;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;;;;ACtNA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACrUA;;;;;;;;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;;;;ACjCA;;;;;;;;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;;;;AChIA;;;;;;;;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;;;;ACvSA;;;;;;;;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;;;;AC5HA;;;;;;;;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;;;;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;;;;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;;;;AChEA;;;;;;;;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;;;;AClRA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AChCA;;;;;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACpWA;;;;;;;;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;;;;AC7FA;;;;;;;;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;;;;AClLA;;;;;;;;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;;;;AC7GA;;;;;;;;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;;;;AClPA;;;;;;;;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;;;;AChPA;;;;;;;;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;;;;ACjtBA;;;;;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACn7CA;;;;;;;;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;;;;ACxpBA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACvtBA;;;;;;;;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;;;;ACn3DA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;AC7FA;;;;;;;;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;;;;ACtPA;;;;;;;;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;;;;AC9GA;;;;;;;;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;;;;AC5LA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC3lCA;;;;;;;;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;;;;ACxHA;;;;;;;;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;;;;AC7XA;;;;;;;;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;;;;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;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC5JA;;;;;;;;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;;;;ACtIA;;;;;;;;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;;;;ACpHA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACtNA;;;;;;;;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;;;;ACjEA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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/iBA;;;;;;;;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;;;;ACtGA;;;;;;;;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;;;;ACnKA;;;;;;;;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;;;;AC5EA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACloBA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACnKA;;;;;;;;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;;;;ACvQA;;;;;;;;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;;;;AC7GA;;;;;;;;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;;;;AC3MA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACRA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AClzCA;;;;;;;;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;;;;ACzsBA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC1wCA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;ACrCA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;AChCA;;;;;;;;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;;;;ACxMA;;;;;;;;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;;;;ACnVA;;;;;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACxaA;;;;;;;;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;;;;ACxgBA;;;;;;;;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;;;;AChCA;;;;;;;;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;;;;ACxHA;;;;;;;;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;;;;AC9WA;;;;;;;;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;;;;ACtRA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC3iBA;;;;;;;;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;;;;ACjCA;;;;;;;;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;;;;AClDA;;;;;;;;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;;;;AC3MA;;;;;;;;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;;;;AClEA;;;;;;;;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;;;;AC/QA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACxPA;;;;;;;;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;;;;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;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;AChFA;;;;;;;;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;;;;AC71CA;;;;;;;;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;;;;ACtWA;;;;;;;;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;;;;AClMA;;;;;;;;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;;;;ACvPA;;;;;;;;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;;;;AChCA;;;;;;;;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;;;;AC7FA;;;;;;;;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;;;;AC9iBA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACvrBA;;;;;;;;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;;;;ACt1BA;;;;;;;;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;;;;ACpTA;;;;;;;;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;;;;AC9KA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACtGA;;;;;;;;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;;;;ACvCA;;;;;;;;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;;;;AClLA;;;;;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC3SA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACjEA;;;;;;;;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;;;;AC3HA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACllBA;;;;;;;;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;;;;ACbA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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/UA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACtPA;;;;;;;;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;;;;AC1HA;;;;;;;;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;;;;AC1HA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;;;;ACJA;;;;;;;;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;;;;AClaA;;;;;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACtWA;;;;;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;ACjFA;;;;;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;;;;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;AACA;AACA;AACA;AACA;;;;AC3EA;;;;;;;;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;;;;ACpBA;;;;;;;;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;;;;ACdA;;;;;;;;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;;;;ACZA;;;;;;;;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;;;;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;;;;AChEA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;ACJA;;;;;;;;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;;;;AClNA;;;;;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC3QA;;;;;;;;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;;;;ACpnBA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AClTA;;;;;;;;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;;;;AC5RA;;;;;;;;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;;;;ACrfA;;;;;;;;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;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;ACpIA;;;;;;;;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;;;;ACrMA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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/KA;;;;;;;;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;;;;AChEA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACrFA;;;;;;;;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;;;;AC1LA;;;;;;;;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;;;;ACpKA;;;;;;;;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;;;;AClMA;;;;;;;;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;;;;ACpHA;;;;;;;;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;;;;ACr8BA;;;;;;;;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;;;;ACvNA;;;;;;;;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;;;;ACpIA;;;;;;;;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;;;;ACVA;;;;;;;;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;;;;ACjEA;;;;;;;;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;;;;ACbA;;;;;;;;AAAA;AACA;AACA;AACA;;;;ACHA;;;;;;;;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;;;;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;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;;;;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;AACA;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;;;;AC3EA;;;;;;;;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;;;;ACjDA;;;;;;;;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;;;;ACtEA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACbA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACRA;;;;;;;;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;;;;ACxFA;;;;;;;;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;;;;ACrHA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACrPA;;;;;;;;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;;;;AC7fA;;;;;;;;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;;;;AChQA;;;;;;;;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;;;;ACpEA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AChCA;;;;;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC5sBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACNA;;;;;;;;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;;;;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;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC1HA;;;;;;;;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;;;;ACjLA;;;;;;;;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;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AClLA;;;;;;;;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;;;;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;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC7JA;;;;;;;;AAAA;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;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;;;;ACbA;;;;;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC3IA;;;;;;;;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;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;;;;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;;;;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;;;;AChCA;;;;;;;;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;;;;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;;;;AC5FA;;;;;;;;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;;;;ACxyBA;;;;;;;;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;;;;AC1SA;;;;;;;;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;;;;ACpXA;;;;;;;;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;;;;ACzsBA;;;;;;;;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;;;;AClPA;;;;;;;;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;;;;AClDA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACj5BA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC3EA;;;;;;;;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;;;;AC5FA;;;;;;;;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;;;;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;;;;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;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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/MA;;;;;;;;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;;;;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;;;;AC5CA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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/HA;;;;;;;;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;;;;AC5JA;;;;;;;;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;;;;AC3HA;;;;;;;;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;;;;ACjFA;;;;;;;;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;;;;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;;;;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;;;;ACpHA;;;;;;;;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;;;;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;;;;AChCA;;;;;;;;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;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;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;;;;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;;;;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;;;;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;;;;ACjFA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACTA;;;;;;;;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;;;;ACjDA;;;;;;;;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;;;;AC1GA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC1GA;;;;;;;;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;;;;AChHA;;;;;;;;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;;;;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;;;;ACxIA;;;;;;;;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;;;;ACrMA;;;;;;;;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;;;;ACvBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACTA;;;;;;;;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;;;;AChRA;;;;;;;;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;;;;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;;;;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;;;;ACzLA;;;;;;;;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;;;;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;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACvpBA;;;;;;;;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;;;;ACvNA;;;;;;;;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;;;;AC3WA;;;;;;;;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;;;;AC/HA;;;;;;;;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;;;;ACjBA;;;;;;;;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;;;;AC7dA;;;;;;;;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;;;;ACpHA;;;;;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC7JA;;;;;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACtGA;;;;;;;;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;;;;ACbA;;;;;;;;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;;;;ACpBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACTA;;;;;;;;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;;;;ACTA;;;;;;;;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;;;;ACjCA;;;;;;;;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;;;;ACtKA;;;;;;;;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;;;;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;;;;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;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACxFA;;;;;;;;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;;;;ACjFA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC9kEA;;;;;;;;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;;;;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;;;;AChCA;;;;;;;;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;;;;AC1DA;;;;;;;;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;;;;AChIA;;;;;;;;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;;;;AC9JA;;;;;;;;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;;;;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;;;;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;;;;ACpFA;;;;;;;;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;;;;ACrFA;;;;;;;;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;;;;ACrBA;;;;;;;;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;;;;ACtBA;;;;;;;;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;;;;AC5EA;;;;;;;;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;;;;AChBA;;;;;;;;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;;;;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;;;;ACnDA;;;;;;;;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;AACA;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;;;;ACnDA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;;;;AC5JA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACrKA;;;;;;;;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;AACA;;;;AClBA;;;;;;;;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;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;AACA;;;;AC1CA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC7FA;;;;;;;;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;;;;ACtIA;;;;;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACpFA;;;;;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACrHA;;;;;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACtTA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC9GA;;;;;;;;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;;;;ACzQA;;;;;;;;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;;;;ACnBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACVA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;;;;ACJA;;;;;;;;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;;;;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;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACxJA;;;;;;;;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;;;;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;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;;;;AClDA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;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;;;;AC5CA;;;;;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC7LA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACzPA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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/WA;;;;;;;;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;;;;ACnJA;;;;;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACtRA;;;;;;;;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;;;;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;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;;;;AC/SA;;;;;;;;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;;;;AC9JA;;;;;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;AACA;AACA;AACA;AACA;;;;AC3BA;;;;;;;;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;;;;AC3EA;;;;;;;;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;;;;ACvCA;;;;;;;;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;;;;ACjDA;;;;;;;;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;;;;ACx6BA;;;;;;;;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;;;;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;;;;ACjCA;;;;;;;;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;;;;ACxBA;;;;;;;;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;;;;ACZA;;;;;;;;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;;;;ACv5EA;;;;;;;;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;;;;AC9PA;;;;;;;;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;;;;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;;;;ACnDA;;;;;;;;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;;;;AC1NA;;;;;;;;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;;;;ACvDA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACjDA;;;;;;;;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;;;;ACvTA;;;;;;;;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;;;;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;;;;AC1EA;;;;;;;;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;;;;ACjBA;;;;;;;;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;;;;AChCA;;;;;;;;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;;;;ACjDA;;;;;;;;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;;;;ACRA;;;;;;;;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;;;;ACvCA;;;;;;;;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;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;;;;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;;;;AC3CA;;;;;;;;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;;;;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;AACA;AACA;AACA;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;;;;AC9BA;;;;;;;;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;;;;ACzYA;;;;;;;;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;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;ACzBA;;;;;;;;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;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;;;;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;;;;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;;;;ACvFA;;;;;;;;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;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AChEA;;;;;;;;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;;;;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;;;;ACnDA;;;;;;;;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;;;;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;;;;ACpIA;;;;;;;;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;;;;ACzRA;;;;;;;;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;;;;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;;;;AC9BA;;;;;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC5EA;;;;;;;;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;;;;ACjFA;;;;;;;;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;;;;AChIA;;;;;;;;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;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;AACA;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;;;;ACrCA;;;;;;;;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;;;;AC7JA;;;;;;;;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;;;;AC3uBA;;;;;;;;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;;;;ACxFA;;;;;;;;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;;;;ACVA;;;;;;;;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;;;;ACjBA;;;;;;;;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;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;;;;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;;;;ACvCA;;;;;;;;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;;;;AChEA;;;;;;;;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;;;;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;;;;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;;;;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;;;;ACpFA;;;;;;;;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;;;;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;;;;AC5CA;;;;;;;;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;;;;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;;;;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;;;;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;;;;ACzDA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;ACxDA;;;;;;;;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;;;;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;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;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;;;;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;;;;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;;;;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;;;;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;;;;ACnDA;;;;;;;;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;;;;AChFA;;;;;;;;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;;;;ACjEA;;;;;;;;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;;;;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;;;;AC3BA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACRA;;;;;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACpDA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACTA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACtFA;;;;;;;;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;;;;AC3EA;;;;;;;;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;;;;AC7FA;;;;;;;;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;;;;ACvkBA;;;;;;;;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;;;;AC7KA;;;;;;;;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;;;;ACzKA;;;;;;;;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;;;;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;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;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;;;;AC7CA;;;;;;;;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;;;;ACdA;;;;;;;;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;;;;AC/CA;;;;;;;;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;;;;ACbA;;;;;;;;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;;;;ACTA;;;;;;;;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;;;;ACbA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACTA;;;;;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AChCA;;;;;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACrrFA;;;;;;;;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;;;;AC9BA;;;;;;;;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;;;;ACpBA;;;;;;;;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;;;;ACzPA;;;;;;;;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;;;;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;;;;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;;;;ACzEA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACpgBA;;;;;;;;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;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACldA;;;;;;;;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;;;;ACjEA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACRA;;;;;;;;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;;;;ACZA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACRA;;;;;;;;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;;;;ACZA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACVA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACVA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACVA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACtTA;;;;;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC1fA;;;;;;;;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;;;;AC1DA;;;;;;;;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;;;;AChHA;;;;;;;;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;;;;AClBA;;;;;;;;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;;;;ACjFA;;;;;;;;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;;;;AClDA;;;;;;;;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;;;;AC/iBA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AClaA;;;;;;;;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;;;;AC9SA;;;;;;;;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;;;;AC3kCA;;;;;;;;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;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACzKA;;;;;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACxPA;;;;;;;;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;;;;AC9FA;;;;;;;;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;;;;ACzBA;;;;;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACvCA;;;;;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACxKA;;;;;;;;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;;;;AC9vBA;;;;;;;;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;;;;AC9iBA;;;;;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACtlCA;;;;;;;;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;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;;;;ACRA;;;;;;;;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;;;;AC1HA;;;;;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC1SA;;;;;;;;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;;;;AChFA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC7wBA;;;;;;;;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;;;;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;;;;AChFA;;;;;;;;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;;;;ACZA;;;;;;;;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;;;;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;;;;AChCA;;;;;;;;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;;;;ACzUA;;;;;;;;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;;;;AChCA;;;;;;;;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;;;;ACrHA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;;;;ACnDA;;;;;;;;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;;;;AC1WA;;;;;;;;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;;;;AC3kBA;;;;;;;;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;;;;AClEA;;;;;;;;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;;;;ACbA;;;;;;;;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;;;;AC1DA;;;;;;;;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;;;;AC9GA;;;;;;;;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;;;;ACtIA;;;;;;;;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;;;;AC/CA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AClTA;;;;;;;;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;;;;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;AACA;;;;AChCA;;;;;;;;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;;;;AC7JA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC7KA;;;;;;;;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;;;;AC5FA;;;;;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC9LA;;;;;;;;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;;;;AC9GA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AClPA;;;;;;;;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;;;;ACvFA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;;;;ACrEA;;;;;;;;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;;;;AChEA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC7FA;;;;;;;;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;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;;;;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;;;;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;;;;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;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;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACrCA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC3EA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AClEA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACtKA;;;;;;;;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;;;;ACnDA;;;;;;;;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;;;;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;;;;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;;;;AC9CA;;;;;;;;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;;;;AChBA;;;;;;;;AAAA;AACA;AACA;AACA;;;;ACHA;;;;;;;;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;;;;AChFA;;;;;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC5RA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;ACjBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACNA;;;;;;;;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;;;;ACvHA;;;;;;;;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;;;;ACjPA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACNA;;;;;;;;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;;;;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;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC9JA;;;;;;;;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;;;;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;;;;ACrFA;;;;;;;;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;;;;ACnSA;;;;;;;;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;;;;ACpHA;;;;;;;;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;;;;AC5JA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC1FA;;;;;;;;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;;;;ACpHA;;;;;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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/HA;;;;;;;;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;;;;AC9LA;;;;;;;;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;;;;AChYA;;;;;;;;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;;;;AClEA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACz9BA;;;;;;;;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;;;;AC9GA;;;;;;;;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;;;;ACvHA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACrNA;;;;;;;;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;;;;ACpKA;;;;;;;;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;;;;ACnDA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;AClDA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;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;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC7JA;;;;;;;;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;;;;AC1TA;;;;;;;;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;;;;AC/DA;;;;;;;;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;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;;;;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;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;;;;ACjDA;;;;;;;;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;;;;ACjKA;;;;;;;;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;;;;AC1FA;;;;;;;;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;;;;ACnZA;;;;;;;;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;;;;AC1DA;;;;;;;;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;;;;AChCA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC3eA;;;;;;;;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;;;;ACxHA;;;;;;;;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;;;;AClEA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC9lBA;;;;;;;;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;;;;AChLA;;;;;;;;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;;;;ACpLA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC3XA;;;;;;;;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;;;;AC5EA;;;;;;;;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;;;;AC/BA;;;;;;;;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;;;;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;;;;ACxBA;;;;;;;;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;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;;;;AC7BA;;;;;;;;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;;;;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;;;;AC5FA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACrUA;;;;;;;;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;;;;ACvCA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AClBA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AClKA;;;;;;;;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;;;;AC7BA;;;;;;;;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;;;;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;;;;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;;;;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;;;;AC5CA;;;;;;;;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;;;;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;;;;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;;;;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;;;;ACpIA;;;;;;;;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;;;;ACrHA;;;;;;;;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;;;;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;;;;AClCA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;;;;ACtFA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;AC7BA;;;;;;;;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;;;;ACrCA;;;;;;;;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;;;;ACxIA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACbA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACRA;;;;;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACzXA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC3EA;;;;;;;;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;;;;ACvDA;;;;;;;;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;;;;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;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;AACA;AACA;AACA;AACA;;;;ACjBA;;;;;;;;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;;;;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;;;;AClEA;;;;;;;;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;;;;ACpBA;;;;;;;;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;;;;ACZA;;;;;;;;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;;;;AChFA;;;;;;;;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;;;;AC9JA;;;;;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;ACtBA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AChCA;;;;;;;;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;;;;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;;;;ACzLA;;;;;;;;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;;;;AC9oBA;;;;;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC5EA;;;;;;;;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;;;;ACpFA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC7aA;;;;;;;;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;;;;ACtKA;;;;;;;;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;;;;ACv8DA;;;;;;;;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;;;;ACjtEA;;;;;;;;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;;;;ACvxBA;;;;;;;;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;;;;AC/LA;;;;;;;;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;;;;ACjCA;;;;;;;;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;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC1JA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACPA;;;;;;;;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;;;;ACdA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC1GA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC1GA;;;;;;;;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;;;;AC3EA;;;;;;;;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;;;;AC3IA;;;;;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACjoBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACTA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACxVA;;;;;;;;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;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACvNA;;;;;;;;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;;;;AC/EA;;;;;;;;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;;;;AChCA;;;;;;;;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;;;;AChCA;;;;;;;;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;;;;ACpFA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC7SA;;;;;;;;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;;;;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;;;;ACtGA;;;;;;;;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;;;;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;;;;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;;;;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;;;;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;;;;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;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;;;;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;;;;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;;;;AClDA;;;;;;;;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;;;;AChCA;;;;;;;;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;;;;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;;;;ACjDA;;;;;;;;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;;;;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;;;;ACnDA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACxSA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACTA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC5FA;;;;;;;;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;;;;AC5EA;;;;;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACz2BA;;;;;;;;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;;;;AClDA;;;;;;;;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;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;;;;ACvDA;;;;;;;;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;;;;AC1DA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC1KA;;;;;;;;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;;;;ACrpBA;;;;;;;;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;;;;AC3IA;;;;;;;;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;;;;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;;;;AChCA;;;;;;;;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;;;;AC12BA;;;;;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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/2BA;;;;;;;;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;;;;AC1HA;;;;;;;;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;;;;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;;;;AChLA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;AC/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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC/OA;;;;;;;;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;;;;AClDA;;;;;;;;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;;;;ACZA;;;;;;;;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;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;;;;AChDA;;;;;;;;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;;;;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;;;;ACnDA;;;;;;;;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;;;;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;;;;AClKA;;;;;;;;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;;;;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;;;;ACjEA;;;;;;;;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;;;;ACZA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AClEA;;;;;;;;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;;;;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;;;;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;;;;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;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC7GA;;;;;;;;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;;;;ACvCA;;;;;;;;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;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACrBA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACtCA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;;;;ACLA;;;;;;;;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;;;;ACXA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACbA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACRA;;;;;;;;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;;;;ACtBA;;;;;;;;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;;;;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;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;;;;ACrCA;;;;;;;;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;;;;AC7FA;;;;;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC7VA;;;;;;;;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;AACA;AACA;AACA;AACA;;;;AClDA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACpEA;;;;;;;;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;;;;ACLA;;;;;;;;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;;;;AClDA;;;;;;;;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;;;;ACvFA;;;;;;;;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;;;;AClEA;;;;;;;;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;;;;AChEA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACTA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;;;;ACvCA;;;;;;;;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;;;;AClEA;;;;;;;;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;;;;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;;;;ACtFA;;;;;;;;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;;;;ACvCA;;;;;;;;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;;;;ACjCA;;;;;;;;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;;;;AC3KA;;;;;;;;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;;;;AC7NA;;;;;;;;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;;;;AChKA;;;;;;;;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;;;;ACxMA;;;;;;;;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;;;;ACtFA;;;;;;;;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;;;;ACpLA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACxNA;;;;;;;;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;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;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACjFA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;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;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;;;;AClCA;;;;;;;;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;;;;ACZA;;;;;;;;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;;;;AC9BA;;;;;;;;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;;;;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;;;;ACjCA;;;;;;;;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;;;;ACRA;;;;;;;;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;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;;;;ACtBA;;;;;;;;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;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC7RA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACTA;;;;;;;;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;;;;AClQA;;;;;;;;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;;;;AC7LA;;;;;;;;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;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;AC7CA;;;;;;;;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;;;;AClCA;;;;;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;AC/MA;;;;;;;;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;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACtCA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC5YA;;;;;;;;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;;;;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;;;;AC5BA;;;;;;;;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;;;;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;;;;ACvKA;;;;;;;;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;;;;AClDA;;;;;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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/MA;;;;;;;;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;;;;AC5HA;;;;;;;;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;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;AACA;;;;ACxEA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACNA;;;;;;;;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;;;;ACtIA;;;;;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;;;;ACrJA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;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;;;;AClBA;;;;;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC9GA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AChIA;;;;;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;AC3BA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;ACvBA;;;;;;;;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;;;;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;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;;;;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;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;;;;ACRA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACNA;;;;;;;;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;;;;ACjDA;;;;;;;;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;;;;ACJA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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/JA;;;;;;;;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;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;;;;ACnCA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;;;;ACpCA;;;;;;;;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;;;;AC5BA;;;;;;;;AAAA;AACA;AACA;AACA;;;;ACHA;;;;;;;;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;;;;AChFA;;;;;;;;AAAA;AACA;AACA;AACA;;;;ACHA;;;;;;;;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;;;;ACpDA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACvCA;;;;;;;;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;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;;;;AC5EA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACRA;;;;;;;;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;;;;ACjBA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACtGA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACTA;;;;;;;;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", "sourcesContent": ["// @odoo-module ignore\n\n//-----------------------------------------------------------------------------\n// Odoo Web Boostrap Code\n//-----------------------------------------------------------------------------\n\n(function (odoo) {\n    \"use strict\";\n\n    if (odoo.loader) {\n        // Allows for duplicate calls to `module_loader`: only the first one is\n        // executed.\n        return;\n    }\n\n    class ModuleLoader {\n        /** @type {OdooModuleLoader[\"bus\"]} */\n        bus = new EventTarget();\n        /** @type {OdooModuleLoader[\"checkErrorProm\"]} */\n        checkErrorProm = null;\n        /** @type {OdooModuleLoader[\"factories\"]} */\n        factories = new Map();\n        /** @type {OdooModuleLoader[\"failed\"]} */\n        failed = new Set();\n        /** @type {OdooModuleLoader[\"jobs\"]} */\n        jobs = new Set();\n        /** @type {OdooModuleLoader[\"modules\"]} */\n        modules = new Map();\n\n        /**\n         * @param {HTMLElement} [root]\n         */\n        constructor(root) {\n            this.root = root;\n\n            const strDebug = new URLSearchParams(location.search).get(\"debug\");\n            this.debug = Boolean(strDebug && strDebug !== \"0\");\n        }\n\n        /** @type {OdooModuleLoader[\"addJob\"]} */\n        addJob(name) {\n            this.jobs.add(name);\n            this.startModules();\n        }\n\n        /** @type {OdooModuleLoader[\"define\"]} */\n        define(name, deps, factory, lazy = false) {\n            if (typeof name !== \"string\") {\n                throw new Error(`Module name should be a string, got: ${String(name)}`);\n            }\n            if (!Array.isArray(deps)) {\n                throw new Error(\n                    `Module dependencies should be a list of strings, got: ${String(deps)}`\n                );\n            }\n            if (typeof factory !== \"function\") {\n                throw new Error(`Module factory should be a function, got: ${String(factory)}`);\n            }\n            if (this.factories.has(name)) {\n                return; // Ignore duplicate modules\n            }\n            this.factories.set(name, {\n                deps,\n                fn: factory,\n                ignoreMissingDeps: globalThis.__odooIgnoreMissingDependencies,\n            });\n            if (!lazy) {\n                this.addJob(name);\n                this.checkErrorProm ||= Promise.resolve().then(() => {\n                    this.checkErrorProm = null;\n                    this.reportErrors(this.findErrors());\n                });\n            }\n        }\n\n        /** @type {OdooModuleLoader[\"findErrors\"]} */\n        findErrors(moduleNames) {\n            /**\n             * @param {Iterable<string>} currentModuleNames\n             * @param {Set<string>} visited\n             * @returns {string | null}\n             */\n            const findCycle = (currentModuleNames, visited) => {\n                for (const name of currentModuleNames || []) {\n                    if (visited.has(name)) {\n                        const cycleModuleNames = [...visited, name];\n                        return cycleModuleNames\n                            .slice(cycleModuleNames.indexOf(name))\n                            .map((j) => `\"${j}\"`)\n                            .join(\" => \");\n                    }\n                    const cycle = findCycle(dependencyGraph[name], new Set(visited).add(name));\n                    if (cycle) {\n                        return cycle;\n                    }\n                }\n                return null;\n            };\n\n            moduleNames ||= this.jobs;\n\n            /** @type {Record<string, Iterable<string>>} */\n            const dependencyGraph = Object.create(null);\n            /** @type {Set<string>} */\n            const missing = new Set();\n            /** @type {Set<string>} */\n            const unloaded = new Set();\n\n            for (const moduleName of moduleNames) {\n                const { deps, ignoreMissingDeps } = this.factories.get(moduleName);\n\n                dependencyGraph[moduleName] = deps;\n\n                if (ignoreMissingDeps) {\n                    continue;\n                }\n\n                unloaded.add(moduleName);\n                for (const dep of deps) {\n                    if (!this.factories.has(dep)) {\n                        missing.add(dep);\n                    }\n                }\n            }\n\n            const cycle = findCycle(moduleNames, new Set());\n            const errors = {};\n            if (cycle) {\n                errors.cycle = cycle;\n            }\n            if (this.failed.size) {\n                errors.failed = this.failed;\n            }\n            if (missing.size) {\n                errors.missing = missing;\n            }\n            if (unloaded.size) {\n                errors.unloaded = unloaded;\n            }\n            return errors;\n        }\n\n        /** @type {OdooModuleLoader[\"findJob\"]} */\n        findJob() {\n            for (const job of this.jobs) {\n                if (this.factories.get(job).deps.every((dep) => this.modules.has(dep))) {\n                    return job;\n                }\n            }\n            return null;\n        }\n\n        /** @type {OdooModuleLoader[\"reportErrors\"]} */\n        async reportErrors(errors) {\n            if (!Object.keys(errors).length) {\n                return;\n            }\n\n            if (errors.failed) {\n                console.error(\"The following modules failed to load because of an error:\", [\n                    ...errors.failed,\n                ]);\n            }\n            if (errors.missing) {\n                console.error(\n                    \"The following modules are needed by other modules but have not been defined, they may not be present in the correct asset bundle:\",\n                    [...errors.missing]\n                );\n            }\n            if (errors.cycle) {\n                console.error(\n                    \"The following modules could not be loaded because they form a dependency cycle:\",\n                    errors.cycle\n                );\n            }\n            if (errors.unloaded) {\n                console.error(\n                    \"The following modules could not be loaded because they have unmet dependencies, this is a secondary error which is likely caused by one of the above problems:\",\n                    [...errors.unloaded]\n                );\n            }\n\n            const document = this.root?.ownerDocument || globalThis.document;\n            if (document.readyState === \"loading\") {\n                await new Promise((resolve) =>\n                    document.addEventListener(\"DOMContentLoaded\", resolve)\n                );\n            }\n\n            if (this.debug) {\n                const style = document.createElement(\"style\");\n                style.className = \"o_module_error_banner\";\n                style.textContent = `\n                    body::before {\n                        font-weight: bold;\n                        content: \"An error occurred while loading javascript modules, you may find more information in the devtools console\";\n                        position: fixed;\n                        left: 0;\n                        bottom: 0;\n                        z-index: 100000000000;\n                        background-color: #C00;\n                        color: #DDD;\n                    }\n                `;\n                document.head.appendChild(style);\n            }\n        }\n\n        /** @type {OdooModuleLoader[\"startModules\"]} */\n        startModules() {\n            let job;\n            while ((job = this.findJob())) {\n                this.startModule(job);\n            }\n        }\n\n        /** @type {OdooModuleLoader[\"startModule\"]} */\n        startModule(name) {\n            /** @type {(dependency: string) => OdooModule} */\n            const require = (dependency) => this.modules.get(dependency);\n            this.jobs.delete(name);\n            const factory = this.factories.get(name);\n            /** @type {OdooModule | null} */\n            let module = null;\n            try {\n                module = factory.fn(require);\n            } catch (error) {\n                this.failed.add(name);\n                throw new Error(`Error while loading \"${name}\":\\n${error}`);\n            }\n            this.modules.set(name, module);\n            this.bus.dispatchEvent(\n                new CustomEvent(\"module-started\", {\n                    detail: { moduleName: name, module },\n                })\n            );\n            return module;\n        }\n    }\n\n    const loader = new ModuleLoader();\n    odoo.define = loader.define.bind(loader);\n    odoo.loader = loader;\n\n    if (odoo.debug && !loader.debug) {\n        // remove debug mode if not explicitely set in url\n        odoo.debug = \"\";\n    }\n})((globalThis.odoo ||= {}));\n", "var luxon = (function (exports) {\n  'use strict';\n\n  function _defineProperties(target, props) {\n    for (var i = 0; i < props.length; i++) {\n      var descriptor = props[i];\n      descriptor.enumerable = descriptor.enumerable || false;\n      descriptor.configurable = true;\n      if (\"value\" in descriptor) descriptor.writable = true;\n      Object.defineProperty(target, _toPropertyKey(descriptor.key), descriptor);\n    }\n  }\n  function _createClass(Constructor, protoProps, staticProps) {\n    if (protoProps) _defineProperties(Constructor.prototype, protoProps);\n    if (staticProps) _defineProperties(Constructor, staticProps);\n    Object.defineProperty(Constructor, \"prototype\", {\n      writable: false\n    });\n    return Constructor;\n  }\n  function _extends() {\n    _extends = Object.assign ? Object.assign.bind() : function (target) {\n      for (var i = 1; i < arguments.length; i++) {\n        var source = arguments[i];\n        for (var key in source) {\n          if (Object.prototype.hasOwnProperty.call(source, key)) {\n            target[key] = source[key];\n          }\n        }\n      }\n      return target;\n    };\n    return _extends.apply(this, arguments);\n  }\n  function _inheritsLoose(subClass, superClass) {\n    subClass.prototype = Object.create(superClass.prototype);\n    subClass.prototype.constructor = subClass;\n    _setPrototypeOf(subClass, superClass);\n  }\n  function _getPrototypeOf(o) {\n    _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf.bind() : function _getPrototypeOf(o) {\n      return o.__proto__ || Object.getPrototypeOf(o);\n    };\n    return _getPrototypeOf(o);\n  }\n  function _setPrototypeOf(o, p) {\n    _setPrototypeOf = Object.setPrototypeOf ? Object.setPrototypeOf.bind() : function _setPrototypeOf(o, p) {\n      o.__proto__ = p;\n      return o;\n    };\n    return _setPrototypeOf(o, p);\n  }\n  function _isNativeReflectConstruct() {\n    if (typeof Reflect === \"undefined\" || !Reflect.construct) return false;\n    if (Reflect.construct.sham) return false;\n    if (typeof Proxy === \"function\") return true;\n    try {\n      Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function () {}));\n      return true;\n    } catch (e) {\n      return false;\n    }\n  }\n  function _construct(Parent, args, Class) {\n    if (_isNativeReflectConstruct()) {\n      _construct = Reflect.construct.bind();\n    } else {\n      _construct = function _construct(Parent, args, Class) {\n        var a = [null];\n        a.push.apply(a, args);\n        var Constructor = Function.bind.apply(Parent, a);\n        var instance = new Constructor();\n        if (Class) _setPrototypeOf(instance, Class.prototype);\n        return instance;\n      };\n    }\n    return _construct.apply(null, arguments);\n  }\n  function _isNativeFunction(fn) {\n    return Function.toString.call(fn).indexOf(\"[native code]\") !== -1;\n  }\n  function _wrapNativeSuper(Class) {\n    var _cache = typeof Map === \"function\" ? new Map() : undefined;\n    _wrapNativeSuper = function _wrapNativeSuper(Class) {\n      if (Class === null || !_isNativeFunction(Class)) return Class;\n      if (typeof Class !== \"function\") {\n        throw new TypeError(\"Super expression must either be null or a function\");\n      }\n      if (typeof _cache !== \"undefined\") {\n        if (_cache.has(Class)) return _cache.get(Class);\n        _cache.set(Class, Wrapper);\n      }\n      function Wrapper() {\n        return _construct(Class, arguments, _getPrototypeOf(this).constructor);\n      }\n      Wrapper.prototype = Object.create(Class.prototype, {\n        constructor: {\n          value: Wrapper,\n          enumerable: false,\n          writable: true,\n          configurable: true\n        }\n      });\n      return _setPrototypeOf(Wrapper, Class);\n    };\n    return _wrapNativeSuper(Class);\n  }\n  function _objectWithoutPropertiesLoose(source, excluded) {\n    if (source == null) return {};\n    var target = {};\n    var sourceKeys = Object.keys(source);\n    var key, i;\n    for (i = 0; i < sourceKeys.length; i++) {\n      key = sourceKeys[i];\n      if (excluded.indexOf(key) >= 0) continue;\n      target[key] = source[key];\n    }\n    return target;\n  }\n  function _unsupportedIterableToArray(o, minLen) {\n    if (!o) return;\n    if (typeof o === \"string\") return _arrayLikeToArray(o, minLen);\n    var n = Object.prototype.toString.call(o).slice(8, -1);\n    if (n === \"Object\" && o.constructor) n = o.constructor.name;\n    if (n === \"Map\" || n === \"Set\") return Array.from(o);\n    if (n === \"Arguments\" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen);\n  }\n  function _arrayLikeToArray(arr, len) {\n    if (len == null || len > arr.length) len = arr.length;\n    for (var i = 0, arr2 = new Array(len); i < len; i++) arr2[i] = arr[i];\n    return arr2;\n  }\n  function _createForOfIteratorHelperLoose(o, allowArrayLike) {\n    var it = typeof Symbol !== \"undefined\" && o[Symbol.iterator] || o[\"@@iterator\"];\n    if (it) return (it = it.call(o)).next.bind(it);\n    if (Array.isArray(o) || (it = _unsupportedIterableToArray(o)) || allowArrayLike && o && typeof o.length === \"number\") {\n      if (it) o = it;\n      var i = 0;\n      return function () {\n        if (i >= o.length) return {\n          done: true\n        };\n        return {\n          done: false,\n          value: o[i++]\n        };\n      };\n    }\n    throw new TypeError(\"Invalid attempt to iterate non-iterable instance.\\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.\");\n  }\n  function _toPrimitive(input, hint) {\n    if (typeof input !== \"object\" || input === null) return input;\n    var prim = input[Symbol.toPrimitive];\n    if (prim !== undefined) {\n      var res = prim.call(input, hint || \"default\");\n      if (typeof res !== \"object\") return res;\n      throw new TypeError(\"@@toPrimitive must return a primitive value.\");\n    }\n    return (hint === \"string\" ? String : Number)(input);\n  }\n  function _toPropertyKey(arg) {\n    var key = _toPrimitive(arg, \"string\");\n    return typeof key === \"symbol\" ? key : String(key);\n  }\n\n  // these aren't really private, but nor are they really useful to document\n  /**\n   * @private\n   */\n  var LuxonError = /*#__PURE__*/function (_Error) {\n    _inheritsLoose(LuxonError, _Error);\n    function LuxonError() {\n      return _Error.apply(this, arguments) || this;\n    }\n    return LuxonError;\n  }( /*#__PURE__*/_wrapNativeSuper(Error));\n  /**\n   * @private\n   */\n  var InvalidDateTimeError = /*#__PURE__*/function (_LuxonError) {\n    _inheritsLoose(InvalidDateTimeError, _LuxonError);\n    function InvalidDateTimeError(reason) {\n      return _LuxonError.call(this, \"Invalid DateTime: \" + reason.toMessage()) || this;\n    }\n    return InvalidDateTimeError;\n  }(LuxonError);\n\n  /**\n   * @private\n   */\n  var InvalidIntervalError = /*#__PURE__*/function (_LuxonError2) {\n    _inheritsLoose(InvalidIntervalError, _LuxonError2);\n    function InvalidIntervalError(reason) {\n      return _LuxonError2.call(this, \"Invalid Interval: \" + reason.toMessage()) || this;\n    }\n    return InvalidIntervalError;\n  }(LuxonError);\n\n  /**\n   * @private\n   */\n  var InvalidDurationError = /*#__PURE__*/function (_LuxonError3) {\n    _inheritsLoose(InvalidDurationError, _LuxonError3);\n    function InvalidDurationError(reason) {\n      return _LuxonError3.call(this, \"Invalid Duration: \" + reason.toMessage()) || this;\n    }\n    return InvalidDurationError;\n  }(LuxonError);\n\n  /**\n   * @private\n   */\n  var ConflictingSpecificationError = /*#__PURE__*/function (_LuxonError4) {\n    _inheritsLoose(ConflictingSpecificationError, _LuxonError4);\n    function ConflictingSpecificationError() {\n      return _LuxonError4.apply(this, arguments) || this;\n    }\n    return ConflictingSpecificationError;\n  }(LuxonError);\n\n  /**\n   * @private\n   */\n  var InvalidUnitError = /*#__PURE__*/function (_LuxonError5) {\n    _inheritsLoose(InvalidUnitError, _LuxonError5);\n    function InvalidUnitError(unit) {\n      return _LuxonError5.call(this, \"Invalid unit \" + unit) || this;\n    }\n    return InvalidUnitError;\n  }(LuxonError);\n\n  /**\n   * @private\n   */\n  var InvalidArgumentError = /*#__PURE__*/function (_LuxonError6) {\n    _inheritsLoose(InvalidArgumentError, _LuxonError6);\n    function InvalidArgumentError() {\n      return _LuxonError6.apply(this, arguments) || this;\n    }\n    return InvalidArgumentError;\n  }(LuxonError);\n\n  /**\n   * @private\n   */\n  var ZoneIsAbstractError = /*#__PURE__*/function (_LuxonError7) {\n    _inheritsLoose(ZoneIsAbstractError, _LuxonError7);\n    function ZoneIsAbstractError() {\n      return _LuxonError7.call(this, \"Zone is an abstract class\") || this;\n    }\n    return ZoneIsAbstractError;\n  }(LuxonError);\n\n  /**\n   * @private\n   */\n\n  var n = \"numeric\",\n    s = \"short\",\n    l = \"long\";\n  var DATE_SHORT = {\n    year: n,\n    month: n,\n    day: n\n  };\n  var DATE_MED = {\n    year: n,\n    month: s,\n    day: n\n  };\n  var DATE_MED_WITH_WEEKDAY = {\n    year: n,\n    month: s,\n    day: n,\n    weekday: s\n  };\n  var DATE_FULL = {\n    year: n,\n    month: l,\n    day: n\n  };\n  var DATE_HUGE = {\n    year: n,\n    month: l,\n    day: n,\n    weekday: l\n  };\n  var TIME_SIMPLE = {\n    hour: n,\n    minute: n\n  };\n  var TIME_WITH_SECONDS = {\n    hour: n,\n    minute: n,\n    second: n\n  };\n  var TIME_WITH_SHORT_OFFSET = {\n    hour: n,\n    minute: n,\n    second: n,\n    timeZoneName: s\n  };\n  var TIME_WITH_LONG_OFFSET = {\n    hour: n,\n    minute: n,\n    second: n,\n    timeZoneName: l\n  };\n  var TIME_24_SIMPLE = {\n    hour: n,\n    minute: n,\n    hourCycle: \"h23\"\n  };\n  var TIME_24_WITH_SECONDS = {\n    hour: n,\n    minute: n,\n    second: n,\n    hourCycle: \"h23\"\n  };\n  var TIME_24_WITH_SHORT_OFFSET = {\n    hour: n,\n    minute: n,\n    second: n,\n    hourCycle: \"h23\",\n    timeZoneName: s\n  };\n  var TIME_24_WITH_LONG_OFFSET = {\n    hour: n,\n    minute: n,\n    second: n,\n    hourCycle: \"h23\",\n    timeZoneName: l\n  };\n  var DATETIME_SHORT = {\n    year: n,\n    month: n,\n    day: n,\n    hour: n,\n    minute: n\n  };\n  var DATETIME_SHORT_WITH_SECONDS = {\n    year: n,\n    month: n,\n    day: n,\n    hour: n,\n    minute: n,\n    second: n\n  };\n  var DATETIME_MED = {\n    year: n,\n    month: s,\n    day: n,\n    hour: n,\n    minute: n\n  };\n  var DATETIME_MED_WITH_SECONDS = {\n    year: n,\n    month: s,\n    day: n,\n    hour: n,\n    minute: n,\n    second: n\n  };\n  var DATETIME_MED_WITH_WEEKDAY = {\n    year: n,\n    month: s,\n    day: n,\n    weekday: s,\n    hour: n,\n    minute: n\n  };\n  var DATETIME_FULL = {\n    year: n,\n    month: l,\n    day: n,\n    hour: n,\n    minute: n,\n    timeZoneName: s\n  };\n  var DATETIME_FULL_WITH_SECONDS = {\n    year: n,\n    month: l,\n    day: n,\n    hour: n,\n    minute: n,\n    second: n,\n    timeZoneName: s\n  };\n  var DATETIME_HUGE = {\n    year: n,\n    month: l,\n    day: n,\n    weekday: l,\n    hour: n,\n    minute: n,\n    timeZoneName: l\n  };\n  var DATETIME_HUGE_WITH_SECONDS = {\n    year: n,\n    month: l,\n    day: n,\n    weekday: l,\n    hour: n,\n    minute: n,\n    second: n,\n    timeZoneName: l\n  };\n\n  /**\n   * @interface\n   */\n  var Zone = /*#__PURE__*/function () {\n    function Zone() {}\n    var _proto = Zone.prototype;\n    /**\n     * Returns the offset's common name (such as EST) at the specified timestamp\n     * @abstract\n     * @param {number} ts - Epoch milliseconds for which to get the name\n     * @param {Object} opts - Options to affect the format\n     * @param {string} opts.format - What style of offset to return. Accepts 'long' or 'short'.\n     * @param {string} opts.locale - What locale to return the offset name in.\n     * @return {string}\n     */\n    _proto.offsetName = function offsetName(ts, opts) {\n      throw new ZoneIsAbstractError();\n    }\n\n    /**\n     * Returns the offset's value as a string\n     * @abstract\n     * @param {number} ts - Epoch milliseconds for which to get the offset\n     * @param {string} format - What style of offset to return.\n     *                          Accepts 'narrow', 'short', or 'techie'. Returning '+6', '+06:00', or '+0600' respectively\n     * @return {string}\n     */;\n    _proto.formatOffset = function formatOffset(ts, format) {\n      throw new ZoneIsAbstractError();\n    }\n\n    /**\n     * Return the offset in minutes for this zone at the specified timestamp.\n     * @abstract\n     * @param {number} ts - Epoch milliseconds for which to compute the offset\n     * @return {number}\n     */;\n    _proto.offset = function offset(ts) {\n      throw new ZoneIsAbstractError();\n    }\n\n    /**\n     * Return whether this Zone is equal to another zone\n     * @abstract\n     * @param {Zone} otherZone - the zone to compare\n     * @return {boolean}\n     */;\n    _proto.equals = function equals(otherZone) {\n      throw new ZoneIsAbstractError();\n    }\n\n    /**\n     * Return whether this Zone is valid.\n     * @abstract\n     * @type {boolean}\n     */;\n    _createClass(Zone, [{\n      key: \"type\",\n      get:\n      /**\n       * The type of zone\n       * @abstract\n       * @type {string}\n       */\n      function get() {\n        throw new ZoneIsAbstractError();\n      }\n\n      /**\n       * The name of this zone.\n       * @abstract\n       * @type {string}\n       */\n    }, {\n      key: \"name\",\n      get: function get() {\n        throw new ZoneIsAbstractError();\n      }\n\n      /**\n       * The IANA name of this zone.\n       * Defaults to `name` if not overwritten by a subclass.\n       * @abstract\n       * @type {string}\n       */\n    }, {\n      key: \"ianaName\",\n      get: function get() {\n        return this.name;\n      }\n\n      /**\n       * Returns whether the offset is known to be fixed for the whole year.\n       * @abstract\n       * @type {boolean}\n       */\n    }, {\n      key: \"isUniversal\",\n      get: function get() {\n        throw new ZoneIsAbstractError();\n      }\n    }, {\n      key: \"isValid\",\n      get: function get() {\n        throw new ZoneIsAbstractError();\n      }\n    }]);\n    return Zone;\n  }();\n\n  var singleton$1 = null;\n\n  /**\n   * Represents the local zone for this JavaScript environment.\n   * @implements {Zone}\n   */\n  var SystemZone = /*#__PURE__*/function (_Zone) {\n    _inheritsLoose(SystemZone, _Zone);\n    function SystemZone() {\n      return _Zone.apply(this, arguments) || this;\n    }\n    var _proto = SystemZone.prototype;\n    /** @override **/\n    _proto.offsetName = function offsetName(ts, _ref) {\n      var format = _ref.format,\n        locale = _ref.locale;\n      return parseZoneInfo(ts, format, locale);\n    }\n\n    /** @override **/;\n    _proto.formatOffset = function formatOffset$1(ts, format) {\n      return formatOffset(this.offset(ts), format);\n    }\n\n    /** @override **/;\n    _proto.offset = function offset(ts) {\n      return -new Date(ts).getTimezoneOffset();\n    }\n\n    /** @override **/;\n    _proto.equals = function equals(otherZone) {\n      return otherZone.type === \"system\";\n    }\n\n    /** @override **/;\n    _createClass(SystemZone, [{\n      key: \"type\",\n      get: /** @override **/\n      function get() {\n        return \"system\";\n      }\n\n      /** @override **/\n    }, {\n      key: \"name\",\n      get: function get() {\n        return new Intl.DateTimeFormat().resolvedOptions().timeZone;\n      }\n\n      /** @override **/\n    }, {\n      key: \"isUniversal\",\n      get: function get() {\n        return false;\n      }\n    }, {\n      key: \"isValid\",\n      get: function get() {\n        return true;\n      }\n    }], [{\n      key: \"instance\",\n      get:\n      /**\n       * Get a singleton instance of the local zone\n       * @return {SystemZone}\n       */\n      function get() {\n        if (singleton$1 === null) {\n          singleton$1 = new SystemZone();\n        }\n        return singleton$1;\n      }\n    }]);\n    return SystemZone;\n  }(Zone);\n\n  var dtfCache = {};\n  function makeDTF(zone) {\n    if (!dtfCache[zone]) {\n      dtfCache[zone] = new Intl.DateTimeFormat(\"en-US\", {\n        hour12: false,\n        timeZone: zone,\n        year: \"numeric\",\n        month: \"2-digit\",\n        day: \"2-digit\",\n        hour: \"2-digit\",\n        minute: \"2-digit\",\n        second: \"2-digit\",\n        era: \"short\"\n      });\n    }\n    return dtfCache[zone];\n  }\n  var typeToPos = {\n    year: 0,\n    month: 1,\n    day: 2,\n    era: 3,\n    hour: 4,\n    minute: 5,\n    second: 6\n  };\n  function hackyOffset(dtf, date) {\n    var formatted = dtf.format(date).replace(/\\u200E/g, \"\"),\n      parsed = /(\\d+)\\/(\\d+)\\/(\\d+) (AD|BC),? (\\d+):(\\d+):(\\d+)/.exec(formatted),\n      fMonth = parsed[1],\n      fDay = parsed[2],\n      fYear = parsed[3],\n      fadOrBc = parsed[4],\n      fHour = parsed[5],\n      fMinute = parsed[6],\n      fSecond = parsed[7];\n    return [fYear, fMonth, fDay, fadOrBc, fHour, fMinute, fSecond];\n  }\n  function partsOffset(dtf, date) {\n    var formatted = dtf.formatToParts(date);\n    var filled = [];\n    for (var i = 0; i < formatted.length; i++) {\n      var _formatted$i = formatted[i],\n        type = _formatted$i.type,\n        value = _formatted$i.value;\n      var pos = typeToPos[type];\n      if (type === \"era\") {\n        filled[pos] = value;\n      } else if (!isUndefined(pos)) {\n        filled[pos] = parseInt(value, 10);\n      }\n    }\n    return filled;\n  }\n  var ianaZoneCache = {};\n  /**\n   * A zone identified by an IANA identifier, like America/New_York\n   * @implements {Zone}\n   */\n  var IANAZone = /*#__PURE__*/function (_Zone) {\n    _inheritsLoose(IANAZone, _Zone);\n    /**\n     * @param {string} name - Zone name\n     * @return {IANAZone}\n     */\n    IANAZone.create = function create(name) {\n      if (!ianaZoneCache[name]) {\n        ianaZoneCache[name] = new IANAZone(name);\n      }\n      return ianaZoneCache[name];\n    }\n\n    /**\n     * Reset local caches. Should only be necessary in testing scenarios.\n     * @return {void}\n     */;\n    IANAZone.resetCache = function resetCache() {\n      ianaZoneCache = {};\n      dtfCache = {};\n    }\n\n    /**\n     * Returns whether the provided string is a valid specifier. This only checks the string's format, not that the specifier identifies a known zone; see isValidZone for that.\n     * @param {string} s - The string to check validity on\n     * @example IANAZone.isValidSpecifier(\"America/New_York\") //=> true\n     * @example IANAZone.isValidSpecifier(\"Sport~~blorp\") //=> false\n     * @deprecated For backward compatibility, this forwards to isValidZone, better use `isValidZone()` directly instead.\n     * @return {boolean}\n     */;\n    IANAZone.isValidSpecifier = function isValidSpecifier(s) {\n      return this.isValidZone(s);\n    }\n\n    /**\n     * Returns whether the provided string identifies a real zone\n     * @param {string} zone - The string to check\n     * @example IANAZone.isValidZone(\"America/New_York\") //=> true\n     * @example IANAZone.isValidZone(\"Fantasia/Castle\") //=> false\n     * @example IANAZone.isValidZone(\"Sport~~blorp\") //=> false\n     * @return {boolean}\n     */;\n    IANAZone.isValidZone = function isValidZone(zone) {\n      if (!zone) {\n        return false;\n      }\n      try {\n        new Intl.DateTimeFormat(\"en-US\", {\n          timeZone: zone\n        }).format();\n        return true;\n      } catch (e) {\n        return false;\n      }\n    };\n    function IANAZone(name) {\n      var _this;\n      _this = _Zone.call(this) || this;\n      /** @private **/\n      _this.zoneName = name;\n      /** @private **/\n      _this.valid = IANAZone.isValidZone(name);\n      return _this;\n    }\n\n    /**\n     * The type of zone. `iana` for all instances of `IANAZone`.\n     * @override\n     * @type {string}\n     */\n    var _proto = IANAZone.prototype;\n    /**\n     * Returns the offset's common name (such as EST) at the specified timestamp\n     * @override\n     * @param {number} ts - Epoch milliseconds for which to get the name\n     * @param {Object} opts - Options to affect the format\n     * @param {string} opts.format - What style of offset to return. Accepts 'long' or 'short'.\n     * @param {string} opts.locale - What locale to return the offset name in.\n     * @return {string}\n     */\n    _proto.offsetName = function offsetName(ts, _ref) {\n      var format = _ref.format,\n        locale = _ref.locale;\n      return parseZoneInfo(ts, format, locale, this.name);\n    }\n\n    /**\n     * Returns the offset's value as a string\n     * @override\n     * @param {number} ts - Epoch milliseconds for which to get the offset\n     * @param {string} format - What style of offset to return.\n     *                          Accepts 'narrow', 'short', or 'techie'. Returning '+6', '+06:00', or '+0600' respectively\n     * @return {string}\n     */;\n    _proto.formatOffset = function formatOffset$1(ts, format) {\n      return formatOffset(this.offset(ts), format);\n    }\n\n    /**\n     * Return the offset in minutes for this zone at the specified timestamp.\n     * @override\n     * @param {number} ts - Epoch milliseconds for which to compute the offset\n     * @return {number}\n     */;\n    _proto.offset = function offset(ts) {\n      var date = new Date(ts);\n      if (isNaN(date)) return NaN;\n      var dtf = makeDTF(this.name);\n      var _ref2 = dtf.formatToParts ? partsOffset(dtf, date) : hackyOffset(dtf, date),\n        year = _ref2[0],\n        month = _ref2[1],\n        day = _ref2[2],\n        adOrBc = _ref2[3],\n        hour = _ref2[4],\n        minute = _ref2[5],\n        second = _ref2[6];\n      if (adOrBc === \"BC\") {\n        year = -Math.abs(year) + 1;\n      }\n\n      // because we're using hour12 and https://bugs.chromium.org/p/chromium/issues/detail?id=1025564&can=2&q=%2224%3A00%22%20datetimeformat\n      var adjustedHour = hour === 24 ? 0 : hour;\n      var asUTC = objToLocalTS({\n        year: year,\n        month: month,\n        day: day,\n        hour: adjustedHour,\n        minute: minute,\n        second: second,\n        millisecond: 0\n      });\n      var asTS = +date;\n      var over = asTS % 1000;\n      asTS -= over >= 0 ? over : 1000 + over;\n      return (asUTC - asTS) / (60 * 1000);\n    }\n\n    /**\n     * Return whether this Zone is equal to another zone\n     * @override\n     * @param {Zone} otherZone - the zone to compare\n     * @return {boolean}\n     */;\n    _proto.equals = function equals(otherZone) {\n      return otherZone.type === \"iana\" && otherZone.name === this.name;\n    }\n\n    /**\n     * Return whether this Zone is valid.\n     * @override\n     * @type {boolean}\n     */;\n    _createClass(IANAZone, [{\n      key: \"type\",\n      get: function get() {\n        return \"iana\";\n      }\n\n      /**\n       * The name of this zone (i.e. the IANA zone name).\n       * @override\n       * @type {string}\n       */\n    }, {\n      key: \"name\",\n      get: function get() {\n        return this.zoneName;\n      }\n\n      /**\n       * Returns whether the offset is known to be fixed for the whole year:\n       * Always returns false for all IANA zones.\n       * @override\n       * @type {boolean}\n       */\n    }, {\n      key: \"isUniversal\",\n      get: function get() {\n        return false;\n      }\n    }, {\n      key: \"isValid\",\n      get: function get() {\n        return this.valid;\n      }\n    }]);\n    return IANAZone;\n  }(Zone);\n\n  var _excluded = [\"base\"],\n    _excluded2 = [\"padTo\", \"floor\"];\n\n  // todo - remap caching\n\n  var intlLFCache = {};\n  function getCachedLF(locString, opts) {\n    if (opts === void 0) {\n      opts = {};\n    }\n    var key = JSON.stringify([locString, opts]);\n    var dtf = intlLFCache[key];\n    if (!dtf) {\n      dtf = new Intl.ListFormat(locString, opts);\n      intlLFCache[key] = dtf;\n    }\n    return dtf;\n  }\n  var intlDTCache = {};\n  function getCachedDTF(locString, opts) {\n    if (opts === void 0) {\n      opts = {};\n    }\n    var key = JSON.stringify([locString, opts]);\n    var dtf = intlDTCache[key];\n    if (!dtf) {\n      dtf = new Intl.DateTimeFormat(locString, opts);\n      intlDTCache[key] = dtf;\n    }\n    return dtf;\n  }\n  var intlNumCache = {};\n  function getCachedINF(locString, opts) {\n    if (opts === void 0) {\n      opts = {};\n    }\n    var key = JSON.stringify([locString, opts]);\n    var inf = intlNumCache[key];\n    if (!inf) {\n      inf = new Intl.NumberFormat(locString, opts);\n      intlNumCache[key] = inf;\n    }\n    return inf;\n  }\n  var intlRelCache = {};\n  function getCachedRTF(locString, opts) {\n    if (opts === void 0) {\n      opts = {};\n    }\n    var _opts = opts;\n      _opts.base;\n      var cacheKeyOpts = _objectWithoutPropertiesLoose(_opts, _excluded); // exclude `base` from the options\n    var key = JSON.stringify([locString, cacheKeyOpts]);\n    var inf = intlRelCache[key];\n    if (!inf) {\n      inf = new Intl.RelativeTimeFormat(locString, opts);\n      intlRelCache[key] = inf;\n    }\n    return inf;\n  }\n  var sysLocaleCache = null;\n  function systemLocale() {\n    if (sysLocaleCache) {\n      return sysLocaleCache;\n    } else {\n      sysLocaleCache = new Intl.DateTimeFormat().resolvedOptions().locale;\n      return sysLocaleCache;\n    }\n  }\n  var weekInfoCache = {};\n  function getCachedWeekInfo(locString) {\n    var data = weekInfoCache[locString];\n    if (!data) {\n      var locale = new Intl.Locale(locString);\n      // browsers currently implement this as a property, but spec says it should be a getter function\n      data = \"getWeekInfo\" in locale ? locale.getWeekInfo() : locale.weekInfo;\n      weekInfoCache[locString] = data;\n    }\n    return data;\n  }\n  function parseLocaleString(localeStr) {\n    // I really want to avoid writing a BCP 47 parser\n    // see, e.g. https://github.com/wooorm/bcp-47\n    // Instead, we'll do this:\n\n    // a) if the string has no -u extensions, just leave it alone\n    // b) if it does, use Intl to resolve everything\n    // c) if Intl fails, try again without the -u\n\n    // private subtags and unicode subtags have ordering requirements,\n    // and we're not properly parsing this, so just strip out the\n    // private ones if they exist.\n    var xIndex = localeStr.indexOf(\"-x-\");\n    if (xIndex !== -1) {\n      localeStr = localeStr.substring(0, xIndex);\n    }\n    var uIndex = localeStr.indexOf(\"-u-\");\n    if (uIndex === -1) {\n      return [localeStr];\n    } else {\n      var options;\n      var selectedStr;\n      try {\n        options = getCachedDTF(localeStr).resolvedOptions();\n        selectedStr = localeStr;\n      } catch (e) {\n        var smaller = localeStr.substring(0, uIndex);\n        options = getCachedDTF(smaller).resolvedOptions();\n        selectedStr = smaller;\n      }\n      var _options = options,\n        numberingSystem = _options.numberingSystem,\n        calendar = _options.calendar;\n      return [selectedStr, numberingSystem, calendar];\n    }\n  }\n  function intlConfigString(localeStr, numberingSystem, outputCalendar) {\n    if (outputCalendar || numberingSystem) {\n      if (!localeStr.includes(\"-u-\")) {\n        localeStr += \"-u\";\n      }\n      if (outputCalendar) {\n        localeStr += \"-ca-\" + outputCalendar;\n      }\n      if (numberingSystem) {\n        localeStr += \"-nu-\" + numberingSystem;\n      }\n      return localeStr;\n    } else {\n      return localeStr;\n    }\n  }\n  function mapMonths(f) {\n    var ms = [];\n    for (var i = 1; i <= 12; i++) {\n      var dt = DateTime.utc(2009, i, 1);\n      ms.push(f(dt));\n    }\n    return ms;\n  }\n  function mapWeekdays(f) {\n    var ms = [];\n    for (var i = 1; i <= 7; i++) {\n      var dt = DateTime.utc(2016, 11, 13 + i);\n      ms.push(f(dt));\n    }\n    return ms;\n  }\n  function listStuff(loc, length, englishFn, intlFn) {\n    var mode = loc.listingMode();\n    if (mode === \"error\") {\n      return null;\n    } else if (mode === \"en\") {\n      return englishFn(length);\n    } else {\n      return intlFn(length);\n    }\n  }\n  function supportsFastNumbers(loc) {\n    if (loc.numberingSystem && loc.numberingSystem !== \"latn\") {\n      return false;\n    } else {\n      return loc.numberingSystem === \"latn\" || !loc.locale || loc.locale.startsWith(\"en\") || new Intl.DateTimeFormat(loc.intl).resolvedOptions().numberingSystem === \"latn\";\n    }\n  }\n\n  /**\n   * @private\n   */\n  var PolyNumberFormatter = /*#__PURE__*/function () {\n    function PolyNumberFormatter(intl, forceSimple, opts) {\n      this.padTo = opts.padTo || 0;\n      this.floor = opts.floor || false;\n      opts.padTo;\n        opts.floor;\n        var otherOpts = _objectWithoutPropertiesLoose(opts, _excluded2);\n      if (!forceSimple || Object.keys(otherOpts).length > 0) {\n        var intlOpts = _extends({\n          useGrouping: false\n        }, opts);\n        if (opts.padTo > 0) intlOpts.minimumIntegerDigits = opts.padTo;\n        this.inf = getCachedINF(intl, intlOpts);\n      }\n    }\n    var _proto = PolyNumberFormatter.prototype;\n    _proto.format = function format(i) {\n      if (this.inf) {\n        var fixed = this.floor ? Math.floor(i) : i;\n        return this.inf.format(fixed);\n      } else {\n        // to match the browser's numberformatter defaults\n        var _fixed = this.floor ? Math.floor(i) : roundTo(i, 3);\n        return padStart(_fixed, this.padTo);\n      }\n    };\n    return PolyNumberFormatter;\n  }();\n  /**\n   * @private\n   */\n  var PolyDateFormatter = /*#__PURE__*/function () {\n    function PolyDateFormatter(dt, intl, opts) {\n      this.opts = opts;\n      this.originalZone = undefined;\n      var z = undefined;\n      if (this.opts.timeZone) {\n        // Don't apply any workarounds if a timeZone is explicitly provided in opts\n        this.dt = dt;\n      } else if (dt.zone.type === \"fixed\") {\n        // UTC-8 or Etc/UTC-8 are not part of tzdata, only Etc/GMT+8 and the like.\n        // That is why fixed-offset TZ is set to that unless it is:\n        // 1. Representing offset 0 when UTC is used to maintain previous behavior and does not become GMT.\n        // 2. Unsupported by the browser:\n        //    - some do not support Etc/\n        //    - < Etc/GMT-14, > Etc/GMT+12, and 30-minute or 45-minute offsets are not part of tzdata\n        var gmtOffset = -1 * (dt.offset / 60);\n        var offsetZ = gmtOffset >= 0 ? \"Etc/GMT+\" + gmtOffset : \"Etc/GMT\" + gmtOffset;\n        if (dt.offset !== 0 && IANAZone.create(offsetZ).valid) {\n          z = offsetZ;\n          this.dt = dt;\n        } else {\n          // Not all fixed-offset zones like Etc/+4:30 are present in tzdata so\n          // we manually apply the offset and substitute the zone as needed.\n          z = \"UTC\";\n          this.dt = dt.offset === 0 ? dt : dt.setZone(\"UTC\").plus({\n            minutes: dt.offset\n          });\n          this.originalZone = dt.zone;\n        }\n      } else if (dt.zone.type === \"system\") {\n        this.dt = dt;\n      } else if (dt.zone.type === \"iana\") {\n        this.dt = dt;\n        z = dt.zone.name;\n      } else {\n        // Custom zones can have any offset / offsetName so we just manually\n        // apply the offset and substitute the zone as needed.\n        z = \"UTC\";\n        this.dt = dt.setZone(\"UTC\").plus({\n          minutes: dt.offset\n        });\n        this.originalZone = dt.zone;\n      }\n      var intlOpts = _extends({}, this.opts);\n      intlOpts.timeZone = intlOpts.timeZone || z;\n      this.dtf = getCachedDTF(intl, intlOpts);\n    }\n    var _proto2 = PolyDateFormatter.prototype;\n    _proto2.format = function format() {\n      if (this.originalZone) {\n        // If we have to substitute in the actual zone name, we have to use\n        // formatToParts so that the timezone can be replaced.\n        return this.formatToParts().map(function (_ref) {\n          var value = _ref.value;\n          return value;\n        }).join(\"\");\n      }\n      return this.dtf.format(this.dt.toJSDate());\n    };\n    _proto2.formatToParts = function formatToParts() {\n      var _this = this;\n      var parts = this.dtf.formatToParts(this.dt.toJSDate());\n      if (this.originalZone) {\n        return parts.map(function (part) {\n          if (part.type === \"timeZoneName\") {\n            var offsetName = _this.originalZone.offsetName(_this.dt.ts, {\n              locale: _this.dt.locale,\n              format: _this.opts.timeZoneName\n            });\n            return _extends({}, part, {\n              value: offsetName\n            });\n          } else {\n            return part;\n          }\n        });\n      }\n      return parts;\n    };\n    _proto2.resolvedOptions = function resolvedOptions() {\n      return this.dtf.resolvedOptions();\n    };\n    return PolyDateFormatter;\n  }();\n  /**\n   * @private\n   */\n  var PolyRelFormatter = /*#__PURE__*/function () {\n    function PolyRelFormatter(intl, isEnglish, opts) {\n      this.opts = _extends({\n        style: \"long\"\n      }, opts);\n      if (!isEnglish && hasRelative()) {\n        this.rtf = getCachedRTF(intl, opts);\n      }\n    }\n    var _proto3 = PolyRelFormatter.prototype;\n    _proto3.format = function format(count, unit) {\n      if (this.rtf) {\n        return this.rtf.format(count, unit);\n      } else {\n        return formatRelativeTime(unit, count, this.opts.numeric, this.opts.style !== \"long\");\n      }\n    };\n    _proto3.formatToParts = function formatToParts(count, unit) {\n      if (this.rtf) {\n        return this.rtf.formatToParts(count, unit);\n      } else {\n        return [];\n      }\n    };\n    return PolyRelFormatter;\n  }();\n  var fallbackWeekSettings = {\n    firstDay: 1,\n    minimalDays: 4,\n    weekend: [6, 7]\n  };\n\n  /**\n   * @private\n   */\n  var Locale = /*#__PURE__*/function () {\n    Locale.fromOpts = function fromOpts(opts) {\n      return Locale.create(opts.locale, opts.numberingSystem, opts.outputCalendar, opts.weekSettings, opts.defaultToEN);\n    };\n    Locale.create = function create(locale, numberingSystem, outputCalendar, weekSettings, defaultToEN) {\n      if (defaultToEN === void 0) {\n        defaultToEN = false;\n      }\n      var specifiedLocale = locale || Settings.defaultLocale;\n      // the system locale is useful for human-readable strings but annoying for parsing/formatting known formats\n      var localeR = specifiedLocale || (defaultToEN ? \"en-US\" : systemLocale());\n      var numberingSystemR = numberingSystem || Settings.defaultNumberingSystem;\n      var outputCalendarR = outputCalendar || Settings.defaultOutputCalendar;\n      var weekSettingsR = validateWeekSettings(weekSettings) || Settings.defaultWeekSettings;\n      return new Locale(localeR, numberingSystemR, outputCalendarR, weekSettingsR, specifiedLocale);\n    };\n    Locale.resetCache = function resetCache() {\n      sysLocaleCache = null;\n      intlDTCache = {};\n      intlNumCache = {};\n      intlRelCache = {};\n    };\n    Locale.fromObject = function fromObject(_temp) {\n      var _ref2 = _temp === void 0 ? {} : _temp,\n        locale = _ref2.locale,\n        numberingSystem = _ref2.numberingSystem,\n        outputCalendar = _ref2.outputCalendar,\n        weekSettings = _ref2.weekSettings;\n      return Locale.create(locale, numberingSystem, outputCalendar, weekSettings);\n    };\n    function Locale(locale, numbering, outputCalendar, weekSettings, specifiedLocale) {\n      var _parseLocaleString = parseLocaleString(locale),\n        parsedLocale = _parseLocaleString[0],\n        parsedNumberingSystem = _parseLocaleString[1],\n        parsedOutputCalendar = _parseLocaleString[2];\n      this.locale = parsedLocale;\n      this.numberingSystem = numbering || parsedNumberingSystem || null;\n      this.outputCalendar = outputCalendar || parsedOutputCalendar || null;\n      this.weekSettings = weekSettings;\n      this.intl = intlConfigString(this.locale, this.numberingSystem, this.outputCalendar);\n      this.weekdaysCache = {\n        format: {},\n        standalone: {}\n      };\n      this.monthsCache = {\n        format: {},\n        standalone: {}\n      };\n      this.meridiemCache = null;\n      this.eraCache = {};\n      this.specifiedLocale = specifiedLocale;\n      this.fastNumbersCached = null;\n    }\n    var _proto4 = Locale.prototype;\n    _proto4.listingMode = function listingMode() {\n      var isActuallyEn = this.isEnglish();\n      var hasNoWeirdness = (this.numberingSystem === null || this.numberingSystem === \"latn\") && (this.outputCalendar === null || this.outputCalendar === \"gregory\");\n      return isActuallyEn && hasNoWeirdness ? \"en\" : \"intl\";\n    };\n    _proto4.clone = function clone(alts) {\n      if (!alts || Object.getOwnPropertyNames(alts).length === 0) {\n        return this;\n      } else {\n        return Locale.create(alts.locale || this.specifiedLocale, alts.numberingSystem || this.numberingSystem, alts.outputCalendar || this.outputCalendar, validateWeekSettings(alts.weekSettings) || this.weekSettings, alts.defaultToEN || false);\n      }\n    };\n    _proto4.redefaultToEN = function redefaultToEN(alts) {\n      if (alts === void 0) {\n        alts = {};\n      }\n      return this.clone(_extends({}, alts, {\n        defaultToEN: true\n      }));\n    };\n    _proto4.redefaultToSystem = function redefaultToSystem(alts) {\n      if (alts === void 0) {\n        alts = {};\n      }\n      return this.clone(_extends({}, alts, {\n        defaultToEN: false\n      }));\n    };\n    _proto4.months = function months$1(length, format) {\n      var _this2 = this;\n      if (format === void 0) {\n        format = false;\n      }\n      return listStuff(this, length, months, function () {\n        var intl = format ? {\n            month: length,\n            day: \"numeric\"\n          } : {\n            month: length\n          },\n          formatStr = format ? \"format\" : \"standalone\";\n        if (!_this2.monthsCache[formatStr][length]) {\n          _this2.monthsCache[formatStr][length] = mapMonths(function (dt) {\n            return _this2.extract(dt, intl, \"month\");\n          });\n        }\n        return _this2.monthsCache[formatStr][length];\n      });\n    };\n    _proto4.weekdays = function weekdays$1(length, format) {\n      var _this3 = this;\n      if (format === void 0) {\n        format = false;\n      }\n      return listStuff(this, length, weekdays, function () {\n        var intl = format ? {\n            weekday: length,\n            year: \"numeric\",\n            month: \"long\",\n            day: \"numeric\"\n          } : {\n            weekday: length\n          },\n          formatStr = format ? \"format\" : \"standalone\";\n        if (!_this3.weekdaysCache[formatStr][length]) {\n          _this3.weekdaysCache[formatStr][length] = mapWeekdays(function (dt) {\n            return _this3.extract(dt, intl, \"weekday\");\n          });\n        }\n        return _this3.weekdaysCache[formatStr][length];\n      });\n    };\n    _proto4.meridiems = function meridiems$1() {\n      var _this4 = this;\n      return listStuff(this, undefined, function () {\n        return meridiems;\n      }, function () {\n        // In theory there could be aribitrary day periods. We're gonna assume there are exactly two\n        // for AM and PM. This is probably wrong, but it's makes parsing way easier.\n        if (!_this4.meridiemCache) {\n          var intl = {\n            hour: \"numeric\",\n            hourCycle: \"h12\"\n          };\n          _this4.meridiemCache = [DateTime.utc(2016, 11, 13, 9), DateTime.utc(2016, 11, 13, 19)].map(function (dt) {\n            return _this4.extract(dt, intl, \"dayperiod\");\n          });\n        }\n        return _this4.meridiemCache;\n      });\n    };\n    _proto4.eras = function eras$1(length) {\n      var _this5 = this;\n      return listStuff(this, length, eras, function () {\n        var intl = {\n          era: length\n        };\n\n        // This is problematic. Different calendars are going to define eras totally differently. What I need is the minimum set of dates\n        // to definitely enumerate them.\n        if (!_this5.eraCache[length]) {\n          _this5.eraCache[length] = [DateTime.utc(-40, 1, 1), DateTime.utc(2017, 1, 1)].map(function (dt) {\n            return _this5.extract(dt, intl, \"era\");\n          });\n        }\n        return _this5.eraCache[length];\n      });\n    };\n    _proto4.extract = function extract(dt, intlOpts, field) {\n      var df = this.dtFormatter(dt, intlOpts),\n        results = df.formatToParts(),\n        matching = results.find(function (m) {\n          return m.type.toLowerCase() === field;\n        });\n      return matching ? matching.value : null;\n    };\n    _proto4.numberFormatter = function numberFormatter(opts) {\n      if (opts === void 0) {\n        opts = {};\n      }\n      // this forcesimple option is never used (the only caller short-circuits on it, but it seems safer to leave)\n      // (in contrast, the rest of the condition is used heavily)\n      return new PolyNumberFormatter(this.intl, opts.forceSimple || this.fastNumbers, opts);\n    };\n    _proto4.dtFormatter = function dtFormatter(dt, intlOpts) {\n      if (intlOpts === void 0) {\n        intlOpts = {};\n      }\n      return new PolyDateFormatter(dt, this.intl, intlOpts);\n    };\n    _proto4.relFormatter = function relFormatter(opts) {\n      if (opts === void 0) {\n        opts = {};\n      }\n      return new PolyRelFormatter(this.intl, this.isEnglish(), opts);\n    };\n    _proto4.listFormatter = function listFormatter(opts) {\n      if (opts === void 0) {\n        opts = {};\n      }\n      return getCachedLF(this.intl, opts);\n    };\n    _proto4.isEnglish = function isEnglish() {\n      return this.locale === \"en\" || this.locale.toLowerCase() === \"en-us\" || new Intl.DateTimeFormat(this.intl).resolvedOptions().locale.startsWith(\"en-us\");\n    };\n    _proto4.getWeekSettings = function getWeekSettings() {\n      if (this.weekSettings) {\n        return this.weekSettings;\n      } else if (!hasLocaleWeekInfo()) {\n        return fallbackWeekSettings;\n      } else {\n        return getCachedWeekInfo(this.locale);\n      }\n    };\n    _proto4.getStartOfWeek = function getStartOfWeek() {\n      return this.getWeekSettings().firstDay;\n    };\n    _proto4.getMinDaysInFirstWeek = function getMinDaysInFirstWeek() {\n      return this.getWeekSettings().minimalDays;\n    };\n    _proto4.getWeekendDays = function getWeekendDays() {\n      return this.getWeekSettings().weekend;\n    };\n    _proto4.equals = function equals(other) {\n      return this.locale === other.locale && this.numberingSystem === other.numberingSystem && this.outputCalendar === other.outputCalendar;\n    };\n    _proto4.toString = function toString() {\n      return \"Locale(\" + this.locale + \", \" + this.numberingSystem + \", \" + this.outputCalendar + \")\";\n    };\n    _createClass(Locale, [{\n      key: \"fastNumbers\",\n      get: function get() {\n        if (this.fastNumbersCached == null) {\n          this.fastNumbersCached = supportsFastNumbers(this);\n        }\n        return this.fastNumbersCached;\n      }\n    }]);\n    return Locale;\n  }();\n\n  var singleton = null;\n\n  /**\n   * A zone with a fixed offset (meaning no DST)\n   * @implements {Zone}\n   */\n  var FixedOffsetZone = /*#__PURE__*/function (_Zone) {\n    _inheritsLoose(FixedOffsetZone, _Zone);\n    /**\n     * Get an instance with a specified offset\n     * @param {number} offset - The offset in minutes\n     * @return {FixedOffsetZone}\n     */\n    FixedOffsetZone.instance = function instance(offset) {\n      return offset === 0 ? FixedOffsetZone.utcInstance : new FixedOffsetZone(offset);\n    }\n\n    /**\n     * Get an instance of FixedOffsetZone from a UTC offset string, like \"UTC+6\"\n     * @param {string} s - The offset string to parse\n     * @example FixedOffsetZone.parseSpecifier(\"UTC+6\")\n     * @example FixedOffsetZone.parseSpecifier(\"UTC+06\")\n     * @example FixedOffsetZone.parseSpecifier(\"UTC-6:00\")\n     * @return {FixedOffsetZone}\n     */;\n    FixedOffsetZone.parseSpecifier = function parseSpecifier(s) {\n      if (s) {\n        var r = s.match(/^utc(?:([+-]\\d{1,2})(?::(\\d{2}))?)?$/i);\n        if (r) {\n          return new FixedOffsetZone(signedOffset(r[1], r[2]));\n        }\n      }\n      return null;\n    };\n    function FixedOffsetZone(offset) {\n      var _this;\n      _this = _Zone.call(this) || this;\n      /** @private **/\n      _this.fixed = offset;\n      return _this;\n    }\n\n    /**\n     * The type of zone. `fixed` for all instances of `FixedOffsetZone`.\n     * @override\n     * @type {string}\n     */\n    var _proto = FixedOffsetZone.prototype;\n    /**\n     * Returns the offset's common name at the specified timestamp.\n     *\n     * For fixed offset zones this equals to the zone name.\n     * @override\n     */\n    _proto.offsetName = function offsetName() {\n      return this.name;\n    }\n\n    /**\n     * Returns the offset's value as a string\n     * @override\n     * @param {number} ts - Epoch milliseconds for which to get the offset\n     * @param {string} format - What style of offset to return.\n     *                          Accepts 'narrow', 'short', or 'techie'. Returning '+6', '+06:00', or '+0600' respectively\n     * @return {string}\n     */;\n    _proto.formatOffset = function formatOffset$1(ts, format) {\n      return formatOffset(this.fixed, format);\n    }\n\n    /**\n     * Returns whether the offset is known to be fixed for the whole year:\n     * Always returns true for all fixed offset zones.\n     * @override\n     * @type {boolean}\n     */;\n    /**\n     * Return the offset in minutes for this zone at the specified timestamp.\n     *\n     * For fixed offset zones, this is constant and does not depend on a timestamp.\n     * @override\n     * @return {number}\n     */\n    _proto.offset = function offset() {\n      return this.fixed;\n    }\n\n    /**\n     * Return whether this Zone is equal to another zone (i.e. also fixed and same offset)\n     * @override\n     * @param {Zone} otherZone - the zone to compare\n     * @return {boolean}\n     */;\n    _proto.equals = function equals(otherZone) {\n      return otherZone.type === \"fixed\" && otherZone.fixed === this.fixed;\n    }\n\n    /**\n     * Return whether this Zone is valid:\n     * All fixed offset zones are valid.\n     * @override\n     * @type {boolean}\n     */;\n    _createClass(FixedOffsetZone, [{\n      key: \"type\",\n      get: function get() {\n        return \"fixed\";\n      }\n\n      /**\n       * The name of this zone.\n       * All fixed zones' names always start with \"UTC\" (plus optional offset)\n       * @override\n       * @type {string}\n       */\n    }, {\n      key: \"name\",\n      get: function get() {\n        return this.fixed === 0 ? \"UTC\" : \"UTC\" + formatOffset(this.fixed, \"narrow\");\n      }\n\n      /**\n       * The IANA name of this zone, i.e. `Etc/UTC` or `Etc/GMT+/-nn`\n       *\n       * @override\n       * @type {string}\n       */\n    }, {\n      key: \"ianaName\",\n      get: function get() {\n        if (this.fixed === 0) {\n          return \"Etc/UTC\";\n        } else {\n          return \"Etc/GMT\" + formatOffset(-this.fixed, \"narrow\");\n        }\n      }\n    }, {\n      key: \"isUniversal\",\n      get: function get() {\n        return true;\n      }\n    }, {\n      key: \"isValid\",\n      get: function get() {\n        return true;\n      }\n    }], [{\n      key: \"utcInstance\",\n      get:\n      /**\n       * Get a singleton instance of UTC\n       * @return {FixedOffsetZone}\n       */\n      function get() {\n        if (singleton === null) {\n          singleton = new FixedOffsetZone(0);\n        }\n        return singleton;\n      }\n    }]);\n    return FixedOffsetZone;\n  }(Zone);\n\n  /**\n   * A zone that failed to parse. You should never need to instantiate this.\n   * @implements {Zone}\n   */\n  var InvalidZone = /*#__PURE__*/function (_Zone) {\n    _inheritsLoose(InvalidZone, _Zone);\n    function InvalidZone(zoneName) {\n      var _this;\n      _this = _Zone.call(this) || this;\n      /**  @private */\n      _this.zoneName = zoneName;\n      return _this;\n    }\n\n    /** @override **/\n    var _proto = InvalidZone.prototype;\n    /** @override **/\n    _proto.offsetName = function offsetName() {\n      return null;\n    }\n\n    /** @override **/;\n    _proto.formatOffset = function formatOffset() {\n      return \"\";\n    }\n\n    /** @override **/;\n    _proto.offset = function offset() {\n      return NaN;\n    }\n\n    /** @override **/;\n    _proto.equals = function equals() {\n      return false;\n    }\n\n    /** @override **/;\n    _createClass(InvalidZone, [{\n      key: \"type\",\n      get: function get() {\n        return \"invalid\";\n      }\n\n      /** @override **/\n    }, {\n      key: \"name\",\n      get: function get() {\n        return this.zoneName;\n      }\n\n      /** @override **/\n    }, {\n      key: \"isUniversal\",\n      get: function get() {\n        return false;\n      }\n    }, {\n      key: \"isValid\",\n      get: function get() {\n        return false;\n      }\n    }]);\n    return InvalidZone;\n  }(Zone);\n\n  /**\n   * @private\n   */\n  function normalizeZone(input, defaultZone) {\n    if (isUndefined(input) || input === null) {\n      return defaultZone;\n    } else if (input instanceof Zone) {\n      return input;\n    } else if (isString(input)) {\n      var lowered = input.toLowerCase();\n      if (lowered === \"default\") return defaultZone;else if (lowered === \"local\" || lowered === \"system\") return SystemZone.instance;else if (lowered === \"utc\" || lowered === \"gmt\") return FixedOffsetZone.utcInstance;else return FixedOffsetZone.parseSpecifier(lowered) || IANAZone.create(input);\n    } else if (isNumber(input)) {\n      return FixedOffsetZone.instance(input);\n    } else if (typeof input === \"object\" && \"offset\" in input && typeof input.offset === \"function\") {\n      // This is dumb, but the instanceof check above doesn't seem to really work\n      // so we're duck checking it\n      return input;\n    } else {\n      return new InvalidZone(input);\n    }\n  }\n\n  var numberingSystems = {\n    arab: \"[\\u0660-\\u0669]\",\n    arabext: \"[\\u06F0-\\u06F9]\",\n    bali: \"[\\u1B50-\\u1B59]\",\n    beng: \"[\\u09E6-\\u09EF]\",\n    deva: \"[\\u0966-\\u096F]\",\n    fullwide: \"[\\uFF10-\\uFF19]\",\n    gujr: \"[\\u0AE6-\\u0AEF]\",\n    hanidec: \"[\u3007|\u4e00|\u4e8c|\u4e09|\u56db|\u4e94|\u516d|\u4e03|\u516b|\u4e5d]\",\n    khmr: \"[\\u17E0-\\u17E9]\",\n    knda: \"[\\u0CE6-\\u0CEF]\",\n    laoo: \"[\\u0ED0-\\u0ED9]\",\n    limb: \"[\\u1946-\\u194F]\",\n    mlym: \"[\\u0D66-\\u0D6F]\",\n    mong: \"[\\u1810-\\u1819]\",\n    mymr: \"[\\u1040-\\u1049]\",\n    orya: \"[\\u0B66-\\u0B6F]\",\n    tamldec: \"[\\u0BE6-\\u0BEF]\",\n    telu: \"[\\u0C66-\\u0C6F]\",\n    thai: \"[\\u0E50-\\u0E59]\",\n    tibt: \"[\\u0F20-\\u0F29]\",\n    latn: \"\\\\d\"\n  };\n  var numberingSystemsUTF16 = {\n    arab: [1632, 1641],\n    arabext: [1776, 1785],\n    bali: [6992, 7001],\n    beng: [2534, 2543],\n    deva: [2406, 2415],\n    fullwide: [65296, 65303],\n    gujr: [2790, 2799],\n    khmr: [6112, 6121],\n    knda: [3302, 3311],\n    laoo: [3792, 3801],\n    limb: [6470, 6479],\n    mlym: [3430, 3439],\n    mong: [6160, 6169],\n    mymr: [4160, 4169],\n    orya: [2918, 2927],\n    tamldec: [3046, 3055],\n    telu: [3174, 3183],\n    thai: [3664, 3673],\n    tibt: [3872, 3881]\n  };\n  var hanidecChars = numberingSystems.hanidec.replace(/[\\[|\\]]/g, \"\").split(\"\");\n  function parseDigits(str) {\n    var value = parseInt(str, 10);\n    if (isNaN(value)) {\n      value = \"\";\n      for (var i = 0; i < str.length; i++) {\n        var code = str.charCodeAt(i);\n        if (str[i].search(numberingSystems.hanidec) !== -1) {\n          value += hanidecChars.indexOf(str[i]);\n        } else {\n          for (var key in numberingSystemsUTF16) {\n            var _numberingSystemsUTF = numberingSystemsUTF16[key],\n              min = _numberingSystemsUTF[0],\n              max = _numberingSystemsUTF[1];\n            if (code >= min && code <= max) {\n              value += code - min;\n            }\n          }\n        }\n      }\n      return parseInt(value, 10);\n    } else {\n      return value;\n    }\n  }\n\n  // cache of {numberingSystem: {append: regex}}\n  var digitRegexCache = {};\n  function resetDigitRegexCache() {\n    digitRegexCache = {};\n  }\n  function digitRegex(_ref, append) {\n    var numberingSystem = _ref.numberingSystem;\n    if (append === void 0) {\n      append = \"\";\n    }\n    var ns = numberingSystem || \"latn\";\n    if (!digitRegexCache[ns]) {\n      digitRegexCache[ns] = {};\n    }\n    if (!digitRegexCache[ns][append]) {\n      digitRegexCache[ns][append] = new RegExp(\"\" + numberingSystems[ns] + append);\n    }\n    return digitRegexCache[ns][append];\n  }\n\n  var now = function now() {\n      return Date.now();\n    },\n    defaultZone = \"system\",\n    defaultLocale = null,\n    defaultNumberingSystem = null,\n    defaultOutputCalendar = null,\n    twoDigitCutoffYear = 60,\n    throwOnInvalid,\n    defaultWeekSettings = null;\n\n  /**\n   * Settings contains static getters and setters that control Luxon's overall behavior. Luxon is a simple library with few options, but the ones it does have live here.\n   */\n  var Settings = /*#__PURE__*/function () {\n    function Settings() {}\n    /**\n     * Reset Luxon's global caches. Should only be necessary in testing scenarios.\n     * @return {void}\n     */\n    Settings.resetCaches = function resetCaches() {\n      Locale.resetCache();\n      IANAZone.resetCache();\n      DateTime.resetCache();\n      resetDigitRegexCache();\n    };\n    _createClass(Settings, null, [{\n      key: \"now\",\n      get:\n      /**\n       * Get the callback for returning the current timestamp.\n       * @type {function}\n       */\n      function get() {\n        return now;\n      }\n\n      /**\n       * Set the callback for returning the current timestamp.\n       * The function should return a number, which will be interpreted as an Epoch millisecond count\n       * @type {function}\n       * @example Settings.now = () => Date.now() + 3000 // pretend it is 3 seconds in the future\n       * @example Settings.now = () => 0 // always pretend it's Jan 1, 1970 at midnight in UTC time\n       */,\n      set: function set(n) {\n        now = n;\n      }\n\n      /**\n       * Set the default time zone to create DateTimes in. Does not affect existing instances.\n       * Use the value \"system\" to reset this value to the system's time zone.\n       * @type {string}\n       */\n    }, {\n      key: \"defaultZone\",\n      get:\n      /**\n       * Get the default time zone object currently used to create DateTimes. Does not affect existing instances.\n       * The default value is the system's time zone (the one set on the machine that runs this code).\n       * @type {Zone}\n       */\n      function get() {\n        return normalizeZone(defaultZone, SystemZone.instance);\n      }\n\n      /**\n       * Get the default locale to create DateTimes with. Does not affect existing instances.\n       * @type {string}\n       */,\n      set: function set(zone) {\n        defaultZone = zone;\n      }\n    }, {\n      key: \"defaultLocale\",\n      get: function get() {\n        return defaultLocale;\n      }\n\n      /**\n       * Set the default locale to create DateTimes with. Does not affect existing instances.\n       * @type {string}\n       */,\n      set: function set(locale) {\n        defaultLocale = locale;\n      }\n\n      /**\n       * Get the default numbering system to create DateTimes with. Does not affect existing instances.\n       * @type {string}\n       */\n    }, {\n      key: \"defaultNumberingSystem\",\n      get: function get() {\n        return defaultNumberingSystem;\n      }\n\n      /**\n       * Set the default numbering system to create DateTimes with. Does not affect existing instances.\n       * @type {string}\n       */,\n      set: function set(numberingSystem) {\n        defaultNumberingSystem = numberingSystem;\n      }\n\n      /**\n       * Get the default output calendar to create DateTimes with. Does not affect existing instances.\n       * @type {string}\n       */\n    }, {\n      key: \"defaultOutputCalendar\",\n      get: function get() {\n        return defaultOutputCalendar;\n      }\n\n      /**\n       * Set the default output calendar to create DateTimes with. Does not affect existing instances.\n       * @type {string}\n       */,\n      set: function set(outputCalendar) {\n        defaultOutputCalendar = outputCalendar;\n      }\n\n      /**\n       * @typedef {Object} WeekSettings\n       * @property {number} firstDay\n       * @property {number} minimalDays\n       * @property {number[]} weekend\n       */\n\n      /**\n       * @return {WeekSettings|null}\n       */\n    }, {\n      key: \"defaultWeekSettings\",\n      get: function get() {\n        return defaultWeekSettings;\n      }\n\n      /**\n       * Allows overriding the default locale week settings, i.e. the start of the week, the weekend and\n       * how many days are required in the first week of a year.\n       * Does not affect existing instances.\n       *\n       * @param {WeekSettings|null} weekSettings\n       */,\n      set: function set(weekSettings) {\n        defaultWeekSettings = validateWeekSettings(weekSettings);\n      }\n\n      /**\n       * Get the cutoff year for whether a 2-digit year string is interpreted in the current or previous century. Numbers higher than the cutoff will be considered to mean 19xx and numbers lower or equal to the cutoff will be considered 20xx.\n       * @type {number}\n       */\n    }, {\n      key: \"twoDigitCutoffYear\",\n      get: function get() {\n        return twoDigitCutoffYear;\n      }\n\n      /**\n       * Set the cutoff year for whether a 2-digit year string is interpreted in the current or previous century. Numbers higher than the cutoff will be considered to mean 19xx and numbers lower or equal to the cutoff will be considered 20xx.\n       * @type {number}\n       * @example Settings.twoDigitCutoffYear = 0 // all 'yy' are interpreted as 20th century\n       * @example Settings.twoDigitCutoffYear = 99 // all 'yy' are interpreted as 21st century\n       * @example Settings.twoDigitCutoffYear = 50 // '49' -> 2049; '50' -> 1950\n       * @example Settings.twoDigitCutoffYear = 1950 // interpreted as 50\n       * @example Settings.twoDigitCutoffYear = 2050 // ALSO interpreted as 50\n       */,\n      set: function set(cutoffYear) {\n        twoDigitCutoffYear = cutoffYear % 100;\n      }\n\n      /**\n       * Get whether Luxon will throw when it encounters invalid DateTimes, Durations, or Intervals\n       * @type {boolean}\n       */\n    }, {\n      key: \"throwOnInvalid\",\n      get: function get() {\n        return throwOnInvalid;\n      }\n\n      /**\n       * Set whether Luxon will throw when it encounters invalid DateTimes, Durations, or Intervals\n       * @type {boolean}\n       */,\n      set: function set(t) {\n        throwOnInvalid = t;\n      }\n    }]);\n    return Settings;\n  }();\n\n  var Invalid = /*#__PURE__*/function () {\n    function Invalid(reason, explanation) {\n      this.reason = reason;\n      this.explanation = explanation;\n    }\n    var _proto = Invalid.prototype;\n    _proto.toMessage = function toMessage() {\n      if (this.explanation) {\n        return this.reason + \": \" + this.explanation;\n      } else {\n        return this.reason;\n      }\n    };\n    return Invalid;\n  }();\n\n  var nonLeapLadder = [0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334],\n    leapLadder = [0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335];\n  function unitOutOfRange(unit, value) {\n    return new Invalid(\"unit out of range\", \"you specified \" + value + \" (of type \" + typeof value + \") as a \" + unit + \", which is invalid\");\n  }\n  function dayOfWeek(year, month, day) {\n    var d = new Date(Date.UTC(year, month - 1, day));\n    if (year < 100 && year >= 0) {\n      d.setUTCFullYear(d.getUTCFullYear() - 1900);\n    }\n    var js = d.getUTCDay();\n    return js === 0 ? 7 : js;\n  }\n  function computeOrdinal(year, month, day) {\n    return day + (isLeapYear(year) ? leapLadder : nonLeapLadder)[month - 1];\n  }\n  function uncomputeOrdinal(year, ordinal) {\n    var table = isLeapYear(year) ? leapLadder : nonLeapLadder,\n      month0 = table.findIndex(function (i) {\n        return i < ordinal;\n      }),\n      day = ordinal - table[month0];\n    return {\n      month: month0 + 1,\n      day: day\n    };\n  }\n  function isoWeekdayToLocal(isoWeekday, startOfWeek) {\n    return (isoWeekday - startOfWeek + 7) % 7 + 1;\n  }\n\n  /**\n   * @private\n   */\n\n  function gregorianToWeek(gregObj, minDaysInFirstWeek, startOfWeek) {\n    if (minDaysInFirstWeek === void 0) {\n      minDaysInFirstWeek = 4;\n    }\n    if (startOfWeek === void 0) {\n      startOfWeek = 1;\n    }\n    var year = gregObj.year,\n      month = gregObj.month,\n      day = gregObj.day,\n      ordinal = computeOrdinal(year, month, day),\n      weekday = isoWeekdayToLocal(dayOfWeek(year, month, day), startOfWeek);\n    var weekNumber = Math.floor((ordinal - weekday + 14 - minDaysInFirstWeek) / 7),\n      weekYear;\n    if (weekNumber < 1) {\n      weekYear = year - 1;\n      weekNumber = weeksInWeekYear(weekYear, minDaysInFirstWeek, startOfWeek);\n    } else if (weekNumber > weeksInWeekYear(year, minDaysInFirstWeek, startOfWeek)) {\n      weekYear = year + 1;\n      weekNumber = 1;\n    } else {\n      weekYear = year;\n    }\n    return _extends({\n      weekYear: weekYear,\n      weekNumber: weekNumber,\n      weekday: weekday\n    }, timeObject(gregObj));\n  }\n  function weekToGregorian(weekData, minDaysInFirstWeek, startOfWeek) {\n    if (minDaysInFirstWeek === void 0) {\n      minDaysInFirstWeek = 4;\n    }\n    if (startOfWeek === void 0) {\n      startOfWeek = 1;\n    }\n    var weekYear = weekData.weekYear,\n      weekNumber = weekData.weekNumber,\n      weekday = weekData.weekday,\n      weekdayOfJan4 = isoWeekdayToLocal(dayOfWeek(weekYear, 1, minDaysInFirstWeek), startOfWeek),\n      yearInDays = daysInYear(weekYear);\n    var ordinal = weekNumber * 7 + weekday - weekdayOfJan4 - 7 + minDaysInFirstWeek,\n      year;\n    if (ordinal < 1) {\n      year = weekYear - 1;\n      ordinal += daysInYear(year);\n    } else if (ordinal > yearInDays) {\n      year = weekYear + 1;\n      ordinal -= daysInYear(weekYear);\n    } else {\n      year = weekYear;\n    }\n    var _uncomputeOrdinal = uncomputeOrdinal(year, ordinal),\n      month = _uncomputeOrdinal.month,\n      day = _uncomputeOrdinal.day;\n    return _extends({\n      year: year,\n      month: month,\n      day: day\n    }, timeObject(weekData));\n  }\n  function gregorianToOrdinal(gregData) {\n    var year = gregData.year,\n      month = gregData.month,\n      day = gregData.day;\n    var ordinal = computeOrdinal(year, month, day);\n    return _extends({\n      year: year,\n      ordinal: ordinal\n    }, timeObject(gregData));\n  }\n  function ordinalToGregorian(ordinalData) {\n    var year = ordinalData.year,\n      ordinal = ordinalData.ordinal;\n    var _uncomputeOrdinal2 = uncomputeOrdinal(year, ordinal),\n      month = _uncomputeOrdinal2.month,\n      day = _uncomputeOrdinal2.day;\n    return _extends({\n      year: year,\n      month: month,\n      day: day\n    }, timeObject(ordinalData));\n  }\n\n  /**\n   * Check if local week units like localWeekday are used in obj.\n   * If so, validates that they are not mixed with ISO week units and then copies them to the normal week unit properties.\n   * Modifies obj in-place!\n   * @param obj the object values\n   */\n  function usesLocalWeekValues(obj, loc) {\n    var hasLocaleWeekData = !isUndefined(obj.localWeekday) || !isUndefined(obj.localWeekNumber) || !isUndefined(obj.localWeekYear);\n    if (hasLocaleWeekData) {\n      var hasIsoWeekData = !isUndefined(obj.weekday) || !isUndefined(obj.weekNumber) || !isUndefined(obj.weekYear);\n      if (hasIsoWeekData) {\n        throw new ConflictingSpecificationError(\"Cannot mix locale-based week fields with ISO-based week fields\");\n      }\n      if (!isUndefined(obj.localWeekday)) obj.weekday = obj.localWeekday;\n      if (!isUndefined(obj.localWeekNumber)) obj.weekNumber = obj.localWeekNumber;\n      if (!isUndefined(obj.localWeekYear)) obj.weekYear = obj.localWeekYear;\n      delete obj.localWeekday;\n      delete obj.localWeekNumber;\n      delete obj.localWeekYear;\n      return {\n        minDaysInFirstWeek: loc.getMinDaysInFirstWeek(),\n        startOfWeek: loc.getStartOfWeek()\n      };\n    } else {\n      return {\n        minDaysInFirstWeek: 4,\n        startOfWeek: 1\n      };\n    }\n  }\n  function hasInvalidWeekData(obj, minDaysInFirstWeek, startOfWeek) {\n    if (minDaysInFirstWeek === void 0) {\n      minDaysInFirstWeek = 4;\n    }\n    if (startOfWeek === void 0) {\n      startOfWeek = 1;\n    }\n    var validYear = isInteger(obj.weekYear),\n      validWeek = integerBetween(obj.weekNumber, 1, weeksInWeekYear(obj.weekYear, minDaysInFirstWeek, startOfWeek)),\n      validWeekday = integerBetween(obj.weekday, 1, 7);\n    if (!validYear) {\n      return unitOutOfRange(\"weekYear\", obj.weekYear);\n    } else if (!validWeek) {\n      return unitOutOfRange(\"week\", obj.weekNumber);\n    } else if (!validWeekday) {\n      return unitOutOfRange(\"weekday\", obj.weekday);\n    } else return false;\n  }\n  function hasInvalidOrdinalData(obj) {\n    var validYear = isInteger(obj.year),\n      validOrdinal = integerBetween(obj.ordinal, 1, daysInYear(obj.year));\n    if (!validYear) {\n      return unitOutOfRange(\"year\", obj.year);\n    } else if (!validOrdinal) {\n      return unitOutOfRange(\"ordinal\", obj.ordinal);\n    } else return false;\n  }\n  function hasInvalidGregorianData(obj) {\n    var validYear = isInteger(obj.year),\n      validMonth = integerBetween(obj.month, 1, 12),\n      validDay = integerBetween(obj.day, 1, daysInMonth(obj.year, obj.month));\n    if (!validYear) {\n      return unitOutOfRange(\"year\", obj.year);\n    } else if (!validMonth) {\n      return unitOutOfRange(\"month\", obj.month);\n    } else if (!validDay) {\n      return unitOutOfRange(\"day\", obj.day);\n    } else return false;\n  }\n  function hasInvalidTimeData(obj) {\n    var hour = obj.hour,\n      minute = obj.minute,\n      second = obj.second,\n      millisecond = obj.millisecond;\n    var validHour = integerBetween(hour, 0, 23) || hour === 24 && minute === 0 && second === 0 && millisecond === 0,\n      validMinute = integerBetween(minute, 0, 59),\n      validSecond = integerBetween(second, 0, 59),\n      validMillisecond = integerBetween(millisecond, 0, 999);\n    if (!validHour) {\n      return unitOutOfRange(\"hour\", hour);\n    } else if (!validMinute) {\n      return unitOutOfRange(\"minute\", minute);\n    } else if (!validSecond) {\n      return unitOutOfRange(\"second\", second);\n    } else if (!validMillisecond) {\n      return unitOutOfRange(\"millisecond\", millisecond);\n    } else return false;\n  }\n\n  /**\n   * @private\n   */\n\n  // TYPES\n\n  function isUndefined(o) {\n    return typeof o === \"undefined\";\n  }\n  function isNumber(o) {\n    return typeof o === \"number\";\n  }\n  function isInteger(o) {\n    return typeof o === \"number\" && o % 1 === 0;\n  }\n  function isString(o) {\n    return typeof o === \"string\";\n  }\n  function isDate(o) {\n    return Object.prototype.toString.call(o) === \"[object Date]\";\n  }\n\n  // CAPABILITIES\n\n  function hasRelative() {\n    try {\n      return typeof Intl !== \"undefined\" && !!Intl.RelativeTimeFormat;\n    } catch (e) {\n      return false;\n    }\n  }\n  function hasLocaleWeekInfo() {\n    try {\n      return typeof Intl !== \"undefined\" && !!Intl.Locale && (\"weekInfo\" in Intl.Locale.prototype || \"getWeekInfo\" in Intl.Locale.prototype);\n    } catch (e) {\n      return false;\n    }\n  }\n\n  // OBJECTS AND ARRAYS\n\n  function maybeArray(thing) {\n    return Array.isArray(thing) ? thing : [thing];\n  }\n  function bestBy(arr, by, compare) {\n    if (arr.length === 0) {\n      return undefined;\n    }\n    return arr.reduce(function (best, next) {\n      var pair = [by(next), next];\n      if (!best) {\n        return pair;\n      } else if (compare(best[0], pair[0]) === best[0]) {\n        return best;\n      } else {\n        return pair;\n      }\n    }, null)[1];\n  }\n  function pick(obj, keys) {\n    return keys.reduce(function (a, k) {\n      a[k] = obj[k];\n      return a;\n    }, {});\n  }\n  function hasOwnProperty(obj, prop) {\n    return Object.prototype.hasOwnProperty.call(obj, prop);\n  }\n  function validateWeekSettings(settings) {\n    if (settings == null) {\n      return null;\n    } else if (typeof settings !== \"object\") {\n      throw new InvalidArgumentError(\"Week settings must be an object\");\n    } else {\n      if (!integerBetween(settings.firstDay, 1, 7) || !integerBetween(settings.minimalDays, 1, 7) || !Array.isArray(settings.weekend) || settings.weekend.some(function (v) {\n        return !integerBetween(v, 1, 7);\n      })) {\n        throw new InvalidArgumentError(\"Invalid week settings\");\n      }\n      return {\n        firstDay: settings.firstDay,\n        minimalDays: settings.minimalDays,\n        weekend: Array.from(settings.weekend)\n      };\n    }\n  }\n\n  // NUMBERS AND STRINGS\n\n  function integerBetween(thing, bottom, top) {\n    return isInteger(thing) && thing >= bottom && thing <= top;\n  }\n\n  // x % n but takes the sign of n instead of x\n  function floorMod(x, n) {\n    return x - n * Math.floor(x / n);\n  }\n  function padStart(input, n) {\n    if (n === void 0) {\n      n = 2;\n    }\n    var isNeg = input < 0;\n    var padded;\n    if (isNeg) {\n      padded = \"-\" + (\"\" + -input).padStart(n, \"0\");\n    } else {\n      padded = (\"\" + input).padStart(n, \"0\");\n    }\n    return padded;\n  }\n  function parseInteger(string) {\n    if (isUndefined(string) || string === null || string === \"\") {\n      return undefined;\n    } else {\n      return parseInt(string, 10);\n    }\n  }\n  function parseFloating(string) {\n    if (isUndefined(string) || string === null || string === \"\") {\n      return undefined;\n    } else {\n      return parseFloat(string);\n    }\n  }\n  function parseMillis(fraction) {\n    // Return undefined (instead of 0) in these cases, where fraction is not set\n    if (isUndefined(fraction) || fraction === null || fraction === \"\") {\n      return undefined;\n    } else {\n      var f = parseFloat(\"0.\" + fraction) * 1000;\n      return Math.floor(f);\n    }\n  }\n  function roundTo(number, digits, towardZero) {\n    if (towardZero === void 0) {\n      towardZero = false;\n    }\n    var factor = Math.pow(10, digits),\n      rounder = towardZero ? Math.trunc : Math.round;\n    return rounder(number * factor) / factor;\n  }\n\n  // DATE BASICS\n\n  function isLeapYear(year) {\n    return year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0);\n  }\n  function daysInYear(year) {\n    return isLeapYear(year) ? 366 : 365;\n  }\n  function daysInMonth(year, month) {\n    var modMonth = floorMod(month - 1, 12) + 1,\n      modYear = year + (month - modMonth) / 12;\n    if (modMonth === 2) {\n      return isLeapYear(modYear) ? 29 : 28;\n    } else {\n      return [31, null, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][modMonth - 1];\n    }\n  }\n\n  // convert a calendar object to a local timestamp (epoch, but with the offset baked in)\n  function objToLocalTS(obj) {\n    var d = Date.UTC(obj.year, obj.month - 1, obj.day, obj.hour, obj.minute, obj.second, obj.millisecond);\n\n    // for legacy reasons, years between 0 and 99 are interpreted as 19XX; revert that\n    if (obj.year < 100 && obj.year >= 0) {\n      d = new Date(d);\n      // set the month and day again, this is necessary because year 2000 is a leap year, but year 100 is not\n      // so if obj.year is in 99, but obj.day makes it roll over into year 100,\n      // the calculations done by Date.UTC are using year 2000 - which is incorrect\n      d.setUTCFullYear(obj.year, obj.month - 1, obj.day);\n    }\n    return +d;\n  }\n\n  // adapted from moment.js: https://github.com/moment/moment/blob/000ac1800e620f770f4eb31b5ae908f6167b0ab2/src/lib/units/week-calendar-utils.js\n  function firstWeekOffset(year, minDaysInFirstWeek, startOfWeek) {\n    var fwdlw = isoWeekdayToLocal(dayOfWeek(year, 1, minDaysInFirstWeek), startOfWeek);\n    return -fwdlw + minDaysInFirstWeek - 1;\n  }\n  function weeksInWeekYear(weekYear, minDaysInFirstWeek, startOfWeek) {\n    if (minDaysInFirstWeek === void 0) {\n      minDaysInFirstWeek = 4;\n    }\n    if (startOfWeek === void 0) {\n      startOfWeek = 1;\n    }\n    var weekOffset = firstWeekOffset(weekYear, minDaysInFirstWeek, startOfWeek);\n    var weekOffsetNext = firstWeekOffset(weekYear + 1, minDaysInFirstWeek, startOfWeek);\n    return (daysInYear(weekYear) - weekOffset + weekOffsetNext) / 7;\n  }\n  function untruncateYear(year) {\n    if (year > 99) {\n      return year;\n    } else return year > Settings.twoDigitCutoffYear ? 1900 + year : 2000 + year;\n  }\n\n  // PARSING\n\n  function parseZoneInfo(ts, offsetFormat, locale, timeZone) {\n    if (timeZone === void 0) {\n      timeZone = null;\n    }\n    var date = new Date(ts),\n      intlOpts = {\n        hourCycle: \"h23\",\n        year: \"numeric\",\n        month: \"2-digit\",\n        day: \"2-digit\",\n        hour: \"2-digit\",\n        minute: \"2-digit\"\n      };\n    if (timeZone) {\n      intlOpts.timeZone = timeZone;\n    }\n    var modified = _extends({\n      timeZoneName: offsetFormat\n    }, intlOpts);\n    var parsed = new Intl.DateTimeFormat(locale, modified).formatToParts(date).find(function (m) {\n      return m.type.toLowerCase() === \"timezonename\";\n    });\n    return parsed ? parsed.value : null;\n  }\n\n  // signedOffset('-5', '30') -> -330\n  function signedOffset(offHourStr, offMinuteStr) {\n    var offHour = parseInt(offHourStr, 10);\n\n    // don't || this because we want to preserve -0\n    if (Number.isNaN(offHour)) {\n      offHour = 0;\n    }\n    var offMin = parseInt(offMinuteStr, 10) || 0,\n      offMinSigned = offHour < 0 || Object.is(offHour, -0) ? -offMin : offMin;\n    return offHour * 60 + offMinSigned;\n  }\n\n  // COERCION\n\n  function asNumber(value) {\n    var numericValue = Number(value);\n    if (typeof value === \"boolean\" || value === \"\" || Number.isNaN(numericValue)) throw new InvalidArgumentError(\"Invalid unit value \" + value);\n    return numericValue;\n  }\n  function normalizeObject(obj, normalizer) {\n    var normalized = {};\n    for (var u in obj) {\n      if (hasOwnProperty(obj, u)) {\n        var v = obj[u];\n        if (v === undefined || v === null) continue;\n        normalized[normalizer(u)] = asNumber(v);\n      }\n    }\n    return normalized;\n  }\n\n  /**\n   * Returns the offset's value as a string\n   * @param {number} ts - Epoch milliseconds for which to get the offset\n   * @param {string} format - What style of offset to return.\n   *                          Accepts 'narrow', 'short', or 'techie'. Returning '+6', '+06:00', or '+0600' respectively\n   * @return {string}\n   */\n  function formatOffset(offset, format) {\n    var hours = Math.trunc(Math.abs(offset / 60)),\n      minutes = Math.trunc(Math.abs(offset % 60)),\n      sign = offset >= 0 ? \"+\" : \"-\";\n    switch (format) {\n      case \"short\":\n        return \"\" + sign + padStart(hours, 2) + \":\" + padStart(minutes, 2);\n      case \"narrow\":\n        return \"\" + sign + hours + (minutes > 0 ? \":\" + minutes : \"\");\n      case \"techie\":\n        return \"\" + sign + padStart(hours, 2) + padStart(minutes, 2);\n      default:\n        throw new RangeError(\"Value format \" + format + \" is out of range for property format\");\n    }\n  }\n  function timeObject(obj) {\n    return pick(obj, [\"hour\", \"minute\", \"second\", \"millisecond\"]);\n  }\n\n  /**\n   * @private\n   */\n\n  var monthsLong = [\"January\", \"February\", \"March\", \"April\", \"May\", \"June\", \"July\", \"August\", \"September\", \"October\", \"November\", \"December\"];\n  var monthsShort = [\"Jan\", \"Feb\", \"Mar\", \"Apr\", \"May\", \"Jun\", \"Jul\", \"Aug\", \"Sep\", \"Oct\", \"Nov\", \"Dec\"];\n  var monthsNarrow = [\"J\", \"F\", \"M\", \"A\", \"M\", \"J\", \"J\", \"A\", \"S\", \"O\", \"N\", \"D\"];\n  function months(length) {\n    switch (length) {\n      case \"narrow\":\n        return [].concat(monthsNarrow);\n      case \"short\":\n        return [].concat(monthsShort);\n      case \"long\":\n        return [].concat(monthsLong);\n      case \"numeric\":\n        return [\"1\", \"2\", \"3\", \"4\", \"5\", \"6\", \"7\", \"8\", \"9\", \"10\", \"11\", \"12\"];\n      case \"2-digit\":\n        return [\"01\", \"02\", \"03\", \"04\", \"05\", \"06\", \"07\", \"08\", \"09\", \"10\", \"11\", \"12\"];\n      default:\n        return null;\n    }\n  }\n  var weekdaysLong = [\"Monday\", \"Tuesday\", \"Wednesday\", \"Thursday\", \"Friday\", \"Saturday\", \"Sunday\"];\n  var weekdaysShort = [\"Mon\", \"Tue\", \"Wed\", \"Thu\", \"Fri\", \"Sat\", \"Sun\"];\n  var weekdaysNarrow = [\"M\", \"T\", \"W\", \"T\", \"F\", \"S\", \"S\"];\n  function weekdays(length) {\n    switch (length) {\n      case \"narrow\":\n        return [].concat(weekdaysNarrow);\n      case \"short\":\n        return [].concat(weekdaysShort);\n      case \"long\":\n        return [].concat(weekdaysLong);\n      case \"numeric\":\n        return [\"1\", \"2\", \"3\", \"4\", \"5\", \"6\", \"7\"];\n      default:\n        return null;\n    }\n  }\n  var meridiems = [\"AM\", \"PM\"];\n  var erasLong = [\"Before Christ\", \"Anno Domini\"];\n  var erasShort = [\"BC\", \"AD\"];\n  var erasNarrow = [\"B\", \"A\"];\n  function eras(length) {\n    switch (length) {\n      case \"narrow\":\n        return [].concat(erasNarrow);\n      case \"short\":\n        return [].concat(erasShort);\n      case \"long\":\n        return [].concat(erasLong);\n      default:\n        return null;\n    }\n  }\n  function meridiemForDateTime(dt) {\n    return meridiems[dt.hour < 12 ? 0 : 1];\n  }\n  function weekdayForDateTime(dt, length) {\n    return weekdays(length)[dt.weekday - 1];\n  }\n  function monthForDateTime(dt, length) {\n    return months(length)[dt.month - 1];\n  }\n  function eraForDateTime(dt, length) {\n    return eras(length)[dt.year < 0 ? 0 : 1];\n  }\n  function formatRelativeTime(unit, count, numeric, narrow) {\n    if (numeric === void 0) {\n      numeric = \"always\";\n    }\n    if (narrow === void 0) {\n      narrow = false;\n    }\n    var units = {\n      years: [\"year\", \"yr.\"],\n      quarters: [\"quarter\", \"qtr.\"],\n      months: [\"month\", \"mo.\"],\n      weeks: [\"week\", \"wk.\"],\n      days: [\"day\", \"day\", \"days\"],\n      hours: [\"hour\", \"hr.\"],\n      minutes: [\"minute\", \"min.\"],\n      seconds: [\"second\", \"sec.\"]\n    };\n    var lastable = [\"hours\", \"minutes\", \"seconds\"].indexOf(unit) === -1;\n    if (numeric === \"auto\" && lastable) {\n      var isDay = unit === \"days\";\n      switch (count) {\n        case 1:\n          return isDay ? \"tomorrow\" : \"next \" + units[unit][0];\n        case -1:\n          return isDay ? \"yesterday\" : \"last \" + units[unit][0];\n        case 0:\n          return isDay ? \"today\" : \"this \" + units[unit][0];\n      }\n    }\n\n    var isInPast = Object.is(count, -0) || count < 0,\n      fmtValue = Math.abs(count),\n      singular = fmtValue === 1,\n      lilUnits = units[unit],\n      fmtUnit = narrow ? singular ? lilUnits[1] : lilUnits[2] || lilUnits[1] : singular ? units[unit][0] : unit;\n    return isInPast ? fmtValue + \" \" + fmtUnit + \" ago\" : \"in \" + fmtValue + \" \" + fmtUnit;\n  }\n\n  function stringifyTokens(splits, tokenToString) {\n    var s = \"\";\n    for (var _iterator = _createForOfIteratorHelperLoose(splits), _step; !(_step = _iterator()).done;) {\n      var token = _step.value;\n      if (token.literal) {\n        s += token.val;\n      } else {\n        s += tokenToString(token.val);\n      }\n    }\n    return s;\n  }\n  var _macroTokenToFormatOpts = {\n    D: DATE_SHORT,\n    DD: DATE_MED,\n    DDD: DATE_FULL,\n    DDDD: DATE_HUGE,\n    t: TIME_SIMPLE,\n    tt: TIME_WITH_SECONDS,\n    ttt: TIME_WITH_SHORT_OFFSET,\n    tttt: TIME_WITH_LONG_OFFSET,\n    T: TIME_24_SIMPLE,\n    TT: TIME_24_WITH_SECONDS,\n    TTT: TIME_24_WITH_SHORT_OFFSET,\n    TTTT: TIME_24_WITH_LONG_OFFSET,\n    f: DATETIME_SHORT,\n    ff: DATETIME_MED,\n    fff: DATETIME_FULL,\n    ffff: DATETIME_HUGE,\n    F: DATETIME_SHORT_WITH_SECONDS,\n    FF: DATETIME_MED_WITH_SECONDS,\n    FFF: DATETIME_FULL_WITH_SECONDS,\n    FFFF: DATETIME_HUGE_WITH_SECONDS\n  };\n\n  /**\n   * @private\n   */\n  var Formatter = /*#__PURE__*/function () {\n    Formatter.create = function create(locale, opts) {\n      if (opts === void 0) {\n        opts = {};\n      }\n      return new Formatter(locale, opts);\n    };\n    Formatter.parseFormat = function parseFormat(fmt) {\n      // white-space is always considered a literal in user-provided formats\n      // the \" \" token has a special meaning (see unitForToken)\n\n      var current = null,\n        currentFull = \"\",\n        bracketed = false;\n      var splits = [];\n      for (var i = 0; i < fmt.length; i++) {\n        var c = fmt.charAt(i);\n        if (c === \"'\") {\n          if (currentFull.length > 0) {\n            splits.push({\n              literal: bracketed || /^\\s+$/.test(currentFull),\n              val: currentFull\n            });\n          }\n          current = null;\n          currentFull = \"\";\n          bracketed = !bracketed;\n        } else if (bracketed) {\n          currentFull += c;\n        } else if (c === current) {\n          currentFull += c;\n        } else {\n          if (currentFull.length > 0) {\n            splits.push({\n              literal: /^\\s+$/.test(currentFull),\n              val: currentFull\n            });\n          }\n          currentFull = c;\n          current = c;\n        }\n      }\n      if (currentFull.length > 0) {\n        splits.push({\n          literal: bracketed || /^\\s+$/.test(currentFull),\n          val: currentFull\n        });\n      }\n      return splits;\n    };\n    Formatter.macroTokenToFormatOpts = function macroTokenToFormatOpts(token) {\n      return _macroTokenToFormatOpts[token];\n    };\n    function Formatter(locale, formatOpts) {\n      this.opts = formatOpts;\n      this.loc = locale;\n      this.systemLoc = null;\n    }\n    var _proto = Formatter.prototype;\n    _proto.formatWithSystemDefault = function formatWithSystemDefault(dt, opts) {\n      if (this.systemLoc === null) {\n        this.systemLoc = this.loc.redefaultToSystem();\n      }\n      var df = this.systemLoc.dtFormatter(dt, _extends({}, this.opts, opts));\n      return df.format();\n    };\n    _proto.dtFormatter = function dtFormatter(dt, opts) {\n      if (opts === void 0) {\n        opts = {};\n      }\n      return this.loc.dtFormatter(dt, _extends({}, this.opts, opts));\n    };\n    _proto.formatDateTime = function formatDateTime(dt, opts) {\n      return this.dtFormatter(dt, opts).format();\n    };\n    _proto.formatDateTimeParts = function formatDateTimeParts(dt, opts) {\n      return this.dtFormatter(dt, opts).formatToParts();\n    };\n    _proto.formatInterval = function formatInterval(interval, opts) {\n      var df = this.dtFormatter(interval.start, opts);\n      return df.dtf.formatRange(interval.start.toJSDate(), interval.end.toJSDate());\n    };\n    _proto.resolvedOptions = function resolvedOptions(dt, opts) {\n      return this.dtFormatter(dt, opts).resolvedOptions();\n    };\n    _proto.num = function num(n, p) {\n      if (p === void 0) {\n        p = 0;\n      }\n      // we get some perf out of doing this here, annoyingly\n      if (this.opts.forceSimple) {\n        return padStart(n, p);\n      }\n      var opts = _extends({}, this.opts);\n      if (p > 0) {\n        opts.padTo = p;\n      }\n      return this.loc.numberFormatter(opts).format(n);\n    };\n    _proto.formatDateTimeFromString = function formatDateTimeFromString(dt, fmt) {\n      var _this = this;\n      var knownEnglish = this.loc.listingMode() === \"en\",\n        useDateTimeFormatter = this.loc.outputCalendar && this.loc.outputCalendar !== \"gregory\",\n        string = function string(opts, extract) {\n          return _this.loc.extract(dt, opts, extract);\n        },\n        formatOffset = function formatOffset(opts) {\n          if (dt.isOffsetFixed && dt.offset === 0 && opts.allowZ) {\n            return \"Z\";\n          }\n          return dt.isValid ? dt.zone.formatOffset(dt.ts, opts.format) : \"\";\n        },\n        meridiem = function meridiem() {\n          return knownEnglish ? meridiemForDateTime(dt) : string({\n            hour: \"numeric\",\n            hourCycle: \"h12\"\n          }, \"dayperiod\");\n        },\n        month = function month(length, standalone) {\n          return knownEnglish ? monthForDateTime(dt, length) : string(standalone ? {\n            month: length\n          } : {\n            month: length,\n            day: \"numeric\"\n          }, \"month\");\n        },\n        weekday = function weekday(length, standalone) {\n          return knownEnglish ? weekdayForDateTime(dt, length) : string(standalone ? {\n            weekday: length\n          } : {\n            weekday: length,\n            month: \"long\",\n            day: \"numeric\"\n          }, \"weekday\");\n        },\n        maybeMacro = function maybeMacro(token) {\n          var formatOpts = Formatter.macroTokenToFormatOpts(token);\n          if (formatOpts) {\n            return _this.formatWithSystemDefault(dt, formatOpts);\n          } else {\n            return token;\n          }\n        },\n        era = function era(length) {\n          return knownEnglish ? eraForDateTime(dt, length) : string({\n            era: length\n          }, \"era\");\n        },\n        tokenToString = function tokenToString(token) {\n          // Where possible: https://cldr.unicode.org/translation/date-time/date-time-symbols\n          switch (token) {\n            // ms\n            case \"S\":\n              return _this.num(dt.millisecond);\n            case \"u\":\n            // falls through\n            case \"SSS\":\n              return _this.num(dt.millisecond, 3);\n            // seconds\n            case \"s\":\n              return _this.num(dt.second);\n            case \"ss\":\n              return _this.num(dt.second, 2);\n            // fractional seconds\n            case \"uu\":\n              return _this.num(Math.floor(dt.millisecond / 10), 2);\n            case \"uuu\":\n              return _this.num(Math.floor(dt.millisecond / 100));\n            // minutes\n            case \"m\":\n              return _this.num(dt.minute);\n            case \"mm\":\n              return _this.num(dt.minute, 2);\n            // hours\n            case \"h\":\n              return _this.num(dt.hour % 12 === 0 ? 12 : dt.hour % 12);\n            case \"hh\":\n              return _this.num(dt.hour % 12 === 0 ? 12 : dt.hour % 12, 2);\n            case \"H\":\n              return _this.num(dt.hour);\n            case \"HH\":\n              return _this.num(dt.hour, 2);\n            // offset\n            case \"Z\":\n              // like +6\n              return formatOffset({\n                format: \"narrow\",\n                allowZ: _this.opts.allowZ\n              });\n            case \"ZZ\":\n              // like +06:00\n              return formatOffset({\n                format: \"short\",\n                allowZ: _this.opts.allowZ\n              });\n            case \"ZZZ\":\n              // like +0600\n              return formatOffset({\n                format: \"techie\",\n                allowZ: _this.opts.allowZ\n              });\n            case \"ZZZZ\":\n              // like EST\n              return dt.zone.offsetName(dt.ts, {\n                format: \"short\",\n                locale: _this.loc.locale\n              });\n            case \"ZZZZZ\":\n              // like Eastern Standard Time\n              return dt.zone.offsetName(dt.ts, {\n                format: \"long\",\n                locale: _this.loc.locale\n              });\n            // zone\n            case \"z\":\n              // like America/New_York\n              return dt.zoneName;\n            // meridiems\n            case \"a\":\n              return meridiem();\n            // dates\n            case \"d\":\n              return useDateTimeFormatter ? string({\n                day: \"numeric\"\n              }, \"day\") : _this.num(dt.day);\n            case \"dd\":\n              return useDateTimeFormatter ? string({\n                day: \"2-digit\"\n              }, \"day\") : _this.num(dt.day, 2);\n            // weekdays - standalone\n            case \"c\":\n              // like 1\n              return _this.num(dt.weekday);\n            case \"ccc\":\n              // like 'Tues'\n              return weekday(\"short\", true);\n            case \"cccc\":\n              // like 'Tuesday'\n              return weekday(\"long\", true);\n            case \"ccccc\":\n              // like 'T'\n              return weekday(\"narrow\", true);\n            // weekdays - format\n            case \"E\":\n              // like 1\n              return _this.num(dt.weekday);\n            case \"EEE\":\n              // like 'Tues'\n              return weekday(\"short\", false);\n            case \"EEEE\":\n              // like 'Tuesday'\n              return weekday(\"long\", false);\n            case \"EEEEE\":\n              // like 'T'\n              return weekday(\"narrow\", false);\n            // months - standalone\n            case \"L\":\n              // like 1\n              return useDateTimeFormatter ? string({\n                month: \"numeric\",\n                day: \"numeric\"\n              }, \"month\") : _this.num(dt.month);\n            case \"LL\":\n              // like 01, doesn't seem to work\n              return useDateTimeFormatter ? string({\n                month: \"2-digit\",\n                day: \"numeric\"\n              }, \"month\") : _this.num(dt.month, 2);\n            case \"LLL\":\n              // like Jan\n              return month(\"short\", true);\n            case \"LLLL\":\n              // like January\n              return month(\"long\", true);\n            case \"LLLLL\":\n              // like J\n              return month(\"narrow\", true);\n            // months - format\n            case \"M\":\n              // like 1\n              return useDateTimeFormatter ? string({\n                month: \"numeric\"\n              }, \"month\") : _this.num(dt.month);\n            case \"MM\":\n              // like 01\n              return useDateTimeFormatter ? string({\n                month: \"2-digit\"\n              }, \"month\") : _this.num(dt.month, 2);\n            case \"MMM\":\n              // like Jan\n              return month(\"short\", false);\n            case \"MMMM\":\n              // like January\n              return month(\"long\", false);\n            case \"MMMMM\":\n              // like J\n              return month(\"narrow\", false);\n            // years\n            case \"y\":\n              // like 2014\n              return useDateTimeFormatter ? string({\n                year: \"numeric\"\n              }, \"year\") : _this.num(dt.year);\n            case \"yy\":\n              // like 14\n              return useDateTimeFormatter ? string({\n                year: \"2-digit\"\n              }, \"year\") : _this.num(dt.year.toString().slice(-2), 2);\n            case \"yyyy\":\n              // like 0012\n              return useDateTimeFormatter ? string({\n                year: \"numeric\"\n              }, \"year\") : _this.num(dt.year, 4);\n            case \"yyyyyy\":\n              // like 000012\n              return useDateTimeFormatter ? string({\n                year: \"numeric\"\n              }, \"year\") : _this.num(dt.year, 6);\n            // eras\n            case \"G\":\n              // like AD\n              return era(\"short\");\n            case \"GG\":\n              // like Anno Domini\n              return era(\"long\");\n            case \"GGGGG\":\n              return era(\"narrow\");\n            case \"kk\":\n              return _this.num(dt.weekYear.toString().slice(-2), 2);\n            case \"kkkk\":\n              return _this.num(dt.weekYear, 4);\n            case \"W\":\n              return _this.num(dt.weekNumber);\n            case \"WW\":\n              return _this.num(dt.weekNumber, 2);\n            case \"n\":\n              return _this.num(dt.localWeekNumber);\n            case \"nn\":\n              return _this.num(dt.localWeekNumber, 2);\n            case \"ii\":\n              return _this.num(dt.localWeekYear.toString().slice(-2), 2);\n            case \"iiii\":\n              return _this.num(dt.localWeekYear, 4);\n            case \"o\":\n              return _this.num(dt.ordinal);\n            case \"ooo\":\n              return _this.num(dt.ordinal, 3);\n            case \"q\":\n              // like 1\n              return _this.num(dt.quarter);\n            case \"qq\":\n              // like 01\n              return _this.num(dt.quarter, 2);\n            case \"X\":\n              return _this.num(Math.floor(dt.ts / 1000));\n            case \"x\":\n              return _this.num(dt.ts);\n            default:\n              return maybeMacro(token);\n          }\n        };\n      return stringifyTokens(Formatter.parseFormat(fmt), tokenToString);\n    };\n    _proto.formatDurationFromString = function formatDurationFromString(dur, fmt) {\n      var _this2 = this;\n      var tokenToField = function tokenToField(token) {\n          switch (token[0]) {\n            case \"S\":\n              return \"millisecond\";\n            case \"s\":\n              return \"second\";\n            case \"m\":\n              return \"minute\";\n            case \"h\":\n              return \"hour\";\n            case \"d\":\n              return \"day\";\n            case \"w\":\n              return \"week\";\n            case \"M\":\n              return \"month\";\n            case \"y\":\n              return \"year\";\n            default:\n              return null;\n          }\n        },\n        tokenToString = function tokenToString(lildur) {\n          return function (token) {\n            var mapped = tokenToField(token);\n            if (mapped) {\n              return _this2.num(lildur.get(mapped), token.length);\n            } else {\n              return token;\n            }\n          };\n        },\n        tokens = Formatter.parseFormat(fmt),\n        realTokens = tokens.reduce(function (found, _ref) {\n          var literal = _ref.literal,\n            val = _ref.val;\n          return literal ? found : found.concat(val);\n        }, []),\n        collapsed = dur.shiftTo.apply(dur, realTokens.map(tokenToField).filter(function (t) {\n          return t;\n        }));\n      return stringifyTokens(tokens, tokenToString(collapsed));\n    };\n    return Formatter;\n  }();\n\n  /*\n   * This file handles parsing for well-specified formats. Here's how it works:\n   * Two things go into parsing: a regex to match with and an extractor to take apart the groups in the match.\n   * An extractor is just a function that takes a regex match array and returns a { year: ..., month: ... } object\n   * parse() does the work of executing the regex and applying the extractor. It takes multiple regex/extractor pairs to try in sequence.\n   * Extractors can take a \"cursor\" representing the offset in the match to look at. This makes it easy to combine extractors.\n   * combineExtractors() does the work of combining them, keeping track of the cursor through multiple extractions.\n   * Some extractions are super dumb and simpleParse and fromStrings help DRY them.\n   */\n\n  var ianaRegex = /[A-Za-z_+-]{1,256}(?::?\\/[A-Za-z0-9_+-]{1,256}(?:\\/[A-Za-z0-9_+-]{1,256})?)?/;\n  function combineRegexes() {\n    for (var _len = arguments.length, regexes = new Array(_len), _key = 0; _key < _len; _key++) {\n      regexes[_key] = arguments[_key];\n    }\n    var full = regexes.reduce(function (f, r) {\n      return f + r.source;\n    }, \"\");\n    return RegExp(\"^\" + full + \"$\");\n  }\n  function combineExtractors() {\n    for (var _len2 = arguments.length, extractors = new Array(_len2), _key2 = 0; _key2 < _len2; _key2++) {\n      extractors[_key2] = arguments[_key2];\n    }\n    return function (m) {\n      return extractors.reduce(function (_ref, ex) {\n        var mergedVals = _ref[0],\n          mergedZone = _ref[1],\n          cursor = _ref[2];\n        var _ex = ex(m, cursor),\n          val = _ex[0],\n          zone = _ex[1],\n          next = _ex[2];\n        return [_extends({}, mergedVals, val), zone || mergedZone, next];\n      }, [{}, null, 1]).slice(0, 2);\n    };\n  }\n  function parse(s) {\n    if (s == null) {\n      return [null, null];\n    }\n    for (var _len3 = arguments.length, patterns = new Array(_len3 > 1 ? _len3 - 1 : 0), _key3 = 1; _key3 < _len3; _key3++) {\n      patterns[_key3 - 1] = arguments[_key3];\n    }\n    for (var _i = 0, _patterns = patterns; _i < _patterns.length; _i++) {\n      var _patterns$_i = _patterns[_i],\n        regex = _patterns$_i[0],\n        extractor = _patterns$_i[1];\n      var m = regex.exec(s);\n      if (m) {\n        return extractor(m);\n      }\n    }\n    return [null, null];\n  }\n  function simpleParse() {\n    for (var _len4 = arguments.length, keys = new Array(_len4), _key4 = 0; _key4 < _len4; _key4++) {\n      keys[_key4] = arguments[_key4];\n    }\n    return function (match, cursor) {\n      var ret = {};\n      var i;\n      for (i = 0; i < keys.length; i++) {\n        ret[keys[i]] = parseInteger(match[cursor + i]);\n      }\n      return [ret, null, cursor + i];\n    };\n  }\n\n  // ISO and SQL parsing\n  var offsetRegex = /(?:(Z)|([+-]\\d\\d)(?::?(\\d\\d))?)/;\n  var isoExtendedZone = \"(?:\" + offsetRegex.source + \"?(?:\\\\[(\" + ianaRegex.source + \")\\\\])?)?\";\n  var isoTimeBaseRegex = /(\\d\\d)(?::?(\\d\\d)(?::?(\\d\\d)(?:[.,](\\d{1,30}))?)?)?/;\n  var isoTimeRegex = RegExp(\"\" + isoTimeBaseRegex.source + isoExtendedZone);\n  var isoTimeExtensionRegex = RegExp(\"(?:T\" + isoTimeRegex.source + \")?\");\n  var isoYmdRegex = /([+-]\\d{6}|\\d{4})(?:-?(\\d\\d)(?:-?(\\d\\d))?)?/;\n  var isoWeekRegex = /(\\d{4})-?W(\\d\\d)(?:-?(\\d))?/;\n  var isoOrdinalRegex = /(\\d{4})-?(\\d{3})/;\n  var extractISOWeekData = simpleParse(\"weekYear\", \"weekNumber\", \"weekDay\");\n  var extractISOOrdinalData = simpleParse(\"year\", \"ordinal\");\n  var sqlYmdRegex = /(\\d{4})-(\\d\\d)-(\\d\\d)/; // dumbed-down version of the ISO one\n  var sqlTimeRegex = RegExp(isoTimeBaseRegex.source + \" ?(?:\" + offsetRegex.source + \"|(\" + ianaRegex.source + \"))?\");\n  var sqlTimeExtensionRegex = RegExp(\"(?: \" + sqlTimeRegex.source + \")?\");\n  function int(match, pos, fallback) {\n    var m = match[pos];\n    return isUndefined(m) ? fallback : parseInteger(m);\n  }\n  function extractISOYmd(match, cursor) {\n    var item = {\n      year: int(match, cursor),\n      month: int(match, cursor + 1, 1),\n      day: int(match, cursor + 2, 1)\n    };\n    return [item, null, cursor + 3];\n  }\n  function extractISOTime(match, cursor) {\n    var item = {\n      hours: int(match, cursor, 0),\n      minutes: int(match, cursor + 1, 0),\n      seconds: int(match, cursor + 2, 0),\n      milliseconds: parseMillis(match[cursor + 3])\n    };\n    return [item, null, cursor + 4];\n  }\n  function extractISOOffset(match, cursor) {\n    var local = !match[cursor] && !match[cursor + 1],\n      fullOffset = signedOffset(match[cursor + 1], match[cursor + 2]),\n      zone = local ? null : FixedOffsetZone.instance(fullOffset);\n    return [{}, zone, cursor + 3];\n  }\n  function extractIANAZone(match, cursor) {\n    var zone = match[cursor] ? IANAZone.create(match[cursor]) : null;\n    return [{}, zone, cursor + 1];\n  }\n\n  // ISO time parsing\n\n  var isoTimeOnly = RegExp(\"^T?\" + isoTimeBaseRegex.source + \"$\");\n\n  // ISO duration parsing\n\n  var isoDuration = /^-?P(?:(?:(-?\\d{1,20}(?:\\.\\d{1,20})?)Y)?(?:(-?\\d{1,20}(?:\\.\\d{1,20})?)M)?(?:(-?\\d{1,20}(?:\\.\\d{1,20})?)W)?(?:(-?\\d{1,20}(?:\\.\\d{1,20})?)D)?(?:T(?:(-?\\d{1,20}(?:\\.\\d{1,20})?)H)?(?:(-?\\d{1,20}(?:\\.\\d{1,20})?)M)?(?:(-?\\d{1,20})(?:[.,](-?\\d{1,20}))?S)?)?)$/;\n  function extractISODuration(match) {\n    var s = match[0],\n      yearStr = match[1],\n      monthStr = match[2],\n      weekStr = match[3],\n      dayStr = match[4],\n      hourStr = match[5],\n      minuteStr = match[6],\n      secondStr = match[7],\n      millisecondsStr = match[8];\n    var hasNegativePrefix = s[0] === \"-\";\n    var negativeSeconds = secondStr && secondStr[0] === \"-\";\n    var maybeNegate = function maybeNegate(num, force) {\n      if (force === void 0) {\n        force = false;\n      }\n      return num !== undefined && (force || num && hasNegativePrefix) ? -num : num;\n    };\n    return [{\n      years: maybeNegate(parseFloating(yearStr)),\n      months: maybeNegate(parseFloating(monthStr)),\n      weeks: maybeNegate(parseFloating(weekStr)),\n      days: maybeNegate(parseFloating(dayStr)),\n      hours: maybeNegate(parseFloating(hourStr)),\n      minutes: maybeNegate(parseFloating(minuteStr)),\n      seconds: maybeNegate(parseFloating(secondStr), secondStr === \"-0\"),\n      milliseconds: maybeNegate(parseMillis(millisecondsStr), negativeSeconds)\n    }];\n  }\n\n  // These are a little braindead. EDT *should* tell us that we're in, say, America/New_York\n  // and not just that we're in -240 *right now*. But since I don't think these are used that often\n  // I'm just going to ignore that\n  var obsOffsets = {\n    GMT: 0,\n    EDT: -4 * 60,\n    EST: -5 * 60,\n    CDT: -5 * 60,\n    CST: -6 * 60,\n    MDT: -6 * 60,\n    MST: -7 * 60,\n    PDT: -7 * 60,\n    PST: -8 * 60\n  };\n  function fromStrings(weekdayStr, yearStr, monthStr, dayStr, hourStr, minuteStr, secondStr) {\n    var result = {\n      year: yearStr.length === 2 ? untruncateYear(parseInteger(yearStr)) : parseInteger(yearStr),\n      month: monthsShort.indexOf(monthStr) + 1,\n      day: parseInteger(dayStr),\n      hour: parseInteger(hourStr),\n      minute: parseInteger(minuteStr)\n    };\n    if (secondStr) result.second = parseInteger(secondStr);\n    if (weekdayStr) {\n      result.weekday = weekdayStr.length > 3 ? weekdaysLong.indexOf(weekdayStr) + 1 : weekdaysShort.indexOf(weekdayStr) + 1;\n    }\n    return result;\n  }\n\n  // RFC 2822/5322\n  var rfc2822 = /^(?:(Mon|Tue|Wed|Thu|Fri|Sat|Sun),\\s)?(\\d{1,2})\\s(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\\s(\\d{2,4})\\s(\\d\\d):(\\d\\d)(?::(\\d\\d))?\\s(?:(UT|GMT|[ECMP][SD]T)|([Zz])|(?:([+-]\\d\\d)(\\d\\d)))$/;\n  function extractRFC2822(match) {\n    var weekdayStr = match[1],\n      dayStr = match[2],\n      monthStr = match[3],\n      yearStr = match[4],\n      hourStr = match[5],\n      minuteStr = match[6],\n      secondStr = match[7],\n      obsOffset = match[8],\n      milOffset = match[9],\n      offHourStr = match[10],\n      offMinuteStr = match[11],\n      result = fromStrings(weekdayStr, yearStr, monthStr, dayStr, hourStr, minuteStr, secondStr);\n    var offset;\n    if (obsOffset) {\n      offset = obsOffsets[obsOffset];\n    } else if (milOffset) {\n      offset = 0;\n    } else {\n      offset = signedOffset(offHourStr, offMinuteStr);\n    }\n    return [result, new FixedOffsetZone(offset)];\n  }\n  function preprocessRFC2822(s) {\n    // Remove comments and folding whitespace and replace multiple-spaces with a single space\n    return s.replace(/\\([^()]*\\)|[\\n\\t]/g, \" \").replace(/(\\s\\s+)/g, \" \").trim();\n  }\n\n  // http date\n\n  var rfc1123 = /^(Mon|Tue|Wed|Thu|Fri|Sat|Sun), (\\d\\d) (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) (\\d{4}) (\\d\\d):(\\d\\d):(\\d\\d) GMT$/,\n    rfc850 = /^(Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday), (\\d\\d)-(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)-(\\d\\d) (\\d\\d):(\\d\\d):(\\d\\d) GMT$/,\n    ascii = /^(Mon|Tue|Wed|Thu|Fri|Sat|Sun) (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) ( \\d|\\d\\d) (\\d\\d):(\\d\\d):(\\d\\d) (\\d{4})$/;\n  function extractRFC1123Or850(match) {\n    var weekdayStr = match[1],\n      dayStr = match[2],\n      monthStr = match[3],\n      yearStr = match[4],\n      hourStr = match[5],\n      minuteStr = match[6],\n      secondStr = match[7],\n      result = fromStrings(weekdayStr, yearStr, monthStr, dayStr, hourStr, minuteStr, secondStr);\n    return [result, FixedOffsetZone.utcInstance];\n  }\n  function extractASCII(match) {\n    var weekdayStr = match[1],\n      monthStr = match[2],\n      dayStr = match[3],\n      hourStr = match[4],\n      minuteStr = match[5],\n      secondStr = match[6],\n      yearStr = match[7],\n      result = fromStrings(weekdayStr, yearStr, monthStr, dayStr, hourStr, minuteStr, secondStr);\n    return [result, FixedOffsetZone.utcInstance];\n  }\n  var isoYmdWithTimeExtensionRegex = combineRegexes(isoYmdRegex, isoTimeExtensionRegex);\n  var isoWeekWithTimeExtensionRegex = combineRegexes(isoWeekRegex, isoTimeExtensionRegex);\n  var isoOrdinalWithTimeExtensionRegex = combineRegexes(isoOrdinalRegex, isoTimeExtensionRegex);\n  var isoTimeCombinedRegex = combineRegexes(isoTimeRegex);\n  var extractISOYmdTimeAndOffset = combineExtractors(extractISOYmd, extractISOTime, extractISOOffset, extractIANAZone);\n  var extractISOWeekTimeAndOffset = combineExtractors(extractISOWeekData, extractISOTime, extractISOOffset, extractIANAZone);\n  var extractISOOrdinalDateAndTime = combineExtractors(extractISOOrdinalData, extractISOTime, extractISOOffset, extractIANAZone);\n  var extractISOTimeAndOffset = combineExtractors(extractISOTime, extractISOOffset, extractIANAZone);\n\n  /*\n   * @private\n   */\n\n  function parseISODate(s) {\n    return parse(s, [isoYmdWithTimeExtensionRegex, extractISOYmdTimeAndOffset], [isoWeekWithTimeExtensionRegex, extractISOWeekTimeAndOffset], [isoOrdinalWithTimeExtensionRegex, extractISOOrdinalDateAndTime], [isoTimeCombinedRegex, extractISOTimeAndOffset]);\n  }\n  function parseRFC2822Date(s) {\n    return parse(preprocessRFC2822(s), [rfc2822, extractRFC2822]);\n  }\n  function parseHTTPDate(s) {\n    return parse(s, [rfc1123, extractRFC1123Or850], [rfc850, extractRFC1123Or850], [ascii, extractASCII]);\n  }\n  function parseISODuration(s) {\n    return parse(s, [isoDuration, extractISODuration]);\n  }\n  var extractISOTimeOnly = combineExtractors(extractISOTime);\n  function parseISOTimeOnly(s) {\n    return parse(s, [isoTimeOnly, extractISOTimeOnly]);\n  }\n  var sqlYmdWithTimeExtensionRegex = combineRegexes(sqlYmdRegex, sqlTimeExtensionRegex);\n  var sqlTimeCombinedRegex = combineRegexes(sqlTimeRegex);\n  var extractISOTimeOffsetAndIANAZone = combineExtractors(extractISOTime, extractISOOffset, extractIANAZone);\n  function parseSQL(s) {\n    return parse(s, [sqlYmdWithTimeExtensionRegex, extractISOYmdTimeAndOffset], [sqlTimeCombinedRegex, extractISOTimeOffsetAndIANAZone]);\n  }\n\n  var INVALID$2 = \"Invalid Duration\";\n\n  // unit conversion constants\n  var lowOrderMatrix = {\n      weeks: {\n        days: 7,\n        hours: 7 * 24,\n        minutes: 7 * 24 * 60,\n        seconds: 7 * 24 * 60 * 60,\n        milliseconds: 7 * 24 * 60 * 60 * 1000\n      },\n      days: {\n        hours: 24,\n        minutes: 24 * 60,\n        seconds: 24 * 60 * 60,\n        milliseconds: 24 * 60 * 60 * 1000\n      },\n      hours: {\n        minutes: 60,\n        seconds: 60 * 60,\n        milliseconds: 60 * 60 * 1000\n      },\n      minutes: {\n        seconds: 60,\n        milliseconds: 60 * 1000\n      },\n      seconds: {\n        milliseconds: 1000\n      }\n    },\n    casualMatrix = _extends({\n      years: {\n        quarters: 4,\n        months: 12,\n        weeks: 52,\n        days: 365,\n        hours: 365 * 24,\n        minutes: 365 * 24 * 60,\n        seconds: 365 * 24 * 60 * 60,\n        milliseconds: 365 * 24 * 60 * 60 * 1000\n      },\n      quarters: {\n        months: 3,\n        weeks: 13,\n        days: 91,\n        hours: 91 * 24,\n        minutes: 91 * 24 * 60,\n        seconds: 91 * 24 * 60 * 60,\n        milliseconds: 91 * 24 * 60 * 60 * 1000\n      },\n      months: {\n        weeks: 4,\n        days: 30,\n        hours: 30 * 24,\n        minutes: 30 * 24 * 60,\n        seconds: 30 * 24 * 60 * 60,\n        milliseconds: 30 * 24 * 60 * 60 * 1000\n      }\n    }, lowOrderMatrix),\n    daysInYearAccurate = 146097.0 / 400,\n    daysInMonthAccurate = 146097.0 / 4800,\n    accurateMatrix = _extends({\n      years: {\n        quarters: 4,\n        months: 12,\n        weeks: daysInYearAccurate / 7,\n        days: daysInYearAccurate,\n        hours: daysInYearAccurate * 24,\n        minutes: daysInYearAccurate * 24 * 60,\n        seconds: daysInYearAccurate * 24 * 60 * 60,\n        milliseconds: daysInYearAccurate * 24 * 60 * 60 * 1000\n      },\n      quarters: {\n        months: 3,\n        weeks: daysInYearAccurate / 28,\n        days: daysInYearAccurate / 4,\n        hours: daysInYearAccurate * 24 / 4,\n        minutes: daysInYearAccurate * 24 * 60 / 4,\n        seconds: daysInYearAccurate * 24 * 60 * 60 / 4,\n        milliseconds: daysInYearAccurate * 24 * 60 * 60 * 1000 / 4\n      },\n      months: {\n        weeks: daysInMonthAccurate / 7,\n        days: daysInMonthAccurate,\n        hours: daysInMonthAccurate * 24,\n        minutes: daysInMonthAccurate * 24 * 60,\n        seconds: daysInMonthAccurate * 24 * 60 * 60,\n        milliseconds: daysInMonthAccurate * 24 * 60 * 60 * 1000\n      }\n    }, lowOrderMatrix);\n\n  // units ordered by size\n  var orderedUnits$1 = [\"years\", \"quarters\", \"months\", \"weeks\", \"days\", \"hours\", \"minutes\", \"seconds\", \"milliseconds\"];\n  var reverseUnits = orderedUnits$1.slice(0).reverse();\n\n  // clone really means \"create another instance just like this one, but with these changes\"\n  function clone$1(dur, alts, clear) {\n    if (clear === void 0) {\n      clear = false;\n    }\n    // deep merge for vals\n    var conf = {\n      values: clear ? alts.values : _extends({}, dur.values, alts.values || {}),\n      loc: dur.loc.clone(alts.loc),\n      conversionAccuracy: alts.conversionAccuracy || dur.conversionAccuracy,\n      matrix: alts.matrix || dur.matrix\n    };\n    return new Duration(conf);\n  }\n  function durationToMillis(matrix, vals) {\n    var _vals$milliseconds;\n    var sum = (_vals$milliseconds = vals.milliseconds) != null ? _vals$milliseconds : 0;\n    for (var _iterator = _createForOfIteratorHelperLoose(reverseUnits.slice(1)), _step; !(_step = _iterator()).done;) {\n      var unit = _step.value;\n      if (vals[unit]) {\n        sum += vals[unit] * matrix[unit][\"milliseconds\"];\n      }\n    }\n    return sum;\n  }\n\n  // NB: mutates parameters\n  function normalizeValues(matrix, vals) {\n    // the logic below assumes the overall value of the duration is positive\n    // if this is not the case, factor is used to make it so\n    var factor = durationToMillis(matrix, vals) < 0 ? -1 : 1;\n    orderedUnits$1.reduceRight(function (previous, current) {\n      if (!isUndefined(vals[current])) {\n        if (previous) {\n          var previousVal = vals[previous] * factor;\n          var conv = matrix[current][previous];\n\n          // if (previousVal < 0):\n          // lower order unit is negative (e.g. { years: 2, days: -2 })\n          // normalize this by reducing the higher order unit by the appropriate amount\n          // and increasing the lower order unit\n          // this can never make the higher order unit negative, because this function only operates\n          // on positive durations, so the amount of time represented by the lower order unit cannot\n          // be larger than the higher order unit\n          // else:\n          // lower order unit is positive (e.g. { years: 2, days: 450 } or { years: -2, days: 450 })\n          // in this case we attempt to convert as much as possible from the lower order unit into\n          // the higher order one\n          //\n          // Math.floor takes care of both of these cases, rounding away from 0\n          // if previousVal < 0 it makes the absolute value larger\n          // if previousVal >= it makes the absolute value smaller\n          var rollUp = Math.floor(previousVal / conv);\n          vals[current] += rollUp * factor;\n          vals[previous] -= rollUp * conv * factor;\n        }\n        return current;\n      } else {\n        return previous;\n      }\n    }, null);\n\n    // try to convert any decimals into smaller units if possible\n    // for example for { years: 2.5, days: 0, seconds: 0 } we want to get { years: 2, days: 182, hours: 12 }\n    orderedUnits$1.reduce(function (previous, current) {\n      if (!isUndefined(vals[current])) {\n        if (previous) {\n          var fraction = vals[previous] % 1;\n          vals[previous] -= fraction;\n          vals[current] += fraction * matrix[previous][current];\n        }\n        return current;\n      } else {\n        return previous;\n      }\n    }, null);\n  }\n\n  // Remove all properties with a value of 0 from an object\n  function removeZeroes(vals) {\n    var newVals = {};\n    for (var _i = 0, _Object$entries = Object.entries(vals); _i < _Object$entries.length; _i++) {\n      var _Object$entries$_i = _Object$entries[_i],\n        key = _Object$entries$_i[0],\n        value = _Object$entries$_i[1];\n      if (value !== 0) {\n        newVals[key] = value;\n      }\n    }\n    return newVals;\n  }\n\n  /**\n   * A Duration object represents a period of time, like \"2 months\" or \"1 day, 1 hour\". Conceptually, it's just a map of units to their quantities, accompanied by some additional configuration and methods for creating, parsing, interrogating, transforming, and formatting them. They can be used on their own or in conjunction with other Luxon types; for example, you can use {@link DateTime#plus} to add a Duration object to a DateTime, producing another DateTime.\n   *\n   * Here is a brief overview of commonly used methods and getters in Duration:\n   *\n   * * **Creation** To create a Duration, use {@link Duration.fromMillis}, {@link Duration.fromObject}, or {@link Duration.fromISO}.\n   * * **Unit values** See the {@link Duration#years}, {@link Duration#months}, {@link Duration#weeks}, {@link Duration#days}, {@link Duration#hours}, {@link Duration#minutes}, {@link Duration#seconds}, {@link Duration#milliseconds} accessors.\n   * * **Configuration** See  {@link Duration#locale} and {@link Duration#numberingSystem} accessors.\n   * * **Transformation** To create new Durations out of old ones use {@link Duration#plus}, {@link Duration#minus}, {@link Duration#normalize}, {@link Duration#set}, {@link Duration#reconfigure}, {@link Duration#shiftTo}, and {@link Duration#negate}.\n   * * **Output** To convert the Duration into other representations, see {@link Duration#as}, {@link Duration#toISO}, {@link Duration#toFormat}, and {@link Duration#toJSON}\n   *\n   * There's are more methods documented below. In addition, for more information on subtler topics like internationalization and validity, see the external documentation.\n   */\n  var Duration = /*#__PURE__*/function (_Symbol$for) {\n    /**\n     * @private\n     */\n    function Duration(config) {\n      var accurate = config.conversionAccuracy === \"longterm\" || false;\n      var matrix = accurate ? accurateMatrix : casualMatrix;\n      if (config.matrix) {\n        matrix = config.matrix;\n      }\n\n      /**\n       * @access private\n       */\n      this.values = config.values;\n      /**\n       * @access private\n       */\n      this.loc = config.loc || Locale.create();\n      /**\n       * @access private\n       */\n      this.conversionAccuracy = accurate ? \"longterm\" : \"casual\";\n      /**\n       * @access private\n       */\n      this.invalid = config.invalid || null;\n      /**\n       * @access private\n       */\n      this.matrix = matrix;\n      /**\n       * @access private\n       */\n      this.isLuxonDuration = true;\n    }\n\n    /**\n     * Create Duration from a number of milliseconds.\n     * @param {number} count of milliseconds\n     * @param {Object} opts - options for parsing\n     * @param {string} [opts.locale='en-US'] - the locale to use\n     * @param {string} opts.numberingSystem - the numbering system to use\n     * @param {string} [opts.conversionAccuracy='casual'] - the conversion system to use\n     * @return {Duration}\n     */\n    Duration.fromMillis = function fromMillis(count, opts) {\n      return Duration.fromObject({\n        milliseconds: count\n      }, opts);\n    }\n\n    /**\n     * Create a Duration from a JavaScript object with keys like 'years' and 'hours'.\n     * If this object is empty then a zero milliseconds duration is returned.\n     * @param {Object} obj - the object to create the DateTime from\n     * @param {number} obj.years\n     * @param {number} obj.quarters\n     * @param {number} obj.months\n     * @param {number} obj.weeks\n     * @param {number} obj.days\n     * @param {number} obj.hours\n     * @param {number} obj.minutes\n     * @param {number} obj.seconds\n     * @param {number} obj.milliseconds\n     * @param {Object} [opts=[]] - options for creating this Duration\n     * @param {string} [opts.locale='en-US'] - the locale to use\n     * @param {string} opts.numberingSystem - the numbering system to use\n     * @param {string} [opts.conversionAccuracy='casual'] - the preset conversion system to use\n     * @param {string} [opts.matrix=Object] - the custom conversion system to use\n     * @return {Duration}\n     */;\n    Duration.fromObject = function fromObject(obj, opts) {\n      if (opts === void 0) {\n        opts = {};\n      }\n      if (obj == null || typeof obj !== \"object\") {\n        throw new InvalidArgumentError(\"Duration.fromObject: argument expected to be an object, got \" + (obj === null ? \"null\" : typeof obj));\n      }\n      return new Duration({\n        values: normalizeObject(obj, Duration.normalizeUnit),\n        loc: Locale.fromObject(opts),\n        conversionAccuracy: opts.conversionAccuracy,\n        matrix: opts.matrix\n      });\n    }\n\n    /**\n     * Create a Duration from DurationLike.\n     *\n     * @param {Object | number | Duration} durationLike\n     * One of:\n     * - object with keys like 'years' and 'hours'.\n     * - number representing milliseconds\n     * - Duration instance\n     * @return {Duration}\n     */;\n    Duration.fromDurationLike = function fromDurationLike(durationLike) {\n      if (isNumber(durationLike)) {\n        return Duration.fromMillis(durationLike);\n      } else if (Duration.isDuration(durationLike)) {\n        return durationLike;\n      } else if (typeof durationLike === \"object\") {\n        return Duration.fromObject(durationLike);\n      } else {\n        throw new InvalidArgumentError(\"Unknown duration argument \" + durationLike + \" of type \" + typeof durationLike);\n      }\n    }\n\n    /**\n     * Create a Duration from an ISO 8601 duration string.\n     * @param {string} text - text to parse\n     * @param {Object} opts - options for parsing\n     * @param {string} [opts.locale='en-US'] - the locale to use\n     * @param {string} opts.numberingSystem - the numbering system to use\n     * @param {string} [opts.conversionAccuracy='casual'] - the preset conversion system to use\n     * @param {string} [opts.matrix=Object] - the preset conversion system to use\n     * @see https://en.wikipedia.org/wiki/ISO_8601#Durations\n     * @example Duration.fromISO('P3Y6M1W4DT12H30M5S').toObject() //=> { years: 3, months: 6, weeks: 1, days: 4, hours: 12, minutes: 30, seconds: 5 }\n     * @example Duration.fromISO('PT23H').toObject() //=> { hours: 23 }\n     * @example Duration.fromISO('P5Y3M').toObject() //=> { years: 5, months: 3 }\n     * @return {Duration}\n     */;\n    Duration.fromISO = function fromISO(text, opts) {\n      var _parseISODuration = parseISODuration(text),\n        parsed = _parseISODuration[0];\n      if (parsed) {\n        return Duration.fromObject(parsed, opts);\n      } else {\n        return Duration.invalid(\"unparsable\", \"the input \\\"\" + text + \"\\\" can't be parsed as ISO 8601\");\n      }\n    }\n\n    /**\n     * Create a Duration from an ISO 8601 time string.\n     * @param {string} text - text to parse\n     * @param {Object} opts - options for parsing\n     * @param {string} [opts.locale='en-US'] - the locale to use\n     * @param {string} opts.numberingSystem - the numbering system to use\n     * @param {string} [opts.conversionAccuracy='casual'] - the preset conversion system to use\n     * @param {string} [opts.matrix=Object] - the conversion system to use\n     * @see https://en.wikipedia.org/wiki/ISO_8601#Times\n     * @example Duration.fromISOTime('11:22:33.444').toObject() //=> { hours: 11, minutes: 22, seconds: 33, milliseconds: 444 }\n     * @example Duration.fromISOTime('11:00').toObject() //=> { hours: 11, minutes: 0, seconds: 0 }\n     * @example Duration.fromISOTime('T11:00').toObject() //=> { hours: 11, minutes: 0, seconds: 0 }\n     * @example Duration.fromISOTime('1100').toObject() //=> { hours: 11, minutes: 0, seconds: 0 }\n     * @example Duration.fromISOTime('T1100').toObject() //=> { hours: 11, minutes: 0, seconds: 0 }\n     * @return {Duration}\n     */;\n    Duration.fromISOTime = function fromISOTime(text, opts) {\n      var _parseISOTimeOnly = parseISOTimeOnly(text),\n        parsed = _parseISOTimeOnly[0];\n      if (parsed) {\n        return Duration.fromObject(parsed, opts);\n      } else {\n        return Duration.invalid(\"unparsable\", \"the input \\\"\" + text + \"\\\" can't be parsed as ISO 8601\");\n      }\n    }\n\n    /**\n     * Create an invalid Duration.\n     * @param {string} reason - simple string of why this datetime is invalid. Should not contain parameters or anything else data-dependent\n     * @param {string} [explanation=null] - longer explanation, may include parameters and other useful debugging information\n     * @return {Duration}\n     */;\n    Duration.invalid = function invalid(reason, explanation) {\n      if (explanation === void 0) {\n        explanation = null;\n      }\n      if (!reason) {\n        throw new InvalidArgumentError(\"need to specify a reason the Duration is invalid\");\n      }\n      var invalid = reason instanceof Invalid ? reason : new Invalid(reason, explanation);\n      if (Settings.throwOnInvalid) {\n        throw new InvalidDurationError(invalid);\n      } else {\n        return new Duration({\n          invalid: invalid\n        });\n      }\n    }\n\n    /**\n     * @private\n     */;\n    Duration.normalizeUnit = function normalizeUnit(unit) {\n      var normalized = {\n        year: \"years\",\n        years: \"years\",\n        quarter: \"quarters\",\n        quarters: \"quarters\",\n        month: \"months\",\n        months: \"months\",\n        week: \"weeks\",\n        weeks: \"weeks\",\n        day: \"days\",\n        days: \"days\",\n        hour: \"hours\",\n        hours: \"hours\",\n        minute: \"minutes\",\n        minutes: \"minutes\",\n        second: \"seconds\",\n        seconds: \"seconds\",\n        millisecond: \"milliseconds\",\n        milliseconds: \"milliseconds\"\n      }[unit ? unit.toLowerCase() : unit];\n      if (!normalized) throw new InvalidUnitError(unit);\n      return normalized;\n    }\n\n    /**\n     * Check if an object is a Duration. Works across context boundaries\n     * @param {object} o\n     * @return {boolean}\n     */;\n    Duration.isDuration = function isDuration(o) {\n      return o && o.isLuxonDuration || false;\n    }\n\n    /**\n     * Get  the locale of a Duration, such 'en-GB'\n     * @type {string}\n     */;\n    var _proto = Duration.prototype;\n    /**\n     * Returns a string representation of this Duration formatted according to the specified format string. You may use these tokens:\n     * * `S` for milliseconds\n     * * `s` for seconds\n     * * `m` for minutes\n     * * `h` for hours\n     * * `d` for days\n     * * `w` for weeks\n     * * `M` for months\n     * * `y` for years\n     * Notes:\n     * * Add padding by repeating the token, e.g. \"yy\" pads the years to two digits, \"hhhh\" pads the hours out to four digits\n     * * Tokens can be escaped by wrapping with single quotes.\n     * * The duration will be converted to the set of units in the format string using {@link Duration#shiftTo} and the Durations's conversion accuracy setting.\n     * @param {string} fmt - the format string\n     * @param {Object} opts - options\n     * @param {boolean} [opts.floor=true] - floor numerical values\n     * @example Duration.fromObject({ years: 1, days: 6, seconds: 2 }).toFormat(\"y d s\") //=> \"1 6 2\"\n     * @example Duration.fromObject({ years: 1, days: 6, seconds: 2 }).toFormat(\"yy dd sss\") //=> \"01 06 002\"\n     * @example Duration.fromObject({ years: 1, days: 6, seconds: 2 }).toFormat(\"M S\") //=> \"12 518402000\"\n     * @return {string}\n     */\n    _proto.toFormat = function toFormat(fmt, opts) {\n      if (opts === void 0) {\n        opts = {};\n      }\n      // reverse-compat since 1.2; we always round down now, never up, and we do it by default\n      var fmtOpts = _extends({}, opts, {\n        floor: opts.round !== false && opts.floor !== false\n      });\n      return this.isValid ? Formatter.create(this.loc, fmtOpts).formatDurationFromString(this, fmt) : INVALID$2;\n    }\n\n    /**\n     * Returns a string representation of a Duration with all units included.\n     * To modify its behavior, use `listStyle` and any Intl.NumberFormat option, though `unitDisplay` is especially relevant.\n     * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat#options\n     * @param {Object} opts - Formatting options. Accepts the same keys as the options parameter of the native `Intl.NumberFormat` constructor, as well as `listStyle`.\n     * @param {string} [opts.listStyle='narrow'] - How to format the merged list. Corresponds to the `style` property of the options parameter of the native `Intl.ListFormat` constructor.\n     * @example\n     * ```js\n     * var dur = Duration.fromObject({ days: 1, hours: 5, minutes: 6 })\n     * dur.toHuman() //=> '1 day, 5 hours, 6 minutes'\n     * dur.toHuman({ listStyle: \"long\" }) //=> '1 day, 5 hours, and 6 minutes'\n     * dur.toHuman({ unitDisplay: \"short\" }) //=> '1 day, 5 hr, 6 min'\n     * ```\n     */;\n    _proto.toHuman = function toHuman(opts) {\n      var _this = this;\n      if (opts === void 0) {\n        opts = {};\n      }\n      if (!this.isValid) return INVALID$2;\n      var l = orderedUnits$1.map(function (unit) {\n        var val = _this.values[unit];\n        if (isUndefined(val)) {\n          return null;\n        }\n        return _this.loc.numberFormatter(_extends({\n          style: \"unit\",\n          unitDisplay: \"long\"\n        }, opts, {\n          unit: unit.slice(0, -1)\n        })).format(val);\n      }).filter(function (n) {\n        return n;\n      });\n      return this.loc.listFormatter(_extends({\n        type: \"conjunction\",\n        style: opts.listStyle || \"narrow\"\n      }, opts)).format(l);\n    }\n\n    /**\n     * Returns a JavaScript object with this Duration's values.\n     * @example Duration.fromObject({ years: 1, days: 6, seconds: 2 }).toObject() //=> { years: 1, days: 6, seconds: 2 }\n     * @return {Object}\n     */;\n    _proto.toObject = function toObject() {\n      if (!this.isValid) return {};\n      return _extends({}, this.values);\n    }\n\n    /**\n     * Returns an ISO 8601-compliant string representation of this Duration.\n     * @see https://en.wikipedia.org/wiki/ISO_8601#Durations\n     * @example Duration.fromObject({ years: 3, seconds: 45 }).toISO() //=> 'P3YT45S'\n     * @example Duration.fromObject({ months: 4, seconds: 45 }).toISO() //=> 'P4MT45S'\n     * @example Duration.fromObject({ months: 5 }).toISO() //=> 'P5M'\n     * @example Duration.fromObject({ minutes: 5 }).toISO() //=> 'PT5M'\n     * @example Duration.fromObject({ milliseconds: 6 }).toISO() //=> 'PT0.006S'\n     * @return {string}\n     */;\n    _proto.toISO = function toISO() {\n      // we could use the formatter, but this is an easier way to get the minimum string\n      if (!this.isValid) return null;\n      var s = \"P\";\n      if (this.years !== 0) s += this.years + \"Y\";\n      if (this.months !== 0 || this.quarters !== 0) s += this.months + this.quarters * 3 + \"M\";\n      if (this.weeks !== 0) s += this.weeks + \"W\";\n      if (this.days !== 0) s += this.days + \"D\";\n      if (this.hours !== 0 || this.minutes !== 0 || this.seconds !== 0 || this.milliseconds !== 0) s += \"T\";\n      if (this.hours !== 0) s += this.hours + \"H\";\n      if (this.minutes !== 0) s += this.minutes + \"M\";\n      if (this.seconds !== 0 || this.milliseconds !== 0)\n        // this will handle \"floating point madness\" by removing extra decimal places\n        // https://stackoverflow.com/questions/588004/is-floating-point-math-broken\n        s += roundTo(this.seconds + this.milliseconds / 1000, 3) + \"S\";\n      if (s === \"P\") s += \"T0S\";\n      return s;\n    }\n\n    /**\n     * Returns an ISO 8601-compliant string representation of this Duration, formatted as a time of day.\n     * Note that this will return null if the duration is invalid, negative, or equal to or greater than 24 hours.\n     * @see https://en.wikipedia.org/wiki/ISO_8601#Times\n     * @param {Object} opts - options\n     * @param {boolean} [opts.suppressMilliseconds=false] - exclude milliseconds from the format if they're 0\n     * @param {boolean} [opts.suppressSeconds=false] - exclude seconds from the format if they're 0\n     * @param {boolean} [opts.includePrefix=false] - include the `T` prefix\n     * @param {string} [opts.format='extended'] - choose between the basic and extended format\n     * @example Duration.fromObject({ hours: 11 }).toISOTime() //=> '11:00:00.000'\n     * @example Duration.fromObject({ hours: 11 }).toISOTime({ suppressMilliseconds: true }) //=> '11:00:00'\n     * @example Duration.fromObject({ hours: 11 }).toISOTime({ suppressSeconds: true }) //=> '11:00'\n     * @example Duration.fromObject({ hours: 11 }).toISOTime({ includePrefix: true }) //=> 'T11:00:00.000'\n     * @example Duration.fromObject({ hours: 11 }).toISOTime({ format: 'basic' }) //=> '110000.000'\n     * @return {string}\n     */;\n    _proto.toISOTime = function toISOTime(opts) {\n      if (opts === void 0) {\n        opts = {};\n      }\n      if (!this.isValid) return null;\n      var millis = this.toMillis();\n      if (millis < 0 || millis >= 86400000) return null;\n      opts = _extends({\n        suppressMilliseconds: false,\n        suppressSeconds: false,\n        includePrefix: false,\n        format: \"extended\"\n      }, opts, {\n        includeOffset: false\n      });\n      var dateTime = DateTime.fromMillis(millis, {\n        zone: \"UTC\"\n      });\n      return dateTime.toISOTime(opts);\n    }\n\n    /**\n     * Returns an ISO 8601 representation of this Duration appropriate for use in JSON.\n     * @return {string}\n     */;\n    _proto.toJSON = function toJSON() {\n      return this.toISO();\n    }\n\n    /**\n     * Returns an ISO 8601 representation of this Duration appropriate for use in debugging.\n     * @return {string}\n     */;\n    _proto.toString = function toString() {\n      return this.toISO();\n    }\n\n    /**\n     * Returns a string representation of this Duration appropriate for the REPL.\n     * @return {string}\n     */;\n    _proto[_Symbol$for] = function () {\n      if (this.isValid) {\n        return \"Duration { values: \" + JSON.stringify(this.values) + \" }\";\n      } else {\n        return \"Duration { Invalid, reason: \" + this.invalidReason + \" }\";\n      }\n    }\n\n    /**\n     * Returns an milliseconds value of this Duration.\n     * @return {number}\n     */;\n    _proto.toMillis = function toMillis() {\n      if (!this.isValid) return NaN;\n      return durationToMillis(this.matrix, this.values);\n    }\n\n    /**\n     * Returns an milliseconds value of this Duration. Alias of {@link toMillis}\n     * @return {number}\n     */;\n    _proto.valueOf = function valueOf() {\n      return this.toMillis();\n    }\n\n    /**\n     * Make this Duration longer by the specified amount. Return a newly-constructed Duration.\n     * @param {Duration|Object|number} duration - The amount to add. Either a Luxon Duration, a number of milliseconds, the object argument to Duration.fromObject()\n     * @return {Duration}\n     */;\n    _proto.plus = function plus(duration) {\n      if (!this.isValid) return this;\n      var dur = Duration.fromDurationLike(duration),\n        result = {};\n      for (var _i2 = 0, _orderedUnits = orderedUnits$1; _i2 < _orderedUnits.length; _i2++) {\n        var k = _orderedUnits[_i2];\n        if (hasOwnProperty(dur.values, k) || hasOwnProperty(this.values, k)) {\n          result[k] = dur.get(k) + this.get(k);\n        }\n      }\n      return clone$1(this, {\n        values: result\n      }, true);\n    }\n\n    /**\n     * Make this Duration shorter by the specified amount. Return a newly-constructed Duration.\n     * @param {Duration|Object|number} duration - The amount to subtract. Either a Luxon Duration, a number of milliseconds, the object argument to Duration.fromObject()\n     * @return {Duration}\n     */;\n    _proto.minus = function minus(duration) {\n      if (!this.isValid) return this;\n      var dur = Duration.fromDurationLike(duration);\n      return this.plus(dur.negate());\n    }\n\n    /**\n     * Scale this Duration by the specified amount. Return a newly-constructed Duration.\n     * @param {function} fn - The function to apply to each unit. Arity is 1 or 2: the value of the unit and, optionally, the unit name. Must return a number.\n     * @example Duration.fromObject({ hours: 1, minutes: 30 }).mapUnits(x => x * 2) //=> { hours: 2, minutes: 60 }\n     * @example Duration.fromObject({ hours: 1, minutes: 30 }).mapUnits((x, u) => u === \"hours\" ? x * 2 : x) //=> { hours: 2, minutes: 30 }\n     * @return {Duration}\n     */;\n    _proto.mapUnits = function mapUnits(fn) {\n      if (!this.isValid) return this;\n      var result = {};\n      for (var _i3 = 0, _Object$keys = Object.keys(this.values); _i3 < _Object$keys.length; _i3++) {\n        var k = _Object$keys[_i3];\n        result[k] = asNumber(fn(this.values[k], k));\n      }\n      return clone$1(this, {\n        values: result\n      }, true);\n    }\n\n    /**\n     * Get the value of unit.\n     * @param {string} unit - a unit such as 'minute' or 'day'\n     * @example Duration.fromObject({years: 2, days: 3}).get('years') //=> 2\n     * @example Duration.fromObject({years: 2, days: 3}).get('months') //=> 0\n     * @example Duration.fromObject({years: 2, days: 3}).get('days') //=> 3\n     * @return {number}\n     */;\n    _proto.get = function get(unit) {\n      return this[Duration.normalizeUnit(unit)];\n    }\n\n    /**\n     * \"Set\" the values of specified units. Return a newly-constructed Duration.\n     * @param {Object} values - a mapping of units to numbers\n     * @example dur.set({ years: 2017 })\n     * @example dur.set({ hours: 8, minutes: 30 })\n     * @return {Duration}\n     */;\n    _proto.set = function set(values) {\n      if (!this.isValid) return this;\n      var mixed = _extends({}, this.values, normalizeObject(values, Duration.normalizeUnit));\n      return clone$1(this, {\n        values: mixed\n      });\n    }\n\n    /**\n     * \"Set\" the locale and/or numberingSystem.  Returns a newly-constructed Duration.\n     * @example dur.reconfigure({ locale: 'en-GB' })\n     * @return {Duration}\n     */;\n    _proto.reconfigure = function reconfigure(_temp) {\n      var _ref = _temp === void 0 ? {} : _temp,\n        locale = _ref.locale,\n        numberingSystem = _ref.numberingSystem,\n        conversionAccuracy = _ref.conversionAccuracy,\n        matrix = _ref.matrix;\n      var loc = this.loc.clone({\n        locale: locale,\n        numberingSystem: numberingSystem\n      });\n      var opts = {\n        loc: loc,\n        matrix: matrix,\n        conversionAccuracy: conversionAccuracy\n      };\n      return clone$1(this, opts);\n    }\n\n    /**\n     * Return the length of the duration in the specified unit.\n     * @param {string} unit - a unit such as 'minutes' or 'days'\n     * @example Duration.fromObject({years: 1}).as('days') //=> 365\n     * @example Duration.fromObject({years: 1}).as('months') //=> 12\n     * @example Duration.fromObject({hours: 60}).as('days') //=> 2.5\n     * @return {number}\n     */;\n    _proto.as = function as(unit) {\n      return this.isValid ? this.shiftTo(unit).get(unit) : NaN;\n    }\n\n    /**\n     * Reduce this Duration to its canonical representation in its current units.\n     * Assuming the overall value of the Duration is positive, this means:\n     * - excessive values for lower-order units are converted to higher-order units (if possible, see first and second example)\n     * - negative lower-order units are converted to higher order units (there must be such a higher order unit, otherwise\n     *   the overall value would be negative, see third example)\n     * - fractional values for higher-order units are converted to lower-order units (if possible, see fourth example)\n     *\n     * If the overall value is negative, the result of this method is equivalent to `this.negate().normalize().negate()`.\n     * @example Duration.fromObject({ years: 2, days: 5000 }).normalize().toObject() //=> { years: 15, days: 255 }\n     * @example Duration.fromObject({ days: 5000 }).normalize().toObject() //=> { days: 5000 }\n     * @example Duration.fromObject({ hours: 12, minutes: -45 }).normalize().toObject() //=> { hours: 11, minutes: 15 }\n     * @example Duration.fromObject({ years: 2.5, days: 0, hours: 0 }).normalize().toObject() //=> { years: 2, days: 182, hours: 12 }\n     * @return {Duration}\n     */;\n    _proto.normalize = function normalize() {\n      if (!this.isValid) return this;\n      var vals = this.toObject();\n      normalizeValues(this.matrix, vals);\n      return clone$1(this, {\n        values: vals\n      }, true);\n    }\n\n    /**\n     * Rescale units to its largest representation\n     * @example Duration.fromObject({ milliseconds: 90000 }).rescale().toObject() //=> { minutes: 1, seconds: 30 }\n     * @return {Duration}\n     */;\n    _proto.rescale = function rescale() {\n      if (!this.isValid) return this;\n      var vals = removeZeroes(this.normalize().shiftToAll().toObject());\n      return clone$1(this, {\n        values: vals\n      }, true);\n    }\n\n    /**\n     * Convert this Duration into its representation in a different set of units.\n     * @example Duration.fromObject({ hours: 1, seconds: 30 }).shiftTo('minutes', 'milliseconds').toObject() //=> { minutes: 60, milliseconds: 30000 }\n     * @return {Duration}\n     */;\n    _proto.shiftTo = function shiftTo() {\n      for (var _len = arguments.length, units = new Array(_len), _key = 0; _key < _len; _key++) {\n        units[_key] = arguments[_key];\n      }\n      if (!this.isValid) return this;\n      if (units.length === 0) {\n        return this;\n      }\n      units = units.map(function (u) {\n        return Duration.normalizeUnit(u);\n      });\n      var built = {},\n        accumulated = {},\n        vals = this.toObject();\n      var lastUnit;\n      for (var _i4 = 0, _orderedUnits2 = orderedUnits$1; _i4 < _orderedUnits2.length; _i4++) {\n        var k = _orderedUnits2[_i4];\n        if (units.indexOf(k) >= 0) {\n          lastUnit = k;\n          var own = 0;\n\n          // anything we haven't boiled down yet should get boiled to this unit\n          for (var ak in accumulated) {\n            own += this.matrix[ak][k] * accumulated[ak];\n            accumulated[ak] = 0;\n          }\n\n          // plus anything that's already in this unit\n          if (isNumber(vals[k])) {\n            own += vals[k];\n          }\n\n          // only keep the integer part for now in the hopes of putting any decimal part\n          // into a smaller unit later\n          var i = Math.trunc(own);\n          built[k] = i;\n          accumulated[k] = (own * 1000 - i * 1000) / 1000;\n\n          // otherwise, keep it in the wings to boil it later\n        } else if (isNumber(vals[k])) {\n          accumulated[k] = vals[k];\n        }\n      }\n\n      // anything leftover becomes the decimal for the last unit\n      // lastUnit must be defined since units is not empty\n      for (var key in accumulated) {\n        if (accumulated[key] !== 0) {\n          built[lastUnit] += key === lastUnit ? accumulated[key] : accumulated[key] / this.matrix[lastUnit][key];\n        }\n      }\n      normalizeValues(this.matrix, built);\n      return clone$1(this, {\n        values: built\n      }, true);\n    }\n\n    /**\n     * Shift this Duration to all available units.\n     * Same as shiftTo(\"years\", \"months\", \"weeks\", \"days\", \"hours\", \"minutes\", \"seconds\", \"milliseconds\")\n     * @return {Duration}\n     */;\n    _proto.shiftToAll = function shiftToAll() {\n      if (!this.isValid) return this;\n      return this.shiftTo(\"years\", \"months\", \"weeks\", \"days\", \"hours\", \"minutes\", \"seconds\", \"milliseconds\");\n    }\n\n    /**\n     * Return the negative of this Duration.\n     * @example Duration.fromObject({ hours: 1, seconds: 30 }).negate().toObject() //=> { hours: -1, seconds: -30 }\n     * @return {Duration}\n     */;\n    _proto.negate = function negate() {\n      if (!this.isValid) return this;\n      var negated = {};\n      for (var _i5 = 0, _Object$keys2 = Object.keys(this.values); _i5 < _Object$keys2.length; _i5++) {\n        var k = _Object$keys2[_i5];\n        negated[k] = this.values[k] === 0 ? 0 : -this.values[k];\n      }\n      return clone$1(this, {\n        values: negated\n      }, true);\n    }\n\n    /**\n     * Get the years.\n     * @type {number}\n     */;\n    /**\n     * Equality check\n     * Two Durations are equal iff they have the same units and the same values for each unit.\n     * @param {Duration} other\n     * @return {boolean}\n     */\n    _proto.equals = function equals(other) {\n      if (!this.isValid || !other.isValid) {\n        return false;\n      }\n      if (!this.loc.equals(other.loc)) {\n        return false;\n      }\n      function eq(v1, v2) {\n        // Consider 0 and undefined as equal\n        if (v1 === undefined || v1 === 0) return v2 === undefined || v2 === 0;\n        return v1 === v2;\n      }\n      for (var _i6 = 0, _orderedUnits3 = orderedUnits$1; _i6 < _orderedUnits3.length; _i6++) {\n        var u = _orderedUnits3[_i6];\n        if (!eq(this.values[u], other.values[u])) {\n          return false;\n        }\n      }\n      return true;\n    };\n    _createClass(Duration, [{\n      key: \"locale\",\n      get: function get() {\n        return this.isValid ? this.loc.locale : null;\n      }\n\n      /**\n       * Get the numbering system of a Duration, such 'beng'. The numbering system is used when formatting the Duration\n       *\n       * @type {string}\n       */\n    }, {\n      key: \"numberingSystem\",\n      get: function get() {\n        return this.isValid ? this.loc.numberingSystem : null;\n      }\n    }, {\n      key: \"years\",\n      get: function get() {\n        return this.isValid ? this.values.years || 0 : NaN;\n      }\n\n      /**\n       * Get the quarters.\n       * @type {number}\n       */\n    }, {\n      key: \"quarters\",\n      get: function get() {\n        return this.isValid ? this.values.quarters || 0 : NaN;\n      }\n\n      /**\n       * Get the months.\n       * @type {number}\n       */\n    }, {\n      key: \"months\",\n      get: function get() {\n        return this.isValid ? this.values.months || 0 : NaN;\n      }\n\n      /**\n       * Get the weeks\n       * @type {number}\n       */\n    }, {\n      key: \"weeks\",\n      get: function get() {\n        return this.isValid ? this.values.weeks || 0 : NaN;\n      }\n\n      /**\n       * Get the days.\n       * @type {number}\n       */\n    }, {\n      key: \"days\",\n      get: function get() {\n        return this.isValid ? this.values.days || 0 : NaN;\n      }\n\n      /**\n       * Get the hours.\n       * @type {number}\n       */\n    }, {\n      key: \"hours\",\n      get: function get() {\n        return this.isValid ? this.values.hours || 0 : NaN;\n      }\n\n      /**\n       * Get the minutes.\n       * @type {number}\n       */\n    }, {\n      key: \"minutes\",\n      get: function get() {\n        return this.isValid ? this.values.minutes || 0 : NaN;\n      }\n\n      /**\n       * Get the seconds.\n       * @return {number}\n       */\n    }, {\n      key: \"seconds\",\n      get: function get() {\n        return this.isValid ? this.values.seconds || 0 : NaN;\n      }\n\n      /**\n       * Get the milliseconds.\n       * @return {number}\n       */\n    }, {\n      key: \"milliseconds\",\n      get: function get() {\n        return this.isValid ? this.values.milliseconds || 0 : NaN;\n      }\n\n      /**\n       * Returns whether the Duration is invalid. Invalid durations are returned by diff operations\n       * on invalid DateTimes or Intervals.\n       * @return {boolean}\n       */\n    }, {\n      key: \"isValid\",\n      get: function get() {\n        return this.invalid === null;\n      }\n\n      /**\n       * Returns an error code if this Duration became invalid, or null if the Duration is valid\n       * @return {string}\n       */\n    }, {\n      key: \"invalidReason\",\n      get: function get() {\n        return this.invalid ? this.invalid.reason : null;\n      }\n\n      /**\n       * Returns an explanation of why this Duration became invalid, or null if the Duration is valid\n       * @type {string}\n       */\n    }, {\n      key: \"invalidExplanation\",\n      get: function get() {\n        return this.invalid ? this.invalid.explanation : null;\n      }\n    }]);\n    return Duration;\n  }(Symbol.for(\"nodejs.util.inspect.custom\"));\n\n  var INVALID$1 = \"Invalid Interval\";\n\n  // checks if the start is equal to or before the end\n  function validateStartEnd(start, end) {\n    if (!start || !start.isValid) {\n      return Interval.invalid(\"missing or invalid start\");\n    } else if (!end || !end.isValid) {\n      return Interval.invalid(\"missing or invalid end\");\n    } else if (end < start) {\n      return Interval.invalid(\"end before start\", \"The end of an interval must be after its start, but you had start=\" + start.toISO() + \" and end=\" + end.toISO());\n    } else {\n      return null;\n    }\n  }\n\n  /**\n   * An Interval object represents a half-open interval of time, where each endpoint is a {@link DateTime}. Conceptually, it's a container for those two endpoints, accompanied by methods for creating, parsing, interrogating, comparing, transforming, and formatting them.\n   *\n   * Here is a brief overview of the most commonly used methods and getters in Interval:\n   *\n   * * **Creation** To create an Interval, use {@link Interval.fromDateTimes}, {@link Interval.after}, {@link Interval.before}, or {@link Interval.fromISO}.\n   * * **Accessors** Use {@link Interval#start} and {@link Interval#end} to get the start and end.\n   * * **Interrogation** To analyze the Interval, use {@link Interval#count}, {@link Interval#length}, {@link Interval#hasSame}, {@link Interval#contains}, {@link Interval#isAfter}, or {@link Interval#isBefore}.\n   * * **Transformation** To create other Intervals out of this one, use {@link Interval#set}, {@link Interval#splitAt}, {@link Interval#splitBy}, {@link Interval#divideEqually}, {@link Interval.merge}, {@link Interval.xor}, {@link Interval#union}, {@link Interval#intersection}, or {@link Interval#difference}.\n   * * **Comparison** To compare this Interval to another one, use {@link Interval#equals}, {@link Interval#overlaps}, {@link Interval#abutsStart}, {@link Interval#abutsEnd}, {@link Interval#engulfs}\n   * * **Output** To convert the Interval into other representations, see {@link Interval#toString}, {@link Interval#toLocaleString}, {@link Interval#toISO}, {@link Interval#toISODate}, {@link Interval#toISOTime}, {@link Interval#toFormat}, and {@link Interval#toDuration}.\n   */\n  var Interval = /*#__PURE__*/function (_Symbol$for) {\n    /**\n     * @private\n     */\n    function Interval(config) {\n      /**\n       * @access private\n       */\n      this.s = config.start;\n      /**\n       * @access private\n       */\n      this.e = config.end;\n      /**\n       * @access private\n       */\n      this.invalid = config.invalid || null;\n      /**\n       * @access private\n       */\n      this.isLuxonInterval = true;\n    }\n\n    /**\n     * Create an invalid Interval.\n     * @param {string} reason - simple string of why this Interval is invalid. Should not contain parameters or anything else data-dependent\n     * @param {string} [explanation=null] - longer explanation, may include parameters and other useful debugging information\n     * @return {Interval}\n     */\n    Interval.invalid = function invalid(reason, explanation) {\n      if (explanation === void 0) {\n        explanation = null;\n      }\n      if (!reason) {\n        throw new InvalidArgumentError(\"need to specify a reason the Interval is invalid\");\n      }\n      var invalid = reason instanceof Invalid ? reason : new Invalid(reason, explanation);\n      if (Settings.throwOnInvalid) {\n        throw new InvalidIntervalError(invalid);\n      } else {\n        return new Interval({\n          invalid: invalid\n        });\n      }\n    }\n\n    /**\n     * Create an Interval from a start DateTime and an end DateTime. Inclusive of the start but not the end.\n     * @param {DateTime|Date|Object} start\n     * @param {DateTime|Date|Object} end\n     * @return {Interval}\n     */;\n    Interval.fromDateTimes = function fromDateTimes(start, end) {\n      var builtStart = friendlyDateTime(start),\n        builtEnd = friendlyDateTime(end);\n      var validateError = validateStartEnd(builtStart, builtEnd);\n      if (validateError == null) {\n        return new Interval({\n          start: builtStart,\n          end: builtEnd\n        });\n      } else {\n        return validateError;\n      }\n    }\n\n    /**\n     * Create an Interval from a start DateTime and a Duration to extend to.\n     * @param {DateTime|Date|Object} start\n     * @param {Duration|Object|number} duration - the length of the Interval.\n     * @return {Interval}\n     */;\n    Interval.after = function after(start, duration) {\n      var dur = Duration.fromDurationLike(duration),\n        dt = friendlyDateTime(start);\n      return Interval.fromDateTimes(dt, dt.plus(dur));\n    }\n\n    /**\n     * Create an Interval from an end DateTime and a Duration to extend backwards to.\n     * @param {DateTime|Date|Object} end\n     * @param {Duration|Object|number} duration - the length of the Interval.\n     * @return {Interval}\n     */;\n    Interval.before = function before(end, duration) {\n      var dur = Duration.fromDurationLike(duration),\n        dt = friendlyDateTime(end);\n      return Interval.fromDateTimes(dt.minus(dur), dt);\n    }\n\n    /**\n     * Create an Interval from an ISO 8601 string.\n     * Accepts `<start>/<end>`, `<start>/<duration>`, and `<duration>/<end>` formats.\n     * @param {string} text - the ISO string to parse\n     * @param {Object} [opts] - options to pass {@link DateTime#fromISO} and optionally {@link Duration#fromISO}\n     * @see https://en.wikipedia.org/wiki/ISO_8601#Time_intervals\n     * @return {Interval}\n     */;\n    Interval.fromISO = function fromISO(text, opts) {\n      var _split = (text || \"\").split(\"/\", 2),\n        s = _split[0],\n        e = _split[1];\n      if (s && e) {\n        var start, startIsValid;\n        try {\n          start = DateTime.fromISO(s, opts);\n          startIsValid = start.isValid;\n        } catch (e) {\n          startIsValid = false;\n        }\n        var end, endIsValid;\n        try {\n          end = DateTime.fromISO(e, opts);\n          endIsValid = end.isValid;\n        } catch (e) {\n          endIsValid = false;\n        }\n        if (startIsValid && endIsValid) {\n          return Interval.fromDateTimes(start, end);\n        }\n        if (startIsValid) {\n          var dur = Duration.fromISO(e, opts);\n          if (dur.isValid) {\n            return Interval.after(start, dur);\n          }\n        } else if (endIsValid) {\n          var _dur = Duration.fromISO(s, opts);\n          if (_dur.isValid) {\n            return Interval.before(end, _dur);\n          }\n        }\n      }\n      return Interval.invalid(\"unparsable\", \"the input \\\"\" + text + \"\\\" can't be parsed as ISO 8601\");\n    }\n\n    /**\n     * Check if an object is an Interval. Works across context boundaries\n     * @param {object} o\n     * @return {boolean}\n     */;\n    Interval.isInterval = function isInterval(o) {\n      return o && o.isLuxonInterval || false;\n    }\n\n    /**\n     * Returns the start of the Interval\n     * @type {DateTime}\n     */;\n    var _proto = Interval.prototype;\n    /**\n     * Returns the length of the Interval in the specified unit.\n     * @param {string} unit - the unit (such as 'hours' or 'days') to return the length in.\n     * @return {number}\n     */\n    _proto.length = function length(unit) {\n      if (unit === void 0) {\n        unit = \"milliseconds\";\n      }\n      return this.isValid ? this.toDuration.apply(this, [unit]).get(unit) : NaN;\n    }\n\n    /**\n     * Returns the count of minutes, hours, days, months, or years included in the Interval, even in part.\n     * Unlike {@link Interval#length} this counts sections of the calendar, not periods of time, e.g. specifying 'day'\n     * asks 'what dates are included in this interval?', not 'how many days long is this interval?'\n     * @param {string} [unit='milliseconds'] - the unit of time to count.\n     * @param {Object} opts - options\n     * @param {boolean} [opts.useLocaleWeeks=false] - If true, use weeks based on the locale, i.e. use the locale-dependent start of the week; this operation will always use the locale of the start DateTime\n     * @return {number}\n     */;\n    _proto.count = function count(unit, opts) {\n      if (unit === void 0) {\n        unit = \"milliseconds\";\n      }\n      if (!this.isValid) return NaN;\n      var start = this.start.startOf(unit, opts);\n      var end;\n      if (opts != null && opts.useLocaleWeeks) {\n        end = this.end.reconfigure({\n          locale: start.locale\n        });\n      } else {\n        end = this.end;\n      }\n      end = end.startOf(unit, opts);\n      return Math.floor(end.diff(start, unit).get(unit)) + (end.valueOf() !== this.end.valueOf());\n    }\n\n    /**\n     * Returns whether this Interval's start and end are both in the same unit of time\n     * @param {string} unit - the unit of time to check sameness on\n     * @return {boolean}\n     */;\n    _proto.hasSame = function hasSame(unit) {\n      return this.isValid ? this.isEmpty() || this.e.minus(1).hasSame(this.s, unit) : false;\n    }\n\n    /**\n     * Return whether this Interval has the same start and end DateTimes.\n     * @return {boolean}\n     */;\n    _proto.isEmpty = function isEmpty() {\n      return this.s.valueOf() === this.e.valueOf();\n    }\n\n    /**\n     * Return whether this Interval's start is after the specified DateTime.\n     * @param {DateTime} dateTime\n     * @return {boolean}\n     */;\n    _proto.isAfter = function isAfter(dateTime) {\n      if (!this.isValid) return false;\n      return this.s > dateTime;\n    }\n\n    /**\n     * Return whether this Interval's end is before the specified DateTime.\n     * @param {DateTime} dateTime\n     * @return {boolean}\n     */;\n    _proto.isBefore = function isBefore(dateTime) {\n      if (!this.isValid) return false;\n      return this.e <= dateTime;\n    }\n\n    /**\n     * Return whether this Interval contains the specified DateTime.\n     * @param {DateTime} dateTime\n     * @return {boolean}\n     */;\n    _proto.contains = function contains(dateTime) {\n      if (!this.isValid) return false;\n      return this.s <= dateTime && this.e > dateTime;\n    }\n\n    /**\n     * \"Sets\" the start and/or end dates. Returns a newly-constructed Interval.\n     * @param {Object} values - the values to set\n     * @param {DateTime} values.start - the starting DateTime\n     * @param {DateTime} values.end - the ending DateTime\n     * @return {Interval}\n     */;\n    _proto.set = function set(_temp) {\n      var _ref = _temp === void 0 ? {} : _temp,\n        start = _ref.start,\n        end = _ref.end;\n      if (!this.isValid) return this;\n      return Interval.fromDateTimes(start || this.s, end || this.e);\n    }\n\n    /**\n     * Split this Interval at each of the specified DateTimes\n     * @param {...DateTime} dateTimes - the unit of time to count.\n     * @return {Array}\n     */;\n    _proto.splitAt = function splitAt() {\n      var _this = this;\n      if (!this.isValid) return [];\n      for (var _len = arguments.length, dateTimes = new Array(_len), _key = 0; _key < _len; _key++) {\n        dateTimes[_key] = arguments[_key];\n      }\n      var sorted = dateTimes.map(friendlyDateTime).filter(function (d) {\n          return _this.contains(d);\n        }).sort(function (a, b) {\n          return a.toMillis() - b.toMillis();\n        }),\n        results = [];\n      var s = this.s,\n        i = 0;\n      while (s < this.e) {\n        var added = sorted[i] || this.e,\n          next = +added > +this.e ? this.e : added;\n        results.push(Interval.fromDateTimes(s, next));\n        s = next;\n        i += 1;\n      }\n      return results;\n    }\n\n    /**\n     * Split this Interval into smaller Intervals, each of the specified length.\n     * Left over time is grouped into a smaller interval\n     * @param {Duration|Object|number} duration - The length of each resulting interval.\n     * @return {Array}\n     */;\n    _proto.splitBy = function splitBy(duration) {\n      var dur = Duration.fromDurationLike(duration);\n      if (!this.isValid || !dur.isValid || dur.as(\"milliseconds\") === 0) {\n        return [];\n      }\n      var s = this.s,\n        idx = 1,\n        next;\n      var results = [];\n      while (s < this.e) {\n        var added = this.start.plus(dur.mapUnits(function (x) {\n          return x * idx;\n        }));\n        next = +added > +this.e ? this.e : added;\n        results.push(Interval.fromDateTimes(s, next));\n        s = next;\n        idx += 1;\n      }\n      return results;\n    }\n\n    /**\n     * Split this Interval into the specified number of smaller intervals.\n     * @param {number} numberOfParts - The number of Intervals to divide the Interval into.\n     * @return {Array}\n     */;\n    _proto.divideEqually = function divideEqually(numberOfParts) {\n      if (!this.isValid) return [];\n      return this.splitBy(this.length() / numberOfParts).slice(0, numberOfParts);\n    }\n\n    /**\n     * Return whether this Interval overlaps with the specified Interval\n     * @param {Interval} other\n     * @return {boolean}\n     */;\n    _proto.overlaps = function overlaps(other) {\n      return this.e > other.s && this.s < other.e;\n    }\n\n    /**\n     * Return whether this Interval's end is adjacent to the specified Interval's start.\n     * @param {Interval} other\n     * @return {boolean}\n     */;\n    _proto.abutsStart = function abutsStart(other) {\n      if (!this.isValid) return false;\n      return +this.e === +other.s;\n    }\n\n    /**\n     * Return whether this Interval's start is adjacent to the specified Interval's end.\n     * @param {Interval} other\n     * @return {boolean}\n     */;\n    _proto.abutsEnd = function abutsEnd(other) {\n      if (!this.isValid) return false;\n      return +other.e === +this.s;\n    }\n\n    /**\n     * Returns true if this Interval fully contains the specified Interval, specifically if the intersect (of this Interval and the other Interval) is equal to the other Interval; false otherwise.\n     * @param {Interval} other\n     * @return {boolean}\n     */;\n    _proto.engulfs = function engulfs(other) {\n      if (!this.isValid) return false;\n      return this.s <= other.s && this.e >= other.e;\n    }\n\n    /**\n     * Return whether this Interval has the same start and end as the specified Interval.\n     * @param {Interval} other\n     * @return {boolean}\n     */;\n    _proto.equals = function equals(other) {\n      if (!this.isValid || !other.isValid) {\n        return false;\n      }\n      return this.s.equals(other.s) && this.e.equals(other.e);\n    }\n\n    /**\n     * Return an Interval representing the intersection of this Interval and the specified Interval.\n     * Specifically, the resulting Interval has the maximum start time and the minimum end time of the two Intervals.\n     * Returns null if the intersection is empty, meaning, the intervals don't intersect.\n     * @param {Interval} other\n     * @return {Interval}\n     */;\n    _proto.intersection = function intersection(other) {\n      if (!this.isValid) return this;\n      var s = this.s > other.s ? this.s : other.s,\n        e = this.e < other.e ? this.e : other.e;\n      if (s >= e) {\n        return null;\n      } else {\n        return Interval.fromDateTimes(s, e);\n      }\n    }\n\n    /**\n     * Return an Interval representing the union of this Interval and the specified Interval.\n     * Specifically, the resulting Interval has the minimum start time and the maximum end time of the two Intervals.\n     * @param {Interval} other\n     * @return {Interval}\n     */;\n    _proto.union = function union(other) {\n      if (!this.isValid) return this;\n      var s = this.s < other.s ? this.s : other.s,\n        e = this.e > other.e ? this.e : other.e;\n      return Interval.fromDateTimes(s, e);\n    }\n\n    /**\n     * Merge an array of Intervals into a equivalent minimal set of Intervals.\n     * Combines overlapping and adjacent Intervals.\n     * @param {Array} intervals\n     * @return {Array}\n     */;\n    Interval.merge = function merge(intervals) {\n      var _intervals$sort$reduc = intervals.sort(function (a, b) {\n          return a.s - b.s;\n        }).reduce(function (_ref2, item) {\n          var sofar = _ref2[0],\n            current = _ref2[1];\n          if (!current) {\n            return [sofar, item];\n          } else if (current.overlaps(item) || current.abutsStart(item)) {\n            return [sofar, current.union(item)];\n          } else {\n            return [sofar.concat([current]), item];\n          }\n        }, [[], null]),\n        found = _intervals$sort$reduc[0],\n        final = _intervals$sort$reduc[1];\n      if (final) {\n        found.push(final);\n      }\n      return found;\n    }\n\n    /**\n     * Return an array of Intervals representing the spans of time that only appear in one of the specified Intervals.\n     * @param {Array} intervals\n     * @return {Array}\n     */;\n    Interval.xor = function xor(intervals) {\n      var _Array$prototype;\n      var start = null,\n        currentCount = 0;\n      var results = [],\n        ends = intervals.map(function (i) {\n          return [{\n            time: i.s,\n            type: \"s\"\n          }, {\n            time: i.e,\n            type: \"e\"\n          }];\n        }),\n        flattened = (_Array$prototype = Array.prototype).concat.apply(_Array$prototype, ends),\n        arr = flattened.sort(function (a, b) {\n          return a.time - b.time;\n        });\n      for (var _iterator = _createForOfIteratorHelperLoose(arr), _step; !(_step = _iterator()).done;) {\n        var i = _step.value;\n        currentCount += i.type === \"s\" ? 1 : -1;\n        if (currentCount === 1) {\n          start = i.time;\n        } else {\n          if (start && +start !== +i.time) {\n            results.push(Interval.fromDateTimes(start, i.time));\n          }\n          start = null;\n        }\n      }\n      return Interval.merge(results);\n    }\n\n    /**\n     * Return an Interval representing the span of time in this Interval that doesn't overlap with any of the specified Intervals.\n     * @param {...Interval} intervals\n     * @return {Array}\n     */;\n    _proto.difference = function difference() {\n      var _this2 = this;\n      for (var _len2 = arguments.length, intervals = new Array(_len2), _key2 = 0; _key2 < _len2; _key2++) {\n        intervals[_key2] = arguments[_key2];\n      }\n      return Interval.xor([this].concat(intervals)).map(function (i) {\n        return _this2.intersection(i);\n      }).filter(function (i) {\n        return i && !i.isEmpty();\n      });\n    }\n\n    /**\n     * Returns a string representation of this Interval appropriate for debugging.\n     * @return {string}\n     */;\n    _proto.toString = function toString() {\n      if (!this.isValid) return INVALID$1;\n      return \"[\" + this.s.toISO() + \" \\u2013 \" + this.e.toISO() + \")\";\n    }\n\n    /**\n     * Returns a string representation of this Interval appropriate for the REPL.\n     * @return {string}\n     */;\n    _proto[_Symbol$for] = function () {\n      if (this.isValid) {\n        return \"Interval { start: \" + this.s.toISO() + \", end: \" + this.e.toISO() + \" }\";\n      } else {\n        return \"Interval { Invalid, reason: \" + this.invalidReason + \" }\";\n      }\n    }\n\n    /**\n     * Returns a localized string representing this Interval. Accepts the same options as the\n     * Intl.DateTimeFormat constructor and any presets defined by Luxon, such as\n     * {@link DateTime.DATE_FULL} or {@link DateTime.TIME_SIMPLE}. The exact behavior of this method\n     * is browser-specific, but in general it will return an appropriate representation of the\n     * Interval in the assigned locale. Defaults to the system's locale if no locale has been\n     * specified.\n     * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DateTimeFormat\n     * @param {Object} [formatOpts=DateTime.DATE_SHORT] - Either a DateTime preset or\n     * Intl.DateTimeFormat constructor options.\n     * @param {Object} opts - Options to override the configuration of the start DateTime.\n     * @example Interval.fromISO('2022-11-07T09:00Z/2022-11-08T09:00Z').toLocaleString(); //=> 11/7/2022 \u2013 11/8/2022\n     * @example Interval.fromISO('2022-11-07T09:00Z/2022-11-08T09:00Z').toLocaleString(DateTime.DATE_FULL); //=> November 7 \u2013 8, 2022\n     * @example Interval.fromISO('2022-11-07T09:00Z/2022-11-08T09:00Z').toLocaleString(DateTime.DATE_FULL, { locale: 'fr-FR' }); //=> 7\u20138 novembre 2022\n     * @example Interval.fromISO('2022-11-07T17:00Z/2022-11-07T19:00Z').toLocaleString(DateTime.TIME_SIMPLE); //=> 6:00 \u2013 8:00 PM\n     * @example Interval.fromISO('2022-11-07T17:00Z/2022-11-07T19:00Z').toLocaleString({ weekday: 'short', month: 'short', day: '2-digit', hour: '2-digit', minute: '2-digit' }); //=> Mon, Nov 07, 6:00 \u2013 8:00 p\n     * @return {string}\n     */;\n    _proto.toLocaleString = function toLocaleString(formatOpts, opts) {\n      if (formatOpts === void 0) {\n        formatOpts = DATE_SHORT;\n      }\n      if (opts === void 0) {\n        opts = {};\n      }\n      return this.isValid ? Formatter.create(this.s.loc.clone(opts), formatOpts).formatInterval(this) : INVALID$1;\n    }\n\n    /**\n     * Returns an ISO 8601-compliant string representation of this Interval.\n     * @see https://en.wikipedia.org/wiki/ISO_8601#Time_intervals\n     * @param {Object} opts - The same options as {@link DateTime#toISO}\n     * @return {string}\n     */;\n    _proto.toISO = function toISO(opts) {\n      if (!this.isValid) return INVALID$1;\n      return this.s.toISO(opts) + \"/\" + this.e.toISO(opts);\n    }\n\n    /**\n     * Returns an ISO 8601-compliant string representation of date of this Interval.\n     * The time components are ignored.\n     * @see https://en.wikipedia.org/wiki/ISO_8601#Time_intervals\n     * @return {string}\n     */;\n    _proto.toISODate = function toISODate() {\n      if (!this.isValid) return INVALID$1;\n      return this.s.toISODate() + \"/\" + this.e.toISODate();\n    }\n\n    /**\n     * Returns an ISO 8601-compliant string representation of time of this Interval.\n     * The date components are ignored.\n     * @see https://en.wikipedia.org/wiki/ISO_8601#Time_intervals\n     * @param {Object} opts - The same options as {@link DateTime#toISO}\n     * @return {string}\n     */;\n    _proto.toISOTime = function toISOTime(opts) {\n      if (!this.isValid) return INVALID$1;\n      return this.s.toISOTime(opts) + \"/\" + this.e.toISOTime(opts);\n    }\n\n    /**\n     * Returns a string representation of this Interval formatted according to the specified format\n     * string. **You may not want this.** See {@link Interval#toLocaleString} for a more flexible\n     * formatting tool.\n     * @param {string} dateFormat - The format string. This string formats the start and end time.\n     * See {@link DateTime#toFormat} for details.\n     * @param {Object} opts - Options.\n     * @param {string} [opts.separator =  ' \u2013 '] - A separator to place between the start and end\n     * representations.\n     * @return {string}\n     */;\n    _proto.toFormat = function toFormat(dateFormat, _temp2) {\n      var _ref3 = _temp2 === void 0 ? {} : _temp2,\n        _ref3$separator = _ref3.separator,\n        separator = _ref3$separator === void 0 ? \" \u2013 \" : _ref3$separator;\n      if (!this.isValid) return INVALID$1;\n      return \"\" + this.s.toFormat(dateFormat) + separator + this.e.toFormat(dateFormat);\n    }\n\n    /**\n     * Return a Duration representing the time spanned by this interval.\n     * @param {string|string[]} [unit=['milliseconds']] - the unit or units (such as 'hours' or 'days') to include in the duration.\n     * @param {Object} opts - options that affect the creation of the Duration\n     * @param {string} [opts.conversionAccuracy='casual'] - the conversion system to use\n     * @example Interval.fromDateTimes(dt1, dt2).toDuration().toObject() //=> { milliseconds: 88489257 }\n     * @example Interval.fromDateTimes(dt1, dt2).toDuration('days').toObject() //=> { days: 1.0241812152777778 }\n     * @example Interval.fromDateTimes(dt1, dt2).toDuration(['hours', 'minutes']).toObject() //=> { hours: 24, minutes: 34.82095 }\n     * @example Interval.fromDateTimes(dt1, dt2).toDuration(['hours', 'minutes', 'seconds']).toObject() //=> { hours: 24, minutes: 34, seconds: 49.257 }\n     * @example Interval.fromDateTimes(dt1, dt2).toDuration('seconds').toObject() //=> { seconds: 88489.257 }\n     * @return {Duration}\n     */;\n    _proto.toDuration = function toDuration(unit, opts) {\n      if (!this.isValid) {\n        return Duration.invalid(this.invalidReason);\n      }\n      return this.e.diff(this.s, unit, opts);\n    }\n\n    /**\n     * Run mapFn on the interval start and end, returning a new Interval from the resulting DateTimes\n     * @param {function} mapFn\n     * @return {Interval}\n     * @example Interval.fromDateTimes(dt1, dt2).mapEndpoints(endpoint => endpoint.toUTC())\n     * @example Interval.fromDateTimes(dt1, dt2).mapEndpoints(endpoint => endpoint.plus({ hours: 2 }))\n     */;\n    _proto.mapEndpoints = function mapEndpoints(mapFn) {\n      return Interval.fromDateTimes(mapFn(this.s), mapFn(this.e));\n    };\n    _createClass(Interval, [{\n      key: \"start\",\n      get: function get() {\n        return this.isValid ? this.s : null;\n      }\n\n      /**\n       * Returns the end of the Interval\n       * @type {DateTime}\n       */\n    }, {\n      key: \"end\",\n      get: function get() {\n        return this.isValid ? this.e : null;\n      }\n\n      /**\n       * Returns whether this Interval's end is at least its start, meaning that the Interval isn't 'backwards'.\n       * @type {boolean}\n       */\n    }, {\n      key: \"isValid\",\n      get: function get() {\n        return this.invalidReason === null;\n      }\n\n      /**\n       * Returns an error code if this Interval is invalid, or null if the Interval is valid\n       * @type {string}\n       */\n    }, {\n      key: \"invalidReason\",\n      get: function get() {\n        return this.invalid ? this.invalid.reason : null;\n      }\n\n      /**\n       * Returns an explanation of why this Interval became invalid, or null if the Interval is valid\n       * @type {string}\n       */\n    }, {\n      key: \"invalidExplanation\",\n      get: function get() {\n        return this.invalid ? this.invalid.explanation : null;\n      }\n    }]);\n    return Interval;\n  }(Symbol.for(\"nodejs.util.inspect.custom\"));\n\n  /**\n   * The Info class contains static methods for retrieving general time and date related data. For example, it has methods for finding out if a time zone has a DST, for listing the months in any supported locale, and for discovering which of Luxon features are available in the current environment.\n   */\n  var Info = /*#__PURE__*/function () {\n    function Info() {}\n    /**\n     * Return whether the specified zone contains a DST.\n     * @param {string|Zone} [zone='local'] - Zone to check. Defaults to the environment's local zone.\n     * @return {boolean}\n     */\n    Info.hasDST = function hasDST(zone) {\n      if (zone === void 0) {\n        zone = Settings.defaultZone;\n      }\n      var proto = DateTime.now().setZone(zone).set({\n        month: 12\n      });\n      return !zone.isUniversal && proto.offset !== proto.set({\n        month: 6\n      }).offset;\n    }\n\n    /**\n     * Return whether the specified zone is a valid IANA specifier.\n     * @param {string} zone - Zone to check\n     * @return {boolean}\n     */;\n    Info.isValidIANAZone = function isValidIANAZone(zone) {\n      return IANAZone.isValidZone(zone);\n    }\n\n    /**\n     * Converts the input into a {@link Zone} instance.\n     *\n     * * If `input` is already a Zone instance, it is returned unchanged.\n     * * If `input` is a string containing a valid time zone name, a Zone instance\n     *   with that name is returned.\n     * * If `input` is a string that doesn't refer to a known time zone, a Zone\n     *   instance with {@link Zone#isValid} == false is returned.\n     * * If `input is a number, a Zone instance with the specified fixed offset\n     *   in minutes is returned.\n     * * If `input` is `null` or `undefined`, the default zone is returned.\n     * @param {string|Zone|number} [input] - the value to be converted\n     * @return {Zone}\n     */;\n    Info.normalizeZone = function normalizeZone$1(input) {\n      return normalizeZone(input, Settings.defaultZone);\n    }\n\n    /**\n     * Get the weekday on which the week starts according to the given locale.\n     * @param {Object} opts - options\n     * @param {string} [opts.locale] - the locale code\n     * @param {string} [opts.locObj=null] - an existing locale object to use\n     * @returns {number} the start of the week, 1 for Monday through 7 for Sunday\n     */;\n    Info.getStartOfWeek = function getStartOfWeek(_temp) {\n      var _ref = _temp === void 0 ? {} : _temp,\n        _ref$locale = _ref.locale,\n        locale = _ref$locale === void 0 ? null : _ref$locale,\n        _ref$locObj = _ref.locObj,\n        locObj = _ref$locObj === void 0 ? null : _ref$locObj;\n      return (locObj || Locale.create(locale)).getStartOfWeek();\n    }\n\n    /**\n     * Get the minimum number of days necessary in a week before it is considered part of the next year according\n     * to the given locale.\n     * @param {Object} opts - options\n     * @param {string} [opts.locale] - the locale code\n     * @param {string} [opts.locObj=null] - an existing locale object to use\n     * @returns {number}\n     */;\n    Info.getMinimumDaysInFirstWeek = function getMinimumDaysInFirstWeek(_temp2) {\n      var _ref2 = _temp2 === void 0 ? {} : _temp2,\n        _ref2$locale = _ref2.locale,\n        locale = _ref2$locale === void 0 ? null : _ref2$locale,\n        _ref2$locObj = _ref2.locObj,\n        locObj = _ref2$locObj === void 0 ? null : _ref2$locObj;\n      return (locObj || Locale.create(locale)).getMinDaysInFirstWeek();\n    }\n\n    /**\n     * Get the weekdays, which are considered the weekend according to the given locale\n     * @param {Object} opts - options\n     * @param {string} [opts.locale] - the locale code\n     * @param {string} [opts.locObj=null] - an existing locale object to use\n     * @returns {number[]} an array of weekdays, 1 for Monday through 7 for Sunday\n     */;\n    Info.getWeekendWeekdays = function getWeekendWeekdays(_temp3) {\n      var _ref3 = _temp3 === void 0 ? {} : _temp3,\n        _ref3$locale = _ref3.locale,\n        locale = _ref3$locale === void 0 ? null : _ref3$locale,\n        _ref3$locObj = _ref3.locObj,\n        locObj = _ref3$locObj === void 0 ? null : _ref3$locObj;\n      // copy the array, because we cache it internally\n      return (locObj || Locale.create(locale)).getWeekendDays().slice();\n    }\n\n    /**\n     * Return an array of standalone month names.\n     * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DateTimeFormat\n     * @param {string} [length='long'] - the length of the month representation, such as \"numeric\", \"2-digit\", \"narrow\", \"short\", \"long\"\n     * @param {Object} opts - options\n     * @param {string} [opts.locale] - the locale code\n     * @param {string} [opts.numberingSystem=null] - the numbering system\n     * @param {string} [opts.locObj=null] - an existing locale object to use\n     * @param {string} [opts.outputCalendar='gregory'] - the calendar\n     * @example Info.months()[0] //=> 'January'\n     * @example Info.months('short')[0] //=> 'Jan'\n     * @example Info.months('numeric')[0] //=> '1'\n     * @example Info.months('short', { locale: 'fr-CA' } )[0] //=> 'janv.'\n     * @example Info.months('numeric', { locale: 'ar' })[0] //=> '\u0661'\n     * @example Info.months('long', { outputCalendar: 'islamic' })[0] //=> 'Rabi\u02bb I'\n     * @return {Array}\n     */;\n    Info.months = function months(length, _temp4) {\n      if (length === void 0) {\n        length = \"long\";\n      }\n      var _ref4 = _temp4 === void 0 ? {} : _temp4,\n        _ref4$locale = _ref4.locale,\n        locale = _ref4$locale === void 0 ? null : _ref4$locale,\n        _ref4$numberingSystem = _ref4.numberingSystem,\n        numberingSystem = _ref4$numberingSystem === void 0 ? null : _ref4$numberingSystem,\n        _ref4$locObj = _ref4.locObj,\n        locObj = _ref4$locObj === void 0 ? null : _ref4$locObj,\n        _ref4$outputCalendar = _ref4.outputCalendar,\n        outputCalendar = _ref4$outputCalendar === void 0 ? \"gregory\" : _ref4$outputCalendar;\n      return (locObj || Locale.create(locale, numberingSystem, outputCalendar)).months(length);\n    }\n\n    /**\n     * Return an array of format month names.\n     * Format months differ from standalone months in that they're meant to appear next to the day of the month. In some languages, that\n     * changes the string.\n     * See {@link Info#months}\n     * @param {string} [length='long'] - the length of the month representation, such as \"numeric\", \"2-digit\", \"narrow\", \"short\", \"long\"\n     * @param {Object} opts - options\n     * @param {string} [opts.locale] - the locale code\n     * @param {string} [opts.numberingSystem=null] - the numbering system\n     * @param {string} [opts.locObj=null] - an existing locale object to use\n     * @param {string} [opts.outputCalendar='gregory'] - the calendar\n     * @return {Array}\n     */;\n    Info.monthsFormat = function monthsFormat(length, _temp5) {\n      if (length === void 0) {\n        length = \"long\";\n      }\n      var _ref5 = _temp5 === void 0 ? {} : _temp5,\n        _ref5$locale = _ref5.locale,\n        locale = _ref5$locale === void 0 ? null : _ref5$locale,\n        _ref5$numberingSystem = _ref5.numberingSystem,\n        numberingSystem = _ref5$numberingSystem === void 0 ? null : _ref5$numberingSystem,\n        _ref5$locObj = _ref5.locObj,\n        locObj = _ref5$locObj === void 0 ? null : _ref5$locObj,\n        _ref5$outputCalendar = _ref5.outputCalendar,\n        outputCalendar = _ref5$outputCalendar === void 0 ? \"gregory\" : _ref5$outputCalendar;\n      return (locObj || Locale.create(locale, numberingSystem, outputCalendar)).months(length, true);\n    }\n\n    /**\n     * Return an array of standalone week names.\n     * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DateTimeFormat\n     * @param {string} [length='long'] - the length of the weekday representation, such as \"narrow\", \"short\", \"long\".\n     * @param {Object} opts - options\n     * @param {string} [opts.locale] - the locale code\n     * @param {string} [opts.numberingSystem=null] - the numbering system\n     * @param {string} [opts.locObj=null] - an existing locale object to use\n     * @example Info.weekdays()[0] //=> 'Monday'\n     * @example Info.weekdays('short')[0] //=> 'Mon'\n     * @example Info.weekdays('short', { locale: 'fr-CA' })[0] //=> 'lun.'\n     * @example Info.weekdays('short', { locale: 'ar' })[0] //=> '\u0627\u0644\u0627\u062b\u0646\u064a\u0646'\n     * @return {Array}\n     */;\n    Info.weekdays = function weekdays(length, _temp6) {\n      if (length === void 0) {\n        length = \"long\";\n      }\n      var _ref6 = _temp6 === void 0 ? {} : _temp6,\n        _ref6$locale = _ref6.locale,\n        locale = _ref6$locale === void 0 ? null : _ref6$locale,\n        _ref6$numberingSystem = _ref6.numberingSystem,\n        numberingSystem = _ref6$numberingSystem === void 0 ? null : _ref6$numberingSystem,\n        _ref6$locObj = _ref6.locObj,\n        locObj = _ref6$locObj === void 0 ? null : _ref6$locObj;\n      return (locObj || Locale.create(locale, numberingSystem, null)).weekdays(length);\n    }\n\n    /**\n     * Return an array of format week names.\n     * Format weekdays differ from standalone weekdays in that they're meant to appear next to more date information. In some languages, that\n     * changes the string.\n     * See {@link Info#weekdays}\n     * @param {string} [length='long'] - the length of the month representation, such as \"narrow\", \"short\", \"long\".\n     * @param {Object} opts - options\n     * @param {string} [opts.locale=null] - the locale code\n     * @param {string} [opts.numberingSystem=null] - the numbering system\n     * @param {string} [opts.locObj=null] - an existing locale object to use\n     * @return {Array}\n     */;\n    Info.weekdaysFormat = function weekdaysFormat(length, _temp7) {\n      if (length === void 0) {\n        length = \"long\";\n      }\n      var _ref7 = _temp7 === void 0 ? {} : _temp7,\n        _ref7$locale = _ref7.locale,\n        locale = _ref7$locale === void 0 ? null : _ref7$locale,\n        _ref7$numberingSystem = _ref7.numberingSystem,\n        numberingSystem = _ref7$numberingSystem === void 0 ? null : _ref7$numberingSystem,\n        _ref7$locObj = _ref7.locObj,\n        locObj = _ref7$locObj === void 0 ? null : _ref7$locObj;\n      return (locObj || Locale.create(locale, numberingSystem, null)).weekdays(length, true);\n    }\n\n    /**\n     * Return an array of meridiems.\n     * @param {Object} opts - options\n     * @param {string} [opts.locale] - the locale code\n     * @example Info.meridiems() //=> [ 'AM', 'PM' ]\n     * @example Info.meridiems({ locale: 'my' }) //=> [ '\u1014\u1036\u1014\u1000\u103a', '\u100a\u1014\u1031' ]\n     * @return {Array}\n     */;\n    Info.meridiems = function meridiems(_temp8) {\n      var _ref8 = _temp8 === void 0 ? {} : _temp8,\n        _ref8$locale = _ref8.locale,\n        locale = _ref8$locale === void 0 ? null : _ref8$locale;\n      return Locale.create(locale).meridiems();\n    }\n\n    /**\n     * Return an array of eras, such as ['BC', 'AD']. The locale can be specified, but the calendar system is always Gregorian.\n     * @param {string} [length='short'] - the length of the era representation, such as \"short\" or \"long\".\n     * @param {Object} opts - options\n     * @param {string} [opts.locale] - the locale code\n     * @example Info.eras() //=> [ 'BC', 'AD' ]\n     * @example Info.eras('long') //=> [ 'Before Christ', 'Anno Domini' ]\n     * @example Info.eras('long', { locale: 'fr' }) //=> [ 'avant J\u00e9sus-Christ', 'apr\u00e8s J\u00e9sus-Christ' ]\n     * @return {Array}\n     */;\n    Info.eras = function eras(length, _temp9) {\n      if (length === void 0) {\n        length = \"short\";\n      }\n      var _ref9 = _temp9 === void 0 ? {} : _temp9,\n        _ref9$locale = _ref9.locale,\n        locale = _ref9$locale === void 0 ? null : _ref9$locale;\n      return Locale.create(locale, null, \"gregory\").eras(length);\n    }\n\n    /**\n     * Return the set of available features in this environment.\n     * Some features of Luxon are not available in all environments. For example, on older browsers, relative time formatting support is not available. Use this function to figure out if that's the case.\n     * Keys:\n     * * `relative`: whether this environment supports relative time formatting\n     * * `localeWeek`: whether this environment supports different weekdays for the start of the week based on the locale\n     * @example Info.features() //=> { relative: false, localeWeek: true }\n     * @return {Object}\n     */;\n    Info.features = function features() {\n      return {\n        relative: hasRelative(),\n        localeWeek: hasLocaleWeekInfo()\n      };\n    };\n    return Info;\n  }();\n\n  function dayDiff(earlier, later) {\n    var utcDayStart = function utcDayStart(dt) {\n        return dt.toUTC(0, {\n          keepLocalTime: true\n        }).startOf(\"day\").valueOf();\n      },\n      ms = utcDayStart(later) - utcDayStart(earlier);\n    return Math.floor(Duration.fromMillis(ms).as(\"days\"));\n  }\n  function highOrderDiffs(cursor, later, units) {\n    var differs = [[\"years\", function (a, b) {\n      return b.year - a.year;\n    }], [\"quarters\", function (a, b) {\n      return b.quarter - a.quarter + (b.year - a.year) * 4;\n    }], [\"months\", function (a, b) {\n      return b.month - a.month + (b.year - a.year) * 12;\n    }], [\"weeks\", function (a, b) {\n      var days = dayDiff(a, b);\n      return (days - days % 7) / 7;\n    }], [\"days\", dayDiff]];\n    var results = {};\n    var earlier = cursor;\n    var lowestOrder, highWater;\n\n    /* This loop tries to diff using larger units first.\n       If we overshoot, we backtrack and try the next smaller unit.\n       \"cursor\" starts out at the earlier timestamp and moves closer and closer to \"later\"\n       as we use smaller and smaller units.\n       highWater keeps track of where we would be if we added one more of the smallest unit,\n       this is used later to potentially convert any difference smaller than the smallest higher order unit\n       into a fraction of that smallest higher order unit\n    */\n    for (var _i = 0, _differs = differs; _i < _differs.length; _i++) {\n      var _differs$_i = _differs[_i],\n        unit = _differs$_i[0],\n        differ = _differs$_i[1];\n      if (units.indexOf(unit) >= 0) {\n        lowestOrder = unit;\n        results[unit] = differ(cursor, later);\n        highWater = earlier.plus(results);\n        if (highWater > later) {\n          // we overshot the end point, backtrack cursor by 1\n          results[unit]--;\n          cursor = earlier.plus(results);\n\n          // if we are still overshooting now, we need to backtrack again\n          // this happens in certain situations when diffing times in different zones,\n          // because this calculation ignores time zones\n          if (cursor > later) {\n            // keep the \"overshot by 1\" around as highWater\n            highWater = cursor;\n            // backtrack cursor by 1\n            results[unit]--;\n            cursor = earlier.plus(results);\n          }\n        } else {\n          cursor = highWater;\n        }\n      }\n    }\n    return [cursor, results, highWater, lowestOrder];\n  }\n  function _diff (earlier, later, units, opts) {\n    var _highOrderDiffs = highOrderDiffs(earlier, later, units),\n      cursor = _highOrderDiffs[0],\n      results = _highOrderDiffs[1],\n      highWater = _highOrderDiffs[2],\n      lowestOrder = _highOrderDiffs[3];\n    var remainingMillis = later - cursor;\n    var lowerOrderUnits = units.filter(function (u) {\n      return [\"hours\", \"minutes\", \"seconds\", \"milliseconds\"].indexOf(u) >= 0;\n    });\n    if (lowerOrderUnits.length === 0) {\n      if (highWater < later) {\n        var _cursor$plus;\n        highWater = cursor.plus((_cursor$plus = {}, _cursor$plus[lowestOrder] = 1, _cursor$plus));\n      }\n      if (highWater !== cursor) {\n        results[lowestOrder] = (results[lowestOrder] || 0) + remainingMillis / (highWater - cursor);\n      }\n    }\n    var duration = Duration.fromObject(results, opts);\n    if (lowerOrderUnits.length > 0) {\n      var _Duration$fromMillis;\n      return (_Duration$fromMillis = Duration.fromMillis(remainingMillis, opts)).shiftTo.apply(_Duration$fromMillis, lowerOrderUnits).plus(duration);\n    } else {\n      return duration;\n    }\n  }\n\n  var MISSING_FTP = \"missing Intl.DateTimeFormat.formatToParts support\";\n  function intUnit(regex, post) {\n    if (post === void 0) {\n      post = function post(i) {\n        return i;\n      };\n    }\n    return {\n      regex: regex,\n      deser: function deser(_ref) {\n        var s = _ref[0];\n        return post(parseDigits(s));\n      }\n    };\n  }\n  var NBSP = String.fromCharCode(160);\n  var spaceOrNBSP = \"[ \" + NBSP + \"]\";\n  var spaceOrNBSPRegExp = new RegExp(spaceOrNBSP, \"g\");\n  function fixListRegex(s) {\n    // make dots optional and also make them literal\n    // make space and non breakable space characters interchangeable\n    return s.replace(/\\./g, \"\\\\.?\").replace(spaceOrNBSPRegExp, spaceOrNBSP);\n  }\n  function stripInsensitivities(s) {\n    return s.replace(/\\./g, \"\") // ignore dots that were made optional\n    .replace(spaceOrNBSPRegExp, \" \") // interchange space and nbsp\n    .toLowerCase();\n  }\n  function oneOf(strings, startIndex) {\n    if (strings === null) {\n      return null;\n    } else {\n      return {\n        regex: RegExp(strings.map(fixListRegex).join(\"|\")),\n        deser: function deser(_ref2) {\n          var s = _ref2[0];\n          return strings.findIndex(function (i) {\n            return stripInsensitivities(s) === stripInsensitivities(i);\n          }) + startIndex;\n        }\n      };\n    }\n  }\n  function offset(regex, groups) {\n    return {\n      regex: regex,\n      deser: function deser(_ref3) {\n        var h = _ref3[1],\n          m = _ref3[2];\n        return signedOffset(h, m);\n      },\n      groups: groups\n    };\n  }\n  function simple(regex) {\n    return {\n      regex: regex,\n      deser: function deser(_ref4) {\n        var s = _ref4[0];\n        return s;\n      }\n    };\n  }\n  function escapeToken(value) {\n    return value.replace(/[\\-\\[\\]{}()*+?.,\\\\\\^$|#\\s]/g, \"\\\\$&\");\n  }\n\n  /**\n   * @param token\n   * @param {Locale} loc\n   */\n  function unitForToken(token, loc) {\n    var one = digitRegex(loc),\n      two = digitRegex(loc, \"{2}\"),\n      three = digitRegex(loc, \"{3}\"),\n      four = digitRegex(loc, \"{4}\"),\n      six = digitRegex(loc, \"{6}\"),\n      oneOrTwo = digitRegex(loc, \"{1,2}\"),\n      oneToThree = digitRegex(loc, \"{1,3}\"),\n      oneToSix = digitRegex(loc, \"{1,6}\"),\n      oneToNine = digitRegex(loc, \"{1,9}\"),\n      twoToFour = digitRegex(loc, \"{2,4}\"),\n      fourToSix = digitRegex(loc, \"{4,6}\"),\n      literal = function literal(t) {\n        return {\n          regex: RegExp(escapeToken(t.val)),\n          deser: function deser(_ref5) {\n            var s = _ref5[0];\n            return s;\n          },\n          literal: true\n        };\n      },\n      unitate = function unitate(t) {\n        if (token.literal) {\n          return literal(t);\n        }\n        switch (t.val) {\n          // era\n          case \"G\":\n            return oneOf(loc.eras(\"short\"), 0);\n          case \"GG\":\n            return oneOf(loc.eras(\"long\"), 0);\n          // years\n          case \"y\":\n            return intUnit(oneToSix);\n          case \"yy\":\n            return intUnit(twoToFour, untruncateYear);\n          case \"yyyy\":\n            return intUnit(four);\n          case \"yyyyy\":\n            return intUnit(fourToSix);\n          case \"yyyyyy\":\n            return intUnit(six);\n          // months\n          case \"M\":\n            return intUnit(oneOrTwo);\n          case \"MM\":\n            return intUnit(two);\n          case \"MMM\":\n            return oneOf(loc.months(\"short\", true), 1);\n          case \"MMMM\":\n            return oneOf(loc.months(\"long\", true), 1);\n          case \"L\":\n            return intUnit(oneOrTwo);\n          case \"LL\":\n            return intUnit(two);\n          case \"LLL\":\n            return oneOf(loc.months(\"short\", false), 1);\n          case \"LLLL\":\n            return oneOf(loc.months(\"long\", false), 1);\n          // dates\n          case \"d\":\n            return intUnit(oneOrTwo);\n          case \"dd\":\n            return intUnit(two);\n          // ordinals\n          case \"o\":\n            return intUnit(oneToThree);\n          case \"ooo\":\n            return intUnit(three);\n          // time\n          case \"HH\":\n            return intUnit(two);\n          case \"H\":\n            return intUnit(oneOrTwo);\n          case \"hh\":\n            return intUnit(two);\n          case \"h\":\n            return intUnit(oneOrTwo);\n          case \"mm\":\n            return intUnit(two);\n          case \"m\":\n            return intUnit(oneOrTwo);\n          case \"q\":\n            return intUnit(oneOrTwo);\n          case \"qq\":\n            return intUnit(two);\n          case \"s\":\n            return intUnit(oneOrTwo);\n          case \"ss\":\n            return intUnit(two);\n          case \"S\":\n            return intUnit(oneToThree);\n          case \"SSS\":\n            return intUnit(three);\n          case \"u\":\n            return simple(oneToNine);\n          case \"uu\":\n            return simple(oneOrTwo);\n          case \"uuu\":\n            return intUnit(one);\n          // meridiem\n          case \"a\":\n            return oneOf(loc.meridiems(), 0);\n          // weekYear (k)\n          case \"kkkk\":\n            return intUnit(four);\n          case \"kk\":\n            return intUnit(twoToFour, untruncateYear);\n          // weekNumber (W)\n          case \"W\":\n            return intUnit(oneOrTwo);\n          case \"WW\":\n            return intUnit(two);\n          // weekdays\n          case \"E\":\n          case \"c\":\n            return intUnit(one);\n          case \"EEE\":\n            return oneOf(loc.weekdays(\"short\", false), 1);\n          case \"EEEE\":\n            return oneOf(loc.weekdays(\"long\", false), 1);\n          case \"ccc\":\n            return oneOf(loc.weekdays(\"short\", true), 1);\n          case \"cccc\":\n            return oneOf(loc.weekdays(\"long\", true), 1);\n          // offset/zone\n          case \"Z\":\n          case \"ZZ\":\n            return offset(new RegExp(\"([+-]\" + oneOrTwo.source + \")(?::(\" + two.source + \"))?\"), 2);\n          case \"ZZZ\":\n            return offset(new RegExp(\"([+-]\" + oneOrTwo.source + \")(\" + two.source + \")?\"), 2);\n          // we don't support ZZZZ (PST) or ZZZZZ (Pacific Standard Time) in parsing\n          // because we don't have any way to figure out what they are\n          case \"z\":\n            return simple(/[a-z_+-/]{1,256}?/i);\n          // this special-case \"token\" represents a place where a macro-token expanded into a white-space literal\n          // in this case we accept any non-newline white-space\n          case \" \":\n            return simple(/[^\\S\\n\\r]/);\n          default:\n            return literal(t);\n        }\n      };\n    var unit = unitate(token) || {\n      invalidReason: MISSING_FTP\n    };\n    unit.token = token;\n    return unit;\n  }\n  var partTypeStyleToTokenVal = {\n    year: {\n      \"2-digit\": \"yy\",\n      numeric: \"yyyyy\"\n    },\n    month: {\n      numeric: \"M\",\n      \"2-digit\": \"MM\",\n      short: \"MMM\",\n      long: \"MMMM\"\n    },\n    day: {\n      numeric: \"d\",\n      \"2-digit\": \"dd\"\n    },\n    weekday: {\n      short: \"EEE\",\n      long: \"EEEE\"\n    },\n    dayperiod: \"a\",\n    dayPeriod: \"a\",\n    hour12: {\n      numeric: \"h\",\n      \"2-digit\": \"hh\"\n    },\n    hour24: {\n      numeric: \"H\",\n      \"2-digit\": \"HH\"\n    },\n    minute: {\n      numeric: \"m\",\n      \"2-digit\": \"mm\"\n    },\n    second: {\n      numeric: \"s\",\n      \"2-digit\": \"ss\"\n    },\n    timeZoneName: {\n      long: \"ZZZZZ\",\n      short: \"ZZZ\"\n    }\n  };\n  function tokenForPart(part, formatOpts, resolvedOpts) {\n    var type = part.type,\n      value = part.value;\n    if (type === \"literal\") {\n      var isSpace = /^\\s+$/.test(value);\n      return {\n        literal: !isSpace,\n        val: isSpace ? \" \" : value\n      };\n    }\n    var style = formatOpts[type];\n\n    // The user might have explicitly specified hour12 or hourCycle\n    // if so, respect their decision\n    // if not, refer back to the resolvedOpts, which are based on the locale\n    var actualType = type;\n    if (type === \"hour\") {\n      if (formatOpts.hour12 != null) {\n        actualType = formatOpts.hour12 ? \"hour12\" : \"hour24\";\n      } else if (formatOpts.hourCycle != null) {\n        if (formatOpts.hourCycle === \"h11\" || formatOpts.hourCycle === \"h12\") {\n          actualType = \"hour12\";\n        } else {\n          actualType = \"hour24\";\n        }\n      } else {\n        // tokens only differentiate between 24 hours or not,\n        // so we do not need to check hourCycle here, which is less supported anyways\n        actualType = resolvedOpts.hour12 ? \"hour12\" : \"hour24\";\n      }\n    }\n    var val = partTypeStyleToTokenVal[actualType];\n    if (typeof val === \"object\") {\n      val = val[style];\n    }\n    if (val) {\n      return {\n        literal: false,\n        val: val\n      };\n    }\n    return undefined;\n  }\n  function buildRegex(units) {\n    var re = units.map(function (u) {\n      return u.regex;\n    }).reduce(function (f, r) {\n      return f + \"(\" + r.source + \")\";\n    }, \"\");\n    return [\"^\" + re + \"$\", units];\n  }\n  function match(input, regex, handlers) {\n    var matches = input.match(regex);\n    if (matches) {\n      var all = {};\n      var matchIndex = 1;\n      for (var i in handlers) {\n        if (hasOwnProperty(handlers, i)) {\n          var h = handlers[i],\n            groups = h.groups ? h.groups + 1 : 1;\n          if (!h.literal && h.token) {\n            all[h.token.val[0]] = h.deser(matches.slice(matchIndex, matchIndex + groups));\n          }\n          matchIndex += groups;\n        }\n      }\n      return [matches, all];\n    } else {\n      return [matches, {}];\n    }\n  }\n  function dateTimeFromMatches(matches) {\n    var toField = function toField(token) {\n      switch (token) {\n        case \"S\":\n          return \"millisecond\";\n        case \"s\":\n          return \"second\";\n        case \"m\":\n          return \"minute\";\n        case \"h\":\n        case \"H\":\n          return \"hour\";\n        case \"d\":\n          return \"day\";\n        case \"o\":\n          return \"ordinal\";\n        case \"L\":\n        case \"M\":\n          return \"month\";\n        case \"y\":\n          return \"year\";\n        case \"E\":\n        case \"c\":\n          return \"weekday\";\n        case \"W\":\n          return \"weekNumber\";\n        case \"k\":\n          return \"weekYear\";\n        case \"q\":\n          return \"quarter\";\n        default:\n          return null;\n      }\n    };\n    var zone = null;\n    var specificOffset;\n    if (!isUndefined(matches.z)) {\n      zone = IANAZone.create(matches.z);\n    }\n    if (!isUndefined(matches.Z)) {\n      if (!zone) {\n        zone = new FixedOffsetZone(matches.Z);\n      }\n      specificOffset = matches.Z;\n    }\n    if (!isUndefined(matches.q)) {\n      matches.M = (matches.q - 1) * 3 + 1;\n    }\n    if (!isUndefined(matches.h)) {\n      if (matches.h < 12 && matches.a === 1) {\n        matches.h += 12;\n      } else if (matches.h === 12 && matches.a === 0) {\n        matches.h = 0;\n      }\n    }\n    if (matches.G === 0 && matches.y) {\n      matches.y = -matches.y;\n    }\n    if (!isUndefined(matches.u)) {\n      matches.S = parseMillis(matches.u);\n    }\n    var vals = Object.keys(matches).reduce(function (r, k) {\n      var f = toField(k);\n      if (f) {\n        r[f] = matches[k];\n      }\n      return r;\n    }, {});\n    return [vals, zone, specificOffset];\n  }\n  var dummyDateTimeCache = null;\n  function getDummyDateTime() {\n    if (!dummyDateTimeCache) {\n      dummyDateTimeCache = DateTime.fromMillis(1555555555555);\n    }\n    return dummyDateTimeCache;\n  }\n  function maybeExpandMacroToken(token, locale) {\n    if (token.literal) {\n      return token;\n    }\n    var formatOpts = Formatter.macroTokenToFormatOpts(token.val);\n    var tokens = formatOptsToTokens(formatOpts, locale);\n    if (tokens == null || tokens.includes(undefined)) {\n      return token;\n    }\n    return tokens;\n  }\n  function expandMacroTokens(tokens, locale) {\n    var _Array$prototype;\n    return (_Array$prototype = Array.prototype).concat.apply(_Array$prototype, tokens.map(function (t) {\n      return maybeExpandMacroToken(t, locale);\n    }));\n  }\n\n  /**\n   * @private\n   */\n\n  var TokenParser = /*#__PURE__*/function () {\n    function TokenParser(locale, format) {\n      this.locale = locale;\n      this.format = format;\n      this.tokens = expandMacroTokens(Formatter.parseFormat(format), locale);\n      this.units = this.tokens.map(function (t) {\n        return unitForToken(t, locale);\n      });\n      this.disqualifyingUnit = this.units.find(function (t) {\n        return t.invalidReason;\n      });\n      if (!this.disqualifyingUnit) {\n        var _buildRegex = buildRegex(this.units),\n          regexString = _buildRegex[0],\n          handlers = _buildRegex[1];\n        this.regex = RegExp(regexString, \"i\");\n        this.handlers = handlers;\n      }\n    }\n    var _proto = TokenParser.prototype;\n    _proto.explainFromTokens = function explainFromTokens(input) {\n      if (!this.isValid) {\n        return {\n          input: input,\n          tokens: this.tokens,\n          invalidReason: this.invalidReason\n        };\n      } else {\n        var _match = match(input, this.regex, this.handlers),\n          rawMatches = _match[0],\n          matches = _match[1],\n          _ref6 = matches ? dateTimeFromMatches(matches) : [null, null, undefined],\n          result = _ref6[0],\n          zone = _ref6[1],\n          specificOffset = _ref6[2];\n        if (hasOwnProperty(matches, \"a\") && hasOwnProperty(matches, \"H\")) {\n          throw new ConflictingSpecificationError(\"Can't include meridiem when specifying 24-hour format\");\n        }\n        return {\n          input: input,\n          tokens: this.tokens,\n          regex: this.regex,\n          rawMatches: rawMatches,\n          matches: matches,\n          result: result,\n          zone: zone,\n          specificOffset: specificOffset\n        };\n      }\n    };\n    _createClass(TokenParser, [{\n      key: \"isValid\",\n      get: function get() {\n        return !this.disqualifyingUnit;\n      }\n    }, {\n      key: \"invalidReason\",\n      get: function get() {\n        return this.disqualifyingUnit ? this.disqualifyingUnit.invalidReason : null;\n      }\n    }]);\n    return TokenParser;\n  }();\n  function explainFromTokens(locale, input, format) {\n    var parser = new TokenParser(locale, format);\n    return parser.explainFromTokens(input);\n  }\n  function parseFromTokens(locale, input, format) {\n    var _explainFromTokens = explainFromTokens(locale, input, format),\n      result = _explainFromTokens.result,\n      zone = _explainFromTokens.zone,\n      specificOffset = _explainFromTokens.specificOffset,\n      invalidReason = _explainFromTokens.invalidReason;\n    return [result, zone, specificOffset, invalidReason];\n  }\n  function formatOptsToTokens(formatOpts, locale) {\n    if (!formatOpts) {\n      return null;\n    }\n    var formatter = Formatter.create(locale, formatOpts);\n    var df = formatter.dtFormatter(getDummyDateTime());\n    var parts = df.formatToParts();\n    var resolvedOpts = df.resolvedOptions();\n    return parts.map(function (p) {\n      return tokenForPart(p, formatOpts, resolvedOpts);\n    });\n  }\n\n  var INVALID = \"Invalid DateTime\";\n  var MAX_DATE = 8.64e15;\n  function unsupportedZone(zone) {\n    return new Invalid(\"unsupported zone\", \"the zone \\\"\" + zone.name + \"\\\" is not supported\");\n  }\n\n  // we cache week data on the DT object and this intermediates the cache\n  /**\n   * @param {DateTime} dt\n   */\n  function possiblyCachedWeekData(dt) {\n    if (dt.weekData === null) {\n      dt.weekData = gregorianToWeek(dt.c);\n    }\n    return dt.weekData;\n  }\n\n  /**\n   * @param {DateTime} dt\n   */\n  function possiblyCachedLocalWeekData(dt) {\n    if (dt.localWeekData === null) {\n      dt.localWeekData = gregorianToWeek(dt.c, dt.loc.getMinDaysInFirstWeek(), dt.loc.getStartOfWeek());\n    }\n    return dt.localWeekData;\n  }\n\n  // clone really means, \"make a new object with these modifications\". all \"setters\" really use this\n  // to create a new object while only changing some of the properties\n  function clone(inst, alts) {\n    var current = {\n      ts: inst.ts,\n      zone: inst.zone,\n      c: inst.c,\n      o: inst.o,\n      loc: inst.loc,\n      invalid: inst.invalid\n    };\n    return new DateTime(_extends({}, current, alts, {\n      old: current\n    }));\n  }\n\n  // find the right offset a given local time. The o input is our guess, which determines which\n  // offset we'll pick in ambiguous cases (e.g. there are two 3 AMs b/c Fallback DST)\n  function fixOffset(localTS, o, tz) {\n    // Our UTC time is just a guess because our offset is just a guess\n    var utcGuess = localTS - o * 60 * 1000;\n\n    // Test whether the zone matches the offset for this ts\n    var o2 = tz.offset(utcGuess);\n\n    // If so, offset didn't change and we're done\n    if (o === o2) {\n      return [utcGuess, o];\n    }\n\n    // If not, change the ts by the difference in the offset\n    utcGuess -= (o2 - o) * 60 * 1000;\n\n    // If that gives us the local time we want, we're done\n    var o3 = tz.offset(utcGuess);\n    if (o2 === o3) {\n      return [utcGuess, o2];\n    }\n\n    // If it's different, we're in a hole time. The offset has changed, but the we don't adjust the time\n    return [localTS - Math.min(o2, o3) * 60 * 1000, Math.max(o2, o3)];\n  }\n\n  // convert an epoch timestamp into a calendar object with the given offset\n  function tsToObj(ts, offset) {\n    ts += offset * 60 * 1000;\n    var d = new Date(ts);\n    return {\n      year: d.getUTCFullYear(),\n      month: d.getUTCMonth() + 1,\n      day: d.getUTCDate(),\n      hour: d.getUTCHours(),\n      minute: d.getUTCMinutes(),\n      second: d.getUTCSeconds(),\n      millisecond: d.getUTCMilliseconds()\n    };\n  }\n\n  // convert a calendar object to a epoch timestamp\n  function objToTS(obj, offset, zone) {\n    return fixOffset(objToLocalTS(obj), offset, zone);\n  }\n\n  // create a new DT instance by adding a duration, adjusting for DSTs\n  function adjustTime(inst, dur) {\n    var oPre = inst.o,\n      year = inst.c.year + Math.trunc(dur.years),\n      month = inst.c.month + Math.trunc(dur.months) + Math.trunc(dur.quarters) * 3,\n      c = _extends({}, inst.c, {\n        year: year,\n        month: month,\n        day: Math.min(inst.c.day, daysInMonth(year, month)) + Math.trunc(dur.days) + Math.trunc(dur.weeks) * 7\n      }),\n      millisToAdd = Duration.fromObject({\n        years: dur.years - Math.trunc(dur.years),\n        quarters: dur.quarters - Math.trunc(dur.quarters),\n        months: dur.months - Math.trunc(dur.months),\n        weeks: dur.weeks - Math.trunc(dur.weeks),\n        days: dur.days - Math.trunc(dur.days),\n        hours: dur.hours,\n        minutes: dur.minutes,\n        seconds: dur.seconds,\n        milliseconds: dur.milliseconds\n      }).as(\"milliseconds\"),\n      localTS = objToLocalTS(c);\n    var _fixOffset = fixOffset(localTS, oPre, inst.zone),\n      ts = _fixOffset[0],\n      o = _fixOffset[1];\n    if (millisToAdd !== 0) {\n      ts += millisToAdd;\n      // that could have changed the offset by going over a DST, but we want to keep the ts the same\n      o = inst.zone.offset(ts);\n    }\n    return {\n      ts: ts,\n      o: o\n    };\n  }\n\n  // helper useful in turning the results of parsing into real dates\n  // by handling the zone options\n  function parseDataToDateTime(parsed, parsedZone, opts, format, text, specificOffset) {\n    var setZone = opts.setZone,\n      zone = opts.zone;\n    if (parsed && Object.keys(parsed).length !== 0 || parsedZone) {\n      var interpretationZone = parsedZone || zone,\n        inst = DateTime.fromObject(parsed, _extends({}, opts, {\n          zone: interpretationZone,\n          specificOffset: specificOffset\n        }));\n      return setZone ? inst : inst.setZone(zone);\n    } else {\n      return DateTime.invalid(new Invalid(\"unparsable\", \"the input \\\"\" + text + \"\\\" can't be parsed as \" + format));\n    }\n  }\n\n  // if you want to output a technical format (e.g. RFC 2822), this helper\n  // helps handle the details\n  function toTechFormat(dt, format, allowZ) {\n    if (allowZ === void 0) {\n      allowZ = true;\n    }\n    return dt.isValid ? Formatter.create(Locale.create(\"en-US\"), {\n      allowZ: allowZ,\n      forceSimple: true\n    }).formatDateTimeFromString(dt, format) : null;\n  }\n  function _toISODate(o, extended) {\n    var longFormat = o.c.year > 9999 || o.c.year < 0;\n    var c = \"\";\n    if (longFormat && o.c.year >= 0) c += \"+\";\n    c += padStart(o.c.year, longFormat ? 6 : 4);\n    if (extended) {\n      c += \"-\";\n      c += padStart(o.c.month);\n      c += \"-\";\n      c += padStart(o.c.day);\n    } else {\n      c += padStart(o.c.month);\n      c += padStart(o.c.day);\n    }\n    return c;\n  }\n  function _toISOTime(o, extended, suppressSeconds, suppressMilliseconds, includeOffset, extendedZone) {\n    var c = padStart(o.c.hour);\n    if (extended) {\n      c += \":\";\n      c += padStart(o.c.minute);\n      if (o.c.millisecond !== 0 || o.c.second !== 0 || !suppressSeconds) {\n        c += \":\";\n      }\n    } else {\n      c += padStart(o.c.minute);\n    }\n    if (o.c.millisecond !== 0 || o.c.second !== 0 || !suppressSeconds) {\n      c += padStart(o.c.second);\n      if (o.c.millisecond !== 0 || !suppressMilliseconds) {\n        c += \".\";\n        c += padStart(o.c.millisecond, 3);\n      }\n    }\n    if (includeOffset) {\n      if (o.isOffsetFixed && o.offset === 0 && !extendedZone) {\n        c += \"Z\";\n      } else if (o.o < 0) {\n        c += \"-\";\n        c += padStart(Math.trunc(-o.o / 60));\n        c += \":\";\n        c += padStart(Math.trunc(-o.o % 60));\n      } else {\n        c += \"+\";\n        c += padStart(Math.trunc(o.o / 60));\n        c += \":\";\n        c += padStart(Math.trunc(o.o % 60));\n      }\n    }\n    if (extendedZone) {\n      c += \"[\" + o.zone.ianaName + \"]\";\n    }\n    return c;\n  }\n\n  // defaults for unspecified units in the supported calendars\n  var defaultUnitValues = {\n      month: 1,\n      day: 1,\n      hour: 0,\n      minute: 0,\n      second: 0,\n      millisecond: 0\n    },\n    defaultWeekUnitValues = {\n      weekNumber: 1,\n      weekday: 1,\n      hour: 0,\n      minute: 0,\n      second: 0,\n      millisecond: 0\n    },\n    defaultOrdinalUnitValues = {\n      ordinal: 1,\n      hour: 0,\n      minute: 0,\n      second: 0,\n      millisecond: 0\n    };\n\n  // Units in the supported calendars, sorted by bigness\n  var orderedUnits = [\"year\", \"month\", \"day\", \"hour\", \"minute\", \"second\", \"millisecond\"],\n    orderedWeekUnits = [\"weekYear\", \"weekNumber\", \"weekday\", \"hour\", \"minute\", \"second\", \"millisecond\"],\n    orderedOrdinalUnits = [\"year\", \"ordinal\", \"hour\", \"minute\", \"second\", \"millisecond\"];\n\n  // standardize case and plurality in units\n  function normalizeUnit(unit) {\n    var normalized = {\n      year: \"year\",\n      years: \"year\",\n      month: \"month\",\n      months: \"month\",\n      day: \"day\",\n      days: \"day\",\n      hour: \"hour\",\n      hours: \"hour\",\n      minute: \"minute\",\n      minutes: \"minute\",\n      quarter: \"quarter\",\n      quarters: \"quarter\",\n      second: \"second\",\n      seconds: \"second\",\n      millisecond: \"millisecond\",\n      milliseconds: \"millisecond\",\n      weekday: \"weekday\",\n      weekdays: \"weekday\",\n      weeknumber: \"weekNumber\",\n      weeksnumber: \"weekNumber\",\n      weeknumbers: \"weekNumber\",\n      weekyear: \"weekYear\",\n      weekyears: \"weekYear\",\n      ordinal: \"ordinal\"\n    }[unit.toLowerCase()];\n    if (!normalized) throw new InvalidUnitError(unit);\n    return normalized;\n  }\n  function normalizeUnitWithLocalWeeks(unit) {\n    switch (unit.toLowerCase()) {\n      case \"localweekday\":\n      case \"localweekdays\":\n        return \"localWeekday\";\n      case \"localweeknumber\":\n      case \"localweeknumbers\":\n        return \"localWeekNumber\";\n      case \"localweekyear\":\n      case \"localweekyears\":\n        return \"localWeekYear\";\n      default:\n        return normalizeUnit(unit);\n    }\n  }\n\n  // cache offsets for zones based on the current timestamp when this function is\n  // first called. When we are handling a datetime from components like (year,\n  // month, day, hour) in a time zone, we need a guess about what the timezone\n  // offset is so that we can convert into a UTC timestamp. One way is to find the\n  // offset of now in the zone. The actual date may have a different offset (for\n  // example, if we handle a date in June while we're in December in a zone that\n  // observes DST), but we can check and adjust that.\n  //\n  // When handling many dates, calculating the offset for now every time is\n  // expensive. It's just a guess, so we can cache the offset to use even if we\n  // are right on a time change boundary (we'll just correct in the other\n  // direction). Using a timestamp from first read is a slight optimization for\n  // handling dates close to the current date, since those dates will usually be\n  // in the same offset (we could set the timestamp statically, instead). We use a\n  // single timestamp for all zones to make things a bit more predictable.\n  //\n  // This is safe for quickDT (used by local() and utc()) because we don't fill in\n  // higher-order units from tsNow (as we do in fromObject, this requires that\n  // offset is calculated from tsNow).\n  function guessOffsetForZone(zone) {\n    if (!zoneOffsetGuessCache[zone]) {\n      if (zoneOffsetTs === undefined) {\n        zoneOffsetTs = Settings.now();\n      }\n      zoneOffsetGuessCache[zone] = zone.offset(zoneOffsetTs);\n    }\n    return zoneOffsetGuessCache[zone];\n  }\n\n  // this is a dumbed down version of fromObject() that runs about 60% faster\n  // but doesn't do any validation, makes a bunch of assumptions about what units\n  // are present, and so on.\n  function quickDT(obj, opts) {\n    var zone = normalizeZone(opts.zone, Settings.defaultZone);\n    if (!zone.isValid) {\n      return DateTime.invalid(unsupportedZone(zone));\n    }\n    var loc = Locale.fromObject(opts);\n    var ts, o;\n\n    // assume we have the higher-order units\n    if (!isUndefined(obj.year)) {\n      for (var _i = 0, _orderedUnits = orderedUnits; _i < _orderedUnits.length; _i++) {\n        var u = _orderedUnits[_i];\n        if (isUndefined(obj[u])) {\n          obj[u] = defaultUnitValues[u];\n        }\n      }\n      var invalid = hasInvalidGregorianData(obj) || hasInvalidTimeData(obj);\n      if (invalid) {\n        return DateTime.invalid(invalid);\n      }\n      var offsetProvis = guessOffsetForZone(zone);\n      var _objToTS = objToTS(obj, offsetProvis, zone);\n      ts = _objToTS[0];\n      o = _objToTS[1];\n    } else {\n      ts = Settings.now();\n    }\n    return new DateTime({\n      ts: ts,\n      zone: zone,\n      loc: loc,\n      o: o\n    });\n  }\n  function diffRelative(start, end, opts) {\n    var round = isUndefined(opts.round) ? true : opts.round,\n      format = function format(c, unit) {\n        c = roundTo(c, round || opts.calendary ? 0 : 2, true);\n        var formatter = end.loc.clone(opts).relFormatter(opts);\n        return formatter.format(c, unit);\n      },\n      differ = function differ(unit) {\n        if (opts.calendary) {\n          if (!end.hasSame(start, unit)) {\n            return end.startOf(unit).diff(start.startOf(unit), unit).get(unit);\n          } else return 0;\n        } else {\n          return end.diff(start, unit).get(unit);\n        }\n      };\n    if (opts.unit) {\n      return format(differ(opts.unit), opts.unit);\n    }\n    for (var _iterator = _createForOfIteratorHelperLoose(opts.units), _step; !(_step = _iterator()).done;) {\n      var unit = _step.value;\n      var count = differ(unit);\n      if (Math.abs(count) >= 1) {\n        return format(count, unit);\n      }\n    }\n    return format(start > end ? -0 : 0, opts.units[opts.units.length - 1]);\n  }\n  function lastOpts(argList) {\n    var opts = {},\n      args;\n    if (argList.length > 0 && typeof argList[argList.length - 1] === \"object\") {\n      opts = argList[argList.length - 1];\n      args = Array.from(argList).slice(0, argList.length - 1);\n    } else {\n      args = Array.from(argList);\n    }\n    return [opts, args];\n  }\n\n  /**\n   * Timestamp to use for cached zone offset guesses (exposed for test)\n   */\n  var zoneOffsetTs;\n  /**\n   * Cache for zone offset guesses (exposed for test).\n   *\n   * This optimizes quickDT via guessOffsetForZone to avoid repeated calls of\n   * zone.offset().\n   */\n  var zoneOffsetGuessCache = {};\n\n  /**\n   * A DateTime is an immutable data structure representing a specific date and time and accompanying methods. It contains class and instance methods for creating, parsing, interrogating, transforming, and formatting them.\n   *\n   * A DateTime comprises of:\n   * * A timestamp. Each DateTime instance refers to a specific millisecond of the Unix epoch.\n   * * A time zone. Each instance is considered in the context of a specific zone (by default the local system's zone).\n   * * Configuration properties that effect how output strings are formatted, such as `locale`, `numberingSystem`, and `outputCalendar`.\n   *\n   * Here is a brief overview of the most commonly used functionality it provides:\n   *\n   * * **Creation**: To create a DateTime from its components, use one of its factory class methods: {@link DateTime.local}, {@link DateTime.utc}, and (most flexibly) {@link DateTime.fromObject}. To create one from a standard string format, use {@link DateTime.fromISO}, {@link DateTime.fromHTTP}, and {@link DateTime.fromRFC2822}. To create one from a custom string format, use {@link DateTime.fromFormat}. To create one from a native JS date, use {@link DateTime.fromJSDate}.\n   * * **Gregorian calendar and time**: To examine the Gregorian properties of a DateTime individually (i.e as opposed to collectively through {@link DateTime#toObject}), use the {@link DateTime#year}, {@link DateTime#month},\n   * {@link DateTime#day}, {@link DateTime#hour}, {@link DateTime#minute}, {@link DateTime#second}, {@link DateTime#millisecond} accessors.\n   * * **Week calendar**: For ISO week calendar attributes, see the {@link DateTime#weekYear}, {@link DateTime#weekNumber}, and {@link DateTime#weekday} accessors.\n   * * **Configuration** See the {@link DateTime#locale} and {@link DateTime#numberingSystem} accessors.\n   * * **Transformation**: To transform the DateTime into other DateTimes, use {@link DateTime#set}, {@link DateTime#reconfigure}, {@link DateTime#setZone}, {@link DateTime#setLocale}, {@link DateTime.plus}, {@link DateTime#minus}, {@link DateTime#endOf}, {@link DateTime#startOf}, {@link DateTime#toUTC}, and {@link DateTime#toLocal}.\n   * * **Output**: To convert the DateTime to other representations, use the {@link DateTime#toRelative}, {@link DateTime#toRelativeCalendar}, {@link DateTime#toJSON}, {@link DateTime#toISO}, {@link DateTime#toHTTP}, {@link DateTime#toObject}, {@link DateTime#toRFC2822}, {@link DateTime#toString}, {@link DateTime#toLocaleString}, {@link DateTime#toFormat}, {@link DateTime#toMillis} and {@link DateTime#toJSDate}.\n   *\n   * There's plenty others documented below. In addition, for more information on subtler topics like internationalization, time zones, alternative calendars, validity, and so on, see the external documentation.\n   */\n  var DateTime = /*#__PURE__*/function (_Symbol$for) {\n    /**\n     * @access private\n     */\n    function DateTime(config) {\n      var zone = config.zone || Settings.defaultZone;\n      var invalid = config.invalid || (Number.isNaN(config.ts) ? new Invalid(\"invalid input\") : null) || (!zone.isValid ? unsupportedZone(zone) : null);\n      /**\n       * @access private\n       */\n      this.ts = isUndefined(config.ts) ? Settings.now() : config.ts;\n      var c = null,\n        o = null;\n      if (!invalid) {\n        var unchanged = config.old && config.old.ts === this.ts && config.old.zone.equals(zone);\n        if (unchanged) {\n          var _ref = [config.old.c, config.old.o];\n          c = _ref[0];\n          o = _ref[1];\n        } else {\n          // If an offset has been passed and we have not been called from\n          // clone(), we can trust it and avoid the offset calculation.\n          var ot = isNumber(config.o) && !config.old ? config.o : zone.offset(this.ts);\n          c = tsToObj(this.ts, ot);\n          invalid = Number.isNaN(c.year) ? new Invalid(\"invalid input\") : null;\n          c = invalid ? null : c;\n          o = invalid ? null : ot;\n        }\n      }\n\n      /**\n       * @access private\n       */\n      this._zone = zone;\n      /**\n       * @access private\n       */\n      this.loc = config.loc || Locale.create();\n      /**\n       * @access private\n       */\n      this.invalid = invalid;\n      /**\n       * @access private\n       */\n      this.weekData = null;\n      /**\n       * @access private\n       */\n      this.localWeekData = null;\n      /**\n       * @access private\n       */\n      this.c = c;\n      /**\n       * @access private\n       */\n      this.o = o;\n      /**\n       * @access private\n       */\n      this.isLuxonDateTime = true;\n    }\n\n    // CONSTRUCT\n\n    /**\n     * Create a DateTime for the current instant, in the system's time zone.\n     *\n     * Use Settings to override these default values if needed.\n     * @example DateTime.now().toISO() //~> now in the ISO format\n     * @return {DateTime}\n     */\n    DateTime.now = function now() {\n      return new DateTime({});\n    }\n\n    /**\n     * Create a local DateTime\n     * @param {number} [year] - The calendar year. If omitted (as in, call `local()` with no arguments), the current time will be used\n     * @param {number} [month=1] - The month, 1-indexed\n     * @param {number} [day=1] - The day of the month, 1-indexed\n     * @param {number} [hour=0] - The hour of the day, in 24-hour time\n     * @param {number} [minute=0] - The minute of the hour, meaning a number between 0 and 59\n     * @param {number} [second=0] - The second of the minute, meaning a number between 0 and 59\n     * @param {number} [millisecond=0] - The millisecond of the second, meaning a number between 0 and 999\n     * @example DateTime.local()                                  //~> now\n     * @example DateTime.local({ zone: \"America/New_York\" })      //~> now, in US east coast time\n     * @example DateTime.local(2017)                              //~> 2017-01-01T00:00:00\n     * @example DateTime.local(2017, 3)                           //~> 2017-03-01T00:00:00\n     * @example DateTime.local(2017, 3, 12, { locale: \"fr\" })     //~> 2017-03-12T00:00:00, with a French locale\n     * @example DateTime.local(2017, 3, 12, 5)                    //~> 2017-03-12T05:00:00\n     * @example DateTime.local(2017, 3, 12, 5, { zone: \"utc\" })   //~> 2017-03-12T05:00:00, in UTC\n     * @example DateTime.local(2017, 3, 12, 5, 45)                //~> 2017-03-12T05:45:00\n     * @example DateTime.local(2017, 3, 12, 5, 45, 10)            //~> 2017-03-12T05:45:10\n     * @example DateTime.local(2017, 3, 12, 5, 45, 10, 765)       //~> 2017-03-12T05:45:10.765\n     * @return {DateTime}\n     */;\n    DateTime.local = function local() {\n      var _lastOpts = lastOpts(arguments),\n        opts = _lastOpts[0],\n        args = _lastOpts[1],\n        year = args[0],\n        month = args[1],\n        day = args[2],\n        hour = args[3],\n        minute = args[4],\n        second = args[5],\n        millisecond = args[6];\n      return quickDT({\n        year: year,\n        month: month,\n        day: day,\n        hour: hour,\n        minute: minute,\n        second: second,\n        millisecond: millisecond\n      }, opts);\n    }\n\n    /**\n     * Create a DateTime in UTC\n     * @param {number} [year] - The calendar year. If omitted (as in, call `utc()` with no arguments), the current time will be used\n     * @param {number} [month=1] - The month, 1-indexed\n     * @param {number} [day=1] - The day of the month\n     * @param {number} [hour=0] - The hour of the day, in 24-hour time\n     * @param {number} [minute=0] - The minute of the hour, meaning a number between 0 and 59\n     * @param {number} [second=0] - The second of the minute, meaning a number between 0 and 59\n     * @param {number} [millisecond=0] - The millisecond of the second, meaning a number between 0 and 999\n     * @param {Object} options - configuration options for the DateTime\n     * @param {string} [options.locale] - a locale to set on the resulting DateTime instance\n     * @param {string} [options.outputCalendar] - the output calendar to set on the resulting DateTime instance\n     * @param {string} [options.numberingSystem] - the numbering system to set on the resulting DateTime instance\n     * @param {string} [options.weekSettings] - the week settings to set on the resulting DateTime instance\n     * @example DateTime.utc()                                              //~> now\n     * @example DateTime.utc(2017)                                          //~> 2017-01-01T00:00:00Z\n     * @example DateTime.utc(2017, 3)                                       //~> 2017-03-01T00:00:00Z\n     * @example DateTime.utc(2017, 3, 12)                                   //~> 2017-03-12T00:00:00Z\n     * @example DateTime.utc(2017, 3, 12, 5)                                //~> 2017-03-12T05:00:00Z\n     * @example DateTime.utc(2017, 3, 12, 5, 45)                            //~> 2017-03-12T05:45:00Z\n     * @example DateTime.utc(2017, 3, 12, 5, 45, { locale: \"fr\" })          //~> 2017-03-12T05:45:00Z with a French locale\n     * @example DateTime.utc(2017, 3, 12, 5, 45, 10)                        //~> 2017-03-12T05:45:10Z\n     * @example DateTime.utc(2017, 3, 12, 5, 45, 10, 765, { locale: \"fr\" }) //~> 2017-03-12T05:45:10.765Z with a French locale\n     * @return {DateTime}\n     */;\n    DateTime.utc = function utc() {\n      var _lastOpts2 = lastOpts(arguments),\n        opts = _lastOpts2[0],\n        args = _lastOpts2[1],\n        year = args[0],\n        month = args[1],\n        day = args[2],\n        hour = args[3],\n        minute = args[4],\n        second = args[5],\n        millisecond = args[6];\n      opts.zone = FixedOffsetZone.utcInstance;\n      return quickDT({\n        year: year,\n        month: month,\n        day: day,\n        hour: hour,\n        minute: minute,\n        second: second,\n        millisecond: millisecond\n      }, opts);\n    }\n\n    /**\n     * Create a DateTime from a JavaScript Date object. Uses the default zone.\n     * @param {Date} date - a JavaScript Date object\n     * @param {Object} options - configuration options for the DateTime\n     * @param {string|Zone} [options.zone='local'] - the zone to place the DateTime into\n     * @return {DateTime}\n     */;\n    DateTime.fromJSDate = function fromJSDate(date, options) {\n      if (options === void 0) {\n        options = {};\n      }\n      var ts = isDate(date) ? date.valueOf() : NaN;\n      if (Number.isNaN(ts)) {\n        return DateTime.invalid(\"invalid input\");\n      }\n      var zoneToUse = normalizeZone(options.zone, Settings.defaultZone);\n      if (!zoneToUse.isValid) {\n        return DateTime.invalid(unsupportedZone(zoneToUse));\n      }\n      return new DateTime({\n        ts: ts,\n        zone: zoneToUse,\n        loc: Locale.fromObject(options)\n      });\n    }\n\n    /**\n     * Create a DateTime from a number of milliseconds since the epoch (meaning since 1 January 1970 00:00:00 UTC). Uses the default zone.\n     * @param {number} milliseconds - a number of milliseconds since 1970 UTC\n     * @param {Object} options - configuration options for the DateTime\n     * @param {string|Zone} [options.zone='local'] - the zone to place the DateTime into\n     * @param {string} [options.locale] - a locale to set on the resulting DateTime instance\n     * @param {string} options.outputCalendar - the output calendar to set on the resulting DateTime instance\n     * @param {string} options.numberingSystem - the numbering system to set on the resulting DateTime instance\n     * @param {string} options.weekSettings - the week settings to set on the resulting DateTime instance\n     * @return {DateTime}\n     */;\n    DateTime.fromMillis = function fromMillis(milliseconds, options) {\n      if (options === void 0) {\n        options = {};\n      }\n      if (!isNumber(milliseconds)) {\n        throw new InvalidArgumentError(\"fromMillis requires a numerical input, but received a \" + typeof milliseconds + \" with value \" + milliseconds);\n      } else if (milliseconds < -MAX_DATE || milliseconds > MAX_DATE) {\n        // this isn't perfect because we can still end up out of range because of additional shifting, but it's a start\n        return DateTime.invalid(\"Timestamp out of range\");\n      } else {\n        return new DateTime({\n          ts: milliseconds,\n          zone: normalizeZone(options.zone, Settings.defaultZone),\n          loc: Locale.fromObject(options)\n        });\n      }\n    }\n\n    /**\n     * Create a DateTime from a number of seconds since the epoch (meaning since 1 January 1970 00:00:00 UTC). Uses the default zone.\n     * @param {number} seconds - a number of seconds since 1970 UTC\n     * @param {Object} options - configuration options for the DateTime\n     * @param {string|Zone} [options.zone='local'] - the zone to place the DateTime into\n     * @param {string} [options.locale] - a locale to set on the resulting DateTime instance\n     * @param {string} options.outputCalendar - the output calendar to set on the resulting DateTime instance\n     * @param {string} options.numberingSystem - the numbering system to set on the resulting DateTime instance\n     * @param {string} options.weekSettings - the week settings to set on the resulting DateTime instance\n     * @return {DateTime}\n     */;\n    DateTime.fromSeconds = function fromSeconds(seconds, options) {\n      if (options === void 0) {\n        options = {};\n      }\n      if (!isNumber(seconds)) {\n        throw new InvalidArgumentError(\"fromSeconds requires a numerical input\");\n      } else {\n        return new DateTime({\n          ts: seconds * 1000,\n          zone: normalizeZone(options.zone, Settings.defaultZone),\n          loc: Locale.fromObject(options)\n        });\n      }\n    }\n\n    /**\n     * Create a DateTime from a JavaScript object with keys like 'year' and 'hour' with reasonable defaults.\n     * @param {Object} obj - the object to create the DateTime from\n     * @param {number} obj.year - a year, such as 1987\n     * @param {number} obj.month - a month, 1-12\n     * @param {number} obj.day - a day of the month, 1-31, depending on the month\n     * @param {number} obj.ordinal - day of the year, 1-365 or 366\n     * @param {number} obj.weekYear - an ISO week year\n     * @param {number} obj.weekNumber - an ISO week number, between 1 and 52 or 53, depending on the year\n     * @param {number} obj.weekday - an ISO weekday, 1-7, where 1 is Monday and 7 is Sunday\n     * @param {number} obj.localWeekYear - a week year, according to the locale\n     * @param {number} obj.localWeekNumber - a week number, between 1 and 52 or 53, depending on the year, according to the locale\n     * @param {number} obj.localWeekday - a weekday, 1-7, where 1 is the first and 7 is the last day of the week, according to the locale\n     * @param {number} obj.hour - hour of the day, 0-23\n     * @param {number} obj.minute - minute of the hour, 0-59\n     * @param {number} obj.second - second of the minute, 0-59\n     * @param {number} obj.millisecond - millisecond of the second, 0-999\n     * @param {Object} opts - options for creating this DateTime\n     * @param {string|Zone} [opts.zone='local'] - interpret the numbers in the context of a particular zone. Can take any value taken as the first argument to setZone()\n     * @param {string} [opts.locale='system\\'s locale'] - a locale to set on the resulting DateTime instance\n     * @param {string} opts.outputCalendar - the output calendar to set on the resulting DateTime instance\n     * @param {string} opts.numberingSystem - the numbering system to set on the resulting DateTime instance\n     * @param {string} opts.weekSettings - the week settings to set on the resulting DateTime instance\n     * @example DateTime.fromObject({ year: 1982, month: 5, day: 25}).toISODate() //=> '1982-05-25'\n     * @example DateTime.fromObject({ year: 1982 }).toISODate() //=> '1982-01-01'\n     * @example DateTime.fromObject({ hour: 10, minute: 26, second: 6 }) //~> today at 10:26:06\n     * @example DateTime.fromObject({ hour: 10, minute: 26, second: 6 }, { zone: 'utc' }),\n     * @example DateTime.fromObject({ hour: 10, minute: 26, second: 6 }, { zone: 'local' })\n     * @example DateTime.fromObject({ hour: 10, minute: 26, second: 6 }, { zone: 'America/New_York' })\n     * @example DateTime.fromObject({ weekYear: 2016, weekNumber: 2, weekday: 3 }).toISODate() //=> '2016-01-13'\n     * @example DateTime.fromObject({ localWeekYear: 2022, localWeekNumber: 1, localWeekday: 1 }, { locale: \"en-US\" }).toISODate() //=> '2021-12-26'\n     * @return {DateTime}\n     */;\n    DateTime.fromObject = function fromObject(obj, opts) {\n      if (opts === void 0) {\n        opts = {};\n      }\n      obj = obj || {};\n      var zoneToUse = normalizeZone(opts.zone, Settings.defaultZone);\n      if (!zoneToUse.isValid) {\n        return DateTime.invalid(unsupportedZone(zoneToUse));\n      }\n      var loc = Locale.fromObject(opts);\n      var normalized = normalizeObject(obj, normalizeUnitWithLocalWeeks);\n      var _usesLocalWeekValues = usesLocalWeekValues(normalized, loc),\n        minDaysInFirstWeek = _usesLocalWeekValues.minDaysInFirstWeek,\n        startOfWeek = _usesLocalWeekValues.startOfWeek;\n      var tsNow = Settings.now(),\n        offsetProvis = !isUndefined(opts.specificOffset) ? opts.specificOffset : zoneToUse.offset(tsNow),\n        containsOrdinal = !isUndefined(normalized.ordinal),\n        containsGregorYear = !isUndefined(normalized.year),\n        containsGregorMD = !isUndefined(normalized.month) || !isUndefined(normalized.day),\n        containsGregor = containsGregorYear || containsGregorMD,\n        definiteWeekDef = normalized.weekYear || normalized.weekNumber;\n\n      // cases:\n      // just a weekday -> this week's instance of that weekday, no worries\n      // (gregorian data or ordinal) + (weekYear or weekNumber) -> error\n      // (gregorian month or day) + ordinal -> error\n      // otherwise just use weeks or ordinals or gregorian, depending on what's specified\n\n      if ((containsGregor || containsOrdinal) && definiteWeekDef) {\n        throw new ConflictingSpecificationError(\"Can't mix weekYear/weekNumber units with year/month/day or ordinals\");\n      }\n      if (containsGregorMD && containsOrdinal) {\n        throw new ConflictingSpecificationError(\"Can't mix ordinal dates with month/day\");\n      }\n      var useWeekData = definiteWeekDef || normalized.weekday && !containsGregor;\n\n      // configure ourselves to deal with gregorian dates or week stuff\n      var units,\n        defaultValues,\n        objNow = tsToObj(tsNow, offsetProvis);\n      if (useWeekData) {\n        units = orderedWeekUnits;\n        defaultValues = defaultWeekUnitValues;\n        objNow = gregorianToWeek(objNow, minDaysInFirstWeek, startOfWeek);\n      } else if (containsOrdinal) {\n        units = orderedOrdinalUnits;\n        defaultValues = defaultOrdinalUnitValues;\n        objNow = gregorianToOrdinal(objNow);\n      } else {\n        units = orderedUnits;\n        defaultValues = defaultUnitValues;\n      }\n\n      // set default values for missing stuff\n      var foundFirst = false;\n      for (var _iterator2 = _createForOfIteratorHelperLoose(units), _step2; !(_step2 = _iterator2()).done;) {\n        var u = _step2.value;\n        var v = normalized[u];\n        if (!isUndefined(v)) {\n          foundFirst = true;\n        } else if (foundFirst) {\n          normalized[u] = defaultValues[u];\n        } else {\n          normalized[u] = objNow[u];\n        }\n      }\n\n      // make sure the values we have are in range\n      var higherOrderInvalid = useWeekData ? hasInvalidWeekData(normalized, minDaysInFirstWeek, startOfWeek) : containsOrdinal ? hasInvalidOrdinalData(normalized) : hasInvalidGregorianData(normalized),\n        invalid = higherOrderInvalid || hasInvalidTimeData(normalized);\n      if (invalid) {\n        return DateTime.invalid(invalid);\n      }\n\n      // compute the actual time\n      var gregorian = useWeekData ? weekToGregorian(normalized, minDaysInFirstWeek, startOfWeek) : containsOrdinal ? ordinalToGregorian(normalized) : normalized,\n        _objToTS2 = objToTS(gregorian, offsetProvis, zoneToUse),\n        tsFinal = _objToTS2[0],\n        offsetFinal = _objToTS2[1],\n        inst = new DateTime({\n          ts: tsFinal,\n          zone: zoneToUse,\n          o: offsetFinal,\n          loc: loc\n        });\n\n      // gregorian data + weekday serves only to validate\n      if (normalized.weekday && containsGregor && obj.weekday !== inst.weekday) {\n        return DateTime.invalid(\"mismatched weekday\", \"you can't specify both a weekday of \" + normalized.weekday + \" and a date of \" + inst.toISO());\n      }\n      if (!inst.isValid) {\n        return DateTime.invalid(inst.invalid);\n      }\n      return inst;\n    }\n\n    /**\n     * Create a DateTime from an ISO 8601 string\n     * @param {string} text - the ISO string\n     * @param {Object} opts - options to affect the creation\n     * @param {string|Zone} [opts.zone='local'] - use this zone if no offset is specified in the input string itself. Will also convert the time to this zone\n     * @param {boolean} [opts.setZone=false] - override the zone with a fixed-offset zone specified in the string itself, if it specifies one\n     * @param {string} [opts.locale='system's locale'] - a locale to set on the resulting DateTime instance\n     * @param {string} [opts.outputCalendar] - the output calendar to set on the resulting DateTime instance\n     * @param {string} [opts.numberingSystem] - the numbering system to set on the resulting DateTime instance\n     * @param {string} [opts.weekSettings] - the week settings to set on the resulting DateTime instance\n     * @example DateTime.fromISO('2016-05-25T09:08:34.123')\n     * @example DateTime.fromISO('2016-05-25T09:08:34.123+06:00')\n     * @example DateTime.fromISO('2016-05-25T09:08:34.123+06:00', {setZone: true})\n     * @example DateTime.fromISO('2016-05-25T09:08:34.123', {zone: 'utc'})\n     * @example DateTime.fromISO('2016-W05-4')\n     * @return {DateTime}\n     */;\n    DateTime.fromISO = function fromISO(text, opts) {\n      if (opts === void 0) {\n        opts = {};\n      }\n      var _parseISODate = parseISODate(text),\n        vals = _parseISODate[0],\n        parsedZone = _parseISODate[1];\n      return parseDataToDateTime(vals, parsedZone, opts, \"ISO 8601\", text);\n    }\n\n    /**\n     * Create a DateTime from an RFC 2822 string\n     * @param {string} text - the RFC 2822 string\n     * @param {Object} opts - options to affect the creation\n     * @param {string|Zone} [opts.zone='local'] - convert the time to this zone. Since the offset is always specified in the string itself, this has no effect on the interpretation of string, merely the zone the resulting DateTime is expressed in.\n     * @param {boolean} [opts.setZone=false] - override the zone with a fixed-offset zone specified in the string itself, if it specifies one\n     * @param {string} [opts.locale='system's locale'] - a locale to set on the resulting DateTime instance\n     * @param {string} opts.outputCalendar - the output calendar to set on the resulting DateTime instance\n     * @param {string} opts.numberingSystem - the numbering system to set on the resulting DateTime instance\n     * @param {string} opts.weekSettings - the week settings to set on the resulting DateTime instance\n     * @example DateTime.fromRFC2822('25 Nov 2016 13:23:12 GMT')\n     * @example DateTime.fromRFC2822('Fri, 25 Nov 2016 13:23:12 +0600')\n     * @example DateTime.fromRFC2822('25 Nov 2016 13:23 Z')\n     * @return {DateTime}\n     */;\n    DateTime.fromRFC2822 = function fromRFC2822(text, opts) {\n      if (opts === void 0) {\n        opts = {};\n      }\n      var _parseRFC2822Date = parseRFC2822Date(text),\n        vals = _parseRFC2822Date[0],\n        parsedZone = _parseRFC2822Date[1];\n      return parseDataToDateTime(vals, parsedZone, opts, \"RFC 2822\", text);\n    }\n\n    /**\n     * Create a DateTime from an HTTP header date\n     * @see https://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.3.1\n     * @param {string} text - the HTTP header date\n     * @param {Object} opts - options to affect the creation\n     * @param {string|Zone} [opts.zone='local'] - convert the time to this zone. Since HTTP dates are always in UTC, this has no effect on the interpretation of string, merely the zone the resulting DateTime is expressed in.\n     * @param {boolean} [opts.setZone=false] - override the zone with the fixed-offset zone specified in the string. For HTTP dates, this is always UTC, so this option is equivalent to setting the `zone` option to 'utc', but this option is included for consistency with similar methods.\n     * @param {string} [opts.locale='system's locale'] - a locale to set on the resulting DateTime instance\n     * @param {string} opts.outputCalendar - the output calendar to set on the resulting DateTime instance\n     * @param {string} opts.numberingSystem - the numbering system to set on the resulting DateTime instance\n     * @param {string} opts.weekSettings - the week settings to set on the resulting DateTime instance\n     * @example DateTime.fromHTTP('Sun, 06 Nov 1994 08:49:37 GMT')\n     * @example DateTime.fromHTTP('Sunday, 06-Nov-94 08:49:37 GMT')\n     * @example DateTime.fromHTTP('Sun Nov  6 08:49:37 1994')\n     * @return {DateTime}\n     */;\n    DateTime.fromHTTP = function fromHTTP(text, opts) {\n      if (opts === void 0) {\n        opts = {};\n      }\n      var _parseHTTPDate = parseHTTPDate(text),\n        vals = _parseHTTPDate[0],\n        parsedZone = _parseHTTPDate[1];\n      return parseDataToDateTime(vals, parsedZone, opts, \"HTTP\", opts);\n    }\n\n    /**\n     * Create a DateTime from an input string and format string.\n     * Defaults to en-US if no locale has been specified, regardless of the system's locale. For a table of tokens and their interpretations, see [here](https://moment.github.io/luxon/#/parsing?id=table-of-tokens).\n     * @param {string} text - the string to parse\n     * @param {string} fmt - the format the string is expected to be in (see the link below for the formats)\n     * @param {Object} opts - options to affect the creation\n     * @param {string|Zone} [opts.zone='local'] - use this zone if no offset is specified in the input string itself. Will also convert the DateTime to this zone\n     * @param {boolean} [opts.setZone=false] - override the zone with a zone specified in the string itself, if it specifies one\n     * @param {string} [opts.locale='en-US'] - a locale string to use when parsing. Will also set the DateTime to this locale\n     * @param {string} opts.numberingSystem - the numbering system to use when parsing. Will also set the resulting DateTime to this numbering system\n     * @param {string} opts.weekSettings - the week settings to set on the resulting DateTime instance\n     * @param {string} opts.outputCalendar - the output calendar to set on the resulting DateTime instance\n     * @return {DateTime}\n     */;\n    DateTime.fromFormat = function fromFormat(text, fmt, opts) {\n      if (opts === void 0) {\n        opts = {};\n      }\n      if (isUndefined(text) || isUndefined(fmt)) {\n        throw new InvalidArgumentError(\"fromFormat requires an input string and a format\");\n      }\n      var _opts = opts,\n        _opts$locale = _opts.locale,\n        locale = _opts$locale === void 0 ? null : _opts$locale,\n        _opts$numberingSystem = _opts.numberingSystem,\n        numberingSystem = _opts$numberingSystem === void 0 ? null : _opts$numberingSystem,\n        localeToUse = Locale.fromOpts({\n          locale: locale,\n          numberingSystem: numberingSystem,\n          defaultToEN: true\n        }),\n        _parseFromTokens = parseFromTokens(localeToUse, text, fmt),\n        vals = _parseFromTokens[0],\n        parsedZone = _parseFromTokens[1],\n        specificOffset = _parseFromTokens[2],\n        invalid = _parseFromTokens[3];\n      if (invalid) {\n        return DateTime.invalid(invalid);\n      } else {\n        return parseDataToDateTime(vals, parsedZone, opts, \"format \" + fmt, text, specificOffset);\n      }\n    }\n\n    /**\n     * @deprecated use fromFormat instead\n     */;\n    DateTime.fromString = function fromString(text, fmt, opts) {\n      if (opts === void 0) {\n        opts = {};\n      }\n      return DateTime.fromFormat(text, fmt, opts);\n    }\n\n    /**\n     * Create a DateTime from a SQL date, time, or datetime\n     * Defaults to en-US if no locale has been specified, regardless of the system's locale\n     * @param {string} text - the string to parse\n     * @param {Object} opts - options to affect the creation\n     * @param {string|Zone} [opts.zone='local'] - use this zone if no offset is specified in the input string itself. Will also convert the DateTime to this zone\n     * @param {boolean} [opts.setZone=false] - override the zone with a zone specified in the string itself, if it specifies one\n     * @param {string} [opts.locale='en-US'] - a locale string to use when parsing. Will also set the DateTime to this locale\n     * @param {string} opts.numberingSystem - the numbering system to use when parsing. Will also set the resulting DateTime to this numbering system\n     * @param {string} opts.weekSettings - the week settings to set on the resulting DateTime instance\n     * @param {string} opts.outputCalendar - the output calendar to set on the resulting DateTime instance\n     * @example DateTime.fromSQL('2017-05-15')\n     * @example DateTime.fromSQL('2017-05-15 09:12:34')\n     * @example DateTime.fromSQL('2017-05-15 09:12:34.342')\n     * @example DateTime.fromSQL('2017-05-15 09:12:34.342+06:00')\n     * @example DateTime.fromSQL('2017-05-15 09:12:34.342 America/Los_Angeles')\n     * @example DateTime.fromSQL('2017-05-15 09:12:34.342 America/Los_Angeles', { setZone: true })\n     * @example DateTime.fromSQL('2017-05-15 09:12:34.342', { zone: 'America/Los_Angeles' })\n     * @example DateTime.fromSQL('09:12:34.342')\n     * @return {DateTime}\n     */;\n    DateTime.fromSQL = function fromSQL(text, opts) {\n      if (opts === void 0) {\n        opts = {};\n      }\n      var _parseSQL = parseSQL(text),\n        vals = _parseSQL[0],\n        parsedZone = _parseSQL[1];\n      return parseDataToDateTime(vals, parsedZone, opts, \"SQL\", text);\n    }\n\n    /**\n     * Create an invalid DateTime.\n     * @param {string} reason - simple string of why this DateTime is invalid. Should not contain parameters or anything else data-dependent.\n     * @param {string} [explanation=null] - longer explanation, may include parameters and other useful debugging information\n     * @return {DateTime}\n     */;\n    DateTime.invalid = function invalid(reason, explanation) {\n      if (explanation === void 0) {\n        explanation = null;\n      }\n      if (!reason) {\n        throw new InvalidArgumentError(\"need to specify a reason the DateTime is invalid\");\n      }\n      var invalid = reason instanceof Invalid ? reason : new Invalid(reason, explanation);\n      if (Settings.throwOnInvalid) {\n        throw new InvalidDateTimeError(invalid);\n      } else {\n        return new DateTime({\n          invalid: invalid\n        });\n      }\n    }\n\n    /**\n     * Check if an object is an instance of DateTime. Works across context boundaries\n     * @param {object} o\n     * @return {boolean}\n     */;\n    DateTime.isDateTime = function isDateTime(o) {\n      return o && o.isLuxonDateTime || false;\n    }\n\n    /**\n     * Produce the format string for a set of options\n     * @param formatOpts\n     * @param localeOpts\n     * @returns {string}\n     */;\n    DateTime.parseFormatForOpts = function parseFormatForOpts(formatOpts, localeOpts) {\n      if (localeOpts === void 0) {\n        localeOpts = {};\n      }\n      var tokenList = formatOptsToTokens(formatOpts, Locale.fromObject(localeOpts));\n      return !tokenList ? null : tokenList.map(function (t) {\n        return t ? t.val : null;\n      }).join(\"\");\n    }\n\n    /**\n     * Produce the the fully expanded format token for the locale\n     * Does NOT quote characters, so quoted tokens will not round trip correctly\n     * @param fmt\n     * @param localeOpts\n     * @returns {string}\n     */;\n    DateTime.expandFormat = function expandFormat(fmt, localeOpts) {\n      if (localeOpts === void 0) {\n        localeOpts = {};\n      }\n      var expanded = expandMacroTokens(Formatter.parseFormat(fmt), Locale.fromObject(localeOpts));\n      return expanded.map(function (t) {\n        return t.val;\n      }).join(\"\");\n    };\n    DateTime.resetCache = function resetCache() {\n      zoneOffsetTs = undefined;\n      zoneOffsetGuessCache = {};\n    }\n\n    // INFO\n\n    /**\n     * Get the value of unit.\n     * @param {string} unit - a unit such as 'minute' or 'day'\n     * @example DateTime.local(2017, 7, 4).get('month'); //=> 7\n     * @example DateTime.local(2017, 7, 4).get('day'); //=> 4\n     * @return {number}\n     */;\n    var _proto = DateTime.prototype;\n    _proto.get = function get(unit) {\n      return this[unit];\n    }\n\n    /**\n     * Returns whether the DateTime is valid. Invalid DateTimes occur when:\n     * * The DateTime was created from invalid calendar information, such as the 13th month or February 30\n     * * The DateTime was created by an operation on another invalid date\n     * @type {boolean}\n     */;\n    /**\n     * Get those DateTimes which have the same local time as this DateTime, but a different offset from UTC\n     * in this DateTime's zone. During DST changes local time can be ambiguous, for example\n     * `2023-10-29T02:30:00` in `Europe/Berlin` can have offset `+01:00` or `+02:00`.\n     * This method will return both possible DateTimes if this DateTime's local time is ambiguous.\n     * @returns {DateTime[]}\n     */\n    _proto.getPossibleOffsets = function getPossibleOffsets() {\n      if (!this.isValid || this.isOffsetFixed) {\n        return [this];\n      }\n      var dayMs = 86400000;\n      var minuteMs = 60000;\n      var localTS = objToLocalTS(this.c);\n      var oEarlier = this.zone.offset(localTS - dayMs);\n      var oLater = this.zone.offset(localTS + dayMs);\n      var o1 = this.zone.offset(localTS - oEarlier * minuteMs);\n      var o2 = this.zone.offset(localTS - oLater * minuteMs);\n      if (o1 === o2) {\n        return [this];\n      }\n      var ts1 = localTS - o1 * minuteMs;\n      var ts2 = localTS - o2 * minuteMs;\n      var c1 = tsToObj(ts1, o1);\n      var c2 = tsToObj(ts2, o2);\n      if (c1.hour === c2.hour && c1.minute === c2.minute && c1.second === c2.second && c1.millisecond === c2.millisecond) {\n        return [clone(this, {\n          ts: ts1\n        }), clone(this, {\n          ts: ts2\n        })];\n      }\n      return [this];\n    }\n\n    /**\n     * Returns true if this DateTime is in a leap year, false otherwise\n     * @example DateTime.local(2016).isInLeapYear //=> true\n     * @example DateTime.local(2013).isInLeapYear //=> false\n     * @type {boolean}\n     */;\n    /**\n     * Returns the resolved Intl options for this DateTime.\n     * This is useful in understanding the behavior of formatting methods\n     * @param {Object} opts - the same options as toLocaleString\n     * @return {Object}\n     */\n    _proto.resolvedLocaleOptions = function resolvedLocaleOptions(opts) {\n      if (opts === void 0) {\n        opts = {};\n      }\n      var _Formatter$create$res = Formatter.create(this.loc.clone(opts), opts).resolvedOptions(this),\n        locale = _Formatter$create$res.locale,\n        numberingSystem = _Formatter$create$res.numberingSystem,\n        calendar = _Formatter$create$res.calendar;\n      return {\n        locale: locale,\n        numberingSystem: numberingSystem,\n        outputCalendar: calendar\n      };\n    }\n\n    // TRANSFORM\n\n    /**\n     * \"Set\" the DateTime's zone to UTC. Returns a newly-constructed DateTime.\n     *\n     * Equivalent to {@link DateTime#setZone}('utc')\n     * @param {number} [offset=0] - optionally, an offset from UTC in minutes\n     * @param {Object} [opts={}] - options to pass to `setZone()`\n     * @return {DateTime}\n     */;\n    _proto.toUTC = function toUTC(offset, opts) {\n      if (offset === void 0) {\n        offset = 0;\n      }\n      if (opts === void 0) {\n        opts = {};\n      }\n      return this.setZone(FixedOffsetZone.instance(offset), opts);\n    }\n\n    /**\n     * \"Set\" the DateTime's zone to the host's local zone. Returns a newly-constructed DateTime.\n     *\n     * Equivalent to `setZone('local')`\n     * @return {DateTime}\n     */;\n    _proto.toLocal = function toLocal() {\n      return this.setZone(Settings.defaultZone);\n    }\n\n    /**\n     * \"Set\" the DateTime's zone to specified zone. Returns a newly-constructed DateTime.\n     *\n     * By default, the setter keeps the underlying time the same (as in, the same timestamp), but the new instance will report different local times and consider DSTs when making computations, as with {@link DateTime#plus}. You may wish to use {@link DateTime#toLocal} and {@link DateTime#toUTC} which provide simple convenience wrappers for commonly used zones.\n     * @param {string|Zone} [zone='local'] - a zone identifier. As a string, that can be any IANA zone supported by the host environment, or a fixed-offset name of the form 'UTC+3', or the strings 'local' or 'utc'. You may also supply an instance of a {@link DateTime#Zone} class.\n     * @param {Object} opts - options\n     * @param {boolean} [opts.keepLocalTime=false] - If true, adjust the underlying time so that the local time stays the same, but in the target zone. You should rarely need this.\n     * @return {DateTime}\n     */;\n    _proto.setZone = function setZone(zone, _temp) {\n      var _ref2 = _temp === void 0 ? {} : _temp,\n        _ref2$keepLocalTime = _ref2.keepLocalTime,\n        keepLocalTime = _ref2$keepLocalTime === void 0 ? false : _ref2$keepLocalTime,\n        _ref2$keepCalendarTim = _ref2.keepCalendarTime,\n        keepCalendarTime = _ref2$keepCalendarTim === void 0 ? false : _ref2$keepCalendarTim;\n      zone = normalizeZone(zone, Settings.defaultZone);\n      if (zone.equals(this.zone)) {\n        return this;\n      } else if (!zone.isValid) {\n        return DateTime.invalid(unsupportedZone(zone));\n      } else {\n        var newTS = this.ts;\n        if (keepLocalTime || keepCalendarTime) {\n          var offsetGuess = zone.offset(this.ts);\n          var asObj = this.toObject();\n          var _objToTS3 = objToTS(asObj, offsetGuess, zone);\n          newTS = _objToTS3[0];\n        }\n        return clone(this, {\n          ts: newTS,\n          zone: zone\n        });\n      }\n    }\n\n    /**\n     * \"Set\" the locale, numberingSystem, or outputCalendar. Returns a newly-constructed DateTime.\n     * @param {Object} properties - the properties to set\n     * @example DateTime.local(2017, 5, 25).reconfigure({ locale: 'en-GB' })\n     * @return {DateTime}\n     */;\n    _proto.reconfigure = function reconfigure(_temp2) {\n      var _ref3 = _temp2 === void 0 ? {} : _temp2,\n        locale = _ref3.locale,\n        numberingSystem = _ref3.numberingSystem,\n        outputCalendar = _ref3.outputCalendar;\n      var loc = this.loc.clone({\n        locale: locale,\n        numberingSystem: numberingSystem,\n        outputCalendar: outputCalendar\n      });\n      return clone(this, {\n        loc: loc\n      });\n    }\n\n    /**\n     * \"Set\" the locale. Returns a newly-constructed DateTime.\n     * Just a convenient alias for reconfigure({ locale })\n     * @example DateTime.local(2017, 5, 25).setLocale('en-GB')\n     * @return {DateTime}\n     */;\n    _proto.setLocale = function setLocale(locale) {\n      return this.reconfigure({\n        locale: locale\n      });\n    }\n\n    /**\n     * \"Set\" the values of specified units. Returns a newly-constructed DateTime.\n     * You can only set units with this method; for \"setting\" metadata, see {@link DateTime#reconfigure} and {@link DateTime#setZone}.\n     *\n     * This method also supports setting locale-based week units, i.e. `localWeekday`, `localWeekNumber` and `localWeekYear`.\n     * They cannot be mixed with ISO-week units like `weekday`.\n     * @param {Object} values - a mapping of units to numbers\n     * @example dt.set({ year: 2017 })\n     * @example dt.set({ hour: 8, minute: 30 })\n     * @example dt.set({ weekday: 5 })\n     * @example dt.set({ year: 2005, ordinal: 234 })\n     * @return {DateTime}\n     */;\n    _proto.set = function set(values) {\n      if (!this.isValid) return this;\n      var normalized = normalizeObject(values, normalizeUnitWithLocalWeeks);\n      var _usesLocalWeekValues2 = usesLocalWeekValues(normalized, this.loc),\n        minDaysInFirstWeek = _usesLocalWeekValues2.minDaysInFirstWeek,\n        startOfWeek = _usesLocalWeekValues2.startOfWeek;\n      var settingWeekStuff = !isUndefined(normalized.weekYear) || !isUndefined(normalized.weekNumber) || !isUndefined(normalized.weekday),\n        containsOrdinal = !isUndefined(normalized.ordinal),\n        containsGregorYear = !isUndefined(normalized.year),\n        containsGregorMD = !isUndefined(normalized.month) || !isUndefined(normalized.day),\n        containsGregor = containsGregorYear || containsGregorMD,\n        definiteWeekDef = normalized.weekYear || normalized.weekNumber;\n      if ((containsGregor || containsOrdinal) && definiteWeekDef) {\n        throw new ConflictingSpecificationError(\"Can't mix weekYear/weekNumber units with year/month/day or ordinals\");\n      }\n      if (containsGregorMD && containsOrdinal) {\n        throw new ConflictingSpecificationError(\"Can't mix ordinal dates with month/day\");\n      }\n      var mixed;\n      if (settingWeekStuff) {\n        mixed = weekToGregorian(_extends({}, gregorianToWeek(this.c, minDaysInFirstWeek, startOfWeek), normalized), minDaysInFirstWeek, startOfWeek);\n      } else if (!isUndefined(normalized.ordinal)) {\n        mixed = ordinalToGregorian(_extends({}, gregorianToOrdinal(this.c), normalized));\n      } else {\n        mixed = _extends({}, this.toObject(), normalized);\n\n        // if we didn't set the day but we ended up on an overflow date,\n        // use the last day of the right month\n        if (isUndefined(normalized.day)) {\n          mixed.day = Math.min(daysInMonth(mixed.year, mixed.month), mixed.day);\n        }\n      }\n      var _objToTS4 = objToTS(mixed, this.o, this.zone),\n        ts = _objToTS4[0],\n        o = _objToTS4[1];\n      return clone(this, {\n        ts: ts,\n        o: o\n      });\n    }\n\n    /**\n     * Add a period of time to this DateTime and return the resulting DateTime\n     *\n     * Adding hours, minutes, seconds, or milliseconds increases the timestamp by the right number of milliseconds. Adding days, months, or years shifts the calendar, accounting for DSTs and leap years along the way. Thus, `dt.plus({ hours: 24 })` may result in a different time than `dt.plus({ days: 1 })` if there's a DST shift in between.\n     * @param {Duration|Object|number} duration - The amount to add. Either a Luxon Duration, a number of milliseconds, the object argument to Duration.fromObject()\n     * @example DateTime.now().plus(123) //~> in 123 milliseconds\n     * @example DateTime.now().plus({ minutes: 15 }) //~> in 15 minutes\n     * @example DateTime.now().plus({ days: 1 }) //~> this time tomorrow\n     * @example DateTime.now().plus({ days: -1 }) //~> this time yesterday\n     * @example DateTime.now().plus({ hours: 3, minutes: 13 }) //~> in 3 hr, 13 min\n     * @example DateTime.now().plus(Duration.fromObject({ hours: 3, minutes: 13 })) //~> in 3 hr, 13 min\n     * @return {DateTime}\n     */;\n    _proto.plus = function plus(duration) {\n      if (!this.isValid) return this;\n      var dur = Duration.fromDurationLike(duration);\n      return clone(this, adjustTime(this, dur));\n    }\n\n    /**\n     * Subtract a period of time to this DateTime and return the resulting DateTime\n     * See {@link DateTime#plus}\n     * @param {Duration|Object|number} duration - The amount to subtract. Either a Luxon Duration, a number of milliseconds, the object argument to Duration.fromObject()\n     @return {DateTime}\n     */;\n    _proto.minus = function minus(duration) {\n      if (!this.isValid) return this;\n      var dur = Duration.fromDurationLike(duration).negate();\n      return clone(this, adjustTime(this, dur));\n    }\n\n    /**\n     * \"Set\" this DateTime to the beginning of a unit of time.\n     * @param {string} unit - The unit to go to the beginning of. Can be 'year', 'quarter', 'month', 'week', 'day', 'hour', 'minute', 'second', or 'millisecond'.\n     * @param {Object} opts - options\n     * @param {boolean} [opts.useLocaleWeeks=false] - If true, use weeks based on the locale, i.e. use the locale-dependent start of the week\n     * @example DateTime.local(2014, 3, 3).startOf('month').toISODate(); //=> '2014-03-01'\n     * @example DateTime.local(2014, 3, 3).startOf('year').toISODate(); //=> '2014-01-01'\n     * @example DateTime.local(2014, 3, 3).startOf('week').toISODate(); //=> '2014-03-03', weeks always start on Mondays\n     * @example DateTime.local(2014, 3, 3, 5, 30).startOf('day').toISOTime(); //=> '00:00.000-05:00'\n     * @example DateTime.local(2014, 3, 3, 5, 30).startOf('hour').toISOTime(); //=> '05:00:00.000-05:00'\n     * @return {DateTime}\n     */;\n    _proto.startOf = function startOf(unit, _temp3) {\n      var _ref4 = _temp3 === void 0 ? {} : _temp3,\n        _ref4$useLocaleWeeks = _ref4.useLocaleWeeks,\n        useLocaleWeeks = _ref4$useLocaleWeeks === void 0 ? false : _ref4$useLocaleWeeks;\n      if (!this.isValid) return this;\n      var o = {},\n        normalizedUnit = Duration.normalizeUnit(unit);\n      switch (normalizedUnit) {\n        case \"years\":\n          o.month = 1;\n        // falls through\n        case \"quarters\":\n        case \"months\":\n          o.day = 1;\n        // falls through\n        case \"weeks\":\n        case \"days\":\n          o.hour = 0;\n        // falls through\n        case \"hours\":\n          o.minute = 0;\n        // falls through\n        case \"minutes\":\n          o.second = 0;\n        // falls through\n        case \"seconds\":\n          o.millisecond = 0;\n          break;\n        // no default, invalid units throw in normalizeUnit()\n      }\n\n      if (normalizedUnit === \"weeks\") {\n        if (useLocaleWeeks) {\n          var startOfWeek = this.loc.getStartOfWeek();\n          var weekday = this.weekday;\n          if (weekday < startOfWeek) {\n            o.weekNumber = this.weekNumber - 1;\n          }\n          o.weekday = startOfWeek;\n        } else {\n          o.weekday = 1;\n        }\n      }\n      if (normalizedUnit === \"quarters\") {\n        var q = Math.ceil(this.month / 3);\n        o.month = (q - 1) * 3 + 1;\n      }\n      return this.set(o);\n    }\n\n    /**\n     * \"Set\" this DateTime to the end (meaning the last millisecond) of a unit of time\n     * @param {string} unit - The unit to go to the end of. Can be 'year', 'quarter', 'month', 'week', 'day', 'hour', 'minute', 'second', or 'millisecond'.\n     * @param {Object} opts - options\n     * @param {boolean} [opts.useLocaleWeeks=false] - If true, use weeks based on the locale, i.e. use the locale-dependent start of the week\n     * @example DateTime.local(2014, 3, 3).endOf('month').toISO(); //=> '2014-03-31T23:59:59.999-05:00'\n     * @example DateTime.local(2014, 3, 3).endOf('year').toISO(); //=> '2014-12-31T23:59:59.999-05:00'\n     * @example DateTime.local(2014, 3, 3).endOf('week').toISO(); // => '2014-03-09T23:59:59.999-05:00', weeks start on Mondays\n     * @example DateTime.local(2014, 3, 3, 5, 30).endOf('day').toISO(); //=> '2014-03-03T23:59:59.999-05:00'\n     * @example DateTime.local(2014, 3, 3, 5, 30).endOf('hour').toISO(); //=> '2014-03-03T05:59:59.999-05:00'\n     * @return {DateTime}\n     */;\n    _proto.endOf = function endOf(unit, opts) {\n      var _this$plus;\n      return this.isValid ? this.plus((_this$plus = {}, _this$plus[unit] = 1, _this$plus)).startOf(unit, opts).minus(1) : this;\n    }\n\n    // OUTPUT\n\n    /**\n     * Returns a string representation of this DateTime formatted according to the specified format string.\n     * **You may not want this.** See {@link DateTime#toLocaleString} for a more flexible formatting tool. For a table of tokens and their interpretations, see [here](https://moment.github.io/luxon/#/formatting?id=table-of-tokens).\n     * Defaults to en-US if no locale has been specified, regardless of the system's locale.\n     * @param {string} fmt - the format string\n     * @param {Object} opts - opts to override the configuration options on this DateTime\n     * @example DateTime.now().toFormat('yyyy LLL dd') //=> '2017 Apr 22'\n     * @example DateTime.now().setLocale('fr').toFormat('yyyy LLL dd') //=> '2017 avr. 22'\n     * @example DateTime.now().toFormat('yyyy LLL dd', { locale: \"fr\" }) //=> '2017 avr. 22'\n     * @example DateTime.now().toFormat(\"HH 'hours and' mm 'minutes'\") //=> '20 hours and 55 minutes'\n     * @return {string}\n     */;\n    _proto.toFormat = function toFormat(fmt, opts) {\n      if (opts === void 0) {\n        opts = {};\n      }\n      return this.isValid ? Formatter.create(this.loc.redefaultToEN(opts)).formatDateTimeFromString(this, fmt) : INVALID;\n    }\n\n    /**\n     * Returns a localized string representing this date. Accepts the same options as the Intl.DateTimeFormat constructor and any presets defined by Luxon, such as `DateTime.DATE_FULL` or `DateTime.TIME_SIMPLE`.\n     * The exact behavior of this method is browser-specific, but in general it will return an appropriate representation\n     * of the DateTime in the assigned locale.\n     * Defaults to the system's locale if no locale has been specified\n     * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DateTimeFormat\n     * @param formatOpts {Object} - Intl.DateTimeFormat constructor options and configuration options\n     * @param {Object} opts - opts to override the configuration options on this DateTime\n     * @example DateTime.now().toLocaleString(); //=> 4/20/2017\n     * @example DateTime.now().setLocale('en-gb').toLocaleString(); //=> '20/04/2017'\n     * @example DateTime.now().toLocaleString(DateTime.DATE_FULL); //=> 'April 20, 2017'\n     * @example DateTime.now().toLocaleString(DateTime.DATE_FULL, { locale: 'fr' }); //=> '28 ao\u00fbt 2022'\n     * @example DateTime.now().toLocaleString(DateTime.TIME_SIMPLE); //=> '11:32 AM'\n     * @example DateTime.now().toLocaleString(DateTime.DATETIME_SHORT); //=> '4/20/2017, 11:32 AM'\n     * @example DateTime.now().toLocaleString({ weekday: 'long', month: 'long', day: '2-digit' }); //=> 'Thursday, April 20'\n     * @example DateTime.now().toLocaleString({ weekday: 'short', month: 'short', day: '2-digit', hour: '2-digit', minute: '2-digit' }); //=> 'Thu, Apr 20, 11:27 AM'\n     * @example DateTime.now().toLocaleString({ hour: '2-digit', minute: '2-digit', hourCycle: 'h23' }); //=> '11:32'\n     * @return {string}\n     */;\n    _proto.toLocaleString = function toLocaleString(formatOpts, opts) {\n      if (formatOpts === void 0) {\n        formatOpts = DATE_SHORT;\n      }\n      if (opts === void 0) {\n        opts = {};\n      }\n      return this.isValid ? Formatter.create(this.loc.clone(opts), formatOpts).formatDateTime(this) : INVALID;\n    }\n\n    /**\n     * Returns an array of format \"parts\", meaning individual tokens along with metadata. This is allows callers to post-process individual sections of the formatted output.\n     * Defaults to the system's locale if no locale has been specified\n     * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DateTimeFormat/formatToParts\n     * @param opts {Object} - Intl.DateTimeFormat constructor options, same as `toLocaleString`.\n     * @example DateTime.now().toLocaleParts(); //=> [\n     *                                   //=>   { type: 'day', value: '25' },\n     *                                   //=>   { type: 'literal', value: '/' },\n     *                                   //=>   { type: 'month', value: '05' },\n     *                                   //=>   { type: 'literal', value: '/' },\n     *                                   //=>   { type: 'year', value: '1982' }\n     *                                   //=> ]\n     */;\n    _proto.toLocaleParts = function toLocaleParts(opts) {\n      if (opts === void 0) {\n        opts = {};\n      }\n      return this.isValid ? Formatter.create(this.loc.clone(opts), opts).formatDateTimeParts(this) : [];\n    }\n\n    /**\n     * Returns an ISO 8601-compliant string representation of this DateTime\n     * @param {Object} opts - options\n     * @param {boolean} [opts.suppressMilliseconds=false] - exclude milliseconds from the format if they're 0\n     * @param {boolean} [opts.suppressSeconds=false] - exclude seconds from the format if they're 0\n     * @param {boolean} [opts.includeOffset=true] - include the offset, such as 'Z' or '-04:00'\n     * @param {boolean} [opts.extendedZone=false] - add the time zone format extension\n     * @param {string} [opts.format='extended'] - choose between the basic and extended format\n     * @example DateTime.utc(1983, 5, 25).toISO() //=> '1982-05-25T00:00:00.000Z'\n     * @example DateTime.now().toISO() //=> '2017-04-22T20:47:05.335-04:00'\n     * @example DateTime.now().toISO({ includeOffset: false }) //=> '2017-04-22T20:47:05.335'\n     * @example DateTime.now().toISO({ format: 'basic' }) //=> '20170422T204705.335-0400'\n     * @return {string}\n     */;\n    _proto.toISO = function toISO(_temp4) {\n      var _ref5 = _temp4 === void 0 ? {} : _temp4,\n        _ref5$format = _ref5.format,\n        format = _ref5$format === void 0 ? \"extended\" : _ref5$format,\n        _ref5$suppressSeconds = _ref5.suppressSeconds,\n        suppressSeconds = _ref5$suppressSeconds === void 0 ? false : _ref5$suppressSeconds,\n        _ref5$suppressMillise = _ref5.suppressMilliseconds,\n        suppressMilliseconds = _ref5$suppressMillise === void 0 ? false : _ref5$suppressMillise,\n        _ref5$includeOffset = _ref5.includeOffset,\n        includeOffset = _ref5$includeOffset === void 0 ? true : _ref5$includeOffset,\n        _ref5$extendedZone = _ref5.extendedZone,\n        extendedZone = _ref5$extendedZone === void 0 ? false : _ref5$extendedZone;\n      if (!this.isValid) {\n        return null;\n      }\n      var ext = format === \"extended\";\n      var c = _toISODate(this, ext);\n      c += \"T\";\n      c += _toISOTime(this, ext, suppressSeconds, suppressMilliseconds, includeOffset, extendedZone);\n      return c;\n    }\n\n    /**\n     * Returns an ISO 8601-compliant string representation of this DateTime's date component\n     * @param {Object} opts - options\n     * @param {string} [opts.format='extended'] - choose between the basic and extended format\n     * @example DateTime.utc(1982, 5, 25).toISODate() //=> '1982-05-25'\n     * @example DateTime.utc(1982, 5, 25).toISODate({ format: 'basic' }) //=> '19820525'\n     * @return {string}\n     */;\n    _proto.toISODate = function toISODate(_temp5) {\n      var _ref6 = _temp5 === void 0 ? {} : _temp5,\n        _ref6$format = _ref6.format,\n        format = _ref6$format === void 0 ? \"extended\" : _ref6$format;\n      if (!this.isValid) {\n        return null;\n      }\n      return _toISODate(this, format === \"extended\");\n    }\n\n    /**\n     * Returns an ISO 8601-compliant string representation of this DateTime's week date\n     * @example DateTime.utc(1982, 5, 25).toISOWeekDate() //=> '1982-W21-2'\n     * @return {string}\n     */;\n    _proto.toISOWeekDate = function toISOWeekDate() {\n      return toTechFormat(this, \"kkkk-'W'WW-c\");\n    }\n\n    /**\n     * Returns an ISO 8601-compliant string representation of this DateTime's time component\n     * @param {Object} opts - options\n     * @param {boolean} [opts.suppressMilliseconds=false] - exclude milliseconds from the format if they're 0\n     * @param {boolean} [opts.suppressSeconds=false] - exclude seconds from the format if they're 0\n     * @param {boolean} [opts.includeOffset=true] - include the offset, such as 'Z' or '-04:00'\n     * @param {boolean} [opts.extendedZone=true] - add the time zone format extension\n     * @param {boolean} [opts.includePrefix=false] - include the `T` prefix\n     * @param {string} [opts.format='extended'] - choose between the basic and extended format\n     * @example DateTime.utc().set({ hour: 7, minute: 34 }).toISOTime() //=> '07:34:19.361Z'\n     * @example DateTime.utc().set({ hour: 7, minute: 34, seconds: 0, milliseconds: 0 }).toISOTime({ suppressSeconds: true }) //=> '07:34Z'\n     * @example DateTime.utc().set({ hour: 7, minute: 34 }).toISOTime({ format: 'basic' }) //=> '073419.361Z'\n     * @example DateTime.utc().set({ hour: 7, minute: 34 }).toISOTime({ includePrefix: true }) //=> 'T07:34:19.361Z'\n     * @return {string}\n     */;\n    _proto.toISOTime = function toISOTime(_temp6) {\n      var _ref7 = _temp6 === void 0 ? {} : _temp6,\n        _ref7$suppressMillise = _ref7.suppressMilliseconds,\n        suppressMilliseconds = _ref7$suppressMillise === void 0 ? false : _ref7$suppressMillise,\n        _ref7$suppressSeconds = _ref7.suppressSeconds,\n        suppressSeconds = _ref7$suppressSeconds === void 0 ? false : _ref7$suppressSeconds,\n        _ref7$includeOffset = _ref7.includeOffset,\n        includeOffset = _ref7$includeOffset === void 0 ? true : _ref7$includeOffset,\n        _ref7$includePrefix = _ref7.includePrefix,\n        includePrefix = _ref7$includePrefix === void 0 ? false : _ref7$includePrefix,\n        _ref7$extendedZone = _ref7.extendedZone,\n        extendedZone = _ref7$extendedZone === void 0 ? false : _ref7$extendedZone,\n        _ref7$format = _ref7.format,\n        format = _ref7$format === void 0 ? \"extended\" : _ref7$format;\n      if (!this.isValid) {\n        return null;\n      }\n      var c = includePrefix ? \"T\" : \"\";\n      return c + _toISOTime(this, format === \"extended\", suppressSeconds, suppressMilliseconds, includeOffset, extendedZone);\n    }\n\n    /**\n     * Returns an RFC 2822-compatible string representation of this DateTime\n     * @example DateTime.utc(2014, 7, 13).toRFC2822() //=> 'Sun, 13 Jul 2014 00:00:00 +0000'\n     * @example DateTime.local(2014, 7, 13).toRFC2822() //=> 'Sun, 13 Jul 2014 00:00:00 -0400'\n     * @return {string}\n     */;\n    _proto.toRFC2822 = function toRFC2822() {\n      return toTechFormat(this, \"EEE, dd LLL yyyy HH:mm:ss ZZZ\", false);\n    }\n\n    /**\n     * Returns a string representation of this DateTime appropriate for use in HTTP headers. The output is always expressed in GMT.\n     * Specifically, the string conforms to RFC 1123.\n     * @see https://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.3.1\n     * @example DateTime.utc(2014, 7, 13).toHTTP() //=> 'Sun, 13 Jul 2014 00:00:00 GMT'\n     * @example DateTime.utc(2014, 7, 13, 19).toHTTP() //=> 'Sun, 13 Jul 2014 19:00:00 GMT'\n     * @return {string}\n     */;\n    _proto.toHTTP = function toHTTP() {\n      return toTechFormat(this.toUTC(), \"EEE, dd LLL yyyy HH:mm:ss 'GMT'\");\n    }\n\n    /**\n     * Returns a string representation of this DateTime appropriate for use in SQL Date\n     * @example DateTime.utc(2014, 7, 13).toSQLDate() //=> '2014-07-13'\n     * @return {string}\n     */;\n    _proto.toSQLDate = function toSQLDate() {\n      if (!this.isValid) {\n        return null;\n      }\n      return _toISODate(this, true);\n    }\n\n    /**\n     * Returns a string representation of this DateTime appropriate for use in SQL Time\n     * @param {Object} opts - options\n     * @param {boolean} [opts.includeZone=false] - include the zone, such as 'America/New_York'. Overrides includeOffset.\n     * @param {boolean} [opts.includeOffset=true] - include the offset, such as 'Z' or '-04:00'\n     * @param {boolean} [opts.includeOffsetSpace=true] - include the space between the time and the offset, such as '05:15:16.345 -04:00'\n     * @example DateTime.utc().toSQL() //=> '05:15:16.345'\n     * @example DateTime.now().toSQL() //=> '05:15:16.345 -04:00'\n     * @example DateTime.now().toSQL({ includeOffset: false }) //=> '05:15:16.345'\n     * @example DateTime.now().toSQL({ includeZone: false }) //=> '05:15:16.345 America/New_York'\n     * @return {string}\n     */;\n    _proto.toSQLTime = function toSQLTime(_temp7) {\n      var _ref8 = _temp7 === void 0 ? {} : _temp7,\n        _ref8$includeOffset = _ref8.includeOffset,\n        includeOffset = _ref8$includeOffset === void 0 ? true : _ref8$includeOffset,\n        _ref8$includeZone = _ref8.includeZone,\n        includeZone = _ref8$includeZone === void 0 ? false : _ref8$includeZone,\n        _ref8$includeOffsetSp = _ref8.includeOffsetSpace,\n        includeOffsetSpace = _ref8$includeOffsetSp === void 0 ? true : _ref8$includeOffsetSp;\n      var fmt = \"HH:mm:ss.SSS\";\n      if (includeZone || includeOffset) {\n        if (includeOffsetSpace) {\n          fmt += \" \";\n        }\n        if (includeZone) {\n          fmt += \"z\";\n        } else if (includeOffset) {\n          fmt += \"ZZ\";\n        }\n      }\n      return toTechFormat(this, fmt, true);\n    }\n\n    /**\n     * Returns a string representation of this DateTime appropriate for use in SQL DateTime\n     * @param {Object} opts - options\n     * @param {boolean} [opts.includeZone=false] - include the zone, such as 'America/New_York'. Overrides includeOffset.\n     * @param {boolean} [opts.includeOffset=true] - include the offset, such as 'Z' or '-04:00'\n     * @param {boolean} [opts.includeOffsetSpace=true] - include the space between the time and the offset, such as '05:15:16.345 -04:00'\n     * @example DateTime.utc(2014, 7, 13).toSQL() //=> '2014-07-13 00:00:00.000 Z'\n     * @example DateTime.local(2014, 7, 13).toSQL() //=> '2014-07-13 00:00:00.000 -04:00'\n     * @example DateTime.local(2014, 7, 13).toSQL({ includeOffset: false }) //=> '2014-07-13 00:00:00.000'\n     * @example DateTime.local(2014, 7, 13).toSQL({ includeZone: true }) //=> '2014-07-13 00:00:00.000 America/New_York'\n     * @return {string}\n     */;\n    _proto.toSQL = function toSQL(opts) {\n      if (opts === void 0) {\n        opts = {};\n      }\n      if (!this.isValid) {\n        return null;\n      }\n      return this.toSQLDate() + \" \" + this.toSQLTime(opts);\n    }\n\n    /**\n     * Returns a string representation of this DateTime appropriate for debugging\n     * @return {string}\n     */;\n    _proto.toString = function toString() {\n      return this.isValid ? this.toISO() : INVALID;\n    }\n\n    /**\n     * Returns a string representation of this DateTime appropriate for the REPL.\n     * @return {string}\n     */;\n    _proto[_Symbol$for] = function () {\n      if (this.isValid) {\n        return \"DateTime { ts: \" + this.toISO() + \", zone: \" + this.zone.name + \", locale: \" + this.locale + \" }\";\n      } else {\n        return \"DateTime { Invalid, reason: \" + this.invalidReason + \" }\";\n      }\n    }\n\n    /**\n     * Returns the epoch milliseconds of this DateTime. Alias of {@link DateTime#toMillis}\n     * @return {number}\n     */;\n    _proto.valueOf = function valueOf() {\n      return this.toMillis();\n    }\n\n    /**\n     * Returns the epoch milliseconds of this DateTime.\n     * @return {number}\n     */;\n    _proto.toMillis = function toMillis() {\n      return this.isValid ? this.ts : NaN;\n    }\n\n    /**\n     * Returns the epoch seconds of this DateTime.\n     * @return {number}\n     */;\n    _proto.toSeconds = function toSeconds() {\n      return this.isValid ? this.ts / 1000 : NaN;\n    }\n\n    /**\n     * Returns the epoch seconds (as a whole number) of this DateTime.\n     * @return {number}\n     */;\n    _proto.toUnixInteger = function toUnixInteger() {\n      return this.isValid ? Math.floor(this.ts / 1000) : NaN;\n    }\n\n    /**\n     * Returns an ISO 8601 representation of this DateTime appropriate for use in JSON.\n     * @return {string}\n     */;\n    _proto.toJSON = function toJSON() {\n      return this.toISO();\n    }\n\n    /**\n     * Returns a BSON serializable equivalent to this DateTime.\n     * @return {Date}\n     */;\n    _proto.toBSON = function toBSON() {\n      return this.toJSDate();\n    }\n\n    /**\n     * Returns a JavaScript object with this DateTime's year, month, day, and so on.\n     * @param opts - options for generating the object\n     * @param {boolean} [opts.includeConfig=false] - include configuration attributes in the output\n     * @example DateTime.now().toObject() //=> { year: 2017, month: 4, day: 22, hour: 20, minute: 49, second: 42, millisecond: 268 }\n     * @return {Object}\n     */;\n    _proto.toObject = function toObject(opts) {\n      if (opts === void 0) {\n        opts = {};\n      }\n      if (!this.isValid) return {};\n      var base = _extends({}, this.c);\n      if (opts.includeConfig) {\n        base.outputCalendar = this.outputCalendar;\n        base.numberingSystem = this.loc.numberingSystem;\n        base.locale = this.loc.locale;\n      }\n      return base;\n    }\n\n    /**\n     * Returns a JavaScript Date equivalent to this DateTime.\n     * @return {Date}\n     */;\n    _proto.toJSDate = function toJSDate() {\n      return new Date(this.isValid ? this.ts : NaN);\n    }\n\n    // COMPARE\n\n    /**\n     * Return the difference between two DateTimes as a Duration.\n     * @param {DateTime} otherDateTime - the DateTime to compare this one to\n     * @param {string|string[]} [unit=['milliseconds']] - the unit or array of units (such as 'hours' or 'days') to include in the duration.\n     * @param {Object} opts - options that affect the creation of the Duration\n     * @param {string} [opts.conversionAccuracy='casual'] - the conversion system to use\n     * @example\n     * var i1 = DateTime.fromISO('1982-05-25T09:45'),\n     *     i2 = DateTime.fromISO('1983-10-14T10:30');\n     * i2.diff(i1).toObject() //=> { milliseconds: 43807500000 }\n     * i2.diff(i1, 'hours').toObject() //=> { hours: 12168.75 }\n     * i2.diff(i1, ['months', 'days']).toObject() //=> { months: 16, days: 19.03125 }\n     * i2.diff(i1, ['months', 'days', 'hours']).toObject() //=> { months: 16, days: 19, hours: 0.75 }\n     * @return {Duration}\n     */;\n    _proto.diff = function diff(otherDateTime, unit, opts) {\n      if (unit === void 0) {\n        unit = \"milliseconds\";\n      }\n      if (opts === void 0) {\n        opts = {};\n      }\n      if (!this.isValid || !otherDateTime.isValid) {\n        return Duration.invalid(\"created by diffing an invalid DateTime\");\n      }\n      var durOpts = _extends({\n        locale: this.locale,\n        numberingSystem: this.numberingSystem\n      }, opts);\n      var units = maybeArray(unit).map(Duration.normalizeUnit),\n        otherIsLater = otherDateTime.valueOf() > this.valueOf(),\n        earlier = otherIsLater ? this : otherDateTime,\n        later = otherIsLater ? otherDateTime : this,\n        diffed = _diff(earlier, later, units, durOpts);\n      return otherIsLater ? diffed.negate() : diffed;\n    }\n\n    /**\n     * Return the difference between this DateTime and right now.\n     * See {@link DateTime#diff}\n     * @param {string|string[]} [unit=['milliseconds']] - the unit or units units (such as 'hours' or 'days') to include in the duration\n     * @param {Object} opts - options that affect the creation of the Duration\n     * @param {string} [opts.conversionAccuracy='casual'] - the conversion system to use\n     * @return {Duration}\n     */;\n    _proto.diffNow = function diffNow(unit, opts) {\n      if (unit === void 0) {\n        unit = \"milliseconds\";\n      }\n      if (opts === void 0) {\n        opts = {};\n      }\n      return this.diff(DateTime.now(), unit, opts);\n    }\n\n    /**\n     * Return an Interval spanning between this DateTime and another DateTime\n     * @param {DateTime} otherDateTime - the other end point of the Interval\n     * @return {Interval}\n     */;\n    _proto.until = function until(otherDateTime) {\n      return this.isValid ? Interval.fromDateTimes(this, otherDateTime) : this;\n    }\n\n    /**\n     * Return whether this DateTime is in the same unit of time as another DateTime.\n     * Higher-order units must also be identical for this function to return `true`.\n     * Note that time zones are **ignored** in this comparison, which compares the **local** calendar time. Use {@link DateTime#setZone} to convert one of the dates if needed.\n     * @param {DateTime} otherDateTime - the other DateTime\n     * @param {string} unit - the unit of time to check sameness on\n     * @param {Object} opts - options\n     * @param {boolean} [opts.useLocaleWeeks=false] - If true, use weeks based on the locale, i.e. use the locale-dependent start of the week; only the locale of this DateTime is used\n     * @example DateTime.now().hasSame(otherDT, 'day'); //~> true if otherDT is in the same current calendar day\n     * @return {boolean}\n     */;\n    _proto.hasSame = function hasSame(otherDateTime, unit, opts) {\n      if (!this.isValid) return false;\n      var inputMs = otherDateTime.valueOf();\n      var adjustedToZone = this.setZone(otherDateTime.zone, {\n        keepLocalTime: true\n      });\n      return adjustedToZone.startOf(unit, opts) <= inputMs && inputMs <= adjustedToZone.endOf(unit, opts);\n    }\n\n    /**\n     * Equality check\n     * Two DateTimes are equal if and only if they represent the same millisecond, have the same zone and location, and are both valid.\n     * To compare just the millisecond values, use `+dt1 === +dt2`.\n     * @param {DateTime} other - the other DateTime\n     * @return {boolean}\n     */;\n    _proto.equals = function equals(other) {\n      return this.isValid && other.isValid && this.valueOf() === other.valueOf() && this.zone.equals(other.zone) && this.loc.equals(other.loc);\n    }\n\n    /**\n     * Returns a string representation of a this time relative to now, such as \"in two days\". Can only internationalize if your\n     * platform supports Intl.RelativeTimeFormat. Rounds down by default.\n     * @param {Object} options - options that affect the output\n     * @param {DateTime} [options.base=DateTime.now()] - the DateTime to use as the basis to which this time is compared. Defaults to now.\n     * @param {string} [options.style=\"long\"] - the style of units, must be \"long\", \"short\", or \"narrow\"\n     * @param {string|string[]} options.unit - use a specific unit or array of units; if omitted, or an array, the method will pick the best unit. Use an array or one of \"years\", \"quarters\", \"months\", \"weeks\", \"days\", \"hours\", \"minutes\", or \"seconds\"\n     * @param {boolean} [options.round=true] - whether to round the numbers in the output.\n     * @param {number} [options.padding=0] - padding in milliseconds. This allows you to round up the result if it fits inside the threshold. Don't use in combination with {round: false} because the decimal output will include the padding.\n     * @param {string} options.locale - override the locale of this DateTime\n     * @param {string} options.numberingSystem - override the numberingSystem of this DateTime. The Intl system may choose not to honor this\n     * @example DateTime.now().plus({ days: 1 }).toRelative() //=> \"in 1 day\"\n     * @example DateTime.now().setLocale(\"es\").toRelative({ days: 1 }) //=> \"dentro de 1 d\u00eda\"\n     * @example DateTime.now().plus({ days: 1 }).toRelative({ locale: \"fr\" }) //=> \"dans 23 heures\"\n     * @example DateTime.now().minus({ days: 2 }).toRelative() //=> \"2 days ago\"\n     * @example DateTime.now().minus({ days: 2 }).toRelative({ unit: \"hours\" }) //=> \"48 hours ago\"\n     * @example DateTime.now().minus({ hours: 36 }).toRelative({ round: false }) //=> \"1.5 days ago\"\n     */;\n    _proto.toRelative = function toRelative(options) {\n      if (options === void 0) {\n        options = {};\n      }\n      if (!this.isValid) return null;\n      var base = options.base || DateTime.fromObject({}, {\n          zone: this.zone\n        }),\n        padding = options.padding ? this < base ? -options.padding : options.padding : 0;\n      var units = [\"years\", \"months\", \"days\", \"hours\", \"minutes\", \"seconds\"];\n      var unit = options.unit;\n      if (Array.isArray(options.unit)) {\n        units = options.unit;\n        unit = undefined;\n      }\n      return diffRelative(base, this.plus(padding), _extends({}, options, {\n        numeric: \"always\",\n        units: units,\n        unit: unit\n      }));\n    }\n\n    /**\n     * Returns a string representation of this date relative to today, such as \"yesterday\" or \"next month\".\n     * Only internationalizes on platforms that supports Intl.RelativeTimeFormat.\n     * @param {Object} options - options that affect the output\n     * @param {DateTime} [options.base=DateTime.now()] - the DateTime to use as the basis to which this time is compared. Defaults to now.\n     * @param {string} options.locale - override the locale of this DateTime\n     * @param {string} options.unit - use a specific unit; if omitted, the method will pick the unit. Use one of \"years\", \"quarters\", \"months\", \"weeks\", or \"days\"\n     * @param {string} options.numberingSystem - override the numberingSystem of this DateTime. The Intl system may choose not to honor this\n     * @example DateTime.now().plus({ days: 1 }).toRelativeCalendar() //=> \"tomorrow\"\n     * @example DateTime.now().setLocale(\"es\").plus({ days: 1 }).toRelative() //=> \"\"ma\u00f1ana\"\n     * @example DateTime.now().plus({ days: 1 }).toRelativeCalendar({ locale: \"fr\" }) //=> \"demain\"\n     * @example DateTime.now().minus({ days: 2 }).toRelativeCalendar() //=> \"2 days ago\"\n     */;\n    _proto.toRelativeCalendar = function toRelativeCalendar(options) {\n      if (options === void 0) {\n        options = {};\n      }\n      if (!this.isValid) return null;\n      return diffRelative(options.base || DateTime.fromObject({}, {\n        zone: this.zone\n      }), this, _extends({}, options, {\n        numeric: \"auto\",\n        units: [\"years\", \"months\", \"days\"],\n        calendary: true\n      }));\n    }\n\n    /**\n     * Return the min of several date times\n     * @param {...DateTime} dateTimes - the DateTimes from which to choose the minimum\n     * @return {DateTime} the min DateTime, or undefined if called with no argument\n     */;\n    DateTime.min = function min() {\n      for (var _len = arguments.length, dateTimes = new Array(_len), _key = 0; _key < _len; _key++) {\n        dateTimes[_key] = arguments[_key];\n      }\n      if (!dateTimes.every(DateTime.isDateTime)) {\n        throw new InvalidArgumentError(\"min requires all arguments be DateTimes\");\n      }\n      return bestBy(dateTimes, function (i) {\n        return i.valueOf();\n      }, Math.min);\n    }\n\n    /**\n     * Return the max of several date times\n     * @param {...DateTime} dateTimes - the DateTimes from which to choose the maximum\n     * @return {DateTime} the max DateTime, or undefined if called with no argument\n     */;\n    DateTime.max = function max() {\n      for (var _len2 = arguments.length, dateTimes = new Array(_len2), _key2 = 0; _key2 < _len2; _key2++) {\n        dateTimes[_key2] = arguments[_key2];\n      }\n      if (!dateTimes.every(DateTime.isDateTime)) {\n        throw new InvalidArgumentError(\"max requires all arguments be DateTimes\");\n      }\n      return bestBy(dateTimes, function (i) {\n        return i.valueOf();\n      }, Math.max);\n    }\n\n    // MISC\n\n    /**\n     * Explain how a string would be parsed by fromFormat()\n     * @param {string} text - the string to parse\n     * @param {string} fmt - the format the string is expected to be in (see description)\n     * @param {Object} options - options taken by fromFormat()\n     * @return {Object}\n     */;\n    DateTime.fromFormatExplain = function fromFormatExplain(text, fmt, options) {\n      if (options === void 0) {\n        options = {};\n      }\n      var _options = options,\n        _options$locale = _options.locale,\n        locale = _options$locale === void 0 ? null : _options$locale,\n        _options$numberingSys = _options.numberingSystem,\n        numberingSystem = _options$numberingSys === void 0 ? null : _options$numberingSys,\n        localeToUse = Locale.fromOpts({\n          locale: locale,\n          numberingSystem: numberingSystem,\n          defaultToEN: true\n        });\n      return explainFromTokens(localeToUse, text, fmt);\n    }\n\n    /**\n     * @deprecated use fromFormatExplain instead\n     */;\n    DateTime.fromStringExplain = function fromStringExplain(text, fmt, options) {\n      if (options === void 0) {\n        options = {};\n      }\n      return DateTime.fromFormatExplain(text, fmt, options);\n    }\n\n    /**\n     * Build a parser for `fmt` using the given locale. This parser can be passed\n     * to {@link DateTime.fromFormatParser} to a parse a date in this format. This\n     * can be used to optimize cases where many dates need to be parsed in a\n     * specific format.\n     *\n     * @param {String} fmt - the format the string is expected to be in (see\n     * description)\n     * @param {Object} options - options used to set locale and numberingSystem\n     * for parser\n     * @returns {TokenParser} - opaque object to be used\n     */;\n    DateTime.buildFormatParser = function buildFormatParser(fmt, options) {\n      if (options === void 0) {\n        options = {};\n      }\n      var _options2 = options,\n        _options2$locale = _options2.locale,\n        locale = _options2$locale === void 0 ? null : _options2$locale,\n        _options2$numberingSy = _options2.numberingSystem,\n        numberingSystem = _options2$numberingSy === void 0 ? null : _options2$numberingSy,\n        localeToUse = Locale.fromOpts({\n          locale: locale,\n          numberingSystem: numberingSystem,\n          defaultToEN: true\n        });\n      return new TokenParser(localeToUse, fmt);\n    }\n\n    /**\n     * Create a DateTime from an input string and format parser.\n     *\n     * The format parser must have been created with the same locale as this call.\n     *\n     * @param {String} text - the string to parse\n     * @param {TokenParser} formatParser - parser from {@link DateTime.buildFormatParser}\n     * @param {Object} opts - options taken by fromFormat()\n     * @returns {DateTime}\n     */;\n    DateTime.fromFormatParser = function fromFormatParser(text, formatParser, opts) {\n      if (opts === void 0) {\n        opts = {};\n      }\n      if (isUndefined(text) || isUndefined(formatParser)) {\n        throw new InvalidArgumentError(\"fromFormatParser requires an input string and a format parser\");\n      }\n      var _opts2 = opts,\n        _opts2$locale = _opts2.locale,\n        locale = _opts2$locale === void 0 ? null : _opts2$locale,\n        _opts2$numberingSyste = _opts2.numberingSystem,\n        numberingSystem = _opts2$numberingSyste === void 0 ? null : _opts2$numberingSyste,\n        localeToUse = Locale.fromOpts({\n          locale: locale,\n          numberingSystem: numberingSystem,\n          defaultToEN: true\n        });\n      if (!localeToUse.equals(formatParser.locale)) {\n        throw new InvalidArgumentError(\"fromFormatParser called with a locale of \" + localeToUse + \", \" + (\"but the format parser was created for \" + formatParser.locale));\n      }\n      var _formatParser$explain = formatParser.explainFromTokens(text),\n        result = _formatParser$explain.result,\n        zone = _formatParser$explain.zone,\n        specificOffset = _formatParser$explain.specificOffset,\n        invalidReason = _formatParser$explain.invalidReason;\n      if (invalidReason) {\n        return DateTime.invalid(invalidReason);\n      } else {\n        return parseDataToDateTime(result, zone, opts, \"format \" + formatParser.format, text, specificOffset);\n      }\n    }\n\n    // FORMAT PRESETS\n\n    /**\n     * {@link DateTime#toLocaleString} format like 10/14/1983\n     * @type {Object}\n     */;\n    _createClass(DateTime, [{\n      key: \"isValid\",\n      get: function get() {\n        return this.invalid === null;\n      }\n\n      /**\n       * Returns an error code if this DateTime is invalid, or null if the DateTime is valid\n       * @type {string}\n       */\n    }, {\n      key: \"invalidReason\",\n      get: function get() {\n        return this.invalid ? this.invalid.reason : null;\n      }\n\n      /**\n       * Returns an explanation of why this DateTime became invalid, or null if the DateTime is valid\n       * @type {string}\n       */\n    }, {\n      key: \"invalidExplanation\",\n      get: function get() {\n        return this.invalid ? this.invalid.explanation : null;\n      }\n\n      /**\n       * Get the locale of a DateTime, such 'en-GB'. The locale is used when formatting the DateTime\n       *\n       * @type {string}\n       */\n    }, {\n      key: \"locale\",\n      get: function get() {\n        return this.isValid ? this.loc.locale : null;\n      }\n\n      /**\n       * Get the numbering system of a DateTime, such 'beng'. The numbering system is used when formatting the DateTime\n       *\n       * @type {string}\n       */\n    }, {\n      key: \"numberingSystem\",\n      get: function get() {\n        return this.isValid ? this.loc.numberingSystem : null;\n      }\n\n      /**\n       * Get the output calendar of a DateTime, such 'islamic'. The output calendar is used when formatting the DateTime\n       *\n       * @type {string}\n       */\n    }, {\n      key: \"outputCalendar\",\n      get: function get() {\n        return this.isValid ? this.loc.outputCalendar : null;\n      }\n\n      /**\n       * Get the time zone associated with this DateTime.\n       * @type {Zone}\n       */\n    }, {\n      key: \"zone\",\n      get: function get() {\n        return this._zone;\n      }\n\n      /**\n       * Get the name of the time zone.\n       * @type {string}\n       */\n    }, {\n      key: \"zoneName\",\n      get: function get() {\n        return this.isValid ? this.zone.name : null;\n      }\n\n      /**\n       * Get the year\n       * @example DateTime.local(2017, 5, 25).year //=> 2017\n       * @type {number}\n       */\n    }, {\n      key: \"year\",\n      get: function get() {\n        return this.isValid ? this.c.year : NaN;\n      }\n\n      /**\n       * Get the quarter\n       * @example DateTime.local(2017, 5, 25).quarter //=> 2\n       * @type {number}\n       */\n    }, {\n      key: \"quarter\",\n      get: function get() {\n        return this.isValid ? Math.ceil(this.c.month / 3) : NaN;\n      }\n\n      /**\n       * Get the month (1-12).\n       * @example DateTime.local(2017, 5, 25).month //=> 5\n       * @type {number}\n       */\n    }, {\n      key: \"month\",\n      get: function get() {\n        return this.isValid ? this.c.month : NaN;\n      }\n\n      /**\n       * Get the day of the month (1-30ish).\n       * @example DateTime.local(2017, 5, 25).day //=> 25\n       * @type {number}\n       */\n    }, {\n      key: \"day\",\n      get: function get() {\n        return this.isValid ? this.c.day : NaN;\n      }\n\n      /**\n       * Get the hour of the day (0-23).\n       * @example DateTime.local(2017, 5, 25, 9).hour //=> 9\n       * @type {number}\n       */\n    }, {\n      key: \"hour\",\n      get: function get() {\n        return this.isValid ? this.c.hour : NaN;\n      }\n\n      /**\n       * Get the minute of the hour (0-59).\n       * @example DateTime.local(2017, 5, 25, 9, 30).minute //=> 30\n       * @type {number}\n       */\n    }, {\n      key: \"minute\",\n      get: function get() {\n        return this.isValid ? this.c.minute : NaN;\n      }\n\n      /**\n       * Get the second of the minute (0-59).\n       * @example DateTime.local(2017, 5, 25, 9, 30, 52).second //=> 52\n       * @type {number}\n       */\n    }, {\n      key: \"second\",\n      get: function get() {\n        return this.isValid ? this.c.second : NaN;\n      }\n\n      /**\n       * Get the millisecond of the second (0-999).\n       * @example DateTime.local(2017, 5, 25, 9, 30, 52, 654).millisecond //=> 654\n       * @type {number}\n       */\n    }, {\n      key: \"millisecond\",\n      get: function get() {\n        return this.isValid ? this.c.millisecond : NaN;\n      }\n\n      /**\n       * Get the week year\n       * @see https://en.wikipedia.org/wiki/ISO_week_date\n       * @example DateTime.local(2014, 12, 31).weekYear //=> 2015\n       * @type {number}\n       */\n    }, {\n      key: \"weekYear\",\n      get: function get() {\n        return this.isValid ? possiblyCachedWeekData(this).weekYear : NaN;\n      }\n\n      /**\n       * Get the week number of the week year (1-52ish).\n       * @see https://en.wikipedia.org/wiki/ISO_week_date\n       * @example DateTime.local(2017, 5, 25).weekNumber //=> 21\n       * @type {number}\n       */\n    }, {\n      key: \"weekNumber\",\n      get: function get() {\n        return this.isValid ? possiblyCachedWeekData(this).weekNumber : NaN;\n      }\n\n      /**\n       * Get the day of the week.\n       * 1 is Monday and 7 is Sunday\n       * @see https://en.wikipedia.org/wiki/ISO_week_date\n       * @example DateTime.local(2014, 11, 31).weekday //=> 4\n       * @type {number}\n       */\n    }, {\n      key: \"weekday\",\n      get: function get() {\n        return this.isValid ? possiblyCachedWeekData(this).weekday : NaN;\n      }\n\n      /**\n       * Returns true if this date is on a weekend according to the locale, false otherwise\n       * @returns {boolean}\n       */\n    }, {\n      key: \"isWeekend\",\n      get: function get() {\n        return this.isValid && this.loc.getWeekendDays().includes(this.weekday);\n      }\n\n      /**\n       * Get the day of the week according to the locale.\n       * 1 is the first day of the week and 7 is the last day of the week.\n       * If the locale assigns Sunday as the first day of the week, then a date which is a Sunday will return 1,\n       * @returns {number}\n       */\n    }, {\n      key: \"localWeekday\",\n      get: function get() {\n        return this.isValid ? possiblyCachedLocalWeekData(this).weekday : NaN;\n      }\n\n      /**\n       * Get the week number of the week year according to the locale. Different locales assign week numbers differently,\n       * because the week can start on different days of the week (see localWeekday) and because a different number of days\n       * is required for a week to count as the first week of a year.\n       * @returns {number}\n       */\n    }, {\n      key: \"localWeekNumber\",\n      get: function get() {\n        return this.isValid ? possiblyCachedLocalWeekData(this).weekNumber : NaN;\n      }\n\n      /**\n       * Get the week year according to the locale. Different locales assign week numbers (and therefor week years)\n       * differently, see localWeekNumber.\n       * @returns {number}\n       */\n    }, {\n      key: \"localWeekYear\",\n      get: function get() {\n        return this.isValid ? possiblyCachedLocalWeekData(this).weekYear : NaN;\n      }\n\n      /**\n       * Get the ordinal (meaning the day of the year)\n       * @example DateTime.local(2017, 5, 25).ordinal //=> 145\n       * @type {number|DateTime}\n       */\n    }, {\n      key: \"ordinal\",\n      get: function get() {\n        return this.isValid ? gregorianToOrdinal(this.c).ordinal : NaN;\n      }\n\n      /**\n       * Get the human readable short month name, such as 'Oct'.\n       * Defaults to the system's locale if no locale has been specified\n       * @example DateTime.local(2017, 10, 30).monthShort //=> Oct\n       * @type {string}\n       */\n    }, {\n      key: \"monthShort\",\n      get: function get() {\n        return this.isValid ? Info.months(\"short\", {\n          locObj: this.loc\n        })[this.month - 1] : null;\n      }\n\n      /**\n       * Get the human readable long month name, such as 'October'.\n       * Defaults to the system's locale if no locale has been specified\n       * @example DateTime.local(2017, 10, 30).monthLong //=> October\n       * @type {string}\n       */\n    }, {\n      key: \"monthLong\",\n      get: function get() {\n        return this.isValid ? Info.months(\"long\", {\n          locObj: this.loc\n        })[this.month - 1] : null;\n      }\n\n      /**\n       * Get the human readable short weekday, such as 'Mon'.\n       * Defaults to the system's locale if no locale has been specified\n       * @example DateTime.local(2017, 10, 30).weekdayShort //=> Mon\n       * @type {string}\n       */\n    }, {\n      key: \"weekdayShort\",\n      get: function get() {\n        return this.isValid ? Info.weekdays(\"short\", {\n          locObj: this.loc\n        })[this.weekday - 1] : null;\n      }\n\n      /**\n       * Get the human readable long weekday, such as 'Monday'.\n       * Defaults to the system's locale if no locale has been specified\n       * @example DateTime.local(2017, 10, 30).weekdayLong //=> Monday\n       * @type {string}\n       */\n    }, {\n      key: \"weekdayLong\",\n      get: function get() {\n        return this.isValid ? Info.weekdays(\"long\", {\n          locObj: this.loc\n        })[this.weekday - 1] : null;\n      }\n\n      /**\n       * Get the UTC offset of this DateTime in minutes\n       * @example DateTime.now().offset //=> -240\n       * @example DateTime.utc().offset //=> 0\n       * @type {number}\n       */\n    }, {\n      key: \"offset\",\n      get: function get() {\n        return this.isValid ? +this.o : NaN;\n      }\n\n      /**\n       * Get the short human name for the zone's current offset, for example \"EST\" or \"EDT\".\n       * Defaults to the system's locale if no locale has been specified\n       * @type {string}\n       */\n    }, {\n      key: \"offsetNameShort\",\n      get: function get() {\n        if (this.isValid) {\n          return this.zone.offsetName(this.ts, {\n            format: \"short\",\n            locale: this.locale\n          });\n        } else {\n          return null;\n        }\n      }\n\n      /**\n       * Get the long human name for the zone's current offset, for example \"Eastern Standard Time\" or \"Eastern Daylight Time\".\n       * Defaults to the system's locale if no locale has been specified\n       * @type {string}\n       */\n    }, {\n      key: \"offsetNameLong\",\n      get: function get() {\n        if (this.isValid) {\n          return this.zone.offsetName(this.ts, {\n            format: \"long\",\n            locale: this.locale\n          });\n        } else {\n          return null;\n        }\n      }\n\n      /**\n       * Get whether this zone's offset ever changes, as in a DST.\n       * @type {boolean}\n       */\n    }, {\n      key: \"isOffsetFixed\",\n      get: function get() {\n        return this.isValid ? this.zone.isUniversal : null;\n      }\n\n      /**\n       * Get whether the DateTime is in a DST.\n       * @type {boolean}\n       */\n    }, {\n      key: \"isInDST\",\n      get: function get() {\n        if (this.isOffsetFixed) {\n          return false;\n        } else {\n          return this.offset > this.set({\n            month: 1,\n            day: 1\n          }).offset || this.offset > this.set({\n            month: 5\n          }).offset;\n        }\n      }\n    }, {\n      key: \"isInLeapYear\",\n      get: function get() {\n        return isLeapYear(this.year);\n      }\n\n      /**\n       * Returns the number of days in this DateTime's month\n       * @example DateTime.local(2016, 2).daysInMonth //=> 29\n       * @example DateTime.local(2016, 3).daysInMonth //=> 31\n       * @type {number}\n       */\n    }, {\n      key: \"daysInMonth\",\n      get: function get() {\n        return daysInMonth(this.year, this.month);\n      }\n\n      /**\n       * Returns the number of days in this DateTime's year\n       * @example DateTime.local(2016).daysInYear //=> 366\n       * @example DateTime.local(2013).daysInYear //=> 365\n       * @type {number}\n       */\n    }, {\n      key: \"daysInYear\",\n      get: function get() {\n        return this.isValid ? daysInYear(this.year) : NaN;\n      }\n\n      /**\n       * Returns the number of weeks in this DateTime's year\n       * @see https://en.wikipedia.org/wiki/ISO_week_date\n       * @example DateTime.local(2004).weeksInWeekYear //=> 53\n       * @example DateTime.local(2013).weeksInWeekYear //=> 52\n       * @type {number}\n       */\n    }, {\n      key: \"weeksInWeekYear\",\n      get: function get() {\n        return this.isValid ? weeksInWeekYear(this.weekYear) : NaN;\n      }\n\n      /**\n       * Returns the number of weeks in this DateTime's local week year\n       * @example DateTime.local(2020, 6, {locale: 'en-US'}).weeksInLocalWeekYear //=> 52\n       * @example DateTime.local(2020, 6, {locale: 'de-DE'}).weeksInLocalWeekYear //=> 53\n       * @type {number}\n       */\n    }, {\n      key: \"weeksInLocalWeekYear\",\n      get: function get() {\n        return this.isValid ? weeksInWeekYear(this.localWeekYear, this.loc.getMinDaysInFirstWeek(), this.loc.getStartOfWeek()) : NaN;\n      }\n    }], [{\n      key: \"DATE_SHORT\",\n      get: function get() {\n        return DATE_SHORT;\n      }\n\n      /**\n       * {@link DateTime#toLocaleString} format like 'Oct 14, 1983'\n       * @type {Object}\n       */\n    }, {\n      key: \"DATE_MED\",\n      get: function get() {\n        return DATE_MED;\n      }\n\n      /**\n       * {@link DateTime#toLocaleString} format like 'Fri, Oct 14, 1983'\n       * @type {Object}\n       */\n    }, {\n      key: \"DATE_MED_WITH_WEEKDAY\",\n      get: function get() {\n        return DATE_MED_WITH_WEEKDAY;\n      }\n\n      /**\n       * {@link DateTime#toLocaleString} format like 'October 14, 1983'\n       * @type {Object}\n       */\n    }, {\n      key: \"DATE_FULL\",\n      get: function get() {\n        return DATE_FULL;\n      }\n\n      /**\n       * {@link DateTime#toLocaleString} format like 'Tuesday, October 14, 1983'\n       * @type {Object}\n       */\n    }, {\n      key: \"DATE_HUGE\",\n      get: function get() {\n        return DATE_HUGE;\n      }\n\n      /**\n       * {@link DateTime#toLocaleString} format like '09:30 AM'. Only 12-hour if the locale is.\n       * @type {Object}\n       */\n    }, {\n      key: \"TIME_SIMPLE\",\n      get: function get() {\n        return TIME_SIMPLE;\n      }\n\n      /**\n       * {@link DateTime#toLocaleString} format like '09:30:23 AM'. Only 12-hour if the locale is.\n       * @type {Object}\n       */\n    }, {\n      key: \"TIME_WITH_SECONDS\",\n      get: function get() {\n        return TIME_WITH_SECONDS;\n      }\n\n      /**\n       * {@link DateTime#toLocaleString} format like '09:30:23 AM EDT'. Only 12-hour if the locale is.\n       * @type {Object}\n       */\n    }, {\n      key: \"TIME_WITH_SHORT_OFFSET\",\n      get: function get() {\n        return TIME_WITH_SHORT_OFFSET;\n      }\n\n      /**\n       * {@link DateTime#toLocaleString} format like '09:30:23 AM Eastern Daylight Time'. Only 12-hour if the locale is.\n       * @type {Object}\n       */\n    }, {\n      key: \"TIME_WITH_LONG_OFFSET\",\n      get: function get() {\n        return TIME_WITH_LONG_OFFSET;\n      }\n\n      /**\n       * {@link DateTime#toLocaleString} format like '09:30', always 24-hour.\n       * @type {Object}\n       */\n    }, {\n      key: \"TIME_24_SIMPLE\",\n      get: function get() {\n        return TIME_24_SIMPLE;\n      }\n\n      /**\n       * {@link DateTime#toLocaleString} format like '09:30:23', always 24-hour.\n       * @type {Object}\n       */\n    }, {\n      key: \"TIME_24_WITH_SECONDS\",\n      get: function get() {\n        return TIME_24_WITH_SECONDS;\n      }\n\n      /**\n       * {@link DateTime#toLocaleString} format like '09:30:23 EDT', always 24-hour.\n       * @type {Object}\n       */\n    }, {\n      key: \"TIME_24_WITH_SHORT_OFFSET\",\n      get: function get() {\n        return TIME_24_WITH_SHORT_OFFSET;\n      }\n\n      /**\n       * {@link DateTime#toLocaleString} format like '09:30:23 Eastern Daylight Time', always 24-hour.\n       * @type {Object}\n       */\n    }, {\n      key: \"TIME_24_WITH_LONG_OFFSET\",\n      get: function get() {\n        return TIME_24_WITH_LONG_OFFSET;\n      }\n\n      /**\n       * {@link DateTime#toLocaleString} format like '10/14/1983, 9:30 AM'. Only 12-hour if the locale is.\n       * @type {Object}\n       */\n    }, {\n      key: \"DATETIME_SHORT\",\n      get: function get() {\n        return DATETIME_SHORT;\n      }\n\n      /**\n       * {@link DateTime#toLocaleString} format like '10/14/1983, 9:30:33 AM'. Only 12-hour if the locale is.\n       * @type {Object}\n       */\n    }, {\n      key: \"DATETIME_SHORT_WITH_SECONDS\",\n      get: function get() {\n        return DATETIME_SHORT_WITH_SECONDS;\n      }\n\n      /**\n       * {@link DateTime#toLocaleString} format like 'Oct 14, 1983, 9:30 AM'. Only 12-hour if the locale is.\n       * @type {Object}\n       */\n    }, {\n      key: \"DATETIME_MED\",\n      get: function get() {\n        return DATETIME_MED;\n      }\n\n      /**\n       * {@link DateTime#toLocaleString} format like 'Oct 14, 1983, 9:30:33 AM'. Only 12-hour if the locale is.\n       * @type {Object}\n       */\n    }, {\n      key: \"DATETIME_MED_WITH_SECONDS\",\n      get: function get() {\n        return DATETIME_MED_WITH_SECONDS;\n      }\n\n      /**\n       * {@link DateTime#toLocaleString} format like 'Fri, 14 Oct 1983, 9:30 AM'. Only 12-hour if the locale is.\n       * @type {Object}\n       */\n    }, {\n      key: \"DATETIME_MED_WITH_WEEKDAY\",\n      get: function get() {\n        return DATETIME_MED_WITH_WEEKDAY;\n      }\n\n      /**\n       * {@link DateTime#toLocaleString} format like 'October 14, 1983, 9:30 AM EDT'. Only 12-hour if the locale is.\n       * @type {Object}\n       */\n    }, {\n      key: \"DATETIME_FULL\",\n      get: function get() {\n        return DATETIME_FULL;\n      }\n\n      /**\n       * {@link DateTime#toLocaleString} format like 'October 14, 1983, 9:30:33 AM EDT'. Only 12-hour if the locale is.\n       * @type {Object}\n       */\n    }, {\n      key: \"DATETIME_FULL_WITH_SECONDS\",\n      get: function get() {\n        return DATETIME_FULL_WITH_SECONDS;\n      }\n\n      /**\n       * {@link DateTime#toLocaleString} format like 'Friday, October 14, 1983, 9:30 AM Eastern Daylight Time'. Only 12-hour if the locale is.\n       * @type {Object}\n       */\n    }, {\n      key: \"DATETIME_HUGE\",\n      get: function get() {\n        return DATETIME_HUGE;\n      }\n\n      /**\n       * {@link DateTime#toLocaleString} format like 'Friday, October 14, 1983, 9:30:33 AM Eastern Daylight Time'. Only 12-hour if the locale is.\n       * @type {Object}\n       */\n    }, {\n      key: \"DATETIME_HUGE_WITH_SECONDS\",\n      get: function get() {\n        return DATETIME_HUGE_WITH_SECONDS;\n      }\n    }]);\n    return DateTime;\n  }(Symbol.for(\"nodejs.util.inspect.custom\"));\n  function friendlyDateTime(dateTimeish) {\n    if (DateTime.isDateTime(dateTimeish)) {\n      return dateTimeish;\n    } else if (dateTimeish && dateTimeish.valueOf && isNumber(dateTimeish.valueOf())) {\n      return DateTime.fromJSDate(dateTimeish);\n    } else if (dateTimeish && typeof dateTimeish === \"object\") {\n      return DateTime.fromObject(dateTimeish);\n    } else {\n      throw new InvalidArgumentError(\"Unknown datetime argument: \" + dateTimeish + \", of type \" + typeof dateTimeish);\n    }\n  }\n\n  var VERSION = \"3.5.0\";\n\n  exports.DateTime = DateTime;\n  exports.Duration = Duration;\n  exports.FixedOffsetZone = FixedOffsetZone;\n  exports.IANAZone = IANAZone;\n  exports.Info = Info;\n  exports.Interval = Interval;\n  exports.InvalidZone = InvalidZone;\n  exports.Settings = Settings;\n  exports.SystemZone = SystemZone;\n  exports.VERSION = VERSION;\n  exports.Zone = Zone;\n\n  Object.defineProperty(exports, '__esModule', { value: true });\n\n  return exports;\n\n})({});\n// start Odoo customization\n// The following prevents luxon objects from being made reactive by Owl, because they are immutable\nluxon.DateTime.prototype[Symbol.toStringTag] = \"LuxonDateTime\";\nluxon.Duration.prototype[Symbol.toStringTag] = \"LuxonDuration\";\nluxon.Interval.prototype[Symbol.toStringTag] = \"LuxonInterval\";\nluxon.Settings.prototype[Symbol.toStringTag] = \"LuxonSettings\";\nluxon.Info.prototype[Symbol.toStringTag] = \"LuxonInfo\";\nluxon.Zone.prototype[Symbol.toStringTag] = \"LuxonZone\";\n// end Odoo customization\n//# sourceMappingURL=luxon.js.map\n", "(function (exports) {\r\n    'use strict';\r\n\r\n    function filterOutModifiersFromData(dataList) {\r\n        dataList = dataList.slice();\r\n        const modifiers = [];\r\n        let elm;\r\n        while ((elm = dataList[0]) && typeof elm === \"string\") {\r\n            modifiers.push(dataList.shift());\r\n        }\r\n        return { modifiers, data: dataList };\r\n    }\r\n    const config = {\r\n        // whether or not blockdom should normalize DOM whenever a block is created.\r\n        // Normalizing dom mean removing empty text nodes (or containing only spaces)\r\n        shouldNormalizeDom: true,\r\n        // this is the main event handler. Every event handler registered with blockdom\r\n        // will go through this function, giving it the data registered in the block\r\n        // and the event\r\n        mainEventHandler: (data, ev, currentTarget) => {\r\n            if (typeof data === \"function\") {\r\n                data(ev);\r\n            }\r\n            else if (Array.isArray(data)) {\r\n                data = filterOutModifiersFromData(data).data;\r\n                data[0](data[1], ev);\r\n            }\r\n            return false;\r\n        },\r\n    };\r\n\r\n    // -----------------------------------------------------------------------------\r\n    // Toggler node\r\n    // -----------------------------------------------------------------------------\r\n    class VToggler {\r\n        constructor(key, child) {\r\n            this.key = key;\r\n            this.child = child;\r\n        }\r\n        mount(parent, afterNode) {\r\n            this.parentEl = parent;\r\n            this.child.mount(parent, afterNode);\r\n        }\r\n        moveBeforeDOMNode(node, parent) {\r\n            this.child.moveBeforeDOMNode(node, parent);\r\n        }\r\n        moveBeforeVNode(other, afterNode) {\r\n            this.moveBeforeDOMNode((other && other.firstNode()) || afterNode);\r\n        }\r\n        patch(other, withBeforeRemove) {\r\n            if (this === other) {\r\n                return;\r\n            }\r\n            let child1 = this.child;\r\n            let child2 = other.child;\r\n            if (this.key === other.key) {\r\n                child1.patch(child2, withBeforeRemove);\r\n            }\r\n            else {\r\n                child2.mount(this.parentEl, child1.firstNode());\r\n                if (withBeforeRemove) {\r\n                    child1.beforeRemove();\r\n                }\r\n                child1.remove();\r\n                this.child = child2;\r\n                this.key = other.key;\r\n            }\r\n        }\r\n        beforeRemove() {\r\n            this.child.beforeRemove();\r\n        }\r\n        remove() {\r\n            this.child.remove();\r\n        }\r\n        firstNode() {\r\n            return this.child.firstNode();\r\n        }\r\n        toString() {\r\n            return this.child.toString();\r\n        }\r\n    }\r\n    function toggler(key, child) {\r\n        return new VToggler(key, child);\r\n    }\r\n\r\n    // Custom error class that wraps error that happen in the owl lifecycle\r\n    class OwlError extends Error {\r\n    }\r\n\r\n    const { setAttribute: elemSetAttribute, removeAttribute } = Element.prototype;\r\n    const tokenList = DOMTokenList.prototype;\r\n    const tokenListAdd = tokenList.add;\r\n    const tokenListRemove = tokenList.remove;\r\n    const isArray = Array.isArray;\r\n    const { split, trim } = String.prototype;\r\n    const wordRegexp = /\\s+/;\r\n    /**\r\n     * We regroup here all code related to updating attributes in a very loose sense:\r\n     * attributes, properties and classs are all managed by the functions in this\r\n     * file.\r\n     */\r\n    function setAttribute(key, value) {\r\n        switch (value) {\r\n            case false:\r\n            case undefined:\r\n                removeAttribute.call(this, key);\r\n                break;\r\n            case true:\r\n                elemSetAttribute.call(this, key, \"\");\r\n                break;\r\n            default:\r\n                elemSetAttribute.call(this, key, value);\r\n        }\r\n    }\r\n    function createAttrUpdater(attr) {\r\n        return function (value) {\r\n            setAttribute.call(this, attr, value);\r\n        };\r\n    }\r\n    function attrsSetter(attrs) {\r\n        if (isArray(attrs)) {\r\n            if (attrs[0] === \"class\") {\r\n                setClass.call(this, attrs[1]);\r\n            }\r\n            else {\r\n                setAttribute.call(this, attrs[0], attrs[1]);\r\n            }\r\n        }\r\n        else {\r\n            for (let k in attrs) {\r\n                if (k === \"class\") {\r\n                    setClass.call(this, attrs[k]);\r\n                }\r\n                else {\r\n                    setAttribute.call(this, k, attrs[k]);\r\n                }\r\n            }\r\n        }\r\n    }\r\n    function attrsUpdater(attrs, oldAttrs) {\r\n        if (isArray(attrs)) {\r\n            const name = attrs[0];\r\n            const val = attrs[1];\r\n            if (name === oldAttrs[0]) {\r\n                if (val === oldAttrs[1]) {\r\n                    return;\r\n                }\r\n                if (name === \"class\") {\r\n                    updateClass.call(this, val, oldAttrs[1]);\r\n                }\r\n                else {\r\n                    setAttribute.call(this, name, val);\r\n                }\r\n            }\r\n            else {\r\n                removeAttribute.call(this, oldAttrs[0]);\r\n                setAttribute.call(this, name, val);\r\n            }\r\n        }\r\n        else {\r\n            for (let k in oldAttrs) {\r\n                if (!(k in attrs)) {\r\n                    if (k === \"class\") {\r\n                        updateClass.call(this, \"\", oldAttrs[k]);\r\n                    }\r\n                    else {\r\n                        removeAttribute.call(this, k);\r\n                    }\r\n                }\r\n            }\r\n            for (let k in attrs) {\r\n                const val = attrs[k];\r\n                if (val !== oldAttrs[k]) {\r\n                    if (k === \"class\") {\r\n                        updateClass.call(this, val, oldAttrs[k]);\r\n                    }\r\n                    else {\r\n                        setAttribute.call(this, k, val);\r\n                    }\r\n                }\r\n            }\r\n        }\r\n    }\r\n    function toClassObj(expr) {\r\n        const result = {};\r\n        switch (typeof expr) {\r\n            case \"string\":\r\n                // we transform here a list of classes into an object:\r\n                //  'hey you' becomes {hey: true, you: true}\r\n                const str = trim.call(expr);\r\n                if (!str) {\r\n                    return {};\r\n                }\r\n                let words = split.call(str, wordRegexp);\r\n                for (let i = 0, l = words.length; i < l; i++) {\r\n                    result[words[i]] = true;\r\n                }\r\n                return result;\r\n            case \"object\":\r\n                // this is already an object but we may need to split keys:\r\n                // {'a': true, 'b c': true} should become {a: true, b: true, c: true}\r\n                for (let key in expr) {\r\n                    const value = expr[key];\r\n                    if (value) {\r\n                        key = trim.call(key);\r\n                        if (!key) {\r\n                            continue;\r\n                        }\r\n                        const words = split.call(key, wordRegexp);\r\n                        for (let word of words) {\r\n                            result[word] = value;\r\n                        }\r\n                    }\r\n                }\r\n                return result;\r\n            case \"undefined\":\r\n                return {};\r\n            case \"number\":\r\n                return { [expr]: true };\r\n            default:\r\n                return { [expr]: true };\r\n        }\r\n    }\r\n    function setClass(val) {\r\n        val = val === \"\" ? {} : toClassObj(val);\r\n        // add classes\r\n        const cl = this.classList;\r\n        for (let c in val) {\r\n            tokenListAdd.call(cl, c);\r\n        }\r\n    }\r\n    function updateClass(val, oldVal) {\r\n        oldVal = oldVal === \"\" ? {} : toClassObj(oldVal);\r\n        val = val === \"\" ? {} : toClassObj(val);\r\n        const cl = this.classList;\r\n        // remove classes\r\n        for (let c in oldVal) {\r\n            if (!(c in val)) {\r\n                tokenListRemove.call(cl, c);\r\n            }\r\n        }\r\n        // add classes\r\n        for (let c in val) {\r\n            if (!(c in oldVal)) {\r\n                tokenListAdd.call(cl, c);\r\n            }\r\n        }\r\n    }\r\n\r\n    /**\r\n     * Creates a batched version of a callback so that all calls to it in the same\r\n     * microtick will only call the original callback once.\r\n     *\r\n     * @param callback the callback to batch\r\n     * @returns a batched version of the original callback\r\n     */\r\n    function batched(callback) {\r\n        let scheduled = false;\r\n        return async (...args) => {\r\n            if (!scheduled) {\r\n                scheduled = true;\r\n                await Promise.resolve();\r\n                scheduled = false;\r\n                callback(...args);\r\n            }\r\n        };\r\n    }\r\n    /**\r\n     * Determine whether the given element is contained in its ownerDocument:\r\n     * either directly or with a shadow root in between.\r\n     */\r\n    function inOwnerDocument(el) {\r\n        if (!el) {\r\n            return false;\r\n        }\r\n        if (el.ownerDocument.contains(el)) {\r\n            return true;\r\n        }\r\n        const rootNode = el.getRootNode();\r\n        return rootNode instanceof ShadowRoot && el.ownerDocument.contains(rootNode.host);\r\n    }\r\n    /**\r\n     * Determine whether the given element is contained in a specific root documnet:\r\n     * either directly or with a shadow root in between or in an iframe.\r\n     */\r\n    function isAttachedToDocument(element, documentElement) {\r\n        let current = element;\r\n        const shadowRoot = documentElement.defaultView.ShadowRoot;\r\n        while (current) {\r\n            if (current === documentElement) {\r\n                return true;\r\n            }\r\n            if (current.parentNode) {\r\n                current = current.parentNode;\r\n            }\r\n            else if (current instanceof shadowRoot && current.host) {\r\n                current = current.host;\r\n            }\r\n            else {\r\n                return false;\r\n            }\r\n        }\r\n        return false;\r\n    }\r\n    function validateTarget(target) {\r\n        // Get the document and HTMLElement corresponding to the target to allow mounting in iframes\r\n        const document = target && target.ownerDocument;\r\n        if (document) {\r\n            if (!document.defaultView) {\r\n                throw new OwlError(\"Cannot mount a component: the target document is not attached to a window (defaultView is missing)\");\r\n            }\r\n            const HTMLElement = document.defaultView.HTMLElement;\r\n            if (target instanceof HTMLElement || target instanceof ShadowRoot) {\r\n                if (!isAttachedToDocument(target, document)) {\r\n                    throw new OwlError(\"Cannot mount a component on a detached dom node\");\r\n                }\r\n                return;\r\n            }\r\n        }\r\n        throw new OwlError(\"Cannot mount component: the target is not a valid DOM element\");\r\n    }\r\n    class EventBus extends EventTarget {\r\n        trigger(name, payload) {\r\n            this.dispatchEvent(new CustomEvent(name, { detail: payload }));\r\n        }\r\n    }\r\n    function whenReady(fn) {\r\n        return new Promise(function (resolve) {\r\n            if (document.readyState !== \"loading\") {\r\n                resolve(true);\r\n            }\r\n            else {\r\n                document.addEventListener(\"DOMContentLoaded\", resolve, false);\r\n            }\r\n        }).then(fn || function () { });\r\n    }\r\n    async function loadFile(url) {\r\n        const result = await fetch(url);\r\n        if (!result.ok) {\r\n            throw new OwlError(\"Error while fetching xml templates\");\r\n        }\r\n        return await result.text();\r\n    }\r\n    /*\r\n     * This class just transports the fact that a string is safe\r\n     * to be injected as HTML. Overriding a JS primitive is quite painful though\r\n     * so we need to redfine toString and valueOf.\r\n     */\r\n    class Markup extends String {\r\n    }\r\n    function htmlEscape(str) {\r\n        if (str instanceof Markup) {\r\n            return str;\r\n        }\r\n        if (str === undefined) {\r\n            return markup(\"\");\r\n        }\r\n        if (typeof str === \"number\") {\r\n            return markup(String(str));\r\n        }\r\n        [\r\n            [\"&\", \"&amp;\"],\r\n            [\"<\", \"&lt;\"],\r\n            [\">\", \"&gt;\"],\r\n            [\"'\", \"&#x27;\"],\r\n            ['\"', \"&quot;\"],\r\n            [\"`\", \"&#x60;\"],\r\n        ].forEach((pairs) => {\r\n            str = String(str).replace(new RegExp(pairs[0], \"g\"), pairs[1]);\r\n        });\r\n        return markup(str);\r\n    }\r\n    function markup(valueOrStrings, ...placeholders) {\r\n        if (!Array.isArray(valueOrStrings)) {\r\n            return new Markup(valueOrStrings);\r\n        }\r\n        const strings = valueOrStrings;\r\n        let acc = \"\";\r\n        let i = 0;\r\n        for (; i < placeholders.length; ++i) {\r\n            acc += strings[i] + htmlEscape(placeholders[i]);\r\n        }\r\n        acc += strings[i];\r\n        return new Markup(acc);\r\n    }\r\n\r\n    function createEventHandler(rawEvent) {\r\n        const eventName = rawEvent.split(\".\")[0];\r\n        const capture = rawEvent.includes(\".capture\");\r\n        if (rawEvent.includes(\".synthetic\")) {\r\n            return createSyntheticHandler(eventName, capture);\r\n        }\r\n        else {\r\n            return createElementHandler(eventName, capture);\r\n        }\r\n    }\r\n    // Native listener\r\n    let nextNativeEventId = 1;\r\n    function createElementHandler(evName, capture = false) {\r\n        let eventKey = `__event__${evName}_${nextNativeEventId++}`;\r\n        if (capture) {\r\n            eventKey = `${eventKey}_capture`;\r\n        }\r\n        function listener(ev) {\r\n            const currentTarget = ev.currentTarget;\r\n            if (!currentTarget || !inOwnerDocument(currentTarget))\r\n                return;\r\n            const data = currentTarget[eventKey];\r\n            if (!data)\r\n                return;\r\n            config.mainEventHandler(data, ev, currentTarget);\r\n        }\r\n        function setup(data) {\r\n            this[eventKey] = data;\r\n            this.addEventListener(evName, listener, { capture });\r\n        }\r\n        function remove() {\r\n            delete this[eventKey];\r\n            this.removeEventListener(evName, listener, { capture });\r\n        }\r\n        function update(data) {\r\n            this[eventKey] = data;\r\n        }\r\n        return { setup, update, remove };\r\n    }\r\n    // Synthetic handler: a form of event delegation that allows placing only one\r\n    // listener per event type.\r\n    let nextSyntheticEventId = 1;\r\n    function createSyntheticHandler(evName, capture = false) {\r\n        let eventKey = `__event__synthetic_${evName}`;\r\n        if (capture) {\r\n            eventKey = `${eventKey}_capture`;\r\n        }\r\n        setupSyntheticEvent(evName, eventKey, capture);\r\n        const currentId = nextSyntheticEventId++;\r\n        function setup(data) {\r\n            const _data = this[eventKey] || {};\r\n            _data[currentId] = data;\r\n            this[eventKey] = _data;\r\n        }\r\n        function remove() {\r\n            delete this[eventKey];\r\n        }\r\n        return { setup, update: setup, remove };\r\n    }\r\n    function nativeToSyntheticEvent(eventKey, event) {\r\n        let dom = event.target;\r\n        while (dom !== null) {\r\n            const _data = dom[eventKey];\r\n            if (_data) {\r\n                for (const data of Object.values(_data)) {\r\n                    const stopped = config.mainEventHandler(data, event, dom);\r\n                    if (stopped)\r\n                        return;\r\n                }\r\n            }\r\n            dom = dom.parentNode;\r\n        }\r\n    }\r\n    const CONFIGURED_SYNTHETIC_EVENTS = {};\r\n    function setupSyntheticEvent(evName, eventKey, capture = false) {\r\n        if (CONFIGURED_SYNTHETIC_EVENTS[eventKey]) {\r\n            return;\r\n        }\r\n        document.addEventListener(evName, (event) => nativeToSyntheticEvent(eventKey, event), {\r\n            capture,\r\n        });\r\n        CONFIGURED_SYNTHETIC_EVENTS[eventKey] = true;\r\n    }\r\n\r\n    const getDescriptor$3 = (o, p) => Object.getOwnPropertyDescriptor(o, p);\r\n    const nodeProto$4 = Node.prototype;\r\n    const nodeInsertBefore$3 = nodeProto$4.insertBefore;\r\n    const nodeSetTextContent$1 = getDescriptor$3(nodeProto$4, \"textContent\").set;\r\n    const nodeRemoveChild$3 = nodeProto$4.removeChild;\r\n    // -----------------------------------------------------------------------------\r\n    // Multi NODE\r\n    // -----------------------------------------------------------------------------\r\n    class VMulti {\r\n        constructor(children) {\r\n            this.children = children;\r\n        }\r\n        mount(parent, afterNode) {\r\n            const children = this.children;\r\n            const l = children.length;\r\n            const anchors = new Array(l);\r\n            for (let i = 0; i < l; i++) {\r\n                let child = children[i];\r\n                if (child) {\r\n                    child.mount(parent, afterNode);\r\n                }\r\n                else {\r\n                    const childAnchor = document.createTextNode(\"\");\r\n                    anchors[i] = childAnchor;\r\n                    nodeInsertBefore$3.call(parent, childAnchor, afterNode);\r\n                }\r\n            }\r\n            this.anchors = anchors;\r\n            this.parentEl = parent;\r\n        }\r\n        moveBeforeDOMNode(node, parent = this.parentEl) {\r\n            this.parentEl = parent;\r\n            const children = this.children;\r\n            const anchors = this.anchors;\r\n            for (let i = 0, l = children.length; i < l; i++) {\r\n                let child = children[i];\r\n                if (child) {\r\n                    child.moveBeforeDOMNode(node, parent);\r\n                }\r\n                else {\r\n                    const anchor = anchors[i];\r\n                    nodeInsertBefore$3.call(parent, anchor, node);\r\n                }\r\n            }\r\n        }\r\n        moveBeforeVNode(other, afterNode) {\r\n            if (other) {\r\n                const next = other.children[0];\r\n                afterNode = (next ? next.firstNode() : other.anchors[0]) || null;\r\n            }\r\n            const children = this.children;\r\n            const parent = this.parentEl;\r\n            const anchors = this.anchors;\r\n            for (let i = 0, l = children.length; i < l; i++) {\r\n                let child = children[i];\r\n                if (child) {\r\n                    child.moveBeforeVNode(null, afterNode);\r\n                }\r\n                else {\r\n                    const anchor = anchors[i];\r\n                    nodeInsertBefore$3.call(parent, anchor, afterNode);\r\n                }\r\n            }\r\n        }\r\n        patch(other, withBeforeRemove) {\r\n            if (this === other) {\r\n                return;\r\n            }\r\n            const children1 = this.children;\r\n            const children2 = other.children;\r\n            const anchors = this.anchors;\r\n            const parentEl = this.parentEl;\r\n            for (let i = 0, l = children1.length; i < l; i++) {\r\n                const vn1 = children1[i];\r\n                const vn2 = children2[i];\r\n                if (vn1) {\r\n                    if (vn2) {\r\n                        vn1.patch(vn2, withBeforeRemove);\r\n                    }\r\n                    else {\r\n                        const afterNode = vn1.firstNode();\r\n                        const anchor = document.createTextNode(\"\");\r\n                        anchors[i] = anchor;\r\n                        nodeInsertBefore$3.call(parentEl, anchor, afterNode);\r\n                        if (withBeforeRemove) {\r\n                            vn1.beforeRemove();\r\n                        }\r\n                        vn1.remove();\r\n                        children1[i] = undefined;\r\n                    }\r\n                }\r\n                else if (vn2) {\r\n                    children1[i] = vn2;\r\n                    const anchor = anchors[i];\r\n                    vn2.mount(parentEl, anchor);\r\n                    nodeRemoveChild$3.call(parentEl, anchor);\r\n                }\r\n            }\r\n        }\r\n        beforeRemove() {\r\n            const children = this.children;\r\n            for (let i = 0, l = children.length; i < l; i++) {\r\n                const child = children[i];\r\n                if (child) {\r\n                    child.beforeRemove();\r\n                }\r\n            }\r\n        }\r\n        remove() {\r\n            const parentEl = this.parentEl;\r\n            if (this.isOnlyChild) {\r\n                nodeSetTextContent$1.call(parentEl, \"\");\r\n            }\r\n            else {\r\n                const children = this.children;\r\n                const anchors = this.anchors;\r\n                for (let i = 0, l = children.length; i < l; i++) {\r\n                    const child = children[i];\r\n                    if (child) {\r\n                        child.remove();\r\n                    }\r\n                    else {\r\n                        nodeRemoveChild$3.call(parentEl, anchors[i]);\r\n                    }\r\n                }\r\n            }\r\n        }\r\n        firstNode() {\r\n            const child = this.children[0];\r\n            return child ? child.firstNode() : this.anchors[0];\r\n        }\r\n        toString() {\r\n            return this.children.map((c) => (c ? c.toString() : \"\")).join(\"\");\r\n        }\r\n    }\r\n    function multi(children) {\r\n        return new VMulti(children);\r\n    }\r\n\r\n    const getDescriptor$2 = (o, p) => Object.getOwnPropertyDescriptor(o, p);\r\n    const nodeProto$3 = Node.prototype;\r\n    const characterDataProto$1 = CharacterData.prototype;\r\n    const nodeInsertBefore$2 = nodeProto$3.insertBefore;\r\n    const characterDataSetData$1 = getDescriptor$2(characterDataProto$1, \"data\").set;\r\n    const nodeRemoveChild$2 = nodeProto$3.removeChild;\r\n    class VSimpleNode {\r\n        constructor(text) {\r\n            this.text = text;\r\n        }\r\n        mountNode(node, parent, afterNode) {\r\n            this.parentEl = parent;\r\n            nodeInsertBefore$2.call(parent, node, afterNode);\r\n            this.el = node;\r\n        }\r\n        moveBeforeDOMNode(node, parent = this.parentEl) {\r\n            this.parentEl = parent;\r\n            nodeInsertBefore$2.call(parent, this.el, node);\r\n        }\r\n        moveBeforeVNode(other, afterNode) {\r\n            nodeInsertBefore$2.call(this.parentEl, this.el, other ? other.el : afterNode);\r\n        }\r\n        beforeRemove() { }\r\n        remove() {\r\n            nodeRemoveChild$2.call(this.parentEl, this.el);\r\n        }\r\n        firstNode() {\r\n            return this.el;\r\n        }\r\n        toString() {\r\n            return this.text;\r\n        }\r\n    }\r\n    class VText$1 extends VSimpleNode {\r\n        mount(parent, afterNode) {\r\n            this.mountNode(document.createTextNode(toText(this.text)), parent, afterNode);\r\n        }\r\n        patch(other) {\r\n            const text2 = other.text;\r\n            if (this.text !== text2) {\r\n                characterDataSetData$1.call(this.el, toText(text2));\r\n                this.text = text2;\r\n            }\r\n        }\r\n    }\r\n    class VComment extends VSimpleNode {\r\n        mount(parent, afterNode) {\r\n            this.mountNode(document.createComment(toText(this.text)), parent, afterNode);\r\n        }\r\n        patch() { }\r\n    }\r\n    function text(str) {\r\n        return new VText$1(str);\r\n    }\r\n    function comment(str) {\r\n        return new VComment(str);\r\n    }\r\n    function toText(value) {\r\n        switch (typeof value) {\r\n            case \"string\":\r\n                return value;\r\n            case \"number\":\r\n                return String(value);\r\n            case \"boolean\":\r\n                return value ? \"true\" : \"false\";\r\n            default:\r\n                return value || \"\";\r\n        }\r\n    }\r\n\r\n    const getDescriptor$1 = (o, p) => Object.getOwnPropertyDescriptor(o, p);\r\n    const nodeProto$2 = Node.prototype;\r\n    const elementProto = Element.prototype;\r\n    const characterDataProto = CharacterData.prototype;\r\n    const characterDataSetData = getDescriptor$1(characterDataProto, \"data\").set;\r\n    const nodeGetFirstChild = getDescriptor$1(nodeProto$2, \"firstChild\").get;\r\n    const nodeGetNextSibling = getDescriptor$1(nodeProto$2, \"nextSibling\").get;\r\n    const NO_OP = () => { };\r\n    function makePropSetter(name) {\r\n        return function setProp(value) {\r\n            // support 0, fallback to empty string for other falsy values\r\n            this[name] = value === 0 ? 0 : value ? value.valueOf() : \"\";\r\n        };\r\n    }\r\n    const cache$1 = {};\r\n    /**\r\n     * Compiling blocks is a multi-step process:\r\n     *\r\n     * 1. build an IntermediateTree from the HTML element. This intermediate tree\r\n     *    is a binary tree structure that encode dynamic info sub nodes, and the\r\n     *    path required to reach them\r\n     * 2. process the tree to build a block context, which is an object that aggregate\r\n     *    all dynamic info in a list, and also, all ref indexes.\r\n     * 3. process the context to build appropriate builder/setter functions\r\n     * 4. make a dynamic block class, which will efficiently collect references and\r\n     *    create/update dynamic locations/children\r\n     *\r\n     * @param str\r\n     * @returns a new block type, that can build concrete blocks\r\n     */\r\n    function createBlock(str) {\r\n        if (str in cache$1) {\r\n            return cache$1[str];\r\n        }\r\n        // step 0: prepare html base element\r\n        const doc = new DOMParser().parseFromString(`<t>${str}</t>`, \"text/xml\");\r\n        const node = doc.firstChild.firstChild;\r\n        if (config.shouldNormalizeDom) {\r\n            normalizeNode(node);\r\n        }\r\n        // step 1: prepare intermediate tree\r\n        const tree = buildTree(node);\r\n        // step 2: prepare block context\r\n        const context = buildContext(tree);\r\n        // step 3: build the final block class\r\n        const template = tree.el;\r\n        const Block = buildBlock(template, context);\r\n        cache$1[str] = Block;\r\n        return Block;\r\n    }\r\n    // -----------------------------------------------------------------------------\r\n    // Helper\r\n    // -----------------------------------------------------------------------------\r\n    function normalizeNode(node) {\r\n        if (node.nodeType === Node.TEXT_NODE) {\r\n            if (!/\\S/.test(node.textContent)) {\r\n                node.remove();\r\n                return;\r\n            }\r\n        }\r\n        if (node.nodeType === Node.ELEMENT_NODE) {\r\n            if (node.tagName === \"pre\") {\r\n                return;\r\n            }\r\n        }\r\n        for (let i = node.childNodes.length - 1; i >= 0; --i) {\r\n            normalizeNode(node.childNodes.item(i));\r\n        }\r\n    }\r\n    function buildTree(node, parent = null, domParentTree = null) {\r\n        switch (node.nodeType) {\r\n            case Node.ELEMENT_NODE: {\r\n                // HTMLElement\r\n                let currentNS = domParentTree && domParentTree.currentNS;\r\n                const tagName = node.tagName;\r\n                let el = undefined;\r\n                const info = [];\r\n                if (tagName.startsWith(\"block-text-\")) {\r\n                    const index = parseInt(tagName.slice(11), 10);\r\n                    info.push({ type: \"text\", idx: index });\r\n                    el = document.createTextNode(\"\");\r\n                }\r\n                if (tagName.startsWith(\"block-child-\")) {\r\n                    if (!domParentTree.isRef) {\r\n                        addRef(domParentTree);\r\n                    }\r\n                    const index = parseInt(tagName.slice(12), 10);\r\n                    info.push({ type: \"child\", idx: index });\r\n                    el = document.createTextNode(\"\");\r\n                }\r\n                currentNS || (currentNS = node.namespaceURI);\r\n                if (!el) {\r\n                    el = currentNS\r\n                        ? document.createElementNS(currentNS, tagName)\r\n                        : document.createElement(tagName);\r\n                }\r\n                if (el instanceof Element) {\r\n                    if (!domParentTree) {\r\n                        // some html elements may have side effects when setting their attributes.\r\n                        // For example, setting the src attribute of an <img/> will trigger a\r\n                        // request to get the corresponding image. This is something that we\r\n                        // don't want at compile time. We avoid that by putting the content of\r\n                        // the block in a <template/> element\r\n                        const fragment = document.createElement(\"template\").content;\r\n                        fragment.appendChild(el);\r\n                    }\r\n                    const attrs = node.attributes;\r\n                    for (let i = 0; i < attrs.length; i++) {\r\n                        const attrName = attrs[i].name;\r\n                        const attrValue = attrs[i].value;\r\n                        if (attrName.startsWith(\"block-handler-\")) {\r\n                            const idx = parseInt(attrName.slice(14), 10);\r\n                            info.push({\r\n                                type: \"handler\",\r\n                                idx,\r\n                                event: attrValue,\r\n                            });\r\n                        }\r\n                        else if (attrName.startsWith(\"block-attribute-\")) {\r\n                            const idx = parseInt(attrName.slice(16), 10);\r\n                            info.push({\r\n                                type: \"attribute\",\r\n                                idx,\r\n                                name: attrValue,\r\n                                tag: tagName,\r\n                            });\r\n                        }\r\n                        else if (attrName.startsWith(\"block-property-\")) {\r\n                            const idx = parseInt(attrName.slice(15), 10);\r\n                            info.push({\r\n                                type: \"property\",\r\n                                idx,\r\n                                name: attrValue,\r\n                                tag: tagName,\r\n                            });\r\n                        }\r\n                        else if (attrName === \"block-attributes\") {\r\n                            info.push({\r\n                                type: \"attributes\",\r\n                                idx: parseInt(attrValue, 10),\r\n                            });\r\n                        }\r\n                        else if (attrName === \"block-ref\") {\r\n                            info.push({\r\n                                type: \"ref\",\r\n                                idx: parseInt(attrValue, 10),\r\n                            });\r\n                        }\r\n                        else {\r\n                            el.setAttribute(attrs[i].name, attrValue);\r\n                        }\r\n                    }\r\n                }\r\n                const tree = {\r\n                    parent,\r\n                    firstChild: null,\r\n                    nextSibling: null,\r\n                    el,\r\n                    info,\r\n                    refN: 0,\r\n                    currentNS,\r\n                };\r\n                if (node.firstChild) {\r\n                    const childNode = node.childNodes[0];\r\n                    if (node.childNodes.length === 1 &&\r\n                        childNode.nodeType === Node.ELEMENT_NODE &&\r\n                        childNode.tagName.startsWith(\"block-child-\")) {\r\n                        const tagName = childNode.tagName;\r\n                        const index = parseInt(tagName.slice(12), 10);\r\n                        info.push({ idx: index, type: \"child\", isOnlyChild: true });\r\n                    }\r\n                    else {\r\n                        tree.firstChild = buildTree(node.firstChild, tree, tree);\r\n                        el.appendChild(tree.firstChild.el);\r\n                        let curNode = node.firstChild;\r\n                        let curTree = tree.firstChild;\r\n                        while ((curNode = curNode.nextSibling)) {\r\n                            curTree.nextSibling = buildTree(curNode, curTree, tree);\r\n                            el.appendChild(curTree.nextSibling.el);\r\n                            curTree = curTree.nextSibling;\r\n                        }\r\n                    }\r\n                }\r\n                if (tree.info.length) {\r\n                    addRef(tree);\r\n                }\r\n                return tree;\r\n            }\r\n            case Node.TEXT_NODE:\r\n            case Node.COMMENT_NODE: {\r\n                // text node or comment node\r\n                const el = node.nodeType === Node.TEXT_NODE\r\n                    ? document.createTextNode(node.textContent)\r\n                    : document.createComment(node.textContent);\r\n                return {\r\n                    parent: parent,\r\n                    firstChild: null,\r\n                    nextSibling: null,\r\n                    el,\r\n                    info: [],\r\n                    refN: 0,\r\n                    currentNS: null,\r\n                };\r\n            }\r\n        }\r\n        throw new OwlError(\"boom\");\r\n    }\r\n    function addRef(tree) {\r\n        tree.isRef = true;\r\n        do {\r\n            tree.refN++;\r\n        } while ((tree = tree.parent));\r\n    }\r\n    function parentTree(tree) {\r\n        let parent = tree.parent;\r\n        while (parent && parent.nextSibling === tree) {\r\n            tree = parent;\r\n            parent = parent.parent;\r\n        }\r\n        return parent;\r\n    }\r\n    function buildContext(tree, ctx, fromIdx) {\r\n        if (!ctx) {\r\n            const children = new Array(tree.info.filter((v) => v.type === \"child\").length);\r\n            ctx = { collectors: [], locations: [], children, cbRefs: [], refN: tree.refN, refList: [] };\r\n            fromIdx = 0;\r\n        }\r\n        if (tree.refN) {\r\n            const initialIdx = fromIdx;\r\n            const isRef = tree.isRef;\r\n            const firstChild = tree.firstChild ? tree.firstChild.refN : 0;\r\n            const nextSibling = tree.nextSibling ? tree.nextSibling.refN : 0;\r\n            //node\r\n            if (isRef) {\r\n                for (let info of tree.info) {\r\n                    info.refIdx = initialIdx;\r\n                }\r\n                tree.refIdx = initialIdx;\r\n                updateCtx(ctx, tree);\r\n                fromIdx++;\r\n            }\r\n            // right\r\n            if (nextSibling) {\r\n                const idx = fromIdx + firstChild;\r\n                ctx.collectors.push({ idx, prevIdx: initialIdx, getVal: nodeGetNextSibling });\r\n                buildContext(tree.nextSibling, ctx, idx);\r\n            }\r\n            // left\r\n            if (firstChild) {\r\n                ctx.collectors.push({ idx: fromIdx, prevIdx: initialIdx, getVal: nodeGetFirstChild });\r\n                buildContext(tree.firstChild, ctx, fromIdx);\r\n            }\r\n        }\r\n        return ctx;\r\n    }\r\n    function updateCtx(ctx, tree) {\r\n        for (let info of tree.info) {\r\n            switch (info.type) {\r\n                case \"text\":\r\n                    ctx.locations.push({\r\n                        idx: info.idx,\r\n                        refIdx: info.refIdx,\r\n                        setData: setText,\r\n                        updateData: setText,\r\n                    });\r\n                    break;\r\n                case \"child\":\r\n                    if (info.isOnlyChild) {\r\n                        // tree is the parentnode here\r\n                        ctx.children[info.idx] = {\r\n                            parentRefIdx: info.refIdx,\r\n                            isOnlyChild: true,\r\n                        };\r\n                    }\r\n                    else {\r\n                        // tree is the anchor text node\r\n                        ctx.children[info.idx] = {\r\n                            parentRefIdx: parentTree(tree).refIdx,\r\n                            afterRefIdx: info.refIdx,\r\n                        };\r\n                    }\r\n                    break;\r\n                case \"property\": {\r\n                    const refIdx = info.refIdx;\r\n                    const setProp = makePropSetter(info.name);\r\n                    ctx.locations.push({\r\n                        idx: info.idx,\r\n                        refIdx,\r\n                        setData: setProp,\r\n                        updateData: setProp,\r\n                    });\r\n                    break;\r\n                }\r\n                case \"attribute\": {\r\n                    const refIdx = info.refIdx;\r\n                    let updater;\r\n                    let setter;\r\n                    if (info.name === \"class\") {\r\n                        setter = setClass;\r\n                        updater = updateClass;\r\n                    }\r\n                    else {\r\n                        setter = createAttrUpdater(info.name);\r\n                        updater = setter;\r\n                    }\r\n                    ctx.locations.push({\r\n                        idx: info.idx,\r\n                        refIdx,\r\n                        setData: setter,\r\n                        updateData: updater,\r\n                    });\r\n                    break;\r\n                }\r\n                case \"attributes\":\r\n                    ctx.locations.push({\r\n                        idx: info.idx,\r\n                        refIdx: info.refIdx,\r\n                        setData: attrsSetter,\r\n                        updateData: attrsUpdater,\r\n                    });\r\n                    break;\r\n                case \"handler\": {\r\n                    const { setup, update } = createEventHandler(info.event);\r\n                    ctx.locations.push({\r\n                        idx: info.idx,\r\n                        refIdx: info.refIdx,\r\n                        setData: setup,\r\n                        updateData: update,\r\n                    });\r\n                    break;\r\n                }\r\n                case \"ref\":\r\n                    const index = ctx.cbRefs.push(info.idx) - 1;\r\n                    ctx.locations.push({\r\n                        idx: info.idx,\r\n                        refIdx: info.refIdx,\r\n                        setData: makeRefSetter(index, ctx.refList),\r\n                        updateData: NO_OP,\r\n                    });\r\n            }\r\n        }\r\n    }\r\n    // -----------------------------------------------------------------------------\r\n    // building the concrete block class\r\n    // -----------------------------------------------------------------------------\r\n    function buildBlock(template, ctx) {\r\n        let B = createBlockClass(template, ctx);\r\n        if (ctx.cbRefs.length) {\r\n            const cbRefs = ctx.cbRefs;\r\n            const refList = ctx.refList;\r\n            let cbRefsNumber = cbRefs.length;\r\n            B = class extends B {\r\n                mount(parent, afterNode) {\r\n                    refList.push(new Array(cbRefsNumber));\r\n                    super.mount(parent, afterNode);\r\n                    for (let cbRef of refList.pop()) {\r\n                        cbRef();\r\n                    }\r\n                }\r\n                remove() {\r\n                    super.remove();\r\n                    for (let cbRef of cbRefs) {\r\n                        let fn = this.data[cbRef];\r\n                        fn(null);\r\n                    }\r\n                }\r\n            };\r\n        }\r\n        if (ctx.children.length) {\r\n            B = class extends B {\r\n                constructor(data, children) {\r\n                    super(data);\r\n                    this.children = children;\r\n                }\r\n            };\r\n            B.prototype.beforeRemove = VMulti.prototype.beforeRemove;\r\n            return (data, children = []) => new B(data, children);\r\n        }\r\n        return (data) => new B(data);\r\n    }\r\n    function createBlockClass(template, ctx) {\r\n        const { refN, collectors, children } = ctx;\r\n        const colN = collectors.length;\r\n        ctx.locations.sort((a, b) => a.idx - b.idx);\r\n        const locations = ctx.locations.map((loc) => ({\r\n            refIdx: loc.refIdx,\r\n            setData: loc.setData,\r\n            updateData: loc.updateData,\r\n        }));\r\n        const locN = locations.length;\r\n        const childN = children.length;\r\n        const childrenLocs = children;\r\n        const isDynamic = refN > 0;\r\n        // these values are defined here to make them faster to lookup in the class\r\n        // block scope\r\n        const nodeCloneNode = nodeProto$2.cloneNode;\r\n        const nodeInsertBefore = nodeProto$2.insertBefore;\r\n        const elementRemove = elementProto.remove;\r\n        class Block {\r\n            constructor(data) {\r\n                this.data = data;\r\n            }\r\n            beforeRemove() { }\r\n            remove() {\r\n                elementRemove.call(this.el);\r\n            }\r\n            firstNode() {\r\n                return this.el;\r\n            }\r\n            moveBeforeDOMNode(node, parent = this.parentEl) {\r\n                this.parentEl = parent;\r\n                nodeInsertBefore.call(parent, this.el, node);\r\n            }\r\n            moveBeforeVNode(other, afterNode) {\r\n                nodeInsertBefore.call(this.parentEl, this.el, other ? other.el : afterNode);\r\n            }\r\n            toString() {\r\n                const div = document.createElement(\"div\");\r\n                this.mount(div, null);\r\n                return div.innerHTML;\r\n            }\r\n            mount(parent, afterNode) {\r\n                const el = nodeCloneNode.call(template, true);\r\n                nodeInsertBefore.call(parent, el, afterNode);\r\n                this.el = el;\r\n                this.parentEl = parent;\r\n            }\r\n            patch(other, withBeforeRemove) { }\r\n        }\r\n        if (isDynamic) {\r\n            Block.prototype.mount = function mount(parent, afterNode) {\r\n                const el = nodeCloneNode.call(template, true);\r\n                // collecting references\r\n                const refs = new Array(refN);\r\n                this.refs = refs;\r\n                refs[0] = el;\r\n                for (let i = 0; i < colN; i++) {\r\n                    const w = collectors[i];\r\n                    refs[w.idx] = w.getVal.call(refs[w.prevIdx]);\r\n                }\r\n                // applying data to all update points\r\n                if (locN) {\r\n                    const data = this.data;\r\n                    for (let i = 0; i < locN; i++) {\r\n                        const loc = locations[i];\r\n                        loc.setData.call(refs[loc.refIdx], data[i]);\r\n                    }\r\n                }\r\n                nodeInsertBefore.call(parent, el, afterNode);\r\n                // preparing all children\r\n                if (childN) {\r\n                    const children = this.children;\r\n                    for (let i = 0; i < childN; i++) {\r\n                        const child = children[i];\r\n                        if (child) {\r\n                            const loc = childrenLocs[i];\r\n                            const afterNode = loc.afterRefIdx ? refs[loc.afterRefIdx] : null;\r\n                            child.isOnlyChild = loc.isOnlyChild;\r\n                            child.mount(refs[loc.parentRefIdx], afterNode);\r\n                        }\r\n                    }\r\n                }\r\n                this.el = el;\r\n                this.parentEl = parent;\r\n            };\r\n            Block.prototype.patch = function patch(other, withBeforeRemove) {\r\n                if (this === other) {\r\n                    return;\r\n                }\r\n                const refs = this.refs;\r\n                // update texts/attributes/\r\n                if (locN) {\r\n                    const data1 = this.data;\r\n                    const data2 = other.data;\r\n                    for (let i = 0; i < locN; i++) {\r\n                        const val1 = data1[i];\r\n                        const val2 = data2[i];\r\n                        if (val1 !== val2) {\r\n                            const loc = locations[i];\r\n                            loc.updateData.call(refs[loc.refIdx], val2, val1);\r\n                        }\r\n                    }\r\n                    this.data = data2;\r\n                }\r\n                // update children\r\n                if (childN) {\r\n                    let children1 = this.children;\r\n                    const children2 = other.children;\r\n                    for (let i = 0; i < childN; i++) {\r\n                        const child1 = children1[i];\r\n                        const child2 = children2[i];\r\n                        if (child1) {\r\n                            if (child2) {\r\n                                child1.patch(child2, withBeforeRemove);\r\n                            }\r\n                            else {\r\n                                if (withBeforeRemove) {\r\n                                    child1.beforeRemove();\r\n                                }\r\n                                child1.remove();\r\n                                children1[i] = undefined;\r\n                            }\r\n                        }\r\n                        else if (child2) {\r\n                            const loc = childrenLocs[i];\r\n                            const afterNode = loc.afterRefIdx ? refs[loc.afterRefIdx] : null;\r\n                            child2.mount(refs[loc.parentRefIdx], afterNode);\r\n                            children1[i] = child2;\r\n                        }\r\n                    }\r\n                }\r\n            };\r\n        }\r\n        return Block;\r\n    }\r\n    function setText(value) {\r\n        characterDataSetData.call(this, toText(value));\r\n    }\r\n    function makeRefSetter(index, refs) {\r\n        return function setRef(fn) {\r\n            refs[refs.length - 1][index] = () => fn(this);\r\n        };\r\n    }\r\n\r\n    const getDescriptor = (o, p) => Object.getOwnPropertyDescriptor(o, p);\r\n    const nodeProto$1 = Node.prototype;\r\n    const nodeInsertBefore$1 = nodeProto$1.insertBefore;\r\n    const nodeAppendChild = nodeProto$1.appendChild;\r\n    const nodeRemoveChild$1 = nodeProto$1.removeChild;\r\n    const nodeSetTextContent = getDescriptor(nodeProto$1, \"textContent\").set;\r\n    // -----------------------------------------------------------------------------\r\n    // List Node\r\n    // -----------------------------------------------------------------------------\r\n    class VList {\r\n        constructor(children) {\r\n            this.children = children;\r\n        }\r\n        mount(parent, afterNode) {\r\n            const children = this.children;\r\n            const _anchor = document.createTextNode(\"\");\r\n            this.anchor = _anchor;\r\n            nodeInsertBefore$1.call(parent, _anchor, afterNode);\r\n            const l = children.length;\r\n            if (l) {\r\n                const mount = children[0].mount;\r\n                for (let i = 0; i < l; i++) {\r\n                    mount.call(children[i], parent, _anchor);\r\n                }\r\n            }\r\n            this.parentEl = parent;\r\n        }\r\n        moveBeforeDOMNode(node, parent = this.parentEl) {\r\n            this.parentEl = parent;\r\n            const children = this.children;\r\n            for (let i = 0, l = children.length; i < l; i++) {\r\n                children[i].moveBeforeDOMNode(node, parent);\r\n            }\r\n            parent.insertBefore(this.anchor, node);\r\n        }\r\n        moveBeforeVNode(other, afterNode) {\r\n            if (other) {\r\n                const next = other.children[0];\r\n                afterNode = (next ? next.firstNode() : other.anchor) || null;\r\n            }\r\n            const children = this.children;\r\n            for (let i = 0, l = children.length; i < l; i++) {\r\n                children[i].moveBeforeVNode(null, afterNode);\r\n            }\r\n            this.parentEl.insertBefore(this.anchor, afterNode);\r\n        }\r\n        patch(other, withBeforeRemove) {\r\n            if (this === other) {\r\n                return;\r\n            }\r\n            const ch1 = this.children;\r\n            const ch2 = other.children;\r\n            if (ch2.length === 0 && ch1.length === 0) {\r\n                return;\r\n            }\r\n            this.children = ch2;\r\n            const proto = ch2[0] || ch1[0];\r\n            const { mount: cMount, patch: cPatch, remove: cRemove, beforeRemove, moveBeforeVNode: cMoveBefore, firstNode: cFirstNode, } = proto;\r\n            const _anchor = this.anchor;\r\n            const isOnlyChild = this.isOnlyChild;\r\n            const parent = this.parentEl;\r\n            // fast path: no new child => only remove\r\n            if (ch2.length === 0 && isOnlyChild) {\r\n                if (withBeforeRemove) {\r\n                    for (let i = 0, l = ch1.length; i < l; i++) {\r\n                        beforeRemove.call(ch1[i]);\r\n                    }\r\n                }\r\n                nodeSetTextContent.call(parent, \"\");\r\n                nodeAppendChild.call(parent, _anchor);\r\n                return;\r\n            }\r\n            let startIdx1 = 0;\r\n            let startIdx2 = 0;\r\n            let startVn1 = ch1[0];\r\n            let startVn2 = ch2[0];\r\n            let endIdx1 = ch1.length - 1;\r\n            let endIdx2 = ch2.length - 1;\r\n            let endVn1 = ch1[endIdx1];\r\n            let endVn2 = ch2[endIdx2];\r\n            let mapping = undefined;\r\n            while (startIdx1 <= endIdx1 && startIdx2 <= endIdx2) {\r\n                // -------------------------------------------------------------------\r\n                if (startVn1 === null) {\r\n                    startVn1 = ch1[++startIdx1];\r\n                    continue;\r\n                }\r\n                // -------------------------------------------------------------------\r\n                if (endVn1 === null) {\r\n                    endVn1 = ch1[--endIdx1];\r\n                    continue;\r\n                }\r\n                // -------------------------------------------------------------------\r\n                let startKey1 = startVn1.key;\r\n                let startKey2 = startVn2.key;\r\n                if (startKey1 === startKey2) {\r\n                    cPatch.call(startVn1, startVn2, withBeforeRemove);\r\n                    ch2[startIdx2] = startVn1;\r\n                    startVn1 = ch1[++startIdx1];\r\n                    startVn2 = ch2[++startIdx2];\r\n                    continue;\r\n                }\r\n                // -------------------------------------------------------------------\r\n                let endKey1 = endVn1.key;\r\n                let endKey2 = endVn2.key;\r\n                if (endKey1 === endKey2) {\r\n                    cPatch.call(endVn1, endVn2, withBeforeRemove);\r\n                    ch2[endIdx2] = endVn1;\r\n                    endVn1 = ch1[--endIdx1];\r\n                    endVn2 = ch2[--endIdx2];\r\n                    continue;\r\n                }\r\n                // -------------------------------------------------------------------\r\n                if (startKey1 === endKey2) {\r\n                    // bnode moved right\r\n                    cPatch.call(startVn1, endVn2, withBeforeRemove);\r\n                    ch2[endIdx2] = startVn1;\r\n                    const nextChild = ch2[endIdx2 + 1];\r\n                    cMoveBefore.call(startVn1, nextChild, _anchor);\r\n                    startVn1 = ch1[++startIdx1];\r\n                    endVn2 = ch2[--endIdx2];\r\n                    continue;\r\n                }\r\n                // -------------------------------------------------------------------\r\n                if (endKey1 === startKey2) {\r\n                    // bnode moved left\r\n                    cPatch.call(endVn1, startVn2, withBeforeRemove);\r\n                    ch2[startIdx2] = endVn1;\r\n                    const nextChild = ch1[startIdx1];\r\n                    cMoveBefore.call(endVn1, nextChild, _anchor);\r\n                    endVn1 = ch1[--endIdx1];\r\n                    startVn2 = ch2[++startIdx2];\r\n                    continue;\r\n                }\r\n                // -------------------------------------------------------------------\r\n                mapping = mapping || createMapping(ch1, startIdx1, endIdx1);\r\n                let idxInOld = mapping[startKey2];\r\n                if (idxInOld === undefined) {\r\n                    cMount.call(startVn2, parent, cFirstNode.call(startVn1) || null);\r\n                }\r\n                else {\r\n                    const elmToMove = ch1[idxInOld];\r\n                    cMoveBefore.call(elmToMove, startVn1, null);\r\n                    cPatch.call(elmToMove, startVn2, withBeforeRemove);\r\n                    ch2[startIdx2] = elmToMove;\r\n                    ch1[idxInOld] = null;\r\n                }\r\n                startVn2 = ch2[++startIdx2];\r\n            }\r\n            // ---------------------------------------------------------------------\r\n            if (startIdx1 <= endIdx1 || startIdx2 <= endIdx2) {\r\n                if (startIdx1 > endIdx1) {\r\n                    const nextChild = ch2[endIdx2 + 1];\r\n                    const anchor = nextChild ? cFirstNode.call(nextChild) || null : _anchor;\r\n                    for (let i = startIdx2; i <= endIdx2; i++) {\r\n                        cMount.call(ch2[i], parent, anchor);\r\n                    }\r\n                }\r\n                else {\r\n                    for (let i = startIdx1; i <= endIdx1; i++) {\r\n                        let ch = ch1[i];\r\n                        if (ch) {\r\n                            if (withBeforeRemove) {\r\n                                beforeRemove.call(ch);\r\n                            }\r\n                            cRemove.call(ch);\r\n                        }\r\n                    }\r\n                }\r\n            }\r\n        }\r\n        beforeRemove() {\r\n            const children = this.children;\r\n            const l = children.length;\r\n            if (l) {\r\n                const beforeRemove = children[0].beforeRemove;\r\n                for (let i = 0; i < l; i++) {\r\n                    beforeRemove.call(children[i]);\r\n                }\r\n            }\r\n        }\r\n        remove() {\r\n            const { parentEl, anchor } = this;\r\n            if (this.isOnlyChild) {\r\n                nodeSetTextContent.call(parentEl, \"\");\r\n            }\r\n            else {\r\n                const children = this.children;\r\n                const l = children.length;\r\n                if (l) {\r\n                    const remove = children[0].remove;\r\n                    for (let i = 0; i < l; i++) {\r\n                        remove.call(children[i]);\r\n                    }\r\n                }\r\n                nodeRemoveChild$1.call(parentEl, anchor);\r\n            }\r\n        }\r\n        firstNode() {\r\n            const child = this.children[0];\r\n            return child ? child.firstNode() : undefined;\r\n        }\r\n        toString() {\r\n            return this.children.map((c) => c.toString()).join(\"\");\r\n        }\r\n    }\r\n    function list(children) {\r\n        return new VList(children);\r\n    }\r\n    function createMapping(ch1, startIdx1, endIdx2) {\r\n        let mapping = {};\r\n        for (let i = startIdx1; i <= endIdx2; i++) {\r\n            mapping[ch1[i].key] = i;\r\n        }\r\n        return mapping;\r\n    }\r\n\r\n    const nodeProto = Node.prototype;\r\n    const nodeInsertBefore = nodeProto.insertBefore;\r\n    const nodeRemoveChild = nodeProto.removeChild;\r\n    class VHtml {\r\n        constructor(html) {\r\n            this.content = [];\r\n            this.html = html;\r\n        }\r\n        mount(parent, afterNode) {\r\n            this.parentEl = parent;\r\n            const template = document.createElement(\"template\");\r\n            template.innerHTML = this.html;\r\n            this.content = [...template.content.childNodes];\r\n            for (let elem of this.content) {\r\n                nodeInsertBefore.call(parent, elem, afterNode);\r\n            }\r\n            if (!this.content.length) {\r\n                const textNode = document.createTextNode(\"\");\r\n                this.content.push(textNode);\r\n                nodeInsertBefore.call(parent, textNode, afterNode);\r\n            }\r\n        }\r\n        moveBeforeDOMNode(node, parent = this.parentEl) {\r\n            this.parentEl = parent;\r\n            for (let elem of this.content) {\r\n                nodeInsertBefore.call(parent, elem, node);\r\n            }\r\n        }\r\n        moveBeforeVNode(other, afterNode) {\r\n            const target = other ? other.content[0] : afterNode;\r\n            this.moveBeforeDOMNode(target);\r\n        }\r\n        patch(other) {\r\n            if (this === other) {\r\n                return;\r\n            }\r\n            const html2 = other.html;\r\n            if (this.html !== html2) {\r\n                const parent = this.parentEl;\r\n                // insert new html in front of current\r\n                const afterNode = this.content[0];\r\n                const template = document.createElement(\"template\");\r\n                template.innerHTML = html2;\r\n                const content = [...template.content.childNodes];\r\n                for (let elem of content) {\r\n                    nodeInsertBefore.call(parent, elem, afterNode);\r\n                }\r\n                if (!content.length) {\r\n                    const textNode = document.createTextNode(\"\");\r\n                    content.push(textNode);\r\n                    nodeInsertBefore.call(parent, textNode, afterNode);\r\n                }\r\n                // remove current content\r\n                this.remove();\r\n                this.content = content;\r\n                this.html = other.html;\r\n            }\r\n        }\r\n        beforeRemove() { }\r\n        remove() {\r\n            const parent = this.parentEl;\r\n            for (let elem of this.content) {\r\n                nodeRemoveChild.call(parent, elem);\r\n            }\r\n        }\r\n        firstNode() {\r\n            return this.content[0];\r\n        }\r\n        toString() {\r\n            return this.html;\r\n        }\r\n    }\r\n    function html(str) {\r\n        return new VHtml(str);\r\n    }\r\n\r\n    function createCatcher(eventsSpec) {\r\n        const n = Object.keys(eventsSpec).length;\r\n        class VCatcher {\r\n            constructor(child, handlers) {\r\n                this.handlerFns = [];\r\n                this.afterNode = null;\r\n                this.child = child;\r\n                this.handlerData = handlers;\r\n            }\r\n            mount(parent, afterNode) {\r\n                this.parentEl = parent;\r\n                this.child.mount(parent, afterNode);\r\n                this.afterNode = document.createTextNode(\"\");\r\n                parent.insertBefore(this.afterNode, afterNode);\r\n                this.wrapHandlerData();\r\n                for (let name in eventsSpec) {\r\n                    const index = eventsSpec[name];\r\n                    const handler = createEventHandler(name);\r\n                    this.handlerFns[index] = handler;\r\n                    handler.setup.call(parent, this.handlerData[index]);\r\n                }\r\n            }\r\n            wrapHandlerData() {\r\n                for (let i = 0; i < n; i++) {\r\n                    let handler = this.handlerData[i];\r\n                    // handler = [...mods, fn, comp], so we need to replace second to last elem\r\n                    let idx = handler.length - 2;\r\n                    let origFn = handler[idx];\r\n                    const self = this;\r\n                    handler[idx] = function (ev) {\r\n                        const target = ev.target;\r\n                        let currentNode = self.child.firstNode();\r\n                        const afterNode = self.afterNode;\r\n                        while (currentNode && currentNode !== afterNode) {\r\n                            if (currentNode.contains(target)) {\r\n                                return origFn.call(this, ev);\r\n                            }\r\n                            currentNode = currentNode.nextSibling;\r\n                        }\r\n                    };\r\n                }\r\n            }\r\n            moveBeforeDOMNode(node, parent = this.parentEl) {\r\n                this.parentEl = parent;\r\n                this.child.moveBeforeDOMNode(node, parent);\r\n                parent.insertBefore(this.afterNode, node);\r\n            }\r\n            moveBeforeVNode(other, afterNode) {\r\n                if (other) {\r\n                    // check this with @ged-odoo for use in foreach\r\n                    afterNode = other.firstNode() || afterNode;\r\n                }\r\n                this.child.moveBeforeVNode(other ? other.child : null, afterNode);\r\n                this.parentEl.insertBefore(this.afterNode, afterNode);\r\n            }\r\n            patch(other, withBeforeRemove) {\r\n                if (this === other) {\r\n                    return;\r\n                }\r\n                this.handlerData = other.handlerData;\r\n                this.wrapHandlerData();\r\n                for (let i = 0; i < n; i++) {\r\n                    this.handlerFns[i].update.call(this.parentEl, this.handlerData[i]);\r\n                }\r\n                this.child.patch(other.child, withBeforeRemove);\r\n            }\r\n            beforeRemove() {\r\n                this.child.beforeRemove();\r\n            }\r\n            remove() {\r\n                for (let i = 0; i < n; i++) {\r\n                    this.handlerFns[i].remove.call(this.parentEl);\r\n                }\r\n                this.child.remove();\r\n                this.afterNode.remove();\r\n            }\r\n            firstNode() {\r\n                return this.child.firstNode();\r\n            }\r\n            toString() {\r\n                return this.child.toString();\r\n            }\r\n        }\r\n        return function (child, handlers) {\r\n            return new VCatcher(child, handlers);\r\n        };\r\n    }\r\n\r\n    function mount$1(vnode, fixture, afterNode = null) {\r\n        vnode.mount(fixture, afterNode);\r\n    }\r\n    function patch(vnode1, vnode2, withBeforeRemove = false) {\r\n        vnode1.patch(vnode2, withBeforeRemove);\r\n    }\r\n    function remove(vnode, withBeforeRemove = false) {\r\n        if (withBeforeRemove) {\r\n            vnode.beforeRemove();\r\n        }\r\n        vnode.remove();\r\n    }\r\n\r\n    // Maps fibers to thrown errors\r\n    const fibersInError = new WeakMap();\r\n    const nodeErrorHandlers = new WeakMap();\r\n    function _handleError(node, error) {\r\n        if (!node) {\r\n            return false;\r\n        }\r\n        const fiber = node.fiber;\r\n        if (fiber) {\r\n            fibersInError.set(fiber, error);\r\n        }\r\n        const errorHandlers = nodeErrorHandlers.get(node);\r\n        if (errorHandlers) {\r\n            let handled = false;\r\n            // execute in the opposite order\r\n            for (let i = errorHandlers.length - 1; i >= 0; i--) {\r\n                try {\r\n                    errorHandlers[i](error);\r\n                    handled = true;\r\n                    break;\r\n                }\r\n                catch (e) {\r\n                    error = e;\r\n                }\r\n            }\r\n            if (handled) {\r\n                return true;\r\n            }\r\n        }\r\n        return _handleError(node.parent, error);\r\n    }\r\n    function handleError(params) {\r\n        let { error } = params;\r\n        // Wrap error if it wasn't wrapped by wrapError (ie when not in dev mode)\r\n        if (!(error instanceof OwlError)) {\r\n            error = Object.assign(new OwlError(`An error occured in the owl lifecycle (see this Error's \"cause\" property)`), { cause: error });\r\n        }\r\n        const node = \"node\" in params ? params.node : params.fiber.node;\r\n        const fiber = \"fiber\" in params ? params.fiber : node.fiber;\r\n        if (fiber) {\r\n            // resets the fibers on components if possible. This is important so that\r\n            // new renderings can be properly included in the initial one, if any.\r\n            let current = fiber;\r\n            do {\r\n                current.node.fiber = current;\r\n                current = current.parent;\r\n            } while (current);\r\n            fibersInError.set(fiber.root, error);\r\n        }\r\n        const handled = _handleError(node, error);\r\n        if (!handled) {\r\n            console.warn(`[Owl] Unhandled error. Destroying the root component`);\r\n            try {\r\n                node.app.destroy();\r\n            }\r\n            catch (e) {\r\n                console.error(e);\r\n            }\r\n            throw error;\r\n        }\r\n    }\r\n\r\n    function makeChildFiber(node, parent) {\r\n        let current = node.fiber;\r\n        if (current) {\r\n            cancelFibers(current.children);\r\n            current.root = null;\r\n        }\r\n        return new Fiber(node, parent);\r\n    }\r\n    function makeRootFiber(node) {\r\n        let current = node.fiber;\r\n        if (current) {\r\n            let root = current.root;\r\n            // lock root fiber because canceling children fibers may destroy components,\r\n            // which means any arbitrary code can be run in onWillDestroy, which may\r\n            // trigger new renderings\r\n            root.locked = true;\r\n            root.setCounter(root.counter + 1 - cancelFibers(current.children));\r\n            root.locked = false;\r\n            current.children = [];\r\n            current.childrenMap = {};\r\n            current.bdom = null;\r\n            if (fibersInError.has(current)) {\r\n                fibersInError.delete(current);\r\n                fibersInError.delete(root);\r\n                current.appliedToDom = false;\r\n                if (current instanceof RootFiber) {\r\n                    // it is possible that this fiber is a fiber that crashed while being\r\n                    // mounted, so the mounted list is possibly corrupted. We restore it to\r\n                    // its normal initial state (which is empty list or a list with a mount\r\n                    // fiber.\r\n                    current.mounted = current instanceof MountFiber ? [current] : [];\r\n                }\r\n            }\r\n            return current;\r\n        }\r\n        const fiber = new RootFiber(node, null);\r\n        if (node.willPatch.length) {\r\n            fiber.willPatch.push(fiber);\r\n        }\r\n        if (node.patched.length) {\r\n            fiber.patched.push(fiber);\r\n        }\r\n        return fiber;\r\n    }\r\n    function throwOnRender() {\r\n        throw new OwlError(\"Attempted to render cancelled fiber\");\r\n    }\r\n    /**\r\n     * @returns number of not-yet rendered fibers cancelled\r\n     */\r\n    function cancelFibers(fibers) {\r\n        let result = 0;\r\n        for (let fiber of fibers) {\r\n            let node = fiber.node;\r\n            fiber.render = throwOnRender;\r\n            if (node.status === 0 /* NEW */) {\r\n                node.cancel();\r\n            }\r\n            node.fiber = null;\r\n            if (fiber.bdom) {\r\n                // if fiber has been rendered, this means that the component props have\r\n                // been updated. however, this fiber will not be patched to the dom, so\r\n                // it could happen that the next render compare the current props with\r\n                // the same props, and skip the render completely. With the next line,\r\n                // we kindly request the component code to force a render, so it works as\r\n                // expected.\r\n                node.forceNextRender = true;\r\n            }\r\n            else {\r\n                result++;\r\n            }\r\n            result += cancelFibers(fiber.children);\r\n        }\r\n        return result;\r\n    }\r\n    class Fiber {\r\n        constructor(node, parent) {\r\n            this.bdom = null;\r\n            this.children = [];\r\n            this.appliedToDom = false;\r\n            this.deep = false;\r\n            this.childrenMap = {};\r\n            this.node = node;\r\n            this.parent = parent;\r\n            if (parent) {\r\n                this.deep = parent.deep;\r\n                const root = parent.root;\r\n                root.setCounter(root.counter + 1);\r\n                this.root = root;\r\n                parent.children.push(this);\r\n            }\r\n            else {\r\n                this.root = this;\r\n            }\r\n        }\r\n        render() {\r\n            // if some parent has a fiber => register in followup\r\n            let prev = this.root.node;\r\n            let scheduler = prev.app.scheduler;\r\n            let current = prev.parent;\r\n            while (current) {\r\n                if (current.fiber) {\r\n                    let root = current.fiber.root;\r\n                    if (root.counter === 0 && prev.parentKey in current.fiber.childrenMap) {\r\n                        current = root.node;\r\n                    }\r\n                    else {\r\n                        scheduler.delayedRenders.push(this);\r\n                        return;\r\n                    }\r\n                }\r\n                prev = current;\r\n                current = current.parent;\r\n            }\r\n            // there are no current rendering from above => we can render\r\n            this._render();\r\n        }\r\n        _render() {\r\n            const node = this.node;\r\n            const root = this.root;\r\n            if (root) {\r\n                try {\r\n                    this.bdom = true;\r\n                    this.bdom = node.renderFn();\r\n                }\r\n                catch (e) {\r\n                    node.app.handleError({ node, error: e });\r\n                }\r\n                root.setCounter(root.counter - 1);\r\n            }\r\n        }\r\n    }\r\n    class RootFiber extends Fiber {\r\n        constructor() {\r\n            super(...arguments);\r\n            this.counter = 1;\r\n            // only add stuff in this if they have registered some hooks\r\n            this.willPatch = [];\r\n            this.patched = [];\r\n            this.mounted = [];\r\n            // A fiber is typically locked when it is completing and the patch has not, or is being applied.\r\n            // i.e.: render triggered in onWillUnmount or in willPatch will be delayed\r\n            this.locked = false;\r\n        }\r\n        complete() {\r\n            const node = this.node;\r\n            this.locked = true;\r\n            let current = undefined;\r\n            let mountedFibers = this.mounted;\r\n            try {\r\n                // Step 1: calling all willPatch lifecycle hooks\r\n                for (current of this.willPatch) {\r\n                    // because of the asynchronous nature of the rendering, some parts of the\r\n                    // UI may have been rendered, then deleted in a followup rendering, and we\r\n                    // do not want to call onWillPatch in that case.\r\n                    let node = current.node;\r\n                    if (node.fiber === current) {\r\n                        const component = node.component;\r\n                        for (let cb of node.willPatch) {\r\n                            cb.call(component);\r\n                        }\r\n                    }\r\n                }\r\n                current = undefined;\r\n                // Step 2: patching the dom\r\n                node._patch();\r\n                this.locked = false;\r\n                // Step 4: calling all mounted lifecycle hooks\r\n                while ((current = mountedFibers.pop())) {\r\n                    current = current;\r\n                    if (current.appliedToDom) {\r\n                        for (let cb of current.node.mounted) {\r\n                            cb();\r\n                        }\r\n                    }\r\n                }\r\n                // Step 5: calling all patched hooks\r\n                let patchedFibers = this.patched;\r\n                while ((current = patchedFibers.pop())) {\r\n                    current = current;\r\n                    if (current.appliedToDom) {\r\n                        for (let cb of current.node.patched) {\r\n                            cb();\r\n                        }\r\n                    }\r\n                }\r\n            }\r\n            catch (e) {\r\n                // if mountedFibers is not empty, this means that a crash occured while\r\n                // calling the mounted hooks of some component. So, there may still be\r\n                // some component that have been mounted, but for which the mounted hooks\r\n                // have not been called. Here, we remove the willUnmount hooks for these\r\n                // specific component to prevent a worse situation (willUnmount being\r\n                // called even though mounted has not been called)\r\n                for (let fiber of mountedFibers) {\r\n                    fiber.node.willUnmount = [];\r\n                }\r\n                this.locked = false;\r\n                node.app.handleError({ fiber: current || this, error: e });\r\n            }\r\n        }\r\n        setCounter(newValue) {\r\n            this.counter = newValue;\r\n            if (newValue === 0) {\r\n                this.node.app.scheduler.flush();\r\n            }\r\n        }\r\n    }\r\n    class MountFiber extends RootFiber {\r\n        constructor(node, target, options = {}) {\r\n            super(node, null);\r\n            this.target = target;\r\n            this.position = options.position || \"last-child\";\r\n        }\r\n        complete() {\r\n            let current = this;\r\n            try {\r\n                const node = this.node;\r\n                node.children = this.childrenMap;\r\n                node.app.constructor.validateTarget(this.target);\r\n                if (node.bdom) {\r\n                    // this is a complicated situation: if we mount a fiber with an existing\r\n                    // bdom, this means that this same fiber was already completed, mounted,\r\n                    // but a crash occurred in some mounted hook. Then, it was handled and\r\n                    // the new rendering is being applied.\r\n                    node.updateDom();\r\n                }\r\n                else {\r\n                    node.bdom = this.bdom;\r\n                    if (this.position === \"last-child\" || this.target.childNodes.length === 0) {\r\n                        mount$1(node.bdom, this.target);\r\n                    }\r\n                    else {\r\n                        const firstChild = this.target.childNodes[0];\r\n                        mount$1(node.bdom, this.target, firstChild);\r\n                    }\r\n                }\r\n                // unregistering the fiber before mounted since it can do another render\r\n                // and that the current rendering is obviously completed\r\n                node.fiber = null;\r\n                node.status = 1 /* MOUNTED */;\r\n                this.appliedToDom = true;\r\n                let mountedFibers = this.mounted;\r\n                while ((current = mountedFibers.pop())) {\r\n                    if (current.appliedToDom) {\r\n                        for (let cb of current.node.mounted) {\r\n                            cb();\r\n                        }\r\n                    }\r\n                }\r\n            }\r\n            catch (e) {\r\n                this.node.app.handleError({ fiber: current, error: e });\r\n            }\r\n        }\r\n    }\r\n\r\n    // Special key to subscribe to, to be notified of key creation/deletion\r\n    const KEYCHANGES = Symbol(\"Key changes\");\r\n    // Used to specify the absence of a callback, can be used as WeakMap key but\r\n    // should only be used as a sentinel value and never called.\r\n    const NO_CALLBACK = () => {\r\n        throw new Error(\"Called NO_CALLBACK. Owl is broken, please report this to the maintainers.\");\r\n    };\r\n    const objectToString = Object.prototype.toString;\r\n    const objectHasOwnProperty = Object.prototype.hasOwnProperty;\r\n    // Use arrays because Array.includes is faster than Set.has for small arrays\r\n    const SUPPORTED_RAW_TYPES = [\"Object\", \"Array\", \"Set\", \"Map\", \"WeakMap\"];\r\n    const COLLECTION_RAW_TYPES = [\"Set\", \"Map\", \"WeakMap\"];\r\n    /**\r\n     * extract \"RawType\" from strings like \"[object RawType]\" => this lets us ignore\r\n     * many native objects such as Promise (whose toString is [object Promise])\r\n     * or Date ([object Date]), while also supporting collections without using\r\n     * instanceof in a loop\r\n     *\r\n     * @param obj the object to check\r\n     * @returns the raw type of the object\r\n     */\r\n    function rawType(obj) {\r\n        return objectToString.call(toRaw(obj)).slice(8, -1);\r\n    }\r\n    /**\r\n     * Checks whether a given value can be made into a reactive object.\r\n     *\r\n     * @param value the value to check\r\n     * @returns whether the value can be made reactive\r\n     */\r\n    function canBeMadeReactive(value) {\r\n        if (typeof value !== \"object\") {\r\n            return false;\r\n        }\r\n        return SUPPORTED_RAW_TYPES.includes(rawType(value));\r\n    }\r\n    /**\r\n     * Creates a reactive from the given object/callback if possible and returns it,\r\n     * returns the original object otherwise.\r\n     *\r\n     * @param value the value make reactive\r\n     * @returns a reactive for the given object when possible, the original otherwise\r\n     */\r\n    function possiblyReactive(val, cb) {\r\n        return canBeMadeReactive(val) ? reactive(val, cb) : val;\r\n    }\r\n    const skipped = new WeakSet();\r\n    /**\r\n     * Mark an object or array so that it is ignored by the reactivity system\r\n     *\r\n     * @param value the value to mark\r\n     * @returns the object itself\r\n     */\r\n    function markRaw(value) {\r\n        skipped.add(value);\r\n        return value;\r\n    }\r\n    /**\r\n     * Given a reactive objet, return the raw (non reactive) underlying object\r\n     *\r\n     * @param value a reactive value\r\n     * @returns the underlying value\r\n     */\r\n    function toRaw(value) {\r\n        return targets.has(value) ? targets.get(value) : value;\r\n    }\r\n    const targetToKeysToCallbacks = new WeakMap();\r\n    /**\r\n     * Observes a given key on a target with an callback. The callback will be\r\n     * called when the given key changes on the target.\r\n     *\r\n     * @param target the target whose key should be observed\r\n     * @param key the key to observe (or Symbol(KEYCHANGES) for key creation\r\n     *  or deletion)\r\n     * @param callback the function to call when the key changes\r\n     */\r\n    function observeTargetKey(target, key, callback) {\r\n        if (callback === NO_CALLBACK) {\r\n            return;\r\n        }\r\n        if (!targetToKeysToCallbacks.get(target)) {\r\n            targetToKeysToCallbacks.set(target, new Map());\r\n        }\r\n        const keyToCallbacks = targetToKeysToCallbacks.get(target);\r\n        if (!keyToCallbacks.get(key)) {\r\n            keyToCallbacks.set(key, new Set());\r\n        }\r\n        keyToCallbacks.get(key).add(callback);\r\n        if (!callbacksToTargets.has(callback)) {\r\n            callbacksToTargets.set(callback, new Set());\r\n        }\r\n        callbacksToTargets.get(callback).add(target);\r\n    }\r\n    /**\r\n     * Notify Reactives that are observing a given target that a key has changed on\r\n     * the target.\r\n     *\r\n     * @param target target whose Reactives should be notified that the target was\r\n     *  changed.\r\n     * @param key the key that changed (or Symbol `KEYCHANGES` if a key was created\r\n     *   or deleted)\r\n     */\r\n    function notifyReactives(target, key) {\r\n        const keyToCallbacks = targetToKeysToCallbacks.get(target);\r\n        if (!keyToCallbacks) {\r\n            return;\r\n        }\r\n        const callbacks = keyToCallbacks.get(key);\r\n        if (!callbacks) {\r\n            return;\r\n        }\r\n        // Loop on copy because clearReactivesForCallback will modify the set in place\r\n        for (const callback of [...callbacks]) {\r\n            clearReactivesForCallback(callback);\r\n            callback();\r\n        }\r\n    }\r\n    const callbacksToTargets = new WeakMap();\r\n    /**\r\n     * Clears all subscriptions of the Reactives associated with a given callback.\r\n     *\r\n     * @param callback the callback for which the reactives need to be cleared\r\n     */\r\n    function clearReactivesForCallback(callback) {\r\n        const targetsToClear = callbacksToTargets.get(callback);\r\n        if (!targetsToClear) {\r\n            return;\r\n        }\r\n        for (const target of targetsToClear) {\r\n            const observedKeys = targetToKeysToCallbacks.get(target);\r\n            if (!observedKeys) {\r\n                continue;\r\n            }\r\n            for (const [key, callbacks] of observedKeys.entries()) {\r\n                callbacks.delete(callback);\r\n                if (!callbacks.size) {\r\n                    observedKeys.delete(key);\r\n                }\r\n            }\r\n        }\r\n        targetsToClear.clear();\r\n    }\r\n    function getSubscriptions(callback) {\r\n        const targets = callbacksToTargets.get(callback) || [];\r\n        return [...targets].map((target) => {\r\n            const keysToCallbacks = targetToKeysToCallbacks.get(target);\r\n            let keys = [];\r\n            if (keysToCallbacks) {\r\n                for (const [key, cbs] of keysToCallbacks) {\r\n                    if (cbs.has(callback)) {\r\n                        keys.push(key);\r\n                    }\r\n                }\r\n            }\r\n            return { target, keys };\r\n        });\r\n    }\r\n    // Maps reactive objects to the underlying target\r\n    const targets = new WeakMap();\r\n    const reactiveCache = new WeakMap();\r\n    /**\r\n     * Creates a reactive proxy for an object. Reading data on the reactive object\r\n     * subscribes to changes to the data. Writing data on the object will cause the\r\n     * notify callback to be called if there are suscriptions to that data. Nested\r\n     * objects and arrays are automatically made reactive as well.\r\n     *\r\n     * Whenever you are notified of a change, all subscriptions are cleared, and if\r\n     * you would like to be notified of any further changes, you should go read\r\n     * the underlying data again. We assume that if you don't go read it again after\r\n     * being notified, it means that you are no longer interested in that data.\r\n     *\r\n     * Subscriptions:\r\n     * + Reading a property on an object will subscribe you to changes in the value\r\n     *    of that property.\r\n     * + Accessing an object's keys (eg with Object.keys or with `for..in`) will\r\n     *    subscribe you to the creation/deletion of keys. Checking the presence of a\r\n     *    key on the object with 'in' has the same effect.\r\n     * - getOwnPropertyDescriptor does not currently subscribe you to the property.\r\n     *    This is a choice that was made because changing a key's value will trigger\r\n     *    this trap and we do not want to subscribe by writes. This also means that\r\n     *    Object.hasOwnProperty doesn't subscribe as it goes through this trap.\r\n     *\r\n     * @param target the object for which to create a reactive proxy\r\n     * @param callback the function to call when an observed property of the\r\n     *  reactive has changed\r\n     * @returns a proxy that tracks changes to it\r\n     */\r\n    function reactive(target, callback = NO_CALLBACK) {\r\n        if (!canBeMadeReactive(target)) {\r\n            throw new OwlError(`Cannot make the given value reactive`);\r\n        }\r\n        if (skipped.has(target)) {\r\n            return target;\r\n        }\r\n        if (targets.has(target)) {\r\n            // target is reactive, create a reactive on the underlying object instead\r\n            return reactive(targets.get(target), callback);\r\n        }\r\n        if (!reactiveCache.has(target)) {\r\n            reactiveCache.set(target, new WeakMap());\r\n        }\r\n        const reactivesForTarget = reactiveCache.get(target);\r\n        if (!reactivesForTarget.has(callback)) {\r\n            const targetRawType = rawType(target);\r\n            const handler = COLLECTION_RAW_TYPES.includes(targetRawType)\r\n                ? collectionsProxyHandler(target, callback, targetRawType)\r\n                : basicProxyHandler(callback);\r\n            const proxy = new Proxy(target, handler);\r\n            reactivesForTarget.set(callback, proxy);\r\n            targets.set(proxy, target);\r\n        }\r\n        return reactivesForTarget.get(callback);\r\n    }\r\n    /**\r\n     * Creates a basic proxy handler for regular objects and arrays.\r\n     *\r\n     * @param callback @see reactive\r\n     * @returns a proxy handler object\r\n     */\r\n    function basicProxyHandler(callback) {\r\n        return {\r\n            get(target, key, receiver) {\r\n                // non-writable non-configurable properties cannot be made reactive\r\n                const desc = Object.getOwnPropertyDescriptor(target, key);\r\n                if (desc && !desc.writable && !desc.configurable) {\r\n                    return Reflect.get(target, key, receiver);\r\n                }\r\n                observeTargetKey(target, key, callback);\r\n                return possiblyReactive(Reflect.get(target, key, receiver), callback);\r\n            },\r\n            set(target, key, value, receiver) {\r\n                const hadKey = objectHasOwnProperty.call(target, key);\r\n                const originalValue = Reflect.get(target, key, receiver);\r\n                const ret = Reflect.set(target, key, toRaw(value), receiver);\r\n                if (!hadKey && objectHasOwnProperty.call(target, key)) {\r\n                    notifyReactives(target, KEYCHANGES);\r\n                }\r\n                // While Array length may trigger the set trap, it's not actually set by this\r\n                // method but is updated behind the scenes, and the trap is not called with the\r\n                // new value. We disable the \"same-value-optimization\" for it because of that.\r\n                if (originalValue !== Reflect.get(target, key, receiver) ||\r\n                    (key === \"length\" && Array.isArray(target))) {\r\n                    notifyReactives(target, key);\r\n                }\r\n                return ret;\r\n            },\r\n            deleteProperty(target, key) {\r\n                const ret = Reflect.deleteProperty(target, key);\r\n                // TODO: only notify when something was actually deleted\r\n                notifyReactives(target, KEYCHANGES);\r\n                notifyReactives(target, key);\r\n                return ret;\r\n            },\r\n            ownKeys(target) {\r\n                observeTargetKey(target, KEYCHANGES, callback);\r\n                return Reflect.ownKeys(target);\r\n            },\r\n            has(target, key) {\r\n                // TODO: this observes all key changes instead of only the presence of the argument key\r\n                // observing the key itself would observe value changes instead of presence changes\r\n                // so we may need a finer grained system to distinguish observing value vs presence.\r\n                observeTargetKey(target, KEYCHANGES, callback);\r\n                return Reflect.has(target, key);\r\n            },\r\n        };\r\n    }\r\n    /**\r\n     * Creates a function that will observe the key that is passed to it when called\r\n     * and delegates to the underlying method.\r\n     *\r\n     * @param methodName name of the method to delegate to\r\n     * @param target @see reactive\r\n     * @param callback @see reactive\r\n     */\r\n    function makeKeyObserver(methodName, target, callback) {\r\n        return (key) => {\r\n            key = toRaw(key);\r\n            observeTargetKey(target, key, callback);\r\n            return possiblyReactive(target[methodName](key), callback);\r\n        };\r\n    }\r\n    /**\r\n     * Creates an iterable that will delegate to the underlying iteration method and\r\n     * observe keys as necessary.\r\n     *\r\n     * @param methodName name of the method to delegate to\r\n     * @param target @see reactive\r\n     * @param callback @see reactive\r\n     */\r\n    function makeIteratorObserver(methodName, target, callback) {\r\n        return function* () {\r\n            observeTargetKey(target, KEYCHANGES, callback);\r\n            const keys = target.keys();\r\n            for (const item of target[methodName]()) {\r\n                const key = keys.next().value;\r\n                observeTargetKey(target, key, callback);\r\n                yield possiblyReactive(item, callback);\r\n            }\r\n        };\r\n    }\r\n    /**\r\n     * Creates a forEach function that will delegate to forEach on the underlying\r\n     * collection while observing key changes, and keys as they're iterated over,\r\n     * and making the passed keys/values reactive.\r\n     *\r\n     * @param target @see reactive\r\n     * @param callback @see reactive\r\n     */\r\n    function makeForEachObserver(target, callback) {\r\n        return function forEach(forEachCb, thisArg) {\r\n            observeTargetKey(target, KEYCHANGES, callback);\r\n            target.forEach(function (val, key, targetObj) {\r\n                observeTargetKey(target, key, callback);\r\n                forEachCb.call(thisArg, possiblyReactive(val, callback), possiblyReactive(key, callback), possiblyReactive(targetObj, callback));\r\n            }, thisArg);\r\n        };\r\n    }\r\n    /**\r\n     * Creates a function that will delegate to an underlying method, and check if\r\n     * that method has modified the presence or value of a key, and notify the\r\n     * reactives appropriately.\r\n     *\r\n     * @param setterName name of the method to delegate to\r\n     * @param getterName name of the method which should be used to retrieve the\r\n     *  value before calling the delegate method for comparison purposes\r\n     * @param target @see reactive\r\n     */\r\n    function delegateAndNotify(setterName, getterName, target) {\r\n        return (key, value) => {\r\n            key = toRaw(key);\r\n            const hadKey = target.has(key);\r\n            const originalValue = target[getterName](key);\r\n            const ret = target[setterName](key, value);\r\n            const hasKey = target.has(key);\r\n            if (hadKey !== hasKey) {\r\n                notifyReactives(target, KEYCHANGES);\r\n            }\r\n            if (originalValue !== target[getterName](key)) {\r\n                notifyReactives(target, key);\r\n            }\r\n            return ret;\r\n        };\r\n    }\r\n    /**\r\n     * Creates a function that will clear the underlying collection and notify that\r\n     * the keys of the collection have changed.\r\n     *\r\n     * @param target @see reactive\r\n     */\r\n    function makeClearNotifier(target) {\r\n        return () => {\r\n            const allKeys = [...target.keys()];\r\n            target.clear();\r\n            notifyReactives(target, KEYCHANGES);\r\n            for (const key of allKeys) {\r\n                notifyReactives(target, key);\r\n            }\r\n        };\r\n    }\r\n    /**\r\n     * Maps raw type of an object to an object containing functions that can be used\r\n     * to build an appropritate proxy handler for that raw type. Eg: when making a\r\n     * reactive set, calling the has method should mark the key that is being\r\n     * retrieved as observed, and calling the add or delete method should notify the\r\n     * reactives that the key which is being added or deleted has been modified.\r\n     */\r\n    const rawTypeToFuncHandlers = {\r\n        Set: (target, callback) => ({\r\n            has: makeKeyObserver(\"has\", target, callback),\r\n            add: delegateAndNotify(\"add\", \"has\", target),\r\n            delete: delegateAndNotify(\"delete\", \"has\", target),\r\n            keys: makeIteratorObserver(\"keys\", target, callback),\r\n            values: makeIteratorObserver(\"values\", target, callback),\r\n            entries: makeIteratorObserver(\"entries\", target, callback),\r\n            [Symbol.iterator]: makeIteratorObserver(Symbol.iterator, target, callback),\r\n            forEach: makeForEachObserver(target, callback),\r\n            clear: makeClearNotifier(target),\r\n            get size() {\r\n                observeTargetKey(target, KEYCHANGES, callback);\r\n                return target.size;\r\n            },\r\n        }),\r\n        Map: (target, callback) => ({\r\n            has: makeKeyObserver(\"has\", target, callback),\r\n            get: makeKeyObserver(\"get\", target, callback),\r\n            set: delegateAndNotify(\"set\", \"get\", target),\r\n            delete: delegateAndNotify(\"delete\", \"has\", target),\r\n            keys: makeIteratorObserver(\"keys\", target, callback),\r\n            values: makeIteratorObserver(\"values\", target, callback),\r\n            entries: makeIteratorObserver(\"entries\", target, callback),\r\n            [Symbol.iterator]: makeIteratorObserver(Symbol.iterator, target, callback),\r\n            forEach: makeForEachObserver(target, callback),\r\n            clear: makeClearNotifier(target),\r\n            get size() {\r\n                observeTargetKey(target, KEYCHANGES, callback);\r\n                return target.size;\r\n            },\r\n        }),\r\n        WeakMap: (target, callback) => ({\r\n            has: makeKeyObserver(\"has\", target, callback),\r\n            get: makeKeyObserver(\"get\", target, callback),\r\n            set: delegateAndNotify(\"set\", \"get\", target),\r\n            delete: delegateAndNotify(\"delete\", \"has\", target),\r\n        }),\r\n    };\r\n    /**\r\n     * Creates a proxy handler for collections (Set/Map/WeakMap)\r\n     *\r\n     * @param callback @see reactive\r\n     * @param target @see reactive\r\n     * @returns a proxy handler object\r\n     */\r\n    function collectionsProxyHandler(target, callback, targetRawType) {\r\n        // TODO: if performance is an issue we can create the special handlers lazily when each\r\n        // property is read.\r\n        const specialHandlers = rawTypeToFuncHandlers[targetRawType](target, callback);\r\n        return Object.assign(basicProxyHandler(callback), {\r\n            // FIXME: probably broken when part of prototype chain since we ignore the receiver\r\n            get(target, key) {\r\n                if (objectHasOwnProperty.call(specialHandlers, key)) {\r\n                    return specialHandlers[key];\r\n                }\r\n                observeTargetKey(target, key, callback);\r\n                return possiblyReactive(target[key], callback);\r\n            },\r\n        });\r\n    }\r\n\r\n    let currentNode = null;\r\n    function saveCurrent() {\r\n        let n = currentNode;\r\n        return () => {\r\n            currentNode = n;\r\n        };\r\n    }\r\n    function getCurrent() {\r\n        if (!currentNode) {\r\n            throw new OwlError(\"No active component (a hook function should only be called in 'setup')\");\r\n        }\r\n        return currentNode;\r\n    }\r\n    function useComponent() {\r\n        return currentNode.component;\r\n    }\r\n    /**\r\n     * Apply default props (only top level).\r\n     */\r\n    function applyDefaultProps(props, defaultProps) {\r\n        for (let propName in defaultProps) {\r\n            if (props[propName] === undefined) {\r\n                props[propName] = defaultProps[propName];\r\n            }\r\n        }\r\n    }\r\n    // -----------------------------------------------------------------------------\r\n    // Integration with reactivity system (useState)\r\n    // -----------------------------------------------------------------------------\r\n    const batchedRenderFunctions = new WeakMap();\r\n    /**\r\n     * Creates a reactive object that will be observed by the current component.\r\n     * Reading data from the returned object (eg during rendering) will cause the\r\n     * component to subscribe to that data and be rerendered when it changes.\r\n     *\r\n     * @param state the state to observe\r\n     * @returns a reactive object that will cause the component to re-render on\r\n     *  relevant changes\r\n     * @see reactive\r\n     */\r\n    function useState(state) {\r\n        const node = getCurrent();\r\n        let render = batchedRenderFunctions.get(node);\r\n        if (!render) {\r\n            render = batched(node.render.bind(node, false));\r\n            batchedRenderFunctions.set(node, render);\r\n            // manual implementation of onWillDestroy to break cyclic dependency\r\n            node.willDestroy.push(clearReactivesForCallback.bind(null, render));\r\n        }\r\n        return reactive(state, render);\r\n    }\r\n    class ComponentNode {\r\n        constructor(C, props, app, parent, parentKey) {\r\n            this.fiber = null;\r\n            this.bdom = null;\r\n            this.status = 0 /* NEW */;\r\n            this.forceNextRender = false;\r\n            this.nextProps = null;\r\n            this.children = Object.create(null);\r\n            this.refs = {};\r\n            this.willStart = [];\r\n            this.willUpdateProps = [];\r\n            this.willUnmount = [];\r\n            this.mounted = [];\r\n            this.willPatch = [];\r\n            this.patched = [];\r\n            this.willDestroy = [];\r\n            currentNode = this;\r\n            this.app = app;\r\n            this.parent = parent;\r\n            this.props = props;\r\n            this.parentKey = parentKey;\r\n            const defaultProps = C.defaultProps;\r\n            props = Object.assign({}, props);\r\n            if (defaultProps) {\r\n                applyDefaultProps(props, defaultProps);\r\n            }\r\n            const env = (parent && parent.childEnv) || app.env;\r\n            this.childEnv = env;\r\n            for (const key in props) {\r\n                const prop = props[key];\r\n                if (prop && typeof prop === \"object\" && targets.has(prop)) {\r\n                    props[key] = useState(prop);\r\n                }\r\n            }\r\n            this.component = new C(props, env, this);\r\n            const ctx = Object.assign(Object.create(this.component), { this: this.component });\r\n            this.renderFn = app.getTemplate(C.template).bind(this.component, ctx, this);\r\n            this.component.setup();\r\n            currentNode = null;\r\n        }\r\n        mountComponent(target, options) {\r\n            const fiber = new MountFiber(this, target, options);\r\n            this.app.scheduler.addFiber(fiber);\r\n            this.initiateRender(fiber);\r\n        }\r\n        async initiateRender(fiber) {\r\n            this.fiber = fiber;\r\n            if (this.mounted.length) {\r\n                fiber.root.mounted.push(fiber);\r\n            }\r\n            const component = this.component;\r\n            try {\r\n                await Promise.all(this.willStart.map((f) => f.call(component)));\r\n            }\r\n            catch (e) {\r\n                this.app.handleError({ node: this, error: e });\r\n                return;\r\n            }\r\n            if (this.status === 0 /* NEW */ && this.fiber === fiber) {\r\n                fiber.render();\r\n            }\r\n        }\r\n        async render(deep) {\r\n            if (this.status >= 2 /* CANCELLED */) {\r\n                return;\r\n            }\r\n            let current = this.fiber;\r\n            if (current && (current.root.locked || current.bdom === true)) {\r\n                await Promise.resolve();\r\n                // situation may have changed after the microtask tick\r\n                current = this.fiber;\r\n            }\r\n            if (current) {\r\n                if (!current.bdom && !fibersInError.has(current)) {\r\n                    if (deep) {\r\n                        // we want the render from this point on to be with deep=true\r\n                        current.deep = deep;\r\n                    }\r\n                    return;\r\n                }\r\n                // if current rendering was with deep=true, we want this one to be the same\r\n                deep = deep || current.deep;\r\n            }\r\n            else if (!this.bdom) {\r\n                return;\r\n            }\r\n            const fiber = makeRootFiber(this);\r\n            fiber.deep = deep;\r\n            this.fiber = fiber;\r\n            this.app.scheduler.addFiber(fiber);\r\n            await Promise.resolve();\r\n            if (this.status >= 2 /* CANCELLED */) {\r\n                return;\r\n            }\r\n            // We only want to actually render the component if the following two\r\n            // conditions are true:\r\n            // * this.fiber: it could be null, in which case the render has been cancelled\r\n            // * (current || !fiber.parent): if current is not null, this means that the\r\n            //   render function was called when a render was already occurring. In this\r\n            //   case, the pending rendering was cancelled, and the fiber needs to be\r\n            //   rendered to complete the work.  If current is null, we check that the\r\n            //   fiber has no parent.  If that is the case, the fiber was downgraded from\r\n            //   a root fiber to a child fiber in the previous microtick, because it was\r\n            //   embedded in a rendering coming from above, so the fiber will be rendered\r\n            //   in the next microtick anyway, so we should not render it again.\r\n            if (this.fiber === fiber && (current || !fiber.parent)) {\r\n                fiber.render();\r\n            }\r\n        }\r\n        cancel() {\r\n            this._cancel();\r\n            delete this.parent.children[this.parentKey];\r\n            this.app.scheduler.scheduleDestroy(this);\r\n        }\r\n        _cancel() {\r\n            this.status = 2 /* CANCELLED */;\r\n            const children = this.children;\r\n            for (let childKey in children) {\r\n                children[childKey]._cancel();\r\n            }\r\n        }\r\n        destroy() {\r\n            let shouldRemove = this.status === 1 /* MOUNTED */;\r\n            this._destroy();\r\n            if (shouldRemove) {\r\n                this.bdom.remove();\r\n            }\r\n        }\r\n        _destroy() {\r\n            const component = this.component;\r\n            if (this.status === 1 /* MOUNTED */) {\r\n                for (let cb of this.willUnmount) {\r\n                    cb.call(component);\r\n                }\r\n            }\r\n            for (let child of Object.values(this.children)) {\r\n                child._destroy();\r\n            }\r\n            if (this.willDestroy.length) {\r\n                try {\r\n                    for (let cb of this.willDestroy) {\r\n                        cb.call(component);\r\n                    }\r\n                }\r\n                catch (e) {\r\n                    this.app.handleError({ error: e, node: this });\r\n                }\r\n            }\r\n            this.status = 3 /* DESTROYED */;\r\n        }\r\n        async updateAndRender(props, parentFiber) {\r\n            this.nextProps = props;\r\n            props = Object.assign({}, props);\r\n            // update\r\n            const fiber = makeChildFiber(this, parentFiber);\r\n            this.fiber = fiber;\r\n            const component = this.component;\r\n            const defaultProps = component.constructor.defaultProps;\r\n            if (defaultProps) {\r\n                applyDefaultProps(props, defaultProps);\r\n            }\r\n            currentNode = this;\r\n            for (const key in props) {\r\n                const prop = props[key];\r\n                if (prop && typeof prop === \"object\" && targets.has(prop)) {\r\n                    props[key] = useState(prop);\r\n                }\r\n            }\r\n            currentNode = null;\r\n            const prom = Promise.all(this.willUpdateProps.map((f) => f.call(component, props)));\r\n            await prom;\r\n            if (fiber !== this.fiber) {\r\n                return;\r\n            }\r\n            component.props = props;\r\n            fiber.render();\r\n            const parentRoot = parentFiber.root;\r\n            if (this.willPatch.length) {\r\n                parentRoot.willPatch.push(fiber);\r\n            }\r\n            if (this.patched.length) {\r\n                parentRoot.patched.push(fiber);\r\n            }\r\n        }\r\n        /**\r\n         * Finds a child that has dom that is not yet updated, and update it. This\r\n         * method is meant to be used only in the context of repatching the dom after\r\n         * a mounted hook failed and was handled.\r\n         */\r\n        updateDom() {\r\n            if (!this.fiber) {\r\n                return;\r\n            }\r\n            if (this.bdom === this.fiber.bdom) {\r\n                // If the error was handled by some child component, we need to find it to\r\n                // apply its change\r\n                for (let k in this.children) {\r\n                    const child = this.children[k];\r\n                    child.updateDom();\r\n                }\r\n            }\r\n            else {\r\n                // if we get here, this is the component that handled the error and rerendered\r\n                // itself, so we can simply patch the dom\r\n                this.bdom.patch(this.fiber.bdom, false);\r\n                this.fiber.appliedToDom = true;\r\n                this.fiber = null;\r\n            }\r\n        }\r\n        /**\r\n         * Sets a ref to a given HTMLElement.\r\n         *\r\n         * @param name the name of the ref to set\r\n         * @param el the HTMLElement to set the ref to. The ref is not set if the el\r\n         *  is null, but useRef will not return elements that are not in the DOM\r\n         */\r\n        setRef(name, el) {\r\n            if (el) {\r\n                this.refs[name] = el;\r\n            }\r\n        }\r\n        // ---------------------------------------------------------------------------\r\n        // Block DOM methods\r\n        // ---------------------------------------------------------------------------\r\n        firstNode() {\r\n            const bdom = this.bdom;\r\n            return bdom ? bdom.firstNode() : undefined;\r\n        }\r\n        mount(parent, anchor) {\r\n            const bdom = this.fiber.bdom;\r\n            this.bdom = bdom;\r\n            bdom.mount(parent, anchor);\r\n            this.status = 1 /* MOUNTED */;\r\n            this.fiber.appliedToDom = true;\r\n            this.children = this.fiber.childrenMap;\r\n            this.fiber = null;\r\n        }\r\n        moveBeforeDOMNode(node, parent) {\r\n            this.bdom.moveBeforeDOMNode(node, parent);\r\n        }\r\n        moveBeforeVNode(other, afterNode) {\r\n            this.bdom.moveBeforeVNode(other ? other.bdom : null, afterNode);\r\n        }\r\n        patch() {\r\n            if (this.fiber && this.fiber.parent) {\r\n                // we only patch here renderings coming from above. renderings initiated\r\n                // by the component will be patched independently in the appropriate\r\n                // fiber.complete\r\n                this._patch();\r\n                this.props = this.nextProps;\r\n            }\r\n        }\r\n        _patch() {\r\n            let hasChildren = false;\r\n            // eslint-disable-next-line @typescript-eslint/no-unused-vars\r\n            for (let _k in this.children) {\r\n                hasChildren = true;\r\n                break;\r\n            }\r\n            const fiber = this.fiber;\r\n            this.children = fiber.childrenMap;\r\n            this.bdom.patch(fiber.bdom, hasChildren);\r\n            fiber.appliedToDom = true;\r\n            this.fiber = null;\r\n        }\r\n        beforeRemove() {\r\n            this._destroy();\r\n        }\r\n        remove() {\r\n            this.bdom.remove();\r\n        }\r\n        // ---------------------------------------------------------------------------\r\n        // Some debug helpers\r\n        // ---------------------------------------------------------------------------\r\n        get name() {\r\n            return this.component.constructor.name;\r\n        }\r\n        get subscriptions() {\r\n            const render = batchedRenderFunctions.get(this);\r\n            return render ? getSubscriptions(render) : [];\r\n        }\r\n    }\r\n\r\n    const TIMEOUT = Symbol(\"timeout\");\r\n    const HOOK_TIMEOUT = {\r\n        onWillStart: 3000,\r\n        onWillUpdateProps: 3000,\r\n    };\r\n    function wrapError(fn, hookName) {\r\n        const error = new OwlError();\r\n        const timeoutError = new OwlError();\r\n        const node = getCurrent();\r\n        return (...args) => {\r\n            const onError = (cause) => {\r\n                error.cause = cause;\r\n                error.message =\r\n                    cause instanceof Error\r\n                        ? `The following error occurred in ${hookName}: \"${cause.message}\"`\r\n                        : `Something that is not an Error was thrown in ${hookName} (see this Error's \"cause\" property)`;\r\n                throw error;\r\n            };\r\n            let result;\r\n            try {\r\n                result = fn(...args);\r\n            }\r\n            catch (cause) {\r\n                onError(cause);\r\n            }\r\n            if (!(result instanceof Promise)) {\r\n                return result;\r\n            }\r\n            const timeout = HOOK_TIMEOUT[hookName];\r\n            if (timeout) {\r\n                const fiber = node.fiber;\r\n                Promise.race([\r\n                    result.catch(() => { }),\r\n                    new Promise((resolve) => setTimeout(() => resolve(TIMEOUT), timeout)),\r\n                ]).then((res) => {\r\n                    if (res === TIMEOUT && node.fiber === fiber && node.status <= 2) {\r\n                        timeoutError.message = `${hookName}'s promise hasn't resolved after ${timeout / 1000} seconds`;\r\n                        console.log(timeoutError);\r\n                    }\r\n                });\r\n            }\r\n            return result.catch(onError);\r\n        };\r\n    }\r\n    // -----------------------------------------------------------------------------\r\n    //  hooks\r\n    // -----------------------------------------------------------------------------\r\n    function onWillStart(fn) {\r\n        const node = getCurrent();\r\n        const decorate = node.app.dev ? wrapError : (fn) => fn;\r\n        node.willStart.push(decorate(fn.bind(node.component), \"onWillStart\"));\r\n    }\r\n    function onWillUpdateProps(fn) {\r\n        const node = getCurrent();\r\n        const decorate = node.app.dev ? wrapError : (fn) => fn;\r\n        node.willUpdateProps.push(decorate(fn.bind(node.component), \"onWillUpdateProps\"));\r\n    }\r\n    function onMounted(fn) {\r\n        const node = getCurrent();\r\n        const decorate = node.app.dev ? wrapError : (fn) => fn;\r\n        node.mounted.push(decorate(fn.bind(node.component), \"onMounted\"));\r\n    }\r\n    function onWillPatch(fn) {\r\n        const node = getCurrent();\r\n        const decorate = node.app.dev ? wrapError : (fn) => fn;\r\n        node.willPatch.unshift(decorate(fn.bind(node.component), \"onWillPatch\"));\r\n    }\r\n    function onPatched(fn) {\r\n        const node = getCurrent();\r\n        const decorate = node.app.dev ? wrapError : (fn) => fn;\r\n        node.patched.push(decorate(fn.bind(node.component), \"onPatched\"));\r\n    }\r\n    function onWillUnmount(fn) {\r\n        const node = getCurrent();\r\n        const decorate = node.app.dev ? wrapError : (fn) => fn;\r\n        node.willUnmount.unshift(decorate(fn.bind(node.component), \"onWillUnmount\"));\r\n    }\r\n    function onWillDestroy(fn) {\r\n        const node = getCurrent();\r\n        const decorate = node.app.dev ? wrapError : (fn) => fn;\r\n        node.willDestroy.push(decorate(fn.bind(node.component), \"onWillDestroy\"));\r\n    }\r\n    function onWillRender(fn) {\r\n        const node = getCurrent();\r\n        const renderFn = node.renderFn;\r\n        const decorate = node.app.dev ? wrapError : (fn) => fn;\r\n        fn = decorate(fn.bind(node.component), \"onWillRender\");\r\n        node.renderFn = () => {\r\n            fn();\r\n            return renderFn();\r\n        };\r\n    }\r\n    function onRendered(fn) {\r\n        const node = getCurrent();\r\n        const renderFn = node.renderFn;\r\n        const decorate = node.app.dev ? wrapError : (fn) => fn;\r\n        fn = decorate(fn.bind(node.component), \"onRendered\");\r\n        node.renderFn = () => {\r\n            const result = renderFn();\r\n            fn();\r\n            return result;\r\n        };\r\n    }\r\n    function onError(callback) {\r\n        const node = getCurrent();\r\n        let handlers = nodeErrorHandlers.get(node);\r\n        if (!handlers) {\r\n            handlers = [];\r\n            nodeErrorHandlers.set(node, handlers);\r\n        }\r\n        handlers.push(callback.bind(node.component));\r\n    }\r\n\r\n    class Component {\r\n        constructor(props, env, node) {\r\n            this.props = props;\r\n            this.env = env;\r\n            this.__owl__ = node;\r\n        }\r\n        setup() { }\r\n        render(deep = false) {\r\n            this.__owl__.render(deep === true);\r\n        }\r\n    }\r\n    Component.template = \"\";\r\n\r\n    const VText = text(\"\").constructor;\r\n    class VPortal extends VText {\r\n        constructor(selector, content) {\r\n            super(\"\");\r\n            this.target = null;\r\n            this.selector = selector;\r\n            this.content = content;\r\n        }\r\n        mount(parent, anchor) {\r\n            super.mount(parent, anchor);\r\n            this.target = document.querySelector(this.selector);\r\n            if (this.target) {\r\n                this.content.mount(this.target, null);\r\n            }\r\n            else {\r\n                this.content.mount(parent, anchor);\r\n            }\r\n        }\r\n        beforeRemove() {\r\n            this.content.beforeRemove();\r\n        }\r\n        remove() {\r\n            if (this.content) {\r\n                super.remove();\r\n                this.content.remove();\r\n                this.content = null;\r\n            }\r\n        }\r\n        patch(other) {\r\n            super.patch(other);\r\n            if (this.content) {\r\n                this.content.patch(other.content, true);\r\n            }\r\n            else {\r\n                this.content = other.content;\r\n                this.content.mount(this.target, null);\r\n            }\r\n        }\r\n    }\r\n    /**\r\n     * kind of similar to <t t-slot=\"default\"/>, but it wraps it around a VPortal\r\n     */\r\n    function portalTemplate(app, bdom, helpers) {\r\n        let { callSlot } = helpers;\r\n        return function template(ctx, node, key = \"\") {\r\n            return new VPortal(ctx.props.target, callSlot(ctx, node, key, \"default\", false, null));\r\n        };\r\n    }\r\n    class Portal extends Component {\r\n        setup() {\r\n            const node = this.__owl__;\r\n            onMounted(() => {\r\n                const portal = node.bdom;\r\n                if (!portal.target) {\r\n                    const target = document.querySelector(this.props.target);\r\n                    if (target) {\r\n                        portal.content.moveBeforeDOMNode(target.firstChild, target);\r\n                    }\r\n                    else {\r\n                        throw new OwlError(\"invalid portal target\");\r\n                    }\r\n                }\r\n            });\r\n            onWillUnmount(() => {\r\n                const portal = node.bdom;\r\n                portal.remove();\r\n            });\r\n        }\r\n    }\r\n    Portal.template = \"__portal__\";\r\n    Portal.props = {\r\n        target: {\r\n            type: String,\r\n        },\r\n        slots: true,\r\n    };\r\n\r\n    // -----------------------------------------------------------------------------\r\n    // helpers\r\n    // -----------------------------------------------------------------------------\r\n    const isUnionType = (t) => Array.isArray(t);\r\n    const isBaseType = (t) => typeof t !== \"object\";\r\n    const isValueType = (t) => typeof t === \"object\" && t && \"value\" in t;\r\n    function isOptional(t) {\r\n        return typeof t === \"object\" && \"optional\" in t ? t.optional || false : false;\r\n    }\r\n    function describeType(type) {\r\n        return type === \"*\" || type === true ? \"value\" : type.name.toLowerCase();\r\n    }\r\n    function describe(info) {\r\n        if (isBaseType(info)) {\r\n            return describeType(info);\r\n        }\r\n        else if (isUnionType(info)) {\r\n            return info.map(describe).join(\" or \");\r\n        }\r\n        else if (isValueType(info)) {\r\n            return String(info.value);\r\n        }\r\n        if (\"element\" in info) {\r\n            return `list of ${describe({ type: info.element, optional: false })}s`;\r\n        }\r\n        if (\"shape\" in info) {\r\n            return `object`;\r\n        }\r\n        return describe(info.type || \"*\");\r\n    }\r\n    function toSchema(spec) {\r\n        return Object.fromEntries(spec.map((e) => e.endsWith(\"?\") ? [e.slice(0, -1), { optional: true }] : [e, { type: \"*\", optional: false }]));\r\n    }\r\n    /**\r\n     * Main validate function\r\n     */\r\n    function validate(obj, spec) {\r\n        let errors = validateSchema(obj, spec);\r\n        if (errors.length) {\r\n            throw new OwlError(\"Invalid object: \" + errors.join(\", \"));\r\n        }\r\n    }\r\n    /**\r\n     * Helper validate function, to get the list of errors. useful if one want to\r\n     * manipulate the errors without parsing an error object\r\n     */\r\n    function validateSchema(obj, schema) {\r\n        if (Array.isArray(schema)) {\r\n            schema = toSchema(schema);\r\n        }\r\n        obj = toRaw(obj);\r\n        let errors = [];\r\n        // check if each value in obj has correct shape\r\n        for (let key in obj) {\r\n            if (key in schema) {\r\n                let result = validateType(key, obj[key], schema[key]);\r\n                if (result) {\r\n                    errors.push(result);\r\n                }\r\n            }\r\n            else if (!(\"*\" in schema)) {\r\n                errors.push(`unknown key '${key}'`);\r\n            }\r\n        }\r\n        // check that all specified keys are defined in obj\r\n        for (let key in schema) {\r\n            const spec = schema[key];\r\n            if (key !== \"*\" && !isOptional(spec) && !(key in obj)) {\r\n                const isObj = typeof spec === \"object\" && !Array.isArray(spec);\r\n                const isAny = spec === \"*\" || (isObj && \"type\" in spec ? spec.type === \"*\" : isObj);\r\n                let detail = isAny ? \"\" : ` (should be a ${describe(spec)})`;\r\n                errors.push(`'${key}' is missing${detail}`);\r\n            }\r\n        }\r\n        return errors;\r\n    }\r\n    function validateBaseType(key, value, type) {\r\n        if (typeof type === \"function\") {\r\n            if (typeof value === \"object\") {\r\n                if (!(value instanceof type)) {\r\n                    return `'${key}' is not a ${describeType(type)}`;\r\n                }\r\n            }\r\n            else if (typeof value !== type.name.toLowerCase()) {\r\n                return `'${key}' is not a ${describeType(type)}`;\r\n            }\r\n        }\r\n        return null;\r\n    }\r\n    function validateArrayType(key, value, descr) {\r\n        if (!Array.isArray(value)) {\r\n            return `'${key}' is not a list of ${describe(descr)}s`;\r\n        }\r\n        for (let i = 0; i < value.length; i++) {\r\n            const error = validateType(`${key}[${i}]`, value[i], descr);\r\n            if (error) {\r\n                return error;\r\n            }\r\n        }\r\n        return null;\r\n    }\r\n    function validateType(key, value, descr) {\r\n        if (value === undefined) {\r\n            return isOptional(descr) ? null : `'${key}' is undefined (should be a ${describe(descr)})`;\r\n        }\r\n        else if (isBaseType(descr)) {\r\n            return validateBaseType(key, value, descr);\r\n        }\r\n        else if (isValueType(descr)) {\r\n            return value === descr.value ? null : `'${key}' is not equal to '${descr.value}'`;\r\n        }\r\n        else if (isUnionType(descr)) {\r\n            let validDescr = descr.find((p) => !validateType(key, value, p));\r\n            return validDescr ? null : `'${key}' is not a ${describe(descr)}`;\r\n        }\r\n        let result = null;\r\n        if (\"element\" in descr) {\r\n            result = validateArrayType(key, value, descr.element);\r\n        }\r\n        else if (\"shape\" in descr) {\r\n            if (typeof value !== \"object\" || Array.isArray(value)) {\r\n                result = `'${key}' is not an object`;\r\n            }\r\n            else {\r\n                const errors = validateSchema(value, descr.shape);\r\n                if (errors.length) {\r\n                    result = `'${key}' doesn't have the correct shape (${errors.join(\", \")})`;\r\n                }\r\n            }\r\n        }\r\n        else if (\"values\" in descr) {\r\n            if (typeof value !== \"object\" || Array.isArray(value)) {\r\n                result = `'${key}' is not an object`;\r\n            }\r\n            else {\r\n                const errors = Object.entries(value)\r\n                    .map(([key, value]) => validateType(key, value, descr.values))\r\n                    .filter(Boolean);\r\n                if (errors.length) {\r\n                    result = `some of the values in '${key}' are invalid (${errors.join(\", \")})`;\r\n                }\r\n            }\r\n        }\r\n        if (\"type\" in descr && !result) {\r\n            result = validateType(key, value, descr.type);\r\n        }\r\n        if (\"validate\" in descr && !result) {\r\n            result = !descr.validate(value) ? `'${key}' is not valid` : null;\r\n        }\r\n        return result;\r\n    }\r\n\r\n    const ObjectCreate = Object.create;\r\n    /**\r\n     * This file contains utility functions that will be injected in each template,\r\n     * to perform various useful tasks in the compiled code.\r\n     */\r\n    function withDefault(value, defaultValue) {\r\n        return value === undefined || value === null || value === false ? defaultValue : value;\r\n    }\r\n    function callSlot(ctx, parent, key, name, dynamic, extra, defaultContent) {\r\n        key = key + \"__slot_\" + name;\r\n        const slots = ctx.props.slots || {};\r\n        const { __render, __ctx, __scope } = slots[name] || {};\r\n        const slotScope = ObjectCreate(__ctx || {});\r\n        if (__scope) {\r\n            slotScope[__scope] = extra;\r\n        }\r\n        const slotBDom = __render ? __render(slotScope, parent, key) : null;\r\n        if (defaultContent) {\r\n            let child1 = undefined;\r\n            let child2 = undefined;\r\n            if (slotBDom) {\r\n                child1 = dynamic ? toggler(name, slotBDom) : slotBDom;\r\n            }\r\n            else {\r\n                child2 = defaultContent(ctx, parent, key);\r\n            }\r\n            return multi([child1, child2]);\r\n        }\r\n        return slotBDom || text(\"\");\r\n    }\r\n    function capture(ctx) {\r\n        const result = ObjectCreate(ctx);\r\n        for (let k in ctx) {\r\n            result[k] = ctx[k];\r\n        }\r\n        return result;\r\n    }\r\n    function withKey(elem, k) {\r\n        elem.key = k;\r\n        return elem;\r\n    }\r\n    function prepareList(collection) {\r\n        let keys;\r\n        let values;\r\n        if (Array.isArray(collection)) {\r\n            keys = collection;\r\n            values = collection;\r\n        }\r\n        else if (collection instanceof Map) {\r\n            keys = [...collection.keys()];\r\n            values = [...collection.values()];\r\n        }\r\n        else if (Symbol.iterator in Object(collection)) {\r\n            keys = [...collection];\r\n            values = keys;\r\n        }\r\n        else if (collection && typeof collection === \"object\") {\r\n            values = Object.values(collection);\r\n            keys = Object.keys(collection);\r\n        }\r\n        else {\r\n            throw new OwlError(`Invalid loop expression: \"${collection}\" is not iterable`);\r\n        }\r\n        const n = values.length;\r\n        return [keys, values, n, new Array(n)];\r\n    }\r\n    const isBoundary = Symbol(\"isBoundary\");\r\n    function setContextValue(ctx, key, value) {\r\n        const ctx0 = ctx;\r\n        while (!ctx.hasOwnProperty(key) && !ctx.hasOwnProperty(isBoundary)) {\r\n            const newCtx = ctx.__proto__;\r\n            if (!newCtx) {\r\n                ctx = ctx0;\r\n                break;\r\n            }\r\n            ctx = newCtx;\r\n        }\r\n        ctx[key] = value;\r\n    }\r\n    function toNumber(val) {\r\n        const n = parseFloat(val);\r\n        return isNaN(n) ? val : n;\r\n    }\r\n    function shallowEqual(l1, l2) {\r\n        for (let i = 0, l = l1.length; i < l; i++) {\r\n            if (l1[i] !== l2[i]) {\r\n                return false;\r\n            }\r\n        }\r\n        return true;\r\n    }\r\n    class LazyValue {\r\n        constructor(fn, ctx, component, node, key) {\r\n            this.fn = fn;\r\n            this.ctx = capture(ctx);\r\n            this.component = component;\r\n            this.node = node;\r\n            this.key = key;\r\n        }\r\n        evaluate() {\r\n            return this.fn.call(this.component, this.ctx, this.node, this.key);\r\n        }\r\n        toString() {\r\n            return this.evaluate().toString();\r\n        }\r\n    }\r\n    /*\r\n     * Safely outputs `value` as a block depending on the nature of `value`\r\n     */\r\n    function safeOutput(value, defaultValue) {\r\n        if (value === undefined || value === null) {\r\n            return defaultValue ? toggler(\"default\", defaultValue) : toggler(\"undefined\", text(\"\"));\r\n        }\r\n        let safeKey;\r\n        let block;\r\n        switch (typeof value) {\r\n            case \"object\":\r\n                if (value instanceof Markup) {\r\n                    safeKey = `string_safe`;\r\n                    block = html(value);\r\n                }\r\n                else if (value instanceof LazyValue) {\r\n                    safeKey = `lazy_value`;\r\n                    block = value.evaluate();\r\n                }\r\n                else if (value instanceof String) {\r\n                    safeKey = \"string_unsafe\";\r\n                    block = text(value);\r\n                }\r\n                else {\r\n                    // Assuming it is a block\r\n                    safeKey = \"block_safe\";\r\n                    block = value;\r\n                }\r\n                break;\r\n            case \"string\":\r\n                safeKey = \"string_unsafe\";\r\n                block = text(value);\r\n                break;\r\n            default:\r\n                safeKey = \"string_unsafe\";\r\n                block = text(String(value));\r\n        }\r\n        return toggler(safeKey, block);\r\n    }\r\n    /**\r\n     * Validate the component props (or next props) against the (static) props\r\n     * description.  This is potentially an expensive operation: it may needs to\r\n     * visit recursively the props and all the children to check if they are valid.\r\n     * This is why it is only done in 'dev' mode.\r\n     */\r\n    function validateProps(name, props, comp) {\r\n        const ComponentClass = typeof name !== \"string\"\r\n            ? name\r\n            : comp.constructor.components[name];\r\n        if (!ComponentClass) {\r\n            // this is an error, wrong component. We silently return here instead so the\r\n            // error is triggered by the usual path ('component' function)\r\n            return;\r\n        }\r\n        const schema = ComponentClass.props;\r\n        if (!schema) {\r\n            if (comp.__owl__.app.warnIfNoStaticProps) {\r\n                console.warn(`Component '${ComponentClass.name}' does not have a static props description`);\r\n            }\r\n            return;\r\n        }\r\n        const defaultProps = ComponentClass.defaultProps;\r\n        if (defaultProps) {\r\n            let isMandatory = (name) => Array.isArray(schema)\r\n                ? schema.includes(name)\r\n                : name in schema && !(\"*\" in schema) && !isOptional(schema[name]);\r\n            for (let p in defaultProps) {\r\n                if (isMandatory(p)) {\r\n                    throw new OwlError(`A default value cannot be defined for a mandatory prop (name: '${p}', component: ${ComponentClass.name})`);\r\n                }\r\n            }\r\n        }\r\n        const errors = validateSchema(props, schema);\r\n        if (errors.length) {\r\n            throw new OwlError(`Invalid props for component '${ComponentClass.name}': ` + errors.join(\", \"));\r\n        }\r\n    }\r\n    function makeRefWrapper(node) {\r\n        let refNames = new Set();\r\n        return (name, fn) => {\r\n            if (refNames.has(name)) {\r\n                throw new OwlError(`Cannot set the same ref more than once in the same component, ref \"${name}\" was set multiple times in ${node.name}`);\r\n            }\r\n            refNames.add(name);\r\n            return fn;\r\n        };\r\n    }\r\n    const helpers = {\r\n        withDefault,\r\n        zero: Symbol(\"zero\"),\r\n        isBoundary,\r\n        callSlot,\r\n        capture,\r\n        withKey,\r\n        prepareList,\r\n        setContextValue,\r\n        shallowEqual,\r\n        toNumber,\r\n        validateProps,\r\n        LazyValue,\r\n        safeOutput,\r\n        createCatcher,\r\n        markRaw,\r\n        OwlError,\r\n        makeRefWrapper,\r\n    };\r\n\r\n    /**\r\n     * Parses an XML string into an XML document, throwing errors on parser errors\r\n     * instead of returning an XML document containing the parseerror.\r\n     *\r\n     * @param xml the string to parse\r\n     * @returns an XML document corresponding to the content of the string\r\n     */\r\n    function parseXML(xml) {\r\n        const parser = new DOMParser();\r\n        const doc = parser.parseFromString(xml, \"text/xml\");\r\n        if (doc.getElementsByTagName(\"parsererror\").length) {\r\n            let msg = \"Invalid XML in template.\";\r\n            const parsererrorText = doc.getElementsByTagName(\"parsererror\")[0].textContent;\r\n            if (parsererrorText) {\r\n                msg += \"\\nThe parser has produced the following error message:\\n\" + parsererrorText;\r\n                const re = /\\d+/g;\r\n                const firstMatch = re.exec(parsererrorText);\r\n                if (firstMatch) {\r\n                    const lineNumber = Number(firstMatch[0]);\r\n                    const line = xml.split(\"\\n\")[lineNumber - 1];\r\n                    const secondMatch = re.exec(parsererrorText);\r\n                    if (line && secondMatch) {\r\n                        const columnIndex = Number(secondMatch[0]) - 1;\r\n                        if (line[columnIndex]) {\r\n                            msg +=\r\n                                `\\nThe error might be located at xml line ${lineNumber} column ${columnIndex}\\n` +\r\n                                    `${line}\\n${\"-\".repeat(columnIndex - 1)}^`;\r\n                        }\r\n                    }\r\n                }\r\n            }\r\n            throw new OwlError(msg);\r\n        }\r\n        return doc;\r\n    }\r\n\r\n    const bdom = { text, createBlock, list, multi, html, toggler, comment };\r\n    class TemplateSet {\r\n        constructor(config = {}) {\r\n            this.rawTemplates = Object.create(globalTemplates);\r\n            this.templates = {};\r\n            this.Portal = Portal;\r\n            this.dev = config.dev || false;\r\n            this.translateFn = config.translateFn;\r\n            this.translatableAttributes = config.translatableAttributes;\r\n            if (config.templates) {\r\n                if (config.templates instanceof Document || typeof config.templates === \"string\") {\r\n                    this.addTemplates(config.templates);\r\n                }\r\n                else {\r\n                    for (const name in config.templates) {\r\n                        this.addTemplate(name, config.templates[name]);\r\n                    }\r\n                }\r\n            }\r\n            this.getRawTemplate = config.getTemplate;\r\n            this.customDirectives = config.customDirectives || {};\r\n            this.runtimeUtils = { ...helpers, __globals__: config.globalValues || {} };\r\n            this.hasGlobalValues = Boolean(config.globalValues && Object.keys(config.globalValues).length);\r\n        }\r\n        static registerTemplate(name, fn) {\r\n            globalTemplates[name] = fn;\r\n        }\r\n        addTemplate(name, template) {\r\n            if (name in this.rawTemplates) {\r\n                // this check can be expensive, just silently ignore double definitions outside dev mode\r\n                if (!this.dev) {\r\n                    return;\r\n                }\r\n                const rawTemplate = this.rawTemplates[name];\r\n                const currentAsString = typeof rawTemplate === \"string\"\r\n                    ? rawTemplate\r\n                    : rawTemplate instanceof Element\r\n                        ? rawTemplate.outerHTML\r\n                        : rawTemplate.toString();\r\n                const newAsString = typeof template === \"string\" ? template : template.outerHTML;\r\n                if (currentAsString === newAsString) {\r\n                    return;\r\n                }\r\n                throw new OwlError(`Template ${name} already defined with different content`);\r\n            }\r\n            this.rawTemplates[name] = template;\r\n        }\r\n        addTemplates(xml) {\r\n            if (!xml) {\r\n                // empty string\r\n                return;\r\n            }\r\n            xml = xml instanceof Document ? xml : parseXML(xml);\r\n            for (const template of xml.querySelectorAll(\"[t-name]\")) {\r\n                const name = template.getAttribute(\"t-name\");\r\n                this.addTemplate(name, template);\r\n            }\r\n        }\r\n        getTemplate(name) {\r\n            var _a;\r\n            if (!(name in this.templates)) {\r\n                const rawTemplate = ((_a = this.getRawTemplate) === null || _a === void 0 ? void 0 : _a.call(this, name)) || this.rawTemplates[name];\r\n                if (rawTemplate === undefined) {\r\n                    let extraInfo = \"\";\r\n                    try {\r\n                        const componentName = getCurrent().component.constructor.name;\r\n                        extraInfo = ` (for component \"${componentName}\")`;\r\n                    }\r\n                    catch { }\r\n                    throw new OwlError(`Missing template: \"${name}\"${extraInfo}`);\r\n                }\r\n                const isFn = typeof rawTemplate === \"function\" && !(rawTemplate instanceof Element);\r\n                const templateFn = isFn ? rawTemplate : this._compileTemplate(name, rawTemplate);\r\n                // first add a function to lazily get the template, in case there is a\r\n                // recursive call to the template name\r\n                const templates = this.templates;\r\n                this.templates[name] = function (context, parent) {\r\n                    return templates[name].call(this, context, parent);\r\n                };\r\n                const template = templateFn(this, bdom, this.runtimeUtils);\r\n                this.templates[name] = template;\r\n            }\r\n            return this.templates[name];\r\n        }\r\n        _compileTemplate(name, template) {\r\n            throw new OwlError(`Unable to compile a template. Please use owl full build instead`);\r\n        }\r\n        callTemplate(owner, subTemplate, ctx, parent, key) {\r\n            const template = this.getTemplate(subTemplate);\r\n            return toggler(subTemplate, template.call(owner, ctx, parent, key + subTemplate));\r\n        }\r\n    }\r\n    // -----------------------------------------------------------------------------\r\n    //  xml tag helper\r\n    // -----------------------------------------------------------------------------\r\n    const globalTemplates = {};\r\n    function xml(...args) {\r\n        const name = `__template__${xml.nextId++}`;\r\n        const value = String.raw(...args);\r\n        globalTemplates[name] = value;\r\n        return name;\r\n    }\r\n    xml.nextId = 1;\r\n    TemplateSet.registerTemplate(\"__portal__\", portalTemplate);\r\n\r\n    /**\r\n     * Owl QWeb Expression Parser\r\n     *\r\n     * Owl needs in various contexts to be able to understand the structure of a\r\n     * string representing a javascript expression.  The usual goal is to be able\r\n     * to rewrite some variables.  For example, if a template has\r\n     *\r\n     *  ```xml\r\n     *  <t t-if=\"computeSomething({val: state.val})\">...</t>\r\n     * ```\r\n     *\r\n     * this needs to be translated in something like this:\r\n     *\r\n     * ```js\r\n     *   if (context[\"computeSomething\"]({val: context[\"state\"].val})) { ... }\r\n     * ```\r\n     *\r\n     * This file contains the implementation of an extremely naive tokenizer/parser\r\n     * and evaluator for javascript expressions.  The supported grammar is basically\r\n     * only expressive enough to understand the shape of objects, of arrays, and\r\n     * various operators.\r\n     */\r\n    //------------------------------------------------------------------------------\r\n    // Misc types, constants and helpers\r\n    //------------------------------------------------------------------------------\r\n    const RESERVED_WORDS = \"true,false,NaN,null,undefined,debugger,console,window,in,instanceof,new,function,return,eval,void,Math,RegExp,Array,Object,Date,__globals__\".split(\",\");\r\n    const WORD_REPLACEMENT = Object.assign(Object.create(null), {\r\n        and: \"&&\",\r\n        or: \"||\",\r\n        gt: \">\",\r\n        gte: \">=\",\r\n        lt: \"<\",\r\n        lte: \"<=\",\r\n    });\r\n    const STATIC_TOKEN_MAP = Object.assign(Object.create(null), {\r\n        \"{\": \"LEFT_BRACE\",\r\n        \"}\": \"RIGHT_BRACE\",\r\n        \"[\": \"LEFT_BRACKET\",\r\n        \"]\": \"RIGHT_BRACKET\",\r\n        \":\": \"COLON\",\r\n        \",\": \"COMMA\",\r\n        \"(\": \"LEFT_PAREN\",\r\n        \")\": \"RIGHT_PAREN\",\r\n    });\r\n    // note that the space after typeof is relevant. It makes sure that the formatted\r\n    // expression has a space after typeof. Currently we don't support delete and void\r\n    const OPERATORS = \"...,.,===,==,+,!==,!=,!,||,&&,>=,>,<=,<,?,-,*,/,%,typeof ,=>,=,;,in ,new ,|,&,^,~\".split(\",\");\r\n    let tokenizeString = function (expr) {\r\n        let s = expr[0];\r\n        let start = s;\r\n        if (s !== \"'\" && s !== '\"' && s !== \"`\") {\r\n            return false;\r\n        }\r\n        let i = 1;\r\n        let cur;\r\n        while (expr[i] && expr[i] !== start) {\r\n            cur = expr[i];\r\n            s += cur;\r\n            if (cur === \"\\\\\") {\r\n                i++;\r\n                cur = expr[i];\r\n                if (!cur) {\r\n                    throw new OwlError(\"Invalid expression\");\r\n                }\r\n                s += cur;\r\n            }\r\n            i++;\r\n        }\r\n        if (expr[i] !== start) {\r\n            throw new OwlError(\"Invalid expression\");\r\n        }\r\n        s += start;\r\n        if (start === \"`\") {\r\n            return {\r\n                type: \"TEMPLATE_STRING\",\r\n                value: s,\r\n                replace(replacer) {\r\n                    return s.replace(/\\$\\{(.*?)\\}/g, (match, group) => {\r\n                        return \"${\" + replacer(group) + \"}\";\r\n                    });\r\n                },\r\n            };\r\n        }\r\n        return { type: \"VALUE\", value: s };\r\n    };\r\n    let tokenizeNumber = function (expr) {\r\n        let s = expr[0];\r\n        if (s && s.match(/[0-9]/)) {\r\n            let i = 1;\r\n            while (expr[i] && expr[i].match(/[0-9]|\\./)) {\r\n                s += expr[i];\r\n                i++;\r\n            }\r\n            return { type: \"VALUE\", value: s };\r\n        }\r\n        else {\r\n            return false;\r\n        }\r\n    };\r\n    let tokenizeSymbol = function (expr) {\r\n        let s = expr[0];\r\n        if (s && s.match(/[a-zA-Z_\\$]/)) {\r\n            let i = 1;\r\n            while (expr[i] && expr[i].match(/\\w/)) {\r\n                s += expr[i];\r\n                i++;\r\n            }\r\n            if (s in WORD_REPLACEMENT) {\r\n                return { type: \"OPERATOR\", value: WORD_REPLACEMENT[s], size: s.length };\r\n            }\r\n            return { type: \"SYMBOL\", value: s };\r\n        }\r\n        else {\r\n            return false;\r\n        }\r\n    };\r\n    const tokenizeStatic = function (expr) {\r\n        const char = expr[0];\r\n        if (char && char in STATIC_TOKEN_MAP) {\r\n            return { type: STATIC_TOKEN_MAP[char], value: char };\r\n        }\r\n        return false;\r\n    };\r\n    const tokenizeOperator = function (expr) {\r\n        for (let op of OPERATORS) {\r\n            if (expr.startsWith(op)) {\r\n                return { type: \"OPERATOR\", value: op };\r\n            }\r\n        }\r\n        return false;\r\n    };\r\n    const TOKENIZERS = [\r\n        tokenizeString,\r\n        tokenizeNumber,\r\n        tokenizeOperator,\r\n        tokenizeSymbol,\r\n        tokenizeStatic,\r\n    ];\r\n    /**\r\n     * Convert a javascript expression (as a string) into a list of tokens. For\r\n     * example: `tokenize(\"1 + b\")` will return:\r\n     * ```js\r\n     *  [\r\n     *   {type: \"VALUE\", value: \"1\"},\r\n     *   {type: \"OPERATOR\", value: \"+\"},\r\n     *   {type: \"SYMBOL\", value: \"b\"}\r\n     * ]\r\n     * ```\r\n     */\r\n    function tokenize(expr) {\r\n        const result = [];\r\n        let token = true;\r\n        let error;\r\n        let current = expr;\r\n        try {\r\n            while (token) {\r\n                current = current.trim();\r\n                if (current) {\r\n                    for (let tokenizer of TOKENIZERS) {\r\n                        token = tokenizer(current);\r\n                        if (token) {\r\n                            result.push(token);\r\n                            current = current.slice(token.size || token.value.length);\r\n                            break;\r\n                        }\r\n                    }\r\n                }\r\n                else {\r\n                    token = false;\r\n                }\r\n            }\r\n        }\r\n        catch (e) {\r\n            error = e; // Silence all errors and throw a generic error below\r\n        }\r\n        if (current.length || error) {\r\n            throw new OwlError(`Tokenizer error: could not tokenize \\`${expr}\\``);\r\n        }\r\n        return result;\r\n    }\r\n    //------------------------------------------------------------------------------\r\n    // Expression \"evaluator\"\r\n    //------------------------------------------------------------------------------\r\n    const isLeftSeparator = (token) => token && (token.type === \"LEFT_BRACE\" || token.type === \"COMMA\");\r\n    const isRightSeparator = (token) => token && (token.type === \"RIGHT_BRACE\" || token.type === \"COMMA\");\r\n    /**\r\n     * This is the main function exported by this file. This is the code that will\r\n     * process an expression (given as a string) and returns another expression with\r\n     * proper lookups in the context.\r\n     *\r\n     * Usually, this kind of code would be very simple to do if we had an AST (so,\r\n     * if we had a javascript parser), since then, we would only need to find the\r\n     * variables and replace them.  However, a parser is more complicated, and there\r\n     * are no standard builtin parser API.\r\n     *\r\n     * Since this method is applied to simple javasript expressions, and the work to\r\n     * be done is actually quite simple, we actually can get away with not using a\r\n     * parser, which helps with the code size.\r\n     *\r\n     * Here is the heuristic used by this method to determine if a token is a\r\n     * variable:\r\n     * - by default, all symbols are considered a variable\r\n     * - unless the previous token is a dot (in that case, this is a property: `a.b`)\r\n     * - or if the previous token is a left brace or a comma, and the next token is\r\n     *   a colon (in that case, this is an object key: `{a: b}`)\r\n     *\r\n     * Some specific code is also required to support arrow functions. If we detect\r\n     * the arrow operator, then we add the current (or some previous tokens) token to\r\n     * the list of variables so it does not get replaced by a lookup in the context\r\n     */\r\n    function compileExprToArray(expr) {\r\n        const localVars = new Set();\r\n        const tokens = tokenize(expr);\r\n        let i = 0;\r\n        let stack = []; // to track last opening (, [ or {\r\n        while (i < tokens.length) {\r\n            let token = tokens[i];\r\n            let prevToken = tokens[i - 1];\r\n            let nextToken = tokens[i + 1];\r\n            let groupType = stack[stack.length - 1];\r\n            switch (token.type) {\r\n                case \"LEFT_BRACE\":\r\n                case \"LEFT_BRACKET\":\r\n                case \"LEFT_PAREN\":\r\n                    stack.push(token.type);\r\n                    break;\r\n                case \"RIGHT_BRACE\":\r\n                case \"RIGHT_BRACKET\":\r\n                case \"RIGHT_PAREN\":\r\n                    stack.pop();\r\n            }\r\n            let isVar = token.type === \"SYMBOL\" && !RESERVED_WORDS.includes(token.value);\r\n            if (token.type === \"SYMBOL\" && !RESERVED_WORDS.includes(token.value)) {\r\n                if (prevToken) {\r\n                    // normalize missing tokens: {a} should be equivalent to {a:a}\r\n                    if (groupType === \"LEFT_BRACE\" &&\r\n                        isLeftSeparator(prevToken) &&\r\n                        isRightSeparator(nextToken)) {\r\n                        tokens.splice(i + 1, 0, { type: \"COLON\", value: \":\" }, { ...token });\r\n                        nextToken = tokens[i + 1];\r\n                    }\r\n                    if (prevToken.type === \"OPERATOR\" && prevToken.value === \".\") {\r\n                        isVar = false;\r\n                    }\r\n                    else if (prevToken.type === \"LEFT_BRACE\" || prevToken.type === \"COMMA\") {\r\n                        if (nextToken && nextToken.type === \"COLON\") {\r\n                            isVar = false;\r\n                        }\r\n                    }\r\n                }\r\n            }\r\n            if (token.type === \"TEMPLATE_STRING\") {\r\n                token.value = token.replace((expr) => compileExpr(expr));\r\n            }\r\n            if (nextToken && nextToken.type === \"OPERATOR\" && nextToken.value === \"=>\") {\r\n                if (token.type === \"RIGHT_PAREN\") {\r\n                    let j = i - 1;\r\n                    while (j > 0 && tokens[j].type !== \"LEFT_PAREN\") {\r\n                        if (tokens[j].type === \"SYMBOL\" && tokens[j].originalValue) {\r\n                            tokens[j].value = tokens[j].originalValue;\r\n                            localVars.add(tokens[j].value); //] = { id: tokens[j].value, expr: tokens[j].value };\r\n                        }\r\n                        j--;\r\n                    }\r\n                }\r\n                else {\r\n                    localVars.add(token.value); //] = { id: token.value, expr: token.value };\r\n                }\r\n            }\r\n            if (isVar) {\r\n                token.varName = token.value;\r\n                if (!localVars.has(token.value)) {\r\n                    token.originalValue = token.value;\r\n                    token.value = `ctx['${token.value}']`;\r\n                }\r\n            }\r\n            i++;\r\n        }\r\n        // Mark all variables that have been used locally.\r\n        // This assumes the expression has only one scope (incorrect but \"good enough for now\")\r\n        for (const token of tokens) {\r\n            if (token.type === \"SYMBOL\" && token.varName && localVars.has(token.value)) {\r\n                token.originalValue = token.value;\r\n                token.value = `_${token.value}`;\r\n                token.isLocal = true;\r\n            }\r\n        }\r\n        return tokens;\r\n    }\r\n    // Leading spaces are trimmed during tokenization, so they need to be added back for some values\r\n    const paddedValues = new Map([[\"in \", \" in \"]]);\r\n    function compileExpr(expr) {\r\n        return compileExprToArray(expr)\r\n            .map((t) => paddedValues.get(t.value) || t.value)\r\n            .join(\"\");\r\n    }\r\n    const INTERP_REGEXP = /\\{\\{.*?\\}\\}|\\#\\{.*?\\}/g;\r\n    function replaceDynamicParts(s, replacer) {\r\n        let matches = s.match(INTERP_REGEXP);\r\n        if (matches && matches[0].length === s.length) {\r\n            return `(${replacer(s.slice(2, matches[0][0] === \"{\" ? -2 : -1))})`;\r\n        }\r\n        let r = s.replace(INTERP_REGEXP, (s) => \"${\" + replacer(s.slice(2, s[0] === \"{\" ? -2 : -1)) + \"}\");\r\n        return \"`\" + r + \"`\";\r\n    }\r\n    function interpolate(s) {\r\n        return replaceDynamicParts(s, compileExpr);\r\n    }\r\n\r\n    const whitespaceRE = /\\s+/g;\r\n    // using a non-html document so that <inner/outer>HTML serializes as XML instead\r\n    // of HTML (as we will parse it as xml later)\r\n    const xmlDoc = document.implementation.createDocument(null, null, null);\r\n    const MODS = new Set([\"stop\", \"capture\", \"prevent\", \"self\", \"synthetic\"]);\r\n    let nextDataIds = {};\r\n    function generateId(prefix = \"\") {\r\n        nextDataIds[prefix] = (nextDataIds[prefix] || 0) + 1;\r\n        return prefix + nextDataIds[prefix];\r\n    }\r\n    function isProp(tag, key) {\r\n        switch (tag) {\r\n            case \"input\":\r\n                return (key === \"checked\" ||\r\n                    key === \"indeterminate\" ||\r\n                    key === \"value\" ||\r\n                    key === \"readonly\" ||\r\n                    key === \"readOnly\" ||\r\n                    key === \"disabled\");\r\n            case \"option\":\r\n                return key === \"selected\" || key === \"disabled\";\r\n            case \"textarea\":\r\n                return key === \"value\" || key === \"readonly\" || key === \"readOnly\" || key === \"disabled\";\r\n            case \"select\":\r\n                return key === \"value\" || key === \"disabled\";\r\n            case \"button\":\r\n            case \"optgroup\":\r\n                return key === \"disabled\";\r\n        }\r\n        return false;\r\n    }\r\n    /**\r\n     * Returns a template literal that evaluates to str. You can add interpolation\r\n     * sigils into the string if required\r\n     */\r\n    function toStringExpression(str) {\r\n        return `\\`${str.replace(/\\\\/g, \"\\\\\\\\\").replace(/`/g, \"\\\\`\").replace(/\\$\\{/, \"\\\\${\")}\\``;\r\n    }\r\n    // -----------------------------------------------------------------------------\r\n    // BlockDescription\r\n    // -----------------------------------------------------------------------------\r\n    class BlockDescription {\r\n        constructor(target, type) {\r\n            this.dynamicTagName = null;\r\n            this.isRoot = false;\r\n            this.hasDynamicChildren = false;\r\n            this.children = [];\r\n            this.data = [];\r\n            this.childNumber = 0;\r\n            this.parentVar = \"\";\r\n            this.id = BlockDescription.nextBlockId++;\r\n            this.varName = \"b\" + this.id;\r\n            this.blockName = \"block\" + this.id;\r\n            this.target = target;\r\n            this.type = type;\r\n        }\r\n        insertData(str, prefix = \"d\") {\r\n            const id = generateId(prefix);\r\n            this.target.addLine(`let ${id} = ${str};`);\r\n            return this.data.push(id) - 1;\r\n        }\r\n        insert(dom) {\r\n            if (this.currentDom) {\r\n                this.currentDom.appendChild(dom);\r\n            }\r\n            else {\r\n                this.dom = dom;\r\n            }\r\n        }\r\n        generateExpr(expr) {\r\n            if (this.type === \"block\") {\r\n                const hasChildren = this.children.length;\r\n                let params = this.data.length ? `[${this.data.join(\", \")}]` : hasChildren ? \"[]\" : \"\";\r\n                if (hasChildren) {\r\n                    params += \", [\" + this.children.map((c) => c.varName).join(\", \") + \"]\";\r\n                }\r\n                if (this.dynamicTagName) {\r\n                    return `toggler(${this.dynamicTagName}, ${this.blockName}(${this.dynamicTagName})(${params}))`;\r\n                }\r\n                return `${this.blockName}(${params})`;\r\n            }\r\n            else if (this.type === \"list\") {\r\n                return `list(c_block${this.id})`;\r\n            }\r\n            return expr;\r\n        }\r\n        asXmlString() {\r\n            // Can't use outerHTML on text/comment nodes\r\n            // append dom to any element and use innerHTML instead\r\n            const t = xmlDoc.createElement(\"t\");\r\n            t.appendChild(this.dom);\r\n            return t.innerHTML;\r\n        }\r\n    }\r\n    BlockDescription.nextBlockId = 1;\r\n    function createContext(parentCtx, params) {\r\n        return Object.assign({\r\n            block: null,\r\n            index: 0,\r\n            forceNewBlock: true,\r\n            translate: parentCtx.translate,\r\n            translationCtx: parentCtx.translationCtx,\r\n            tKeyExpr: null,\r\n            nameSpace: parentCtx.nameSpace,\r\n            tModelSelectedExpr: parentCtx.tModelSelectedExpr,\r\n        }, params);\r\n    }\r\n    class CodeTarget {\r\n        constructor(name, on) {\r\n            this.indentLevel = 0;\r\n            this.loopLevel = 0;\r\n            this.code = [];\r\n            this.hasRoot = false;\r\n            this.hasCache = false;\r\n            this.shouldProtectScope = false;\r\n            this.hasRefWrapper = false;\r\n            this.name = name;\r\n            this.on = on || null;\r\n        }\r\n        addLine(line, idx) {\r\n            const prefix = new Array(this.indentLevel + 2).join(\"  \");\r\n            if (idx === undefined) {\r\n                this.code.push(prefix + line);\r\n            }\r\n            else {\r\n                this.code.splice(idx, 0, prefix + line);\r\n            }\r\n        }\r\n        generateCode() {\r\n            let result = [];\r\n            result.push(`function ${this.name}(ctx, node, key = \"\") {`);\r\n            if (this.shouldProtectScope) {\r\n                result.push(`  ctx = Object.create(ctx);`);\r\n                result.push(`  ctx[isBoundary] = 1`);\r\n            }\r\n            if (this.hasRefWrapper) {\r\n                result.push(`  let refWrapper = makeRefWrapper(this.__owl__);`);\r\n            }\r\n            if (this.hasCache) {\r\n                result.push(`  let cache = ctx.cache || {};`);\r\n                result.push(`  let nextCache = ctx.cache = {};`);\r\n            }\r\n            for (let line of this.code) {\r\n                result.push(line);\r\n            }\r\n            if (!this.hasRoot) {\r\n                result.push(`return text('');`);\r\n            }\r\n            result.push(`}`);\r\n            return result.join(\"\\n  \");\r\n        }\r\n        currentKey(ctx) {\r\n            let key = this.loopLevel ? `key${this.loopLevel}` : \"key\";\r\n            if (ctx.tKeyExpr) {\r\n                key = `${ctx.tKeyExpr} + ${key}`;\r\n            }\r\n            return key;\r\n        }\r\n    }\r\n    const TRANSLATABLE_ATTRS = [\r\n        \"alt\",\r\n        \"aria-label\",\r\n        \"aria-placeholder\",\r\n        \"aria-roledescription\",\r\n        \"aria-valuetext\",\r\n        \"label\",\r\n        \"placeholder\",\r\n        \"title\",\r\n    ];\r\n    const translationRE = /^(\\s*)([\\s\\S]+?)(\\s*)$/;\r\n    class CodeGenerator {\r\n        constructor(ast, options) {\r\n            this.blocks = [];\r\n            this.nextBlockId = 1;\r\n            this.isDebug = false;\r\n            this.targets = [];\r\n            this.target = new CodeTarget(\"template\");\r\n            this.translatableAttributes = TRANSLATABLE_ATTRS;\r\n            this.staticDefs = [];\r\n            this.slotNames = new Set();\r\n            this.helpers = new Set();\r\n            this.translateFn = options.translateFn || ((s) => s);\r\n            if (options.translatableAttributes) {\r\n                const attrs = new Set(TRANSLATABLE_ATTRS);\r\n                for (let attr of options.translatableAttributes) {\r\n                    if (attr.startsWith(\"-\")) {\r\n                        attrs.delete(attr.slice(1));\r\n                    }\r\n                    else {\r\n                        attrs.add(attr);\r\n                    }\r\n                }\r\n                this.translatableAttributes = [...attrs];\r\n            }\r\n            this.hasSafeContext = options.hasSafeContext || false;\r\n            this.dev = options.dev || false;\r\n            this.ast = ast;\r\n            this.templateName = options.name;\r\n            if (options.hasGlobalValues) {\r\n                this.helpers.add(\"__globals__\");\r\n            }\r\n        }\r\n        generateCode() {\r\n            const ast = this.ast;\r\n            this.isDebug = ast.type === 12 /* TDebug */;\r\n            BlockDescription.nextBlockId = 1;\r\n            nextDataIds = {};\r\n            this.compileAST(ast, {\r\n                block: null,\r\n                index: 0,\r\n                forceNewBlock: false,\r\n                isLast: true,\r\n                translate: true,\r\n                translationCtx: \"\",\r\n                tKeyExpr: null,\r\n            });\r\n            // define blocks and utility functions\r\n            let mainCode = [`  let { text, createBlock, list, multi, html, toggler, comment } = bdom;`];\r\n            if (this.helpers.size) {\r\n                mainCode.push(`let { ${[...this.helpers].join(\", \")} } = helpers;`);\r\n            }\r\n            if (this.templateName) {\r\n                mainCode.push(`// Template name: \"${this.templateName}\"`);\r\n            }\r\n            for (let { id, expr } of this.staticDefs) {\r\n                mainCode.push(`const ${id} = ${expr};`);\r\n            }\r\n            // define all blocks\r\n            if (this.blocks.length) {\r\n                mainCode.push(``);\r\n                for (let block of this.blocks) {\r\n                    if (block.dom) {\r\n                        let xmlString = toStringExpression(block.asXmlString());\r\n                        if (block.dynamicTagName) {\r\n                            xmlString = xmlString.replace(/^`<\\w+/, `\\`<\\${tag || '${block.dom.nodeName}'}`);\r\n                            xmlString = xmlString.replace(/\\w+>`$/, `\\${tag || '${block.dom.nodeName}'}>\\``);\r\n                            mainCode.push(`let ${block.blockName} = tag => createBlock(${xmlString});`);\r\n                        }\r\n                        else {\r\n                            mainCode.push(`let ${block.blockName} = createBlock(${xmlString});`);\r\n                        }\r\n                    }\r\n                }\r\n            }\r\n            // define all slots/defaultcontent function\r\n            if (this.targets.length) {\r\n                for (let fn of this.targets) {\r\n                    mainCode.push(\"\");\r\n                    mainCode = mainCode.concat(fn.generateCode());\r\n                }\r\n            }\r\n            // generate main code\r\n            mainCode.push(\"\");\r\n            mainCode = mainCode.concat(\"return \" + this.target.generateCode());\r\n            const code = mainCode.join(\"\\n  \");\r\n            if (this.isDebug) {\r\n                const msg = `[Owl Debug]\\n${code}`;\r\n                console.log(msg);\r\n            }\r\n            return code;\r\n        }\r\n        compileInNewTarget(prefix, ast, ctx, on) {\r\n            const name = generateId(prefix);\r\n            const initialTarget = this.target;\r\n            const target = new CodeTarget(name, on);\r\n            this.targets.push(target);\r\n            this.target = target;\r\n            this.compileAST(ast, createContext(ctx));\r\n            this.target = initialTarget;\r\n            return name;\r\n        }\r\n        addLine(line, idx) {\r\n            this.target.addLine(line, idx);\r\n        }\r\n        define(varName, expr) {\r\n            this.addLine(`const ${varName} = ${expr};`);\r\n        }\r\n        insertAnchor(block, index = block.children.length) {\r\n            const tag = `block-child-${index}`;\r\n            const anchor = xmlDoc.createElement(tag);\r\n            block.insert(anchor);\r\n        }\r\n        createBlock(parentBlock, type, ctx) {\r\n            const hasRoot = this.target.hasRoot;\r\n            const block = new BlockDescription(this.target, type);\r\n            if (!hasRoot) {\r\n                this.target.hasRoot = true;\r\n                block.isRoot = true;\r\n            }\r\n            if (parentBlock) {\r\n                parentBlock.children.push(block);\r\n                if (parentBlock.type === \"list\") {\r\n                    block.parentVar = `c_block${parentBlock.id}`;\r\n                }\r\n            }\r\n            return block;\r\n        }\r\n        insertBlock(expression, block, ctx) {\r\n            let blockExpr = block.generateExpr(expression);\r\n            if (block.parentVar) {\r\n                let key = this.target.currentKey(ctx);\r\n                this.helpers.add(\"withKey\");\r\n                this.addLine(`${block.parentVar}[${ctx.index}] = withKey(${blockExpr}, ${key});`);\r\n                return;\r\n            }\r\n            if (ctx.tKeyExpr) {\r\n                blockExpr = `toggler(${ctx.tKeyExpr}, ${blockExpr})`;\r\n            }\r\n            if (block.isRoot) {\r\n                if (this.target.on) {\r\n                    blockExpr = this.wrapWithEventCatcher(blockExpr, this.target.on);\r\n                }\r\n                this.addLine(`return ${blockExpr};`);\r\n            }\r\n            else {\r\n                this.define(block.varName, blockExpr);\r\n            }\r\n        }\r\n        /**\r\n         * Captures variables that are used inside of an expression. This is useful\r\n         * because in compiled code, almost all variables are accessed through the ctx\r\n         * object. In the case of functions, that lookup in the context can be delayed\r\n         * which can cause issues if the value has changed since the function was\r\n         * defined.\r\n         *\r\n         * @param expr the expression to capture\r\n         * @param forceCapture whether the expression should capture its scope even if\r\n         *  it doesn't contain a function. Useful when the expression will be used as\r\n         *  a function body.\r\n         * @returns a new expression that uses the captured values\r\n         */\r\n        captureExpression(expr, forceCapture = false) {\r\n            if (!forceCapture && !expr.includes(\"=>\")) {\r\n                return compileExpr(expr);\r\n            }\r\n            const tokens = compileExprToArray(expr);\r\n            const mapping = new Map();\r\n            return tokens\r\n                .map((tok) => {\r\n                if (tok.varName && !tok.isLocal) {\r\n                    if (!mapping.has(tok.varName)) {\r\n                        const varId = generateId(\"v\");\r\n                        mapping.set(tok.varName, varId);\r\n                        this.define(varId, tok.value);\r\n                    }\r\n                    tok.value = mapping.get(tok.varName);\r\n                }\r\n                return tok.value;\r\n            })\r\n                .join(\"\");\r\n        }\r\n        translate(str, translationCtx) {\r\n            const match = translationRE.exec(str);\r\n            return match[1] + this.translateFn(match[2], translationCtx) + match[3];\r\n        }\r\n        /**\r\n         * @returns the newly created block name, if any\r\n         */\r\n        compileAST(ast, ctx) {\r\n            switch (ast.type) {\r\n                case 1 /* Comment */:\r\n                    return this.compileComment(ast, ctx);\r\n                case 0 /* Text */:\r\n                    return this.compileText(ast, ctx);\r\n                case 2 /* DomNode */:\r\n                    return this.compileTDomNode(ast, ctx);\r\n                case 4 /* TEsc */:\r\n                    return this.compileTEsc(ast, ctx);\r\n                case 8 /* TOut */:\r\n                    return this.compileTOut(ast, ctx);\r\n                case 5 /* TIf */:\r\n                    return this.compileTIf(ast, ctx);\r\n                case 9 /* TForEach */:\r\n                    return this.compileTForeach(ast, ctx);\r\n                case 10 /* TKey */:\r\n                    return this.compileTKey(ast, ctx);\r\n                case 3 /* Multi */:\r\n                    return this.compileMulti(ast, ctx);\r\n                case 7 /* TCall */:\r\n                    return this.compileTCall(ast, ctx);\r\n                case 15 /* TCallBlock */:\r\n                    return this.compileTCallBlock(ast, ctx);\r\n                case 6 /* TSet */:\r\n                    return this.compileTSet(ast, ctx);\r\n                case 11 /* TComponent */:\r\n                    return this.compileComponent(ast, ctx);\r\n                case 12 /* TDebug */:\r\n                    return this.compileDebug(ast, ctx);\r\n                case 13 /* TLog */:\r\n                    return this.compileLog(ast, ctx);\r\n                case 14 /* TSlot */:\r\n                    return this.compileTSlot(ast, ctx);\r\n                case 16 /* TTranslation */:\r\n                    return this.compileTTranslation(ast, ctx);\r\n                case 17 /* TTranslationContext */:\r\n                    return this.compileTTranslationContext(ast, ctx);\r\n                case 18 /* TPortal */:\r\n                    return this.compileTPortal(ast, ctx);\r\n            }\r\n        }\r\n        compileDebug(ast, ctx) {\r\n            this.addLine(`debugger;`);\r\n            if (ast.content) {\r\n                return this.compileAST(ast.content, ctx);\r\n            }\r\n            return null;\r\n        }\r\n        compileLog(ast, ctx) {\r\n            this.addLine(`console.log(${compileExpr(ast.expr)});`);\r\n            if (ast.content) {\r\n                return this.compileAST(ast.content, ctx);\r\n            }\r\n            return null;\r\n        }\r\n        compileComment(ast, ctx) {\r\n            let { block, forceNewBlock } = ctx;\r\n            const isNewBlock = !block || forceNewBlock;\r\n            if (isNewBlock) {\r\n                block = this.createBlock(block, \"comment\", ctx);\r\n                this.insertBlock(`comment(${toStringExpression(ast.value)})`, block, {\r\n                    ...ctx,\r\n                    forceNewBlock: forceNewBlock && !block,\r\n                });\r\n            }\r\n            else {\r\n                const text = xmlDoc.createComment(ast.value);\r\n                block.insert(text);\r\n            }\r\n            return block.varName;\r\n        }\r\n        compileText(ast, ctx) {\r\n            let { block, forceNewBlock } = ctx;\r\n            let value = ast.value;\r\n            if (value && ctx.translate !== false) {\r\n                value = this.translate(value, ctx.translationCtx);\r\n            }\r\n            if (!ctx.inPreTag) {\r\n                value = value.replace(whitespaceRE, \" \");\r\n            }\r\n            if (!block || forceNewBlock) {\r\n                block = this.createBlock(block, \"text\", ctx);\r\n                this.insertBlock(`text(${toStringExpression(value)})`, block, {\r\n                    ...ctx,\r\n                    forceNewBlock: forceNewBlock && !block,\r\n                });\r\n            }\r\n            else {\r\n                const createFn = ast.type === 0 /* Text */ ? xmlDoc.createTextNode : xmlDoc.createComment;\r\n                block.insert(createFn.call(xmlDoc, value));\r\n            }\r\n            return block.varName;\r\n        }\r\n        generateHandlerCode(rawEvent, handler) {\r\n            const modifiers = rawEvent\r\n                .split(\".\")\r\n                .slice(1)\r\n                .map((m) => {\r\n                if (!MODS.has(m)) {\r\n                    throw new OwlError(`Unknown event modifier: '${m}'`);\r\n                }\r\n                return `\"${m}\"`;\r\n            });\r\n            let modifiersCode = \"\";\r\n            if (modifiers.length) {\r\n                modifiersCode = `${modifiers.join(\",\")}, `;\r\n            }\r\n            return `[${modifiersCode}${this.captureExpression(handler)}, ctx]`;\r\n        }\r\n        compileTDomNode(ast, ctx) {\r\n            var _a;\r\n            let { block, forceNewBlock } = ctx;\r\n            const isNewBlock = !block || forceNewBlock || ast.dynamicTag !== null || ast.ns;\r\n            let codeIdx = this.target.code.length;\r\n            if (isNewBlock) {\r\n                if ((ast.dynamicTag || ctx.tKeyExpr || ast.ns) && ctx.block) {\r\n                    this.insertAnchor(ctx.block);\r\n                }\r\n                block = this.createBlock(block, \"block\", ctx);\r\n                this.blocks.push(block);\r\n                if (ast.dynamicTag) {\r\n                    const tagExpr = generateId(\"tag\");\r\n                    this.define(tagExpr, compileExpr(ast.dynamicTag));\r\n                    block.dynamicTagName = tagExpr;\r\n                }\r\n            }\r\n            // attributes\r\n            const attrs = {};\r\n            for (let key in ast.attrs) {\r\n                let expr, attrName;\r\n                if (key.startsWith(\"t-attf\")) {\r\n                    expr = interpolate(ast.attrs[key]);\r\n                    const idx = block.insertData(expr, \"attr\");\r\n                    attrName = key.slice(7);\r\n                    attrs[\"block-attribute-\" + idx] = attrName;\r\n                }\r\n                else if (key.startsWith(\"t-att\")) {\r\n                    attrName = key === \"t-att\" ? null : key.slice(6);\r\n                    expr = compileExpr(ast.attrs[key]);\r\n                    if (attrName && isProp(ast.tag, attrName)) {\r\n                        if (attrName === \"readonly\") {\r\n                            // the property has a different name than the attribute\r\n                            attrName = \"readOnly\";\r\n                        }\r\n                        // we force a new string or new boolean to bypass the equality check in blockdom when patching same value\r\n                        if (attrName === \"value\") {\r\n                            // When the expression is falsy (except 0), fall back to an empty string\r\n                            expr = `new String((${expr}) === 0 ? 0 : ((${expr}) || \"\"))`;\r\n                        }\r\n                        else {\r\n                            expr = `new Boolean(${expr})`;\r\n                        }\r\n                        const idx = block.insertData(expr, \"prop\");\r\n                        attrs[`block-property-${idx}`] = attrName;\r\n                    }\r\n                    else {\r\n                        const idx = block.insertData(expr, \"attr\");\r\n                        if (key === \"t-att\") {\r\n                            attrs[`block-attributes`] = String(idx);\r\n                        }\r\n                        else {\r\n                            attrs[`block-attribute-${idx}`] = attrName;\r\n                        }\r\n                    }\r\n                }\r\n                else if (this.translatableAttributes.includes(key)) {\r\n                    const attrTranslationCtx = ((_a = ast.attrsTranslationCtx) === null || _a === void 0 ? void 0 : _a[key]) || ctx.translationCtx;\r\n                    attrs[key] = this.translateFn(ast.attrs[key], attrTranslationCtx);\r\n                }\r\n                else {\r\n                    expr = `\"${ast.attrs[key]}\"`;\r\n                    attrName = key;\r\n                    attrs[key] = ast.attrs[key];\r\n                }\r\n                if (attrName === \"value\" && ctx.tModelSelectedExpr) {\r\n                    let selectedId = block.insertData(`${ctx.tModelSelectedExpr} === ${expr}`, \"attr\");\r\n                    attrs[`block-attribute-${selectedId}`] = \"selected\";\r\n                }\r\n            }\r\n            // t-model\r\n            let tModelSelectedExpr;\r\n            if (ast.model) {\r\n                const { hasDynamicChildren, baseExpr, expr, eventType, shouldNumberize, shouldTrim, targetAttr, specialInitTargetAttr, } = ast.model;\r\n                const baseExpression = compileExpr(baseExpr);\r\n                const bExprId = generateId(\"bExpr\");\r\n                this.define(bExprId, baseExpression);\r\n                const expression = compileExpr(expr);\r\n                const exprId = generateId(\"expr\");\r\n                this.define(exprId, expression);\r\n                const fullExpression = `${bExprId}[${exprId}]`;\r\n                let idx;\r\n                if (specialInitTargetAttr) {\r\n                    let targetExpr = targetAttr in attrs && `'${attrs[targetAttr]}'`;\r\n                    if (!targetExpr && ast.attrs) {\r\n                        // look at the dynamic attribute counterpart\r\n                        const dynamicTgExpr = ast.attrs[`t-att-${targetAttr}`];\r\n                        if (dynamicTgExpr) {\r\n                            targetExpr = compileExpr(dynamicTgExpr);\r\n                        }\r\n                    }\r\n                    idx = block.insertData(`${fullExpression} === ${targetExpr}`, \"prop\");\r\n                    attrs[`block-property-${idx}`] = specialInitTargetAttr;\r\n                }\r\n                else if (hasDynamicChildren) {\r\n                    const bValueId = generateId(\"bValue\");\r\n                    tModelSelectedExpr = `${bValueId}`;\r\n                    this.define(tModelSelectedExpr, fullExpression);\r\n                }\r\n                else {\r\n                    idx = block.insertData(`${fullExpression}`, \"prop\");\r\n                    attrs[`block-property-${idx}`] = targetAttr;\r\n                }\r\n                this.helpers.add(\"toNumber\");\r\n                let valueCode = `ev.target.${targetAttr}`;\r\n                valueCode = shouldTrim ? `${valueCode}.trim()` : valueCode;\r\n                valueCode = shouldNumberize ? `toNumber(${valueCode})` : valueCode;\r\n                const handler = `[(ev) => { ${fullExpression} = ${valueCode}; }]`;\r\n                idx = block.insertData(handler, \"hdlr\");\r\n                attrs[`block-handler-${idx}`] = eventType;\r\n            }\r\n            // event handlers\r\n            for (let ev in ast.on) {\r\n                const name = this.generateHandlerCode(ev, ast.on[ev]);\r\n                const idx = block.insertData(name, \"hdlr\");\r\n                attrs[`block-handler-${idx}`] = ev;\r\n            }\r\n            // t-ref\r\n            if (ast.ref) {\r\n                if (this.dev) {\r\n                    this.helpers.add(\"makeRefWrapper\");\r\n                    this.target.hasRefWrapper = true;\r\n                }\r\n                const isDynamic = INTERP_REGEXP.test(ast.ref);\r\n                let name = `\\`${ast.ref}\\``;\r\n                if (isDynamic) {\r\n                    name = replaceDynamicParts(ast.ref, (expr) => this.captureExpression(expr, true));\r\n                }\r\n                let setRefStr = `(el) => this.__owl__.setRef((${name}), el)`;\r\n                if (this.dev) {\r\n                    setRefStr = `refWrapper(${name}, ${setRefStr})`;\r\n                }\r\n                const idx = block.insertData(setRefStr, \"ref\");\r\n                attrs[\"block-ref\"] = String(idx);\r\n            }\r\n            const nameSpace = ast.ns || ctx.nameSpace;\r\n            const dom = nameSpace\r\n                ? xmlDoc.createElementNS(nameSpace, ast.tag)\r\n                : xmlDoc.createElement(ast.tag);\r\n            for (const [attr, val] of Object.entries(attrs)) {\r\n                if (!(attr === \"class\" && val === \"\")) {\r\n                    dom.setAttribute(attr, val);\r\n                }\r\n            }\r\n            block.insert(dom);\r\n            if (ast.content.length) {\r\n                const initialDom = block.currentDom;\r\n                block.currentDom = dom;\r\n                const children = ast.content;\r\n                for (let i = 0; i < children.length; i++) {\r\n                    const child = ast.content[i];\r\n                    const subCtx = createContext(ctx, {\r\n                        block,\r\n                        index: block.childNumber,\r\n                        forceNewBlock: false,\r\n                        isLast: ctx.isLast && i === children.length - 1,\r\n                        tKeyExpr: ctx.tKeyExpr,\r\n                        nameSpace,\r\n                        tModelSelectedExpr,\r\n                        inPreTag: ctx.inPreTag || ast.tag === \"pre\",\r\n                    });\r\n                    this.compileAST(child, subCtx);\r\n                }\r\n                block.currentDom = initialDom;\r\n            }\r\n            if (isNewBlock) {\r\n                this.insertBlock(`${block.blockName}(ddd)`, block, ctx);\r\n                // may need to rewrite code!\r\n                if (block.children.length && block.hasDynamicChildren) {\r\n                    const code = this.target.code;\r\n                    const children = block.children.slice();\r\n                    let current = children.shift();\r\n                    for (let i = codeIdx; i < code.length; i++) {\r\n                        if (code[i].trimStart().startsWith(`const ${current.varName} `)) {\r\n                            code[i] = code[i].replace(`const ${current.varName}`, current.varName);\r\n                            current = children.shift();\r\n                            if (!current)\r\n                                break;\r\n                        }\r\n                    }\r\n                    this.addLine(`let ${block.children.map((c) => c.varName).join(\", \")};`, codeIdx);\r\n                }\r\n            }\r\n            return block.varName;\r\n        }\r\n        compileTEsc(ast, ctx) {\r\n            let { block, forceNewBlock } = ctx;\r\n            let expr;\r\n            if (ast.expr === \"0\") {\r\n                this.helpers.add(\"zero\");\r\n                expr = `ctx[zero]`;\r\n            }\r\n            else {\r\n                expr = compileExpr(ast.expr);\r\n                if (ast.defaultValue) {\r\n                    this.helpers.add(\"withDefault\");\r\n                    // FIXME: defaultValue is not translated\r\n                    expr = `withDefault(${expr}, ${toStringExpression(ast.defaultValue)})`;\r\n                }\r\n            }\r\n            if (!block || forceNewBlock) {\r\n                block = this.createBlock(block, \"text\", ctx);\r\n                this.insertBlock(`text(${expr})`, block, { ...ctx, forceNewBlock: forceNewBlock && !block });\r\n            }\r\n            else {\r\n                const idx = block.insertData(expr, \"txt\");\r\n                const text = xmlDoc.createElement(`block-text-${idx}`);\r\n                block.insert(text);\r\n            }\r\n            return block.varName;\r\n        }\r\n        compileTOut(ast, ctx) {\r\n            let { block } = ctx;\r\n            if (block) {\r\n                this.insertAnchor(block);\r\n            }\r\n            block = this.createBlock(block, \"html\", ctx);\r\n            let blockStr;\r\n            if (ast.expr === \"0\") {\r\n                this.helpers.add(\"zero\");\r\n                blockStr = `ctx[zero]`;\r\n            }\r\n            else if (ast.body) {\r\n                let bodyValue = null;\r\n                bodyValue = BlockDescription.nextBlockId;\r\n                const subCtx = createContext(ctx);\r\n                this.compileAST({ type: 3 /* Multi */, content: ast.body }, subCtx);\r\n                this.helpers.add(\"safeOutput\");\r\n                blockStr = `safeOutput(${compileExpr(ast.expr)}, b${bodyValue})`;\r\n            }\r\n            else {\r\n                this.helpers.add(\"safeOutput\");\r\n                blockStr = `safeOutput(${compileExpr(ast.expr)})`;\r\n            }\r\n            this.insertBlock(blockStr, block, ctx);\r\n            return block.varName;\r\n        }\r\n        compileTIfBranch(content, block, ctx) {\r\n            this.target.indentLevel++;\r\n            let childN = block.children.length;\r\n            this.compileAST(content, createContext(ctx, { block, index: ctx.index }));\r\n            if (block.children.length > childN) {\r\n                // we have some content => need to insert an anchor at correct index\r\n                this.insertAnchor(block, childN);\r\n            }\r\n            this.target.indentLevel--;\r\n        }\r\n        compileTIf(ast, ctx, nextNode) {\r\n            let { block, forceNewBlock } = ctx;\r\n            const codeIdx = this.target.code.length;\r\n            const isNewBlock = !block || (block.type !== \"multi\" && forceNewBlock);\r\n            if (block) {\r\n                block.hasDynamicChildren = true;\r\n            }\r\n            if (!block || (block.type !== \"multi\" && forceNewBlock)) {\r\n                block = this.createBlock(block, \"multi\", ctx);\r\n            }\r\n            this.addLine(`if (${compileExpr(ast.condition)}) {`);\r\n            this.compileTIfBranch(ast.content, block, ctx);\r\n            if (ast.tElif) {\r\n                for (let clause of ast.tElif) {\r\n                    this.addLine(`} else if (${compileExpr(clause.condition)}) {`);\r\n                    this.compileTIfBranch(clause.content, block, ctx);\r\n                }\r\n            }\r\n            if (ast.tElse) {\r\n                this.addLine(`} else {`);\r\n                this.compileTIfBranch(ast.tElse, block, ctx);\r\n            }\r\n            this.addLine(\"}\");\r\n            if (isNewBlock) {\r\n                // note: this part is duplicated from end of compiledomnode:\r\n                if (block.children.length) {\r\n                    const code = this.target.code;\r\n                    const children = block.children.slice();\r\n                    let current = children.shift();\r\n                    for (let i = codeIdx; i < code.length; i++) {\r\n                        if (code[i].trimStart().startsWith(`const ${current.varName} `)) {\r\n                            code[i] = code[i].replace(`const ${current.varName}`, current.varName);\r\n                            current = children.shift();\r\n                            if (!current)\r\n                                break;\r\n                        }\r\n                    }\r\n                    this.addLine(`let ${block.children.map((c) => c.varName).join(\", \")};`, codeIdx);\r\n                }\r\n                // note: this part is duplicated from end of compilemulti:\r\n                const args = block.children.map((c) => c.varName).join(\", \");\r\n                this.insertBlock(`multi([${args}])`, block, ctx);\r\n            }\r\n            return block.varName;\r\n        }\r\n        compileTForeach(ast, ctx) {\r\n            let { block } = ctx;\r\n            if (block) {\r\n                this.insertAnchor(block);\r\n            }\r\n            block = this.createBlock(block, \"list\", ctx);\r\n            this.target.loopLevel++;\r\n            const loopVar = `i${this.target.loopLevel}`;\r\n            this.addLine(`ctx = Object.create(ctx);`);\r\n            const vals = `v_block${block.id}`;\r\n            const keys = `k_block${block.id}`;\r\n            const l = `l_block${block.id}`;\r\n            const c = `c_block${block.id}`;\r\n            this.helpers.add(\"prepareList\");\r\n            this.define(`[${keys}, ${vals}, ${l}, ${c}]`, `prepareList(${compileExpr(ast.collection)});`);\r\n            // Throw errors on duplicate keys in dev mode\r\n            if (this.dev) {\r\n                this.define(`keys${block.id}`, `new Set()`);\r\n            }\r\n            this.addLine(`for (let ${loopVar} = 0; ${loopVar} < ${l}; ${loopVar}++) {`);\r\n            this.target.indentLevel++;\r\n            this.addLine(`ctx[\\`${ast.elem}\\`] = ${keys}[${loopVar}];`);\r\n            if (!ast.hasNoFirst) {\r\n                this.addLine(`ctx[\\`${ast.elem}_first\\`] = ${loopVar} === 0;`);\r\n            }\r\n            if (!ast.hasNoLast) {\r\n                this.addLine(`ctx[\\`${ast.elem}_last\\`] = ${loopVar} === ${keys}.length - 1;`);\r\n            }\r\n            if (!ast.hasNoIndex) {\r\n                this.addLine(`ctx[\\`${ast.elem}_index\\`] = ${loopVar};`);\r\n            }\r\n            if (!ast.hasNoValue) {\r\n                this.addLine(`ctx[\\`${ast.elem}_value\\`] = ${vals}[${loopVar}];`);\r\n            }\r\n            this.define(`key${this.target.loopLevel}`, ast.key ? compileExpr(ast.key) : loopVar);\r\n            if (this.dev) {\r\n                // Throw error on duplicate keys in dev mode\r\n                this.helpers.add(\"OwlError\");\r\n                this.addLine(`if (keys${block.id}.has(String(key${this.target.loopLevel}))) { throw new OwlError(\\`Got duplicate key in t-foreach: \\${key${this.target.loopLevel}}\\`)}`);\r\n                this.addLine(`keys${block.id}.add(String(key${this.target.loopLevel}));`);\r\n            }\r\n            let id;\r\n            if (ast.memo) {\r\n                this.target.hasCache = true;\r\n                id = generateId();\r\n                this.define(`memo${id}`, compileExpr(ast.memo));\r\n                this.define(`vnode${id}`, `cache[key${this.target.loopLevel}];`);\r\n                this.addLine(`if (vnode${id}) {`);\r\n                this.target.indentLevel++;\r\n                this.addLine(`if (shallowEqual(vnode${id}.memo, memo${id})) {`);\r\n                this.target.indentLevel++;\r\n                this.addLine(`${c}[${loopVar}] = vnode${id};`);\r\n                this.addLine(`nextCache[key${this.target.loopLevel}] = vnode${id};`);\r\n                this.addLine(`continue;`);\r\n                this.target.indentLevel--;\r\n                this.addLine(\"}\");\r\n                this.target.indentLevel--;\r\n                this.addLine(\"}\");\r\n            }\r\n            const subCtx = createContext(ctx, { block, index: loopVar });\r\n            this.compileAST(ast.body, subCtx);\r\n            if (ast.memo) {\r\n                this.addLine(`nextCache[key${this.target.loopLevel}] = Object.assign(${c}[${loopVar}], {memo: memo${id}});`);\r\n            }\r\n            this.target.indentLevel--;\r\n            this.target.loopLevel--;\r\n            this.addLine(`}`);\r\n            if (!ctx.isLast) {\r\n                this.addLine(`ctx = ctx.__proto__;`);\r\n            }\r\n            this.insertBlock(\"l\", block, ctx);\r\n            return block.varName;\r\n        }\r\n        compileTKey(ast, ctx) {\r\n            const tKeyExpr = generateId(\"tKey_\");\r\n            this.define(tKeyExpr, compileExpr(ast.expr));\r\n            ctx = createContext(ctx, {\r\n                tKeyExpr,\r\n                block: ctx.block,\r\n                index: ctx.index,\r\n            });\r\n            return this.compileAST(ast.content, ctx);\r\n        }\r\n        compileMulti(ast, ctx) {\r\n            let { block, forceNewBlock } = ctx;\r\n            const isNewBlock = !block || forceNewBlock;\r\n            let codeIdx = this.target.code.length;\r\n            if (isNewBlock) {\r\n                const n = ast.content.filter((c) => !c.hasNoRepresentation).length;\r\n                let result = null;\r\n                if (n <= 1) {\r\n                    for (let child of ast.content) {\r\n                        const blockName = this.compileAST(child, ctx);\r\n                        result = result || blockName;\r\n                    }\r\n                    return result;\r\n                }\r\n                block = this.createBlock(block, \"multi\", ctx);\r\n            }\r\n            let index = 0;\r\n            for (let i = 0, l = ast.content.length; i < l; i++) {\r\n                const child = ast.content[i];\r\n                const forceNewBlock = !child.hasNoRepresentation;\r\n                const subCtx = createContext(ctx, {\r\n                    block,\r\n                    index,\r\n                    forceNewBlock,\r\n                    isLast: ctx.isLast && i === l - 1,\r\n                });\r\n                this.compileAST(child, subCtx);\r\n                if (forceNewBlock) {\r\n                    index++;\r\n                }\r\n            }\r\n            if (isNewBlock) {\r\n                if (block.hasDynamicChildren && block.children.length) {\r\n                    const code = this.target.code;\r\n                    const children = block.children.slice();\r\n                    let current = children.shift();\r\n                    for (let i = codeIdx; i < code.length; i++) {\r\n                        if (code[i].trimStart().startsWith(`const ${current.varName} `)) {\r\n                            code[i] = code[i].replace(`const ${current.varName}`, current.varName);\r\n                            current = children.shift();\r\n                            if (!current)\r\n                                break;\r\n                        }\r\n                    }\r\n                    this.addLine(`let ${block.children.map((c) => c.varName).join(\", \")};`, codeIdx);\r\n                }\r\n                const args = block.children.map((c) => c.varName).join(\", \");\r\n                this.insertBlock(`multi([${args}])`, block, ctx);\r\n            }\r\n            return block.varName;\r\n        }\r\n        compileTCall(ast, ctx) {\r\n            let { block, forceNewBlock } = ctx;\r\n            let ctxVar = ctx.ctxVar || \"ctx\";\r\n            if (ast.context) {\r\n                ctxVar = generateId(\"ctx\");\r\n                this.addLine(`let ${ctxVar} = ${compileExpr(ast.context)};`);\r\n            }\r\n            const isDynamic = INTERP_REGEXP.test(ast.name);\r\n            const subTemplate = isDynamic ? interpolate(ast.name) : \"`\" + ast.name + \"`\";\r\n            if (block && !forceNewBlock) {\r\n                this.insertAnchor(block);\r\n            }\r\n            block = this.createBlock(block, \"multi\", ctx);\r\n            if (ast.body) {\r\n                this.addLine(`${ctxVar} = Object.create(${ctxVar});`);\r\n                this.addLine(`${ctxVar}[isBoundary] = 1;`);\r\n                this.helpers.add(\"isBoundary\");\r\n                const subCtx = createContext(ctx, { ctxVar });\r\n                const bl = this.compileMulti({ type: 3 /* Multi */, content: ast.body }, subCtx);\r\n                if (bl) {\r\n                    this.helpers.add(\"zero\");\r\n                    this.addLine(`${ctxVar}[zero] = ${bl};`);\r\n                }\r\n            }\r\n            const key = this.generateComponentKey();\r\n            if (isDynamic) {\r\n                const templateVar = generateId(\"template\");\r\n                if (!this.staticDefs.find((d) => d.id === \"call\")) {\r\n                    this.staticDefs.push({ id: \"call\", expr: `app.callTemplate.bind(app)` });\r\n                }\r\n                this.define(templateVar, subTemplate);\r\n                this.insertBlock(`call(this, ${templateVar}, ${ctxVar}, node, ${key})`, block, {\r\n                    ...ctx,\r\n                    forceNewBlock: !block,\r\n                });\r\n            }\r\n            else {\r\n                const id = generateId(`callTemplate_`);\r\n                this.staticDefs.push({ id, expr: `app.getTemplate(${subTemplate})` });\r\n                this.insertBlock(`${id}.call(this, ${ctxVar}, node, ${key})`, block, {\r\n                    ...ctx,\r\n                    forceNewBlock: !block,\r\n                });\r\n            }\r\n            if (ast.body && !ctx.isLast) {\r\n                this.addLine(`${ctxVar} = ${ctxVar}.__proto__;`);\r\n            }\r\n            return block.varName;\r\n        }\r\n        compileTCallBlock(ast, ctx) {\r\n            let { block, forceNewBlock } = ctx;\r\n            if (block) {\r\n                if (!forceNewBlock) {\r\n                    this.insertAnchor(block);\r\n                }\r\n            }\r\n            block = this.createBlock(block, \"multi\", ctx);\r\n            this.insertBlock(compileExpr(ast.name), block, { ...ctx, forceNewBlock: !block });\r\n            return block.varName;\r\n        }\r\n        compileTSet(ast, ctx) {\r\n            this.target.shouldProtectScope = true;\r\n            this.helpers.add(\"isBoundary\").add(\"withDefault\");\r\n            const expr = ast.value ? compileExpr(ast.value || \"\") : \"null\";\r\n            if (ast.body) {\r\n                this.helpers.add(\"LazyValue\");\r\n                const bodyAst = { type: 3 /* Multi */, content: ast.body };\r\n                const name = this.compileInNewTarget(\"value\", bodyAst, ctx);\r\n                let key = this.target.currentKey(ctx);\r\n                let value = `new LazyValue(${name}, ctx, this, node, ${key})`;\r\n                value = ast.value ? (value ? `withDefault(${expr}, ${value})` : expr) : value;\r\n                this.addLine(`ctx[\\`${ast.name}\\`] = ${value};`);\r\n            }\r\n            else {\r\n                let value;\r\n                if (ast.defaultValue) {\r\n                    const defaultValue = toStringExpression(ctx.translate ? this.translate(ast.defaultValue, ctx.translationCtx) : ast.defaultValue);\r\n                    if (ast.value) {\r\n                        value = `withDefault(${expr}, ${defaultValue})`;\r\n                    }\r\n                    else {\r\n                        value = defaultValue;\r\n                    }\r\n                }\r\n                else {\r\n                    value = expr;\r\n                }\r\n                this.helpers.add(\"setContextValue\");\r\n                this.addLine(`setContextValue(${ctx.ctxVar || \"ctx\"}, \"${ast.name}\", ${value});`);\r\n            }\r\n            return null;\r\n        }\r\n        generateComponentKey(currentKey = \"key\") {\r\n            const parts = [generateId(\"__\")];\r\n            for (let i = 0; i < this.target.loopLevel; i++) {\r\n                parts.push(`\\${key${i + 1}}`);\r\n            }\r\n            return `${currentKey} + \\`${parts.join(\"__\")}\\``;\r\n        }\r\n        /**\r\n         * Formats a prop name and value into a string suitable to be inserted in the\r\n         * generated code. For example:\r\n         *\r\n         * Name              Value            Result\r\n         * ---------------------------------------------------------\r\n         * \"number\"          \"state\"          \"number: ctx['state']\"\r\n         * \"something\"       \"\"               \"something: undefined\"\r\n         * \"some-prop\"       \"state\"          \"'some-prop': ctx['state']\"\r\n         * \"onClick.bind\"    \"onClick\"        \"onClick: bind(ctx, ctx['onClick'])\"\r\n         */\r\n        formatProp(name, value, attrsTranslationCtx, translationCtx) {\r\n            if (name.endsWith(\".translate\")) {\r\n                const attrTranslationCtx = (attrsTranslationCtx === null || attrsTranslationCtx === void 0 ? void 0 : attrsTranslationCtx[name]) || translationCtx;\r\n                value = toStringExpression(this.translateFn(value, attrTranslationCtx));\r\n            }\r\n            else {\r\n                value = this.captureExpression(value);\r\n            }\r\n            if (name.includes(\".\")) {\r\n                let [_name, suffix] = name.split(\".\");\r\n                name = _name;\r\n                switch (suffix) {\r\n                    case \"bind\":\r\n                        value = `(${value}).bind(this)`;\r\n                        break;\r\n                    case \"alike\":\r\n                    case \"translate\":\r\n                        break;\r\n                    default:\r\n                        throw new OwlError(`Invalid prop suffix: ${suffix}`);\r\n                }\r\n            }\r\n            name = /^[a-z_]+$/i.test(name) ? name : `'${name}'`;\r\n            return `${name}: ${value || undefined}`;\r\n        }\r\n        formatPropObject(obj, attrsTranslationCtx, translationCtx) {\r\n            return Object.entries(obj).map(([k, v]) => this.formatProp(k, v, attrsTranslationCtx, translationCtx));\r\n        }\r\n        getPropString(props, dynProps) {\r\n            let propString = `{${props.join(\",\")}}`;\r\n            if (dynProps) {\r\n                propString = `Object.assign({}, ${compileExpr(dynProps)}${props.length ? \", \" + propString : \"\"})`;\r\n            }\r\n            return propString;\r\n        }\r\n        compileComponent(ast, ctx) {\r\n            let { block } = ctx;\r\n            // props\r\n            const hasSlotsProp = \"slots\" in (ast.props || {});\r\n            const props = ast.props\r\n                ? this.formatPropObject(ast.props, ast.propsTranslationCtx, ctx.translationCtx)\r\n                : [];\r\n            // slots\r\n            let slotDef = \"\";\r\n            if (ast.slots) {\r\n                let ctxStr = \"ctx\";\r\n                if (this.target.loopLevel || !this.hasSafeContext) {\r\n                    ctxStr = generateId(\"ctx\");\r\n                    this.helpers.add(\"capture\");\r\n                    this.define(ctxStr, `capture(ctx)`);\r\n                }\r\n                let slotStr = [];\r\n                for (let slotName in ast.slots) {\r\n                    const slotAst = ast.slots[slotName];\r\n                    const params = [];\r\n                    if (slotAst.content) {\r\n                        const name = this.compileInNewTarget(\"slot\", slotAst.content, ctx, slotAst.on);\r\n                        params.push(`__render: ${name}.bind(this), __ctx: ${ctxStr}`);\r\n                    }\r\n                    const scope = ast.slots[slotName].scope;\r\n                    if (scope) {\r\n                        params.push(`__scope: \"${scope}\"`);\r\n                    }\r\n                    if (ast.slots[slotName].attrs) {\r\n                        params.push(...this.formatPropObject(ast.slots[slotName].attrs, ast.slots[slotName].attrsTranslationCtx, ctx.translationCtx));\r\n                    }\r\n                    const slotInfo = `{${params.join(\", \")}}`;\r\n                    slotStr.push(`'${slotName}': ${slotInfo}`);\r\n                }\r\n                slotDef = `{${slotStr.join(\", \")}}`;\r\n            }\r\n            if (slotDef && !(ast.dynamicProps || hasSlotsProp)) {\r\n                this.helpers.add(\"markRaw\");\r\n                props.push(`slots: markRaw(${slotDef})`);\r\n            }\r\n            let propString = this.getPropString(props, ast.dynamicProps);\r\n            let propVar;\r\n            if ((slotDef && (ast.dynamicProps || hasSlotsProp)) || this.dev) {\r\n                propVar = generateId(\"props\");\r\n                this.define(propVar, propString);\r\n                propString = propVar;\r\n            }\r\n            if (slotDef && (ast.dynamicProps || hasSlotsProp)) {\r\n                this.helpers.add(\"markRaw\");\r\n                this.addLine(`${propVar}.slots = markRaw(Object.assign(${slotDef}, ${propVar}.slots))`);\r\n            }\r\n            // cmap key\r\n            let expr;\r\n            if (ast.isDynamic) {\r\n                expr = generateId(\"Comp\");\r\n                this.define(expr, compileExpr(ast.name));\r\n            }\r\n            else {\r\n                expr = `\\`${ast.name}\\``;\r\n            }\r\n            if (this.dev) {\r\n                this.addLine(`helpers.validateProps(${expr}, ${propVar}, this);`);\r\n            }\r\n            if (block && (ctx.forceNewBlock === false || ctx.tKeyExpr)) {\r\n                // todo: check the forcenewblock condition\r\n                this.insertAnchor(block);\r\n            }\r\n            let keyArg = this.generateComponentKey();\r\n            if (ctx.tKeyExpr) {\r\n                keyArg = `${ctx.tKeyExpr} + ${keyArg}`;\r\n            }\r\n            let id = generateId(\"comp\");\r\n            const propList = [];\r\n            for (let p in ast.props || {}) {\r\n                let [name, suffix] = p.split(\".\");\r\n                if (!suffix) {\r\n                    propList.push(`\"${name}\"`);\r\n                }\r\n            }\r\n            this.staticDefs.push({\r\n                id,\r\n                expr: `app.createComponent(${ast.isDynamic ? null : expr}, ${!ast.isDynamic}, ${!!ast.slots}, ${!!ast.dynamicProps}, [${propList}])`,\r\n            });\r\n            if (ast.isDynamic) {\r\n                // If the component class changes, this can cause delayed renders to go\r\n                // through if the key doesn't change. Use the component name for now.\r\n                // This means that two component classes with the same name isn't supported\r\n                // in t-component. We can generate a unique id per class later if needed.\r\n                keyArg = `(${expr}).name + ${keyArg}`;\r\n            }\r\n            let blockExpr = `${id}(${propString}, ${keyArg}, node, this, ${ast.isDynamic ? expr : null})`;\r\n            if (ast.isDynamic) {\r\n                blockExpr = `toggler(${expr}, ${blockExpr})`;\r\n            }\r\n            // event handling\r\n            if (ast.on) {\r\n                blockExpr = this.wrapWithEventCatcher(blockExpr, ast.on);\r\n            }\r\n            block = this.createBlock(block, \"multi\", ctx);\r\n            this.insertBlock(blockExpr, block, ctx);\r\n            return block.varName;\r\n        }\r\n        wrapWithEventCatcher(expr, on) {\r\n            this.helpers.add(\"createCatcher\");\r\n            let name = generateId(\"catcher\");\r\n            let spec = {};\r\n            let handlers = [];\r\n            for (let ev in on) {\r\n                let handlerId = generateId(\"hdlr\");\r\n                let idx = handlers.push(handlerId) - 1;\r\n                spec[ev] = idx;\r\n                const handler = this.generateHandlerCode(ev, on[ev]);\r\n                this.define(handlerId, handler);\r\n            }\r\n            this.staticDefs.push({ id: name, expr: `createCatcher(${JSON.stringify(spec)})` });\r\n            return `${name}(${expr}, [${handlers.join(\",\")}])`;\r\n        }\r\n        compileTSlot(ast, ctx) {\r\n            this.helpers.add(\"callSlot\");\r\n            let { block } = ctx;\r\n            let blockString;\r\n            let slotName;\r\n            let dynamic = false;\r\n            let isMultiple = false;\r\n            if (ast.name.match(INTERP_REGEXP)) {\r\n                dynamic = true;\r\n                isMultiple = true;\r\n                slotName = interpolate(ast.name);\r\n            }\r\n            else {\r\n                slotName = \"'\" + ast.name + \"'\";\r\n                isMultiple = isMultiple || this.slotNames.has(ast.name);\r\n                this.slotNames.add(ast.name);\r\n            }\r\n            const attrs = { ...ast.attrs };\r\n            const dynProps = attrs[\"t-props\"];\r\n            delete attrs[\"t-props\"];\r\n            let key = this.target.loopLevel ? `key${this.target.loopLevel}` : \"key\";\r\n            if (isMultiple) {\r\n                key = this.generateComponentKey(key);\r\n            }\r\n            const props = ast.attrs\r\n                ? this.formatPropObject(attrs, ast.attrsTranslationCtx, ctx.translationCtx)\r\n                : [];\r\n            const scope = this.getPropString(props, dynProps);\r\n            if (ast.defaultContent) {\r\n                const name = this.compileInNewTarget(\"defaultContent\", ast.defaultContent, ctx);\r\n                blockString = `callSlot(ctx, node, ${key}, ${slotName}, ${dynamic}, ${scope}, ${name}.bind(this))`;\r\n            }\r\n            else {\r\n                if (dynamic) {\r\n                    let name = generateId(\"slot\");\r\n                    this.define(name, slotName);\r\n                    blockString = `toggler(${name}, callSlot(ctx, node, ${key}, ${name}, ${dynamic}, ${scope}))`;\r\n                }\r\n                else {\r\n                    blockString = `callSlot(ctx, node, ${key}, ${slotName}, ${dynamic}, ${scope})`;\r\n                }\r\n            }\r\n            // event handling\r\n            if (ast.on) {\r\n                blockString = this.wrapWithEventCatcher(blockString, ast.on);\r\n            }\r\n            if (block) {\r\n                this.insertAnchor(block);\r\n            }\r\n            block = this.createBlock(block, \"multi\", ctx);\r\n            this.insertBlock(blockString, block, { ...ctx, forceNewBlock: false });\r\n            return block.varName;\r\n        }\r\n        compileTTranslation(ast, ctx) {\r\n            if (ast.content) {\r\n                return this.compileAST(ast.content, Object.assign({}, ctx, { translate: false }));\r\n            }\r\n            return null;\r\n        }\r\n        compileTTranslationContext(ast, ctx) {\r\n            if (ast.content) {\r\n                return this.compileAST(ast.content, Object.assign({}, ctx, { translationCtx: ast.translationCtx }));\r\n            }\r\n            return null;\r\n        }\r\n        compileTPortal(ast, ctx) {\r\n            if (!this.staticDefs.find((d) => d.id === \"Portal\")) {\r\n                this.staticDefs.push({ id: \"Portal\", expr: `app.Portal` });\r\n            }\r\n            let { block } = ctx;\r\n            const name = this.compileInNewTarget(\"slot\", ast.content, ctx);\r\n            let ctxStr = \"ctx\";\r\n            if (this.target.loopLevel || !this.hasSafeContext) {\r\n                ctxStr = generateId(\"ctx\");\r\n                this.helpers.add(\"capture\");\r\n                this.define(ctxStr, `capture(ctx)`);\r\n            }\r\n            let id = generateId(\"comp\");\r\n            this.staticDefs.push({\r\n                id,\r\n                expr: `app.createComponent(null, false, true, false, false)`,\r\n            });\r\n            const target = compileExpr(ast.target);\r\n            const key = this.generateComponentKey();\r\n            const blockString = `${id}({target: ${target},slots: {'default': {__render: ${name}.bind(this), __ctx: ${ctxStr}}}}, ${key}, node, ctx, Portal)`;\r\n            if (block) {\r\n                this.insertAnchor(block);\r\n            }\r\n            block = this.createBlock(block, \"multi\", ctx);\r\n            this.insertBlock(blockString, block, { ...ctx, forceNewBlock: false });\r\n            return block.varName;\r\n        }\r\n    }\r\n\r\n    // -----------------------------------------------------------------------------\r\n    // Parser\r\n    // -----------------------------------------------------------------------------\r\n    const cache = new WeakMap();\r\n    function parse(xml, customDir) {\r\n        const ctx = {\r\n            inPreTag: false,\r\n            customDirectives: customDir,\r\n        };\r\n        if (typeof xml === \"string\") {\r\n            const elem = parseXML(`<t>${xml}</t>`).firstChild;\r\n            return _parse(elem, ctx);\r\n        }\r\n        let ast = cache.get(xml);\r\n        if (!ast) {\r\n            // we clone here the xml to prevent modifying it in place\r\n            ast = _parse(xml.cloneNode(true), ctx);\r\n            cache.set(xml, ast);\r\n        }\r\n        return ast;\r\n    }\r\n    function _parse(xml, ctx) {\r\n        normalizeXML(xml);\r\n        return parseNode(xml, ctx) || { type: 0 /* Text */, value: \"\" };\r\n    }\r\n    function parseNode(node, ctx) {\r\n        if (!(node instanceof Element)) {\r\n            return parseTextCommentNode(node, ctx);\r\n        }\r\n        return (parseTCustom(node, ctx) ||\r\n            parseTDebugLog(node, ctx) ||\r\n            parseTForEach(node, ctx) ||\r\n            parseTIf(node, ctx) ||\r\n            parseTPortal(node, ctx) ||\r\n            parseTCall(node, ctx) ||\r\n            parseTCallBlock(node) ||\r\n            parseTTranslation(node, ctx) ||\r\n            parseTTranslationContext(node, ctx) ||\r\n            parseTKey(node, ctx) ||\r\n            parseTEscNode(node, ctx) ||\r\n            parseTOutNode(node, ctx) ||\r\n            parseTSlot(node, ctx) ||\r\n            parseComponent(node, ctx) ||\r\n            parseDOMNode(node, ctx) ||\r\n            parseTSetNode(node, ctx) ||\r\n            parseTNode(node, ctx));\r\n    }\r\n    // -----------------------------------------------------------------------------\r\n    // <t /> tag\r\n    // -----------------------------------------------------------------------------\r\n    function parseTNode(node, ctx) {\r\n        if (node.tagName !== \"t\") {\r\n            return null;\r\n        }\r\n        return parseChildNodes(node, ctx);\r\n    }\r\n    // -----------------------------------------------------------------------------\r\n    // Text and Comment Nodes\r\n    // -----------------------------------------------------------------------------\r\n    const lineBreakRE = /[\\r\\n]/;\r\n    function parseTextCommentNode(node, ctx) {\r\n        if (node.nodeType === Node.TEXT_NODE) {\r\n            let value = node.textContent || \"\";\r\n            if (!ctx.inPreTag && lineBreakRE.test(value) && !value.trim()) {\r\n                return null;\r\n            }\r\n            return { type: 0 /* Text */, value };\r\n        }\r\n        else if (node.nodeType === Node.COMMENT_NODE) {\r\n            return { type: 1 /* Comment */, value: node.textContent || \"\" };\r\n        }\r\n        return null;\r\n    }\r\n    function parseTCustom(node, ctx) {\r\n        if (!ctx.customDirectives) {\r\n            return null;\r\n        }\r\n        const nodeAttrsNames = node.getAttributeNames();\r\n        for (let attr of nodeAttrsNames) {\r\n            if (attr === \"t-custom\" || attr === \"t-custom-\") {\r\n                throw new OwlError(\"Missing custom directive name with t-custom directive\");\r\n            }\r\n            if (attr.startsWith(\"t-custom-\")) {\r\n                const directiveName = attr.split(\".\")[0].slice(9);\r\n                const customDirective = ctx.customDirectives[directiveName];\r\n                if (!customDirective) {\r\n                    throw new OwlError(`Custom directive \"${directiveName}\" is not defined`);\r\n                }\r\n                const value = node.getAttribute(attr);\r\n                const modifiers = attr.split(\".\").slice(1);\r\n                node.removeAttribute(attr);\r\n                try {\r\n                    customDirective(node, value, modifiers);\r\n                }\r\n                catch (error) {\r\n                    throw new OwlError(`Custom directive \"${directiveName}\" throw the following error: ${error}`);\r\n                }\r\n                return parseNode(node, ctx);\r\n            }\r\n        }\r\n        return null;\r\n    }\r\n    // -----------------------------------------------------------------------------\r\n    // debugging\r\n    // -----------------------------------------------------------------------------\r\n    function parseTDebugLog(node, ctx) {\r\n        if (node.hasAttribute(\"t-debug\")) {\r\n            node.removeAttribute(\"t-debug\");\r\n            const content = parseNode(node, ctx);\r\n            const ast = {\r\n                type: 12 /* TDebug */,\r\n                content,\r\n            };\r\n            if (content === null || content === void 0 ? void 0 : content.hasNoRepresentation) {\r\n                ast.hasNoRepresentation = true;\r\n            }\r\n            return ast;\r\n        }\r\n        if (node.hasAttribute(\"t-log\")) {\r\n            const expr = node.getAttribute(\"t-log\");\r\n            node.removeAttribute(\"t-log\");\r\n            const content = parseNode(node, ctx);\r\n            const ast = {\r\n                type: 13 /* TLog */,\r\n                expr,\r\n                content,\r\n            };\r\n            if (content === null || content === void 0 ? void 0 : content.hasNoRepresentation) {\r\n                ast.hasNoRepresentation = true;\r\n            }\r\n            return ast;\r\n        }\r\n        return null;\r\n    }\r\n    // -----------------------------------------------------------------------------\r\n    // Regular dom node\r\n    // -----------------------------------------------------------------------------\r\n    const hasDotAtTheEnd = /\\.[\\w_]+\\s*$/;\r\n    const hasBracketsAtTheEnd = /\\[[^\\[]+\\]\\s*$/;\r\n    const ROOT_SVG_TAGS = new Set([\"svg\", \"g\", \"path\"]);\r\n    function parseDOMNode(node, ctx) {\r\n        const { tagName } = node;\r\n        const dynamicTag = node.getAttribute(\"t-tag\");\r\n        node.removeAttribute(\"t-tag\");\r\n        if (tagName === \"t\" && !dynamicTag) {\r\n            return null;\r\n        }\r\n        if (tagName.startsWith(\"block-\")) {\r\n            throw new OwlError(`Invalid tag name: '${tagName}'`);\r\n        }\r\n        ctx = Object.assign({}, ctx);\r\n        if (tagName === \"pre\") {\r\n            ctx.inPreTag = true;\r\n        }\r\n        let ns = !ctx.nameSpace && ROOT_SVG_TAGS.has(tagName) ? \"http://www.w3.org/2000/svg\" : null;\r\n        const ref = node.getAttribute(\"t-ref\");\r\n        node.removeAttribute(\"t-ref\");\r\n        const nodeAttrsNames = node.getAttributeNames();\r\n        let attrs = null;\r\n        let attrsTranslationCtx = null;\r\n        let on = null;\r\n        let model = null;\r\n        for (let attr of nodeAttrsNames) {\r\n            const value = node.getAttribute(attr);\r\n            if (attr === \"t-on\" || attr === \"t-on-\") {\r\n                throw new OwlError(\"Missing event name with t-on directive\");\r\n            }\r\n            if (attr.startsWith(\"t-on-\")) {\r\n                on = on || {};\r\n                on[attr.slice(5)] = value;\r\n            }\r\n            else if (attr.startsWith(\"t-model\")) {\r\n                if (![\"input\", \"select\", \"textarea\"].includes(tagName)) {\r\n                    throw new OwlError(\"The t-model directive only works with <input>, <textarea> and <select>\");\r\n                }\r\n                let baseExpr, expr;\r\n                if (hasDotAtTheEnd.test(value)) {\r\n                    const index = value.lastIndexOf(\".\");\r\n                    baseExpr = value.slice(0, index);\r\n                    expr = `'${value.slice(index + 1)}'`;\r\n                }\r\n                else if (hasBracketsAtTheEnd.test(value)) {\r\n                    const index = value.lastIndexOf(\"[\");\r\n                    baseExpr = value.slice(0, index);\r\n                    expr = value.slice(index + 1, -1);\r\n                }\r\n                else {\r\n                    throw new OwlError(`Invalid t-model expression: \"${value}\" (it should be assignable)`);\r\n                }\r\n                const typeAttr = node.getAttribute(\"type\");\r\n                const isInput = tagName === \"input\";\r\n                const isSelect = tagName === \"select\";\r\n                const isCheckboxInput = isInput && typeAttr === \"checkbox\";\r\n                const isRadioInput = isInput && typeAttr === \"radio\";\r\n                const hasTrimMod = attr.includes(\".trim\");\r\n                const hasLazyMod = hasTrimMod || attr.includes(\".lazy\");\r\n                const hasNumberMod = attr.includes(\".number\");\r\n                const eventType = isRadioInput ? \"click\" : isSelect || hasLazyMod ? \"change\" : \"input\";\r\n                model = {\r\n                    baseExpr,\r\n                    expr,\r\n                    targetAttr: isCheckboxInput ? \"checked\" : \"value\",\r\n                    specialInitTargetAttr: isRadioInput ? \"checked\" : null,\r\n                    eventType,\r\n                    hasDynamicChildren: false,\r\n                    shouldTrim: hasTrimMod,\r\n                    shouldNumberize: hasNumberMod,\r\n                };\r\n                if (isSelect) {\r\n                    // don't pollute the original ctx\r\n                    ctx = Object.assign({}, ctx);\r\n                    ctx.tModelInfo = model;\r\n                }\r\n            }\r\n            else if (attr.startsWith(\"block-\")) {\r\n                throw new OwlError(`Invalid attribute: '${attr}'`);\r\n            }\r\n            else if (attr === \"xmlns\") {\r\n                ns = value;\r\n            }\r\n            else if (attr.startsWith(\"t-translation-context-\")) {\r\n                const attrName = attr.slice(22);\r\n                attrsTranslationCtx = attrsTranslationCtx || {};\r\n                attrsTranslationCtx[attrName] = value;\r\n            }\r\n            else if (attr !== \"t-name\") {\r\n                if (attr.startsWith(\"t-\") && !attr.startsWith(\"t-att\")) {\r\n                    throw new OwlError(`Unknown QWeb directive: '${attr}'`);\r\n                }\r\n                const tModel = ctx.tModelInfo;\r\n                if (tModel && [\"t-att-value\", \"t-attf-value\"].includes(attr)) {\r\n                    tModel.hasDynamicChildren = true;\r\n                }\r\n                attrs = attrs || {};\r\n                attrs[attr] = value;\r\n            }\r\n        }\r\n        if (ns) {\r\n            ctx.nameSpace = ns;\r\n        }\r\n        const children = parseChildren(node, ctx);\r\n        return {\r\n            type: 2 /* DomNode */,\r\n            tag: tagName,\r\n            dynamicTag,\r\n            attrs,\r\n            attrsTranslationCtx,\r\n            on,\r\n            ref,\r\n            content: children,\r\n            model,\r\n            ns,\r\n        };\r\n    }\r\n    // -----------------------------------------------------------------------------\r\n    // t-esc\r\n    // -----------------------------------------------------------------------------\r\n    function parseTEscNode(node, ctx) {\r\n        if (!node.hasAttribute(\"t-esc\")) {\r\n            return null;\r\n        }\r\n        const escValue = node.getAttribute(\"t-esc\");\r\n        node.removeAttribute(\"t-esc\");\r\n        const tesc = {\r\n            type: 4 /* TEsc */,\r\n            expr: escValue,\r\n            defaultValue: node.textContent || \"\",\r\n        };\r\n        let ref = node.getAttribute(\"t-ref\");\r\n        node.removeAttribute(\"t-ref\");\r\n        const ast = parseNode(node, ctx);\r\n        if (!ast) {\r\n            return tesc;\r\n        }\r\n        if (ast.type === 2 /* DomNode */) {\r\n            return {\r\n                ...ast,\r\n                ref,\r\n                content: [tesc],\r\n            };\r\n        }\r\n        return tesc;\r\n    }\r\n    // -----------------------------------------------------------------------------\r\n    // t-out\r\n    // -----------------------------------------------------------------------------\r\n    function parseTOutNode(node, ctx) {\r\n        if (!node.hasAttribute(\"t-out\") && !node.hasAttribute(\"t-raw\")) {\r\n            return null;\r\n        }\r\n        if (node.hasAttribute(\"t-raw\")) {\r\n            console.warn(`t-raw has been deprecated in favor of t-out. If the value to render is not wrapped by the \"markup\" function, it will be escaped`);\r\n        }\r\n        const expr = (node.getAttribute(\"t-out\") || node.getAttribute(\"t-raw\"));\r\n        node.removeAttribute(\"t-out\");\r\n        node.removeAttribute(\"t-raw\");\r\n        const tOut = { type: 8 /* TOut */, expr, body: null };\r\n        const ref = node.getAttribute(\"t-ref\");\r\n        node.removeAttribute(\"t-ref\");\r\n        const ast = parseNode(node, ctx);\r\n        if (!ast) {\r\n            return tOut;\r\n        }\r\n        if (ast.type === 2 /* DomNode */) {\r\n            tOut.body = ast.content.length ? ast.content : null;\r\n            return {\r\n                ...ast,\r\n                ref,\r\n                content: [tOut],\r\n            };\r\n        }\r\n        return tOut;\r\n    }\r\n    // -----------------------------------------------------------------------------\r\n    // t-foreach and t-key\r\n    // -----------------------------------------------------------------------------\r\n    function parseTForEach(node, ctx) {\r\n        if (!node.hasAttribute(\"t-foreach\")) {\r\n            return null;\r\n        }\r\n        const html = node.outerHTML;\r\n        const collection = node.getAttribute(\"t-foreach\");\r\n        node.removeAttribute(\"t-foreach\");\r\n        const elem = node.getAttribute(\"t-as\") || \"\";\r\n        node.removeAttribute(\"t-as\");\r\n        const key = node.getAttribute(\"t-key\");\r\n        if (!key) {\r\n            throw new OwlError(`\"Directive t-foreach should always be used with a t-key!\" (expression: t-foreach=\"${collection}\" t-as=\"${elem}\")`);\r\n        }\r\n        node.removeAttribute(\"t-key\");\r\n        const memo = node.getAttribute(\"t-memo\") || \"\";\r\n        node.removeAttribute(\"t-memo\");\r\n        const body = parseNode(node, ctx);\r\n        if (!body) {\r\n            return null;\r\n        }\r\n        const hasNoTCall = !html.includes(\"t-call\");\r\n        const hasNoFirst = hasNoTCall && !html.includes(`${elem}_first`);\r\n        const hasNoLast = hasNoTCall && !html.includes(`${elem}_last`);\r\n        const hasNoIndex = hasNoTCall && !html.includes(`${elem}_index`);\r\n        const hasNoValue = hasNoTCall && !html.includes(`${elem}_value`);\r\n        return {\r\n            type: 9 /* TForEach */,\r\n            collection,\r\n            elem,\r\n            body,\r\n            memo,\r\n            key,\r\n            hasNoFirst,\r\n            hasNoLast,\r\n            hasNoIndex,\r\n            hasNoValue,\r\n        };\r\n    }\r\n    function parseTKey(node, ctx) {\r\n        if (!node.hasAttribute(\"t-key\")) {\r\n            return null;\r\n        }\r\n        const key = node.getAttribute(\"t-key\");\r\n        node.removeAttribute(\"t-key\");\r\n        const content = parseNode(node, ctx);\r\n        if (!content) {\r\n            return null;\r\n        }\r\n        const ast = {\r\n            type: 10 /* TKey */,\r\n            expr: key,\r\n            content,\r\n        };\r\n        if (content.hasNoRepresentation) {\r\n            ast.hasNoRepresentation = true;\r\n        }\r\n        return ast;\r\n    }\r\n    // -----------------------------------------------------------------------------\r\n    // t-call\r\n    // -----------------------------------------------------------------------------\r\n    function parseTCall(node, ctx) {\r\n        if (!node.hasAttribute(\"t-call\")) {\r\n            return null;\r\n        }\r\n        const subTemplate = node.getAttribute(\"t-call\");\r\n        const context = node.getAttribute(\"t-call-context\");\r\n        node.removeAttribute(\"t-call\");\r\n        node.removeAttribute(\"t-call-context\");\r\n        if (node.tagName !== \"t\") {\r\n            const ast = parseNode(node, ctx);\r\n            const tcall = { type: 7 /* TCall */, name: subTemplate, body: null, context };\r\n            if (ast && ast.type === 2 /* DomNode */) {\r\n                ast.content = [tcall];\r\n                return ast;\r\n            }\r\n            if (ast && ast.type === 11 /* TComponent */) {\r\n                return {\r\n                    ...ast,\r\n                    slots: {\r\n                        default: {\r\n                            content: tcall,\r\n                            scope: null,\r\n                            on: null,\r\n                            attrs: null,\r\n                            attrsTranslationCtx: null,\r\n                        },\r\n                    },\r\n                };\r\n            }\r\n        }\r\n        const body = parseChildren(node, ctx);\r\n        return {\r\n            type: 7 /* TCall */,\r\n            name: subTemplate,\r\n            body: body.length ? body : null,\r\n            context,\r\n        };\r\n    }\r\n    // -----------------------------------------------------------------------------\r\n    // t-call-block\r\n    // -----------------------------------------------------------------------------\r\n    function parseTCallBlock(node, ctx) {\r\n        if (!node.hasAttribute(\"t-call-block\")) {\r\n            return null;\r\n        }\r\n        const name = node.getAttribute(\"t-call-block\");\r\n        return {\r\n            type: 15 /* TCallBlock */,\r\n            name,\r\n        };\r\n    }\r\n    // -----------------------------------------------------------------------------\r\n    // t-if\r\n    // -----------------------------------------------------------------------------\r\n    function parseTIf(node, ctx) {\r\n        if (!node.hasAttribute(\"t-if\")) {\r\n            return null;\r\n        }\r\n        const condition = node.getAttribute(\"t-if\");\r\n        node.removeAttribute(\"t-if\");\r\n        const content = parseNode(node, ctx) || { type: 0 /* Text */, value: \"\" };\r\n        let nextElement = node.nextElementSibling;\r\n        // t-elifs\r\n        const tElifs = [];\r\n        while (nextElement && nextElement.hasAttribute(\"t-elif\")) {\r\n            const condition = nextElement.getAttribute(\"t-elif\");\r\n            nextElement.removeAttribute(\"t-elif\");\r\n            const tElif = parseNode(nextElement, ctx);\r\n            const next = nextElement.nextElementSibling;\r\n            nextElement.remove();\r\n            nextElement = next;\r\n            if (tElif) {\r\n                tElifs.push({ condition, content: tElif });\r\n            }\r\n        }\r\n        // t-else\r\n        let tElse = null;\r\n        if (nextElement && nextElement.hasAttribute(\"t-else\")) {\r\n            nextElement.removeAttribute(\"t-else\");\r\n            tElse = parseNode(nextElement, ctx);\r\n            nextElement.remove();\r\n        }\r\n        return {\r\n            type: 5 /* TIf */,\r\n            condition,\r\n            content,\r\n            tElif: tElifs.length ? tElifs : null,\r\n            tElse,\r\n        };\r\n    }\r\n    // -----------------------------------------------------------------------------\r\n    // t-set directive\r\n    // -----------------------------------------------------------------------------\r\n    function parseTSetNode(node, ctx) {\r\n        if (!node.hasAttribute(\"t-set\")) {\r\n            return null;\r\n        }\r\n        const name = node.getAttribute(\"t-set\");\r\n        const value = node.getAttribute(\"t-value\") || null;\r\n        const defaultValue = node.innerHTML === node.textContent ? node.textContent || null : null;\r\n        let body = null;\r\n        if (node.textContent !== node.innerHTML) {\r\n            body = parseChildren(node, ctx);\r\n        }\r\n        return { type: 6 /* TSet */, name, value, defaultValue, body, hasNoRepresentation: true };\r\n    }\r\n    // -----------------------------------------------------------------------------\r\n    // Components\r\n    // -----------------------------------------------------------------------------\r\n    // Error messages when trying to use an unsupported directive on a component\r\n    const directiveErrorMap = new Map([\r\n        [\r\n            \"t-ref\",\r\n            \"t-ref is no longer supported on components. Consider exposing only the public part of the component's API through a callback prop.\",\r\n        ],\r\n        [\"t-att\", \"t-att makes no sense on component: props are already treated as expressions\"],\r\n        [\r\n            \"t-attf\",\r\n            \"t-attf is not supported on components: use template strings for string interpolation in props\",\r\n        ],\r\n    ]);\r\n    function parseComponent(node, ctx) {\r\n        let name = node.tagName;\r\n        const firstLetter = name[0];\r\n        let isDynamic = node.hasAttribute(\"t-component\");\r\n        if (isDynamic && name !== \"t\") {\r\n            throw new OwlError(`Directive 't-component' can only be used on <t> nodes (used on a <${name}>)`);\r\n        }\r\n        if (!(firstLetter === firstLetter.toUpperCase() || isDynamic)) {\r\n            return null;\r\n        }\r\n        if (isDynamic) {\r\n            name = node.getAttribute(\"t-component\");\r\n            node.removeAttribute(\"t-component\");\r\n        }\r\n        const dynamicProps = node.getAttribute(\"t-props\");\r\n        node.removeAttribute(\"t-props\");\r\n        const defaultSlotScope = node.getAttribute(\"t-slot-scope\");\r\n        node.removeAttribute(\"t-slot-scope\");\r\n        let on = null;\r\n        let props = null;\r\n        let propsTranslationCtx = null;\r\n        for (let name of node.getAttributeNames()) {\r\n            const value = node.getAttribute(name);\r\n            if (name.startsWith(\"t-translation-context-\")) {\r\n                const attrName = name.slice(22);\r\n                propsTranslationCtx = propsTranslationCtx || {};\r\n                propsTranslationCtx[attrName] = value;\r\n            }\r\n            else if (name.startsWith(\"t-\")) {\r\n                if (name.startsWith(\"t-on-\")) {\r\n                    on = on || {};\r\n                    on[name.slice(5)] = value;\r\n                }\r\n                else {\r\n                    const message = directiveErrorMap.get(name.split(\"-\").slice(0, 2).join(\"-\"));\r\n                    throw new OwlError(message || `unsupported directive on Component: ${name}`);\r\n                }\r\n            }\r\n            else {\r\n                props = props || {};\r\n                props[name] = value;\r\n            }\r\n        }\r\n        let slots = null;\r\n        if (node.hasChildNodes()) {\r\n            const clone = node.cloneNode(true);\r\n            // named slots\r\n            const slotNodes = Array.from(clone.querySelectorAll(\"[t-set-slot]\"));\r\n            for (let slotNode of slotNodes) {\r\n                if (slotNode.tagName !== \"t\") {\r\n                    throw new OwlError(`Directive 't-set-slot' can only be used on <t> nodes (used on a <${slotNode.tagName}>)`);\r\n                }\r\n                const name = slotNode.getAttribute(\"t-set-slot\");\r\n                // check if this is defined in a sub component (in which case it should\r\n                // be ignored)\r\n                let el = slotNode.parentElement;\r\n                let isInSubComponent = false;\r\n                while (el && el !== clone) {\r\n                    if (el.hasAttribute(\"t-component\") || el.tagName[0] === el.tagName[0].toUpperCase()) {\r\n                        isInSubComponent = true;\r\n                        break;\r\n                    }\r\n                    el = el.parentElement;\r\n                }\r\n                if (isInSubComponent || !el) {\r\n                    continue;\r\n                }\r\n                slotNode.removeAttribute(\"t-set-slot\");\r\n                slotNode.remove();\r\n                const slotAst = parseNode(slotNode, ctx);\r\n                let on = null;\r\n                let attrs = null;\r\n                let attrsTranslationCtx = null;\r\n                let scope = null;\r\n                for (let attributeName of slotNode.getAttributeNames()) {\r\n                    const value = slotNode.getAttribute(attributeName);\r\n                    if (attributeName === \"t-slot-scope\") {\r\n                        scope = value;\r\n                        continue;\r\n                    }\r\n                    else if (attributeName.startsWith(\"t-translation-context-\")) {\r\n                        const attrName = attributeName.slice(22);\r\n                        attrsTranslationCtx = attrsTranslationCtx || {};\r\n                        attrsTranslationCtx[attrName] = value;\r\n                    }\r\n                    else if (attributeName.startsWith(\"t-on-\")) {\r\n                        on = on || {};\r\n                        on[attributeName.slice(5)] = value;\r\n                    }\r\n                    else {\r\n                        attrs = attrs || {};\r\n                        attrs[attributeName] = value;\r\n                    }\r\n                }\r\n                slots = slots || {};\r\n                slots[name] = { content: slotAst, on, attrs, attrsTranslationCtx, scope };\r\n            }\r\n            // default slot\r\n            const defaultContent = parseChildNodes(clone, ctx);\r\n            slots = slots || {};\r\n            // t-set-slot=\"default\" has priority over content\r\n            if (defaultContent && !slots.default) {\r\n                slots.default = {\r\n                    content: defaultContent,\r\n                    on,\r\n                    attrs: null,\r\n                    attrsTranslationCtx: null,\r\n                    scope: defaultSlotScope,\r\n                };\r\n            }\r\n        }\r\n        return {\r\n            type: 11 /* TComponent */,\r\n            name,\r\n            isDynamic,\r\n            dynamicProps,\r\n            props,\r\n            propsTranslationCtx,\r\n            slots,\r\n            on,\r\n        };\r\n    }\r\n    // -----------------------------------------------------------------------------\r\n    // Slots\r\n    // -----------------------------------------------------------------------------\r\n    function parseTSlot(node, ctx) {\r\n        if (!node.hasAttribute(\"t-slot\")) {\r\n            return null;\r\n        }\r\n        const name = node.getAttribute(\"t-slot\");\r\n        node.removeAttribute(\"t-slot\");\r\n        let attrs = null;\r\n        let attrsTranslationCtx = null;\r\n        let on = null;\r\n        for (let attributeName of node.getAttributeNames()) {\r\n            const value = node.getAttribute(attributeName);\r\n            if (attributeName.startsWith(\"t-on-\")) {\r\n                on = on || {};\r\n                on[attributeName.slice(5)] = value;\r\n            }\r\n            else if (attributeName.startsWith(\"t-translation-context-\")) {\r\n                const attrName = attributeName.slice(22);\r\n                attrsTranslationCtx = attrsTranslationCtx || {};\r\n                attrsTranslationCtx[attrName] = value;\r\n            }\r\n            else {\r\n                attrs = attrs || {};\r\n                attrs[attributeName] = value;\r\n            }\r\n        }\r\n        return {\r\n            type: 14 /* TSlot */,\r\n            name,\r\n            attrs,\r\n            attrsTranslationCtx,\r\n            on,\r\n            defaultContent: parseChildNodes(node, ctx),\r\n        };\r\n    }\r\n    // -----------------------------------------------------------------------------\r\n    // Translation\r\n    // -----------------------------------------------------------------------------\r\n    function wrapInTTranslationAST(r) {\r\n        const ast = { type: 16 /* TTranslation */, content: r };\r\n        if (r === null || r === void 0 ? void 0 : r.hasNoRepresentation) {\r\n            ast.hasNoRepresentation = true;\r\n        }\r\n        return ast;\r\n    }\r\n    function parseTTranslation(node, ctx) {\r\n        if (node.getAttribute(\"t-translation\") !== \"off\") {\r\n            return null;\r\n        }\r\n        node.removeAttribute(\"t-translation\");\r\n        const result = parseNode(node, ctx);\r\n        if ((result === null || result === void 0 ? void 0 : result.type) === 3 /* Multi */) {\r\n            const children = result.content.map(wrapInTTranslationAST);\r\n            return makeASTMulti(children);\r\n        }\r\n        return wrapInTTranslationAST(result);\r\n    }\r\n    // -----------------------------------------------------------------------------\r\n    // Translation Context\r\n    // -----------------------------------------------------------------------------\r\n    function wrapInTTranslationContextAST(r, translationCtx) {\r\n        const ast = {\r\n            type: 17 /* TTranslationContext */,\r\n            content: r,\r\n            translationCtx,\r\n        };\r\n        if (r === null || r === void 0 ? void 0 : r.hasNoRepresentation) {\r\n            ast.hasNoRepresentation = true;\r\n        }\r\n        return ast;\r\n    }\r\n    function parseTTranslationContext(node, ctx) {\r\n        const translationCtx = node.getAttribute(\"t-translation-context\");\r\n        if (!translationCtx) {\r\n            return null;\r\n        }\r\n        node.removeAttribute(\"t-translation-context\");\r\n        const result = parseNode(node, ctx);\r\n        if ((result === null || result === void 0 ? void 0 : result.type) === 3 /* Multi */) {\r\n            const children = result.content.map((c) => wrapInTTranslationContextAST(c, translationCtx));\r\n            return makeASTMulti(children);\r\n        }\r\n        return wrapInTTranslationContextAST(result, translationCtx);\r\n    }\r\n    // -----------------------------------------------------------------------------\r\n    // Portal\r\n    // -----------------------------------------------------------------------------\r\n    function parseTPortal(node, ctx) {\r\n        if (!node.hasAttribute(\"t-portal\")) {\r\n            return null;\r\n        }\r\n        const target = node.getAttribute(\"t-portal\");\r\n        node.removeAttribute(\"t-portal\");\r\n        const content = parseNode(node, ctx);\r\n        if (!content) {\r\n            return {\r\n                type: 0 /* Text */,\r\n                value: \"\",\r\n            };\r\n        }\r\n        return {\r\n            type: 18 /* TPortal */,\r\n            target,\r\n            content,\r\n        };\r\n    }\r\n    // -----------------------------------------------------------------------------\r\n    // helpers\r\n    // -----------------------------------------------------------------------------\r\n    /**\r\n     * Parse all the child nodes of a given node and return a list of ast elements\r\n     */\r\n    function parseChildren(node, ctx) {\r\n        const children = [];\r\n        for (let child of node.childNodes) {\r\n            const childAst = parseNode(child, ctx);\r\n            if (childAst) {\r\n                if (childAst.type === 3 /* Multi */) {\r\n                    children.push(...childAst.content);\r\n                }\r\n                else {\r\n                    children.push(childAst);\r\n                }\r\n            }\r\n        }\r\n        return children;\r\n    }\r\n    function makeASTMulti(children) {\r\n        const ast = { type: 3 /* Multi */, content: children };\r\n        if (children.every((c) => c.hasNoRepresentation)) {\r\n            ast.hasNoRepresentation = true;\r\n        }\r\n        return ast;\r\n    }\r\n    /**\r\n     * Parse all the child nodes of a given node and return an ast if possible.\r\n     * In the case there are multiple children, they are wrapped in a astmulti.\r\n     */\r\n    function parseChildNodes(node, ctx) {\r\n        const children = parseChildren(node, ctx);\r\n        switch (children.length) {\r\n            case 0:\r\n                return null;\r\n            case 1:\r\n                return children[0];\r\n            default:\r\n                return makeASTMulti(children);\r\n        }\r\n    }\r\n    /**\r\n     * Normalizes the content of an Element so that t-if/t-elif/t-else directives\r\n     * immediately follow one another (by removing empty text nodes or comments).\r\n     * Throws an error when a conditional branching statement is malformed. This\r\n     * function modifies the Element in place.\r\n     *\r\n     * @param el the element containing the tree that should be normalized\r\n     */\r\n    function normalizeTIf(el) {\r\n        let tbranch = el.querySelectorAll(\"[t-elif], [t-else]\");\r\n        for (let i = 0, ilen = tbranch.length; i < ilen; i++) {\r\n            let node = tbranch[i];\r\n            let prevElem = node.previousElementSibling;\r\n            let pattr = (name) => prevElem.getAttribute(name);\r\n            let nattr = (name) => +!!node.getAttribute(name);\r\n            if (prevElem && (pattr(\"t-if\") || pattr(\"t-elif\"))) {\r\n                if (pattr(\"t-foreach\")) {\r\n                    throw new OwlError(\"t-if cannot stay at the same level as t-foreach when using t-elif or t-else\");\r\n                }\r\n                if ([\"t-if\", \"t-elif\", \"t-else\"].map(nattr).reduce(function (a, b) {\r\n                    return a + b;\r\n                }) > 1) {\r\n                    throw new OwlError(\"Only one conditional branching directive is allowed per node\");\r\n                }\r\n                // All text (with only spaces) and comment nodes (nodeType 8) between\r\n                // branch nodes are removed\r\n                let textNode;\r\n                while ((textNode = node.previousSibling) !== prevElem) {\r\n                    if (textNode.nodeValue.trim().length && textNode.nodeType !== 8) {\r\n                        throw new OwlError(\"text is not allowed between branching directives\");\r\n                    }\r\n                    textNode.remove();\r\n                }\r\n            }\r\n            else {\r\n                throw new OwlError(\"t-elif and t-else directives must be preceded by a t-if or t-elif directive\");\r\n            }\r\n        }\r\n    }\r\n    /**\r\n     * Normalizes the content of an Element so that t-esc directives on components\r\n     * are removed and instead places a <t t-esc=\"\"> as the default slot of the\r\n     * component. Also throws if the component already has content. This function\r\n     * modifies the Element in place.\r\n     *\r\n     * @param el the element containing the tree that should be normalized\r\n     */\r\n    function normalizeTEscTOut(el) {\r\n        for (const d of [\"t-esc\", \"t-out\"]) {\r\n            const elements = [...el.querySelectorAll(`[${d}]`)].filter((el) => el.tagName[0] === el.tagName[0].toUpperCase() || el.hasAttribute(\"t-component\"));\r\n            for (const el of elements) {\r\n                if (el.childNodes.length) {\r\n                    throw new OwlError(`Cannot have ${d} on a component that already has content`);\r\n                }\r\n                const value = el.getAttribute(d);\r\n                el.removeAttribute(d);\r\n                const t = el.ownerDocument.createElement(\"t\");\r\n                if (value != null) {\r\n                    t.setAttribute(d, value);\r\n                }\r\n                el.appendChild(t);\r\n            }\r\n        }\r\n    }\r\n    /**\r\n     * Normalizes the tree inside a given element and do some preliminary validation\r\n     * on it. This function modifies the Element in place.\r\n     *\r\n     * @param el the element containing the tree that should be normalized\r\n     */\r\n    function normalizeXML(el) {\r\n        normalizeTIf(el);\r\n        normalizeTEscTOut(el);\r\n    }\r\n\r\n    function compile(template, options = {\r\n        hasGlobalValues: false,\r\n    }) {\r\n        // parsing\r\n        const ast = parse(template, options.customDirectives);\r\n        // some work\r\n        const hasSafeContext = template instanceof Node\r\n            ? !(template instanceof Element) || template.querySelector(\"[t-set], [t-call]\") === null\r\n            : !template.includes(\"t-set\") && !template.includes(\"t-call\");\r\n        // code generation\r\n        const codeGenerator = new CodeGenerator(ast, { ...options, hasSafeContext });\r\n        const code = codeGenerator.generateCode();\r\n        // template function\r\n        try {\r\n            return new Function(\"app, bdom, helpers\", code);\r\n        }\r\n        catch (originalError) {\r\n            const { name } = options;\r\n            const nameStr = name ? `template \"${name}\"` : \"anonymous template\";\r\n            const err = new OwlError(`Failed to compile ${nameStr}: ${originalError.message}\\n\\ngenerated code:\\nfunction(app, bdom, helpers) {\\n${code}\\n}`);\r\n            err.cause = originalError;\r\n            throw err;\r\n        }\r\n    }\r\n\r\n    // do not modify manually. This file is generated by the release script.\r\n    const version = \"2.8.1\";\r\n\r\n    // -----------------------------------------------------------------------------\r\n    //  Scheduler\r\n    // -----------------------------------------------------------------------------\r\n    class Scheduler {\r\n        constructor() {\r\n            this.tasks = new Set();\r\n            this.frame = 0;\r\n            this.delayedRenders = [];\r\n            this.cancelledNodes = new Set();\r\n            this.processing = false;\r\n            this.requestAnimationFrame = Scheduler.requestAnimationFrame;\r\n        }\r\n        addFiber(fiber) {\r\n            this.tasks.add(fiber.root);\r\n        }\r\n        scheduleDestroy(node) {\r\n            this.cancelledNodes.add(node);\r\n            if (this.frame === 0) {\r\n                this.frame = this.requestAnimationFrame(() => this.processTasks());\r\n            }\r\n        }\r\n        /**\r\n         * Process all current tasks. This only applies to the fibers that are ready.\r\n         * Other tasks are left unchanged.\r\n         */\r\n        flush() {\r\n            if (this.delayedRenders.length) {\r\n                let renders = this.delayedRenders;\r\n                this.delayedRenders = [];\r\n                for (let f of renders) {\r\n                    if (f.root && f.node.status !== 3 /* DESTROYED */ && f.node.fiber === f) {\r\n                        f.render();\r\n                    }\r\n                }\r\n            }\r\n            if (this.frame === 0) {\r\n                this.frame = this.requestAnimationFrame(() => this.processTasks());\r\n            }\r\n        }\r\n        processTasks() {\r\n            if (this.processing) {\r\n                return;\r\n            }\r\n            this.processing = true;\r\n            this.frame = 0;\r\n            for (let node of this.cancelledNodes) {\r\n                node._destroy();\r\n            }\r\n            this.cancelledNodes.clear();\r\n            for (let task of this.tasks) {\r\n                this.processFiber(task);\r\n            }\r\n            for (let task of this.tasks) {\r\n                if (task.node.status === 3 /* DESTROYED */) {\r\n                    this.tasks.delete(task);\r\n                }\r\n            }\r\n            this.processing = false;\r\n        }\r\n        processFiber(fiber) {\r\n            if (fiber.root !== fiber) {\r\n                this.tasks.delete(fiber);\r\n                return;\r\n            }\r\n            const hasError = fibersInError.has(fiber);\r\n            if (hasError && fiber.counter !== 0) {\r\n                this.tasks.delete(fiber);\r\n                return;\r\n            }\r\n            if (fiber.node.status === 3 /* DESTROYED */) {\r\n                this.tasks.delete(fiber);\r\n                return;\r\n            }\r\n            if (fiber.counter === 0) {\r\n                if (!hasError) {\r\n                    fiber.complete();\r\n                }\r\n                // at this point, the fiber should have been applied to the DOM, so we can\r\n                // remove it from the task list. If it is not the case, it means that there\r\n                // was an error and an error handler triggered a new rendering that recycled\r\n                // the fiber, so in that case, we actually want to keep the fiber around,\r\n                // otherwise it will just be ignored.\r\n                if (fiber.appliedToDom) {\r\n                    this.tasks.delete(fiber);\r\n                }\r\n            }\r\n        }\r\n    }\r\n    // capture the value of requestAnimationFrame as soon as possible, to avoid\r\n    // interactions with other code, such as test frameworks that override them\r\n    Scheduler.requestAnimationFrame = window.requestAnimationFrame.bind(window);\r\n\r\n    let hasBeenLogged = false;\r\n    const apps = new Set();\r\n    window.__OWL_DEVTOOLS__ || (window.__OWL_DEVTOOLS__ = { apps, Fiber, RootFiber, toRaw, reactive });\r\n    class App extends TemplateSet {\r\n        constructor(Root, config = {}) {\r\n            super(config);\r\n            this.scheduler = new Scheduler();\r\n            this.subRoots = new Set();\r\n            this.root = null;\r\n            this.name = config.name || \"\";\r\n            this.Root = Root;\r\n            apps.add(this);\r\n            if (config.test) {\r\n                this.dev = true;\r\n            }\r\n            this.warnIfNoStaticProps = config.warnIfNoStaticProps || false;\r\n            if (this.dev && !config.test && !hasBeenLogged) {\r\n                console.info(`Owl is running in 'dev' mode.`);\r\n                hasBeenLogged = true;\r\n            }\r\n            const env = config.env || {};\r\n            const descrs = Object.getOwnPropertyDescriptors(env);\r\n            this.env = Object.freeze(Object.create(Object.getPrototypeOf(env), descrs));\r\n            this.props = config.props || {};\r\n        }\r\n        mount(target, options) {\r\n            const root = this.createRoot(this.Root, { props: this.props });\r\n            this.root = root.node;\r\n            this.subRoots.delete(root.node);\r\n            return root.mount(target, options);\r\n        }\r\n        createRoot(Root, config = {}) {\r\n            const props = config.props || {};\r\n            // hack to make sure the sub root get the sub env if necessary. for owl 3,\r\n            // would be nice to rethink the initialization process to make sure that\r\n            // we can create a ComponentNode and give it explicitely the env, instead\r\n            // of looking it up in the app\r\n            const env = this.env;\r\n            if (config.env) {\r\n                this.env = config.env;\r\n            }\r\n            const restore = saveCurrent();\r\n            const node = this.makeNode(Root, props);\r\n            restore();\r\n            if (config.env) {\r\n                this.env = env;\r\n            }\r\n            this.subRoots.add(node);\r\n            return {\r\n                node,\r\n                mount: (target, options) => {\r\n                    App.validateTarget(target);\r\n                    if (this.dev) {\r\n                        validateProps(Root, props, { __owl__: { app: this } });\r\n                    }\r\n                    const prom = this.mountNode(node, target, options);\r\n                    return prom;\r\n                },\r\n                destroy: () => {\r\n                    this.subRoots.delete(node);\r\n                    node.destroy();\r\n                    this.scheduler.processTasks();\r\n                },\r\n            };\r\n        }\r\n        makeNode(Component, props) {\r\n            return new ComponentNode(Component, props, this, null, null);\r\n        }\r\n        mountNode(node, target, options) {\r\n            const promise = new Promise((resolve, reject) => {\r\n                let isResolved = false;\r\n                // manually set a onMounted callback.\r\n                // that way, we are independant from the current node.\r\n                node.mounted.push(() => {\r\n                    resolve(node.component);\r\n                    isResolved = true;\r\n                });\r\n                // Manually add the last resort error handler on the node\r\n                let handlers = nodeErrorHandlers.get(node);\r\n                if (!handlers) {\r\n                    handlers = [];\r\n                    nodeErrorHandlers.set(node, handlers);\r\n                }\r\n                handlers.unshift((e) => {\r\n                    if (!isResolved) {\r\n                        reject(e);\r\n                    }\r\n                    throw e;\r\n                });\r\n            });\r\n            node.mountComponent(target, options);\r\n            return promise;\r\n        }\r\n        destroy() {\r\n            if (this.root) {\r\n                for (let subroot of this.subRoots) {\r\n                    subroot.destroy();\r\n                }\r\n                this.root.destroy();\r\n                this.scheduler.processTasks();\r\n            }\r\n            apps.delete(this);\r\n        }\r\n        createComponent(name, isStatic, hasSlotsProp, hasDynamicPropList, propList) {\r\n            const isDynamic = !isStatic;\r\n            let arePropsDifferent;\r\n            const hasNoProp = propList.length === 0;\r\n            if (hasSlotsProp) {\r\n                arePropsDifferent = (_1, _2) => true;\r\n            }\r\n            else if (hasDynamicPropList) {\r\n                arePropsDifferent = function (props1, props2) {\r\n                    for (let k in props1) {\r\n                        if (props1[k] !== props2[k]) {\r\n                            return true;\r\n                        }\r\n                    }\r\n                    return Object.keys(props1).length !== Object.keys(props2).length;\r\n                };\r\n            }\r\n            else if (hasNoProp) {\r\n                arePropsDifferent = (_1, _2) => false;\r\n            }\r\n            else {\r\n                arePropsDifferent = function (props1, props2) {\r\n                    for (let p of propList) {\r\n                        if (props1[p] !== props2[p]) {\r\n                            return true;\r\n                        }\r\n                    }\r\n                    return false;\r\n                };\r\n            }\r\n            const updateAndRender = ComponentNode.prototype.updateAndRender;\r\n            const initiateRender = ComponentNode.prototype.initiateRender;\r\n            return (props, key, ctx, parent, C) => {\r\n                let children = ctx.children;\r\n                let node = children[key];\r\n                if (isDynamic && node && node.component.constructor !== C) {\r\n                    node = undefined;\r\n                }\r\n                const parentFiber = ctx.fiber;\r\n                if (node) {\r\n                    if (arePropsDifferent(node.props, props) || parentFiber.deep || node.forceNextRender) {\r\n                        node.forceNextRender = false;\r\n                        updateAndRender.call(node, props, parentFiber);\r\n                    }\r\n                }\r\n                else {\r\n                    // new component\r\n                    if (isStatic) {\r\n                        const components = parent.constructor.components;\r\n                        if (!components) {\r\n                            throw new OwlError(`Cannot find the definition of component \"${name}\", missing static components key in parent`);\r\n                        }\r\n                        C = components[name];\r\n                        if (!C) {\r\n                            throw new OwlError(`Cannot find the definition of component \"${name}\"`);\r\n                        }\r\n                        else if (!(C.prototype instanceof Component)) {\r\n                            throw new OwlError(`\"${name}\" is not a Component. It must inherit from the Component class`);\r\n                        }\r\n                    }\r\n                    node = new ComponentNode(C, props, this, ctx, key);\r\n                    children[key] = node;\r\n                    initiateRender.call(node, new Fiber(node, parentFiber));\r\n                }\r\n                parentFiber.childrenMap[key] = node;\r\n                return node;\r\n            };\r\n        }\r\n        handleError(...args) {\r\n            return handleError(...args);\r\n        }\r\n    }\r\n    App.validateTarget = validateTarget;\r\n    App.apps = apps;\r\n    App.version = version;\r\n    async function mount(C, target, config = {}) {\r\n        return new App(C, config).mount(target, config);\r\n    }\r\n\r\n    const mainEventHandler = (data, ev, currentTarget) => {\r\n        const { data: _data, modifiers } = filterOutModifiersFromData(data);\r\n        data = _data;\r\n        let stopped = false;\r\n        if (modifiers.length) {\r\n            let selfMode = false;\r\n            const isSelf = ev.target === currentTarget;\r\n            for (const mod of modifiers) {\r\n                switch (mod) {\r\n                    case \"self\":\r\n                        selfMode = true;\r\n                        if (isSelf) {\r\n                            continue;\r\n                        }\r\n                        else {\r\n                            return stopped;\r\n                        }\r\n                    case \"prevent\":\r\n                        if ((selfMode && isSelf) || !selfMode)\r\n                            ev.preventDefault();\r\n                        continue;\r\n                    case \"stop\":\r\n                        if ((selfMode && isSelf) || !selfMode)\r\n                            ev.stopPropagation();\r\n                        stopped = true;\r\n                        continue;\r\n                }\r\n            }\r\n        }\r\n        // If handler is empty, the array slot 0 will also be empty, and data will not have the property 0\r\n        // We check this rather than data[0] being truthy (or typeof function) so that it crashes\r\n        // as expected when there is a handler expression that evaluates to a falsy value\r\n        if (Object.hasOwnProperty.call(data, 0)) {\r\n            const handler = data[0];\r\n            if (typeof handler !== \"function\") {\r\n                throw new OwlError(`Invalid handler (expected a function, received: '${handler}')`);\r\n            }\r\n            let node = data[1] ? data[1].__owl__ : null;\r\n            if (node ? node.status === 1 /* MOUNTED */ : true) {\r\n                handler.call(node ? node.component : null, ev);\r\n            }\r\n        }\r\n        return stopped;\r\n    };\r\n\r\n    function status(component) {\r\n        switch (component.__owl__.status) {\r\n            case 0 /* NEW */:\r\n                return \"new\";\r\n            case 2 /* CANCELLED */:\r\n                return \"cancelled\";\r\n            case 1 /* MOUNTED */:\r\n                return \"mounted\";\r\n            case 3 /* DESTROYED */:\r\n                return \"destroyed\";\r\n        }\r\n    }\r\n\r\n    // -----------------------------------------------------------------------------\r\n    // useRef\r\n    // -----------------------------------------------------------------------------\r\n    /**\r\n     * The purpose of this hook is to allow components to get a reference to a sub\r\n     * html node or component.\r\n     */\r\n    function useRef(name) {\r\n        const node = getCurrent();\r\n        const refs = node.refs;\r\n        return {\r\n            get el() {\r\n                const el = refs[name];\r\n                return inOwnerDocument(el) ? el : null;\r\n            },\r\n        };\r\n    }\r\n    // -----------------------------------------------------------------------------\r\n    // useEnv and useSubEnv\r\n    // -----------------------------------------------------------------------------\r\n    /**\r\n     * This hook is useful as a building block for some customized hooks, that may\r\n     * need a reference to the env of the component calling them.\r\n     */\r\n    function useEnv() {\r\n        return getCurrent().component.env;\r\n    }\r\n    function extendEnv(currentEnv, extension) {\r\n        const env = Object.create(currentEnv);\r\n        const descrs = Object.getOwnPropertyDescriptors(extension);\r\n        return Object.freeze(Object.defineProperties(env, descrs));\r\n    }\r\n    /**\r\n     * This hook is a simple way to let components use a sub environment.  Note that\r\n     * like for all hooks, it is important that this is only called in the\r\n     * constructor method.\r\n     */\r\n    function useSubEnv(envExtension) {\r\n        const node = getCurrent();\r\n        node.component.env = extendEnv(node.component.env, envExtension);\r\n        useChildSubEnv(envExtension);\r\n    }\r\n    function useChildSubEnv(envExtension) {\r\n        const node = getCurrent();\r\n        node.childEnv = extendEnv(node.childEnv, envExtension);\r\n    }\r\n    /**\r\n     * This hook will run a callback when a component is mounted and patched, and\r\n     * will run a cleanup function before patching and before unmounting the\r\n     * the component.\r\n     *\r\n     * @template T\r\n     * @param {Effect<T>} effect the effect to run on component mount and/or patch\r\n     * @param {()=>[...T]} [computeDependencies=()=>[NaN]] a callback to compute\r\n     *      dependencies that will decide if the effect needs to be cleaned up and\r\n     *      run again. If the dependencies did not change, the effect will not run\r\n     *      again. The default value returns an array containing only NaN because\r\n     *      NaN !== NaN, which will cause the effect to rerun on every patch.\r\n     */\r\n    function useEffect(effect, computeDependencies = () => [NaN]) {\r\n        let cleanup;\r\n        let dependencies;\r\n        onMounted(() => {\r\n            dependencies = computeDependencies();\r\n            cleanup = effect(...dependencies);\r\n        });\r\n        onPatched(() => {\r\n            const newDeps = computeDependencies();\r\n            const shouldReapply = newDeps.some((val, i) => val !== dependencies[i]);\r\n            if (shouldReapply) {\r\n                dependencies = newDeps;\r\n                if (cleanup) {\r\n                    cleanup();\r\n                }\r\n                cleanup = effect(...dependencies);\r\n            }\r\n        });\r\n        onWillUnmount(() => cleanup && cleanup());\r\n    }\r\n    // -----------------------------------------------------------------------------\r\n    // useExternalListener\r\n    // -----------------------------------------------------------------------------\r\n    /**\r\n     * When a component needs to listen to DOM Events on element(s) that are not\r\n     * part of his hierarchy, we can use the `useExternalListener` hook.\r\n     * It will correctly add and remove the event listener, whenever the\r\n     * component is mounted and unmounted.\r\n     *\r\n     * Example:\r\n     *  a menu needs to listen to the click on window to be closed automatically\r\n     *\r\n     * Usage:\r\n     *  in the constructor of the OWL component that needs to be notified,\r\n     *  `useExternalListener(window, 'click', this._doSomething);`\r\n     * */\r\n    function useExternalListener(target, eventName, handler, eventParams) {\r\n        const node = getCurrent();\r\n        const boundHandler = handler.bind(node.component);\r\n        onMounted(() => target.addEventListener(eventName, boundHandler, eventParams));\r\n        onWillUnmount(() => target.removeEventListener(eventName, boundHandler, eventParams));\r\n    }\r\n\r\n    config.shouldNormalizeDom = false;\r\n    config.mainEventHandler = mainEventHandler;\r\n    const blockDom = {\r\n        config,\r\n        // bdom entry points\r\n        mount: mount$1,\r\n        patch,\r\n        remove,\r\n        // bdom block types\r\n        list,\r\n        multi,\r\n        text,\r\n        toggler,\r\n        createBlock,\r\n        html,\r\n        comment,\r\n    };\r\n    const __info__ = {\r\n        version: App.version,\r\n    };\r\n\r\n    TemplateSet.prototype._compileTemplate = function _compileTemplate(name, template) {\r\n        return compile(template, {\r\n            name,\r\n            dev: this.dev,\r\n            translateFn: this.translateFn,\r\n            translatableAttributes: this.translatableAttributes,\r\n            customDirectives: this.customDirectives,\r\n            hasGlobalValues: this.hasGlobalValues,\r\n        });\r\n    };\r\n\r\n    exports.App = App;\r\n    exports.Component = Component;\r\n    exports.EventBus = EventBus;\r\n    exports.OwlError = OwlError;\r\n    exports.__info__ = __info__;\r\n    exports.batched = batched;\r\n    exports.blockDom = blockDom;\r\n    exports.htmlEscape = htmlEscape;\r\n    exports.loadFile = loadFile;\r\n    exports.markRaw = markRaw;\r\n    exports.markup = markup;\r\n    exports.mount = mount;\r\n    exports.onError = onError;\r\n    exports.onMounted = onMounted;\r\n    exports.onPatched = onPatched;\r\n    exports.onRendered = onRendered;\r\n    exports.onWillDestroy = onWillDestroy;\r\n    exports.onWillPatch = onWillPatch;\r\n    exports.onWillRender = onWillRender;\r\n    exports.onWillStart = onWillStart;\r\n    exports.onWillUnmount = onWillUnmount;\r\n    exports.onWillUpdateProps = onWillUpdateProps;\r\n    exports.reactive = reactive;\r\n    exports.status = status;\r\n    exports.toRaw = toRaw;\r\n    exports.useChildSubEnv = useChildSubEnv;\r\n    exports.useComponent = useComponent;\r\n    exports.useEffect = useEffect;\r\n    exports.useEnv = useEnv;\r\n    exports.useExternalListener = useExternalListener;\r\n    exports.useRef = useRef;\r\n    exports.useState = useState;\r\n    exports.useSubEnv = useSubEnv;\r\n    exports.validate = validate;\r\n    exports.validateType = validateType;\r\n    exports.whenReady = whenReady;\r\n    exports.xml = xml;\r\n\r\n    Object.defineProperty(exports, '__esModule', { value: true });\r\n\r\n\r\n    __info__.date = '2025-09-23T07:17:45.055Z';\r\n    __info__.hash = '5211116';\r\n    __info__.url = 'https://github.com/odoo/owl';\r\n\r\n\r\n})(this.owl = this.owl || {});\r\n", "odoo.define(\"@odoo/owl\", [], function () {\n    \"use strict\";\n\n    return owl;\n});\n", "import { App, EventBus } from \"@odoo/owl\";\nimport { SERVICES_METADATA } from \"@web/core/utils/hooks\";\nimport { registry } from \"@web/core/registry\";\nimport { getTemplate } from \"@web/core/templates\";\nimport { appTranslateFn } from \"@web/core/l10n/translation\";\nimport { session } from \"@web/session\";\nimport { isMacOS } from \"@web/core/browser/feature_detection\";\n\n// -----------------------------------------------------------------------------\n// Types\n// -----------------------------------------------------------------------------\n\n/**\n * @typedef {Object} OdooEnv\n * @property {import(\"services\").Services} services\n * @property {EventBus} bus\n * @property {string} debug\n * @property {boolean} [isSmall]\n */\n\n// -----------------------------------------------------------------------------\n// makeEnv\n// -----------------------------------------------------------------------------\n\n/**\n * Return a value Odoo Env object\n *\n * @returns {OdooEnv}\n */\nexport function makeEnv() {\n    const bus = new EventBus();\n    const prom = new Promise((resolve) => {\n        bus.addEventListener(\"SERVICES-LOADED\", resolve, { once: true });\n    });\n    return {\n        bus,\n        isReady: prom,\n        services: {},\n        debug: odoo.debug,\n        get isSmall() {\n            throw new Error(\"UI service not initialized!\");\n        },\n    };\n}\n\n// -----------------------------------------------------------------------------\n// Service Launcher\n// -----------------------------------------------------------------------------\n\nconst serviceRegistry = registry.category(\"services\");\n\nserviceRegistry.addValidation({\n    start: Function,\n    dependencies: { type: Array, element: String, optional: true },\n    async: { type: [{ type: Array, element: String }, { value: true }], optional: true },\n    \"*\": true,\n});\n\nlet startServicesPromise = null;\n\n/**\n * Start all services registered in the service registry, while making sure\n * each service dependencies are properly fulfilled.\n *\n * @param {OdooEnv} env\n * @returns {Promise<void>}\n */\nexport async function startServices(env) {\n    // Wait for all synchronous code so that if new services that depend on\n    // one another are added to the registry, they're all present before we\n    // start them regardless of the order they're added to the registry.\n    await Promise.resolve();\n\n    const toStart = new Map();\n    serviceRegistry.addEventListener(\"UPDATE\", async (ev) => {\n        // Wait for all synchronous code so that if new services that depend on\n        // one another are added to the registry, they're all present before we\n        // start them regardless of the order they're added to the registry.\n        await Promise.resolve();\n        const { operation, key: name, value: service } = ev.detail;\n        if (operation === \"delete\") {\n            // We hardly see why it would be usefull to remove a service.\n            // Furthermore we could encounter problems with dependencies.\n            // Keep it simple!\n            return;\n        }\n        if (toStart.size) {\n            const namedService = Object.assign(Object.create(service), { name });\n            toStart.set(name, namedService);\n        } else {\n            await _startServices(env, toStart);\n        }\n    });\n    await _startServices(env, toStart);\n}\n\nasync function _startServices(env, toStart) {\n    if (startServicesPromise) {\n        return startServicesPromise.then(() => _startServices(env, toStart));\n    }\n    const services = env.services;\n    for (const [name, service] of serviceRegistry.getEntries()) {\n        if (!(name in services)) {\n            const namedService = Object.assign(Object.create(service), { name });\n            toStart.set(name, namedService);\n        }\n    }\n\n    // start as many services in parallel as possible\n    async function start() {\n        let service = null;\n        const proms = [];\n        while ((service = findNext())) {\n            const name = service.name;\n            toStart.delete(name);\n            const entries = (service.dependencies || []).map((dep) => [dep, services[dep]]);\n            const dependencies = Object.fromEntries(entries);\n            if (name in services) {\n                continue;\n            }\n            const value = service.start(env, dependencies);\n            if (\"async\" in service) {\n                SERVICES_METADATA[name] = service.async;\n            }\n            proms.push(\n                Promise.resolve(value).then((val) => {\n                    services[name] = val || null;\n                })\n            );\n        }\n        await Promise.all(proms);\n        if (proms.length) {\n            return start();\n        }\n    }\n    startServicesPromise = start().finally(() => {\n        startServicesPromise = null;\n    });\n    await startServicesPromise;\n    env.bus.trigger(\"SERVICES-LOADED\");\n    if (toStart.size) {\n        const missingDeps = new Set();\n        for (const service of toStart.values()) {\n            for (const dependency of service.dependencies) {\n                if (!(dependency in services) && !toStart.has(dependency)) {\n                    missingDeps.add(dependency);\n                }\n            }\n        }\n        const depNames = [...missingDeps].join(\", \");\n        throw new Error(\n            `Some services could not be started: ${[\n                ...toStart.keys(),\n            ]}. Missing dependencies: ${depNames}`\n        );\n    }\n\n    function findNext() {\n        for (const s of toStart.values()) {\n            if (s.dependencies) {\n                if (s.dependencies.every((d) => d in services)) {\n                    return s;\n                }\n            } else {\n                return s;\n            }\n        }\n        return null;\n    }\n}\n\nexport const customDirectives = {\n    // t-custom-click=\"handler\"\n    // This custom directive will add two even listeners (\"click\"; \"auxclick\") and call the global value \"click\".\n    // The global value \"click\" will call the handler with two parameters :\n    //      - ev (the original event)\n    //      - isMiddleClick (a boolean that says if the user middle clicked, or if he did a ctrl+click)\n    //\n    click: (node, value, modifiers) => {\n        let mods = \"\";\n        if (modifiers.includes(\"synthetic\")) {\n            mods += \".synthetic\";\n        }\n        if (modifiers.includes(\"capture\")) {\n            mods += \".capture\";\n        }\n        const handlerFunction = `(ev) => __globals__.click(ev, (${value}).bind(this), '${JSON.stringify(\n            modifiers\n        )}')`;\n        node.setAttribute(`t-on-click${mods}`, handlerFunction);\n        node.setAttribute(`t-on-auxclick${mods}`, handlerFunction);\n    },\n};\n\nexport const globalValues = {\n    click: (ev, value, modifiers) => {\n        if (ev.button === 0 || ev.button === 1) {\n            modifiers = JSON.parse(modifiers);\n            for (const modifier of modifiers) {\n                if (modifier === \"stop\") {\n                    ev.stopPropagation();\n                }\n                if (modifier === \"prevent\") {\n                    ev.preventDefault();\n                }\n            }\n            const ctrlKey = isMacOS() ? ev.metaKey : ev.ctrlKey;\n            const isMiddleClick = (ctrlKey && ev.button === 0) || ev.button === 1;\n            value(ev, isMiddleClick);\n        }\n    },\n};\n\n/**\n * Create an application with a given component as root and mount it. If no env\n * is provided, the application will be treated as a \"root\": an env will be\n * created and the services will be started, it will also be set as the root\n * in `__WOWL_DEBUG__`\n *\n * @param {import(\"@odoo/owl\").Component} component the component to mount\n * @param {HTMLElement} target the HTML element in which to mount the app\n * @param {Partial<ConstructorParameters<typeof App>[1]>} [appConfig] object\n *  containing a (partial) config for the app.\n */\nexport async function mountComponent(component, target, appConfig = {}) {\n    let { env } = appConfig;\n    const isRoot = !env;\n    if (isRoot) {\n        env = await makeEnv();\n        await startServices(env);\n    }\n    const app = new App(component, {\n        env,\n        getTemplate,\n        dev: env.debug || session.test_mode,\n        warnIfNoStaticProps: !session.test_mode,\n        name: component.constructor.name,\n        translatableAttributes: [\"data-tooltip\"],\n        translateFn: appTranslateFn,\n        customDirectives,\n        globalValues,\n        ...appConfig,\n    });\n    const root = await app.mount(target);\n    if (isRoot) {\n        odoo.__WOWL_DEBUG__ = { root };\n    }\n    return app;\n}\n", "export const session = odoo.__session_info__ || {};\ndelete odoo.__session_info__;\n", "import { browser } from \"@web/core/browser/browser\";\nimport { localization } from \"@web/core/l10n/localization\";\nimport { clamp } from \"@web/core/utils/numbers\";\n\nimport { Component, onMounted, onWillUnmount, useRef, useState } from \"@odoo/owl\";\nimport { Deferred } from \"@web/core/utils/concurrency\";\n\nconst isScrollSwipable = (scrollables) => {\n    return {\n        left: !scrollables.filter((e) => e.scrollLeft !== 0).length,\n        right: !scrollables.filter(\n            (e) => e.scrollLeft + Math.round(e.getBoundingClientRect().width) !== e.scrollWidth\n        ).length,\n    };\n};\n\n/**\n * Action Swiper\n *\n * This component is intended to perform action once a user has completed a touch swipe.\n * You can choose the direction allowed for such behavior (left, right or both).\n * The action to perform must be passed as a props. It is possible to define a condition\n * to allow the swipe interaction conditionnally.\n * @extends Component\n */\nexport class ActionSwiper extends Component {\n    static template = \"web.ActionSwiper\";\n    static props = {\n        onLeftSwipe: {\n            type: Object,\n            args: {\n                action: Function,\n                icon: String,\n                bgColor: String,\n            },\n            optional: true,\n        },\n        onRightSwipe: {\n            type: Object,\n            args: {\n                action: Function,\n                icon: String,\n                bgColor: String,\n            },\n            optional: true,\n        },\n        slots: Object,\n        animationOnMove: { type: Boolean, optional: true },\n        animationType: { type: String, optional: true },\n        swipeDistanceRatio: { type: Number, optional: true },\n        swipeInvalid: { type: Function, optional: true },\n    };\n\n    static defaultProps = {\n        onLeftSwipe: undefined,\n        onRightSwipe: undefined,\n        animationOnMove: true,\n        animationType: \"bounce\",\n        swipeDistanceRatio: 2,\n    };\n\n    setup() {\n        this.actionTimeoutId = null;\n        this.resetTimeoutId = null;\n        this.defaultState = {\n            containerStyle: \"\",\n            isSwiping: false,\n            width: undefined,\n        };\n        this.root = useRef(\"root\");\n        this.targetContainer = useRef(\"targetContainer\");\n        this.state = useState({ ...this.defaultState });\n        this.scrollables = undefined;\n        this.startX = undefined;\n        this.swipedDistance = 0;\n        this.isScrollValidated = false;\n        onMounted(() => {\n            if (this.targetContainer.el) {\n                this.state.width = this.targetContainer.el.getBoundingClientRect().width;\n            }\n            // Forward classes set on component to slot, as we only want to wrap an\n            // existing component without altering the DOM structure any more than\n            // strictly necessary\n            if (this.props.onLeftSwipe || this.props.onRightSwipe) {\n                const classes = new Set(this.root.el.classList);\n                classes.delete(\"o_actionswiper\");\n                for (const className of classes) {\n                    this.targetContainer.el.firstChild.classList.add(className);\n                    this.root.el.classList.remove(className);\n                }\n            }\n        });\n        onWillUnmount(() => {\n            browser.clearTimeout(this.actionTimeoutId);\n            browser.clearTimeout(this.resetTimeoutId);\n        });\n    }\n    get localizedProps() {\n        return {\n            onLeftSwipe:\n                localization.direction === \"rtl\" ? this.props.onRightSwipe : this.props.onLeftSwipe,\n            onRightSwipe:\n                localization.direction === \"rtl\" ? this.props.onLeftSwipe : this.props.onRightSwipe,\n        };\n    }\n\n    /**\n     * @private\n     * @param {TouchEvent} ev\n     */\n    _onTouchEndSwipe() {\n        if (this.state.isSwiping) {\n            this.state.isSwiping = false;\n            if (\n                this.localizedProps.onRightSwipe &&\n                this.swipedDistance > this.state.width / this.props.swipeDistanceRatio\n            ) {\n                this.swipedDistance = this.state.width;\n                this.handleSwipe(this.localizedProps.onRightSwipe.action);\n            } else if (\n                this.localizedProps.onLeftSwipe &&\n                this.swipedDistance < -this.state.width / this.props.swipeDistanceRatio\n            ) {\n                this.swipedDistance = -this.state.width;\n                this.handleSwipe(this.localizedProps.onLeftSwipe.action);\n            } else {\n                this.state.containerStyle = \"\";\n            }\n        }\n    }\n    /**\n     * @private\n     * @param {TouchEvent} ev\n     */\n    _onTouchMoveSwipe(ev) {\n        if (this.state.isSwiping) {\n            if (this.props.swipeInvalid && this.props.swipeInvalid()) {\n                this.state.isSwiping = false;\n                return;\n            }\n            const { onLeftSwipe, onRightSwipe } = this.localizedProps;\n            this.swipedDistance = clamp(\n                ev.touches[0].clientX - this.startX,\n                onLeftSwipe ? -this.state.width : 0,\n                onRightSwipe ? this.state.width : 0\n            );\n            // Prevent the browser to navigate back/forward when using swipe\n            // gestures while still allowing to scroll vertically.\n            if (Math.abs(this.swipedDistance) > 40) {\n                ev.preventDefault();\n            }\n            // If there are scrollable elements under touch pressure,\n            // they must be at their limits to allow swiping.\n            if (\n                !this.isScrollValidated &&\n                this.scrollables &&\n                !isScrollSwipable(this.scrollables)[this.swipedDistance > 0 ? \"left\" : \"right\"]\n            ) {\n                return this._reset();\n            }\n            this.isScrollValidated = true;\n\n            if (this.props.animationOnMove) {\n                this.state.containerStyle = `transform: translateX(${this.swipedDistance}px)`;\n            }\n        }\n    }\n    /**\n     * @private\n     * @param {TouchEvent} ev\n     */\n    _onTouchStartSwipe(ev) {\n        this.scrollables = ev\n            .composedPath()\n            .filter(\n                (e) =>\n                    e.nodeType === 1 &&\n                    this.targetContainer.el.contains(e) &&\n                    e.scrollWidth > e.getBoundingClientRect().width &&\n                    [\"auto\", \"scroll\"].includes(window.getComputedStyle(e)[\"overflow-x\"])\n            );\n        if (!this.state.width) {\n            this.state.width =\n                this.targetContainer && this.targetContainer.el.getBoundingClientRect().width;\n        }\n        this.state.isSwiping = true;\n        this.isScrollValidated = false;\n        this.startX = ev.touches[0].clientX;\n    }\n\n    /**\n     * @private\n     */\n    _reset() {\n        Object.assign(this.state, { ...this.defaultState });\n        this.scrollables = undefined;\n        this.startX = undefined;\n        this.swipedDistance = 0;\n        this.isScrollValidated = false;\n    }\n\n    handleSwipe(action) {\n        if (this.props.animationType === \"bounce\") {\n            this.state.containerStyle = `transform: translateX(${this.swipedDistance}px)`;\n            this.actionTimeoutId = browser.setTimeout(async () => {\n                await action(Promise.resolve());\n                this._reset();\n            }, 500);\n        } else if (this.props.animationType === \"forwards\") {\n            this.state.containerStyle = `transform: translateX(${this.swipedDistance}px)`;\n            this.actionTimeoutId = browser.setTimeout(async () => {\n                const prom = new Deferred();\n                await action(prom);\n                this.state.isSwiping = true;\n                this.state.containerStyle = `transform: translateX(${-this.swipedDistance}px)`;\n                this.resetTimeoutId = browser.setTimeout(() => {\n                    prom.resolve();\n                    this._reset();\n                }, 100);\n            }, 100);\n        } else {\n            return action(Promise.resolve());\n        }\n    }\n}\n", "import { browser } from \"./browser/browser\";\n\nbrowser.addEventListener(\"click\", (ev) => {\n    const href = ev.target.closest(\"a\")?.getAttribute(\"href\");\n    if (href && href === \"#\") {\n        ev.preventDefault(); // single hash in href are just a way to activate A-tags node\n        return;\n    }\n});\n", "import { Component, onWillStart, whenReady, xml } from \"@odoo/owl\";\nimport { session } from \"@web/session\";\nimport { registry } from \"./registry\";\n\n/**\n * @typedef {{\n *  cssLibs: string[];\n *  jsLibs: string[];\n * }} BundleFileNames\n */\n\nexport const globalBundleCache = new Map();\nexport const assetCacheByDocument = new WeakMap();\n\nfunction getGlobalBundleCache() {\n    return globalBundleCache;\n}\n\nfunction getAssetCache(targetDoc) {\n    if (!assetCacheByDocument.has(targetDoc)) {\n        assetCacheByDocument.set(targetDoc, new Map());\n    }\n    return assetCacheByDocument.get(targetDoc);\n}\n\nexport function computeBundleCacheMap(targetDoc) {\n    const cacheMap = getGlobalBundleCache();\n    for (const script of targetDoc.head.querySelectorAll(\"script[src]\")) {\n        cacheMap.set(script.getAttribute(\"src\"), Promise.resolve());\n    }\n    for (const link of targetDoc.head.querySelectorAll(\"link[rel=stylesheet][href]\")) {\n        cacheMap.set(link.getAttribute(\"href\"), Promise.resolve());\n    }\n}\n\nwhenReady(() => computeBundleCacheMap(document));\n\n/**\n * @param {HTMLLinkElement | HTMLScriptElement} el\n * @param {(event: Event) => any} onLoad\n * @param {(error: Error) => any} onError\n */\nconst onLoadAndError = (el, onLoad, onError) => {\n    const onLoadListener = (event) => {\n        removeListeners();\n        onLoad(event);\n    };\n\n    const onErrorListener = (error) => {\n        removeListeners();\n        onError(error);\n    };\n\n    const removeListeners = () => {\n        el.removeEventListener(\"load\", onLoadListener);\n        el.removeEventListener(\"error\", onErrorListener);\n    };\n\n    el.addEventListener(\"load\", onLoadListener);\n    el.addEventListener(\"error\", onErrorListener);\n\n    window.addEventListener(\"pagehide\", () => {\n        removeListeners();\n    });\n};\n\n/** @type {typeof assets[\"getBundle\"]} */\nexport function getBundle() {\n    return assets.getBundle(...arguments);\n}\n\n/** @type {typeof assets[\"loadBundle\"]} */\nexport function loadBundle() {\n    return assets.loadBundle(...arguments);\n}\n\n/** @type {typeof assets[\"loadJS\"]} */\nexport function loadJS() {\n    return assets.loadJS(...arguments);\n}\n\n/** @type {typeof assets[\"loadCSS\"]} */\nexport function loadCSS() {\n    return assets.loadCSS(...arguments);\n}\n\nexport class AssetsLoadingError extends Error {}\n\n/**\n * Utility component that loads an asset bundle before instanciating a component\n */\nexport class LazyComponent extends Component {\n    static template = xml`<t t-component=\"Component\" t-props=\"componentProps\"/>`;\n    static props = {\n        Component: String,\n        bundle: String,\n        props: { type: [Object, Function], optional: true },\n    };\n    setup() {\n        onWillStart(async () => {\n            await loadBundle(this.props.bundle);\n            this.Component = registry.category(\"lazy_components\").get(this.props.Component);\n        });\n    }\n\n    get componentProps() {\n        return typeof this.props.props === \"function\" ? this.props.props() : this.props.props;\n    }\n}\n\n/**\n * This export is done only in order to modify the behavior of the exported\n * functions. This is done in order to be able to make a test environment.\n * Modules should only use the methods exported below.\n */\nexport const assets = {\n    retries: {\n        count: 3,\n        delay: 5000,\n        extraDelay: 2500,\n    },\n\n    /**\n     * Get the files information as descriptor object from a public asset template.\n     *\n     * @param {string} bundleName Name of the bundle containing the list of files\n     * @returns {Promise<BundleFileNames>}\n     */\n    getBundle(bundleName) {\n        const cacheMap = getGlobalBundleCache();\n        if (cacheMap.has(bundleName)) {\n            return cacheMap.get(bundleName);\n        }\n        const url = new URL(`/web/bundle/${bundleName}`, location.origin);\n        for (const [key, value] of Object.entries(session.bundle_params || {})) {\n            url.searchParams.set(key, value);\n        }\n        const promise = fetch(url)\n            .then(async (response) => {\n                const cssLibs = [];\n                const jsLibs = [];\n                if (!response.bodyUsed) {\n                    const result = await response.json();\n                    for (const { src, type } of Object.values(result)) {\n                        if (type === \"link\" && src) {\n                            cssLibs.push(src);\n                        } else if (type === \"script\" && src) {\n                            jsLibs.push(src);\n                        }\n                    }\n                }\n                return { cssLibs, jsLibs };\n            })\n            .catch((reason) => {\n                cacheMap.delete(bundleName);\n                throw new AssetsLoadingError(`The loading of ${url} failed`, { cause: reason });\n            });\n        cacheMap.set(bundleName, promise);\n        return promise;\n    },\n\n    /**\n     * Loads the given js/css libraries and asset bundles. Note that no library or\n     * asset will be loaded if it was already done before.\n     *\n     * @param {string} bundleName\n     * @param {Object} options\n     * @param {Document} [options.targetDoc=document] document to which the bundle will be applied (e.g. iframe document)\n     * @param {Boolean} [options.css=true] apply bundle css on targetDoc\n     * @param {Boolean} [options.js=true] apply bundle js on targetDoc\n     * @returns {Promise<void[]>}\n     */\n    loadBundle(bundleName, { targetDoc = document, css = true, js = true } = {}) {\n        if (typeof bundleName !== \"string\") {\n            throw new Error(\n                `loadBundle(bundleName:string) accepts only bundleName argument as a string ! Not ${JSON.stringify(\n                    bundleName\n                )} as ${typeof bundleName}`\n            );\n        }\n        return getBundle(bundleName).then(({ cssLibs, jsLibs }) => {\n            const promises = [];\n            if (css && cssLibs) {\n                promises.push(...cssLibs.map((url) => assets.loadCSS(url, { targetDoc })));\n            }\n            if (js && jsLibs) {\n                promises.push(...jsLibs.map((url) => assets.loadJS(url, { targetDoc })));\n            }\n            return Promise.all(promises);\n        });\n    },\n\n    /**\n     * Loads the given url as a stylesheet.\n     *\n     * @param {string} url the url of the stylesheet\n     * @param {number} [retryCount]\n     * @param {Object} options\n     * @param {number} [retryCount]\n     * @param {Document} [options.targetDoc=document] document to which the bundle will be applied (e.g. iframe document)\n     * @returns {Promise<void>} resolved when the stylesheet has been loaded\n     */\n    loadCSS(url, { retryCount = 0, targetDoc = document } = {}) {\n        const cacheMap = getAssetCache(targetDoc);\n        if (cacheMap.has(url)) {\n            return cacheMap.get(url);\n        }\n        const linkEl = targetDoc.createElement(\"link\");\n        linkEl.setAttribute(\"href\", url);\n        linkEl.type = \"text/css\";\n        linkEl.rel = \"stylesheet\";\n        const promise = new Promise((resolve, reject) =>\n            onLoadAndError(linkEl, resolve, async (error) => {\n                cacheMap.delete(url);\n                if (retryCount < assets.retries.count) {\n                    const delay = assets.retries.delay + assets.retries.extraDelay * retryCount;\n                    await new Promise((res) => setTimeout(res, delay));\n                    linkEl.remove();\n                    loadCSS(url, { retryCount: retryCount + 1, targetDoc })\n                        .then(resolve)\n                        .catch((reason) => {\n                            cacheMap.delete(url);\n                            reject(reason);\n                        });\n                } else {\n                    reject(\n                        new AssetsLoadingError(`The loading of ${url} failed`, { cause: error })\n                    );\n                }\n            })\n        );\n        cacheMap.set(url, promise);\n        targetDoc.head.appendChild(linkEl);\n        return promise;\n    },\n\n    /**\n     * Loads the given url inside a script tag.\n     *\n     * @param {string} url the url of the script\n     * @param {Document} targetDoc document to which the bundle will be applied (e.g. iframe document)\n     * @returns {Promise<void>} resolved when the script has been loaded\n     */\n    loadJS(url, { targetDoc = document } = {}) {\n        const cacheMap = getAssetCache(targetDoc);\n        if (cacheMap.has(url)) {\n            return cacheMap.get(url);\n        }\n        const scriptEl = targetDoc.createElement(\"script\");\n        scriptEl.setAttribute(\"src\", url);\n        scriptEl.type = url.includes(\"web/static/lib/pdfjs/\") ? \"module\" : \"text/javascript\";\n        const promise = new Promise((resolve, reject) =>\n            onLoadAndError(scriptEl, resolve, (error) => {\n                cacheMap.delete(url);\n                reject(new AssetsLoadingError(`The loading of ${url} failed`, { cause: error }));\n            })\n        );\n        cacheMap.set(url, promise);\n        targetDoc.head.appendChild(scriptEl);\n        return promise;\n    },\n};\n", "import { Deferred } from \"@web/core/utils/concurrency\";\nimport { useAutofocus, useForwardRefToParent, useService } from \"@web/core/utils/hooks\";\nimport { isScrollableY, scrollTo } from \"@web/core/utils/scrolling\";\nimport { useDebounced } from \"@web/core/utils/timing\";\nimport { getActiveHotkey } from \"@web/core/hotkeys/hotkey_service\";\nimport { usePosition } from \"@web/core/position/position_hook\";\nimport { Component, onWillUpdateProps, useExternalListener, useRef, useState } from \"@odoo/owl\";\nimport { mergeClasses } from \"@web/core/utils/classname\";\n\nexport class AutoComplete extends Component {\n    static template = \"web.AutoComplete\";\n    static props = {\n        value: { type: String, optional: true },\n        id: { type: String, optional: true },\n        sources: {\n            type: Array,\n            element: {\n                type: Object,\n                shape: {\n                    placeholder: { type: String, optional: true },\n                    options: [Array, Function],\n                    optionSlot: { type: String, optional: true },\n                },\n            },\n        },\n        placeholder: { type: String, optional: true },\n        autocomplete: { type: String, optional: true },\n        autoSelect: { type: Boolean, optional: true },\n        resetOnSelect: { type: Boolean, optional: true },\n        onInput: { type: Function, optional: true },\n        onCancel: { type: Function, optional: true },\n        onChange: { type: Function, optional: true },\n        onBlur: { type: Function, optional: true },\n        onFocus: { type: Function, optional: true },\n        searchOnInputClick: { type: Boolean, optional: true },\n        input: { type: Function, optional: true },\n        inputDebounceDelay: { type: Number, optional: true },\n        dropdown: { type: Boolean, optional: true },\n        autofocus: { type: Boolean, optional: true },\n        class: { type: String, optional: true },\n        slots: { type: Object, optional: true },\n        menuPositionOptions: { type: Object, optional: true },\n        menuCssClass: { type: [String, Array, Object], optional: true },\n        selectOnBlur: { type: Boolean, optional: true },\n    };\n    static defaultProps = {\n        value: \"\",\n        placeholder: \"\",\n        autocomplete: \"new-password\",\n        autoSelect: false,\n        dropdown: true,\n        onInput: () => {},\n        onCancel: () => {},\n        onChange: () => {},\n        onBlur: () => {},\n        onFocus: () => {},\n        searchOnInputClick: true,\n        inputDebounceDelay: 250,\n        menuPositionOptions: {},\n        menuCssClass: {},\n    };\n\n    get timeout() {\n        return this.props.inputDebounceDelay;\n    }\n\n    setup() {\n        this.nextSourceId = 0;\n        this.nextOptionId = 0;\n        this.sources = [];\n        this.inEdition = false;\n        this.mouseSelectionActive = false;\n        this.isOptionSelected = false;\n\n        this.state = useState({\n            navigationRev: 0,\n            optionsRev: 0,\n            open: false,\n            activeSourceOption: null,\n            value: this.props.value,\n        });\n\n        this.inputRef = useForwardRefToParent(\"input\");\n        this.listRef = useRef(\"sourcesList\");\n        if (this.props.autofocus) {\n            useAutofocus({ refName: \"input\" });\n        }\n        this.root = useRef(\"root\");\n\n        this.debouncedProcessInput = useDebounced(async () => {\n            const currentPromise = this.pendingPromise;\n            this.pendingPromise = null;\n            this.props.onInput({\n                inputValue: this.inputRef.el.value,\n            });\n            try {\n                await this.open(true);\n                currentPromise.resolve();\n            } catch {\n                currentPromise.reject();\n            } finally {\n                if (currentPromise === this.loadingPromise) {\n                    this.loadingPromise = null;\n                }\n            }\n        }, this.timeout);\n\n        useExternalListener(window, \"scroll\", this.externalClose, true);\n        useExternalListener(window, \"pointerdown\", this.externalClose, true);\n        useExternalListener(window, \"mousemove\", () => (this.mouseSelectionActive = true), true);\n\n        this.hotkey = useService(\"hotkey\");\n        this.hotkeysToRemove = [];\n\n        onWillUpdateProps((nextProps) => {\n            if (this.props.value !== nextProps.value || this.forceValFromProp) {\n                this.forceValFromProp = false;\n                if (!this.inEdition) {\n                    this.state.value = nextProps.value;\n                    this.inputRef.el.value = nextProps.value;\n                }\n                this.close();\n            }\n        });\n\n        // position and size\n        if (this.props.dropdown) {\n            usePosition(\"sourcesList\", () => this.targetDropdown, this.dropdownOptions);\n        } else {\n            this.open(false);\n        }\n    }\n\n    get targetDropdown() {\n        return this.inputRef.el;\n    }\n\n    get activeSourceOptionId() {\n        if (!this.isOpened || !this.state.activeSourceOption) {\n            return undefined;\n        }\n        const [sourceIndex, optionIndex] = this.state.activeSourceOption;\n        const source = this.sources[sourceIndex];\n        return `${this.props.id || \"autocomplete\"}_${sourceIndex}_${\n            source.isLoading ? \"loading\" : optionIndex\n        }`;\n    }\n\n    get dropdownOptions() {\n        return {\n            position: \"bottom-start\",\n            ...this.props.menuPositionOptions,\n        };\n    }\n\n    get isOpened() {\n        return this.state.open;\n    }\n\n    get hasOptions() {\n        for (const source of this.sources) {\n            if (source.isLoading || source.options.length) {\n                return true;\n            }\n        }\n        return false;\n    }\n\n    get activeOption() {\n        if (!this.state.activeSourceOption) {\n            return null;\n        }\n        const [sourceIndex, optionIndex] = this.state.activeSourceOption;\n        return this.sources[sourceIndex].options[optionIndex];\n    }\n\n    open(useInput = false) {\n        this.state.open = true;\n        return this.loadSources(useInput);\n    }\n\n    close() {\n        this.state.open = false;\n        this.state.activeSourceOption = null;\n        this.mouseSelectionActive = false;\n    }\n\n    cancel() {\n        if (this.inputRef.el.value.length) {\n            if (this.props.autoSelect) {\n                this.inputRef.el.value = this.props.value;\n                this.props.onCancel();\n            }\n        }\n        this.close();\n    }\n\n    async loadSources(useInput) {\n        this.sources = [];\n        this.state.activeSourceOption = null;\n        const proms = [];\n        for (const pSource of this.props.sources) {\n            const source = this.makeSource(pSource);\n            this.sources.push(source);\n\n            const options = this.loadOptions(\n                pSource.options,\n                useInput ? this.inputRef.el.value.trim() : \"\"\n            );\n            if (options instanceof Promise) {\n                source.isLoading = true;\n                const prom = options.then((options) => {\n                    source.options = options.map((option) => this.makeOption(option));\n                    source.isLoading = false;\n                    this.state.optionsRev++;\n                });\n                proms.push(prom);\n            } else {\n                source.options = options.map((option) => this.makeOption(option));\n            }\n        }\n\n        await Promise.all(proms);\n        this.navigate(0);\n        this.scroll();\n    }\n    get displayOptions() {\n        return !this.props.dropdown || (this.isOpened && this.hasOptions);\n    }\n    loadOptions(options, request) {\n        if (typeof options === \"function\") {\n            return options(request);\n        } else {\n            return options;\n        }\n    }\n    makeOption(option) {\n        return {\n            cssClass: \"\",\n            data: {},\n            ...option,\n            id: ++this.nextOptionId,\n            unselectable: !option.onSelect,\n        };\n    }\n    makeSource(source) {\n        return {\n            id: ++this.nextSourceId,\n            options: [],\n            isLoading: false,\n            placeholder: source.placeholder,\n            optionSlot: source.optionSlot,\n        };\n    }\n\n    isActiveSourceOption([sourceIndex, optionIndex]) {\n        return (\n            this.state.activeSourceOption &&\n            this.state.activeSourceOption[0] === sourceIndex &&\n            this.state.activeSourceOption[1] === optionIndex\n        );\n    }\n\n    selectOption(option) {\n        this.inEdition = false;\n        if (option.unselectable) {\n            return;\n        }\n\n        if (this.props.resetOnSelect) {\n            this.inputRef.el.value = \"\";\n        }\n        this.isOptionSelected = true;\n        this.forceValFromProp = true;\n        option.onSelect();\n        this.close();\n    }\n\n    navigate(direction) {\n        let step = Math.sign(direction);\n        if (!step) {\n            this.state.activeSourceOption = null;\n            step = 1;\n        } else {\n            this.state.navigationRev++;\n        }\n\n        do {\n            if (this.state.activeSourceOption) {\n                let [sourceIndex, optionIndex] = this.state.activeSourceOption;\n                let source = this.sources[sourceIndex];\n\n                optionIndex += step;\n                if (0 > optionIndex || optionIndex >= source.options.length) {\n                    sourceIndex += step;\n                    source = this.sources[sourceIndex];\n\n                    while (source && source.isLoading) {\n                        sourceIndex += step;\n                        source = this.sources[sourceIndex];\n                    }\n\n                    if (source) {\n                        optionIndex = step < 0 ? source.options.length - 1 : 0;\n                    }\n                }\n\n                this.state.activeSourceOption = source ? [sourceIndex, optionIndex] : null;\n            } else {\n                let sourceIndex = step < 0 ? this.sources.length - 1 : 0;\n                let source = this.sources[sourceIndex];\n\n                while (source && source.isLoading) {\n                    sourceIndex += step;\n                    source = this.sources[sourceIndex];\n                }\n\n                if (source) {\n                    const optionIndex = step < 0 ? source.options.length - 1 : 0;\n                    if (optionIndex < source.options.length) {\n                        this.state.activeSourceOption = [sourceIndex, optionIndex];\n                    }\n                }\n            }\n        } while (this.activeOption?.unselectable);\n    }\n\n    onInputBlur() {\n        if (this.ignoreBlur) {\n            this.ignoreBlur = false;\n            return;\n        }\n        // If selectOnBlur is true, we select the first element\n        // of the autocomplete suggestions list, if this element exists\n        if (this.props.selectOnBlur && !this.isOptionSelected && this.sources[0]) {\n            const firstOption = this.sources[0].options[0];\n            if (firstOption) {\n                this.state.activeSourceOption = firstOption.unselectable ? null : [0, 0];\n                this.selectOption(this.activeOption);\n            }\n        }\n        this.props.onBlur({\n            inputValue: this.inputRef.el.value,\n        });\n        this.inEdition = false;\n        this.isOptionSelected = false;\n    }\n    onInputClick() {\n        if (!this.isOpened && this.props.searchOnInputClick) {\n            this.open(this.inputRef.el.value.trim() !== this.props.value.trim());\n        } else {\n            this.close();\n        }\n    }\n    onInputChange(ev) {\n        if (this.ignoreBlur) {\n            ev.stopImmediatePropagation();\n        }\n        this.props.onChange({\n            inputValue: this.inputRef.el.value,\n        });\n    }\n    async onInput() {\n        this.inEdition = true;\n        this.pendingPromise = this.pendingPromise || new Deferred();\n        this.loadingPromise = this.pendingPromise;\n        this.debouncedProcessInput();\n    }\n\n    onInputFocus(ev) {\n        this.inputRef.el.setSelectionRange(0, this.inputRef.el.value.length);\n        this.props.onFocus(ev);\n    }\n\n    get autoCompleteRootClass() {\n        let classList = \"\";\n        if (this.props.class) {\n            classList += this.props.class;\n        }\n        if (this.props.dropdown) {\n            classList += \" dropdown\";\n        }\n        return classList;\n    }\n\n    get ulDropdownClass() {\n        return mergeClasses(this.props.menuCssClass, {\n            \"dropdown-menu ui-autocomplete\": this.props.dropdown,\n            \"list-group\": !this.props.dropdown,\n        });\n    }\n\n    async onInputKeydown(ev) {\n        const hotkey = getActiveHotkey(ev);\n        const isSelectKey = hotkey === \"enter\" || hotkey === \"tab\";\n\n        if (this.loadingPromise && isSelectKey) {\n            if (hotkey === \"enter\") {\n                ev.stopPropagation();\n                ev.preventDefault();\n            }\n\n            await this.loadingPromise;\n        }\n\n        switch (hotkey) {\n            case \"enter\":\n                if (!this.isOpened || !this.state.activeSourceOption) {\n                    return;\n                }\n                this.selectOption(this.activeOption);\n                break;\n            case \"escape\":\n                if (!this.isOpened) {\n                    return;\n                }\n                this.cancel();\n                break;\n            case \"tab\":\n            case \"shift+tab\":\n                if (!this.isOpened) {\n                    return;\n                }\n                if (\n                    this.props.autoSelect &&\n                    this.state.activeSourceOption &&\n                    (this.state.navigationRev > 0 || this.inputRef.el.value.length > 0)\n                ) {\n                    this.selectOption(this.activeOption);\n                }\n                this.close();\n                return;\n            case \"arrowup\":\n                this.navigate(-1);\n                if (!this.isOpened) {\n                    this.open(true);\n                }\n                this.scroll();\n                break;\n            case \"arrowdown\":\n                this.navigate(+1);\n                if (!this.isOpened) {\n                    this.open(true);\n                }\n                this.scroll();\n                break;\n            default:\n                return;\n        }\n\n        ev.stopPropagation();\n        ev.preventDefault();\n    }\n\n    onOptionMouseEnter(indices) {\n        if (!this.mouseSelectionActive) {\n            return;\n        }\n\n        const [sourceIndex, optionIndex] = indices;\n        if (this.sources[sourceIndex].options[optionIndex]?.unselectable) {\n            this.state.activeSourceOption = null;\n        } else {\n            this.state.activeSourceOption = indices;\n        }\n    }\n    onOptionMouseLeave() {\n        this.state.activeSourceOption = null;\n    }\n    onOptionClick(option) {\n        this.selectOption(option);\n        this.inputRef.el.focus();\n    }\n    onOptionPointerDown(option, ev) {\n        this.ignoreBlur = true;\n        if (option.unselectable) {\n            ev.preventDefault();\n        }\n    }\n\n    externalClose(ev) {\n        if (this.isOpened && !this.root.el.contains(ev.target)) {\n            this.cancel();\n        }\n    }\n\n    scroll() {\n        if (!this.activeSourceOptionId) {\n            return;\n        }\n        if (isScrollableY(this.listRef.el)) {\n            scrollTo(this.listRef.el.querySelector(`#${this.activeSourceOptionId}`));\n        }\n    }\n}\n", "/**\n * Builder for BarcodeDetector-like polyfill class using ZXing library.\n *\n * @param {ZXing} ZXing Zxing library\n * @returns {class} ZxingBarcodeDetector class\n */\nexport function buildZXingBarcodeDetector(ZXing) {\n    const ZXingFormats = new Map([\n        [\"aztec\", ZXing.BarcodeFormat.AZTEC],\n        [\"code_39\", ZXing.BarcodeFormat.CODE_39],\n        [\"code_128\", ZXing.BarcodeFormat.CODE_128],\n        [\"data_matrix\", ZXing.BarcodeFormat.DATA_MATRIX],\n        [\"ean_8\", ZXing.BarcodeFormat.EAN_8],\n        [\"ean_13\", ZXing.BarcodeFormat.EAN_13],\n        [\"itf\", ZXing.BarcodeFormat.ITF],\n        [\"pdf417\", ZXing.BarcodeFormat.PDF_417],\n        [\"qr_code\", ZXing.BarcodeFormat.QR_CODE],\n        [\"upc_a\", ZXing.BarcodeFormat.UPC_A],\n        [\"upc_e\", ZXing.BarcodeFormat.UPC_E],\n    ]);\n\n    const allSupportedFormats = Array.from(ZXingFormats.keys());\n\n    /**\n     * ZXingBarcodeDetector class\n     *\n     * BarcodeDetector-like polyfill class using ZXing library.\n     * API follows the Shape Detection Web API (specifically Barcode Detection).\n     */\n    class ZXingBarcodeDetector {\n        /**\n         * @param {object} opts\n         * @param {Array} opts.formats list of codes' formats to detect\n         */\n        constructor(opts = {}) {\n            const formats = opts.formats || allSupportedFormats;\n            const hints = new Map([\n                [\n                    ZXing.DecodeHintType.POSSIBLE_FORMATS,\n                    formats.map((format) => ZXingFormats.get(format)),\n                ],\n                // Enable Scanning at 90 degrees rotation\n                // https://github.com/zxing-js/library/issues/291\n                [ZXing.DecodeHintType.TRY_HARDER, true],\n            ]);\n            this.reader = new ZXing.MultiFormatReader();\n            this.reader.setHints(hints);\n        }\n\n        /**\n         * Detect codes in image.\n         *\n         * @param {HTMLVideoElement} video source video element\n         * @returns {Promise<Array>} array of detected codes\n         */\n        async detect(video) {\n            if (!(video instanceof HTMLVideoElement)) {\n                throw new DOMException(\n                    \"imageDataFrom() requires an HTMLVideoElement\",\n                    \"InvalidArgumentError\"\n                );\n            }\n            if (!isVideoElementReady(video)) {\n                throw new DOMException(\"HTMLVideoElement is not ready\", \"InvalidStateError\");\n            }\n            const canvas = document.createElement(\"canvas\");\n\n            let barcodeArea;\n            if (this.cropArea && (this.cropArea.x || this.cropArea.y)) {\n                barcodeArea = this.cropArea;\n            } else {\n                barcodeArea = {\n                    x: 0,\n                    y: 0,\n                    width: video.videoWidth,\n                    height: video.videoHeight,\n                };\n            }\n            canvas.width = barcodeArea.width;\n            canvas.height = barcodeArea.height;\n\n            const ctx = canvas.getContext(\"2d\");\n\n            ctx.drawImage(\n                video,\n                barcodeArea.x,\n                barcodeArea.y,\n                barcodeArea.width,\n                barcodeArea.height,\n                0,\n                0,\n                barcodeArea.width,\n                barcodeArea.height\n            );\n\n            const luminanceSource = new ZXing.HTMLCanvasElementLuminanceSource(canvas);\n            const binaryBitmap = new ZXing.BinaryBitmap(new ZXing.HybridBinarizer(luminanceSource));\n            try {\n                const result = this.reader.decodeWithState(binaryBitmap);\n                const { resultPoints } = result;\n                const boundingBox = DOMRectReadOnly.fromRect({\n                    x: resultPoints[0].x,\n                    y: resultPoints[0].y,\n                    height: Math.max(1, Math.abs(resultPoints[1].y - resultPoints[0].y)),\n                    width: Math.max(1, Math.abs(resultPoints[1].x - resultPoints[0].x)),\n                });\n                const cornerPoints = resultPoints;\n                const format = Array.from(ZXingFormats).find(\n                    ([k, val]) => val === result.getBarcodeFormat()\n                );\n                const rawValue = result.getText();\n                return [\n                    {\n                        boundingBox,\n                        cornerPoints,\n                        format,\n                        rawValue,\n                    },\n                ];\n            } catch (err) {\n                if (err.name === \"NotFoundException\") {\n                    return [];\n                }\n                throw err;\n            }\n        }\n\n        setCropArea(cropArea) {\n            this.cropArea = cropArea;\n        }\n    }\n\n    /**\n     * Supported codes formats\n     *\n     * @static\n     * @returns {Promise<string[]>}\n     */\n    ZXingBarcodeDetector.getSupportedFormats = async () => allSupportedFormats;\n\n    return ZXingBarcodeDetector;\n}\n\n/**\n * Check for HTMLVideoElement readiness.\n *\n * See https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/readyState\n */\nconst HAVE_NOTHING = 0;\nconst HAVE_METADATA = 1;\nexport function isVideoElementReady(video) {\n    return ![HAVE_NOTHING, HAVE_METADATA].includes(video.readyState);\n}\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { Dialog } from \"@web/core/dialog/dialog\";\nimport { Component, useState } from \"@odoo/owl\";\nimport { BarcodeVideoScanner, isBarcodeScannerSupported } from \"./barcode_video_scanner\";\n\nexport class BarcodeDialog extends Component {\n    static template = \"web.BarcodeDialog\";\n    static components = {\n        BarcodeVideoScanner,\n        Dialog,\n    };\n    static props = [\"facingMode\", \"close\", \"onResult\", \"onError\"];\n\n    setup() {\n        this.state = useState({\n            barcodeScannerSupported: isBarcodeScannerSupported(),\n            errorMessage: _t(\"Check your browser permissions\"),\n        });\n    }\n\n    /**\n     * Detection success handler\n     *\n     * @param {string} result found code\n     */\n    onResult(result) {\n        this.props.close();\n        this.props.onResult(result);\n    }\n\n    /**\n     * Detection error handler\n     *\n     * @param {Error} error\n     */\n    onError(error) {\n        this.state.barcodeScannerSupported = false;\n        this.state.errorMessage = error.message;\n    }\n}\n\n/**\n * Opens the BarcodeScanning dialog and begins code detection using the device's camera.\n *\n * @returns {Promise<string>} resolves when a {qr,bar}code has been detected\n */\nexport async function scanBarcode(env, facingMode = \"environment\") {\n    let res;\n    let rej;\n    const promise = new Promise((resolve, reject) => {\n        res = resolve;\n        rej = reject;\n    });\n    env.services.dialog.add(BarcodeDialog, {\n        facingMode,\n        onResult: (result) => res(result),\n        onError: (error) => rej(error),\n    });\n    return promise;\n}\n", "/* global BarcodeDetector */\n\nimport { browser } from \"@web/core/browser/browser\";\nimport { delay } from \"@web/core/utils/concurrency\";\nimport { loadJS } from \"@web/core/assets\";\nimport { isVideoElementReady, buildZXingBarcodeDetector } from \"./ZXingBarcodeDetector\";\nimport { CropOverlay } from \"./crop_overlay\";\nimport { Component, onMounted, onWillStart, onWillUnmount, status, useRef, useState } from \"@odoo/owl\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { pick } from \"@web/core/utils/objects\";\n\nexport class BarcodeVideoScanner extends Component {\n    static template = \"web.BarcodeVideoScanner\";\n    static components = {\n        CropOverlay,\n    };\n    static props = {\n        cssClass: { type: String, optional: true },\n        facingMode: {\n            type: String,\n            validate: (fm) => [\"environment\", \"left\", \"right\", \"user\"].includes(fm),\n        },\n        close: { type: Function, optional: true },\n        onReady: { type: Function, optional: true },\n        onResult: Function,\n        onError: Function,\n        placeholder: { type: String, optional: true },\n        delayBetweenScan: { type: Number, optional: true },\n    };\n    static defaultProps = {\n        cssClass: \"w-100 h-100\",\n    };\n    /**\n     * @override\n     */\n    setup() {\n        this.videoPreviewRef = useRef(\"videoPreview\");\n        this.detectorTimeout = null;\n        this.stream = null;\n        this.detector = null;\n        this.overlayInfo = {};\n        this.zoomRatio = 1;\n        this.scanPaused = false;\n        this.state = useState({\n            isReady: false,\n        });\n\n        onWillStart(async () => {\n            let DetectorClass;\n            // Use Barcode Detection API if available.\n            // As support is still bleeding edge (mainly Chrome on Android),\n            // also provides a fallback using ZXing library.\n            if (\"BarcodeDetector\" in window) {\n                DetectorClass = BarcodeDetector;\n            } else {\n                await loadJS(\"/web/static/lib/zxing-library/zxing-library.js\");\n                DetectorClass = buildZXingBarcodeDetector(window.ZXing);\n            }\n            const formats = await DetectorClass.getSupportedFormats();\n            this.detector = new DetectorClass({ formats });\n        });\n\n        onMounted(async () => {\n            const constraints = {\n                video: { facingMode: this.props.facingMode },\n                audio: false,\n            };\n\n            try {\n                this.stream = await browser.navigator.mediaDevices.getUserMedia(constraints);\n            } catch (err) {\n                const errors = {\n                    NotFoundError: _t(\"No device can be found.\"),\n                    NotAllowedError: _t(\"Odoo needs your authorization first.\"),\n                };\n                const errorMessage = _t(\"Could not start scanning. %(message)s\", {\n                    message: errors[err.name] || err.message,\n                });\n                this.props.onError(new Error(errorMessage));\n                return;\n            }\n            if (!this.videoPreviewRef.el) {\n                this.cleanStreamAndTimeout();\n                const errorMessage = _t(\"Barcode Video Scanner could not be mounted properly.\");\n                this.props.onError(new Error(errorMessage));\n                return;\n            }\n            this.videoPreviewRef.el.srcObject = this.stream;\n            const ready = await this.isVideoReady();\n            if (!ready) {\n                return;\n            }\n            const { height, width } = getComputedStyle(this.videoPreviewRef.el);\n            const divWidth = width.slice(0, -2);\n            const divHeight = height.slice(0, -2);\n            const tracks = this.stream.getVideoTracks();\n            if (tracks.length) {\n                const [track] = tracks;\n                const settings = track.getSettings();\n                this.zoomRatio = Math.min(divWidth / settings.width, divHeight / settings.height);\n            }\n            this.detectorTimeout = setTimeout(this.detectCode.bind(this), 100);\n        });\n\n        onWillUnmount(() => this.cleanStreamAndTimeout());\n    }\n\n    cleanStreamAndTimeout() {\n        clearTimeout(this.detectorTimeout);\n        this.detectorTimeout = null;\n        if (this.stream) {\n            this.stream.getTracks().forEach((track) => track.stop());\n            this.stream = null;\n        }\n    }\n\n    isZXingBarcodeDetector() {\n        return this.detector && this.detector.__proto__.constructor.name === \"ZXingBarcodeDetector\";\n    }\n\n    /**\n     * Check for camera preview element readiness\n     *\n     * @returns {Promise} resolves when the video element is ready\n     */\n    async isVideoReady() {\n        // FIXME: even if it shouldn't happened, a timeout could be useful here.\n        while (!isVideoElementReady(this.videoPreviewRef.el)) {\n            await delay(10);\n            if (status(this) === \"destroyed\"){\n                return false;\n            }\n        }\n        this.state.isReady = true;\n        if (this.props.onReady) {\n            this.props.onReady();\n        }\n        return true;\n    }\n\n    onResize(overlayInfo) {\n        this.overlayInfo = overlayInfo;\n        if (this.isZXingBarcodeDetector()) {\n            // TODO need refactoring when ZXing will support multiple result in one scan\n            // https://github.com/zxing-js/library/issues/346\n            this.detector.setCropArea(this.adaptValuesWithRatio(this.overlayInfo, true));\n        }\n    }\n\n    /**\n     * Attempt to detect codes in the current camera preview's frame\n     */\n    async detectCode() {\n        let barcodeDetected = false;\n        let codes = [];\n        try {\n            codes = await this.detector.detect(this.videoPreviewRef.el);\n        } catch (err) {\n            this.props.onError(err);\n        }\n        for (const code of codes) {\n            if (\n                !this.isZXingBarcodeDetector() &&\n                this.overlayInfo.x !== undefined &&\n                this.overlayInfo.y !== undefined\n            ) {\n                const { x, y, width, height } = this.adaptValuesWithRatio(code.boundingBox);\n                if (\n                    x < this.overlayInfo.x ||\n                    x + width > this.overlayInfo.x + this.overlayInfo.width ||\n                    y < this.overlayInfo.y ||\n                    y + height > this.overlayInfo.y + this.overlayInfo.height\n                ) {\n                    continue;\n                }\n            }\n            barcodeDetected = true;\n            this.barcodeDetected(code.rawValue);\n            break;\n        }\n        if (this.stream && (!barcodeDetected || !this.props.delayBetweenScan)) {\n            this.detectorTimeout = setTimeout(this.detectCode.bind(this), 100);\n        }\n    }\n\n    barcodeDetected(barcode) {\n        if (this.props.delayBetweenScan && !this.scanPaused) {\n            this.scanPaused = true;\n            this.detectorTimeout = setTimeout(() => {\n                this.scanPaused = false;\n                this.detectorTimeout = setTimeout(this.detectCode.bind(this), 100);\n            }, this.props.delayBetweenScan);\n        }\n        this.props.onResult(barcode);\n    }\n\n    adaptValuesWithRatio(domRect, dividerRatio = false) {\n        const newObject = pick(domRect, \"x\", \"y\", \"width\", \"height\");\n        for (const key of Object.keys(newObject)) {\n            if (dividerRatio) {\n                newObject[key] /= this.zoomRatio;\n            } else {\n                newObject[key] *= this.zoomRatio;\n            }\n        }\n        return newObject;\n    }\n}\n\n/**\n * Check for BarcodeScanner support\n * @returns {boolean}\n */\nexport function isBarcodeScannerSupported() {\n    return Boolean(browser.navigator.mediaDevices && browser.navigator.mediaDevices.getUserMedia);\n}\n", "import { Component, useRef, onPatched } from \"@odoo/owl\";\nimport { browser } from \"@web/core/browser/browser\";\nimport { clamp } from \"@web/core/utils/numbers\";\n\nexport class CropOverlay extends Component {\n    static template = \"web.CropOverlay\";\n    static props = {\n        onResize: Function,\n        isReady: Boolean,\n        slots: {\n            type: Object,\n            shape: {\n                default: {},\n            },\n        },\n    };\n\n    setup() {\n        this.localStorageKey = \"o-barcode-scanner-overlay\";\n        this.cropContainerRef = useRef(\"crop-container\");\n        this.isMoving = false;\n        this.boundaryOverlay = {};\n        this.relativePosition = {\n            x: 0,\n            y: 0,\n        };\n        onPatched(() => {\n            this.setupCropRect();\n        });\n    }\n\n    setupCropRect() {\n        if (!this.props.isReady) {\n            return;\n        }\n        this.computeDefaultPoint();\n        this.computeOverlayPosition();\n        this.calculateAndSetTransparentRect();\n        this.executeOnResizeCallback();\n    }\n\n    boundPoint(pointValue, boundaryRect) {\n        return {\n            x: clamp(pointValue.x, boundaryRect.left, boundaryRect.left + boundaryRect.width),\n            y: clamp(pointValue.y, boundaryRect.top, boundaryRect.top + boundaryRect.height),\n        };\n    }\n\n    calculateAndSetTransparentRect() {\n        const cropTransparentRect = this.getTransparentRec(\n            this.relativePosition,\n            this.boundaryOverlay\n        );\n        this.setCropValue(cropTransparentRect, this.relativePosition);\n    }\n\n    computeOverlayPosition() {\n        const cropOverlayElement = this.cropContainerRef.el.querySelector(\".o_crop_overlay\");\n        this.boundaryOverlay = cropOverlayElement.getBoundingClientRect();\n    }\n\n    executeOnResizeCallback() {\n        const transparentRec = this.getTransparentRec(this.relativePosition, this.boundaryOverlay);\n        browser.localStorage.setItem(this.localStorageKey, JSON.stringify(transparentRec));\n        this.props.onResize({\n            ...transparentRec,\n            width: this.boundaryOverlay.width - 2 * transparentRec.x,\n            height: this.boundaryOverlay.height - 2 * transparentRec.y,\n        });\n    }\n\n    computeDefaultPoint() {\n        const firstChildComputedStyle = getComputedStyle(this.cropContainerRef.el.firstChild);\n        const elementWidth = firstChildComputedStyle.width.slice(0, -2);\n        const elementHeight = firstChildComputedStyle.height.slice(0, -2);\n\n        const stringSavedPoint = browser.localStorage.getItem(this.localStorageKey);\n        if (stringSavedPoint) {\n            const savedPoint = JSON.parse(stringSavedPoint);\n            this.relativePosition = {\n                x: clamp(savedPoint.x, 0, elementWidth),\n                y: clamp(savedPoint.y, 0, elementHeight),\n            };\n        } else {\n            const stepWidth = elementWidth / 10;\n            const width = stepWidth * 8;\n            const height = width / 4;\n            const startY = elementHeight / 2 - height / 2;\n            this.relativePosition = {\n                x: stepWidth + width,\n                y: startY + height,\n            };\n        }\n    }\n    getTransparentRec(point, rect) {\n        const middleX = rect.width / 2;\n        const middleY = rect.height / 2;\n        const newDeltaX = Math.abs(point.x - middleX);\n        const newDeltaY = Math.abs(point.y - middleY);\n        return {\n            x: middleX - newDeltaX,\n            y: middleY - newDeltaY,\n        };\n    }\n\n    setCropValue(point, iconPoint) {\n        if (!iconPoint) {\n            iconPoint = point;\n        }\n        this.cropContainerRef.el.style.setProperty(\"--o-crop-x\", `${point.x}px`);\n        this.cropContainerRef.el.style.setProperty(\"--o-crop-y\", `${point.y}px`);\n        this.cropContainerRef.el.style.setProperty(\"--o-crop-icon-x\", `${iconPoint.x}px`);\n        this.cropContainerRef.el.style.setProperty(\"--o-crop-icon-y\", `${iconPoint.y}px`);\n    }\n\n    pointerDown(event) {\n        event.preventDefault();\n        if (event.target.matches(\".o_crop_icon\")) {\n            this.computeOverlayPosition();\n            this.isMoving = true;\n        }\n    }\n\n    pointerMove(event) {\n        if (!this.isMoving) {\n            return;\n        }\n        let eventPosition;\n        if (event.touches && event.touches.length) {\n            eventPosition = event.touches[0];\n        } else {\n            eventPosition = event;\n        }\n        const { clientX, clientY } = eventPosition;\n        const restrictedPosition = this.boundPoint(\n            {\n                x: clientX,\n                y: clientY,\n            },\n            this.boundaryOverlay\n        );\n        this.relativePosition = {\n            x: restrictedPosition.x - this.boundaryOverlay.left,\n            y: restrictedPosition.y - this.boundaryOverlay.top,\n        };\n        this.calculateAndSetTransparentRect(this.relativePosition);\n    }\n\n    pointerUp(event) {\n        this.isMoving = false;\n        this.executeOnResizeCallback();\n    }\n}\n", "/**\n * BottomSheet\n *\n * @class\n */\nimport { Component, useState, useRef, onMounted, useExternalListener } from \"@odoo/owl\";\nimport { useHotkey } from \"@web/core/hotkeys/hotkey_hook\";\nimport { useForwardRefToParent } from \"@web/core/utils/hooks\";\nimport { useThrottleForAnimation } from \"@web/core/utils/timing\";\nimport { compensateScrollbar } from \"@web/core/utils/scrolling\";\nimport { getViewportDimensions, useViewportChange } from \"@web/core/utils/dvu\";\nimport { clamp } from \"@web/core/utils/numbers\";\nimport { browser } from \"@web/core/browser/browser\";\n\nexport class BottomSheet extends Component {\n    static template = \"web.BottomSheet\";\n\n    static defaultProps = {\n        class: \"\",\n    };\n\n    static props = {\n        // Main props\n        component: { type: Function },\n        componentProps: { optional: true, type: Object },\n        close: { type: Function },\n\n        class: { optional: true },\n        role: { optional: true, type: String },\n\n        // Technical props\n        ref: { optional: true, type: Function },\n        slots: { optional: true, type: Object },\n    };\n\n    setup() {\n        this.maxHeightPercent = 90;\n\n        this.state = useState({\n            isPositionedReady: false, // Sheet is ready for display\n            isSnappingEnabled: false,\n            isDismissing: false, // Sheet is being dismissed\n            progress: 0, // Visual progress (0-1)\n        });\n\n        // Measurements and configuration\n        this.measurements = {\n            viewportHeight: 0,\n            naturalHeight: 0,\n            maxHeight: 0,\n            dismissThreshold: 0,\n        };\n\n        // Popover Ref Requirement\n        useForwardRefToParent(\"ref\");\n\n        // References\n        this.containerRef = useRef(\"container\");\n        this.scrollRailRef = useRef(\"scrollRail\");\n        this.sheetRef = useRef(\"sheet\");\n        this.sheetBodyRef = useRef(\"ref\");\n\n        // Create throttled version for onScroll\n        this.throttledOnScroll = useThrottleForAnimation(this.onScroll.bind(this));\n\n        // Adapt dimensions when mobile virtual-keyboards or browsers bars toggle\n        useViewportChange(() => {\n            if (this.state.isPositionedReady && !this.state.isDismissing) {\n                this.updateDimensions();\n            }\n        });\n\n        // Handle \"ESC\" key press.\n        useHotkey(\"escape\", () => this.slideOut());\n\n        // Handle mobile \"back\" gesture and \"back\" navigation button.\n        // Push a history state when the BottomSheet opens, intercept the browser's\n        // history events, prevents navigation by pushing another state and closes the sheet.\n        window.history.pushState({ bottomSheet: true }, \"\");\n        this.handlePopState = () => {\n            if (this.state.isPositionedReady && !this.state.isDismissing) {\n                window.history.pushState({ bottomSheet: true }, \"\");\n                this.slideOut();\n            }\n        };\n        useExternalListener(window, \"popstate\", this.handlePopState);\n\n        onMounted(() => {\n            const isReduced =\n                browser.matchMedia(`(prefers-reduced-motion: reduce)`) === true ||\n                browser.matchMedia(`(prefers-reduced-motion: reduce)`).matches === true;\n\n            this.prefersReducedMotion =\n                isReduced || getComputedStyle(this.containerRef.el).animationName === \"none\";\n\n            this.initializeSheet();\n            compensateScrollbar(this.scrollRailRef.el, true, true, \"padding-right\");\n        });\n    }\n\n    /**\n     * Main initialization method for the sheet\n     * Sets up measurements, snap points, and event handlers\n     */\n    initializeSheet() {\n        if (!this.containerRef.el || !this.scrollRailRef.el || !this.sheetRef.el) {\n            return;\n        }\n\n        // Step 1: Take measurements\n        this.measureDimensions();\n\n        // Step 2: Apply Dimensions\n        this.applyDimensions();\n\n        // Step 3: Set initial position\n        this.positionSheet();\n\n        // Step 4: Setup event handlers after everything has been properly resized and positioned\n        this.setupEventHandlers();\n\n        // Step 5: Mark as ready\n        this.state.isPositionedReady = true;\n\n        if (this.prefersReducedMotion) {\n            this.state.isSnappingEnabled = true;\n        } else {\n            this.sheetRef.el?.addEventListener(\n                \"animationend\",\n                () => (this.state.isSnappingEnabled = true),\n                {\n                    once: true,\n                }\n            );\n            this.sheetRef.el?.addEventListener(\n                \"animationcancel\",\n                () => (this.state.isSnappingEnabled = true),\n                {\n                    once: true,\n                }\n            );\n        }\n    }\n\n    /**\n     * Updates dimensions when viewport changes\n     * Recalculates measurements and snap points while preserving extended state\n     */\n    updateDimensions() {\n        // Temporarily disable snapping during update\n        this.state.isSnappingEnabled = false;\n\n        // Update measurements with new viewport dimensions\n        this.measureDimensions();\n        this.applyDimensions();\n\n        // // Update scroll position\n        const scrollTop = this.scrollRailRef.el.scrollTop;\n\n        // Update progress value\n        this.updateProgressValue(scrollTop);\n    }\n\n    /**\n     * Takes measurements of viewport and sheet dimensions\n     * Calculates natural height and other key measurements\n     */\n    measureDimensions() {\n        const viewportHeight = getViewportDimensions().height;\n\n        // Calculate heights based on percentages\n        const maxHeightPx = (this.maxHeightPercent / 100) * viewportHeight;\n\n        // Reset any previously set constraints to measure natural height\n        const sheet = this.sheetRef.el;\n        sheet.style.removeProperty(\"min-height\");\n        sheet.style.removeProperty(\"height\");\n\n        const naturalHeight = sheet.offsetHeight;\n        const initialHeightPx = Math.min(naturalHeight, maxHeightPx);\n\n        // Store all measurements\n        this.measurements = {\n            viewportHeight,\n            naturalHeight,\n            initialHeight: initialHeightPx,\n            maxHeight: maxHeightPx,\n            dismissThreshold: Math.min(initialHeightPx * 0.3, 100),\n        };\n    }\n\n    /**\n     * Applies calculated dimensions to the DOM elements\n     * Sets CSS variables and styles based on measurements and snap points\n     */\n    applyDimensions() {\n        const rail = this.scrollRailRef.el;\n\n        // Convert heights to dvh percentages for CSS variables\n        const heightPercent = Math.min(\n            (this.measurements.initialHeight / this.measurements.viewportHeight) * 100,\n            this.maxHeightPercent\n        );\n\n        // Set CSS variables for heights\n        rail.style.setProperty(\"--sheet-height\", `${heightPercent}dvh`);\n        rail.style.setProperty(\"--sheet-max-height\", `${this.measurements.viewportHeight}px`);\n        rail.style.setProperty(\"--dismiss-height\", `${this.measurements.initialHeight || 0}px`);\n    }\n\n    /**\n     * Sets the initial position of the sheet\n     * Configures initial scroll position and overflow behavior\n     */\n    positionSheet() {\n        const scrollRail = this.scrollRailRef.el;\n        const bodyContent = this.sheetBodyRef.el;\n\n        const scrollValue = this.measurements.maxHeight;\n\n        // Configure body content overflow\n        if (bodyContent) {\n            bodyContent.style.overflowY = \"auto\";\n        }\n\n        // Set scroll position\n        scrollRail.scrollTop = scrollValue || 0;\n        scrollRail.style.containerType = \"scroll-state size\";\n    }\n\n    /**\n     * Sets up event handlers for scroll and touch events\n     */\n    setupEventHandlers() {\n        const scrollRail = this.scrollRailRef.el;\n\n        // Add scroll event listener\n        scrollRail.addEventListener(\"scroll\", this.throttledOnScroll);\n    }\n\n    /**\n     * Handles scroll events on the rail element\n     * Updates progress, handles position snapping, and triggers dismissal\n     */\n    onScroll() {\n        if (!this.scrollRailRef.el) {\n            return;\n        }\n\n        const scrollTop = this.scrollRailRef.el.scrollTop;\n\n        // Update progress value for visual effects\n        this.updateProgressValue(scrollTop);\n\n        // Check for dismissal condition\n        if (scrollTop < this.measurements.dismissThreshold) {\n            this.slideOut();\n        }\n    }\n\n    /**\n     * Calculates and updates the progress value based on scroll position\n     *\n     * @param {number} scrollTop - Current scroll position\n     */\n    updateProgressValue(scrollTop) {\n        const initialPosition = this.measurements.naturalHeight;\n        const progress = clamp(scrollTop / initialPosition, 0, 1);\n\n        if (Math.abs(this.state.progress - progress) > 0.01) {\n            this.state.progress = progress;\n        }\n    }\n\n    /**\n     * Initiates the slide out animation and dismissal\n     */\n    slideOut() {\n        // Prevent duplicate calls\n        if (this.state.isDismissing) {\n            return;\n        }\n\n        if (this.prefersReducedMotion) {\n            this.props.close?.();\n        } else {\n            this.sheetRef.el?.addEventListener(\"animationend\", () => this.props.close?.(), {\n                once: true,\n            });\n            this.sheetRef.el?.addEventListener(\"animationcancel\", () => this.props.close?.(), {\n                once: true,\n            });\n        }\n\n        // Update state to trigger animation\n        this.state.isDismissing = true;\n        this.state.isSnappingEnabled = false;\n    }\n\n    /**\n     * Closes the sheet (public API)\n     */\n    close() {\n        this.slideOut();\n    }\n\n    /**\n     * Handles back button press (public API)\n     */\n    back() {\n        if (this.props.onBack) {\n            this.props.onBack();\n        } else {\n            this.slideOut();\n        }\n    }\n}\n", "import { markRaw } from \"@odoo/owl\";\nimport { BottomSheet } from \"@web/core/bottom_sheet/bottom_sheet\";\nimport { registry } from \"@web/core/registry\";\n\n/**\n * @typedef {{\n *   env?: object;\n *   onClose?: () => void;\n *   class?: string;\n *   role?: string;\n *   ref?: Function;\n *   useBottomSheet?: Boolean;\n * }} PopoverServiceAddOptions\n *\n * @typedef {ReturnType<popoverService[\"start\"]>[\"add\"]} PopoverServiceAddFunction\n */\n\nexport const popoverService = {\n    dependencies: [\"overlay\"],\n    start(_, { overlay }) {\n        let bottomSheetCount = 0;\n        /**\n         * Signals the manager to add a popover.\n         *\n         * @param {HTMLElement} target\n         * @param {typeof import(\"@odoo/owl\").Component} component\n         * @param {object} [props]\n         * @param {PopoverServiceAddOptions} [options]\n         * @returns {() => void}\n         */\n        const add = (target, component, props = {}, options = {}) => {\n            function removeAndUpdateCount() {\n                _remove();\n                bottomSheetCount--;\n                if (bottomSheetCount === 0) {\n                    document.body.classList.remove(\"bottom-sheet-open\");\n                } else if (bottomSheetCount === 1) {\n                    document.body.classList.remove(\"bottom-sheet-open-multiple\");\n                }\n            }\n            const _remove = overlay.add(\n                BottomSheet,\n                {\n                    close: removeAndUpdateCount,\n                    component,\n                    componentProps: markRaw(props),\n                    ref: options.ref,\n                    class: options.class,\n                    role: options.role,\n                },\n                {\n                    env: options.env,\n                    onRemove: options.onClose,\n                    rootId: target.getRootNode()?.host?.id,\n                }\n            );\n            bottomSheetCount++;\n            if (bottomSheetCount === 1) {\n                document.body.classList.add(\"bottom-sheet-open\");\n            } else if (bottomSheetCount > 1) {\n                document.body.classList.add(\"bottom-sheet-open-multiple\");\n            }\n\n            return removeAndUpdateCount;\n        };\n\n        return { add };\n    },\n};\n\nregistry.category(\"services\").add(\"bottom_sheet\", popoverService);\n", "/**\n * Browser\n *\n * This file exports an object containing common browser API. It may not look\n * incredibly useful, but it is very convenient when one needs to test code using\n * these methods. With this indirection, it is possible to patch the browser\n * object for a test.\n */\n\nlet sessionStorage;\nlet localStorage;\ntry {\n    sessionStorage = window.sessionStorage;\n    localStorage = window.localStorage;\n    // Safari crashes in Private Browsing\n    localStorage.setItem(\"__localStorage__\", \"true\");\n    localStorage.removeItem(\"__localStorage__\");\n} catch {\n    localStorage = makeRAMLocalStorage();\n    sessionStorage = makeRAMLocalStorage();\n}\n\nexport const browser = {\n    addEventListener: window.addEventListener.bind(window),\n    dispatchEvent: window.dispatchEvent.bind(window),\n    AnalyserNode: window.AnalyserNode,\n    Audio: window.Audio,\n    AudioBufferSourceNode: window.AudioBufferSourceNode,\n    AudioContext: window.AudioContext,\n    AudioWorkletNode: window.AudioWorkletNode,\n    BeforeInstallPromptEvent: window.BeforeInstallPromptEvent?.bind(window),\n    GainNode: window.GainNode,\n    MediaStreamAudioSourceNode: window.MediaStreamAudioSourceNode,\n    removeEventListener: window.removeEventListener.bind(window),\n    setTimeout: window.setTimeout.bind(window),\n    clearTimeout: window.clearTimeout.bind(window),\n    setInterval: window.setInterval.bind(window),\n    clearInterval: window.clearInterval.bind(window),\n    performance: window.performance,\n    requestAnimationFrame: window.requestAnimationFrame.bind(window),\n    cancelAnimationFrame: window.cancelAnimationFrame.bind(window),\n    console: window.console,\n    history: window.history,\n    matchMedia: window.matchMedia.bind(window),\n    navigator,\n    Notification: window.Notification,\n    open: window.open.bind(window),\n    SharedWorker: window.SharedWorker,\n    Worker: window.Worker,\n    XMLHttpRequest: window.XMLHttpRequest,\n    localStorage,\n    sessionStorage,\n    fetch: window.fetch.bind(window),\n    innerHeight: window.innerHeight,\n    innerWidth: window.innerWidth,\n    ontouchstart: window.ontouchstart,\n    BroadcastChannel: window.BroadcastChannel,\n    visualViewport: window.visualViewport,\n};\n\nObject.defineProperty(browser, \"location\", {\n    set(val) {\n        window.location = val;\n    },\n    get() {\n        return window.location;\n    },\n    configurable: true,\n});\n\nObject.defineProperty(browser, \"innerHeight\", {\n    get: () => window.innerHeight,\n    configurable: true,\n});\nObject.defineProperty(browser, \"innerWidth\", {\n    get: () => window.innerWidth,\n    configurable: true,\n});\n\n// -----------------------------------------------------------------------------\n// memory localStorage\n// -----------------------------------------------------------------------------\n\n/**\n * @returns {typeof window[\"localStorage\"]}\n */\nexport function makeRAMLocalStorage() {\n    let store = {};\n    return {\n        setItem(key, value) {\n            const newValue = String(value);\n            store[key] = newValue;\n            window.dispatchEvent(new StorageEvent(\"storage\", { key, newValue }));\n        },\n        getItem(key) {\n            return store[key] ?? null;\n        },\n        clear() {\n            store = {};\n        },\n        removeItem(key) {\n            delete store[key];\n            window.dispatchEvent(new StorageEvent(\"storage\", { key, newValue: null }));\n        },\n        get length() {\n            return Object.keys(store).length;\n        },\n        key() {\n            return \"\";\n        },\n    };\n}\n", "/**\n * Utils to make use of document.cookie\n * https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies\n * As recommended, storage should not be done by the cookie\n * but with localStorage/sessionStorage\n */\n\nconst COOKIE_TTL = 24 * 60 * 60 * 365;\n\nexport const cookie = {\n    get _cookieMonster() {\n        return document.cookie;\n    },\n    set _cookieMonster(value) {\n        document.cookie = value;\n    },\n    get(str) {\n        const parts = this._cookieMonster.split(\"; \");\n        for (const part of parts) {\n            const [key, value] = part.split(/=(.*)/);\n            if (key === str) {\n                return value || \"\";\n            }\n        }\n    },\n    set(key, value, ttl = COOKIE_TTL) {\n        let fullCookie = [];\n        if (value !== undefined) {\n            fullCookie.push(`${key}=${value}`);\n        }\n        fullCookie = fullCookie.concat([\"path=/\", `max-age=${Math.floor(ttl)}`]);\n        this._cookieMonster = fullCookie.join(\"; \");\n    },\n    delete(key) {\n        this.set(key, \"kill\", 0);\n    },\n};\n", "import { browser } from \"./browser\";\n\n// -----------------------------------------------------------------------------\n// Feature detection\n// -----------------------------------------------------------------------------\n\n/**\n * True if the browser is based on Chromium (Google Chrome, Opera, Edge).\n */\nexport function isBrowserChrome() {\n    return /Chrome/i.test(browser.navigator.userAgent);\n}\n\nexport function isBrowserFirefox() {\n    return /Firefox/i.test(browser.navigator.userAgent);\n}\n\n/**\n * True if the browser is Microsoft Edge.\n */\nexport function isBrowserMicrosoftEdge() {\n    return /Edg/i.test(browser.navigator.userAgent);\n}\n\n/**\n * true if the browser is based on Safari (Safari, Epiphany)\n *\n * @returns {boolean}\n */\nexport function isBrowserSafari() {\n    return !isBrowserChrome() && browser.navigator.userAgent?.includes(\"Safari\");\n}\n\nexport function isAndroid() {\n    return /Android/i.test(browser.navigator.userAgent);\n}\n\nexport function isIOS() {\n    let isIOSPlatform = false;\n    if (\"platform\" in browser.navigator) {\n        isIOSPlatform = browser.navigator.platform === \"MacIntel\";\n    }\n    return (\n        /(iPad|iPhone|iPod)/i.test(browser.navigator.userAgent) ||\n        (isIOSPlatform && maxTouchPoints() > 1)\n    );\n}\n\nexport function isOtherMobileOS() {\n    return /(webOS|BlackBerry|Windows Phone)/i.test(browser.navigator.userAgent);\n}\n\nexport function isMacOS() {\n    return /Mac/i.test(browser.navigator.userAgent);\n}\n\nexport function isMobileOS() {\n    return isAndroid() || isIOS() || isOtherMobileOS();\n}\n\nexport function isIosApp() {\n    return /OdooMobile \\(iOS\\)/i.test(browser.navigator.userAgent);\n}\n\nexport function isAndroidApp() {\n    return /OdooMobile.+Android/i.test(browser.navigator.userAgent);\n}\n\nexport function isDisplayStandalone() {\n    return browser.matchMedia(\"(display-mode: standalone)\").matches;\n}\n\nexport function hasTouch() {\n    return browser.ontouchstart !== undefined || browser.matchMedia(\"(pointer:coarse)\").matches;\n}\n\nexport function maxTouchPoints() {\n    return browser.navigator.maxTouchPoints || 1;\n}\n\nexport function isVirtualKeyboardSupported() {\n    return \"virtualKeyboard\" in browser.navigator;\n}\n", "import { EventBus } from \"@odoo/owl\";\nimport { omit, pick } from \"../utils/objects\";\nimport { compareUrls, objectToUrlEncodedString } from \"../utils/urls\";\nimport { browser } from \"./browser\";\nimport { isDisplayStandalone } from \"@web/core/browser/feature_detection\";\nimport { slidingWindow } from \"@web/core/utils/arrays\";\nimport { isNumeric } from \"@web/core/utils/strings\";\n\n// Keys that are serialized in the URL as path segments instead of query string\nexport const PATH_KEYS = [\"resId\", \"action\", \"active_id\", \"model\"];\n\nexport const routerBus = new EventBus();\n\nfunction isScopedApp() {\n    return browser.location.href.includes(\"/scoped_app\") && isDisplayStandalone();\n}\n\n/**\n * Casts the given string to a number if possible.\n *\n * @param {string} value\n * @returns {string|number}\n */\nfunction cast(value) {\n    return !value || isNaN(value) ? value : Number(value);\n}\n\n/**\n * @typedef {{ [key: string]: string }} Query\n * @typedef {{ [key: string]: any }} Route\n */\n\nfunction parseString(str) {\n    const parts = str.split(\"&\");\n    const result = {};\n    for (const part of parts) {\n        const [key, value] = part.split(\"=\");\n        const decoded = decodeURIComponent(value || \"\");\n        result[key] = cast(decoded);\n    }\n    return result;\n}\n/**\n * @param {object} values An object with the values of the new state\n * @param {boolean} replace whether the values should replace the state or be\n *  layered on top of the current state\n * @returns {object} the next state of the router\n */\nfunction computeNextState(values, replace) {\n    const nextState = replace ? pick(state, ..._lockedKeys) : { ...state };\n    Object.assign(nextState, values);\n    // Update last entry in the actionStack\n    if (nextState.actionStack?.length) {\n        Object.assign(nextState.actionStack.at(-1), pick(nextState, ...PATH_KEYS));\n    }\n    return sanitizeSearch(nextState);\n}\n\nfunction sanitize(obj, valueToRemove) {\n    return Object.fromEntries(\n        Object.entries(obj)\n            .filter(([, v]) => v !== valueToRemove)\n            .map(([k, v]) => [k, cast(v)])\n    );\n}\n\nfunction sanitizeSearch(search) {\n    return sanitize(search);\n}\n\nfunction sanitizeHash(hash) {\n    return sanitize(hash, \"\");\n}\n\n/**\n * @param {string} hash\n * @returns {any}\n */\nexport function parseHash(hash) {\n    return hash && hash !== \"#\" ? parseString(hash.slice(1)) : {};\n}\n\n/**\n * @param {string} search\n * @returns {any}\n */\nexport function parseSearchQuery(search) {\n    return search ? parseString(search.slice(1)) : {};\n}\n\nfunction pathFromActionState(state) {\n    const path = [];\n    const { action, model, active_id, resId } = state;\n    if (active_id && typeof active_id === \"number\") {\n        path.push(active_id);\n    }\n    if (action) {\n        if (typeof action === \"number\" || action.includes(\".\")) {\n            path.push(`action-${action}`);\n        } else {\n            path.push(action);\n        }\n    } else if (model) {\n        if (model.includes(\".\")) {\n            path.push(model);\n        } else {\n            // A few models don't have a dot at all, we need to distinguish\n            // them from action paths (eg: website)\n            path.push(`m-${model}`);\n        }\n    }\n    if (resId && (typeof resId === \"number\" || resId === \"new\")) {\n        path.push(resId);\n    }\n    return path.join(\"/\");\n}\n\nexport function startUrl() {\n    return isScopedApp() ? \"scoped_app\" : \"odoo\";\n}\n\n/**\n * @param {{ [key: string]: any }} state\n * @returns\n */\nfunction stateToUrl(state) {\n    let path = \"\";\n    const pathKeysToOmit = [..._hiddenKeysFromUrl];\n    const actionStack = (state.actionStack || [state]).map((a) => ({ ...a }));\n    if (actionStack.at(-1)?.action !== \"menu\") {\n        for (const [prevAct, currentAct] of slidingWindow(actionStack, 2).reverse()) {\n            const { action: prevAction, resId: prevResId, active_id: prevActiveId } = prevAct;\n            const { action: currentAction, active_id: currentActiveId } = currentAct;\n            // actions would typically map to a path like `active_id/action/res_id`\n            if (currentActiveId === prevResId) {\n                // avoid doubling up when the active_id is the same as the previous action's res_id\n                delete currentAct.active_id;\n            }\n            if (prevAction === currentAction && !prevResId && currentActiveId === prevActiveId) {\n                //avoid doubling up the action and the active_id when a single-record action is preceded by a multi-record action\n                delete currentAct.action;\n                delete currentAct.active_id;\n            }\n        }\n        const pathSegments = actionStack.map(pathFromActionState).filter(Boolean);\n        if (pathSegments.length) {\n            path = `/${pathSegments.join(\"/\")}`;\n        }\n    }\n    if (state.active_id && typeof state.active_id !== \"number\") {\n        pathKeysToOmit.splice(pathKeysToOmit.indexOf(\"active_id\"), 1);\n    }\n    if (state.resId && typeof state.resId !== \"number\" && state.resId !== \"new\") {\n        pathKeysToOmit.splice(pathKeysToOmit.indexOf(\"resId\"), 1);\n    }\n    const search = objectToUrlEncodedString(omit(state, ...pathKeysToOmit));\n    const start_url = startUrl();\n    return `/${start_url}${path}${search ? `?${search}` : \"\"}`;\n}\n\nfunction urlToState(urlObj) {\n    const { pathname, hash, search } = urlObj;\n    const state = parseSearchQuery(search);\n\n    // ** url-retrocompatibility **\n    // If the url contains a hash, it can be for two motives:\n    // 1. It is an anchor link, in that case, we ignore it, as it will not have a keys/values format\n    //    the sanitizeHash function will remove it from the hash object.\n    // 2. It has one or more keys/values, in that case, we merge it with the search.\n    if (pathname === \"/web\") {\n        const sanitizedHash = sanitizeHash(parseHash(hash));\n        // Old urls used \"id\", it is now resId for clarity. Remap to the new name.\n        if (sanitizedHash.id) {\n            sanitizedHash.resId = sanitizedHash.id;\n            delete sanitizedHash.id;\n            delete sanitizedHash.view_type;\n        } else if (sanitizedHash.view_type === \"form\") {\n            sanitizedHash.resId = \"new\";\n            delete sanitizedHash.view_type;\n        }\n        Object.assign(state, sanitizedHash);\n        const url = browser.location.origin + router.stateToUrl(state);\n        urlObj.href = url;\n    }\n\n    const [prefix, ...splitPath] = urlObj.pathname.split(\"/\").filter(Boolean);\n\n    if ([\"odoo\", \"scoped_app\"].includes(prefix)) {\n        const actionParts = [...splitPath.entries()].filter(\n            ([_, part]) => !isNumeric(part) && part !== \"new\"\n        );\n        const actions = [];\n        for (const [i, part] of actionParts) {\n            const action = {};\n            const [left, right] = [splitPath[i - 1], splitPath[i + 1]];\n            if (isNumeric(left)) {\n                action.active_id = parseInt(left);\n            }\n\n            if (right === \"new\") {\n                action.resId = \"new\";\n            } else if (isNumeric(right)) {\n                action.resId = parseInt(right);\n            }\n\n            if (part.startsWith(\"action-\")) {\n                // numeric id or xml_id\n                const actionId = part.slice(7);\n                action.action = isNumeric(actionId) ? parseInt(actionId) : actionId;\n            } else if (part.startsWith(\"m-\")) {\n                action.model = part.slice(2);\n            } else if (part.includes(\".\")) {\n                action.model = part;\n            } else {\n                // action tag or path\n                action.action = part;\n            }\n\n            if (action.resId && action.action) {\n                actions.push(omit(action, \"resId\"));\n            }\n            // Don't create actions for models without resId unless they're the last one.\n            // If the last one is a model but doesn't have a view_type, the action service will not mount it anyway.\n            if (action.action || action.resId || i === splitPath.length - 1) {\n                actions.push(action);\n            }\n        }\n        const activeAction = actions.at(-1);\n        if (activeAction) {\n            Object.assign(state, activeAction);\n            state.actionStack = actions;\n        }\n        if (prefix === \"scoped_app\" && !isDisplayStandalone()) {\n            // make sure /scoped_app are redirected to /odoo when using the browser instead of the PWA\n            const url = browser.location.origin + router.stateToUrl(state);\n            urlObj.href = url;\n        }\n    }\n    return state;\n}\n\nlet state;\nlet pushTimeout;\nlet pushArgs;\nlet _lockedKeys;\nlet _hiddenKeysFromUrl = new Set();\n\nexport function startRouter() {\n    const url = new URL(browser.location);\n    state = router.urlToState(url);\n    // ** url-retrocompatibility **\n    if (browser.location.pathname === \"/web\") {\n        // Change the url of the current history entry to the canonical url.\n        // This change should be done only at the first load, and not when clicking on old style internal urls.\n        // Or when clicking back/forward on the browser.\n        browser.history.replaceState(browser.history.state, null, url.href);\n    }\n    pushTimeout = null;\n    pushArgs = {\n        replace: false,\n        reload: false,\n        state: {},\n    };\n    _lockedKeys = new Set([\"debug\", \"lang\"]);\n    _hiddenKeysFromUrl = new Set([...PATH_KEYS, \"actionStack\"]);\n}\n\n/**\n * When the user navigates history using the back/forward button, the browser\n * dispatches a popstate event with the state that was in the history for the\n * corresponding history entry. We just adopt that state so that the webclient\n * can use that previous state without forcing a full page reload.\n */\nbrowser.addEventListener(\"popstate\", (ev) => {\n    browser.clearTimeout(pushTimeout);\n    if (!ev.state) {\n        // We are coming from a click on an anchor.\n        // Add the current state to the history entry so that a future loadstate behaves as expected.\n        browser.history.replaceState({ nextState: state }, \"\", browser.location.href);\n        return;\n    }\n    state = ev.state?.nextState || router.urlToState(new URL(browser.location));\n    // Some client actions want to handle loading their own state. This is a ugly hack to allow not\n    // reloading the webclient's state when they manipulate history.\n    if (!ev.state?.skipRouteChange && !router.skipLoad) {\n        routerBus.trigger(\"ROUTE_CHANGE\");\n    }\n    router.skipLoad = false;\n});\n\n/**\n * When the user navigates the history using the back/forward button, some browsers (Safari iOS and\n * Safari MacOS) can restore the page using the `bfcache` (especially when we come back from an\n * external website). Unfortunately, Odoo wasn't designed to be compatible with this cache, which\n * leads to inconsistencies. When the `bfcache` is used to restore a page, we reload the current\n * page, to be sure that all the elements have been rendered correctly.\n */\nbrowser.addEventListener(\"pageshow\", (ev) => {\n    if (ev.persisted) {\n        browser.clearTimeout(pushTimeout);\n        routerBus.trigger(\"ROUTE_CHANGE\");\n    }\n});\n\n/**\n * When clicking internal links, do a loadState instead of a full page reload.\n * This also alows the mobile app to not open an in-app browser for them.\n */\nbrowser.addEventListener(\"click\", (ev) => {\n    if (ev.defaultPrevented || ev.target.closest(\"[contenteditable]\")) {\n        return;\n    }\n    const a = ev.target.closest(\"a\");\n    const href = a?.getAttribute(\"href\");\n    if (href && !href.startsWith(\"#\")) {\n        let url;\n        try {\n            // ev.target.href is the full url including current path\n            url = new URL(a.href);\n        } catch {\n            return;\n        }\n        if (\n            browser.location.host === url.host &&\n            browser.location.pathname.startsWith(\"/odoo\") &&\n            ([\"/web\", \"/odoo\"].includes(url.pathname) || url.pathname.startsWith(\"/odoo/\")) &&\n            a.target !== \"_blank\"\n        ) {\n            ev.preventDefault();\n            state = router.urlToState(url);\n            if (url.pathname.startsWith(\"/odoo\") && url.hash) {\n                browser.history.pushState({}, \"\", url.href);\n            }\n            new Promise((res) => setTimeout(res, 0)).then(() => routerBus.trigger(\"ROUTE_CHANGE\"));\n        }\n    }\n});\n\n/**\n * @param {string} mode\n */\nfunction makeDebouncedPush(mode) {\n    function doPush() {\n        // Calculates new route based on aggregated search and options\n        const nextState = computeNextState(pushArgs.state, pushArgs.replace);\n        const url = browser.location.origin + router.stateToUrl(nextState);\n        if (!compareUrls(url + browser.location.hash, browser.location.href)) {\n            // If the route changed: pushes or replaces browser state\n            if (mode === \"push\") {\n                // Because doPush is delayed, the history entry will have the wrong name.\n                // We set the document title to what it was at the time of the pushState\n                // call, then push, which generates the history entry with the right title\n                // then restore the title to what it's supposed to be\n                const originalTitle = document.title;\n                document.title = pushArgs.title;\n                browser.history.pushState({ nextState }, \"\", url);\n                document.title = originalTitle;\n            } else {\n                browser.history.replaceState({ nextState }, \"\", url);\n            }\n        } else {\n            // URL didn't change but state might have, update it in place\n            browser.history.replaceState({ nextState }, \"\", browser.location.href);\n        }\n        state = nextState;\n        if (pushArgs.reload) {\n            browser.location.reload();\n        }\n    }\n    /**\n     * @param {object} state\n     * @param {object} options\n     */\n    return function pushOrReplaceState(state, options = {}) {\n        pushArgs.replace ||= options.replace;\n        pushArgs.reload ||= options.reload;\n        pushArgs.title = document.title;\n        Object.assign(pushArgs.state, state);\n        browser.clearTimeout(pushTimeout);\n        const push = () => {\n            doPush();\n            pushTimeout = null;\n            pushArgs = {\n                replace: false,\n                reload: false,\n                state: {},\n            };\n        };\n        if (options.sync) {\n            push();\n        } else {\n            pushTimeout = browser.setTimeout(() => {\n                push();\n            });\n        }\n    };\n}\n\nexport const router = {\n    get current() {\n        return state;\n    },\n    // state <-> url conversions can be patched if needed in a custom webclient.\n    stateToUrl,\n    urlToState,\n    // TODO: stop debouncing these and remove the ugly hack to have the correct title for history entries\n    pushState: makeDebouncedPush(\"push\"),\n    replaceState: makeDebouncedPush(\"replace\"),\n    cancelPushes: () => browser.clearTimeout(pushTimeout),\n    addLockedKey: (key) => _lockedKeys.add(key),\n    hideKeyFromUrl: (key) => _hiddenKeysFromUrl.add(key),\n    skipLoad: false,\n};\n\nstartRouter();\n\nexport function objectToQuery(obj) {\n    const query = {};\n    Object.entries(obj).forEach(([k, v]) => {\n        query[k] = v ? String(v) : v;\n    });\n    return query;\n}\n", "import { registry } from \"../registry\";\n\nexport const titleService = {\n    start() {\n        const titleCounters = {};\n        const titleParts = {};\n\n        function getParts() {\n            return Object.assign({}, titleParts);\n        }\n\n        function setCounters(counters) {\n            for (const key in counters) {\n                const val = counters[key];\n                if (!val) {\n                    delete titleCounters[key];\n                } else {\n                    titleCounters[key] = val;\n                }\n            }\n            updateTitle();\n        }\n\n        function setParts(parts) {\n            for (const key in parts) {\n                const val = parts[key];\n                if (!val) {\n                    delete titleParts[key];\n                } else {\n                    titleParts[key] = val;\n                }\n            }\n            updateTitle();\n        }\n\n        function updateTitle() {\n            const counter = Object.values(titleCounters).reduce((acc, count) => acc + count, 0);\n            const name = Object.values(titleParts).join(\" - \") || \"Odoo\";\n            if (!counter) {\n                document.title = name;\n            } else {\n                document.title = `(${counter}) ${name}`;\n            }\n        }\n\n        return {\n            /**\n             * @returns {string}\n             */\n            get current() {\n                return document.title;\n            },\n            getParts,\n            setCounters,\n            setParts,\n        };\n    },\n};\n\nregistry.category(\"services\").add(\"title\", titleService);\n", "import { useHotkey } from \"../hotkeys/hotkey_hook\";\n\nimport { Component, useRef } from \"@odoo/owl\";\n\n/**\n * Custom checkbox\n *\n * <CheckBox\n *    value=\"boolean\"\n *    disabled=\"boolean\"\n *    onChange=\"_onValueChange\"\n * >\n *    Change the label text\n * </CheckBox>\n *\n * @extends Component\n */\n\nexport class CheckBox extends Component {\n    static template = \"web.CheckBox\";\n    static nextId = 1;\n    static defaultProps = {\n        onChange: () => {},\n    };\n    static props = {\n        id: {\n            type: true,\n            optional: true,\n        },\n        disabled: {\n            type: Boolean,\n            optional: true,\n        },\n        value: {\n            type: Boolean,\n            optional: true,\n        },\n        slots: {\n            type: Object,\n            optional: true,\n        },\n        onChange: {\n            type: Function,\n            optional: true,\n        },\n        className: {\n            type: String,\n            optional: true,\n        },\n        name: {\n            type: String,\n            optional: true,\n        },\n        indeterminate: {\n            type: Boolean,\n            optional: true,\n        },\n    };\n\n    setup() {\n        this.id = `checkbox-comp-${CheckBox.nextId++}`;\n        this.rootRef = useRef(\"root\");\n\n        // Make it toggleable through the Enter hotkey\n        // when the focus is inside the root element\n        useHotkey(\n            \"Enter\",\n            ({ area }) => {\n                const oldValue = area.querySelector(\"input\").checked;\n                this.props.onChange(!oldValue);\n            },\n            { area: () => this.rootRef.el, bypassEditableProtection: true }\n        );\n    }\n\n    onClick(ev) {\n        if (ev.composedPath().find((el) => [\"INPUT\", \"LABEL\"].includes(el.tagName))) {\n            // The onChange will handle these cases.\n            ev.stopPropagation();\n            return;\n        }\n\n        // Reproduce the click event behavior as if it comes from the input element.\n        const input = this.rootRef.el.querySelector(\"input\");\n        input.focus();\n        if (!this.props.disabled) {\n            ev.stopPropagation();\n            input.checked = !input.checked;\n            this.props.onChange(input.checked);\n        }\n    }\n\n    onChange(ev) {\n        if (!this.props.disabled) {\n            this.props.onChange(ev.target.checked);\n        }\n    }\n}\n", "import { Component, onMounted, onWillStart, useEffect, useRef, useState, status } from \"@odoo/owl\";\nimport { loadBundle } from \"@web/core/assets\";\n\nexport class CodeEditor extends Component {\n    static template = \"web.CodeEditor\";\n    static components = {};\n    static props = {\n        mode: {\n            type: String,\n            optional: true,\n            validate: (mode) => CodeEditor.MODES.includes(mode),\n        },\n        value: { validate: (v) => typeof v === \"string\", optional: true },\n        readonly: { type: Boolean, optional: true },\n        onChange: { type: Function, optional: true },\n        onBlur: { type: Function, optional: true },\n        class: { type: String, optional: true },\n        theme: {\n            type: String,\n            optional: true,\n            validate: (theme) => CodeEditor.THEMES.includes(theme),\n        },\n        maxLines: { type: Number, optional: true },\n        sessionId: { type: [Number, String], optional: true },\n        initialCursorPosition: { type: Object, optional: true },\n        showLineNumbers: { type: Boolean, optional: true },\n    };\n    static defaultProps = {\n        readonly: false,\n        value: \"\",\n        onChange: () => {},\n        class: \"\",\n        theme: \"\",\n        sessionId: 1,\n        showLineNumbers: true,\n    };\n\n    static MODES = [\"javascript\", \"xml\", \"qweb\", \"scss\", \"python\"];\n    static THEMES = [\"\", \"monokai\"];\n\n    setup() {\n        this.editorRef = useRef(\"editorRef\");\n        this.state = useState({\n            activeMode: undefined,\n        });\n\n        onWillStart(async () => await loadBundle(\"web.ace_lib\"));\n\n        const sessions = {};\n        // The ace library triggers the \"change\" event even if the change is\n        // programmatic. Even worse, it triggers 2 \"change\" events in that case,\n        // one with the empty string, and one with the new value. We only want\n        // to notify the parent of changes done by the user, in the UI, so we\n        // use this flag to filter out noisy \"change\" events.\n        let ignoredAceChange = false;\n        useEffect(\n            (el) => {\n                if (!el) {\n                    return;\n                }\n\n                // keep in closure\n                const aceEditor = window.ace.edit(el);\n                this.aceEditor = aceEditor;\n\n                this.aceEditor.setOptions({\n                    maxLines: this.props.maxLines,\n                    showPrintMargin: false,\n                    useWorker: false,\n                });\n                this.aceEditor.$blockScrolling = true;\n\n                this.aceEditor.on(\"changeMode\", () => {\n                    this.state.activeMode = this.aceEditor.getSession().$modeId.split(\"/\").at(-1);\n                });\n\n                const session = aceEditor.getSession();\n                if (!sessions[this.props.sessionId]) {\n                    sessions[this.props.sessionId] = session;\n                }\n                session.setValue(this.props.value);\n                session.on(\"change\", () => {\n                    if (this.props.onChange && !ignoredAceChange) {\n                        this.props.onChange(\n                            this.aceEditor.getValue(),\n                            this.aceEditor.getCursorPosition()\n                        );\n                    }\n                });\n                this.aceEditor.on(\"blur\", () => {\n                    if (this.props.onBlur) {\n                        this.props.onBlur();\n                    }\n                });\n\n                return () => {\n                    aceEditor.destroy();\n                };\n            },\n            () => [this.editorRef.el]\n        );\n\n        useEffect(\n            (theme) => this.aceEditor.setTheme(theme ? `ace/theme/${theme}` : \"\"),\n            () => [this.props.theme]\n        );\n\n        useEffect(\n            (readonly, showLineNumbers) => {\n                this.aceEditor.setOptions({\n                    readOnly: readonly,\n                    highlightActiveLine: !readonly,\n                    highlightGutterLine: !readonly,\n                });\n\n                this.aceEditor.renderer.setOptions({\n                    displayIndentGuides: !readonly,\n                    showGutter: !readonly && showLineNumbers,\n                });\n\n                this.aceEditor.renderer.$cursorLayer.element.style.display = readonly\n                    ? \"none\"\n                    : \"block\";\n            },\n            () => [this.props.readonly, this.props.showLineNumbers]\n        );\n\n        useEffect(\n            (sessionId, mode, value) => {\n                let session = sessions[sessionId];\n                if (session) {\n                    if (session.getValue() !== value) {\n                        ignoredAceChange = true;\n                        session.setValue(value);\n                        ignoredAceChange = false;\n                    }\n                } else {\n                    session = new window.ace.EditSession(value);\n                    session.setUndoManager(new window.ace.UndoManager());\n                    session.setOptions({\n                        useWorker: false,\n                        tabSize: 2,\n                        useSoftTabs: true,\n                    });\n                    session.on(\"change\", () => {\n                        if (this.props.onChange && !ignoredAceChange) {\n                            this.props.onChange(\n                                this.aceEditor.getValue(),\n                                this.aceEditor.getCursorPosition()\n                            );\n                        }\n                    });\n                    sessions[sessionId] = session;\n                }\n                session.setMode(mode ? `ace/mode/${mode}` : \"\");\n                this.aceEditor.setSession(session);\n            },\n            () => [this.props.sessionId, this.props.mode, this.props.value]\n        );\n\n        const initialCursorPosition = this.props.initialCursorPosition;\n        if (initialCursorPosition) {\n            onMounted(() => {\n                // Wait for ace to be fully operational\n                window.requestAnimationFrame(() => {\n                    if (status(this) != \"destroyed\" && this.aceEditor) {\n                        this.aceEditor.focus();\n                        const { row, column } = initialCursorPosition;\n                        const pos = {\n                            row: row || 0,\n                            column: column || 0,\n                        };\n                        this.aceEditor.selection.moveToPosition(pos);\n                        this.aceEditor.renderer.scrollCursorIntoView(pos, 0.5);\n                    }\n                });\n            });\n        }\n    }\n}\n", "import { Component, useEffect, useRef, useState } from \"@odoo/owl\";\nimport { CustomColorPicker } from \"@web/core/color_picker/custom_color_picker/custom_color_picker\";\nimport { usePopover } from \"@web/core/popover/popover_hook\";\nimport { isCSSColor, isColorGradient, normalizeCSSColor } from \"@web/core/utils/colors\";\nimport { cookie } from \"@web/core/browser/cookie\";\nimport { POSITION_BUS } from \"../position/position_hook\";\nimport { registry } from \"../registry\";\n\n// These colors are already normalized as per normalizeCSSColor in @web/legacy/js/widgets/colorpicker\nexport const DEFAULT_COLORS = [\n    [\"#000000\", \"#424242\", \"#636363\", \"#9C9C94\", \"#CEC6CE\", \"#EFEFEF\", \"#F7F7F7\", \"#FFFFFF\"],\n    [\"#FF0000\", \"#FF9C00\", \"#FFFF00\", \"#00FF00\", \"#00FFFF\", \"#0000FF\", \"#9C00FF\", \"#FF00FF\"],\n    [\"#F7C6CE\", \"#FFE7CE\", \"#FFEFC6\", \"#D6EFD6\", \"#CEDEE7\", \"#CEE7F7\", \"#D6D6E7\", \"#E7D6DE\"],\n    [\"#E79C9C\", \"#FFC69C\", \"#FFE79C\", \"#B5D6A5\", \"#A5C6CE\", \"#9CC6EF\", \"#B5A5D6\", \"#D6A5BD\"],\n    [\"#E76363\", \"#F7AD6B\", \"#FFD663\", \"#94BD7B\", \"#73A5AD\", \"#6BADDE\", \"#8C7BC6\", \"#C67BA5\"],\n    [\"#CE0000\", \"#E79439\", \"#EFC631\", \"#6BA54A\", \"#4A7B8C\", \"#3984C6\", \"#634AA5\", \"#A54A7B\"],\n    [\"#9C0000\", \"#B56308\", \"#BD9400\", \"#397B21\", \"#104A5A\", \"#085294\", \"#311873\", \"#731842\"],\n    [\"#630000\", \"#7B3900\", \"#846300\", \"#295218\", \"#083139\", \"#003163\", \"#21104A\", \"#4A1031\"],\n];\n\nexport const DEFAULT_GRAYSCALES = {\n    solid: [\"black\", \"900\", \"800\", \"600\", \"400\", \"200\", \"100\", \"white\"],\n};\n\n// These CSS variables are defined in html_editor.\n// Using ColorPicker without html_editor installed is extremely unlikely.\nexport const DEFAULT_THEME_COLOR_VARS = [\n    \"o-color-1\",\n    \"o-color-2\",\n    \"o-color-3\",\n    \"o-color-4\",\n    \"o-color-5\",\n];\n\nexport class ColorPicker extends Component {\n    static template = \"web.ColorPicker\";\n    static components = { CustomColorPicker };\n    static props = {\n        state: {\n            type: Object,\n            shape: {\n                selectedColor: String,\n                selectedColorCombination: { type: String, optional: true },\n                getTargetedElements: { type: Function, optional: true },\n                defaultTab: String,\n                selectedTab: { type: String, optional: true },\n                // todo: remove the `mode` prop in master\n                mode: { type: String, optional: true },\n            },\n        },\n        getUsedCustomColors: Function,\n        applyColor: Function,\n        applyColorPreview: Function,\n        applyColorResetPreview: Function,\n        editColorCombination: { type: Function, optional: true },\n        setOnCloseCallback: { type: Function, optional: true },\n        setOperationCallbacks: { type: Function, optional: true },\n        enabledTabs: { type: Array, optional: true },\n        colorPrefix: { type: String },\n        cssVarColorPrefix: { type: String, optional: true },\n        defaultOpacity: { type: Number, optional: true },\n        grayscales: { type: Object, optional: true },\n        noTransparency: { type: Boolean, optional: true },\n        close: { type: Function, optional: true },\n        className: { type: String, optional: true },\n    };\n    static defaultProps = {\n        close: () => {},\n        defaultOpacity: 100,\n        enabledTabs: [\"solid\", \"custom\"],\n        cssVarColorPrefix: \"\",\n        setOnCloseCallback: () => {},\n    };\n\n    setup() {\n        this.tabs = registry\n            .category(\"color_picker_tabs\")\n            .getAll()\n            .filter((tab) => this.props.enabledTabs.includes(tab.id));\n        this.root = useRef(\"root\");\n\n        this.DEFAULT_COLORS = DEFAULT_COLORS;\n        this.grayscales = Object.assign({}, DEFAULT_GRAYSCALES, this.props.grayscales);\n        this.DEFAULT_THEME_COLOR_VARS = DEFAULT_THEME_COLOR_VARS;\n        this.defaultColorSet = this.getDefaultColorSet();\n        this.defaultColor = this.props.state.selectedColor;\n        this.focusedBtn = null;\n        this.onApplyCallback = () => {};\n        this.onPreviewRevertCallback = () => {};\n        this.getPreviewColor = () => {};\n\n        this.state = useState({\n            activeTab: this.props.state.selectedTab || this.getDefaultTab(),\n            currentCustomColor: this.props.state.selectedColor,\n            currentColorPreview: undefined,\n            showGradientPicker: false,\n        });\n        this.usedCustomColors = this.props.getUsedCustomColors();\n        useEffect(\n            () => {\n                // Recompute the positioning of the popover if any.\n                this.env[POSITION_BUS]?.trigger(\"update\");\n            },\n            () => [this.state.activeTab]\n        );\n    }\n\n    getDefaultTab() {\n        if (this.props.enabledTabs.includes(this.props.state.defaultTab)) {\n            return this.props.state.defaultTab;\n        }\n        return this.props.enabledTabs[0];\n    }\n\n    get selectedColor() {\n        return this.props.state.selectedColor;\n    }\n\n    get isDarkTheme() {\n        return cookie.get(\"color_scheme\") === \"dark\";\n    }\n\n    setTab(tab) {\n        this.state.activeTab = tab;\n        // Reset the preview revert callback, as it is tab-specific.\n        this.setOperationCallbacks({ onPreviewRevertCallback: () => {} });\n        this.applyColorResetPreview();\n    }\n\n    processColorFromEvent(ev) {\n        const target = this.getTarget(ev);\n        let color = target.dataset.color || \"\";\n        if (color && isColorCombination(color)) {\n            return color;\n        }\n        if (color && !isCSSColor(color) && !isColorGradient(color)) {\n            color = this.props.colorPrefix + color;\n        }\n        return color;\n    }\n    /**\n     * @param {Object} cbs - callbacks\n     * @param {Function} cbs.onApplyCallback\n     * @param {Function} cbs.onPreviewRevertCallback\n     */\n    setOperationCallbacks(cbs) {\n        // The gradient colorpicker has a nested ColorPicker. We need to use the\n        // `setOperationCallbacks` from the parent ColorPicker for it to be\n        // impacted.\n        if (this.props.setOperationCallbacks) {\n            this.props.setOperationCallbacks(cbs);\n        }\n        if (cbs.onApplyCallback) {\n            this.onApplyCallback = cbs.onApplyCallback;\n        }\n        if (cbs.onPreviewRevertCallback) {\n            this.onPreviewRevertCallback = cbs.onPreviewRevertCallback;\n        }\n        if (cbs.getPreviewColor) {\n            this.getPreviewColor = cbs.getPreviewColor;\n        }\n    }\n\n    applyColor(color) {\n        this.state.currentCustomColor = color;\n        this.props.applyColor(color);\n        this.defaultColorSet = this.getDefaultColorSet();\n        this.onApplyCallback();\n    }\n\n    onColorApply(ev) {\n        if (!this.isColorButton(this.getTarget(ev))) {\n            return;\n        }\n        const color = this.processColorFromEvent(ev);\n        this.applyColor(color);\n        this.props.close();\n    }\n\n    applyColorResetPreview() {\n        this.props.applyColorResetPreview();\n        this.state.currentColorPreview = undefined;\n        this.onPreviewRevertCallback();\n    }\n\n    onColorPreview(ev) {\n        const color = ev.hex || ev.gradient || this.processColorFromEvent(ev);\n        this.props.applyColorPreview(color);\n        this.state.currentColorPreview = this.getPreviewColor();\n    }\n\n    onColorHover(ev) {\n        if (!this.isColorButton(this.getTarget(ev))) {\n            return;\n        }\n        this.onColorPreview(ev);\n    }\n\n    onColorHoverOut(ev) {\n        if (!this.isColorButton(this.getTarget(ev))) {\n            return;\n        }\n        this.applyColorResetPreview();\n    }\n    getTarget(ev) {\n        const target = ev.target.closest(`[data-color]`);\n        return this.root.el.contains(target) ? target : ev.target;\n    }\n\n    onColorFocusin(ev) {\n        // In the editor color picker, the preview and reset reapply the\n        // selection, which can remove the focus from the current button (if the\n        // node is recreated). We need to force the focus and break the infinite\n        // loop that it could trigger.\n        if (this.focusedBtn === ev.target) {\n            this.focusedBtn = null;\n            return;\n        }\n        this.focusedBtn = ev.target;\n        this.onColorHover(ev);\n        if (document.activeElement !== ev.target) {\n            // The focus was lost during revert. Reset it where it should be.\n            ev.target.focus();\n        }\n    }\n\n    onColorFocusout(ev) {\n        if (!ev.relatedTarget || !this.isColorButton(ev.relatedTarget)) {\n            // Do not trigger a revert if we are in the focus loop (i.e. focus\n            // a button > selection is reset > focusout). Otherwise, the\n            // relatedTarget should always be one of the colorpicker's buttons.\n            return;\n        }\n        const activeEl = document.activeElement;\n        this.applyColorResetPreview();\n        if (document.activeElement !== activeEl) {\n            // The focus was lost during revert. Reset it where it should be.\n            ev.relatedTarget.focus();\n        }\n    }\n\n    getDefaultColorSet() {\n        if (!this.props.state.selectedColor) {\n            return;\n        }\n        let defaultColors = this.props.enabledTabs.includes(\"solid\")\n            ? this.DEFAULT_THEME_COLOR_VARS\n            : [];\n        for (const grayscale of Object.values(this.grayscales)) {\n            defaultColors = defaultColors.concat(grayscale);\n        }\n\n        const targetedElement =\n            this.props.state.getTargetedElements?.()[0] || document.documentElement;\n        const selectedColor = this.props.state.selectedColor.toUpperCase();\n        const htmlStyle =\n            targetedElement.ownerDocument.defaultView.getComputedStyle(targetedElement);\n\n        for (const color of defaultColors) {\n            const cssVar = normalizeCSSColor(htmlStyle.getPropertyValue(`--${color}`));\n            if (cssVar?.toUpperCase() === selectedColor) {\n                return color;\n            }\n        }\n\n        return false;\n    }\n\n    colorPickerNavigation(ev) {\n        const { target, key } = ev;\n        if (!target.classList.contains(\"o_color_button\")) {\n            return;\n        }\n        if (![\"ArrowRight\", \"ArrowLeft\", \"ArrowUp\", \"ArrowDown\"].includes(key)) {\n            return;\n        }\n\n        let targetBtn;\n        if (key === \"ArrowRight\") {\n            targetBtn = target.nextElementSibling;\n        } else if (key === \"ArrowLeft\") {\n            targetBtn = target.previousElementSibling;\n        } else if (key === \"ArrowUp\" || key === \"ArrowDown\") {\n            const buttonIndex = [...target.parentElement.children].indexOf(target);\n            const nbColumns = getComputedStyle(target).getPropertyValue(\n                \"--o-color-picker-grid-columns\"\n            );\n            targetBtn =\n                target.parentElement.children[\n                    buttonIndex + (key === \"ArrowUp\" ? -1 : 1) * nbColumns\n                ];\n            if (!targetBtn) {\n                const row =\n                    key === \"ArrowUp\"\n                        ? target.parentElement.previousElementSibling\n                        : target.parentElement.nextElementSibling;\n                if (row?.matches(\".o_color_section, .o_colorpicker_section\")) {\n                    targetBtn = row.children[buttonIndex];\n                }\n            }\n        }\n        if (targetBtn && targetBtn.classList.contains(\"o_color_button\")) {\n            targetBtn.focus();\n        }\n    }\n\n    isColorButton(targetEl) {\n        return targetEl.tagName === \"BUTTON\" && !targetEl.matches(\".o_colorpicker_ignore\");\n    }\n}\n\nexport function useColorPicker(refName, props, options = {}) {\n    // Callback to be overridden by child components (e.g. custom color picker).\n    let onCloseCallback = () => {};\n    const setOnCloseCallback = (cb) => {\n        onCloseCallback = cb;\n    };\n    props.setOnCloseCallback = setOnCloseCallback;\n    if (options.onClose) {\n        const onClose = options.onClose;\n        options.onClose = () => {\n            onCloseCallback();\n            onClose();\n        };\n    }\n\n    const colorPicker = usePopover(ColorPicker, options);\n    const root = useRef(refName);\n\n    function onClick() {\n        colorPicker.isOpen ? colorPicker.close() : colorPicker.open(root.el, props);\n    }\n\n    useEffect(\n        (el) => {\n            if (!el) {\n                return;\n            }\n            el.addEventListener(\"click\", onClick);\n            return () => {\n                el.removeEventListener(\"click\", onClick);\n            };\n        },\n        () => [root.el]\n    );\n\n    return colorPicker;\n}\n\n/**\n * Checks if a given string is a color combination.\n *\n * @param {string} color\n * @returns {boolean}\n */\nfunction isColorCombination(color) {\n    return color.startsWith(\"o_cc\");\n}\n", "import { getActiveHotkey } from \"@web/core/hotkeys/hotkey_service\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport {\n    convertCSSColorToRgba,\n    convertHslToRgb,\n    convertRgbaToCSSColor,\n    convertRgbToHsl,\n} from \"@web/core/utils/colors\";\nimport { uniqueId } from \"@web/core/utils/functions\";\nimport { clamp } from \"@web/core/utils/numbers\";\nimport { debounce, useThrottleForAnimation } from \"@web/core/utils/timing\";\n\nimport { Component, onMounted, onWillUpdateProps, useExternalListener, useRef } from \"@odoo/owl\";\n\nconst ARROW_KEYS = [\"arrowup\", \"arrowdown\", \"arrowleft\", \"arrowright\"];\nconst SLIDER_KEYS = [...ARROW_KEYS, \"pageup\", \"pagedown\", \"home\", \"end\"];\n\nconst DEFAULT_COLOR = \"#FF0000\";\n\nexport class CustomColorPicker extends Component {\n    static template = \"web.CustomColorPicker\";\n    static props = {\n        document: { type: true, optional: true },\n        defaultColor: { type: String, optional: true },\n        selectedColor: { type: String, optional: true },\n        noTransparency: { type: Boolean, optional: true },\n        stopClickPropagation: { type: Boolean, optional: true },\n        onColorSelect: { type: Function, optional: true },\n        onColorPreview: { type: Function, optional: true },\n        onInputEnter: { type: Function, optional: true },\n        defaultOpacity: { type: Number, optional: true },\n        setOnCloseCallback: { type: Function, optional: true },\n        setOperationCallbacks: { type: Function, optional: true },\n    };\n    static defaultProps = {\n        document: window.document,\n        defaultColor: DEFAULT_COLOR,\n        defaultOpacity: 100,\n        noTransparency: false,\n        stopClickPropagation: false,\n        onColorSelect: () => {},\n        onColorPreview: () => {},\n        onInputEnter: () => {},\n    };\n\n    setup() {\n        this.pickerFlag = false;\n        this.sliderFlag = false;\n        this.opacitySliderFlag = false;\n        if (this.props.defaultOpacity > 0 && this.props.defaultOpacity <= 1) {\n            this.props.defaultOpacity *= 100;\n        }\n        if (this.props.defaultColor.length <= 7) {\n            const opacityHex = Math.round((this.props.defaultOpacity / 100) * 255)\n                .toString(16)\n                .padStart(2, \"0\");\n            this.props.defaultColor += opacityHex;\n        }\n        this.colorComponents = {};\n        this.uniqueId = uniqueId(\"colorpicker\");\n        this.selectedHexValue = \"\";\n        this.shouldSetSelectedColor = false;\n        this.lastFocusedSliderEl = undefined;\n        if (!this.props.selectedColor) {\n            this.props.selectedColor = this.props.defaultColor;\n        }\n        this.debouncedOnChangeInputs = debounce(this.onChangeInputs.bind(this), 10, true);\n\n        this.elRef = useRef(\"el\");\n        this.colorPickerAreaRef = useRef(\"colorPickerArea\");\n        this.colorPickerPointerRef = useRef(\"colorPickerPointer\");\n        this.colorSliderRef = useRef(\"colorSlider\");\n        this.colorSliderPointerRef = useRef(\"colorSliderPointer\");\n        this.opacitySliderRef = useRef(\"opacitySlider\");\n        this.opacitySliderPointerRef = useRef(\"opacitySliderPointer\");\n\n        // Need to be bound on all documents to work in all possible cases (we\n        // have to be able to start dragging/moving from the colorpicker to\n        // anywhere on the screen, crossing iframes).\n        const documents = [\n            window.top,\n            ...Array.from(window.top.frames).filter((frame) => {\n                try {\n                    const document = frame.document;\n                    return !!document;\n                } catch {\n                    // We cannot access the document (cross origin).\n                    return false;\n                }\n            }),\n        ].map((w) => w.document);\n        this.throttleOnPointerMove = useThrottleForAnimation((ev) => {\n            this.onPointerMovePicker(ev);\n            this.onPointerMoveSlider(ev);\n            this.onPointerMoveOpacitySlider(ev);\n        });\n\n        for (const doc of documents) {\n            useExternalListener(doc, \"pointermove\", this.throttleOnPointerMove);\n            useExternalListener(doc, \"pointerup\", this.onPointerUp.bind(this));\n            useExternalListener(doc, \"keydown\", this.onEscapeKeydown.bind(this), { capture: true });\n        }\n        // Apply the previewed custom color when the popover is closed.\n        this.props.setOnCloseCallback?.(() => {\n            if (this.shouldSetSelectedColor) {\n                this._colorSelected();\n            }\n        });\n        this.props.setOperationCallbacks?.({\n            getPreviewColor: () => {\n                if (this.shouldSetSelectedColor) {\n                    return this.colorComponents.hex;\n                }\n            },\n            onApplyCallback: () => {\n                this.shouldSetSelectedColor = false;\n            },\n            // Reapply the current custom color preview after reverting a preview.\n            // Typical usecase: 1) modify the custom color, 2) hover one of the\n            // black-white tints, 3) hover out.\n            onPreviewRevertCallback: () => {\n                if (this.previewActive && this.shouldSetSelectedColor) {\n                    this.props.onColorPreview(this.colorComponents);\n                }\n            },\n        });\n        onMounted(async () => {\n            const rgba =\n                convertCSSColorToRgba(this.props.selectedColor) ||\n                convertCSSColorToRgba(this.props.defaultColor);\n            if (rgba) {\n                this._updateRgba(rgba.red, rgba.green, rgba.blue, rgba.opacity);\n            }\n\n            this.previewActive = true;\n            this._updateUI();\n        });\n        onWillUpdateProps((newProps) => {\n            const newSelectedColor = newProps.selectedColor\n                ? newProps.selectedColor\n                : newProps.defaultColor;\n            this.setSelectedColor(newSelectedColor);\n        });\n    }\n\n    /**\n     * Sets the currently selected color\n     *\n     * @param {string} color rgb[a]\n     */\n    setSelectedColor(color) {\n        const rgba = convertCSSColorToRgba(color);\n        if (rgba) {\n            const oldPreviewActive = this.previewActive;\n            this.previewActive = false;\n            this._updateRgba(rgba.red, rgba.green, rgba.blue, rgba.opacity);\n            this.previewActive = oldPreviewActive;\n            this._updateUI();\n        }\n    }\n    /**\n     * @param {string[]} allowedKeys\n     * @returns {string[]} allowed keys + modifiers\n     */\n    getAllowedHotkeys(allowedKeys) {\n        return allowedKeys.flatMap((key) => [key, `control+${key}`]);\n    }\n    /**\n     * @param {HTMLElement} el\n     */\n    setLastFocusedSliderEl(el) {\n        this.lastFocusedSliderEl = el;\n        document.activeElement.blur();\n    }\n\n    get el() {\n        return this.elRef.el;\n    }\n    /**\n     * @param {string} hotkey\n     * @param {number} value\n     * @param {Object} [options]\n     * @param {number} [options.min=0]\n     * @param {number} [options.max=100]\n     * @param {number} [options.defaultStep=10] - default step\n     * @param {number} [options.modifierStep=1] - step when holding ctrl+key\n     * @param {number} [options.leap=20] - step for pageup / pagedown\n     * @returns {number} updated and clamped value\n     */\n    handleRangeKeydownValue(\n        hotkey,\n        value,\n        { min = 0, max = 100, defaultStep = 10, modifierStep = 1, leap = 20 } = {}\n    ) {\n        let step = defaultStep;\n        if (hotkey.startsWith(\"control+\")) {\n            step = modifierStep;\n        }\n        const mainKey = hotkey.replace(\"control+\", \"\");\n        if (mainKey === \"pageup\" || mainKey === \"pagedown\") {\n            step = leap;\n        }\n        if ([\"arrowup\", \"arrowright\", \"pageup\"].includes(mainKey)) {\n            value += step;\n        } else if ([\"arrowdown\", \"arrowleft\", \"pagedown\"].includes(mainKey)) {\n            value -= step;\n        } else if (mainKey === \"home\") {\n            value = min;\n        } else if (mainKey === \"end\") {\n            value = max;\n        }\n        return clamp(value, min, max);\n    }\n    /**\n     * Selects and applies a currently previewed color if \"Enter\" was pressed.\n     *\n     * @param {String} hotkey\n     */\n    selectColorOnEnter(hotkey) {\n        if (hotkey === \"enter\" && this.shouldSetSelectedColor) {\n            this.pickerFlag = false;\n            this.sliderFlag = false;\n            this.opacitySliderFlag = false;\n            this._colorSelected();\n        }\n    }\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * Updates input values, color preview, picker and slider pointer positions.\n     *\n     * @private\n     */\n    _updateUI() {\n        // Update inputs\n        for (const [color, value] of Object.entries(this.colorComponents)) {\n            const input = this.el.querySelector(`.o_${color}_input`);\n            if (input) {\n                input.value = value;\n            }\n        }\n\n        // Update picker area and picker pointer position\n        const colorPickerArea = this.colorPickerAreaRef.el;\n        colorPickerArea.style.backgroundColor = `hsl(${this.colorComponents.hue}, 100%, 50%)`;\n        const top = ((100 - this.colorComponents.lightness) * colorPickerArea.clientHeight) / 100;\n        const left = (this.colorComponents.saturation * colorPickerArea.clientWidth) / 100;\n\n        const colorpickerPointer = this.colorPickerPointerRef.el;\n        colorpickerPointer.style.top = top - 5 + \"px\";\n        colorpickerPointer.style.left = left - 5 + \"px\";\n        colorpickerPointer.setAttribute(\n            \"aria-label\",\n            _t(\"Saturation: %(saturationLvl)s %. Brightness: %(brightnessLvl)s %\", {\n                saturationLvl: this.colorComponents.saturation?.toFixed(2) || \"0\",\n                brightnessLvl: this.colorComponents.lightness?.toFixed(2) || \"0\",\n            })\n        );\n\n        // Update color slider position\n        const colorSlider = this.colorSliderRef.el;\n        const height = colorSlider.clientHeight;\n        const y = (this.colorComponents.hue * height) / 360;\n        this.colorSliderPointerRef.el.style.bottom = `${Math.round(y - 4)}px`;\n        this.colorSliderPointerRef.el.setAttribute(\n            \"aria-valuenow\",\n            this.colorComponents.hue.toFixed(2)\n        );\n\n        if (!this.props.noTransparency) {\n            // Update opacity slider position\n            const opacitySlider = this.opacitySliderRef.el;\n            const heightOpacity = opacitySlider.clientHeight;\n            const z = heightOpacity * (1 - this.colorComponents.opacity / 100.0);\n            this.opacitySliderPointerRef.el.style.top = `${Math.round(z - 2)}px`;\n            this.opacitySliderPointerRef.el.setAttribute(\n                \"aria-valuenow\",\n                this.colorComponents.opacity.toFixed(2)\n            );\n\n            // Add gradient color on opacity slider\n            const sliderColor = this.colorComponents.hex.slice(0, 7);\n            opacitySlider.style.background = `linear-gradient(${sliderColor} 0%, transparent 100%)`;\n        }\n    }\n    /**\n     * Updates colors according to given hex value. Opacity is left unchanged.\n     *\n     * @private\n     * @param {string} hex - hexadecimal code\n     */\n    _updateHex(hex) {\n        const rgb = convertCSSColorToRgba(hex);\n        if (!rgb) {\n            return;\n        }\n        Object.assign(\n            this.colorComponents,\n            { hex: hex },\n            rgb,\n            convertRgbToHsl(rgb.red, rgb.green, rgb.blue)\n        );\n        this._updateCssColor();\n    }\n    /**\n     * Updates colors according to given RGB values.\n     *\n     * @private\n     * @param {integer} r\n     * @param {integer} g\n     * @param {integer} b\n     * @param {integer} [a]\n     */\n    _updateRgba(r, g, b, a) {\n        // Remove full transparency in case some lightness is added\n        const opacity = a || this.colorComponents.opacity;\n        if (opacity < 0.1 && (r > 0.1 || g > 0.1 || b > 0.1)) {\n            a = this.props.defaultOpacity;\n        }\n\n        const hex = convertRgbaToCSSColor(r, g, b, a);\n        if (!hex) {\n            return;\n        }\n        Object.assign(\n            this.colorComponents,\n            { red: r, green: g, blue: b },\n            a === undefined ? {} : { opacity: a },\n            { hex: hex },\n            convertRgbToHsl(r, g, b)\n        );\n        this._updateCssColor();\n    }\n    /**\n     * Updates colors according to given HSL values.\n     *\n     * @private\n     * @param {integer} h\n     * @param {integer} s\n     * @param {integer} l\n     */\n    _updateHsl(h, s, l) {\n        // Remove full darkness/brightness and non-saturation in case hue is changed\n        if (0.1 < Math.abs(h - this.colorComponents.hue)) {\n            if (l < 0.1 || 99.9 < l) {\n                l = 50;\n            }\n            if (s < 0.1) {\n                s = 100;\n            }\n        }\n        // Remove full transparency in case some lightness is added\n        let a = this.colorComponents.opacity;\n        if (a < 0.1 && l > 0.1) {\n            a = this.props.defaultOpacity;\n        }\n\n        const rgb = convertHslToRgb(h, s, l);\n        if (!rgb) {\n            return;\n        }\n        // We receive an hexa as we ignore the opacity\n        const hex = convertRgbaToCSSColor(rgb.red, rgb.green, rgb.blue, a);\n        Object.assign(\n            this.colorComponents,\n            { hue: h, saturation: s, lightness: l },\n            rgb,\n            { hex: hex },\n            { opacity: a }\n        );\n        this._updateCssColor();\n    }\n    /**\n     * Updates color opacity.\n     *\n     * @private\n     * @param {integer} a\n     */\n    _updateOpacity(a) {\n        if (a < 0 || a > 100) {\n            return;\n        }\n        Object.assign(this.colorComponents, { opacity: a });\n        const r = this.colorComponents.red;\n        const g = this.colorComponents.green;\n        const b = this.colorComponents.blue;\n        Object.assign(this.colorComponents, { hex: convertRgbaToCSSColor(r, g, b, a) });\n        this._updateCssColor();\n    }\n    /**\n     * Trigger an event to annonce that the widget value has changed\n     *\n     * @private\n     */\n    _colorSelected() {\n        this.props.onColorSelect(this.colorComponents);\n    }\n    /**\n     * Updates css color representation.\n     *\n     * @private\n     */\n    _updateCssColor() {\n        const r = this.colorComponents.red;\n        const g = this.colorComponents.green;\n        const b = this.colorComponents.blue;\n        const a = this.colorComponents.opacity;\n        Object.assign(this.colorComponents, { cssColor: convertRgbaToCSSColor(r, g, b, a) });\n        if (this.previewActive) {\n            this.props.onColorPreview(this.colorComponents);\n        }\n    }\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n     * @private\n     * @param {Event} ev\n     */\n    onKeydown(ev) {\n        if (ev.key === \"Enter\") {\n            if (ev.target.tagName === \"INPUT\") {\n                this.onChangeInputs(ev);\n            }\n            ev.preventDefault();\n            this.props.onInputEnter(ev);\n        }\n    }\n    /**\n     * @param {Event} ev\n     */\n    onClick(ev) {\n        if (this.props.stopClickPropagation) {\n            ev.stopPropagation();\n        }\n        //TODO: we should remove it with legacy web_editor\n        ev.__isColorpickerClick = true;\n\n        if (ev.target.dataset.colorMethod === \"hex\" && !this.selectedHexValue) {\n            ev.target.select();\n            this.selectedHexValue = ev.target.value;\n            return;\n        }\n        this.selectedHexValue = \"\";\n    }\n    onPointerUp() {\n        if (this.pickerFlag || this.sliderFlag || this.opacitySliderFlag) {\n            this.shouldSetSelectedColor = true;\n            this._updateCssColor();\n        }\n        this.pickerFlag = false;\n        this.sliderFlag = false;\n        this.opacitySliderFlag = false;\n\n        if (this.lastFocusedSliderEl) {\n            this.lastFocusedSliderEl.focus();\n            this.lastFocusedSliderEl = undefined;\n        }\n    }\n    /**\n     * Removes the close callback on Escape, so that a preview is cancelled with\n     * escape instead of being applied.\n     *\n     * @param {KeydownEvent} ev\n     */\n    onEscapeKeydown(ev) {\n        const hotkey = getActiveHotkey(ev);\n        if (hotkey === \"escape\") {\n            this.props.setOnCloseCallback?.(() => {});\n        }\n    }\n    /**\n     * Updates color when the user starts clicking on the picker.\n     *\n     * @private\n     * @param {Event} ev\n     */\n    onPointerDownPicker(ev) {\n        this.pickerFlag = true;\n        ev.preventDefault();\n        this.onPointerMovePicker(ev);\n        this.setLastFocusedSliderEl(this.colorPickerPointerRef.el);\n    }\n    /**\n     * Updates saturation and lightness values on pointer drag over picker.\n     *\n     * @private\n     * @param {Event} ev\n     */\n    onPointerMovePicker(ev) {\n        if (!this.pickerFlag) {\n            return;\n        }\n\n        const colorPickerArea = this.colorPickerAreaRef.el;\n        const rect = colorPickerArea.getClientRects()[0];\n        const top = ev.pageY - rect.top;\n        const left = ev.pageX - rect.left;\n        let saturation = Math.round((100 * left) / colorPickerArea.clientWidth);\n        let lightness = Math.round(\n            (100 * (colorPickerArea.clientHeight - top)) / colorPickerArea.clientHeight\n        );\n        saturation = clamp(saturation, 0, 100);\n        lightness = clamp(lightness, 0, 100);\n\n        this._updateHsl(this.colorComponents.hue, saturation, lightness);\n        this._updateUI();\n    }\n    /**\n     * Updates saturation and lightness values on arrow keydown over picker.\n     *\n     * @private\n     * @param {Event} ev\n     */\n    onPickerKeydown(ev) {\n        const hotkey = getActiveHotkey(ev);\n        this.selectColorOnEnter(hotkey);\n        if (!this.getAllowedHotkeys(ARROW_KEYS).includes(hotkey)) {\n            return;\n        }\n        let saturation = this.colorComponents.saturation;\n        let lightness = this.colorComponents.lightness;\n        let step = 10;\n        if (hotkey.startsWith(\"control+\")) {\n            step = 1;\n        }\n        const mainKey = hotkey.replace(\"control+\", \"\");\n        if (mainKey === \"arrowup\") {\n            lightness += step;\n        } else if (mainKey === \"arrowdown\") {\n            lightness -= step;\n        } else if (mainKey === \"arrowright\") {\n            saturation += step;\n        } else if (mainKey === \"arrowleft\") {\n            saturation -= step;\n        }\n        lightness = clamp(lightness, 0, 100);\n        saturation = clamp(saturation, 0, 100);\n\n        this._updateHsl(this.colorComponents.hue, saturation, lightness);\n        this._updateUI();\n        this.shouldSetSelectedColor = true;\n    }\n    /**\n     * Updates color when user starts clicking on slider.\n     *\n     * @private\n     * @param {Event} ev\n     */\n    onPointerDownSlider(ev) {\n        this.sliderFlag = true;\n        ev.preventDefault();\n        this.onPointerMoveSlider(ev);\n        this.setLastFocusedSliderEl(this.colorSliderPointerRef.el);\n    }\n    /**\n     * Updates hue value on pointer drag over slider.\n     *\n     * @private\n     * @param {Event} ev\n     */\n    onPointerMoveSlider(ev) {\n        if (!this.sliderFlag) {\n            return;\n        }\n\n        const colorSlider = this.colorSliderRef.el;\n        const colorSliderRects = colorSlider.getClientRects();\n        const y = colorSliderRects[0].height - (ev.pageY - colorSliderRects[0].top);\n        let hue = Math.round((360 * y) / colorSlider.clientHeight);\n        hue = clamp(hue, 0, 360);\n\n        this._updateHsl(hue, this.colorComponents.saturation, this.colorComponents.lightness);\n        this._updateUI();\n    }\n    /**\n     * Updates hue value on arrow keydown on slider.\n     *\n     * @param {Event} ev\n     */\n    onSliderKeydown(ev) {\n        const hotkey = getActiveHotkey(ev);\n        this.selectColorOnEnter(hotkey);\n        if (!this.getAllowedHotkeys(SLIDER_KEYS).includes(hotkey)) {\n            return;\n        }\n        const hue = this.handleRangeKeydownValue(hotkey, this.colorComponents.hue, {\n            min: 0,\n            max: 360,\n            leap: 30,\n        });\n        this._updateHsl(hue, this.colorComponents.saturation, this.colorComponents.lightness);\n        this._updateUI();\n        this.shouldSetSelectedColor = true;\n    }\n    /**\n     * Updates opacity when user starts clicking on opacity slider.\n     *\n     * @private\n     * @param {Event} ev\n     */\n    onPointerDownOpacitySlider(ev) {\n        this.opacitySliderFlag = true;\n        ev.preventDefault();\n        this.onPointerMoveOpacitySlider(ev);\n        this.setLastFocusedSliderEl(this.opacitySliderPointerRef.el);\n    }\n    /**\n     * Updates opacity value on pointer drag over opacity slider.\n     *\n     * @private\n     * @param {Event} ev\n     */\n    onPointerMoveOpacitySlider(ev) {\n        if (!this.opacitySliderFlag || this.props.noTransparency) {\n            return;\n        }\n\n        const opacitySlider = this.opacitySliderRef.el;\n        const y = ev.pageY - opacitySlider.getClientRects()[0].top;\n        let opacity = Math.round(100 * (1 - y / opacitySlider.clientHeight));\n        opacity = clamp(opacity, 0, 100);\n\n        this._updateOpacity(opacity);\n        this._updateUI();\n    }\n    /**\n     * Updates opacity value on arrow keydown on opacity slider.\n     *\n     * @param {Event} ev\n     */\n    onOpacitySliderKeydown(ev) {\n        const hotkey = getActiveHotkey(ev);\n        this.selectColorOnEnter(hotkey);\n        if (!this.getAllowedHotkeys(SLIDER_KEYS).includes(hotkey)) {\n            return;\n        }\n        const opacity = this.handleRangeKeydownValue(hotkey, this.colorComponents.opacity);\n\n        this._updateOpacity(opacity);\n        this._updateUI();\n        this.shouldSetSelectedColor = true;\n    }\n    /**\n     * Called when input value is changed -> Updates UI: Set picker and slider\n     * position and set colors.\n     *\n     * @private\n     * @param {Event} ev\n     */\n    onChangeInputs(ev) {\n        switch (ev.target.dataset.colorMethod) {\n            case \"hex\":\n                // Handled by the \"input\" event (see \"onHexColorInput\").\n                return;\n            case \"hsl\":\n                this._updateHsl(\n                    parseInt(this.el.querySelector(\".o_hue_input\").value),\n                    parseInt(this.el.querySelector(\".o_saturation_input\").value),\n                    parseInt(this.el.querySelector(\".o_lightness_input\").value)\n                );\n                break;\n        }\n        this._updateUI();\n        this._colorSelected();\n    }\n    /**\n     * Called when the hex color input's input event is triggered.\n     *\n     * @private\n     * @param {Event} ev\n     */\n    onHexColorInput(ev) {\n        const hexColorValue = ev.target.value.replaceAll(\"#\", \"\");\n        if (hexColorValue.length === 6 || hexColorValue.length === 8) {\n            this._updateHex(`#${hexColorValue}`);\n            this._updateUI();\n            this._colorSelected();\n        }\n    }\n}\n", "import { Component } from \"@odoo/owl\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { isColorGradient } from \"@web/core/utils/colors\";\nimport { CustomColorPicker } from \"../custom_color_picker/custom_color_picker\";\n\nexport class ColorPickerCustomTab extends Component {\n    static template = \"web.ColorPickerCustomTab\";\n    static components = { CustomColorPicker };\n    static props = {\n        applyColor: Function,\n        colorPickerNavigation: Function,\n        onColorClick: Function,\n        onColorPreview: Function,\n        onColorPointerOver: Function,\n        onColorPointerOut: Function,\n        onFocusin: Function,\n        onFocusout: Function,\n        getUsedCustomColors: { type: Function, optional: true },\n        currentColorPreview: { type: String, optional: true },\n        currentCustomColor: { type: String, optional: true },\n        defaultColorSet: { type: String | Boolean, optional: true },\n        defaultOpacity: { type: Number, optional: true },\n        grayscales: { type: Object, optional: true },\n        cssVarColorPrefix: { type: String, optional: true },\n        noTransparency: { type: Boolean, optional: true },\n        setOnCloseCallback: { type: Function, optional: true },\n        setOperationCallbacks: { type: Function, optional: true },\n        \"*\": { optional: true },\n    };\n\n    setup() {\n        this.usedCustomColors = this.props.getUsedCustomColors();\n    }\n\n    isValidCustomColor(color) {\n        return color && color.slice(7, 9) !== \"00\" && !isColorGradient(color);\n    }\n}\n\nregistry.category(\"color_picker_tabs\").add(\"web.custom\", {\n    id: \"custom\",\n    name: _t(\"Custom\"),\n    component: ColorPickerCustomTab,\n});\n", "import { Component } from \"@odoo/owl\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\n\nexport class ColorPickerSolidTab extends Component {\n    static template = \"web.ColorPickerSolidTab\";\n    static props = {\n        colorPickerNavigation: Function,\n        onColorClick: Function,\n        onColorPointerOver: Function,\n        onColorPointerOut: Function,\n        onFocusin: Function,\n        onFocusout: Function,\n        currentCustomColor: { type: String, optional: true },\n        defaultColorSet: { type: String | Boolean, optional: true },\n        cssVarColorPrefix: { type: String, optional: true },\n        defaultColors: Array,\n        defaultThemeColorVars: Array,\n        \"*\": { optional: true },\n    };\n}\n\nregistry.category(\"color_picker_tabs\").add(\"web.solid\", {\n    id: \"solid\",\n    name: _t(\"Solid\"),\n    component: ColorPickerSolidTab,\n});\n", "import { _t } from \"@web/core/l10n/translation\";\n\nimport { Component, useRef, useState, useExternalListener } from \"@odoo/owl\";\n\nexport class ColorList extends Component {\n    static COLORS = [\n        _t(\"No color\"),\n        _t(\"Red\"),\n        _t(\"Orange\"),\n        _t(\"Yellow\"),\n        _t(\"Cyan\"),\n        _t(\"Purple\"),\n        _t(\"Almond\"),\n        _t(\"Teal\"),\n        _t(\"Blue\"),\n        _t(\"Raspberry\"),\n        _t(\"Green\"),\n        _t(\"Violet\"),\n    ];\n    static template = \"web.ColorList\";\n    static defaultProps = {\n        forceExpanded: false,\n        isExpanded: false,\n    };\n    static props = {\n        canToggle: { type: Boolean, optional: true },\n        colors: Array,\n        forceExpanded: { type: Boolean, optional: true },\n        isExpanded: { type: Boolean, optional: true },\n        onColorSelected: Function,\n        selectedColor: { type: Number, optional: true },\n    };\n\n    setup() {\n        this.colorlistRef = useRef(\"colorlist\");\n        this.state = useState({ isExpanded: this.props.isExpanded });\n        useExternalListener(window, \"click\", this.onOutsideClick);\n    }\n    get colors() {\n        return this.constructor.COLORS;\n    }\n    onColorSelected(id) {\n        this.props.onColorSelected(id);\n        if (!this.props.forceExpanded) {\n            this.state.isExpanded = false;\n        }\n    }\n    onOutsideClick(ev) {\n        if (this.colorlistRef.el.contains(ev.target) || this.props.forceExpanded) {\n            return;\n        }\n        this.state.isExpanded = false;\n    }\n    onToggle(ev) {\n        if (this.props.canToggle) {\n            ev.preventDefault();\n            ev.stopPropagation();\n            this.state.isExpanded = !this.state.isExpanded;\n            this.colorlistRef.el.firstElementChild.focus();\n        }\n    }\n}\n", "import { clamp } from \"@web/core/utils/numbers\";\n/**\n * Lists of colors that contrast well with each other to be used in various\n * visualizations (eg. graphs/charts), both in bright and dark themes.\n */\n\nconst COLORS_ENT_BRIGHT = [\"#875A7B\", \"#A5D8D7\", \"#DCD0D9\"];\nconst COLORS_ENT_DARK = [\"#6B3E66\", \"#147875\", \"#5A395A\"];\nconst COLORS_SM = [\n    \"#4EA7F2\", // Blue\n    \"#EA6175\", // Red\n    \"#43C5B1\", // Teal\n    \"#F4A261\", // Orange\n    \"#8481DD\", // Purple\n    \"#FFD86D\", // Yellow\n];\nconst COLORS_MD = [\n    \"#4EA7F2\", // Blue #1\n    \"#3188E6\", // Blue #2\n    \"#43C5B1\", // Teal #1\n    \"#00A78D\", // Teal #2\n    \"#EA6175\", // Red #1\n    \"#CE4257\", // Red #2\n    \"#F4A261\", // Orange #1\n    \"#F48935\", // Orange #2\n    \"#8481DD\", // Purple #1\n    \"#5752D1\", // Purple #2\n    \"#FFD86D\", // Yellow #1\n    \"#FFBC2C\", // Yellow #2\n];\nconst COLORS_LG = [\n    \"#4EA7F2\", // Blue #1\n    \"#3188E6\", // Blue #2\n    \"#056BD9\", // Blue #3\n    \"#A76DBC\", // Violet #1\n    \"#7F4295\", // Violet #2\n    \"#6D2387\", // Violet #3\n    \"#EA6175\", // Red #1\n    \"#CE4257\", // Red #2\n    \"#982738\", // Red #3\n    \"#43C5B1\", // Teal #1\n    \"#00A78D\", // Teal #2\n    \"#0E8270\", // Teal #3\n    \"#F4A261\", // Orange #1\n    \"#F48935\", // Orange #2\n    \"#BE5D10\", // Orange #3\n    \"#8481DD\", // Purple #1\n    \"#5752D1\", // Purple #2\n    \"#3A3580\", // Purple #3\n    \"#A4A8B6\", // Gray #1\n    \"#7E8290\", // Gray #2\n    \"#545B70\", // Gray #3\n    \"#FFD86D\", // Yellow #1\n    \"#FFBC2C\", // Yellow #2\n    \"#C08A16\", // Yellow #3\n];\nconst COLORS_XL = [\n    \"#4EA7F2\", // Blue #1\n    \"#3188E6\", // Blue #2\n    \"#056BD9\", // Blue #3\n    \"#155193\", // Blue #4\n    \"#A76DBC\", // Violet #1\n    \"#7F4295\", // Violet #1\n    \"#6D2387\", // Violet #1\n    \"#4F1565\", // Violet #1\n    \"#EA6175\", // Red #1\n    \"#CE4257\", // Red #2\n    \"#982738\", // Red #3\n    \"#791B29\", // Red #4\n    \"#43C5B1\", // Teal #1\n    \"#00A78D\", // Teal #2\n    \"#0E8270\", // Teal #3\n    \"#105F53\", // Teal #4\n    \"#F4A261\", // Orange #1\n    \"#F48935\", // Orange #2\n    \"#BE5D10\", // Orange #3\n    \"#7D380D\", // Orange #4\n    \"#8481DD\", // Purple #1\n    \"#5752D1\", // Purple #2\n    \"#3A3580\", // Purple #3\n    \"#26235F\", // Purple #4\n    \"#A4A8B6\", // Grey #1\n    \"#7E8290\", // Grey #2\n    \"#545B70\", // Grey #3\n    \"#3F4250\", // Grey #4\n    \"#FFD86D\", // Yellow #1\n    \"#FFBC2C\", // Yellow #2\n    \"#C08A16\", // Yellow #3\n    \"#936A12\", // Yellow #4\n];\n\n/**\n * @param {string} colorScheme\n * @param {string} paletteName\n * @returns {array}\n */\nexport function getColors(colorScheme, paletteName) {\n    switch (paletteName) {\n        case \"odoo\":\n            return colorScheme === \"dark\" ? COLORS_ENT_DARK : COLORS_ENT_BRIGHT;\n        case \"sm\":\n            return COLORS_SM;\n        case \"md\":\n            return COLORS_MD;\n        case \"lg\":\n            return COLORS_LG;\n        default:\n            return COLORS_XL;\n    }\n}\n\n/**\n * @param {number} index\n * @param {string} colorScheme\n * @returns {string}\n */\nexport function getColor(index, colorScheme, paletteSizeOrName) {\n    let paletteName;\n    if (paletteSizeOrName === \"odoo\") {\n        paletteName = \"odoo\";\n    } else if (paletteSizeOrName <= 6 || paletteSizeOrName === \"sm\") {\n        paletteName = \"sm\";\n    } else if (paletteSizeOrName <= 12 || paletteSizeOrName === \"md\") {\n        paletteName = \"md\";\n    } else if (paletteSizeOrName <= 24 || paletteSizeOrName === \"lg\") {\n        paletteName = \"lg\";\n    } else {\n        paletteName = \"xl\";\n    }\n    const colors = getColors(colorScheme, paletteName);\n    return colors[index % colors.length];\n}\n\nexport const DEFAULT_BG = \"#d3d3d3\";\n\nexport function getBorderWhite(colorScheme) {\n    return colorScheme === \"dark\" ? \"rgba(38, 42, 54, .2)\" : \"rgba(249,250,251, .2)\";\n}\n\nconst RGB_REGEX = /^#?([a-f\\d]{2})([a-f\\d]{2})([a-f\\d]{2})$/i;\n\n/**\n * @param {string} hex\n * @param {number} opacity\n * @returns {string}\n */\nexport function hexToRGBA(hex, opacity) {\n    const rgb = RGB_REGEX.exec(hex)\n        .slice(1, 4)\n        .map((n) => parseInt(n, 16))\n        .join(\",\");\n    return `rgba(${rgb},${opacity})`;\n}\n\n/**\n * Used to return custom colors depending on the color scheme\n * @param {string} colorScheme\n * @param {string} brightModeColor\n * @param {string} darkModeColor\n * @returns {string|Number|Boolean}\n */\n\nexport function getCustomColor(colorScheme, brightModeColor, darkModeColor) {\n    if (darkModeColor === undefined) {\n        return brightModeColor;\n    } else {\n        return colorScheme === \"dark\" ? darkModeColor : brightModeColor;\n    }\n}\n\n/**\n * Used to lighten a color\n * @param {string} color\n * @param {number} factor\n * @returns {string}\n */\nexport function lightenColor(color, factor) {\n    factor = clamp(factor, 0, 1);\n\n    let r = parseInt(color.substring(1, 3), 16);\n    let g = parseInt(color.substring(3, 5), 16);\n    let b = parseInt(color.substring(5, 7), 16);\n\n    r = Math.round(r + (255 - r) * factor);\n    g = Math.round(g + (255 - g) * factor);\n    b = Math.round(b + (255 - b) * factor);\n\n    r = r.toString(16).padStart(2, \"0\");\n    g = g.toString(16).padStart(2, \"0\");\n    b = b.toString(16).padStart(2, \"0\");\n\n    return `#${r}${g}${b}`;\n}\n\n/**\n * Used to darken a color\n * @param {string} color\n * @param {number} factor\n * @returns {string}\n */\nexport function darkenColor(color, factor) {\n    factor = clamp(factor, 0, 1);\n\n    let r = parseInt(color.substring(1, 3), 16);\n    let g = parseInt(color.substring(3, 5), 16);\n    let b = parseInt(color.substring(5, 7), 16);\n\n    r = Math.round(r * (1 - factor));\n    g = Math.round(g * (1 - factor));\n    b = Math.round(b * (1 - factor));\n\n    r = r.toString(16).padStart(2, \"0\");\n    g = g.toString(16).padStart(2, \"0\");\n    b = b.toString(16).padStart(2, \"0\");\n\n    return `#${r}${g}${b}`;\n}\n", "import { registry } from \"@web/core/registry\";\n\nconst commandCategoryRegistry = registry.category(\"command_categories\");\ncommandCategoryRegistry\n    .add(\"app\", {}, { sequence: 10 })\n    .add(\"smart_action\", {}, { sequence: 15 })\n    .add(\"actions\", {}, { sequence: 30 })\n    .add(\"default\", {}, { sequence: 50 })\n    .add(\"view_switcher\", {}, { sequence: 100 })\n    .add(\"debug\", {}, { sequence: 110 })\n    .add(\"disabled\", {});\n", "import { useService } from \"@web/core/utils/hooks\";\n\nimport { useEffect } from \"@odoo/owl\";\n\n/**\n * @typedef {import(\"./command_service\").CommandOptions} CommandOptions\n */\n\n/**\n * This hook will subscribe/unsubscribe the given subscription\n * when the caller component will mount/unmount.\n *\n * @param {string} name\n * @param {()=>(void | import(\"@web/core/commands/command_palette\").CommandPaletteConfig)} action\n * @param {CommandOptions} [options]\n */\nexport function useCommand(name, action, options = {}) {\n    const commandService = useService(\"command\");\n    useEffect(\n        () => commandService.add(name, action, options),\n        () => []\n    );\n}\n", "import { Dialog } from \"@web/core/dialog/dialog\";\nimport { useHotkey } from \"@web/core/hotkeys/hotkey_hook\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { KeepLast, Race } from \"@web/core/utils/concurrency\";\nimport { useAutofocus, useService } from \"@web/core/utils/hooks\";\nimport { scrollTo } from \"@web/core/utils/scrolling\";\nimport { fuzzyLookup } from \"@web/core/utils/search\";\nimport { debounce } from \"@web/core/utils/timing\";\nimport { isMacOS, isMobileOS } from \"@web/core/browser/feature_detection\";\nimport { highlightText } from \"@web/core/utils/html\";\n\nimport {\n    Component,\n    onWillStart,\n    onWillDestroy,\n    EventBus,\n    useRef,\n    useState,\n    markRaw,\n    useExternalListener,\n} from \"@odoo/owl\";\n\nconst DEFAULT_PLACEHOLDER = _t(\"Search...\");\nconst DEFAULT_EMPTY_MESSAGE = _t(\"No result found\");\nconst FUZZY_NAMESPACES = [\"default\"];\n\n/**\n * @typedef {import(\"./command_service\").Command} Command\n */\n\n/**\n * @typedef {Command & {\n *  Component?: Component;\n *  props?: object;\n * }} CommandItem\n */\n\n/**\n * @typedef {{\n *  namespace?: string;\n *  provide: ()=>CommandItem[];\n * }} Provider\n */\n\n/**\n * @typedef {{\n *  categories: string[];\n *  debounceDelay: number;\n *  emptyMessage: string;\n *  placeholder: string;\n * }} NamespaceConfig\n */\n\n/**\n * @typedef {{\n *  configByNamespace?: {[namespace: string]: NamespaceConfig};\n *  FooterComponent?: Component;\n *  providers: Provider[];\n *  searchValue?: string;\n * }} CommandPaletteConfig\n */\n\n/**\n * Util used to filter commands that are within category.\n * Note: for the default category, also get all commands having invalid category.\n *\n * @param {string} categoryName the category key\n * @param {string[]} categories\n * @returns an array filter predicate\n */\nfunction commandsWithinCategory(categoryName, categories) {\n    return (cmd) => {\n        const inCurrentCategory = categoryName === cmd.category;\n        const fallbackCategory = categoryName === \"default\" && !categories.includes(cmd.category);\n        return inCurrentCategory || fallbackCategory;\n    };\n}\n\nexport class DefaultCommandItem extends Component {\n    static template = \"web.DefaultCommandItem\";\n    static props = {\n        slots: { type: Object, optional: true },\n        // Props send by the command palette:\n        hotkey: { type: String, optional: true },\n        hotkeyOptions: { type: String, optional: true },\n        name: { type: String, optional: true },\n        searchValue: { type: String, optional: true },\n        executeCommand: { type: Function, optional: true },\n    };\n}\n\nexport class CommandPalette extends Component {\n    static template = \"web.CommandPalette\";\n    static components = { Dialog };\n    static lastSessionId = 0;\n    static props = {\n        bus: { type: EventBus, optional: true },\n        close: Function,\n        config: Object,\n        closeMe: { type: Function, optional: true },\n    };\n\n    setup() {\n        if (this.props.bus) {\n            const setConfig = ({ detail }) => this.setCommandPaletteConfig(detail);\n            this.props.bus.addEventListener(`SET-CONFIG`, setConfig);\n            onWillDestroy(() => this.props.bus.removeEventListener(`SET-CONFIG`, setConfig));\n        }\n\n        this.keyId = 1;\n        this.race = new Race();\n        this.keepLast = new KeepLast();\n        this._sessionId = CommandPalette.lastSessionId++;\n        this.DefaultCommandItem = DefaultCommandItem;\n        this.activeElement = useService(\"ui\").activeElement;\n        this.inputRef = useAutofocus();\n\n        useHotkey(\"Enter\", () => this.executeSelectedCommand(), { bypassEditableProtection: true });\n        useHotkey(\"Control+Enter\", () => this.executeSelectedCommand(true), {\n            bypassEditableProtection: true,\n        });\n        useHotkey(\"ArrowUp\", () => this.selectCommandAndScrollTo(\"PREV\"), {\n            bypassEditableProtection: true,\n            allowRepeat: true,\n        });\n        useHotkey(\"ArrowDown\", () => this.selectCommandAndScrollTo(\"NEXT\"), {\n            bypassEditableProtection: true,\n            allowRepeat: true,\n        });\n        useExternalListener(window, \"mousedown\", this.onWindowMouseDown);\n\n        /**\n         * @type {{ commands: CommandItem[],\n         *          emptyMessage: string,\n         *          FooterComponent: Component,\n         *          namespace: string,\n         *          placeholder: string,\n         *          searchValue: string,\n         *          selectedCommand: CommandItem }}\n         */\n        this.state = useState({});\n\n        this.root = useRef(\"root\");\n        this.listboxRef = useRef(\"listbox\");\n\n        onWillStart(() => this.setCommandPaletteConfig(this.props.config));\n    }\n\n    get commandsByCategory() {\n        const categories = [];\n        for (const category of this.categoryKeys) {\n            const commands = this.state.commands.filter(\n                commandsWithinCategory(category, this.categoryKeys)\n            );\n            if (commands.length) {\n                categories.push({\n                    commands,\n                    name: this.categoryNames[category],\n                    keyId: category,\n                });\n            }\n        }\n        return categories;\n    }\n\n    /**\n     * Apply the new config to the command pallet\n     * @param {CommandPaletteConfig} config\n     */\n    async setCommandPaletteConfig(config) {\n        this.configByNamespace = config.configByNamespace || {};\n        this.state.FooterComponent = config.FooterComponent;\n\n        this.providersByNamespace = { default: [] };\n        for (const provider of config.providers) {\n            const namespace = provider.namespace || \"default\";\n            if (namespace in this.providersByNamespace) {\n                this.providersByNamespace[namespace].push(provider);\n            } else {\n                this.providersByNamespace[namespace] = [provider];\n            }\n        }\n\n        const { namespace, searchValue } = this.processSearchValue(config.searchValue || \"\");\n        this.switchNamespace(namespace);\n        this.state.searchValue = searchValue;\n        await this.race.add(this.search(searchValue));\n    }\n\n    /**\n     * Modifies the commands to be displayed according to the namespace and the options.\n     * Selects the first command in the new list.\n     * @param {string} namespace\n     * @param {object} options\n     */\n    async setCommands(namespace, options = {}) {\n        this.categoryKeys = [\"default\"];\n        this.categoryNames = {};\n        const proms = this.providersByNamespace[namespace].map((provider) => {\n            const { provide } = provider;\n            const result = provide(this.env, options);\n            return result;\n        });\n        let commands = (await this.keepLast.add(Promise.all(proms))).flat();\n        const namespaceConfig = this.configByNamespace[namespace] || {};\n        if (options.searchValue && FUZZY_NAMESPACES.includes(namespace)) {\n            commands = fuzzyLookup(options.searchValue, commands, (c) => c.name);\n        } else {\n            // we have to sort the commands by category to avoid navigation issues with the arrows\n            if (namespaceConfig.categories) {\n                let commandsSorted = [];\n                this.categoryKeys = namespaceConfig.categories;\n                this.categoryNames = namespaceConfig.categoryNames || {};\n                if (!this.categoryKeys.includes(\"default\")) {\n                    this.categoryKeys.push(\"default\");\n                }\n                for (const category of this.categoryKeys) {\n                    commandsSorted = commandsSorted.concat(\n                        commands.filter(commandsWithinCategory(category, this.categoryKeys))\n                    );\n                }\n                commands = commandsSorted;\n            }\n        }\n\n        this.state.commands = markRaw(\n            commands.slice(0, 100).map((command) => ({\n                ...command,\n                keyId: this.keyId++,\n                text: highlightText(options.searchValue, command.name, \"fw-bolder text-primary\"),\n            }))\n        );\n        this.selectCommand(this.state.commands.length ? 0 : -1);\n        this.mouseSelectionActive = false;\n        this.state.emptyMessage = (\n            namespaceConfig.emptyMessage || DEFAULT_EMPTY_MESSAGE\n        ).toString();\n    }\n\n    selectCommand(index) {\n        if (index === -1 || index >= this.state.commands.length) {\n            this.state.selectedCommand = null;\n            return;\n        }\n        this.state.selectedCommand = markRaw(this.state.commands[index]);\n    }\n\n    selectCommandAndScrollTo(type) {\n        // In case the mouse is on the palette command, it avoids the selection\n        // of a command caused by a scroll.\n        this.mouseSelectionActive = false;\n        const index = this.state.commands.indexOf(this.state.selectedCommand);\n        if (index === -1) {\n            return;\n        }\n        let nextIndex;\n        if (type === \"NEXT\") {\n            nextIndex = index < this.state.commands.length - 1 ? index + 1 : 0;\n        } else if (type === \"PREV\") {\n            nextIndex = index > 0 ? index - 1 : this.state.commands.length - 1;\n        }\n        this.selectCommand(nextIndex);\n\n        const command = this.listboxRef.el.querySelector(`#o_command_${nextIndex}`);\n        scrollTo(command, { scrollable: this.listboxRef.el });\n    }\n\n    onCommandClicked(event, index) {\n        event.preventDefault(); // Prevent redirect for commands with href\n        this.selectCommand(index);\n        const ctrlKey = isMacOS() ? event.metaKey : event.ctrlKey;\n        this.executeSelectedCommand(ctrlKey);\n    }\n\n    /**\n     * Execute the action related to the order.\n     * If this action returns a config, then we will use it in the command palette,\n     * otherwise we close the command palette.\n     * @param {CommandItem} command\n     */\n    async executeCommand(command) {\n        const config = await command.action();\n        if (config) {\n            this.setCommandPaletteConfig(config);\n        } else {\n            this.props.close();\n        }\n    }\n\n    async executeSelectedCommand(ctrlKey) {\n        await this.searchValuePromise;\n        const selectedCommand = this.state.selectedCommand;\n        if (selectedCommand) {\n            if (!ctrlKey) {\n                this.executeCommand(selectedCommand);\n            } else if (selectedCommand.href) {\n                window.open(selectedCommand.href, \"_blank\");\n            }\n        }\n    }\n\n    onCommandMouseEnter(index) {\n        if (this.mouseSelectionActive) {\n            this.selectCommand(index);\n        } else {\n            this.mouseSelectionActive = true;\n        }\n    }\n\n    async search(searchValue) {\n        this.state.isLoading = true;\n        try {\n            await this.setCommands(this.state.namespace, {\n                searchValue,\n                activeElement: this.activeElement,\n                sessionId: this._sessionId,\n            });\n        } finally {\n            this.state.isLoading = false;\n        }\n        if (this.inputRef.el) {\n            this.inputRef.el.focus();\n        }\n    }\n\n    debounceSearch(value) {\n        const { namespace, searchValue } = this.processSearchValue(value);\n        if (namespace !== \"default\" && this.state.namespace !== namespace) {\n            this.switchNamespace(namespace);\n        }\n        this.state.searchValue = searchValue;\n        this.searchValuePromise = this.lastDebounceSearch(searchValue).catch(() => {\n            this.searchValuePromise = null;\n        });\n    }\n\n    onSearchInput(ev) {\n        this.debounceSearch(ev.target.value);\n    }\n\n    onKeyDown(ev) {\n        if (ev.key.toLowerCase() === \"backspace\" && !ev.target.value.length && !ev.repeat) {\n            this.switchNamespace(\"default\");\n            this.state.searchValue = \"\";\n            this.searchValuePromise = this.lastDebounceSearch(\"\").catch(() => {\n                this.searchValuePromise = null;\n            });\n        }\n    }\n\n    /**\n     * Close the palette on outside click.\n     */\n    onWindowMouseDown(ev) {\n        if (!this.root.el.contains(ev.target)) {\n            this.props.close();\n        }\n    }\n\n    switchNamespace(namespace) {\n        if (this.lastDebounceSearch) {\n            this.lastDebounceSearch.cancel();\n        }\n        const namespaceConfig = this.configByNamespace[namespace] || {};\n        this.lastDebounceSearch = debounce(\n            (value) => this.search(value),\n            namespaceConfig.debounceDelay || 0\n        );\n        this.state.namespace = namespace;\n        this.state.placeholder = namespaceConfig.placeholder || DEFAULT_PLACEHOLDER.toString();\n    }\n\n    processSearchValue(searchValue) {\n        let namespace = \"default\";\n        if (searchValue.length && this.providersByNamespace[searchValue[0]]) {\n            namespace = searchValue[0];\n            searchValue = searchValue.slice(1);\n        }\n        return { namespace, searchValue };\n    }\n\n    get isMacOS() {\n        return isMacOS();\n    }\n    get isMobileOS() {\n        return isMobileOS();\n    }\n}\n", "import { registry } from \"@web/core/registry\";\nimport { CommandPalette } from \"./command_palette\";\n\nimport { Component, EventBus } from \"@odoo/owl\";\n\n/**\n * @typedef {import(\"./command_palette\").CommandPaletteConfig} CommandPaletteConfig\n * @typedef {import(\"../hotkeys/hotkey_service\").HotkeyOptions} HotkeyOptions\n */\n\n/**\n * @typedef {{\n *  name: string;\n *  action: ()=>(void | CommandPaletteConfig);\n *  category?: string;\n *  href?: string;\n *  className?: string;\n * }} Command\n */\n\n/**\n * @typedef {{\n *  category?: string;\n *  isAvailable?: ()=>(boolean);\n *  global?: boolean;\n *  hotkey?: string;\n *  hotkeyOptions?: HotkeyOptions\n * }} CommandOptions\n */\n\n/**\n * @typedef {Command & CommandOptions & {\n *  removeHotkey?: ()=>void;\n * }} CommandRegistration\n */\n\nconst commandCategoryRegistry = registry.category(\"command_categories\");\nconst commandProviderRegistry = registry.category(\"command_provider\");\nconst commandSetupRegistry = registry.category(\"command_setup\");\n\nclass DefaultFooter extends Component {\n    static template = \"web.DefaultFooter\";\n    static props = {\n        switchNamespace: { type: Function },\n    };\n    setup() {\n        this.elements = commandSetupRegistry\n            .getEntries()\n            .map((el) => ({ namespace: el[0], name: el[1].name }))\n            .filter((el) => el.name);\n    }\n\n    onClick(namespace) {\n        this.props.switchNamespace(namespace);\n    }\n}\n\nexport const commandService = {\n    dependencies: [\"dialog\", \"hotkey\", \"ui\"],\n    start(env, { dialog, hotkey: hotkeyService, ui }) {\n        /** @type {Map<CommandRegistration>} */\n        const registeredCommands = new Map();\n        let nextToken = 0;\n        let isPaletteOpened = false;\n        const bus = new EventBus();\n\n        hotkeyService.add(\"control+k\", openMainPalette, {\n            bypassEditableProtection: true,\n            global: true,\n        });\n\n        /**\n         * @param {CommandPaletteConfig} config command palette config merged with default config\n         * @param {Function} onClose called when the command palette is closed\n         * @returns the actual command palette config if the command palette is already open\n         */\n        function openMainPalette(config = {}, onClose) {\n            const configByNamespace = {};\n            for (const provider of commandProviderRegistry.getAll()) {\n                const namespace = provider.namespace || \"default\";\n                if (!configByNamespace[namespace]) {\n                    configByNamespace[namespace] = {\n                        categories: [],\n                        categoryNames: {},\n                    };\n                }\n            }\n\n            for (const [category, el] of commandCategoryRegistry.getEntries()) {\n                const namespace = el.namespace || \"default\";\n                const name = el.name;\n                if (namespace in configByNamespace) {\n                    configByNamespace[namespace].categories.push(category);\n                    configByNamespace[namespace].categoryNames[category] = name;\n                }\n            }\n\n            for (const [\n                namespace,\n                { emptyMessage, debounceDelay, placeholder },\n            ] of commandSetupRegistry.getEntries()) {\n                if (namespace in configByNamespace) {\n                    if (emptyMessage) {\n                        configByNamespace[namespace].emptyMessage = emptyMessage;\n                    }\n                    if (debounceDelay !== undefined) {\n                        configByNamespace[namespace].debounceDelay = debounceDelay;\n                    }\n                    if (placeholder) {\n                        configByNamespace[namespace].placeholder = placeholder;\n                    }\n                }\n            }\n\n            config = Object.assign(\n                {\n                    configByNamespace,\n                    FooterComponent: DefaultFooter,\n                    providers: commandProviderRegistry.getAll(),\n                },\n                config\n            );\n            return openPalette(config, onClose);\n        }\n\n        /**\n         * @param {CommandPaletteConfig} config\n         * @param {Function} onClose called when the command palette is closed\n         */\n        function openPalette(config, onClose) {\n            if (isPaletteOpened) {\n                bus.trigger(\"SET-CONFIG\", config);\n                return;\n            }\n\n            // Open Command Palette dialog\n            isPaletteOpened = true;\n            dialog.add(\n                CommandPalette,\n                {\n                    config,\n                    bus,\n                },\n                {\n                    onClose: () => {\n                        isPaletteOpened = false;\n                        if (onClose) {\n                            onClose();\n                        }\n                    },\n                }\n            );\n        }\n\n        /**\n         * @param {Command} command\n         * @param {CommandOptions} options\n         * @returns {number} token\n         */\n        function registerCommand(command, options) {\n            if (!command.name || !command.action || typeof command.action !== \"function\") {\n                throw new Error(\"A Command must have a name and an action function.\");\n            }\n            const registration = Object.assign({}, command, options);\n            if (registration.identifier) {\n                const commandsArray = Array.from(registeredCommands.values());\n                const sameName = commandsArray.find((com) => com.name === registration.name);\n                if (sameName) {\n                    if (registration.identifier !== sameName.identifier) {\n                        registration.name += ` (${registration.identifier})`;\n                        sameName.name += ` (${sameName.identifier})`;\n                    }\n                } else {\n                    const sameFullName = commandsArray.find(\n                        (com) => com.name === registration.name + `(${registration.identifier})`\n                    );\n                    if (sameFullName) {\n                        registration.name += ` (${registration.identifier})`;\n                    }\n                }\n            }\n            if (registration.hotkey) {\n                const action = async () => {\n                    const commandService = env.services.command;\n                    const config = await command.action();\n                    if (!isPaletteOpened && config) {\n                        commandService.openPalette(config);\n                    }\n                };\n                registration.removeHotkey = hotkeyService.add(registration.hotkey, action, {\n                    ...options.hotkeyOptions,\n                    global: registration.global,\n                    isAvailable: (...args) => {\n                        let available = true;\n                        if (registration.isAvailable) {\n                            available = registration.isAvailable(...args);\n                        }\n                        if (available && options.hotkeyOptions?.isAvailable) {\n                            available = options.hotkeyOptions?.isAvailable(...args);\n                        }\n                        return available;\n                    },\n                });\n            }\n\n            const token = nextToken++;\n            registeredCommands.set(token, registration);\n            if (!options.activeElement) {\n                // Due to the way elements are mounted in the DOM by Owl (bottom-to-top),\n                // we need to wait the next micro task tick to set the context activate\n                // element of the subscription.\n                Promise.resolve().then(() => {\n                    registration.activeElement = ui.activeElement;\n                });\n            }\n\n            return token;\n        }\n\n        /**\n         * Unsubscribes the token corresponding subscription.\n         *\n         * @param {number} token\n         */\n        function unregisterCommand(token) {\n            const cmd = registeredCommands.get(token);\n            if (cmd && cmd.removeHotkey) {\n                cmd.removeHotkey();\n            }\n            registeredCommands.delete(token);\n        }\n\n        return {\n            /**\n             * @param {string} name\n             * @param {()=>(void | CommandPaletteConfig)} action\n             * @param {CommandOptions} [options]\n             * @returns {() => void}\n             */\n            add(name, action, options = {}) {\n                const token = registerCommand({ name, action }, options);\n                return () => {\n                    unregisterCommand(token);\n                };\n            },\n            /**\n             * @param {HTMLElement} activeElement\n             * @returns {Command[]}\n             */\n            getCommands(activeElement) {\n                return [...registeredCommands.values()].filter(\n                    (command) => command.activeElement === activeElement || command.global\n                );\n            },\n            openMainPalette,\n            openPalette,\n        };\n    },\n};\n\nregistry.category(\"services\").add(\"command\", commandService);\n", "import { isMacOS } from \"@web/core/browser/feature_detection\";\nimport { useHotkey } from \"@web/core/hotkeys/hotkey_hook\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { capitalize } from \"@web/core/utils/strings\";\nimport { getVisibleElements } from \"@web/core/utils/ui\";\nimport { DefaultCommandItem } from \"./command_palette\";\n\nimport { Component } from \"@odoo/owl\";\n\nconst commandSetupRegistry = registry.category(\"command_setup\");\ncommandSetupRegistry.add(\"default\", {\n    emptyMessage: _t(\"No command found\"),\n    placeholder: _t(\"Search for a command...\"),\n});\n\nexport class HotkeyCommandItem extends Component {\n    static template = \"web.HotkeyCommandItem\";\n    static props = [\"hotkey\", \"hotkeyOptions?\", \"name?\", \"searchValue?\", \"executeCommand\", \"slots\"];\n    setup() {\n        useHotkey(this.props.hotkey, this.props.executeCommand);\n    }\n\n    getKeysToPress(command) {\n        const { hotkey } = command;\n        let result = hotkey.split(\"+\");\n        if (isMacOS()) {\n            result = result\n                .map((x) => x.replace(\"control\", \"command\"))\n                .map((x) => x.replace(\"alt\", \"control\"));\n        }\n        return result.map((key) => key.toUpperCase());\n    }\n}\n\nconst commandCategoryRegistry = registry.category(\"command_categories\");\nconst commandProviderRegistry = registry.category(\"command_provider\");\ncommandProviderRegistry.add(\"command\", {\n    provide: (env, options = {}) => {\n        const commands = env.services.command\n            .getCommands(options.activeElement)\n            .map((cmd) => {\n                cmd.category = commandCategoryRegistry.contains(cmd.category)\n                    ? cmd.category\n                    : \"default\";\n                return cmd;\n            })\n            .filter((command) => command.isAvailable === undefined || command.isAvailable());\n        // Filter out same category dupplicate commands\n        const uniqueCommands = commands.filter((obj, index) => {\n            return (\n                index ===\n                commands.findIndex((o) => obj.name === o.name && obj.category === o.category)\n            );\n        });\n        return uniqueCommands.map((command) => ({\n            Component: command.hotkey ? HotkeyCommandItem : DefaultCommandItem,\n            action: command.action,\n            category: command.category,\n            name: command.name,\n            props: {\n                hotkey: command.hotkey,\n                hotkeyOptions: command.hotkeyOptions,\n            },\n        }));\n    },\n});\n\ncommandProviderRegistry.add(\"data-hotkeys\", {\n    provide: (env, options = {}) => {\n        const commands = [];\n        const overlayModifier = registry.category(\"services\").get(\"hotkey\").overlayModifier;\n        // Also retrieve all hotkeyables elements\n        for (const el of getVisibleElements(\n            options.activeElement,\n            \"[data-hotkey]:not(:disabled)\"\n        )) {\n            const closest = el.closest(\"[data-command-category]\");\n            const category = closest ? closest.dataset.commandCategory : \"default\";\n            if (category === \"disabled\") {\n                continue;\n            }\n\n            const description =\n                el.title ||\n                el.dataset.bsOriginalTitle || // LEGACY: bootstrap moves title to data-bs-original-title\n                el.dataset.tooltip ||\n                el.placeholder ||\n                (el.innerText &&\n                    `${el.innerText.slice(0, 50)}${el.innerText.length > 50 ? \"...\" : \"\"}`) ||\n                _t(\"no description provided\");\n\n            commands.push({\n                Component: HotkeyCommandItem,\n                action: () => {\n                    // AAB: not sure it is enough, we might need to trigger all events that occur when you actually click\n                    el.focus();\n                    el.click();\n                },\n                category,\n                name: capitalize(description.trim().toLowerCase()),\n                props: {\n                    hotkey: `${overlayModifier}+${el.dataset.hotkey}`,\n                },\n            });\n        }\n        return commands;\n    },\n});\n", "import { Dialog } from \"../dialog/dialog\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { useChildRef } from \"@web/core/utils/hooks\";\n\nimport { Component } from \"@odoo/owl\";\n\nexport const deleteConfirmationMessage = _t(\n    `Ready to make your record disappear into thin air? Are you sure?\nIt will be gone forever!\n\nThink twice before you click that 'Delete' button!`\n);\n\nexport class ConfirmationDialog extends Component {\n    static template = \"web.ConfirmationDialog\";\n    static components = { Dialog };\n    static props = {\n        close: Function,\n        title: {\n            validate: (m) => {\n                return (\n                    typeof m === \"string\" ||\n                    (typeof m === \"object\" && typeof m.toString === \"function\")\n                );\n            },\n            optional: true,\n        },\n        body: { type: String, optional: true },\n        confirm: { type: Function, optional: true },\n        confirmLabel: { type: String, optional: true },\n        confirmClass: { type: String, optional: true },\n        cancel: { type: Function, optional: true },\n        cancelLabel: { type: String, optional: true },\n        dismiss: { type: Function, optional: true },\n    };\n    static defaultProps = {\n        confirmLabel: _t(\"Ok\"),\n        cancelLabel: _t(\"Cancel\"),\n        confirmClass: \"btn-primary\",\n        title: _t(\"Confirmation\"),\n    };\n\n    setup() {\n        this.env.dialogData.dismiss = () => this._dismiss();\n        this.modalRef = useChildRef();\n        this.isProcess = false;\n    }\n\n    async _cancel() {\n        return this.execButton(this.props.cancel);\n    }\n\n    async _confirm() {\n        return this.execButton(this.props.confirm);\n    }\n\n    async _dismiss() {\n        return this.execButton(this.props.dismiss || this.props.cancel);\n    }\n\n    setButtonsDisabled(disabled) {\n        this.isProcess = disabled;\n        if (!this.modalRef.el) {\n            return; // safety belt for stable versions\n        }\n        for (const button of [...this.modalRef.el.querySelectorAll(\".modal-footer button\")]) {\n            button.disabled = disabled;\n        }\n    }\n\n    async execButton(callback) {\n        if (this.isProcess) {\n            return;\n        }\n        this.setButtonsDisabled(true);\n        if (callback) {\n            let shouldClose;\n            try {\n                shouldClose = await callback();\n            } catch (e) {\n                this.props.close();\n                throw e;\n            }\n            if (shouldClose === false) {\n                this.setButtonsDisabled(false);\n                return;\n            }\n        }\n        this.props.close();\n    }\n}\n\nexport class AlertDialog extends ConfirmationDialog {\n    static template = \"web.AlertDialog\";\n    static props = {\n        ...ConfirmationDialog.props,\n        contentClass: { type: String, optional: true },\n    };\n    static defaultProps = {\n        ...ConfirmationDialog.defaultProps,\n        title: _t(\"Alert\"),\n    };\n}\n", "import { evaluateExpr, parseExpr } from \"./py_js/py\";\nimport { BUILTINS } from \"./py_js/py_builtin\";\nimport { evaluate } from \"./py_js/py_interpreter\";\n\n/**\n * @typedef {{\n *  lang?: string;\n *  tz?: string;\n *  uid?: number | false;\n *  [key: string]: any;\n * }} Context\n * @typedef {Context | string | undefined} ContextDescription\n */\n\n/**\n * Create an evaluated context from an arbitrary list of context representations.\n * The evaluated context in construction is used along the way to evaluate further parts.\n *\n * @param {ContextDescription[]} contexts\n * @param {Context} [initialEvaluationContext] optional evaluation context to start from.\n * @returns {Context}\n */\nexport function makeContext(contexts, initialEvaluationContext) {\n    const evaluationContext = Object.assign({}, initialEvaluationContext);\n    const context = {};\n    for (let ctx of contexts) {\n        if (ctx !== \"\") {\n            ctx = typeof ctx === \"string\" ? evaluateExpr(ctx, evaluationContext) : ctx;\n            Object.assign(context, ctx);\n            Object.assign(evaluationContext, context); // is this behavior really wanted ?\n        }\n    }\n    return context;\n}\n\n/**\n * Extract a partial list of variable names found in the AST.\n * Note that it is not complete. It is used as an heuristic to avoid\n * evaluating expressions that we know for sure will fail.\n *\n * @param {AST} ast\n * @returns string[]\n */\nfunction getPartialNames(ast) {\n    if (ast.type === 5) {\n        return [ast.value];\n    }\n    if (ast.type === 6) {\n        return getPartialNames(ast.right);\n    }\n    if (ast.type === 14 || ast.type === 7) {\n        return getPartialNames(ast.left).concat(getPartialNames(ast.right));\n    }\n    if (ast.type === 15) {\n        return getPartialNames(ast.obj);\n    }\n    return [];\n}\n\n/**\n * Allow to evaluate a context with an incomplete evaluation context. The evaluated context only\n * contains keys whose values are static or can be evaluated with the given evaluation context.\n *\n * @param {string} context\n * @param {Context} [evaluationContext={}]\n * @returns {Context}\n */\nexport function evalPartialContext(_context, evaluationContext = {}) {\n    const ast = parseExpr(_context);\n    const context = {};\n    for (const key in ast.value) {\n        const value = ast.value[key];\n        if (\n            getPartialNames(value).some((name) => !(name in evaluationContext || name in BUILTINS))\n        ) {\n            continue;\n        }\n        try {\n            context[key] = evaluate(value, evaluationContext);\n        } catch {\n            // ignore this key as we can't evaluate its value\n        }\n    }\n    return context;\n}\n", "import { browser } from \"@web/core/browser/browser\";\nimport { Tooltip } from \"@web/core/tooltip/tooltip\";\nimport { usePopover } from \"@web/core/popover/popover_hook\";\nimport { Component, useRef } from \"@odoo/owl\";\n\nexport class CopyButton extends Component {\n    static template = \"web.CopyButton\";\n    static props = {\n        className: { type: String, optional: true },\n        copyText: { type: String, optional: true },\n        disabled: { type: Boolean, optional: true },\n        successText: { type: String, optional: true },\n        icon: { type: String, optional: true },\n        content: { type: [String, Object, Function], optional: true },\n    };\n\n    setup() {\n        this.button = useRef(\"button\");\n        this.popover = usePopover(Tooltip);\n    }\n\n    showTooltip() {\n        this.popover.open(this.button.el, { tooltip: this.props.successText });\n        browser.setTimeout(this.popover.close, 800);\n    }\n\n    async onClick() {\n        let write, content;\n        if (typeof this.props.content === \"function\") {\n            content = this.props.content();\n        } else {\n            content = this.props.content;\n        }\n        // any kind of content can be copied into the clipboard using\n        // the appropriate native methods\n        if (typeof content === \"string\" || content instanceof String) {\n            write = (value) => browser.navigator.clipboard.writeText(value);\n        } else {\n            write = (value) => browser.navigator.clipboard.write(value);\n        }\n        try {\n            await write(content);\n        } catch (error) {\n            return browser.console.warn(error);\n        }\n        this.showTooltip();\n    }\n}\n", "import { reactive } from \"@odoo/owl\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { user } from \"@web/core/user\";\nimport { formatFloat, humanNumber } from \"@web/core/utils/numbers\";\nimport { nbsp } from \"@web/core/utils/strings\";\nimport { session } from \"@web/session\";\n\nexport const currencies = session.currencies || {};\n// to make sure code is reading currencies from here\ndelete session.currencies;\n\nexport function getCurrency(id) {\n    return currencies[id];\n}\n\nexport async function getCurrencyRates() {\n    const rates = reactive({});\n\n    function recordsToRates(records) {\n        return Object.fromEntries(records.map((r) => [r.id, r.inverse_rate]));\n    }\n\n    const model = \"res.currency\";\n    const method = \"read\";\n    const url = `/web/dataset/call_kw/${model}/${method}`;\n    const context = {\n        ...user.context,\n        to_currency: user.activeCompany.currency_id,\n    };\n    const params = {\n        model,\n        method,\n        args: [Object.keys(currencies).map(Number), [\"inverse_rate\"]],\n        kwargs: { context },\n    };\n    const records = await rpc(url, params, {\n        cache: {\n            type: \"disk\",\n            update: \"once\",\n            callback: (records, hasChanged) => {\n                if (hasChanged) {\n                    Object.assign(rates, recordsToRates(records));\n                }\n            },\n        },\n    });\n    Object.assign(rates, recordsToRates(records));\n    return rates;\n}\n\n/**\n * Returns a string representing a monetary value. The result takes into account\n * the user settings (to display the correct decimal separator, currency, ...).\n *\n * @param {number} value the value that should be formatted\n * @param {number} [currencyId] the id of the 'res.currency' to use\n * @param {Object} [options]\n *   additional options to override the values in the python description of the\n *   field.\n * @param {Object} [options.data] a mapping of field names to field values,\n *   required with options.currencyField\n * @param {boolean} [options.noSymbol] this currency has not a sympbol\n * @param {boolean} [options.humanReadable] if true, large numbers are formatted\n *   to a human readable format.\n * @param {number} [options.minDigits] see @humanNumber\n * @param {boolean} [options.trailingZeros] if false, numbers will have zeros\n *  to the right of the last non-zero digit hidden\n * @param {[number, number]} [options.digits] the number of digits that should\n *   be used, instead of the default digits precision in the field.  The first\n *   number is always ignored (legacy constraint)\n * @returns {string}\n */\nexport function formatCurrency(amount, currencyId, options = {}) {\n    const currency = getCurrency(currencyId);\n    const digits = options.digits || (currency && currency.digits);\n\n    let formattedAmount;\n    if (options.humanReadable) {\n        formattedAmount = humanNumber(amount, {\n            decimals: digits ? digits[1] : 2,\n            minDigits: options.minDigits,\n        });\n    } else {\n        formattedAmount = formatFloat(amount, { digits, trailingZeros: options.trailingZeros });\n    }\n\n    if (!currency || options.noSymbol) {\n        return formattedAmount;\n    }\n    const formatted = [currency.symbol, formattedAmount];\n    if (currency.position === \"after\") {\n        formatted.reverse();\n    }\n    return formatted.join(nbsp);\n}\n", "import { Component } from \"@odoo/owl\";\nimport { omit } from \"../utils/objects\";\nimport { DateTimePicker } from \"./datetime_picker\";\nimport { useDateTimePicker } from \"./datetime_picker_hook\";\n\n/**\n * @typedef {import(\"./datetime_picker\").DateTimePickerProps & {\n *  format?: string;\n *  id?: string;\n *  onApply?: (value: DateTime) => any;\n *  onChange?: (value: DateTime) => any;\n *  placeholder?: string;\n * }} DateTimeInputProps\n */\n\nconst dateTimeInputOwnProps = {\n    format: { type: String, optional: true },\n    id: { type: String, optional: true },\n    class: { type: String, optional: true },\n    onChange: { type: Function, optional: true },\n    onApply: { type: Function, optional: true },\n    placeholder: { type: String, optional: true },\n    disabled: { type: Boolean, optional: true },\n};\n\n/** @extends {Component<DateTimeInputProps>} */\nexport class DateTimeInput extends Component {\n    static props = {\n        ...DateTimePicker.props,\n        ...dateTimeInputOwnProps,\n    };\n\n    static template = \"web.DateTimeInput\";\n\n    setup() {\n        const getPickerProps = () => omit(this.props, ...Object.keys(dateTimeInputOwnProps));\n\n        useDateTimePicker({\n            format: this.props.format,\n            showSeconds: this.props.rounding <= 0,\n            get pickerProps() {\n                return getPickerProps();\n            },\n            onApply: (...args) => this.props.onApply?.(...args),\n            onChange: (...args) => this.props.onChange?.(...args),\n        });\n    }\n}\n", "import { Component, onWillRender, onWillUpdateProps, useState } from \"@odoo/owl\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { MAX_VALID_DATE, MIN_VALID_DATE, clampDate, isInRange, today } from \"../l10n/dates\";\nimport { localization } from \"../l10n/localization\";\nimport { ensureArray } from \"../utils/arrays\";\nimport { TimePicker } from \"@web/core/time_picker/time_picker\";\nimport { Time } from \"@web/core/l10n/time\";\n\nconst { DateTime, Info } = luxon;\n\n/**\n * @typedef DateItem\n * @property {string} id\n * @property {boolean} includesToday\n * @property {boolean} isOutOfRange\n * @property {boolean} isValid\n * @property {string} label\n * @property {DateRange} range\n * @property {string} extraClass\n *\n * @typedef {\"today\" | NullableDateTime} DateLimit\n *\n * @typedef {[DateTime, DateTime]} DateRange\n *\n * @typedef {luxon[\"DateTime\"][\"prototype\"]} DateTime\n *\n * @typedef DateTimePickerProps\n * @property {number} [focusedDateIndex=0]\n * @property {boolean} [showWeekNumbers=true]\n * @property {DaysOfWeekFormat} [daysOfWeekFormat=\"narrow\"]\n * @property {DateLimit} [maxDate]\n * @property {PrecisionLevel} [maxPrecision=\"decades\"]\n * @property {DateLimit} [minDate]\n * @property {PrecisionLevel} [minPrecision=\"days\"]\n * @property {() => any} [onReset]\n * @property {(value: DateTime | DateRange, unit: \"date\" | \"time\") => any} [onSelect]\n * @property {() => any} [onToggleRange]\n * @property {boolean} [range]\n * @property {number} [rounding=5] the rounding in minutes, pass 0 to show seconds, pass 1 to avoid\n *  rounding minutes without displaying seconds.\n * @property {() => boolean} [showRangeToggler]\n * @property {{ buttons?: any }} [slots]\n * @property {\"date\" | \"datetime\"} [type]\n * @property {NullableDateTime | NullableDateRange} [value]\n * @property {(date: DateTime) => boolean} [isDateValid]\n * @property {(date: DateTime) => string} [dayCellClass]\n *\n * @typedef {DateItem | MonthItem} Item\n *\n * @typedef MonthItem\n * @property {[string, string][]} daysOfWeek\n * @property {string} id\n * @property {number} number\n * @property {WeekItem[]} weeks\n *\n * @typedef {import(\"@web/core/l10n/dates\").NullableDateTime} NullableDateTime\n *\n * @typedef {import(\"@web/core/l10n/dates\").NullableDateRange} NullableDateRange\n *\n * @typedef PrecisionInfo\n * @property {(date: DateTime, params: Partial<DateTimePickerProps>) => string} getTitle\n * @property {(date: DateTime, params: Partial<DateTimePickerProps>) => Item[]} getItems\n * @property {string} mainTitle\n * @property {string} nextTitle\n * @property {string} prevTitle\n * @property {Record<string, number>} step\n *\n * @typedef {\"days\" | \"months\" | \"years\" | \"decades\"} PrecisionLevel\n *\n * @typedef {\"short\" | \"narrow\"} DaysOfWeekFormat\n *\n * @typedef WeekItem\n * @property {DateItem[]} days\n * @property {number} number\n */\n\n/**\n * @param {DateTime} date\n */\nconst getStartOfDecade = (date) => Math.floor(date.year / 10) * 10;\n\n/**\n * @param {DateTime} date\n */\nconst getStartOfCentury = (date) => Math.floor(date.year / 100) * 100;\n\n/**\n * @param {DateTime} date\n */\nconst getStartOfWeek = (date) => {\n    const { weekStart } = localization;\n    return date.set({ weekday: date.weekday < weekStart ? weekStart - 7 : weekStart });\n};\n\n/**\n * @param {number} min\n * @param {number} max\n */\nconst numberRange = (min, max) => [...Array(max - min)].map((_, i) => i + min);\n\n/**\n * @param {NullableDateTime | \"today\"} value\n * @param {NullableDateTime | \"today\"} defaultValue\n */\nconst parseLimitDate = (value, defaultValue) =>\n    clampDate(value === \"today\" ? today() : value || defaultValue, MIN_VALID_DATE, MAX_VALID_DATE);\n\n/**\n * @param {Object} params\n * @param {boolean} [params.isOutOfRange=false]\n * @param {boolean} [params.isValid=true]\n * @param {keyof DateTime} params.label\n * @param {string} [params.extraClass]\n * @param {[DateTime, DateTime]} params.range\n * @returns {DateItem}\n */\nconst toDateItem = ({ isOutOfRange = false, isValid = true, label, range, extraClass }) => ({\n    id: range[0].toISODate(),\n    includesToday: isInRange(today(), range),\n    isOutOfRange,\n    isValid,\n    label: String(range[0][label]),\n    range,\n    extraClass,\n});\n\n/**\n * @param {DateItem[]} weekDayItems\n * @returns {WeekItem}\n */\nconst toWeekItem = (weekDayItems) => ({\n    number: weekDayItems[3].range[0].weekNumber,\n    days: weekDayItems,\n});\n\n/**\n * Precision levels\n * @type {Map<PrecisionLevel, PrecisionInfo>}\n */\nconst PRECISION_LEVELS = new Map()\n    .set(\"days\", {\n        mainTitle: _t(\"Select month\"),\n        nextTitle: _t(\"Next month\"),\n        prevTitle: _t(\"Previous month\"),\n        step: { month: 1 },\n        getTitle: (date) => `${date.monthLong} ${date.year}`,\n        getItems: (date, { maxDate, minDate, showWeekNumbers, isDateValid, dayCellClass }) => {\n            const startDates = [date];\n\n            /** @type {WeekItem[]} */\n            const lastWeeks = [];\n            let shouldAddLastWeek = false;\n\n            const dayItems = startDates.map((date, i) => {\n                const monthRange = [date.startOf(\"month\"), date.endOf(\"month\")];\n                /** @type {WeekItem[]} */\n                const weeks = [];\n\n                // Generate 6 weeks for current month\n                let startOfNextWeek = getStartOfWeek(monthRange[0]);\n                for (let w = 0; w < WEEKS_PER_MONTH; w++) {\n                    const weekDayItems = [];\n                    // Generate all days of the week\n                    for (let d = 0; d < DAYS_PER_WEEK; d++) {\n                        const day = startOfNextWeek.plus({ day: d });\n                        const range = [day, day.endOf(\"day\")];\n                        const dayItem = toDateItem({\n                            isOutOfRange: !isInRange(day, monthRange),\n                            isValid: isInRange(range, [minDate, maxDate]) && isDateValid?.(day),\n                            label: \"day\",\n                            range,\n                            extraClass: dayCellClass?.(day) || \"\",\n                        });\n                        weekDayItems.push(dayItem);\n                        if (d === DAYS_PER_WEEK - 1) {\n                            startOfNextWeek = day.plus({ day: 1 });\n                        }\n                        if (w === WEEKS_PER_MONTH - 1) {\n                            shouldAddLastWeek = true;\n                        }\n                    }\n\n                    const weekItem = toWeekItem(weekDayItems);\n                    if (w === WEEKS_PER_MONTH - 1) {\n                        lastWeeks.push(weekItem);\n                    } else {\n                        weeks.push(weekItem);\n                    }\n                }\n\n                // Generate days of week labels\n                const daysOfWeek = weeks[0].days.map((d) => [\n                    d.range[0].weekdayShort,\n                    d.range[0].weekdayLong,\n                    Info.weekdays(\"narrow\", { locale: d.range[0].locale })[d.range[0].weekday - 1],\n                ]);\n                if (showWeekNumbers) {\n                    daysOfWeek.unshift([\"\", _t(\"Week numbers\"), \"\"]);\n                }\n\n                return {\n                    id: `__month__${i}`,\n                    number: monthRange[0].month,\n                    daysOfWeek,\n                    weeks,\n                };\n            });\n\n            if (shouldAddLastWeek) {\n                // Add last empty week item if the other month has an extra week\n                for (let i = 0; i < dayItems.length; i++) {\n                    dayItems[i].weeks.push(lastWeeks[i]);\n                }\n            }\n\n            return dayItems;\n        },\n    })\n    .set(\"months\", {\n        mainTitle: _t(\"Select year\"),\n        nextTitle: _t(\"Next year\"),\n        prevTitle: _t(\"Previous year\"),\n        step: { year: 1 },\n        getTitle: (date) => String(date.year),\n        getItems: (date, { maxDate, minDate }) => {\n            const startOfYear = date.startOf(\"year\");\n            return numberRange(0, 12).map((i) => {\n                const startOfMonth = startOfYear.plus({ month: i });\n                const range = [startOfMonth, startOfMonth.endOf(\"month\")];\n                return toDateItem({\n                    isValid: isInRange(range, [minDate, maxDate]),\n                    label: \"monthShort\",\n                    range,\n                });\n            });\n        },\n    })\n    .set(\"years\", {\n        mainTitle: _t(\"Select decade\"),\n        nextTitle: _t(\"Next decade\"),\n        prevTitle: _t(\"Previous decade\"),\n        step: { year: 10 },\n        getTitle: (date) => `${getStartOfDecade(date) - 1} - ${getStartOfDecade(date) + 10}`,\n        getItems: (date, { maxDate, minDate }) => {\n            const startOfDecade = date.startOf(\"year\").set({ year: getStartOfDecade(date) });\n            return numberRange(-GRID_MARGIN, GRID_COUNT + GRID_MARGIN).map((i) => {\n                const startOfYear = startOfDecade.plus({ year: i });\n                const range = [startOfYear, startOfYear.endOf(\"year\")];\n                return toDateItem({\n                    isOutOfRange: i < 0 || i >= GRID_COUNT,\n                    isValid: isInRange(range, [minDate, maxDate]),\n                    label: \"year\",\n                    range,\n                });\n            });\n        },\n    })\n    .set(\"decades\", {\n        mainTitle: _t(\"Select century\"),\n        nextTitle: _t(\"Next century\"),\n        prevTitle: _t(\"Previous century\"),\n        step: { year: 100 },\n        getTitle: (date) => `${getStartOfCentury(date) - 10} - ${getStartOfCentury(date) + 100}`,\n        getItems: (date, { maxDate, minDate }) => {\n            const startOfCentury = date.startOf(\"year\").set({ year: getStartOfCentury(date) });\n            return numberRange(-GRID_MARGIN, GRID_COUNT + GRID_MARGIN).map((i) => {\n                const startOfDecade = startOfCentury.plus({ year: i * 10 });\n                const range = [startOfDecade, startOfDecade.plus({ year: 10, millisecond: -1 })];\n                return toDateItem({\n                    label: \"year\",\n                    isOutOfRange: i < 0 || i >= GRID_COUNT,\n                    isValid: isInRange(range, [minDate, maxDate]),\n                    range,\n                });\n            });\n        },\n    });\n\n// Other constants\nconst GRID_COUNT = 10;\nconst GRID_MARGIN = 1;\nconst NULLABLE_DATETIME_PROPERTY = [DateTime, { value: false }, { value: null }];\n\nconst DAYS_PER_WEEK = 7;\nconst WEEKS_PER_MONTH = 6;\n\n/** @extends {Component<DateTimePickerProps>} */\nexport class DateTimePicker extends Component {\n    static props = {\n        focusedDateIndex: { type: Number, optional: true },\n        showWeekNumbers: { type: Boolean, optional: true },\n        daysOfWeekFormat: { type: String, optional: true },\n        maxDate: { type: [NULLABLE_DATETIME_PROPERTY, { value: \"today\" }], optional: true },\n        maxPrecision: {\n            type: [...PRECISION_LEVELS.keys()].map((value) => ({ value })),\n            optional: true,\n        },\n        minDate: { type: [NULLABLE_DATETIME_PROPERTY, { value: \"today\" }], optional: true },\n        minPrecision: {\n            type: [...PRECISION_LEVELS.keys()].map((value) => ({ value })),\n            optional: true,\n        },\n        onReset: { type: Function, optional: true },\n        onSelect: { type: Function, optional: true },\n        onToggleRange: { type: Function, optional: true },\n        range: { type: Boolean, optional: true },\n        rounding: { type: Number, optional: true },\n        showRangeToggler: { type: Boolean, optional: true },\n        slots: {\n            type: Object,\n            shape: { buttons: { type: Object, optional: true } },\n            optional: true,\n        },\n        type: { type: [{ value: \"date\" }, { value: \"datetime\" }], optional: true },\n        value: {\n            type: [\n                NULLABLE_DATETIME_PROPERTY,\n                { type: Array, element: NULLABLE_DATETIME_PROPERTY },\n            ],\n            optional: true,\n        },\n        isDateValid: { type: Function, optional: true },\n        dayCellClass: { type: Function, optional: true },\n        tz: { type: String, optional: true },\n    };\n\n    static defaultProps = {\n        focusedDateIndex: 0,\n        daysOfWeekFormat: \"narrow\",\n        maxPrecision: \"decades\",\n        minPrecision: \"days\",\n        rounding: 5,\n        showWeekNumbers: true,\n        type: \"datetime\",\n    };\n\n    static template = \"web.DateTimePicker\";\n    static components = { TimePicker };\n\n    //-------------------------------------------------------------------------\n    // Getters\n    //-------------------------------------------------------------------------\n\n    get activePrecisionLevel() {\n        return PRECISION_LEVELS.get(this.state.precision);\n    }\n\n    get isLastPrecisionLevel() {\n        return (\n            this.allowedPrecisionLevels.indexOf(this.state.precision) ===\n            this.allowedPrecisionLevels.length - 1\n        );\n    }\n\n    get titles() {\n        return ensureArray(this.title);\n    }\n\n    //-------------------------------------------------------------------------\n    // Lifecycle\n    //-------------------------------------------------------------------------\n\n    setup() {\n        /** @type {PrecisionLevel[]} */\n        this.allowedPrecisionLevels = [];\n        /** @type {Item[]} */\n        this.items = [];\n        this.title = \"\";\n        this.shouldAdjustFocusDate = false;\n\n        this.state = useState({\n            /** @type {DateTime | null} */\n            focusDate: null,\n            /** @type {DateTime | null} */\n            hoveredDate: null,\n            /** @type {Time[]} */\n            timeValues: [],\n            /** @type {PrecisionLevel} */\n            precision: this.props.minPrecision,\n        });\n\n        this.onPropsUpdated(this.props);\n        onWillUpdateProps((nextProps) => this.onPropsUpdated(nextProps));\n\n        onWillRender(() => this.onWillRender());\n    }\n\n    /**\n     * @param {DateTimePickerProps} props\n     */\n    onPropsUpdated(props) {\n        /** @type {[NullableDateTime] | NullableDateRange} */\n        this.values = ensureArray(props.value).map((value) =>\n            value && !value.isValid ? null : value\n        );\n        this.allowedPrecisionLevels = this.filterPrecisionLevels(\n            props.minPrecision,\n            props.maxPrecision\n        );\n\n        this.maxDate = parseLimitDate(props.maxDate, MAX_VALID_DATE);\n        this.minDate = parseLimitDate(props.minDate, MIN_VALID_DATE);\n        if (this.props.type === \"date\") {\n            this.maxDate = this.maxDate.endOf(\"day\");\n            this.minDate = this.minDate.startOf(\"day\");\n        }\n\n        if (this.maxDate < this.minDate) {\n            throw new Error(`DateTimePicker error: given \"maxDate\" comes before \"minDate\".`);\n        }\n\n        this.state.timeValues = this.getTimeValues(props);\n        this.shouldAdjustFocusDate = !props.range;\n        this.adjustFocus(this.values, props.focusedDateIndex);\n    }\n\n    onWillRender() {\n        const { dayCellClass, focusedDateIndex, isDateValid, range, showWeekNumbers } = this.props;\n        const { focusDate, hoveredDate } = this.state;\n        const precision = this.activePrecisionLevel;\n        const getterParams = {\n            maxDate: this.maxDate,\n            minDate: this.minDate,\n            showWeekNumbers: showWeekNumbers ?? !range,\n            isDateValid,\n            dayCellClass,\n        };\n\n        this.title = precision.getTitle(focusDate);\n        this.items = precision.getItems(focusDate, getterParams);\n\n        this.selectedRange = [...this.values];\n        if (range && focusedDateIndex > 0 && (!this.values[1] || hoveredDate > this.values[0])) {\n            this.selectedRange[1] = hoveredDate;\n        }\n    }\n\n    //-------------------------------------------------------------------------\n    // Methods\n    //-------------------------------------------------------------------------\n\n    /**\n     * @param {NullableDateTime[]} values\n     * @param {number} focusedDateIndex\n     */\n    adjustFocus(values, focusedDateIndex) {\n        if (!this.shouldAdjustFocusDate && this.state.focusDate) {\n            return;\n        }\n\n        const dateToFocus =\n            values[focusedDateIndex] || values[focusedDateIndex === 1 ? 0 : 1] || today();\n\n        this.shouldAdjustFocusDate = false;\n        this.state.focusDate = this.clamp(dateToFocus.startOf(\"month\"));\n    }\n\n    /**\n     * @param {DateTime} value\n     */\n    clamp(value) {\n        return clampDate(value, this.minDate, this.maxDate);\n    }\n\n    /**\n     * @param {PrecisionLevel} minPrecision\n     * @param {PrecisionLevel} maxPrecision\n     */\n    filterPrecisionLevels(minPrecision, maxPrecision) {\n        const levels = [...PRECISION_LEVELS.keys()];\n        return levels.slice(levels.indexOf(minPrecision), levels.indexOf(maxPrecision) + 1);\n    }\n\n    /**\n     * Returns various flags indicating what ranges the current date item belongs\n     * to. Note that these ranges are computed differently according to the current\n     * value mode (range or single date). This is done to simplify CSS selectors.\n     * - Selected Range:\n     *      > range: current values with hovered date applied\n     *      > single date: just the hovered date\n     * - Highlighted Range:\n     *      > range: union of selection range and current values\n     *      > single date: just the current value\n     * - Current Range (range only):\n     *      > range: current start date or current end date.\n     * @param {DateItem} item\n     */\n    getActiveRangeInfo({ range }) {\n        const result = {\n            isSelected: isInRange(this.selectedRange, range),\n            isSelectStart: false,\n            isSelectEnd: false,\n            isHighlighted: isInRange(this.state.hoveredDate, range),\n        };\n\n        if (this.props.range) {\n            if (result.isSelected) {\n                const [selectStart, selectEnd] = this.selectedRange.sort();\n                result.isSelectStart = !selectStart || isInRange(selectStart, range);\n                result.isSelectEnd = !selectEnd || isInRange(selectEnd, range);\n            }\n        } else {\n            result.isSelectStart = result.isSelectEnd = result.isSelected;\n        }\n\n        return result;\n    }\n\n    /**\n     * @param {DateTimePickerProps} props\n     */\n    getTimeValues(props) {\n        const timeValues = this.values.map(\n            (val, index) =>\n                new Time({\n                    hour:\n                        index === 1 && !this.values[1]\n                            ? (val || DateTime.local()).hour + 1\n                            : (val || DateTime.local()).hour,\n                    minute: val?.minute || 0,\n                    second: val?.second || 0,\n                })\n        );\n\n        if (props.range) {\n            return timeValues;\n        } else {\n            const values = [];\n            values[props.focusedDateIndex] = timeValues[props.focusedDateIndex];\n            return values;\n        }\n    }\n\n    /**\n     * @param {DateItem} item\n     */\n    isSelectedDate({ range }) {\n        return this.values.some((value) => isInRange(value, range));\n    }\n\n    /**\n     * Goes to the next panel (e.g. next month if precision is \"days\").\n     * If an event is given it will be prevented.\n     * @param {PointerEvent} ev\n     */\n    next(ev) {\n        ev.preventDefault();\n        const { step } = this.activePrecisionLevel;\n        this.state.focusDate = this.clamp(this.state.focusDate.plus(step));\n    }\n\n    /**\n     * Goes to the previous panel (e.g. previous month if precision is \"days\").\n     * If an event is given it will be prevented.\n     * @param {PointerEvent} ev\n     */\n    previous(ev) {\n        ev.preventDefault();\n        const { step } = this.activePrecisionLevel;\n        this.state.focusDate = this.clamp(this.state.focusDate.minus(step));\n    }\n\n    /**\n     * @param {number} valueIndex\n     * @param {Time} newTime\n     */\n    onTimeChange(valueIndex, newTime) {\n        this.state.timeValues[valueIndex] = newTime;\n        const value = this.values[valueIndex] || today();\n        this.validateAndSelect(value, valueIndex, \"time\");\n    }\n\n    /**\n     * @param {DateTime} value\n     * @param {number} valueIndex\n     * @param {\"date\" | \"time\"} unit\n     */\n    validateAndSelect(value, valueIndex, unit) {\n        if (!this.props.onSelect) {\n            // No onSelect handler\n            return false;\n        }\n\n        const result = [...this.values];\n        result[valueIndex] = value;\n\n        if (this.props.type === \"datetime\") {\n            // Adjusts result according to the current time values\n            const { hour, minute, second } = this.state.timeValues[valueIndex];\n            result[valueIndex] = result[valueIndex].set({ hour, minute, second });\n        }\n        if (!isInRange(result[valueIndex], [this.minDate, this.maxDate])) {\n            // Date is outside range defined by min and max dates\n            return false;\n        }\n        this.props.onSelect(result.length === 2 ? result : result[0], unit);\n        return true;\n    }\n\n    /**\n     * Returns whether the zoom has occurred\n     * @param {DateTime} date\n     */\n    zoomIn(date) {\n        const index = this.allowedPrecisionLevels.indexOf(this.state.precision) - 1;\n        if (index in this.allowedPrecisionLevels) {\n            this.state.focusDate = this.clamp(date);\n            this.state.precision = this.allowedPrecisionLevels[index];\n            return true;\n        }\n        return false;\n    }\n\n    /**\n     * Returns whether the zoom has occurred\n     */\n    zoomOut() {\n        const index = this.allowedPrecisionLevels.indexOf(this.state.precision) + 1;\n        if (index in this.allowedPrecisionLevels) {\n            this.state.precision = this.allowedPrecisionLevels[index];\n            return true;\n        }\n        return false;\n    }\n\n    /**\n     * Happens when a date item is selected:\n     * - first tries to zoom in on the item\n     * - if could not zoom in: date is considered as final value and triggers a hard select\n     * @param {DateItem} dateItem\n     */\n    zoomOrSelect(dateItem) {\n        if (!dateItem.isValid) {\n            // Invalid item\n            return;\n        }\n        if (this.zoomIn(dateItem.range[0])) {\n            // Zoom was successful\n            return;\n        }\n        const [value] = dateItem.range;\n        const valueIndex = this.props.focusedDateIndex;\n        const isValid = this.validateAndSelect(value, valueIndex, \"date\");\n        this.shouldAdjustFocusDate = isValid && !this.props.range;\n    }\n}\n", "import { onWillDestroy, useRef } from \"@odoo/owl\";\nimport { useService } from \"@web/core/utils/hooks\";\n\n/**\n * @typedef {import(\"./datetimepicker_service\").DateTimePickerServiceParams & {\n *  endDateRefName?: string;\n *  startDateRefName?: string;\n * }} DateTimePickerHookParams\n */\n\n/**\n * @param {DateTimePickerHookParams} params\n */\nexport function useDateTimePicker(params) {\n    function getInputs() {\n        return inputRefs.map((ref) => ref.el);\n    }\n\n    const inputRefs = [\n        useRef(params.startDateRefName || \"start-date\"),\n        useRef(params.endDateRefName || \"end-date\"),\n    ];\n\n    // Need original object since 'pickerProps' (or any other param) can be defined\n    // as getters\n    const serviceParams = Object.assign(Object.create(params), {\n        getInputs,\n        useOwlHooks: true,\n    });\n\n    const picker = useService(\"datetime_picker\").create(serviceParams);\n    onWillDestroy(() => {\n        picker.disable();\n    });\n    return picker;\n}\n", "import { Component } from \"@odoo/owl\";\nimport { useHotkey } from \"../hotkeys/hotkey_hook\";\nimport { DateTimePicker } from \"./datetime_picker\";\n\n/**\n * @typedef {import(\"./datetime_picker\").DateTimePickerProps} DateTimePickerProps\n *\n * @typedef DateTimePickerPopoverProps\n * @property {() => void} close\n * @property {DateTimePickerProps} pickerProps\n */\n\n/** @extends {Component<DateTimePickerPopoverProps>} */\nexport class DateTimePickerPopover extends Component {\n    static components = { DateTimePicker };\n\n    static props = {\n        close: Function, // Given by the Popover service\n        pickerProps: { type: Object, shape: DateTimePicker.props },\n    };\n\n    static template = \"web.DateTimePickerPopover\";\n\n    //-------------------------------------------------------------------------\n    // Lifecycle\n    //-------------------------------------------------------------------------\n\n    setup() {\n        useHotkey(\"enter\", () => this.props.close());\n    }\n}\n", "import { markRaw, onPatched, onWillRender, reactive, useEffect, useRef } from \"@odoo/owl\";\nimport { areDatesEqual, formatDate, formatDateTime, parseDate, parseDateTime } from \"../l10n/dates\";\nimport { makePopover } from \"../popover/popover_hook\";\nimport { registry } from \"../registry\";\nimport { ensureArray, zip, zipWith } from \"../utils/arrays\";\nimport { shallowEqual } from \"../utils/objects\";\nimport { DateTimePicker } from \"./datetime_picker\";\nimport { DateTimePickerPopover } from \"./datetime_picker_popover\";\n\n/**\n * @typedef {luxon[\"DateTime\"][\"prototype\"]} DateTime\n *\n * @typedef {import(\"./datetime_picker\").DateTimePickerProps} DateTimePickerProps\n * @typedef {import(\"../popover/popover_hook\").PopoverHookReturnType} PopoverHookReturnType\n * @typedef {import(\"../popover/popover_service\").PopoverServiceAddOptions} PopoverServiceAddOptions\n * @typedef {import(\"@odoo/owl\").Component} Component\n * @typedef {ReturnType<typeof import(\"@odoo/owl\").useRef>} OwlRef\n *\n * @typedef {{\n *  createPopover?: (component: Component, options: PopoverServiceAddOptions) => PopoverHookReturnType;\n *  ensureVisibility?: () => boolean;\n *  format?: string;\n *  getInputs?: () => HTMLElement[];\n *  onApply?: (value: DateTimePickerProps[\"value\"]) => any;\n *  onChange?: (value: DateTimePickerProps[\"value\"]) => any;\n *  onClose?: () => any;\n *  pickerProps?: DateTimePickerProps;\n *  showSeconds?: boolean;\n *  target: HTMLElement | string;\n *  useOwlHooks?: boolean;\n * }} DateTimePickerServiceParams\n */\n\n/**\n * @template {object} T\n * @param {T} obj\n */\nfunction markValuesRaw(obj) {\n    /** @type {T} */\n    const copy = {};\n    for (const [key, value] of Object.entries(obj)) {\n        if (value && typeof value === \"object\") {\n            copy[key] = markRaw(value);\n        } else {\n            copy[key] = value;\n        }\n    }\n    return copy;\n}\n\n/**\n * @param {Record<string, any>} props\n */\nfunction stringifyProps(props) {\n    const copy = {};\n    for (const [key, value] of Object.entries(props)) {\n        copy[key] = JSON.stringify(value);\n    }\n    return copy;\n}\n\nconst FOCUS_CLASSNAME = \"text-primary\";\n\nconst formatters = {\n    date: formatDate,\n    datetime: formatDateTime,\n};\nconst listenedElements = new WeakSet();\nconst parsers = {\n    date: parseDate,\n    datetime: parseDateTime,\n};\n\nexport const datetimePickerService = {\n    dependencies: [\"popover\"],\n    start(env, { popover: popoverService }) {\n        const dateTimePickerList = new Set();\n        return {\n            /**\n             * @param {DateTimePickerServiceParams} [params]\n             */\n            create(params = {}) {\n                /**\n                 * Wrapper method on the \"onApply\" callback to only call it when the\n                 * value has changed, and set other internal variables accordingly.\n                 */\n                async function apply() {\n                    const { value } = pickerProps;\n                    const stringValue = JSON.stringify(value);\n                    if (\n                        stringValue === lastAppliedStringValue ||\n                        stringValue === stringProps.value\n                    ) {\n                        return;\n                    }\n\n                    lastAppliedStringValue = stringValue;\n                    inputsChanged = ensureArray(value).map(() => false);\n\n                    await params.onApply?.(value);\n\n                    stringProps.value = stringValue;\n                }\n\n                function enable() {\n                    for (const [el, value] of zip(\n                        getInputs(),\n                        ensureArray(pickerProps.value),\n                        true\n                    )) {\n                        updateInput(el, value);\n                        if (el && !el.disabled && !el.readOnly && !listenedElements.has(el)) {\n                            listenedElements.add(el);\n                            el.addEventListener(\"change\", onInputChange);\n                            el.addEventListener(\"click\", onInputClick);\n                            el.addEventListener(\"focus\", onInputFocus);\n                            el.addEventListener(\"keydown\", onInputKeydown);\n                        }\n                    }\n                    const calendarIconGroupEl = getInput(0)?.parentElement.querySelector(\n                        \".o_input_group_date_icon\"\n                    );\n                    if (calendarIconGroupEl) {\n                        calendarIconGroupEl.classList.add(\"cursor-pointer\");\n                        calendarIconGroupEl.addEventListener(\"click\", () => open(0));\n                    }\n                    return () => {};\n                }\n\n                /**\n                 * Ensures the current focused input (indicated by `pickerProps.focusedDateIndex`)\n                 * is actually focused.\n                 */\n                function focusActiveInput() {\n                    const inputEl = getInput(pickerProps.focusedDateIndex);\n                    if (!inputEl) {\n                        shouldFocus = true;\n                        return;\n                    }\n\n                    const { activeElement } = inputEl.ownerDocument;\n                    if (activeElement !== inputEl) {\n                        inputEl.focus();\n                    }\n                    setInputFocus(inputEl);\n                }\n\n                /**\n                 * @param {number} valueIndex\n                 * @returns {HTMLInputElement | null}\n                 */\n                function getInput(valueIndex) {\n                    const el = getInputs()[valueIndex];\n                    if (el?.isConnected) {\n                        return el;\n                    }\n                    return null;\n                }\n\n                /**\n                 * Returns the appropriate root element to attach the popover:\n                 * - if the value is a range: the closest common parent of the two inputs\n                 * - if not: the first input\n                 */\n                function getPopoverTarget() {\n                    const target = getTarget();\n                    if (target) {\n                        return target;\n                    }\n                    if (pickerProps.range) {\n                        let parentElement = getInput(0).parentElement;\n                        const inputEls = getInputs();\n                        while (\n                            parentElement &&\n                            !inputEls.every((inputEl) => parentElement.contains(inputEl))\n                        ) {\n                            parentElement = parentElement.parentElement;\n                        }\n                        return parentElement || getInput(0);\n                    } else {\n                        return getInput(0);\n                    }\n                }\n\n                function getTarget() {\n                    return targetRef ? targetRef.el : params.target;\n                }\n\n                function isOpen() {\n                    return popover.isOpen;\n                }\n\n                /**\n                 * Inputs \"change\" event handler. This will trigger an \"onApply\" callback if\n                 * one of the following is true:\n                 * - there is only one input;\n                 * - the popover is closed;\n                 * - the other input has also changed.\n                 *\n                 * @param {Event} ev\n                 */\n                function onInputChange(ev) {\n                    updateValueFromInputs();\n                    inputsChanged[ev.target === getInput(1) ? 1 : 0] = true;\n                    if (!isOpen() || inputsChanged.every(Boolean)) {\n                        saveAndClose();\n                    }\n                }\n\n                /**\n                 * @param {PointerEvent} ev\n                 */\n                function onInputClick({ target }) {\n                    open(target === getInput(1) ? 1 : 0);\n                }\n\n                /**\n                 * @param {FocusEvent} ev\n                 */\n                function onInputFocus({ target }) {\n                    pickerProps.focusedDateIndex = target === getInput(1) ? 1 : 0;\n                    setInputFocus(target);\n                }\n\n                /**\n                 * @param {KeyboardEvent} ev\n                 */\n                function onInputKeydown(ev) {\n                    if (ev.key == \"Enter\" && ev.ctrlKey) {\n                        ev.preventDefault();\n                        updateValueFromInputs();\n                        return open(ev.target === getInput(1) ? 1 : 0);\n                    }\n                    switch (ev.key) {\n                        case \"Enter\":\n                        case \"Escape\": {\n                            return saveAndClose();\n                        }\n                        case \"Tab\": {\n                            if (\n                                !getInput(0) ||\n                                !getInput(1) ||\n                                ev.target !== getInput(ev.shiftKey ? 1 : 0)\n                            ) {\n                                return saveAndClose();\n                            }\n                        }\n                    }\n                }\n\n                /**\n                 * @param {number} inputIndex Input from which to open the picker\n                 */\n                function open(inputIndex) {\n                    pickerProps.focusedDateIndex = inputIndex;\n\n                    if (!isOpen()) {\n                        const popoverTarget = getPopoverTarget();\n                        if (ensureVisibility()) {\n                            const { marginBottom } = popoverTarget.style;\n                            // Adds enough space for the popover to be displayed below the target\n                            // even on small screens.\n                            popoverTarget.style.marginBottom = `100vh`;\n                            popoverTarget.scrollIntoView(true);\n                            restoreTargetMargin = async () => {\n                                popoverTarget.style.marginBottom = marginBottom;\n                            };\n                        }\n                        for (const picker of dateTimePickerList) {\n                            picker.close();\n                        }\n                        popover.open(popoverTarget, { pickerProps });\n                    }\n\n                    focusActiveInput();\n                }\n\n                /**\n                 * @template {\"format\" | \"parse\"} T\n                 * @param {T} operation\n                 * @param {T extends \"format\" ? DateTime : string} value\n                 * @returns {[T extends \"format\" ? string : DateTime, null] | [null, Error]}\n                 */\n                function safeConvert(operation, value) {\n                    const { type } = pickerProps;\n                    const convertFn = (operation === \"format\" ? formatters : parsers)[type];\n                    const options = { tz: pickerProps.tz, format: params.format };\n                    if (operation === \"format\") {\n                        options.showSeconds = params.showSeconds ?? true;\n                    }\n                    try {\n                        return [convertFn(value, options), null];\n                    } catch (error) {\n                        if (error?.name === \"ConversionError\") {\n                            return [null, error];\n                        } else {\n                            throw error;\n                        }\n                    }\n                }\n\n                /**\n                 * Wrapper method to ensure the \"onApply\" callback is called, either:\n                 * - by closing the popover (if any);\n                 * - or by directly calling \"apply\", without updating the values.\n                 */\n                function saveAndClose() {\n                    if (isOpen()) {\n                        // apply will be done in the \"onClose\" callback\n                        popover.close();\n                    } else {\n                        apply();\n                    }\n                }\n\n                /**\n                 * Updates class names on given inputs according to the currently selected input.\n                 *\n                 * @param {HTMLInputElement | null} input\n                 */\n                function setFocusClass(input) {\n                    for (const el of getInputs()) {\n                        if (el) {\n                            el.classList.toggle(FOCUS_CLASSNAME, isOpen() && el === input);\n                        }\n                    }\n                }\n\n                /**\n                 * Applies class names to all inputs according to whether they are focused or not.\n                 *\n                 * @param {HTMLInputElement} inputEl\n                 */\n                function setInputFocus(inputEl) {\n                    inputEl.selectionStart = 0;\n                    inputEl.selectionEnd = inputEl.value.length;\n\n                    setFocusClass(inputEl);\n\n                    shouldFocus = false;\n                }\n\n                /**\n                 * Synchronizes the given input with the given value.\n                 *\n                 * @param {HTMLInputElement} el\n                 * @param {DateTime} value\n                 */\n                function updateInput(el, value) {\n                    if (!el) {\n                        return;\n                    }\n                    const [formattedValue] = safeConvert(\"format\", value);\n                    el.value = formattedValue || \"\";\n                }\n\n                /**\n                 * @param {DateTimePickerProps[\"value\"]} value\n                 * @param {\"date\" | \"time\"} unit\n                 * @param {\"input\" | \"picker\"} source\n                 */\n                function updateValue(value, unit, source) {\n                    if (source === \"input\" && areDatesEqual(pickerProps.value, value)) {\n                        return;\n                    }\n\n                    pickerProps.value = value;\n\n                    if (pickerProps.range && unit !== \"time\" && source === \"picker\") {\n                        if (!value[0]) {\n                            pickerProps.focusedDateIndex = 0;\n                        } else if (\n                            pickerProps.focusedDateIndex === 0 ||\n                            (value[0] && value[1] && value[1] < value[0])\n                        ) {\n                            // If selecting either:\n                            // - the first value\n                            // - OR a second value before the first:\n                            // Then:\n                            // - Set the DATE (year + month + day) of all values\n                            // to the one that has been selected.\n                            const { year, month, day } = value[pickerProps.focusedDateIndex];\n                            for (let i = 0; i < value.length; i++) {\n                                value[i] = value[i] && value[i].set({ year, month, day });\n                            }\n                            pickerProps.focusedDateIndex = 1;\n                        } else {\n                            // If selecting the second value after the first:\n                            // - simply toggle the focus index\n                            pickerProps.focusedDateIndex =\n                                pickerProps.focusedDateIndex === 1 ? 0 : 1;\n                        }\n                    }\n\n                    params.onChange?.(value);\n                }\n\n                function updateValueFromInputs() {\n                    const values = zipWith(\n                        getInputs(),\n                        ensureArray(pickerProps.value),\n                        (el, currentValue) => {\n                            if (!el || el.tagName?.toLowerCase() !== \"input\") {\n                                return currentValue;\n                            }\n                            const [parsedValue, error] = safeConvert(\"parse\", el.value);\n                            if (error) {\n                                updateInput(el, currentValue);\n                                return currentValue;\n                            } else {\n                                return parsedValue;\n                            }\n                        }\n                    );\n                    updateValue(values.length === 2 ? values : values[0], \"date\", \"input\");\n                }\n\n                const createPopover =\n                    params.createPopover ||\n                    function defaultCreatePopover(...args) {\n                        return makePopover(popoverService.add, ...args);\n                    };\n                const ensureVisibility =\n                    params.ensureVisibility ||\n                    function defaultEnsureVisibility() {\n                        return env.isSmall;\n                    };\n                const getInputs =\n                    params.getInputs ||\n                    function defaultGetInputs() {\n                        return [getTarget(), null];\n                    };\n\n                // Hook variables\n\n                /** @type {DateTimePickerProps} */\n                const rawPickerProps = {\n                    ...DateTimePicker.defaultProps,\n                    onReset: () => {\n                        updateValue(\n                            ensureArray(pickerProps.value).length === 2 ? [false, false] : false,\n                            \"date\",\n                            \"picker\"\n                        );\n                        saveAndClose();\n                    },\n                    onSelect: (value, unit) => {\n                        value &&= markRaw(value);\n                        updateValue(value, unit, \"picker\");\n                        if (!pickerProps.range && pickerProps.type === \"date\") {\n                            saveAndClose();\n                        }\n                    },\n                    ...markValuesRaw(params.pickerProps),\n                };\n                const pickerProps = reactive(rawPickerProps, () => {\n                    // Update inputs\n                    for (const [el, value] of zip(\n                        getInputs(),\n                        ensureArray(pickerProps.value),\n                        true\n                    )) {\n                        if (el) {\n                            updateInput(el, value);\n                            // Apply changes immediately if the popover is already closed.\n                            // Otherwise \u00b4apply()\u00b4 will be called later on close.\n                            if (!isOpen()) {\n                                apply();\n                            }\n                        }\n                    }\n\n                    shouldFocus = true;\n                });\n                const popover = createPopover(DateTimePickerPopover, {\n                    async onClose() {\n                        updateValueFromInputs();\n                        setFocusClass(null);\n                        restoreTargetMargin?.();\n                        restoreTargetMargin = null;\n                        await apply();\n                        params.onClose?.();\n                    },\n                });\n\n                /** @type {boolean[]} */\n                let inputsChanged = [];\n                let lastAppliedStringValue = \"\";\n                /** @type {(() => void) | null} */\n                let restoreTargetMargin = null;\n                let shouldFocus = false;\n                /** @type {Partial<DateTimePickerProps>} */\n                let stringProps = {};\n                /** @type {OwlRef | null} */\n                let targetRef = null;\n\n                if (params.useOwlHooks) {\n                    if (typeof params.target === \"string\") {\n                        targetRef = useRef(params.target);\n                    }\n\n                    onWillRender(function computeBasePickerProps() {\n                        const nextProps = markValuesRaw(params.pickerProps);\n                        const oldStringProps = stringProps;\n\n                        stringProps = stringifyProps(nextProps);\n                        lastAppliedStringValue = stringProps.value;\n\n                        if (shallowEqual(oldStringProps, stringProps)) {\n                            return;\n                        }\n\n                        inputsChanged = ensureArray(nextProps.value).map(() => false);\n\n                        for (const [key, value] of Object.entries(nextProps)) {\n                            if (!areDatesEqual(pickerProps[key], value)) {\n                                pickerProps[key] = value;\n                            }\n                        }\n                    });\n\n                    useEffect(enable, getInputs);\n\n                    // Note: this `onPatched` callback must be called after the `useEffect` since\n                    // the effect may change input values that will be selected by the patch callback.\n                    onPatched(function focusIfNeeded() {\n                        if (isOpen() && shouldFocus) {\n                            focusActiveInput();\n                        }\n                    });\n                } else if (typeof params.target === \"string\") {\n                    throw new Error(\n                        `datetime picker service error: cannot use target as ref name when not using Owl hooks`\n                    );\n                }\n                const picker = {\n                    enable,\n                    disable: () => dateTimePickerList.delete(picker),\n                    isOpen,\n                    open,\n                    close: () => popover.close(),\n                    state: pickerProps,\n                };\n                dateTimePickerList.add(picker);\n                return picker;\n            },\n        };\n    },\n};\n\nregistry.category(\"services\").add(\"datetime_picker\", datetimePickerService);\n", "import { user } from \"@web/core/user\";\nimport { registry } from \"../registry\";\n\nimport { useEffect, useEnv, useSubEnv } from \"@odoo/owl\";\nconst debugRegistry = registry.category(\"debug\");\n\nconst getAccessRights = async () => {\n    const rightsToCheck = {\n        \"ir.ui.view\": \"write\",\n        \"ir.rule\": \"read\",\n        \"ir.model.access\": \"read\",\n    };\n    const proms = Object.entries(rightsToCheck).map(([model, operation]) => {\n        return user.checkAccessRight(model, operation);\n    });\n    const [canEditView, canSeeRecordRules, canSeeModelAccess] = await Promise.all(proms);\n    const accessRights = { canEditView, canSeeRecordRules, canSeeModelAccess };\n    return accessRights;\n};\n\nclass DebugContext {\n    constructor(defaultCategories) {\n        this.categories = new Map(defaultCategories.map((cat) => [cat, [{}]]));\n    }\n\n    activateCategory(category, context) {\n        const contexts = this.categories.get(category) || new Set();\n        contexts.add(context);\n        this.categories.set(category, contexts);\n\n        return () => {\n            contexts.delete(context);\n            if (contexts.size === 0) {\n                this.categories.delete(category);\n            }\n        };\n    }\n\n    async getItems(env) {\n        const accessRights = await getAccessRights();\n        return [...this.categories.entries()]\n            .flatMap(([category, contexts]) => {\n                return debugRegistry\n                    .category(category)\n                    .getAll()\n                    .map((factory) => factory(Object.assign({ env, accessRights }, ...contexts)));\n            })\n            .filter(Boolean)\n            .sort((x, y) => {\n                const xSeq = x.sequence || 1000;\n                const ySeq = y.sequence || 1000;\n                return xSeq - ySeq;\n            });\n    }\n}\n\nconst debugContextSymbol = Symbol(\"debugContext\");\nexport function createDebugContext({ categories = [] } = {}) {\n    return { [debugContextSymbol]: new DebugContext(categories) };\n}\n\nexport function useOwnDebugContext({ categories = [] } = {}) {\n    useSubEnv(createDebugContext({ categories }));\n}\n\nexport function useEnvDebugContext() {\n    const debugContext = useEnv()[debugContextSymbol];\n    if (!debugContext) {\n        throw new Error(\"There is no debug context available in the current environment.\");\n    }\n    return debugContext;\n}\n\nexport function useDebugCategory(category, context = {}) {\n    const env = useEnv();\n    if (env.debug) {\n        const debugContext = useEnvDebugContext();\n        useEffect(\n            () => debugContext.activateCategory(category, context),\n            () => []\n        );\n    }\n}\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\nimport { DebugMenuBasic } from \"@web/core/debug/debug_menu_basic\";\nimport { useCommand } from \"@web/core/commands/command_hook\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { useEnvDebugContext } from \"./debug_context\";\n\nexport class DebugMenu extends DebugMenuBasic {\n    static components = { Dropdown, DropdownItem };\n    static props = {};\n    setup() {\n        super.setup();\n        const debugContext = useEnvDebugContext();\n        this.command = useService(\"command\");\n        useCommand(\n            _t(\"Debug tools...\"),\n            async () => {\n                const items = await debugContext.getItems(this.env);\n                let index = 0;\n                const defaultCategories = items\n                    .filter((item) => item.type === \"separator\")\n                    .map(() => (index += 1));\n                const provider = {\n                    async provide() {\n                        const categories = [...defaultCategories];\n                        let category = categories.shift();\n                        const result = [];\n                        items.forEach((item) => {\n                            if (item.type === \"item\") {\n                                result.push({\n                                    name: item.description.toString(),\n                                    action: item.callback,\n                                    category,\n                                });\n                            } else if (item.type === \"separator\") {\n                                category = categories.shift();\n                            }\n                        });\n                        return result;\n                    },\n                };\n                const configByNamespace = {\n                    default: {\n                        categories: defaultCategories,\n                        emptyMessage: _t(\"No debug command found\"),\n                        placeholder: _t(\"Choose a debug command...\"),\n                    },\n                };\n                const commandPaletteConfig = {\n                    configByNamespace,\n                    providers: [provider],\n                };\n                return commandPaletteConfig;\n            },\n            {\n                category: \"debug\",\n            }\n        );\n    }\n}\n", "import { useEnvDebugContext } from \"./debug_context\";\nimport { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { groupBy, sortBy } from \"@web/core/utils/arrays\";\n\nimport { Component } from \"@odoo/owl\";\nimport { registry } from \"@web/core/registry\";\n\nconst debugSectionRegistry = registry.category(\"debug_section\");\n\ndebugSectionRegistry\n    .add(\"record\", { label: _t(\"Record\"), sequence: 10 })\n    .add(\"records\", { label: _t(\"Records\"), sequence: 10 })\n    .add(\"ui\", { label: _t(\"User Interface\"), sequence: 20 })\n    .add(\"security\", { label: _t(\"Security\"), sequence: 30 })\n    .add(\"testing\", { label: _t(\"Tours & Testing\"), sequence: 40 })\n    .add(\"tools\", { label: _t(\"Tools\"), sequence: 50 });\n\nexport class DebugMenuBasic extends Component {\n    static template = \"web.DebugMenu\";\n    static components = {\n        Dropdown,\n        DropdownItem,\n    };\n    static props = {};\n\n    setup() {\n        this.debugContext = useEnvDebugContext();\n    }\n\n    async loadGroupedItems() {\n        const items = await this.debugContext.getItems(this.env);\n        const sections = groupBy(items, (item) => item.section || \"\");\n        this.sectionEntries = sortBy(\n            Object.entries(sections),\n            ([section]) => debugSectionRegistry.get(section, { sequence: 50 }).sequence\n        );\n    }\n\n    getSectionLabel(section) {\n        return debugSectionRegistry.get(section, { label: section }).label;\n    }\n}\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { browser } from \"@web/core/browser/browser\";\nimport { router } from \"@web/core/browser/router\";\nimport { registry } from \"@web/core/registry\";\nimport { user } from \"@web/core/user\";\n\nfunction activateTestsAssetsDebugging({ env }) {\n    if (String(router.current.debug).includes(\"tests\")) {\n        return;\n    }\n\n    return {\n        type: \"item\",\n        description: _t(\"Activate Test Mode\"),\n        callback: () => {\n            router.pushState({ debug: \"assets,tests\" }, { reload: true });\n        },\n        sequence: 580,\n        section: \"tools\",\n    };\n}\n\nexport function regenerateAssets({ env }) {\n    return {\n        type: \"item\",\n        description: _t(\"Regenerate Assets\"),\n        callback: async () => {\n            await env.services.orm.call(\"ir.attachment\", \"regenerate_assets_bundles\");\n            browser.location.reload();\n        },\n        sequence: 550,\n        section: \"tools\",\n    };\n}\n\nexport function becomeSuperuser({ env }) {\n    const becomeSuperuserURL = browser.location.origin + \"/web/become\";\n    if (!user.isAdmin) {\n        return false;\n    }\n    return {\n        type: \"item\",\n        description: _t(\"Become Superuser\"),\n        href: becomeSuperuserURL,\n        callback: () => {\n            browser.open(becomeSuperuserURL, \"_self\");\n        },\n        sequence: 560,\n        section: \"tools\",\n    };\n}\n\nfunction leaveDebugMode() {\n    return {\n        type: \"item\",\n        description: _t(\"Leave Debug Mode\"),\n        callback: () => {\n            router.pushState({ debug: 0 }, { reload: true });\n        },\n        sequence: 650,\n    };\n}\n\nregistry\n    .category(\"debug\")\n    .category(\"default\")\n    .add(\"regenerateAssets\", regenerateAssets)\n    .add(\"becomeSuperuser\", becomeSuperuser)\n    .add(\"activateTestsAssetsDebugging\", activateTestsAssetsDebugging)\n    .add(\"leaveDebugMode\", leaveDebugMode);\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"../registry\";\nimport { browser } from \"../browser/browser\";\nimport { router } from \"../browser/router\";\n\nconst commandProviderRegistry = registry.category(\"command_provider\");\n\ncommandProviderRegistry.add(\"debug\", {\n    provide: (env, options) => {\n        const result = [];\n        if (env.debug) {\n            if (!env.debug.includes(\"assets\")) {\n                result.push({\n                    action() {\n                        router.pushState({ debug: \"assets\" }, { reload: true });\n                    },\n                    category: \"debug\",\n                    name: _t(\"Activate debug mode (with assets)\"),\n                });\n            }\n            result.push({\n                action() {\n                    router.pushState({ debug: 0 }, { reload: true });\n                },\n                category: \"debug\",\n                name: _t(\"Deactivate debug mode\"),\n            });\n            result.push({\n                action() {\n                    browser.open(\"/web/tests?debug=assets\");\n                },\n                category: \"debug\",\n                name: _t(\"Run Unit Tests\"),\n            });\n        } else {\n            const debugKey = \"debug\";\n            if (options.searchValue.toLowerCase() === debugKey) {\n                result.push({\n                    action() {\n                        router.pushState({ debug: \"1\" }, { reload: true });\n                    },\n                    category: \"debug\",\n                    name: `${_t(\"Activate debug mode\")} (${debugKey})`,\n                });\n                result.push({\n                    action() {\n                        router.pushState({ debug: \"assets\" }, { reload: true });\n                    },\n                    category: \"debug\",\n                    name: `${_t(\"Activate debug mode (with assets)\")} (${debugKey})`,\n                });\n            }\n        }\n        return result;\n    },\n});\n", "export function editModelDebug(env, title, model, id) {\n    return env.services.action.doAction({\n        res_model: model,\n        res_id: id,\n        name: title,\n        type: \"ir.actions.act_window\",\n        views: [[false, \"form\"]],\n        view_mode: \"form\",\n        target: \"current\",\n    });\n}\n", "import { useHotkey } from \"@web/core/hotkeys/hotkey_hook\";\nimport { useActiveElement } from \"../ui/ui_service\";\nimport { useForwardRefToParent } from \"@web/core/utils/hooks\";\nimport { Component, onWillDestroy, useChildSubEnv, useExternalListener, useState } from \"@odoo/owl\";\nimport { throttleForAnimation } from \"@web/core/utils/timing\";\nimport { makeDraggableHook } from \"../utils/draggable_hook_builder_owl\";\n\nconst useDialogDraggable = makeDraggableHook({\n    name: \"useDialogDraggable\",\n    onWillStartDrag({ ctx, addCleanup, addStyle, getRect }) {\n        const { height, width } = getRect(ctx.current.element);\n        ctx.current.container = document.createElement(\"div\");\n        addStyle(ctx.current.container, {\n            position: \"fixed\",\n            top: \"0\",\n            bottom: `${70 - height}px`,\n            left: `${70 - width}px`,\n            right: `${70 - width}px`,\n        });\n        ctx.current.element.after(ctx.current.container);\n        addCleanup(() => ctx.current.container.remove());\n    },\n    onDrop({ ctx, getRect }) {\n        const { top, left } = getRect(ctx.current.element);\n        return {\n            left: left - ctx.current.elementRect.left,\n            top: top - ctx.current.elementRect.top,\n        };\n    },\n});\n\nexport class Dialog extends Component {\n    static template = \"web.Dialog\";\n    static props = {\n        contentClass: { type: String, optional: true },\n        bodyClass: { type: String, optional: true },\n        fullscreen: { type: Boolean, optional: true },\n        footer: { type: Boolean, optional: true },\n        header: { type: Boolean, optional: true },\n        size: {\n            type: String,\n            optional: true,\n            validate: (s) => [\"sm\", \"md\", \"lg\", \"xl\", \"fs\", \"fullscreen\"].includes(s),\n        },\n        technical: { type: Boolean, optional: true },\n        title: { type: String, optional: true },\n        modalRef: { type: Function, optional: true },\n        slots: {\n            type: Object,\n            shape: {\n                default: Object, // Content is not optional\n                header: { type: Object, optional: true },\n                footer: { type: Object, optional: true },\n            },\n        },\n        withBodyPadding: { type: Boolean, optional: true },\n        onExpand: { type: Function, optional: true },\n    };\n    static defaultProps = {\n        contentClass: \"\",\n        bodyClass: \"\",\n        fullscreen: false,\n        footer: true,\n        header: true,\n        size: \"lg\",\n        technical: true,\n        title: \"Odoo\",\n        withBodyPadding: true,\n    };\n\n    setup() {\n        this.modalRef = useForwardRefToParent(\"modalRef\");\n        useActiveElement(\"modalRef\");\n        this.data = useState(this.env.dialogData);\n        useHotkey(\"escape\", () => this.onEscape());\n        useHotkey(\n            \"control+enter\",\n            () => {\n                const btns = document.querySelectorAll(\n                    \".o_dialog:not(.o_inactive_modal) .modal-footer button\"\n                );\n                const firstVisibleBtn = Array.from(btns).find((btn) => {\n                    const styles = getComputedStyle(btn);\n                    return styles.display !== \"none\";\n                });\n                if (firstVisibleBtn) {\n                    firstVisibleBtn.click();\n                }\n            },\n            { bypassEditableProtection: true }\n        );\n        this.id = `dialog_${this.data.id}`;\n        useChildSubEnv({ inDialog: true, dialogId: this.id });\n        this.isMovable = this.props.header;\n        if (this.isMovable) {\n            this.position = useState({ left: 0, top: 0 });\n            useDialogDraggable({\n                enable: () => !this.env.isSmall,\n                ref: this.modalRef,\n                elements: \".modal-content\",\n                handle: \".modal-header\",\n                ignore: \"button, input\",\n                edgeScrolling: { enabled: false },\n                onDrop: ({ top, left }) => {\n                    this.position.left += left;\n                    this.position.top += top;\n                },\n            });\n            const throttledResize = throttleForAnimation(this.onResize.bind(this));\n            useExternalListener(window, \"resize\", throttledResize);\n        }\n        onWillDestroy(() => {\n            if (this.env.isSmall) {\n                this.data.scrollToOrigin();\n            }\n        });\n    }\n\n    get isFullscreen() {\n        return this.props.fullscreen || this.env.isSmall;\n    }\n\n    get contentStyle() {\n        if (this.isMovable) {\n            return `top: ${this.position.top}px; left: ${this.position.left}px;`;\n        }\n        return \"\";\n    }\n\n    onResize() {\n        this.position.left = 0;\n        this.position.top = 0;\n    }\n\n    onEscape() {\n        return this.dismiss();\n    }\n\n    async dismiss() {\n        if (this.data.dismiss) {\n            await this.data.dismiss();\n        }\n        return this.data.close({ dismiss: true });\n    }\n}\n", "import { Component, markRaw, reactive, useChildSubEnv, xml } from \"@odoo/owl\";\nimport { registry } from \"@web/core/registry\";\n\nclass DialogWrapper extends Component {\n    static template = xml`<t t-component=\"props.subComponent\" t-props=\"props.subProps\" />`;\n    static props = [\"*\"];\n    setup() {\n        useChildSubEnv({ dialogData: this.props.subEnv });\n    }\n}\n\n/**\n *  @typedef {{\n *      onClose?(): void;\n *  }} DialogServiceInterfaceAddOptions\n */\n/**\n *  @typedef {{\n *      add(\n *          Component: typeof import(\"@odoo/owl\").Component,\n *          props: {},\n *          options?: DialogServiceInterfaceAddOptions\n *      ): () => void;\n *  }} DialogServiceInterface\n */\n\nexport const dialogService = {\n    dependencies: [\"overlay\"],\n    /** @returns {DialogServiceInterface} */\n    start(env, { overlay }) {\n        const stack = [];\n        let nextId = 0;\n\n        const deactivate = () => {\n            for (const subEnv of stack) {\n                subEnv.isActive = false;\n            }\n        };\n\n        const add = (dialogClass, props, options = {}) => {\n            const id = nextId++;\n            const close = (params) => remove(params);\n            const subEnv = reactive({\n                id,\n                close,\n                isActive: true,\n            });\n\n            deactivate();\n            stack.push(subEnv);\n            document.body.classList.add(\"modal-open\");\n            let isBeingClosed = false;\n\n            const scrollOrigin = { top: window.scrollY, left: window.scrollX };\n            subEnv.scrollToOrigin = () => {\n                if (!stack.length) {\n                    window.scrollTo(scrollOrigin);\n                }\n            };\n\n            const remove = overlay.add(\n                DialogWrapper,\n                {\n                    subComponent: dialogClass,\n                    subProps: markRaw({ ...props, close }),\n                    subEnv,\n                },\n                {\n                    onRemove: async (closeParams) => {\n                        if (isBeingClosed) {\n                            return;\n                        }\n                        isBeingClosed = true;\n                        await options.onClose?.(closeParams);\n                        stack.splice(\n                            stack.findIndex((d) => d.id === id),\n                            1\n                        );\n                        deactivate();\n                        if (stack.length) {\n                            stack.at(-1).isActive = true;\n                        } else {\n                            document.body.classList.remove(\"modal-open\");\n                        }\n                    },\n                    rootId: options.context?.root?.el?.getRootNode()?.host?.id,\n                }\n            );\n\n            return remove;\n        };\n\n        function closeAll(params) {\n            for (const dialog of [...stack].reverse()) {\n                dialog.close(params);\n            }\n        }\n\n        return { add, closeAll };\n    },\n};\n\nregistry.category(\"services\").add(\"dialog\", dialogService);\n", "import { shallowEqual } from \"@web/core/utils/arrays\";\nimport { evaluate, formatAST, parseExpr } from \"./py_js/py\";\nimport { toPyValue } from \"./py_js/py_utils\";\nimport { escapeRegExp } from \"@web/core/utils/strings\";\n\n/**\n * @typedef {import(\"./py_js/py_parser\").AST} AST\n * @typedef {[string | 0 | 1, string, any]} Condition\n * @typedef {(\"&\" | \"|\" | \"!\" | Condition)[]} DomainListRepr\n * @typedef {DomainListRepr | string | Domain} DomainRepr\n */\n\nexport class InvalidDomainError extends Error {}\n\n/**\n * Javascript representation of an Odoo domain\n */\nexport class Domain {\n    /**\n     * Combine various domains together with a given operator\n     * @param {DomainRepr[]} domains\n     * @param {\"AND\" | \"OR\"} operator\n     * @returns {Domain}\n     */\n    static combine(domains, operator) {\n        if (domains.length === 0) {\n            return new Domain([]);\n        }\n        const domain1 = domains[0] instanceof Domain ? domains[0] : new Domain(domains[0]);\n        if (domains.length === 1) {\n            return domain1;\n        }\n        const domain2 = Domain.combine(domains.slice(1), operator);\n        const result = new Domain([]);\n        const astValues1 = domain1.ast.value;\n        const astValues2 = domain2.ast.value;\n        const op = operator === \"AND\" ? \"&\" : \"|\";\n        const combinedAST = { type: 4 /* List */, value: astValues1.concat(astValues2) };\n        result.ast = normalizeDomainAST(combinedAST, op);\n        return result;\n    }\n\n    /**\n     * Combine various domains together with `AND` operator\n     * @param {DomainRepr[]} domains\n     * @returns {Domain}\n     */\n    static and(domains) {\n        return Domain.combine(domains, \"AND\");\n    }\n\n    /**\n     * Combine various domains together with `OR` operator\n     * @param {DomainRepr[]} domains\n     * @returns {Domain}\n     */\n    static or(domains) {\n        return Domain.combine(domains, \"OR\");\n    }\n\n    /**\n     * Return the negation of the domain\n     * @returns {Domain}\n     */\n    static not(domain) {\n        const result = new Domain(domain);\n        result.ast.value.unshift({ type: 1, value: \"!\" });\n        return result;\n    }\n\n    /**\n     * Return a new domain with `neutralized` leaves (for the leaves that are applied on the field that are part of\n     * keysToRemove).\n     * @param {DomainRepr} domain\n     * @param {string[]} keysToRemove\n     * @return {Domain}\n     */\n    static removeDomainLeaves(domain, keysToRemove) {\n        function processLeaf(elements, idx, operatorCtx, newDomain) {\n            const leaf = elements[idx];\n            if (leaf.type === 10) {\n                if (keysToRemove.includes(leaf.value[0].value)) {\n                    if (operatorCtx === \"&\") {\n                        newDomain.ast.value.push(...Domain.TRUE.ast.value);\n                    } else if (operatorCtx === \"|\") {\n                        newDomain.ast.value.push(...Domain.FALSE.ast.value);\n                    }\n                } else {\n                    newDomain.ast.value.push(leaf);\n                }\n                return 1;\n            } else if (leaf.type === 1) {\n                // Special case to avoid OR ('|') that can never resolve to true\n                if (\n                    leaf.value === \"|\" &&\n                    elements[idx + 1].type === 10 &&\n                    elements[idx + 2].type === 10 &&\n                    keysToRemove.includes(elements[idx + 1].value[0].value) &&\n                    keysToRemove.includes(elements[idx + 2].value[0].value)\n                ) {\n                    newDomain.ast.value.push(...Domain.TRUE.ast.value);\n                    return 3;\n                }\n                newDomain.ast.value.push(leaf);\n                if (leaf.value === \"!\") {\n                    return 1 + processLeaf(elements, idx + 1, \"&\", newDomain);\n                }\n                const firstLeafSkip = processLeaf(elements, idx + 1, leaf.value, newDomain);\n                const secondLeafSkip = processLeaf(\n                    elements,\n                    idx + 1 + firstLeafSkip,\n                    leaf.value,\n                    newDomain\n                );\n                return 1 + firstLeafSkip + secondLeafSkip;\n            }\n            return 0;\n        }\n\n        domain = new Domain(domain);\n        if (domain.ast.value.length === 0) {\n            return domain;\n        }\n        const newDomain = new Domain([]);\n        processLeaf(domain.ast.value, 0, \"&\", newDomain);\n        return newDomain;\n    }\n\n    /**\n     * @param {DomainRepr} [descr]\n     */\n    constructor(descr = []) {\n        if (descr instanceof Domain) {\n            /** @type {AST} */\n            return new Domain(descr.toString());\n        } else {\n            let rawAST;\n            try {\n                rawAST = typeof descr === \"string\" ? parseExpr(descr) : toAST(descr);\n            } catch (error) {\n                throw new InvalidDomainError(`Invalid domain representation: ${descr.toString()}`, {\n                    cause: error,\n                });\n            }\n            this.ast = normalizeDomainAST(rawAST);\n        }\n    }\n\n    /**\n     * Check if the set of records represented by a domain contains a record\n     * Warning: smart dates (see parseSmartDateInput) are not handled here.\n     *\n     * @param {Object} record\n     * @returns {boolean}\n     */\n    contains(record) {\n        const expr = evaluate(this.ast, record);\n        return matchDomain(record, expr);\n    }\n\n    /**\n     * @returns {string}\n     */\n    toString() {\n        return formatAST(this.ast);\n    }\n\n    /**\n     * @param {Object} context\n     * @returns {DomainListRepr}\n     */\n    toList(context) {\n        return evaluate(this.ast, context);\n    }\n\n    /**\n     * Converts the domain into a human-readable format for JSON representation.\n     * If the domain does not contain any contextual value, it is converted to a list.\n     * Otherwise, it is returned as a string.\n     *\n     * The string format is less readable due to escaped double quotes.\n     * Example: \"[\\\"&\\\",[\\\"user_id\\\",\\\"=\\\",uid],[\\\"team_id\\\",\\\"!=\\\",false]]\"\n     * @returns {DomainListRepr | string}\n     */\n    toJson() {\n        try {\n            // Attempt to evaluate the domain without context\n            const evaluatedAsList = this.toList({});\n            const evaluatedDomain = new Domain(evaluatedAsList);\n            if (evaluatedDomain.toString() === this.toString()) {\n                return evaluatedAsList;\n            }\n            return this.toString();\n        } catch {\n            // The domain couldn't be evaluated due to contextual values\n            return this.toString();\n        }\n    }\n}\n\n/** @type {Condition} */\nconst TRUE_LEAF = [1, \"=\", 1];\n/** @type {Condition} */\nconst FALSE_LEAF = [0, \"=\", 1];\nconst TRUE_DOMAIN = new Domain([TRUE_LEAF]);\nconst FALSE_DOMAIN = new Domain([FALSE_LEAF]);\n\nDomain.TRUE = TRUE_DOMAIN;\nDomain.FALSE = FALSE_DOMAIN;\n\n// -----------------------------------------------------------------------------\n// Helpers\n// -----------------------------------------------------------------------------\n\n/**\n * @param {DomainListRepr} domain\n * @returns {AST}\n */\nfunction toAST(domain) {\n    const elems = domain.map((elem) => {\n        switch (elem) {\n            case \"!\":\n            case \"&\":\n            case \"|\":\n                return { type: 1 /* String */, value: elem };\n            default:\n                return {\n                    type: 10 /* Tuple */,\n                    value: elem.map(toPyValue),\n                };\n        }\n    });\n    return { type: 4 /* List */, value: elems };\n}\n\n/**\n * Normalizes a domain\n *\n * @param {AST} domain\n * @param {'&' | '|'} [op]\n * @returns {AST}\n */\n\nfunction normalizeDomainAST(domain, op = \"&\") {\n    if (domain.type !== 4 /* List */) {\n        if (domain.type === 10 /* Tuple */) {\n            const value = domain.value;\n            /* Tuple contains at least one Tuple and optionally string */\n            if (\n                value.findIndex((e) => e.type === 10) === -1 ||\n                !value.every((e) => e.type === 10 || e.type === 1)\n            ) {\n                throw new InvalidDomainError(\"Invalid domain AST\");\n            }\n        } else {\n            throw new InvalidDomainError(\"Invalid domain AST\");\n        }\n    }\n    if (domain.value.length === 0) {\n        return domain;\n    }\n    let expected = 1;\n    for (const child of domain.value) {\n        switch (child.type) {\n            case 1 /* String */:\n                if (child.value === \"&\" || child.value === \"|\") {\n                    expected++;\n                } else if (child.value !== \"!\") {\n                    throw new InvalidDomainError(\"Invalid domain AST\");\n                }\n                break;\n            case 4: /* list */\n            case 10 /* tuple */:\n                if (child.value.length === 3) {\n                    expected--;\n                    break;\n                }\n                throw new InvalidDomainError(\"Invalid domain AST\");\n            default:\n                throw new InvalidDomainError(\"Invalid domain AST\");\n        }\n    }\n    const values = domain.value.slice();\n    while (expected < 0) {\n        expected++;\n        values.unshift({ type: 1 /* String */, value: op });\n    }\n    if (expected > 0) {\n        throw new InvalidDomainError(\n            `invalid domain ${formatAST(domain)} (missing ${expected} segment(s))`\n        );\n    }\n    return { type: 4 /* List */, value: values };\n}\n\n/**\n * @param {Object} record\n * @param {Condition | boolean} condition\n * @returns {boolean}\n */\nfunction matchCondition(record, condition) {\n    if (typeof condition === \"boolean\") {\n        return condition;\n    }\n    const [field, operator, value] = condition;\n\n    if (typeof field === \"string\") {\n        const names = field.split(\".\");\n        if (names.length >= 2) {\n            return matchCondition(record[names[0]], [names.slice(1).join(\".\"), operator, value]);\n        }\n    }\n    let likeRegexp, ilikeRegexp;\n    if ([\"like\", \"not like\", \"ilike\", \"not ilike\"].includes(operator)) {\n        likeRegexp = new RegExp(`(.*)${escapeRegExp(value).replaceAll(\"%\", \"(.*)\")}(.*)`, \"g\");\n        ilikeRegexp = new RegExp(`(.*)${escapeRegExp(value).replaceAll(\"%\", \"(.*)\")}(.*)`, \"gi\");\n    }\n    const fieldValue = typeof field === \"number\" ? field : record[field];\n    const isNot = operator.startsWith(\"not \");\n    switch (operator) {\n        case \"=?\":\n            if ([false, null].includes(value)) {\n                return true;\n            }\n        // eslint-disable-next-line no-fallthrough\n        case \"=\":\n        case \"==\":\n            if (Array.isArray(fieldValue) && Array.isArray(value)) {\n                return shallowEqual(fieldValue, value);\n            }\n            return fieldValue === value;\n        case \"!=\":\n        case \"<>\":\n            return !matchCondition(record, [field, \"=\", value]);\n        case \"<\":\n            return fieldValue < value;\n        case \"<=\":\n            return fieldValue <= value;\n        case \">\":\n            return fieldValue > value;\n        case \">=\":\n            return fieldValue >= value;\n        case \"in\":\n        case \"not in\": {\n            const val = Array.isArray(value) ? value : [value];\n            const fieldVal = Array.isArray(fieldValue) ? fieldValue : [fieldValue];\n            return Boolean(fieldVal.some((fv) => val.includes(fv))) != isNot;\n        }\n        case \"like\":\n        case \"not like\":\n            if (fieldValue === false) {\n                return isNot;\n            }\n            return Boolean(fieldValue.match(likeRegexp)) != isNot;\n        case \"=like\":\n        case \"not =like\":\n            if (fieldValue === false) {\n                return isNot;\n            }\n            return (\n                Boolean(new RegExp(escapeRegExp(value).replace(/%/g, \".*\")).test(fieldValue)) !=\n                isNot\n            );\n        case \"ilike\":\n        case \"not ilike\":\n            if (fieldValue === false) {\n                return isNot;\n            }\n            return Boolean(fieldValue.match(ilikeRegexp)) != isNot;\n        case \"=ilike\":\n        case \"not =ilike\":\n            if (fieldValue === false) {\n                return isNot;\n            }\n            return (\n                Boolean(\n                    new RegExp(escapeRegExp(value).replace(/%/g, \".*\"), \"i\").test(fieldValue)\n                ) != isNot\n            );\n        case \"any\":\n        case \"not any\":\n            return true;\n        case \"child_of\":\n        case \"parent_of\":\n            return true;\n    }\n    throw new InvalidDomainError(\"could not match domain\");\n}\n\n/**\n * @param {Object} record\n * @returns {Object}\n */\nfunction makeOperators(record) {\n    const match = matchCondition.bind(null, record);\n    return {\n        \"!\": (x) => !match(x),\n        \"&\": (a, b) => match(a) && match(b),\n        \"|\": (a, b) => match(a) || match(b),\n    };\n}\n\n/**\n *\n * @param {Object} record\n * @param {DomainListRepr} domain\n * @returns {boolean}\n */\nfunction matchDomain(record, domain) {\n    if (domain.length === 0) {\n        return true;\n    }\n    const operators = makeOperators(record);\n    const reversedDomain = Array.from(domain).reverse();\n    const condStack = [];\n    for (const item of reversedDomain) {\n        const operator = typeof item === \"string\" && operators[item];\n        if (operator) {\n            const operands = condStack.splice(-operator.length);\n            condStack.push(operator(...operands));\n        } else {\n            condStack.push(item);\n        }\n    }\n    return matchCondition(record, condStack.pop());\n}\n", "import { Component, onWillStart, onWillUpdateProps } from \"@odoo/owl\";\nimport { CheckBox } from \"@web/core/checkbox/checkbox\";\nimport { Domain } from \"@web/core/domain\";\nimport { getDomainDisplayedOperators } from \"@web/core/domain_selector/domain_selector_operator_editor\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { ModelFieldSelector } from \"@web/core/model_field_selector/model_field_selector\";\nimport {\n    areEqualTrees,\n    condition,\n    connector,\n    formatValue,\n} from \"@web/core/tree_editor/condition_tree\";\nimport { domainFromTree } from \"@web/core/tree_editor/domain_from_tree\";\nimport { TreeEditor } from \"@web/core/tree_editor/tree_editor\";\nimport { getOperatorEditorInfo } from \"@web/core/tree_editor/tree_editor_operator_editor\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { getDefaultCondition } from \"./utils\";\n\nconst ARCHIVED_CONDITION = condition(\"active\", \"in\", [true, false]);\nconst ARCHIVED_DOMAIN = `[(\"active\", \"in\", [True, False])]`;\n\nexport class DomainSelector extends Component {\n    static template = \"web.DomainSelector\";\n    static components = { TreeEditor, CheckBox };\n    static props = {\n        domain: String,\n        resModel: String,\n        className: { type: String, optional: true },\n        defaultConnector: { type: [{ value: \"&\" }, { value: \"|\" }], optional: true },\n        isDebugMode: { type: Boolean, optional: true },\n        readonly: { type: Boolean, optional: true },\n        update: { type: Function, optional: true },\n        debugUpdate: { type: Function, optional: true },\n    };\n    static defaultProps = {\n        isDebugMode: false,\n        readonly: true,\n        update: () => {},\n    };\n\n    setup() {\n        this.fieldService = useService(\"field\");\n        this.treeProcessor = useService(\"tree_processor\");\n\n        this.tree = null;\n        this.showArchivedCheckbox = false;\n        this.includeArchived = false;\n\n        onWillStart(() => this.onPropsUpdated(this.props));\n        onWillUpdateProps((np) => this.onPropsUpdated(np));\n    }\n\n    async onPropsUpdated(p) {\n        let domain;\n        let isSupported = true;\n        try {\n            domain = new Domain(p.domain);\n        } catch {\n            isSupported = false;\n        }\n        if (!isSupported) {\n            this.tree = null;\n            this.showArchivedCheckbox = false;\n            this.includeArchived = false;\n            return;\n        }\n\n        const [tree, { fieldDef: activeFieldDef }] = await Promise.all([\n            this.treeProcessor.treeFromDomain(p.resModel, domain, !p.isDebugMode),\n            this.fieldService.loadFieldInfo(p.resModel, \"active\"),\n        ]);\n\n        this.tree = tree;\n        this.showArchivedCheckbox = this.getShowArchivedCheckBox(Boolean(activeFieldDef), p);\n\n        this.includeArchived = false;\n        if (this.showArchivedCheckbox) {\n            if (this.tree.type === \"connector\" && this.tree.value === \"&\") {\n                this.tree.children = this.tree.children.filter((child) => {\n                    if (areEqualTrees(child, ARCHIVED_CONDITION)) {\n                        this.includeArchived = true;\n                        return false;\n                    }\n                    return true;\n                });\n                if (this.tree.children.length === 1) {\n                    this.tree = this.tree.children[0];\n                }\n            } else if (areEqualTrees(this.tree, ARCHIVED_CONDITION)) {\n                this.includeArchived = true;\n                this.tree = connector(\"&\");\n            }\n        }\n    }\n\n    getShowArchivedCheckBox(hasActiveField, props) {\n        return hasActiveField;\n    }\n\n    getDefaultCondition(fieldDefs) {\n        return getDefaultCondition(fieldDefs);\n    }\n\n    getDefaultOperator(fieldDef) {\n        return getDomainDisplayedOperators(fieldDef)[0];\n    }\n\n    getOperatorEditorInfo(fieldDef) {\n        const operators = getDomainDisplayedOperators(fieldDef);\n        return getOperatorEditorInfo(operators, fieldDef);\n    }\n\n    getPathEditorInfo(resModel, defaultCondition) {\n        const { isDebugMode } = this.props;\n        return {\n            component: ModelFieldSelector,\n            extractProps: ({ update, value: path }) => ({\n                path,\n                update,\n                resModel,\n                isDebugMode,\n                readonly: false,\n            }),\n            isSupported: (path) => [0, 1].includes(path) || typeof path === \"string\",\n            defaultValue: () => defaultCondition.path,\n            stringify: (path) => formatValue(path),\n            message: _t(\"Invalid field chain\"),\n        };\n    }\n\n    toggleIncludeArchived() {\n        this.includeArchived = !this.includeArchived;\n        this.update(this.tree);\n    }\n\n    resetDomain() {\n        this.props.update(\"[]\");\n    }\n\n    onDomainInput(domain) {\n        if (this.props.debugUpdate) {\n            this.props.debugUpdate(domain);\n        }\n    }\n\n    onDomainChange(domain) {\n        this.props.update(domain, true);\n    }\n    update(tree) {\n        const archiveDomain = this.includeArchived ? ARCHIVED_DOMAIN : `[]`;\n        const domain = tree\n            ? Domain.and([domainFromTree(tree), archiveDomain]).toString()\n            : archiveDomain;\n        this.props.update(domain);\n    }\n}\n", "export function getDomainDisplayedOperators(fieldDef) {\n    if (!fieldDef) {\n        fieldDef = {};\n    }\n    const { type, is_property } = fieldDef;\n\n    if (is_property) {\n        switch (type) {\n            case \"many2many\":\n            case \"tags\":\n                return [\"in\", \"not in\", \"set\", \"not set\"];\n            case \"many2one\":\n            case \"selection\":\n                return [\"=\", \"!=\", \"set\", \"not set\"];\n        }\n    }\n    switch (type) {\n        case \"boolean\":\n            return [\"set\", \"not set\"];\n        case \"selection\":\n            return [\"=\", \"!=\", \"in\", \"not in\", \"set\", \"not set\"];\n        case \"char\":\n        case \"text\":\n        case \"html\":\n            return [\"=\", \"!=\", \"ilike\", \"not ilike\", \"starts with\", \"set\", \"not set\"];\n        case \"date\":\n        case \"datetime\":\n            return [\"in range\", \"=\", \"<\", \">\", \"set\", \"not set\"];\n        case \"integer\":\n        case \"float\":\n        case \"monetary\":\n            return [\"=\", \"!=\", \"<\", \">\", \"between\"];\n        case \"many2one\":\n        case \"many2many\":\n        case \"one2many\":\n            return [\"in\", \"not in\", \"ilike\", \"not ilike\", \"set\", \"not set\"];\n        case \"json\":\n            return [\"=\", \"!=\", \"ilike\", \"not ilike\", \"set\", \"not set\"];\n        case \"binary\":\n        case \"properties\":\n            return [\"set\", \"not set\"];\n        case undefined:\n            return [\"=\"];\n        default:\n            return [\n                \"=\",\n                \"!=\",\n                \"<\",\n                \">\",\n                \"ilike\",\n                \"not ilike\",\n                \"like\",\n                \"not like\",\n                \"=like\",\n                \"=ilike\",\n                \"in\",\n                \"not in\",\n                \"set\",\n                \"not set\",\n            ];\n    }\n}\n", "import { getDomainDisplayedOperators } from \"@web/core/domain_selector/domain_selector_operator_editor\";\nimport { condition } from \"@web/core/tree_editor/condition_tree\";\nimport { domainFromTree } from \"@web/core/tree_editor/domain_from_tree\";\nimport { getDefaultValue } from \"@web/core/tree_editor/tree_editor_value_editors\";\nimport { getDefaultPath } from \"@web/core/tree_editor/utils\";\nimport { useService } from \"@web/core/utils/hooks\";\n\nexport function getDefaultCondition(fieldDefs) {\n    const defaultPath = getDefaultPath(fieldDefs);\n    const fieldDef = fieldDefs[defaultPath];\n    const operator = getDomainDisplayedOperators(fieldDef)[0];\n    const value = getDefaultValue(fieldDef, operator);\n    return condition(fieldDef.name, operator, value);\n}\n\nexport function getDefaultDomain(fieldDefs) {\n    return domainFromTree(getDefaultCondition(fieldDefs));\n}\n\nexport function useGetDefaultLeafDomain() {\n    const fieldService = useService(\"field\");\n    return async (resModel) => {\n        const fieldDefs = await fieldService.loadFields(resModel);\n        return getDefaultDomain(fieldDefs);\n    };\n}\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { Component, useRef, useState } from \"@odoo/owl\";\nimport { Dialog } from \"@web/core/dialog/dialog\";\nimport { Domain } from \"@web/core/domain\";\nimport { DomainSelector } from \"@web/core/domain_selector/domain_selector\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { user } from \"@web/core/user\";\n\nexport class DomainSelectorDialog extends Component {\n    static template = \"web.DomainSelectorDialog\";\n    static components = {\n        Dialog,\n        DomainSelector,\n    };\n    static props = {\n        close: Function,\n        onConfirm: Function,\n        resModel: String,\n        className: { type: String, optional: true },\n        defaultConnector: { type: [{ value: \"&\" }, { value: \"|\" }], optional: true },\n        domain: String,\n        isDebugMode: { type: Boolean, optional: true },\n        readonly: { type: Boolean, optional: true },\n        text: { type: String, optional: true },\n        confirmButtonText: { type: String, optional: true },\n        disableConfirmButton: { type: Function, optional: true },\n        discardButtonText: { type: String, optional: true },\n        title: { type: String, optional: true },\n        context: { type: Object, optional: true },\n    };\n    static defaultProps = {\n        isDebugMode: false,\n        readonly: false,\n        context: {},\n    };\n\n    setup() {\n        this.notification = useService(\"notification\");\n        this.orm = useService(\"orm\");\n        this.state = useState({ domain: this.props.domain });\n        this.confirmButtonRef = useRef(\"confirm\");\n    }\n\n    get confirmButtonText() {\n        return this.props.confirmButtonText || _t(\"Confirm\");\n    }\n\n    get dialogTitle() {\n        return this.props.title || _t(\"Domain\");\n    }\n\n    get disabled() {\n        if (this.props.disableConfirmButton) {\n            return this.props.disableConfirmButton(this.state.domain);\n        }\n        return false;\n    }\n\n    get discardButtonText() {\n        return this.props.discardButtonText || _t(\"Discard\");\n    }\n\n    get domainSelectorProps() {\n        return {\n            className: this.props.className,\n            resModel: this.props.resModel,\n            readonly: this.props.readonly,\n            isDebugMode: this.props.isDebugMode,\n            defaultConnector: this.props.defaultConnector,\n            domain: this.state.domain,\n            update: (domain) => {\n                this.state.domain = domain;\n            },\n        };\n    }\n\n    async onConfirm() {\n        this.confirmButtonRef.el.disabled = true;\n        let domain;\n        let isValid;\n        try {\n            const evalContext = { ...user.context, ...this.props.context };\n            domain = new Domain(this.state.domain).toList(evalContext);\n        } catch {\n            isValid = false;\n        }\n        if (isValid === undefined) {\n            isValid = await rpc(\"/web/domain/validate\", {\n                model: this.props.resModel,\n                domain,\n            });\n        }\n        if (!isValid) {\n            if (this.confirmButtonRef.el) {\n                this.confirmButtonRef.el.disabled = false;\n            }\n            this.notification.add(_t(\"Domain is invalid. Please correct it\"), {\n                type: \"danger\",\n            });\n            return;\n        }\n        this.props.onConfirm(this.state.domain);\n        this.props.close();\n    }\n\n    onDiscard() {\n        this.props.close();\n    }\n}\n", "import { useComponent, useEffect, useEnv } from \"@odoo/owl\";\nimport { DROPDOWN_GROUP } from \"@web/core/dropdown/dropdown_group\";\n\n/**\n * @typedef DropdownGroupState\n * @property {boolean} isInGroup\n * @property {boolean} isOpen\n */\n\n/**\n * Will add (and remove) a dropdown from a parent\n * DropdownGroup component, allowing it to know\n * if it's in a group and if the group is open.\n *\n * @returns {DropdownGroupState}\n */\nexport function useDropdownGroup() {\n    const env = useEnv();\n\n    const group = {\n        isInGroup: DROPDOWN_GROUP in env,\n        get isOpen() {\n            return this.isInGroup && [...env[DROPDOWN_GROUP]].some((dropdown) => dropdown.isOpen);\n        },\n    };\n\n    if (group.isInGroup) {\n        const dropdown = useComponent();\n        useEffect(() => {\n            env[DROPDOWN_GROUP].add(dropdown.state);\n            return () => env[DROPDOWN_GROUP].delete(dropdown.state);\n        });\n    }\n\n    return group;\n}\n", "import { EventBus, onWillDestroy, useChildSubEnv, useEffect, useEnv } from \"@odoo/owl\";\nimport { localization } from \"@web/core/l10n/localization\";\nimport { useBus, useService } from \"@web/core/utils/hooks\";\nimport { effect } from \"@web/core/utils/reactive\";\n\nexport const DROPDOWN_NESTING = Symbol(\"dropdownNesting\");\nconst BUS = new EventBus();\n\nclass DropdownNestingState {\n    constructor({ parent, close }) {\n        this._isOpen = false;\n        this.parent = parent;\n        this.children = new Set();\n        this.close = close;\n\n        parent?.children.add(this);\n    }\n\n    set isOpen(value) {\n        this._isOpen = value;\n        if (this._isOpen) {\n            BUS.trigger(\"dropdown-opened\", this);\n        }\n    }\n\n    get isOpen() {\n        return this._isOpen;\n    }\n\n    remove() {\n        this.parent?.children.delete(this);\n    }\n\n    closeAllParents() {\n        this.close();\n        if (this.parent) {\n            this.parent.closeAllParents();\n        }\n    }\n\n    closeChildren() {\n        this.children.forEach((child) => child.close());\n    }\n\n    shouldIgnoreChanges(other) {\n        return (\n            other === this ||\n            other.activeEl !== this.activeEl ||\n            [...this.children].some((child) => child.shouldIgnoreChanges(other))\n        );\n    }\n\n    handleChange(other) {\n        // Prevents closing the dropdown when a change is coming from itself or from a children.\n        if (this.shouldIgnoreChanges(other)) {\n            return;\n        }\n\n        if (other.isOpen && this.isOpen) {\n            this.close();\n        }\n    }\n}\n\n/**\n * This hook is used to manage communication between dropdowns.\n *\n * When a dropdown is open, every other dropdown that is not a parent\n * is closed. It also uses the current's ui active element to only\n * close itself when the active element is the same as the current\n * dropdown to separate dropdowns in different dialogs.\n *\n * @param {import(\"@web/core/dropdown/dropdown_hooks\").DropdownState} state\n * @returns\n */\nexport function useDropdownNesting(state) {\n    const env = useEnv();\n    const current = new DropdownNestingState({\n        parent: env[DROPDOWN_NESTING],\n        close: () => state.close(),\n    });\n\n    // Set up UI active element related behavior ---------------------------\n    const uiService = useService(\"ui\");\n    useEffect(\n        () => {\n            Promise.resolve().then(() => {\n                current.activeEl = uiService.activeElement;\n            });\n        },\n        () => []\n    );\n\n    useChildSubEnv({ [DROPDOWN_NESTING]: current });\n    useBus(BUS, \"dropdown-opened\", ({ detail: other }) => current.handleChange(other));\n\n    effect(\n        (state) => {\n            current.isOpen = state.isOpen;\n        },\n        [state]\n    );\n\n    onWillDestroy(() => {\n        current.remove();\n    });\n\n    const isDropdown = (target) => target && target.classList.contains(\"o-dropdown\");\n    const isRTL = () => localization.direction === \"rtl\";\n\n    return {\n        get hasParent() {\n            return Boolean(current.parent);\n        },\n        /**@type {import(\"@web/core/navigation/navigation\").NavigationOptions} */\n        navigationOptions: {\n            onUpdated: (navigator) => {\n                if (current.parent && !navigator.activeItem) {\n                    navigator.items[0]?.setActive();\n                }\n            },\n            hotkeys: {\n                escape: () => current.close(),\n                arrowleft: {\n                    isAvailable: () => true,\n                    callback: (navigator) => {\n                        if (isRTL() && isDropdown(navigator.activeItem?.target)) {\n                            navigator.activeItem?.select();\n                        } else if (current.parent) {\n                            current.close();\n                        }\n                    },\n                },\n                arrowright: {\n                    isAvailable: () => true,\n                    callback: (navigator) => {\n                        if (isRTL() && current.parent) {\n                            current.close();\n                        } else if (isDropdown(navigator.activeItem?.target)) {\n                            navigator.activeItem?.select();\n                        }\n                    },\n                },\n            },\n        },\n    };\n}\n", "import { Component, onMounted, onRendered, onWillDestroy, onWillStart, xml } from \"@odoo/owl\";\nimport { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\n\nexport class DropdownPopover extends Component {\n    static components = { DropdownItem };\n    static template = xml`\n        <t t-if=\"this.props.items\">\n            <t t-foreach=\"this.props.items\" t-as=\"item\" t-key=\"this.getKey(item, item_index)\">\n                <DropdownItem class=\"item.class\" onSelected=\"() => item.onSelected()\" t-out=\"item.label\"/>\n            </t>\n        </t>\n        <t t-slot=\"content\" />\n    `;\n    static props = {\n        // Popover service\n        close: { type: Function, optional: true },\n\n        // Events & Handlers\n        beforeOpen: { type: Function, optional: true },\n        onOpened: { type: Function, optional: true },\n        onClosed: { type: Function, optional: true },\n\n        // Rendering & Context\n        refresher: Object,\n        slots: Object,\n        items: { type: Array, optional: true },\n    };\n\n    setup() {\n        onRendered(() => {\n            // Note that the Dropdown component and the DropdownPopover component\n            // are not in the same context.\n            // So when the Dropdown component is re-rendered, the DropdownPopover\n            // component must also re-render itself.\n            // This is why we subscribe to this reactive, which is changed when\n            // the Dropdown component is re-rendered.\n            this.props.refresher.token;\n        });\n\n        onWillStart(async () => {\n            await this.props.beforeOpen?.();\n        });\n\n        onMounted(() => {\n            this.props.onOpened?.();\n        });\n\n        onWillDestroy(() => {\n            this.props.onClosed?.();\n        });\n    }\n\n    getKey(item, index) {\n        return \"id\" in item ? item.id : index;\n    }\n}\n", "import { Component, onPatched, useState } from \"@odoo/owl\";\n\nexport const ACCORDION = Symbol(\"Accordion\");\nexport class AccordionItem extends Component {\n    static template = \"web.AccordionItem\";\n    static components = {};\n    static props = {\n        slots: {\n            type: Object,\n            shape: {\n                default: {},\n            },\n        },\n        description: String,\n        selected: {\n            type: Boolean,\n            optional: true,\n        },\n        class: {\n            type: String,\n            optional: true,\n        },\n    };\n    static defaultProps = {\n        class: \"\",\n        selected: false,\n    };\n\n    setup() {\n        this.state = useState({\n            open: false,\n        });\n        this.parentComponent = this.env[ACCORDION];\n        onPatched(() => {\n            this.parentComponent?.accordionStateChanged?.();\n        });\n    }\n}\n", "import { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\n\nexport class CheckboxItem extends DropdownItem {\n    static template = \"web.CheckboxItem\";\n    static props = {\n        ...DropdownItem.props,\n        checked: {\n            type: Boolean,\n            optional: false,\n        },\n    };\n}\n", "import {\n    Component,\n    onMounted,\n    onRendered,\n    onWillUpdateProps,\n    reactive,\n    status,\n    useEffect,\n    xml,\n} from \"@odoo/owl\";\nimport { useDropdownGroup } from \"@web/core/dropdown/_behaviours/dropdown_group_hook\";\nimport { useDropdownNesting } from \"@web/core/dropdown/_behaviours/dropdown_nesting\";\nimport { DropdownPopover } from \"@web/core/dropdown/_behaviours/dropdown_popover\";\nimport { useDropdownState } from \"@web/core/dropdown/dropdown_hooks\";\nimport { useNavigation } from \"@web/core/navigation/navigation\";\nimport { usePopover } from \"@web/core/popover/popover_hook\";\nimport { mergeClasses } from \"@web/core/utils/classname\";\nimport { useChildRef, useService } from \"@web/core/utils/hooks\";\nimport { deepMerge } from \"@web/core/utils/objects\";\nimport { effect } from \"@web/core/utils/reactive\";\nimport { utils } from \"@web/core/ui/ui_service\";\nimport { hasTouch } from \"@web/core/browser/feature_detection\";\n\nfunction getFirstElementOfNode(node) {\n    if (!node) {\n        return null;\n    }\n    if (node.el) {\n        return node.el.nodeType === Node.ELEMENT_NODE ? node.el : null;\n    }\n    if (node.bdom || node.child) {\n        return getFirstElementOfNode(node.bdom || node.child);\n    }\n    if (node.children) {\n        for (const child of node.children) {\n            const el = getFirstElementOfNode(child);\n            if (el) {\n                return el;\n            }\n        }\n    }\n    return null;\n}\n\n/**\n * The Dropdown component allows to define a menu that will\n * show itself when a target is toggled.\n *\n * Items are defined using DropdownItems. Dropdowns are\n * also allowed as items to be able to create nested\n * dropdown menus.\n */\nexport class Dropdown extends Component {\n    static template = xml`<t t-slot=\"default\"/>`;\n    static components = {};\n    static props = {\n        menuClass: { optional: true },\n        position: { type: String, optional: true },\n        slots: {\n            type: Object,\n            shape: {\n                default: { optional: true },\n                content: { optional: true },\n            },\n        },\n\n        items: {\n            optional: true,\n            type: Array,\n            elements: {\n                type: Object,\n                shape: {\n                    label: String,\n                    onSelected: Function,\n                    class: { optional: true },\n                    \"*\": true,\n                },\n            },\n        },\n\n        menuRef: { type: Function, optional: true }, // to be used with useChildRef\n        disabled: { type: Boolean, optional: true },\n        holdOnHover: { type: Boolean, optional: true },\n        focusToggleOnClosed: { type: Boolean, optional: true },\n\n        beforeOpen: { type: Function, optional: true },\n        onOpened: { type: Function, optional: true },\n        onStateChanged: { type: Function, optional: true },\n\n        /** Manual state handling, @see useDropdownState */\n        state: {\n            type: Object,\n            shape: {\n                isOpen: Boolean,\n                close: Function,\n                open: Function,\n                \"*\": true,\n            },\n            optional: true,\n        },\n        manual: { type: Boolean, optional: true },\n\n        /** When true, do not add optional styling css classes on the target*/\n        noClasses: { type: Boolean, optional: true },\n\n        /**\n         * Override the internal navigation hook options\n         * @type {import(\"@web/core/navigation/navigation\").NavigationOptions}\n         */\n        navigationOptions: { type: Object, optional: true },\n        bottomSheet: { type: Boolean, optional: true },\n    };\n    static defaultProps = {\n        disabled: false,\n        holdOnHover: false,\n        focusToggleOnClosed: true,\n        menuClass: \"\",\n        state: undefined,\n        noClasses: false,\n        navigationOptions: {},\n        bottomSheet: true,\n    };\n\n    setup() {\n        this.menuRef = this.props.menuRef || useChildRef();\n\n        this.state = this.props.state || useDropdownState();\n        this.nesting = useDropdownNesting(this.state);\n        this.group = useDropdownGroup();\n\n        this.navigation = useNavigation(this.menuRef, {\n            shouldRegisterHotkeys: false,\n            isNavigationAvailable: () => this.state.isOpen,\n            getItems: () => {\n                if (this.state.isOpen && this.menuRef.el) {\n                    return this.menuRef.el.querySelectorAll(\n                        \":scope .o-navigable, :scope .o-dropdown\"\n                    );\n                } else {\n                    return [];\n                }\n            },\n            // Using deepMerge allows to keep entries of both option.hotkeys\n            ...deepMerge(this.nesting.navigationOptions, this.props.navigationOptions),\n        });\n\n        this.uiService = useService(\"ui\");\n\n        const getPosition = () => this.position;\n        const options = {\n            animation: false,\n            arrow: false,\n            closeOnClickAway: (target) => this.popoverCloseOnClickAway(target),\n            closeOnEscape: false, // Handled via navigation and prevents closing root of nested dropdown\n            env: this.__owl__.childEnv,\n            holdOnHover: this.props.holdOnHover,\n            onClose: () => this.state.close(),\n            onPositioned: (el, { direction }) => this.setTargetDirectionClass(direction),\n            popoverClass: mergeClasses(\n                \"o-dropdown--menu dropdown-menu mx-0\",\n                { \"o-dropdown--menu-submenu\": this.hasParent },\n                this.props.menuClass\n            ),\n            role: \"menu\",\n            get position() {\n                return getPosition();\n            },\n            ref: this.menuRef,\n            setActiveElement: false,\n        };\n        if (this.isBottomSheet) {\n            Object.assign(options, {\n                useBottomSheet: true,\n                class: mergeClasses(\"o-dropdown--menu dropdown-menu show\", this.props.menuClass),\n            });\n        }\n        this.popover = usePopover(DropdownPopover, options);\n\n        // As the popover is in another context we need to force\n        // its re-rendering when the dropdown re-renders\n        onRendered(() => (this.popoverRefresher ? this.popoverRefresher.token++ : null));\n\n        onMounted(() => this.onStateChanged(this.state));\n        effect((state) => this.onStateChanged(state), [this.state]);\n\n        useEffect(\n            (target) => this.setTargetElement(target),\n            () => [this.target]\n        );\n\n        onWillUpdateProps(({ disabled }) => {\n            if (disabled) {\n                this.closePopover();\n            }\n        });\n    }\n\n    get isBottomSheet() {\n        return utils.isSmall() && hasTouch() && this.props.bottomSheet;\n    }\n\n    /** @type {string} */\n    get position() {\n        return this.props.position || (this.hasParent ? \"right-start\" : \"bottom-start\");\n    }\n\n    get hasParent() {\n        return this.nesting.hasParent;\n    }\n\n    /** @type {HTMLElement|null} */\n    get target() {\n        const target = getFirstElementOfNode(this.__owl__.bdom);\n        if (!target) {\n            throw new Error(\n                \"Could not find a valid dropdown toggler, prefer a single html element and put any dynamic content inside of it.\"\n            );\n        }\n        return target;\n    }\n\n    handleClick(event) {\n        if (this.props.disabled) {\n            return;\n        }\n\n        event.stopPropagation();\n        if (this.state.isOpen && !this.hasParent) {\n            this.state.close();\n        } else {\n            this.state.open();\n        }\n    }\n\n    handleMouseEnter() {\n        if (this.props.disabled) {\n            return;\n        }\n\n        if (this.hasParent || this.group.isOpen) {\n            this.target.focus();\n            this.state.open();\n        }\n    }\n\n    onStateChanged(state) {\n        if (state.isOpen) {\n            this.openPopover();\n        } else {\n            this.closePopover();\n        }\n    }\n\n    popoverCloseOnClickAway(target) {\n        const rootNode = target.getRootNode();\n        if (rootNode instanceof ShadowRoot) {\n            target = rootNode.host;\n        }\n        return this.uiService.getActiveElementOf(target) === this.activeEl;\n    }\n\n    setTargetElement(target) {\n        if (!target) {\n            return;\n        }\n\n        target.ariaExpanded = false;\n        const optionalClasses = [];\n        const requiredClasses = [];\n        optionalClasses.push(\"o-dropdown\");\n\n        if (this.hasParent) {\n            requiredClasses.push(\"o-dropdown--has-parent\");\n        }\n\n        const tagName = target.tagName.toLowerCase();\n        if (![\"input\", \"textarea\", \"table\", \"thead\", \"tbody\", \"tr\", \"th\", \"td\"].includes(tagName)) {\n            optionalClasses.push(\"dropdown-toggle\");\n            if (this.hasParent) {\n                optionalClasses.push(\"o-dropdown-item\", \"dropdown-item\");\n                requiredClasses.push(\"o-navigable\");\n\n                if (!target.classList.contains(\"o-dropdown--no-caret\")) {\n                    requiredClasses.push(\"o-dropdown-caret\");\n                }\n            }\n        }\n\n        target.classList.add(...requiredClasses);\n        if (!this.props.noClasses) {\n            target.classList.add(...optionalClasses);\n        }\n\n        this.defaultDirection = this.position.split(\"-\")[0];\n        this.setTargetDirectionClass(this.defaultDirection);\n\n        if (!this.props.manual) {\n            target.addEventListener(\"click\", this.handleClick.bind(this));\n            target.addEventListener(\"mouseenter\", this.handleMouseEnter.bind(this));\n\n            return () => {\n                target.removeEventListener(\"click\", this.handleClick.bind(this));\n                target.removeEventListener(\"mouseenter\", this.handleMouseEnter.bind(this));\n            };\n        }\n    }\n\n    setTargetDirectionClass(direction) {\n        if (!this.target || this.props.noClasses) {\n            return;\n        }\n        const directionClasses = {\n            bottom: \"dropdown\",\n            top: \"dropup\",\n            left: \"dropstart\",\n            right: \"dropend\",\n        };\n        this.target.classList.remove(...Object.values(directionClasses));\n        this.target.classList.add(directionClasses[direction]);\n    }\n\n    openPopover() {\n        if (this.popover.isOpen || status(this) !== \"mounted\") {\n            return;\n        }\n        if (!this.target || !this.target.isConnected) {\n            this.state.close();\n            return;\n        }\n\n        this.popoverRefresher = reactive({ token: 0 });\n        const props = {\n            beforeOpen: () => this.props.beforeOpen?.(),\n            onOpened: () => this.onOpened(),\n            onClosed: () => this.onClosed(),\n            refresher: this.popoverRefresher,\n            items: this.props.items,\n            slots: this.props.slots,\n        };\n        this.popover.open(this.target, props);\n    }\n\n    closePopover() {\n        this.popover.close();\n        if (this.props.focusToggleOnClosed && !this.group.isInGroup) {\n            this._focusedElBeforeOpen?.focus();\n            this._focusedElBeforeOpen = undefined;\n        }\n    }\n\n    onOpened() {\n        this._focusedElBeforeOpen = document.activeElement;\n        this.activeEl = this.uiService.activeElement;\n        this.navigation.registerHotkeys();\n        this.navigation.update();\n        this.props.onOpened?.();\n        this.props.onStateChanged?.(true);\n\n        if (this.target) {\n            this.target.ariaExpanded = true;\n            this.target.classList.add(\"show\");\n        }\n\n        this.observer = new MutationObserver(() => this.navigation.update());\n        this.observer.observe(this.menuRef.el, {\n            childList: true,\n            subtree: true,\n        });\n    }\n\n    onClosed() {\n        this.navigation.unregisterHotkeys();\n        this.navigation.update();\n        this.props.onStateChanged?.(false);\n        delete this.activeEl;\n\n        if (this.target) {\n            this.target.ariaExpanded = false;\n            this.target.classList.remove(\"show\");\n            this.setTargetDirectionClass(this.defaultDirection);\n        }\n\n        if (this.observer) {\n            this.observer.disconnect();\n            this.observer = null;\n        }\n    }\n}\n", "import { Component, onWillDestroy, useChildSubEnv, xml } from \"@odoo/owl\";\n\nconst GROUPS = new Map();\n\nfunction getGroup(id) {\n    if (!GROUPS.has(id)) {\n        GROUPS.set(id, {\n            group: new Set(),\n            count: 0,\n        });\n    }\n    GROUPS.get(id).count++;\n    return GROUPS.get(id).group;\n}\n\nfunction removeGroup(id) {\n    const groupData = GROUPS.get(id);\n    groupData.count--;\n    if (groupData.count <= 0) {\n        GROUPS.delete(id);\n    }\n}\n\nexport const DROPDOWN_GROUP = Symbol(\"dropdownGroup\");\nexport class DropdownGroup extends Component {\n    static template = xml`<t t-slot=\"default\"/>`;\n    static props = {\n        group: { type: String, optional: true },\n        slots: Object,\n    };\n\n    setup() {\n        if (this.props.group) {\n            const group = getGroup(this.props.group);\n            onWillDestroy(() => removeGroup(this.props.group));\n            useChildSubEnv({ [DROPDOWN_GROUP]: group });\n        } else {\n            useChildSubEnv({ [DROPDOWN_GROUP]: new Set() });\n        }\n    }\n}\n", "import { useEnv, useState } from \"@odoo/owl\";\nimport { DROPDOWN_NESTING } from \"@web/core/dropdown/_behaviours/dropdown_nesting\";\nimport { Reactive } from \"@web/core/utils/reactive\";\n\n/**\n * Represents the state of a dropdown.\n * In order to use it, pass the state instance to the dropdown component, i.e.:\n *  <Dropdown state=\"dropdownState\" ...>...</Dropdown>\n * @param {Object} callbacks\n * @param {Function} callbacks.onOpen\n * @param {Function} callbacks.onClose\n */\nexport class DropdownState extends Reactive {\n    isOpen = false;\n    constructor({ onOpen, onClose } = {}) {\n        super();\n        this._onOpen = onOpen;\n        this._onClose = onClose;\n    }\n    open() {\n        this.isOpen = true;\n        this._onOpen?.();\n    }\n    close() {\n        this.isOpen = false;\n        this._onClose?.();\n    }\n}\n\n/**\n * Hook used to interact with the Dropdown state and to subscribe to changes.\n * @param {Object} callbacks\n * @param {Function} callbacks.onOpen\n * @param {Function} callbacks.onClose\n * @returns {DropdownState}\n */\nexport function useDropdownState({ onOpen, onClose } = {}) {\n    return useState(new DropdownState({ onOpen, onClose }));\n}\n\n/**\n * Can be used by components to have some control\n * how and when a wrapping dropdown should close.\n */\nexport function useDropdownCloser() {\n    const env = useEnv();\n    const dropdown = env[DROPDOWN_NESTING];\n    return {\n        close: () => dropdown?.close(),\n        closeChildren: () => dropdown?.closeChildren(),\n        closeAll: () => dropdown?.closeAllParents(),\n    };\n}\n", "import { Component } from \"@odoo/owl\";\nimport { useDropdownCloser } from \"@web/core/dropdown/dropdown_hooks\";\n\nconst ClosingMode = {\n    None: \"none\",\n    ClosestParent: \"closest\",\n    AllParents: \"all\",\n};\n\nexport class DropdownItem extends Component {\n    static template = \"web.DropdownItem\";\n    static props = {\n        tag: {\n            type: String,\n            optional: true,\n        },\n        class: {\n            type: [String, Object],\n            optional: true,\n        },\n        onSelected: {\n            type: Function,\n            optional: true,\n        },\n        closingMode: {\n            type: ClosingMode,\n            optional: true,\n        },\n        attrs: {\n            type: Object,\n            optional: true,\n        },\n        slots: { Object, optional: true },\n    };\n    static defaultProps = {\n        closingMode: ClosingMode.AllParents,\n        attrs: {},\n    };\n\n    setup() {\n        this.dropdownControl = useDropdownCloser();\n    }\n\n    onClick(ev) {\n        if (this.props.attrs && this.props.attrs.href) {\n            ev.preventDefault();\n        }\n        this.props.onSelected?.(ev);\n        switch (this.props.closingMode) {\n            case ClosingMode.ClosestParent:\n                this.dropdownControl.close();\n                break;\n            case ClosingMode.AllParents:\n                this.dropdownControl.closeAll();\n                break;\n        }\n    }\n}\n", "import { Component, useEffect, useRef, useState } from \"@odoo/owl\";\n\nexport class Dropzone extends Component {\n    static props = {\n        extraClass: { type: String, optional: true },\n        onDrop: { type: Function, optional: true },\n        ref: [Object, Function],\n        slots: { type: Object, optional: true },\n    };\n    static template = \"web.Dropzone\";\n\n    setup() {\n        super.setup();\n        this.root = useRef(\"root\");\n        this.state = useState({\n            isDraggingInside: false,\n        });\n        useEffect(() => {\n            const { top, left, width, height } = this.props.ref.el.getBoundingClientRect();\n            this.root.el.style = `top:${top}px;left:${left}px;width:${width}px;height:${height}px;`;\n        });\n    }\n}\n", "import { Dropzone } from \"@web/core/dropzone/dropzone\";\nimport { useEffect, useExternalListener } from \"@odoo/owl\";\nimport { useService } from \"@web/core/utils/hooks\";\n\n/**\n * @param {Ref} targetRef - Element on which to place the dropzone.\n * @param {Class} dropzoneComponent - Class used to instantiate the dropzone component.\n * @param {Object} dropzoneComponentProps - Props given to the instantiated dropzone component.\n * @param {function} isDropzoneEnabled - Function that determines whether the dropzone should be enabled.\n */\nexport function useCustomDropzone(targetRef, dropzoneComponent, dropzoneComponentProps, isDropzoneEnabled = () => true) {\n    const overlayService = useService(\"overlay\");\n    const uiService = useService(\"ui\");\n\n    let dragCount = 0;\n    let hasTarget = false;\n    let removeDropzone = false;\n\n    useExternalListener(document, \"dragenter\", onDragEnter, { capture: true });\n    useExternalListener(document, \"dragleave\", onDragLeave, { capture: true });\n    // Prevents the browser to open or download the file when it is dropped\n    // outside of the dropzone.\n    useExternalListener(window, \"dragover\", (ev) => {\n        if (ev.dataTransfer && ev.dataTransfer.types.includes(\"Files\")) {\n            ev.preventDefault();\n        }\n    });\n    useExternalListener(\n        window,\n        \"drop\",\n        (ev) => {\n            if (ev.dataTransfer && ev.dataTransfer.types.includes(\"Files\")) {\n                ev.preventDefault();\n            }\n            dragCount = 0;\n            updateDropzone();\n        },\n        { capture: true }\n    );\n\n    function updateDropzone() {\n        const hasDropzone = !!removeDropzone;\n        const isTargetInActiveElement = uiService.activeElement.contains(targetRef.el);\n        const shouldDisplayDropzone = dragCount && hasTarget && isTargetInActiveElement && isDropzoneEnabled();\n\n        if (shouldDisplayDropzone && !hasDropzone) {\n            removeDropzone = overlayService.add(dropzoneComponent, {\n                ref: targetRef,\n                ...dropzoneComponentProps\n            });\n        }\n        if (!shouldDisplayDropzone && hasDropzone) {\n            removeDropzone();\n            removeDropzone = false;\n        }\n    }\n\n    function onDragEnter(ev) {\n        if (dragCount || (ev.dataTransfer && ev.dataTransfer.types.includes(\"Files\"))) {\n            dragCount++;\n            updateDropzone();\n        }\n    }\n\n    function onDragLeave() {\n        if (dragCount) {\n            dragCount--;\n            updateDropzone();\n        }\n    }\n\n    useEffect(\n        (el) => {\n            hasTarget = !!el;\n            updateDropzone();\n        },\n        () => [targetRef.el]\n    );\n}\n\n/**\n * @param {Ref} targetRef - Element on which to place the dropzone.\n * @param {function} onDrop - Callback function called when the user drops a file on the dropzone.\n * @param {string} extraClass - Classes that will be added to the standard `Dropzone` component.\n * @param {function} isDropzoneEnabled - Function that determines whether the dropzone should be enabled.\n */\nexport function useDropzone(targetRef, onDrop, extraClass, isDropzoneEnabled = () => true) {\n    const dropzoneComponent = Dropzone;\n    const dropzoneComponentProps = { extraClass, onDrop };\n    useCustomDropzone(targetRef, dropzoneComponent, dropzoneComponentProps, isDropzoneEnabled);\n}\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { user } from \"@web/core/user\";\nimport { RainbowMan } from \"./rainbow_man\";\n\nconst effectRegistry = registry.category(\"effects\");\n\n// -----------------------------------------------------------------------------\n// RainbowMan effect\n// -----------------------------------------------------------------------------\n\n/**\n * Handles effect of type \"rainbow_man\". If the effects aren't disabled, returns\n * the RainbowMan component to instantiate and its props. If the effects are\n * disabled, displays the message in a notification.\n *\n * @param {Object} env\n * @param {Object} [params={}]\n * @param {string} [params.message=\"Well Done!\"]\n *    The message in the notice the rainbowman holds or the content of the notification if effects are disabled\n *    Can be a simple a string\n *    Can be a string representation of html (prefer component if you want interactions in the DOM)\n * @param {string} [params.img_url=\"/web/static/img/smile.svg\"]\n *    The url of the image to display inside the rainbow\n * @param {\"slow\"|\"medium\"|\"fast\"|\"no\"} [params.fadeout=\"medium\"]\n *    Delay for rainbowman to disappear\n *    'fast' will make rainbowman dissapear quickly\n *    'medium' and 'slow' will wait little longer before disappearing (can be used when options.message is longer)\n *    'no' will keep rainbowman on screen until user clicks anywhere outside rainbowman\n * @param {typeof import(\"@odoo/owl\").Component} [params.Component]\n *    Custom Component class to instantiate inside the Rainbow Man\n * @param {Object} [params.props]\n *    If params.Component is given, its props can be passed with this argument\n */\nfunction rainbowMan(env, params = {}) {\n    let message = params.message;\n    if (message instanceof Element) {\n        console.log(\n            \"Providing an HTML element to an effect is deprecated. Note that all event handlers will be lost.\"\n        );\n        message = message.outerHTML;\n    } else if (!message) {\n        message = _t(\"Well Done!\");\n    }\n    if (user.showEffect) {\n        /** @type {import(\"./rainbow_man\").RainbowManProps} */\n        const props = {\n            imgUrl: params.img_url || \"/web/static/img/smile.svg\",\n            fadeout: params.fadeout || \"medium\",\n            message,\n            Component: params.Component,\n            props: params.props,\n        };\n        return { Component: RainbowMan, props };\n    }\n    env.services.notification.add(message);\n}\neffectRegistry.add(\"rainbow_man\", rainbowMan);\n\n// -----------------------------------------------------------------------------\n// Effect service\n// -----------------------------------------------------------------------------\n\nexport const effectService = {\n    dependencies: [\"overlay\"],\n    start(env, { overlay }) {\n        /**\n         * @param {Object} [params] various params depending on the type of effect\n         * @param {string} [params.type=\"rainbow_man\"] the effect to display\n         */\n        const add = (params = {}) => {\n            const type = params.type || \"rainbow_man\";\n            const effect = effectRegistry.get(type);\n            const { Component, props } = effect(env, params) || {};\n            if (Component) {\n                const remove = overlay.add(Component, {\n                    ...props,\n                    close: () => remove(),\n                });\n            }\n        };\n\n        return { add };\n    },\n};\n\nregistry.category(\"services\").add(\"effect\", effectService);\n", "import { browser } from \"@web/core/browser/browser\";\n\nimport { Component, useEffect, useExternalListener, useState } from \"@odoo/owl\";\n\n/**\n * @typedef Common\n * @property {string} [fadeout='medium'] Delay for rainbowman to disappear.\n *  - 'fast' will make rainbowman dissapear quickly,\n *  - 'medium' and 'slow' will wait little longer before disappearing\n *      (can be used when props.message is longer),\n *  - 'no' will keep rainbowman on screen until user clicks anywhere outside rainbowman\n * @property {string} [imgUrl] URL of the image to be displayed\n *\n * @typedef Simple\n * @property {string} message Message to be displayed on rainbowman card\n *\n * @typedef Custom\n * @property {typeof import(\"@odoo/owl\").Component} Component\n * @property {any} [props]\n *\n * @typedef {Common & (Simple | Custom)} RainbowManProps\n */\n\n/**\n * The RainbowMan Component is meant to display a 'fun/rewarding' message.  For\n * example, when the user marked a large deal as won, or when he cleared its inbox.\n *\n * This component is mostly a picture and a message with a rainbow animation around.\n * If you want to display a RainbowMan, you probably do not want to do it by\n * importing this file.  The usual way to do that would be to use the effect\n * service.\n */\nexport class RainbowMan extends Component {\n    static template = \"web.RainbowMan\";\n    static rainbowFadeouts = { slow: 4500, medium: 3500, fast: 2000, no: false };\n    static props = {\n        fadeout: String,\n        close: Function,\n        message: String,\n        imgUrl: String,\n        Component: { type: Function, optional: true },\n        props: { type: Object, optional: true },\n    };\n\n    setup() {\n        useExternalListener(document.body, \"click\", this.closeRainbowMan);\n        this.state = useState({ isFading: false });\n        this.delay = RainbowMan.rainbowFadeouts[this.props.fadeout];\n        if (this.delay) {\n            useEffect(\n                () => {\n                    const timeout = browser.setTimeout(() => {\n                        this.state.isFading = true;\n                    }, this.delay);\n                    return () => browser.clearTimeout(timeout);\n                },\n                () => []\n            );\n        }\n    }\n\n    onAnimationEnd(ev) {\n        if (this.delay && ev.animationName === \"reward-fading-reverse\") {\n            ev.stopPropagation();\n            this.closeRainbowMan();\n        }\n    }\n\n    closeRainbowMan() {\n        this.props.close();\n    }\n}\n", "import { markEventHandled } from \"@web/core/utils/misc\";\n\nimport {\n    App,\n    Component,\n    onMounted,\n    onPatched,\n    onWillPatch,\n    onWillStart,\n    onWillUnmount,\n    reactive,\n    useComponent,\n    useEffect,\n    useExternalListener,\n    useRef,\n    useState,\n    xml,\n} from \"@odoo/owl\";\n\nimport { loadBundle } from \"@web/core/assets\";\nimport { _t, appTranslateFn } from \"@web/core/l10n/translation\";\nimport { usePopover } from \"@web/core/popover/popover_hook\";\nimport { fuzzyLookup } from \"@web/core/utils/search\";\nimport { useAutofocus, useService } from \"@web/core/utils/hooks\";\nimport { isMobileOS } from \"@web/core/browser/feature_detection\";\nimport { Deferred } from \"../utils/concurrency\";\nimport { Dialog } from \"../dialog/dialog\";\nimport { getTemplate } from \"@web/core/templates\";\n\n/**\n * @typedef Emoji\n * @property {string} category\n * @property {string} codepoints the emoji itself to be displayed\n * @property {string[]} emoticons string substitution (eg: \":p\")\n * @property {string[]} keywords\n * @property {string} name\n * @property {string[]} shortcodes\n */\n\nexport function useEmojiPicker(...args) {\n    return usePicker(EmojiPicker, ...args);\n}\n\nexport const loader = reactive({\n    loadEmoji: () => loadBundle(\"web.assets_emoji\"),\n    /** @type {{ emojiValueToShortcodes: Object<string, string[]>, emojiRegex: RegExp} }} */\n    loaded: undefined,\n});\n\n/** @returns {Promise<{ categories: Object[], emojis: Emoji[] }>\")} */\nexport async function loadEmoji() {\n    const res = { categories: [], emojis: [] };\n    try {\n        await loader.loadEmoji();\n        const { getCategories, getEmojis } = odoo.loader.modules.get(\n            \"@web/core/emoji_picker/emoji_data\"\n        );\n        res.categories = getCategories();\n        res.emojis = getEmojis();\n        return res;\n    } catch {\n        // Could be intentional (tour ended successfully while emoji still loading)\n        return res;\n    } finally {\n        if (!loader.loaded) {\n            const emojiValueToShortcodes = {};\n            for (const emoji of res.emojis) {\n                emojiValueToShortcodes[emoji.codepoints] = emoji.shortcodes;\n            }\n            loader.loaded = {\n                emojiValueToShortcodes,\n                emojiRegex: new RegExp(\n                    Object.keys(emojiValueToShortcodes).length\n                        ? Object.keys(emojiValueToShortcodes)\n                              .map((c) => c.replace(/[-/\\\\^$*+?.()|[\\]{}]/g, \"\\\\$&\"))\n                              .sort((a, b) => b.length - a.length) // Sort to get composed emojis first\n                              .join(\"|\")\n                        : /(?!)/,\n                    \"gu\"\n                ),\n            };\n        }\n    }\n}\n\nexport const PICKER_PROPS = [\n    \"PickerComponent?\",\n    \"close?\",\n    \"onClose?\",\n    \"onSelect\",\n    \"state?\",\n    \"storeScroll?\",\n    \"mobile?\",\n];\n\nexport class EmojiPicker extends Component {\n    static props = [...PICKER_PROPS, \"class?\", \"initialSearchTerm?\"];\n    static template = \"web.EmojiPicker\";\n\n    categories = null;\n    /** @type {Emoji[]|null} */\n    emojis = null;\n    shouldScrollElem = null;\n    lastSearchTerm;\n    keyboardNavigated = false;\n\n    setup() {\n        this.gridRef = useRef(\"emoji-grid\");\n        this.navbarRef = useRef(\"navbar\");\n        this.ui = useService(\"ui\");\n        this.isMobileOS = isMobileOS();\n        this.state = useState({\n            activeEmojiIndex: 0,\n            categoryId: null,\n            searchTerm: this.props.initialSearchTerm ?? \"\",\n            /** @type {Emoji|undefined} */\n            hoveredEmoji: undefined,\n        });\n        this.frequentEmojiService = useService(\"web.frequent.emoji\");\n        useAutofocus();\n        onWillStart(async () => {\n            const { categories, emojis } = await loadEmoji();\n            this.categories = categories;\n            this.emojis = emojis;\n            this.emojiByCodepoints = Object.fromEntries(\n                this.emojis.map((emoji) => [emoji.codepoints, emoji])\n            );\n            this.recentCategory = {\n                name: \"Frequently used\",\n                displayName: _t(\"Frequently used\"),\n                title: \"\ud83d\udd53\",\n                sortId: 0,\n            };\n            this.state.categoryId = this.recentEmojis.length\n                ? this.recentCategory.sortId\n                : this.categories[0].sortId;\n        });\n        onMounted(() => {\n            if (this.emojis.length === 0) {\n                return;\n            }\n            this.navbarResizeObserver = new ResizeObserver(() => this.adaptNavbar());\n            this.navbarResizeObserver.observe(this.navbarRef.el);\n            this.adaptNavbar();\n            this.highlightActiveCategory();\n            if (this.props.storeScroll) {\n                this.gridRef.el.scrollTop = this.props.storeScroll.get();\n            }\n            this.state.hoveredEmoji = this.activeEmoji;\n        });\n        onPatched(() => {\n            if (this.emojis.length === 0) {\n                return;\n            }\n            if (this.shouldScrollElem) {\n                this.shouldScrollElem = false;\n                const getElement = () =>\n                    this.gridRef.el.querySelector(\n                        `.o-EmojiPicker-category[data-category=\"${this.state.categoryId}\"`\n                    );\n                const elem = getElement();\n                if (elem) {\n                    elem.scrollIntoView();\n                } else {\n                    this.shouldScrollElem = getElement;\n                }\n            }\n        });\n        useEffect(\n            () => this.updateEmojiPickerRepr(),\n            () => [this.state.categoryId, this.state.searchTerm]\n        );\n        useEffect(\n            (el) => {\n                const gridEl = this.gridRef.el;\n                const activeEl = gridEl?.querySelector(\".o-Emoji.o-active\");\n                if (!gridEl) {\n                    return;\n                }\n                if (activeEl && this.keyboardNavigated && !isElementVisible(activeEl, gridEl)) {\n                    activeEl.scrollIntoView({ block: \"center\", behavior: \"instant\" });\n                    this.keyboardNavigated = false;\n                }\n                this.state.hoveredEmoji = this.activeEmoji;\n            },\n            () => [this.state.activeEmojiIndex, this.gridRef.el]\n        );\n        useEffect(\n            () => {\n                if (!this.gridRef.el) {\n                    return;\n                }\n                if (this.searchTerm) {\n                    this.gridRef.el.scrollTop = 0;\n                    this.state.categoryId = null;\n                } else {\n                    if (this.lastSearchTerm) {\n                        this.gridRef.el.scrollTop = 0;\n                    }\n                    this.highlightActiveCategory();\n                }\n                this.lastSearchTerm = this.searchTerm;\n            },\n            () => [this.searchTerm]\n        );\n        onWillUnmount(() => {\n            this.navbarResizeObserver?.disconnect();\n            if (!this.gridRef.el) {\n                return;\n            }\n            if (this.props.storeScroll) {\n                this.props.storeScroll.set(this.gridRef.el.scrollTop);\n            }\n        });\n    }\n\n    adaptNavbar() {\n        if (!this.navbarRef.el) {\n            return;\n        }\n        const computedStyle = getComputedStyle(this.navbarRef.el);\n        const availableWidth =\n            this.navbarRef.el.getBoundingClientRect().width -\n            parseInt(computedStyle.paddingLeft) -\n            parseInt(computedStyle.marginLeft) -\n            parseInt(computedStyle.paddingLeft) -\n            parseInt(computedStyle.marginLeft);\n        const itemWidth = this.navbarRef.el.querySelector(\".o-Emoji\").getBoundingClientRect().width;\n        const gapWidth = parseInt(computedStyle.gap);\n        const maxAvailableNavbarItemAmountAtOnce = Math.floor(\n            availableWidth / (itemWidth + gapWidth)\n        );\n        const repr = [];\n        let panel = [];\n        const allCategories = this.getAllCategories();\n        for (const category of allCategories) {\n            if (\n                panel.length === maxAvailableNavbarItemAmountAtOnce - 1 &&\n                category !== allCategories.at(-1)\n            ) {\n                panel.push(\"next\");\n                repr.push(panel);\n                panel = [];\n                panel.push(\"previous\");\n            }\n            panel.push(category.sortId);\n        }\n        if (panel.length > 0) {\n            if (repr.length > 0) {\n                panel.push(\n                    ...[...Array(maxAvailableNavbarItemAmountAtOnce - panel.length)].map(\n                        (_, idx) => \"empty_\" + idx\n                    )\n                );\n            }\n            repr.push(panel);\n        }\n        this.state.emojiNavbarRepr = repr;\n    }\n\n    get currentNavbarPanel() {\n        if (!this.state.emojiNavbarRepr) {\n            return this.getAllCategories().map((c) => c.sortId);\n        }\n        if (this.state.categoryId === null || Number.isNaN(this.state.categoryId)) {\n            return this.state.emojiNavbarRepr[0];\n        }\n        return this.state.emojiNavbarRepr.find((panel) => panel.includes(this.state.categoryId));\n    }\n\n    get searchTerm() {\n        return this.props.state ? this.props.state.searchTerm : this.state.searchTerm;\n    }\n\n    set searchTerm(value) {\n        if (this.props.state) {\n            this.props.state.searchTerm = value;\n        } else {\n            this.state.searchTerm = value;\n        }\n    }\n\n    get itemsNumber() {\n        return this.recentEmojis.length + this.getEmojis().length;\n    }\n\n    get recentEmojis() {\n        const recent = Object.entries(this.frequentEmojiService.all)\n            .sort(([, usage_1], [, usage_2]) => usage_2 - usage_1)\n            .map(([codepoints]) => this.emojiByCodepoints[codepoints]);\n        if (this.searchTerm && recent.length > 0) {\n            return fuzzyLookup(this.searchTerm, recent, (emoji) => [\n                emoji.name,\n                ...emoji.keywords,\n                ...emoji.emoticons,\n                ...emoji.shortcodes,\n            ]);\n        }\n        return recent.slice(0, 42);\n    }\n\n    get placeholder() {\n        return this.state.hoveredEmoji?.shortcodes.join(\" \") ?? _t(\"Search emoji\");\n    }\n\n    onMouseenterEmoji(ev, emoji) {\n        this.state.hoveredEmoji = emoji;\n    }\n\n    onMouseleaveEmoji(ev, emoji) {\n        this.state.hoveredEmoji = this.activeEmoji;\n    }\n\n    onClick(ev) {\n        markEventHandled(ev, \"emoji.selectEmoji\");\n    }\n\n    onClickToNextCategories() {\n        const panelIndex = this.state.emojiNavbarRepr.findIndex((p) =>\n            p.includes(this.state.categoryId)\n        );\n        this.selectCategory(this.state.emojiNavbarRepr[panelIndex + 1][1]);\n    }\n\n    onClickToPreviousCategories() {\n        const panelIndex = this.state.emojiNavbarRepr.findIndex((p) =>\n            p.includes(this.state.categoryId)\n        );\n        this.selectCategory(this.state.emojiNavbarRepr[panelIndex - 1].at(-2));\n    }\n\n    /**\n     * Builds the representation of the emoji picker (a 2D matrix of emojis)\n     * from the current DOM state. This is necessary to handle keyboard\n     * navigation of the emoji picker.\n     */\n    updateEmojiPickerRepr() {\n        if (this.emojis.length === 0) {\n            return;\n        }\n        const emojiEls = Array.from(this.gridRef.el.querySelectorAll(\".o-Emoji\"));\n        const emojiRects = emojiEls.map((el) => el.getBoundingClientRect());\n        this.emojiMatrix = [];\n        for (const [index, pos] of emojiRects.entries()) {\n            const emojiIndex = emojiEls[index].dataset.index;\n            if (this.emojiMatrix.length === 0 || pos.top > emojiRects[index - 1].top) {\n                this.emojiMatrix.push([]);\n            }\n            this.emojiMatrix.at(-1).push(parseInt(emojiIndex));\n        }\n    }\n\n    handleNavigation(key) {\n        const currentIdx = this.state.activeEmojiIndex;\n        let currentRow = -1;\n        let currentCol = -1;\n        const rowIdx = this.emojiMatrix.findIndex((row) => row.includes(currentIdx));\n        if (rowIdx !== -1) {\n            currentRow = rowIdx;\n            currentCol = this.emojiMatrix[currentRow].indexOf(currentIdx);\n        }\n        let newIdx;\n        switch (key) {\n            case \"ArrowDown\": {\n                const rowBelow = this.emojiMatrix[currentRow + 1];\n                const rowBelowBelow = this.emojiMatrix[currentRow + 2];\n                if (rowBelow?.length <= currentCol && rowBelowBelow?.length >= currentCol) {\n                    newIdx = rowBelowBelow?.[currentCol];\n                } else {\n                    newIdx = rowBelow?.[Math.min(currentCol, rowBelow.length - 1)];\n                }\n                break;\n            }\n            case \"ArrowUp\": {\n                const rowAbove = this.emojiMatrix[currentRow - 1];\n                const rowAboveAbove = this.emojiMatrix[currentRow - 2];\n                if (rowAbove?.length <= currentCol && rowAboveAbove?.length >= currentCol) {\n                    newIdx = rowAboveAbove?.[currentCol];\n                } else {\n                    newIdx = rowAbove?.[Math.min(currentCol, rowAbove.length - 1)];\n                }\n                break;\n            }\n            case \"ArrowRight\": {\n                const colRight = currentCol + 1;\n                if (colRight === this.emojiMatrix[currentRow]?.length) {\n                    const rowBelowRight = this.emojiMatrix[currentRow + 1];\n                    newIdx = rowBelowRight?.[0];\n                } else {\n                    newIdx = this.emojiMatrix[currentRow]?.[colRight];\n                }\n                break;\n            }\n            case \"ArrowLeft\": {\n                const colLeft = currentCol - 1;\n                if (colLeft < 0) {\n                    const rowAboveLeft = this.emojiMatrix[currentRow - 1];\n                    newIdx = rowAboveLeft?.[rowAboveLeft.length - 1] ?? this.state.activeEmojiIndex;\n                } else {\n                    newIdx = this.emojiMatrix[currentRow][colLeft];\n                }\n                break;\n            }\n        }\n        this.state.activeEmojiIndex = newIdx ?? this.state.activeEmojiIndex;\n    }\n\n    get activeEmoji() {\n        const activeCodepoints = this.gridRef.el.querySelector(\n            `.o-EmojiPicker-content .o-Emoji[data-index=\"${this.state.activeEmojiIndex}\"]`\n        )?.dataset.codepoints;\n        return activeCodepoints ? this.emojiByCodepoints[activeCodepoints] : undefined;\n    }\n\n    onKeydown(ev) {\n        switch (ev.key) {\n            case \"ArrowDown\":\n            case \"ArrowUp\":\n            case \"ArrowRight\":\n            case \"ArrowLeft\":\n                this.handleNavigation(ev.key);\n                this.keyboardNavigated = true;\n                break;\n            case \"Enter\":\n                ev.preventDefault();\n                this.gridRef.el\n                    ?.querySelector(\n                        `.o-EmojiPicker-content .o-Emoji[data-index=\"${this.state.activeEmojiIndex}\"]`\n                    )\n                    ?.click();\n                break;\n            case \"Escape\":\n                this.props.close?.();\n                this.props.onClose?.();\n                ev.stopPropagation();\n        }\n    }\n\n    getAllCategories() {\n        const res = [...this.categories];\n        if (this.recentEmojis.length > 0) {\n            res.unshift(this.recentCategory);\n        }\n        return res;\n    }\n\n    getEmojis() {\n        let emojisToDisplay = [...this.emojis];\n        const recentEmojis = this.recentEmojis;\n        if (recentEmojis.length > 0 && this.searchTerm) {\n            emojisToDisplay = emojisToDisplay.filter((emoji) => !recentEmojis.includes(emoji));\n        }\n        if (this.searchTerm.length > 0) {\n            return fuzzyLookup(this.searchTerm, emojisToDisplay, (emoji) => [\n                emoji.name,\n                ...emoji.keywords,\n                ...emoji.emoticons,\n                ...emoji.shortcodes,\n            ]);\n        }\n        return emojisToDisplay;\n    }\n\n    getEmojisFromSearch() {\n        return [...this.recentEmojis, ...this.getEmojis()];\n    }\n\n    selectCategory(categoryId) {\n        this.searchTerm = \"\";\n        this.state.categoryId = categoryId;\n        this.shouldScrollElem = true;\n    }\n\n    selectEmoji(ev) {\n        const codepoints = ev.currentTarget.dataset.codepoints;\n        let resetOnSelect = !ev.shiftKey;\n        const res = this.props.onSelect(codepoints, resetOnSelect);\n        if (res === false) {\n            resetOnSelect = false;\n        }\n        this.frequentEmojiService.incrementEmojiUsage(codepoints);\n        if (resetOnSelect) {\n            this.gridRef.el.scrollTop = 0;\n            this.props.close?.();\n            this.props.onClose?.();\n        }\n    }\n\n    highlightActiveCategory() {\n        if (!this.gridRef || !this.gridRef.el) {\n            return;\n        }\n        const coords = this.gridRef.el.getBoundingClientRect();\n        const res = document.elementFromPoint(coords.x + 10, coords.y + 10);\n        if (!res) {\n            return;\n        }\n        this.state.categoryId = parseInt(res.dataset.category);\n    }\n}\n\n/**\n * @param {() => {}} PickerComponent\n * @param {import(\"@web/core/utils/hooks\").Ref} [ref]\n * @param {Object} props\n * @param {import(\"@web/core/popover/popover_service\").PopoverServiceAddOptions} [options]\n * @param {function} [props.onSelect] function that is invoked when an item in picker has been selected.\n *   When explicit value `false` is returned, this will keep the picker open (= it won't auto-close it)\n * @param {function} [props.onClose]\n */\nexport function usePicker(PickerComponent, ref, props, options = {}) {\n    const component = useComponent();\n    const targets = [];\n    const state = useState({ isOpen: false });\n    const ui = useService(\"ui\");\n    const dialog = useService(\"dialog\");\n    let remove;\n    const newOptions = {\n        ...options,\n        onClose: () => {\n            state.isOpen = false;\n            options.onClose?.();\n        },\n    };\n    const popover = usePopover(PickerComponent, {\n        ...newOptions,\n        animation: false,\n        popoverClass: options.popoverClass ?? \"\" + \" bg-100 border border-secondary\",\n    });\n    props.storeScroll = {\n        scrollValue: 0,\n        set: (value) => {\n            props.storeScroll.scrollValue = value;\n        },\n        get: () => props.storeScroll.scrollValue,\n    };\n\n    /**\n     * @param {import(\"@web/core/utils/hooks\").Ref} ref\n     */\n    function add(ref, onSelect, { show = false } = {}) {\n        const toggler = () => toggle(isMobileOS() ? undefined : ref, onSelect);\n        targets.push([ref, toggler]);\n        if (!ref.el) {\n            return;\n        }\n        ref.el.addEventListener(\"click\", toggler);\n        ref.el.addEventListener(\"mouseenter\", loadEmoji);\n        if (show) {\n            ref.el.click();\n        }\n    }\n\n    function open(ref, openProps) {\n        state.isOpen = true;\n        if (ui.isSmall || isMobileOS()) {\n            const def = new Deferred();\n            const pickerMobileProps = {\n                PickerComponent,\n                onSelect: (...args) => {\n                    const func = openProps?.onSelect ?? props?.onSelect;\n                    const res = func?.(...args);\n                    def.resolve(true);\n                    return res;\n                },\n            };\n            if (ref?.el) {\n                pickerMobileProps.close = () => remove();\n                const app = new App(PickerMobile, {\n                    name: \"Popout\",\n                    env: component.env,\n                    props: pickerMobileProps,\n                    getTemplate,\n                    translatableAttributes: [\"data-tooltip\"],\n                    translateFn: appTranslateFn,\n                });\n                app.mount(ref.el);\n                remove = () => {\n                    state.isOpen = false;\n                    props.onClose?.();\n                    app.destroy();\n                };\n            } else {\n                remove = dialog.add(PickerMobileInDialog, pickerMobileProps, {\n                    context: component,\n                    onClose: () => {\n                        state.isOpen = false;\n                        return def.resolve(false);\n                    },\n                });\n            }\n            return def;\n        }\n        return popover.open(ref.el, { ...props, ...openProps });\n    }\n\n    function close() {\n        remove?.();\n        popover.close?.();\n    }\n\n    function toggle(ref, onSelect = props.onSelect) {\n        if (state.isOpen) {\n            close();\n        } else {\n            open(ref, { ...props, onSelect });\n        }\n    }\n\n    if (ref) {\n        add(ref);\n    }\n    onMounted(() => {\n        for (const [ref, toggle] of targets) {\n            if (!ref.el) {\n                continue;\n            }\n            ref.el.addEventListener(\"click\", toggle);\n            ref.el.addEventListener(\"mouseenter\", loadEmoji);\n        }\n    });\n    onWillPatch(() => {\n        for (const [ref, toggle] of targets) {\n            if (!ref.el) {\n                continue;\n            }\n            ref.el.removeEventListener(\"click\", toggle);\n            ref.el.removeEventListener(\"mouseenter\", loadEmoji);\n        }\n    });\n    onPatched(() => {\n        for (const [ref, toggle] of targets) {\n            if (!ref.el) {\n                continue;\n            }\n            ref.el.addEventListener(\"click\", toggle);\n            ref.el.addEventListener(\"mouseenter\", loadEmoji);\n        }\n    });\n    Object.assign(state, { open, close, toggle });\n    return state;\n}\n\nclass PickerMobile extends Component {\n    static props = [...PICKER_PROPS, \"onClose?\"];\n    static template = xml`\n        <t t-component=\"props.PickerComponent\" t-props=\"pickerProps\"/>\n    `;\n\n    get pickerProps() {\n        return {\n            ...this.props,\n            onSelect: (...args) => this.props.onSelect(...args),\n            mobile: true,\n        };\n    }\n}\n\nclass PickerMobileInDialog extends PickerMobile {\n    static components = { Dialog };\n    static props = [...PICKER_PROPS, \"onClose?\"];\n    static template = xml`\n        <Dialog size=\"'lg'\" header=\"false\" footer=\"false\" contentClass=\"'o-discuss-mobileContextMenu d-flex position-absolute bottom-0 rounded-0 h-50 bg-100'\" bodyClass=\"'p-1'\">\n            <div class=\"h-100\" t-ref=\"root\">\n                <t t-component=\"props.PickerComponent\" t-props=\"pickerProps\"/>\n            </div>\n        </Dialog>\n    `;\n\n    setup() {\n        super.setup();\n        this.root = useRef(\"root\");\n        useExternalListener(\n            window,\n            \"click\",\n            (ev) => {\n                if (ev.target !== this.root.el && !this.root.el.contains(ev.target)) {\n                    this.props.close?.();\n                }\n            },\n            { capture: true }\n        );\n    }\n}\n\nfunction isElementVisible(el, holder) {\n    const offset = 20;\n    holder = holder || document.body;\n    const { top, bottom, height } = el.getBoundingClientRect();\n    let { top: holderTop, bottom: holderBottom } = holder.getBoundingClientRect();\n    holderTop += offset * 2; // section are position sticky top so emoji can be \"visible\" under section name. Overestimate to assume invisible.\n    holderBottom -= offset;\n    return top - offset <= holderTop ? holderTop - top <= height : bottom - holderBottom <= height;\n}\n", "import { reactive } from \"@odoo/owl\";\nimport { browser } from \"@web/core/browser/browser\";\nimport { registry } from \"@web/core/registry\";\n\nexport const frequentEmojiService = {\n    start() {\n        const state = reactive({\n            all: JSON.parse(browser.localStorage.getItem(\"web.emoji.frequent\") || \"{}\"),\n            incrementEmojiUsage(codepoints) {\n                state.all[codepoints] ??= 0;\n                state.all[codepoints]++;\n                browser.localStorage.setItem(\"web.emoji.frequent\", JSON.stringify(state.all));\n            },\n            getMostFrequent(limit) {\n                return Object.entries(state.all)\n                    .sort(([, usage_1], [, usage_2]) => usage_2 - usage_1)\n                    .slice(0, limit ?? Infinity)\n                    .map(([codepoints]) => codepoints);\n            },\n        });\n        browser.addEventListener(\"storage\", (ev) => {\n            if (ev.key === \"web.emoji.frequent\") {\n                state.all = ev.newValue ? JSON.parse(ev.newValue) : {};\n            } else if (ev.key === null) {\n                state.all = {};\n            }\n        });\n        return state;\n    },\n};\n\nregistry.category(\"services\").add(\"web.frequent.emoji\", frequentEmojiService);\n", "import { loadBundle, loadJS } from \"./assets\";\n\nexport async function ensureJQuery() {\n    if (!window.jQuery) {\n        await loadBundle(\"web._assets_jquery\");\n        // allow to instantiate Bootstrap classes via jQuery: e.g. $(...).dropdown\n        const BTS_CLASSES = [\"Carousel\", \"Dropdown\", \"Modal\", \"Popover\", \"Tooltip\", \"Collapse\"];\n        const $ = window.jQuery;\n        for (const CLS of BTS_CLASSES) {\n            const plugin = window[CLS];\n            if (plugin) {\n                const name = plugin.NAME;\n                const JQUERY_NO_CONFLICT = $.fn[name];\n                $.fn[name] = plugin.jQueryInterface;\n                $.fn[name].Constructor = plugin;\n                $.fn[name].noConflict = () => {\n                    $.fn[name] = JQUERY_NO_CONFLICT;\n                    return plugin.jQueryInterface;\n                };\n            }\n        }\n    } else if (!window.jQuery.fn.getScrollingElement) {\n        await loadJS(\"/web/static/src/legacy/js/libs/jquery.js\");\n    }\n}\n", "import { browser } from \"../browser/browser\";\nimport { Dialog } from \"../dialog/dialog\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"../registry\";\nimport { Tooltip } from \"@web/core/tooltip/tooltip\";\nimport { usePopover } from \"@web/core/popover/popover_hook\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { capitalize } from \"../utils/strings\";\n\nimport { Component, useRef, useState, markup } from \"@odoo/owl\";\n\nconst { DateTime } = luxon;\n\n// This props are added by the error handler\nexport const standardErrorDialogProps = {\n    traceback: { type: [String, { value: null }], optional: true },\n    message: { type: String, optional: true },\n    name: { type: String, optional: true },\n    exceptionName: { type: [String, { value: null }], optional: true },\n    data: { type: [Object, { value: null }], optional: true },\n    subType: { type: [String, { value: null }], optional: true },\n    code: { type: [Number, String, { value: null }], optional: true },\n    type: { type: [String, { value: null }], optional: true },\n    serverHost: { type: [String, { value: null }], optional: true },\n    id: { type: [Number, { value: null }], optional: true },\n    model: { type: [String, { value: null }], optional: true },\n    close: Function, // prop added by the Dialog service\n};\n\nexport const odooExceptionTitleMap = new Map(\n    Object.entries({\n        \"odoo.addons.base.models.ir_mail_server.MailDeliveryException\": _t(\"MailDeliveryException\"),\n        \"odoo.exceptions.AccessDenied\": _t(\"Access Denied\"),\n        \"odoo.exceptions.MissingError\": _t(\"Missing Record\"),\n        \"odoo.addons.web.controllers.action.MissingActionError\": _t(\"Missing Action\"),\n        \"odoo.addons.base.models.ir_actions.ServerActionWithWarningsError\": _t(\"Invalid Operation\"),\n        \"odoo.exceptions.UserError\": _t(\"Invalid Operation\"),\n        \"odoo.exceptions.ValidationError\": _t(\"Validation Error\"),\n        \"odoo.exceptions.AccessError\": _t(\"Access Error\"),\n        \"odoo.exceptions.Warning\": _t(\"Warning\"),\n    })\n);\n\n// -----------------------------------------------------------------------------\n// Generic Error Dialog\n// -----------------------------------------------------------------------------\nexport class ErrorDialog extends Component {\n    static template = \"web.ErrorDialog\";\n    static components = { Dialog };\n    static title = _t(\"Odoo Error\");\n    static showTracebackButtonText = _t(\"See technical details\");\n    static hideTracebackButtonText = _t(\"Hide technical details\");\n    static props = { ...standardErrorDialogProps };\n\n    setup() {\n        this.state = useState({\n            showTraceback: false,\n        });\n        this.copyButtonRef = useRef(\"copyButton\");\n        this.popover = usePopover(Tooltip);\n        this.contextDetails = \"Occured \";\n        if (this.props.serverHost) {\n            this.contextDetails += `on ${this.props.serverHost} `;\n        }\n        if (this.props.model) {\n            this.contextDetails += `on model ${this.props.model} `;\n        }\n        this.contextDetails += `on ${DateTime.now()\n            .setZone(\"UTC\")\n            .toFormat(\"yyyy-MM-dd HH:mm:ss\")} GMT`;\n    }\n\n    showTooltip() {\n        this.popover.open(this.copyButtonRef.el, { tooltip: _t(\"Copied\") });\n        browser.setTimeout(this.popover.close, 800);\n    }\n\n    onClickClipboard() {\n        browser.navigator.clipboard.writeText(\n            `${this.props.name}\\n\\n${this.props.message}\\n\\n${this.contextDetails}\\n\\n${this.props.traceback}`\n        );\n        this.showTooltip();\n    }\n}\n\n// -----------------------------------------------------------------------------\n// Client Error Dialog\n// -----------------------------------------------------------------------------\nexport class ClientErrorDialog extends ErrorDialog {}\nClientErrorDialog.title = _t(\"Odoo Client Error\");\n\n// -----------------------------------------------------------------------------\n// Network Error Dialog\n// -----------------------------------------------------------------------------\nexport class NetworkErrorDialog extends ErrorDialog {}\nNetworkErrorDialog.title = _t(\"Odoo Network Error\");\n\n// -----------------------------------------------------------------------------\n// RPC Error Dialog\n// -----------------------------------------------------------------------------\nexport class RPCErrorDialog extends ErrorDialog {\n    setup() {\n        super.setup();\n        this.inferTitle();\n        this.traceback = this.props.traceback;\n        if (this.props.data && this.props.data.debug) {\n            this.traceback = `${this.props.data.debug}\\nThe above server error caused the following client error:\\n${this.traceback}`;\n        }\n    }\n    inferTitle() {\n        // If the server provides an exception name that we have in a registry.\n        if (this.props.exceptionName && odooExceptionTitleMap.has(this.props.exceptionName)) {\n            this.title = odooExceptionTitleMap.get(this.props.exceptionName).toString();\n            return;\n        }\n        // Fall back to a name based on the error type.\n        if (!this.props.type) {\n            return;\n        }\n        switch (this.props.type) {\n            case \"server\":\n                this.title = _t(\"Odoo Server Error\");\n                break;\n            case \"script\":\n                this.title = _t(\"Odoo Client Error\");\n                break;\n            case \"network\":\n                this.title = _t(\"Odoo Network Error\");\n                break;\n        }\n    }\n\n    onClickClipboard() {\n        browser.navigator.clipboard.writeText(\n            `${this.props.name}\\n\\n${this.props.message}\\n\\n${this.contextDetails}\\n\\n${this.traceback}`\n        );\n        this.showTooltip();\n    }\n}\n\n// -----------------------------------------------------------------------------\n// Warning Dialog\n// -----------------------------------------------------------------------------\nexport class WarningDialog extends Component {\n    static template = \"web.WarningDialog\";\n    static components = { Dialog };\n    static props = {\n        ...standardErrorDialogProps,\n        title: { type: String, optional: true },\n    };\n\n    setup() {\n        this.title = this.inferTitle();\n        const { data, message } = this.props;\n        if (data && data.arguments && data.arguments.length > 0) {\n            this.message = data.arguments[0];\n        } else {\n            this.message = message;\n        }\n    }\n    inferTitle() {\n        if (this.props.exceptionName && odooExceptionTitleMap.has(this.props.exceptionName)) {\n            return odooExceptionTitleMap.get(this.props.exceptionName).toString();\n        }\n        return this.props.title || _t(\"Odoo Warning\");\n    }\n}\n\n// -----------------------------------------------------------------------------\n// Redirect Warning Dialog\n// -----------------------------------------------------------------------------\nexport class RedirectWarningDialog extends Component {\n    static template = \"web.RedirectWarningDialog\";\n    static components = { Dialog };\n    static props = { ...standardErrorDialogProps };\n\n    setup() {\n        this.actionService = useService(\"action\");\n        const { data, subType } = this.props;\n        const [message, actionId, buttonText, additionalContext] = data.arguments;\n        this.title = capitalize(subType) || _t(\"Odoo Warning\");\n        this.message = message;\n        this.actionId = actionId;\n        this.buttonText = buttonText;\n        this.additionalContext = additionalContext;\n    }\n    async onClick() {\n        const options = { forceLeave: true };\n        if (this.additionalContext) {\n            options.additionalContext = this.additionalContext;\n        }\n        if (this.actionId.help) {\n            this.actionId.help = markup(this.actionId.help);\n        }\n        await this.actionService.doAction(this.actionId, options);\n        this.props.close();\n    }\n}\n\n// -----------------------------------------------------------------------------\n// Error 504 Dialog\n// -----------------------------------------------------------------------------\nexport class Error504Dialog extends Component {\n    static template = \"web.Error504Dialog\";\n    static components = { Dialog };\n    static props = { ...standardErrorDialogProps };\n}\n\n// -----------------------------------------------------------------------------\n// Expired Session Error Dialog\n// -----------------------------------------------------------------------------\nexport class SessionExpiredDialog extends Component {\n    static template = \"web.SessionExpiredDialog\";\n    static components = { Dialog };\n    static props = { ...standardErrorDialogProps };\n\n    onClick() {\n        browser.location.reload();\n    }\n}\n\nregistry\n    .category(\"error_dialogs\")\n    .add(\"odoo.exceptions.AccessDenied\", WarningDialog)\n    .add(\"odoo.exceptions.AccessError\", WarningDialog)\n    .add(\"odoo.exceptions.MissingError\", WarningDialog)\n    .add(\"odoo.addons.web.controllers.action.MissingActionError\", WarningDialog)\n    .add(\"odoo.addons.base.models.ir_actions.ServerActionWithWarningsError\", WarningDialog)\n    .add(\"odoo.exceptions.UserError\", WarningDialog)\n    .add(\"odoo.exceptions.ValidationError\", WarningDialog)\n    .add(\"odoo.exceptions.RedirectWarning\", RedirectWarningDialog)\n    .add(\"odoo.http.SessionExpiredException\", SessionExpiredDialog)\n    .add(\"werkzeug.exceptions.Forbidden\", SessionExpiredDialog)\n    .add(\"504\", Error504Dialog);\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { browser } from \"../browser/browser\";\nimport { ConnectionLostError, RPCError, rpc } from \"../network/rpc\";\nimport { registry } from \"../registry\";\nimport { session } from \"@web/session\";\nimport { user } from \"@web/core/user\";\nimport {\n    ClientErrorDialog,\n    ErrorDialog,\n    NetworkErrorDialog,\n    RPCErrorDialog,\n} from \"./error_dialogs\";\nimport { UncaughtClientError, ThirdPartyScriptError, UncaughtPromiseError } from \"./error_service\";\n\n/**\n * @typedef {import(\"../../env\").OdooEnv} OdooEnv\n * @typedef {import(\"./error_service\").UncaughtError} UncaughError\n */\n\nconst errorHandlerRegistry = registry.category(\"error_handlers\");\nconst errorDialogRegistry = registry.category(\"error_dialogs\");\nconst errorNotificationRegistry = registry.category(\"error_notifications\");\n\n// -----------------------------------------------------------------------------\n// RPC errors\n// -----------------------------------------------------------------------------\n\n/**\n * @param {OdooEnv} env\n * @param {UncaughError} error\n * @param {Error} originalError\n * @returns {boolean}\n */\nexport function rpcErrorHandler(env, error, originalError) {\n    if (!(error instanceof UncaughtPromiseError)) {\n        return false;\n    }\n    if (originalError instanceof RPCError) {\n        // When an error comes from the server, it can have an exeption name.\n        // (or any string truly). It is used as key in the error dialog from\n        // server registry to know which dialog component to use.\n        // It's how a backend dev can easily map its error to another component.\n        // Note that for a client side exception, we don't use this registry\n        // as we can directly assign a value to `component`.\n        // error is here a RPCError\n        error.unhandledRejectionEvent.preventDefault();\n        const exceptionName = originalError.exceptionName;\n        let ErrorComponent = originalError.Component;\n        if (!ErrorComponent && exceptionName) {\n            if (errorNotificationRegistry.contains(exceptionName)) {\n                const notif = errorNotificationRegistry.get(exceptionName);\n                env.services.notification.add(notif.message || originalError.data.message, notif);\n                return true;\n            }\n            if (errorDialogRegistry.contains(exceptionName)) {\n                ErrorComponent = errorDialogRegistry.get(exceptionName);\n            }\n        }\n        if (!ErrorComponent && originalError.data.context) {\n            const exceptionClass = originalError.data.context.exception_class;\n            if (errorDialogRegistry.contains(exceptionClass)) {\n                ErrorComponent = errorDialogRegistry.get(exceptionClass);\n            }\n        }\n\n        env.services.dialog.add(ErrorComponent || RPCErrorDialog, {\n            traceback: error.traceback,\n            message: originalError.message,\n            name: originalError.name,\n            exceptionName: originalError.exceptionName,\n            data: originalError.data,\n            subType: originalError.subType,\n            code: originalError.code,\n            type: originalError.type,\n            serverHost: error.event?.target?.location.host,\n            model: originalError.model,\n        });\n        return true;\n    }\n}\n\nerrorHandlerRegistry.add(\"rpcErrorHandler\", rpcErrorHandler, { sequence: 97 });\n\n// -----------------------------------------------------------------------------\n// Lost connection errors\n// -----------------------------------------------------------------------------\n\nlet connectionLostNotifRemove = null;\n/**\n * @param {OdooEnv} env\n * @param {UncaughError} error\n * @param {Error} originalError\n * @returns {boolean}\n */\nexport function lostConnectionHandler(env, error, originalError) {\n    if (!(error instanceof UncaughtPromiseError)) {\n        return false;\n    }\n    if (originalError instanceof ConnectionLostError) {\n        if (connectionLostNotifRemove) {\n            // notification already displayed (can occur if there were several\n            // concurrent rpcs when the connection was lost)\n            return true;\n        }\n        connectionLostNotifRemove = env.services.notification.add(\n            _t(\"Connection lost. Trying to reconnect...\"),\n            { sticky: true }\n        );\n        let delay = 2000;\n        browser.setTimeout(function checkConnection() {\n            rpc(\"/web/webclient/version_info\", {})\n                .then(function () {\n                    if (connectionLostNotifRemove) {\n                        connectionLostNotifRemove();\n                        connectionLostNotifRemove = null;\n                    }\n                    env.services.notification.add(_t(\"Connection restored. You are back online.\"), {\n                        type: \"info\",\n                    });\n                })\n                .catch(() => {\n                    // exponential backoff, with some jitter\n                    delay = delay * 1.5 + 500 * Math.random();\n                    browser.setTimeout(checkConnection, delay);\n                });\n        }, delay);\n        return true;\n    }\n}\nerrorHandlerRegistry.add(\"lostConnectionHandler\", lostConnectionHandler, { sequence: 98 });\n\n// -----------------------------------------------------------------------------\n// Default handler\n// -----------------------------------------------------------------------------\n\nconst defaultDialogs = new Map([\n    [UncaughtClientError, ClientErrorDialog],\n    [UncaughtPromiseError, ClientErrorDialog],\n    [ThirdPartyScriptError, NetworkErrorDialog],\n]);\n\n/**\n * Handles the errors based on the very general error categories emitted by the\n * error service. Notice how we do not look at the original error at all.\n *\n * @param {OdooEnv} env\n * @param {UncaughError} error\n * @returns {boolean}\n */\nexport function defaultHandler(env, error) {\n    const DialogComponent = defaultDialogs.get(error.constructor) || ErrorDialog;\n    env.services.dialog.add(DialogComponent, {\n        traceback: error.traceback,\n        message: error.message,\n        name: error.name,\n        serverHost: error.event?.target?.location.host,\n    });\n    return true;\n}\nerrorHandlerRegistry.add(\"defaultHandler\", defaultHandler, { sequence: 100 });\n\n// -----------------------------------------------------------------------------\n// Frontend visitors errors\n// -----------------------------------------------------------------------------\n\n/**\n * We don't want to show tracebacks to non internal users. This handler swallows\n * all errors if we're not an internal user (except in debug or test mode).\n */\nexport function swallowAllVisitorErrors(env, error, originalError) {\n    if (!user.isInternalUser && !odoo.debug && !session.test_mode) {\n        return true;\n    }\n}\n\nif (user.isInternalUser === undefined) {\n    // Only warn about this while on the \"frontend\": the session info might\n    // apparently not be present in all Odoo screens at the moment... TODO ?\n    if (session.is_frontend) {\n        console.warn(\n            \"isInternalUser information is required for this handler to work. It must be available in the page.\"\n        );\n    }\n} else {\n    registry.category(\"error_handlers\").add(\"swallowAllVisitorErrors\", swallowAllVisitorErrors, { sequence: 0 });\n}\n", "import { browser } from \"../browser/browser\";\nimport { registry } from \"../registry\";\nimport { completeUncaughtError, getErrorTechnicalName } from \"./error_utils\";\nimport { isBrowserFirefox, isBrowserChrome } from \"@web/core/browser/feature_detection\";\n\nexport class HTMLElementLoadingError extends Error {\n    static message = \"Error loading an HTML Element\";\n    constructor(message = HTMLElementLoadingError.message, event) {\n        super(message);\n        this.event = event;\n    }\n}\n\n/**\n * Uncaught Errors have 4 properties:\n * - name: technical name of the error (UncaughtError, ...)\n * - message: short user visible description of the issue (\"Uncaught Cors Error\")\n * - traceback: long description, possibly technical of the issue (such as a traceback)\n * - originalError: the error that was actually being caught. Note that it is not\n *      necessarily an error (for ex, if some code does throw \"boom\")\n */\nexport class UncaughtError extends Error {\n    constructor(message) {\n        super(message);\n        this.name = getErrorTechnicalName(this);\n        this.traceback = null;\n    }\n}\n\nexport class UncaughtClientError extends UncaughtError {\n    constructor(message = \"Uncaught Javascript Error\") {\n        super(message);\n    }\n}\n\nexport class UncaughtPromiseError extends UncaughtError {\n    constructor(message = \"Uncaught Promise\") {\n        super(message);\n        this.unhandledRejectionEvent = null;\n    }\n}\n\nexport class ThirdPartyScriptError extends UncaughtError {\n    constructor(message = \"Third-Party Script Error\") {\n        super(message);\n    }\n}\n\nexport const errorService = {\n    start(env) {\n        function handleError(uncaughtError, retry = true) {\n            function shouldLogError() {\n                // Only log errors that are relevant business-wise, following the heuristics:\n                // Error.event and Error.traceback have been assigned\n                // in one of the two error event listeners below.\n                // If preventDefault was already executed on the event, don't log it.\n                return (\n                    uncaughtError.event &&\n                    !uncaughtError.event.defaultPrevented &&\n                    uncaughtError.traceback\n                );\n            }\n            let originalError = uncaughtError;\n            while (originalError instanceof Error && \"cause\" in originalError) {\n                originalError = originalError.cause;\n            }\n            for (const [name, handler] of registry.category(\"error_handlers\").getEntries()) {\n                try {\n                    if (handler(env, uncaughtError, originalError)) {\n                        break;\n                    }\n                } catch (e) {\n                    if (shouldLogError()) {\n                        uncaughtError.event.preventDefault();\n                        console.error(\n                            `@web/core/error_service: handler \"${name}\" failed with \"${\n                                e.cause || e\n                            }\" while trying to handle:\\n` + uncaughtError.traceback\n                        );\n                    }\n                    return;\n                }\n            }\n            if (shouldLogError()) {\n                // Log the full traceback instead of letting the browser log the incomplete one\n                uncaughtError.event.preventDefault();\n                console.error(uncaughtError.traceback);\n            }\n        }\n\n        browser.addEventListener(\"error\", async (ev) => {\n            const { colno, error, filename, lineno, message } = ev;\n            // We never want to display the following ResizeObserver error to the end-user. It\n            // simply indicates that the browser delayed notifications to the next frame to prevent\n            // infinite loop, which is how he's supposed to behave. However, it would be interesting\n            // to track places from where this error could be thrown, and try to fix them.\n            // https://trackjs.com/javascript-errors/resizeobserver-loop-completed-with-undelivered-notifications/\n            const resizeObserverError =\n                \"ResizeObserver loop completed with undelivered notifications.\";\n            if (!(error instanceof Error) && message === resizeObserverError) {\n                ev.preventDefault();\n                return;\n            }\n            const isRedactedError = !filename && !lineno && !colno;\n            const isThirdPartyScriptError =\n                isRedactedError ||\n                // Firefox doesn't hide details of errors occuring in third-party scripts, check origin explicitly\n                (isBrowserFirefox() && new URL(filename).origin !== window.location.origin);\n            // Don't display error dialogs for third party script errors unless we are in debug mode\n            if (isThirdPartyScriptError && !odoo.debug) {\n                return;\n            }\n            let uncaughtError;\n            if (isRedactedError) {\n                uncaughtError = new ThirdPartyScriptError();\n                uncaughtError.traceback =\n                    `An error whose details cannot be accessed by the Odoo framework has occurred.\\n` +\n                    `The error probably originates from a JavaScript file served from a different origin.\\n` +\n                    `The full error is available in the browser console.`;\n            } else {\n                uncaughtError = new UncaughtClientError();\n                uncaughtError.event = ev;\n                if (error instanceof Error) {\n                    error.errorEvent = ev;\n                    const annotated = env.debug && env.debug.includes(\"assets\");\n                    await completeUncaughtError(uncaughtError, error, annotated);\n                }\n            }\n            uncaughtError.cause = error;\n            handleError(uncaughtError);\n        });\n\n        browser.addEventListener(\"unhandledrejection\", async (ev) => {\n            let error = ev.reason;\n\n            if (error && error.type === \"error\" && \"eventPhase\" in error) {\n                // https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/error_event\n                // See also MDN's img, script and iframe docs. The error Event *doesn't* bubble.\n                // We sometimes reject a promise with the Event dispatched by the \"error\" handler\n                // of an HTMLElement. If the code throwing that at us doesn't wrap the event in an\n                // actual Error, there is no reason to do more than the spec: we do not handle\n                // this error bubbling to us via the Promise being rejected.\n                if (!error.bubbles) {\n                    ev.preventDefault();\n                    return;\n                }\n                // If for some reason the error Event bubbles then do something\n                // a bit meaningful.\n                let message;\n                if (error.target) {\n                    message = `${HTMLElementLoadingError.message}: ${error.target.nodeName}`;\n                }\n                error = new HTMLElementLoadingError(message, error);\n            }\n\n            let traceback;\n            if (isBrowserChrome() && ev instanceof CustomEvent && error === undefined) {\n                // This fix is ad-hoc to a bug in the Honey Paypal extension\n                // They throw a CustomEvent instead of the specified PromiseRejectionEvent\n                // https://developer.mozilla.org/en-US/docs/Web/API/Window/unhandledrejection_event\n                // Moreover Chrome doesn't seem to sandbox enough the extension, as it seems irrelevant\n                // to have extension's errors in the main business page.\n                // We want to ignore those errors as they are not produced by us, and are parasiting\n                // the navigation. We do this according to the heuristic expressed in the if.\n                if (!odoo.debug) {\n                    return;\n                }\n                traceback =\n                    `Uncaught unknown Error\\n` +\n                    `An unknown error occured. This may be due to a Chrome extension meddling with Odoo.\\n` +\n                    `(Opening your browser console might give you a hint on the error.)`;\n            }\n            const uncaughtError = new UncaughtPromiseError();\n            uncaughtError.unhandledRejectionEvent = ev;\n            uncaughtError.event = ev;\n            uncaughtError.traceback = traceback;\n            if (error instanceof Error) {\n                error.errorEvent = ev;\n                const annotated = env.debug && env.debug.includes(\"assets\");\n                await completeUncaughtError(uncaughtError, error, annotated);\n            }\n            uncaughtError.cause = error;\n            handleError(uncaughtError);\n        });\n    },\n};\n\nregistry.category(\"services\").add(\"error\", errorService, { sequence: 1 });\n", "import { loadJS } from \"../assets\"; // use the real, non patched (in tests), loadJS\n\n/** @typedef {import(\"./error_service\").UncaughtError} UncaughtError */\n\n/**\n * @param {UncaughtError} uncaughtError\n * @param {Error} originalError\n * @returns {string}\n */\nfunction combineErrorNames(uncaughtError, originalError) {\n    const originalErrorName = getErrorTechnicalName(originalError);\n    const uncaughtErrorName = getErrorTechnicalName(uncaughtError);\n    if (originalErrorName === Error.name) {\n        return uncaughtErrorName;\n    } else {\n        return `${uncaughtErrorName} > ${originalErrorName}`;\n    }\n}\n\n/**\n * Returns the full traceback for an error chain based on error causes\n *\n * @param {Error} error\n * @returns {string}\n */\nexport function fullTraceback(error) {\n    let traceback = formatTraceback(error);\n    let current = error.cause;\n    while (current) {\n        traceback += `\\n\\nCaused by: ${\n            current instanceof Error ? formatTraceback(current) : current\n        }`;\n        current = current.cause;\n    }\n    return traceback;\n}\n\n/**\n * Returns the full annotated traceback for an error chain based on error causes\n *\n * @param {Error} error\n * @returns {Promise<string>}\n */\nexport async function fullAnnotatedTraceback(error) {\n    if (error.annotatedTraceback) {\n        return error.annotatedTraceback;\n    }\n    // If we don't call preventDefault  synchronously while handling the error\n    // event, the error will be logged in the console with an unannotated\n    // traceback. This is a problem because annotating a traceback cannot be\n    // done synchronously. To work around this issue, we always call\n    // preventDefault, which means it is never logged but we rethrow the error\n    // after annotating its traceback, which will cause the error to be handled\n    // again after the traceback has been annotated, and this function will be\n    // called again and return synchronously (see above)\n    if (error.errorEvent) {\n        error.errorEvent.preventDefault();\n    }\n    let traceback;\n    try {\n        traceback = await annotateTraceback(error);\n        let current = error.cause;\n        while (current) {\n            traceback += `\\n\\nCaused by: ${\n                current instanceof Error ? await annotateTraceback(current) : current\n            }`;\n            current = current.cause;\n        }\n    } catch (e) {\n        console.warn(\"Failed to annotate traceback for error:\", error, \"failure reason:\", e);\n        traceback = fullTraceback(error);\n    }\n    error.annotatedTraceback = traceback;\n    if (error.errorEvent) {\n        throw error;\n    }\n    return traceback;\n}\n\n/**\n * @param {UncaughtError} uncaughtError\n * @param {Error} originalError\n * @param {boolean} annotated\n * @returns {Promise<void>}\n */\nexport async function completeUncaughtError(uncaughtError, originalError, annotated = false) {\n    uncaughtError.name = combineErrorNames(uncaughtError, originalError);\n    if (annotated) {\n        uncaughtError.traceback = await fullAnnotatedTraceback(originalError);\n    } else {\n        uncaughtError.traceback = fullTraceback(originalError);\n    }\n    if (originalError.message) {\n        uncaughtError.message = `${uncaughtError.message} > ${originalError.message}`;\n    }\n    uncaughtError.cause = originalError;\n}\n\n/**\n * @param {Error} error\n * @returns {string}\n */\nexport function getErrorTechnicalName(error) {\n    return error.name !== Error.name ? error.name : error.constructor.name;\n}\n\n/**\n * Format the traceback of an error. Basically, we just add the error message\n * in the traceback if necessary (Chrome already does it by default, but not\n * other browser.)\n *\n * @param {Error} error\n * @returns {string}\n */\nexport function formatTraceback(error) {\n    let traceback = error.stack;\n    const errorName = getErrorTechnicalName(error);\n    // ensure the proper error name and error message are present in the traceback, no matter the error.stack brower's formatting.\n    // Stack example:\n    // Error: Mock: Can't write value\n    //     _onOpenFormView@http://localhost:8069/web/content/425-baf33f1/web.assets.js:1064:30\n    //     ...\n    const descriptionLine = `${errorName}: ${error.message}`;\n    if (error.stack.split(\"\\n\")[0].trim() !== descriptionLine) {\n        // avoid having the description line twice if already present\n        traceback = `${descriptionLine}\\n${error.stack}`.replace(/\\n/g, \"\\n    \");\n    }\n    return traceback;\n}\n\n/**\n * Returns an annotated traceback from an error. This is asynchronous because\n * it needs to fetch the sourcemaps for each script involved in the error,\n * then compute the correct file/line numbers and add the information to the\n * correct line.\n *\n * @param {Error} error\n * @returns {Promise<string>}\n */\nexport async function annotateTraceback(error) {\n    const traceback = formatTraceback(error);\n    try {\n        await loadJS(\"/web/static/lib/stacktracejs/stacktrace.js\");\n    } catch {\n        return traceback;\n    }\n    // In Firefox, the error stack generated by anonymous code (example: invalid\n    // code in a template) is not compatible with the stacktrace lib. This code\n    // corrects the stack to make it compatible with the lib stacktrace.\n    if (error.stack) {\n        const regex = / line (\\d*) > (Function):(\\d*)/gm;\n        const subst = `:$1`;\n        error.stack = error.stack.replace(regex, subst);\n    }\n    // eslint-disable-next-line no-undef\n    let frames;\n    try {\n        frames = await StackTrace.fromError(error);\n    } catch (e) {\n        // This can crash if the originalError has no stack/stacktrace property\n        console.warn(\"The following error could not be annotated:\", error, e);\n        return traceback;\n    }\n    const lines = traceback.split(\"\\n\");\n    if (lines[lines.length - 1].trim() === \"\") {\n        // firefox traceback have an empty line at the end\n        lines.splice(-1);\n    }\n\n    let lineIndex = 0;\n    let frameIndex = 0;\n    while (frameIndex < frames.length) {\n        const line = lines[lineIndex];\n        // skip lines that have no location information as they don't correspond to a frame\n        if (!line.match(/:\\d+:\\d+\\)?$/)) {\n            lineIndex++;\n            continue;\n        }\n        const frame = frames[frameIndex];\n        const info = ` (${frame.fileName}:${frame.lineNumber})`;\n        lines[lineIndex] = line + info;\n        lineIndex++;\n        frameIndex++;\n    }\n    return lines.join(\"\\n\");\n}\n", "import { browser } from \"@web/core/browser/browser\";\nimport { registry } from \"@web/core/registry\";\nimport { _t, translationIsReady } from \"@web/core/l10n/translation\";\nimport { getOrigin } from \"@web/core/utils/urls\";\n\nconst scssErrorNotificationService = {\n    dependencies: [\"notification\"],\n    start(env, { notification }) {\n        const origin = getOrigin();\n        // Iframe with src \"about:blank\" origin isn't a valid base URL.\n        if (browser.location.origin === \"null\") {\n            return;\n        }\n        const assets = [...document.styleSheets].filter((sheet) => {\n            return (\n                sheet.href?.includes(\"/web\") &&\n                sheet.href?.includes(\"/assets/\") &&\n                // CORS security rules don't allow reading content in JS\n                new URL(sheet.href, browser.location.origin).origin === origin\n            );\n        });\n        translationIsReady.then(() => {\n            for (const asset of assets) {\n                let cssRules;\n                try {\n                    // The filter above isn't enough to protect against CORS errors when reading\n                    // the cssRules property. Indeed, it seems that if the protocol is http, reading\n                    // that property can also trigger a CORS error, even if the origin is the same.\n                    // Anyway, we never want this line to crash, so we protect it.\n                    // See opw 3746910.\n                    cssRules = asset.cssRules;\n                } catch {\n                    continue;\n                }\n                const lastRule = cssRules?.[cssRules?.length - 1];\n                if (lastRule?.selectorText === \"css_error_message\") {\n                    const message = _t(\n                        \"The style compilation failed. This is an administrator or developer error that must be fixed for the entire database before continuing working. See browser console or server logs for details.\"\n                    );\n                    notification.add(message, {\n                        title: _t(\"Style error\"),\n                        sticky: true,\n                        type: \"danger\",\n                    });\n                    console.log(\n                        lastRule.style.content\n                            .replaceAll(\"\\\\a\", \"\\n\")\n                            .replaceAll(\"\\\\*\", \"*\")\n                            .replaceAll(`\\\\\"`, `\"`)\n                    );\n                }\n            }\n        });\n    },\n};\nregistry.category(\"services\").add(\"scss_error_display\", scssErrorNotificationService);\n", "import { Component, onWillStart, onWillUpdateProps } from \"@odoo/owl\";\nimport { getExpressionDisplayedOperators } from \"@web/core/expression_editor/expression_editor_operator_editor\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { ModelFieldSelector } from \"@web/core/model_field_selector/model_field_selector\";\nimport { condition } from \"@web/core/tree_editor/condition_tree\";\nimport { expressionFromTree } from \"@web/core/tree_editor/expression_from_tree\";\nimport { TreeEditor } from \"@web/core/tree_editor/tree_editor\";\nimport { getOperatorEditorInfo } from \"@web/core/tree_editor/tree_editor_operator_editor\";\nimport { getDefaultValue } from \"@web/core/tree_editor/tree_editor_value_editors\";\nimport { treeFromExpression } from \"@web/core/tree_editor/tree_from_expression\";\nimport { getDefaultPath } from \"@web/core/tree_editor/utils\";\n\nexport class ExpressionEditor extends Component {\n    static template = \"web.ExpressionEditor\";\n    static components = { TreeEditor };\n    static props = {\n        resModel: String,\n        fields: Object,\n        expression: String,\n        update: Function,\n    };\n\n    setup() {\n        onWillStart(() => this.onPropsUpdated(this.props));\n        onWillUpdateProps((nextProps) => this.onPropsUpdated(nextProps));\n    }\n\n    async onPropsUpdated(props) {\n        this.filteredFields = Object.fromEntries(\n            Object.entries(props.fields).filter(([_, fieldDef]) => fieldDef.type !== \"properties\")\n        );\n        try {\n            this.tree = treeFromExpression(props.expression, {\n                getFieldDef: (name) => this.getFieldDef(name, props),\n                distributeNot: !this.isDebugMode,\n                generateSmartDates: false,\n            });\n        } catch {\n            this.tree = null;\n        }\n    }\n\n    getFieldDef(name, props = this.props) {\n        if (typeof name === \"string\") {\n            return props.fields[name] || null;\n        }\n        return null;\n    }\n\n    getDefaultCondition() {\n        const defaultPath = getDefaultPath(this.filteredFields);\n        const fieldDef = this.filteredFields[defaultPath];\n        const operator = getExpressionDisplayedOperators(fieldDef)[0];\n        const value = getDefaultValue(fieldDef, operator);\n        return condition(fieldDef.name, operator, value);\n    }\n\n    getDefaultOperator(fieldDef) {\n        return getExpressionDisplayedOperators(fieldDef)[0];\n    }\n\n    getOperatorEditorInfo(fieldDef) {\n        const operators = getExpressionDisplayedOperators(fieldDef);\n        return getOperatorEditorInfo(operators, fieldDef);\n    }\n\n    getPathEditorInfo(resModel, defaultCondition) {\n        if (resModel !== this.props.resModel) {\n            throw new Error(\n                `Expression editor doesn't support tree as value so resModel has to be props.resModel`\n            );\n        }\n        return {\n            component: ModelFieldSelector,\n            extractProps: ({ value, update }) => ({\n                path: value,\n                update,\n                resModel: this.props.resModel,\n                readonly: false,\n                filter: (fieldDef) => fieldDef.name in this.filteredFields,\n                showDebugInput: false,\n                followRelations: false,\n                isDebugMode: this.isDebugMode,\n            }),\n            isSupported: (value) => [0, 1].includes(value) || value in this.filteredFields,\n            // by construction, all values received by the path editor are O/1 or a field (name) in this.props.fields.\n            // (see _leafFromAST in condition_tree.js)\n            stringify: (value) => this.props.fields[value].string,\n            defaultValue: () => defaultCondition.path,\n            message: _t(\"Field properties not supported\"),\n        };\n    }\n\n    get isDebugMode() {\n        return !!this.env.debug;\n    }\n\n    onExpressionChange(expression) {\n        this.props.update(expression);\n    }\n\n    resetExpression() {\n        this.props.update(\"True\");\n    }\n\n    update(tree) {\n        const expression = expressionFromTree(tree, {\n            getFieldDef: (name) => this.getFieldDef(name),\n            generateSmartDates: false,\n        });\n        this.props.update(expression);\n    }\n}\n", "import { getDomainDisplayedOperators } from \"@web/core/domain_selector/domain_selector_operator_editor\";\n\nconst EXPRESSION_VALID_OPERATORS = [\n    \"<\",\n    \"<=\",\n    \">\",\n    \">=\",\n    \"between\",\n    \"in range\",\n    \"in\",\n    \"not in\",\n    \"=\",\n    \"!=\",\n    \"set\",\n    \"not set\",\n];\n\nexport function getExpressionDisplayedOperators(fieldDef) {\n    const operators = getDomainDisplayedOperators(fieldDef);\n    return operators.filter((operator) => EXPRESSION_VALID_OPERATORS.includes(operator));\n}\n", "import { Component, useRef, useState } from \"@odoo/owl\";\nimport { Dialog } from \"@web/core/dialog/dialog\";\nimport { ExpressionEditor } from \"@web/core/expression_editor/expression_editor\";\nimport { evaluateExpr } from \"@web/core/py_js/py\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { user } from \"@web/core/user\";\n\nexport class ExpressionEditorDialog extends Component {\n    static components = { Dialog, ExpressionEditor };\n    static template = \"web.ExpressionEditorDialog\";\n    static props = {\n        close: Function,\n        resModel: String,\n        fields: Object,\n        expression: String,\n        onConfirm: Function,\n    };\n\n    setup() {\n        this.notification = useService(\"notification\");\n        this.state = useState({\n            expression: this.props.expression,\n        });\n        this.confirmButtonRef = useRef(\"confirm\");\n    }\n\n    get expressionEditorProps() {\n        return {\n            resModel: this.props.resModel,\n            fields: this.props.fields,\n            expression: this.state.expression,\n            update: (expression) => {\n                this.state.expression = expression;\n            },\n        };\n    }\n\n    makeDefaultRecord() {\n        const record = {};\n        for (const [name, { type }] of Object.entries(this.props.fields)) {\n            switch (type) {\n                case \"integer\":\n                case \"float\":\n                case \"monetary\":\n                    record[name] = name === \"id\" ? false : 0;\n                    break;\n                case \"one2many\":\n                case \"many2many\":\n                    record[name] = [];\n                    break;\n                default:\n                    record[name] = false;\n            }\n        }\n        return record;\n    }\n\n    async onConfirm() {\n        this.confirmButtonRef.el.disabled = true;\n        const record = this.makeDefaultRecord();\n        const evalContext = { ...user.context, ...record };\n        try {\n            evaluateExpr(this.state.expression, evalContext);\n        } catch {\n            if (this.confirmButtonRef.el) {\n                this.confirmButtonRef.el.disabled = false;\n            }\n            this.notification.add(_t(\"Expression is invalid. Please correct it\"), {\n                type: \"danger\",\n            });\n            return;\n        }\n        this.props.onConfirm(this.state.expression);\n        this.props.close();\n    }\n\n    onDiscard() {\n        this.props.close();\n    }\n}\n", "import { Domain } from \"@web/core/domain\";\nimport { registry } from \"@web/core/registry\";\n\n/**\n * @typedef {Object} LoadFieldsOptions\n * @property {string[]|false} [fieldNames]\n * @property {string[]} [attributes]\n */\n\nfunction getRelation(fieldDef, followRelationalProperties = false) {\n    if (fieldDef.relation) {\n        return fieldDef.relation;\n    }\n    if (fieldDef.comodel && followRelationalProperties) {\n        return fieldDef.comodel;\n    }\n    return null;\n}\n\nexport const fieldService = {\n    dependencies: [\"orm\"],\n    async: [\n        \"loadFieldInfo\",\n        \"loadFields\",\n        \"loadPath\",\n        \"loadPropertyDefinitions\",\n        \"loadPathDescription\",\n    ],\n    start(env, { orm }) {\n        /**\n         * @param {string} resModel\n         * @param {LoadFieldsOptions} [options]\n         * @returns {Promise<object>}\n         */\n        async function loadFields(resModel, options = {}) {\n            if (typeof resModel !== \"string\" || !resModel) {\n                throw new Error(`Invalid model name: ${resModel}`);\n            }\n            return orm\n                .cache({ type: \"disk\" })\n                .call(resModel, \"fields_get\", [options.fieldNames, options.attributes]);\n        }\n\n        /**\n         * @param {Object} fieldDefs\n         * @param {string} name\n         * @param {import(\"@web/core/domain\").DomainListRepr} [domain=[]]\n         * @returns {Promise<Object>}\n         */\n        async function _loadPropertyDefinitions(resModel, fieldDefs, name, domain = []) {\n            const {\n                definition_record: definitionRecord,\n                definition_record_field: definitionRecordField,\n            } = fieldDefs[name];\n            const definitionRecordModel = fieldDefs[definitionRecord].relation;\n\n            let result;\n            if (definitionRecordModel === \"properties.base.definition\") {\n                // Record without parent (eg `res.partner`)\n                result = await orm.call(\n                    \"properties.base.definition\",\n                    \"get_properties_base_definition\",\n                    [resModel, name]\n                );\n            } else {\n                // @ts-ignore\n                domain = Domain.and([[[definitionRecordField, \"!=\", false]], domain]).toList();\n                result = await orm.webSearchRead(definitionRecordModel, domain, {\n                    specification: {\n                        display_name: {},\n                        [definitionRecordField]: {},\n                    },\n                });\n            }\n\n            const definitions = {};\n            for (const record of result.records) {\n                for (const definition of record[definitionRecordField]) {\n                    definitions[definition.name] = {\n                        is_property: true,\n                        // for now, all properties are searchable but their definitions don't contain that info\n                        searchable: true,\n                        // differentiate definitions with same name but on different parent\n                        record_id: record.id,\n                        record_name: record.display_name,\n                        ...(definition.comodel ? { relation: definition.comodel } : {}),\n                        ...definition,\n                    };\n                }\n            }\n            return definitions;\n        }\n\n        /**\n         * @param {string} resModel\n         * @param {string} fieldName\n         * @param {import(\"@web/core/domain\").DomainListRepr} [domain]\n         * @returns {Promise<object[]>}\n         */\n        async function loadPropertyDefinitions(resModel, fieldName, domain) {\n            const fieldDefs = await loadFields(resModel);\n            return _loadPropertyDefinitions(resModel, fieldDefs, fieldName, domain);\n        }\n\n        /**\n         * @param {string|null} resModel valid model name or null (case virtual)\n         * @param {Object|null} fieldDefs\n         * @param {string[]} names\n         * @param {boolean} [followRelationalProperties=false]\n         */\n        async function _loadPath(resModel, fieldDefs, names, followRelationalProperties = false) {\n            if (!fieldDefs) {\n                return { isInvalid: \"path\", names, modelsInfo: [] };\n            }\n\n            const [name, ...remainingNames] = names;\n            const modelsInfo = [{ resModel, fieldDefs }];\n            if (resModel === \"*\" && remainingNames.length) {\n                return { isInvalid: \"path\", names, modelsInfo };\n            }\n\n            const fieldDef = fieldDefs[name];\n            if ((name !== \"*\" && !fieldDef) || (name === \"*\" && remainingNames.length)) {\n                return { isInvalid: \"path\", names, modelsInfo };\n            }\n\n            if (!remainingNames.length) {\n                return { names, modelsInfo };\n            }\n\n            let subResult;\n            const relation = getRelation(fieldDef, followRelationalProperties);\n            if (relation) {\n                subResult = await _loadPath(relation, await loadFields(relation), remainingNames);\n            } else if (fieldDef.type === \"properties\") {\n                subResult = await _loadPath(\n                    followRelationalProperties ? resModel : \"*\",\n                    await _loadPropertyDefinitions(resModel, fieldDefs, name),\n                    remainingNames\n                );\n            }\n\n            if (subResult) {\n                const result = {\n                    names,\n                    modelsInfo: [...modelsInfo, ...subResult.modelsInfo],\n                };\n                if (subResult.isInvalid) {\n                    result.isInvalid = \"path\";\n                }\n                return result;\n            }\n\n            return { isInvalid: \"path\", names, modelsInfo };\n        }\n\n        /**\n         * Note: the symbol * can be used at the end of path (e.g path=\"*\" or path=\"user_id.*\").\n         * It says to load the fields of the appropriate model.\n         * @param {string} resModel\n         * @param {string} path\n         * @returns {Promise<Object>}\n         */\n        async function loadPath(resModel, path = \"*\", followRelationalProperties = false) {\n            const fieldDefs = await loadFields(resModel);\n            if (typeof path !== \"string\" || !path) {\n                throw new Error(`Invalid path: ${path}`);\n            }\n            return _loadPath(resModel, fieldDefs, path.split(\".\"), followRelationalProperties);\n        }\n\n        /**\n         * @param {string} resModel\n         * @param {string} path\n         * @returns {Promise<Object>}\n         */\n        async function loadFieldInfo(resModel, path) {\n            if (typeof path !== \"string\" || !path || path === \"*\") {\n                return { resModel, fieldDef: null };\n            }\n            const { isInvalid, names, modelsInfo } = await loadPath(resModel, path);\n            if (isInvalid) {\n                return { resModel, fieldDef: null };\n            }\n            const name = names.at(-1);\n            const modelInfo = modelsInfo.at(-1);\n            return { resModel: modelInfo.resModel, fieldDef: modelInfo.fieldDefs[name] };\n        }\n\n        function makeString(value) {\n            return String(value ?? \"-\");\n        }\n\n        async function loadPathDescription(resModel, path, allowEmpty) {\n            if ([0, 1].includes(path)) {\n                return { isInvalid: false, displayNames: [makeString(path)] };\n            }\n            if (allowEmpty && !path) {\n                return { isInvalid: false, displayNames: [] };\n            }\n            if (typeof path !== \"string\" || !path || path === \"*\") {\n                return { isInvalid: true, displayNames: [makeString()] };\n            }\n            const { isInvalid, modelsInfo, names } = await loadPath(resModel, path);\n            const result = { isInvalid: !!isInvalid, displayNames: [] };\n            if (!isInvalid) {\n                const lastName = names.at(-1);\n                const lastFieldDef = modelsInfo.at(-1).fieldDefs[lastName];\n                if ([\"properties\", \"properties_definition\"].includes(lastFieldDef.type)) {\n                    // there is no known case where we want to select a 'properties' field directly\n                    result.isInvalid = true;\n                }\n            }\n            for (let index = 0; index < names.length; index++) {\n                const name = names[index];\n                const fieldDef = modelsInfo[index]?.fieldDefs[name];\n                result.displayNames.push(fieldDef?.string || makeString(name));\n            }\n            return result;\n        }\n\n        return {\n            loadFieldInfo,\n            loadFields,\n            loadPath,\n            loadPathDescription,\n            loadPropertyDefinitions,\n        };\n    },\n};\n\nregistry.category(\"services\").add(\"field\", fieldService);\n", "import { Component, onMounted, useRef, useState } from \"@odoo/owl\";\nimport { useFileUploader } from \"@web/core/utils/files\";\n\n/**\n * Custom file input\n *\n * Component representing a customized input of type file. It takes a sub-template\n * in its default t-slot and uses it as the trigger to open the file upload\n * prompt.\n * @extends Component\n *\n * Props:\n * @param {string} [props.acceptedFileExtensions='*'] Comma-separated\n *      list of authorized file extensions (default to all).\n * @param {string} [props.route='/web/binary/upload'] Route called when\n *      a file is uploaded in the input.\n * @param {string} [props.resId]\n * @param {string} [props.resModel]\n * @param {string} [props.multiUpload=false] Whether the input should allow\n *      to upload multiple files at once.\n */\nexport class FileInput extends Component {\n    static template = \"web.FileInput\";\n    static defaultProps = {\n        acceptedFileExtensions: \"*\",\n        hidden: false,\n        multiUpload: false,\n        onUpload: () => {},\n        route: \"/web/binary/upload_attachment\",\n        beforeOpen: async () => true,\n    };\n    static props = {\n        acceptedFileExtensions: { type: String, optional: true },\n        autoOpen: { type: Boolean, optional: true },\n        hidden: { type: Boolean, optional: true },\n        multiUpload: { type: Boolean, optional: true },\n        onUpload: { type: Function, optional: true },\n        beforeOpen: { type: Function, optional: true },\n        resId: { type: Number, optional: true },\n        resModel: { type: String, optional: true },\n        route: { type: String, optional: true },\n        \"*\": true,\n    };\n\n    setup() {\n        this.uploadFiles = useFileUploader();\n        this.fileInputRef = useRef(\"file-input\");\n        this.state = useState({\n            // Disables upload button if currently uploading.\n            isDisable: false,\n        });\n\n        onMounted(() => {\n            if (this.props.autoOpen) {\n                this.onTriggerClicked();\n            }\n        });\n    }\n\n    get httpParams() {\n        const { resId, resModel } = this.props;\n        const params = {\n            csrf_token: odoo.csrf_token,\n            ufile: [...this.fileInputRef.el.files],\n        };\n        if (resModel) {\n            params.model = resModel;\n        }\n        if (resId !== undefined) {\n            params.id = resId;\n        }\n        return params;\n    }\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n     * Upload an attachment to the given route with the given parameters:\n     * - ufile: list of files contained in the file input\n     * - csrf_token: CSRF token provided by the odoo global object\n     * - resModel: a specific model which will be given when creating the attachment\n     * - resId: the id of the resModel target instance\n     */\n    async onFileInputChange() {\n        this.state.isDisable = true;\n        const httpParams = this.httpParams;\n        if (this.props.onWillUploadFiles) {\n            try {\n                const files = await this.props.onWillUploadFiles(httpParams.ufile);\n                httpParams.ufile = files;\n            } catch (e) {\n                this.state.isDisable = false;\n                throw e;\n            }\n        }\n        const parsedFileData = await this.uploadFiles(this.props.route, httpParams);\n        if (parsedFileData) {\n            // When calling onUpload, also pass the files to allow to get data like their names\n            this.props.onUpload(\n                parsedFileData,\n                this.fileInputRef.el ? this.fileInputRef.el.files : []\n            );\n            // Because the input would not trigger this method if the same file name is uploaded,\n            // we must clear the value after handling the upload\n            this.fileInputRef.el.value = null;\n        }\n        this.state.isDisable = false;\n    }\n\n    /**\n     * Redirect clicks from the trigger element to the input.\n     */\n    async onTriggerClicked() {\n        if (await this.props.beforeOpen()) {\n            this.fileInputRef.el.click();\n        }\n    }\n}\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { useService } from \"../utils/hooks\";\nimport { ConfirmationDialog } from \"../confirmation_dialog/confirmation_dialog\";\n\nimport { Component } from \"@odoo/owl\";\n\nexport class FileUploadProgressBar extends Component {\n    static template = \"web.FileUploadProgressBar\";\n    static props = {\n        fileUpload: { type: Object },\n    };\n\n    setup() {\n        this.dialogService = useService(\"dialog\");\n    }\n\n    onCancel() {\n        if (!this.props.fileUpload.xhr) {\n            return;\n        }\n        this.dialogService.add(ConfirmationDialog, {\n            body: _t(\"Do you really want to cancel the upload of %s?\", this.props.fileUpload.title),\n            confirm: () => {\n                this.props.fileUpload.xhr.abort();\n            },\n        });\n    }\n}\n", "import { Component } from \"@odoo/owl\";\n\nexport class FileUploadProgressContainer extends Component {\n    static template = \"web.FileUploadProgressContainer\";\n    static props = {\n        Component: { optional: false },\n        shouldDisplay: { type: Function, optional: true },\n        fileUploads: { type: Object },\n    };\n}\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { FileUploadProgressBar } from \"./file_upload_progress_bar\";\n\nimport { Component } from \"@odoo/owl\";\n\nexport class FileUploadProgressRecord extends Component {\n    static template = \"\";\n    static components = {\n        FileUploadProgressBar,\n    };\n    static props = {\n        fileUpload: Object,\n        selector: { type: String, optional: true },\n    };\n    getProgressTexts() {\n        const fileUpload = this.props.fileUpload;\n        const percent = Math.round(fileUpload.progress * 100);\n        if (percent === 100) {\n            return {\n                left: _t(\"Processing...\"),\n                right: \"\",\n            };\n        } else {\n            const mbLoaded = Math.round(fileUpload.loaded / 1000000);\n            const mbTotal = Math.round(fileUpload.total / 1000000);\n            return {\n                left: _t(\"Uploading... (%s%)\", percent),\n                right: _t(\"(%(mbLoaded)s/%(mbTotal)sMB)\", { mbLoaded, mbTotal }),\n            };\n        }\n    }\n}\n\nexport class FileUploadProgressKanbanRecord extends FileUploadProgressRecord {\n    static template = \"web.FileUploadProgressKanbanRecord\";\n}\n\nexport class FileUploadProgressDataRow extends FileUploadProgressRecord {\n    static template = \"web.FileUploadProgressDataRow\";\n}\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"../registry\";\n\nimport { EventBus, reactive } from \"@odoo/owl\";\n\nexport const fileUploadService = {\n    dependencies: [\"notification\"],\n    /**\n     * Overridden during tests to return a mocked XHR.\n     *\n     * @private\n     * @returns {XMLHttpRequest}\n     */\n    createXhr() {\n        return new window.XMLHttpRequest();\n    },\n\n    start(env, { notificationService }) {\n        const uploads = reactive({});\n        let nextId = 1;\n        const bus = new EventBus();\n\n        /**\n         * @param {string}                          route\n         * @param {FileList|Array<File>}            files\n         * @param {Object}                          [params]\n         * @param {function(formData): void}        [params.buildFormData]\n         * @param {Boolean}                         [params.displayErrorNotification]\n         * @returns {reactive}                      upload\n         * @returns {XMLHttpRequest}                upload.xhr\n         * @returns {FormData}                      upload.data\n         * @returns {Number}                        upload.progress\n         * @returns {Number}                        upload.loaded\n         * @returns {Number}                        upload.total\n         * @returns {String}                        upload.title\n         * @returns {String||undefined}             upload.type\n         */\n        const upload = async (route, files, params = {}) => {\n            const xhr = this.createXhr();\n            xhr.open(\"POST\", route);\n            const formData = new FormData();\n            formData.append(\"csrf_token\", odoo.csrf_token);\n            for (const file of files) {\n                formData.append(\"ufile\", file);\n            }\n            if (params.buildFormData) {\n                params.buildFormData(formData);\n            }\n            const upload = reactive({\n                id: nextId++,\n                xhr,\n                data: formData,\n                progress: 0,\n                loaded: 0,\n                total: 0,\n                state: \"pending\",\n                title: files.length === 1 ? files[0].name : _t(\"%s Files\", files.length),\n                type: files.length === 1 ? files[0].type : undefined,\n            });\n            uploads[upload.id] = upload;\n            // Progress listener\n            xhr.upload.addEventListener(\"progress\", async (ev) => {\n                upload.progress = ev.loaded / ev.total;\n                upload.loaded = ev.loaded;\n                upload.total = ev.total;\n                upload.state = \"loading\";\n            });\n            // Load listener\n            xhr.addEventListener(\"load\", () => {\n                delete uploads[upload.id];\n                upload.state = \"loaded\";\n                bus.trigger(\"FILE_UPLOAD_LOADED\", { upload });\n            });\n            // Error listener\n            xhr.addEventListener(\"error\", async () => {\n                delete uploads[upload.id];\n                upload.state = \"error\";\n                // Disable this option if you need more explicit error handling.\n                if (\n                    params.displayErrorNotification !== undefined &&\n                    params.displayErrorNotification\n                ) {\n                    notificationService.add(_t(\"An error occured while uploading.\"), {\n                        type: \"danger\",\n                        sticky: true,\n                    });\n                }\n                bus.trigger(\"FILE_UPLOAD_ERROR\", { upload });\n            });\n            // Abort listener, considered as error\n            xhr.addEventListener(\"abort\", async () => {\n                delete uploads[upload.id];\n                upload.state = \"abort\";\n                bus.trigger(\"FILE_UPLOAD_ERROR\", { upload });\n            });\n            xhr.send(formData);\n            bus.trigger(\"FILE_UPLOAD_ADDED\", { upload });\n            return upload;\n        };\n\n        return { bus, upload, uploads };\n    },\n};\n\nregistry.category(\"services\").add(\"file_upload\", fileUploadService);\n", "import { url } from \"@web/core/utils/urls\";\n\nexport const FileModelMixin = (T) =>\n    class extends T {\n        access_token;\n        checksum;\n        extension;\n        id;\n        mimetype;\n        name;\n        /** @type {string} */\n        ownership_token;\n        /** @type {string} */\n        raw_access_token;\n        /** @type {\"binary\"|\"url\"} */\n        type;\n        /** @type {string} */\n        tmpUrl;\n        /**\n         * This URL should not be used as the URL to serve the file. `urlRoute` should be used\n         * instead. The server will properly redirect to the correct URL when necessary.\n         *\n         * @type {string}\n         */\n        url;\n        /** @type {boolean} */\n        uploading;\n\n        get defaultSource() {\n            const route = url(this.urlRoute, this.urlQueryParams);\n            const encodedRoute = encodeURIComponent(route);\n            if (this.isPdf) {\n                return `/web/static/lib/pdfjs/web/viewer.html?file=${encodedRoute}#pagemode=none`;\n            }\n            if (this.isUrlYoutube) {\n                const urlArr = this.url.split(\"/\");\n                let token = urlArr[urlArr.length - 1];\n                if (token.includes(\"watch\")) {\n                    token = token.split(\"v=\")[1];\n                    const amp = token.indexOf(\"&\");\n                    if (amp !== -1) {\n                        token = token.substring(0, amp);\n                    }\n                }\n                return `https://www.youtube.com/embed/${token}`;\n            }\n            return route;\n        }\n\n        get downloadUrl() {\n            return url(this.urlRoute, { ...this.urlQueryParams, download: true });\n        }\n\n        get isImage() {\n            const imageMimetypes = [\n                \"image/bmp\",\n                \"image/gif\",\n                \"image/jpeg\",\n                \"image/png\",\n                \"image/svg+xml\",\n                \"image/tiff\",\n                \"image/x-icon\",\n                \"image/webp\",\n            ];\n            return imageMimetypes.includes(this.mimetype);\n        }\n\n        get isPdf() {\n            return this.mimetype && this.mimetype.startsWith(\"application/pdf\");\n        }\n\n        get isText() {\n            const textMimeType = [\n                \"application/javascript\",\n                \"application/json\",\n                \"text/css\",\n                \"text/html\",\n                \"text/plain\",\n            ];\n            return textMimeType.includes(this.mimetype);\n        }\n\n        get isUrl() {\n            return this.type === \"url\" && this.url;\n        }\n\n        get isUrlYoutube() {\n            return !!this.url && this.url.includes(\"youtu\");\n        }\n\n        get isVideo() {\n            const videoMimeTypes = [\"audio/mpeg\", \"video/x-matroska\", \"video/mp4\", \"video/webm\"];\n            return videoMimeTypes.includes(this.mimetype);\n        }\n\n        get isViewable() {\n            return (\n                (this.isText || this.isImage || this.isVideo || this.isPdf || this.isUrlYoutube) &&\n                !this.uploading\n            );\n        }\n\n        /**\n         * @returns {Object}\n         */\n        get urlQueryParams() {\n            if (this.uploading && this.tmpUrl) {\n                return {};\n            }\n            const params = {\n                access_token: this.raw_access_token || this.access_token,\n                filename: this.name,\n                unique: this.checksum,\n            };\n            for (const prop in params) {\n                if (!params[prop]) {\n                    delete params[prop];\n                }\n            }\n            return params;\n        }\n\n        /**\n         * @returns {string}\n         */\n        get urlRoute() {\n            if (this.uploading && this.tmpUrl) {\n                return this.tmpUrl;\n            }\n            return this.isImage ? `/web/image/${this.id}` : `/web/content/${this.id}`;\n        }\n    };\n\nexport class FileModel extends FileModelMixin(Object) {}\n", "import { Component, useEffect, useRef, useState } from \"@odoo/owl\";\nimport { useAutofocus, useService } from \"@web/core/utils/hooks\";\nimport { hidePDFJSButtons } from \"@web/core/utils/pdfjs\";\n\n/**\n * @typedef {Object} File\n * @property {string} name\n * @property {string} downloadUrl\n * @property {boolean} [isImage]\n * @property {boolean} [isPdf]\n * @property {boolean} [isVideo]\n * @property {boolean} [isText]\n * @property {string} [defaultSource]\n * @property {boolean} [isUrlYoutube]\n * @property {string} [mimetype]\n * @property {boolean} [isViewable]\n * @typedef {Object} Props\n * @property {Array<File>} files\n * @property {number} startIndex\n * @property {function} close\n * @property {boolean} [modal]\n * @extends {Component<Props, Env>}\n */\nexport class FileViewer extends Component {\n    static template = \"web.FileViewer\";\n    static components = {};\n    static props = [\"files\", \"startIndex\", \"close?\", \"modal?\"];\n    static defaultProps = {\n        modal: true,\n    };\n\n    setup() {\n        useAutofocus();\n        this.imageRef = useRef(\"image\");\n        this.zoomerRef = useRef(\"zoomer\");\n        this.iframeViewerPdfRef = useRef(\"iframeViewerPdf\");\n\n        this.isDragging = false;\n        this.dragStartX = 0;\n        this.dragStartY = 0;\n\n        this.scrollZoomStep = 0.1;\n        this.zoomStep = 0.5;\n        this.minScale = 0.5;\n        this.translate = {\n            dx: 0,\n            dy: 0,\n            x: 0,\n            y: 0,\n        };\n\n        this.state = useState({\n            index: this.props.startIndex,\n            file: this.props.files[this.props.startIndex],\n            imageLoaded: false,\n            scale: 1,\n            angle: 0,\n        });\n        this.ui = useService(\"ui\");\n        useEffect(\n            (el) => {\n                if (el) {\n                    hidePDFJSButtons(this.iframeViewerPdfRef.el, {\n                        hideDownload: true,\n                        hidePrint: true,\n                    });\n                }\n            },\n            () => [this.iframeViewerPdfRef.el]\n        );\n    }\n\n    onImageLoaded() {\n        this.state.imageLoaded = true;\n    }\n\n    close() {\n        this.props.close && this.props.close();\n    }\n\n    next() {\n        const last = this.props.files.length - 1;\n        this.activateFile(this.state.index === last ? 0 : this.state.index + 1);\n    }\n\n    previous() {\n        const last = this.props.files.length - 1;\n        this.activateFile(this.state.index === 0 ? last : this.state.index - 1);\n    }\n\n    activateFile(index) {\n        this.state.index = index;\n        this.state.file = this.props.files[index];\n    }\n\n    onKeydown(ev) {\n        switch (ev.key) {\n            case \"ArrowRight\":\n                this.next();\n                break;\n            case \"ArrowLeft\":\n                this.previous();\n                break;\n            case \"Escape\":\n                this.close();\n                break;\n            case \"q\":\n                this.close();\n                break;\n        }\n        if (this.state.file.isImage) {\n            switch (ev.key) {\n                case \"r\":\n                    this.rotate();\n                    break;\n                case \"+\":\n                    this.zoomIn();\n                    break;\n                case \"-\":\n                    this.zoomOut();\n                    break;\n                case \"0\":\n                    this.resetZoom();\n                    break;\n            }\n        }\n    }\n\n    /**\n     * @param {Event} ev\n     */\n    onWheelImage(ev) {\n        if (ev.deltaY > 0) {\n            this.zoomOut({ scroll: true });\n        } else {\n            this.zoomIn({ scroll: true });\n        }\n    }\n\n    /**\n     * @param {DragEvent} ev\n     */\n    onMousedownImage(ev) {\n        if (this.isDragging) {\n            return;\n        }\n        if (ev.button !== 0) {\n            return;\n        }\n        this.isDragging = true;\n        this.dragStartX = ev.clientX;\n        this.dragStartY = ev.clientY;\n    }\n\n    onMouseupImage() {\n        if (!this.isDragging) {\n            return;\n        }\n        this.isDragging = false;\n        this.translate.x += this.translate.dx;\n        this.translate.y += this.translate.dy;\n        this.translate.dx = 0;\n        this.translate.dy = 0;\n        this.updateZoomerStyle();\n    }\n\n    /**\n     * @param {DragEvent}\n     */\n    onMousemoveView(ev) {\n        if (!this.isDragging) {\n            return;\n        }\n        this.translate.dx = ev.clientX - this.dragStartX;\n        this.translate.dy = ev.clientY - this.dragStartY;\n        this.updateZoomerStyle();\n    }\n\n    resetZoom() {\n        this.state.scale = 1;\n        this.updateZoomerStyle();\n    }\n\n    rotate() {\n        this.state.angle += 90;\n    }\n\n    /**\n     * @param {{ scroll?: boolean }}\n     */\n    zoomIn({ scroll = false } = {}) {\n        this.state.scale = this.state.scale + (scroll ? this.scrollZoomStep : this.zoomStep);\n        this.updateZoomerStyle();\n    }\n\n    /**\n     * @param {{ scroll?: boolean }}\n     */\n    zoomOut({ scroll = false } = {}) {\n        if (this.state.scale === this.minScale) {\n            return;\n        }\n        const unflooredAdaptedScale =\n            this.state.scale - (scroll ? this.scrollZoomStep : this.zoomStep);\n        this.state.scale = Math.max(this.minScale, unflooredAdaptedScale);\n        this.updateZoomerStyle();\n    }\n\n    updateZoomerStyle() {\n        const tx =\n            this.imageRef.el.offsetWidth * this.state.scale > this.zoomerRef.el.offsetWidth\n                ? this.translate.x + this.translate.dx\n                : 0;\n        const ty =\n            this.imageRef.el.offsetHeight * this.state.scale > this.zoomerRef.el.offsetHeight\n                ? this.translate.y + this.translate.dy\n                : 0;\n        if (tx === 0) {\n            this.translate.x = 0;\n        }\n        if (ty === 0) {\n            this.translate.y = 0;\n        }\n        this.zoomerRef.el.style = \"transform: \" + `translate(${tx}px, ${ty}px)`;\n    }\n\n    get imageStyle() {\n        let style =\n            \"transform: \" +\n            `scale3d(${this.state.scale}, ${this.state.scale}, 1) ` +\n            `rotate(${this.state.angle}deg);`;\n\n        if (this.state.angle % 180 !== 0) {\n            style += `max-height: ${window.innerWidth}px; max-width: ${window.innerHeight}px;`;\n        } else {\n            style += \"max-height: 100%; max-width: 100%;\";\n        }\n        style += `background: repeating-conic-gradient(#ccc 0deg 90deg, #fff 90deg 180deg) 50% / 20px 20px;`;\n        return style;\n    }\n\n    onClickPrint() {\n        const printWindow = window.open(\"about:blank\", \"_new\");\n        printWindow.document.open();\n        printWindow.document.write(`\n                <html>\n                    <head>\n                        <script>\n                            function onloadImage() {\n                                setTimeout('printImage()', 10);\n                            }\n                            function printImage() {\n                                window.print();\n                                window.close();\n                            }\n                        </script>\n                    </head>\n                    <body onload='onloadImage()'>\n                        <img src=\"${this.state.file.defaultSource}\" alt=\"\"/>\n                    </body>\n                </html>`);\n        printWindow.document.close();\n    }\n}\n", "import { onWillDestroy } from \"@odoo/owl\";\nimport { registry } from \"@web/core/registry\";\nimport { FileViewer } from \"./file_viewer\";\n\nlet id = 1;\n\nexport function createFileViewer() {\n    const fileViewerId = `web.file_viewer${id++}`;\n    /**\n     * @param {import(\"@web/core/file_viewer/file_viewer\").FileViewer.props.files[]} file\n     * @param {import(\"@web/core/file_viewer/file_viewer\").FileViewer.props.files} files\n     */\n    function open(file, files = [file]) {\n        close();\n        if (!file.isViewable) {\n            return;\n        }\n        if (files.length > 0) {\n            const viewableFiles = files.filter((file) => file.isViewable);\n            const index = viewableFiles.indexOf(file);\n            registry.category(\"main_components\").add(fileViewerId, {\n                Component: FileViewer,\n                props: { files: viewableFiles, startIndex: index, close },\n            });\n        }\n    }\n\n    function close() {\n        registry.category(\"main_components\").remove(fileViewerId);\n    }\n    return { open, close };\n}\n\nexport function useFileViewer() {\n    const { open, close } = createFileViewer();\n    onWillDestroy(close);\n    return { open, close };\n}\n", "import { useService } from \"@web/core/utils/hooks\";\n\nimport { useEffect } from \"@odoo/owl\";\n\n/**\n * This hook will register/unregister the given registration\n * when the caller component will mount/unmount.\n *\n * @param {string} hotkey\n * @param {import(\"./hotkey_service\").HotkeyCallback} callback\n * @param {import(\"./hotkey_service\").HotkeyOptions} [options] additional options\n */\nexport function useHotkey(hotkey, callback, options = {}) {\n    const hotkeyService = useService(\"hotkey\");\n    useEffect(\n        () => hotkeyService.add(hotkey, callback, options),\n        () => []\n    );\n}\n", "import { isMacOS } from \"../browser/feature_detection\";\nimport { registry } from \"../registry\";\nimport { browser } from \"../browser/browser\";\nimport { getVisibleElements } from \"../utils/ui\";\n\n/**\n * @typedef {(context: { area: HTMLElement, target: EventTarget }) => void} HotkeyCallback\n *\n * @typedef {Object} HotkeyOptions\n * @property {boolean} [allowRepeat]\n *  allow registration to perform multiple times when hotkey is held down\n * @property {boolean} [bypassEditableProtection]\n *  if true the hotkey service will call this registration\n *  even if an editable element is focused\n * @property {boolean} [global]\n *  allow registration to perform no matter the UI active element\n * @property {() => HTMLElement} [area]\n *  adds a restricted operating area for this hotkey\n * @property {(target: HTMLElement) => boolean} [isAvailable]\n *  adds a validation before calling the hotkey registration's callback\n * @property {() => HTMLElement} [withOverlay]\n *  provides the element on which the overlay should be displayed\n *  Please note that if provided the hotkey will only work with\n *  the overlay access key, similarly to all [data-hotkey] DOM attributes.\n *\n * @typedef {HotkeyOptions & {\n *  hotkey: string,\n *  callback: HotkeyCallback,\n *  activeElement: HTMLElement,\n * }} HotkeyRegistration\n */\n\nconst ALPHANUM_KEYS = \"abcdefghijklmnopqrstuvwxyz0123456789\".split(\"\");\nconst NAV_KEYS = [\n    \"arrowleft\",\n    \"arrowright\",\n    \"arrowup\",\n    \"arrowdown\",\n    \"pageup\",\n    \"pagedown\",\n    \"home\",\n    \"end\",\n    \"backspace\",\n    \"enter\",\n    \"tab\",\n    \"delete\",\n    \"space\",\n];\nconst MODIFIERS = [\"alt\", \"control\", \"shift\"];\nconst AUTHORIZED_KEYS = [...ALPHANUM_KEYS, ...NAV_KEYS, \"escape\", \"<\", \">\"];\n\n/**\n * Get the actual hotkey being pressed.\n *\n * @param {KeyboardEvent} ev\n * @returns {string} the active hotkey, in lowercase\n */\nexport function getActiveHotkey(ev) {\n    if (!ev.key) {\n        // Chrome may trigger incomplete keydown events under certain circumstances.\n        // E.g. when using browser built-in autocomplete on an input.\n        // See https://stackoverflow.com/questions/59534586/google-chrome-fires-keydown-event-when-form-autocomplete\n        return \"\";\n    }\n    if (ev.isComposing) {\n        // This case happens with an IME for example: we let it handle all key events.\n        return \"\";\n    }\n    const hotkey = [];\n\n    // ------- Modifiers -------\n    // Modifiers are pushed in ascending order to the hotkey.\n    if (isMacOS() ? ev.ctrlKey : ev.altKey) {\n        hotkey.push(\"alt\");\n    }\n    if (isMacOS() ? ev.metaKey : ev.ctrlKey) {\n        hotkey.push(\"control\");\n    }\n    if (ev.shiftKey) {\n        hotkey.push(\"shift\");\n    }\n\n    // ------- Key -------\n    let key = ev.key.toLowerCase();\n\n    // The browser space is natively \" \", we want \"space\" for esthetic reasons\n    if (key === \" \") {\n        key = \"space\";\n    }\n\n    // Identify if the user has tapped on the number keys above the text keys.\n    if (ev.code && ev.code.indexOf(\"Digit\") === 0) {\n        key = ev.code.slice(-1);\n    }\n    // Prefer physical keys for non-latin keyboard layout.\n    if (!AUTHORIZED_KEYS.includes(key) && ev.code && ev.code.indexOf(\"Key\") === 0) {\n        key = ev.code.slice(-1).toLowerCase();\n    }\n    // Make sure we do not duplicate a modifier key\n    if (!MODIFIERS.includes(key)) {\n        hotkey.push(key);\n    }\n\n    return hotkey.join(\"+\");\n}\n\nexport const hotkeyService = {\n    dependencies: [\"ui\"],\n    // Be aware that all odoo hotkeys are designed with this modifier in mind,\n    // so changing the overlay modifier may conflict with some shortcuts.\n    overlayModifier: \"alt\",\n    start(env, { ui }) {\n        /** @type {Map<number, HotkeyRegistration>} */\n        const registrations = new Map();\n        let nextToken = 0;\n        let overlaysVisible = false;\n\n        addListeners(browser);\n\n        function addListeners(target) {\n            target.addEventListener(\"keydown\", onKeydown);\n            target.addEventListener(\"keyup\", removeHotkeyOverlays);\n            target.addEventListener(\"blur\", removeHotkeyOverlays);\n            target.addEventListener(\"click\", removeHotkeyOverlays);\n        }\n\n        /**\n         * Handler for keydown events.\n         * Verifies if the keyboard event can be dispatched or not.\n         * Rules sequence to forbid dispatching :\n         * - UI is blocked\n         * - the pressed key is not whitelisted\n         *\n         * @param {KeyboardEvent} event\n         */\n        function onKeydown(event) {\n            if (event.code && event.code.indexOf(\"Numpad\") === 0 && /^\\d$/.test(event.key)) {\n                // Ignore all number keys from the Keypad because of a certain input method\n                // of (advance-)ASCII characters on Windows OS: ALT+[numerical code from keypad]\n                // See https://support.microsoft.com/en-us/office/insert-ascii-or-unicode-latin-based-symbols-and-characters-d13f58d3-7bcb-44a7-a4d5-972ee12e50e0#bm1\n                return;\n            }\n\n            const hotkey = getActiveHotkey(event);\n            if (!hotkey) {\n                return;\n            }\n            const { activeElement, isBlocked } = ui;\n\n            // Do not dispatch if UI is blocked\n            if (isBlocked) {\n                return;\n            }\n\n            // Replace all [accesskey] attrs by [data-hotkey] on all elements.\n            // This is needed to take over on the default accesskey behavior\n            // and also to avoid any conflict with it.\n            const elementsWithAccessKey = document.querySelectorAll(\"[accesskey]\");\n            for (const el of elementsWithAccessKey) {\n                if (el instanceof HTMLElement) {\n                    el.dataset.hotkey = el.accessKey;\n                    el.removeAttribute(\"accesskey\");\n                }\n            }\n\n            // Special case: open hotkey overlays\n            if (!overlaysVisible && hotkey === hotkeyService.overlayModifier) {\n                addHotkeyOverlays(activeElement);\n                event.preventDefault();\n                return;\n            }\n\n            // Is the pressed key NOT whitelisted ?\n            const singleKey = hotkey.split(\"+\").pop();\n            if (!AUTHORIZED_KEYS.includes(singleKey)) {\n                return;\n            }\n\n            // Protect any editable target that does not explicitly accept hotkeys\n            // NB: except for ESC, which is always allowed as hotkey in editables.\n            const targetIsEditable =\n                event.target instanceof HTMLElement &&\n                (/input|textarea/i.test(event.target.tagName) || event.target.isContentEditable) &&\n                !event.target.matches(\"input[type=checkbox], input[type=radio]\");\n            const shouldProtectEditable =\n                targetIsEditable && !event.target.dataset.allowHotkeys && singleKey !== \"escape\";\n\n            // Finally, prepare and dispatch.\n            const infos = {\n                activeElement,\n                hotkey,\n                isRepeated: event.repeat,\n                target: event.target,\n                shouldProtectEditable,\n            };\n            const dispatched = dispatch(infos);\n            if (dispatched) {\n                // Only if event has been handled.\n                // Purpose: prevent browser defaults\n                event.preventDefault();\n                // Purpose: stop other window keydown listeners (e.g. home menu)\n                event.stopImmediatePropagation();\n            }\n\n            // Finally, always remove overlays at that point\n            if (overlaysVisible) {\n                removeHotkeyOverlays();\n                event.preventDefault();\n            }\n        }\n\n        /**\n         * Dispatches an hotkey to first matching registration.\n         * Registrations are iterated in following order:\n         * - priority to all registrations done through the hotkeyService.add()\n         *   method (NB: in descending order of insertion = newer first)\n         * - then all registrations done through the DOM [data-hotkey] attribute\n         *\n         * @param {{\n         *  activeElement: HTMLElement,\n         *  hotkey: string,\n         *  isRepeated: boolean,\n         *  target: EventTarget,\n         *  shouldProtectEditable: boolean,\n         * }} infos\n         * @returns {boolean} true if has been dispatched\n         */\n        function dispatch(infos) {\n            const { activeElement, hotkey, isRepeated, target, shouldProtectEditable } = infos;\n\n            // Prepare registrations and the common filter\n            const reversedRegistrations = Array.from(registrations.values()).reverse();\n            const domRegistrations = getDomRegistrations(hotkey, activeElement);\n            const allRegistrations = reversedRegistrations.concat(domRegistrations);\n\n            // Find all candidates\n            const candidates = allRegistrations.filter(\n                (reg) =>\n                    reg.hotkey === hotkey &&\n                    (reg.allowRepeat || !isRepeated) &&\n                    (reg.bypassEditableProtection || !shouldProtectEditable) &&\n                    (reg.global || reg.activeElement === activeElement) &&\n                    (!reg.isAvailable || reg.isAvailable(target)) &&\n                    (!reg.area || (target && reg.area() && reg.area().contains(target)))\n            );\n\n            // First candidate\n            let winner = candidates.shift();\n            if (winner && winner.area) {\n                // If there is an area, find the closest one\n                for (const candidate of candidates.filter((c) => Boolean(c.area))) {\n                    if (candidate.area() && winner.area().contains(candidate.area())) {\n                        winner = candidate;\n                    }\n                }\n            }\n\n            // Dispatch actual hotkey to the matching registration\n            if (winner) {\n                winner.callback({\n                    area: winner.area && winner.area(),\n                    target,\n                });\n                return true;\n            }\n            return false;\n        }\n\n        /**\n         * Get a list of registrations from the [data-hotkey] defined in the DOM\n         *\n         * @param {string} hotkey\n         * @param {HTMLElement} activeElement\n         * @returns {HotkeyRegistration[]}\n         */\n        function getDomRegistrations(hotkey, activeElement) {\n            const overlayModParts = hotkeyService.overlayModifier.split(\"+\");\n            if (!overlayModParts.every((el) => hotkey.includes(el))) {\n                return [];\n            }\n\n            // Get all elements having a data-hotkey attribute  and matching\n            // the actual hotkey without the overlayModifier.\n            const cleanHotkey = hotkey\n                .split(\"+\")\n                .filter((key) => !overlayModParts.includes(key))\n                .join(\"+\");\n            const elems = getVisibleElements(activeElement, `[data-hotkey='${cleanHotkey}' i]`);\n            return elems.map((el) => ({\n                hotkey,\n                activeElement,\n                bypassEditableProtection: true,\n                callback: () => {\n                    if (document.activeElement) {\n                        document.activeElement.blur();\n                    }\n                    el.focus();\n                    setTimeout(() => el.click());\n                },\n            }));\n        }\n\n        /**\n         * Add the hotkey overlays respecting the ui active element.\n         * @param {HTMLElement} activeElement\n         */\n        function addHotkeyOverlays(activeElement) {\n            // Gather the hotkeys to overlay registered through the useHotkey hook.\n            const hotkeysFromHookToHighlight = [];\n            for (const [, registration] of registrations) {\n                const overlayElement = registration.withOverlay?.();\n                if (overlayElement) {\n                    hotkeysFromHookToHighlight.push({\n                        hotkey: registration.hotkey.replace(\n                            `${hotkeyService.overlayModifier}+`,\n                            \"\"\n                        ),\n                        el: overlayElement,\n                    });\n                }\n            }\n\n            // Gather the hotkeys to overlay registered through the DOM datasets.\n            const hotkeysFromDomToHighlight = getVisibleElements(\n                activeElement,\n                \"[data-hotkey]:not(:disabled)\"\n            ).map((el) => ({ hotkey: el.dataset.hotkey, el }));\n\n            const items = [...hotkeysFromDomToHighlight, ...hotkeysFromHookToHighlight];\n            for (const item of items) {\n                const hotkey = item.hotkey;\n                const overlay = document.createElement(\"div\");\n                overlay.classList.add(\n                    \"o_web_hotkey_overlay\",\n                    \"position-absolute\",\n                    \"top-0\",\n                    \"bottom-0\",\n                    \"start-0\",\n                    \"end-0\",\n                    \"d-flex\",\n                    \"justify-content-center\",\n                    \"align-items-center\",\n                    \"m-0\",\n                    \"bg-black-50\",\n                    \"h6\"\n                );\n                overlay.style.zIndex = 1;\n                const overlayKbd = document.createElement(\"kbd\");\n                overlayKbd.className = \"small\";\n                overlayKbd.appendChild(document.createTextNode(hotkey.toUpperCase()));\n                overlay.appendChild(overlayKbd);\n\n                let overlayParent;\n                if (item.el.tagName.toUpperCase() === \"INPUT\") {\n                    // special case for the search input that has an access key\n                    // defined. We cannot set the overlay on the input itself,\n                    // only on its parent.\n                    overlayParent = item.el.parentElement;\n                } else {\n                    overlayParent = item.el;\n                }\n\n                if (overlayParent.style.position !== \"absolute\") {\n                    overlayParent.style.position = \"relative\";\n                }\n                overlayParent.appendChild(overlay);\n            }\n            overlaysVisible = true;\n        }\n\n        /**\n         * Remove all the hotkey overlays.\n         */\n        function removeHotkeyOverlays() {\n            for (const overlay of document.querySelectorAll(\".o_web_hotkey_overlay\")) {\n                overlay.remove();\n            }\n            overlaysVisible = false;\n        }\n\n        /**\n         * Registers a new hotkey.\n         *\n         * @param {string} hotkey\n         * @param {HotkeyCallback} callback\n         * @param {HotkeyOptions} [options]\n         * @returns {number} registration token\n         */\n        function registerHotkey(hotkey, callback, options = {}) {\n            // Validate some informations\n            if (!hotkey || hotkey.length === 0) {\n                throw new Error(\"You must specify an hotkey when registering a registration.\");\n            }\n\n            if (!callback || typeof callback !== \"function\") {\n                throw new Error(\n                    \"You must specify a callback function when registering a registration.\"\n                );\n            }\n\n            /**\n             * An hotkey must comply to these rules:\n             *  - all parts are whitelisted\n             *  - single key part comes last\n             *  - each part is separated by the dash character: \"+\"\n             */\n            const keys = hotkey\n                .toLowerCase()\n                .split(\"+\")\n                .filter((k) => !MODIFIERS.includes(k));\n            if (keys.some((k) => !AUTHORIZED_KEYS.includes(k))) {\n                throw new Error(\n                    `You are trying to subscribe for an hotkey ('${hotkey}')\n            that contains parts not whitelisted: ${keys.join(\", \")}`\n                );\n            } else if (keys.length > 1) {\n                throw new Error(\n                    `You are trying to subscribe for an hotkey ('${hotkey}')\n            that contains more than one single key part: ${keys.join(\"+\")}`\n                );\n            }\n\n            // Add registration\n            const token = nextToken++;\n            /** @type {HotkeyRegistration} */\n            const registration = {\n                hotkey: hotkey.toLowerCase(),\n                callback,\n                activeElement: null,\n                allowRepeat: options && options.allowRepeat,\n                bypassEditableProtection: options && options.bypassEditableProtection,\n                global: options && options.global,\n                area: options && options.area,\n                isAvailable: options && options.isAvailable,\n                withOverlay: options && options.withOverlay,\n            };\n\n            // Due to the way elements are mounted in the DOM by Owl (bottom-to-top),\n            // we need to wait the next micro task tick to set the context owner of the registration.\n            Promise.resolve().then(() => {\n                registration.activeElement = ui.activeElement;\n            });\n\n            registrations.set(token, registration);\n            return token;\n        }\n\n        /**\n         * Unsubscribes the token corresponding registration.\n         *\n         * @param {number} token\n         */\n        function unregisterHotkey(token) {\n            registrations.delete(token);\n        }\n\n        return {\n            /**\n             * @param {string} hotkey\n             * @param {HotkeyCallback} callback\n             * @param {HotkeyOptions} [options]\n             * @returns {() => void}\n             */\n            add(hotkey, callback, options = {}) {\n                const token = registerHotkey(hotkey, callback, options);\n                return () => {\n                    unregisterHotkey(token);\n                };\n            },\n            /**\n             * @param {HTMLIFrameElement} iframe\n             */\n            registerIframe(iframe) {\n                addListeners(iframe.contentWindow);\n            },\n        };\n    },\n};\n\nregistry.category(\"services\").add(\"hotkey\", hotkeyService);\n/** @typedef {ReturnType<hotkeyService[\"start\"]>} HotkeyService */\n", "import { browser } from \"@web/core/browser/browser\";\nimport { registry } from \"@web/core/registry\";\nimport { Component, onMounted, useState } from \"@odoo/owl\";\nimport { isDisplayStandalone } from \"@web/core/browser/feature_detection\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { Dropdown } from \"@web/core/dropdown/dropdown\";\n\nexport class InstallScopedApp extends Component {\n    static props = {};\n    static template = \"web.InstallScopedApp\";\n    static components = { Dropdown };\n    setup() {\n        this.pwa = useService(\"pwa\");\n        this.state = useState({ manifest: {}, showInstallUI: false });\n        this.isDisplayStandalone = isDisplayStandalone();\n        // BeforeInstallPrompt event can take while before the browser triggers it. Some will display\n        // immediately, others will wait that the user has interacted for some time with the website.\n        this.isInstallationPossible = browser.BeforeInstallPromptEvent !== undefined;\n        onMounted(async () => {\n            this.state.manifest = await this.pwa.getManifest();\n            this.state.showInstallUI = true;\n        });\n    }\n    onChangeName(ev) {\n        const value = ev.target.value;\n        if (value !== this.state.manifest.name) {\n            const url = new URL(document.location.href);\n            url.searchParams.set(\"app_name\", encodeURIComponent(value));\n            browser.location.replace(url);\n        }\n    }\n    onInstall() {\n        this.state.showInstallUI = false;\n        this.pwa.show({\n            onDone: (res) => {\n                if (res.outcome === \"accepted\") {\n                    browser.location.replace(this.state.manifest.start_url);\n                } else {\n                    this.state.showInstallUI = true;\n                }\n            },\n        });\n    }\n}\n\nregistry.category(\"public_components\").add(\"web.install_scoped_app\", InstallScopedApp);\n", "/** @odoo-module **/\nimport { useEffect, onMounted } from \"@odoo/owl\";\nimport { CodeEditor } from \"@web/core/code_editor/code_editor\";\nimport { escapeRegExp } from \"@web/core/utils/strings\";\n\nexport class IrUiViewCodeEditor extends CodeEditor {\n    static props = {\n        ...this.props,\n        record: { type: Object },\n    };\n\n    setup() {\n        super.setup(...arguments);\n        this.markers = [];\n\n        onMounted(() => {\n            this.aceEditor.getSession().on(\"change\", () => {\n                // Markers have fixed pixel positions, so they get wonky on change.\n                this.clearMarkers();\n            });\n        });\n\n        useEffect(\n            (arch, invalid_locators) => {\n                if (arch && invalid_locators) {\n                    this.highlightInvalidLocators(arch, invalid_locators);\n                    return () => this.clearMarkers();\n                }\n            },\n            () => [this.props.value, this.props.record?.data.invalid_locators]\n        );\n    }\n\n    async highlightInvalidLocators(arch, invalid_locators) {\n        const resModel = this.env.model?.config.resModel;\n        const resId = this.env.model?.config.resId;\n        if (resModel === \"ir.ui.view\" && resId) {\n            const { doc } = this.aceEditor.session;\n            for (const spec of invalid_locators) {\n                if (spec.broken_hierarchy) {\n                    continue\n                }\n                const { tag, attrib, sourceline } = spec;\n                const attribRegex = Object.entries(attrib)\n                    .map(([key, value]) => {\n                        const escapedValue = escapeRegExp(value).replace(/\"/g, '(\"|&quot;)');\n                        return (\n                            `(?=[^>]*?\\\\b${escapeRegExp(key)}\\\\s*=\\\\s*` +\n                            `(?:\"[^\"]*${escapedValue}[^\"]*\"|'[^']*${escapedValue}[^']*'))`\n                        );\n                    })\n                    .join(\"\");\n                const nodeRegex = new RegExp(`<${escapeRegExp(tag)}\\\\s+${attribRegex}[^>]*>`, \"g\");\n                for (const match of arch.matchAll(nodeRegex)) {\n                    const startIndex = match.index;\n                    const endIndex = startIndex + match[0].length;\n                    const startPos = doc.indexToPosition(startIndex);\n                    const endPos = doc.indexToPosition(endIndex);\n                    if (startPos.row + 1 === sourceline) {\n                        const range = new window.ace.Range(\n                            startPos.row,\n                            startPos.column,\n                            endPos.row,\n                            endPos.column\n                        );\n                        this.markers.push(\n                            this.aceEditor.session.addMarker(range, \"invalid_locator\", \"text\")\n                        );\n                    }\n                }\n            }\n        }\n    }\n\n    clearMarkers() {\n        this.markers.forEach((marker) => this.aceEditor.session.removeMarker(marker));\n        this.markers = [];\n    }\n}\n", "import { localization } from \"@web/core/l10n/localization\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { memoize } from \"@web/core/utils/functions\";\nimport { ensureArray } from \"../utils/arrays\";\n\nconst { DateTime, Settings } = luxon;\n\n/**\n * @typedef ConversionOptions\n *  This is a list of the available options to either:\n *  - convert a DateTime to a string (format)\n *  - convert a string to a DateTime (parse)\n *  All of these are optional and the default values are issued by the Localization service.\n *\n * @property {string} [format]\n *  Format used to format a DateTime or to parse a formatted string.\n *  > Default: the session localization format.\n *\n * @typedef {luxon.DateTime} DateTime\n *\n * @typedef {[NullableDateTime, NullableDateTime]} NullableDateRange\n *\n * @typedef {DateTime | false | null | undefined} NullableDateTime\n */\n\n/**\n * @typedef ConversionLocalOptions\n *\n * @property {boolean} [showSeconds]\n *  Show the seconds in the final result.\n *  > Default: false.\n * @property {boolean} [showTime]\n *  Show the time in the final result.\n *  > Default: true.\n * @property {boolean} [showDate]\n *  Show the date in the final result.\n *  > Default: true.\n */\n\n/**\n * Limits defining a valid date.\n * This is needed because the server only understands 4-digit years.\n * Note: both of these are in the local timezone\n */\nexport const MIN_VALID_DATE = DateTime.fromObject({ year: 1000 });\nexport const MAX_VALID_DATE = DateTime.fromObject({ year: 9999 }).endOf(\"year\");\n\nconst SERVER_DATE_FORMAT = \"yyyy-MM-dd\";\nconst SERVER_TIME_FORMAT = \"HH:mm:ss\";\nconst SERVER_DATETIME_FORMAT = `${SERVER_DATE_FORMAT} ${SERVER_TIME_FORMAT}`;\n\nconst nonAlphaRegex = /[^a-z]/gi;\nconst nonDigitRegex = /[^\\d]/g;\n\nconst normalizeFormatTable = {\n    // Python strftime to luxon.js conversion table\n    // See odoo/addons/base/views/res_lang_views.xml\n    // for details about supported directives\n    a: \"ccc\",\n    A: \"cccc\",\n    b: \"MMM\",\n    B: \"MMMM\",\n    d: \"dd\",\n    H: \"HH\",\n    I: \"hh\",\n    j: \"o\",\n    m: \"MM\",\n    M: \"mm\",\n    p: \"a\",\n    S: \"ss\",\n    W: \"WW\",\n    w: \"c\",\n    y: \"yy\",\n    Y: \"yyyy\",\n    c: \"ccc MMM d HH:mm:ss yyyy\",\n    x: \"MM/dd/yy\",\n    X: \"HH:mm:ss\",\n};\n\nconst smartDateUnits = {\n    d: \"days\",\n    m: \"months\",\n    w: \"weeks\",\n    y: \"years\",\n    H: \"hours\",\n    M: \"minutes\",\n    S: \"seconds\",\n};\nconst smartWeekdays = {\n    monday: 1,\n    tuesday: 2,\n    wednesday: 3,\n    thursday: 4,\n    friday: 5,\n    saturday: 6,\n    sunday: 7,\n};\n\n/** @type {WeakMap<DateTime, string>} */\nconst dateCache = new WeakMap();\n/** @type {WeakMap<DateTime, string>} */\nconst dateTimeCache = new WeakMap();\n\nexport class ConversionError extends Error {\n    name = \"ConversionError\";\n}\n\n//-----------------------------------------------------------------------------\n// Helpers\n//-----------------------------------------------------------------------------\n\n/**\n * Checks whether 2 given dates or date ranges are equal. Both values are allowed\n * to be falsy or to not be of the same type (which will return false).\n *\n * @param {NullableDateTime | NullableDateRange} d1\n * @param {NullableDateTime | NullableDateRange} d2\n * @returns {boolean}\n */\nexport function areDatesEqual(d1, d2) {\n    if (Array.isArray(d1) || Array.isArray(d2)) {\n        // One of the values is a date range -> checks deep equality between the ranges\n        d1 = ensureArray(d1);\n        d2 = ensureArray(d2);\n        return d1.length === d2.length && d1.every((d1Val, i) => areDatesEqual(d1Val, d2[i]));\n    }\n    if (d1 instanceof DateTime && d2 instanceof DateTime && d1 !== d2) {\n        // Both values are DateTime objects -> use Luxon's comparison\n        return d1.equals(d2);\n    } else {\n        // One of the values is not a DateTime object -> fallback to strict equal\n        return d1 === d2;\n    }\n}\n\n/**\n * @param {DateTime} desired\n * @param {DateTime} minDate\n * @param {DateTime} maxDate\n */\nexport function clampDate(desired, minDate, maxDate) {\n    if (maxDate < desired) {\n        return maxDate;\n    }\n    if (minDate > desired) {\n        return minDate;\n    }\n    return desired;\n}\n\n/**\n * Get the week year and week number of a given date as well as the starting\n * date of the week, in the user's locale settings.\n *\n * @param {Date | luxon.DateTime} date\n * @returns {{ year: number, week: number, startDate: luxon.DateTime }}\n *  the year the week is part of, and\n *  the ISO week number (1-53) of the Monday nearest to the locale's first day of the week\n */\nexport function getLocalYearAndWeek(date) {\n    if (!date.isLuxonDateTime) {\n        date = DateTime.fromJSDate(date);\n    }\n    const { weekStart } = localization;\n    // go to start of week\n    const startDate = date.minus({ days: (date.weekday + 7 - weekStart) % 7 });\n    // go to nearest Monday, up to 3 days back- or forwards\n    date =\n        weekStart > 1 && weekStart < 5 // if firstDay after Mon & before Fri\n            ? startDate.minus({ days: (startDate.weekday + 6) % 7 }) // then go back 1-3 days\n            : startDate.plus({ days: (8 - startDate.weekday) % 7 }); // else go forwards 0-3 days\n    date = date.plus({ days: 6 }); // go to last weekday of ISO week\n    const jan4 = DateTime.local(date.year, 1, 4);\n    // count from previous year if week falls before Jan 4\n    const diffDays =\n        date < jan4 ? date.diff(jan4.minus({ years: 1 }), \"day\").days : date.diff(jan4, \"day\").days;\n    return {\n        year: date.year,\n        week: Math.trunc(diffDays / 7) + 1,\n        startDate,\n    };\n}\n\n/**\n * Get the start of the week for the given date, in the user's locale settings.\n * The start of the week is determined by the `weekStart` setting.\n *\n * Luxon's `.startOf(\"week\")` method uses the ISO week definition, which starts on Monday.\n * Luxon has a `.startOf(\"week\", { useLocaleWeeks: true })` method, but it relies on the\n * Intl API and the `getWeekInfo` method, which is not supported in all browsers.\n * See: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale/getWeekInfo#browser_compatibility\n *\n * @param {luxon.DateTime} date\n * @returns {luxon.DateTime}\n */\nexport function getStartOfLocalWeek(date) {\n    const { weekStart } = localization;\n    const weekday = date.weekday < weekStart ? weekStart - 7 : weekStart;\n    return date.set({ weekday }).startOf(\"day\");\n}\n\n/**\n * Get the end of the week for the given date, in the user's locale settings.\n * The end of the week is determined by the `weekStart` setting.\n *\n * Luxon's `.endOf(\"week\")` method uses the ISO week definition, which starts on Monday.\n * Luxon has a `.endOf(\"week\", { useLocaleWeeks: true })` method, but it relies on the\n * Intl API and the `getWeekInfo` method, which is not supported in all browsers.\n * See: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale/getWeekInfo#browser_compatibility\n *\n * @param {luxon.DateTime} date\n * @returns {luxon.DateTime}\n */\nexport function getEndOfLocalWeek(date) {\n    return getStartOfLocalWeek(date).plus({ days: 6 }).endOf(\"day\");\n}\n\n/**\n * @param {NullableDateTime | NullableDateRange} value\n * @param {NullableDateRange} range\n * @returns {boolean}\n */\nexport function isInRange(value, range) {\n    if (!value || !range) {\n        return false;\n    }\n    if (Array.isArray(value)) {\n        const actualValues = value.filter(Boolean).sort();\n        if (actualValues.length < 2) {\n            return isInRange(actualValues[0], range);\n        }\n        return (\n            (actualValues[0] <= range[0] && range[0] <= actualValues[1]) ||\n            (range[0] <= actualValues[0] && actualValues[0] <= range[1])\n        );\n    } else {\n        return range[0] <= value && value <= range[1];\n    }\n}\n\n/**\n * Returns whether the given DateTime is valid.\n * The date is considered valid if it:\n * - is a DateTime object\n * - has the \"isValid\" flag set to true\n * - is between 1000-01-01 and 9999-12-31 (both included)\n * @see MIN_VALID_DATE\n * @see MAX_VALID_DATE\n *\n * @param {NullableDateTime} date\n */\nfunction isValidDate(date) {\n    return date && date.isValid && isInRange(date, [MIN_VALID_DATE, MAX_VALID_DATE]);\n}\n\n/**\n * Smart date inputs are shortcuts to write dates quicker.\n * These shortcuts are based on python version: `odoo.tools.date_utils.parse_date`.\n * Starting from now (or \"today\"), add relative delta to the date.\n *\n * e.g.\n *   \"+1d\" or \"+1\" will return now + 1 day\n *   \"-2w\" will return now - 2 weeks\n *   \"+3m\" will return now + 3 months\n *   \"-4y\" will return now + 4 years\n *   \"=monday\" will return the previous Monday at midnight\n *   \"=1d\" will return the first day of month at midnight\n *   \"+3H\" will return now + 3 hours\n *   \"+3M\" will return now + 3 minutes\n *   \"+3S\" will return now + 3 seconds\n *   \"today -1d\" will return yesterday at midnight\n *   \"=week_start\" will return the first day of the current week at midnight, according to the locale\n *\n * Difference with python version: a simple \"+1\" means \"+1d\" for the first term,\n * the unit is optional and defaults to \"d\".\n *\n * @param {string} value\n * @returns {NullableDateTime} Luxon datetime object (in the user's local timezone)\n */\nfunction parseSmartDateInput(value) {\n    const terms = value.split(/\\s+/);\n    if (!terms.length) {\n        return false;\n    }\n    var now = DateTime.local().startOf(\"second\");\n    if (terms[0] == \"today\") {\n        terms.shift();\n        now = now.startOf(\"day\");\n    } else if (terms[0] == \"now\") {\n        terms.shift();\n    } else if (terms.length == 1 && /^[=+-]\\d+$/.test(terms[0])) {\n        // handle optional unit for simple input\n        terms[0] += \"d\";\n    }\n\n    for (let i = 0; i < terms.length; i++) {\n        const term = terms[i];\n        const operator = term[0];\n        if (term.length < 3 || ![\"+\", \"-\", \"=\"].includes(operator)) {\n            return false;\n        }\n\n        // Weekday\n        const dayname = term.slice(1);\n        if (Object.hasOwn(smartWeekdays, dayname) || dayname === \"week_start\") {\n            const { weekStart } = localization;\n            const weekdayNumber =\n                dayname === \"week_start\" ? weekStart : smartWeekdays[dayname];\n            let weekdayOffset =\n                ((weekdayNumber - weekStart + 7) % 7) - ((now.weekday - weekStart + 7) % 7);\n            if (operator == \"+\" || operator == \"-\") {\n                if (weekdayOffset > 0 && operator == \"-\") {\n                    weekdayOffset -= 7;\n                } else if (weekdayOffset < 0 && operator == \"+\") {\n                    weekdayOffset += 7;\n                }\n            } else {\n                now = now.startOf(\"day\");\n            }\n            now = now.plus({ days: weekdayOffset });\n            continue;\n        }\n\n        // Operations on dates\n        try {\n            const field_name = smartDateUnits[term[term.length - 1]];\n            const number = parseInt(term.slice(1, -1), 10);\n            if (!field_name || isNaN(number)) {\n                return false;\n            }\n            if (operator == \"+\") {\n                now = now.plus({ [field_name]: number });\n            } else if (operator == \"-\") {\n                now = now.minus({ [field_name]: number });\n            } else if (operator == \"=\") {\n                if (field_name == \"seconds\" || field_name == \"minutes\" || field_name == \"hours\") {\n                    now = now.startOf(field_name);\n                } else if (field_name == \"weeks\") {\n                    return false; // unsupported\n                } else {\n                    now = now.startOf(\"day\");\n                }\n                now = now.set({ [field_name]: number });\n            }\n        } catch {\n            return false;\n        }\n    }\n\n    return now;\n}\n\n/**\n * Removes any duplicate *subsequent* alphabetic characters in a given string.\n * Example: \"aa-bb-CCcc-ddD-c xxxx-Yy-ZZ\" -> \"a-b-Cc-dD-c x-Yy-Z\"\n *\n * @type {(str: string) => string}\n */\nconst stripAlphaDupes = memoize(function stripAlphaDupes(str) {\n    return str.replace(/[a-z]/gi, (letter, index, str) =>\n        letter === str[index - 1] ? \"\" : letter\n    );\n});\n\n/**\n * Convert Python strftime to escaped luxon.js format.\n *\n * @type {(format: string) => string}\n */\nexport const strftimeToLuxonFormat = memoize(function strftimeToLuxonFormat(format) {\n    const output = [];\n    let inToken = false;\n    for (let index = 0; index < format.length; ++index) {\n        let character = format[index];\n        if (character === \"%\" && !inToken) {\n            inToken = true;\n            continue;\n        }\n        if (/[a-z]/gi.test(character)) {\n            if (inToken && normalizeFormatTable[character] !== undefined) {\n                character = normalizeFormatTable[character];\n            } else {\n                character = `'${character}'`; // luxon escape\n            }\n        }\n        output.push(character);\n        inToken = false;\n    }\n    return output.join(\"\");\n});\n\n/**\n * Lazy getter returning the start of the current day.\n */\nexport function today() {\n    return DateTime.local().startOf(\"day\");\n}\n\n//-----------------------------------------------------------------------------\n// Formatting\n//-----------------------------------------------------------------------------\n\n/**\n * Formats a DateTime object to a date string\n *\n * @param {NullableDateTime} value\n * @param {ConversionOptions} [options={}]\n */\nexport function formatDate(value, options = {}) {\n    if (!value) {\n        return \"\";\n    }\n    const format = options.format || localization.dateFormat;\n    return value.toFormat(format);\n}\n\n/**\n * Formats a DateTime object to a datetime string\n *\n * @param {NullableDateTime} value\n * @param {ConversionOptions} [options={}]\n */\nexport function formatDateTime(value, options = {}) {\n    if (!value) {\n        return \"\";\n    }\n    const format = options.format || localization.dateTimeFormat;\n    return value.setZone(options.tz || \"default\").toFormat(format);\n}\n\n/**\n * Format a DateTime object to a locale date string.\n * e.g.: Jan 31, 2024\n * If the year is the current one, then it's omitted\n * from the result.\n *\n * @param {NullableDateTime} value\n */\nexport function toLocaleDateString(value) {\n    if (!value) {\n        return \"\";\n    }\n    const format = { ...DateTime.DATE_MED };\n    if (today().year === value.year) {\n        delete format.year;\n    }\n    return value.toLocaleString(format);\n}\n\n/**\n * Format a DateTime object to a locale datetime string\n * e.g.: Jan 31, 2024, 12:00 AM\n * If the year is the current one, then it's omitted\n * from the result.\n *\n * @param {NullableDateTime} value\n * @param {ConversionLocalOptions} [options={}]\n */\nexport function toLocaleDateTimeString(\n    value,\n    options = { showDate: true, showTime: true, showSeconds: false }\n) {\n    if (!value) {\n        return \"\";\n    }\n    const format = { ...DateTime.DATETIME_MED_WITH_SECONDS };\n    if (!options.showSeconds) {\n        delete format.second;\n    }\n    if (options.showDate === false) {\n        delete format.day;\n        delete format.month;\n        delete format.year;\n    }\n    if (options.showTime === false) {\n        delete format.hour;\n        delete format.minute;\n    }\n    if (today().year === value.year) {\n        delete format.year;\n    }\n    return value.setZone(options.tz || \"default\").toLocaleString(format);\n}\n\n/**\n * Converts a given duration in seconds into a human-readable format.\n *\n * The function takes a duration in seconds and converts it into a human-readable form,\n * such as \"1h\" or \"1 hour, 30 minutes\", depending on the value of the `showFullDuration` parameter.\n * If the `showFullDuration` is set to true, the function will display up to two non-zero duration\n * components in long form (e.g: hours, minutes).\n * Otherwise, it will show just the largest non-zero duration component in narrow form (e.g: y or h).\n * Luxon takes care of translations given the current locale.\n *\n * @param {number} seconds - The duration in seconds to be converted.\n * @param {boolean} showFullDuration - If true, the output will have two components in long form.\n * Otherwise, just one component will be displayed in narrow form.\n *\n * @returns {string} A human-readable string representation of the duration.\n *\n * @example\n * // Sample usage\n * const durationInSeconds = 7320; // 2 hours and 2 minutes (2 * 3600 + 2 * 60)\n * const fullDuration = humanizeDuration(durationInSeconds, true);\n * console.log(fullDuration); // Output: \"2 hours, 2 minutes\"\n *\n * const shortDuration = humanizeDuration(durationInSeconds, false);\n * console.log(shortDuration); // Output: \"2h\"\n */\nexport function formatDuration(seconds, showFullDuration) {\n    const displayStyle = showFullDuration ? \"long\" : \"narrow\";\n    const numberOfValuesToDisplay = showFullDuration ? 2 : 1;\n    const durationKeys = [\"years\", \"months\", \"days\", \"hours\", \"minutes\"];\n\n    if (seconds < 60) {\n        seconds = 60;\n    }\n    seconds -= seconds % 60;\n\n    let duration = luxon.Duration.fromObject({ seconds: seconds }).shiftTo(...durationKeys);\n    duration = duration.shiftTo(...durationKeys.filter((key) => duration.get(key)));\n    const durationSplit = duration.toHuman({ unitDisplay: displayStyle }).split(\",\");\n\n    if (!showFullDuration && duration.loc.locale.includes(\"en\") && duration.months > 0) {\n        durationSplit[0] = durationSplit[0].replace(\"m\", \"M\");\n    }\n    return durationSplit.slice(0, numberOfValuesToDisplay).join(\",\");\n}\n\n/**\n * Formats the given DateTime to the server date format.\n * @param {DateTime} value\n * @returns {string}\n */\nexport function serializeDate(value) {\n    if (!dateCache.has(value)) {\n        dateCache.set(value, value.toFormat(SERVER_DATE_FORMAT, { numberingSystem: \"latn\" }));\n    }\n    return dateCache.get(value);\n}\n\n/**\n * Formats the given DateTime to the server datetime format.\n * @param {DateTime} value\n * @returns {string}\n */\nexport function serializeDateTime(value) {\n    if (!dateTimeCache.has(value)) {\n        dateTimeCache.set(\n            value,\n            value.setZone(\"utc\").toFormat(SERVER_DATETIME_FORMAT, { numberingSystem: \"latn\" })\n        );\n    }\n    return dateTimeCache.get(value);\n}\n\n//-----------------------------------------------------------------------------\n// Parsing\n//-----------------------------------------------------------------------------\n\n/**\n * Parses a string value to a Luxon DateTime object.\n *\n * @param {string} value\n * @param {ConversionOptions} [options={}]\n *\n * @see parseDateTime (Note: since we're only interested by the date itself, the\n *  returned value will always be set at the start of the day)\n */\nexport function parseDate(value, options = {}) {\n    const parsed = parseDateTime(value, {\n        ...options,\n        format: options.format || localization.dateFormat,\n    });\n    return parsed && parsed.startOf(\"day\");\n}\n\n/**\n * Parses a string value to a Luxon DateTime object.\n *\n * @param {string} value value to parse.\n *  - Value can take the form of a smart date:\n *    e.g. \"+3w\" for three weeks from now.\n *    (`options.format` is ignored in this case)\n *\n *  - If value cannot be parsed within the provided format,\n *    ISO8601 and SQL formats are then tried. If these formats\n *    include a timezone information, the returned value will\n *    still be set to the user's timezone.\n *    e.g. \"2020-01-01T12:00:00+06:00\" with the user's timezone being UTC+1,\n *         the returned value will express the same timestamp but in UTC+1 (here time will be 7:00).\n *\n * @param {ConversionOptions} options\n *\n * @returns {NullableDateTime} Luxon DateTime object in user's timezone\n */\nexport function parseDateTime(value, options = {}) {\n    if (!value) {\n        return false;\n    }\n\n    const fmt = options.format || localization.dateTimeFormat;\n    const parseOpts = {\n        setZone: true,\n        zone: options.tz || \"default\",\n    };\n    const switchToLatin = Settings.defaultNumberingSystem !== \"latn\" && /[0-9]/.test(value);\n\n    // Force numbering system to latin if actual numbers are found in the value\n    if (switchToLatin) {\n        parseOpts.numberingSystem = \"latn\";\n    }\n\n    // Base case: try parsing with the given format and options\n    let result = DateTime.fromFormat(value, fmt, parseOpts);\n\n    // Try parsing as a smart date\n    if (!isValidDate(result)) {\n        result = parseSmartDateInput(value);\n    }\n\n    // Try parsing with partial date parts\n    if (!isValidDate(result)) {\n        const fmtWoZero = stripAlphaDupes(fmt);\n        result = DateTime.fromFormat(value, fmtWoZero, parseOpts);\n    }\n\n    // Try parsing with custom shorthand date parts\n    if (!isValidDate(result)) {\n        // Luxon is not permissive regarding delimiting characters in the format.\n        // So if the value to parse has less characters than the format, we would\n        // try to parse without the delimiting characters.\n        const digitList = value.split(nonDigitRegex).filter(Boolean);\n        const fmtList = fmt.split(nonAlphaRegex).filter(Boolean);\n        const valWoSeps = digitList.join(\"\");\n\n        // This is the weird part: we try to adapt the given format to comply with\n        // the amount of digits in the given value. To do this we split the format\n        // and the value on non-letter and non-digit characters respectively. This\n        // should create the same amount of grouping parameters, and the format\n        // groups are trimmed according to the length of their corresponding\n        // digit group. The 'carry' variable allows for the length of a digit\n        // group to overflow to the next format group. This is typically the case\n        // when the given value doesn't have non-digit separators and generates\n        // one big digit group instead.\n        let carry = 0;\n        const fmtWoSeps = fmtList\n            .map((part, i) => {\n                const digitLength = (digitList[i] || \"\").length;\n                const actualPart = part.slice(0, digitLength + carry);\n                carry += digitLength - actualPart.length;\n                return actualPart;\n            })\n            .join(\"\");\n\n        result = DateTime.fromFormat(valWoSeps, fmtWoSeps, parseOpts);\n    }\n\n    // Try with defaul ISO or SQL formats\n    if (!isValidDate(result)) {\n        // Also try some fallback formats, but only if value counts more than\n        // four digit characters as this could get misinterpreted as the time of\n        // the actual date.\n        const valueDigits = value.replace(nonDigitRegex, \"\");\n        if (valueDigits.length > 4) {\n            result = DateTime.fromISO(value, parseOpts); // ISO8601\n            if (!isValidDate(result)) {\n                result = DateTime.fromSQL(value, parseOpts); // last try: SQL\n            }\n        }\n    }\n\n    // No working parsing methods: throw an error\n    if (!isValidDate(result)) {\n        throw new ConversionError(_t(\"'%s' is not a correct date or datetime\", value));\n    }\n\n    // Revert to original numbering system\n    if (switchToLatin) {\n        result = result.reconfigure({\n            numberingSystem: Settings.defaultNumberingSystem,\n        });\n    }\n\n    return result.setZone(options.tz || \"default\");\n}\n\n/**\n * Returns a date object parsed from the given serialized string.\n * @param {string} value serialized date string, e.g. \"2018-01-01\"\n */\nexport function deserializeDate(value, options = {}) {\n    options = { numberingSystem: \"latn\", zone: \"default\", ...options };\n    return DateTime.fromSQL(value, options).reconfigure({\n        numberingSystem: Settings.defaultNumberingSystem,\n    });\n}\n\n/**\n * Returns a datetime object parsed from the given serialized string.\n * @param {string} value serialized datetime string, e.g. \"2018-01-01 00:00:00\", expressed in UTC\n */\nexport function deserializeDateTime(value, options = {}) {\n    return DateTime.fromSQL(value, { numberingSystem: \"latn\", zone: \"utc\" })\n        .setZone(options?.tz || \"default\")\n        .reconfigure({\n            numberingSystem: Settings.defaultNumberingSystem,\n        });\n}\n", "/**\n * @typedef Localization\n * @property {string} dateFormat\n * @property {string} dateTimeFormat\n * @property {string} timeFormat\n * @property {string} decimalPoint\n * @property {\"ltr\" | \"rtl\"} direction\n * @property {[number, number]} grouping\n * @property {boolean} multiLang\n * @property {string} thousandsSep\n * @property {number} weekStart\n * @property {string} code\n */\n\n/**\n * This is the main object holding user specific data about the localization. Its basically\n * the JS counterpart of the \"res.lang\" model.\n * It is useful to directly access those data anywhere, even outside Components.\n *\n * Important Note: its data are actually loaded by the localization_service,\n * so a code like the following would not work:\n *   import { localization } from \"@web/core/l10n/localization\";\n *   const dateFormat = localization.dateFormat; // dateFormat isn't set yet\n * @type {Localization}\n */\nexport const localization = new Proxy(\n    {},\n    {\n        get: (target, p) => {\n            // \"then\" can be called implicitly if the object is returned in an\n            // `async` function, so we need to allow it.\n            if (p in target || p === \"then\") {\n                return Reflect.get(target, p);\n            }\n            throw new Error(\n                `could not access localization parameter \"${p}\": parameters are not ready yet. Maybe add 'localization' to your dependencies?`\n            );\n        },\n    }\n);\n", "import { session } from \"@web/session\";\nimport { jsToPyLocale } from \"@web/core/l10n/utils\";\nimport { user } from \"@web/core/user\";\nimport { browser } from \"../browser/browser\";\nimport { registry } from \"../registry\";\nimport { strftimeToLuxonFormat } from \"./dates\";\nimport { localization } from \"./localization\";\nimport {\n    translatedTerms,\n    translatedTermsGlobal,\n    translationLoaded,\n    translationIsReady,\n} from \"./translation\";\nimport { objectToUrlEncodedString } from \"../utils/urls\";\nimport { IndexedDB } from \"../utils/indexed_db\";\n\nconst { Settings } = luxon;\n\n/** @type {[RegExp, string][]} */\nconst NUMBERING_SYSTEMS = [\n    [/^ar-(sa|sy|001)$/i, \"arab\"],\n    [/^bn/i, \"beng\"],\n    [/^bo/i, \"tibt\"],\n    // [/^fa/i, \"Farsi (Persian)\"], // No numberingSystem found in Intl\n    // [/^(hi|mr|ne)/i, \"Hindi\"], // No numberingSystem found in Intl\n    // [/^my/i, \"Burmese\"], // No numberingSystem found in Intl\n    [/^pa-in/i, \"guru\"],\n    [/^ta/i, \"tamldec\"],\n    [/.*/i, \"latn\"],\n];\n\nexport const localizationService = {\n    start: async () => {\n        const localizationDB = new IndexedDB(\"localization\", session.registry_hash);\n        const translationURL = session.translationURL || \"/web/webclient/translations\";\n        const lang = jsToPyLocale(user.lang || document.documentElement.getAttribute(\"lang\"));\n\n        const fetchTranslations = async (hash) => {\n            let queryString = objectToUrlEncodedString({ hash, lang });\n            queryString = queryString.length > 0 ? `?${queryString}` : queryString;\n            const response = await browser.fetch(`${translationURL}${queryString}`, {\n                cache: \"no-store\",\n            });\n            if (!response.ok) {\n                throw new Error(\"Error while fetching translations\");\n            }\n            const result = await response.json();\n            if (result.hash !== hash) {\n                localizationDB.write(translationURL, JSON.stringify({ lang }), result);\n                updateTranslations(result);\n            }\n        };\n\n        const updateTranslations = (result) => {\n            // Eventually, we want a new python route to return directly the good result.\n            const terms = {};\n            for (const addon of Object.keys(result.modules)) {\n                terms[addon] = {};\n                for (const message of result.modules[addon].messages) {\n                    terms[addon][message.id] = message.string;\n                    translatedTermsGlobal[message.id] = message.string;\n                }\n            }\n            Object.assign(translatedTerms, terms);\n\n            const userLocalization = result.lang_parameters;\n            const dateFormat = strftimeToLuxonFormat(userLocalization.date_format);\n            const timeFormat = strftimeToLuxonFormat(userLocalization.time_format);\n\n            Object.assign(localization, {\n                dateFormat,\n                timeFormat,\n                dateTimeFormat: `${dateFormat} ${timeFormat}`,\n                decimalPoint: userLocalization.decimal_point,\n                direction: userLocalization.direction,\n                grouping: JSON.parse(userLocalization.grouping),\n                multiLang: result.multi_lang,\n                thousandsSep: userLocalization.thousands_sep,\n                weekStart: userLocalization.week_start,\n            });\n        };\n\n        const storedTranslations = await localizationDB.read(\n            translationURL,\n            JSON.stringify({ lang })\n        );\n\n        const translationProm = fetchTranslations(storedTranslations?.hash);\n        if (storedTranslations) {\n            updateTranslations(storedTranslations);\n        } else {\n            await translationProm;\n        }\n\n        translatedTerms[translationLoaded] = true;\n        translationIsReady.resolve(true);\n\n        const locale = user.lang || browser.navigator.language;\n        Settings.defaultLocale = locale;\n        for (const [re, numberingSystem] of NUMBERING_SYSTEMS) {\n            if (re.test(locale)) {\n                Settings.defaultNumberingSystem = numberingSystem;\n                break;\n            }\n        }\n        localization.code = jsToPyLocale(locale);\n        return localization;\n    },\n};\n\nregistry.category(\"services\").add(\"localization\", localizationService);\n", "import { localization } from \"@web/core/l10n/localization\";\n\nconst { DateTime } = luxon;\n\nconst NUMERAL_MAPS = [\n    \"\u0660\u0661\u0662\u0663\u0664\u0665\u0666\u0667\u0668\u0669\", // Arabic\n    \"\u06f0\u06f1\u06f2\u06f3\u06f4\u06f5\u06f6\u06f7\u06f8\u06f9\",\n    \"\u0966\u0967\u0968\u0969\u096a\u096b\u096c\u096d\u096e\u096f\", // Devanagari (Hindi)\n    \"\u0e51\u0e52\u0e53\u0e54\u0e55\u0e56\u0e57\u0e58\u0e59\u0e50\", // Thai\n    \"\u96f6\u4e00\u4e8c\u4e09\u56db\u4e94\u516d\u4e03\u516b\u4e5d\", // Chinese/Japanese/Korean\n];\n\n/**\n * A representation of a specific time in a 24 hour format\n */\nexport class Time {\n    /**\n     * This method will return a Time object contructed\n     * differently depending on the type of {value}\n     *\n     * - If value is already a Time object, it returns it.\n     * - If value is null, undefined or false, it returns null.\n     * - If value is a string, it will try to parse it, @see {parseTime}\n     * - If value is an object, it will use its [hour], [minute] and [second] properties\n     * - Otherwise, return a new Time with default values\n     *\n     * @param {any} value\n     * @returns {Time|null}\n     */\n    static from(value) {\n        if (value === null || value === undefined || value === false) {\n            return null;\n        } else if (value instanceof Time) {\n            return value;\n        } else if (typeof value === \"string\") {\n            return parseTime(value, true);\n        } else if (typeof value === \"object\") {\n            return new Time(value);\n        } else {\n            return null;\n        }\n    }\n\n    /**\n     * @param {{\n     *  hour: 0,\n     *  minute: 0,\n     *  second: 0,\n     * }?} params\n     */\n    constructor({ hour = 0, minute = 0, second = 0 } = {}) {\n        /**@type {number} */\n        this.hour = hour;\n        /**@type {number} */\n        this.minute = minute;\n        /**@type {number} */\n        this.second = second;\n\n        /**\n         * @private\n         * @type {boolean}\n         */\n        this._is24HourFormat = is24HourFormat();\n\n        /**\n         * @private\n         * @type {boolean}\n         */\n        this._isMeridiemFormat = isMeridiemFormat();\n    }\n\n    /**\n     * @param {number} rounding\n     */\n    roundMinutes(rounding) {\n        this.minute = Math.round(this.minute / rounding) * rounding;\n    }\n\n    /**\n     * @returns {Time}\n     */\n    copy() {\n        return new Time(this);\n    }\n\n    /**\n     * @param {Time} other\n     * @param {boolean} [checkSeconds=false]\n     * @returns {boolean}\n     */\n    equals(other, checkSeconds = false) {\n        return (\n            other &&\n            this.hour === other.hour &&\n            this.minute === other.minute &&\n            (!checkSeconds || this.second === other.second)\n        );\n    }\n\n    /**\n     * Returns the formatted value of the time, with 24 of 12 hours\n     * format and with or without meridiems depending on the current\n     * localization time format.\n     *\n     * @param {boolean} [showSeconds=false]\n     * @returns {string}\n     */\n    toString(showSeconds = false) {\n        const hourFormat = this._is24HourFormat ? \"H\" : \"h\";\n        const secondFormat = showSeconds ? \":ss\" : \"\";\n        const meridiemFormat = this._isMeridiemFormat ? \"a\" : \"\";\n        return this.toDateTime()\n            .toFormat(`${hourFormat}:mm${secondFormat}${meridiemFormat}`)\n            .toLowerCase();\n    }\n\n    /**\n     * @returns {DateTime}\n     */\n    toDateTime() {\n        return DateTime.fromObject(this.toObject());\n    }\n\n    /**\n     * Returns the time as an Object\n     * @returns {{hour: number, minute: number, second: number}}\n     */\n    toObject() {\n        return {\n            hour: this.hour,\n            minute: this.minute,\n            second: this.second,\n        };\n    }\n}\n\n/**\n * Returns whether the given format is a 24-hour format.\n * Falls back to localization time format if none is given.\n *\n * @param {string} format\n */\nexport function is24HourFormat(format) {\n    return /H/.test(format || localization.timeFormat);\n}\n\n/**\n * Returns whether the given format uses a meridiem suffix (AM/PM).\n * Falls back to localization time format if none is given.\n *\n * @param {string} format\n */\nexport function isMeridiemFormat(format) {\n    return /a/.test(format || localization.timeFormat);\n}\n\n/**\n * Tries to parse a Time object from a time string\n * representation such as:\n * \"10:15\"  -> 10:15:00\n * \"2h5\"    -> 02:50:00\n * \"1015\"   -> 10:15:00\n * \"125\"    -> 12:50:00\n * \"315\"    -> 03:15:00\n * \"5:15pm\" -> 17:15:00\n *\n * Returns null if the value could not be parsed.\n *\n * @param {string} value\n * @param {boolean} [parseSeconds]\n * @returns {Time | null}\n */\nexport function parseTime(value, parseSeconds) {\n    const { isPm, isAm } = meridiemCheck(value);\n    value = normalizeTimeStr(value);\n\n    if (!value) {\n        return null;\n    }\n\n    let hour = 0;\n    let minute = 0;\n    let second = 0;\n\n    const parse = (str) => {\n        if (str.length === 0) {\n            return 0;\n        } else if (/^[\\d]+$/.test(str)) {\n            return parseInt(str, 10);\n        } else {\n            return NaN;\n        }\n    };\n\n    const parts = value.split(/[\\s:]/g);\n    if (parts.length > 3) {\n        return null;\n    } else if (parts.length === 3) {\n        if (!parseSeconds) {\n            return null;\n        }\n        hour = parse(parts[0]);\n        minute = parse(parts[1].padEnd(2, \"0\"));\n        second = parse(parts[2].padEnd(2, \"0\"));\n    } else if (parts.length === 2) {\n        hour = parse(parts[0]);\n        minute = parse(parts[1].padEnd(2, \"0\"));\n    } else if (parts.length === 1) {\n        const raw = parts[0];\n\n        const pickSolution = (...solutions) => {\n            for (const solution of solutions) {\n                const h = parse(solution[0]);\n                if (h <= 24) {\n                    hour = h;\n                    if (solution[1]) {\n                        minute = parse(solution[1].padEnd(2, \"0\"));\n                    }\n                    break;\n                }\n            }\n        };\n\n        if (raw.length == 1) {\n            hour = parse(raw);\n        } else if (raw.length == 2) {\n            pickSolution([raw], [raw[0], raw[1]]);\n        } else if (raw.length === 3) {\n            pickSolution([raw.slice(0, 2), raw[2]], [raw[0], raw.slice(1)]);\n        } else if (raw.length === 4) {\n            hour = parse(raw.slice(0, 2));\n            minute = parse(raw.slice(2));\n        } else if (raw.length > 4 && raw.length <= 6) {\n            if (!parseSeconds) {\n                return null;\n            }\n            hour = parse(raw.slice(0, 2));\n            minute = parse(raw.slice(2, 4));\n            second = parse(raw.slice(4).padEnd(2, \"0\"));\n        } else {\n            return null;\n        }\n    }\n\n    if (isPm && hour < 12) {\n        hour += 12;\n    } else if (isAm && hour === 12) {\n        hour = 0;\n    }\n\n    if (hour >= 0 && hour <= 24 && minute >= 0 && minute < 60 && second >= 0 && second < 60) {\n        if (hour === 24) {\n            hour = 0;\n        }\n        return new Time({ hour, minute, second });\n    } else {\n        return null;\n    }\n}\n\n/**\n * - Converts other languages numeral systems to western arabic numbers\n * - Replaces with \":\" all chains of non-numeric characters between numbers\n * - Removes all trailing non-numeric characters\n *\n * @param {string} timeStr\n * @returns {string|false}\n */\nfunction normalizeTimeStr(timeStr) {\n    if (typeof timeStr !== \"string\") {\n        return false;\n    }\n\n    timeStr = timeStr.trim().toLowerCase();\n\n    for (const map of NUMERAL_MAPS) {\n        for (let i = 0; i < map.length; i++) {\n            timeStr = timeStr.replaceAll(map[i], i);\n        }\n    }\n\n    return timeStr.replace(/^\\D+|\\D+$/g, \"\").replace(/\\D+/g, \":\");\n}\n\n/**\n * @param {string} timeStr\n * @returns {{ isPm: boolean, isAm: boolean }}\n */\nfunction meridiemCheck(timeStr) {\n    const amPmMatch = typeof timeStr === \"string\" ? timeStr.toLowerCase().match(/(am|pm)/g) : false;\n    return {\n        isPm: amPmMatch && amPmMatch[0] === \"pm\",\n        isAm: amPmMatch && amPmMatch[0] === \"am\",\n    };\n}\n", "import { markup } from \"@odoo/owl\";\n\nimport { formatList } from \"@web/core/l10n/utils\";\nimport { isIterable } from \"@web/core/utils/arrays\";\nimport { Deferred } from \"@web/core/utils/concurrency\";\nimport { htmlSprintf } from \"@web/core/utils/html\";\nimport { isObject } from \"@web/core/utils/objects\";\nimport { sprintf } from \"@web/core/utils/strings\";\n\nexport const translationLoaded = Symbol(\"translationLoaded\");\nexport const translatedTerms = {\n    [translationLoaded]: false,\n};\n/**\n * Contains all the translated terms. Unlike \"translatedTerms\", there is no\n * \"namespacing\" by module. It is used as a fallback when no translation is\n * found within the module's context, or when the context is not known.\n */\nexport const translatedTermsGlobal = {};\nexport const translationIsReady = new Deferred();\n\nconst Markup = markup().constructor;\n\n/**\n * Translates a term, or returns the term as it is if no translation can be\n * found.\n *\n * Extra positional arguments are inserted in place of %s placeholders.\n *\n * If the first extra argument is an object, the keys of that object are used to\n * map its entries to keyworded placeholders (%(kw_placeholder)s) for\n * replacement.\n *\n * If one or more of the extra arguments are iterables, they will be turned\n * into language-specific formatted strings representing the elements of the\n * list.\n *\n * If at least one of the extra arguments is a markup, the translation and\n * non-markup content are escaped, and the result is wrapped in a markup.\n *\n * @example\n * _t(\"Good morning\"); // \"Bonjour\"\n * _t(\"Good morning %s\", user.name); // \"Bonjour Marc\"\n * _t(\"Good morning %(newcomer)s, goodbye %(departer)s\", { newcomer: Marc, departer: Mitchel }); // Bonjour Marc, au revoir Mitchel\n * _t(\"I love %s\", markup`<blink>Minecraft</blink>`); // Markup {\"J'adore <blink>Minecraft</blink>\"}\n * _t(\"Good morning %s!\", [\"Mitchell\", \"Marc\", \"Louis\"]); // Bonjour Mitchell, Marc et Louis !\n *\n * @param {string} term\n * @returns {string|Markup|LazyTranslatedString}\n */\nexport function _t(term, ...values) {\n    if (translatedTerms[translationLoaded]) {\n        const translation = _getTranslation(term, odoo.translationContext);\n        if (values.length === 0) {\n            return translation;\n        }\n        return _safeFormatAndSprintf(translation, ...values);\n    } else {\n        return new LazyTranslatedString(term, values);\n    }\n}\n\nclass LazyTranslatedString extends String {\n    constructor(term, values) {\n        super(term);\n        this.translationContext = odoo.translationContext;\n        this.values = values;\n    }\n    valueOf() {\n        const term = super.valueOf();\n        if (translatedTerms[translationLoaded]) {\n            const translation = _getTranslation(term, this.translationContext);\n            if (this.values.length === 0) {\n                return translation;\n            }\n            return _safeFormatAndSprintf(translation, ...this.values);\n        } else {\n            throw new Error(`translation error`);\n        }\n    }\n    toString() {\n        return this.valueOf();\n    }\n}\n\n/**\n * Load the installed languages long names and code\n *\n * The result of the call is put in cache.\n * If any new language is installed, a full page refresh will happen,\n * so there is no need invalidate it.\n */\nexport async function loadLanguages(orm) {\n    if (!loadLanguages.installedLanguages) {\n        loadLanguages.installedLanguages = await orm.call(\"res.lang\", \"get_installed\");\n    }\n    return loadLanguages.installedLanguages;\n}\n\nfunction _getTranslation(sourceTerm, ctx) {\n    return translatedTerms[ctx]?.[sourceTerm] ?? translatedTermsGlobal[sourceTerm] ?? sourceTerm;\n}\n\n/**\n * Same behavior as sprintf, but doing two additional things:\n * - If any of the provided values is an iterable, it will format its items\n *   as a language-specific formatted string representing the elements of the\n *   list.\n * - If any of the provided values is a markup, it will escape all non-markup\n *   content before performing the interpolation, then wraps the result in a\n *   markup.\n *\n * @param {string} str The string with placeholders (%s) to insert values into.\n * @param  {...any} values Primitive values to insert in place of placeholders.\n * @returns {string|Markup}\n */\nfunction _safeFormatAndSprintf(str, ...values) {\n    let hasMarkup = false;\n    let valuesObject = values;\n    if (values.length === 1 && isObject(values[0])) {\n        valuesObject = values[0];\n    }\n    for (const [key, value] of Object.entries(valuesObject)) {\n        // The `!(value instanceof String)` check is to prevent interpreting `Markup` and `LazyTranslatedString`\n        // objects as iterables, since they are both subclasses of `String`.\n        if (isIterable(value) && !(value instanceof String)) {\n            valuesObject[key] = formatList(value);\n        }\n        hasMarkup ||= value instanceof Markup;\n    }\n    if (hasMarkup) {\n        return htmlSprintf(str, ...values);\n    }\n    return sprintf(str, ...values);\n}\n\n/**\n * This is a wrapper for _t that the transpiler injects in its place\n * to provide the knowledge of the module from which it was called.\n *\n * Providing the context of the module is useful to avoid conflicting\n * translations, e.g. \"table\" has a different meaning depending on the module:\n * the table of a restaurant (POS module) vs. a spreadsheet table.\n *\n * @param {string} str The term to translate\n * @param {string} moduleName The name of the module, used as a context key to\n * retrieve the translation.\n * @param  {...any} args The other arguments passed to _t.\n */\nexport function appTranslateFn(str, moduleName, ...args) {\n    odoo.translationContext = moduleName;\n    const translatedTerm = _t(str, ...args);\n    odoo.translationContext = null;\n    return translatedTerm;\n}\n", "export * from \"@web/core/l10n/utils/format_list\";\nexport * from \"@web/core/l10n/utils/locales\";\nexport * from \"@web/core/l10n/utils/normalize\";\n", "import { user } from \"@web/core/user\";\n\n/**\n * Convert Unicode TR35-49 list pattern types to ES Intl.ListFormat options\n */\nconst LIST_STYLES = {\n    standard: {\n        type: \"conjunction\",\n        style: \"long\",\n    },\n    \"standard-short\": {\n        type: \"conjunction\",\n        style: \"short\",\n    },\n    or: {\n        type: \"disjunction\",\n        style: \"long\",\n    },\n    \"or-short\": {\n        type: \"disjunction\",\n        style: \"short\",\n    },\n    unit: {\n        type: \"unit\",\n        style: \"long\",\n    },\n    \"unit-short\": {\n        type: \"unit\",\n        style: \"short\",\n    },\n    \"unit-narrow\": {\n        type: \"unit\",\n        style: \"narrow\",\n    },\n};\n\n/**\n * Format the items in `list` as a list in a locale-dependent manner with the chosen style.\n *\n * The available styles are defined in the Unicode TR35-49 spec:\n * * standard:\n *   A typical \"and\" list for arbitrary placeholders.\n *   e.g. \"January, February, and March\"\n * * standard-short:\n *   A short version of an \"and\" list, suitable for use with short or abbreviated placeholder values.\n *   e.g. \"Jan., Feb., and Mar.\"\n * * or:\n *   A typical \"or\" list for arbitrary placeholders.\n *   e.g. \"January, February, or March\"\n * * or-short:\n *   A short version of an \"or\" list.\n *   e.g. \"Jan., Feb., or Mar.\"\n * * unit:\n *   A list suitable for wide units.\n *   e.g. \"3 feet, 7 inches\"\n * * unit-short:\n *   A list suitable for short units\n *   e.g. \"3 ft, 7 in\"\n * * unit-narrow:\n *   A list suitable for narrow units, where space on the screen is very limited.\n *   e.g. \"3\u2032 7\u2033\"\n *\n * See https://www.unicode.org/reports/tr35/tr35-49/tr35-general.html#ListPatterns for more details.\n *\n * @param {string[]} list The array of values to format into a list.\n * @param {Object} [param0]\n * @param {string} [param0.localeCode] The locale to use (e.g. en-US).\n * @param {\"standard\"|\"standard-short\"|\"or\"|\"or-short\"|\"unit\"|\"unit-short\"|\"unit-narrow\"} [param0.style=\"standard\"] The style to format the list with.\n * @returns {string} The formatted list.\n */\nexport function formatList(list, { localeCode = \"\", style = \"standard\" } = {}) {\n    const locale = localeCode || user.lang || \"en-US\";\n    const formatter = new Intl.ListFormat(locale, LIST_STYLES[style]);\n    return formatter.format(Array.from(list, String));\n}\n", "/**\n * Converts a locale from JavaScript to Python format.\n *\n * Most of the time the conversion is simply to replace - with _.\n * Example: fr-BE \u2192 fr_BE\n *\n * Exceptions:\n *  - Serbian can be written in both Latin and Cyrillic scripts interchangeably,\n *  therefore its locale includes a special modifier to indicate which script to\n *  use. Example: sr-Latn \u2192 sr@latin\n *  - Tagalog/Filipino: The \"fil\" locale is replaced by \"tl\" for compatibility\n *  with the Python side (where the \"fil\" locale doesn't exist).\n *\n * BCP 47 (JS):\n *  language[-extlang][-script][-region][-variant][-extension][-privateuse]\n *  https://www.ietf.org/rfc/rfc5646.txt\n * XPG syntax (Python):\n *  language[_territory][.codeset][@modifier]\n *  https://www.gnu.org/software/libc/manual/html_node/Locale-Names.html\n *\n * @param {string} locale The locale formatted for use on the JavaScript-side.\n * @returns {string} The locale formatted for use on the Python-side.\n */\nexport function jsToPyLocale(locale) {\n    if (!locale) {\n        return \"\";\n    }\n    try {\n        var { language, script, region } = new Intl.Locale(locale);\n        // new Intl.Locale(\"tl-PH\") produces fil-PH, which one might not expect\n        if (language === \"fil\") {\n            language = \"tl\";\n        }\n    } catch {\n        return locale;\n    }\n    let xpgLocale = language;\n    if (region) {\n        xpgLocale += `_${region}`;\n    }\n    switch (script) {\n        case \"Cyrl\":\n            xpgLocale += \"@Cyrl\";\n            break;\n        case \"Latn\":\n            xpgLocale += \"@latin\";\n            break;\n    }\n    return xpgLocale;\n}\n\n/**\n * Converts a locale from Python to JavaScript format.\n *\n * Most of the time the conversion is simply to replace _ with -.\n * Example: fr_BE \u2192 fr-BE\n *\n * Exception: Serbian can be written in both Latin and Cyrillic scripts\n * interchangeably, therefore its locale includes a special modifier\n * to indicate which script to use.\n * Example: sr@latin \u2192 sr-Latn\n *\n * BCP 47 (JS):\n *  language[-extlang][-script][-region][-variant][-extension][-privateuse]\n *  https://www.ietf.org/rfc/rfc5646.txt\n * XPG syntax (Python):\n *  language[_territory][.codeset][@modifier]\n *  https://www.gnu.org/software/libc/manual/html_node/Locale-Names.html\n *\n * @param {string} locale The locale formatted for use on the Python-side.\n * @returns {string} The locale formatted for use on the JavaScript-side.\n */\nexport function pyToJsLocale(locale) {\n    if (!locale) {\n        return \"\";\n    }\n    const regex = /^([a-z]+)(_[A-Z\\d]+)?(@.+)?$/;\n    const match = locale.match(regex);\n    if (!match) {\n        return locale;\n    }\n    const [, language, territory, modifier] = match;\n    const subtags = [language];\n    switch (modifier) {\n        case \"@Cyrl\":\n            subtags.push(\"Cyrl\");\n            break;\n        case \"@latin\":\n            subtags.push(\"Latn\");\n            break;\n    }\n    if (territory) {\n        subtags.push(territory.slice(1));\n    }\n    return subtags.join(\"-\");\n}\n", "/**\n * Normalizes a string for use in comparison.\n *\n * @example\n * normalize(\"d\u00e9\u00e7\u00fbmes\") === normalize(\"DECUMES\")\n * normalize(\"\ud835\udd16\ud835\udd25\ud835\udd2f\ud835\udd22\ud835\udd28\") === normalize(\"Shrek\")\n * normalize(\"Scle\u00dfin\") === normalize(\"Sclessin\")\n * normalize(\"\u0152dipe\") === normalize(\"OeDiPe\")\n *\n * @param {string} str\n * @returns {string}\n */\nexport function normalize(str) {\n    return casefold(unaccent(expandLigatures(str.normalize(\"NFKC\"))));\n}\n\n/**\n * Searches for \"substr\" in \"src\". The search is performed on normalized strings\n * so that \"ce\" can match \"C\u00e9dric\".\n *\n * @param {string} src\n * @param {string} substr\n * @returns {{match: string, start: number, end: number}}\n */\nexport function normalizedMatch(src, substr) {\n    if (!substr) {\n        return { start: 0, end: 0, match: \"\" };\n    }\n    /**\n     * Array.from splits the string into an array of codepoints. This avoids\n     * processing unpaired surrogates, which could lead to unexpected results.\n     *\n     * \"\ud835\udd16\"[0];              // \"\\ud835\" \u2190 unpaired surrogate!!\n     * Array.from(\"\ud835\udd16\")[0];  // \"\ud835\udd16\"\n     *\n     * \"\ud835\udd16\".split(\"\");   // Array [ \"\\ud835\", \"\\udd16\" ]\n     * Array.from(\"\ud835\udd16\"); // Array [ \"\ud835\udd16\" ]\n     *\n     * \"\ud835\udd16\".split(\"\").map((c) => c.normalize(\"NFKC\")).join(\"\");      // \"\ud835\udd16\"\n     * Array.from(\"\ud835\udd16\").map((c) => c.normalize(\"NFKC\")).join(\"\");    // \"S\"\n     */\n    const srcAsCodepoints = Array.from(src);\n    /**\n     * Instead of calling normalize directly on the source string, the source is\n     * split into an array of codepoints, where each of the elements is\n     * normalized individually. This is because this function is expected to\n     * return the start and end indexes of the match in the *original*,\n     * unnormalized string, but strings can grow in length during normalization,\n     * which would alter the indexes. Now, even if the length of the individual\n     * elements grows, the length of the containing array remains the same.\n     */\n    const normalizedSrc = srcAsCodepoints.map(normalize);\n    const normalizedSubstr = Array.from(normalize(substr));\n    /**\n     * normalizedSrc can contain empty strings if the source is an NFD string,\n     * corresponding to diacritics that have been stripped off. They must be\n     * taken into account in the length calculation to get the indexes right,\n     * hence Math.max(x.length, 1).\n     */\n    const flattenSrcLength = normalizedSrc.reduce((acc, x) => acc + Math.max(x.length, 1), 0);\n    for (let i = 0; i <= flattenSrcLength - normalizedSubstr.length; ++i) {\n        const substrStack = Array.from(normalizedSubstr).reverse();\n        for (let j = 0; i + j < normalizedSrc.length; ++j) {\n            const current = normalizedSrc[i + j];\n            // \"every\" in case normalization expanded current to several chars\n            if (![...current].every((c) => substrStack.length === 0 || c === substrStack.pop())) {\n                break;\n            }\n            if (substrStack.length === 0) {\n                // full substring matched, return the result \ud83d\ude24\n                const start = srcAsCodepoints.slice(0, i).join(\"\").length;\n                const match = srcAsCodepoints.slice(i, i + j + 1).join(\"\");\n                const end = start + match.length;\n                return { start, end, match };\n            }\n        }\n    }\n    return { start: -1, end: -1, match: \"\" };\n}\n\n/**\n * Searches for \"substr\" in \"src\" as is done in normalizedMatch\n * but returns an array of all successful matches\n *\n * @param {string} src\n * @param {string} substr\n * @returns {Array<{match: string, start: number, end: number}>}\n */\nexport function normalizedMatches(src, substr) {\n    const matches = [];\n    let index = 0;\n    while (src.length) {\n        const { start, end, match } = normalizedMatch(src, substr);\n        if (match) {\n            matches.push({ start: index + start, end: index + end, match });\n            index += end;\n            src = src.slice(end);\n        } else {\n            break;\n        }\n    }\n    return matches;\n}\n\nconst DECOMPOSITION_BY_LIGATURE = new Map([\n    [\"\u00c6\", \"Ae\"], // Danish, Norwegian, Icelandic, French (rare)...\n    [\"\u00e6\", \"ae\"],\n    [\"\u0152\", \"Oe\"], // French: \"Richard C\u0153ur de Lion\"\n    [\"\u0153\", \"oe\"],\n    [\"\u0132\", \"IJ\"], // Dutch: \"IJzer\"\n    [\"\u0133\", \"ij\"],\n]);\n\n/**\n * Splits ligatures into their constituent glyphs, e.g. turns \u0152 into Oe.\n *\n * @param {string} str\n * @returns {string}\n */\nfunction expandLigatures(str) {\n    return Array.from(str, (char) => DECOMPOSITION_BY_LIGATURE.get(char) ?? char).join(\"\");\n}\n\n/**\n * Diacritics are marks, such as accents or cedilla, that when added to a letter\n * change its pronunciation or meaning. Unicode has a category for them, but it\n * doesn't consider characters like \"\u00f8\" to be a diacritical \"o\". Below is a list\n * of characters that could be considered \"diacritical characters\" but aren't\n * labeled as such by Unicode.\n */\nconst DIACRITIC_LIKES = new Map([\n    [\"\u00d8\", \"O\"], // notably used in Danish and Norwegian: \"J\u00f8rgen\"\n    [\"\u00f8\", \"o\"],\n    [\"\u0141\", \"L\"], // notably used in Polish: \"Pawe\u0142\"\n    [\"\u0142\", \"l\"],\n    [\"\u00d0\", \"D\"], // Icelandic, \"Borgarfj\u00f6r\u00f0ur\"\n    [\"\u00f0\", \"d\"],\n    [\"\u0126\", \"H\"], // Maltese, \"\u0126amrun Spartans Football Club\"\n    [\"\u0127\", \"h\"],\n    [\"\u0166\", \"T\"], // apparently used in S\u00e1mi languages, very few speakers\n    [\"\u0167\", \"t\"],\n]);\n\n/**\n * Removes \"diacritics\" (funny marks added to letters, such as accents and\n * cedillas) from a string.\n *\n * @param {string} str\n * @returns {string}\n */\nfunction unaccent(str) {\n    return Array.from(\n        str.normalize(\"NFD\").replace(/\\p{Nonspacing_Mark}/gu, \"\"),\n        (char) => DIACRITIC_LIKES.get(char) ?? char\n    ).join(\"\");\n}\n\n/**\n * Normalizes string case for use in comparison.\n *\n * Some characters change length when converted from one case to another. A\n * common example is the German letter \"\u00df,\" which becomes \"SS\" when uppercased.\n * This function ensures that these special cases are handled correctly.\n *\n * \u26a0 Doesn't preserve \"Turkish I\"s.\n *\n * @see https://www.w3.org/TR/charmod-norm/#definitionCaseFolding\n * @see https://www.unicode.org/Public/UNIDATA/CaseFolding.txt\n *\n * @example\n * casefold(\"AAAAAAAA\")                 // \"aaaaaaaa\"\n * casefold(\"\u0587\")                        // \"\u0535\u0552\"\n * casefold(\"Kevin Gro\u00dfkreutz\")         // \"kevin grosskreutz\"\n * casefold(\"Diyarbak\u0131r\")               // \"diyarbakir\"\n * casefold(\"\u00df\") !== \"\u00df\".toLowerCase()  // true\n * casefold(\"\u00df\") === casefold(\"SS\")     // true\n *\n * @param {string} str\n * @returns {string} lowercase string after \"full case folding\"\n */\nfunction casefold(str) {\n    return str.toLowerCase().toUpperCase().toLowerCase();\n}\n", "import { isVisible } from \"@web/core/utils/ui\";\nimport { delay } from \"@web/core/utils/concurrency\";\nimport { validate } from \"@odoo/owl\";\n\nconst macroSchema = {\n    name: { type: String, optional: true },\n    timeout: { type: Number, optional: true },\n    steps: {\n        type: Array,\n        element: {\n            type: Object,\n            shape: {\n                action: { type: [Function, String], optional: true },\n                timeout: { type: Number, optional: true },\n                trigger: { type: [Function, String], optional: true },\n            },\n            validate: (step) => step.action || step.trigger,\n        },\n    },\n    onComplete: { type: Function, optional: true },\n    onStep: { type: Function, optional: true },\n    onError: { type: Function, optional: true },\n};\n\nclass MacroError extends Error {\n    constructor(type, message, options) {\n        super(message, options);\n        this.type = type;\n    }\n}\n\nasync function performAction(trigger, action) {\n    if (!action) {\n        return;\n    }\n    try {\n        return await action(trigger);\n    } catch (error) {\n        throw new MacroError(\n            \"Action\",\n            error.stack || `ERROR during perform action: ${error.message}`,\n            {cause: error}\n        );\n    }\n}\n\nasync function waitForTrigger(trigger) {\n    if (!trigger) {\n        return;\n    }\n    try {\n        await delay(50);\n        return await waitUntil(() => {\n            if (typeof trigger === \"function\") {\n                return trigger();\n            } else if (typeof trigger === \"string\") {\n                const triggerEl = document.querySelector(trigger);\n                return isVisible(triggerEl) && triggerEl;\n            }\n        });\n    } catch (error) {\n        throw new MacroError(\"Trigger\", `ERROR during find trigger:\\n${error.message}`, {\n            cause: error,\n        });\n    }\n}\n\nexport async function waitUntil(predicate) {\n    const result = predicate();\n    if (result) {\n        return Promise.resolve(result);\n    }\n    let handle;\n    return new Promise((resolve) => {\n        const runCheck = () => {\n            const result = predicate();\n            if (result) {\n                resolve(result);\n            }\n            handle = requestAnimationFrame(runCheck);\n        };\n        handle = requestAnimationFrame(runCheck);\n    }).finally(() => {\n        cancelAnimationFrame(handle);\n    });\n}\n\nexport class Macro {\n    currentIndex = 0;\n    isComplete = false;\n    constructor(descr) {\n        try {\n            validate(descr, macroSchema);\n        } catch (error) {\n            throw new Error(\n                `Error in schema for Macro ${JSON.stringify(descr, null, 4)}\\n${error.message}`\n            );\n        }\n        Object.assign(this, descr);\n        this.onComplete = this.onComplete || (() => {});\n        this.onStep = this.onStep || (() => {});\n        this.onError =\n            this.onError ||\n            ((error, step, index) => {\n                console.error(error.message, step, index);\n            });\n    }\n\n    async start() {\n        await this.advance();\n    }\n\n    async advance() {\n        if (this.isComplete || this.currentIndex >= this.steps.length) {\n            this.stop();\n            return;\n        }\n        try {\n            const step = this.steps[this.currentIndex];\n            const timeoutDelay = step.timeout || this.timeout || 10000;\n            const executeStep = async () => {\n                const trigger = await waitForTrigger(step.trigger);\n                const result = await performAction(trigger, step.action);\n                await this.onStep({ step, trigger, index: this.currentIndex });\n                return result;\n            };\n            const launchTimer = async () => {\n                await delay(timeoutDelay);\n                throw new MacroError(\n                    \"Timeout\",\n                    `TIMEOUT step failed to complete within ${timeoutDelay} ms.`\n                );\n            };\n            // If falsy action result, it means the action worked properly.\n            // So we can proceed to the next step.\n            const actionResult = await Promise.race([executeStep(), launchTimer()]);\n            if (actionResult) {\n                this.stop();\n                return;\n            }\n        } catch (error) {\n            this.stop(error);\n            return;\n        }\n        this.currentIndex++;\n        await this.advance();\n    }\n\n    stop(error) {\n        if (this.isComplete) {\n            return;\n        }\n        this.isComplete = true;\n        if (error) {\n            const step = this.steps[this.currentIndex];\n            this.onError({ error, step, index: this.currentIndex });\n        } else if (this.currentIndex === this.steps.length) {\n            this.onComplete();\n        }\n    }\n}\n\nexport class MacroMutationObserver {\n    observerOptions = {\n        attributes: true,\n        childList: true,\n        subtree: true,\n        characterData: true,\n    };\n    constructor(callback) {\n        this.callback = callback;\n        this.observer = new MutationObserver((mutationList, observer) => {\n            callback(mutationList);\n            mutationList.forEach((mutationRecord) =>\n                Array.from(mutationRecord.addedNodes).forEach((node) => {\n                    let iframes = [];\n                    if (String(node.tagName).toLowerCase() === \"iframe\") {\n                        iframes = [node];\n                    } else if (node instanceof HTMLElement) {\n                        iframes = Array.from(node.querySelectorAll(\"iframe\"));\n                    }\n                    iframes.forEach((iframeEl) =>\n                        this.observeIframe(iframeEl, observer, () => callback())\n                    );\n                    this.findAllShadowRoots(node).forEach((shadowRoot) =>\n                        observer.observe(shadowRoot, this.observerOptions)\n                    );\n                })\n            );\n        });\n    }\n    disconnect() {\n        this.observer.disconnect();\n    }\n    findAllShadowRoots(node, shadowRoots = []) {\n        if (node.shadowRoot) {\n            shadowRoots.push(node.shadowRoot);\n            this.findAllShadowRoots(node.shadowRoot, shadowRoots);\n        }\n        node.childNodes.forEach((child) => {\n            this.findAllShadowRoots(child, shadowRoots);\n        });\n        return shadowRoots;\n    }\n    observe(target) {\n        this.observer.observe(target, this.observerOptions);\n        //When iframes already exist at \"this.target\" initialization\n        target\n            .querySelectorAll(\"iframe\")\n            .forEach((el) => this.observeIframe(el, this.observer, () => this.callback()));\n        //When shadowDom already exist at \"this.target\" initialization\n        this.findAllShadowRoots(target).forEach((shadowRoot) => {\n            this.observer.observe(shadowRoot, this.observerOptions);\n        });\n    }\n    observeIframe(iframeEl, observer, callback) {\n        const observerOptions = {\n            attributes: true,\n            childList: true,\n            subtree: true,\n            characterData: true,\n        };\n        const observeIframeContent = () => {\n            if (iframeEl.contentDocument) {\n                iframeEl.contentDocument.addEventListener(\"load\", (event) => {\n                    callback();\n                    observer.observe(event.target, observerOptions);\n                });\n                if (!iframeEl.src || iframeEl.contentDocument.readyState === \"complete\") {\n                    callback();\n                    observer.observe(iframeEl.contentDocument, observerOptions);\n                }\n            }\n        };\n        observeIframeContent();\n        iframeEl.addEventListener(\"load\", observeIframeContent);\n    }\n}\n", "import { Component, xml } from \"@odoo/owl\";\nimport { registry } from \"@web/core/registry\";\nimport { useRegistry } from \"@web/core/registry_hook\";\nimport { ErrorHandler } from \"@web/core/utils/components\";\n\nconst mainComponents = registry.category(\"main_components\");\n\nmainComponents.addValidation({\n    Component: { validate: (c) => c.prototype instanceof Component },\n    props: { type: Object, optional: true }\n});\n\nexport class MainComponentsContainer extends Component {\n    static components = { ErrorHandler };\n    static props = {};\n    static template = xml`\n    <div class=\"o-main-components-container\">\n        <t t-foreach=\"Components.entries\" t-as=\"C\" t-key=\"C[0]\">\n            <ErrorHandler onError=\"error => this.handleComponentError(error, C)\">\n                <t t-component=\"C[1].Component\" t-props=\"C[1].props\"/>\n            </ErrorHandler>\n        </t>\n    </div>\n    `;\n\n    setup() {\n        this.Components = useRegistry(mainComponents);\n    }\n\n    handleComponentError(error, C) {\n        // remove the faulty component and rerender without it\n        this.Components.entries.splice(this.Components.entries.indexOf(C), 1);\n        this.render();\n        /**\n         * we rethrow the error to notify the user something bad happened.\n         * We do it after a tick to make sure owl can properly finish its\n         * rendering\n         */\n        Promise.resolve().then(() => {\n            throw error;\n        });\n    }\n}\n", "import { Component, onWillStart, onWillUpdateProps, useState } from \"@odoo/owl\";\nimport { usePopover } from \"@web/core/popover/popover_hook\";\nimport { KeepLast } from \"@web/core/utils/concurrency\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { ModelFieldSelectorPopover } from \"./model_field_selector_popover\";\n\nexport class ModelFieldSelector extends Component {\n    static template = \"web._ModelFieldSelector\";\n    static components = {\n        Popover: ModelFieldSelectorPopover,\n    };\n    static props = {\n        resModel: String,\n        path: { optional: true },\n        allowEmpty: { type: Boolean, optional: true },\n        readonly: { type: Boolean, optional: true },\n        readProperty: { type: Boolean, optional: true },\n        showSearchInput: { type: Boolean, optional: true },\n        isDebugMode: { type: Boolean, optional: true },\n        update: { type: Function, optional: true },\n        filter: { type: Function, optional: true },\n        sort: { type: Function, optional: true },\n        followRelations: { type: Boolean, optional: true },\n        showDebugInput: { type: Boolean, optional: true },\n    };\n    static defaultProps = {\n        readonly: true,\n        allowEmpty: false,\n        isDebugMode: false,\n        showSearchInput: true,\n        update: () => {},\n        followRelations: true,\n    };\n\n    setup() {\n        this.fieldService = useService(\"field\");\n        this.popover = usePopover(this.constructor.components.Popover, {\n            popoverClass: \"o_popover_field_selector\",\n            onClose: async () => {\n                if (this.newPath !== null) {\n                    const fieldInfo = await this.fieldService.loadFieldInfo(\n                        this.props.resModel,\n                        this.newPath\n                    );\n                    this.props.update(this.newPath, fieldInfo);\n                }\n            },\n        });\n        this.keepLast = new KeepLast();\n        this.state = useState({ isInvalid: false, displayNames: [] });\n        onWillStart(() => this.updateState(this.props));\n        onWillUpdateProps((nextProps) => this.updateState(nextProps));\n    }\n\n    openPopover(currentTarget) {\n        if (this.props.readonly) {\n            return;\n        }\n        this.newPath = null;\n        this.popover.open(currentTarget, {\n            resModel: this.props.resModel,\n            path: this.props.path,\n            readProperty: this.props.readProperty,\n            update: (path, _fieldInfo, debug = false) => {\n                this.newPath = path;\n                if (!debug) {\n                    this.updateState({ ...this.props, path }, true);\n                }\n            },\n            showSearchInput: this.props.showSearchInput,\n            isDebugMode: this.props.isDebugMode,\n            filter: this.props.filter,\n            sort: this.props.sort,\n            followRelations: this.props.followRelations,\n            showDebugInput: this.props.showDebugInput,\n        });\n    }\n\n    async updateState(params, isConcurrent) {\n        const { resModel, path, allowEmpty } = params;\n        let prom = this.fieldService.loadPathDescription(resModel, path, allowEmpty);\n        if (isConcurrent) {\n            prom = this.keepLast.add(prom);\n        }\n        const state = await prom;\n        Object.assign(this.state, state);\n    }\n\n    clear() {\n        if (this.popover.isOpen) {\n            this.newPath = \"\";\n            this.popover.close();\n            return;\n        }\n        this.props.update(\"\", { resModel: this.props.resModel, fieldDef: null });\n    }\n}\n", "import { Component, onWillStart, useEffect, useRef, useState } from \"@odoo/owl\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { sortBy } from \"@web/core/utils/arrays\";\nimport { KeepLast } from \"@web/core/utils/concurrency\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { fuzzyLookup } from \"@web/core/utils/search\";\nimport { debounce } from \"@web/core/utils/timing\";\n\nclass Page {\n    constructor(resModel, fieldDefs, options = {}) {\n        this.resModel = resModel;\n        this.fieldDefs = fieldDefs;\n        const {\n            previousPage = null,\n            selectedName = null,\n            isDebugMode,\n            readProperty = false,\n            sortFn = (fieldDefs) => sortBy(Object.keys(fieldDefs), (key) => fieldDefs[key].string),\n        } = options;\n        this.previousPage = previousPage;\n        this.selectedName = selectedName;\n        this.isDebugMode = isDebugMode;\n        this.readProperty = readProperty;\n        this.sortedFieldNames = sortFn(fieldDefs);\n        this.fieldNames = this.sortedFieldNames;\n        this.query = \"\";\n        this.focusedFieldName = null;\n        this.resetFocusedFieldName();\n    }\n\n    get path() {\n        const previousPath = this.previousPage?.path || \"\";\n        const name = this.selectedName;\n\n        if (this.readProperty && this.selectedField && this.selectedField.is_property) {\n            if (this.selectedField.relation) {\n                return `${previousPath}.get('${name}', env['${this.selectedField.relation}'])`;\n            }\n            return `${previousPath}.get('${name}')`;\n        }\n        if (name) {\n            if (previousPath) {\n                return `${previousPath}.${name}`;\n            }\n            return name;\n        }\n        return previousPath;\n    }\n\n    get selectedField() {\n        return this.fieldDefs[this.selectedName];\n    }\n\n    get title() {\n        const prefix = this.previousPage?.previousPage ? \"... > \" : \"\";\n        const title = this.previousPage?.selectedField?.string || \"\";\n        if (prefix.length || title.length) {\n            return `${prefix}${title}`;\n        }\n        return _t(\"Select a field\");\n    }\n\n    focus(direction) {\n        if (!this.fieldNames.length) {\n            return;\n        }\n        const index = this.fieldNames.indexOf(this.focusedFieldName);\n        if (direction === \"previous\") {\n            if (index === 0) {\n                this.focusedFieldName = this.fieldNames[this.fieldNames.length - 1];\n            } else {\n                this.focusedFieldName = this.fieldNames[index - 1];\n            }\n        } else {\n            if (index === this.fieldNames.length - 1) {\n                this.focusedFieldName = this.fieldNames[0];\n            } else {\n                this.focusedFieldName = this.fieldNames[index + 1];\n            }\n        }\n    }\n\n    resetFocusedFieldName() {\n        if (this.selectedName && this.fieldNames.includes(this.selectedName)) {\n            this.focusedFieldName = this.selectedName;\n        } else {\n            this.focusedFieldName = this.fieldNames.length ? this.fieldNames[0] : null;\n        }\n    }\n\n    searchFields(query = \"\") {\n        this.query = query;\n        this.fieldNames = this.sortedFieldNames;\n        if (query) {\n            this.fieldNames = fuzzyLookup(query, this.fieldNames, (key) => {\n                const vals = [this.fieldDefs[key].string];\n                if (this.isDebugMode) {\n                    vals.push(key);\n                }\n                return vals;\n            });\n        }\n        this.resetFocusedFieldName();\n    }\n}\n\nexport class ModelFieldSelectorPopover extends Component {\n    static template = \"web.ModelFieldSelectorPopover\";\n    static props = {\n        close: Function,\n        filter: { type: Function, optional: true },\n        sort: { type: Function, optional: true },\n        followRelations: { type: Boolean, optional: true },\n        showDebugInput: { type: Boolean, optional: true },\n        isDebugMode: { type: Boolean, optional: true },\n        path: { optional: true },\n        readProperty: { type: Boolean, optional: true },\n        resModel: String,\n        showSearchInput: { type: Boolean, optional: true },\n        update: Function,\n    };\n    static defaultProps = {\n        filter: (value) => value.searchable && value.type != \"json\" && value.type !== \"separator\",\n        isDebugMode: false,\n        followRelations: true,\n    };\n\n    setup() {\n        this.fieldService = useService(\"field\");\n        this.state = useState({ page: null });\n        this.keepLast = new KeepLast();\n        this.debouncedSearchFields = debounce(this.searchFields.bind(this), 250);\n\n        onWillStart(async () => {\n            this.state.page = await this.loadPages(this.props.resModel, this.props.path);\n        });\n\n        const rootRef = useRef(\"root\");\n        useEffect(() => {\n            const focusedElement = rootRef.el.querySelector(\n                \".o_model_field_selector_popover_item.active\"\n            );\n            if (focusedElement) {\n                // current page can be empty (e.g. after a search)\n                focusedElement.scrollIntoView({ block: \"center\" });\n            }\n        });\n        useEffect(\n            () => {\n                if (this.props.showSearchInput) {\n                    const searchInput = rootRef.el.querySelector(\n                        \".o_model_field_selector_popover_search .o_input\"\n                    );\n                    searchInput.focus();\n                }\n            },\n            () => [this.state.page]\n        );\n    }\n\n    get fieldNames() {\n        return this.state.page.fieldNames;\n    }\n\n    get showDebugInput() {\n        return this.props.showDebugInput ?? this.props.isDebugMode;\n    }\n\n    canFollowRelationFor(fieldDef) {\n        if (fieldDef.type === \"properties\") {\n            return true;\n        }\n        if (!this.props.followRelations) {\n            return false;\n        }\n        return fieldDef.relation;\n    }\n\n    filter(fieldDefs, path, resModel) {\n        const filteredKeys = Object.keys(fieldDefs).filter((k) =>\n            this.props.filter(fieldDefs[k], path, resModel)\n        );\n        return Object.fromEntries(filteredKeys.map((k) => [k, fieldDefs[k]]));\n    }\n\n    async followRelation(fieldDef) {\n        const { modelsInfo } = await this.keepLast.add(\n            this.fieldService.loadPath(\n                fieldDef.relation || this.state.page.resModel,\n                `${fieldDef.name}.*`\n            )\n        );\n        this.state.page.selectedName = fieldDef.name;\n        const { resModel, fieldDefs } = modelsInfo.at(-1);\n        this.openPage(\n            new Page(resModel, this.filter(fieldDefs, this.state.page.path, resModel), {\n                previousPage: this.state.page,\n                isDebugMode: this.props.isDebugMode,\n                readProperty: this.props.readProperty,\n                sortFn: this.props.sort,\n            })\n        );\n    }\n\n    goToPreviousPage() {\n        this.keepLast.add(Promise.resolve());\n        this.openPage(this.state.page.previousPage);\n    }\n\n    async loadNewPath(path) {\n        const newPage = await this.keepLast.add(this.loadPages(this.props.resModel, path));\n        this.openPage(newPage);\n    }\n\n    async loadPages(resModel, path) {\n        if (typeof path !== \"string\" || !path.length) {\n            const fieldDefs = await this.fieldService.loadFields(resModel);\n            return new Page(resModel, this.filter(fieldDefs, path, resModel), {\n                isDebugMode: this.props.isDebugMode,\n                readProperty: this.props.readProperty,\n                sortFn: this.props.sort,\n            });\n        }\n        const { isInvalid, modelsInfo, names } = await this.fieldService.loadPath(resModel, path);\n        switch (isInvalid) {\n            case \"model\":\n                throw new Error(`Invalid model name: ${resModel}`);\n            case \"path\": {\n                const { resModel, fieldDefs } = modelsInfo[0];\n                return new Page(resModel, this.filter(fieldDefs, path, resModel), {\n                    selectedName: path,\n                    isDebugMode: this.props.isDebugMode,\n                    readProperty: this.props.readProperty,\n                    sortFn: this.props.sort,\n                });\n            }\n            default: {\n                let page = null;\n                for (let index = 0; index < names.length; index++) {\n                    const name = names[index];\n                    const { resModel, fieldDefs } = modelsInfo[index];\n                    page = new Page(resModel, this.filter(fieldDefs, path, resModel), {\n                        previousPage: page,\n                        selectedName: name,\n                        isDebugMode: this.props.isDebugMode,\n                        readProperty: this.props.readProperty,\n                        sortFn: this.props.sort,\n                    });\n                }\n                return page;\n            }\n        }\n    }\n\n    openPage(page) {\n        this.state.page = page;\n        this.state.page.searchFields();\n        this.props.update(page.path);\n    }\n\n    searchFields(query) {\n        this.state.page.searchFields(query);\n    }\n\n    selectField(field) {\n        if (field.type === \"properties\") {\n            return this.followRelation(field);\n        }\n        this.keepLast.add(Promise.resolve());\n        this.state.page.selectedName = field.name;\n        this.props.update(this.state.page.path, field);\n        this.props.close(true);\n    }\n\n    onDebugInputKeydown(ev) {\n        switch (ev.key) {\n            case \"Enter\": {\n                ev.preventDefault();\n                ev.stopPropagation();\n                this.loadNewPath(ev.currentTarget.value);\n                break;\n            }\n        }\n    }\n\n    // @TODO should rework/improve this and maybe use hotkeys\n    async onInputKeydown(ev) {\n        const { page } = this.state;\n        switch (ev.key) {\n            case \"ArrowUp\": {\n                if (ev.target.selectionStart === 0) {\n                    page.focus(\"previous\");\n                }\n                break;\n            }\n            case \"ArrowDown\": {\n                if (ev.target.selectionStart === page.query.length) {\n                    page.focus(\"next\");\n                }\n                break;\n            }\n            case \"ArrowLeft\": {\n                if (ev.target.selectionStart === 0 && page.previousPage) {\n                    this.goToPreviousPage();\n                }\n                break;\n            }\n            case \"ArrowRight\": {\n                if (ev.target.selectionStart === page.query.length) {\n                    const focusedFieldName = this.state.page.focusedFieldName;\n                    if (focusedFieldName) {\n                        const fieldDef = this.state.page.fieldDefs[focusedFieldName];\n                        if (this.canFollowRelationFor(fieldDef)) {\n                            this.followRelation(fieldDef);\n                        }\n                    }\n                }\n                break;\n            }\n            case \"Enter\": {\n                const focusedFieldName = this.state.page.focusedFieldName;\n                if (focusedFieldName) {\n                    const fieldDef = this.state.page.fieldDefs[focusedFieldName];\n                    this.selectField(fieldDef);\n                } else {\n                    ev.preventDefault();\n                    ev.stopPropagation();\n                }\n                break;\n            }\n            case \"Escape\": {\n                ev.preventDefault();\n                ev.stopPropagation();\n                this.props.close();\n                break;\n            }\n        }\n    }\n}\n", "import { AutoComplete } from \"@web/core/autocomplete/autocomplete\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { fuzzyLookup } from \"@web/core/utils/search\";\nimport { _t } from \"@web/core/l10n/translation\";\n\nimport { Component, onWillStart } from \"@odoo/owl\";\n\nexport class ModelSelector extends Component {\n    static template = \"web.ModelSelector\";\n    static components = { AutoComplete };\n    static props = {\n        onModelSelected: Function,\n        id: { type: String, optional: true },\n        value: { type: String, optional: true },\n        placeholder: { type: String, optional: true },\n        // list of models technical name, if not set\n        // we will fetch all models we have access to\n        models: { type: Array, optional: true },\n        nbVisibleModels: { type: Number, optional: true },\n        autofocus: { type: Boolean, optional: true },\n    };\n\n    setup() {\n        this.orm = useService(\"orm\");\n\n        onWillStart(async () => {\n            if (!this.props.models) {\n                this.models = await this._fetchAvailableModels();\n            } else {\n                this.models = await this.orm.call(\"ir.model\", \"display_name_for\", [\n                    this.props.models,\n                ]);\n            }\n\n            this.models = this.models.map((record) => ({\n                cssClass: `o_model_selector_${record.model.replaceAll(\".\", \"_\")}`,\n                data: {\n                    technical: record.model,\n                },\n                label: record.display_name,\n                onSelect: () =>\n                    this.props.onModelSelected({\n                        label: record.display_name,\n                        technical: record.model,\n                    }),\n            }));\n        });\n    }\n\n    get sources() {\n        return [this.optionsSource];\n    }\n\n    get placeholder() {\n        return this.props.placeholder || _t(\"Type a model here...\");\n    }\n\n    get optionsSource() {\n        return {\n            placeholder: _t(\"Loading...\"),\n            options: this.loadOptionsSource.bind(this),\n        };\n    }\n\n    get nbVisibleModels() {\n        return this.props.nbVisibleModels || 8;\n    }\n\n    filterModels(name) {\n        if (!name) {\n            const visibleModels = this.models.slice(0, this.nbVisibleModels);\n            if (this.models.length - visibleModels.length > 0) {\n                visibleModels.push({\n                    label: _t(\"Start typing...\"),\n                    cssClass: \"o_m2o_start_typing\",\n                });\n            }\n            return visibleModels;\n        }\n        return fuzzyLookup(name, this.models, (model) => model.data.technical + model.label);\n    }\n\n    loadOptionsSource(request) {\n        const options = this.filterModels(request);\n\n        if (!options.length) {\n            options.push({\n                label: _t(\"No records\"),\n                cssClass: \"o_m2o_no_result\",\n            });\n        }\n        return options;\n    }\n\n    /**\n     * Fetch the list of the models that can be\n     * selected for the relational properties.\n     */\n    async _fetchAvailableModels() {\n        const result = await this.orm.call(\"ir.model\", \"get_available_models\");\n        return result || [];\n    }\n}\n", "import { registry } from \"@web/core/registry\";\nimport { unique, zip } from \"@web/core/utils/arrays\";\nimport { Deferred } from \"@web/core/utils/concurrency\";\n\nexport const ERROR_INACCESSIBLE_OR_MISSING = Symbol(\"INACCESSIBLE OR MISSING RECORD ID\");\n\nfunction isId(val) {\n    return Number.isInteger(val) && val >= 1;\n}\n\n/**\n * @typedef {Record<string, (string|ERROR_INACCESSIBLE_OR_MISSING)>} DisplayNames\n */\n\nexport const nameService = {\n    dependencies: [\"orm\"],\n    async: [\"loadDisplayNames\"],\n    start(env, { orm }) {\n        let cache = {};\n        const batches = {};\n\n        function clearCache() {\n            cache = {};\n        }\n\n        env.bus.addEventListener(\"ACTION_MANAGER:UPDATE\", clearCache);\n\n        function getMapping(resModel) {\n            if (!cache[resModel]) {\n                cache[resModel] = {};\n            }\n            return cache[resModel];\n        }\n\n        /**\n         * @param {string} resModel valid resModel name\n         * @param {DisplayNames} displayNames\n         */\n        function addDisplayNames(resModel, displayNames) {\n            const mapping = getMapping(resModel);\n            for (const resId in displayNames) {\n                mapping[resId] = new Deferred();\n                mapping[resId].resolve(displayNames[resId]);\n            }\n        }\n\n        /**\n         * @param {string} resModel valid resModel name\n         * @param {number[]} resIds valid ids\n         * @returns {Promise<DisplayNames>}\n         */\n        async function loadDisplayNames(resModel, resIds) {\n            const mapping = getMapping(resModel);\n            const proms = [];\n            const resIdsToFetch = [];\n            for (const resId of unique(resIds)) {\n                if (!isId(resId)) {\n                    throw new Error(`Invalid ID: ${resId}`);\n                }\n                if (!(resId in mapping)) {\n                    mapping[resId] = new Deferred();\n                    resIdsToFetch.push(resId);\n                }\n                proms.push(mapping[resId]);\n            }\n            if (resIdsToFetch.length) {\n                if (batches[resModel]) {\n                    batches[resModel].push(...resIdsToFetch);\n                } else {\n                    batches[resModel] = resIdsToFetch;\n                    await Promise.resolve();\n                    const idsInBatch = unique(batches[resModel]);\n                    delete batches[resModel];\n\n                    const specification = { display_name: {} };\n                    orm.silent\n                        .webSearchRead(resModel, [[\"id\", \"in\", idsInBatch]], {\n                            specification,\n                            context: { active_test: false },\n                        })\n                        .then(({ records }) => {\n                            const displayNames = Object.fromEntries(\n                                records.map((rec) => [rec.id, rec.display_name])\n                            );\n                            for (const resId of idsInBatch) {\n                                mapping[resId].resolve(\n                                    resId in displayNames\n                                        ? displayNames[resId]\n                                        : ERROR_INACCESSIBLE_OR_MISSING\n                                );\n                            }\n                        });\n                }\n            }\n            const names = await Promise.all(proms);\n            return Object.fromEntries(zip(resIds, names));\n        }\n\n        return { addDisplayNames, clearCache, loadDisplayNames };\n    },\n};\n\nregistry.category(\"services\").add(\"name\", nameService);\n", "import { onWillUnmount, useEffect, useExternalListener, useRef } from \"@odoo/owl\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { deepMerge } from \"@web/core/utils/objects\";\nimport { scrollTo } from \"@web/core/utils/scrolling\";\nimport { throttleForAnimation } from \"@web/core/utils/timing\";\nimport { browser } from \"@web/core/browser/browser\";\n\nexport const ACTIVE_ELEMENT_CLASS = \"focus\";\nconst throttledFocus = throttleForAnimation((el) => el?.focus());\n\nclass NavigationItem {\n    /**@type {number} */\n    index = -1;\n\n    /**\n     * The container element\n     * @type {Element}\n     */\n    el = undefined;\n\n    /**\n     * The actual \"clicked\" element, it can be the same\n     * as @see el but will be the closest child input if\n     * options.shouldFocusChildInput is true\n     * @type {Element}\n     */\n    target = undefined;\n\n    constructor({ index, el, options, navigator }) {\n        this.index = index;\n\n        /**@private */\n        this._options = options;\n\n        /**\n         * @private\n         * @type {Navigator}\n         */\n        this._navigator = navigator;\n\n        this.el = el;\n        if (this._options.shouldFocusChildInput) {\n            const subInput = el.querySelector(\":scope input, :scope button, :scope textarea\");\n            this.target = subInput || el;\n        } else {\n            this.target = el;\n        }\n\n        if (this.el.ariaSelected !== true) {\n            this.el.ariaSelected = false;\n        }\n\n        const onFocus = () => this.setActive(false);\n        const onMouseMove = () => this._onMouseMove();\n\n        this.target.addEventListener(\"focus\", onFocus);\n        this.target.addEventListener(\"mousemove\", onMouseMove);\n\n        /**@private*/\n        this._removeListeners = () => {\n            this.target.removeEventListener(\"focus\", onFocus);\n            this.target.removeEventListener(\"mousemove\", onMouseMove);\n        };\n    }\n\n    select() {\n        this.setActive();\n        this.target.click();\n    }\n\n    setActive(focus = true) {\n        scrollTo(this.target);\n        this._navigator._setActiveItem(this.index);\n        this.target.classList.add(ACTIVE_ELEMENT_CLASS);\n        this.target.ariaSelected = true;\n\n        if (focus && !this._options.virtualFocus) {\n            throttledFocus.cancel();\n            throttledFocus(this.target);\n        }\n    }\n\n    setInactive(blur = true) {\n        this.target.classList.remove(ACTIVE_ELEMENT_CLASS);\n        this.target.ariaSelected = false;\n        if (blur && !this._options.virtualFocus) {\n            this.target.blur();\n        }\n    }\n\n    /**\n     * @private\n     */\n    _onMouseMove() {\n        if (\n            this._navigator.activeItem !== this &&\n            this._navigator._isNavigationAvailable(this.target)\n        ) {\n            this.setActive(false);\n            this._options.onMouseEnter?.(this);\n        }\n    }\n}\n\nexport class Navigator {\n    /**@type {NavigationItem|undefined}*/\n    activeItem = undefined;\n\n    /**@type {number}*/\n    activeItemIndex = -1;\n\n    /**@type {Array<NavigationItem>}*/\n    items = [];\n\n    /**@private*/ _hotkeyRemoves = [];\n    /**@private*/ _hotkeyService = undefined;\n\n    /**\n     * @param {NavigationOptions} options\n     * @param {import(\"@web/core/hotkeys/hotkey_service\").HotkeyService} hotkeyService\n     */\n    constructor(options, hotkeyService) {\n        this._hotkeyService = hotkeyService;\n\n        /**@private*/\n        this._options = deepMerge(\n            {\n                isNavigationAvailable: ({ target }) =>\n                    this.contains(target) && (this.isFocused || this._options.virtualFocus),\n                shouldFocusChildInput: true,\n                shouldFocusFirstItem: false,\n                shouldRegisterHotkeys: true,\n                virtualFocus: false,\n                hotkeys: {\n                    home: () => this.items[0]?.setActive(),\n                    end: () => this.items.at(-1)?.setActive(),\n                    tab: {\n                        callback: () => this.next(),\n                        bypassEditableProtection: true,\n                    },\n                    \"shift+tab\": {\n                        callback: () => this.previous(),\n                        bypassEditableProtection: true,\n                    },\n                    arrowdown: {\n                        callback: () => this.next(),\n                        bypassEditableProtection: true,\n                    },\n                    arrowup: {\n                        callback: () => this.previous(),\n                        bypassEditableProtection: true,\n                    },\n                    enter: {\n                        isAvailable: ({ navigator }) => Boolean(navigator.activeItem),\n                        callback: () => {\n                            const item = this.activeItem || this.items[0];\n                            item?.select();\n                        },\n                        bypassEditableProtection: true,\n                    },\n                },\n            },\n            options\n        );\n\n        if (this._options.shouldRegisterHotkeys) {\n            this.registerHotkeys();\n        }\n    }\n\n    /**\n     * Returns true if the current active item is not null and still inside the DOM\n     * @type {boolean}\n     */\n    get hasActiveItem() {\n        return Boolean(this.activeItem?.el.isConnected);\n    }\n\n    /**\n     * Returns true if the focus is on any of the navigable items\n     * @type {boolean}\n     */\n    get isFocused() {\n        return this.items.some((item) => item.target.contains(document.activeElement));\n    }\n\n    next() {\n        if (!this.hasActiveItem) {\n            this.items[0]?.setActive();\n        } else {\n            this.items[(this.activeItemIndex + 1) % this.items.length]?.setActive();\n        }\n    }\n\n    previous() {\n        const index = this.activeItemIndex - 1;\n        if (!this.hasActiveItem || index < 0) {\n            this.items.at(-1)?.setActive();\n        } else {\n            this.items[index % this.items.length]?.setActive();\n        }\n    }\n\n    update() {\n        const oldItems = new Map(this.items.map((item) => [item.el, item]));\n        const oldActiveItem = this.activeItem;\n        const elements = this._options.getItems();\n        this.items = [];\n\n        let didUpdate = elements.length !== oldItems.size;\n        for (let index = 0; index < elements.length; index++) {\n            const element = elements[index];\n\n            let item = oldItems.get(element);\n            if (item) {\n                if (item.index !== index) {\n                    item.index = index;\n                    didUpdate = true;\n                }\n                oldItems.delete(element);\n            } else {\n                didUpdate = true;\n                item = new NavigationItem({\n                    index,\n                    el: element,\n                    options: this._options,\n                    navigator: this,\n                });\n            }\n            this.items.push(item);\n        }\n\n        for (const item of oldItems.values()) {\n            item._removeListeners();\n        }\n\n        if (didUpdate) {\n            const activeItemIndex =\n                oldActiveItem && oldActiveItem.el.isConnected\n                    ? this.items.findIndex((item) => item.el === oldActiveItem.el)\n                    : -1;\n            const focusedElementIndex = this.items.findIndex((item) => item.el === document.activeElement);\n            if (activeItemIndex > -1) {\n                this._updateActiveItemIndex(activeItemIndex);\n            } else if (this.activeItemIndex >= 0) {\n                const closest = Math.min(this.activeItemIndex, elements.length - 1);\n                this._updateActiveItemIndex(closest);\n            } else if (focusedElementIndex >= 0) {\n                this._updateActiveItemIndex(focusedElementIndex);\n            } else {\n                this._updateActiveItemIndex(-1);\n            }\n\n            this._options.onUpdated?.(this);\n\n            if (this._options.shouldFocusFirstItem) {\n                this.items[0]?.setActive();\n            }\n        }\n    }\n\n    /**\n     * @param {HTMLElement} target\n     * @returns {boolean}\n     */\n    contains(target) {\n        return this.items.some((item) => item.target.contains(target));\n    }\n\n    registerHotkeys() {\n        if (this._hotkeyRemoves.length > 0) {\n            return;\n        }\n\n        for (const [hotkey, hotkeyInfo] of Object.entries(this._options.hotkeys)) {\n            if (!hotkeyInfo) {\n                continue;\n            }\n\n            const callback = typeof hotkeyInfo == \"function\" ? hotkeyInfo : hotkeyInfo.callback;\n            if (!callback) {\n                continue;\n            }\n\n            const isAvailable = hotkeyInfo?.isAvailable ?? (() => true);\n            const bypassEditableProtection = hotkeyInfo?.bypassEditableProtection ?? false;\n            const allowRepeat = hotkeyInfo?.allowRepeat ?? true;\n\n            this._hotkeyRemoves.push(\n                this._hotkeyService.add(hotkey, async () => await callback(this), {\n                    global: true,\n                    allowRepeat,\n                    isAvailable: (target) =>\n                        this._isNavigationAvailable(target) &&\n                        isAvailable({ navigator: this, target }),\n                    bypassEditableProtection,\n                })\n            );\n        }\n    }\n\n    unregisterHotkeys() {\n        for (const removeHotkey of this._hotkeyRemoves) {\n            removeHotkey();\n        }\n        this._hotkeyRemoves = [];\n    }\n\n    /**\n     * @private\n     */\n    _destroy() {\n        for (const item of this.items) {\n            item._removeListeners();\n        }\n        this.items = [];\n        this.unregisterHotkeys();\n    }\n\n    /**\n     * @private\n     */\n    _setActiveItem(index) {\n        this.activeItem?.setInactive(false);\n        this.activeItemIndex = index;\n        if (index >= 0) {\n            this.activeItem = this.items[index];\n            this._options.onItemActivated?.(this.activeItem.el);\n        } else {\n            this.activeItem = null;\n        }\n    }\n\n    /**\n     * @private\n     */\n    _updateActiveItemIndex(index) {\n        if (this.items[index]) {\n            this.items[index].setActive();\n        } else {\n            this.activeItemIndex = -1;\n            this.activeItem = null;\n        }\n    }\n\n    /**\n     * @private\n     */\n    _isNavigationAvailable(target) {\n        return this._options.isNavigationAvailable({ navigator: this, target });\n    }\n\n    /**\n     * @private\n     */\n    _checkFocus(target) {\n        if (!(target instanceof HTMLElement) || !this._isNavigationAvailable(target)) {\n            this._setActiveItem(-1);\n        }\n    }\n}\n\n/**\n * @typedef {Object} NavigationOptions\n * @property {() => HTMLElement[]} getItems\n * @property {({{ navigator: Navigator, target: HTMLElement }}) => bool} isNavigationAvailable\n * @property {NavigationHotkeys} hotkeys\n * @property {Function} onUpdated\n * @property {Function} onItemActivated\n * @property {Boolean} [virtualFocus=false] - If true, items are only visually\n * focused so the actual focus can be kept on another input.\n * @property {Boolean} [shouldFocusChildInput=false] - If true, elements like inputs or buttons\n * inside of the items are focused instead of the items themselves.\n * @property {Boolean} [shouldRegisterHotkeys=true] - If true, registers all hotkeys directly when\n * the hook is called.\n */\n\n/**\n * @typedef {{\n *  home: hotkeyHandler|HotkeyOptions|undefined,\n *  end: hotkeyHandler|HotkeyOptions|undefined,\n *  tab: hotkeyHandler|HotkeyOptions|undefined,\n *  \"shift+tab\": hotkeyHandler|HotkeyOptions|undefined,\n *  arrowup: hotkeyHandler|HotkeyOptions|undefined,\n *  arrowdown: hotkeyHandler|HotkeyOptions|undefined,\n *  enter: hotkeyHandler|HotkeyOptions|undefined,\n *  arrowleft: hotkeyHandler|HotkeyOptions|undefined,\n *  arrowright: hotkeyHandler|HotkeyOptions|undefined,\n *  escape: hotkeyHandler|HotkeyOptions|undefined,\n *  space: hotkeyHandler|HotkeyOptions|undefined,\n * }} NavigationHotkeys\n */\n\n/**\n * @typedef HotkeyOptions\n * @param {hotkeyHandler} callback\n * @param {({{ navigator: Navigator, target: HTMLElement }}) => bool} isAvailable\n * @param {boolean} bypassEditableProtection\n * @param {boolean} [allowRepeat=true]\n */\n\n/**\n * Callback used to override the behaviour of a specific\n * key input.\n *\n * @callback hotkeyHandler\n * @param {Navigator} navigator\n */\n\n/**\n * This hook adds keyboard navigation to items contained in an element.\n * It's purpose is to improve navigation in constrained context such\n * as dropdown and menus.\n *\n * This hook also has the following features:\n * - Hotkeys override and customization\n * - Navigation between inputs elements\n * - Optional virtual focus\n * - Focus on mouse enter\n *\n * @param {string|Object} containerRef\n * @param {NavigationOptions} options\n * @returns {Navigator}\n */\nexport function useNavigation(containerRef, options = {}) {\n    containerRef = typeof containerRef === \"string\" ? useRef(containerRef) : containerRef;\n\n    const newOptions = { ...options };\n    if (!newOptions.getItems) {\n        newOptions.getItems = () => containerRef.el?.querySelectorAll(\":scope .o-navigable\") ?? [];\n    }\n\n    const hotkeyService = useService(\"hotkey\");\n    const navigator = new Navigator(newOptions, hotkeyService);\n    const observer = new MutationObserver(() => navigator.update());\n\n    useEffect(\n        (containerEl) => {\n            if (containerEl) {\n                navigator.update();\n                observer.observe(containerEl, {\n                    childList: true,\n                    subtree: true,\n                });\n            }\n            return () => observer.disconnect();\n        },\n        () => [containerRef.el]\n    );\n\n    useExternalListener(browser, \"focus\", ({ target }) => navigator._checkFocus(target), true);\n    onWillUnmount(() => navigator._destroy());\n\n    return navigator;\n}\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { makeErrorFromResponse, ConnectionLostError } from \"@web/core/network/rpc\";\nimport { browser } from \"@web/core/browser/browser\";\n\n/* eslint-disable */\n/**\n * The following sections are from libraries, they have been slightly modified\n * to allow patching them during tests, but should not be linted, so that we can\n * keep a minimal diff that is easy to reapply when upgrading\n */\n// -----------------------------------------------------------------------------\n// Content Disposition Library\n// -----------------------------------------------------------------------------\n\n/*\n(The MIT License)\nCopyright (c) 2014-2017 Douglas Christopher Wilson\nPermission is hereby granted, free of charge, to any person obtaining\na copy of this software and associated documentation files (the\n'Software'), to deal in the Software without restriction, including\nwithout limitation the rights to use, copy, modify, merge, publish,\ndistribute, sublicense, and/or sell copies of the Software, and to\npermit persons to whom the Software is furnished to do so, subject to\nthe following conditions:\nThe above copyright notice and this permission notice shall be\nincluded in all copies or substantial portions of the Software.\nTHE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.\nIN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY\nCLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,\nTORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE\nSOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n */\n\n/**\n * Stripped down to only parsing/decoding.\n * Slightly changed for export and lint compliance\n */\n\n/**\n * RegExp to match percent encoding escape.\n * @private\n */\nconst HEX_ESCAPE_REPLACE_REGEXP = /%([0-9A-Fa-f]{2})/g;\n\n/**\n * RegExp to match non-latin1 characters.\n * @private\n */\nconst NON_LATIN1_REGEXP = /[^\\x20-\\x7e\\xa0-\\xff]/g;\n\n/**\n * RegExp to match quoted-pair in RFC 2616\n *\n * quoted-pair = \"\\\" CHAR\n * CHAR        = <any US-ASCII character (octets 0 - 127)>\n * @private\n */\nconst QESC_REGEXP = /\\\\([\\u0000-\\u007f])/g;\n\n/**\n * RegExp for various RFC 2616 grammar\n *\n * parameter     = token \"=\" ( token | quoted-string )\n * token         = 1*<any CHAR except CTLs or separators>\n * separators    = \"(\" | \")\" | \"<\" | \">\" | \"@\"\n *               | \",\" | \";\" | \":\" | \"\\\" | <\">\n *               | \"/\" | \"[\" | \"]\" | \"?\" | \"=\"\n *               | \"{\" | \"}\" | SP | HT\n * quoted-string = ( <\"> *(qdtext | quoted-pair ) <\"> )\n * qdtext        = <any TEXT except <\">>\n * quoted-pair   = \"\\\" CHAR\n * CHAR          = <any US-ASCII character (octets 0 - 127)>\n * TEXT          = <any OCTET except CTLs, but including LWS>\n * LWS           = [CRLF] 1*( SP | HT )\n * CRLF          = CR LF\n * CR            = <US-ASCII CR, carriage return (13)>\n * LF            = <US-ASCII LF, linefeed (10)>\n * SP            = <US-ASCII SP, space (32)>\n * HT            = <US-ASCII HT, horizontal-tab (9)>\n * CTL           = <any US-ASCII control character (octets 0 - 31) and DEL (127)>\n * OCTET         = <any 8-bit sequence of data>\n * @private\n */\nconst PARAM_REGEXP = /;[\\x09\\x20]*([!#$%&'*+.0-9A-Z^_`a-z|~-]+)[\\x09\\x20]*=[\\x09\\x20]*(\"(?:[\\x20!\\x23-\\x5b\\x5d-\\x7e\\x80-\\xff]|\\\\[\\x20-\\x7e])*\"|[!#$%&'*+.0-9A-Z^_`a-z|~-]+)[\\x09\\x20]*/g;\n\n/**\n * RegExp for various RFC 5987 grammar\n *\n * ext-value     = charset  \"'\" [ language ] \"'\" value-chars\n * charset       = \"UTF-8\" / \"ISO-8859-1\" / mime-charset\n * mime-charset  = 1*mime-charsetc\n * mime-charsetc = ALPHA / DIGIT\n *               / \"!\" / \"#\" / \"$\" / \"%\" / \"&\"\n *               / \"+\" / \"-\" / \"^\" / \"_\" / \"`\"\n *               / \"{\" / \"}\" / \"~\"\n * language      = ( 2*3ALPHA [ extlang ] )\n *               / 4ALPHA\n *               / 5*8ALPHA\n * extlang       = *3( \"-\" 3ALPHA )\n * value-chars   = *( pct-encoded / attr-char )\n * pct-encoded   = \"%\" HEXDIG HEXDIG\n * attr-char     = ALPHA / DIGIT\n *               / \"!\" / \"#\" / \"$\" / \"&\" / \"+\" / \"-\" / \".\"\n *               / \"^\" / \"_\" / \"`\" / \"|\" / \"~\"\n * @private\n */\nconst EXT_VALUE_REGEXP = /^([A-Za-z0-9!#$%&+\\-^_`{}~]+)'(?:[A-Za-z]{2,3}(?:-[A-Za-z]{3}){0,3}|[A-Za-z]{4,8}|)'((?:%[0-9A-Fa-f]{2}|[A-Za-z0-9!#$&+.^_`|~-])+)$/;\n\n/**\n * RegExp for various RFC 6266 grammar\n *\n * disposition-type = \"inline\" | \"attachment\" | disp-ext-type\n * disp-ext-type    = token\n * disposition-parm = filename-parm | disp-ext-parm\n * filename-parm    = \"filename\" \"=\" value\n *                  | \"filename*\" \"=\" ext-value\n * disp-ext-parm    = token \"=\" value\n *                  | ext-token \"=\" ext-value\n * ext-token        = <the characters in token, followed by \"*\">\n * @private\n */\nconst DISPOSITION_TYPE_REGEXP = /^([!#$%&'*+.0-9A-Z^_`a-z|~-]+)[\\x09\\x20]*(?:$|;)/;\n\n/**\n * Decode a RFC 6987 field value (gracefully).\n *\n * @param {string} str\n * @return {string}\n * @private\n */\nfunction decodefield(str) {\n    const match = EXT_VALUE_REGEXP.exec(str);\n\n    if (!match) {\n        throw new TypeError(\"invalid extended field value\");\n    }\n\n    const charset = match[1].toLowerCase();\n    const encoded = match[2];\n\n    switch (charset) {\n        case \"iso-8859-1\":\n            return encoded\n                .replace(HEX_ESCAPE_REPLACE_REGEXP, pdecode)\n                .replace(NON_LATIN1_REGEXP, \"?\");\n        case \"utf-8\":\n            return decodeURIComponent(encoded);\n        default:\n            throw new TypeError(\"unsupported charset in extended field\");\n    }\n}\n\n/**\n * Parse Content-Disposition header string.\n *\n * @param {string} string\n * @return {ContentDisposition}\n * @public\n */\nexport function parse(string) {\n    if (!string || typeof string !== \"string\") {\n        throw new TypeError(\"argument string is required\");\n    }\n\n    let match = DISPOSITION_TYPE_REGEXP.exec(string);\n\n    if (!match) {\n        throw new TypeError(\"invalid type format\");\n    }\n\n    // normalize type\n    let index = match[0].length;\n    const type = match[1].toLowerCase();\n\n    let key;\n    const names = [];\n    const params = {};\n    let value;\n\n    // calculate index to start at\n    index = PARAM_REGEXP.lastIndex = match[0].substr(-1) === \";\" ? index - 1 : index;\n\n    // match parameters\n    while ((match = PARAM_REGEXP.exec(string))) {\n        if (match.index !== index) {\n            throw new TypeError(\"invalid parameter format\");\n        }\n\n        index += match[0].length;\n        key = match[1].toLowerCase();\n        value = match[2];\n\n        if (names.indexOf(key) !== -1) {\n            throw new TypeError(\"invalid duplicate parameter\");\n        }\n\n        names.push(key);\n\n        if (key.indexOf(\"*\") + 1 === key.length) {\n            // decode extended value\n            key = key.slice(0, -1);\n            value = decodefield(value);\n\n            // overwrite existing value\n            params[key] = value;\n            continue;\n        }\n\n        if (typeof params[key] === \"string\") {\n            continue;\n        }\n\n        if (value[0] === '\"') {\n            // remove quotes and escapes\n            value = value.substr(1, value.length - 2).replace(QESC_REGEXP, \"$1\");\n        }\n\n        params[key] = value;\n    }\n\n    if (index !== -1 && index !== string.length) {\n        throw new TypeError(\"invalid parameter format\");\n    }\n\n    return new ContentDisposition(type, params);\n}\n\n/**\n * Percent decode a single character.\n *\n * @param {string} str\n * @param {string} hex\n * @return {string}\n * @private\n */\nfunction pdecode(str, hex) {\n    return String.fromCharCode(parseInt(hex, 16));\n}\n\n/**\n * Class for parsed Content-Disposition header for v8 optimization\n *\n * @public\n * @param {string} type\n * @param {object} parameters\n * @constructor\n */\nfunction ContentDisposition(type, parameters) {\n    this.type = type;\n    this.parameters = parameters;\n}\n\n// -----------------------------------------------------------------------------\n// download.js library\n// -----------------------------------------------------------------------------\n\n/*\nMIT License\nCopyright (c) 2016 dandavis\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n */\n\n/**\n * download.js v4.2, by dandavis; 2008-2018. [MIT] see http://danml.com/download.html for tests/usage\n * v1 landed a FF+Chrome compat way of downloading strings to local un-named files, upgraded to use a hidden frame and optional mime\n * v2 added named files via a[download], msSaveBlob, IE (10+) support, and window.URL support for larger+faster saves than dataURLs\n * v3 added dataURL and Blob Input, bind-toggle arity, and legacy dataURL fallback was improved with force-download mime and base64 support. 3.1 improved safari handling.\n * v4 adds AMD/UMD, commonJS, and plain browser support\n * v4.1 adds url download capability via solo URL argument (same domain/CORS only)\n * v4.2 adds semantic variable names, long (over 2MB) dataURL support, and hidden by default temp anchors\n *\n * Slightly modified for export and lint compliance\n *\n * @param {Blob | File | String} data\n * @param {String} [filename]\n * @param {String} [mimetype]\n */\nfunction _download(data, filename, mimetype) {\n    let self = window, // this script is only for browsers anyway...\n        defaultMime = \"application/octet-stream\", // this default mime also triggers iframe downloads\n        mimeType = mimetype || defaultMime,\n        payload = data,\n        url = !filename && !mimetype && payload,\n        anchor = document.createElement(\"a\"),\n        toString = function (a) {\n            return String(a);\n        },\n        myBlob = self.Blob || self.MozBlob || self.WebKitBlob || toString,\n        fileName = filename || \"download\",\n        blob,\n        reader;\n    myBlob = myBlob.call ? myBlob.bind(self) : Blob;\n\n    if (String(this) === \"true\") {\n        //reverse arguments, allowing download.bind(true, \"text/xml\", \"export.xml\") to act as a callback\n        payload = [payload, mimeType];\n        mimeType = payload[0];\n        payload = payload[1];\n    }\n\n    if (url && url.length < 2048) {\n        // if no filename and no mime, assume a url was passed as the only argument\n        fileName = url.split(\"/\").pop().split(\"?\")[0];\n        anchor.href = url; // assign href prop to temp anchor\n        if (anchor.href.indexOf(url) !== -1) {\n            // if the browser determines that it's a potentially valid url path:\n            return new Promise((resolve, reject) => {\n                let xhr = new browser.XMLHttpRequest();\n                xhr.open(\"GET\", url, true);\n                configureBlobDownloadXHR(xhr, {\n                    onSuccess: resolve,\n                    onFailure: reject,\n                    url\n                });\n                xhr.send();\n            });\n        }\n    }\n\n    //go ahead and download dataURLs right away\n    if (/^data:[\\w+\\-]+\\/[\\w+\\-]+[,;]/.test(payload)) {\n        if (payload.length > 1024 * 1024 * 1.999 && myBlob !== toString) {\n            payload = dataUrlToBlob(payload);\n            mimeType = payload.type || defaultMime;\n        } else {\n            return navigator.msSaveBlob // IE10 can't do a[download], only Blobs:\n                ? navigator.msSaveBlob(dataUrlToBlob(payload), fileName)\n                : saver(payload); // everyone else can save dataURLs un-processed\n        }\n    }\n\n    blob = payload instanceof myBlob ? payload : new myBlob([payload], { type: mimeType });\n\n    function dataUrlToBlob(strUrl) {\n        let parts = strUrl.split(/[:;,]/),\n            type = parts[1],\n            decoder = parts[2] === \"base64\" ? atob : decodeURIComponent,\n            binData = decoder(parts.pop()),\n            mx = binData.length,\n            i = 0,\n            uiArr = new Uint8Array(mx);\n\n        for (i; i < mx; ++i) {\n            uiArr[i] = binData.charCodeAt(i);\n        }\n\n        return new myBlob([uiArr], { type });\n    }\n\n    function saver(url, winMode) {\n        if (\"download\" in anchor) {\n            //html5 A[download]\n            anchor.href = url;\n            anchor.setAttribute(\"download\", fileName);\n            anchor.className = \"download-js-link\";\n            anchor.innerText = _t(\"downloading...\");\n            anchor.style.display = \"none\";\n            anchor.target = \"_blank\";\n            document.body.appendChild(anchor);\n            setTimeout(() => {\n                anchor.click();\n                document.body.removeChild(anchor);\n                if (winMode === true) {\n                    setTimeout(() => {\n                        self.URL.revokeObjectURL(anchor.href);\n                    }, 250);\n                }\n            }, 66);\n            return true;\n        }\n\n        // handle non-a[download] safari as best we can:\n        if (/(Version)\\/(\\d+)\\.(\\d+)(?:\\.(\\d+))?.*Safari\\//.test(navigator.userAgent)) {\n            url = url.replace(/^data:([\\w\\/\\-+]+)/, defaultMime);\n            if (!window.open(url)) {\n                // popup blocked, offer direct download:\n                if (\n                    confirm(\n                        \"Displaying New Document\\n\\nUse Save As... to download, then click back to return to this page.\"\n                    )\n                ) {\n                    location.href = url;\n                }\n            }\n            return true;\n        }\n\n        //do iframe dataURL download (old ch+FF):\n        let f = document.createElement(\"iframe\");\n        document.body.appendChild(f);\n\n        if (!winMode) {\n            // force a mime that will download:\n            url = `data:${url.replace(/^data:([\\w\\/\\-+]+)/, defaultMime)}`;\n        }\n        f.src = url;\n        setTimeout(() => {\n            document.body.removeChild(f);\n        }, 333);\n    }\n\n    if (navigator.msSaveBlob) {\n        // IE10+ : (has Blob, but not a[download] or URL)\n        return navigator.msSaveBlob(blob, fileName);\n    }\n\n    if (self.URL) {\n        // simple fast and modern way using Blob and URL:\n        saver(self.URL.createObjectURL(blob), true);\n    } else {\n        // handle non-Blob()+non-URL browsers:\n        if (typeof blob === \"string\" || blob.constructor === toString) {\n            try {\n                return saver(`data:${mimeType};base64,${self.btoa(blob)}`);\n            } catch {\n                return saver(`data:${mimeType},${encodeURIComponent(blob)}`);\n            }\n        }\n\n        // Blob but not URL support:\n        reader = new FileReader();\n        reader.onload = function () {\n            saver(this.result);\n        };\n        reader.readAsDataURL(blob);\n    }\n    return true;\n}\n/* eslint-enable */\n\n// -----------------------------------------------------------------------------\n// Exported download functions\n// -----------------------------------------------------------------------------\n\n/**\n * Download data as a file\n *\n * @param {Object} data\n * @param {String} filename\n * @param {String} mimetype\n * @returns {Boolean}\n *\n * Note: the actual implementation is certainly unconventional, but sadly\n * necessary to be able to test code using the download function\n */\nexport function downloadFile(data, filename, mimetype) {\n    return downloadFile._download(data, filename, mimetype);\n}\ndownloadFile._download = _download;\n\n/**\n * Download a file from form or server url\n *\n * This function is meant to call a controller with some data\n * and download the response.\n *\n * Note: the actual implementation is certainly unconventional, but sadly\n * necessary to be able to test code using the download function\n *\n * @param {*} options\n * @returns {Promise<any>}\n */\nexport function download(options) {\n    return download._download(options);\n}\n\ndownload._download = (options) => {\n    return new Promise((resolve, reject) => {\n        const xhr = new browser.XMLHttpRequest();\n        let data;\n        if (Object.prototype.hasOwnProperty.call(options, \"form\")) {\n            xhr.open(options.form.method, options.form.action);\n            data = new FormData(options.form);\n        } else {\n            xhr.open(\"POST\", options.url);\n            data = new FormData();\n            Object.entries(options.data).forEach((entry) => {\n                const [key, value] = entry;\n                data.append(key, value);\n            });\n        }\n        data.append(\"token\", \"dummy-because-api-expects-one\");\n        if (odoo.csrf_token) {\n            data.append(\"csrf_token\", odoo.csrf_token);\n        }\n        configureBlobDownloadXHR(xhr, {\n            onSuccess: resolve,\n            onFailure: reject,\n            url: options.url,\n        });\n        xhr.send(data);\n    });\n};\n\n/**\n * Setup a download xhr request response handling\n * (onload, onerror, responseType), with hooks when the download succeeds or\n * fails.\n *\n * @param {XMLHttpRequest} xhr\n * @param {object} [options]\n * @param {(filename: string) => void} [options.onSuccess]\n * @param {(Error) => void} [options.onFailure]\n * @param {string} [options.url]\n */\nexport function configureBlobDownloadXHR(\n    xhr,\n    { onSuccess = () => {}, onFailure = () => {}, url } = {}\n) {\n    xhr.responseType = \"blob\";\n    xhr.onload = () => {\n        const mimetype = xhr.response.type;\n        const header = (xhr.getResponseHeader(\"Content-Disposition\") || \"\").replace(/;$/, \"\");\n        // replace because apparently we send some C-D headers with a trailing \";\"\n        const filename = header ? parse(header).parameters.filename : null;\n        // In Odoo, the default mimetype, including for JSON errors is text/html (ref: http.py:Root.get_response )\n        // in that case, in order to also be able to download html files, we check if we get a proper filename to be able to download\n        if (xhr.status === 200 && (mimetype !== \"text/html\" || filename)) {\n            _download(xhr.response, filename, mimetype);\n            onSuccess(filename);\n        } else if (xhr.status === 502) {\n            // If Odoo is behind another server (nginx)\n            onFailure(new ConnectionLostError(url));\n        } else {\n            const decoder = new FileReader();\n            decoder.onload = () => {\n                const contents = decoder.result;\n                const doc = new DOMParser().parseFromString(contents, \"text/html\");\n                const nodes =\n                    doc.body.children.length === 0 ? [doc.body] : doc.body.children;\n\n                let error;\n                try {\n                    // a Serialized python Error\n                    const node = nodes[1] || nodes[0];\n                    error = JSON.parse(node.textContent);\n                } catch {\n                    error = {\n                        message: \"Arbitrary Uncaught Python Exception\",\n                        data: {\n                            debug:\n                                `${xhr.status}` +\n                                `\\n` +\n                                `${nodes.length > 0 ? nodes[0].textContent : \"\"}\n                                ${nodes.length > 1 ? nodes[1].textContent : \"\"}`,\n                        },\n                    };\n                }\n                error = makeErrorFromResponse(error);\n                onFailure(error);\n            };\n            decoder.readAsText(xhr.response);\n        }\n    };\n    xhr.onerror = () => {\n        onFailure(new ConnectionLostError(url));\n    };\n}\n", "import { browser } from \"@web/core/browser/browser\";\nimport { registry } from \"../registry\";\n\nfunction checkResponseStatus(response) {\n    if (response.status === 502) {\n        throw new Error(\"Failed to fetch\");\n    }\n    if (response.status === 413) {\n        throw new Error(\"Content too large\");\n    }\n}\n\nexport async function get(route, readMethod = \"json\") {\n    const response = await browser.fetch(route, { method: \"GET\" });\n    checkResponseStatus(response);\n    return response[readMethod]();\n}\n\nexport async function post(route, params = {}, readMethod = \"json\") {\n    let formData = params;\n    if (!(formData instanceof FormData)) {\n        formData = new FormData();\n        for (const key in params) {\n            const value = params[key];\n            if (Array.isArray(value) && value.length) {\n                for (const val of value) {\n                    formData.append(key, val);\n                }\n            } else {\n                formData.append(key, value);\n            }\n        }\n    }\n    const response = await browser.fetch(route, {\n        body: formData,\n        method: \"POST\",\n    });\n    checkResponseStatus(response);\n    return response[readMethod]();\n}\n\nexport const httpService = {\n    start() {\n        return { get, post };\n    },\n};\n\nregistry.category(\"services\").add(\"http\", httpService);\n", "import { EventBus } from \"@odoo/owl\";\nimport { browser } from \"../browser/browser\";\nimport { omit } from \"../utils/objects\";\n\n/**\n * @typedef {{\n *  code: number;\n *  message: string;\n *  data?: unknown;\n *  type?: string;\n * }} JsonRpcError\n */\n\nexport const rpcBus = new EventBus();\n\nconst RPC_SETTINGS = new Set([\"cache\", \"silent\", \"xhr\", \"headers\"]);\nfunction validateRPCSettings(settings) {\n    if (!Object.keys(settings).every((key) => RPC_SETTINGS.has(key))) {\n        throw new Error(`The settings for rpc should be ${[...RPC_SETTINGS].join(\" \")}`);\n    }\n    if (\"cache\" in settings && \"xhr\" in settings) {\n        throw new Error(\"Can't use 'cache' and 'xhr' at the same time\");\n    }\n}\n\n// -----------------------------------------------------------------------------\n// Errors\n// -----------------------------------------------------------------------------\nexport class RPCError extends Error {\n    constructor() {\n        super(...arguments);\n        this.name = \"RPC_ERROR\";\n        this.type = \"server\";\n        this.code = null;\n        this.data = null;\n        this.exceptionName = null;\n        this.subType = null;\n    }\n}\n\nexport class ConnectionLostError extends Error {\n    constructor(url, ...args) {\n        const message = url\n            ? `Connection to \"${url}\" couldn't be established or was interrupted`\n            : \"Connection couldn't be established or was interrupted\";\n        super(message, ...args);\n        this.url = url;\n    }\n}\n\nexport class ConnectionAbortedError extends Error {}\n\n/**\n * @param {JsonRpcError} response\n */\nexport function makeErrorFromResponse(response) {\n    // Odoo returns error like this, in a error field instead of properly\n    // using http error codes...\n    const { code, data: errorData, message, type: subType } = response;\n    const error = new RPCError();\n    error.exceptionName = errorData?.name;\n    error.subType = subType;\n    error.data = errorData;\n    error.message = message;\n    error.code = code;\n    return error;\n}\n\n// -----------------------------------------------------------------------------\n// Cache RPC method\n// -----------------------------------------------------------------------------\n\nlet rpcCache;\n\nrpc.setCache = function (cache) {\n    rpcCache = cache;\n};\n\nrpcBus.addEventListener(\"CLEAR-CACHES\", (event) => {\n    rpcCache?.invalidate(event.detail);\n});\n\n// -----------------------------------------------------------------------------\n// Main RPC\n// -----------------------------------------------------------------------------\nlet rpcId = 0;\nexport function rpc(url, params = {}, settings = {}) {\n    return rpc._rpc(url, params, settings);\n}\n// such that it can be overriden in tests\nrpc._rpc = function (url, params, settings) {\n    validateRPCSettings(settings);\n    if (settings.cache && rpcCache) {\n        return rpcCache.read(\n            params?.method || url, // table\n            JSON.stringify({ url, params }), // key\n            () => rpc._rpc(url, params, omit(settings, \"cache\")),\n            typeof settings.cache === \"boolean\" ? {} : settings.cache // cache can be boolean or an object with options (or an empty object of course)\n        );\n    }\n    const XHR = browser.XMLHttpRequest;\n    const data = {\n        id: rpcId++,\n        jsonrpc: \"2.0\",\n        method: \"call\",\n        params: params,\n    };\n    const request = settings.xhr || new XHR();\n    let rejectFn;\n    const promise = new Promise((resolve, reject) => {\n        rejectFn = reject;\n        rpcBus.trigger(\"RPC:REQUEST\", { data, url, settings });\n        // handle success\n        request.addEventListener(\"load\", () => {\n            if (request.status === 502) {\n                // If Odoo is behind another server (eg.: nginx)\n                const error = new ConnectionLostError(url);\n                rpcBus.trigger(\"RPC:RESPONSE\", { data, settings, error });\n                reject(error);\n                return;\n            }\n            let params;\n            try {\n                params = JSON.parse(request.response);\n            } catch {\n                // the response isn't json parsable, which probably means that the rpc request could\n                // not be handled by the server, e.g. PoolError('The Connection Pool Is Full')\n                const error = new ConnectionLostError(url);\n                rpcBus.trigger(\"RPC:RESPONSE\", { data, settings, error });\n                return reject(error);\n            }\n            const { error: responseError, result: responseResult } = params;\n            if (!params.error) {\n                rpcBus.trigger(\"RPC:RESPONSE\", { data, settings, result: params.result });\n                return resolve(responseResult);\n            }\n            const error = makeErrorFromResponse(responseError);\n            error.model = data.params.model;\n            rpcBus.trigger(\"RPC:RESPONSE\", { data, settings, error });\n            reject(error);\n        });\n        // handle failure\n        request.addEventListener(\"error\", () => {\n            const error = new ConnectionLostError(url);\n            rpcBus.trigger(\"RPC:RESPONSE\", { data, settings, error });\n            reject(error);\n        });\n        // configure and send request\n        request.open(\"POST\", url);\n        const headers = settings.headers || {};\n        headers[\"Content-Type\"] = \"application/json\";\n        for (const [header, value] of Object.entries(headers)) {\n            request.setRequestHeader(header, value);\n        }\n        request.send(JSON.stringify(data));\n    });\n    /**\n     * @param {Boolean} rejectError Returns an error if true. Allows you to cancel\n     *                  ignored rpc's in order to unblock the ui and not display an error.\n     */\n    promise.abort = function (rejectError = true) {\n        if (request.abort) {\n            request.abort();\n        }\n        const error = new ConnectionAbortedError(\"XmlHttpRequestError abort\");\n        rpcBus.trigger(\"RPC:RESPONSE\", { data, settings, error });\n        if (rejectError) {\n            rejectFn(error);\n        }\n    };\n    return promise;\n};\n", "import { Deferred } from \"@web/core/utils/concurrency\";\nimport { IDBQuotaExceededError, IndexedDB } from \"@web/core/utils/indexed_db\";\nimport { deepCopy } from \"../utils/objects\";\n\n/**\n * @typedef {{\n * callback?: function;\n * type?: \"ram\" | \"disk\";\n * update?: \"once\" | \"always\";\n * }} RPCCacheSettings\n */\n\nfunction jsonEqual(v1, v2) {\n    return JSON.stringify(v1) === JSON.stringify(v2);\n}\n\nfunction validateSettings({ type, update }) {\n    if (![\"ram\", \"disk\"].includes(type)) {\n        throw new Error(`Invalid \"type\" settings provided to RPCCache: ${type}`);\n    }\n    if (![\"always\", \"once\"].includes(update)) {\n        throw new Error(`Invalid \"update\" settings provided to RPCCache: ${update}`);\n    }\n}\n\nconst CRYPTO_ALGO = \"AES-GCM\";\nconst MAX_STORAGE_SIZE = 2 * 1024 * 1024 * 1024; // 2Gb\n\nclass Crypto {\n    constructor(secret) {\n        this._cryptoKey = null;\n        this._ready = window.crypto.subtle\n            .importKey(\n                \"raw\",\n                new Uint8Array(secret.match(/../g).map((h) => parseInt(h, 16))).buffer,\n                CRYPTO_ALGO,\n                false,\n                [\"encrypt\", \"decrypt\"]\n            )\n            .then((encryptedKey) => {\n                this._cryptoKey = encryptedKey;\n            });\n    }\n\n    async encrypt(value) {\n        await this._ready;\n        // The iv must never be reused with a given key.\n        const iv = window.crypto.getRandomValues(new Uint8Array(12));\n        const ciphertext = await window.crypto.subtle.encrypt(\n            {\n                name: CRYPTO_ALGO,\n                iv,\n                length: 64, // length of the counter in bits\n            },\n            this._cryptoKey,\n            new TextEncoder().encode(JSON.stringify(value)) // encoded Data\n        );\n        return { ciphertext, iv };\n    }\n\n    async decrypt({ ciphertext, iv }) {\n        await this._ready;\n        const decrypted = await window.crypto.subtle.decrypt(\n            {\n                name: CRYPTO_ALGO,\n                iv,\n                length: 64,\n            },\n            this._cryptoKey,\n            ciphertext\n        );\n        return JSON.parse(new TextDecoder().decode(decrypted));\n    }\n}\n\nclass RamCache {\n    constructor() {\n        this.ram = {};\n    }\n\n    write(table, key, value) {\n        if (!(table in this.ram)) {\n            this.ram[table] = {};\n        }\n        this.ram[table][key] = value;\n    }\n\n    read(table, key) {\n        return this.ram[table]?.[key];\n    }\n\n    delete(table, key) {\n        delete this.ram[table]?.[key];\n    }\n\n    invalidate(tables = null) {\n        if (tables) {\n            tables = typeof tables === \"string\" ? [tables] : tables;\n            for (const table of tables) {\n                if (table in this.ram) {\n                    this.ram[table] = {};\n                }\n            }\n        } else {\n            this.ram = {};\n        }\n    }\n}\n\nexport class RPCCache {\n    constructor(name, version, secret) {\n        this.crypto = new Crypto(secret);\n        this.indexedDB = new IndexedDB(name, version + CRYPTO_ALGO);\n        this.ramCache = new RamCache();\n        this.pendingRequests = {};\n        this.checkSize(); // we want to control the disk space used by Odoo\n    }\n\n    async checkSize() {\n        const { usage } = await navigator.storage.estimate();\n        if (usage > MAX_STORAGE_SIZE) {\n            console.log(`Deleting indexedDB database as maximum storage size is reached`);\n            return this.indexedDB.deleteDatabase();\n        }\n    }\n\n    /**\n     * @param {string} table\n     * @param {string} key\n     * @param {function} fallback\n     * @param {RPCCacheSettings} settings\n     */\n    read(table, key, fallback, { callback = () => {}, type = \"ram\", update = \"once\" } = {}) {\n        validateSettings({ type, update });\n\n        let ramValue = this.ramCache.read(table, key);\n\n        const requestKey = `${table}/${key}`;\n        const hasPendingRequest = requestKey in this.pendingRequests;\n        if (hasPendingRequest) {\n            // never do the same call multiple times in parallel => return the same value for all\n            // those calls, but store their callback to call them when/if the real value is obtained\n            this.pendingRequests[requestKey].callbacks.push(callback);\n            return ramValue.then((result) => deepCopy(result));\n        }\n\n        if (!ramValue || update === \"always\") {\n            const request = { callbacks: [callback], invalidated: false };\n            this.pendingRequests[requestKey] = request;\n\n            // execute the fallback and write the result in the caches\n            const prom = new Promise((resolve, reject) => {\n                const fromCache = new Deferred();\n                let fromCacheValue;\n                const onFullfilled = (result) => {\n                    resolve(result);\n                    // call the pending request callbacks with the result\n                    const hasChanged = !!fromCacheValue && !jsonEqual(fromCacheValue, result);\n                    request.callbacks.forEach((cb) => cb(deepCopy(result), hasChanged));\n                    if (request.invalidated) {\n                        return result;\n                    }\n                    delete this.pendingRequests[requestKey];\n                    // update the ram and optionally the disk caches with the latest data\n                    this.ramCache.write(table, key, Promise.resolve(result));\n                    if (type === \"disk\") {\n                        this.crypto.encrypt(result).then((encryptedResult) => {\n                            this.indexedDB.write(table, key, encryptedResult).catch((e) => {\n                                if (e instanceof IDBQuotaExceededError) {\n                                    this.indexedDB.deleteDatabase();\n                                } else {\n                                    throw e;\n                                }\n                            });\n                        });\n                    }\n                    return result;\n                };\n                const onRejected = async (error) => {\n                    await fromCache;\n                    if (!request.invalidated) {\n                        delete this.pendingRequests[requestKey];\n                        if (!fromCacheValue) {\n                            this.ramCache.delete(table, key); // remove rejected prom from ram cache\n                        }\n                    }\n                    if (fromCacheValue) {\n                        // promise has already been fullfilled with the cached value\n                        throw error;\n                    }\n                    reject(error);\n                };\n                fallback().then(onFullfilled, onRejected);\n\n                // speed up the request by using the caches\n                if (ramValue) {\n                    // ramValue is always already resolved here, as it can't be pending (otherwise\n                    // we would have early returned because of `pendingRequests`) and it would have\n                    // been removed from the ram cache if it had been rejected\n                    // => no need to define a `catch` callback.\n                    ramValue.then((value) => {\n                        resolve(value);\n                        fromCacheValue = value;\n                        fromCache.resolve();\n                    });\n                } else if (type === \"disk\") {\n                    this.indexedDB\n                        .read(table, key)\n                        .then(async (result) => {\n                            if (result) {\n                                let decrypted;\n                                try {\n                                    decrypted = await this.crypto.decrypt(result);\n                                } catch {\n                                    // Do nothing ! The cryptoKey is probably different.\n                                    // The data will be updated with the new cryptoKey.\n                                    return;\n                                }\n                                resolve(decrypted);\n                                fromCacheValue = decrypted;\n                            }\n                        })\n                        .finally(() => fromCache.resolve());\n                } else {\n                    fromCache.resolve(); // fromCacheValue will remain undefined\n                }\n            });\n            this.ramCache.write(table, key, prom);\n            ramValue = prom;\n        }\n\n        return ramValue.then((result) => deepCopy(result));\n    }\n\n    invalidate(tables) {\n        this.indexedDB.invalidate(tables);\n        this.ramCache.invalidate(tables);\n        // flag the pending requests as invalidated s.t. we don't write their results in caches\n        for (const key in this.pendingRequests) {\n            this.pendingRequests[key].invalidated = true;\n        }\n        this.pendingRequests = {};\n    }\n}\n", "import { Component, onWillRender, onWillUpdateProps, useEffect, useRef, useState } from \"@odoo/owl\";\n\n/**\n * A notebook component that will render only the current page and allow\n * switching between its pages.\n *\n * You can also set pages using a template component. Use an array with\n * the `pages` props to do such rendering.\n *\n * Pages can also specify their index in the notebook.\n *\n *      e.g.:\n *          PageTemplate.template = xml`\n                    <h1 t-esc=\"props.heading\" />\n                    <p t-esc=\"props.text\" />`;\n\n *      `pages` could be:\n *      [\n *          {\n *              Component: PageTemplate,\n *              id: 'unique_id' // optional: can be given as defaultPage props to the notebook\n *              index: 1 // optional: page position in the notebook\n *              name: 'some_name' // optional\n *              title: \"Some Title 1\", // title displayed on the tab pane\n *              props: {\n *                  heading: \"Page 1\",\n *                  text: \"Text Content 1\",\n *              },\n *          },\n *          {\n *              Component: PageTemplate,\n *              title: \"Some Title 2\",\n *              props: {\n *                  heading: \"Page 2\",\n *                  text: \"Text Content 2\",\n *              },\n *          },\n *      ]\n *\n * <Notebook pages=\"pages\">\n *    <t t-set-slot=\"Page Name 1\" title=\"Some Title\" isVisible=\"bool\">\n *      <div>Page Content 1</div>\n *    </t>\n *    <t t-set-slot=\"Page Name 2\" title=\"Some Title\" isVisible=\"bool\">\n *      <div>Page Content 2</div>\n *    </t>\n * </Notebook>\n *\n * @extends Component\n */\n\nexport class Notebook extends Component {\n    static template = \"web.Notebook\";\n    static defaultProps = {\n        className: \"\",\n        orientation: \"horizontal\",\n        onPageUpdate: () => {},\n    };\n    static props = {\n        slots: { type: Object, optional: true },\n        pages: { type: Object, optional: true },\n        class: { optional: true },\n        className: { type: String, optional: true },\n        defaultPage: { type: String, optional: true },\n        orientation: { type: String, optional: true },\n        icons: { type: Object, optional: true },\n        onPageUpdate: { type: Function, optional: true },\n    };\n\n    setup() {\n        this.activePane = useRef(\"activePane\");\n        this.pages = this.computePages(this.props);\n        this.invalidPages = new Set();\n        this.state = useState({ currentPage: null });\n        this.state.currentPage = this.computeActivePage(this.props.defaultPage, true);\n        useEffect(\n            () => {\n                this.props.onPageUpdate(this.state.currentPage);\n                this.activePane.el?.classList.add(\"show\");\n            },\n            () => [this.state.currentPage]\n        );\n        onWillRender(() => {\n            this.computeInvalidPages();\n        });\n        onWillUpdateProps((nextProps) => {\n            const activateDefault =\n                this.props.defaultPage !== nextProps.defaultPage || !this.defaultVisible;\n            this.pages = this.computePages(nextProps);\n            this.state.currentPage = this.computeActivePage(nextProps.defaultPage, activateDefault);\n        });\n    }\n\n    get navItems() {\n        return this.pages.filter((e) => e[1].isVisible);\n    }\n\n    get page() {\n        const page = this.pages.find((e) => e[0] === this.state.currentPage)[1];\n        return page.Component && page;\n    }\n\n    activatePage(pageIndex) {\n        if (!this.disabledPages.includes(pageIndex) && this.state.currentPage !== pageIndex) {\n            this.activePane.el?.classList.remove(\"show\");\n            this.state.currentPage = pageIndex;\n        }\n    }\n\n    computePages(props) {\n        if (!props.slots && !props.pages) {\n            return [];\n        }\n        if (props.pages) {\n            for (const page of props.pages) {\n                page.isVisible = true;\n            }\n        }\n        this.disabledPages = [];\n        const pages = [];\n        const pagesWithIndex = [];\n        for (const [k, v] of Object.entries({ ...props.slots, ...props.pages })) {\n            const id = v.id || k;\n            if (v.index) {\n                pagesWithIndex.push([id, v]);\n            } else {\n                pages.push([id, v]);\n            }\n            if (v.isDisabled) {\n                this.disabledPages.push(k);\n            }\n        }\n        for (const page of pagesWithIndex) {\n            pages.splice(page[1].index, 0, page);\n        }\n        return pages;\n    }\n\n    computeActivePage(defaultPage, activateDefault) {\n        if (!this.pages.length) {\n            return null;\n        }\n        const pages = this.pages.filter((e) => e[1].isVisible).map((e) => e[0]);\n\n        if (defaultPage) {\n            if (!pages.includes(defaultPage)) {\n                this.defaultVisible = false;\n            } else {\n                this.defaultVisible = true;\n                if (activateDefault) {\n                    return defaultPage;\n                }\n            }\n        }\n        const current = this.state.currentPage;\n        if (!current || (current && !pages.includes(current))) {\n            return pages[0];\n        }\n\n        return current;\n    }\n\n    computeInvalidPages() {\n        this.invalidPages = new Set();\n        for (const page of this.navItems) {\n            const invalid = page[1].fieldNames?.some((fieldName) =>\n                this.env.model?.root.isFieldInvalid(fieldName)\n            );\n            if (invalid) {\n                this.invalidPages.add(page[0]);\n            }\n        }\n    }\n}\n", "import { Component, useRef, onMounted } from \"@odoo/owl\";\n\nconst AUTOCLOSE_DELAY = 4000;\n\nexport class Notification extends Component {\n    static template = \"web.NotificationWowl\";\n    static props = {\n        message: {\n            validate: (m) =>\n                typeof m === \"string\" ||\n                (typeof m === \"object\" && typeof m.toString === \"function\"),\n        },\n        type: {\n            type: String,\n            optional: true,\n            validate: (t) => [\"warning\", \"danger\", \"success\", \"info\"].includes(t),\n        },\n        title: { type: [String, Boolean, { toString: Function }], optional: true },\n        className: { type: String, optional: true },\n        buttons: {\n            type: Array,\n            element: {\n                type: Object,\n                shape: {\n                    name: { type: String },\n                    icon: { type: String, optional: true },\n                    primary: { type: Boolean, optional: true },\n                    onClick: Function,\n                },\n            },\n            optional: true,\n        },\n        sticky: { type: Boolean, optional: true },\n        autocloseDelay: { type: Number, optional: true },\n        close: { type: Function },\n    };\n    static defaultProps = {\n        buttons: [],\n        className: \"\",\n        type: \"warning\",\n        autocloseDelay: AUTOCLOSE_DELAY,\n    };\n    setup() {\n        this.autocloseProgress = useRef(\"autoclose_progress_bar\");\n        onMounted(() => this.startNotificationTimer());\n    }\n\n    freeze() {\n        this.startedTimestamp = false;\n        this.autocloseProgress.el.style.width = 0;\n    }\n\n    refresh() {\n        this.startNotificationTimer();\n    }\n\n    close() {\n        this.props.close();\n    }\n\n    startNotificationTimer() {\n        if (this.props.sticky) {\n            return;\n        }\n        this.startedTimestamp = luxon.DateTime.now().ts;\n\n        const cb = () => {\n            if (this.startedTimestamp) {\n                const currentProgress =\n                    (luxon.DateTime.now().ts - this.startedTimestamp) / this.props.autocloseDelay;\n                if (currentProgress > 1) {\n                    this.close();\n                    return;\n                }\n                if (this.autocloseProgress.el) {\n                    this.autocloseProgress.el.style.width = `${(1 - currentProgress) * 100}%`;\n                }\n                requestAnimationFrame(cb);\n            }\n        };\n        cb();\n    }\n}\n", "import { Notification } from \"./notification\";\nimport { Transition } from \"@web/core/transition\";\n\nimport { Component, xml, useState } from \"@odoo/owl\";\n\nexport class NotificationContainer extends Component {\n    static props = {\n        notifications: Object,\n    };\n\n    static template = xml`\n        <div class=\"o_notification_manager\">\n            <t t-foreach=\"notifications\" t-as=\"notification\" t-key=\"notification\">\n                <Transition leaveDuration=\"0\" immediate=\"true\" name=\"'o_notification_fade'\" t-slot-scope=\"transition\">\n                    <Notification t-props=\"notification_value.props\" className=\"(notification_value.props.className || '') + ' ' + transition.className\"/>\n                </Transition>\n            </t>\n        </div>`;\n    static components = { Notification, Transition };\n\n    setup() {\n        this.notifications = useState(this.props.notifications);\n    }\n}\n", "import { registry } from \"../registry\";\nimport { NotificationContainer } from \"./notification_container\";\n\nimport { reactive } from \"@odoo/owl\";\n\n/**\n * @typedef {Object} NotificationButton\n * @property {string} name\n * @property {string} [icon]\n * @property {boolean} [primary=false]\n * @property {function(): void} onClick\n *\n * @typedef {Object} NotificationOptions\n * @property {string} [title]\n * @property {number} [autocloseDelay=4000]\n * @property {\"warning\" | \"danger\" | \"success\" | \"info\"} [type]\n * @property {boolean} [sticky=false]\n * @property {string} [className]\n * @property {function(): void} [onClose]\n * @property {NotificationButton[]} [buttons]\n */\n\nexport const notificationService = {\n    notificationContainer: NotificationContainer,\n\n    start() {\n        let notifId = 0;\n        const notifications = reactive({});\n\n        registry.category(\"main_components\").add(\n            this.notificationContainer.name,\n            {\n                Component: this.notificationContainer,\n                props: { notifications },\n            },\n            { sequence: 100 }\n        );\n\n        /**\n         * @param {string} message\n         * @param {NotificationOptions} [options]\n         */\n        function add(message, options = {}) {\n            const id = ++notifId;\n            const closeFn = () => close(id);\n            const props = Object.assign({}, options, { message, close: closeFn });\n            delete props.onClose;\n            const notification = {\n                id,\n                props,\n                onClose: options.onClose,\n            };\n            notifications[id] = notification;\n            return closeFn;\n        }\n\n        function close(id) {\n            if (notifications[id]) {\n                const notification = notifications[id];\n                if (notification.onClose) {\n                    notification.onClose();\n                }\n                delete notifications[id];\n            }\n        }\n\n        return { add };\n    },\n};\n\nregistry.category(\"services\").add(\"notification\", notificationService);\n", "import { registry } from \"@web/core/registry\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { user } from \"@web/core/user\";\nimport { Domain } from \"@web/core/domain\";\n\n/**\n * This ORM service is the standard way to interact with the ORM in python from\n * the javascript codebase.\n */\n\n// -----------------------------------------------------------------------------\n// ORM\n// -----------------------------------------------------------------------------\n\n/**\n * One2many and Many2many fields expect a special command to manipulate the\n * relation they implement.\n *\n * Internally, each command is a 3-elements tuple where the first element is a\n * mandatory integer that identifies the command, the second element is either\n * the related record id to apply the command on (commands update, delete,\n * unlink and link) either 0 (commands create, clear and set), the third\n * element is either the ``values`` to write on the record (commands create\n * and update) either the new ``ids`` list of related records (command set),\n * either 0 (commands delete, unlink, link, and clear).\n */\nexport const x2ManyCommands = {\n    // (0, virtualID | false, { values })\n    CREATE: 0,\n    create(virtualID, values) {\n        delete values.id;\n        return [x2ManyCommands.CREATE, virtualID || false, values];\n    },\n    // (1, id, { values })\n    UPDATE: 1,\n    update(id, values) {\n        delete values.id;\n        return [x2ManyCommands.UPDATE, id, values];\n    },\n    // (2, id[, _])\n    DELETE: 2,\n    delete(id) {\n        return [x2ManyCommands.DELETE, id, false];\n    },\n    // (3, id[, _]) removes relation, but not linked record itself\n    UNLINK: 3,\n    unlink(id) {\n        return [x2ManyCommands.UNLINK, id, false];\n    },\n    // (4, id[, _])\n    LINK: 4,\n    link(id) {\n        return [x2ManyCommands.LINK, id, false];\n    },\n    // (5[, _[, _]])\n    CLEAR: 5,\n    clear() {\n        return [x2ManyCommands.CLEAR, false, false];\n    },\n    // (6, _, ids) replaces all linked records with provided ids\n    SET: 6,\n    set(ids) {\n        return [x2ManyCommands.SET, false, ids];\n    },\n};\n\nfunction validateModel(value) {\n    if (typeof value !== \"string\" || value.length === 0) {\n        throw new Error(`Invalid model name: ${value}`);\n    }\n}\nfunction validatePrimitiveList(name, type, value) {\n    if (!Array.isArray(value) || value.some((val) => typeof val !== type)) {\n        throw new Error(`Invalid ${name} list: ${value}`);\n    }\n}\nfunction validateObject(name, obj) {\n    if (typeof obj !== \"object\" || obj === null || Array.isArray(obj)) {\n        throw new Error(`${name} should be an object`);\n    }\n}\nfunction validateArray(name, array) {\n    if (!Array.isArray(array)) {\n        throw new Error(`${name} should be an array`);\n    }\n}\n\nexport const UPDATE_METHODS = [\n    \"unlink\",\n    \"create\",\n    \"write\",\n    \"web_save\",\n    \"web_save_multi\",\n    \"action_archive\",\n    \"action_unarchive\",\n];\n\nexport class ORM {\n    constructor() {\n        this.rpc = rpc; // to be overridable by the SampleORM\n        /** @protected */\n        this._silent = false;\n        this._cache = false;\n    }\n\n    /** @returns {ORM} */\n    get silent() {\n        return Object.assign(Object.create(this), { _silent: true });\n    }\n\n    /**\n     * @param {object} options\n     * @returns {ORM}\n     */\n    cache(options = {}) {\n        return Object.assign(Object.create(this), { _cache: options });\n    }\n\n    /**\n     * @param {string} model\n     * @param {string} method\n     * @param {any[]} [args=[]]\n     * @param {any} [kwargs={}]\n     * @returns {Promise<any>}\n     */\n    call(model, method, args = [], kwargs = {}) {\n        validateModel(model);\n        const url = `/web/dataset/call_kw/${model}/${method}`;\n        const fullContext = Object.assign({}, user.context, kwargs.context || {});\n        const fullKwargs = Object.assign({}, kwargs, { context: fullContext });\n        const params = {\n            model,\n            method,\n            args,\n            kwargs: fullKwargs,\n        };\n        return this.rpc(url, params, {\n            silent: this._silent,\n            cache: this._cache,\n        });\n    }\n\n    /**\n     * @param {string} model\n     * @param {any[]} records\n     * @param {any} [kwargs=[]]\n     * @returns {Promise<number>}\n     */\n    create(model, records, kwargs = {}) {\n        validateArray(\"records\", records);\n        for (const record of records) {\n            validateObject(\"record\", record);\n        }\n        return this.call(model, \"create\", [records], kwargs);\n    }\n\n    /**\n     * @param {string} model\n     * @param {number[]} ids\n     * @param {string[]} fields\n     * @param {any} [kwargs={}]\n     * @returns {Promise<any[]>}\n     */\n    read(model, ids, fields, kwargs = {}) {\n        validatePrimitiveList(\"ids\", \"number\", ids);\n        if (fields) {\n            validatePrimitiveList(\"fields\", \"string\", fields);\n        }\n        if (!ids.length) {\n            return Promise.resolve([]);\n        }\n        return this.call(model, \"read\", [ids, fields], kwargs);\n    }\n\n    /**\n     * @param {string} model\n     * @param {import(\"@web/core/domain\").DomainListRepr} domain\n     * @param {string[]} fields\n     * @param {string[]} groupby\n     * @param {any} [kwargs={}]\n     * @returns {Promise<any[]>}\n     */\n    formattedReadGroup(model, domain, groupby, aggregates, kwargs = {}) {\n        validateArray(\"domain\", domain);\n        validatePrimitiveList(\"groupby\", \"string\", groupby);\n        validatePrimitiveList(\"aggregates\", \"string\", aggregates);\n        return this.call(model, \"formatted_read_group\", [], {\n            domain,\n            groupby,\n            aggregates,\n            ...kwargs,\n        }).then((res) => {\n            for (const group of res) {\n                group[\"__domain\"] = Domain.and([domain, group[\"__extra_domain\"]]).toList();\n            }\n            return res;\n        });\n    }\n\n    /**\n     * @param {string} model\n     * @param {import(\"@web/core/domain\").DomainListRepr} domain\n     * @param {string[]} fields\n     * @param {string[][]} grouping_sets\n     * @param {any} [kwargs={}]\n     * @returns {Promise<any[]>}\n     */\n    formattedReadGroupingSets(model, domain, grouping_sets, aggregates, kwargs = {}) {\n        validateArray(\"domain\", domain);\n        validateArray(\"groupby\", grouping_sets);\n        validatePrimitiveList(\"aggregates\", \"string\", aggregates);\n        return this.call(model, \"formatted_read_grouping_sets\", [], {\n            domain,\n            grouping_sets,\n            aggregates,\n            ...kwargs,\n        }).then((res) => {\n            for (const groups of res) {\n                for (const group of groups) {\n                    group[\"__domain\"] = Domain.and([domain, group[\"__extra_domain\"]]).toList();\n                }\n            }\n            return res;\n        });\n    }\n\n    /**\n     * @param {string} model\n     * @param {import(\"@web/core/domain\").DomainListRepr} domain\n     * @param {any} [kwargs={}]\n     * @returns {Promise<any[]>}\n     */\n    search(model, domain, kwargs = {}) {\n        validateArray(\"domain\", domain);\n        return this.call(model, \"search\", [domain], kwargs);\n    }\n\n    /**\n     * @param {string} model\n     * @param {import(\"@web/core/domain\").DomainListRepr} domain\n     * @param {string[]} fields\n     * @param {any} [kwargs={}]\n     * @returns {Promise<any[]>}\n     */\n    searchRead(model, domain, fields, kwargs = {}) {\n        validateArray(\"domain\", domain);\n        if (fields) {\n            validatePrimitiveList(\"fields\", \"string\", fields);\n        }\n        return this.call(model, \"search_read\", [], { ...kwargs, domain, fields });\n    }\n\n    /**\n     * @param {string} model\n     * @param {import(\"@web/core/domain\").DomainListRepr} domain\n     * @param {any} [kwargs={}]\n     * @returns {Promise<number>}\n     */\n    searchCount(model, domain, kwargs = {}) {\n        validateArray(\"domain\", domain);\n        return this.call(model, \"search_count\", [domain], kwargs);\n    }\n\n    /**\n     * @param {string} model\n     * @param {number[]} ids\n     * @param {any} [kwargs={}]\n     * @returns {Promise<boolean>}\n     */\n    unlink(model, ids, kwargs = {}) {\n        validatePrimitiveList(\"ids\", \"number\", ids);\n        if (!ids.length) {\n            return Promise.resolve(true);\n        }\n        return this.call(model, \"unlink\", [ids], kwargs);\n    }\n\n    /**\n     * @param {string} model\n     * @param {import(\"@web/core/domain\").DomainListRepr} domain\n     * @param {string[]} groupby\n     * @param {string[]} aggregates\n     * @param {any} [kwargs={}]\n     * @returns {Promise<any[]>}\n     */\n    webReadGroup(model, domain, groupby, aggregates, kwargs = {}) {\n        validateArray(\"domain\", domain);\n        validatePrimitiveList(\"aggregates\", \"string\", aggregates);\n        return this.call(model, \"web_read_group\", [], {\n            domain,\n            groupby,\n            aggregates,\n            ...kwargs,\n        });\n    }\n\n    /**\n     * @param {string} model\n     * @param {number[]} ids\n     * @param {any} [kwargs={}]\n     * @param {Object} [kwargs.specification]\n     * @param {Object} [kwargs.context]\n     * @returns {Promise<any[]>}\n     */\n    webRead(model, ids, kwargs = {}) {\n        validatePrimitiveList(\"ids\", \"number\", ids);\n        return this.call(model, \"web_read\", [ids], kwargs);\n    }\n\n    /**\n     * @param {string} model\n     * @param {number[]} ids\n     * @param {object} [kwargs={}]\n     * @param {object} [kwargs.context]\n     * @param {string} [kwargs.field_name]\n     * @param {number} [kwargs.offset]\n     * @param {object} [kwargs.specification]\n     * @returns {Promise<any[]>}\n     */\n    webResequence(model, ids, kwargs = {}) {\n        validatePrimitiveList(\"ids\", \"number\", ids);\n        return this.call(model, \"web_resequence\", [ids], {\n            ...kwargs,\n            specification: kwargs.specification || {},\n        });\n    }\n\n    /**\n     * @param {string} model\n     * @param {import(\"@web/core/domain\").DomainListRepr} domain\n     * @param {any} [kwargs={}]\n     * @returns {Promise<any[]>}\n     */\n    webSearchRead(model, domain, kwargs = {}) {\n        validateArray(\"domain\", domain);\n        return this.call(model, \"web_search_read\", [], { ...kwargs, domain });\n    }\n\n    /**\n     * @param {string} model\n     * @param {number[]} ids\n     * @param {any} data\n     * @param {any} [kwargs={}]\n     * @returns {Promise<boolean>}\n     */\n    write(model, ids, data, kwargs = {}) {\n        validatePrimitiveList(\"ids\", \"number\", ids);\n        validateObject(\"data\", data);\n        return this.call(model, \"write\", [ids, data], kwargs);\n    }\n\n    /**\n     * @param {string} model\n     * @param {number[]} ids\n     * @param {any} data\n     * @param {any} [kwargs={}]\n     * @param {Object} [kwargs.specification]\n     * @param {Object} [kwargs.context]\n     * @returns {Promise<any[]>}\n     */\n    webSave(model, ids, data, kwargs = {}) {\n        validatePrimitiveList(\"ids\", \"number\", ids);\n        validateObject(\"data\", data);\n        return this.call(model, \"web_save\", [ids, data], kwargs);\n    }\n\n    /**\n     * @param {string} model\n     * @param {number[]} ids\n     * @param {Object[]} data\n     * @param {Object} [kwargs={}]\n     * @param {Object} [kwargs.specification]\n     * @param {Object} [kwargs.context]\n     * @returns {Promise<any[]>}\n     */\n    async webSaveMulti(model, ids, data, kwargs = {}) {\n        validatePrimitiveList(\"ids\", \"number\", ids);\n        validateArray(\"data\", data);\n        data.forEach((d) => {\n            validateObject(\"data item\", d);\n        });\n        return this.call(model, \"web_save_multi\", [ids, data], kwargs);\n    }\n}\n\n/**\n * Note:\n *\n * To hide RPC errors, use the following API:\n *\n * this.orm = useService('orm');\n * ...\n * const result = await this.orm.silent.read('res.partner', [id]);\n */\nexport const ormService = {\n    async: [\n        \"call\",\n        \"create\",\n        \"nameGet\",\n        \"read\",\n        \"formattedReadGroup\",\n        \"webReadGroup\",\n        \"search\",\n        \"searchRead\",\n        \"unlink\",\n        \"webResequence\",\n        \"webSearchRead\",\n        \"write\",\n    ],\n    start() {\n        return new ORM();\n    },\n};\n\nregistry.category(\"services\").add(\"orm\", ormService);\n", "import { Component, onWillDestroy, useChildSubEnv, useEffect, useRef, useState } from \"@odoo/owl\";\nimport { sortBy } from \"@web/core/utils/arrays\";\nimport { ErrorHandler } from \"@web/core/utils/components\";\n\nconst OVERLAY_ITEMS = [];\nexport const OVERLAY_SYMBOL = Symbol(\"Overlay\");\n\nclass OverlayItem extends Component {\n    static template = \"web.OverlayContainer.Item\";\n    static components = {};\n    static props = {\n        component: { type: Function },\n        props: { type: Object },\n        env: { type: Object, optional: true },\n    };\n\n    setup() {\n        this.rootRef = useRef(\"rootRef\");\n\n        OVERLAY_ITEMS.push(this);\n        onWillDestroy(() => {\n            const index = OVERLAY_ITEMS.indexOf(this);\n            OVERLAY_ITEMS.splice(index, 1);\n        });\n\n        if (this.props.env) {\n            this.__owl__.childEnv = this.props.env;\n        }\n\n        useChildSubEnv({\n            [OVERLAY_SYMBOL]: {\n                contains: (target) => this.contains(target),\n            },\n        });\n    }\n\n    get subOverlays() {\n        return OVERLAY_ITEMS.slice(OVERLAY_ITEMS.indexOf(this));\n    }\n\n    contains(target) {\n        return (\n            this.rootRef.el?.contains(target) ||\n            this.subOverlays.some((oi) => oi.rootRef.el?.contains(target))\n        );\n    }\n}\n\nexport class OverlayContainer extends Component {\n    static template = \"web.OverlayContainer\";\n    static components = { ErrorHandler, OverlayItem };\n    static props = { overlays: Object };\n\n    setup() {\n        this.root = useRef(\"root\");\n        this.state = useState({ rootEl: null });\n        useEffect(\n            () => {\n                this.state.rootEl = this.root.el;\n            },\n            () => [this.root.el]\n        );\n    }\n\n    get sortedOverlays() {\n        return sortBy(Object.values(this.props.overlays), (overlay) => overlay.sequence);\n    }\n\n    isVisible(overlay) {\n        return overlay.rootId === this.state.rootEl?.getRootNode()?.host?.id;\n    }\n\n    handleError(overlay, error) {\n        overlay.remove();\n        Promise.resolve().then(() => {\n            throw error;\n        });\n    }\n}\n", "import { markRaw, reactive } from \"@odoo/owl\";\nimport { registry } from \"../registry\";\nimport { OverlayContainer } from \"./overlay_container\";\n\nconst mainComponents = registry.category(\"main_components\");\nconst services = registry.category(\"services\");\n\n/**\n * @typedef {{\n *  env?: object;\n *  onRemove?: () => void;\n *  sequence?: number;\n *  rootId?: string;\n * }} OverlayServiceAddOptions\n */\n\nexport const overlayService = {\n    start() {\n        let nextId = 0;\n        const overlays = reactive({});\n\n        mainComponents.add(\"OverlayContainer\", {\n            Component: OverlayContainer,\n            props: { overlays },\n        });\n\n        const remove = async (id, onRemove = () => {}, removeParams) => {\n            if (id in overlays) {\n                await onRemove(removeParams);\n                delete overlays[id];\n            }\n        };\n\n        /**\n         * @param {typeof Component} component\n         * @param {object} props\n         * @param {OverlayServiceAddOptions} [options]\n         * @returns {() => void}\n         */\n        const add = (component, props, options = {}) => {\n            const id = ++nextId;\n            const removeCurrentOverlay = (removeParams) =>\n                remove(id, options.onRemove, removeParams);\n            overlays[id] = {\n                id,\n                component,\n                env: options.env && markRaw(options.env),\n                props,\n                remove: removeCurrentOverlay,\n                sequence: options.sequence ?? 50,\n                rootId: options.rootId,\n            };\n            return removeCurrentOverlay;\n        };\n\n        return { add, overlays };\n    },\n};\n\nservices.add(\"overlay\", overlayService);\n", "import { useAutofocus } from \"../utils/hooks\";\nimport { clamp } from \"../utils/numbers\";\n\nimport { Component, EventBus, useEffect, useExternalListener, useState } from \"@odoo/owl\";\n\nexport const PAGER_UPDATED_EVENT = \"PAGER:UPDATED\";\nexport const pagerBus = new EventBus();\n\n/**\n * Pager\n *\n * The pager goes from 1 to total (included).\n * The current value is minimum if limit === 1 or the interval:\n *      [minimum, minimum + limit[ if limit > 1].\n * The value can be manually changed by clicking on the pager value and giving\n * an input matching the pattern: min[,max] (in which the comma can be a dash\n * or a semicolon).\n * The pager also provides two buttons to quickly change the current page (next\n * or previous).\n * @extends Component\n */\nexport class Pager extends Component {\n    static template = \"web.Pager\";\n    static defaultProps = {\n        isEditable: true,\n        withAccessKey: true,\n    };\n    static props = {\n        offset: Number,\n        limit: Number,\n        total: Number,\n        onUpdate: Function,\n        isEditable: { type: Boolean, optional: true },\n        withAccessKey: { type: Boolean, optional: true },\n        updateTotal: { type: Function, optional: true },\n    };\n\n    setup() {\n        this.state = useState({\n            isEditing: false,\n            isDisabled: false,\n        });\n        this.inputRef = useAutofocus();\n        useExternalListener(document, \"mousedown\", this.onClickAway, { capture: true });\n        let firstMount = true;\n        useEffect(\n            () => {\n                if (!firstMount && this.env.isSmall) {\n                    pagerBus.trigger(PAGER_UPDATED_EVENT, {\n                        value: this.value,\n                        total: this.props.total,\n                    });\n                }\n                firstMount = false;\n            },\n            () => [this.props.offset, this.props.limit, this.props.total]\n        );\n    }\n\n    /**\n     * @returns {number}\n     */\n    get minimum() {\n        return this.props.offset + 1;\n    }\n    /**\n     * @returns {number}\n     */\n    get maximum() {\n        return Math.min(this.props.offset + this.props.limit, this.props.total);\n    }\n    /**\n     * @returns {string}\n     */\n    get value() {\n        const parts = [this.minimum];\n        if (this.props.limit > 1) {\n            parts.push(this.maximum);\n        }\n        return parts.join(\"-\");\n    }\n    /**\n     * Note: returns false if we received the props \"updateTotal\", as in this case we don't know\n     * the real total so we can't assert that there's a single page.\n     * @returns {boolean} true if there is only one page\n     */\n    get isSinglePage() {\n        return !this.props.updateTotal && this.minimum === 1 && this.maximum === this.props.total;\n    }\n    /**\n     * @param {-1 | 1} direction\n     */\n    async navigate(direction) {\n        let minimum = this.props.offset + this.props.limit * direction;\n        let total = this.props.total;\n        if (this.props.updateTotal && minimum < 0) {\n            // we must know the real total to be able to loop by doing \"previous\"\n            total = await this.props.updateTotal();\n        }\n        if (minimum >= total) {\n            if (!this.props.updateTotal) {\n                // only loop forward if we know the real total, otherwise let the minimum\n                // go out of range\n                minimum = 0;\n            }\n        } else if (minimum < 0 && this.props.limit === 1) {\n            minimum = total - 1;\n        } else if (minimum < 0 && this.props.limit > 1) {\n            minimum = total - (total % this.props.limit || this.props.limit);\n        }\n        this.update(minimum, this.props.limit, true);\n    }\n    /**\n     * @param {string} value\n     * @returns {{ minimum: number, maximum: number }}\n     */\n    async parse(value) {\n        let [minimum, maximum] = value.trim().split(/\\s*[-\\s,;]\\s*/);\n        minimum = parseInt(minimum, 10);\n        maximum = maximum ? parseInt(maximum, 10) : minimum;\n        if (this.props.updateTotal) {\n            // we don't know the real total, so we can't clamp\n            return { minimum: minimum - 1, maximum };\n        }\n        return {\n            minimum: clamp(minimum, 1, this.props.total) - 1,\n            maximum: clamp(maximum, 1, this.props.total),\n        };\n    }\n    /**\n     * @param {string} value\n     */\n    async setValue(value) {\n        const { minimum, maximum } = await this.parse(value);\n\n        if (!isNaN(minimum) && !isNaN(maximum) && minimum < maximum) {\n            this.update(minimum, maximum - minimum);\n        }\n    }\n    /**\n     * @param {number} offset\n     * @param {number} limit\n     * @param {Boolean} hasNavigated\n     */\n    async update(offset, limit, hasNavigated) {\n        this.state.isDisabled = true;\n        try {\n            await this.props.onUpdate({ offset, limit }, hasNavigated);\n        } finally {\n            this.state.isDisabled = false;\n            this.state.isEditing = false;\n        }\n    }\n\n    async updateTotal() {\n        if (!this.state.isDisabled) {\n            this.state.isDisabled = true;\n            await this.props.updateTotal();\n            this.state.isDisabled = false;\n        }\n    }\n\n    /**\n     * @param {MouseEvent} ev\n     */\n    onClickAway(ev) {\n        if (ev.target !== this.inputRef.el) {\n            this.state.isEditing = false;\n        }\n    }\n    onInputBlur() {\n        this.state.isEditing = false;\n    }\n    /**\n     * @param {Event} ev\n     */\n    onInputChange(ev) {\n        this.setValue(ev.target.value);\n        if (!this.state.isDisabled) {\n            ev.preventDefault();\n        }\n    }\n    /**\n     * @param {KeyboardEvent} ev\n     */\n    onInputKeydown(ev) {\n        switch (ev.key) {\n            case \"Enter\":\n                ev.preventDefault();\n                ev.stopPropagation();\n                this.setValue(ev.currentTarget.value);\n                break;\n            case \"Escape\":\n                ev.preventDefault();\n                ev.stopPropagation();\n                this.state.isEditing = false;\n                break;\n        }\n    }\n    onValueClick() {\n        if (this.props.isEditable && !this.state.isEditing && !this.state.isDisabled) {\n            if (this.inputRef.el) {\n                this.inputRef.el.focus();\n            }\n            this.state.isEditing = true;\n        }\n    }\n}\n", "import { browser } from \"../browser/browser\";\nimport { registry } from \"../registry\";\nimport { Transition } from \"../transition\";\nimport { useBus } from \"../utils/hooks\";\n\nimport { Component, useState } from \"@odoo/owl\";\nimport { PAGER_UPDATED_EVENT, pagerBus } from \"./pager\";\n\nexport class PagerIndicator extends Component {\n    static template = \"web.PagerIndicator\";\n    static components = { Transition };\n    static props = {};\n\n    setup() {\n        this.state = useState({\n            show: false,\n            value: \"-\",\n            total: 0,\n        });\n        this.startShowTimer = null;\n        useBus(pagerBus, PAGER_UPDATED_EVENT, this.pagerUpdate);\n    }\n\n    pagerUpdate({ detail }) {\n        this.state.value = detail.value;\n        this.state.total = detail.total;\n        browser.clearTimeout(this.startShowTimer);\n        this.state.show = true;\n        this.startShowTimer = browser.setTimeout(() => {\n            this.state.show = false;\n        }, 1400);\n    }\n}\n\nregistry.category(\"main_components\").add(\"PagerIndicator\", {\n    Component: PagerIndicator,\n});\n", "import { Component, onMounted, onWillDestroy, useRef } from \"@odoo/owl\";\nimport { useHotkey } from \"@web/core/hotkeys/hotkey_hook\";\nimport { OVERLAY_SYMBOL } from \"@web/core/overlay/overlay_container\";\nimport { usePosition } from \"@web/core/position/position_hook\";\nimport { reverseForRTL } from \"@web/core/position/utils\";\nimport { useActiveElement } from \"@web/core/ui/ui_service\";\nimport { mergeClasses } from \"@web/core/utils/classname\";\nimport { useForwardRefToParent } from \"@web/core/utils/hooks\";\n\n/**\n * @param {EventTarget} target\n * @param {keyof HTMLElementEventMap | keyof WindowEventMap} eventName\n * @param {(ev: Event) => any} handler\n * @param {EventInit} [eventParams]\n */\nfunction useEarlyExternalListener(target, eventName, handler, eventParams) {\n    target.addEventListener(eventName, handler, eventParams);\n    onWillDestroy(() => target.removeEventListener(eventName, handler, eventParams));\n}\n\n/**\n * Will trigger the callback when the window is clicked, giving\n * the clicked element as parameter.\n *\n * This also handles the case where an iframe is clicked.\n *\n * @param {(node?: Node) => any} callback\n */\nfunction useClickAway(callback) {\n    function blurHandler(ev) {\n        const target = ev.relatedTarget || document.activeElement;\n        if (target?.tagName === \"IFRAME\") {\n            callback(target);\n        }\n    }\n\n    function navigationHandler() {\n        callback(document.documentElement);\n    }\n\n    function pointerDownHandler(ev) {\n        callback(ev.composedPath()[0]);\n    }\n\n    useEarlyExternalListener(window, \"pointerdown\", pointerDownHandler, { capture: true });\n    useEarlyExternalListener(window, \"blur\", blurHandler, { capture: true });\n    useEarlyExternalListener(window, \"popstate\", navigationHandler, { capture: true });\n}\n\nconst POPOVERS = new WeakMap();\n/**\n * Can be used to retrieve the popover element for a given target.\n * @param {HTMLElement} target\n * @returns {HTMLElement | undefined} the popover element if it exists\n */\nexport function getPopoverForTarget(target) {\n    return POPOVERS.get(target);\n}\n\nexport class Popover extends Component {\n    static template = \"web.Popover\";\n    static defaultProps = {\n        animation: true,\n        arrow: true,\n        class: \"\",\n        closeOnClickAway: () => true,\n        closeOnEscape: true,\n        componentProps: {},\n        fixedPosition: false,\n        position: \"bottom\",\n        setActiveElement: false,\n    };\n    static props = {\n        // Main props\n        component: { type: Function },\n        componentProps: { optional: true, type: Object },\n        target: {\n            validate: (target) => {\n                // target may be inside an iframe, so get the Element constructor\n                // to test against from its owner document's default view\n                const Element = target?.ownerDocument?.defaultView?.Element;\n                return (\n                    (Boolean(Element) &&\n                        (target instanceof Element || target instanceof window.Element)) ||\n                    (typeof target === \"object\" && target?.constructor?.name?.endsWith(\"Element\"))\n                );\n            },\n        },\n        close: { type: Function },\n\n        // Styling and semantical props\n        animation: { optional: true, type: Boolean },\n        arrow: { optional: true, type: Boolean },\n        class: { optional: true },\n        role: { optional: true, type: String },\n\n        // Positioning props\n        fixedPosition: { optional: true, type: Boolean },\n        extendedFlipping: { optional: true, type: Boolean },\n        holdOnHover: { optional: true, type: Boolean },\n        onPositioned: { optional: true, type: Function },\n        position: {\n            optional: true,\n            type: String,\n            validate: (p) => {\n                const [d, v = \"middle\"] = p.split(\"-\");\n                return (\n                    [\"top\", \"bottom\", \"left\", \"right\"].includes(d) &&\n                    [\"start\", \"middle\", \"end\", \"fit\"].includes(v)\n                );\n            },\n        },\n\n        // Control props\n        closeOnClickAway: { optional: true, type: Function },\n        closeOnEscape: { optional: true, type: Boolean },\n        setActiveElement: { optional: true, type: Boolean },\n\n        // Technical props\n        ref: { optional: true, type: Function },\n        slots: { optional: true, type: Object },\n    };\n    static animationTime = 200;\n\n    setup() {\n        if (this.props.setActiveElement) {\n            useActiveElement(\"ref\");\n        }\n\n        useForwardRefToParent(\"ref\");\n        this.popoverRef = useRef(\"ref\");\n        this.position = usePosition(\"ref\", () => this.props.target, this.positioningOptions);\n\n        const resizeObserver = new ResizeObserver(() => {\n            if (!this.props.fixedPosition && this.animationDone) {\n                this.position.unlock();\n            }\n        });\n\n        onMounted(() => {\n            POPOVERS.set(this.props.target, this.popoverRef.el);\n            resizeObserver.observe(this.popoverRef.el);\n        });\n        onWillDestroy(() => POPOVERS.delete(this.props.target));\n\n        if (this.props.target.isConnected) {\n            useClickAway(this.onClickAway.bind(this));\n\n            if (this.props.closeOnEscape) {\n                useHotkey(\"escape\", () => this.props.close());\n            }\n            const targetObserver = new MutationObserver(this.onTargetMutate.bind(this));\n            targetObserver.observe(this.props.target.parentElement, { childList: true });\n            onWillDestroy(() => targetObserver.disconnect());\n        } else {\n            this.props.close();\n        }\n    }\n\n    get defaultClassObj() {\n        return mergeClasses(\"o_popover popover mw-100 bs-popover-auto\", this.props.class);\n    }\n\n    get positioningOptions() {\n        return {\n            extendedFlipping: this.props.extendedFlipping,\n            margin: this.props.arrow ? 8 : 0,\n            onPositioned: (el, solution) => {\n                this.onPositioned(solution);\n                this.props.onPositioned?.(el, solution);\n            },\n            position: this.props.position,\n            shrink: true,\n        };\n    }\n\n    animate(direction) {\n        const transform = {\n            top: [\"translateY(-5%)\", \"translateY(0)\"],\n            right: [\"translateX(5%)\", \"translateX(0)\"],\n            bottom: [\"translateY(5%)\", \"translateY(0)\"],\n            left: [\"translateX(-5%)\", \"translateX(0)\"],\n        }[direction];\n        return this.popoverRef.el.animate(\n            { opacity: [0, 1], transform },\n            this.constructor.animationTime\n        );\n    }\n\n    isInside(target) {\n        return (\n            this.props.target?.contains(target) ||\n            this.popoverRef?.el?.contains(target) ||\n            this.env[OVERLAY_SYMBOL]?.contains(target)\n        );\n    }\n\n    onClickAway(target) {\n        if (this.props.closeOnClickAway(target) && !this.isInside(target)) {\n            this.props.close();\n        }\n    }\n\n    onPositioned({ direction, variant, variantOffset }) {\n        if (this.props.arrow) {\n            this.updateArrow(direction, variant, variantOffset);\n        }\n\n        // opening animation (only once)\n        if (this.props.animation && !this.animationDone) {\n            this.position.lock();\n            this.animate(direction).finished.then(() => {\n                this.animationDone = true;\n                if (!this.props.fixedPosition) {\n                    this.position.unlock();\n                }\n            });\n        }\n\n        if (this.props.fixedPosition) {\n            // Prevent further positioning updates if fixed position is wanted\n            this.position.lock();\n        }\n    }\n\n    onTargetMutate() {\n        if (!this.props.target.isConnected) {\n            this.props.close();\n        }\n    }\n\n    updateArrow(direction, variant, variantOffset) {\n        const { el } = this.popoverRef;\n\n        // Reverse the direction if RTL as bootstrap expects it that way\n        [direction, variant] = reverseForRTL(direction, variant);\n\n        // Update the bootstrap popper placement, in order to give the arrow its shape\n        el.dataset.popperPlacement = direction;\n\n        // Update arrow position\n        const vertical = [\"top\", \"bottom\"].includes(direction);\n        const placementProperty = vertical ? \"left\" : \"top\";\n        const placement = {\n            start: \"--position-min\",\n            middle: \"--position-center\",\n            fit: \"--position-center\",\n            end: \"--position-max\",\n        }[variant];\n        const arrowEl = el.querySelector(\":scope > .popover-arrow\");\n        Object.assign(arrowEl.style, {\n            top: \"\",\n            left: \"\",\n            [placementProperty]: `clamp(\n                var(--position-min),\n                calc(var(${placement}) - ${variantOffset}px),\n                var(--position-max)\n            )`,\n        });\n\n        // Should the arrow get sucked?\n        const sizeProperty = vertical ? \"width\" : \"height\";\n        const { [sizeProperty]: arrowSize, [placementProperty]: arrowPosition } =\n            arrowEl.getBoundingClientRect();\n        const { [sizeProperty]: targetSize, [placementProperty]: targetPosition } =\n            this.props.target.getBoundingClientRect();\n        const arrowCenter = arrowPosition + arrowSize / 2;\n        const margin = arrowSize / 2 - 1;\n        const hasEnoughSpace = arrowSize < targetSize - 2 * margin;\n        const isOutsideSafeEdge =\n            arrowCenter < targetPosition + margin ||\n            arrowCenter > targetPosition + targetSize - margin;\n        arrowEl.classList.toggle(\"sucked\", hasEnoughSpace && isOutsideSafeEdge);\n    }\n}\n", "import { onWillUnmount, status, useComponent } from \"@odoo/owl\";\nimport { useService } from \"@web/core/utils/hooks\";\n\n/**\n * @typedef {import(\"@web/core/popover/popover_service\").PopoverServiceAddFunction} PopoverServiceAddFunction\n * @typedef {import(\"@web/core/popover/popover_service\").PopoverServiceAddOptions} PopoverServiceAddOptions\n */\n\n/**\n * @typedef PopoverHookReturnType\n * @property {(target: string | HTMLElement, props: object) => void} open\n *  - Signals the manager to open the configured popover\n *    component on the target, with the given props.\n * @property {() => void} close\n *  - Signals the manager to remove the popover.\n * @property {boolean} isOpen\n *  - Whether the popover is currently open.\n */\n\n/**\n * @param {PopoverServiceAddFunction} addFn\n * @param {typeof import(\"@odoo/owl\").Component} component\n * @param {PopoverServiceAddOptions} options\n * @returns {PopoverHookReturnType}\n */\nexport function makePopover(addFn, component, options) {\n    let removeFn = null;\n    function close() {\n        removeFn?.();\n    }\n    return {\n        open(target, props) {\n            close();\n            const newOptions = Object.create(options);\n            newOptions.onClose = () => {\n                removeFn = null;\n                options.onClose?.();\n            };\n            removeFn = addFn(target, component, props, newOptions);\n        },\n        close,\n        get isOpen() {\n            return Boolean(removeFn);\n        },\n    };\n}\n\n/**\n * Manages a component to be used as a popover.\n *\n * @param {typeof import(\"@odoo/owl\").Component} component\n * @param {PopoverServiceAddOptions} [options]\n * @returns {PopoverHookReturnType}\n */\nexport function usePopover(component, options = {}) {\n    let service;\n    if (options.useBottomSheet) {\n        service = useService(\"bottom_sheet\");\n    } else {\n        service = useService(\"popover\");\n    }\n    const owner = useComponent();\n    const newOptions = Object.create(options);\n    newOptions.onClose = () => {\n        if (status(owner) !== \"destroyed\") {\n            options.onClose?.();\n        }\n    };\n    const popover = makePopover(service.add, component, newOptions);\n    onWillUnmount(popover.close);\n    return popover;\n}\n", "import { markRaw } from \"@odoo/owl\";\nimport { Popover } from \"@web/core/popover/popover\";\nimport { registry } from \"@web/core/registry\";\n\n/**\n * @typedef {{\n *   animation?: Boolean;\n *   arrow?: Boolean;\n *   closeOnClickAway?: boolean | (target: HTMLElement) => boolean;\n *   closeOnEscape?: boolean;\n *   env?: object;\n *   fixedPosition?: boolean;\n *   onClose?: () => void;\n *   onPositioned?: import(\"@web/core/position/position_hook\").UsePositionOptions[\"onPositioned\"];\n *   popoverClass?: string;\n *   role?: string;\n *   position?: import(\"@web/core/position/position_hook\").UsePositionOptions[\"position\"];\n *   ref?: Function;\n * }} PopoverServiceAddOptions\n *\n * @typedef {ReturnType<popoverService[\"start\"]>[\"add\"]} PopoverServiceAddFunction\n */\n\nexport const popoverService = {\n    dependencies: [\"overlay\"],\n    start(_, { overlay }) {\n        /**\n         * Signals the manager to add a popover.\n         *\n         * @param {HTMLElement} target\n         * @param {typeof import(\"@odoo/owl\").Component} component\n         * @param {object} [props]\n         * @param {PopoverServiceAddOptions} [options]\n         * @returns {() => void}\n         */\n        const add = (target, component, props = {}, options = {}) => {\n            const closeOnClickAway =\n                typeof options.closeOnClickAway === \"function\"\n                    ? options.closeOnClickAway\n                    : () => options.closeOnClickAway ?? true;\n            const remove = overlay.add(\n                Popover,\n                {\n                    target,\n                    close: () => remove(),\n                    closeOnClickAway,\n                    closeOnEscape: options.closeOnEscape,\n                    component,\n                    componentProps: markRaw(props),\n                    extendedFlipping: options.extendedFlipping,\n                    ref: options.ref,\n                    class: options.popoverClass,\n                    animation: options.animation,\n                    arrow: options.arrow,\n                    role: options.role,\n                    position: options.position,\n                    onPositioned: options.onPositioned,\n                    fixedPosition: options.fixedPosition,\n                    holdOnHover: options.holdOnHover,\n                    setActiveElement: options.setActiveElement ?? true,\n                },\n                {\n                    env: options.env,\n                    onRemove: options.onClose,\n                    rootId: target.getRootNode()?.host?.id,\n                }\n            );\n\n            return remove;\n        };\n\n        return { add };\n    },\n};\n\nregistry.category(\"services\").add(\"popover\", popoverService);\n", "import { reposition } from \"@web/core/position/utils\";\nimport { omit } from \"@web/core/utils/objects\";\nimport { useThrottleForAnimation } from \"@web/core/utils/timing\";\nimport {\n    EventBus,\n    onWillDestroy,\n    useChildSubEnv,\n    useComponent,\n    useEffect,\n    useRef,\n} from \"@odoo/owl\";\n\n/**\n * @typedef {import(\"@web/core/position/utils\").ComputePositionOptions} ComputePositionOptions\n * @typedef {import(\"@web/core/position/utils\").PositioningSolution} PositioningSolution\n *\n * @typedef {Object} UsePositionOptionsExtensionType\n * @property {(popperElement: HTMLElement, solution: PositioningSolution) => void} [onPositioned]\n *  callback called when the positioning is done.\n * @typedef {ComputePositionOptions & UsePositionOptionsExtensionType} UsePositionOptions\n *\n * @typedef PositioningControl\n * @property {() => void} lock prevents further positioning updates\n * @property {() => void} unlock allows further positioning updates (triggers an update right away)\n */\n\nexport const POSITION_BUS = Symbol(\"position-bus\");\n\n/**\n * Makes sure that the `popper` element is always\n * placed at `position` from the `target` element.\n * If doing so the `popper` element is clipped off `container`,\n * sensible fallback positions are tried.\n * If all of fallback positions are also clipped off `container`,\n * the original position is used.\n *\n * Note: The popper element should be indicated in your template\n *       with a t-ref reference matching the refName argument.\n *\n * @param {string} refName\n *  name of the reference to the popper element in the template.\n * @param {() => HTMLElement} getTarget\n * @param {UsePositionOptions} [options={}] the options to be used for positioning\n * @returns {PositioningControl}\n *  control object to lock/unlock the positioning.\n */\nexport function usePosition(refName, getTarget, options = {}) {\n    const ref = useRef(refName);\n    let lock = false;\n    const update = () => {\n        const targetEl = getTarget();\n        if (!ref.el || !targetEl?.isConnected || lock) {\n            // No compute needed\n            return;\n        }\n        const repositionOptions = omit(options, \"onPositioned\");\n        const solution = reposition(ref.el, targetEl, repositionOptions);\n        options.position = `${solution.direction}-${solution.variant}`; // memorize last position\n        options.onPositioned?.(ref.el, solution);\n    };\n\n    const component = useComponent();\n    const bus = component.env[POSITION_BUS] || new EventBus();\n\n    let executingUpdate = false;\n    const batchedUpdate = async () => {\n        // not same as batch, here we're executing once and then awaiting\n        if (!executingUpdate) {\n            executingUpdate = true;\n            update();\n            await Promise.resolve();\n            executingUpdate = false;\n        }\n    };\n    bus.addEventListener(\"update\", batchedUpdate);\n    onWillDestroy(() => bus.removeEventListener(\"update\", batchedUpdate));\n\n    const isTopmost = !(POSITION_BUS in component.env);\n    if (isTopmost) {\n        useChildSubEnv({ [POSITION_BUS]: bus });\n    }\n\n    const throttledUpdate = useThrottleForAnimation(() => bus.trigger(\"update\"));\n    useEffect(() => {\n        // Reposition\n        bus.trigger(\"update\");\n\n        if (isTopmost) {\n            // Attach listeners to keep the positioning up to date\n            const scrollListener = (e) => {\n                if (ref.el?.contains(e.target)) {\n                    // In case the scroll event occurs inside the popper, do not reposition\n                    return;\n                }\n                throttledUpdate();\n            };\n            // Get the ownerDocument of the target, and the topmost document\n            // if the target is inside an iframe of same-origin\n            // (c.f. html_builder), to handle scroll events at these 2 levels.\n            const documents = [];\n            const targetDocument = getTarget()?.ownerDocument;\n            if (targetDocument) {\n                documents.push(targetDocument);\n                if (\n                    targetDocument.defaultView &&\n                    targetDocument.defaultView.top !== targetDocument.defaultView\n                ) {\n                    try {\n                        documents.push(targetDocument.defaultView.top.document);\n                    } catch {\n                        // Don't access the top document if it is not allowed.\n                        // (i.e. iframe origin or sandbox restriction)\n                    }\n                }\n            }\n            for (const document of documents) {\n                document.addEventListener(\"scroll\", scrollListener, { capture: true });\n                document.addEventListener(\"load\", throttledUpdate, { capture: true });\n            }\n            window.addEventListener(\"resize\", throttledUpdate);\n            return () => {\n                for (const document of documents) {\n                    document.removeEventListener(\"scroll\", scrollListener, { capture: true });\n                    document.removeEventListener(\"load\", throttledUpdate, { capture: true });\n                }\n                window.removeEventListener(\"resize\", throttledUpdate);\n            };\n        }\n    });\n\n    return {\n        lock: () => {\n            lock = true;\n        },\n        unlock: () => {\n            lock = false;\n            bus.trigger(\"update\");\n        },\n    };\n}\n", "import { localization } from \"@web/core/l10n/localization\";\n\n/**\n * @typedef {\"top\" | \"left\" | \"bottom\" | \"right\"} Direction\n * @typedef {\"start\" | \"middle\" | \"end\" | \"fit\"} Variant\n *\n * @typedef {{[direction in Direction]: string}} DirectionFlipOrder\n *  string values should match regex /^[tbrl]+$/m\n *\n * @typedef {{[variant in Variant]: string}} VariantFlipOrder\n *  string values should match regex /^[smef]+$/m\n *\n * @typedef {{\n *  top: number,\n *  left: number,\n *  maxHeight?: number;\n *  direction: Direction,\n *  variant: Variant,\n *  variantOffset?: number,\n * }} PositioningSolution\n *\n * @typedef ComputePositionOptions\n * @property {HTMLElement | () => HTMLElement} [container] container element\n * @property {number} [margin=0]\n *  margin in pixels between the popper and the target.\n * @property {Direction | `${Direction}-${Variant}`} [position=\"bottom\"]\n *  position of the popper relative to the target\n * @property {boolean} [flip=true]\n *  allow the popper to try a flipped direction when it overflows the container\n * @property {boolean} [extendedFlipping=false]\n *  allow the popper to try for all possible flipping directions (including center)\n *  when it overflows the container\n * @property {boolean} [shrink=false]\n *  reduce the popper's height when it overflows the container\n */\n\n/** @type {ComputePositionOptions} */\nconst DEFAULTS = {\n    flip: true,\n    margin: 0,\n    position: \"bottom\",\n};\n\n/** @type {{[d: string]: Direction}} */\nconst DIRECTIONS = { t: \"top\", r: \"right\", b: \"bottom\", l: \"left\", c: \"center\" };\n/** @type {{[v: string]: Variant}} */\nconst VARIANTS = { s: \"start\", m: \"middle\", e: \"end\", f: \"fit\" };\n/** @type DirectionFlipOrder */\nconst DIRECTION_FLIP_ORDER = { top: \"tb\", right: \"rl\", bottom: \"bt\", left: \"lr\", center: \"c\" };\n/** @type DirectionFlipOrder */\nconst EXTENDED_DIRECTION_FLIP_ORDER = {\n    top: \"tbrlc\",\n    right: \"rlbtc\",\n    bottom: \"btrlc\",\n    left: \"lrbtc\",\n    center: \"c\",\n};\n/** @type VariantFlipOrder */\nconst VARIANT_FLIP_ORDER = { start: \"se\", middle: \"m\", end: \"es\", fit: \"f\" };\n\n/**\n * @param {HTMLElement} popperEl\n * @param {HTMLElement} targetEl\n * @returns {HTMLIFrameElement?}\n */\nfunction getIFrame(popperEl, targetEl) {\n    return [...popperEl.ownerDocument.getElementsByTagName(\"iframe\")].find((iframe) =>\n        iframe.contentDocument?.contains(targetEl)\n    );\n}\n\n/**\n * Returns the RTl adapted direction and variant if needed.\n * If the current localization direction is \"rtl\":\n *  - Direction \"left\" and \"right\" are flipped to \"right\" and \"left\".\n *  - Variant \"start\" and \"end\" are flipped to \"end\" and \"start\".\n *\n * @param {Direction} direction\n * @param {Variant} [variant=\"middle\"] (default value is \"middle\")\n * @returns {[Direction, Variant]}\n */\nexport function reverseForRTL(direction, variant = \"middle\") {\n    if (localization.direction === \"rtl\") {\n        if ([\"left\", \"right\"].includes(direction)) {\n            direction = direction === \"left\" ? \"right\" : \"left\";\n        } else if ([\"start\", \"end\"].includes(variant)) {\n            // here direction is either \"top\" or \"bottom\"\n            variant = variant === \"start\" ? \"end\" : \"start\";\n        }\n    }\n    return [direction, variant];\n}\n\n/**\n * Returns the best positioning solution staying in the container or falls back\n * to the requested position.\n * The positioning data used to determine each possible position is based on\n * the target, popper, and container sizes.\n * Particularly, a popper must not overflow the container in any direction.\n * The popper will stay at `margin` distance from its target. One could also\n * use the CSS margins of the popper element to achieve the same result.\n *\n * Pre-condition: the popper element must have a fixed positioning\n *                with top and left set to 0px.\n *\n * @param {HTMLElement} popper\n * @param {HTMLElement} target\n * @param {ComputePositionOptions} options\n * @returns {PositioningSolution} the best positioning solution, relative to\n *                                the containing block of the popper.\n *                                => can be applied to popper.style.(top|left)\n */\nfunction computePosition(\n    popper,\n    target,\n    { container, extendedFlipping, flip, margin, position, shrink }\n) {\n    // Retrieve directions and variants\n    const [direction, variant = \"middle\"] = reverseForRTL(...position.split(\"-\"));\n    let directions = [direction.at(0)];\n    if (flip) {\n        directions = extendedFlipping\n            ? EXTENDED_DIRECTION_FLIP_ORDER[direction]\n            : DIRECTION_FLIP_ORDER[direction];\n    }\n    const variants = VARIANT_FLIP_ORDER[variant];\n\n    // Retrieve container\n    if (!container) {\n        container = popper.ownerDocument.documentElement;\n    } else if (typeof container === \"function\") {\n        container = container();\n    }\n\n    if (variant === \"fit\") {\n        // make sure the popper has the desired dimensions during the computation of the position\n        const styleProperty = [\"top\", \"bottom\"].includes(direction) ? \"width\" : \"height\";\n        popper.style[styleProperty] = getComputedStyle(target)[styleProperty];\n    }\n\n    // Account for popper actual margins\n    const popperStyle = getComputedStyle(popper);\n    const { marginTop, marginLeft, marginRight, marginBottom } = popperStyle;\n    const popMargins = {\n        top: parseFloat(marginTop),\n        left: parseFloat(marginLeft),\n        right: parseFloat(marginRight),\n        bottom: parseFloat(marginBottom),\n    };\n\n    // IFrame\n    const shouldAccountForIFrame = popper.ownerDocument !== target.ownerDocument;\n    const iframe = shouldAccountForIFrame ? getIFrame(popper, target) : null;\n\n    // Boxes\n    const popBox = popper.getBoundingClientRect();\n    const targetBox = target.getBoundingClientRect();\n    const contBox = container.getBoundingClientRect();\n    const iframeBox = iframe?.getBoundingClientRect() ?? { top: 0, left: 0 };\n\n    const containerIsHTMLNode = container === container.ownerDocument.firstElementChild;\n    const containerIsInIframe =\n        shouldAccountForIFrame && target.ownerDocument === container.ownerDocument;\n\n    // Compute positioning data\n    const directionsData = {\n        t: iframeBox.top + targetBox.top - popMargins.bottom - margin - popBox.height,\n        b: iframeBox.top + targetBox.bottom + popMargins.top + margin,\n        r: iframeBox.left + targetBox.right + popMargins.left + margin,\n        l: iframeBox.left + targetBox.left - popMargins.right - margin - popBox.width,\n        c: iframeBox.top + targetBox.top + targetBox.height / 2 - popBox.height / 2,\n    };\n    const variantsData = {\n        vf: iframeBox.left + targetBox.left,\n        vs: iframeBox.left + targetBox.left + popMargins.left,\n        vm: iframeBox.left + targetBox.left + targetBox.width / 2 - popBox.width / 2,\n        ve: iframeBox.left + targetBox.right - popMargins.right - popBox.width,\n        hf: iframeBox.top + targetBox.top,\n        hs: iframeBox.top + targetBox.top + popMargins.top,\n        hm: iframeBox.top + targetBox.top + targetBox.height / 2 - popBox.height / 2,\n        he: iframeBox.top + targetBox.bottom - popMargins.bottom - popBox.height,\n    };\n\n    function getPositioningData(d, v) {\n        const [direction, variant] = reverseForRTL(DIRECTIONS[d], VARIANTS[v]);\n        const result = { direction, variant };\n        const vertical = [\"t\", \"b\", \"c\"].includes(d);\n        const variantPrefix = vertical ? \"v\" : \"h\";\n        const directionValue = directionsData[d];\n        let variantValue = variantsData[variantPrefix + v];\n        const [leftCompensation, topCompensation] = containerIsInIframe\n            ? [iframeBox.left, iframeBox.top]\n            : [0, 0];\n\n        const [directionSize, variantSize] = vertical\n            ? [popBox.height, popBox.width]\n            : [popBox.width, popBox.height];\n        let [directionMin, directionMax] = vertical\n            ? [contBox.top + topCompensation, contBox.bottom + topCompensation]\n            : [contBox.left + leftCompensation, contBox.right + leftCompensation];\n        let [variantMin, variantMax] = vertical\n            ? [contBox.left + leftCompensation, contBox.right + leftCompensation]\n            : [contBox.top + topCompensation, contBox.bottom + topCompensation];\n\n        if (containerIsHTMLNode) {\n            if (vertical) {\n                directionMin += container.scrollTop;\n                directionMax += container.scrollTop;\n            } else {\n                variantMin += container.scrollTop;\n                variantMax += container.scrollTop;\n            }\n        }\n\n        // Compute overflows\n        let directionOverflow = 0;\n        if (Math.floor(directionValue) < Math.ceil(directionMin)) {\n            directionOverflow = Math.floor(directionValue) - Math.ceil(directionMin);\n        } else if (Math.ceil(directionValue + directionSize) > Math.floor(directionMax)) {\n            directionOverflow =\n                Math.ceil(directionValue + directionSize) - Math.floor(directionMax);\n        }\n        let variantOverflow = 0;\n        if (Math.floor(variantValue) < Math.ceil(variantMin)) {\n            variantOverflow = Math.floor(variantValue) - Math.ceil(variantMin);\n        } else if (Math.ceil(variantValue + variantSize) > Math.floor(variantMax)) {\n            variantOverflow = Math.ceil(variantValue + variantSize) - Math.floor(variantMax);\n        }\n\n        // All non zero values of variantOverflow lead to the\n        // same malus value since it can be corrected by shifting\n        let malus = Math.abs(directionOverflow) + (variantOverflow && 1);\n\n        // Apply variant offset\n        variantValue -= variantOverflow;\n        result.variantOffset = -variantOverflow;\n\n        const positioning = vertical\n            ? { top: directionValue, left: variantValue }\n            : { top: variantValue, left: directionValue };\n        // Subtract the offsets of the containing block (relative to the\n        // viewport). It can be done like that because the style top and\n        // left were reset to 0px in `reposition`\n        // https://developer.mozilla.org/en-US/docs/Web/CSS/Containing_block#identifying_the_containing_block\n        result.top = positioning.top - popBox.top;\n        result.left = positioning.left - popBox.left;\n        if (d === \"c\") {\n            // Artificial way to say the center direction is a fallback to every other\n            // once there is a direction overflow since we can always shift the position\n            // in any direction in that case\n            malus = 1.001;\n            result.top -= directionOverflow;\n        } else if (shrink && malus) {\n            const minTop = Math.floor(!vertical && v === \"s\" ? targetBox.top : contBox.top);\n            result.top = Math.max(minTop, result.top);\n\n            let height;\n            if (vertical) {\n                height = Math.abs(targetBox[direction] - (d === \"t\" ? directionMin : directionMax));\n            } else {\n                height = {\n                    s: variantMax - targetBox.top,\n                    m: variantMax - variantMin,\n                    e: targetBox.bottom - variantMin,\n                }[v];\n            }\n            result.maxHeight = Math.floor(height);\n        }\n        return { result, malus };\n    }\n\n    // Find best solution\n    const matches = [];\n    for (const d of directions) {\n        for (const v of variants) {\n            const match = getPositioningData(d, v);\n            if (!match.malus) {\n                // A perfect position match has been found.\n                return match.result;\n            }\n            matches.push(match);\n        }\n        if (!flip) {\n            // Stop when no flip is allowed\n            break;\n        }\n    }\n    // Settle for the first match with the least malus\n    return matches.sort((a, b) => a.malus - b.malus)[0].result;\n}\n\n/**\n * Repositions the popper element relatively to the target element (according to options).\n * The positioning strategy is always a fixed positioning with top and left.\n *\n * The positioning solution is returned by the `computePosition` function.\n * It will get applied to the popper element and then returned for convenience.\n *\n * @param {HTMLElement} popper\n * @param {HTMLElement} target\n * @param {ComputePositionOptions} options\n * @returns {PositioningSolution} the applied positioning solution.\n */\nexport function reposition(popper, target, options) {\n    // Reset popper style\n    popper.style.position = \"fixed\";\n    popper.style.top = \"0px\";\n    popper.style.left = \"0px\";\n\n    // Compute positioning solution\n    const solution = computePosition(popper, target, { ...DEFAULTS, ...options });\n\n    // Apply it\n    const { top, left, maxHeight } = solution;\n    popper.style.top = `${top}px`;\n    popper.style.left = `${left}px`;\n    if (maxHeight) {\n        const existingMaxHeight = getComputedStyle(popper).maxHeight;\n        popper.style.maxHeight =\n            existingMaxHeight !== \"none\"\n                ? `min(${existingMaxHeight}, ${maxHeight}px)`\n                : `${maxHeight}px`;\n    }\n\n    return solution;\n}\n", "import { Component } from \"@odoo/owl\";\nimport { Dialog } from \"@web/core/dialog/dialog\";\nimport { isIOS } from \"@web/core/browser/feature_detection\";\n\nexport class InstallPrompt extends Component {\n    static props = {\n        close: true,\n        onClose: { type: Function },\n    };\n    static components = {\n        Dialog,\n    };\n    static template = \"web.InstallPrompt\";\n\n    get isMobileSafari() {\n        return isIOS();\n    }\n\n    onClose() {\n        this.props.close();\n        this.props.onClose();\n    }\n}\n", "import { reactive } from \"@odoo/owl\";\nimport { browser } from \"@web/core/browser/browser\";\nimport {\n    isDisplayStandalone,\n    isIOS,\n    isMacOS,\n    isBrowserSafari,\n} from \"@web/core/browser/feature_detection\";\nimport { get } from \"@web/core/network/http_service\";\nimport { registry } from \"@web/core/registry\";\nimport { InstallPrompt } from \"./install_prompt\";\n\nconst serviceRegistry = registry.category(\"services\");\n\n/* Ideally, the service would directly add the event listener. Unfortunately, it happens sometimes that\n * the browser would trigger the event before the webclient (services, components, etc.) is even ready.\n * In that case, we have to get this event as soon as possible. The service can then verify if the event\n * is already stored in this variable, or add an event listener itself, to make sure the `_handleBeforeInstallPrompt`\n * function is called at the right moment, and can give the correct information to the service.\n */\nlet BEFOREINSTALLPROMPT_EVENT;\nlet REGISTER_BEFOREINSTALLPROMPT_EVENT;\n\nbrowser.addEventListener(\"beforeinstallprompt\", (ev) => {\n    // This event is only triggered by the browser when the native prompt to install can be shown\n    // This excludes incognito tabs, as well as visiting the website while the app is installed\n    if (REGISTER_BEFOREINSTALLPROMPT_EVENT) {\n        // service has been started before the event was triggered, update the service\n        return REGISTER_BEFOREINSTALLPROMPT_EVENT(ev);\n    } else {\n        // store the event for later use\n        BEFOREINSTALLPROMPT_EVENT = ev;\n    }\n});\n\nconst pwaService = {\n    dependencies: [\"dialog\"],\n    start(env, { dialog }) {\n        let _manifest;\n        let nativePrompt;\n\n        const state = reactive({\n            canPromptToInstall: false,\n            isAvailable: false,\n            isScopedApp: browser.location.href.includes(\"/scoped_app\"),\n            isSupportedOnBrowser: false,\n            startUrl: \"/odoo\",\n            decline,\n            getManifest,\n            hasScopeBeenInstalled,\n            show,\n        });\n\n        function _getInstallationState(scope = state.startUrl) {\n            const installationState = browser.localStorage.getItem(\"pwaService.installationState\");\n            return installationState ? JSON.parse(installationState)[scope] : \"\";\n        }\n\n        function _setInstallationState(value) {\n            const ls = JSON.parse(\n                browser.localStorage.getItem(\"pwaService.installationState\") || \"{}\"\n            );\n            ls[state.startUrl] = value;\n            browser.localStorage.setItem(\"pwaService.installationState\", JSON.stringify(ls));\n        }\n\n        function _removeInstallationState() {\n            const ls = JSON.parse(browser.localStorage.getItem(\"pwaService.installationState\"));\n            delete ls[state.startUrl];\n            browser.localStorage.setItem(\"pwaService.installationState\", JSON.stringify(ls));\n        }\n\n        if (state.isScopedApp) {\n            if (browser.location.pathname === \"/scoped_app\") {\n                // Installation page, use the path parameter in the URL\n                state.startUrl = \"/\" + new URL(browser.location.href).searchParams.get(\"path\");\n            } else {\n                state.startUrl = browser.location.pathname;\n            }\n        }\n\n        // The PWA can only be installed if the app is not already launched (display-mode standalone)\n        // For Apple devices, PWA are supported on any mobile version of Safari, or in desktop since version 17\n        // On Safari devices, the check is also done on the display-mode and we rely on the installationState to\n        // decide whether we must show the prompt or not\n        state.isSupportedOnBrowser =\n            browser.BeforeInstallPromptEvent !== undefined ||\n            (isBrowserSafari() &&\n                !isDisplayStandalone() &&\n                (isIOS() ||\n                    (isMacOS() && browser.navigator.userAgent.match(/Version\\/(\\d+)/)[1] >= 17)));\n\n        const installationState = _getInstallationState();\n\n        if (state.isSupportedOnBrowser) {\n            if (BEFOREINSTALLPROMPT_EVENT) {\n                _handleBeforeInstallPrompt(BEFOREINSTALLPROMPT_EVENT, installationState);\n                BEFOREINSTALLPROMPT_EVENT = null; // clear this variable as it is no longer useful\n            }\n            // If a user declines the prompt, the browser would triggered it once again. We must be able to catch it\n            REGISTER_BEFOREINSTALLPROMPT_EVENT = (ev) => {\n                _handleBeforeInstallPrompt(ev, installationState);\n            };\n            if (isBrowserSafari()) {\n                // since those platforms don't rely on the beforeinstallprompt event, we handle it ourselves\n                state.canPromptToInstall = installationState !== \"dismissed\";\n                state.isAvailable = true;\n            }\n        }\n\n        function _handleBeforeInstallPrompt(ev, installationState) {\n            nativePrompt = ev;\n            if (installationState === \"accepted\") {\n                // If this event is triggered with the installationState stored, it means that the app has been\n                // removed since its installation. The prompt can be displayed, and the installation state is reset.\n                if (!isDisplayStandalone()) {\n                    // In Scoped Apps, the event might be triggered if a manifest with a different scope is available\n                    _removeInstallationState();\n                }\n            }\n            state.canPromptToInstall = installationState !== \"dismissed\";\n            state.isAvailable = true;\n        }\n\n        async function getManifest() {\n            if (!_manifest) {\n                const manifest = await get(\n                    document.querySelector(\"link[rel=manifest\")?.getAttribute(\"href\"),\n                    \"text\"\n                );\n                _manifest = JSON.parse(manifest);\n            }\n            return _manifest;\n        }\n\n        // This function don't guarantee the scope is still currently installed on the device\n        // The only way to know that is by relying on the BeforeInstallPrompt event from the\n        // page linking the app manifest. This only serves to indicate that the app has previously\n        // been installed\n        function hasScopeBeenInstalled(scope) {\n            return _getInstallationState(scope) === \"accepted\";\n        }\n\n        async function show({ onDone } = {}) {\n            if (!state.isAvailable) {\n                return;\n            }\n            if (nativePrompt) {\n                const res = await nativePrompt.prompt();\n                _setInstallationState(res.outcome);\n                state.canPromptToInstall = false;\n                if (onDone) {\n                    onDone(res);\n                }\n            } else if (isBrowserSafari()) {\n                // since those platforms don't support a native installation prompt yet, we\n                // show a custom dialog to explain how to pin the app to the application menu\n                dialog.add(InstallPrompt, {\n                    onClose: () => {\n                        if (onDone) {\n                            onDone({});\n                        }\n                        this.decline();\n                    },\n                });\n            }\n        }\n\n        function decline() {\n            _setInstallationState(\"dismissed\");\n            state.canPromptToInstall = false;\n        }\n\n        return state;\n    },\n};\nserviceRegistry.add(\"pwa\", pwaService);\n", "import { evaluate } from \"./py_interpreter\";\nimport { parse } from \"./py_parser\";\nimport { tokenize } from \"./py_tokenizer\";\n\nexport { evaluate } from \"./py_interpreter\";\nexport { parse } from \"./py_parser\";\nexport { tokenize } from \"./py_tokenizer\";\nexport { formatAST } from \"./py_utils\";\n\n/**\n * @typedef { import(\"./py_tokenizer\").Token } Token\n * @typedef { import(\"./py_parser\").AST } AST\n */\n\n/**\n * Parses an expression into a valid AST representation\n\n * @param {string} expr\n * @returns { AST }\n */\nexport function parseExpr(expr) {\n    const tokens = tokenize(expr);\n    return parse(tokens);\n}\n\n/**\n * Evaluates a python expression\n *\n * @param {string} expr\n * @param {Object} [context]\n * @returns {any}\n */\nexport function evaluateExpr(expr, context = {}) {\n    let ast;\n    try {\n        ast = parseExpr(expr);\n    } catch (error) {\n        throw new EvalError(`Can not parse python expression: (${expr})\\nError: ${error.message}`);\n    }\n    try {\n        return evaluate(ast, context);\n    } catch (error) {\n        throw new EvalError(`Can not evaluate python expression: (${expr})\\nError: ${error.message}`);\n    }\n}\n\n/**\n * Evaluates a python expression to return a boolean.\n *\n * @param {string} expr\n * @param {Object} [context]\n * @returns {any}\n */\nexport function evaluateBooleanExpr(expr, context = {}) {\n    if (!expr || expr === 'False' || expr === '0') {\n        return false;\n    }\n    if (expr === 'True' || expr === '1') {\n        return true;\n    }\n    return evaluateExpr(`bool(${expr})`, context);\n}\n", "import { PyDate, PyDateTime, PyRelativeDelta, PyTime, PyTimeDelta } from \"./py_date\";\n\nexport class EvaluationError extends Error {}\n\n/**\n * @param {any} iterable\n * @param {Function} func\n */\nexport function execOnIterable(iterable, func) {\n    if (iterable === null) {\n        // new Set(null) is fine in js but set(None) (-> new Set(null))\n        // is not in Python\n        throw new EvaluationError(`value not iterable`);\n    }\n    if (typeof iterable === \"object\" && !Array.isArray(iterable) && !(iterable instanceof Set)) {\n        // dicts are considered as iterable in Python\n        iterable = Object.keys(iterable);\n    }\n    if (typeof iterable?.[Symbol.iterator] !== \"function\") {\n        // rules out undefined and other values not iterable\n        throw new EvaluationError(`value not iterable`);\n    }\n    return func(iterable);\n}\n\nexport const BUILTINS = {\n    /**\n     * @param {any} value\n     * @returns {boolean}\n     */\n    bool(value) {\n        switch (typeof value) {\n            case \"number\":\n                return value !== 0;\n            case \"string\":\n                return value !== \"\";\n            case \"boolean\":\n                return value;\n            case \"object\":\n                if (value === null || value === undefined) {\n                    return false;\n                }\n                if (value.isTrue) {\n                    return value.isTrue();\n                }\n                if (value instanceof Array) {\n                    return !!value.length;\n                }\n                if (value instanceof Set) {\n                    return !!value.size;\n                }\n                return Object.keys(value).length !== 0;\n        }\n        return true;\n    },\n\n    set(iterable) {\n        if (arguments.length > 2) {\n            // we always receive at least one argument: kwargs (return fnValue(...args, kwargs); in FunctionCall case)\n            throw new EvaluationError(\n                `set expected at most 1 argument, got (${arguments.length - 1}`\n            );\n        }\n        return execOnIterable(iterable, (iterable) => {\n            return new Set(iterable);\n        });\n    },\n\n    max(...args) {\n        // kwargs are not supported by Math.max.\n        return Math.max(...args.slice(0, -1));\n    },\n\n    min(...args) {\n        // kwargs are not supported by Math.min.\n        return Math.min(...args.slice(0, -1));\n    },\n\n    time: {\n        strftime(format) {\n            return PyDateTime.now().strftime(format);\n        },\n    },\n\n    context_today() {\n        return PyDate.today();\n    },\n\n    get current_date() {\n        // deprecated: today should be prefered\n        return this.today;\n    },\n\n    get today() {\n        return PyDate.today().strftime(\"%Y-%m-%d\");\n    },\n\n    get now() {\n        return PyDateTime.now().strftime(\"%Y-%m-%d %H:%M:%S\");\n    },\n\n    datetime: {\n        time: PyTime,\n        timedelta: PyTimeDelta,\n        datetime: PyDateTime,\n        date: PyDate,\n    },\n\n    relativedelta: PyRelativeDelta,\n\n    true: true,\n    false: false,\n};\n", "import { parseArgs } from \"./py_parser\";\n\n// -----------------------------------------------------------------------------\n// Errors\n// -----------------------------------------------------------------------------\n\nexport class AssertionError extends Error {}\nexport class ValueError extends Error {}\nexport class NotSupportedError extends Error {}\n\n// -----------------------------------------------------------------------------\n// helpers\n// -----------------------------------------------------------------------------\n\nfunction fmt2(n) {\n    return String(n).padStart(2, \"0\");\n}\nfunction fmt4(n) {\n    return String(n).padStart(4, \"0\");\n}\n\n/**\n * computes (Math.floor(a/b), a%b and passes that to the callback.\n *\n * returns the callback's result\n */\nfunction divmod(a, b, fn) {\n    let mod = a % b;\n    // in python, sign(a % b) === sign(b). Not in JS. If wrong side, add a\n    // round of b\n    if ((mod > 0 && b < 0) || (mod < 0 && b > 0)) {\n        mod += b;\n    }\n    return fn(Math.floor(a / b), mod);\n}\n\nfunction assert(bool, message = \"AssertionError\") {\n    if (!bool) {\n        throw new AssertionError(message);\n    }\n}\n\nconst DAYS_IN_MONTH = [null, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];\nconst DAYS_BEFORE_MONTH = [null];\n\nfor (let dbm = 0, i = 1; i < DAYS_IN_MONTH.length; ++i) {\n    DAYS_BEFORE_MONTH.push(dbm);\n    dbm += DAYS_IN_MONTH[i];\n}\n\nfunction daysInMonth(year, month) {\n    if (month === 2 && isLeap(year)) {\n        return 29;\n    }\n    return DAYS_IN_MONTH[month];\n}\n\nfunction isLeap(year) {\n    return year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0);\n}\n\nfunction daysBeforeYear(year) {\n    const y = year - 1;\n    return y * 365 + Math.floor(y / 4) - Math.floor(y / 100) + Math.floor(y / 400);\n}\n\nfunction daysBeforeMonth(year, month) {\n    const postLeapFeb = month > 2 && isLeap(year);\n    return DAYS_BEFORE_MONTH[month] + (postLeapFeb ? 1 : 0);\n}\n\nfunction ymd2ord(year, month, day) {\n    const dim = daysInMonth(year, month);\n    if (!(1 <= day && day <= dim)) {\n        throw new ValueError(`day must be in 1..${dim}`);\n    }\n    return daysBeforeYear(year) + daysBeforeMonth(year, month) + day;\n}\n\nconst DI400Y = daysBeforeYear(401);\nconst DI100Y = daysBeforeYear(101);\nconst DI4Y = daysBeforeYear(5);\n\nfunction ord2ymd(n) {\n    --n;\n    let n400, n100, n4, n1, n0;\n    divmod(n, DI400Y, function (_n400, n) {\n        n400 = _n400;\n        divmod(n, DI100Y, function (_n100, n) {\n            n100 = _n100;\n            divmod(n, DI4Y, function (_n4, n) {\n                n4 = _n4;\n                divmod(n, 365, function (_n1, n) {\n                    n1 = _n1;\n                    n0 = n;\n                });\n            });\n        });\n    });\n\n    n = n0;\n    const year = n400 * 400 + 1 + n100 * 100 + n4 * 4 + n1;\n    if (n1 == 4 || n100 == 100) {\n        assert(n0 === 0);\n        return {\n            year: year - 1,\n            month: 12,\n            day: 31,\n        };\n    }\n\n    const leapyear = n1 === 3 && (n4 !== 24 || n100 == 3);\n    assert(leapyear == isLeap(year));\n    let month = (n + 50) >> 5;\n    let preceding = DAYS_BEFORE_MONTH[month] + (month > 2 && leapyear ? 1 : 0);\n    if (preceding > n) {\n        --month;\n        preceding -= DAYS_IN_MONTH[month] + (month === 2 && leapyear ? 1 : 0);\n    }\n    n -= preceding;\n    return {\n        year: year,\n        month: month,\n        day: n + 1,\n    };\n}\n\n/**\n * Converts the stuff passed in into a valid date, applying overflows as needed\n */\nfunction tmxxx(year, month, day, hour, minute, second, microsecond) {\n    hour = hour || 0;\n    minute = minute || 0;\n    second = second || 0;\n    microsecond = microsecond || 0;\n\n    if (microsecond < 0 || microsecond > 999999) {\n        divmod(microsecond, 1000000, function (carry, ms) {\n            microsecond = ms;\n            second += carry;\n        });\n    }\n    if (second < 0 || second > 59) {\n        divmod(second, 60, function (carry, s) {\n            second = s;\n            minute += carry;\n        });\n    }\n    if (minute < 0 || minute > 59) {\n        divmod(minute, 60, function (carry, m) {\n            minute = m;\n            hour += carry;\n        });\n    }\n    if (hour < 0 || hour > 23) {\n        divmod(hour, 24, function (carry, h) {\n            hour = h;\n            day += carry;\n        });\n    }\n    // That was easy.  Now it gets muddy:  the proper range for day\n    // can't be determined without knowing the correct month and year,\n    // but if day is, e.g., plus or minus a million, the current month\n    // and year values make no sense (and may also be out of bounds\n    // themselves).\n    // Saying 12 months == 1 year should be non-controversial.\n    if (month < 1 || month > 12) {\n        divmod(month - 1, 12, function (carry, m) {\n            month = m + 1;\n            year += carry;\n        });\n    }\n    // Now only day can be out of bounds (year may also be out of bounds\n    // for a datetime object, but we don't care about that here).\n    // If day is out of bounds, what to do is arguable, but at least the\n    // method here is principled and explainable.\n    const dim = daysInMonth(year, month);\n    if (day < 1 || day > dim) {\n        // Move day-1 days from the first of the month.  First try to\n        // get off cheap if we're only one day out of range (adjustments\n        // for timezone alone can't be worse than that).\n        if (day === 0) {\n            --month;\n            if (month > 0) {\n                day = daysInMonth(year, month);\n            } else {\n                --year;\n                month = 12;\n                day = 31;\n            }\n        } else if (day == dim + 1) {\n            ++month;\n            day = 1;\n            if (month > 12) {\n                month = 1;\n                ++year;\n            }\n        } else {\n            const r = ord2ymd(ymd2ord(year, month, 1) + (day - 1));\n            year = r.year;\n            month = r.month;\n            day = r.day;\n        }\n    }\n    return {\n        year: year,\n        month: month,\n        day: day,\n        hour: hour,\n        minute: minute,\n        second: second,\n        microsecond: microsecond,\n    };\n}\n\n// -----------------------------------------------------------------------------\n// Date/Time and related classes\n// -----------------------------------------------------------------------------\n\nexport class PyDate {\n    /**\n     * @returns {PyDate}\n     */\n    static today() {\n        return this.convertDate(new Date());\n    }\n\n    /**\n     * Convert a date object into PyDate\n     * @param {Date} date\n     * @returns {PyDate}\n     */\n    static convertDate(date) {\n        const year = date.getFullYear();\n        const month = date.getMonth() + 1;\n        const day = date.getDate();\n        return new PyDate(year, month, day);\n    }\n\n    /**\n     * @param {integer} year\n     * @param {integer} month\n     * @param {integer} day\n     */\n    constructor(year, month, day) {\n        this.year = year;\n        this.month = month; // 1-indexed => 1 = january, 2 = february, ...\n        this.day = day; // 1-indexed => 1 = first day of month, ...\n    }\n\n    /**\n     * @param  {...any} args\n     * @returns {PyDate}\n     */\n    static create(...args) {\n        const { year, month, day } = parseArgs(args, [\"year\", \"month\", \"day\"]);\n        return new PyDate(year, month, day);\n    }\n\n    /**\n     * @param {PyTimeDelta} timedelta\n     * @returns {PyDate}\n     */\n    add(timedelta) {\n        const s = tmxxx(this.year, this.month, this.day + timedelta.days);\n        return new PyDate(s.year, s.month, s.day);\n    }\n\n    /**\n     * @param {any} other\n     * @returns {boolean}\n     */\n    isEqual(other) {\n        if (!(other instanceof PyDate)) {\n            return false;\n        }\n        return this.year === other.year && this.month === other.month && this.day === other.day;\n    }\n\n    /**\n     * @param {string} format\n     * @returns {string}\n     */\n    strftime(format) {\n        return format.replace(/%([A-Za-z])/g, (m, c) => {\n            switch (c) {\n                case \"Y\":\n                    return fmt4(this.year);\n                case \"m\":\n                    return fmt2(this.month);\n                case \"d\":\n                    return fmt2(this.day);\n            }\n            throw new ValueError(`No known conversion for ${m}`);\n        });\n    }\n\n    /**\n     * @param {PyTimeDelta | PyDate} other\n     * @returns {PyDate | PyTimeDelta}\n     */\n    substract(other) {\n        if (other instanceof PyTimeDelta) {\n            return this.add(other.negate());\n        }\n        if (other instanceof PyDate) {\n            return PyTimeDelta.create(this.toordinal() - other.toordinal());\n        }\n        throw new NotSupportedError();\n    }\n\n    /**\n     * @returns {string}\n     */\n    toJSON() {\n        return this.strftime(\"%Y-%m-%d\");\n    }\n\n    /**\n     * @returns {integer}\n     */\n    toordinal() {\n        return ymd2ord(this.year, this.month, this.day);\n    }\n}\n\nexport class PyDateTime {\n    /**\n     * @returns {PyDateTime}\n     */\n    static now() {\n        return this.convertDate(new Date());\n    }\n\n    /**\n     * Convert a date object into PyDateTime\n     * @param {Date} date\n     * @returns {PyDateTime}\n     */\n    static convertDate(date) {\n        const year = date.getFullYear();\n        const month = date.getMonth() + 1;\n        const day = date.getDate();\n        const hour = date.getHours();\n        const minute = date.getMinutes();\n        const second = date.getSeconds();\n        return new PyDateTime(year, month, day, hour, minute, second, 0);\n    }\n\n    /**\n     * @param  {...any} args\n     * @returns {PyDateTime}\n     */\n    static create(...args) {\n        const namedArgs = parseArgs(args, [\n            \"year\",\n            \"month\",\n            \"day\",\n            \"hour\",\n            \"minute\",\n            \"second\",\n            \"microsecond\",\n        ]);\n        const year = namedArgs.year;\n        const month = namedArgs.month;\n        const day = namedArgs.day;\n        const hour = namedArgs.hour || 0;\n        const minute = namedArgs.minute || 0;\n        const second = namedArgs.second || 0;\n        const ms = namedArgs.micro / 1000 || 0;\n        return new PyDateTime(year, month, day, hour, minute, second, ms);\n    }\n\n    /**\n     * @param  {...any} args\n     * @returns {PyDateTime}\n     */\n    static combine(...args) {\n        const { date, time } = parseArgs(args, [\"date\", \"time\"]);\n        // not sure. should we go through constructor instead? what about args normalization?\n        return PyDateTime.create(\n            date.year,\n            date.month,\n            date.day,\n            time.hour,\n            time.minute,\n            time.second\n        );\n    }\n\n    /**\n     * @param {integer} year\n     * @param {integer} month\n     * @param {integer} day\n     * @param {integer} hour\n     * @param {integer} minute\n     * @param {integer} second\n     * @param {integer} microsecond\n     */\n    constructor(year, month, day, hour, minute, second, microsecond) {\n        this.year = year;\n        this.month = month; // 1-indexed => 1 = january, 2 = february, ...\n        this.day = day; // 1-indexed => 1 = first day of month, ...\n        this.hour = hour;\n        this.minute = minute;\n        this.second = second;\n        this.microsecond = microsecond;\n    }\n\n    /**\n     * @param {PyTimeDelta} timedelta\n     * @returns {PyDate}\n     */\n    add(timedelta) {\n        const s = tmxxx(\n            this.year,\n            this.month,\n            this.day + timedelta.days,\n            this.hour,\n            this.minute,\n            this.second + timedelta.seconds,\n            this.microsecond + timedelta.microseconds\n        );\n        // does not seem to closely follow python implementation.\n        return new PyDateTime(s.year, s.month, s.day, s.hour, s.minute, s.second, s.microsecond);\n    }\n\n    /**\n     * @param {any} other\n     * @returns {boolean}\n     */\n    isEqual(other) {\n        if (!(other instanceof PyDateTime)) {\n            return false;\n        }\n        return (\n            this.year === other.year &&\n            this.month === other.month &&\n            this.day === other.day &&\n            this.hour === other.hour &&\n            this.minute === other.minute &&\n            this.second === other.second &&\n            this.microsecond === other.microsecond\n        );\n    }\n\n    /**\n     * @param {string} format\n     * @returns {string}\n     */\n    strftime(format) {\n        return format.replace(/%([A-Za-z])/g, (m, c) => {\n            switch (c) {\n                case \"Y\":\n                    return fmt4(this.year);\n                case \"m\":\n                    return fmt2(this.month);\n                case \"d\":\n                    return fmt2(this.day);\n                case \"H\":\n                    return fmt2(this.hour);\n                case \"M\":\n                    return fmt2(this.minute);\n                case \"S\":\n                    return fmt2(this.second);\n            }\n            throw new ValueError(`No known conversion for ${m}`);\n        });\n    }\n\n    /**\n     * @param {PyTimeDelta} timedelta\n     * @returns {PyDateTime}\n     */\n    substract(timedelta) {\n        return this.add(timedelta.negate());\n    }\n\n    /**\n     * @returns {string}\n     */\n    toJSON() {\n        return this.strftime(\"%Y-%m-%d %H:%M:%S\");\n    }\n\n    /**\n     * @returns {PyDateTime}\n     */\n    to_utc() {\n        const d = new Date(\n            this.year,\n            this.month - 1,\n            this.day,\n            this.hour,\n            this.minute,\n            this.second\n        );\n        const timedelta = PyTimeDelta.create({ minutes: d.getTimezoneOffset() });\n        return this.add(timedelta);\n    }\n}\n\nexport class PyTime extends PyDate {\n    /**\n     * @param  {...any} args\n     * @returns {PyTime}\n     */\n    static create(...args) {\n        const namedArgs = parseArgs(args, [\"hour\", \"minute\", \"second\"]);\n        const hour = namedArgs.hour || 0;\n        const minute = namedArgs.minute || 0;\n        const second = namedArgs.second || 0;\n        return new PyTime(hour, minute, second);\n    }\n\n    constructor(hour, minute, second) {\n        const now = new Date();\n        const year = now.getFullYear();\n        const month = now.getMonth();\n        const day = now.getDate();\n        super(year, month, day);\n        this.hour = hour;\n        this.minute = minute;\n        this.second = second;\n    }\n\n    /**\n     * @param {string} format\n     * @returns {string}\n     */\n    strftime(format) {\n        return format.replace(/%([A-Za-z])/g, (m, c) => {\n            switch (c) {\n                case \"Y\":\n                    return fmt4(this.year);\n                case \"m\":\n                    return fmt2(this.month + 1);\n                case \"d\":\n                    return fmt2(this.day);\n                case \"H\":\n                    return fmt2(this.hour);\n                case \"M\":\n                    return fmt2(this.minute);\n                case \"S\":\n                    return fmt2(this.second);\n            }\n            throw new ValueError(`No known conversion for ${m}`);\n        });\n    }\n\n    toJSON() {\n        return this.strftime(\"%H:%M:%S\");\n    }\n}\n\n/*\n * This list is intended to be of that shape (32 days in december), it is used by\n * the algorithm that computes \"relativedelta yearday\". The algorithm was adapted\n * from the one in python (https://github.com/dateutil/dateutil/blob/2.7.3/dateutil/relativedelta.py#L199)\n */\nconst DAYS_IN_YEAR = [31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 366];\n\nconst TIME_PERIODS = [\"hour\", \"minute\", \"second\"];\nconst PERIODS = [\"year\", \"month\", \"day\", ...TIME_PERIODS];\n\nconst RELATIVE_KEYS = \"years months weeks days hours minutes seconds microseconds leapdays\".split(\n    \" \"\n);\nconst ABSOLUTE_KEYS =\n    \"year month day hour minute second microsecond weekday nlyearday yearday\".split(\" \");\n\nconst argsSpec = [\"dt1\", \"dt2\"]; // all other arguments are kwargs\nexport class PyRelativeDelta {\n    /**\n     * @param  {...any} args\n     * @returns {PyRelativeDelta}\n     */\n    static create(...args) {\n        const params = parseArgs(args, argsSpec);\n        if (\"dt1\" in params) {\n            throw new Error(\"relativedelta(dt1, dt2) is not supported for now\");\n        }\n        for (const period of PERIODS) {\n            if (period in params) {\n                const val = params[period];\n                assert(val >= 0, `${period} ${val} is out of range`);\n            }\n        }\n\n        for (const key of RELATIVE_KEYS) {\n            params[key] = params[key] || 0;\n        }\n        for (const key of ABSOLUTE_KEYS) {\n            params[key] = key in params ? params[key] : null;\n        }\n        params.days += 7 * params.weeks;\n\n        let yearDay = 0;\n        if (params.nlyearday) {\n            yearDay = params.nlyearday;\n        } else if (params.yearday) {\n            yearDay = params.yearday;\n            if (yearDay > 59) {\n                params.leapDays = -1;\n            }\n        }\n\n        if (yearDay) {\n            for (let monthIndex = 0; monthIndex < DAYS_IN_YEAR.length; monthIndex++) {\n                if (yearDay <= DAYS_IN_YEAR[monthIndex]) {\n                    params.month = monthIndex + 1;\n                    if (monthIndex === 0) {\n                        params.day = yearDay;\n                    } else {\n                        params.day = yearDay - DAYS_IN_YEAR[monthIndex - 1];\n                    }\n                    break;\n                }\n            }\n        }\n\n        return new PyRelativeDelta(params);\n    }\n\n    /**\n     * @param {PyDateTime|PyDate} date\n     * @param {PyRelativeDelta} delta\n     * @returns {PyDateTime|PyDate}\n     */\n    static add(date, delta) {\n        if (!(date instanceof PyDate || date instanceof PyDateTime)) {\n            throw new NotSupportedError();\n        }\n\n        // First pass: we want to determine which is our target year and if we will apply leap days\n        const s = tmxxx(\n            (delta.year || date.year) + delta.years,\n            (delta.month || date.month) + delta.months,\n            delta.day || date.day,\n            delta.hour || date.hour || 0,\n            delta.minute || date.minute || 0,\n            delta.second || date.seconds || 0,\n            delta.microseconds || date.microseconds || 0\n        );\n\n        const newDateTime = new PyDateTime(\n            s.year,\n            s.month,\n            s.day,\n            s.hour,\n            s.minute,\n            s.second,\n            s.microsecond\n        );\n\n        let leapDays = 0;\n        if (delta.leapDays && newDateTime.month > 2 && isLeap(newDateTime.year)) {\n            leapDays = delta.leapDays;\n        }\n\n        // Second pass: apply the difference in days, and the difference in time values\n        const temp = newDateTime.add(\n            PyTimeDelta.create({\n                days: delta.days + leapDays,\n                hours: delta.hours,\n                minutes: delta.minutes,\n                seconds: delta.seconds,\n                microseconds: delta.microseconds,\n            })\n        );\n\n        // Determine the right return type:\n        // First we look at the type of the incoming date object,\n        // then we look at the actual time values held by the computed date.\n        const hasTime = Boolean(temp.hour || temp.minute || temp.second || temp.microsecond);\n        const returnDate =\n            !hasTime && date instanceof PyDate ? new PyDate(temp.year, temp.month, temp.day) : temp;\n\n        // Final pass: target the wanted day of the week (if necessary)\n        if (delta.weekday !== null) {\n            const wantedDow = delta.weekday + 1; // python: Monday is 0 ; JS: Monday is 1;\n            const _date = new Date(returnDate.year, returnDate.month - 1, returnDate.day);\n            const days = (7 - _date.getDay() + wantedDow) % 7;\n            return returnDate.add(new PyTimeDelta(days, 0, 0));\n        }\n        return returnDate;\n    }\n\n    /**\n     * @param {PyDateTime|PyDate} date\n     * @param {PyRelativeDelta} delta\n     * @returns {PyDateTime|PyDate}\n     */\n    static substract(date, delta) {\n        return PyRelativeDelta.add(date, delta.negate());\n    }\n\n    /**\n     * @param {Object} params\n     * @param {+1|-1} sign\n     */\n    constructor(params = {}, sign = +1) {\n        this.years = sign * params.years;\n        this.months = sign * params.months;\n        this.days = sign * params.days;\n        this.hours = sign * params.hours;\n        this.minutes = sign * params.minutes;\n        this.seconds = sign * params.seconds;\n        this.microseconds = sign * params.microseconds;\n\n        this.leapDays = params.leapDays;\n\n        this.year = params.year;\n        this.month = params.month;\n        this.day = params.day;\n        this.hour = params.hour;\n        this.minute = params.minute;\n        this.second = params.second;\n        this.microsecond = params.microsecond;\n\n        this.weekday = params.weekday;\n    }\n\n    /**\n     * @returns {PyRelativeDelta}\n     */\n    negate() {\n        return new PyRelativeDelta(this, -1);\n    }\n\n    isEqual(other) {\n        // For now we don't do normalization in the constructor (or create method).\n        // That is, we only compute the overflows at the time we add or substract.\n        // This is why we can't support isEqual for now.\n        throw new NotSupportedError();\n    }\n}\n\nconst TIME_DELTA_KEYS = \"weeks days hours minutes seconds milliseconds microseconds\".split(\" \");\n\n/**\n * Returns a \"pair\" with the fractional and integer parts of x\n * @param {float}\n * @returns {[float,integer]}\n */\nfunction modf(x) {\n    const mod = x % 1;\n    return [mod < 0 ? mod + 1 : mod, Math.floor(x)];\n}\n\nexport class PyTimeDelta {\n    /**\n     * @param  {...any} args\n     * @returns {PyTimeDelta}\n     */\n    static create(...args) {\n        const namedArgs = parseArgs(args, [\"days\", \"seconds\", \"microseconds\"]);\n        for (const key of TIME_DELTA_KEYS) {\n            namedArgs[key] = namedArgs[key] || 0;\n        }\n\n        // a timedelta can be created using TIME_DELTA_KEYS with float/integer values\n        // but only days, seconds, microseconds are kept internally.\n        // --> some normalization occurs here\n\n        let d = 0;\n        let s = 0;\n        let us = 0; // ~ \u03bcs standard notation for microseconds\n\n        const days = namedArgs.days + namedArgs.weeks * 7;\n        let seconds = namedArgs.seconds + 60 * namedArgs.minutes + 3600 * namedArgs.hours;\n        let microseconds = namedArgs.microseconds + 1000 * namedArgs.milliseconds;\n\n        const [dFrac, dInt] = modf(days);\n        d = dInt;\n        let daysecondsfrac = 0;\n        if (dFrac) {\n            const [dsFrac, dsInt] = modf(dFrac * 24 * 3600);\n            s = dsInt;\n            daysecondsfrac = dsFrac;\n        }\n\n        const [sFrac, sInt] = modf(seconds);\n        seconds = sInt;\n        const secondsfrac = sFrac + daysecondsfrac;\n\n        divmod(seconds, 24 * 3600, (days, seconds) => {\n            d += days;\n            s += seconds;\n        });\n\n        microseconds += secondsfrac * 1e6;\n        divmod(microseconds, 1000000, (seconds, microseconds) => {\n            divmod(seconds, 24 * 3600, (days, seconds) => {\n                d += days;\n                s += seconds;\n                us += Math.round(microseconds);\n            });\n        });\n\n        return new PyTimeDelta(d, s, us);\n    }\n\n    /**\n     * @param {integer} days\n     * @param {integer} seconds\n     * @param {integer} microseconds\n     */\n    constructor(days, seconds, microseconds) {\n        this.days = days;\n        this.seconds = seconds;\n        this.microseconds = microseconds;\n    }\n\n    /**\n     * @param {PyTimeDelta} other\n     * @returns {PyTimeDelta}\n     */\n    add(other) {\n        return PyTimeDelta.create({\n            days: this.days + other.days,\n            seconds: this.seconds + other.seconds,\n            microseconds: this.microseconds + other.microseconds,\n        });\n    }\n\n    /**\n     * @param {integer} n\n     * @returns {PyTimeDelta}\n     */\n    divide(n) {\n        const us = (this.days * 24 * 3600 + this.seconds) * 1e6 + this.microseconds;\n        return PyTimeDelta.create({ microseconds: Math.floor(us / n) });\n    }\n\n    /**\n     * @param {any} other\n     * @returns {boolean}\n     */\n    isEqual(other) {\n        if (!(other instanceof PyTimeDelta)) {\n            return false;\n        }\n        return (\n            this.days === other.days &&\n            this.seconds === other.seconds &&\n            this.microseconds === other.microseconds\n        );\n    }\n\n    /**\n     * @returns {boolean}\n     */\n    isTrue() {\n        return this.days !== 0 || this.seconds !== 0 || this.microseconds !== 0;\n    }\n\n    /**\n     * @param {float} n\n     * @returns {PyTimeDelta}\n     */\n    multiply(n) {\n        return PyTimeDelta.create({\n            days: n * this.days,\n            seconds: n * this.seconds,\n            microseconds: n * this.microseconds,\n        });\n    }\n\n    /**\n     * @returns {PyTimeDelta}\n     */\n    negate() {\n        return PyTimeDelta.create({\n            days: -this.days,\n            seconds: -this.seconds,\n            microseconds: -this.microseconds,\n        });\n    }\n\n    /**\n     * @param {PyTimeDelta} other\n     * @returns {PyTimeDelta}\n     */\n    substract(other) {\n        return PyTimeDelta.create({\n            days: this.days - other.days,\n            seconds: this.seconds - other.seconds,\n            microseconds: this.microseconds - other.microseconds,\n        });\n    }\n\n    /**\n     * @returns {float}\n     */\n    total_seconds() {\n        return this.days * 86400 + this.seconds + this.microseconds / 1000000;\n    }\n}\n", "import { BUILTINS, EvaluationError, execOnIterable } from \"./py_builtin\";\nimport {\n    NotSupportedError,\n    PyDate,\n    PyDateTime,\n    PyRelativeDelta,\n    PyTime,\n    PyTimeDelta,\n} from \"./py_date\";\nimport { PY_DICT, toPyDict } from \"./py_utils\";\nimport { parseArgs } from \"./py_parser\";\n\n// -----------------------------------------------------------------------------\n// Types\n// -----------------------------------------------------------------------------\n\n/**\n * @typedef { import(\"./py_parser\").AST } AST\n */\n\n// -----------------------------------------------------------------------------\n// Constants and helpers\n// -----------------------------------------------------------------------------\n\nconst isTrue = BUILTINS.bool;\n\n/**\n * @param {AST} ast\n * @param {Object} context\n * @returns {any}\n */\nfunction applyUnaryOp(ast, context) {\n    const value = evaluate(ast.right, context);\n    switch (ast.op) {\n        case \"-\":\n            if (value instanceof Object && value.negate) {\n                return value.negate();\n            }\n            return -value;\n        case \"+\":\n            return value;\n        case \"not\":\n            return !isTrue(value);\n    }\n    throw new EvaluationError(`Unknown unary operator: ${ast.op}`);\n}\n\n/**\n * We want to maintain this order:\n *   None < number (boolean) < dict < string < list < dict\n * So, each type is mapped to a number to represent that order\n *\n * @param {any} val\n * @returns {number} index type\n */\nfunction pytypeIndex(val) {\n    switch (typeof val) {\n        case \"object\":\n            // None, List, Object, Dict\n            return val === null ? 1 : Array.isArray(val) ? 5 : 3;\n        case \"number\":\n            return 2;\n        case \"string\":\n            return 4;\n    }\n    throw new EvaluationError(`Unknown type: ${typeof val}`);\n}\n\n/**\n * @param {Object} obj\n * @returns {boolean}\n */\nfunction isConstructor(obj) {\n    return !!obj.prototype && !!obj.prototype.constructor.name;\n}\n\n/**\n * Compare two values\n *\n * @param {any} left\n * @param {any} right\n * @returns {boolean}\n */\nfunction isLess(left, right) {\n    if (typeof left === \"number\" && typeof right === \"number\") {\n        return left < right;\n    }\n    if (typeof left === \"boolean\") {\n        left = left ? 1 : 0;\n    }\n    if (typeof right === \"boolean\") {\n        right = right ? 1 : 0;\n    }\n    const leftIndex = pytypeIndex(left);\n    const rightIndex = pytypeIndex(right);\n    if (leftIndex === rightIndex) {\n        return left < right;\n    }\n    return leftIndex < rightIndex;\n}\n\n/**\n * @param {any} left\n * @param {any} right\n * @returns {boolean}\n */\nfunction isEqual(left, right) {\n    if (typeof left !== typeof right) {\n        if (typeof left === \"boolean\" && typeof right === \"number\") {\n            return right === (left ? 1 : 0);\n        }\n        if (typeof left === \"number\" && typeof right === \"boolean\") {\n            return left === (right ? 1 : 0);\n        }\n        return false;\n    }\n    if (left instanceof Object && left.isEqual) {\n        return left.isEqual(right);\n    }\n    return left === right;\n}\n\n/**\n * @param {any} left\n * @param {any} right\n * @returns {boolean}\n */\nfunction isIn(left, right) {\n    if (Array.isArray(right)) {\n        return right.includes(left);\n    }\n    if (typeof right === \"string\" && typeof left === \"string\") {\n        return right.includes(left);\n    }\n    if (typeof right === \"object\") {\n        return left in right;\n    }\n    return false;\n}\n\n/**\n * @param {AST} ast\n * @param {object} context\n * @returns {any}\n */\nfunction applyBinaryOp(ast, context) {\n    const left = evaluate(ast.left, context);\n    const right = evaluate(ast.right, context);\n    switch (ast.op) {\n        case \"+\": {\n            const relativeDeltaOnLeft = left instanceof PyRelativeDelta;\n            const relativeDeltaOnRight = right instanceof PyRelativeDelta;\n            if (relativeDeltaOnLeft || relativeDeltaOnRight) {\n                const date = relativeDeltaOnLeft ? right : left;\n                const delta = relativeDeltaOnLeft ? left : right;\n                return PyRelativeDelta.add(date, delta);\n            }\n\n            const timeDeltaOnLeft = left instanceof PyTimeDelta;\n            const timeDeltaOnRight = right instanceof PyTimeDelta;\n            if (timeDeltaOnLeft && timeDeltaOnRight) {\n                return left.add(right);\n            }\n            if (timeDeltaOnLeft) {\n                if (right instanceof PyDate || right instanceof PyDateTime) {\n                    return right.add(left);\n                } else {\n                    throw new NotSupportedError();\n                }\n            }\n            if (timeDeltaOnRight) {\n                if (left instanceof PyDate || left instanceof PyDateTime) {\n                    return left.add(right);\n                } else {\n                    throw new NotSupportedError();\n                }\n            }\n            if (left instanceof Array && right instanceof Array) {\n                return [...left, ...right];\n            }\n\n            return left + right;\n        }\n        case \"-\": {\n            const isRightDelta = right instanceof PyRelativeDelta;\n            if (isRightDelta) {\n                return PyRelativeDelta.substract(left, right);\n            }\n\n            const timeDeltaOnRight = right instanceof PyTimeDelta;\n            if (timeDeltaOnRight) {\n                if (left instanceof PyTimeDelta) {\n                    return left.substract(right);\n                } else if (left instanceof PyDate || left instanceof PyDateTime) {\n                    return left.substract(right);\n                } else {\n                    throw new NotSupportedError();\n                }\n            }\n\n            if (left instanceof PyDate) {\n                return left.substract(right);\n            }\n            return left - right;\n        }\n        case \"*\": {\n            const timeDeltaOnLeft = left instanceof PyTimeDelta;\n            const timeDeltaOnRight = right instanceof PyTimeDelta;\n            if (timeDeltaOnLeft || timeDeltaOnRight) {\n                const number = timeDeltaOnLeft ? right : left;\n                const delta = timeDeltaOnLeft ? left : right;\n                return delta.multiply(number); // check number type?\n            }\n\n            return left * right;\n        }\n        case \"/\":\n            return left / right;\n        case \"%\":\n            return left % right;\n        case \"//\":\n            if (left instanceof PyTimeDelta) {\n                return left.divide(right); // check number type?\n            }\n            return Math.floor(left / right);\n        case \"**\":\n            return left ** right;\n        case \"==\":\n            return isEqual(left, right);\n        case \"<>\":\n        case \"!=\":\n            return !isEqual(left, right);\n        case \"<\":\n            return isLess(left, right);\n        case \">\":\n            return isLess(right, left);\n        case \">=\":\n            return isEqual(left, right) || isLess(right, left);\n        case \"<=\":\n            return isEqual(left, right) || isLess(left, right);\n        case \"in\":\n            return isIn(left, right);\n        case \"not in\":\n            return !isIn(left, right);\n    }\n    throw new EvaluationError(`Unknown binary operator: ${ast.op}`);\n}\n\nconst DICT = {\n    get(...args) {\n        const { key, defValue } = parseArgs(args, [\"key\", \"defValue\"]);\n        if (key in this) {\n            return this[key];\n        } else if (defValue) {\n            return defValue;\n        }\n        return null;\n    },\n};\n\nconst STRING = {\n    lower() {\n        return this.toLowerCase();\n    },\n    upper() {\n        return this.toUpperCase();\n    },\n};\n\nfunction applyFunc(key, func, set, ...args) {\n    // we always receive at least one argument: kwargs (return fnValue(...args, kwargs); in FunctionCall case)\n    if (args.length === 1) {\n        return new Set(set);\n    }\n    if (args.length > 2) {\n        throw new EvaluationError(\n            `${key}: py_js supports at most 1 argument, got (${args.length - 1})`\n        );\n    }\n    return execOnIterable(args[0], func);\n}\n\nconst SET = {\n    intersection(...args) {\n        return applyFunc(\n            \"intersection\",\n            (iterable) => {\n                const intersection = new Set();\n                for (const i of iterable) {\n                    if (this.has(i)) {\n                        intersection.add(i);\n                    }\n                }\n                return intersection;\n            },\n            this,\n            ...args\n        );\n    },\n    difference(...args) {\n        return applyFunc(\n            \"difference\",\n            (iterable) => {\n                iterable = new Set(iterable);\n                const difference = new Set();\n                for (const e of this) {\n                    if (!iterable.has(e)) {\n                        difference.add(e);\n                    }\n                }\n                return difference;\n            },\n            this,\n            ...args\n        );\n    },\n    union(...args) {\n        return applyFunc(\"union\", (iterable) => new Set([...this, ...iterable]), this, ...args);\n    },\n};\n\n// -----------------------------------------------------------------------------\n// Evaluate function\n// -----------------------------------------------------------------------------\n\n/**\n * @param {Function} _class the class whose methods we want\n * @returns {Function[]} an array containing the methods defined on the class,\n *  including the constructor\n */\nfunction methods(_class) {\n    return Object.getOwnPropertyNames(_class.prototype).map((prop) => _class.prototype[prop]);\n}\n\nconst allowedFns = new Set([\n    BUILTINS.time.strftime,\n    BUILTINS.set,\n    BUILTINS.bool,\n    BUILTINS.min,\n    BUILTINS.max,\n    BUILTINS.context_today,\n    BUILTINS.datetime.datetime.now,\n    BUILTINS.datetime.datetime.combine,\n    BUILTINS.datetime.date.today,\n    ...methods(BUILTINS.relativedelta),\n    ...Object.values(BUILTINS.datetime).flatMap((obj) => methods(obj)),\n    ...Object.values(SET),\n    ...Object.values(DICT),\n    ...Object.values(STRING),\n]);\n\nconst unboundFn = Symbol(\"unbound function\");\n\n/**\n * @param {AST} ast\n * @param {Object} context\n * @returns {any}\n */\nexport function evaluate(ast, context = {}) {\n    const dicts = new Set();\n    let pyContext;\n    const evalContext = Object.create(context);\n    if (!evalContext.context) {\n        Object.defineProperty(evalContext, \"context\", {\n            get() {\n                if (!pyContext) {\n                    pyContext = toPyDict(context);\n                }\n                return pyContext;\n            },\n        });\n    }\n\n    function _innerEvaluate(ast) {\n        switch (ast.type) {\n            case 0 /* Number */:\n            case 1 /* String */:\n                return ast.value;\n            case 5 /* Name */:\n                if (ast.value in evalContext) {\n                    return evalContext[ast.value];\n                } else if (ast.value in BUILTINS) {\n                    return BUILTINS[ast.value];\n                } else {\n                    throw new EvaluationError(`Name '${ast.value}' is not defined`);\n                }\n            case 3 /* None */:\n                return null;\n            case 2 /* Boolean */:\n                return ast.value;\n            case 6 /* UnaryOperator */:\n                return applyUnaryOp(ast, evalContext);\n            case 7 /* BinaryOperator */:\n                return applyBinaryOp(ast, evalContext);\n            case 14 /* BooleanOperator */: {\n                const left = _evaluate(ast.left);\n                if (ast.op === \"and\") {\n                    return isTrue(left) ? _evaluate(ast.right) : left;\n                } else {\n                    return isTrue(left) ? left : _evaluate(ast.right);\n                }\n            }\n            case 4 /* List */:\n            case 10 /* Tuple */:\n                return ast.value.map(_evaluate);\n            case 11 /* Dictionary */: {\n                const dict = {};\n                for (const key in ast.value) {\n                    dict[key] = _evaluate(ast.value[key]);\n                }\n                dicts.add(dict);\n                return dict;\n            }\n            case 8 /* FunctionCall */: {\n                const fnValue = _evaluate(ast.fn);\n                const args = ast.args.map(_evaluate);\n                const kwargs = {};\n                for (const kwarg in ast.kwargs) {\n                    kwargs[kwarg] = _evaluate(ast.kwargs[kwarg]);\n                }\n                if (\n                    fnValue === PyDate ||\n                    fnValue === PyDateTime ||\n                    fnValue === PyTime ||\n                    fnValue === PyRelativeDelta ||\n                    fnValue === PyTimeDelta\n                ) {\n                    return fnValue.create(...args, kwargs);\n                }\n                return fnValue(...args, kwargs);\n            }\n            case 12 /* Lookup */: {\n                const dict = _evaluate(ast.target);\n                const key = _evaluate(ast.key);\n                return dict[key];\n            }\n            case 13 /* If */: {\n                if (isTrue(_evaluate(ast.condition))) {\n                    return _evaluate(ast.ifTrue);\n                } else {\n                    return _evaluate(ast.ifFalse);\n                }\n            }\n            case 15 /* ObjLookup */: {\n                let left = _evaluate(ast.obj);\n                let result;\n                if (dicts.has(left) || Object.isPrototypeOf.call(PY_DICT, left)) {\n                    // this is a dictionary => need to apply dict methods\n                    result = DICT[ast.key];\n                } else if (typeof left === \"string\") {\n                    result = STRING[ast.key];\n                } else if (left instanceof Set) {\n                    result = SET[ast.key];\n                } else if (ast.key == \"get\" && typeof left === \"object\") {\n                    result = DICT[ast.key];\n                    left = toPyDict(left);\n                } else {\n                    result = left[ast.key];\n                }\n                if (typeof result === \"function\") {\n                    if (!isConstructor(result)) {\n                        const bound = result.bind(left);\n                        bound[unboundFn] = result;\n                        return bound;\n                    }\n                }\n                return result;\n            }\n        }\n        throw new EvaluationError(`AST of type ${ast.type} cannot be evaluated`);\n    }\n\n    /**\n     * @param {AST} ast\n     */\n    function _evaluate(ast) {\n        const val = _innerEvaluate(ast);\n        if (typeof val === \"function\" && !allowedFns.has(val) && !allowedFns.has(val[unboundFn])) {\n            throw new Error(\"Invalid Function Call\");\n        }\n        return val;\n    }\n    return _evaluate(ast);\n}\n", "import { binaryOperators, comparators } from \"./py_tokenizer\";\n\n// -----------------------------------------------------------------------------\n// Types\n// -----------------------------------------------------------------------------\n\n/**\n * @typedef { import(\"./py_tokenizer\").Token } Token\n */\n\n/**\n * @typedef {{type: 0, value: number}} ASTNumber\n * @typedef {{type: 1, value: string}} ASTString\n * @typedef {{type: 2, value: boolean}} ASTBoolean\n * @typedef {{type: 3}} ASTNone\n * @typedef {{type: 4, value: AST[]}} ASTList\n * @typedef {{type: 5, value: string}} ASTName\n * @typedef {{type: 6, op: string, right: AST}} ASTUnaryOperator\n * @typedef {{type: 7, op: string, left: AST, right: AST}} ASTBinaryOperator\n * @typedef {{type: 8, fn: AST, args: AST[], kwargs: {[key: string]: AST}}} ASTFunctionCall\n * @typedef {{type: 9, name: ASTName, value: AST}} ASTAssignment\n * @typedef {{type: 10, value: AST[]}} ASTTuple\n * @typedef {{type: 11, value: { [key: string]: AST}}} ASTDictionary\n * @typedef {{type: 12, target: AST, key: AST}} ASTLookup\n * @typedef {{type: 13, condition: AST, ifTrue: AST, ifFalse: AST}} ASTIf\n * @typedef {{type: 14, op: string, left: AST, right: AST}} ASTBooleanOperator\n * @typedef {{type: 15, obj: AST, key: string}} ASTObjLookup\n *\n * @typedef { ASTNumber | ASTString | ASTBoolean | ASTNone | ASTList | ASTName | ASTUnaryOperator | ASTBinaryOperator | ASTFunctionCall | ASTAssignment | ASTTuple | ASTDictionary |ASTLookup | ASTIf | ASTBooleanOperator | ASTObjLookup} AST\n */\n\nexport class ParserError extends Error {}\n\n// -----------------------------------------------------------------------------\n// Constants and helpers\n// -----------------------------------------------------------------------------\n\nconst chainedOperators = new Set(comparators);\nconst infixOperators = new Set(binaryOperators.concat(comparators));\n\n/**\n * Compute the \"binding power\" of a symbol\n *\n * @param {string} symbol\n * @returns {number}\n */\nexport function bp(symbol) {\n    switch (symbol) {\n        case \"=\":\n            return 10;\n        case \"if\":\n            return 20;\n        case \"in\":\n        case \"not in\":\n        case \"is\":\n        case \"is not\":\n        case \"<\":\n        case \"<=\":\n        case \">\":\n        case \">=\":\n        case \"<>\":\n        case \"==\":\n        case \"!=\":\n            return 60;\n        case \"or\":\n            return 30;\n        case \"and\":\n            return 40;\n        case \"not\":\n            return 50;\n        case \"|\":\n            return 70;\n        case \"^\":\n            return 80;\n        case \"&\":\n            return 90;\n        case \"<<\":\n        case \">>\":\n            return 100;\n        case \"+\":\n        case \"-\":\n            return 110;\n        case \"*\":\n        case \"/\":\n        case \"//\":\n        case \"%\":\n            return 120;\n        case \"**\":\n            return 140;\n        case \".\":\n        case \"(\":\n        case \"[\":\n            return 150;\n    }\n    return 0;\n}\n\n/**\n * Compute binding power of a symbol\n *\n * @param {Token} token\n * @returns {number}\n */\nfunction bindingPower(token) {\n    return token.type === 2 /* Symbol */ ? bp(token.value) : 0;\n}\n\n/**\n * Check if a token is a symbol of a given value\n *\n * @param {Token} token\n * @param {string} value\n * @returns {boolean}\n */\nfunction isSymbol(token, value) {\n    return token.type === 2 /* Symbol */ && token.value === value;\n}\n\n/**\n * @param {Token} current\n * @param {Token[]} tokens\n * @returns {AST}\n */\nfunction parsePrefix(current, tokens) {\n    switch (current.type) {\n        case 0 /* Number */:\n            return { type: 0 /* Number */, value: current.value };\n        case 1 /* String */:\n            return { type: 1 /* String */, value: current.value };\n        case 4 /* Constant */:\n            if (current.value === \"None\") {\n                return { type: 3 /* None */ };\n            } else {\n                return { type: 2 /* Boolean */, value: current.value === \"True\" };\n            }\n        case 3 /* Name */:\n            return { type: 5 /* Name */, value: current.value };\n        case 2 /* Symbol */:\n            switch (current.value) {\n                case \"-\":\n                case \"+\":\n                case \"~\":\n                    return {\n                        type: 6 /* UnaryOperator */,\n                        op: current.value,\n                        right: _parse(tokens, 130),\n                    };\n                case \"not\":\n                    return {\n                        type: 6 /* UnaryOperator */,\n                        op: current.value,\n                        right: _parse(tokens, 50),\n                    };\n                case \"(\": {\n                    const content = [];\n                    let isTuple = false;\n                    while (tokens[0] && !isSymbol(tokens[0], \")\")) {\n                        content.push(_parse(tokens, 0));\n                        if (tokens[0]) {\n                            if (tokens[0] && isSymbol(tokens[0], \",\")) {\n                                isTuple = true;\n                                tokens.shift();\n                            } else if (!isSymbol(tokens[0], \")\")) {\n                                throw new ParserError(\"parsing error\");\n                            }\n                        } else {\n                            throw new ParserError(\"parsing error\");\n                        }\n                    }\n                    if (!tokens[0] || !isSymbol(tokens[0], \")\")) {\n                        throw new ParserError(\"parsing error\");\n                    }\n                    tokens.shift();\n                    isTuple = isTuple || content.length === 0;\n                    return isTuple ? { type: 10 /* Tuple */, value: content } : content[0];\n                }\n                case \"[\": {\n                    const value = [];\n                    while (tokens[0] && !isSymbol(tokens[0], \"]\")) {\n                        value.push(_parse(tokens, 0));\n                        if (tokens[0]) {\n                            if (isSymbol(tokens[0], \",\")) {\n                                tokens.shift();\n                            } else if (!isSymbol(tokens[0], \"]\")) {\n                                throw new ParserError(\"parsing error\");\n                            }\n                        }\n                    }\n                    if (!tokens[0] || !isSymbol(tokens[0], \"]\")) {\n                        throw new ParserError(\"parsing error\");\n                    }\n                    tokens.shift();\n                    return { type: 4 /* List */, value };\n                }\n                case \"{\": {\n                    const dict = {};\n                    while (tokens[0] && !isSymbol(tokens[0], \"}\")) {\n                        const key = _parse(tokens, 0);\n                        if (\n                            (key.type !== 1 /* String */ && key.type !== 0) /* Number */ ||\n                            !tokens[0] ||\n                            !isSymbol(tokens[0], \":\")\n                        ) {\n                            throw new ParserError(\"parsing error\");\n                        }\n                        tokens.shift();\n                        const value = _parse(tokens, 0);\n                        dict[key.value] = value;\n                        if (isSymbol(tokens[0], \",\")) {\n                            tokens.shift();\n                        }\n                    }\n                    // remove the } token\n                    if (!tokens.shift()) {\n                        throw new ParserError(\"parsing error\");\n                    }\n                    return { type: 11 /* Dictionary */, value: dict };\n                }\n            }\n    }\n    throw new ParserError(\"Token cannot be parsed\");\n}\n\n/**\n * @param {AST} ast\n * @param {Token} current\n * @param {Token[]} tokens\n * @returns {AST}\n */\nfunction parseInfix(left, current, tokens) {\n    switch (current.type) {\n        case 2 /* Symbol */:\n            if (infixOperators.has(current.value)) {\n                let right = _parse(tokens, bindingPower(current));\n                if (current.value === \"and\" || current.value === \"or\") {\n                    return {\n                        type: 14 /* BooleanOperator */,\n                        op: current.value,\n                        left,\n                        right,\n                    };\n                } else if (current.value === \".\") {\n                    if (right.type === 5 /* Name */) {\n                        return {\n                            type: 15 /* ObjLookup */,\n                            obj: left,\n                            key: right.value,\n                        };\n                    } else {\n                        throw new ParserError(\"invalid obj lookup\");\n                    }\n                }\n                let op = {\n                    type: 7 /* BinaryOperator */,\n                    op: current.value,\n                    left,\n                    right,\n                };\n                while (\n                    chainedOperators.has(current.value) &&\n                    tokens[0] &&\n                    tokens[0].type === 2 /* Symbol */ &&\n                    chainedOperators.has(tokens[0].value)\n                ) {\n                    const nextToken = tokens.shift();\n                    op = {\n                        type: 14 /* BooleanOperator */,\n                        op: \"and\",\n                        left: op,\n                        right: {\n                            type: 7 /* BinaryOperator */,\n                            op: nextToken.value,\n                            left: right,\n                            right: _parse(tokens, bindingPower(nextToken)),\n                        },\n                    };\n                    right = op.right.right;\n                }\n                return op;\n            }\n            switch (current.value) {\n                case \"(\": {\n                    // function call\n                    const args = [];\n                    const kwargs = {};\n                    while (tokens[0] && !isSymbol(tokens[0], \")\")) {\n                        const arg = _parse(tokens, 0);\n                        if (arg.type === 9 /* Assignment */) {\n                            kwargs[arg.name.value] = arg.value;\n                        } else {\n                            args.push(arg);\n                        }\n                        if (tokens[0] && isSymbol(tokens[0], \",\")) {\n                            tokens.shift();\n                        }\n                    }\n                    if (!tokens[0] || !isSymbol(tokens[0], \")\")) {\n                        throw new ParserError(\"parsing error\");\n                    }\n                    tokens.shift();\n                    return { type: 8 /* FunctionCall */, fn: left, args, kwargs };\n                }\n                case \"=\":\n                    if (left.type === 5 /* Name */) {\n                        return {\n                            type: 9 /* Assignment */,\n                            name: left,\n                            value: _parse(tokens, 10),\n                        };\n                    }\n                    break;\n                case \"[\": {\n                    // lookup in dictionary\n                    const key = _parse(tokens);\n                    if (!tokens[0] || !isSymbol(tokens[0], \"]\")) {\n                        throw new ParserError(\"parsing error\");\n                    }\n                    tokens.shift();\n                    return {\n                        type: 12 /* Lookup */,\n                        target: left,\n                        key: key,\n                    };\n                }\n                case \"if\": {\n                    const condition = _parse(tokens);\n                    if (!tokens[0] || !isSymbol(tokens[0], \"else\")) {\n                        throw new ParserError(\"parsing error\");\n                    }\n                    tokens.shift();\n                    const ifFalse = _parse(tokens);\n                    return {\n                        type: 13 /* If */,\n                        condition,\n                        ifTrue: left,\n                        ifFalse,\n                    };\n                }\n            }\n    }\n    throw new ParserError(\"Token cannot be parsed\");\n}\n\n/**\n * @param {Token[]} tokens\n * @param {number} [bp]\n * @returns {AST}\n */\nfunction _parse(tokens, bp = 0) {\n    const token = tokens.shift();\n    let expr = parsePrefix(token, tokens);\n    while (tokens[0] && bindingPower(tokens[0]) > bp) {\n        expr = parseInfix(expr, tokens.shift(), tokens);\n    }\n    return expr;\n}\n\n// -----------------------------------------------------------------------------\n// Parse function\n// -----------------------------------------------------------------------------\n\n/**\n * Parse a list of tokens\n *\n * @param {Token[]} tokens\n * @returns {AST}\n */\nexport function parse(tokens) {\n    if (tokens.length) {\n        const ast = _parse(tokens, 0);\n        if (tokens.length) {\n            throw new ParserError(\"Token(s) unused\");\n        }\n        return ast;\n    }\n    throw new ParserError(\"Missing token\");\n}\n\n/**\n * @param {any[]} args\n * @param {string[]} spec\n * @returns {{[name: string]: any}}\n */\nexport function parseArgs(args, spec) {\n    const last = args[args.length - 1];\n    const unnamedArgs = typeof last === \"object\" ? args.slice(0, -1) : args;\n    const kwargs = typeof last === \"object\" ? last : {};\n    for (const [index, val] of unnamedArgs.entries()) {\n        kwargs[spec[index]] = val;\n    }\n    return kwargs;\n}\n", "// -----------------------------------------------------------------------------\n// Types\n// -----------------------------------------------------------------------------\n\n/**\n * @typedef {{type: 0, value: number}} TokenNumber\n *\n * @typedef {{type: 1, value: string}} TokenString\n *\n * @typedef {{type: 2, value: string}} TokenSymbol\n *\n * @typedef {{type: 3, value: string}} TokenName\n *\n * @typedef {{type: 4, value: string}} TokenConstant\n *\n * @typedef {TokenNumber | TokenString | TokenSymbol | TokenName | TokenConstant} Token\n */\n\nexport class TokenizerError extends Error {}\n\n// -----------------------------------------------------------------------------\n// Helpers and Constants\n// -----------------------------------------------------------------------------\n\n/**\n * Directly maps a single escape code to an output character\n */\nconst directMap = {\n    \"\\\\\": \"\\\\\",\n    '\"': '\"',\n    \"'\": \"'\",\n    a: \"\\x07\",\n    b: \"\\x08\",\n    f: \"\\x0c\",\n    n: \"\\n\",\n    r: \"\\r\",\n    t: \"\\t\",\n    v: \"\\v\",\n};\n\n/**\n * Implements the decoding of Python string literals (embedded in\n * JS strings) into actual JS strings. This includes the decoding\n * of escapes into their corresponding JS\n * characters/codepoints/whatever.\n *\n * The ``unicode`` flags notes whether the literal should be\n * decoded as a bytestring literal or a unicode literal, which\n * pretty much only impacts decoding (or not) of unicode escapes\n * at this point since bytestrings are not technically handled\n * (everything is decoded to JS \"unicode\" strings)\n *\n * Eventurally, ``str`` could eventually use typed arrays, that'd\n * be interesting...\n *\n * @param {string} str\n * @param {boolean} unicode\n * @returns {string}\n */\nfunction decodeStringLiteral(str, unicode) {\n    const out = [];\n    let code;\n    for (var i = 0; i < str.length; ++i) {\n        if (str[i] !== \"\\\\\") {\n            out.push(str[i]);\n            continue;\n        }\n        var escape = str[i + 1];\n        if (escape in directMap) {\n            out.push(directMap[escape]);\n            ++i;\n            continue;\n        }\n        switch (escape) {\n            // Ignored\n            case \"\\n\":\n                ++i;\n                continue;\n            // Character named name in the Unicode database (Unicode only)\n            case \"N\":\n                if (!unicode) {\n                    break;\n                }\n                throw new TokenizerError(\"SyntaxError: \\\\N{} escape not implemented\");\n            case \"u\":\n                if (!unicode) {\n                    break;\n                }\n                var uni = str.slice(i + 2, i + 6);\n                if (!/[0-9a-f]{4}/i.test(uni)) {\n                    throw new TokenizerError(\n                        [\n                            \"SyntaxError: (unicode error) 'unicodeescape' codec\",\n                            \" can't decode bytes in position \",\n                            i,\n                            \"-\",\n                            i + 4,\n                            \": truncated \\\\uXXXX escape\",\n                        ].join(\"\")\n                    );\n                }\n                code = parseInt(uni, 16);\n                out.push(String.fromCharCode(code));\n                // escape + 4 hex digits\n                i += 5;\n                continue;\n            case \"U\":\n                if (!unicode) {\n                    break;\n                }\n                // TODO: String.fromCodePoint\n                throw new TokenizerError(\"SyntaxError: \\\\U escape not implemented\");\n            case \"x\":\n                // get 2 hex digits\n                var hex = str.slice(i + 2, i + 4);\n                if (!/[0-9a-f]{2}/i.test(hex)) {\n                    if (!unicode) {\n                        throw new TokenizerError(\"ValueError: invalid \\\\x escape\");\n                    }\n                    throw new TokenizerError(\n                        [\n                            \"SyntaxError: (unicode error) 'unicodeescape'\",\n                            \" codec can't decode bytes in position \",\n                            i,\n                            \"-\",\n                            i + 2,\n                            \": truncated \\\\xXX escape\",\n                        ].join(\"\")\n                    );\n                }\n                code = parseInt(hex, 16);\n                out.push(String.fromCharCode(code));\n                // skip escape + 2 hex digits\n                i += 3;\n                continue;\n            default:\n                // Check if octal\n                if (!/[0-8]/.test(escape)) {\n                    break;\n                }\n                var r = /[0-8]{1,3}/g;\n                r.lastIndex = i + 1;\n                var m = r.exec(str);\n                var oct = m[0];\n                code = parseInt(oct, 8);\n                out.push(String.fromCharCode(code));\n                // skip matchlength\n                i += oct.length;\n                continue;\n        }\n        out.push(\"\\\\\");\n    }\n    return out.join(\"\");\n}\n\nconst constants = new Set([\"None\", \"False\", \"True\"]);\n\nexport const comparators = [\n    \"in\",\n    \"not\",\n    \"not in\",\n    \"is\",\n    \"is not\",\n    \"<\",\n    \"<=\",\n    \">\",\n    \">=\",\n    \"<>\",\n    \"!=\",\n    \"==\",\n];\n\nexport const binaryOperators = [\n    \"or\",\n    \"and\",\n    \"|\",\n    \"^\",\n    \"&\",\n    \"<<\",\n    \">>\",\n    \"+\",\n    \"-\",\n    \"*\",\n    \"/\",\n    \"//\",\n    \"%\",\n    \"~\",\n    \"**\",\n    \".\",\n];\n\nexport const unaryOperators = [\"-\"];\n\nconst symbols = new Set([\n    ...[\"(\", \")\", \"[\", \"]\", \"{\", \"}\", \":\", \",\"],\n    ...[\"if\", \"else\", \"lambda\", \"=\"],\n    ...comparators,\n    ...binaryOperators,\n    ...unaryOperators,\n]);\n\n// Regexps\nfunction group(...args) {\n    return \"(\" + args.join(\"|\") + \")\";\n}\n\nconst Name = \"[a-zA-Z_]\\\\w*\";\nconst Whitespace = \"[ \\\\f\\\\t]*\";\nconst DecNumber = \"\\\\d+(L|l)?\";\nconst IntNumber = DecNumber;\n\nconst Exponent = \"[eE][+-]?\\\\d+\";\nconst PointFloat = group(`\\\\d+\\\\.\\\\d*(${Exponent})?`, `\\\\.\\\\d+(${Exponent})?`);\n// Exponent not optional when no decimal point\nconst FloatNumber = group(PointFloat, `\\\\d+${Exponent}`);\n\nconst Number = group(FloatNumber, IntNumber);\nconst Operator = group(\"\\\\*\\\\*=?\", \">>=?\", \"<<=?\", \"<>\", \"!=\", \"//=?\", \"[+\\\\-*/%&|^=<>]=?\", \"~\");\nconst Bracket = \"[\\\\[\\\\]\\\\(\\\\)\\\\{\\\\}]\";\nconst Special = \"[:;.,`@]\";\nconst Funny = group(Operator, Bracket, Special);\nconst ContStr = group(\n    \"([uU])?'([^\\n'\\\\\\\\]*(?:\\\\\\\\.[^\\n'\\\\\\\\]*)*)'\",\n    '([uU])?\"([^\\n\"\\\\\\\\]*(?:\\\\\\\\.[^\\n\"\\\\\\\\]*)*)\"'\n);\nconst PseudoToken = Whitespace + group(Number, Funny, ContStr, Name);\nconst NumberPattern = new RegExp(\"^\" + Number + \"$\");\nconst StringPattern = new RegExp(\"^\" + ContStr + \"$\");\nconst NamePattern = new RegExp(\"^\" + Name + \"$\");\nconst strip = new RegExp(\"^\" + Whitespace);\n\n// -----------------------------------------------------------------------------\n// Tokenize function\n// -----------------------------------------------------------------------------\n\n/**\n * Transform a string into a list of tokens\n *\n * @param {string} str\n * @returns {Token[]}\n */\nexport function tokenize(str) {\n    const tokens = [];\n    const max = str.length;\n    let start = 0;\n    let end = 0;\n    // /g flag makes repeated exec() have memory\n    const pseudoprog = new RegExp(PseudoToken, \"g\");\n    while (pseudoprog.lastIndex < max) {\n        const pseudomatch = pseudoprog.exec(str);\n        if (!pseudomatch) {\n            // if match failed on trailing whitespace, end tokenizing\n            if (/^\\s+$/.test(str.slice(end))) {\n                break;\n            }\n            throw new TokenizerError(\n                \"Failed to tokenize <<\" +\n                    str +\n                    \">> at index \" +\n                    (end || 0) +\n                    \"; parsed so far: \" +\n                    tokens\n            );\n        }\n        if (pseudomatch.index > end) {\n            if (str.slice(end, pseudomatch.index).trim()) {\n                throw new TokenizerError(\"Invalid expression\");\n            }\n        }\n        start = pseudomatch.index;\n        end = pseudoprog.lastIndex;\n        let token = str.slice(start, end).replace(strip, \"\");\n        if (NumberPattern.test(token)) {\n            tokens.push({\n                type: 0 /* Number */,\n                value: parseFloat(token),\n            });\n        } else if (StringPattern.test(token)) {\n            var m = StringPattern.exec(token);\n            tokens.push({\n                type: 1 /* String */,\n                value: decodeStringLiteral(m[3] !== undefined ? m[3] : m[5], !!(m[2] || m[4])),\n            });\n        } else if (symbols.has(token)) {\n            // transform 'not in' and 'is not' in a single token\n            if (token === \"in\" && tokens.length > 0 && tokens[tokens.length - 1].value === \"not\") {\n                token = \"not in\";\n                tokens.pop();\n            } else if (\n                token === \"not\" &&\n                tokens.length > 0 &&\n                tokens[tokens.length - 1].value === \"is\"\n            ) {\n                token = \"is not\";\n                tokens.pop();\n            }\n            tokens.push({\n                type: 2 /* Symbol */,\n                value: token,\n            });\n        } else if (constants.has(token)) {\n            tokens.push({\n                type: 4 /* Constant */,\n                value: token,\n            });\n        } else if (NamePattern.test(token)) {\n            tokens.push({\n                type: 3 /* Name */,\n                value: token,\n            });\n        } else {\n            throw new TokenizerError(\"Invalid expression\");\n        }\n    }\n    return tokens;\n}\n", "import { bp } from \"./py_parser\";\nimport { PyDate, PyDateTime } from \"./py_date\";\n\n// -----------------------------------------------------------------------------\n// Types\n// -----------------------------------------------------------------------------\n\n/**\n * @typedef { import(\"./py_parser\").AST } AST\n */\n\n// -----------------------------------------------------------------------------\n// Utils\n// -----------------------------------------------------------------------------\n\n/**\n * Represent any value as a primitive AST\n *\n * @param {any} value\n * @returns {AST}\n */\nexport function toPyValue(value) {\n    switch (typeof value) {\n        case \"string\":\n            return { type: 1 /* String */, value };\n        case \"number\":\n            return { type: 0 /* Number */, value };\n        case \"boolean\":\n            return { type: 2 /* Boolean */, value };\n        case \"object\":\n            if (Array.isArray(value)) {\n                return { type: 4 /* List */, value: value.map(toPyValue) };\n            } else if (value === null) {\n                return { type: 3 /* None */ };\n            } else if (value instanceof Date) {\n                return { type: 1, value: PyDateTime.convertDate(value) };\n            } else if (value instanceof PyDate || value instanceof PyDateTime) {\n                return { type: 1, value };\n            } else {\n                const content = {};\n                for (const key in value) {\n                    content[key] = toPyValue(value[key]);\n                }\n                return { type: 11 /* Dictionary */, value: content };\n            }\n        default:\n            throw new Error(\"Invalid type\");\n    }\n}\n\n/**\n * @param {AST} ast\n * @param {number} [lbp] left binding power\n * @return {string}\n */\nexport function formatAST(ast, lbp = 0) {\n    switch (ast.type) {\n        case 3 /* None */:\n            return \"None\";\n        case 1 /* String */:\n            return JSON.stringify(ast.value);\n        case 0 /* Number */:\n            return String(ast.value);\n        case 2 /* Boolean */:\n            return ast.value ? \"True\" : \"False\";\n        case 4 /* List */:\n            return `[${ast.value.map(formatAST).join(\", \")}]`;\n        case 6 /* UnaryOperator */:\n            if (ast.op === \"not\") {\n                return `not ` + formatAST(ast.right, 50);\n            }\n            return ast.op + formatAST(ast.right, 130);\n        case 7 /* BinaryOperator */: {\n            const abp = bp(ast.op);\n            const str = `${formatAST(ast.left, abp)} ${ast.op} ${formatAST(ast.right, abp)}`;\n            return abp < lbp ? `(${str})` : str;\n        }\n        case 11 /* Dictionary */: {\n            const pairs = [];\n            for (const k in ast.value) {\n                pairs.push(`\"${k}\": ${formatAST(ast.value[k])}`);\n            }\n            return `{` + pairs.join(\", \") + `}`;\n        }\n        case 10 /* Tuple */:\n            return `(${ast.value.map(formatAST).join(\", \")})`;\n        case 5 /* Name */:\n            return ast.value;\n        case 12 /* Lookup */: {\n            return `${formatAST(ast.target)}[${formatAST(ast.key)}]`;\n        }\n        case 13 /* If */: {\n            const { ifTrue, condition, ifFalse } = ast;\n            return `${formatAST(ifTrue)} if ${formatAST(condition)} else ${formatAST(ifFalse)}`;\n        }\n        case 14 /* BooleanOperator */: {\n            const abp = bp(ast.op);\n            const str = `${formatAST(ast.left, abp)} ${ast.op} ${formatAST(ast.right, abp)}`;\n            return abp < lbp ? `(${str})` : str;\n        }\n        case 15 /* ObjLookup */:\n            return `${formatAST(ast.obj, 150)}.${ast.key}`;\n        case 8 /* FunctionCall */: {\n            const args = ast.args.map(formatAST);\n            const kwargs = [];\n            for (const kwarg in ast.kwargs) {\n                kwargs.push(`${kwarg} = ${formatAST(ast.kwargs[kwarg])}`);\n            }\n            const argStr = args.concat(kwargs).join(\", \");\n            return `${formatAST(ast.fn)}(${argStr})`;\n        }\n    }\n    throw new Error(\"invalid expression: \" + ast);\n}\n\nexport const PY_DICT = Object.create(null);\n\n/**\n * @param {Object} obj\n * @returns {AST} a python dictionary\n */\nexport function toPyDict(obj) {\n    return new Proxy(obj, {\n        getPrototypeOf() {\n            return PY_DICT;\n        },\n    });\n}\n", "import { Component, onWillStart, onWillUpdateProps } from \"@odoo/owl\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { TagsList } from \"@web/core/tags_list/tags_list\";\nimport { isId } from \"@web/core/tree_editor/utils\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { imageUrl } from \"@web/core/utils/urls\";\nimport { RecordAutocomplete } from \"./record_autocomplete\";\nimport { useTagNavigation } from \"./tag_navigation_hook\";\n\nexport class MultiRecordSelector extends Component {\n    static props = {\n        resIds: { type: Array, element: Number },\n        resModel: String,\n        update: Function,\n        domain: { type: Array, optional: true },\n        context: { type: Object, optional: true },\n        fieldString: { type: String, optional: true },\n        placeholder: { type: String, optional: true },\n    };\n    static components = { RecordAutocomplete, TagsList };\n    static template = \"web.MultiRecordSelector\";\n\n    setup() {\n        this.nameService = useService(\"name\");\n        useTagNavigation(\"multiRecordSelector\", {\n            delete: (index) => this.deleteTag(index),\n        });\n        onWillStart(() => this.computeDerivedParams());\n        onWillUpdateProps((nextProps) => this.computeDerivedParams(nextProps));\n    }\n\n    get isAvatarModel() {\n        // bof\n        return [\"res.partner\", \"res.users\", \"hr.employee\", \"hr.employee.public\"].includes(\n            this.props.resModel\n        );\n    }\n\n    async computeDerivedParams(props = this.props) {\n        const displayNames = await this.getDisplayNames(props);\n        this.tags = this.getTags(props, displayNames);\n    }\n\n    async getDisplayNames(props) {\n        const ids = this.getIds(props);\n        return this.nameService.loadDisplayNames(props.resModel, ids);\n    }\n\n    /**\n     * Placeholder should be empty if there is at least one tag. We cannot use\n     * the default behavior of the input placeholder because even if there is\n     * a tag, the input is still empty.\n     */\n    get placeholder() {\n        return this.getTags(this.props, {}).length ? \"\" : this.props.placeholder;\n    }\n\n    getIds(props = this.props) {\n        return props.resIds;\n    }\n\n    getTags(props, displayNames) {\n        return props.resIds.map((id, index) => {\n            const text =\n                typeof displayNames[id] === \"string\"\n                    ? displayNames[id]\n                    : _t(\"Inaccessible/missing record ID: %s\", id);\n            return {\n                text,\n                onDelete: () => {\n                    this.deleteTag(index);\n                },\n                img:\n                    this.isAvatarModel &&\n                    isId(id) &&\n                    imageUrl(this.props.resModel, id, \"avatar_128\"),\n            };\n        });\n    }\n\n    deleteTag(index) {\n        this.props.update([\n            ...this.props.resIds.slice(0, index),\n            ...this.props.resIds.slice(index + 1),\n        ]);\n    }\n\n    update(resIds) {\n        this.props.update([...this.props.resIds, ...resIds]);\n    }\n}\n", "import { Component } from \"@odoo/owl\";\nimport { AutoComplete } from \"@web/core/autocomplete/autocomplete\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { Domain } from \"@web/core/domain\";\nimport { registry } from \"@web/core/registry\";\nimport { useOwnedDialogs, useService } from \"@web/core/utils/hooks\";\n\nconst SEARCH_LIMIT = 7;\nconst SEARCH_MORE_LIMIT = 320;\n\nexport class RecordAutocomplete extends Component {\n    static props = {\n        resModel: String,\n        update: Function,\n        multiSelect: Boolean,\n        getIds: Function,\n        value: String,\n        domain: { type: Array, optional: true },\n        context: { type: Object, optional: true },\n        className: { type: String, optional: true },\n        fieldString: { type: String, optional: true },\n        placeholder: { type: String, optional: true },\n        slots: { optional: true },\n    };\n    static components = { AutoComplete };\n    static template = \"web.RecordAutocomplete\";\n\n    setup() {\n        this.orm = useService(\"orm\");\n        this.nameService = useService(\"name\");\n        this.addDialog = useOwnedDialogs();\n        this.sources = [\n            {\n                placeholder: _t(\"Loading...\"),\n                options: this.loadOptionsSource.bind(this),\n                optionSlot: this.props.slots?.autoCompleteItem ? \"option\" : undefined,\n            },\n        ];\n    }\n\n    addNames(options) {\n        const displayNames = Object.fromEntries(options);\n        this.nameService.addDisplayNames(this.props.resModel, displayNames);\n    }\n\n    getIds() {\n        return this.props.getIds();\n    }\n\n    async loadOptionsSource(name) {\n        if (this.lastProm) {\n            this.lastProm.abort(false);\n        }\n        this.lastProm = this.search(name, SEARCH_LIMIT + 1);\n        const nameGets = (await this.lastProm).map(([id, label]) => [\n            id,\n            label ? label.split(\"\\n\")[0] : _t(\"Unnamed\"),\n        ]);\n        this.addNames(nameGets);\n        const options = nameGets.map(([id, label]) => ({\n            data: {\n                record: { id, display_name: label },\n            },\n            label,\n            onSelect: () => this.props.update([id]),\n        }));\n        if (SEARCH_LIMIT < nameGets.length) {\n            options.push({\n                cssClass: \"o_m2o_dropdown_option\",\n                label: _t(\"Search More...\"),\n                onSelect: this.onSearchMore.bind(this, name),\n            });\n        }\n        if (options.length === 0) {\n            options.push({ label: _t(\"(no result)\") });\n        }\n        return options;\n    }\n\n    async onSearchMore(name) {\n        const { fieldString, multiSelect, resModel } = this.props;\n        let operator;\n        const ids = [];\n        if (name) {\n            const nameGets = await this.search(name, SEARCH_MORE_LIMIT);\n            this.addNames(nameGets);\n            operator = \"in\";\n            ids.push(...nameGets.map((nameGet) => nameGet[0]));\n        } else {\n            operator = \"not in\";\n            ids.push(...this.getIds());\n        }\n        const dynamicFilters = ids.length\n            ? [\n                  {\n                      description: _t(\"Quick search: %s\", name),\n                      domain: [[\"id\", operator, ids]],\n                  },\n              ]\n            : undefined;\n        // fine for now but we don't like this kind of dependence of core to views\n        const SelectCreateDialog = registry.category(\"dialogs\").get(\"select_create\");\n        this.addDialog(SelectCreateDialog, {\n            title: _t(\"Search: %s\", fieldString),\n            dynamicFilters,\n            domain: this.getDomain(),\n            resModel,\n            noCreate: true,\n            multiSelect,\n            context: this.props.context || {},\n            onSelected: (resId) => {\n                const resIds = Array.isArray(resId) ? resId : [resId];\n                this.props.update([...resIds]);\n            },\n        });\n    }\n\n    getDomain() {\n        const domainIds = Domain.not([[\"id\", \"in\", this.getIds()]]);\n        if (this.props.domain) {\n            return Domain.and([this.props.domain, domainIds]).toList();\n        }\n        return domainIds.toList();\n    }\n\n    search(name, limit) {\n        const domain = this.getDomain();\n        return this.orm.call(this.props.resModel, \"name_search\", [], {\n            name,\n            domain: domain,\n            limit,\n            context: this.props.context || {},\n        });\n    }\n\n    onChange({ inputValue }) {\n        if (!inputValue.length) {\n            this.props.update([]);\n        }\n    }\n}\n", "import { Component, onWillStart, onWillUpdateProps } from \"@odoo/owl\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { isId } from \"@web/core/tree_editor/utils\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { RecordAutocomplete } from \"./record_autocomplete\";\n\nexport class RecordSelector extends Component {\n    static props = {\n        resId: [Number, { value: false }],\n        resModel: String,\n        update: Function,\n        domain: { type: Array, optional: true },\n        context: { type: Object, optional: true },\n        fieldString: { type: String, optional: true },\n        placeholder: { type: String, optional: true },\n    };\n    static components = { RecordAutocomplete };\n    static template = \"web.RecordSelector\";\n\n    setup() {\n        this.nameService = useService(\"name\");\n        onWillStart(() => this.computeDerivedParams());\n        onWillUpdateProps((nextProps) => this.computeDerivedParams(nextProps));\n    }\n\n    get isAvatarModel() {\n        // bof\n        return [\"res.partner\", \"res.users\", \"hr.employee\", \"hr.employee.public\"].includes(\n            this.props.resModel\n        );\n    }\n\n    get hasAvatarImg() {\n        return this.isAvatarModel && isId(this.props.resId);\n    }\n\n    async computeDerivedParams(props = this.props) {\n        const displayNames = await this.getDisplayNames(props);\n        this.displayName = this.getDisplayName(props, displayNames);\n    }\n\n    async getDisplayNames(props) {\n        const ids = this.getIds(props);\n        return this.nameService.loadDisplayNames(props.resModel, ids);\n    }\n\n    getDisplayName(props = this.props, displayNames) {\n        const { resId } = props;\n        if (resId === false) {\n            return \"\";\n        }\n        return typeof displayNames[resId] === \"string\"\n            ? displayNames[resId]\n            : _t(\"Inaccessible/missing record ID: %s\", resId);\n    }\n\n    getIds(props = this.props) {\n        if (props.resId) {\n            return [props.resId];\n        }\n        return [];\n    }\n\n    update(resIds) {\n        this.props.update(resIds[0] || false);\n        this.render(true);\n    }\n}\n", "import { useRef } from \"@odoo/owl\";\nimport { useNavigation } from \"../navigation/navigation\";\n\n/**\n * This hook allows to navigate between tags in a record selector. It also\n * allows to delete tags with the backspace key.\n * It is meant to be used in component which contains both the components\n * `Autocomplete` and `TagList`.\n *\n * @param {string} refName Name of the t-ref which contains the `Autocomplete` and `TagList` components.\n * @param {object} [options]\n * @param {() => boolean} [options.isEnabled]\n * @param {(index: number) => void} [options.delete] Function to be called when a tag is deleted. It should take the index of the tag to delete as parameter.\n */\nexport function useTagNavigation(refName, options = {}) {\n    const tagsContainerRef = useRef(refName);\n\n    const isEnabled = options.isEnabled ?? (() => true);\n\n    const canRemoveTag = (target) =>\n        options.delete && (target.tagName.toLowerCase() !== \"input\" || !target.value);\n\n    const onBackspaceKeydown = (navigator) => {\n        const el = navigator.activeItem.el;\n        if (el.classList.contains(\"o-autocomplete--input\")) {\n            if (!el.value && navigator.items.length > 1) {\n                options.delete(navigator.items.length - 2);\n            }\n        } else {\n            options.delete(navigator.activeItemIndex);\n        }\n        navigator.items.at(-1).setActive();\n    };\n\n    const canNavigateFromInput = (navigator, navNext) => {\n        const el = navigator.activeItem.el;\n        if (el.classList.contains(\"o-autocomplete--input\")) {\n            const menu = tagsContainerRef.el.querySelector(\".o-autocomplete--dropdown-menu\");\n            const index = navNext ? el.value.length : 0;\n            if (el.selectionStart !== index || menu) {\n                return false;\n            }\n        }\n        return true;\n    };\n\n    useNavigation(tagsContainerRef, {\n        getItems: () =>\n            tagsContainerRef.el?.querySelectorAll(\":scope .o_tag, :scope .o-autocomplete--input\") ??\n            [],\n        isNavigationAvailable: ({ navigator, target }) =>\n            isEnabled() && navigator.isFocused && navigator.contains(target),\n        hotkeys: {\n            tab: null,\n            \"shift+tab\": null,\n            home: null,\n            end: null,\n            enter: null,\n            arrowup: null,\n            arrowdown: null,\n            backspace: {\n                bypassEditableProtection: true,\n                isAvailable: ({ target }) => canRemoveTag(target),\n                callback: (navigator) => onBackspaceKeydown(navigator),\n            },\n            arrowleft: {\n                bypassEditableProtection: true,\n                isAvailable: ({ navigator }) => canNavigateFromInput(navigator, false),\n                callback: (navigator) => navigator.previous(),\n            },\n            arrowright: {\n                bypassEditableProtection: true,\n                isAvailable: ({ navigator }) => canNavigateFromInput(navigator, true),\n                callback: (navigator) => navigator.next(),\n            },\n        },\n    });\n}\n", "import { EventBus, validate } from \"@odoo/owl\";\n\n// -----------------------------------------------------------------------------\n// Errors\n// -----------------------------------------------------------------------------\nexport class KeyNotFoundError extends Error {}\n\nexport class DuplicatedKeyError extends Error {}\n\n// -----------------------------------------------------------------------------\n// Validation\n// -----------------------------------------------------------------------------\n\nconst validateSchema = (name, key, value, schema) => {\n    if (!odoo.debug) {\n        return;\n    }\n    try {\n        validate(value, schema);\n    } catch (error) {\n        throw new Error(`Validation error for key \"${key}\" in registry \"${name}\": ${error}`);\n    }\n};\n\n// -----------------------------------------------------------------------------\n// Types\n// -----------------------------------------------------------------------------\n\n/**\n * @template S\n * @template C\n * @typedef {import(\"registries\").RegistryData<S, C>} RegistryData\n */\n\n/**\n * @template T\n * @typedef {T extends RegistryData<any, any> ? T : RegistryData<T, {}>} ToRegistryData\n */\n\n/**\n * @template T\n * @typedef {ToRegistryData<T>[\"__itemShape\"]} GetRegistryItemShape\n */\n\n/**\n * @template T\n * @typedef {ToRegistryData<T>[\"__categories\"]} GetRegistryCategories\n */\n\n/**\n * Registry\n *\n * The Registry class is basically just a mapping from a string key to an object.\n * It is really not much more than an object. It is however useful for the\n * following reasons:\n *\n * 1. it let us react and execute code when someone add something to the registry\n *   (for example, the FunctionRegistry subclass this for this purpose)\n * 2. it throws an error when the get operation fails\n * 3. it provides a chained API to add items to the registry.\n *\n * @template T\n */\nexport class Registry extends EventBus {\n    /**\n     * @param {string} [name]\n     */\n    constructor(name) {\n        super();\n        /** @type {Record<string, [number, GetRegistryItemShape<T>]>}*/\n        this.content = {};\n        /** @type {{ [P in keyof GetRegistryCategories<T>]?: Registry<GetRegistryCategories<T>[P]> }} */\n        this.subRegistries = {};\n        /** @type {GetRegistryItemShape<T>[]}*/\n        this.elements = null;\n        /** @type {[string, GetRegistryItemShape<T>][]}*/\n        this.entries = null;\n        this.name = name;\n        this.validationSchema = null;\n\n        this.addEventListener(\"UPDATE\", () => {\n            this.elements = null;\n            this.entries = null;\n        });\n    }\n\n    /**\n     * Add an entry (key, value) to the registry if key is not already used. If\n     * the parameter force is set to true, an entry with same key (if any) is replaced.\n     *\n     * Note that this also returns the registry, so another add method call can\n     * be chained\n     *\n     * @param {string} key\n     * @param {GetRegistryItemShape<T>} value\n     * @param {{force?: boolean, sequence?: number}} [options]\n     * @returns {Registry<T>}\n     */\n    add(key, value, { force, sequence } = {}) {\n        if (this.validationSchema) {\n            validateSchema(this.name, key, value, this.validationSchema);\n        }\n        if (!force && key in this.content) {\n            throw new DuplicatedKeyError(\n                `Cannot add key \"${key}\" in the \"${this.name}\" registry: it already exists`\n            );\n        }\n        let previousSequence;\n        if (force) {\n            const elem = this.content[key];\n            previousSequence = elem && elem[0];\n        }\n        sequence = sequence === undefined ? previousSequence || 50 : sequence;\n        this.content[key] = [sequence, value];\n        const payload = { operation: \"add\", key, value };\n        this.trigger(\"UPDATE\", payload);\n        return this;\n    }\n\n    /**\n     * Get an item from the registry\n     *\n     * @param {string} key\n     * @returns {GetRegistryItemShape<T>}\n     */\n    get(key, defaultValue) {\n        if (arguments.length < 2 && !(key in this.content)) {\n            throw new KeyNotFoundError(`Cannot find key \"${key}\" in the \"${this.name}\" registry`);\n        }\n        const info = this.content[key];\n        return info ? info[1] : defaultValue;\n    }\n\n    /**\n     * Check the presence of a key in the registry\n     *\n     * @param {string} key\n     * @returns {boolean}\n     */\n    contains(key) {\n        return key in this.content;\n    }\n\n    /**\n     * Get a list of all elements in the registry. Note that it is ordered\n     * according to the sequence numbers.\n     *\n     * @returns {GetRegistryItemShape<T>[]}\n     */\n    getAll() {\n        if (!this.elements) {\n            const content = Object.values(this.content).sort((el1, el2) => el1[0] - el2[0]);\n            this.elements = content.map((elem) => elem[1]);\n        }\n        return this.elements.slice();\n    }\n\n    /**\n     * Return a list of all entries, ordered by sequence numbers.\n     *\n     * @returns {[string, GetRegistryItemShape<T>][]}\n     */\n    getEntries() {\n        if (!this.entries) {\n            const entries = Object.entries(this.content).sort((el1, el2) => el1[1][0] - el2[1][0]);\n            this.entries = entries.map(([str, elem]) => [str, elem[1]]);\n        }\n        return this.entries.slice();\n    }\n\n    /**\n     * Remove an item from the registry\n     *\n     * @param {string} key\n     */\n    remove(key) {\n        const value = this.content[key];\n        delete this.content[key];\n        const payload = { operation: \"delete\", key, value };\n        this.trigger(\"UPDATE\", payload);\n    }\n\n    /**\n     * Open a sub registry (and create it if necessary)\n     *\n     * @template {keyof GetRegistryCategories<T> & string} K\n     * @param {K} subcategory\n     * @returns {Registry<GetRegistryCategories<T>[K]>}\n     */\n    category(subcategory) {\n        if (!(subcategory in this.subRegistries)) {\n            this.subRegistries[subcategory] = new Registry(subcategory);\n        }\n        return this.subRegistries[subcategory];\n    }\n\n    addValidation(schema) {\n        if (this.validationSchema) {\n            throw new Error(\"Validation schema already set on this registry\");\n        }\n        this.validationSchema = schema;\n        for (const [key, value] of this.getEntries()) {\n            validateSchema(this.name, key, value, schema);\n        }\n    }\n}\n\n/** @type {Registry<import(\"registries\").GlobalRegistry>} */\nexport const registry = new Registry();\n", "import { useState, onWillStart, onWillDestroy } from \"@odoo/owl\";\n\nexport function useRegistry(registry) {\n    const state = useState({ entries: registry.getEntries() });\n    const listener = ({ detail }) => {\n        const index = state.entries.findIndex(([k]) => k === detail.key);\n        if (detail.operation === \"add\" && index === -1) {\n            // push the new entry at the right place\n            const newEntries = registry.getEntries();\n            const newEntry = newEntries.find(([k]) => k === detail.key);\n            const newIndex = newEntries.indexOf(newEntry);\n            if (newIndex === newEntries.length - 1) {\n                state.entries.push(newEntry);\n            } else {\n                state.entries.splice(newIndex, 0, newEntry);\n            }\n        } else if (detail.operation === \"delete\" && index >= 0) {\n            state.entries.splice(index, 1);\n        }\n    };\n\n    onWillStart(() => registry.addEventListener(\"UPDATE\", listener));\n    onWillDestroy(() => registry.removeEventListener(\"UPDATE\", listener));\n    return state;\n}\n", "import {\n    Component,\n    onMounted,\n    onWillUpdateProps,\n    onWillUnmount,\n    useEffect,\n    useExternalListener,\n    useRef,\n    useComponent,\n} from \"@odoo/owl\";\n\nfunction useResizable({\n    containerRef,\n    handleRef,\n    initialWidth = 400,\n    getMinWidth = () => 400,\n    onResize = () => {},\n    getResizeSide = () => \"end\",\n}) {\n    containerRef = typeof containerRef == \"string\" ? useRef(containerRef) : containerRef;\n    handleRef = typeof handleRef == \"string\" ? useRef(handleRef) : handleRef;\n    const props = useComponent().props;\n\n    let minWidth = getMinWidth(props);\n    let resizeSide = getResizeSide(props);\n    let isChangingSize = false;\n\n    useExternalListener(document, \"mouseup\", () => onMouseUp());\n    useExternalListener(document, \"mousemove\", (ev) => onMouseMove(ev));\n\n    useExternalListener(window, \"resize\", () => {\n        const limit = getLimitWidth();\n        if (getContainerRect().width >= limit) {\n            resize(computeFinalWidth(limit));\n        }\n    });\n\n    let docDirection;\n    useEffect(\n        (container) => {\n            if (container) {\n                docDirection = getComputedStyle(container).direction;\n            }\n        },\n        () => [containerRef.el]\n    );\n\n    onMounted(() => {\n        if (handleRef.el) {\n            resize(initialWidth);\n            handleRef.el.addEventListener(\"mousedown\", onMouseDown);\n        }\n    });\n\n    onWillUpdateProps((nextProps) => {\n        minWidth = getMinWidth(nextProps);\n        resizeSide = getResizeSide(nextProps);\n    });\n\n    onWillUnmount(() => {\n        if (handleRef.el) {\n            handleRef.el.removeEventListener(\"mousedown\", onMouseDown);\n        }\n    });\n\n    function onMouseDown() {\n        isChangingSize = true;\n        document.body.classList.add(\"pe-none\", \"user-select-none\");\n    }\n\n    function onMouseUp() {\n        isChangingSize = false;\n        document.body.classList.remove(\"pe-none\", \"user-select-none\");\n    }\n\n    function onMouseMove(ev) {\n        if (!isChangingSize || !containerRef.el) {\n            return;\n        }\n        const direction =\n            (docDirection === \"ltr\" && resizeSide === \"end\") ||\n            (docDirection === \"rtl\" && resizeSide === \"start\")\n                ? 1\n                : -1;\n        const fixedSide = direction === 1 ? \"left\" : \"right\";\n        const containerRect = getContainerRect();\n        const newWidth = (ev.clientX - containerRect[fixedSide]) * direction;\n        resize(computeFinalWidth(newWidth));\n    }\n\n    function computeFinalWidth(targetContainerWidth) {\n        const handlerSpacing = handleRef.el ? handleRef.el.offsetWidth / 2 : 10;\n        const w = Math.max(minWidth, targetContainerWidth + handlerSpacing);\n        const limit = getLimitWidth();\n        return Math.min(w, limit - handlerSpacing);\n    }\n\n    function getContainerRect() {\n        const container = containerRef.el;\n        const offsetParent = container.offsetParent;\n        let containerRect = {};\n        if (!offsetParent) {\n            containerRect = container.getBoundingClientRect();\n        } else {\n            containerRect.left = container.offsetLeft;\n            containerRect.right = container.offsetLeft + container.offsetWidth;\n            containerRect.width = container.offsetWidth;\n        }\n        return containerRect;\n    }\n\n    function getLimitWidth() {\n        const offsetParent = containerRef.el.offsetParent;\n        return offsetParent ? offsetParent.offsetWidth : window.innerWidth;\n    }\n\n    function resize(width) {\n        containerRef.el.style.setProperty(\"width\", `${width}px`);\n        onResize(width);\n    }\n}\n\nexport class ResizablePanel extends Component {\n    static template = \"web_studio.ResizablePanel\";\n\n    static components = {};\n    static props = {\n        onResize: { type: Function, optional: true },\n        initialWidth: { type: Number, optional: true },\n        minWidth: { type: Number, optional: true },\n        class: { type: String, optional: true },\n        slots: { type: Object },\n        handleSide: {\n            validate: (val) => [\"start\", \"end\"].includes(val),\n            optional: true,\n        },\n    };\n    static defaultProps = {\n        onResize: () => {},\n        width: 400,\n        minWidth: 400,\n        class: \"\",\n        handleSide: \"end\",\n    };\n\n    setup() {\n        useResizable({\n            containerRef: \"containerRef\",\n            handleRef: \"handleRef\",\n            onResize: this.props.onResize,\n            initialWidth: this.props.initialWidth,\n            getMinWidth: (props) => props.minWidth,\n            getResizeSide: (props) => props.handleSide,\n        });\n    }\n\n    get class() {\n        const classes = this.props.class.split(\" \");\n        if (!classes.some((cls) => cls.startsWith(\"position-\"))) {\n            classes.push(\"position-relative\");\n        }\n        return classes.join(\" \");\n    }\n}\n", "import { Component, onWillUpdateProps, useEffect, useRef, useState } from \"@odoo/owl\";\nimport { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\nimport { useDropdownState } from \"@web/core/dropdown/dropdown_hooks\";\nimport { TagsList } from \"@web/core/tags_list/tags_list\";\nimport { mergeClasses } from \"@web/core/utils/classname\";\nimport { useChildRef } from \"@web/core/utils/hooks\";\nimport { scrollTo } from \"@web/core/utils/scrolling\";\nimport { fuzzyLookup } from \"@web/core/utils/search\";\nimport { useDebounced } from \"@web/core/utils/timing\";\nimport { hasTouch } from \"@web/core/browser/feature_detection\";\n\nlet selectMenuId = 0;\n\nexport const DEBOUNCED_DELAY = 250;\n\nexport class SelectMenu extends Component {\n    static template = \"web.SelectMenu\";\n    static choiceItemTemplate = \"web.SelectMenu.ChoiceItem\";\n\n    static components = { Dropdown, DropdownItem, TagsList };\n\n    static defaultProps = {\n        value: undefined,\n        id: \"\",\n        name: \"\",\n        class: \"\",\n        menuClass: \"\",\n        togglerClass: \"\",\n        multiSelect: false,\n        onSelect: () => {},\n        onNavigated: () => {},\n        onOpened: () => {},\n        onClosed: () => {},\n        required: false,\n        searchable: true,\n        autoSort: true,\n        searchPlaceholder: \"\",\n        choices: [],\n        groups: [],\n        sections: [],\n        disabled: false,\n    };\n\n    static props = {\n        choices: {\n            optional: true,\n            type: Array,\n            element: {\n                type: Object,\n                shape: {\n                    value: true,\n                    label: { type: String },\n                    \"*\": true,\n                },\n            },\n        },\n        groups: {\n            type: Array,\n            optional: true,\n            element: {\n                type: Object,\n                shape: {\n                    label: { type: String, optional: true },\n                    choices: {\n                        type: Array,\n                        element: {\n                            type: Object,\n                            shape: {\n                                value: true,\n                                label: { type: String },\n                                \"*\": true,\n                            },\n                        },\n                    },\n                    section: {\n                        type: String,\n                        optional: true,\n                    },\n                },\n            },\n        },\n        sections: {\n            type: Array,\n            optional: true,\n            element: {\n                label: { type: String },\n                name: { type: String },\n            },\n        },\n        id: { type: String, optional: true },\n        name: { type: String, optional: true },\n        class: { type: String, optional: true },\n        menuClass: { type: String, optional: true },\n        togglerClass: { type: String, optional: true },\n        required: { type: Boolean, optional: true },\n        searchable: { type: Boolean, optional: true },\n        autoSort: { type: Boolean, optional: true },\n        placeholder: { type: String, optional: true },\n        searchPlaceholder: { type: String, optional: true },\n        searchClass: { type: String, optional: true },\n        value: { optional: true },\n        multiSelect: { type: Boolean, optional: true },\n        onInput: { type: Function, optional: true },\n        onSelect: { type: Function, optional: true },\n        onNavigated: { type: Function, optional: true },\n        onOpened: { type: Function, optional: true },\n        onClosed: { type: Function, optional: true },\n        slots: { type: Object, optional: true },\n        disabled: { type: Boolean, optional: true },\n        menuRef: { type: Function, optional: true },\n    };\n\n    static SCROLL_SETTINGS = {\n        defaultCount: 500,\n        increaseAmount: 300,\n        distanceBeforeReload: 500,\n    };\n\n    setup() {\n        this.selectMenuId = selectMenuId++;\n        this.state = useState({\n            choices: [],\n            displayedOptions: [],\n            searchValue: null,\n            isFocused: false,\n        });\n        this.inputRef = useRef(\"inputRef\");\n        this.menuRef = useChildRef();\n        this.props.menuRef?.(this.menuRef);\n        this.debouncedOnInput = useDebounced((ev) => {\n            if (!this.dropdownState.isOpen) {\n                this.dropdownState.open();\n            }\n            const searchString = ev.target.value;\n            this.state.searchValue = searchString;\n            this.onInput(searchString);\n        }, DEBOUNCED_DELAY);\n        this.dropdownState = useDropdownState();\n\n        this.selectedChoice = this.getSelectedChoice(this.props);\n        onWillUpdateProps((nextProps) => {\n            const choicesChanged = this.state.choices !== nextProps.choices;\n            if (choicesChanged) {\n                this.state.choices = nextProps.choices;\n            }\n            if (choicesChanged || this.props.value !== nextProps.value) {\n                this.selectedChoice = this.getSelectedChoice(nextProps);\n            }\n        });\n        useEffect(\n            () => {\n                if (this.dropdownState.isOpen) {\n                    const groups = [{ choices: this.props.choices }, ...this.props.groups];\n                    this.filterOptions(this.state.searchValue, groups);\n                }\n            },\n            () => [this.props.choices, this.props.groups]\n        );\n\n        this.navigationOptions = {\n            shouldFocusFirstItem: !hasTouch(),\n            virtualFocus: this.props.searchable,\n            hotkeys: {\n                enter: {\n                    isAvailable: ({ navigator }) => navigator.items.length > 0,\n                    callback: (navigator) => {\n                        if (navigator.activeItem) {\n                            return navigator.activeItem.select();\n                        }\n                        if (document.activeElement.value) {\n                            navigator.items[0].select();\n                        }\n                    },\n                },\n            },\n            onItemActivated: (element) => {\n                const index = parseInt(element.dataset.choiceIndex);\n                if (index >= 0 && this.state.displayedOptions[index]) {\n                    this.props.onNavigated(this.state.displayedOptions[index]);\n                } else {\n                    this.props.onNavigated();\n                }\n            },\n        };\n    }\n\n    get displayValue() {\n        return this.state.searchValue === null\n            ? this.selectedChoice?.label || \"\"\n            : this.state.searchValue;\n    }\n\n    get displayInputInToggler() {\n        return !this.props.slots || !this.props.slots.default;\n    }\n\n    get displayInputInDropdown() {\n        return (this.isBottomSheet || !this.displayInputInToggler) && this.props.searchable;\n    }\n\n    get isBottomSheet() {\n        return this.env.isSmall && hasTouch();\n    }\n\n    get canDeselect() {\n        return !this.props.required && this.selectedChoice !== undefined;\n    }\n\n    get multiSelectChoices() {\n        return this.selectedChoice.map((c) => ({\n            id: c.value,\n            text: c.label,\n            onDelete: () => {\n                const values = [...this.props.value];\n                values.splice(values.indexOf(c.value), 1);\n                this.props.onSelect(values);\n            },\n        }));\n    }\n\n    get menuClass() {\n        return mergeClasses(\n            {\n                \"my-0\": this.displayInputInToggler,\n                o_select_menu_menu: true,\n                o_select_menu_multi_select: this.props.multiSelect,\n            },\n            this.props.menuClass\n        );\n    }\n\n    get placeholderValue() {\n        if (this.state.isFocused && this.props.searchPlaceholder) {\n            return this.props.searchPlaceholder;\n        }\n        return this.props.placeholder;\n    }\n\n    async onBeforeOpen() {\n        this.onInput(\"\");\n    }\n\n    onInputFocus(ev) {\n        if (!this.props.searchable) {\n            return ev.target.blur();\n        }\n        if (ev.target.classList.contains(\"o_select_menu_input\")) {\n            this.state.isFocused = true;\n            ev.target.select();\n        }\n    }\n\n    onInputBlur(ev) {\n        this.state.isFocused = false;\n        if (ev.target.value === \"\" && this.canDeselect && !this.props.multiSelect) {\n            this.onInputClear();\n        }\n    }\n\n    onInputClick(ev) {\n        if (!ev.target.classList.contains(\"o_select_menu_toggler\")) {\n            ev.stopPropagation();\n        }\n    }\n\n    onInputClear() {\n        this.props.onSelect(null);\n        this.dropdownState.close();\n    }\n\n    onStateChanged(open) {\n        if (open) {\n            if (this.isBottomSheet) {\n                // the toggler input must not be focused\n                document.activeElement.blur();\n            }\n            if (this.displayInputInDropdown && !this.isBottomSheet) {\n                this.inputRef.el.focus();\n            }\n            this.menuRef.el?.addEventListener(\"scroll\", (ev) => this.onScroll(ev));\n            const selectedElement = this.menuRef.el?.querySelectorAll(\".active\")[0];\n            if (selectedElement) {\n                scrollTo(selectedElement);\n            }\n            this.props.onOpened();\n        } else {\n            this.state.searchValue = null;\n            this.props.onClosed();\n        }\n    }\n\n    isOptionSelected(choice) {\n        if (this.props.multiSelect) {\n            return this.props.value.includes(choice.value);\n        }\n        return this.props.value === choice.value;\n    }\n\n    getItemClass(choice) {\n        if (this.isOptionSelected(choice)) {\n            return \"o_select_menu_item fw-bolder active\";\n        } else {\n            return \"o_select_menu_item\";\n        }\n    }\n\n    async onInput(searchString) {\n        this.filterOptions(searchString);\n        if (this.props.onInput) {\n            await this.props.onInput(searchString);\n        }\n    }\n\n    getSelectedChoice(props) {\n        const choices = [...props.choices, ...props.groups.flatMap((g) => g.choices || [])];\n        if (!this.props.multiSelect) {\n            return choices.find((c) => c.value === props.value);\n        }\n\n        const valueSet = new Set(props.value);\n        // Combine previously selected choices + newly selected choice from\n        // the searched choices and then filter the choices based on\n        // props.value i.e. valueSet.\n        return [...(this.selectedChoice || []), ...choices].filter(\n            (c, index, self) =>\n                valueSet.has(c.value) && self.findIndex((t) => t.value === c.value) === index\n        );\n    }\n\n    onItemSelected(value) {\n        if (this.props.multiSelect) {\n            const values = [...this.props.value];\n            const valueIndex = values.indexOf(value);\n\n            if (valueIndex !== -1) {\n                values.splice(valueIndex, 1);\n                this.props.onSelect(values);\n            } else {\n                this.props.onSelect([...this.props.value, value]);\n            }\n        } else if (!this.selectedChoice || this.selectedChoice.value !== value) {\n            this.props.onSelect(value);\n            if (this.inputRef.el) {\n                this.inputRef.el.value = this.state.choices.find((c) => c.value === value).label;\n            }\n        }\n        this.state.searchValue = null;\n    }\n\n    // ==========================================================================================\n    // #                                         Search                                         #\n    // ==========================================================================================\n\n    /**\n     * Filters the choices based on the searchString and\n     * slice the result to display a reasonable amount to\n     * try to prevent any delay when opening the select.\n     *\n     * @param {String} searchString\n     */\n    filterOptions(searchString = \"\", groups) {\n        const groupsList = groups || [\n            { choices: this.props.choices, section: \"\" },\n            ...this.props.groups,\n        ];\n\n        const _choices = [];\n        const _sections = new Set();\n        groupsList.sort((a, b) => (a.section || \"\").localeCompare(b.section || \"\"));\n\n        for (const group of groupsList) {\n            let filteredOptions = group.choices || [];\n\n            if (searchString) {\n                filteredOptions = fuzzyLookup(\n                    searchString.trim(),\n                    filteredOptions,\n                    (choice) => choice.label\n                );\n            } else {\n                if (this.props.autoSort) {\n                    filteredOptions.sort((optionA, optionB) =>\n                        optionA.label.localeCompare(optionB.label)\n                    );\n                }\n            }\n\n            if (filteredOptions.length === 0) {\n                continue;\n            }\n            if (group.section) {\n                const section = this.props.sections.find((e) => e.name === group.section);\n                if (!_sections.has(section)) {\n                    _sections.add(section);\n                    _choices.push({ ...section, isGroup: true });\n                }\n            }\n            if (group.label) {\n                _choices.push({ ...group, isGroup: true });\n            }\n            _choices.push(...filteredOptions);\n        }\n\n        this.state.choices = _choices;\n        this.sliceDisplayedOptions();\n    }\n\n    // ==========================================================================================\n    // #                                         Scroll                                         #\n    // ==========================================================================================\n\n    /**\n     * If the user scrolls to the end of the dropdown,\n     * more choices are loaded.\n     *\n     * @param {*} event\n     */\n    onScroll(event) {\n        const el = event.target;\n        const hasReachMax = this.state.displayedOptions.length >= this.state.choices.length;\n        const remainingDistance = el.scrollHeight - el.scrollTop;\n        const distanceToReload =\n            el.clientHeight + this.constructor.SCROLL_SETTINGS.distanceBeforeReload;\n\n        if (!hasReachMax && remainingDistance < distanceToReload) {\n            const displayCount =\n                this.state.displayedOptions.length +\n                this.constructor.SCROLL_SETTINGS.increaseAmount;\n\n            this.state.displayedOptions = this.state.choices.slice(0, displayCount);\n        }\n    }\n\n    /**\n     * Finds the selected choice and set [displayOptions] to at\n     * least show the selected choice and [defaultCount] more\n     * or show at least the [defaultDisplayCount].\n     */\n    sliceDisplayedOptions() {\n        const selectedIndex = this.getSelectedOptionIndex();\n        const defaultCount = this.constructor.SCROLL_SETTINGS.defaultCount;\n\n        if (selectedIndex === -1) {\n            this.state.displayedOptions = this.state.choices.slice(0, defaultCount);\n        } else {\n            const endIndex = Math.max(\n                selectedIndex + this.constructor.SCROLL_SETTINGS.increaseAmount,\n                defaultCount\n            );\n            this.state.displayedOptions = this.state.choices.slice(0, endIndex);\n        }\n    }\n\n    getSelectedOptionIndex() {\n        let selectedIndex = -1;\n        for (let i = 0; i < this.state.choices.length; i++) {\n            if (this.isOptionSelected(this.state.choices[i])) {\n                selectedIndex = i;\n            }\n        }\n        return selectedIndex;\n    }\n}\n", "/* global SignaturePad */\n\nimport { loadJS } from \"@web/core/assets\";\nimport { isMobileOS } from \"@web/core/browser/feature_detection\";\nimport { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { useAutofocus } from \"@web/core/utils/hooks\";\nimport { renderToString } from \"@web/core/utils/render\";\nimport { getDataURLFromFile } from \"@web/core/utils/urls\";\n\nimport { Component, useState, onWillStart, useRef, useEffect } from \"@odoo/owl\";\n\nlet htmlId = 0;\nexport class NameAndSignature extends Component {\n    static template = \"web.NameAndSignature\";\n    static components = { Dropdown, DropdownItem };\n    static props = {\n        signature: { type: Object },\n        defaultFont: { type: String, optional: true },\n        displaySignatureRatio: { type: Number, optional: true },\n        fontColor: { type: String, optional: true },\n        signatureType: { type: String, optional: true },\n        noInputName: { type: Boolean, optional: true },\n        mode: { type: String, optional: true },\n        onSignatureChange: { type: Function, optional: true },\n    };\n    static defaultProps = {\n        defaultFont: \"\",\n        displaySignatureRatio: 3.0,\n        fontColor: \"DarkBlue\",\n        signatureType: \"signature\",\n        noInputName: false,\n        onSignatureChange: () => {},\n    };\n\n    setup() {\n        this.htmlId = htmlId++;\n        this.defaultName = this.props.signature.name || \"\";\n        this.currentFont = 0;\n        this.drawTimeout = null;\n\n        this.state = useState({\n            signMode:\n                this.props.mode || (this.props.noInputName && !this.defaultName ? \"draw\" : \"auto\"),\n            showSignatureArea: !!(this.props.noInputName || this.defaultName),\n            showFontList: false,\n        });\n\n        this.signNameInputRef = useRef(\"signNameInput\");\n        this.signInputLoad = useRef(\"signInputLoad\");\n        useAutofocus({ refName: \"signNameInput\" });\n        useEffect(\n            (el) => {\n                if (el) {\n                    el.click();\n                }\n            },\n            () => [this.signInputLoad.el]\n        );\n\n        onWillStart(async () => {\n            this.fonts = await rpc(`/web/sign/get_fonts/${this.props.defaultFont}`);\n        });\n\n        onWillStart(async () => {\n            await loadJS(\"/web/static/lib/signature_pad/signature_pad.umd.js\");\n        });\n\n        this.signatureRef = useRef(\"signature\");\n        useEffect(\n            (el) => {\n                if (el) {\n                    this.signaturePad = new SignaturePad(el, {\n                        penColor: this.props.fontColor,\n                        backgroundColor: \"rgba(255,255,255,0)\",\n                        minWidth: 2,\n                        maxWidth: 2,\n                    });\n                    this.signaturePad.addEventListener(\"endStroke\", () => {\n                        this.props.signature.isSignatureEmpty = this.isSignatureEmpty;\n                        this.props.onSignatureChange(this.state.signMode);\n                    });\n                    this.resetSignature();\n                    this.props.signature.getSignatureImage = () => this.signaturePad.toDataURL();\n                    this.props.signature.resetSignature = () => this.resetSignature();\n                    if (this.state.signMode === \"auto\") {\n                        this.drawCurrentName();\n                    }\n                    if (this.props.signature.signatureImage) {\n                        this.clear();\n                        this.fromDataURL(this.props.signature.signatureImage);\n                    }\n                }\n            },\n            () => [this.signatureRef.el]\n        );\n    }\n\n    /**\n     * Draws the current name with the current font in the signature field.\n     */\n    async drawCurrentName() {\n        const font = this.fonts[this.currentFont];\n        const text = this.getCleanedName();\n        const canvas = this.signatureRef.el;\n        const img = this.getSVGText(font, text, canvas.width, canvas.height);\n        await this.printImage(img);\n    }\n\n    focusName() {\n        // Don't focus on mobile\n        if (!isMobileOS() && this.signNameInputRef.el) {\n            this.signNameInputRef.el.focus();\n        }\n    }\n\n    /**\n     * Clear the signature field.\n     */\n    clear() {\n        this.signaturePad.clear();\n        this.props.signature.isSignatureEmpty = this.isSignatureEmpty;\n    }\n\n    /**\n    * Loads a signature image from a base64 dataURL and updates the empty state.\n    */\n    async fromDataURL() {\n        await this.signaturePad.fromDataURL(...arguments);\n        this.props.signature.isSignatureEmpty = this.isSignatureEmpty;\n        this.props.onSignatureChange(this.state.signMode);\n    }\n\n    /**\n     * Returns the given name after cleaning it by removing characters that\n     * are not supposed to be used in a signature. If @see signatureType is set\n     * to 'initial', returns the first letter of each word, separated by dots.\n     *\n     * @returns {string} cleaned name\n     */\n    getCleanedName() {\n        // This replaces non-breaking spaces with breaking spaces\n        const text = this.props.signature.name.replace(/\u00a0/g, \" \");\n        if (this.props.signatureType === \"initial\" && text) {\n            return (\n                text\n                    .split(\" \")\n                    .map(function (w) {\n                        return w[0];\n                    })\n                    .join(\".\") + \".\"\n            );\n        }\n        return text;\n    }\n\n    /**\n     * Gets an SVG matching the given parameters, output compatible with the\n     * src attribute of <img/>.\n     *\n     * @private\n     * @param {string} font: base64 encoded font to use\n     * @param {string} text: the name to draw\n     * @param {number} width: the width of the resulting image in px\n     * @param {number} height: the height of the resulting image in px\n     * @returns {string} image = mimetype + image data\n     */\n    getSVGText(font, text, width, height) {\n        const svg = renderToString(\"web.sign_svg_text\", {\n            width: width,\n            height: height,\n            font: font,\n            text: text,\n            type: this.props.signatureType,\n            color: this.props.fontColor,\n        });\n\n        return \"data:image/svg+xml,\" + encodeURI(svg);\n    }\n\n    getSVGTextFont(font) {\n        const height = 100;\n        const width = parseInt(height * this.props.displaySignatureRatio);\n        return this.getSVGText(font, this.getCleanedName(), width, height);\n    }\n\n    uploadFile() {\n        this.signInputLoad.el?.click();\n    }\n\n    /**\n     * Handles change on load file input: displays the loaded image if the\n     * format is correct, or displays an error otherwise.\n     *\n     * @see mode 'load'\n     * @private\n     * @param {Event} ev\n     * @return bool|undefined\n     */\n    async onChangeSignLoadInput(ev) {\n        var file = ev.target.files[0];\n        if (file === undefined) {\n            return false;\n        }\n        if (file.type.substr(0, 5) !== \"image\") {\n            this.clear();\n            this.state.loadIsInvalid = true;\n            return false;\n        }\n        this.state.loadIsInvalid = false;\n\n        const result = await getDataURLFromFile(file);\n        await this.printImage(result);\n    }\n\n    onClickSignAutoSelectStyle() {\n        this.state.showFontList = true;\n    }\n\n    onClickSignDrawClear() {\n        this.clear();\n        this.props.onSignatureChange(this.state.signMode);\n    }\n\n    onClickSignLoad() {\n        this.setMode(\"load\");\n    }\n\n    onClickSignAuto() {\n        this.setMode(\"auto\");\n    }\n\n    onInputSignName(ev) {\n        this.props.signature.name = ev.target.value;\n        if (!this.state.showSignatureArea && this.getCleanedName()) {\n            this.state.showSignatureArea = true;\n            return;\n        }\n        if (this.state.signMode === \"auto\") {\n            this.drawCurrentName();\n        }\n    }\n\n    onSelectFont(index) {\n        this.currentFont = index;\n        this.drawCurrentName();\n    }\n\n    /**\n     * Displays the given image in the signature field.\n     * If needed, resizes the image to fit the existing area.\n     *\n     * @param {string} imgSrc - data of the image to display\n     */\n    async printImage(imgSrc) {\n        this.clear();\n        const c = this.signaturePad.canvas;\n        const img = new Image();\n        img.onload = () => {\n            const ctx = c.getContext(\"2d\");\n            var ratio = ((img.width / img.height) > (c.width / c.height)) ? c.width / img.width : c.height / img.height;\n            ctx.drawImage( \n                img,\n                (c.width / 2) - (img.width * ratio / 2),\n                (c.height / 2) - (img.height * ratio / 2)\n                , img.width * ratio\n                , img.height * ratio\n            );\n            this.props.signature.isSignatureEmpty = this.isSignatureEmpty;\n            this.props.onSignatureChange(this.state.signMode);\n        };\n        img.src = imgSrc;\n        this.signaturePad._isEmpty = false;\n    }\n\n    /**\n     * (Re)initializes the signature area:\n     *  - set the correct width and height of the drawing based on the width\n     *      of the container and the ratio option\n     *  - empty any previous content\n     *  - correctly reset the empty state\n     *  - call @see setMode with reset\n     */\n    resetSignature() {\n        this.resizeSignature();\n        this.clear();\n        this.setMode(this.state.signMode, true);\n        this.focusName();\n    }\n\n    resizeSignature() {\n        // recompute size based on the current width\n        const width = this.signatureRef.el.clientWidth;\n        const height = parseInt(width / this.props.displaySignatureRatio);\n\n        Object.assign(this.signatureRef.el, { width, height });\n    }\n\n    /**\n     * Changes the signature mode. Toggles the display of the relevant\n     * controls and resets the drawing.\n     *\n     * @param {string} mode - the mode to use. Can be one of the following:\n     *  - 'draw': the user draws the signature manually with the mouse\n     *  - 'auto': the signature is drawn automatically using a selected font\n     *  - 'load': the signature is loaded from an image file\n     * @param {boolean} [reset=false] - Set to true to reset the elements\n     *  even if the @see mode has not changed. By default nothing happens\n     *  if the @see mode is already selected.\n     */\n    setMode(mode, reset) {\n        if (reset !== true && mode === this.signMode) {\n            // prevent flickering and unnecessary compute\n            return;\n        }\n\n        this.state.signMode = mode;\n        this.signaturePad[this.state.signMode === \"draw\" ? \"on\" : \"off\"]();\n        this.clear();\n\n        if (this.state.signMode === \"auto\") {\n            // draw based on name\n            this.drawCurrentName();\n        }\n        this.props.onSignatureChange(this.state.signMode);\n    }\n\n    /**\n     * Returns whether the drawing area is currently empty.\n     *\n     * @returns {boolean} Whether the drawing area is currently empty.\n     */\n    get isSignatureEmpty() {\n        return this.signaturePad.isEmpty();\n    }\n\n    get loadIsInvalid() {\n        return this.state.signMode === \"load\" && this.state.loadIsInvalid;\n    }\n}\n", "import { Dialog } from \"@web/core/dialog/dialog\";\nimport { NameAndSignature } from \"./name_and_signature\";\n\nimport { Component, useState } from \"@odoo/owl\";\n\nexport class SignatureDialog extends Component {\n    static template = \"web.SignatureDialog\";\n    static components = { Dialog, NameAndSignature };\n    static props = {\n        defaultName: { type: String, optional: true },\n        nameAndSignatureProps: Object,\n        uploadSignature: Function,\n        close: Function,\n    };\n    static defaultProps = {\n        defaultName: \"\",\n    };\n\n    setup() {\n        this.signature = useState({\n            name: this.props.defaultName,\n            isSignatureEmpty: true,\n        });\n    }\n\n    /**\n     * Upload the signature image when confirm.\n     *\n     * @private\n     */\n    onClickConfirm() {\n        this.props.uploadSignature({\n            name: this.signature.name,\n            signatureImage: this.signature.getSignatureImage(),\n        });\n        this.props.close();\n    }\n\n    get nameAndSignatureProps() {\n        return {\n            ...this.props.nameAndSignatureProps,\n            signature: this.signature,\n        };\n    }\n}\n", "import { Component } from \"@odoo/owl\";\n\nexport class TagsList extends Component {\n    static template = \"web.TagsList\";\n    static defaultProps = {\n        displayText: true,\n    };\n    static props = {\n        displayText: { type: Boolean, optional: true },\n        visibleItemsLimit: { type: Number, optional: true },\n        tags: { type: Array, element: Object },\n    };\n\n    get visibleTagsCount() {\n        return this.props.visibleItemsLimit - 1;\n    }\n    get visibleTags() {\n        if (this.props.visibleItemsLimit && this.props.tags.length > this.props.visibleItemsLimit) {\n            return this.props.tags.slice(0, this.visibleTagsCount);\n        }\n        return this.props.tags;\n    }\n    get otherTags() {\n        if (this.props.visibleItemsLimit && this.props.tags.length > this.props.visibleItemsLimit) {\n            return this.props.tags.slice(this.visibleTagsCount);\n        }\n        return [];\n    }\n    get tooltipInfo() {\n        return JSON.stringify({\n            tags: this.otherTags.map((tag) => ({\n                text: tag.text,\n                id: tag.id,\n            })),\n        });\n    }\n}\n", "const RSTRIP_REGEXP = /(?=\\n[ \\t]*$)/;\n\nlet translationContext = null;\n\nconst TCTX = \"t-translation-context\";\nfunction getTranslationContext(node) {\n    if (node.hasAttribute(TCTX)) {\n        return node.getAttribute(TCTX);\n    }\n    return getTranslationContext(node.parentElement);\n}\n\nconst contextByTextNode = new Map();\nfunction setTranslationContext(node) {\n    switch (node.nodeType) {\n        case Node.TEXT_NODE:\n            if (node.nodeValue.trim() != \"\") {\n                contextByTextNode.set(node, translationContext);\n            }\n            break;\n        case Node.ELEMENT_NODE:\n            node.setAttribute(TCTX, translationContext);\n            break;\n    }\n}\n\nexport function applyContextToTextNode() {\n    for (const [textNode, context] of contextByTextNode) {\n        const wrapper = document.createElement(\"t\");\n        wrapper.setAttribute(TCTX, context);\n        textNode.before(wrapper);\n        wrapper.appendChild(textNode);\n    }\n    contextByTextNode.clear();\n}\n\nexport function deepClone(node) {\n    const clone = node.cloneNode();\n    if (node.nodeType === Node.TEXT_NODE) {\n        if (contextByTextNode.has(node)) {\n            contextByTextNode.set(clone, contextByTextNode.get(node));\n        }\n    }\n    if (node.childNodes?.length) {\n        for (const childNode of [...node.childNodes]) {\n            clone.append(deepClone(childNode));\n        }\n    }\n    return clone;\n}\n\n/**\n * The child nodes of operation represent new content to create before target or\n * or other elements to move before target from the target tree (tree from which target is part of).\n * Some processing of text nodes has to be done in order to normalize the situation.\n * Note: we assume that target has a parent element.\n * @param {Element} target\n * @param {Element} operation\n */\nfunction addBefore(target, operation) {\n    const nodes = getNodes(target, operation);\n    if (nodes.length === 0) {\n        return;\n    }\n    const { previousSibling } = target;\n    target.before(...nodes);\n    if (previousSibling?.nodeType === Node.TEXT_NODE) {\n        const [text1, text2] = previousSibling.data.split(RSTRIP_REGEXP);\n        previousSibling.data = text1.trimEnd();\n        if (text2 && nodes.some((n) => n.nodeType !== Node.TEXT_NODE)) {\n            const textNode = document.createTextNode(text2);\n            target.before(textNode);\n            if (textNode.previousSibling.nodeType === Node.TEXT_NODE) {\n                textNode.previousSibling.data = textNode.previousSibling.data.trimEnd();\n            }\n        }\n    }\n}\n\n/**\n * element is part of a tree. Here we return the root element of that tree.\n * Note: this root element is not necessarily the documentElement of the ownerDocument\n * of element (hence the following code).\n * @param {Element} element\n * @returns {Element}\n */\nfunction getRoot(element) {\n    while (element.parentElement) {\n        element = element.parentElement;\n    }\n    return element;\n}\n\nconst HASCLASS_REGEXP = /hasclass\\(([^)]*)\\)/g;\nconst CLASS_CONTAINS_REGEX = /contains\\(@class.*\\)/g;\n/**\n * @param {Element} operation\n * @returns {string}\n */\nfunction getXpath(operation) {\n    const xpath = operation.getAttribute(\"expr\");\n    if (odoo.debug) {\n        if (CLASS_CONTAINS_REGEX.test(xpath)) {\n            const parent = operation.closest(\"t[t-inherit]\");\n            const templateName = parent.getAttribute(\"t-name\") || parent.getAttribute(\"t-inherit\");\n            console.warn(\n                `Error-prone use of @class in template \"${templateName}\" (or one of its inheritors).` +\n                    \" Use the hasclass(*classes) function to filter elements by their classes\"\n            );\n        }\n    }\n    // hasclass does not exist in XPath 1.0 but is a custom function defined server side (see _hasclass) usable in lxml.\n    // Here we have to replace it by a complex condition (which is not nice).\n    // Note: we assume that classes do not contain the 2 chars , and )\n    return xpath.replaceAll(HASCLASS_REGEXP, (_, capturedGroup) =>\n        capturedGroup\n            .split(\",\")\n            .map((c) => `contains(concat(' ', @class, ' '), ' ${c.trim().slice(1, -1)} ')`)\n            .join(\" and \")\n    );\n}\n\n/**\n * @param {Element} element\n * @param {Element} operation\n * @returns {Node|null}\n */\nfunction getNode(element, operation) {\n    const root = getRoot(element);\n    const doc = new Document();\n    doc.appendChild(root); // => root is the documentElement of its ownerDocument (we do that in case root is a clone)\n    if (operation.tagName === \"xpath\") {\n        const xpath = getXpath(operation);\n        const result = doc.evaluate(xpath, root, null, XPathResult.FIRST_ORDERED_NODE_TYPE);\n        return result.singleNodeValue;\n    }\n    const attributes = [...operation.attributes].filter((attr) => !attr.name.startsWith(TCTX));\n    for (const elem of root.querySelectorAll(operation.tagName)) {\n        if (\n            attributes.every(\n                ({ name, value }) => name === \"position\" || elem.getAttribute(name) === value\n            )\n        ) {\n            return elem;\n        }\n    }\n    return null;\n}\n\n/**\n * @param {Element} element\n * @param {Element} operation\n * @returns {Element}\n */\nfunction getElement(element, operation) {\n    const node = getNode(element, operation);\n    if (!node) {\n        throw new Error(`Element '${operation.outerHTML}' cannot be located in element tree`);\n    }\n    if (!(node instanceof Element)) {\n        throw new Error(`Found node ${node} instead of an element`);\n    }\n    return node;\n}\n\n/**\n * @param {Element} element\n * @param {Element} operation\n * @returns {Node[]}\n */\nfunction getNodes(element, operation) {\n    const nodes = [];\n    for (const childNode of operation.childNodes) {\n        if (childNode.tagName === \"xpath\" && childNode.getAttribute?.(\"position\") === \"move\") {\n            const node = getElement(element, childNode);\n            node.setAttribute(TCTX, getTranslationContext(node));\n            removeNode(node);\n            nodes.push(node);\n        } else {\n            setTranslationContext(childNode);\n            nodes.push(childNode);\n        }\n    }\n    return nodes;\n}\n\nfunction splitAndTrim(str, separator) {\n    return str.split(separator).map((s) => s.trim());\n}\n\n/**\n * @param {Element} target\n * @param {Element} operation\n */\nfunction modifyAttributes(target, operation) {\n    for (const child of operation.children) {\n        if (child.tagName !== \"attribute\") {\n            continue;\n        }\n        const attributeName = child.getAttribute(\"name\");\n        const firstNode = child.childNodes[0];\n        let value = firstNode?.nodeType === Node.TEXT_NODE ? firstNode.data : \"\";\n\n        const add = child.getAttribute(\"add\") || \"\";\n        const remove = child.getAttribute(\"remove\") || \"\";\n        if (add || remove) {\n            if (firstNode?.nodeType === Node.TEXT_NODE) {\n                throw new Error(`Useless element content ${firstNode.outerHTML}`);\n            }\n            const separator = child.getAttribute(\"separator\") || \",\";\n            const toRemove = new Set(splitAndTrim(remove, separator));\n            const values = splitAndTrim(target.getAttribute(attributeName) || \"\", separator).filter(\n                (s) => !toRemove.has(s)\n            );\n            values.push(...splitAndTrim(add, separator).filter((s) => s));\n            value = values.join(separator);\n        }\n\n        if (value) {\n            target.setAttribute(attributeName, value);\n            if (!(add || remove)) {\n                target.setAttribute(`t-translation-context-${attributeName}`, translationContext);\n            }\n        } else {\n            target.removeAttribute(attributeName);\n        }\n    }\n}\n\n/**\n * Remove node and normalize surrounind text nodes (if any)\n * Note: we assume that node has a parent element\n * @param {Node} node\n */\nfunction removeNode(node) {\n    const { nextSibling, previousSibling } = node;\n    node.remove();\n    if (\n        nextSibling?.nodeType === Node.TEXT_NODE &&\n        previousSibling?.nodeType === Node.TEXT_NODE &&\n        previousSibling.parentElement.firstChild === previousSibling\n    ) {\n        previousSibling.data = previousSibling.data.trimEnd();\n    }\n}\n\n/**\n * @param {Element} root\n * @param {Element} target\n * @param {Element} operation\n */\nfunction replace(root, target, operation) {\n    const mode = operation.getAttribute(\"mode\") || \"outer\";\n    switch (mode) {\n        case \"outer\": {\n            const result = operation.ownerDocument.evaluate(\n                \".//*[text()='$0']\",\n                operation,\n                null,\n                XPathResult.ORDERED_NODE_SNAPSHOT_TYPE\n            );\n            target.setAttribute(TCTX, getTranslationContext(target));\n            for (let i = 0; i < result.snapshotLength; i++) {\n                const loc = result.snapshotItem(i);\n                loc.firstChild.replaceWith(deepClone(target));\n            }\n            if (target.parentElement) {\n                const nodes = getNodes(target, operation);\n                target.replaceWith(...nodes);\n            } else {\n                let operationContent = null;\n                let comment = null;\n                for (const child of operation.childNodes) {\n                    if (child.nodeType === Node.ELEMENT_NODE) {\n                        setTranslationContext(child);\n                        operationContent = child;\n                        break;\n                    }\n                    if (child.nodeType === Node.COMMENT_NODE) {\n                        comment = child;\n                    }\n                }\n                root = deepClone(operationContent);\n                if (target.hasAttribute(\"t-name\")) {\n                    root.setAttribute(\"t-name\", target.getAttribute(\"t-name\"));\n                }\n                if (comment) {\n                    root.prepend(comment);\n                }\n            }\n            break;\n        }\n        case \"inner\":\n            while (target.firstChild) {\n                target.removeChild(target.lastChild);\n            }\n            for (const node of [...operation.childNodes]) {\n                setTranslationContext(node);\n                target.append(node);\n            }\n            break;\n        default:\n            throw new Error(`Invalid mode attribute: '${mode}'`);\n    }\n    return root;\n}\n\n/**\n * @param {Element} root\n * @param {Element} operations is a single element whose children represent operations to perform on root\n * @param {string} [url=\"\"]\n * @returns {Element} root modified (in place) by the operations\n */\nexport function applyInheritance(root, operations, url = \"\") {\n    translationContext = url.split(\"/\")[1] ?? \"\"; // use addon name as context\n    for (const operation of operations.children) {\n        const target = getElement(root, operation);\n        const position = operation.getAttribute(\"position\") || \"inside\";\n\n        if (odoo.debug && url) {\n            const attributes = [...operation.attributes].map(\n                ({ name, value }) =>\n                    `${name}=${JSON.stringify(name === \"position\" ? position : value)}`\n            );\n            const comment = document.createComment(\n                ` From file: ${url} ; ${attributes.join(\" ; \")} `\n            );\n            if (position === \"attributes\") {\n                target.before(comment); // comment won't be visible if target is root\n            } else {\n                operation.prepend(comment);\n            }\n        }\n\n        switch (position) {\n            case \"replace\": {\n                root = replace(root, target, operation); // root can be replaced (see outer mode)\n                break;\n            }\n            case \"attributes\": {\n                modifyAttributes(target, operation);\n                break;\n            }\n            case \"inside\": {\n                const sentinel = document.createElement(\"sentinel\");\n                target.append(sentinel);\n                addBefore(sentinel, operation);\n                removeNode(sentinel);\n                break;\n            }\n            case \"after\": {\n                const sentinel = document.createElement(\"sentinel\");\n                target.after(sentinel);\n                addBefore(sentinel, operation);\n                removeNode(sentinel);\n                break;\n            }\n            case \"before\": {\n                addBefore(target, operation);\n                break;\n            }\n            default:\n                throw new Error(`Invalid position attribute: '${position}'`);\n        }\n    }\n    translationContext = null;\n    return root;\n}\n", "import {\n    applyContextToTextNode,\n    applyInheritance,\n    deepClone,\n} from \"@web/core/template_inheritance\";\n\nfunction getClone(template) {\n    const c = deepClone(template);\n    new Document().append(c); // => c is the documentElement of its ownerDocument\n    return c;\n}\n\nfunction getParsedTemplate(templateString) {\n    const doc = parser.parseFromString(templateString, \"text/xml\");\n    for (const processor of templateProcessors) {\n        processor(doc);\n    }\n    return doc.firstChild;\n}\n\nfunction _getTemplate(name, blockId = null) {\n    if (!(name in parsedTemplates)) {\n        if (!(name in templates)) {\n            return null;\n        }\n        const templateString = templates[name];\n        parsedTemplates[name] = getParsedTemplate(templateString);\n        const inheritFrom = parsedTemplates[name].getAttribute(\"t-inherit\");\n        if (!inheritFrom) {\n            const addon = info[name].url.split(\"/\")[1];\n            parsedTemplates[name].setAttribute(\"t-translation-context\", addon);\n        }\n    }\n    let processedTemplate = parsedTemplates[name];\n\n    const inheritFrom = processedTemplate.getAttribute(\"t-inherit\");\n    if (inheritFrom) {\n        const parentTemplate = _getTemplate(inheritFrom, blockId || info[name].blockId);\n        if (!parentTemplate) {\n            throw new Error(\n                `Constructing template ${name}: template parent ${inheritFrom} not found`\n            );\n        }\n        const element = getClone(processedTemplate);\n        processedTemplate = applyInheritance(getClone(parentTemplate), element, info[name].url);\n        if (processedTemplate.tagName !== element.tagName) {\n            const temp = processedTemplate;\n            processedTemplate = new Document().createElement(element.tagName);\n            processedTemplate.append(...temp.childNodes);\n        }\n        for (const { name, value } of element.attributes) {\n            if (![\"t-inherit\", \"t-inherit-mode\"].includes(name)) {\n                processedTemplate.setAttribute(name, value);\n            }\n        }\n    }\n\n    let cloned = false;\n    for (const otherBlockId in templateExtensions[name] || {}) {\n        if (blockId && otherBlockId > blockId) {\n            break;\n        }\n        if (!(name in parsedTemplateExtensions)) {\n            parsedTemplateExtensions[name] = {};\n        }\n        if (!(otherBlockId in parsedTemplateExtensions[name])) {\n            parsedTemplateExtensions[name][otherBlockId] = [];\n            for (const { templateString, url } of templateExtensions[name][otherBlockId]) {\n                parsedTemplateExtensions[name][otherBlockId].push({\n                    template: getParsedTemplate(templateString),\n                    url,\n                });\n            }\n        }\n        for (const { template, url } of parsedTemplateExtensions[name][otherBlockId]) {\n            if (!urlFilters.every((filter) => filter(url))) {\n                continue;\n            }\n            if (!inheritFrom && !cloned) {\n                cloned = true;\n                processedTemplate = getClone(processedTemplate);\n            }\n            processedTemplate = applyInheritance(processedTemplate, getClone(template), url);\n        }\n    }\n\n    return processedTemplate;\n}\n\nfunction isRegistered(...args) {\n    const key = JSON.stringify([...args]);\n    if (registered.has(key)) {\n        return true;\n    }\n    registered.add(key);\n    return false;\n}\n\nconst info = Object.create(null);\nconst parsedTemplateExtensions = Object.create(null);\nconst parsedTemplates = Object.create(null);\nconst parser = new DOMParser();\n/** @type {Map<string, Element>} */\nconst processedTemplates = new Map();\nconst registered = new Set();\n/** @type {Record<string, Record<number, ({ templateString: string, url: string })[]>>} */\nconst templateExtensions = Object.create(null);\n/** @type {((document: Document) => void)[]} */\nconst templateProcessors = [];\n/** @type {Record<string, string>} */\nconst templates = Object.create(null);\nlet blockType = null;\nlet blockId = 0;\n/** @type {((url: string) => boolean)[]} */\nlet urlFilters = [];\n\nexport function checkPrimaryTemplateParents(namesToCheck) {\n    const missing = new Set(namesToCheck.filter((name) => !(name in templates)));\n    if (missing.size) {\n        console.error(`Missing (primary) parent templates: ${[...missing].join(\", \")}`);\n    }\n}\n\nexport function clearProcessedTemplates() {\n    processedTemplates.clear();\n}\n\n/**\n * @param {string} name\n */\nexport function getTemplate(name) {\n    if (!processedTemplates.has(name)) {\n        processedTemplates.set(name, _getTemplate(name));\n        applyContextToTextNode();\n    }\n    return processedTemplates.get(name);\n}\n\nexport function registerTemplate(name, url, templateString) {\n    if (isRegistered(...arguments)) {\n        return;\n    }\n    if (blockType !== \"templates\") {\n        blockType = \"templates\";\n        blockId++;\n    }\n    if (name in templates && (info[name].url !== url || templates[name] !== templateString)) {\n        throw new Error(`Template ${name} already exists`);\n    }\n    templates[name] = templateString;\n    info[name] = { blockId, url };\n\n    return () => {\n        delete templates[name];\n        delete info[name];\n        delete parsedTemplates[name];\n        delete parsedTemplateExtensions[name];\n        processedTemplates.delete(name);\n        registered.delete(JSON.stringify([...arguments]));\n    };\n}\n\nexport function registerTemplateExtension(inheritFrom, url, templateString) {\n    if (isRegistered(...arguments)) {\n        return;\n    }\n    if (blockType !== \"extensions\") {\n        blockType = \"extensions\";\n        blockId++;\n    }\n    if (!templateExtensions[inheritFrom]) {\n        templateExtensions[inheritFrom] = [];\n    }\n    if (!templateExtensions[inheritFrom][blockId]) {\n        templateExtensions[inheritFrom][blockId] = [];\n    }\n    templateExtensions[inheritFrom][blockId].push({\n        templateString,\n        url,\n    });\n\n    return () => {\n        const index = templateExtensions[inheritFrom]?.[blockId]?.findIndex(\n            (ext) => ext.templateString === templateString && ext.url === url\n        );\n        if (Number.isInteger(index) && index > -1) {\n            templateExtensions[inheritFrom][blockId].splice(index, 1);\n        }\n        registered.delete(JSON.stringify([...arguments]));\n    };\n}\n\n/**\n * @param {(document: Document) => void} processor\n */\nexport function registerTemplateProcessor(processor) {\n    templateProcessors.push(processor);\n}\n\n/**\n * @param {typeof urlFilters} filters\n */\nexport function setUrlFilters(filters) {\n    const prev = urlFilters;\n    urlFilters = filters;\n    return () => {\n        urlFilters = prev;\n    };\n}\n", "import { Component, onWillUpdateProps, useRef, useState } from \"@odoo/owl\";\nimport { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { useDropdownState } from \"@web/core/dropdown/dropdown_hooks\";\nimport { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\nimport { getActiveHotkey } from \"@web/core/hotkeys/hotkey_service\";\nimport { Time, parseTime } from \"@web/core/l10n/time\";\nimport { mergeClasses } from \"@web/core/utils/classname\";\nimport { useChildRef } from \"@web/core/utils/hooks\";\n\nconst HOURS = [...Array(24)].map((_, i) => i);\nconst MINUTES = [...Array(60)].map((_, i) => i);\n\n/**\n * @typedef TimePickerProps\n * @property {string} [class=\"\"]\n * @property {string|Time} [value]\n * @property {(value: Time) => any} [onChange]\n * @property {() => {}} [onInvalid]\n * @property {boolean} [showSeconds=false]\n * @property {number} [minutesRounding=5]\n */\n\nexport class TimePicker extends Component {\n    static template = \"web.TimePicker\";\n    static components = {\n        Dropdown,\n        DropdownItem,\n    };\n    static props = {\n        cssClass: { type: [String, Array, Object], optional: true },\n        inputCssClass: { type: [String, Array, Object], optional: true },\n        value: { type: [String, Time, { value: false }, { value: null }], optional: true },\n        onChange: { type: Function, optional: true },\n        onInvalid: { type: Function, optional: true },\n        showSeconds: { type: Boolean, optional: true },\n        minutesRounding: { type: Number, optional: true },\n        placeholder: { type: String, optional: true },\n    };\n    static defaultProps = {\n        cssClass: {},\n        inputCssClass: {},\n        value: \"00:00\",\n        onChange: () => {},\n        onInvalid: () => {},\n        showSeconds: false,\n        minutesRounding: 5,\n    };\n\n    setup() {\n        this.inputRef = useRef(\"inputRef\");\n        this.menuRef = useChildRef();\n        this.dropdownState = useDropdownState();\n\n        this.state = useState({\n            value: null,\n            inputValue: \"\",\n            isValid: true,\n        });\n\n        /**@type {Time[]}*/\n        this.suggestions = [];\n        this.isNavigating = false;\n        this.navigationOptions = this.getNavigationOptions();\n        this.onPropsUpdated(this.props);\n\n        onWillUpdateProps((nextProps) => this.onPropsUpdated(nextProps));\n    }\n\n    get cssClass() {\n        return mergeClasses(this.props.cssClass, {\n            o_time_picker_seconds: this.props.showSeconds,\n        });\n    }\n\n    get inputCssClass() {\n        return mergeClasses(this.props.inputCssClass, {\n            o_invalid: !this.state.isValid,\n        });\n    }\n\n    /**\n     * @returns {import(\"@web/core/navigation/navigation\").NavigationOptions}\n     */\n    getNavigationOptions() {\n        const handleArrow = (navigator) => {\n            const value = this.suggestions[navigator.activeItemIndex];\n            if (value) {\n                this.state.inputValue = value.toString(this.props.showSeconds);\n            }\n        };\n\n        return {\n            virtualFocus: true,\n            onUpdated: (navigator) => (this.navigator = navigator),\n            hotkeys: {\n                enter: {\n                    bypassEditableProtection: true,\n                    callback: (navigator) => {\n                        if (!this.isNavigating) {\n                            const value = parseTime(this.inputRef.el.value, this.props.showSeconds);\n                            if (value) {\n                                this.setValue(value);\n                                this.close();\n                            }\n                        } else if (navigator.activeItem) {\n                            navigator.activeItem.select();\n                        }\n                    },\n                },\n                tab: {\n                    bypassEditableProtection: true,\n                    callback: (navigator) => {\n                        if (navigator.activeItemIndex >= 0) {\n                            this.setValue(this.suggestions[navigator.activeItemIndex]);\n                            this.close();\n                        }\n                    },\n                },\n                arrowdown: {\n                    callback: (navigator) => {\n                        navigator.next();\n                        handleArrow(navigator);\n                    },\n                },\n                arrowup: {\n                    callback: (navigator) => {\n                        navigator.previous();\n                        handleArrow(navigator);\n                    },\n                },\n            },\n        };\n    }\n\n    /**\n     * @param {TimePickerProps} props\n     */\n    onPropsUpdated(props) {\n        if (this.suggestions.length === 0) {\n            this.suggestions = this.getSuggestions(props);\n        }\n\n        this.updateStateValue(Time.from(props.value));\n    }\n\n    /**\n     * @param {TimePickerProps} props\n     * @returns {Time[]}\n     */\n    getSuggestions(props) {\n        const suggestions = [];\n        const rounding = props.minutesRounding <= 5 ? 15 : props.minutesRounding;\n        const minutes = MINUTES.filter((m) => !(m % rounding));\n        for (const hour of HOURS) {\n            for (const minute of minutes) {\n                suggestions.push(new Time({ hour, minute }));\n            }\n        }\n        return suggestions;\n    }\n\n    /**\n     * @param {Time|null} newValue\n     * @param {boolean} [cleanValue=true]\n     */\n    setValue(newValue, cleanValue = true) {\n        if (newValue && cleanValue) {\n            if (this.props.minutesRounding > 1) {\n                newValue.roundMinutes(this.props.minutesRounding);\n            }\n            // If showSeconds is false, keep the seconds from\n            // the original props.value\n            if (!this.props.showSeconds && this.state.value) {\n                newValue.second = this.state.value.second;\n            }\n        }\n\n        const lastValue = this.lastValue;\n        this.updateStateValue(newValue);\n        if (newValue && !newValue.equals(lastValue, this.props.showSeconds)) {\n            this.props.onChange(newValue.copy());\n        }\n    }\n\n    /**\n     * @param {Time|null} newValue\n     */\n    updateStateValue(newValue) {\n        if (\n            newValue === this.lastValue ||\n            newValue?.equals(this.lastValue, this.props.showSeconds)\n        ) {\n            return;\n        }\n\n        this.lastValue = newValue?.copy() ?? newValue;\n        this.state.value = newValue;\n        this.state.inputValue = newValue ? newValue.toString(this.props.showSeconds) : \"\";\n        this.state.isValid = true;\n    }\n\n    /**\n     * @param {Time} value\n     */\n    onItemSelected(value) {\n        this.setValue(value);\n        this.close();\n    }\n\n    /**\n     * @param {InputEvent} event\n     */\n    onInput(event) {\n        this.ensureOpen();\n\n        const value = parseTime(this.inputRef.el.value, this.props.showSeconds);\n        this.state.isValid = value !== null;\n\n        if (!this.navigator) {\n            return;\n        }\n\n        let index = -1;\n        if (this.state.isValid) {\n            index = this.suggestions.findIndex((s) => s.equals(value));\n        }\n\n        if (index === -1) {\n            this.navigator.activeItem?.setInactive();\n        } else {\n            this.navigator.items[index]?.setActive();\n        }\n    }\n\n    onChange() {\n        const value = parseTime(this.inputRef.el.value, this.props.showSeconds);\n        this.state.isValid = value !== null;\n        if (this.state.isValid) {\n            this.setValue(value);\n            this.close();\n        } else {\n            this.props.onInvalid();\n        }\n    }\n\n    /**\n     * @param {KeyboardEvent} event\n     */\n    onKeydown(event) {\n        this.isNavigating = [\"arrowup\", \"arrowdown\"].includes(getActiveHotkey(event));\n    }\n\n    ensureOpen() {\n        if (!this.dropdownState.isOpen) {\n            this.isNavigating = false;\n            this.dropdownState.open();\n            this.inputRef.el.select();\n        }\n    }\n\n    close() {\n        this.dropdownState.close();\n    }\n\n    /**\n     * @returns {string}\n     */\n    getPlaceholder() {\n        if (typeof this.props.placeholder === \"string\") {\n            return this.props.placeholder;\n        }\n        const seconds = this.props.showSeconds ? \":ss\" : \"\";\n        return `hh:mm${seconds}`;\n    }\n\n    onDropdownOpened() {\n        if (this.navigator) {\n            const index = this.state.value\n                ? this.suggestions.findIndex((s) =>\n                      s.equals(this.state.value, this.props.showSeconds)\n                  )\n                : 0;\n            this.navigator.items[index]?.setActive();\n        }\n    }\n}\n", "import { Component } from \"@odoo/owl\";\n\nexport class Tooltip extends Component {\n    static template = \"web.Tooltip\";\n    static props = {\n        close: Function,\n        tooltip: { type: String, optional: true },\n        template: { type: String, optional: true },\n        info: { optional: true },\n    };\n}\n", "import { useService } from \"@web/core/utils/hooks\";\n\nimport { useEffect, useRef } from \"@odoo/owl\";\n\nexport function useTooltip(refName, params) {\n    const tooltip = useService(\"tooltip\");\n    const ref = useRef(refName);\n    useEffect(\n        (el) => tooltip.add(el, params),\n        () => [ref.el]\n    );\n}\n", "import { browser } from \"@web/core/browser/browser\";\nimport { registry } from \"@web/core/registry\";\nimport { Tooltip } from \"./tooltip\";\nimport { hasTouch } from \"@web/core/browser/feature_detection\";\n\nimport { whenReady } from \"@odoo/owl\";\n\n/**\n * The tooltip service allows to display custom tooltips on every elements with\n * a \"data-tooltip\" attribute. This attribute can be set on elements for which\n * we prefer a custom tooltip instead of the native one displaying the value of\n * the \"title\" attribute.\n *\n * Usage:\n *   <button data-tooltip=\"This is a tooltip\">Do something</button>\n *\n * The ideal position of the tooltip can be specified thanks to the attribute\n * \"data-tooltip-position\":\n *   <button data-tooltip=\"This is a tooltip\" data-tooltip-position=\"left\">Do something</button>\n *\n * The opening delay can be modified with the \"data-tooltip-delay\" attribute (default: 400):\n *   <button data-tooltip=\"This is a tooltip\" data-tooltip-delay=\"0\">Do something</button>\n *\n * The default behaviour on touch devices to open the tooltip can be modified from \"hold-to-show\"\n * to \"tap-to-show\" \"with the data-tooltip-touch-tap-to-show\" attribute:\n *  <button data-tooltip=\"This is a tooltip\" data-tooltip-touch-tap-to-show=\"true\">Do something</button>\n *\n * For advanced tooltips containing dynamic and/or html content, the\n * \"data-tooltip-template\" and \"data-tooltip-info\" attributes can be used.\n * For example, let's suppose the following qweb template:\n *   <t t-name=\"some_template\">\n *     <ul>\n *       <li>info.x</li>\n *       <li>info.y</li>\n *     </ul>\n *   </t>\n * This template can then be used in a tooltip as follows:\n *   <button data-tooltip-template=\"some_template\" data-tooltip-info=\"info\">Do something</button>\n * with \"info\" being a stringified object with two keys \"x\" and \"y\".\n */\n\nexport const OPEN_DELAY = 400;\nexport const CLOSE_DELAY = 200;\nexport const SHOW_AFTER_DELAY = 250;\n\nexport const tooltipService = {\n    dependencies: [\"popover\"],\n    start(env, { popover }) {\n        let openTooltipTimeout;\n        let closeTooltip;\n        let showTimer;\n        let target = null;\n        const elementsWithTooltips = new WeakMap();\n\n        /**\n         * Detect if the current node is the `sup` tooltip node\n         * @param {HTMLElement} el\n         * @return {boolean}\n         */\n        function isHelpNode(el) {\n            return (\n                el.textContent === \"?\" &&\n                (el.hasAttribute(\"data-tooltip\") || el.hasAttribute(\"data-tooltip-template\"))\n            );\n        }\n\n        /**\n         * Closes the currently opened tooltip if any, or prevent it from opening.\n         */\n        function cleanup() {\n            target = null;\n            browser.clearTimeout(openTooltipTimeout);\n            openTooltipTimeout = null;\n            if (closeTooltip) {\n                closeTooltip();\n                closeTooltip = null;\n            }\n        }\n\n        /**\n         * Checks that the target is in the DOM and we're hovering the target.\n         * @returns {boolean}\n         */\n        function shouldCleanup() {\n            if (!target) {\n                return false;\n            }\n            if (!document.body.contains(target)) {\n                return true; // target is no longer in the DOM\n            }\n            return false;\n        }\n\n        /**\n         * Checks whether there is a tooltip registered on the event target, and\n         * if there is, creates a timeout to open the corresponding tooltip\n         * after a delay.\n         *\n         * @param {HTMLElement} el the element on which to add the tooltip\n         * @param {object} param1\n         * @param {string} [param1.tooltip] the string to add as a tooltip, if\n         *  no tooltip template is specified\n         * @param {string} [param1.template] the name of the template to use for\n         *  tooltip, if any\n         * @param {object} [param1.info] info for the tooltip template\n         * @param {'top'|'bottom'|'left'|'right'} param1.position\n         * @param {number} [param1.delay] delay after which the popover should\n         *  open\n         */\n        function openTooltip(el, { tooltip = \"\", template, info, position, delay = OPEN_DELAY }) {\n            cleanup();\n            if (!tooltip && !template) {\n                return;\n            }\n\n            target = el;\n            // Prevent title from showing on a parent at the same time\n            target.title = \"\";\n            const timeoutDelay = isHelpNode(el) ? 0 : delay;\n            openTooltipTimeout = browser.setTimeout(() => {\n                // verify that the element is still in the DOM\n                if (target.isConnected) {\n                    closeTooltip = popover.add(\n                        target,\n                        Tooltip,\n                        { tooltip, template, info },\n                        { position }\n                    );\n                }\n            }, timeoutDelay);\n        }\n\n        /**\n         * Checks whether there is a tooltip registered on the element, and\n         * if there is, creates a timeout to open the corresponding tooltip\n         * after a delay.\n         *\n         * @param {HTMLElement} el\n         */\n        function openElementsTooltip(el) {\n            // Fix weird behavior in Firefox where MouseEvent can be dispatched\n            // from TEXT_NODE, even if they shouldn't...\n            if (el.nodeType === Node.TEXT_NODE) {\n                return;\n            }\n            const element = el.closest(\"[data-tooltip], [data-tooltip-template]\");\n            if (elementsWithTooltips.has(el)) {\n                openTooltip(el, elementsWithTooltips.get(el));\n            } else if (element) {\n                const dataset = element.dataset;\n                const params = {\n                    tooltip: dataset.tooltip,\n                    template: dataset.tooltipTemplate,\n                    position: dataset.tooltipPosition,\n                };\n                if (dataset.tooltipInfo) {\n                    params.info = JSON.parse(dataset.tooltipInfo);\n                }\n                if (dataset.tooltipDelay) {\n                    params.delay = parseInt(dataset.tooltipDelay, 10);\n                }\n                openTooltip(element, params);\n            }\n        }\n\n        /**\n         * Checks whether there is a tooltip registered on the event target, and\n         * if there is, creates a timeout to open the corresponding tooltip\n         * after a delay.\n         *\n         * @param {MouseEvent} ev a \"mouseenter\" event\n         */\n        function onMouseenter(ev) {\n            openElementsTooltip(ev.target);\n        }\n\n        /**\n         * Check whether there is a tooltip registered on the event target, and if there is,\n         * cleanup it.\n         * @param {MouseEvent} ev a \"click\" event\n         */\n        function onClick(ev) {\n            if (isHelpNode(ev.target)) {\n                ev.preventDefault();\n            }\n            cleanupTooltip(ev);\n        }\n\n        function cleanupTooltip(ev) {\n            if (target === ev.target.closest(\"[data-tooltip], [data-tooltip-template]\")) {\n                cleanup();\n            }\n        }\n        /**\n         * Checks whether there is a tooltip registered on the event target, and\n         * if there is, creates a timeout to open the corresponding tooltip\n         * after a delay.\n         *\n         * @param {TouchEvent} ev a \"touchstart\" event\n         */\n        function onTouchStart(ev) {\n            cleanup();\n            const timeoutDelay = isHelpNode(ev.target) ? 0 : SHOW_AFTER_DELAY;\n            showTimer = browser.setTimeout(() => {\n                openElementsTooltip(ev.target);\n            }, timeoutDelay);\n        }\n\n        whenReady(() => {\n            // Regularly check that the target is still in the DOM and if not, close the tooltip\n            browser.setInterval(() => {\n                if (shouldCleanup()) {\n                    cleanup();\n                }\n            }, CLOSE_DELAY);\n\n            if (hasTouch()) {\n                document.body.addEventListener(\"touchstart\", onTouchStart);\n\n                document.body.addEventListener(\"touchend\", (ev) => {\n                    if (isHelpNode(ev.target)) {\n                        ev.preventDefault();\n                        return;\n                    }\n                    if (ev.target.closest(\"[data-tooltip], [data-tooltip-template]\")) {\n                        if (!ev.target.dataset.tooltipTouchTapToShow) {\n                            browser.clearTimeout(showTimer);\n                            browser.clearTimeout(openTooltipTimeout);\n                        }\n                    }\n                });\n                document.body.addEventListener(\"touchcancel\", (ev) => {\n                    if (isHelpNode(ev.target)) {\n                        ev.preventDefault();\n                        return;\n                    }\n                    if (ev.target.closest(\"[data-tooltip], [data-tooltip-template]\")) {\n                        if (!ev.target.dataset.tooltipTouchTapToShow) {\n                            browser.clearTimeout(showTimer);\n                            browser.clearTimeout(openTooltipTimeout);\n                        }\n                    }\n                });\n            }\n\n            // Listen (using event delegation) to \"mouseenter\" events to open the tooltip if any\n            document.body.addEventListener(\"mouseenter\", onMouseenter, { capture: true });\n            // Listen (using event delegation) to \"mouseleave\" events to close the tooltip if any\n            document.body.addEventListener(\"mouseleave\", cleanupTooltip, { capture: true });\n            document.body.addEventListener(\"click\", onClick, { capture: true });\n        });\n\n        return {\n            add(el, params) {\n                elementsWithTooltips.set(el, params);\n                return () => {\n                    elementsWithTooltips.delete(el);\n                    if (target === el) {\n                        cleanup();\n                    }\n                };\n            },\n        };\n    },\n};\n\nregistry.category(\"services\").add(\"tooltip\", tooltipService);\n", "import { browser } from \"./browser/browser\";\n\nimport {\n    Component,\n    onWillUpdateProps,\n    status,\n    useComponent,\n    useEffect,\n    useState,\n    xml,\n} from \"@odoo/owl\";\n\n// Allows to disable transitions globally, useful for testing (and maybe for\n// a reduced motion setting in the future?)\nexport const config = {\n    disabled: false,\n};\n/**\n * Creates a transition to be used within the current component. Usage:\n *  --- in JS:\n *  this.transition = useTransition({ name: \"myClass\" });\n *  --- in XML:\n *  <div t-if=\"transition.shouldMount\" t-att-class=\"transition.class\"/>\n *\n * @param {Object} options\n * @param {string} options.name the prefix to use for the transition classes\n * @param {boolean} [options.initialVisibility=true] whether to start the\n *  transition in the on or off state\n * @param {number} [options.immediate=false] (only relevant when initialVisibility\n *  is true) set to true to animate initially. By default, there's no animation\n *  if the element is initially visible.\n * @param {number} [options.leaveDuration] the leaveDuration of the transition\n * @param {Function} [options.onLeave] a function that will be called when the\n *  element will be removed in the next render cycle\n * @returns {{ shouldMount, class }} an object containing two fields that\n *  indicate whether an element on which the transition is applied should be\n *  mounted and the class string that should be put on it\n */\nexport function useTransition({\n    name,\n    initialVisibility = true,\n    immediate = false,\n    leaveDuration = 500,\n    onLeave = () => {},\n}) {\n    const component = useComponent();\n    const state = useState({\n        shouldMount: initialVisibility,\n        stage: initialVisibility ? \"enter\" : \"leave\",\n    });\n\n    if (config.disabled) {\n        return {\n            get shouldMount() {\n                return state.shouldMount;\n            },\n            set shouldMount(val) {\n                state.shouldMount = val;\n            },\n            get className() {\n                return `${name} ${name}-enter-active`;\n            },\n            get stage() {\n                return \"enter-active\";\n            },\n        };\n    }\n    // We need to allow the element to be mounted in the enter state so that it\n    // will get the transition when we activate the enter-active class. This\n    // onNextPatch allows us to activate the class that we want the next time\n    // the component is patched.\n    let onNextPatch = null;\n    useEffect(() => {\n        if (onNextPatch) {\n            onNextPatch();\n            onNextPatch = null;\n        }\n    });\n\n    let prevState, timer;\n    const transition = {\n        get shouldMount() {\n            return state.shouldMount;\n        },\n        set shouldMount(newState) {\n            if (newState === prevState) {\n                return;\n            }\n            browser.clearTimeout(timer);\n            prevState = newState;\n            // when true - transition from enter to enter-active\n            // when false - transition from enter-active to leave, unmount after leaveDuration\n            if (newState) {\n                if (status(component) === \"mounted\" || immediate) {\n                    state.stage = \"enter\";\n                    // force a render here so that we get a patch even if the state didn't change\n                    component.render();\n                    onNextPatch = () => {\n                        state.stage = \"enter-active\";\n                    };\n                } else {\n                    state.stage = \"enter-active\";\n                }\n                state.shouldMount = true;\n            } else {\n                state.stage = \"leave\";\n                timer = browser.setTimeout(() => {\n                    state.shouldMount = false;\n                    onLeave();\n                }, leaveDuration);\n            }\n        },\n        get className() {\n            return `${name} ${name}-${state.stage}`;\n        },\n        get stage() {\n            return state.stage;\n        },\n    };\n    transition.shouldMount = initialVisibility;\n    return transition;\n}\n\n/**\n * A higher order component that handles a transition to be used within its\n * default slot. Generally, the useTransition hook is simpler to use, but the\n * HOC has the advantage that it can be spawned as needed during the render (eg:\n * in a t-foreach loop) without knowing at setup-time how many transitions need\n * to be created. @see useTransition\n */\nexport class Transition extends Component {\n    static template = xml`<t t-slot=\"default\" t-if=\"transition.shouldMount\" className=\"transition.className\"/>`;\n    static props = {\n        name: String,\n        visible: { type: Boolean, optional: true },\n        immediate: { type: Boolean, optional: true },\n        leaveDuration: { type: Number, optional: true },\n        onLeave: { type: Function, optional: true },\n        slots: Object,\n    };\n\n    setup() {\n        const { immediate, visible, leaveDuration, name, onLeave } = this.props;\n        this.transition = useTransition({\n            initialVisibility: visible,\n            immediate,\n            leaveDuration,\n            name,\n            onLeave,\n        });\n        onWillUpdateProps(({ visible = true }) => {\n            this.transition.shouldMount = visible;\n        });\n    }\n}\n", "import { COMPARATORS, TERM_OPERATORS_NEGATION_EXTENDED } from \"./operators\";\n\nexport function isBool(ast) {\n    return ast.type === 8 && ast.fn.type === 5 && ast.fn.value === \"bool\" && ast.args.length === 1;\n}\n\nexport function isNot(ast) {\n    return ast.type === 6 && ast.op === \"not\";\n}\n\nexport function not(ast) {\n    if (isNot(ast)) {\n        return ast.right;\n    }\n    if (ast.type === 2) {\n        return { ...ast, value: !ast.value };\n    }\n    if (ast.type === 7 && COMPARATORS.includes(ast.op)) {\n        return { ...ast, op: TERM_OPERATORS_NEGATION_EXTENDED[ast.op] }; // do not use this if ast is within a domain context!\n    }\n    return { type: 6, op: \"not\", right: isBool(ast) ? ast.args[0] : ast };\n}\n\nexport function isValidPath(ast, options) {\n    const getFieldDef = options.getFieldDef || (() => null);\n    if (ast.type === 5) {\n        return getFieldDef(ast.value) !== null;\n    }\n    return false;\n}\n", "import { Domain } from \"@web/core/domain\";\nimport { formatAST, parseExpr } from \"@web/core/py_js/py\";\nimport { toPyValue } from \"@web/core/py_js/py_utils\";\n\n/** @typedef { import(\"@web/core/py_js/py_parser\").AST } AST */\n/** @typedef {import(\"@web/core/domain\").DomainRepr} DomainRepr */\n\n/**\n * @typedef {number|string|boolean|Expression} Atom\n */\n\n/**\n * @typedef {Atom|Atom[]} Value\n */\n\n/**\n * @typedef {Object} Condition\n * @property {\"condition\"} type\n * @property {Value} path\n * @property {Value} operator\n * @property {Value|Tree} value\n * @property {boolean} negate\n */\n\n/**\n * @typedef {Object} ComplexCondition\n * @property {\"complex_condition\"} type\n * @property {string} value expression\n */\n\n/**\n * @typedef {Object} Connector\n * @property {\"connector\"} type\n * @property {boolean} negate\n * @property {\"|\"|\"&\"} value\n * @property {Tree[]} children\n */\n\n/**\n * @typedef {Connector|Condition|ComplexCondition} Tree\n */\n\n/**\n * @typedef {Object} Options\n * @property {(value: Value) => (null|Object)} [getFieldDef]\n * @property {boolean} [distributeNot]\n */\n\nexport class Expression {\n    constructor(ast) {\n        if (typeof ast === \"string\") {\n            ast = parseExpr(ast);\n        }\n        this._ast = ast;\n        this._expr = formatAST(ast);\n    }\n\n    toAST() {\n        return this._ast;\n    }\n\n    toString() {\n        return this._expr;\n    }\n}\n\n/**\n * @param {string} expr\n * @returns {Expression}\n */\nexport function expression(expr) {\n    return new Expression(expr);\n}\n\n/**\n * @param {\"|\"|\"&\"} value\n * @param {Tree[]} [children=[]]\n * @param {boolean} [negate=false]\n * @returns {Connector}\n */\nexport function connector(value, children = [], negate = false) {\n    return { type: \"connector\", value, children, negate };\n}\n\n/**\n * @param {string} value\n * @returns {ComplexCondition}\n */\nexport function complexCondition(value) {\n    parseExpr(value);\n    return { type: \"complex_condition\", value };\n}\n\n/**\n * @param {Value} path\n * @param {Value} operator\n * @param {Value|Tree} value\n * @param {boolean} [negate=false]\n * @returns {Condition}\n */\nexport function condition(path, operator, value, negate = false) {\n    return { type: \"condition\", path, operator, value, negate };\n}\n\nexport const TRUE_TREE = condition(1, \"=\", 1);\nexport const FALSE_TREE = condition(0, \"=\", 1);\n\n/**\n * @param {Value} value\n * @returns {Value}\n */\nfunction cloneValue(value) {\n    if (value instanceof Expression) {\n        return new Expression(value.toAST());\n    }\n    if (Array.isArray(value)) {\n        return value.map(cloneValue);\n    }\n    return value;\n}\n\n/**\n * @param {Tree} tree\n * @returns {Tree}\n */\nexport function cloneTree(tree) {\n    const clone = {};\n    for (const key in tree) {\n        clone[key] = cloneValue(tree[key]);\n    }\n    return clone;\n}\n\nconst areEqualValues = (value, otherValue) => formatValue(value) === formatValue(otherValue);\n\nconst areEqualArraysOfTrees = (array, otherArray) => {\n    if (array.length !== otherArray.length) {\n        return false;\n    }\n    for (let i = 0; i < array.length; i++) {\n        const elem = array[i];\n        const otherElem = otherArray[i];\n        if (!areEqualTrees(elem, otherElem)) {\n            return false;\n        }\n    }\n    return true;\n};\n\nexport const areEqualTrees = (tree, otherTree) => {\n    if (tree.type !== otherTree.type) {\n        return false;\n    }\n    if (tree.negate !== otherTree.negate) {\n        return false;\n    }\n    if (tree.type === \"condition\") {\n        if (!areEqualValues(tree.path, otherTree.path)) {\n            return false;\n        }\n        if (!areEqualValues(tree.operator, otherTree.operator)) {\n            return false;\n        }\n        if (isTree(tree.value)) {\n            if (isTree(otherTree.value)) {\n                return areEqualTrees(tree.value, otherTree.value);\n            }\n            return false;\n        } else if (isTree(otherTree.value)) {\n            return false;\n        }\n        if (!areEqualValues(tree.value, otherTree.value)) {\n            return false;\n        }\n        return true;\n    }\n    if (!areEqualValues(tree.value, otherTree.value)) {\n        return false;\n    }\n    if (tree.type === \"complex_condition\") {\n        return true;\n    }\n    return areEqualArraysOfTrees(tree.children, otherTree.children);\n};\n\n/**\n * @param {import(\"@web/core/py_js/py_parser\").AST} ast\n * @returns {Value}\n */\nexport function toValue(ast, isWithinArray = false) {\n    if ([4, 10].includes(ast.type) && !isWithinArray) {\n        /** 4: list, 10: tuple */\n        return ast.value.map((v) => toValue(v, true));\n    } else if ([0, 1, 2].includes(ast.type)) {\n        /** 0: number, 1: string, 2: boolean */\n        return ast.value;\n    } else if (ast.type === 6 && ast.op === \"-\" && ast.right.type === 0) {\n        /** 6: unary operator */\n        return -ast.right.value;\n    } else if (ast.type === 5 && [\"false\", \"true\"].includes(ast.value)) {\n        /** 5: name */\n        return JSON.parse(ast.value);\n    } else {\n        return new Expression(ast);\n    }\n}\n\nexport function astFromValue(value) {\n    if (value instanceof Expression) {\n        return value.toAST();\n    }\n    if (Array.isArray(value)) {\n        return { type: 4, value: value.map(astFromValue) };\n    }\n    return toPyValue(value);\n}\n\nexport function formatValue(value) {\n    return formatAST(astFromValue(value));\n}\n\nexport function normalizeValue(value) {\n    return toValue(astFromValue(value)); // no array in array (see isWithinArray)\n}\n\nexport function isTree(value) {\n    return (\n        typeof value === \"object\" &&\n        !(value instanceof Domain) &&\n        !(value instanceof Expression) &&\n        !Array.isArray(value) &&\n        value !== null\n    );\n}\n\n/**\n * @param {Connector} parent\n * @param {Tree} child\n */\nexport function addChild(parent, child) {\n    if (child.type === \"connector\" && !child.negate && child.value === parent.value) {\n        parent.children.push(...child.children);\n    } else {\n        parent.children.push(child);\n    }\n}\n\nexport function applyTransformations(transformations, transformed, ...fixedParams) {\n    for (let i = transformations.length - 1; i >= 0; i--) {\n        const fn = transformations[i];\n        transformed = fn(transformed, ...fixedParams);\n    }\n    return transformed;\n}\n\nfunction normalizeConnector(connector) {\n    const newTree = { ...connector, children: [] };\n    for (const child of connector.children) {\n        addChild(newTree, child);\n    }\n    if (newTree.children.length === 1) {\n        const child = newTree.children[0];\n        if (newTree.negate) {\n            const newChild = { ...child, negate: !child.negate };\n            if (newChild.type === \"condition\") {\n                return newChild;\n            }\n            return newChild;\n        }\n        return child;\n    }\n    return newTree;\n}\n\nfunction makeOptions(path, options) {\n    return {\n        ...options,\n        getFieldDef: (p) => {\n            if (typeof path === \"string\" && typeof p === \"string\") {\n                return options.getFieldDef?.(`${path}.${p}`) || null;\n            }\n            return null;\n        },\n    };\n}\n\n/**\n * @param {Function} transformation\n * @param {Tree} tree\n * @param {Options} [options={}]\n * @param {\"condition\"|\"connector\"|\"complex_condition\"} [treeType=\"condition\"]\n * @returns {Tree}\n */\nexport function operate(\n    transformation,\n    tree,\n    options = {},\n    treeType = \"condition\",\n    traverseSubTrees = true\n) {\n    if (tree.type === \"connector\") {\n        const newTree = {\n            ...tree,\n            children: tree.children.map((c) =>\n                operate(transformation, c, options, treeType, traverseSubTrees)\n            ),\n        };\n        if (treeType === \"connector\") {\n            return normalizeConnector(transformation(newTree, options) || newTree);\n        }\n        return normalizeConnector(newTree);\n    }\n    const clone = cloneTree(tree);\n    if (traverseSubTrees && tree.type === \"condition\" && isTree(tree.value)) {\n        clone.value = operate(\n            transformation,\n            tree.value,\n            makeOptions(tree.path, options),\n            treeType,\n            traverseSubTrees\n        );\n    }\n    if (treeType === tree.type) {\n        return transformation(clone, options) || clone;\n    }\n    return clone;\n}\n\nexport function rewriteNConsecutiveChildren(transformation, N = 2) {\n    return (c, options) => {\n        const children = [];\n        const currentChildren = c.children;\n        for (let i = 0; i < currentChildren.length; i++) {\n            const NconsecutiveChildren = currentChildren.slice(i, i + N);\n            let replacement = null;\n            if (NconsecutiveChildren.length === N) {\n                replacement = transformation(connector(c.value, NconsecutiveChildren), options);\n            }\n            if (replacement) {\n                children.push(replacement);\n                i += N - 1;\n            } else {\n                children.push(NconsecutiveChildren[0]);\n            }\n        }\n        return { ...c, children };\n    };\n}\n", "import { formatAST, parseExpr } from \"@web/core/py_js/py\";\nimport { isBool, isNot } from \"./ast_utils\";\nimport {\n    astFromValue,\n    condition,\n    Expression,\n    FALSE_TREE,\n    isTree,\n    TRUE_TREE,\n} from \"./condition_tree\";\n\nfunction bool(ast) {\n    if (isBool(ast) || isNot(ast) || ast.type === 2) {\n        return ast;\n    }\n    return { type: 8, fn: { type: 5, value: \"bool\" }, args: [ast], kwargs: {} };\n}\n\nfunction getASTs(tree, isSubTree = false) {\n    const ASTs = [];\n    if (tree.type === \"condition\") {\n        if (tree.negate) {\n            ASTs.push(toAST(\"!\"));\n        }\n        ASTs.push({\n            type: 10,\n            value: [tree.path, tree.operator, tree.value].map(toAST),\n        });\n        return ASTs;\n    }\n\n    if (tree.type === \"complex_condition\") {\n        const ast = parseExpr(tree.value);\n        return getASTs(condition(new Expression(bool(ast)), \"=\", 1));\n    }\n\n    const length = tree.children.length;\n    if (length === 0) {\n        if (tree.value === \"|\") {\n            return tree.negate ? getASTs(TRUE_TREE) : getASTs(FALSE_TREE);\n        } else {\n            return tree.negate ? getASTs(FALSE_TREE) : isSubTree ? getASTs(TRUE_TREE) : [];\n        }\n    }\n\n    if (tree.negate) {\n        ASTs.push(toAST(\"!\"));\n    }\n    for (let i = 0; i < length - 1; i++) {\n        ASTs.push(toAST(tree.value));\n    }\n    for (const child of tree.children) {\n        ASTs.push(...getASTs(child, true));\n    }\n    return ASTs;\n}\n\nfunction toAST(value) {\n    if (isTree(value)) {\n        return { type: 4, value: getASTs(value) };\n    }\n    return astFromValue(value);\n}\n\nexport function constructDomainFromTree(tree) {\n    return formatAST(toAST(tree));\n}\n", "import { formatAST } from \"@web/core/py_js/py\";\nimport { isValidPath, not } from \"./ast_utils\";\nimport { Expression, astFromValue, isTree } from \"./condition_tree\";\nimport { COMPARATORS, TERM_OPERATORS_NEGATION } from \"./operators\";\n\nfunction getNormalizedCondition(condition) {\n    let { operator, negate } = condition;\n    if (negate && typeof operator === \"string\" && TERM_OPERATORS_NEGATION[operator]) {\n        operator = TERM_OPERATORS_NEGATION[operator];\n        negate = false;\n    }\n    return { ...condition, operator, negate };\n}\n\nfunction isX2Many(ast, options) {\n    if (isValidPath(ast, options)) {\n        const fieldDef = options.getFieldDef(ast.value); // safe: isValidPath has not returned null;\n        return [\"many2many\", \"one2many\"].includes(fieldDef.type);\n    }\n    return false;\n}\n\nfunction _constructExpressionFromTree(tree, options, isRoot = false) {\n    if (tree.type === \"connector\" && tree.value === \"|\" && tree.children.length === 2) {\n        // check if we have an \"if else\"\n        const isSimpleAnd = (tree) =>\n            tree.type === \"connector\" && tree.value === \"&\" && tree.children.length === 2;\n        if (tree.children.every((c) => isSimpleAnd(c))) {\n            const [c1, c2] = tree.children;\n            for (let i = 0; i < 2; i++) {\n                const c1Child = c1.children[i];\n                const str1 = _constructExpressionFromTree({ ...c1Child }, options);\n                for (let j = 0; j < 2; j++) {\n                    const c2Child = c2.children[j];\n                    const str2 = _constructExpressionFromTree(c2Child, options);\n                    if (str1 === `not ${str2}` || `not ${str1}` === str2) {\n                        /** @todo smth smarter. this is very fragile */\n                        const others = [c1.children[1 - i], c2.children[1 - j]];\n                        const str = _constructExpressionFromTree(c1Child, options);\n                        const strs = others.map((c) => _constructExpressionFromTree(c, options));\n                        return `${strs[0]} if ${str} else ${strs[1]}`;\n                    }\n                }\n            }\n        }\n    }\n\n    if (tree.type === \"connector\") {\n        const connector = tree.value === \"&\" ? \"and\" : \"or\";\n        const subExpressions = tree.children.map((c) => _constructExpressionFromTree(c, options));\n        if (!subExpressions.length) {\n            return connector === \"and\" ? \"1\" : \"0\";\n        }\n        let expression = subExpressions.join(` ${connector} `);\n        if (!isRoot || tree.negate) {\n            expression = `( ${expression} )`;\n        }\n        if (tree.negate) {\n            expression = `not ${expression}`;\n        }\n        return expression;\n    }\n\n    if (tree.type === \"complex_condition\") {\n        return tree.value;\n    }\n\n    tree = getNormalizedCondition(tree);\n    const { path, operator, value } = tree;\n\n    if (path instanceof Expression && operator === \"=\" && value === 1) {\n        return path.toString();\n    }\n\n    const op = operator === \"=\" ? \"==\" : operator; // do something about is ?\n    if (typeof op !== \"string\" || !COMPARATORS.includes(op)) {\n        throw new Error(\"Invalid operator\");\n    }\n\n    // we can assume that negate = false here: comparators have negation defined\n    // and the tree has been normalized\n\n    if ([0, 1].includes(path)) {\n        if (operator !== \"=\" || value !== 1) {\n            // check if this is too restricive for us\n            return new Error(\"Invalid condition\");\n        }\n        return formatAST({ type: 2, value: Boolean(path) });\n    }\n\n    const pathAST = astFromValue(path);\n    if (typeof path == \"string\" && isValidPath({ type: 5, value: path }, options)) {\n        pathAST.type = 5;\n    }\n\n    if (value === false && [\"=\", \"!=\"].includes(operator)) {\n        // true makes sense for non boolean fields?\n        return formatAST(operator === \"=\" ? not(pathAST) : pathAST);\n    }\n\n    if (isTree(value)) {\n        throw new Error(\"Invalid value\");\n    }\n\n    let valueAST = astFromValue(value);\n    if (\n        [\"in\", \"not in\"].includes(operator) &&\n        !(value instanceof Expression) &&\n        ![4, 10].includes(valueAST.type)\n    ) {\n        valueAST = { type: 4, value: [valueAST] };\n    }\n\n    if (pathAST.type === 5 && isX2Many(pathAST, options) && [\"in\", \"not in\"].includes(operator)) {\n        const ast = {\n            type: 8,\n            fn: {\n                type: 15,\n                obj: {\n                    args: [pathAST],\n                    type: 8,\n                    fn: {\n                        type: 5,\n                        value: \"set\",\n                    },\n                },\n                key: \"intersection\",\n            },\n            args: [valueAST],\n        };\n        return formatAST(operator === \"not in\" ? not(ast) : ast);\n    }\n\n    // add case true for boolean fields\n\n    return formatAST({\n        type: 7,\n        op,\n        left: pathAST,\n        right: valueAST,\n    });\n}\n\nexport function constructExpressionFromTree(tree, options = {}) {\n    return _constructExpressionFromTree(tree, options, true);\n}\n", "import { Domain } from \"@web/core/domain\";\nimport { formatAST } from \"@web/core/py_js/py\";\nimport { addChild, connector, toValue } from \"./condition_tree\";\n\n/** @typedef { import(\"@web/core/py_js/py_parser\").AST } AST */\n/** @typedef {import(\"@web/core/domain\").DomainRepr} DomainRepr */\n/** @typedef {import(\"./condition_tree\").Tree} Tree */\n\n/**\n * @param {AST[]} ASTs\n * @param {boolean} [distributeNot=false]\n * @param {boolean} [negate=false]\n * @returns {{ tree: Tree, remaimingASTs: AST[] }}\n */\nfunction _constructTree(ASTs, distributeNot = false, negate = false) {\n    const [firstAST, ...tailASTs] = ASTs;\n\n    if (firstAST.type === 1 && firstAST.value === \"!\") {\n        return _constructTree(tailASTs, distributeNot, !negate);\n    }\n\n    const tree = { type: firstAST.type === 1 ? \"connector\" : \"condition\" };\n    if (tree.type === \"connector\") {\n        tree.value = firstAST.value;\n        if (distributeNot && negate) {\n            tree.value = tree.value === \"&\" ? \"|\" : \"&\";\n            tree.negate = false;\n        } else {\n            tree.negate = negate;\n        }\n        tree.children = [];\n    } else {\n        const [pathAST, operatorAST, valueAST] = firstAST.value;\n        tree.path = toValue(pathAST);\n        tree.negate = negate;\n        tree.operator = toValue(operatorAST);\n        tree.value = toValue(valueAST);\n        if ([\"any\", \"not any\"].includes(tree.operator)) {\n            try {\n                tree.value = constructTreeFromDomain(formatAST(valueAST), distributeNot);\n            } catch {\n                tree.value = Array.isArray(tree.value) ? tree.value : [tree.value];\n            }\n        }\n    }\n    let remaimingASTs = tailASTs;\n    if (tree.type === \"connector\") {\n        for (let i = 0; i < 2; i++) {\n            const { tree: child, remaimingASTs: otherASTs } = _constructTree(\n                remaimingASTs,\n                distributeNot,\n                distributeNot && negate\n            );\n            remaimingASTs = otherASTs;\n            addChild(tree, child);\n        }\n    }\n    return { tree, remaimingASTs };\n}\n\n/**\n * @param {DomainRepr} domain\n * @param {boolean} [distributeNot=false]\n * @returns {Tree}\n */\nexport function constructTreeFromDomain(domain, distributeNot = false) {\n    domain = new Domain(domain);\n    const domainAST = domain.ast;\n    // @ts-ignore\n    const initialASTs = domainAST.value;\n    if (!initialASTs.length) {\n        return connector(\"&\");\n    }\n    const { tree } = _constructTree(initialASTs, distributeNot);\n    return tree;\n}\n", "import { formatAST, parseExpr } from \"@web/core/py_js/py\";\nimport { isNot, isValidPath, not } from \"./ast_utils\";\nimport { addChild, complexCondition, condition, connector, toValue } from \"./condition_tree\";\nimport { COMPARATORS } from \"./operators\";\n\nconst EXCHANGE = {\n    \"<\": \">\",\n    \"<=\": \">=\",\n    \">\": \"<\",\n    \">=\": \"<=\",\n    \"=\": \"=\",\n    \"!=\": \"!=\",\n};\n\nfunction or(left, right) {\n    return { type: 14, op: \"or\", left, right };\n}\n\nfunction and(left, right) {\n    return { type: 14, op: \"and\", left, right };\n}\n\nfunction isSet(ast) {\n    return ast.type === 8 && ast.fn.type === 5 && ast.fn.value === \"set\" && ast.args.length <= 1;\n}\n\nfunction isValidPath2(ast, options) {\n    if (!ast) {\n        return null;\n    }\n    if ([4, 10].includes(ast.type) && ast.value.length === 1) {\n        return isValidPath(ast.value[0], options);\n    }\n    return isValidPath(ast, options);\n}\n\nfunction _getConditionFromComparator(ast, options) {\n    if ([\"is\", \"is not\"].includes(ast.op)) {\n        // we could do something smarter here\n        // e.g. if left is a boolean field and right is a boolean\n        // we can create a condition based on \"=\"\n        return null;\n    }\n\n    let operator = ast.op;\n    if (operator === \"==\") {\n        operator = \"=\";\n    }\n\n    let left = ast.left;\n    let right = ast.right;\n    if (isValidPath(left, options) == isValidPath(right, options)) {\n        return null;\n    }\n\n    if (!isValidPath(left, options)) {\n        if (operator in EXCHANGE) {\n            const temp = left;\n            left = right;\n            right = temp;\n            operator = EXCHANGE[operator];\n        } else {\n            return null;\n        }\n    }\n\n    return condition(left.value, operator, toValue(right));\n}\n\nfunction _getConditionFromIntersection(ast, options, negate = false) {\n    let left = ast.fn.obj.args[0];\n    let right = ast.args[0];\n\n    if (!left) {\n        return condition(negate ? 1 : 0, \"=\", 1);\n    }\n\n    // left/right exchange\n    if (isValidPath2(left, options) == isValidPath2(right, options)) {\n        return null;\n    }\n    if (!isValidPath2(left, options)) {\n        const temp = left;\n        left = right;\n        right = temp;\n    }\n\n    if ([4, 10].includes(left.type) && left.value.length === 1) {\n        left = left.value[0];\n    }\n\n    if (!right) {\n        return condition(left.value, negate ? \"=\" : \"!=\", false);\n    }\n\n    // try to extract the ast of an iterable\n    // we only make simple conversions here\n    if (isSet(right)) {\n        if (!right.args[0]) {\n            right = { type: 4, value: [] };\n        }\n        if ([4, 10].includes(right.args[0].type)) {\n            right = right.args[0];\n        }\n    }\n\n    if (![4, 10].includes(right.type)) {\n        return null;\n    }\n\n    return condition(left.value, negate ? \"not in\" : \"in\", toValue(right));\n}\n\nfunction _leafFromAST(ast, options, negate = false) {\n    if (isNot(ast)) {\n        return _treeFromAST(ast.right, options, !negate);\n    }\n\n    if (ast.type === 5 /** name */ && isValidPath(ast, options)) {\n        return condition(ast.value, negate ? \"=\" : \"!=\", false);\n    }\n\n    const astValue = toValue(ast);\n    if ([\"boolean\", \"number\", \"string\"].includes(typeof astValue)) {\n        return condition(astValue ? 1 : 0, \"=\", 1);\n    }\n\n    if (\n        ast.type === 8 &&\n        ast.fn.type === 15 /** object lookup */ &&\n        isSet(ast.fn.obj) &&\n        ast.fn.key === \"intersection\"\n    ) {\n        const tree = _getConditionFromIntersection(ast, options, negate);\n        if (tree) {\n            return tree;\n        }\n    }\n\n    if (ast.type === 7 && COMPARATORS.includes(ast.op)) {\n        if (negate) {\n            return _leafFromAST(not(ast), options);\n        }\n        const tree = _getConditionFromComparator(ast, options);\n        if (tree) {\n            return tree;\n        }\n    }\n\n    // no conclusive/simple way to transform ast in a condition\n    return complexCondition(formatAST(negate ? not(ast) : ast));\n}\n\nfunction _treeFromAST(ast, options, negate = false) {\n    if (isNot(ast)) {\n        return _treeFromAST(ast.right, options, !negate);\n    }\n\n    if (ast.type === 14) {\n        const tree = connector(\n            ast.op === \"and\" ? \"&\" : \"|\" // and/or are the only ops that are given type 14 (for now)\n        );\n        if (options.distributeNot && negate) {\n            tree.value = tree.value === \"&\" ? \"|\" : \"&\";\n        } else {\n            tree.negate = negate;\n        }\n        const subASTs = [ast.left, ast.right];\n        for (const subAST of subASTs) {\n            const child = _treeFromAST(subAST, options, options.distributeNot && negate);\n            addChild(tree, child);\n        }\n        return tree;\n    }\n\n    if (ast.type === 13) {\n        const newAST = or(and(ast.condition, ast.ifTrue), and(not(ast.condition), ast.ifFalse));\n        return _treeFromAST(newAST, options, negate);\n    }\n\n    return _leafFromAST(ast, options, negate);\n}\n\nexport function constructTreeFromExpression(expression, options = {}) {\n    const ast = parseExpr(expression);\n    return _treeFromAST(ast, options);\n}\n", "import { Expression, isTree } from \"./condition_tree\";\nimport { constructTreeFromDomain } from \"./construct_tree_from_domain\";\n\nfunction treeContainsExpressions(tree) {\n    if (tree.type === \"condition\") {\n        const { path, operator, value } = tree;\n        if (isTree(value) && treeContainsExpressions(value)) {\n            return true;\n        }\n        return [path, operator, value].some(\n            (v) =>\n                v instanceof Expression ||\n                (Array.isArray(v) && v.some((w) => w instanceof Expression))\n        );\n    }\n    for (const child of tree.children) {\n        if (treeContainsExpressions(child)) {\n            return true;\n        }\n    }\n    return false;\n}\n\nexport function domainContainsExpressions(domain) {\n    let tree;\n    try {\n        tree = constructTreeFromDomain(domain);\n    } catch {\n        return null;\n    }\n    // detect expressions in the domain tree, which we know is well-formed\n    return treeContainsExpressions(tree);\n}\n", "import { constructDomainFromTree } from \"./construct_domain_from_tree\";\nimport { eliminateVirtualOperators } from \"./virtual_operators\";\n\nexport function domainFromTree(tree) {\n    const simplifiedTree = eliminateVirtualOperators(tree);\n    return constructDomainFromTree(simplifiedTree);\n}\n", "import { constructExpressionFromTree } from \"./construct_expression_from_tree\";\nimport { eliminateVirtualOperators } from \"./virtual_operators\";\n\nexport function expressionFromTree(tree, options = {}) {\n    const simplifiedTree = eliminateVirtualOperators(tree, options);\n    return constructExpressionFromTree(simplifiedTree, options);\n}\n", "export const TERM_OPERATORS_NEGATION = {\n    \"<\": \">=\",\n    \">\": \"<=\",\n    \"<=\": \">\",\n    \">=\": \"<\",\n    \"=\": \"!=\",\n    \"!=\": \"=\",\n    in: \"not in\",\n    like: \"not like\",\n    ilike: \"not ilike\",\n    \"not in\": \"in\",\n    \"not like\": \"like\",\n    \"not ilike\": \"ilike\",\n};\n\nexport const TERM_OPERATORS_NEGATION_EXTENDED = {\n    ...TERM_OPERATORS_NEGATION,\n    is: \"is not\",\n    \"is not\": \"is\",\n    \"==\": \"!=\",\n    \"!=\": \"==\", // override here\n};\n\nexport const COMPARATORS = [\"<\", \"<=\", \">\", \">=\", \"in\", \"not in\", \"==\", \"is\", \"!=\", \"is not\"];\n", "import { Component, onWillStart, onWillUpdateProps } from \"@odoo/owl\";\nimport { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\nimport { cloneTree, connector, isTree, TRUE_TREE } from \"@web/core/tree_editor/condition_tree\";\nimport {\n    getDefaultValue,\n    getValueEditorInfo,\n} from \"@web/core/tree_editor/tree_editor_value_editors\";\nimport { getResModel } from \"@web/core/tree_editor/utils\";\nimport { areEquivalentTrees } from \"@web/core/tree_editor/virtual_operators\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { shallowEqual } from \"@web/core/utils/objects\";\n\nexport class TreeEditor extends Component {\n    static template = \"web.TreeEditor\";\n    static components = {\n        Dropdown,\n        DropdownItem,\n        TreeEditor,\n    };\n    static props = {\n        tree: Object,\n        resModel: String,\n        update: Function,\n        getDefaultCondition: Function,\n        getPathEditorInfo: Function,\n        getOperatorEditorInfo: Function,\n        getDefaultOperator: Function,\n        readonly: { type: Boolean, optional: true },\n        slots: { type: Object, optional: true },\n        isDebugMode: { type: Boolean, optional: true },\n        defaultConnector: { type: [{ value: \"&\" }, { value: \"|\" }], optional: true },\n        isSubTree: { type: Boolean, optional: true },\n    };\n    static defaultProps = {\n        defaultConnector: \"&\",\n        readonly: false,\n        isSubTree: false,\n    };\n\n    setup() {\n        this.isTree = isTree;\n        this.fieldService = useService(\"field\");\n        this.treeProcessor = useService(\"tree_processor\");\n        onWillStart(() => this.onPropsUpdated(this.props));\n        onWillUpdateProps((nextProps) => this.onPropsUpdated(nextProps));\n    }\n\n    async onPropsUpdated(props) {\n        if (this.tree) {\n            this.previousTree = this.tree;\n        }\n        this.tree = cloneTree(props.tree);\n        if (shallowEqual(this.tree, TRUE_TREE)) {\n            this.tree = connector(props.defaultConnector);\n        } else if (this.tree.type !== \"connector\") {\n            this.tree = connector(props.defaultConnector, [this.tree]);\n        }\n\n        if (this.previousTree && areEquivalentTrees(this.tree, this.previousTree)) {\n            this.tree = this.previousTree;\n            this.previousTree = null;\n        }\n\n        await this.prepareInfo(props);\n    }\n\n    async prepareInfo(props) {\n        const [fieldDefs, getFieldDef] = await Promise.all([\n            this.fieldService.loadFields(props.resModel),\n            this.treeProcessor.makeGetFieldDef(props.resModel, this.tree),\n        ]);\n        this.getFieldDef = getFieldDef;\n        this.defaultCondition = props.getDefaultCondition(fieldDefs);\n\n        if (props.readonly) {\n            this.getConditionDescription = await this.treeProcessor.makeGetConditionDescription(\n                props.resModel,\n                this.tree\n            );\n        }\n    }\n\n    get className() {\n        return `${this.props.readonly ? \"o_read_mode\" : \"o_edit_mode\"}`;\n    }\n\n    get isDebugMode() {\n        return this.props.isDebugMode !== undefined ? this.props.isDebugMode : !!this.env.debug;\n    }\n\n    notifyChanges() {\n        this.props.update(this.tree);\n    }\n\n    _updateConnector(node) {\n        node.value = node.value === \"&\" ? \"|\" : \"&\";\n        node.negate = false;\n    }\n\n    updateConnector(node) {\n        this.updateNode(node, () => this._updateConnector(node));\n    }\n\n    _updateComplexCondition(node, value) {\n        node.value = value;\n    }\n\n    updateComplexCondition(node, value) {\n        this.updateNode(node, () => this._updateComplexCondition(node, value));\n    }\n\n    makeCondition(parent, condition) {\n        condition ||= parent.children.findLast((c) => c.type === \"condition\");\n        return cloneTree(condition || this.defaultCondition);\n    }\n\n    _addNewCondition(parent, node) {\n        if (node) {\n            const index = parent.children.indexOf(node);\n            parent.children.splice(index + 1, 0, this.makeCondition(parent, node));\n        } else {\n            parent.children.push(this.makeCondition(parent));\n        }\n    }\n\n    addNewCondition(parent, node) {\n        this.updateNode(parent, () => this._addNewCondition(parent, node));\n    }\n\n    _addNewConnector(parent, node) {\n        const index = parent.children.indexOf(node);\n        const nextConnector = parent.value === \"&\" ? \"|\" : \"&\";\n        parent.children.splice(\n            index + 1,\n            0,\n            connector(nextConnector, [this.makeCondition(parent, node)])\n        );\n    }\n\n    addNewConnector(parent, node) {\n        this.updateNode(parent, () => this._addNewConnector(parent, node));\n    }\n\n    _delete(ancestors, node) {\n        if (ancestors.length === 0) {\n            return;\n        }\n        const parent = ancestors.at(-1);\n        const index = parent.children.indexOf(node);\n        parent.children.splice(index, 1);\n        ancestors = ancestors.slice(0, ancestors.length - 1);\n        if (parent.children.length === 0) {\n            this._delete(ancestors, parent);\n        }\n    }\n\n    delete(ancestors, node) {\n        const upperNode = ancestors[0] || node;\n        this.updateNode(upperNode, () => this._delete(ancestors, node));\n    }\n\n    getResModel(node) {\n        const fieldDef = this.getFieldDef(node.path);\n        const resModel = getResModel(fieldDef);\n        return resModel;\n    }\n\n    getPathEditorInfo() {\n        return this.props.getPathEditorInfo(this.props.resModel, this.defaultCondition);\n    }\n\n    getOperatorEditorInfo(node) {\n        const fieldDef = this.getFieldDef(node.path);\n        return this.props.getOperatorEditorInfo(fieldDef);\n    }\n\n    getValueEditorInfo(node) {\n        const fieldDef = this.getFieldDef(node.path);\n        return getValueEditorInfo(fieldDef, node.operator);\n    }\n\n    async _updatePath(node, path) {\n        const { fieldDef } = await this.fieldService.loadFieldInfo(this.props.resModel, path);\n        node.path = path;\n        node.negate = false;\n        node.operator = this.props.getDefaultOperator(fieldDef);\n        node.value = getDefaultValue(fieldDef, node.operator);\n    }\n\n    async updatePath(node, path) {\n        this.updateNode(node, () => this._updatePath(node, path));\n    }\n\n    _updateLeafOperator(node, operator, negate) {\n        const fieldDef = this.getFieldDef(node.path);\n        node.negate = negate;\n        node.operator = operator;\n        node.value = getDefaultValue(fieldDef, operator, node.value);\n    }\n\n    updateLeafOperator(node, operator, negate) {\n        this.updateNode(node, () => this._updateLeafOperator(node, operator, negate));\n    }\n\n    _updateLeafValue(node, value) {\n        node.value = value;\n    }\n\n    updateLeafValue(node, value) {\n        this.updateNode(node, () => this._updateLeafValue(node, value));\n    }\n\n    async updateNode(node, operation) {\n        const previousNode = cloneTree(node);\n        await operation();\n        if (areEquivalentTrees(node, previousNode)) {\n            // no interesting changes for parent\n            // this means that the parent might not render the domain selector\n            // but we need to udpate editors\n            await this.prepareInfo(this.props);\n            this.render();\n        }\n        this.notifyChanges();\n    }\n\n    highlightNode(target) {\n        const nodeEl = target.closest(\".o_tree_editor_node\");\n        nodeEl.classList.toggle(\"o_hovered_button\");\n    }\n}\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { formatAST, toPyValue } from \"@web/core/py_js/py_utils\";\nimport { MultiRecordSelector } from \"@web/core/record_selectors/multi_record_selector\";\nimport { RecordSelector } from \"@web/core/record_selectors/record_selector\";\nimport { Expression } from \"@web/core/tree_editor/condition_tree\";\nimport { isId } from \"@web/core/tree_editor/utils\";\nimport { imageUrl } from \"@web/core/utils/urls\";\n\nexport const getFormat = (val, displayNames) => {\n    let text;\n    let colorIndex;\n    if (isId(val)) {\n        text =\n            typeof displayNames[val] === \"string\"\n                ? displayNames[val]\n                : _t(\"Inaccessible/missing record ID: %s\", val);\n        colorIndex = typeof displayNames[val] === \"string\" ? 0 : 2; // 0 = grey, 2 = orange\n    } else {\n        text =\n            val instanceof Expression\n                ? String(val)\n                : _t(\"Invalid record ID: %s\", formatAST(toPyValue(val)));\n        colorIndex = val instanceof Expression ? 2 : 1; // 1 = red\n    }\n    return { text, colorIndex };\n};\n\nexport class DomainSelectorAutocomplete extends MultiRecordSelector {\n    static props = {\n        ...MultiRecordSelector.props,\n        resIds: true, //resIds could be an array of ids or an array of expressions\n    };\n\n    getIds(props = this.props) {\n        return props.resIds.filter((val) => isId(val));\n    }\n\n    getTags(props, displayNames) {\n        return props.resIds.map((val, index) => {\n            const { text, colorIndex } = getFormat(val, displayNames);\n            return {\n                text,\n                colorIndex,\n                onDelete: () => {\n                    this.props.update([\n                        ...this.props.resIds.slice(0, index),\n                        ...this.props.resIds.slice(index + 1),\n                    ]);\n                },\n                img:\n                    this.isAvatarModel &&\n                    isId(val) &&\n                    imageUrl(this.props.resModel, val, \"avatar_128\"),\n            };\n        });\n    }\n}\n\nexport class DomainSelectorSingleAutocomplete extends RecordSelector {\n    static props = {\n        ...RecordSelector.props,\n        resId: true,\n    };\n\n    getDisplayName(props = this.props, displayNames) {\n        const { resId } = props;\n        if (resId === false) {\n            return \"\";\n        }\n        const { text } = getFormat(resId, displayNames);\n        return text;\n    }\n\n    getIds(props = this.props) {\n        if (isId(props.resId)) {\n            return [props.resId];\n        }\n        return [];\n    }\n}\n", "import { Component } from \"@odoo/owl\";\nimport { TagsList } from \"@web/core/tags_list/tags_list\";\nimport { _t } from \"@web/core/l10n/translation\";\n\nexport class Input extends Component {\n    static props = [\"value\", \"update\", \"placeholder?\", \"startEmpty?\"];\n    static template = \"web.TreeEditor.Input\";\n}\n\nexport class Select extends Component {\n    static props = [\"value\", \"update\", \"options\", \"placeholder?\", \"addBlankOption?\"];\n    static template = \"web.TreeEditor.Select\";\n\n    deserialize(value) {\n        return JSON.parse(value);\n    }\n\n    serialize(value) {\n        return JSON.stringify(value);\n    }\n}\n\nexport class Range extends Component {\n    static props = [\"value\", \"update\", \"editorInfo\"];\n    static template = \"web.TreeEditor.Range\";\n\n    update(index, newValue) {\n        const result = [...this.props.value];\n        result[index] = newValue;\n        return this.props.update(result);\n    }\n}\n\nexport class InRange extends Component {\n    static props = [\"value\", \"update\", \"valueTypeEditorInfo\", \"betweenEditorInfo\"];\n    static template = \"web.TreeEditor.InRange\";\n    static options = [\n        [\"today\", _t(\"Today\")],\n        [\"last 7 days\", _t(\"Last 7 days\")],\n        [\"last 30 days\", _t(\"Last 30 days\")],\n        [\"month to date\", _t(\"Month to date\")],\n        [\"last month\", _t(\"Last month\")],\n        [\"year to date\", _t(\"Year to date\")],\n        [\"last 12 months\", _t(\"Last 12 months\")],\n        [\"custom range\", _t(\"Custom range\")],\n    ];\n    updateValueType(newValueType) {\n        const [fieldType, currentValueType] = this.props.value;\n        if (currentValueType !== newValueType) {\n            const values =\n                newValueType === \"custom range\"\n                    ? this.props.betweenEditorInfo.defaultValue()\n                    : [false, false];\n            return this.props.update([fieldType, newValueType, ...values]);\n        }\n    }\n    updateValues(values) {\n        const [fieldType, currentValueType] = this.props.value;\n        return this.props.update([fieldType, currentValueType, ...values]);\n    }\n}\n\nexport class List extends Component {\n    static components = { TagsList };\n    static props = [\"value\", \"update\", \"editorInfo\"];\n    static template = \"web.TreeEditor.List\";\n\n    get tags() {\n        const { isSupported, stringify } = this.props.editorInfo;\n        return this.props.value.map((val, index) => ({\n            text: stringify(val),\n            colorIndex: isSupported(val) ? 0 : 2,\n            onDelete: () => {\n                this.props.update([\n                    ...this.props.value.slice(0, index),\n                    ...this.props.value.slice(index + 1),\n                ]);\n            },\n        }));\n    }\n\n    update(newValue) {\n        return this.props.update([...this.props.value, newValue]);\n    }\n}\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { parseExpr } from \"@web/core/py_js/py\";\nimport { formatValue, toValue } from \"@web/core/tree_editor/condition_tree\";\nimport { Select } from \"@web/core/tree_editor/tree_editor_components\";\n\nconst OPERATOR_DESCRIPTIONS = {\n    // valid operators (see TERM_OPERATORS in expression.py)\n    \"=\": (fieldDefType) => {\n        switch (fieldDefType) {\n            case \"many2one\":\n            case \"many2many\":\n            case \"one2many\":\n                return _t(\"=\");\n            default:\n                return _t(\"is equal to\");\n        }\n    },\n    \"!=\": (fieldDefType) => {\n        switch (fieldDefType) {\n            case \"many2one\":\n            case \"many2many\":\n            case \"one2many\":\n                return _t(\"!=\");\n            default:\n                return _t(\"is not equal to\");\n        }\n    },\n    \"<=\": _t(\"lower or equal to\"),\n    \"<\": (fieldDefType) => {\n        switch (fieldDefType) {\n            case \"date\":\n            case \"datetime\":\n                return _t(\"before\");\n            default:\n                return _t(\"lower than\");\n        }\n    },\n    \">\": (fieldDefType) => {\n        switch (fieldDefType) {\n            case \"date\":\n            case \"datetime\":\n                return _t(\"after\");\n            default:\n                return _t(\"greater than\");\n        }\n    },\n    \">=\": _t(\"greater or equal to\"),\n    \"=?\": \"=?\",\n    \"=like\": _t(\"=like\"),\n    \"=ilike\": _t(\"=ilike\"),\n    like: _t(\"like\"),\n    \"not like\": _t(\"not like\"),\n    ilike: _t(\"contains\"),\n    \"not ilike\": _t(\"does not contain\"),\n    in: (fieldDefType) => {\n        switch (fieldDefType) {\n            case \"many2one\":\n            case \"many2many\":\n            case \"one2many\":\n                return _t(\"is equal to\");\n            default:\n                return _t(\"is in\");\n        }\n    },\n    \"not in\": (fieldDefType) => {\n        switch (fieldDefType) {\n            case \"many2one\":\n            case \"many2many\":\n            case \"one2many\":\n                return _t(\"is not equal to\");\n            default:\n                return _t(\"is not in\");\n        }\n    },\n    child_of: _t(\"child of\"),\n    parent_of: _t(\"parent of\"),\n    any: (fieldDefType) => {\n        switch (fieldDefType) {\n            case \"many2one\":\n                return _t(\"matches\");\n            default:\n                return _t(\"match\");\n        }\n    },\n    \"not any\": (fieldDefType) => {\n        switch (fieldDefType) {\n            case \"many2one\":\n                return _t(\"matches none of\");\n            default:\n                return _t(\"match none of\");\n        }\n    },\n\n    // virtual operators\n    set: _t(\"is set\"),\n    \"not set\": _t(\"is not set\"),\n\n    \"starts with\": _t(\"starts with\"),\n\n    between: _t(\"between\"),\n    \"in range\": _t(\"is in\"),\n};\n\nfunction toKey(operator, negate = false) {\n    if (!negate && typeof operator === \"string\" && operator in OPERATOR_DESCRIPTIONS) {\n        // this case is the main one. We keep it simple\n        return operator;\n    }\n    return JSON.stringify([formatValue(operator), negate]);\n}\n\nfunction toOperator(key) {\n    if (!key.includes(\"[\")) {\n        return [key, false];\n    }\n    const [expr, negate] = JSON.parse(key);\n    return [toValue(parseExpr(expr)), negate];\n}\n\nfunction getOperatorDescription(operator, fieldDefType) {\n    const description = OPERATOR_DESCRIPTIONS[operator];\n    if (\n        typeof description === \"function\" &&\n        description.constructor?.name !== \"LazyTranslatedString\"\n    ) {\n        return description(fieldDefType);\n    }\n    return description;\n}\n\nexport function getOperatorLabel(\n    operator,\n    fieldDefType,\n    negate = false,\n    getDescr = (operator, fieldDefType) => null\n) {\n    let label;\n    if (typeof operator === \"string\" && operator in OPERATOR_DESCRIPTIONS) {\n        label = getDescr(operator, fieldDefType) || getOperatorDescription(operator, fieldDefType);\n    } else {\n        label = formatValue(operator);\n    }\n    if (negate) {\n        return _t(`not %(operator_label)s`, { operator_label: label });\n    }\n    return label;\n}\n\nfunction getOperatorInfo(operator, fieldDefType, negate = false) {\n    const key = toKey(operator, negate);\n    const label = getOperatorLabel(operator, fieldDefType, negate);\n    return [key, label];\n}\n\nexport function getOperatorEditorInfo(operators, fieldDef) {\n    const defaultOperator = operators[0];\n    const operatorsInfo = operators.map((operator) => getOperatorInfo(operator, fieldDef?.type));\n    return {\n        component: Select,\n        extractProps: ({ update, value: [operator, negate] }) => {\n            const [operatorKey, operatorLabel] = getOperatorInfo(operator, fieldDef?.type, negate);\n            const options = [...operatorsInfo];\n            if (!options.some(([key]) => key === operatorKey)) {\n                options.push([operatorKey, operatorLabel]);\n            }\n            return {\n                value: operatorKey,\n                update: (operatorKey) => update(...toOperator(operatorKey)),\n                options,\n            };\n        },\n        defaultValue: () => defaultOperator,\n        isSupported: ([operator]) =>\n            typeof operator === \"string\" && operator in OPERATOR_DESCRIPTIONS, // should depend on fieldDef too... (e.g. parent_id does not always make sense)\n        message: _t(\"Operator not supported\"),\n        stringify: ([operator, negate]) => getOperatorLabel(operator, fieldDef?.type, negate),\n    };\n}\n", "import { DateTimeInput } from \"@web/core/datetime/datetime_input\";\nimport { Domain } from \"@web/core/domain\";\nimport {\n    deserializeDate,\n    deserializeDateTime,\n    serializeDate,\n    serializeDateTime,\n} from \"@web/core/l10n/dates\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { connector, formatValue, isTree } from \"@web/core/tree_editor/condition_tree\";\nimport {\n    DomainSelectorAutocomplete,\n    DomainSelectorSingleAutocomplete,\n} from \"@web/core/tree_editor/tree_editor_autocomplete\";\nimport { Input, InRange, List, Range, Select } from \"@web/core/tree_editor/tree_editor_components\";\nimport { disambiguate, getResModel, isId } from \"@web/core/tree_editor/utils\";\nimport { unique } from \"@web/core/utils/arrays\";\n\nconst { DateTime } = luxon;\n\n// ============================================================================\n\nconst formatters = registry.category(\"formatters\");\nconst parsers = registry.category(\"parsers\");\n\nfunction parseValue(fieldType, value) {\n    const parser = parsers.get(fieldType, (value) => value);\n    try {\n        return parser(value);\n    } catch {\n        return value;\n    }\n}\n\nfunction isParsable(fieldType, value) {\n    const parser = parsers.get(fieldType, (value) => value);\n    try {\n        parser(value);\n    } catch {\n        return false;\n    }\n    return true;\n}\n\nfunction genericSerializeDate(type, value) {\n    return type === \"date\" ? serializeDate(value) : serializeDateTime(value);\n}\n\nfunction genericDeserializeDate(type, value) {\n    return type === \"date\" ? deserializeDate(value) : deserializeDateTime(value);\n}\n\nfunction placeholderForSelect(displayPlaceholder) {\n    if (displayPlaceholder) {\n        return _t(`Select one or several criteria`);\n    }\n}\n\nfunction placeholderForInput(displayPlaceholder) {\n    if (displayPlaceholder) {\n        return _t(`Press \"Enter\" to add criterion`);\n    }\n}\n\nconst STRING_EDITOR = {\n    component: Input,\n    extractProps: ({ value, update, displayPlaceholder }) => ({\n        value,\n        update,\n        placeholder: placeholderForInput(displayPlaceholder),\n    }),\n    isSupported: (value) => typeof value === \"string\",\n    defaultValue: () => \"\",\n};\n\nfunction makeSelectEditor(options, params = {}) {\n    const getOption = (value) => options.find(([v]) => v === value) || null;\n    return {\n        component: Select,\n        extractProps: ({ value, update, displayPlaceholder }) => ({\n            value,\n            update,\n            options,\n            addBlankOption: params.addBlankOption,\n            placeholder: placeholderForSelect(displayPlaceholder),\n        }),\n        isSupported: (value) => Boolean(getOption(value)),\n        defaultValue: () => options[0]?.[0] ?? false,\n        stringify: (value, disambiguate) => {\n            const option = getOption(value);\n            return option ? option[1] : disambiguate ? formatValue(value) : String(value);\n        },\n        message: _t(\"Value not in selection\"),\n    };\n}\n\nfunction getDomain(fieldDef) {\n    if (fieldDef.type === \"many2one\") {\n        return [];\n    }\n    try {\n        return new Domain(fieldDef.domain || []).toList();\n    } catch {\n        return [];\n    }\n}\n\nfunction makeAutoCompleteEditor(fieldDef) {\n    return {\n        component: DomainSelectorAutocomplete,\n        extractProps: ({ value, update }) => ({\n            resModel: getResModel(fieldDef),\n            fieldString: fieldDef.string,\n            domain: getDomain(fieldDef),\n            update: (value) => update(unique(value)),\n            resIds: unique(value),\n            placeholder: placeholderForSelect(true),\n        }),\n        isSupported: (value) => Array.isArray(value),\n        defaultValue: () => [],\n    };\n}\n\nfunction isLitteralObject(value) {\n    return typeof value === \"object\" && !Array.isArray(value) && value !== null;\n}\n\n// ============================================================================\n\nfunction getPartialValueEditorInfo(fieldDef, operator, params = {}) {\n    switch (operator) {\n        case \"set\":\n        case \"not set\":\n            return {\n                component: null,\n                extractProps: null,\n                isSupported: (value) =>\n                    value === false || (fieldDef.type === \"boolean\" && value === true),\n                defaultValue: () => false,\n            };\n        case \"=like\":\n        case \"=ilike\":\n        case \"like\":\n        case \"not like\":\n        case \"ilike\":\n        case \"not ilike\":\n            return STRING_EDITOR;\n        case \"between\": {\n            const editorInfo = getValueEditorInfo(fieldDef, \"=\", params);\n            const { defaultValue } = getValueEditorInfo(fieldDef, \"=\", {\n                ...params,\n                forBetween: true,\n            });\n            return {\n                component: Range,\n                extractProps: ({ value, update }) => ({\n                    value,\n                    update,\n                    editorInfo,\n                }),\n                isSupported: (value) => Array.isArray(value) && value.length === 2,\n                defaultValue: () => {\n                    const value = defaultValue();\n                    return isLitteralObject(value) ? [value.start, value.end] : [value, value];\n                },\n                shouldResetValue: (value) =>\n                    !editorInfo.isSupported(value[0]) || !editorInfo.isSupported(value[1]),\n            };\n        }\n        case \"in range\": {\n            return {\n                component: InRange,\n                extractProps: ({ value, update }) => ({\n                    value,\n                    update,\n                    valueTypeEditorInfo: makeSelectEditor(InRange.options, params),\n                    betweenEditorInfo: getValueEditorInfo(fieldDef, \"between\", params),\n                }),\n                isSupported: (value) =>\n                    Array.isArray(value) &&\n                    value.length === 4 &&\n                    value[0] === fieldDef.type &&\n                    InRange.options.some(([t]) => t === value[1]),\n                defaultValue: () => [fieldDef.type, \"today\", false, false],\n            };\n        }\n        case \"in\":\n        case \"not in\": {\n            switch (fieldDef.type) {\n                case \"tags\":\n                    return STRING_EDITOR;\n                case \"many2one\":\n                case \"many2many\":\n                case \"one2many\":\n                    return makeAutoCompleteEditor(fieldDef);\n                default: {\n                    const editorInfo = getValueEditorInfo(fieldDef, \"=\", {\n                        ...params,\n                        addBlankOption: true,\n                        startEmpty: true,\n                    });\n                    return {\n                        component: List,\n                        extractProps: ({ value, update }) => {\n                            if (!disambiguate(value)) {\n                                const { stringify } = editorInfo;\n                                editorInfo.stringify = (val) => stringify(val, false);\n                            }\n                            return {\n                                value,\n                                update,\n                                editorInfo,\n                            };\n                        },\n                        isSupported: (value) => Array.isArray(value),\n                        defaultValue: () => [],\n                        shouldResetValue: (value) => !value.every(editorInfo.isSupported),\n                    };\n                }\n            }\n        }\n        case \"any\":\n        case \"not any\": {\n            switch (fieldDef.type) {\n                case \"many2one\":\n                case \"many2many\":\n                case \"one2many\": {\n                    return {\n                        component: null,\n                        extractProps: null,\n                        isSupported: isTree,\n                        defaultValue: () => connector(\"&\"),\n                    };\n                }\n            }\n        }\n    }\n\n    const { type } = fieldDef;\n    switch (type) {\n        case \"integer\":\n        case \"float\":\n        case \"monetary\": {\n            const formatType = type === \"integer\" ? \"integer\" : \"float\";\n            return {\n                component: Input,\n                extractProps: ({ value, update, displayPlaceholder }) => ({\n                    value: String(value),\n                    update: (value) => update(parseValue(formatType, value)),\n                    startEmpty: params.startEmpty,\n                    placeholder: placeholderForInput(displayPlaceholder),\n                }),\n                isSupported: () => true,\n                defaultValue: () => 1,\n                shouldResetValue: (value) => parseValue(formatType, value) === value,\n            };\n        }\n        case \"date\":\n        case \"datetime\":\n            return {\n                component: DateTimeInput,\n                extractProps: ({ value, update, displayPlaceholder }) => ({\n                    value:\n                        params.startEmpty || value === false\n                            ? false\n                            : genericDeserializeDate(type, value),\n                    type,\n                    onApply: (value) => {\n                        if (!params.startEmpty || value) {\n                            update(\n                                genericSerializeDate(type, value || DateTime.local().startOf(\"day\"))\n                            );\n                        }\n                    },\n                    placeholder: placeholderForSelect(displayPlaceholder),\n                }),\n                isSupported: (value) => typeof value === \"string\" && isParsable(type, value),\n                defaultValue: (operator) => {\n                    const datetime = DateTime.local();\n                    if (operator === \">\") {\n                        return genericSerializeDate(type, datetime.endOf(\"day\"));\n                    }\n                    const start = genericSerializeDate(type, datetime.startOf(\"day\"));\n                    if (params.forBetween) {\n                        return { start, end: genericSerializeDate(type, datetime.endOf(\"day\")) };\n                    }\n                    return start;\n                },\n                shouldResetValue: () => true,\n                stringify: (value) => {\n                    if (value === false) {\n                        return _t(\"False\");\n                    }\n                    if (typeof value === \"string\" && isParsable(type, value)) {\n                        const formatter = formatters.get(type, formatValue);\n                        return formatter(genericDeserializeDate(type, value));\n                    }\n                    return formatValue(value);\n                },\n                message: _t(\"Not a valid %s\", type),\n            };\n        case \"char\":\n        case \"html\":\n        case \"text\":\n            return STRING_EDITOR;\n        case \"many2one\": {\n            if ([\"=\", \"!=\"].includes(operator)) {\n                return {\n                    component: DomainSelectorSingleAutocomplete,\n                    extractProps: ({ value, update }) => ({\n                        resModel: getResModel(fieldDef),\n                        fieldString: fieldDef.string,\n                        update,\n                        resId: value,\n                    }),\n                    isSupported: () => true,\n                    defaultValue: () => false,\n                    shouldResetValue: (value) => value !== false && !isId(value),\n                };\n            } else if ([\"parent_of\", \"child_of\"].includes(operator)) {\n                return makeAutoCompleteEditor(fieldDef);\n            }\n            break;\n        }\n        case \"many2many\":\n        case \"one2many\":\n            if ([\"=\", \"!=\"].includes(operator)) {\n                return makeAutoCompleteEditor(fieldDef);\n            }\n            break;\n        case \"selection\": {\n            const options = fieldDef.selection || [];\n            return makeSelectEditor(options, params);\n        }\n        case undefined: {\n            const options = [[1, \"1\"]];\n            return makeSelectEditor(options, params);\n        }\n    }\n\n    // Global default for visualization mainly. It is there to visualize what\n    // has been produced in the debug textarea (in o_domain_selector_debug_container)\n    // It is hardly useful to produce a string in general.\n    return {\n        component: Input,\n        extractProps: ({ value, update }) => ({\n            value: String(value),\n            update,\n        }),\n        isSupported: () => true,\n        defaultValue: () => \"\",\n    };\n}\n\nexport function getValueEditorInfo(fieldDef, operator, options = {}) {\n    const info = getPartialValueEditorInfo(fieldDef || {}, operator, options);\n    return {\n        extractProps: ({ value, update }) => ({ value, update }),\n        message: _t(\"Value not supported\"),\n        stringify: (val, disambiguate = true) => {\n            if (disambiguate) {\n                return formatValue(val);\n            }\n            return String(val);\n        },\n        ...info,\n    };\n}\n\nexport function getDefaultValue(fieldDef, operator, value = null) {\n    const { isSupported, shouldResetValue, defaultValue } = getValueEditorInfo(fieldDef, operator);\n    if (value === null || !isSupported(value) || shouldResetValue?.(value)) {\n        return defaultValue(operator);\n    }\n    return value;\n}\n", "import { constructTreeFromDomain } from \"./construct_tree_from_domain\";\nimport { introduceVirtualOperators } from \"./virtual_operators\";\n\nexport function treeFromDomain(domain, options = {}) {\n    const tree = constructTreeFromDomain(domain, options.distributeNot);\n    return introduceVirtualOperators(tree, options);\n}\n", "import { constructTreeFromExpression } from \"./construct_tree_from_expression\";\nimport { introduceVirtualOperators } from \"./virtual_operators\";\n\nexport function treeFromExpression(expression, options = {}) {\n    const tree = constructTreeFromExpression(expression, options);\n    return introduceVirtualOperators(tree, options);\n}\n", "import {\n    deserializeDate,\n    deserializeDateTime,\n    formatDate,\n    formatDateTime,\n} from \"@web/core/l10n/dates\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { getOperatorLabel } from \"@web/core/tree_editor/tree_editor_operator_editor\";\nimport { unique, zip } from \"@web/core/utils/arrays\";\nimport { condition, Expression, isTree, normalizeValue } from \"./condition_tree\";\nimport { constructTreeFromDomain } from \"./construct_tree_from_domain\";\nimport { disambiguate, getResModel, isId } from \"./utils\";\nimport { introduceVirtualOperators } from \"./virtual_operators\";\nimport { InRange } from \"./tree_editor_components\";\n\n/**\n * @param {import(\"@web/core/tree_editor/condition_tree\").Value} val\n * @param {boolean} disambiguate\n * @param {Object|null} fieldDef\n * @param {Object} displayNames\n * @returns\n */\nfunction formatValue(val, disambiguate, fieldDef, displayNames) {\n    if (val instanceof Expression) {\n        return val.toString();\n    }\n    if (displayNames && isId(val)) {\n        if (typeof displayNames[val] === \"string\") {\n            val = displayNames[val];\n        } else {\n            return _t(\"Inaccessible/missing record ID: %s\", val);\n        }\n    }\n    if (fieldDef?.type === \"selection\") {\n        const [, label] = (fieldDef.selection || []).find(([v]) => v === val) || [];\n        if (label !== undefined) {\n            val = label;\n        }\n    }\n    if (typeof val === \"string\") {\n        if (fieldDef?.type === \"datetime\") {\n            return formatDateTime(deserializeDateTime(val));\n        }\n        if (fieldDef?.type === \"date\") {\n            return formatDate(deserializeDate(val));\n        }\n    }\n    if (disambiguate && typeof val === \"string\") {\n        return JSON.stringify(val);\n    }\n    return val;\n}\n\nfunction getPathsInTree(tree, lookInSubTrees = false) {\n    const paths = [];\n    if (tree.type === \"condition\") {\n        paths.push(tree.path);\n        if (typeof tree.path === \"string\" && lookInSubTrees && isTree(tree.value)) {\n            const subTreePaths = getPathsInTree(tree.value, lookInSubTrees);\n            for (const p of subTreePaths) {\n                if (typeof p === \"string\") {\n                    paths.push(`${tree.path}.${p}`);\n                }\n            }\n        }\n    }\n    if (tree.type === \"connector\" && tree.children) {\n        for (const child of tree.children) {\n            paths.push(...getPathsInTree(child, lookInSubTrees));\n        }\n    }\n    return unique(paths);\n}\n\nfunction simplifyTree(tree) {\n    if (tree.type === \"condition\") {\n        return tree;\n    }\n    const processedChildren = tree.children.map(simplifyTree);\n    if (tree.value === \"&\") {\n        return { ...tree, children: processedChildren };\n    }\n    const children = [];\n    const childrenByPath = {};\n    for (let index = 0; index < processedChildren.length; index++) {\n        const child = processedChildren[index];\n        if (\n            child.type === \"connector\" ||\n            typeof child.path !== \"string\" ||\n            ![\"=\", \"in\"].includes(child.operator)\n        ) {\n            children.push(child);\n        } else {\n            if (!childrenByPath[child.path]) {\n                childrenByPath[child.path] = { elems: [], index };\n                children.push(child); // will be replaced if necessary\n            }\n            childrenByPath[child.path].elems.push(child);\n        }\n    }\n    for (const path in childrenByPath) {\n        if (childrenByPath[path].elems.length === 1) {\n            continue;\n        }\n        const value = [];\n        for (const child of childrenByPath[path].elems) {\n            if (child.operator === \"=\") {\n                value.push(child.value);\n            } else {\n                value.push(...child.value);\n            }\n        }\n        children[childrenByPath[path].index] = condition(path, \"in\", normalizeValue(unique(value)));\n    }\n    if (children.length === 1) {\n        return { ...children[0] };\n    }\n    return { ...tree, children };\n}\n\nfunction _extractIdsRecursive(tree, getFieldDef, idsByModel) {\n    if (tree.type === \"condition\") {\n        const fieldDef = getFieldDef(tree.path);\n        if ([\"many2one\", \"many2many\", \"one2many\"].includes(fieldDef?.type)) {\n            const value = tree.value;\n            const values = Array.isArray(value) ? value : [value];\n            const ids = values.filter((val) => isId(val));\n            const resModel = getResModel(fieldDef);\n            if (ids.length) {\n                if (!idsByModel[resModel]) {\n                    idsByModel[resModel] = [];\n                }\n                idsByModel[resModel].push(...ids);\n            }\n        }\n    }\n    if (tree.type === \"connector\") {\n        for (const child of tree.children) {\n            _extractIdsRecursive(child, getFieldDef, idsByModel);\n        }\n    }\n    return idsByModel;\n}\n\nfunction extractIdsFromTree(tree, getFieldDef) {\n    const idsByModel = _extractIdsRecursive(tree, getFieldDef, {});\n\n    for (const resModel in idsByModel) {\n        idsByModel[resModel] = unique(idsByModel[resModel]);\n    }\n\n    return idsByModel;\n}\n\nexport const treeProcessorService = {\n    dependencies: [\"field\", \"name\"],\n    async: [\n        \"getDomainTreeDescription\",\n        \"getDomainTreeTooltip\",\n        \"makeGetConditionDescription\",\n        \"makeGetFieldDef\",\n        \"treeFromDomain\",\n    ],\n    start(_, { field: fieldService, name: nameService }) {\n        async function getDisplayNames(tree, getFieldDef) {\n            const resIdsByModel = extractIdsFromTree(tree, getFieldDef);\n            const proms = [];\n            const resModels = [];\n            for (const [resModel, resIds] of Object.entries(resIdsByModel)) {\n                resModels.push(resModel);\n                proms.push(nameService.loadDisplayNames(resModel, resIds));\n            }\n            return Object.fromEntries(zip(resModels, await Promise.all(proms)));\n        }\n\n        async function makeGetPathDescriptions(resModel, tree, limit) {\n            const paths = getPathsInTree(tree);\n            const promises = [];\n            const pathDescriptions = new Map();\n            for (const path of paths) {\n                promises.push(\n                    fieldService.loadPathDescription(resModel, path).then(({ displayNames }) => {\n                        pathDescriptions.set(\n                            path,\n                            `${displayNames.slice(0, limit).join(\" \\u2794 \")}${\n                                displayNames.length > limit ? \"...\" : \"\"\n                            }`\n                        );\n                    })\n                );\n            }\n            await Promise.all(promises);\n            return (path) => pathDescriptions.get(path);\n        }\n\n        async function makeGetConditionDescription(resModel, tree, limit, pathLimit) {\n            tree = simplifyTree(tree);\n            const [getFieldDef, getPathDescription] = await Promise.all([\n                makeGetFieldDef(resModel, tree),\n                makeGetPathDescriptions(resModel, tree, pathLimit),\n            ]);\n            const displayNames = await getDisplayNames(tree, getFieldDef);\n            return (node) =>\n                _getConditionDescription(\n                    node,\n                    getFieldDef,\n                    getPathDescription,\n                    displayNames,\n                    limit\n                );\n        }\n\n        function _getConditionDescription(\n            node,\n            getFieldDef,\n            getPathDescription,\n            displayNames,\n            limit = 5\n        ) {\n            let { operator, negate, value, path } = node;\n            if (operator === \"in range\" && value[1] === \"custom range\") {\n                operator = \"between\";\n                value = value.slice(2);\n            }\n            if ([\"=\", \"!=\"].includes(operator) && value === false) {\n                operator = operator === \"=\" ? \"not set\" : \"set\";\n            }\n            const fieldDef = getFieldDef(path);\n            const operatorLabel = getOperatorLabel(operator, fieldDef?.type, negate, (operator) => {\n                switch (operator) {\n                    case \"=\":\n                    case \"in\":\n                        return \"=\";\n                    case \"!=\":\n                    case \"not in\":\n                        return _t(\"not =\");\n                    case \"any\":\n                        return \":\";\n                    case \"not any\":\n                        return _t(\": not\");\n                }\n            });\n\n            const pathDescription = getPathDescription(path);\n            const description = {\n                pathDescription,\n                operatorDescription: operatorLabel,\n                valueDescription: null,\n            };\n\n            if (isTree(node.value)) {\n                return description;\n            }\n            if ([\"set\", \"not set\"].includes(operator)) {\n                return description;\n            }\n\n            const coModeldisplayNames = displayNames[getResModel(fieldDef)];\n            const dis = disambiguate(value, coModeldisplayNames);\n            let values;\n            if (operator === \"in range\") {\n                const valueType = value[1];\n                values = [InRange.options.find(([t]) => t === valueType)[1].toString()];\n            } else {\n                values = (Array.isArray(value) ? value : [value])\n                    .slice(0, limit)\n                    .map((val, index) =>\n                        index < limit - 1\n                            ? formatValue(val, dis, fieldDef, coModeldisplayNames)\n                            : \"...\"\n                    );\n            }\n\n            let join;\n            let addParenthesis = Array.isArray(value);\n            switch (operator) {\n                case \"between\":\n                    join = _t(\"and\");\n                    addParenthesis = false;\n                    break;\n                case \"in range\":\n                    join = _t(\" \");\n                    addParenthesis = false;\n                    break;\n                case \"in\":\n                case \"not in\":\n                    addParenthesis = values.length === 0;\n                // eslint-disable-next-line no-fallthrough\n                default:\n                    join = _t(\"or\");\n            }\n            description.valueDescription = { values, join, addParenthesis };\n            return description;\n        }\n\n        async function getDomainTreeDescription(\n            resModel,\n            tree,\n            isSubExpression = false,\n            limit = undefined,\n            pathLimit = undefined\n        ) {\n            tree = simplifyTree(tree);\n            if (tree.type === \"connector\") {\n                // we assume that the domain tree is normalized (--> there is at least two children)\n                const childDescriptions = tree.children.map((node) =>\n                    getDomainTreeDescription(resModel, node, true)\n                );\n                const separator = tree.value === \"&\" ? _t(\"and\") : _t(\"or\");\n                let description = await Promise.all(childDescriptions);\n                description = description.join(` ${separator} `);\n                if (isSubExpression || tree.negate) {\n                    description = `( ${description} )`;\n                }\n                if (tree.negate) {\n                    description = `! ${description}`;\n                }\n                return description;\n            }\n            const getFieldDef = await makeGetFieldDef(resModel, tree);\n            const getConditionDescription = await makeGetConditionDescription(\n                resModel,\n                tree,\n                limit,\n                pathLimit\n            );\n            const { pathDescription, operatorDescription, valueDescription } =\n                getConditionDescription(tree);\n            const stringDescription = [pathDescription, operatorDescription];\n            if (valueDescription) {\n                const { values, join, addParenthesis } = valueDescription;\n                const jointedValues = values.join(` ${join} `);\n                stringDescription.push(addParenthesis ? `( ${jointedValues} )` : jointedValues);\n            } else if (isTree(tree.value)) {\n                const _fieldDef = getFieldDef(tree.path);\n                const _resModel = getResModel(_fieldDef);\n                const _tree = tree.value;\n                const description = await getDomainTreeDescription(_resModel, _tree);\n                stringDescription.push(`( ${description} )`);\n            }\n            return stringDescription.join(\" \");\n        }\n\n        async function getTooltipLines(resModel, tree, depth = 0) {\n            const tabs = \" \".repeat(depth * 4);\n            tree = simplifyTree(tree);\n            if (tree.type === \"connector\") {\n                // we assume that the domain tree is normalized (--> there is at least two children)\n                let connector = tree.value === \"&\" ? _t(\"all\") : _t(\"any\");\n                if (tree.negate) {\n                    connector = tree.value === \"&\" ? _t(\"not all\") : _t(\"none\");\n                }\n                connector = `${tabs}${connector}`;\n                const childrenTooltipLines = await Promise.all(\n                    tree.children.map((node) => getTooltipLines(resModel, node, depth + 1))\n                );\n                return [connector, ...childrenTooltipLines].flat();\n            }\n            const getFieldDef = await makeGetFieldDef(resModel, tree);\n            const getConditionDescription = await makeGetConditionDescription(resModel, tree, 20);\n            const { pathDescription, operatorDescription, valueDescription } =\n                getConditionDescription(tree);\n            const descr = [];\n            const stringDescriptions = [pathDescription, operatorDescription];\n            if (valueDescription) {\n                const { values, join, addParenthesis } = valueDescription;\n                const jointedValues = values.join(` ${join} `);\n                stringDescriptions.push(addParenthesis ? `( ${jointedValues} )` : jointedValues);\n            }\n            descr.push(`${tabs}${stringDescriptions.join(\" \")}`);\n            if (isTree(tree.value)) {\n                const _fieldDef = getFieldDef(tree.path);\n                const _resModel = getResModel(_fieldDef);\n                const _tree = tree.value;\n                const tooltipLines = await getTooltipLines(_resModel, _tree, depth + 1);\n                descr.push(...tooltipLines);\n            }\n            return descr;\n        }\n\n        async function getDomainTreeTooltip(resModel, tree) {\n            const descriptions = await getTooltipLines(resModel, tree);\n            return descriptions.join(\"\\n\");\n        }\n\n        async function makeGetFieldDef(resModel, tree) {\n            const paths = new Set(getPathsInTree(tree, true));\n            const promises = [];\n            const fieldDefs = {};\n            for (const path of paths) {\n                promises.push(\n                    fieldService.loadFieldInfo(resModel, path).then(({ fieldDef }) => {\n                        fieldDefs[path] = fieldDef;\n                    })\n                );\n            }\n            await Promise.all(promises);\n            return (path) => {\n                if (typeof path === \"string\") {\n                    return fieldDefs[path];\n                }\n                return null;\n            };\n        }\n\n        async function treeFromDomain(resModel, domain, distributeNot = true) {\n            const tree = constructTreeFromDomain(domain, distributeNot);\n            const getFieldDef = await makeGetFieldDef(resModel, tree);\n            return introduceVirtualOperators(tree, { getFieldDef });\n        }\n\n        return {\n            getDomainTreeDescription,\n            getDomainTreeTooltip,\n            makeGetConditionDescription,\n            makeGetFieldDef,\n            treeFromDomain,\n        };\n    },\n};\n\nregistry.category(\"services\").add(\"tree_processor\", treeProcessorService);\n", "export function disambiguate(value, displayNames) {\n    if (!Array.isArray(value)) {\n        return value === \"\";\n    }\n    let hasSomeString = false;\n    let hasSomethingElse = false;\n    for (const val of value) {\n        if (val === \"\") {\n            return true;\n        }\n        if (typeof val === \"string\" || (displayNames && isId(val))) {\n            hasSomeString = true;\n        } else {\n            hasSomethingElse = true;\n        }\n    }\n    return hasSomeString && hasSomethingElse;\n}\n\nexport function isId(value) {\n    return Number.isInteger(value) && value >= 1;\n}\n\nexport function getResModel(fieldDef) {\n    if (fieldDef) {\n        return fieldDef.is_property ? fieldDef.comodel : fieldDef.relation;\n    }\n    return null;\n}\n\nconst SPECIAL_FIELDS = [\"country_id\", \"user_id\", \"partner_id\", \"stage_id\", \"id\"];\n\nexport function getDefaultPath(fieldDefs) {\n    for (const name of SPECIAL_FIELDS) {\n        const fieldDef = fieldDefs[name];\n        if (fieldDef) {\n            return fieldDef.name;\n        }\n    }\n    const name = Object.keys(fieldDefs)[0];\n    if (name) {\n        return name;\n    }\n    throw new Error(`No field found`);\n}\n", "import {\n    applyTransformations,\n    areEqualTrees,\n    cloneTree,\n    condition,\n    connector,\n    expression,\n    FALSE_TREE,\n    isTree,\n    normalizeValue,\n    operate,\n    rewriteNConsecutiveChildren,\n    TRUE_TREE,\n} from \"./condition_tree\";\n\nfunction splitPath(path) {\n    const pathParts = typeof path === \"string\" ? path.split(\".\") : [];\n    const lastPart = pathParts.pop() || \"\";\n    const initialPath = pathParts.join(\".\");\n    return { initialPath, lastPart };\n}\n\nfunction isSimplePath(path) {\n    return typeof path === \"string\" && !splitPath(path).initialPath;\n}\n\nfunction wrapInAny(tree, initialPath, negate) {\n    let con = cloneTree(tree);\n    if (initialPath) {\n        con = condition(initialPath, \"any\", con);\n    }\n    con.negate = negate;\n    return con;\n}\n\nfunction introduceSetOperators(tree, options = {}) {\n    function _introduceSetOperator(c, options = {}) {\n        const { negate, path, operator, value } = c;\n        const fieldType = options.getFieldDef?.(path)?.type;\n        if ([\"=\", \"!=\"].includes(operator)) {\n            if (fieldType) {\n                if (fieldType === \"boolean\" && value === true) {\n                    return condition(path, operator === \"=\" ? \"set\" : \"not set\", value, negate);\n                } else if (\n                    ![\"many2one\", \"date\", \"datetime\"].includes(fieldType) &&\n                    value === false\n                ) {\n                    return condition(path, operator === \"=\" ? \"not set\" : \"set\", value, negate);\n                }\n            }\n        }\n    }\n    return operate(_introduceSetOperator, tree, options);\n}\n\nfunction eliminateSetOperators(tree) {\n    function _removeSetOperator(c) {\n        const { negate, path, operator, value } = c;\n        if ([\"set\", \"not set\"].includes(operator)) {\n            if (value === true) {\n                return condition(path, operator === \"set\" ? \"=\" : \"!=\", value, negate);\n            }\n            return condition(path, operator === \"set\" ? \"!=\" : \"=\", value, negate);\n        }\n    }\n    return operate(_removeSetOperator, tree);\n}\n\nfunction introduceStartsWithOperators(tree, options) {\n    function _introduceStartsWithOperator(c, options) {\n        const { negate, path, operator, value } = c;\n        const fieldType = options.getFieldDef?.(path)?.type;\n        if (\n            [\"char\", \"text\", \"html\"].includes(fieldType) &&\n            operator === \"=ilike\" &&\n            typeof value === \"string\"\n        ) {\n            if (value.endsWith(\"%\")) {\n                return condition(path, \"starts with\", value.slice(0, -1), negate);\n            }\n        }\n    }\n    return operate(_introduceStartsWithOperator, tree, options);\n}\n\nfunction eliminateStartsWithOperators(tree) {\n    function _eliminateStartsWithOperator(c) {\n        const { negate, path, operator, value } = c;\n        if (operator === \"starts with\") {\n            return condition(path, \"=ilike\", `${value}%`, negate);\n        }\n    }\n    return operate(_eliminateStartsWithOperator, tree);\n}\n\nfunction isSimpleAnd(c) {\n    if (\n        c.type === \"connector\" &&\n        c.value === \"&\" &&\n        !c.negate &&\n        c.children.length === 2 &&\n        c.children.every((child) => child.type === \"condition\" && !child.negate)\n    ) {\n        return true;\n    }\n    return false;\n}\n\nfunction isBetween(c) {\n    if (isSimpleAnd(c)) {\n        const [\n            { path: p1, operator: op1, value: value1 },\n            { path: p2, operator: op2, value: value2 },\n        ] = c.children;\n        if (p1 === p2 && op1 === \">=\" && op2 === \"<=\") {\n            return { path: p1, value1, value2 };\n        }\n    }\n    return false;\n}\n\nfunction makeBetween(path, value1, value2) {\n    return connector(\"&\", [condition(path, \">=\", value1), condition(path, \"<=\", value2)]);\n}\n\nfunction isStrictBetween(c) {\n    if (isSimpleAnd(c)) {\n        const [\n            { path: p1, operator: op1, value: value1 },\n            { path: p2, operator: op2, value: value2 },\n        ] = c.children;\n        if (p1 === p2 && op1 === \">=\" && op2 === \"<\") {\n            return { path: p1, value1, value2 };\n        }\n    }\n    return false;\n}\n\nfunction makeStrictBetween(path, value1, value2) {\n    return connector(\"&\", [condition(path, \">=\", value1), condition(path, \"<\", value2)]);\n}\n\nfunction boundDate(delta) {\n    if (!delta) {\n        return expression(`context_today().strftime(\"%Y-%m-%d\")`);\n    }\n    return expression(`(context_today() + relativedelta(${delta})).strftime('%Y-%m-%d')`);\n}\n\nfunction boundDatetime(delta) {\n    if (!delta) {\n        return expression(\n            `datetime.datetime.combine(context_today(), datetime.time(0, 0, 0)).to_utc().strftime(\"%Y-%m-%d %H:%M:%S\")`\n        );\n    }\n    return expression(\n        `datetime.datetime.combine(context_today() + relativedelta(${delta}), datetime.time(0, 0, 0)).to_utc().strftime(\"%Y-%m-%d %H:%M:%S\")`\n    );\n}\n\nconst BOUNDS_SMART_DATES = [\n    [\"today\", \"today\", \"today +1d\"],\n    [\"last 7 days\", \"today -7d\", \"today\"],\n    [\"last 30 days\", \"today -30d\", \"today\"],\n    [\"month to date\", \"today =1d\", \"today +1d\"],\n    [\"last month\", \"today =1d -1m\", \"today =1d\"],\n    [\"year to date\", \"today =1m =1d\", \"today +1d\"],\n    [\"last 12 months\", \"today =1d -12m\", \"today =1d\"],\n];\nconst DELTAS = [\n    [\"today\", \"\", \"days = 1\"],\n    [\"last 7 days\", \"days = -7\", \"\"],\n    [\"last 30 days\", \"days = -30\", \"\"],\n    [\"month to date\", \"day = 1\", \"days = 1\"],\n    [\"last month\", \"day = 1, months = -1\", \"day = 1\"],\n    [\"year to date\", \"day = 1, month = 1\", \"days = 1\"],\n    [\"last 12 months\", \"day = 1, months = -12\", \"day = 1\"],\n];\nconst BOUNDS_DATE = DELTAS.map(([k, l, r]) => [k, boundDate(l), boundDate(r)]);\nconst BOUNDS_DATETIME = DELTAS.map(([k, l, r]) => [k, boundDatetime(l), boundDatetime(r)]);\n\nfunction getBounds(generateSmartDates, fieldType) {\n    return generateSmartDates\n        ? BOUNDS_SMART_DATES\n        : fieldType === \"date\"\n        ? BOUNDS_DATE\n        : BOUNDS_DATETIME;\n}\n\nfunction introduceInRangeOperators(tree, options = {}) {\n    function _introduceInRangeOperator(c, options) {\n        const res1 = isStrictBetween(c);\n        if (res1) {\n            const generateSmartDates =\n                \"generateSmartDates\" in options ? options.generateSmartDates : true;\n            // @ts-ignore\n            const { path, value1, value2 } = res1;\n            const fieldType = options.getFieldDef?.(path)?.type;\n            if ([\"date\", \"datetime\"].includes(fieldType) && isSimplePath(path)) {\n                const bounds = getBounds(generateSmartDates, fieldType);\n                for (const [valueType, leftBound, rightBound] of bounds) {\n                    if (\n                        generateSmartDates\n                            ? value1 === leftBound && value2 === rightBound\n                            : value1._expr === leftBound._expr && value2._expr === rightBound._expr\n                    ) {\n                        return condition(path, \"in range\", [fieldType, valueType, false, false]);\n                    }\n                }\n            }\n        }\n        const res2 = isBetween(c);\n        if (res2) {\n            // @ts-ignore\n            const { path, value1, value2 } = res2;\n            const fieldType = options.getFieldDef?.(path)?.type;\n            if ([\"date\", \"datetime\"].includes(fieldType) && isSimplePath(path)) {\n                return condition(path, \"in range\", [\n                    fieldType,\n                    \"custom range\",\n                    // @ts-ignore\n                    ...normalizeValue([value1, value2]),\n                ]);\n            }\n        }\n    }\n    return operate(\n        rewriteNConsecutiveChildren(_introduceInRangeOperator),\n        tree,\n        options,\n        \"connector\"\n    );\n}\n\nfunction eliminateInRangeOperators(tree, options = {}) {\n    function _eliminateInRangeOperator(c, options) {\n        const { negate, path, operator, value } = c;\n        // @ts-ignore\n        if (operator !== \"in range\") {\n            return;\n        }\n        const { initialPath, lastPart } = splitPath(path);\n        const [fieldType, valueType, value1, value2] = value;\n        let tree;\n        if (valueType === \"custom range\") {\n            tree = makeBetween(lastPart, value1, value2);\n        } else {\n            const generateSmartDates =\n                \"generateSmartDates\" in options ? options.generateSmartDates : true;\n            const bounds = getBounds(generateSmartDates, fieldType);\n            const [, leftBound, rightBound] = bounds.find(([v]) => v === valueType);\n            tree = makeStrictBetween(lastPart, leftBound, rightBound);\n        }\n        return wrapInAny(tree, initialPath, negate);\n    }\n    return operate(_eliminateInRangeOperator, tree, options);\n}\n\nfunction introduceBetweenOperators(tree, options = {}) {\n    function _introduceBetweenOperator(c, options) {\n        const res = isBetween(c);\n        if (!res) {\n            return;\n        }\n        // @ts-ignore\n        const { path, value1, value2 } = res;\n        const fieldType = options.getFieldDef?.(path)?.type;\n        if ([\"integer\", \"float\", \"monetary\"].includes(fieldType) && isSimplePath(path)) {\n            return condition(path, \"between\", normalizeValue([value1, value2]));\n        }\n    }\n    return operate(\n        rewriteNConsecutiveChildren(_introduceBetweenOperator),\n        tree,\n        options,\n        \"connector\"\n    );\n}\n\nfunction eliminateBetweenOperators(tree) {\n    function _eliminateBetweenOperator(c) {\n        const { negate, path, operator, value } = c;\n        // @ts-ignore\n        if (operator !== \"between\") {\n            return;\n        }\n        const { initialPath, lastPart } = splitPath(path);\n        return wrapInAny(makeBetween(lastPart, value[0], value[1]), initialPath, negate);\n    }\n    return operate(_eliminateBetweenOperator, tree);\n}\n\nfunction _eliminateAnyOperator(c) {\n    const { path, operator, value, negate } = c;\n    if (\n        operator === \"any\" &&\n        isTree(value) &&\n        value.type === \"condition\" &&\n        typeof path === \"string\" &&\n        typeof value.path === \"string\" &&\n        !negate &&\n        !value.negate &&\n        [\"between\", \"in range\"].includes(value.operator)\n    ) {\n        return condition(`${path}.${value.path}`, value.operator, value.value);\n    }\n}\n\nfunction eliminateAnyOperators(tree) {\n    return operate(_eliminateAnyOperator, tree);\n}\n\nfunction removeFalseTrueLeaves(tree) {\n    function _removeFalseTrueLeave(c) {\n        const { path, operator, value, negate } = c;\n        if (areEqualTrees(condition(path, operator, value), FALSE_TREE)) {\n            return connector(negate ? \"&\" : \"|\", []);\n        }\n        if (areEqualTrees(condition(path, operator, value), TRUE_TREE)) {\n            return connector(negate ? \"|\" : \"&\", []);\n        }\n    }\n    return operate(_removeFalseTrueLeave, tree);\n}\n\nexport function introduceVirtualOperators(tree, options = {}) {\n    return applyTransformations(\n        [\n            eliminateAnyOperators,\n            introduceSetOperators,\n            introduceStartsWithOperators,\n            introduceBetweenOperators,\n            introduceInRangeOperators,\n        ],\n        tree,\n        options\n    );\n}\n\nexport function eliminateVirtualOperators(tree, options = {}) {\n    return applyTransformations(\n        [\n            eliminateInRangeOperators,\n            eliminateBetweenOperators,\n            eliminateStartsWithOperators,\n            eliminateSetOperators,\n        ],\n        tree,\n        options\n    );\n}\n\nexport function areEquivalentTrees(tree, otherTree) {\n    const simplifiedTree = removeFalseTrueLeaves(eliminateVirtualOperators(tree));\n    const otherSimplifiedTree = removeFalseTrueLeaves(eliminateVirtualOperators(otherTree));\n    return areEqualTrees(simplifiedTree, otherSimplifiedTree);\n}\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { browser } from \"@web/core/browser/browser\";\n\nimport { EventBus, Component, useState } from \"@odoo/owl\";\n\nexport class BlockUI extends Component {\n    static props = {\n        bus: EventBus,\n    };\n\n    static template = \"web.BlockUI\";\n\n    setup() {\n        this.messagesByDuration = [\n            { time: 20, l1: _t(\"Loading...\") },\n            { time: 40, l1: _t(\"Still loading...\") },\n            {\n                time: 60,\n                l1: _t(\"Still loading...\"),\n                l2: _t(\"Please be patient.\"),\n            },\n            {\n                time: 180,\n                l1: _t(\"Don't leave yet,\"),\n                l2: _t(\"it's still loading...\"),\n            },\n            {\n                time: 120,\n                l1: _t(\"You may not believe it,\"),\n                l2: _t(\"but the application is actually loading...\"),\n            },\n            {\n                time: 3180,\n                l1: _t(\"Take a minute to get a coffee,\"),\n                l2: _t(\"because it's loading...\"),\n            },\n            {\n                time: null,\n                l1: _t(\"Maybe you should consider reloading the application by pressing F5...\"),\n            },\n        ];\n        this.BLOCK_STATES = { UNBLOCKED: 0, BLOCKED: 1, VISIBLY_BLOCKED: 2 };\n        this.state = useState({\n            blockState: this.BLOCK_STATES.UNBLOCKED,\n            line1: \"\",\n            line2: \"\",\n        });\n\n        this.props.bus.addEventListener(\"BLOCK\", this.block.bind(this));\n        this.props.bus.addEventListener(\"UNBLOCK\", this.unblock.bind(this));\n    }\n\n    replaceMessage(index) {\n        const message = this.messagesByDuration[index];\n        this.state.line1 = message.l1;\n        this.state.line2 = message.l2 || \"\";\n        if (message.time !== null) {\n            this.msgTimer = browser.setTimeout(() => {\n                this.replaceMessage(index + 1);\n            }, message.time * 1000);\n        }\n    }\n\n    block(ev) {\n        const showBlockedUI = () => (this.state.blockState = this.BLOCK_STATES.VISIBLY_BLOCKED);\n        const delay = ev.detail?.delay;\n        if (delay) {\n            this.state.blockState = this.BLOCK_STATES.BLOCKED;\n            this.showBlockedUITimer = setTimeout(showBlockedUI, delay);\n        } else {\n            showBlockedUI();\n        }\n\n        if (ev.detail?.message) {\n            this.state.line1 = ev.detail.message;\n        } else {\n            this.replaceMessage(0);\n        }\n    }\n\n    unblock() {\n        this.state.blockState = this.BLOCK_STATES.UNBLOCKED;\n        clearTimeout(this.showBlockedUITimer);\n        clearTimeout(this.msgTimer);\n        this.state.line1 = \"\";\n        this.state.line2 = \"\";\n    }\n}\n", "import { useService } from \"@web/core/utils/hooks\";\nimport { registry } from \"@web/core/registry\";\nimport { throttleForAnimation } from \"@web/core/utils/timing\";\nimport { BlockUI } from \"./block_ui\";\nimport { browser } from \"@web/core/browser/browser\";\nimport { getTabableElements, isFocusable } from \"@web/core/utils/ui\";\nimport { getActiveHotkey } from \"../hotkeys/hotkey_service\";\n\nimport { EventBus, reactive, useEffect, useRef } from \"@odoo/owl\";\n\nexport const SIZES = { XS: 0, SM: 1, MD: 2, LG: 3, XL: 4, XXL: 5 };\n\nexport function getFirstAndLastTabableElements(el) {\n    const tabableEls = getTabableElements(el);\n    return [tabableEls[0], tabableEls[tabableEls.length - 1]];\n}\n\n/**\n * This hook will set the UI active element\n * when the caller component will mount/patch and\n * only if the t-reffed element has some tabable elements\n * or is itself focusable.\n *\n * The caller component could pass a `t-ref` value of its template\n * to delegate the UI active element to another element than itself.\n *\n * @param {string} refName\n */\nexport function useActiveElement(refName) {\n    if (!refName) {\n        throw new Error(\"refName not given to useActiveElement\");\n    }\n    const uiService = useService(\"ui\");\n    const ref = useRef(refName);\n\n    function trapFocus(e) {\n        const hotkey = getActiveHotkey(e);\n        if (![\"tab\", \"shift+tab\"].includes(hotkey)) {\n            return;\n        }\n        const el = e.currentTarget;\n        const [firstTabableEl, lastTabableEl] = getFirstAndLastTabableElements(el);\n        if (!firstTabableEl && !lastTabableEl) {\n            e.preventDefault();\n            e.stopPropagation();\n            return;\n        }\n        switch (hotkey) {\n            case \"tab\":\n                if (document.activeElement === lastTabableEl) {\n                    firstTabableEl.focus();\n                    e.preventDefault();\n                    e.stopPropagation();\n                }\n                break;\n            case \"shift+tab\":\n                if (document.activeElement === firstTabableEl) {\n                    lastTabableEl.focus();\n                    e.preventDefault();\n                    e.stopPropagation();\n                }\n                break;\n        }\n    }\n\n    useEffect(\n        (el) => {\n            if (el) {\n                const [firstTabableEl] = getFirstAndLastTabableElements(el);\n                if (!firstTabableEl && !isFocusable(el)) {\n                    // no tabable elements: no need to trap focus nor become the UI active element\n                    return;\n                }\n                const oldActiveElement = document.activeElement;\n                uiService.activateElement(el);\n\n                el.addEventListener(\"keydown\", trapFocus);\n\n                if (firstTabableEl) {\n                    if (!el.contains(document.activeElement)) {\n                        firstTabableEl.focus();\n                    }\n                } else if (el !== document.activeElement) {\n                    el.focus();\n                }\n                return async () => {\n                    // Components are destroyed from top to bottom, meaning that this cleanup is\n                    // called before the ones of children. As a consequence, event handlers added on\n                    // the current active element in children aren't removed yet, and can thus be\n                    // executed if we deactivate that active element right away (e.g. the blur and\n                    // change events could be triggered). For that reason, we wait for a micro-tick.\n                    await Promise.resolve();\n                    uiService.deactivateElement(el);\n                    el.removeEventListener(\"keydown\", trapFocus);\n\n                    /**\n                     * In some cases, the current active element is not\n                     * anymore in el (e.g. with ConfirmationDialog, the\n                     * confirm button is disabled when clicked, so the\n                     * focus is lost). In that case, we also want to restore\n                     * the focus to the previous active element so we\n                     * check if the current active element is the body\n                     */\n                    if (\n                        el.contains(document.activeElement) ||\n                        document.activeElement === document.body\n                    ) {\n                        oldActiveElement.focus();\n                    }\n                };\n            }\n        },\n        () => [ref.el]\n    );\n}\n\n// window size handling\nexport const MEDIAS_BREAKPOINTS = [\n    { maxWidth: 575 },\n    { minWidth: 576, maxWidth: 767 },\n    { minWidth: 768, maxWidth: 991 },\n    { minWidth: 992, maxWidth: 1199 },\n    { minWidth: 1200, maxWidth: 1399 },\n    { minWidth: 1400 },\n];\n\n/**\n * Create the MediaQueryList used both by the uiService and config from\n * `MEDIA_BREAKPOINTS`.\n *\n * @returns {MediaQueryList[]}\n */\nexport function getMediaQueryLists() {\n    return MEDIAS_BREAKPOINTS.map(({ minWidth, maxWidth }) => {\n        if (!maxWidth) {\n            return window.matchMedia(`(min-width: ${minWidth}px)`);\n        }\n        if (!minWidth) {\n            return window.matchMedia(`(max-width: ${maxWidth}px)`);\n        }\n        return window.matchMedia(`(min-width: ${minWidth}px) and (max-width: ${maxWidth}px)`);\n    });\n}\n\n// window size handling.\nconst MEDIAS = getMediaQueryLists();\n\nexport const utils = {\n    getSize() {\n        return MEDIAS.findIndex((media) => media.matches);\n    },\n    isSmall(ui = {}) {\n        return (ui.size || utils.getSize()) <= SIZES.SM;\n    },\n};\n\nconst bus = new EventBus();\n\nexport function listenSizeChange(callback) {\n    bus.addEventListener(\"resize\", callback);\n    return () => bus.removeEventListener(\"resize\", callback);\n}\n\nexport const uiService = {\n    start(env) {\n        // block/unblock code\n        registry.category(\"main_components\").add(\"BlockUI\", { Component: BlockUI, props: { bus } });\n\n        let blockCount = 0;\n        function block(data) {\n            blockCount++;\n            // TODO could probably be improved to handle multiple block demands\n            // but that have different messages and delays\n            if (blockCount === 1) {\n                bus.trigger(\"BLOCK\", {\n                    message: data?.message,\n                    delay: data?.delay,\n                });\n            }\n        }\n        function unblock() {\n            blockCount--;\n            if (blockCount < 0) {\n                console.warn(\n                    \"Unblock ui was called more times than block, you should only unblock the UI if you have previously blocked it.\"\n                );\n                blockCount = 0;\n            }\n            if (blockCount === 0) {\n                bus.trigger(\"UNBLOCK\");\n            }\n        }\n\n        // UI active element code\n        let activeElems = [document];\n\n        function activateElement(el) {\n            activeElems.push(el);\n            bus.trigger(\"active-element-changed\", el);\n        }\n        function deactivateElement(el) {\n            activeElems = activeElems.filter((x) => x !== el);\n            bus.trigger(\"active-element-changed\", ui.activeElement);\n        }\n        function getActiveElementOf(el) {\n            for (const activeElement of [...activeElems].reverse()) {\n                if (activeElement.contains(el)) {\n                    return activeElement;\n                }\n            }\n        }\n\n        const ui = reactive({\n            bus,\n            size: utils.getSize(),\n            get activeElement() {\n                return activeElems[activeElems.length - 1];\n            },\n            get isBlocked() {\n                return blockCount > 0;\n            },\n            isSmall: utils.isSmall(),\n            block,\n            unblock,\n            activateElement,\n            deactivateElement,\n            getActiveElementOf,\n        });\n\n        // listen to media query status changes\n        const updateSize = () => {\n            const prevSize = ui.size;\n            ui.size = utils.getSize();\n            if (ui.size !== prevSize) {\n                ui.isSmall = utils.isSmall(ui);\n                bus.trigger(\"resize\");\n            }\n        };\n        browser.addEventListener(\"resize\", throttleForAnimation(updateSize));\n\n        Object.defineProperty(env, \"isSmall\", {\n            get() {\n                return ui.isSmall;\n            },\n        });\n\n        return ui;\n    },\n};\n\nregistry.category(\"services\").add(\"ui\", uiService);\n", "import { browser } from \"@web/core/browser/browser\";\nimport { pyToJsLocale } from \"@web/core/l10n/utils/locales\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { Cache } from \"@web/core/utils/cache\";\nimport { session } from \"@web/session\";\nimport { ensureArray, sortBy } from \"./utils/arrays\";\nimport { cookie } from \"@web/core/browser/cookie\";\nimport { EventBus } from \"@odoo/owl\";\n\n// This file exports an object containing user-related information and functions\n// allowing to obtain/alter user-related information from the server.\n\nexport const userBus = new EventBus();\n\nfunction getCookieCompanyIds() {\n    if (cookie.get(\"cids\")) {\n        const cids = cookie.get(\"cids\");\n        if (typeof cids === \"string\") {\n            return cids.split(\"-\").map(Number);\n        }\n        if (typeof cids === \"number\") {\n            return [cids];\n        }\n    }\n    return [];\n}\n\n/**\n * This function exists for testing purposes. We don't want tests to share the\n * same cache. It allows to generate new caches at the beginning of tests.\n *\n * Note: with hoot, this will no longer be necessary.\n *\n * @returns Object\n */\nexport function _makeUser(session) {\n    // Retrieve user-related information from the session\n    const {\n        home_action_id: homeActionId,\n        is_admin: isAdmin,\n        is_internal_user: isInternalUser,\n        is_system: isSystem,\n        is_public: isPublic,\n        name,\n        partner_id: partnerId,\n        show_effect: showEffect,\n        uid: userId,\n        username: login,\n        user_context: context,\n        user_settings,\n        partner_write_date: writeDate,\n        user_companies: userCompanies,\n        groups = {},\n    } = session;\n    const settings = user_settings || {};\n\n    function updateActiveCompanies(cids, allowedCompanies, defaultCompanyId) {\n        activeCompanies = [];\n        cids.forEach((cid) => {\n            activeCompanies.push(allowedCompanies.find((c) => c.id === cid));\n        });\n        if (\n            activeCompanies.length === 0 ||\n            activeCompanies.length !== activeCompanies.filter(Boolean).length\n        ) {\n            activeCompanies = [defaultCompanyId];\n        }\n        // Sort companies, except for the first one which has a different status, as the order of\n        // the others doesn't matter, and we want to reduce the entropy of the `allowed_company_ids`\n        // key in the context. This is important for the caches, as the stringified context is\n        // always present in the rpc cache keys.\n        activeCompanies = [activeCompanies[0]].concat(\n            sortBy(activeCompanies.slice(1), (c) => c.id)\n        );\n\n        // update browser data\n        cookie.set(\"cids\", activeCompanies.map((c) => c.id).join(\"-\"));\n        Object.assign(context, { allowed_company_ids: activeCompanies.map((c) => c.id) });\n\n        userBus.trigger(\"ACTIVE_COMPANIES_CHANGED\");\n    }\n\n    // Companies information\n    let allowedCompanies = [];\n    const allowedCompaniesWithAncestors = [];\n    let activeCompanies = [];\n    let defaultCompany;\n\n    if (userCompanies) {\n        allowedCompanies = Object.values(userCompanies.allowed_companies);\n        allowedCompaniesWithAncestors.push(...Object.values(userCompanies.allowed_companies));\n        if (userCompanies.disallowed_ancestor_companies) {\n            allowedCompaniesWithAncestors.push(\n                ...Object.values(userCompanies.disallowed_ancestor_companies)\n            );\n        }\n        defaultCompany = allowedCompanies.find((c) => c.id === userCompanies.current_company); // TODO: change the name in the session current_company to default_company\n        updateActiveCompanies(getCookieCompanyIds(), allowedCompanies, defaultCompany);\n    }\n\n    // Delete user-related information from the session, s.t. there's a single source of truth\n    delete session.home_action_id;\n    delete session.is_admin;\n    delete session.is_internal_user;\n    delete session.is_system;\n    delete session.name;\n    delete session.partner_id;\n    delete session.show_effect;\n    delete session.uid;\n    delete session.username;\n    delete session.user_context;\n    delete session.user_settings;\n    delete session.partner_write_date;\n    delete session.user_companies;\n    delete session.groups;\n\n    // Generate caches for has_group and has_access calls\n    const getGroupCacheValue = (group, context) => {\n        if (!userId) {\n            return Promise.resolve(false);\n        }\n        return rpc(\"/web/dataset/call_kw/res.users/has_group\", {\n            model: \"res.users\",\n            method: \"has_group\",\n            args: [userId, group],\n            kwargs: { context },\n        });\n    };\n    const getGroupCacheKey = (group) => group;\n    const groupCache = new Cache(getGroupCacheValue, getGroupCacheKey);\n    if (isInternalUser !== undefined) {\n        groupCache.cache[\"base.group_user\"] = Promise.resolve(isInternalUser);\n    }\n    if (isSystem !== undefined) {\n        groupCache.cache[\"base.group_system\"] = Promise.resolve(isSystem);\n    }\n    if (isAdmin !== undefined) {\n        groupCache.cache[\"base.group_erp_manager\"] = Promise.resolve(isAdmin);\n    }\n    if (isPublic !== undefined) {\n        groupCache.cache[\"base.group_public\"] = Promise.resolve(isPublic);\n    }\n    for (const group in groups) {\n        groupCache.cache[group] = Promise.resolve(!!groups[group]);\n    }\n    const getAccessRightCacheValue = (model, operation, ids, context) => {\n        const url = `/web/dataset/call_kw/${model}/has_access`;\n        return rpc(url, {\n            model,\n            method: \"has_access\",\n            args: [ids, operation],\n            kwargs: { context },\n        });\n    };\n    const getAccessRightCacheKey = (model, operation, ids) =>\n        JSON.stringify([model, operation, ids]);\n    const accessRightCache = new Cache(getAccessRightCacheValue, getAccessRightCacheKey);\n    const lang = pyToJsLocale(context?.lang);\n\n    return {\n        name,\n        login,\n        isAdmin,\n        isSystem,\n        isInternalUser,\n        partnerId,\n        homeActionId,\n        showEffect,\n        userId, // TODO: rename into id?\n        writeDate,\n        get context() {\n            return Object.assign({}, context, { uid: this.userId });\n        },\n        get lang() {\n            return lang;\n        },\n        get tz() {\n            return this.context.tz;\n        },\n        get settings() {\n            return Object.assign({}, settings);\n        },\n        updateContext(update) {\n            Object.assign(context, update);\n        },\n        hasGroup(group) {\n            return groupCache.read(group, this.context);\n        },\n        checkAccessRight(model, operation, ids = []) {\n            return accessRightCache.read(model, operation, ensureArray(ids), this.context);\n        },\n        async setUserSettings(key, value) {\n            const model = \"res.users.settings\";\n            const method = \"set_res_users_settings\";\n            const changedSettings = await rpc(`/web/dataset/call_kw/${model}/${method}`, {\n                model,\n                method,\n                args: [[this.settings.id]],\n                kwargs: {\n                    new_settings: {\n                        [key]: value,\n                    },\n                    context: this.context,\n                },\n            });\n            Object.assign(settings, changedSettings);\n        },\n        updateUserSettings(key, value) {\n            settings[key] = value;\n        },\n        defaultCompany, // default company of the user, used if no cookie set\n        allowedCompanies, // list of authorized companies for the user\n        allowedCompaniesWithAncestors,\n        // list of companies the user is currently logged into\n        get activeCompanies() {\n            return activeCompanies;\n        },\n        // main company the user is currently logged into (default company for created records)\n        get activeCompany() {\n            return activeCompanies?.[0];\n        },\n        async activateCompanies(\n            companyIds,\n            options = { includeChildCompanies: true, reload: true }\n        ) {\n            const newCompanyIds = companyIds.length ? companyIds : [activeCompanies[0].id];\n\n            function addCompanies(companyIds) {\n                for (const companyId of companyIds) {\n                    if (!newCompanyIds.includes(companyId)) {\n                        newCompanyIds.push(companyId);\n                        addCompanies(allowedCompanies.find((c) => c.id === companyId).child_ids);\n                    }\n                }\n            }\n\n            if (options.includeChildCompanies) {\n                addCompanies(\n                    companyIds.flatMap(\n                        (companyId) => allowedCompanies.find((c) => c.id === companyId).child_ids\n                    )\n                );\n            }\n\n            updateActiveCompanies(newCompanyIds, allowedCompanies, defaultCompany);\n\n            if (options.reload) {\n                browser.location.reload();\n            }\n        },\n    };\n}\n\nexport const user = _makeUser(session);\n\nconst LAST_CONNECTED_USER_KEY = \"web.lastConnectedUser\";\n\nexport const getLastConnectedUsers = () => {\n    const lastConnectedUsers = browser.localStorage.getItem(LAST_CONNECTED_USER_KEY);\n    return lastConnectedUsers ? JSON.parse(lastConnectedUsers) : [];\n};\n\nexport const setLastConnectedUsers = (users) => {\n    browser.localStorage.setItem(LAST_CONNECTED_USER_KEY, JSON.stringify(users.slice(0, 5)));\n};\n\nif (!session.quick_login) {\n    browser.localStorage.removeItem(LAST_CONNECTED_USER_KEY);\n} else if (user.login && user.login !== \"__system__\") {\n    const users = getLastConnectedUsers();\n    const lastConnectedUsers = [\n        {\n            login: user.login,\n            name: user.name,\n            partnerId: user.partnerId,\n            partnerWriteDate: user.writeDate,\n            userId: user.userId,\n        },\n        ...users.filter((u) => u.userId !== user.userId),\n    ];\n    setLastConnectedUsers(lastConnectedUsers);\n}\ndelete session.quick_login;\n", "import { Component, useRef, useState, useEffect } from \"@odoo/owl\";\nimport { registry } from \"@web/core/registry\";\nimport { getLastConnectedUsers, setLastConnectedUsers } from \"@web/core/user\";\nimport { imageUrl } from \"@web/core/utils/urls\";\n\nexport class UserSwitch extends Component {\n    static template = \"web.login_user_switch\";\n    static props = {};\n\n    setup() {\n        const users = getLastConnectedUsers();\n        this.root = useRef(\"root\");\n        this.state = useState({\n            users,\n            displayUserChoice: users.length > 1,\n        });\n        this.form = document.querySelector(\"form.oe_login_form\");\n        this.form.classList.toggle(\"d-none\", users.length > 1);\n        this.form.querySelector(\":placeholder-shown\")?.focus();\n        useEffect(\n            (el) => el?.querySelector(\"button.list-group-item-action\")?.focus(),\n            () => [this.root.el]\n        );\n    }\n\n    toggleFormDisplay() {\n        this.state.displayUserChoice = !this.state.displayUserChoice && this.state.users.length;\n        this.form.classList.toggle(\"d-none\", this.state.displayUserChoice);\n        this.form.querySelector(\":placeholder-shown\")?.focus();\n    }\n\n    getAvatarUrl({ partnerId, partnerWriteDate: unique }) {\n        return imageUrl(\"res.partner\", partnerId, \"avatar_128\", { unique });\n    }\n\n    remove(deletedUser) {\n        this.state.users = this.state.users.filter((user) => user !== deletedUser);\n        setLastConnectedUsers(this.state.users);\n        if (!this.state.users.length) {\n            this.fillForm();\n        }\n    }\n\n    fillForm(login = \"\") {\n        this.form.querySelector(\"input#login\").value = login;\n        this.form.querySelector(\"input#password\").value = \"\";\n        this.toggleFormDisplay();\n    }\n}\n\nregistry.category(\"public_components\").add(\"web.user_switch\", UserSwitch);\n", "import { shallowEqual as _shallowEqual } from \"./objects\";\n\n/**\n * @template T\n * @template {string | number | symbol} K\n * @typedef {keyof T | ((item: T) => K)} Criterion\n */\n\n/**\n * Same values returned as those returned by cartesian function for case n = 0\n * and n > 1. For n = 1, brackets are put around the unique parameter elements.\n *\n * @template T\n * @param {...T[]} args\n * @returns {T[][]}\n */\nfunction _cartesian(...args) {\n    if (args.length === 0) {\n        return [undefined];\n    }\n    const firstArray = args.shift().map((elem) => [elem]);\n    if (args.length === 0) {\n        return firstArray;\n    }\n    const result = [];\n    const productOfOtherArrays = _cartesian(...args);\n    for (const array of firstArray) {\n        for (const tuple of productOfOtherArrays) {\n            result.push([...array, ...tuple]);\n        }\n    }\n    return result;\n}\n\n/**\n * Helper function returning an extraction handler to use on array elements to\n * return a certain attribute or mutated form of the element.\n *\n * @private\n * @template T\n * @template {string | number | symbol} K\n * @param {Criterion<T, K>} [criterion]\n * @returns {(element: T) => any}\n */\nfunction _getExtractorFrom(criterion) {\n    if (criterion) {\n        switch (typeof criterion) {\n            case \"string\":\n                return (element) => element[criterion];\n            case \"function\":\n                return criterion;\n            default:\n                throw new Error(\n                    `Expected criterion of type 'string' or 'function' and got '${typeof criterion}'`\n                );\n        }\n    } else {\n        return (element) => element;\n    }\n}\n\n/**\n * Returns an array containing either:\n * - the elements contained in the given iterable OR\n * - the given element if it is not an iterable\n *\n * @template T\n * @param {T | Iterable<T>} [value]\n * @returns {T[]}\n */\nexport function ensureArray(value) {\n    return isIterable(value) ? [...value] : [value];\n}\n\n/**\n * Returns the array of elements contained in both arrays.\n *\n * @template T\n * @param {Iterable<T>} iter1\n * @param {Iterable<T>} iter2\n * @returns {T[]}\n */\nexport function intersection(iter1, iter2) {\n    const set2 = new Set(iter2);\n    return unique(iter1).filter((v) => set2.has(v));\n}\n\n/**\n * Returns whether the given value is an iterable object (excluding strings).\n *\n * @param {unknown} value\n */\nexport function isIterable(value) {\n    return Boolean(value && typeof value === \"object\" && value[Symbol.iterator]);\n}\n\n/**\n * Returns an object holding different groups defined by a given criterion\n * or a default one. Each group is a subset of the original given list.\n * The given criterion can either be:\n * - a string: a property name on the list elements which value will be the\n * group name,\n * - a function: a handler that will return the group name from a given\n * element.\n *\n * @template T\n * @template {string | number | symbol} K\n * @param {Iterable<T>} iterable\n * @param {Criterion<T, K>} [criterion]\n * @returns {Record<K, T[]>}\n */\nexport function groupBy(iterable, criterion) {\n    const extract = _getExtractorFrom(criterion);\n    /** @type {Partial<Record<K, T[]>>} */\n    const groups = {};\n    for (const element of iterable) {\n        const group = String(extract(element));\n        if (!(group in groups)) {\n            groups[group] = [];\n        }\n        groups[group].push(element);\n    }\n    return groups;\n}\n\n/**\n * Return a shallow copy of a given array sorted by a given criterion or a default one.\n * The given criterion can either be:\n * - a string: a property name on the array elements returning the sortable primitive\n * - a function: a handler that will return the sortable primitive from a given element.\n * The default order is ascending ('asc'). It can be modified by setting the extra param 'order' to 'desc'.\n *\n * @template T\n * @template {string | number | symbol} K\n * @param {Iterable<T>} iterable\n * @param {Criterion<T, K>} [criterion]\n * @param {\"asc\" | \"desc\"} [order=\"asc\"]\n * @returns {T[]}\n */\nexport function sortBy(iterable, criterion, order = \"asc\") {\n    const extract = _getExtractorFrom(criterion);\n    return [...iterable].sort((elA, elB) => {\n        const a = extract(elA);\n        const b = extract(elB);\n        let result;\n        if (isNaN(a) && isNaN(b)) {\n            result = a > b ? 1 : a < b ? -1 : 0;\n        } else {\n            result = a - b;\n        }\n        return order === \"asc\" ? result : -result;\n    });\n}\n\n/**\n * Returns an array containing all the elements of arrayA\n * that are not in arrayB and vice-versa.\n *\n * @template T\n * @param {Iterable<T>} iter1\n * @param {Iterable<T>} iter2\n * @returns {T[]} an array containing all the elements of iter1\n * that are not in iter2 and vice-versa.\n */\nexport function symmetricalDifference(iter1, iter2) {\n    const array1 = [...iter1];\n    const array2 = [...iter2];\n    return [\n        ...array1.filter((value) => !array2.includes(value)),\n        ...array2.filter((value) => !array1.includes(value)),\n    ];\n}\n\n/**\n * Returns the product of any number n of arrays.\n * The internal structures of their elements is preserved.\n * For n = 1, no brackets are put around the unique parameter elements\n * For n = 0, [undefined] is returned since it is the unit\n * of the cartesian product (up to isomorphism).\n *\n * @template T\n * @param {...T[]} args\n * @returns {T[] | T[][]}\n */\nexport function cartesian(...args) {\n    if (args.length === 0) {\n        return [undefined];\n    } else if (args.length === 1) {\n        return args[0];\n    } else {\n        return _cartesian(...args);\n    }\n}\n\nexport const shallowEqual = _shallowEqual;\n\n/**\n * Returns all initial sections of a given array, e.g. for [1, 2] the array\n * [[], [1], [1, 2]] is returned.\n *\n * @template T\n * @param {Iterable<T>} iterable\n * @returns {T[][]}\n */\nexport function sections(iterable) {\n    const array = [...iterable];\n    const sections = [];\n    for (let i = 0; i < array.length + 1; i++) {\n        sections.push(array.slice(0, i));\n    }\n    return sections;\n}\n\n/**\n * Returns an array containing all elements of the given\n * array but without duplicates.\n *\n * @template T\n * @param {Iterable<T>} iterable\n * @returns {T[]}\n */\nexport function unique(iterable) {\n    return [...new Set(iterable)];\n}\n\n/**\n * @template T1, T2\n * @param {Iterable<T1>} iter1\n * @param {Iterable<T2>} iter2\n * @param {boolean} [fill=false]\n * @returns {[T1, T2][]}\n */\nexport function zip(iter1, iter2, fill = false) {\n    const array1 = [...iter1];\n    const array2 = [...iter2];\n    /** @type {[T1, T2][]} */\n    const result = [];\n    const getLength = fill ? Math.max : Math.min;\n    for (let i = 0; i < getLength(array1.length, array2.length); i++) {\n        result.push([array1[i], array2[i]]);\n    }\n    return result;\n}\n\n/**\n * @template T1, T2, T\n * @param {Iterable<T1>} iter1\n * @param {Iterable<T2>} iter2\n * @param {(e1: T1, e2: T2) => T} mapFn\n * @returns {T[]}\n */\nexport function zipWith(iter1, iter2, mapFn) {\n    return zip(iter1, iter2).map(([e1, e2]) => mapFn(e1, e2));\n}\n/**\n * Creates an sliding window over an array of a given width. Eg:\n * slidingWindow([1, 2, 3, 4], 2) => [[1, 2], [2, 3], [3, 4]]\n *\n * @template T\n * @param {T[]} arr the array over which to create a sliding window\n * @param {number} width the width of the window\n * @returns {T[][]} an array of tuples of size width\n */\nexport function slidingWindow(arr, width) {\n    const res = [];\n    for (let i = 0; i <= arr.length - width; i++) {\n        res.push(arr.slice(i, i + width));\n    }\n    return res;\n}\n\nexport function rotate(i, arr, inc = 1) {\n    return (arr.length + i + inc) % arr.length;\n}\n", "import { useEffect } from \"@odoo/owl\";\nimport { browser } from \"../browser/browser\";\n\n/**\n * This is used on text inputs or textareas to automatically resize it based on its\n * content each time it is updated. It takes the reference of the element as\n * parameter and some options. Do note that it may introduce mild performance issues\n * since it will force a reflow of the layout each time the element is updated.\n * Do also note that it only works with textareas that are nested as only child\n * of some parent div (like in the text_field component).\n *\n * @param {Ref} ref\n */\nexport function useAutoresize(ref, options = {}) {\n    let wasProgrammaticallyResized = false;\n    let resize = null;\n    useEffect(\n        (el) => {\n            if (el) {\n                resize = (programmaticResize = false) => {\n                    wasProgrammaticallyResized = programmaticResize;\n                    if (options.ignoreIfEmpty && !el.value) {\n                        return;\n                    }\n                    if (el instanceof HTMLInputElement) {\n                        resizeInput(el, options);\n                    } else {\n                        resizeTextArea(el, options);\n                    }\n                    options.onResize?.(el, options);\n                };\n                el.addEventListener(\"input\", () => resize(true));\n                const resizeObserver = new ResizeObserver(() => {\n                    // This ensures that the resize function is not called twice on input or page load\n                    if (wasProgrammaticallyResized) {\n                        wasProgrammaticallyResized = false;\n                        return;\n                    }\n                    resize();\n                });\n                resizeObserver.observe(el);\n                return () => {\n                    el.removeEventListener(\"input\", resize);\n                    resizeObserver.unobserve(el);\n                    resizeObserver.disconnect();\n                    resize = null;\n                };\n            }\n        },\n        () => [ref.el]\n    );\n    useEffect(() => {\n        if (resize) {\n            resize(true);\n        }\n    });\n}\n\n/**\n * @param {HTMLInputElement} input\n * @param {{ offset?: number }} [options]\n */\nfunction resizeInput(input, options) {\n    // This mesures the maximum width of the input which can get from the flex layout.\n    input.style.width = \"100%\";\n    const maxWidth = input.clientWidth;\n    // Somehow Safari 16 computes input sizes incorrectly. This is fixed in Safari 17\n    const isSafari16 = /Version\\/16.+Safari/i.test(browser.navigator.userAgent);\n    // Minimum width of the input\n    input.style.width = \"10px\";\n    if (input.value === \"\" && input.placeholder !== \"\") {\n        input.style.width = \"auto\";\n        return;\n    }\n    if (input.scrollWidth + 5 + (isSafari16 ? 8 : 0) > maxWidth) {\n        input.style.width = \"100%\";\n        return;\n    }\n    input.style.width = input.scrollWidth + 5 + (isSafari16 ? 8 : 0) + (options.offset || 0) + \"px\";\n}\n\n/**\n * @param {HTMLTextAreaElement} input\n * @param {{ minimumHeight?: number }} [options]\n */\nexport function resizeTextArea(textarea, options = {}) {\n    const minimumHeight = options.minimumHeight || 0;\n    let heightOffset = 0;\n    const style = window.getComputedStyle(textarea);\n    if (style.boxSizing === \"border-box\") {\n        const paddingHeight = parseFloat(style.paddingTop) + parseFloat(style.paddingBottom);\n        const borderHeight = parseFloat(style.borderTopWidth) + parseFloat(style.borderBottomWidth);\n        heightOffset = borderHeight + paddingHeight;\n    }\n    const previousStyle = {\n        borderTopWidth: style.borderTopWidth,\n        borderBottomWidth: style.borderBottomWidth,\n        padding: style.padding,\n    };\n    Object.assign(textarea.style, {\n        height: \"auto\",\n        borderTopWidth: 0,\n        borderBottomWidth: 0,\n        paddingTop: 0,\n        paddingBottom: 0,\n    });\n    textarea.style.height = \"auto\";\n    const height = Math.max(minimumHeight, textarea.scrollHeight + heightOffset);\n    Object.assign(textarea.style, previousStyle, { height: `${height}px` });\n    textarea.parentElement.style.height = `${height}px`;\n}\n", "import { _t } from \"@web/core/l10n/translation\";\n\n/**\n * @param {string} value\n * @returns {boolean}\n */\nexport function isBinarySize(value) {\n    return /^\\d+(\\.\\d*)? [^0-9]+$/.test(value);\n}\n\n/**\n * Get the length necessary for a base64 str to encode maxBytes\n * @param {number} maxBytes number of bytes we want to encode in base64\n * @returns {number} number of char\n */\nexport function toBase64Length(maxBytes) {\n    return Math.ceil(maxBytes * 4 / 3);\n}\n\n/**\n * @param {number} size number of bytes\n * @param {string}\n */\nexport function humanSize(size) {\n    const units = _t(\"Bytes|Kb|Mb|Gb|Tb|Pb|Eb|Zb|Yb\").split(\"|\");\n    let i = 0;\n    while (size >= 1024) {\n        size /= 1024;\n        ++i;\n    }\n    return `${size.toFixed(2)} ${units[i].trim()}`;\n}\n", "export class Cache {\n    constructor(getValue, getKey) {\n        this.cache = {};\n        this.getKey = getKey;\n        this.getValue = getValue;\n    }\n    _getCacheAndKey(...path) {\n        let cache = this.cache;\n        let key;\n        if (this.getKey) {\n            key = this.getKey(...path);\n        } else {\n            for (let i = 0; i < path.length - 1; i++) {\n                cache = cache[path[i]] = cache[path[i]] || {};\n            }\n            key = path[path.length - 1];\n        }\n        return { cache, key };\n    }\n    clear(...path) {\n        const { cache, key } = this._getCacheAndKey(...path);\n        delete cache[key];\n    }\n    invalidate() {\n        this.cache = {};\n    }\n    read(...path) {\n        const { cache, key } = this._getCacheAndKey(...path);\n        if (!(key in cache)) {\n            cache[key] = this.getValue(...path);\n        }\n        return cache[key];\n    }\n}\n", "/**\n * Adds the given classes to an element, whether the classes\n * are strings or objects.\n *\n * @param {HTMLElement} el\n * @param {String|Object|undefined} classes\n *\n * @example\n * addClassesToElement(el, \"hello\", { \"world\": 0 == 1, }...)\n */\nexport function addClassesToElement(el, ...classes) {\n    for (const classDefinition of classes) {\n        const classObj = toClassObj(classDefinition);\n        for (const className in classObj) {\n            if (classObj[className]) {\n                el.classList.add(className.trim());\n            }\n        }\n    }\n}\n\n/**\n * Merges two classes to a single class object, whether the\n * classes are strings or objects.\n *\n * @param {String|Object|undefined} classes\n * @returns {Object}\n *\n * @example\n * mergeClasses(\"hello\", { \"world\": 0 == 1, }...)\n */\nexport function mergeClasses(...classes) {\n    const classObj = {};\n    for (const classDefinition of classes) {\n        Object.assign(classObj, toClassObj(classDefinition));\n    }\n    return classObj;\n}\n\n/**\n * Returns an object from a class definition, whether it\n * is a string or an object.\n *\n * The returned object keys are css class names and the\n * values are expressions which represent if the class\n * should be added or not.\n *\n * @param {String|Object|undefined} classDefinition\n * @returns {Object}\n */\nfunction toClassObj(classDefinition) {\n    if (!classDefinition) {\n        return {};\n    } else if (typeof classDefinition === \"object\") {\n        return classDefinition;\n    } else if (typeof classDefinition === \"string\") {\n        const classObj = {};\n        classDefinition\n            .trim()\n            .split(/\\s+/)\n            .forEach((s) => {\n                classObj[s] = true;\n            });\n        return classObj;\n    } else {\n        console.warn(\n            `toClassObj only supports strings, objects and undefined className (got ${typeof classProp})`\n        );\n        return {};\n    }\n}\n", "/**\n * Adds opacity to the gradient\n *\n * @static\n * @param {string} gradient - css gradient string\n * @param {number} opacity - [0, 1] {float}\n * @returns {string} - gradient string with opacity\n */\nexport function applyOpacityToGradient(gradient, opacity = 100) {\n    if (opacity === 100) {\n        return gradient;\n    }\n    return gradient.replace(/rgb\\(([^)]+)\\)/g, `rgba($1, ${opacity / 100.0})`);\n}\n/**\n * Converts RGB color components to HSL components.\n *\n * @static\n * @param {integer} r - [0, 255]\n * @param {integer} g - [0, 255]\n * @param {integer} b - [0, 255]\n * @returns {Object|false}\n *          - hue [0, 360[ (float)\n *          - saturation [0, 100] (float)\n *          - lightness [0, 100] (float)\n */\nexport function convertRgbToHsl(r, g, b) {\n    if (\n        typeof r !== \"number\" ||\n        isNaN(r) ||\n        r < 0 ||\n        r > 255 ||\n        typeof g !== \"number\" ||\n        isNaN(g) ||\n        g < 0 ||\n        g > 255 ||\n        typeof b !== \"number\" ||\n        isNaN(b) ||\n        b < 0 ||\n        b > 255\n    ) {\n        return false;\n    }\n\n    var red = r / 255;\n    var green = g / 255;\n    var blue = b / 255;\n    var maxColor = Math.max(red, green, blue);\n    var minColor = Math.min(red, green, blue);\n    var delta = maxColor - minColor;\n    var hue = 0;\n    var saturation = 0;\n    var lightness = (maxColor + minColor) / 2;\n    if (delta) {\n        if (maxColor === red) {\n            hue = (green - blue) / delta;\n        }\n        if (maxColor === green) {\n            hue = 2 + (blue - red) / delta;\n        }\n        if (maxColor === blue) {\n            hue = 4 + (red - green) / delta;\n        }\n        if (maxColor) {\n            saturation = delta / (1 - Math.abs(2 * lightness - 1));\n        }\n    }\n    hue = 60 * hue;\n    return {\n        hue: hue < 0 ? hue + 360 : hue,\n        saturation: saturation * 100,\n        lightness: lightness * 100,\n    };\n}\n/**\n * Converts HSL color components to RGB components.\n *\n * @static\n * @param {number} h - [0, 360[ (float)\n * @param {number} s - [0, 100] (float)\n * @param {number} l - [0, 100] (float)\n * @returns {Object|false}\n *          - red [0, 255] (integer)\n *          - green [0, 255] (integer)\n *          - blue [0, 255] (integer)\n */\nexport function convertHslToRgb(h, s, l) {\n    if (\n        typeof h !== \"number\" ||\n        isNaN(h) ||\n        h < 0 ||\n        h > 360 ||\n        typeof s !== \"number\" ||\n        isNaN(s) ||\n        s < 0 ||\n        s > 100 ||\n        typeof l !== \"number\" ||\n        isNaN(l) ||\n        l < 0 ||\n        l > 100\n    ) {\n        return false;\n    }\n\n    var huePrime = h / 60;\n    var saturation = s / 100;\n    var lightness = l / 100;\n    var chroma = saturation * (1 - Math.abs(2 * lightness - 1));\n    var secondComponent = chroma * (1 - Math.abs((huePrime % 2) - 1));\n    var lightnessAdjustment = lightness - chroma / 2;\n    var precision = 255;\n    chroma = Math.round((chroma + lightnessAdjustment) * precision);\n    secondComponent = Math.round((secondComponent + lightnessAdjustment) * precision);\n    lightnessAdjustment = Math.round(lightnessAdjustment * precision);\n    if (huePrime >= 0 && huePrime < 1) {\n        return {\n            red: chroma,\n            green: secondComponent,\n            blue: lightnessAdjustment,\n        };\n    }\n    if (huePrime >= 1 && huePrime < 2) {\n        return {\n            red: secondComponent,\n            green: chroma,\n            blue: lightnessAdjustment,\n        };\n    }\n    if (huePrime >= 2 && huePrime < 3) {\n        return {\n            red: lightnessAdjustment,\n            green: chroma,\n            blue: secondComponent,\n        };\n    }\n    if (huePrime >= 3 && huePrime < 4) {\n        return {\n            red: lightnessAdjustment,\n            green: secondComponent,\n            blue: chroma,\n        };\n    }\n    if (huePrime >= 4 && huePrime < 5) {\n        return {\n            red: secondComponent,\n            green: lightnessAdjustment,\n            blue: chroma,\n        };\n    }\n    if (huePrime >= 5 && huePrime <= 6) {\n        return {\n            red: chroma,\n            green: lightnessAdjustment,\n            blue: secondComponent,\n        };\n    }\n    return false;\n}\n/**\n * Converts RGBA color components to a normalized CSS color: if the opacity\n * is invalid or equal to 100, a hex color excluding opacity is returned;\n * otherwise a hex color including opacity component is returned.\n *\n * @static\n * @param {integer} r - [0, 255]\n * @param {integer} g - [0, 255]\n * @param {integer} b - [0, 255]\n * @param {float} a - [0, 100]\n * @returns {string}\n */\nexport function convertRgbaToCSSColor(r, g, b, a) {\n    if (\n        typeof r !== \"number\" ||\n        isNaN(r) ||\n        r < 0 ||\n        r > 255 ||\n        typeof g !== \"number\" ||\n        isNaN(g) ||\n        g < 0 ||\n        g > 255 ||\n        typeof b !== \"number\" ||\n        isNaN(b) ||\n        b < 0 ||\n        b > 255\n    ) {\n        return false;\n    }\n    const rr = r < 16 ? \"0\" + r.toString(16) : r.toString(16);\n    const gg = g < 16 ? \"0\" + g.toString(16) : g.toString(16);\n    const bb = b < 16 ? \"0\" + b.toString(16) : b.toString(16);\n    if (\n        typeof a !== \"number\" ||\n        isNaN(a) ||\n        a < 0 ||\n        a > 100 ||\n        Math.abs(a - 100) < Number.EPSILON\n    ) {\n        return `#${rr}${gg}${bb}`.toUpperCase();\n    }\n    const alpha = Math.round((a / 100) * 255);\n    const aa = alpha < 16 ? \"0\" + alpha.toString(16) : alpha.toString(16);\n    return `#${rr}${gg}${bb}${aa}`.toUpperCase();\n}\n/**\n * Converts a CSS color (rgb(), rgba(), hexadecimal) to RGBA color components.\n *\n * Note: we don't support using and displaying hexadecimal color with opacity\n * but this method allows to receive one and returns the correct opacity value.\n *\n * @static\n * @param {string} cssColor - hexadecimal code or rgb() or rgba() or color()\n * @returns {Object|false}\n *          - red [0, 255] (integer)\n *          - green [0, 255] (integer)\n *          - blue [0, 255] (integer)\n *          - opacity [0, 100.0] (float)\n */\nexport function convertCSSColorToRgba(cssColor = \"\") {\n    // Check if cssColor is a rgba() or rgb() color\n    const rgba = cssColor.match(/^rgba?\\((\\d+),\\s*(\\d+),\\s*(\\d+)(?:,\\s*(\\d*(?:\\.\\d+)?))?\\)$/);\n    if (rgba) {\n        if (rgba[4] === undefined) {\n            rgba[4] = 1;\n        }\n        return {\n            red: parseInt(rgba[1]),\n            green: parseInt(rgba[2]),\n            blue: parseInt(rgba[3]),\n            opacity: Math.round(parseFloat(rgba[4]) * 100),\n        };\n    }\n\n    // Otherwise, check if cssColor is an hexadecimal code color\n    // first check if it's in its compact form (e.g. #FFF)\n    if (/^#([0-9a-f]{3})$/i.test(cssColor)) {\n        return {\n            red: parseInt(cssColor[1] + cssColor[1], 16),\n            green: parseInt(cssColor[2] + cssColor[2], 16),\n            blue: parseInt(cssColor[3] + cssColor[3], 16),\n            opacity: 100,\n        };\n    }\n\n    if (/^#([0-9A-F]{6}|[0-9A-F]{8})$/i.test(cssColor)) {\n        return {\n            red: parseInt(cssColor.substr(1, 2), 16),\n            green: parseInt(cssColor.substr(3, 2), 16),\n            blue: parseInt(cssColor.substr(5, 2), 16),\n            opacity: (cssColor.length === 9 ? parseInt(cssColor.substr(7, 2), 16) / 255 : 1) * 100,\n        };\n    }\n\n    // TODO maybe implement a support for receiving css color like 'red' or\n    // 'transparent' (which are now considered non-css color by isCSSColor...)\n    // Note: however, if ever implemented be careful of 'white'/'black' which\n    // actually are color names for our color system...\n\n    // Check if cssColor is a color() functional notation allowing colorspace\n    // with implicit sRGB.\n    // \"<color()>\" allows to define a color specification in a formalized\n    // manner. It starts with the \"color(\" keyword, specifies color space\n    // parameters, and optionally includes an alpha value for transparency.\n    if (/color\\(.+\\)/.test(cssColor)) {\n        const canvasEl = document.createElement(\"canvas\");\n        canvasEl.height = 1;\n        canvasEl.width = 1;\n        const ctx = canvasEl.getContext(\"2d\");\n        ctx.fillStyle = cssColor;\n        ctx.fillRect(0, 0, 1, 1);\n        const data = ctx.getImageData(0, 0, 1, 1).data;\n        return {\n            red: data[0],\n            green: data[1],\n            blue: data[2],\n            opacity: data[3] / 2.55, // Convert 0-255 to percentage\n        };\n    }\n    return false;\n}\n/**\n * Converts a CSS color (rgb(), rgba(), hexadecimal) to a normalized version\n * of the same color (@see convertRgbaToCSSColor).\n *\n * Normalized color can be safely compared using string comparison.\n *\n * @static\n * @param {string} cssColor - hexadecimal code or rgb() or rgba()\n * @returns {string} - the normalized css color or the given css color if it\n *                     failed to be normalized\n */\nexport function normalizeCSSColor(cssColor) {\n    const rgba = convertCSSColorToRgba(cssColor);\n    if (!rgba) {\n        return cssColor;\n    }\n    return convertRgbaToCSSColor(rgba.red, rgba.green, rgba.blue, rgba.opacity);\n}\n/**\n * Checks if a given string is a css color.\n *\n * @static\n * @param {string} cssColor\n * @returns {boolean}\n */\nexport function isCSSColor(cssColor) {\n    return convertCSSColorToRgba(cssColor) !== false;\n}\n/**\n * Mixes two colors by applying a weighted average of their red, green and blue\n * components.\n *\n * @static\n * @param {string} cssColor1 - hexadecimal code or rgb() or rgba()\n * @param {string} cssColor2 - hexadecimal code or rgb() or rgba()\n * @param {number} weight - a number between 0 and 1\n * @returns {string} - mixed color in hexadecimal format\n */\nexport function mixCssColors(cssColor1, cssColor2, weight) {\n    const rgba1 = convertCSSColorToRgba(cssColor1);\n    const rgba2 = convertCSSColorToRgba(cssColor2);\n    const rgb1 = [rgba1.red, rgba1.green, rgba1.blue];\n    const rgb2 = [rgba2.red, rgba2.green, rgba2.blue];\n    const [r, g, b] = rgb1.map((_, idx) =>\n        Math.round(rgb2[idx] + (rgb1[idx] - rgb2[idx]) * weight)\n    );\n    return convertRgbaToCSSColor(r, g, b);\n}\n\n/**\n * @param {string} [value]\n * @returns {boolean}\n */\nexport function isColorGradient(value) {\n    return value && value.includes(\"-gradient(\");\n}\n\n/**\n * @param {string} gradient\n * @returns {string} standardized gradient\n */\nexport function standardizeGradient(gradient) {\n    if (isColorGradient(gradient)) {\n        const el = document.createElement(\"div\");\n        el.style.setProperty(\"background-image\", gradient);\n        gradient = el.style.getPropertyValue(\"background-image\");\n    }\n    return gradient;\n}\n\nexport const RGBA_REGEX = /[\\d.]{1,5}/g;\n\n/**\n * Takes a color (rgb, rgba or hex) and returns its hex representation. If the\n * color is given in rgba, the background color of the node whose color we're\n * converting is used in conjunction with the alpha to compute the resulting\n * color (using the formula: `alpha*color + (1 - alpha)*background` for each\n * channel).\n *\n * @param {string} rgb\n * @param {HTMLElement} [node]\n * @returns {string} hexadecimal color (#RRGGBB)\n */\nexport function rgbToHex(rgb = \"\", node = null) {\n    if (rgb.startsWith(\"#\")) {\n        return rgb;\n    } else if (rgb.startsWith(\"rgba\")) {\n        const values = rgb.match(RGBA_REGEX) || [];\n        const alpha = parseFloat(values.pop());\n        // Retrieve the background color.\n        let bgRgbValues = [];\n        if (node) {\n            let bgColor = getComputedStyle(node).backgroundColor;\n            if (bgColor.startsWith(\"rgba\")) {\n                // The background color is itself rgba so we need to compute\n                // the resulting color using the background color of its\n                // parent.\n                bgColor = rgbToHex(bgColor, node.parentElement);\n            }\n            if (bgColor && bgColor.startsWith(\"#\")) {\n                bgRgbValues = (bgColor.match(/[\\da-f]{2}/gi) || []).map((val) => parseInt(val, 16));\n            } else if (bgColor && bgColor.startsWith(\"rgb\")) {\n                bgRgbValues = (bgColor.match(RGBA_REGEX) || []).map((val) => parseInt(val));\n            }\n        }\n        bgRgbValues = bgRgbValues.length ? bgRgbValues : [255, 255, 255]; // Default to white.\n\n        return (\n            \"#\" +\n            values\n                .map((value, index) => {\n                    const converted = Math.floor(\n                        alpha * parseInt(value) + (1 - alpha) * bgRgbValues[index]\n                    );\n                    const hex = parseInt(converted).toString(16);\n                    return hex.length === 1 ? \"0\" + hex : hex;\n                })\n                .join(\"\")\n        );\n    } else {\n        return (\n            \"#\" +\n            (rgb.match(/\\d{1,3}/g) || [])\n                .map((x) => {\n                    x = parseInt(x).toString(16);\n                    return x.length === 1 ? \"0\" + x : x;\n                })\n                .join(\"\")\n        );\n    }\n}\n\n/**\n * Converts an RGBA or RGB color string to a hexadecimal color string.\n * - If the input color is already in hex format, it returns the hex string directly.\n * - If the input color is in rgba format, it converts it to a hex string, including the alpha value.\n * - If the input color is in rgb format, it converts it to a hex string (with no alpha).\n *\n * @param {string} rgba - The color string to convert (can be in RGBA, RGB, or hex format).\n * @returns {string} - The resulting color in hex format (including alpha if applicable).\n */\nexport function rgbaToHex(rgba = \"\") {\n    if (rgba.startsWith(\"#\")) {\n        return rgba;\n    } else if (rgba.startsWith(\"rgba\")) {\n        const values = rgba.match(RGBA_REGEX) || [];\n        return convertRgbaToCSSColor(\n            parseInt(values[0]),\n            parseInt(values[1]),\n            parseInt(values[2]),\n            parseFloat(values[3]) * 100\n        );\n    } else {\n        return rgbToHex(rgba);\n    }\n}\n\n/**\n * Blends an RGBA color with the background color of a given DOM node.\n * - If the input color is not RGBA, it is converted to hex.\n * - If the node has an RGBA background, the function recursively blends it with its parent's background.\n * - If no valid background is found, it defaults to white (#FFFFFF).\n *\n * @param {string} color - The RGBA color to blend.\n * @param {HTMLElement|null} node - The DOM node to get the background color from.\n * @returns {string} - The resulting blended color as a hex string.\n */\nexport function blendColors(color, node) {\n    if (!color.startsWith(\"rgba\")) {\n        return rgbaToHex(color);\n    }\n    let bgRgbValues = [255, 255, 255];\n    if (node) {\n        let bgColor = getComputedStyle(node).backgroundColor;\n\n        if (bgColor.startsWith(\"rgba\")) {\n            // The background color is itself rgba so we need to compute\n            // the resulting color using the background color of its\n            // parent.\n            bgColor = blendColors(bgColor, node.parentElement);\n        }\n        if (bgColor.startsWith(\"#\")) {\n            bgRgbValues = (bgColor.match(/[\\da-f]{2}/gi) || []).map((val) => parseInt(val, 16));\n        } else if (bgColor.startsWith(\"rgb\")) {\n            bgRgbValues = (bgColor.match(/[\\d.]{1,5}/g) || []).map((val) => parseInt(val));\n        }\n    }\n\n    const values = color.match(/[\\d.]{1,5}/g) || [];\n    const alpha = values.length === 4 ? parseFloat(values.pop()) : 1;\n\n    return (\n        \"#\" +\n        values\n            .map((value, index) => {\n                const converted = Math.round(\n                    alpha * parseInt(value) + (1 - alpha) * bgRgbValues[index]\n                );\n                const hex = parseInt(converted).toString(16);\n                return hex.length === 1 ? \"0\" + hex : hex;\n            })\n            .join(\"\")\n    );\n}\n", "import { Component, onError, xml } from \"@odoo/owl\";\n\nexport class ErrorHandler extends Component {\n    static template = xml`<t t-slot=\"default\" />`;\n    static props = [\"onError\", \"slots\"];\n    setup() {\n        onError((error) => {\n            this.props.onError(error);\n        });\n    }\n}\n", "/**\n * Returns a promise resolved after 'wait' milliseconds\n *\n * @param {int} [wait=0] the delay in ms\n * @return {Promise}\n */\nexport function delay(wait) {\n    return new Promise(function (resolve) {\n        setTimeout(resolve, wait);\n    });\n}\n\n/**\n * KeepLast is a concurrency primitive that manages a list of tasks, and only\n * keeps the last task active.\n *\n * @template T\n */\nexport class KeepLast {\n    constructor() {\n        this._id = 0;\n    }\n    /**\n     * Register a new task\n     *\n     * @param {Promise<T>} promise\n     * @returns {Promise<T>}\n     */\n    add(promise) {\n        this._id++;\n        const currentId = this._id;\n        return new Promise((resolve, reject) => {\n            promise\n                .then((value) => {\n                    if (this._id === currentId) {\n                        resolve(value);\n                    }\n                })\n                .catch((reason) => {\n                    // not sure about this part\n                    if (this._id === currentId) {\n                        reject(reason);\n                    }\n                });\n        });\n    }\n}\n\n/**\n * A (Odoo) mutex is a primitive for serializing computations.  This is\n * useful to avoid a situation where two computations modify some shared\n * state and cause some corrupted state.\n *\n * Imagine that we have a function to fetch some data _load(), which returns\n * a promise which resolves to something useful. Now, we have some code\n * looking like this::\n *\n *      return this._load().then(function (result) {\n *          this.state = result;\n *      });\n *\n * If this code is run twice, but the second execution ends before the\n * first, then the final state will be the result of the first call to\n * _load.  However, if we have a mutex::\n *\n *      this.mutex = new Mutex();\n *\n * and if we wrap the calls to _load in a mutex::\n *\n *      return this.mutex.exec(function() {\n *          return this._load().then(function (result) {\n *              this.state = result;\n *          });\n *      });\n *\n * Then, it is guaranteed that the final state will be the result of the\n * second execution.\n *\n * A Mutex has to be a class, and not a function, because we have to keep\n * track of some internal state.\n */\nexport class Mutex {\n    constructor() {\n        this._lock = Promise.resolve();\n        this._queueSize = 0;\n        this._unlockedProm = undefined;\n        this._unlock = undefined;\n    }\n    /**\n     * Add a computation to the queue, it will be executed as soon as the\n     * previous computations are completed.\n     *\n     * @param {() => (void | Promise<void>)} action a function which may return a Promise\n     * @returns {Promise<void>}\n     */\n    async exec(action) {\n        this._queueSize++;\n        if (!this._unlockedProm) {\n            this._unlockedProm = new Promise((resolve) => {\n                this._unlock = () => {\n                    resolve();\n                    this._unlockedProm = undefined;\n                };\n            });\n        }\n        const always = () => {\n            return Promise.resolve(action()).finally(() => {\n                if (--this._queueSize === 0) {\n                    this._unlock();\n                }\n            });\n        };\n        this._lock = this._lock.then(always, always);\n        return this._lock;\n    }\n    /**\n     * @returns {Promise<void>} resolved as soon as the Mutex is unlocked\n     *   (directly if it is currently idle)\n     */\n    getUnlockedDef() {\n        return this._unlockedProm || Promise.resolve();\n    }\n}\n\n/**\n * Race is a class designed to manage concurrency problems inspired by\n * Promise.race(), except that it is dynamic in the sense that promises can be\n * added anytime to a Race instance. When a promise is added, it returns another\n * promise which resolves as soon as a promise, among all added promises, is\n * resolved. The race is thus over. From that point, a new race will begin the\n * next time a promise will be added.\n *\n * @template T\n */\nexport class Race {\n    constructor() {\n        this.currentProm = null;\n        this.currentPromResolver = null;\n        this.currentPromRejecter = null;\n    }\n    /**\n     * Register a new promise. If there is an ongoing race, the promise is added\n     * to that race. Otherwise, it starts a new race. The returned promise\n     * resolves as soon as the race is over, with the value of the first resolved\n     * promise added to the race.\n     *\n     * @param {Promise<T>} promise\n     * @returns {Promise<T>}\n     */\n    add(promise) {\n        if (!this.currentProm) {\n            this.currentProm = new Promise((resolve, reject) => {\n                this.currentPromResolver = (value) => {\n                    this.currentProm = null;\n                    this.currentPromResolver = null;\n                    this.currentPromRejecter = null;\n                    resolve(value);\n                };\n                this.currentPromRejecter = (error) => {\n                    this.currentProm = null;\n                    this.currentPromResolver = null;\n                    this.currentPromRejecter = null;\n                    reject(error);\n                };\n            });\n        }\n        promise.then(this.currentPromResolver).catch(this.currentPromRejecter);\n        return this.currentProm;\n    }\n    /**\n     * @returns {Promise<T>|null} promise resolved as soon as the race is over, or\n     *   null if there is no race ongoing)\n     */\n    getCurrentProm() {\n        return this.currentProm;\n    }\n}\n\n/**\n * Deferred is basically a resolvable/rejectable extension of Promise.\n */\nexport class Deferred extends Promise {\n    constructor() {\n        let resolve;\n        let reject;\n        const prom = new Promise((res, rej) => {\n            resolve = res;\n            reject = rej;\n        });\n        return Object.assign(prom, { resolve, reject });\n    }\n}\n", "import { makeDraggableHook } from \"@web/core/utils/draggable_hook_builder_owl\";\nimport { pick } from \"@web/core/utils/objects\";\n\n/** @typedef {import(\"@web/core/utils/draggable_hook_builder\").DraggableHandlerParams} DraggableHandlerParams */\n\n/**\n * @typedef DraggableParams\n *\n * MANDATORY\n *\n * @property {{ el: HTMLElement | null }} ref\n * @property {string} elements defines draggable elements\n *\n * OPTIONAL\n *\n * @property {boolean | () => boolean} [enable] whether the draggable system should\n *  be enabled.\n * @property {string | () => string} [handle] additional selector for when the dragging\n *  sequence must be initiated when dragging on a certain part of the element.\n * @property {string | () => string} [ignore] selector targetting elements that must\n *  initiate a drag.\n * @property {string | () => string} [cursor] cursor style during the dragging sequence.\n *\n * HANDLERS (also optional)\n *\n * @property {(params: DraggableHandlerParams) => any} [onDragStart]\n *  called when a dragging sequence is initiated.\n * @property {(params: DraggableHandlerParams) => any} [onDrag]\n *  called on each \"mousemove\" during the drag sequence.\n * @property {(params: DraggableHandlerParams) => any} [onDragEnd]\n *  called when the dragging sequence ends, regardless of the reason.\n * @property {(params: DraggableHandlerParams) => any} [onDrop] called when the dragging sequence\n *  ends on a mouseup action.\n */\n\n/**\n * @typedef DraggableState\n * @property {boolean} dragging\n */\n\n/** @type {(params: DraggableParams) => DraggableState} */\nexport const useDraggable = makeDraggableHook({\n    name: \"useDraggable\",\n    onWillStartDrag: ({ ctx }) => pick(ctx.current, \"element\"),\n    onDragStart: ({ ctx }) => pick(ctx.current, \"element\"),\n    onDrag: ({ ctx }) => pick(ctx.current, \"element\"),\n    onDragEnd: ({ ctx }) => pick(ctx.current, \"element\"),\n    onDrop: ({ ctx }) => pick(ctx.current, \"element\"),\n});\n", "import { clamp } from \"@web/core/utils/numbers\";\nimport { omit } from \"@web/core/utils/objects\";\nimport { closestScrollableX, closestScrollableY } from \"@web/core/utils/scrolling\";\nimport { setRecurringAnimationFrame } from \"@web/core/utils/timing\";\nimport { browser } from \"../browser/browser\";\nimport { hasTouch, isBrowserFirefox, isIOS } from \"../browser/feature_detection\";\n\n/**\n * @typedef {ReturnType<typeof makeCleanupManager>} CleanupManager\n *\n * @typedef {ReturnType<typeof makeDOMHelpers>} DOMHelpers\n *\n * @typedef DraggableBuilderParams\n * Hook params\n * @property {string} [name=\"useAnonymousDraggable\"]\n * @property {EdgeScrollingOptions} [edgeScrolling]\n * @property {Record<string, string[]>} [acceptedParams]\n * @property {Record<string, any>} [defaultParams]\n * Setup hooks\n * @property {{\n *  addListener: typeof import(\"@odoo/owl\")[\"useExternalListener\"];\n *  setup: typeof import(\"@odoo/owl\")[\"useEffect\"];\n *  teardown: typeof import(\"@odoo/owl\")[\"onWillUnmount\"];\n *  throttle: typeof import(\"./timing\")[\"useThrottleForAnimation\"];\n *  wrapState: typeof import(\"@odoo/owl\")[\"reactive\"];\n * }} setupHooks\n * Build hooks\n * @property {(params: DraggableBuildHandlerParams) => any} onComputeParams\n * Runtime hooks\n * @property {(params: DraggableBuildHandlerParams) => any} onDragStart\n * @property {(params: DraggableBuildHandlerParams) => any} onDrag\n * @property {(params: DraggableBuildHandlerParams) => any} onDragEnd\n * @property {(params: DraggableBuildHandlerParams) => any} onDrop\n * @property {(params: DraggableBuildHandlerParams) => any} onWillStartDrag\n *\n * @typedef DraggableHookContext\n * @property {{ el: HTMLElement | null }} ref\n * @property {string | null} [elementSelector=null]\n * @property {string | null} [ignoreSelector=null]\n * @property {string | null} [fullSelector=null]\n * @property {boolean} [followCursor=true]\n * @property {string | null} [cursor=null]\n * @property {() => boolean} [enable=() => false]\n * @property {(HTMLElement) => boolean} [preventDrag=(el) => false]\n * @property {Position} [pointer={ x: 0, y: 0 }]\n * @property {EdgeScrollingOptions} [edgeScrolling]\n * @property {number} [delay]\n * @property {number} [tolerance]\n * @property {DraggableHookCurrentContext} current\n *\n * @typedef DraggableHookCurrentContext\n * @property {HTMLElement} [current.container]\n * @property {DOMRect} [current.containerRect]\n * @property {HTMLElement} [current.element]\n * @property {DOMRect} [current.elementRect]\n * @property {HTMLElement | null} [current.scrollParentX]\n * @property {DOMRect | null} [current.scrollParentXRect]\n * @property {HTMLElement | null} [current.scrollParentY]\n * @property {DOMRect | null} [current.scrollParentYRect]\n * @property {\"left\"|\"right\"|\"top\"|\"bottom\"|null} [scrollingEdge]\n * @property {number} [timeout]\n * @property {Position} [initialPosition]\n * @property {Position} [offset={ x: 0, y: 0 }]\n *\n * @typedef EdgeScrollingOptions\n * @property {boolean} [enabled=true]\n * @property {number} [speed=10]\n * @property {number} [threshold=20]\n * @property {\"horizontal\"|\"vertical\"} [direction]\n *\n * @typedef Position\n * @property {number} x\n * @property {number} y\n *\n * @typedef {DOMHelpers & {\n *  ctx: DraggableHookContext,\n *  addCleanup(cleanupFn: () => any): void,\n *  addEffectCleanup(cleanupFn: () => any): void,\n *  callHandler(handlerName: string, arg: Record<any, any>): void,\n * }} DraggableBuildHandlerParams\n *\n * @typedef {DOMHelpers & Position & { element: HTMLElement }} DraggableHandlerParams\n */\n\nconst DRAGGABLE_CLASS = \"o_draggable\";\nexport const DRAGGED_CLASS = \"o_dragged\";\n\nconst DEFAULT_ACCEPTED_PARAMS = {\n    allowDisconnected: [Boolean], // do not use, introduced for stable versions, to challenge in master\n    enable: [Boolean, Function],\n    preventDrag: [Function],\n    ref: [Object],\n    elements: [String],\n    handle: [String, Function],\n    ignore: [String, Function],\n    cursor: [String],\n    edgeScrolling: [Object, Function],\n    delay: [Number],\n    tolerance: [Number],\n    touchDelay: [Number],\n    iframeWindow: [Object, Function],\n};\nconst DEFAULT_DEFAULT_PARAMS = {\n    allowDisconnected: false,\n    elements: `.${DRAGGABLE_CLASS}`,\n    enable: true,\n    preventDrag: () => false,\n    edgeScrolling: {\n        speed: 10,\n        threshold: 30,\n    },\n    delay: 0,\n    tolerance: 10,\n    touchDelay: 300,\n};\nconst LEFT_CLICK = 0;\nconst MANDATORY_PARAMS = [\"ref\"];\nconst WHITE_LISTED_KEYS = [\"Alt\", \"Control\", \"Meta\", \"Shift\"];\n\n/**\n * Cache containing the elements in which an attribute has been modified by a hook.\n * It is global since multiple draggable hooks can interact with the same elements.\n * @type {Record<string, Set<HTMLElement>>}\n */\nconst elCache = {};\n\n/**\n * Transforms a camelCased string to return its kebab-cased version.\n * Typically used to generate CSS properties from JS objects.\n *\n * @param {string} str\n * @returns {string}\n */\nfunction camelToKebab(str) {\n    return str.replace(/([a-z])([A-Z])/g, \"$1-$2\").toLowerCase();\n}\n\n/**\n * @template T\n * @param {T | () => T} valueOrFn\n * @returns {T}\n */\nfunction getReturnValue(valueOrFn) {\n    if (typeof valueOrFn === \"function\") {\n        return valueOrFn();\n    }\n    return valueOrFn;\n}\n\n/**\n * Returns the first scrollable parent of the given element (recursively), or null\n * if none is found. A 'scrollable' element is defined by 2 things:\n *\n * - for either in width or in height: the 'scroll' value is larger than the 'client'\n * value;\n *\n * - its computed 'overflow' property is set to either \"auto\" or \"scroll\"\n *\n * If both of these assertions are true, it means that the element can effectively\n * be scrolled on at least one axis.\n * @param {HTMLElement} el\n * @returns {(HTMLElement | null)[]}\n */\nfunction getScrollParents(el) {\n    return [closestScrollableX(el), closestScrollableY(el)];\n}\n\n/**\n * @param {() => any} [defaultCleanupFn]\n */\nfunction makeCleanupManager(defaultCleanupFn) {\n    /**\n     * Registers the given cleanup function to be called when cleaning up hooks.\n     * @param {() => any} [cleanupFn]\n     */\n    const add = (cleanupFn) => typeof cleanupFn === \"function\" && cleanups.push(cleanupFn);\n\n    /**\n     * Runs all cleanup functions while clearing the cleanups list.\n     */\n    const cleanup = () => {\n        while (cleanups.length) {\n            cleanups.pop()();\n        }\n        add(defaultCleanupFn);\n    };\n\n    const cleanups = [];\n\n    add(defaultCleanupFn);\n\n    return { add, cleanup };\n}\n\n/**\n * @param {CleanupManager} cleanup\n */\nfunction makeDOMHelpers(cleanup) {\n    /**\n     * @param {HTMLElement} el\n     * @param  {...string} classNames\n     */\n    const addClass = (el, ...classNames) => {\n        if (!el || !classNames.length) {\n            return;\n        }\n        cleanup.add(() => el.classList.remove(...classNames));\n        el.classList.add(...classNames);\n    };\n\n    /**\n     * Adds an event listener to be cleaned up after the next drag sequence\n     * has stopped.\n     * @param {EventTarget} el\n     * @param {string} event\n     * @param {(...args: any[]) => any} callback\n     * @param {AddEventListenerOptions & { noAddedStyle?: boolean }} [options]\n     */\n    const addListener = (el, event, callback, options = {}) => {\n        if (!el || !event || !callback) {\n            return;\n        }\n        const { noAddedStyle } = options;\n        delete options.noAddedStyle;\n        el.addEventListener(event, callback, options);\n        if (!noAddedStyle && /mouse|pointer|touch/.test(event)) {\n            // Restore pointer events on elements listening on mouse/pointer/touch events.\n            addStyle(el, { pointerEvents: \"auto\" });\n        }\n        cleanup.add(() => el.removeEventListener(event, callback, options));\n    };\n\n    /**\n     * Adds style to an element to be cleaned up after the next drag sequence has\n     * stopped.\n     * @param {HTMLElement} el\n     * @param {Record<string, string | number>} style\n     */\n    const addStyle = (el, style) => {\n        if (!el || !style || !Object.keys(style).length) {\n            return;\n        }\n        cleanup.add(saveAttribute(el, \"style\"));\n        for (const key in style) {\n            const [value, priority] = String(style[key]).split(/\\s*!\\s*/);\n            el.style.setProperty(camelToKebab(key), value, priority);\n        }\n    };\n\n    /**\n     * Returns the bounding rect of the given element. If the `adjust` option is set\n     * to true, the rect will be reduced by the padding of the element.\n     * @param {HTMLElement} el\n     * @param {Object} [options={}]\n     * @param {boolean} [options.adjust=false]\n     * @returns {DOMRect}\n     */\n    const getRect = (el, options = {}) => {\n        if (!el) {\n            return {};\n        }\n        const rect = el.getBoundingClientRect();\n        if (options.adjust) {\n            const style = getComputedStyle(el);\n            const [pl, pr, pt, pb] = [\n                \"padding-left\",\n                \"padding-right\",\n                \"padding-top\",\n                \"padding-bottom\",\n            ].map((prop) => pixelValueToNumber(style.getPropertyValue(prop)));\n\n            rect.x += pl;\n            rect.y += pt;\n            rect.width -= pl + pr;\n            rect.height -= pt + pb;\n        }\n        return rect;\n    };\n\n    /**\n     * @param {HTMLElement} el\n     * @param {string} attribute\n     */\n    const removeAttribute = (el, attribute) => {\n        if (!el || !attribute) {\n            return;\n        }\n        cleanup.add(saveAttribute(el, attribute));\n        el.removeAttribute(attribute);\n    };\n\n    /**\n     * @param {HTMLElement} el\n     * @param {...string} classNames\n     */\n    const removeClass = (el, ...classNames) => {\n        if (!el || !classNames.length) {\n            return;\n        }\n        cleanup.add(saveAttribute(el, \"class\"));\n        el.classList.remove(...classNames);\n    };\n\n    /**\n     * Adds style to an element to be cleaned up after the next drag sequence has\n     * stopped.\n     * @param {HTMLElement} el\n     * @param {...string} properties\n     */\n    const removeStyle = (el, ...properties) => {\n        if (!el || !properties.length) {\n            return;\n        }\n        cleanup.add(saveAttribute(el, \"style\"));\n        for (const key of properties) {\n            el.style.removeProperty(camelToKebab(key));\n        }\n    };\n\n    /**\n     * @param {HTMLElement} el\n     * @param {string} attribute\n     * @param {any} value\n     */\n    const setAttribute = (el, attribute, value) => {\n        if (!el || !attribute) {\n            return;\n        }\n        cleanup.add(saveAttribute(el, attribute));\n        el.setAttribute(attribute, String(value));\n    };\n\n    return {\n        addClass,\n        addListener,\n        addStyle,\n        getRect,\n        removeAttribute,\n        removeClass,\n        removeStyle,\n        setAttribute,\n    };\n}\n\n/**\n * Converts a CSS pixel value to a number, removing the 'px' part.\n * @param {string} val\n * @returns {number}\n */\nfunction pixelValueToNumber(val) {\n    return Number(val.endsWith(\"px\") ? val.slice(0, -2) : val);\n}\n\n/**\n * @param {Event} ev\n * @param {{ stop?: boolean }} params\n */\nfunction safePrevent(ev, { stop } = {}) {\n    if (ev.cancelable) {\n        ev.preventDefault();\n        if (stop) {\n            ev.stopPropagation();\n        }\n    }\n}\n\nfunction saveAttribute(el, attribute) {\n    const restoreAttribute = () => {\n        cache.delete(el);\n        if (hasAttribute) {\n            el.setAttribute(attribute, originalValue);\n        } else {\n            el.removeAttribute(attribute);\n        }\n    };\n\n    if (!(attribute in elCache)) {\n        elCache[attribute] = new Set();\n    }\n    const cache = elCache[attribute];\n\n    if (cache.has(el)) {\n        return;\n    }\n\n    cache.add(el);\n    const hasAttribute = el.hasAttribute(attribute);\n    const originalValue = el.getAttribute(attribute);\n\n    return restoreAttribute;\n}\n\n/**\n * @template T\n * @param {T | () => T} value\n * @returns {() => T}\n */\nfunction toFunction(value) {\n    return typeof value === \"function\" ? value : () => value;\n}\n\n/**\n * @param {DraggableBuilderParams} hookParams\n * @returns {(params: Record<keyof typeof DEFAULT_ACCEPTED_PARAMS, any>) => { dragging: boolean }}\n */\nexport function makeDraggableHook(hookParams) {\n    hookParams = getReturnValue(hookParams);\n\n    const hookName = hookParams.name || \"useAnonymousDraggable\";\n    const { setupHooks } = hookParams;\n    const allAcceptedParams = { ...DEFAULT_ACCEPTED_PARAMS, ...hookParams.acceptedParams };\n    const defaultParams = { ...DEFAULT_DEFAULT_PARAMS, ...hookParams.defaultParams };\n\n    /**\n     * Computes the current params and converts the params definition\n     * @param {SortableParams} params\n     * @returns {[string, string | boolean][]}\n     */\n    const computeParams = (params) => {\n        const computedParams = { enable: () => true };\n        for (const prop in allAcceptedParams) {\n            if (prop in params) {\n                if (prop === \"enable\") {\n                    computedParams[prop] = toFunction(params[prop]);\n                } else if (\n                    allAcceptedParams[prop].length === 1 &&\n                    allAcceptedParams[prop][0] === Function\n                ) {\n                    computedParams[prop] = params[prop];\n                } else {\n                    computedParams[prop] = getReturnValue(params[prop]);\n                }\n            }\n        }\n        return Object.entries(computedParams);\n    };\n\n    /**\n     * Basic error builder for the hook.\n     * @param {string} reason\n     * @returns {Error}\n     */\n    const makeError = (reason) => new Error(`Error in hook ${hookName}: ${reason}.`);\n    let preventClick = false;\n\n    return {\n        [hookName](params) {\n            /**\n             * Executes a handler from the `hookParams`.\n             * @param {string} hookHandlerName\n             * @param {Record<any, any>} arg\n             */\n            const callBuildHandler = (hookHandlerName, arg) => {\n                if (typeof hookParams[hookHandlerName] !== \"function\") {\n                    return;\n                }\n                const returnValue = hookParams[hookHandlerName]({ ctx, ...helpers, ...arg });\n                if (returnValue) {\n                    callHandler(hookHandlerName, returnValue);\n                }\n            };\n\n            /**\n             * Safely executes a handler from the `params`, so that the drag sequence can\n             * be interrupted if an error occurs.\n             * @param {string} handlerName\n             * @param {Record<any, any>} arg\n             */\n            const callHandler = (handlerName, arg) => {\n                if (typeof params[handlerName] !== \"function\") {\n                    return;\n                }\n                try {\n                    params[handlerName]({ ...dom, ...ctx.pointer, ...arg });\n                } catch (err) {\n                    dragEnd(null, true);\n                    throw err;\n                }\n            };\n\n            /**\n             * Returns whether the user has moved from at least the number of pixels\n             * that are tolerated from the initial pointer position.\n             */\n            const canStartDrag = () => {\n                const {\n                    pointer,\n                    current: { initialPosition },\n                } = ctx;\n                return (\n                    !ctx.tolerance ||\n                    Math.hypot(pointer.x - initialPosition.x, pointer.y - initialPosition.y) >=\n                        ctx.tolerance\n                );\n            };\n\n            /**\n             * Main entry function to start a drag sequence.\n             */\n            const dragStart = () => {\n                state.dragging = true;\n                state.willDrag = false;\n\n                // Compute scrollable parent\n                const isDocumentScrollingElement =\n                    ctx.current.container === ctx.current.container.ownerDocument.scrollingElement;\n                // If the container is the \"ownerDocument.scrollingElement\",\n                // there is no need to get the scroll parent as it is the\n                // scrollable element itself.\n                // TODO: investigate if \"getScrollParents\" should not consider\n                // the \"ownerDocument.scrollingElement\" directly.\n                [ctx.current.scrollParentX, ctx.current.scrollParentY] = isDocumentScrollingElement\n                    ? [ctx.current.container, ctx.current.container]\n                    : getScrollParents(ctx.current.container);\n\n                updateRects();\n                const { x, y, width, height } = ctx.current.elementRect;\n\n                // Adjusts the offset\n                ctx.current.offset = {\n                    x: ctx.current.initialPosition.x - x,\n                    y: ctx.current.initialPosition.y - y,\n                };\n\n                if (ctx.followCursor) {\n                    dom.addStyle(ctx.current.element, {\n                        width: `${width}px`,\n                        height: `${height}px`,\n                        // Limit the impact of width and height !important on the dragged element\n                        \"max-width\": `${width}px`,\n                        \"max-height\": `${height}px`,\n                        position: \"fixed !important\",\n                    });\n\n                    // First adjustment\n                    updateElementPosition();\n                }\n\n                dom.addClass(document.body, \"pe-none\", \"user-select-none\");\n                if (params.iframeWindow) {\n                    for (const iframe of document.getElementsByTagName(\"iframe\")) {\n                        if (iframe.contentWindow === params.iframeWindow) {\n                            dom.addClass(iframe, \"pe-none\", \"user-select-none\");\n                        }\n                    }\n                }\n                // FIXME: adding pe-none and cursor on the same element makes\n                // no sense as pe-none prevents the cursor to be displayed.\n                if (ctx.cursor) {\n                    dom.addStyle(document.body, { cursor: ctx.cursor });\n                }\n\n                if (\n                    (ctx.current.scrollParentX || ctx.current.scrollParentY) &&\n                    ctx.edgeScrolling.enabled\n                ) {\n                    const cleanupFn = setRecurringAnimationFrame(handleEdgeScrolling);\n                    cleanup.add(cleanupFn);\n                }\n\n                dom.addClass(ctx.current.element, DRAGGED_CLASS);\n\n                callBuildHandler(\"onDragStart\");\n            };\n\n            /**\n             * Main exit function to stop a drag sequence. Note that it can be called\n             * even if a drag sequence did not start yet to perform a cleanup of all\n             * current context variables.\n             * @param {HTMLElement | null} target\n             * @param {boolean} [inErrorState] can be set to true when an error\n             *  occurred to avoid falling into an infinite loop if the error\n             *  originated from one of the handlers.\n             */\n            const dragEnd = (target, inErrorState) => {\n                if (state.dragging) {\n                    preventClick = true;\n                    if (!inErrorState) {\n                        if (\n                            target &&\n                            (params.allowDisconnected || ctx.current.element.isConnected)\n                        ) {\n                            callBuildHandler(\"onDrop\", { target });\n                        }\n                        callBuildHandler(\"onDragEnd\");\n                    }\n                }\n\n                cleanup.cleanup();\n            };\n\n            /**\n             * Applies scroll to the container if the current element is near\n             * the edge of the container.\n             */\n            const handleEdgeScrolling = (deltaTime) => {\n                updateRects();\n                const { x: pointerX, y: pointerY } = ctx.pointer;\n                const xRect = ctx.current.scrollParentXRect;\n                const yRect = ctx.current.scrollParentYRect;\n\n                // \"getBoundingClientRect()\"\" (used in \"getRect()\") gives the\n                // distance from the element's top to the viewport, excluding\n                // scroll position. Only the \"document.scrollingElement\" element\n                // (\"<html>\") accounts for scrollTop.\n                const scrollParentYEl = ctx.current.scrollParentY;\n                if (scrollParentYEl === ctx.current.container.ownerDocument.scrollingElement) {\n                    yRect.y += scrollParentYEl.scrollTop;\n                }\n\n                const { direction, speed, threshold } = ctx.edgeScrolling;\n                const correctedSpeed = (speed / 16) * deltaTime;\n\n                const diff = {};\n                ctx.current.scrollingEdge = null;\n                if (xRect) {\n                    const maxWidth = xRect.x + xRect.width;\n                    if (pointerX - xRect.x < threshold) {\n                        diff.x = [pointerX - xRect.x, -1];\n                        ctx.current.scrollingEdge = \"left\";\n                    } else if (maxWidth - pointerX < threshold) {\n                        diff.x = [maxWidth - pointerX, 1];\n                        ctx.current.scrollingEdge = \"right\";\n                    }\n                }\n                if (yRect) {\n                    const maxHeight = yRect.y + yRect.height;\n                    if (pointerY - yRect.y < threshold) {\n                        diff.y = [pointerY - yRect.y, -1];\n                        ctx.current.scrollingEdge = \"top\";\n                    } else if (maxHeight - pointerY < threshold) {\n                        diff.y = [maxHeight - pointerY, 1];\n                        ctx.current.scrollingEdge = \"bottom\";\n                    }\n                }\n\n                const diffToScroll = ([delta, sign]) =>\n                    (1 - Math.max(delta, 0) / threshold) * correctedSpeed * sign;\n                if ((!direction || direction === \"vertical\") && diff.y) {\n                    ctx.current.scrollParentY.scrollBy({ top: diffToScroll(diff.y) });\n                }\n                if ((!direction || direction === \"horizontal\") && diff.x) {\n                    ctx.current.scrollParentX.scrollBy({ left: diffToScroll(diff.x) });\n                }\n                callBuildHandler(\"onDrag\");\n            };\n\n            /**\n             * Global (= ref) \"click\" event handler.\n             * Used to prevent click events after dragEnd\n             * @param {PointerEvent} ev\n             */\n            const onClick = (ev) => {\n                if (preventClick) {\n                    safePrevent(ev, { stop: true });\n                }\n            };\n\n            /**\n             * Window \"keydown\" event handler.\n             * @param {KeyboardEvent} ev\n             */\n            const onKeyDown = (ev) => {\n                if (!state.dragging || !ctx.enable()) {\n                    return;\n                }\n                if (!WHITE_LISTED_KEYS.includes(ev.key)) {\n                    safePrevent(ev, { stop: true });\n\n                    // Cancels drag sequences on every non-whitelisted key down event.\n                    dragEnd(null);\n                }\n            };\n\n            /**\n             * Global (= ref) \"pointercancel\" event handler.\n             */\n            const onPointerCancel = () => {\n                dragEnd(null);\n            };\n\n            /**\n             * Global (= ref) \"pointerdown\" event handler.\n             * @param {PointerEvent} ev\n             */\n            const onPointerDown = (ev) => {\n                preventClick = false;\n                updatePointerPosition(ev);\n\n                const initiationDelay = ev.pointerType === \"touch\" ? ctx.touchDelay : ctx.delay;\n\n                // A drag sequence can still be in progress if the pointerup occurred\n                // outside of the window.\n                dragEnd(null);\n\n                const fullSelectorEl = ev.target.closest(ctx.fullSelector);\n                if (\n                    ev.button !== LEFT_CLICK ||\n                    !ctx.enable() ||\n                    !fullSelectorEl ||\n                    (ctx.ignoreSelector && ev.target.closest(ctx.ignoreSelector)) ||\n                    ctx.preventDrag(fullSelectorEl)\n                ) {\n                    return;\n                }\n\n                // In FireFox: elements with `overflow: hidden` will prevent mouseenter and mouseleave\n                // events from firing on elements underneath them. This is the case when dragging a card\n                // by the heading. In such cases, we can prevent the default\n                // action on the pointerdown event to allow pointer events to fire properly.\n                // https://bugzilla.mozilla.org/show_bug.cgi?id=1352061\n                // https://bugzilla.mozilla.org/show_bug.cgi?id=339293\n                safePrevent(ev);\n                ev.target.focus();\n                let activeElement = document.activeElement;\n                while (activeElement?.nodeName === \"IFRAME\") {\n                    activeElement = activeElement.contentDocument?.activeElement;\n                }\n                if (activeElement && !activeElement.contains(ev.target)) {\n                    activeElement.blur();\n                }\n\n                const { currentTarget, pointerId, target } = ev;\n                ctx.current.initialPosition = { ...ctx.pointer };\n\n                if (target.hasPointerCapture(pointerId)) {\n                    target.releasePointerCapture(pointerId);\n                }\n\n                if (initiationDelay) {\n                    if (hasTouch()) {\n                        if (ev.pointerType === \"touch\") {\n                            dom.addClass(target.closest(ctx.elementSelector), \"o_touch_bounce\");\n                        }\n                        if (isBrowserFirefox()) {\n                            // On Firefox mobile, long-touch events trigger an unpreventable\n                            // context menu to appear. To prevent this, all linkes are removed\n                            // from the dragged elements during the drag sequence.\n                            const links = [...currentTarget.querySelectorAll(\"[href]\")];\n                            if (currentTarget.hasAttribute(\"href\")) {\n                                links.unshift(currentTarget);\n                            }\n                            for (const link of links) {\n                                dom.removeAttribute(link, \"href\");\n                            }\n                        }\n                        if (isIOS()) {\n                            // On Safari mobile, any image can be dragged regardless\n                            // of the 'user-select' property.\n                            for (const image of currentTarget.getElementsByTagName(\"img\")) {\n                                dom.setAttribute(image, \"draggable\", false);\n                            }\n                        }\n                    }\n\n                    ctx.current.timeout = browser.setTimeout(() => {\n                        ctx.current.initialPosition = { ...ctx.pointer };\n\n                        willStartDrag(target);\n\n                        const { x: px, y: py } = ctx.pointer;\n                        const { x, y, width, height } = dom.getRect(ctx.current.element);\n                        if (px < x || x + width < px || py < y || y + height < py) {\n                            // Pointer left the target\n                            // Note that the timeout is cleared in dragEnd\n                            dragEnd(null);\n                        }\n                    }, initiationDelay);\n                    cleanup.add(() => browser.clearTimeout(ctx.current.timeout));\n                } else {\n                    willStartDrag(target);\n                }\n            };\n\n            /**\n             * Window \"pointermove\" event handler.\n             * @param {PointerEvent} ev\n             */\n            const onPointerMove = (ev) => {\n                updatePointerPosition(ev);\n\n                if (!ctx.current.element || !ctx.enable()) {\n                    return;\n                }\n\n                safePrevent(ev);\n\n                if (!state.dragging) {\n                    if (!canStartDrag()) {\n                        return;\n                    }\n                    dragStart();\n                } else if (!params.allowDisconnected && !ctx.current.element.isConnected) {\n                    return dragEnd(null);\n                }\n\n                if (ctx.followCursor) {\n                    updateElementPosition();\n                }\n\n                callBuildHandler(\"onDrag\");\n            };\n\n            /**\n             * Window \"pointerup\" event handler.\n             * @param {PointerEvent} ev\n             */\n            const onPointerUp = (ev) => {\n                updatePointerPosition(ev);\n                dragEnd(ev.target);\n            };\n\n            /**\n             * Updates the position of the current dragged element according to\n             * the current pointer position.\n             */\n            const updateElementPosition = () => {\n                const { containerRect, element, elementRect, offset } = ctx.current;\n                const { width: ew, height: eh } = elementRect;\n                const { x: cx, y: cy, width: cw, height: ch } = containerRect;\n\n                // Updates the position of the dragged element.\n                dom.addStyle(element, {\n                    left: `${clamp(ctx.pointer.x - offset.x, cx, cx + cw - ew)}px`,\n                    top: `${clamp(ctx.pointer.y - offset.y, cy, cy + ch - eh)}px`,\n                });\n            };\n\n            /**\n             * Updates the current pointer position from a given event.\n             * @param {PointerEvent} ev\n             */\n            const updatePointerPosition = (ev) => {\n                ctx.pointer.x = ev.clientX;\n                ctx.pointer.y = ev.clientY;\n            };\n\n            const updateRects = () => {\n                const { current } = ctx;\n                const { container, element, scrollParentX, scrollParentY } = current;\n                // Container rect\n                current.containerRect = dom.getRect(container, { adjust: true });\n                // If the scrolling element is within an iframe and the draggable\n                // element is outside this iframe, the offsets must be computed taking\n                // into account the iframe.\n                let iframeOffsetX = 0;\n                let iframeOffsetY = 0;\n                const iframeEl = container.ownerDocument.defaultView.frameElement;\n                if (iframeEl && !iframeEl.contentDocument?.contains(element)) {\n                    const { x, y } = dom.getRect(iframeEl);\n                    iframeOffsetX = x;\n                    iframeOffsetY = y;\n                    current.containerRect.x += iframeOffsetX;\n                    current.containerRect.y += iframeOffsetY;\n                }\n                // Adjust container rect according to its overflowing size\n                current.containerRect.width = container.scrollWidth;\n                current.containerRect.height = container.scrollHeight;\n                // ScrollParent rect\n                current.scrollParentXRect = null;\n                current.scrollParentYRect = null;\n                if (ctx.edgeScrolling.enabled) {\n                    // Adjust container rect according to scrollParents\n                    if (scrollParentX) {\n                        current.scrollParentXRect = dom.getRect(scrollParentX, { adjust: true });\n                        current.scrollParentXRect.x += iframeOffsetX;\n                        current.scrollParentXRect.y += iframeOffsetY;\n                        const right = Math.min(\n                            current.containerRect.left + container.scrollWidth,\n                            current.scrollParentXRect.right\n                        );\n                        current.containerRect.x = Math.max(\n                            current.containerRect.x,\n                            current.scrollParentXRect.x\n                        );\n                        current.containerRect.width = right - current.containerRect.x;\n                    }\n                    if (scrollParentY) {\n                        current.scrollParentYRect = dom.getRect(scrollParentY, { adjust: true });\n                        current.scrollParentYRect.x += iframeOffsetX;\n                        current.scrollParentYRect.y += iframeOffsetY;\n                        const bottom = Math.min(\n                            current.containerRect.top + container.scrollHeight,\n                            current.scrollParentYRect.bottom\n                        );\n                        current.containerRect.y = Math.max(\n                            current.containerRect.y,\n                            current.scrollParentYRect.y\n                        );\n                        current.containerRect.height = bottom - current.containerRect.y;\n                    }\n                }\n\n                // Element rect\n                ctx.current.elementRect = dom.getRect(element);\n            };\n\n            /**\n             * @param {Element} target\n             */\n            const willStartDrag = (target) => {\n                ctx.current.element = target.closest(ctx.elementSelector);\n                ctx.current.container = ctx.ref.el;\n\n                cleanup.add(() => (ctx.current = {}));\n                state.willDrag = true;\n\n                callBuildHandler(\"onWillStartDrag\");\n\n                if (hasTouch()) {\n                    // Prevents panning/zooming after a long press\n                    dom.addListener(window, \"touchmove\", safePrevent, {\n                        passive: false,\n                        noAddedStyle: true,\n                    });\n                    if (params.iframeWindow) {\n                        dom.addListener(params.iframeWindow, \"touchmove\", safePrevent, {\n                            passive: false,\n                            noAddedStyle: true,\n                        });\n                    }\n                }\n            };\n\n            // Initialize helpers\n            const cleanup = makeCleanupManager(() => (state.dragging = false));\n            const effectCleanup = makeCleanupManager();\n            const dom = makeDOMHelpers(cleanup);\n\n            const helpers = {\n                ...dom,\n                addCleanup: cleanup.add,\n                addEffectCleanup: effectCleanup.add,\n                callHandler,\n            };\n\n            // Component infos\n            const state = setupHooks.wrapState({ dragging: false });\n\n            // Basic error handling asserting that the parameters are valid.\n            for (const prop in allAcceptedParams) {\n                const type = typeof params[prop];\n                const acceptedTypes = allAcceptedParams[prop].map((t) => t.name.toLowerCase());\n                if (params[prop]) {\n                    if (!acceptedTypes.includes(type)) {\n                        throw makeError(\n                            `invalid type for property \"${prop}\" in parameters: expected { ${acceptedTypes.join(\n                                \", \"\n                            )} } and got ${type}`\n                        );\n                    }\n                } else if (MANDATORY_PARAMS.includes(prop) && !defaultParams[prop]) {\n                    throw makeError(`missing required property \"${prop}\" in parameters`);\n                }\n            }\n\n            /** @type {DraggableHookContext} */\n            const ctx = {\n                enable: () => false,\n                preventDrag: () => false,\n                ref: params.ref,\n                ignoreSelector: null,\n                fullSelector: null,\n                followCursor: true,\n                cursor: null,\n                pointer: { x: 0, y: 0 },\n                edgeScrolling: { enabled: true },\n                get dragging() {\n                    return state.dragging;\n                },\n                get willDrag() {\n                    return state.willDrag;\n                },\n                // Current context\n                current: {},\n            };\n\n            // Effect depending on the params to update them.\n            setupHooks.setup(\n                (...deps) => {\n                    const params = Object.fromEntries(deps);\n                    const actualParams = { ...defaultParams, ...omit(params, \"edgeScrolling\") };\n                    if (params.edgeScrolling) {\n                        actualParams.edgeScrolling = {\n                            ...actualParams.edgeScrolling,\n                            ...params.edgeScrolling,\n                        };\n                    }\n\n                    if (!ctx.ref.el) {\n                        return;\n                    }\n\n                    // Enable getter\n                    ctx.enable = actualParams.enable;\n\n                    // Dragging constraint\n                    if (actualParams.preventDrag) {\n                        ctx.preventDrag = actualParams.preventDrag;\n                    }\n\n                    // Selectors\n                    ctx.elementSelector = actualParams.elements;\n                    if (!ctx.elementSelector) {\n                        throw makeError(\n                            `no value found by \"elements\" selector: ${ctx.elementSelector}`\n                        );\n                    }\n                    const allSelectors = [ctx.elementSelector];\n                    ctx.cursor = actualParams.cursor || null;\n                    if (actualParams.handle) {\n                        allSelectors.push(actualParams.handle);\n                    }\n                    if (actualParams.ignore) {\n                        ctx.ignoreSelector = actualParams.ignore;\n                    }\n                    ctx.fullSelector = allSelectors.join(\" \");\n\n                    // Edge scrolling\n                    Object.assign(ctx.edgeScrolling, actualParams.edgeScrolling);\n\n                    // Delay & tolerance\n                    ctx.delay = actualParams.delay;\n                    ctx.touchDelay = actualParams.delay || actualParams.touchDelay;\n                    ctx.tolerance = actualParams.tolerance;\n\n                    callBuildHandler(\"onComputeParams\", { params: actualParams });\n\n                    // Calls effect cleanup functions when preparing to re-render.\n                    return effectCleanup.cleanup;\n                },\n                () => computeParams(params)\n            );\n            // Firefox currently (119.0.1) does not handle our pointer events\n            // nicely when they happen from within the iframe. To work around\n            // this, we use mouse events instead of pointer events.\n            const useMouseEvents = isBrowserFirefox() && !hasTouch() && params.iframeWindow;\n            // Effect depending on the `ref.el` to add triggering pointer events listener.\n            setupHooks.setup(\n                (el) => {\n                    if (el) {\n                        const { add, cleanup } = makeCleanupManager();\n                        const { addListener } = makeDOMHelpers({ add });\n                        const event = useMouseEvents ? \"mousedown\" : \"pointerdown\";\n                        addListener(el, event, onPointerDown, { noAddedStyle: true });\n                        addListener(el, \"click\", onClick);\n                        if (hasTouch()) {\n                            addListener(el, \"contextmenu\", safePrevent);\n                            // Adds a non-passive listener on touchstart: this allows\n                            // the subsequent \"touchmove\" events to be cancelable\n                            // and thus prevent parasitic \"touchcancel\" events to\n                            // be fired. Note that we DO NOT want to prevent touchstart\n                            // events since they're responsible of the native swipe\n                            // scrolling.\n                            addListener(el, \"touchstart\", () => {}, {\n                                passive: false,\n                                noAddedStyle: true,\n                            });\n                        }\n                        return cleanup;\n                    }\n                },\n                () => [ctx.ref.el]\n            );\n            const addWindowListener = (type, listener, options) => {\n                if (params.iframeWindow) {\n                    setupHooks.addListener(params.iframeWindow, type, listener, options);\n                }\n                setupHooks.addListener(window, type, listener, options);\n            };\n            // Other global event listeners.\n            const throttledOnPointerMove = setupHooks.throttle(onPointerMove);\n            addWindowListener(\n                useMouseEvents ? \"mousemove\" : \"pointermove\",\n                throttledOnPointerMove,\n                { passive: false }\n            );\n            addWindowListener(useMouseEvents ? \"mouseup\" : \"pointerup\", onPointerUp);\n            addWindowListener(\"pointercancel\", onPointerCancel);\n            addWindowListener(\"keydown\", onKeyDown, { capture: true });\n            setupHooks.teardown(() => dragEnd(null));\n\n            return state;\n        },\n    }[hookName];\n}\n", "import { onWillUnmount, reactive, useEffect, useExternalListener } from \"@odoo/owl\";\nimport { useThrottleForAnimation } from \"./timing\";\nimport { makeDraggableHook as nativeMakeDraggableHook } from \"./draggable_hook_builder\";\n\n/**\n * Set of default `makeDraggableHook` setup hooks that makes use of Owl lifecycle\n * and reactivity hooks to properly set up, update and tear down the elements and\n * listeners added by the draggable hook builder.\n *\n * @see {nativeMakeDraggableHook}\n * @type {typeof nativeMakeDraggableHook}\n */\nexport function makeDraggableHook(params) {\n    return nativeMakeDraggableHook({\n        ...params,\n        setupHooks: {\n            addListener: useExternalListener,\n            setup: useEffect,\n            teardown: onWillUnmount,\n            throttle: useThrottleForAnimation,\n            wrapState: reactive,\n        },\n    });\n}\n", "/**\n * Dynamic Viewport Units (DVU)\n *\n * Provides viewport measurement tools focusing on:\n * - Viewport change tracking for responsive components\n * - Viewport dimensions that respond to virtual keyboard\n *\n * Key differences between visualViewport and standard window dimensions:\n * - On mobile, when virtual keyboards appear, visualViewport.height decreases while\n *   innerHeight often doesn't\n * - During pinch-zoom on mobile, visualViewport dimensions change, while innerWidth/innerHeight\n *   remain static\n * - When mobile browser UI elements (address bars, toolbars) appear/disappear, visualViewport\n *   reflects these changes\n *\n * Enhanced with VirtualKeyboard API support:\n * - Reacts to the keyboard's appearance/disappearance via the geometrychange event\n * - Automatically updates viewport dimensions when keyboard visibility changes\n * - Triggers viewport change listeners when keyboard visibility changes\n *\n * The module will fall back to standard window dimensions when visualViewport API is not\n * available (primarily older browsers or some embedded webviews).\n *\n * References:\n * - https://www.w3.org/blog/CSS/2021/07/15/css-values-4-viewport-units/\n * - https://developer.mozilla.org/en-US/docs/Web/API/VirtualKeyboard_API\n */\n\nimport { throttleForAnimation } from \"@web/core/utils/timing\";\nimport { onWillUnmount } from \"@odoo/owl\";\nimport { browser } from \"@web/core/browser/browser\";\nimport { isVirtualKeyboardSupported } from \"@web/core/browser/feature_detection\";\n\nconst viewport = {\n    listeners: [],\n\n    /**\n     * Register a callback for viewport changes\n     *\n     * @param {Function} listener - Function to call when viewport changes\n     * @returns {Function} - Function to remove the listener\n     */\n    addListener(listener) {\n        this.listeners.push(listener);\n        return () => {\n            const index = this.listeners.indexOf(listener);\n            if (index !== -1) {\n                this.listeners.splice(index, 1);\n            }\n        };\n    },\n\n    /**\n     * Notify all listeners of viewport changes\n     */\n    notifyListeners() {\n        this.listeners.forEach((listener) => listener());\n    },\n};\n\n// Initialize viewport tracking\nif (typeof window !== \"undefined\") {\n    const throttledUpdate = throttleForAnimation(() => viewport.notifyListeners());\n\n    if (browser.visualViewport) {\n        browser.visualViewport.addEventListener(\"resize\", throttledUpdate);\n    }\n\n    if (isVirtualKeyboardSupported()) {\n        browser.navigator.virtualKeyboard.addEventListener(\"geometrychange\", throttledUpdate);\n    }\n\n    // Fallback to window resize for browsers without VisualViewport or VirtualKeyboard\n    browser.addEventListener(\"resize\", throttledUpdate);\n}\n\n/**\n * Get current viewport dimensions\n * Takes into account VirtualKeyboard API if available\n *\n * @returns {Object} - Object with width and height properties in pixels\n */\nexport function getViewportDimensions() {\n    return {\n        width: browser.visualViewport?.width || browser.innerWidth,\n        height: browser.visualViewport?.height || browser.innerHeight,\n    };\n}\n\n/**\n * Register a callback for viewport dimension changes\n * This will trigger for regular viewport changes and virtual keyboard visibility changes\n *\n * @param {Function} callback - Function to call on viewport change\n * @returns {Function} - Function to remove the listener\n */\nexport function onViewportChange(callback) {\n    return viewport.addListener(callback);\n}\n\n/**\n * OWL hook to use viewport change tracking in components\n * Automatically cleans up listener when component is unmounted\n *\n * @param {Function} callback - Function to call when viewport changes\n */\nexport function useViewportChange(callback) {\n    const removeListener = onViewportChange(callback);\n    onWillUnmount(() => removeListener());\n}\n", "import { humanNumber } from \"@web/core/utils/numbers\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { session } from \"@web/session\";\nimport { _t } from \"@web/core/l10n/translation\";\n\nexport const DEFAULT_MAX_FILE_SIZE = 128 * 1024 * 1024;\n\n/**\n * @param {Services[\"notification\"]} notificationService\n * @param {File} file\n * @param {Number} maxUploadSize\n * @returns {boolean}\n */\nexport function checkFileSize(fileSize, notificationService) {\n    const maxUploadSize = session.max_file_upload_size || DEFAULT_MAX_FILE_SIZE;\n    if (fileSize > maxUploadSize) {\n        notificationService.add(\n            _t(\n                \"The selected file (%(size)sB) is larger than the maximum allowed file size (%(maxSize)sB).\",\n                { size: humanNumber(fileSize), maxSize: humanNumber(maxUploadSize) }\n            ),\n            {\n                type: \"danger\",\n            }\n        );\n        return false;\n    }\n    return true;\n}\n\n/**\n * Hook to upload a file to the server.\n * @returns {function}\n */\nexport function useFileUploader() {\n    const http = useService(\"http\");\n    const notification = useService(\"notification\");\n    /**\n     * @param {string} route\n     * @param {Object} params\n     */\n    return async (route, params) => {\n        if ((params.ufile && params.ufile.length) || params.file) {\n            const fileSize = (params.ufile && params.ufile[0].size) || params.file.size;\n            if (!checkFileSize(fileSize, notification)) {\n                return null;\n            }\n        }\n        const fileData = await http.post(route, params, \"text\");\n        const parsedFileData = JSON.parse(fileData);\n        if (parsedFileData.error) {\n            throw new Error(parsedFileData.error);\n        }\n        return parsedFileData;\n    };\n}\n\nexport function resizeBlobImg(blob, params = {}) {\n    if (!blob.type || !blob.type.startsWith(\"image/\")) {\n        return Promise.reject(new Error(_t(\"The file is not an image, resizing is not possible\")));\n    }\n    const { width, height, offsetX, offsetY } = {\n        width: 256,\n        height: 256,\n        offsetX: 0.5,\n        offsetY: 0.5,\n        ...params,\n    };\n    return new Promise((resolve, reject) => {\n        const img = new Image();\n        img.onload = () => {\n            if (width < img.width || height < img.height) {\n                const canvas = document.createElement(\"canvas\");\n                canvas.width = width;\n                canvas.height = height;\n                const ctx = canvas.getContext(\"2d\");\n                ctx.imageSmoothingQuality = \"high\";\n                ctx.mozImageSmoothingEnabled = true;\n                ctx.webkitImageSmoothingEnabled = true;\n                ctx.msImageSmoothingEnabled = true;\n                ctx.imageSmoothingEnabled = true;\n\n                // Keep src image's aspect ratio\n                // while drawing in dest image with different ratio\n                const srcRatio = img.width / img.height;\n                const dWidth = Math.min(Math.floor(height * srcRatio), width);\n                const dHeight = Math.min(Math.floor(width / srcRatio), height);\n\n                // Start drawing at some proportion from the edges\n                // 0.5 means the image is centered on the image's shortest axis\n                const dx = Math.round((width - dWidth) * offsetX);\n                const dy = Math.round((height - dHeight) * offsetY);\n\n                ctx.drawImage(img, 0, 0, img.width, img.height, dx, dy, dWidth, dHeight);\n                canvas.toBlob(resolve);\n            } else {\n                resolve(blob);\n            }\n        };\n        img.onerror = () => {\n            reject(new Error(_t(\"The resizing of the image failed\")));\n        };\n        img.src = URL.createObjectURL(blob);\n    });\n}\n", "/**\n * Creates a version of the function that's memoized on the value of its first\n * argument, if any.\n *\n * @template T, U\n * @param {(arg: T) => U} func the function to memoize\n * @returns {(arg: T) => U} a memoized version of the original function\n */\nexport function memoize(func) {\n    const cache = new Map();\n    const funcName = func.name ? func.name + \" (memoized)\" : \"memoized\";\n    return {\n        [funcName](...args) {\n            if (!cache.has(args[0])) {\n                cache.set(args[0], func(...args));\n            }\n            return cache.get(...args);\n        },\n    }[funcName];\n}\n\n/**\n * Generate a unique integer id (unique within the entire client session).\n * Useful for temporary DOM ids.\n *\n * @param {string} prefix\n * @returns {string}\n */\nexport function uniqueId(prefix = \"\") {\n    return `${prefix}${++uniqueId.nextId}`;\n}\n// set nextId on the function itself to be able to patch then\nuniqueId.nextId = 0;\n", "import { hasTouch, isMobileOS } from \"@web/core/browser/feature_detection\";\n\nimport { status, useComponent, useEffect, useRef, onWillUnmount, useState, toRaw } from \"@odoo/owl\";\n\n/**\n * This file contains various custom hooks.\n * Their inner working is rather simple:\n * Each custom hook simply hooks itself to any number of owl lifecycle hooks.\n * You can then use them just like an owl hook in any Component\n * e.g.:\n * import { useBus } from \"@web/core/utils/hooks\";\n * ...\n * setup() {\n *    ...\n *    useBus(someBus, someEvent, callback)\n *    ...\n * }\n */\n\n/**\n * @typedef {{ readonly el: HTMLElement | null; }} Ref\n */\n\n// -----------------------------------------------------------------------------\n// useAutofocus\n// -----------------------------------------------------------------------------\n\n/**\n * Focus an element referenced by a t-ref=\"autofocus\" in the active component\n * as soon as it appears in the DOM and if it was not displayed before.\n * If it is an input/textarea, set the selection at the end.\n * @param {Object} [params]\n * @param {string} [params.refName] override the ref name \"autofocus\"\n * @param {boolean} [params.selectAll] if true, will select the entire text value.\n * @param {boolean} [params.mobile] if true, will force autofocus on touch devices.\n * @returns {Ref} the element reference\n */\nexport function useAutofocus({ refName, selectAll, mobile } = {}) {\n    const ref = useRef(refName || \"autofocus\");\n    const uiService = useService(\"ui\");\n\n    // Prevent autofocus on touch devices to avoid the virtual keyboard from popping up unexpectedly\n    if (!mobile && hasTouch()) {\n        return ref;\n    }\n    // LEGACY\n    if (!mobile && isMobileOS()) {\n        return ref;\n    }\n    function isFocusable(el) {\n        if (!el) {\n            return;\n        }\n        if (!uiService.activeElement || uiService.activeElement.contains(el)) {\n            return true;\n        }\n        const rootNode = el.getRootNode();\n        return rootNode instanceof ShadowRoot && uiService.activeElement.contains(rootNode.host);\n    }\n    // LEGACY\n    useEffect(\n        (el) => {\n            if (isFocusable(el)) {\n                el.focus();\n                if ([\"INPUT\", \"TEXTAREA\"].includes(el.tagName) && el.type !== \"number\") {\n                    el.selectionEnd = el.value.length;\n                    el.selectionStart = selectAll ? 0 : el.value.length;\n                }\n            }\n        },\n        () => [ref.el]\n    );\n    return ref;\n}\n\n// -----------------------------------------------------------------------------\n// useBus\n// -----------------------------------------------------------------------------\n\n/**\n * Ensures a bus event listener is attached and cleared the proper way.\n *\n * @param {import(\"@odoo/owl\").EventBus} bus\n * @param {string} eventName\n * @param {EventListener} callback\n */\nexport function useBus(bus, eventName, callback) {\n    const component = useComponent();\n    useEffect(\n        () => {\n            const listener = callback.bind(component);\n            bus.addEventListener(eventName, listener);\n            return () => bus.removeEventListener(eventName, listener);\n        },\n        () => []\n    );\n}\n\n// In an object so that it can be patched in tests (prevent error on blocking RPCs after tests)\nexport const useServiceProtectMethodHandling = {\n    fn() {\n        return this.original();\n    },\n    mocked() {\n        // Keep them unresolved so that no crash in test due to triggered RPCs by services\n        return new Promise(() => {});\n    },\n    original() {\n        return Promise.reject(new Error(\"Component is destroyed\"));\n    },\n};\n\n// -----------------------------------------------------------------------------\n// useService\n// -----------------------------------------------------------------------------\nfunction _protectMethod(component, fn) {\n    return function (...args) {\n        if (status(component) === \"destroyed\") {\n            return useServiceProtectMethodHandling.fn();\n        }\n\n        const prom = Promise.resolve(fn.call(this, ...args));\n        const protectedProm = prom.then((result) =>\n            status(component) === \"destroyed\" ? new Promise(() => {}) : result\n        );\n        return Object.assign(protectedProm, {\n            abort: prom.abort,\n            cancel: prom.cancel,\n        });\n    };\n}\n\nexport const SERVICES_METADATA = {};\n\n/**\n * Import a service into a component\n *\n * @template {keyof import(\"services\").ServiceFactories} K\n * @param {K} serviceName\n * @returns {import(\"services\").ServiceFactories[K]}\n */\nexport function useService(serviceName) {\n    const component = useComponent();\n    const { services } = component.env;\n    if (!(serviceName in services)) {\n        throw new Error(`Service ${serviceName} is not available`);\n    }\n    const service = services[serviceName];\n    if (SERVICES_METADATA[serviceName]) {\n        if (service instanceof Function) {\n            return _protectMethod(component, service);\n        } else {\n            const methods = SERVICES_METADATA[serviceName] ?? [];\n            const result = Object.create(service);\n            for (const method of methods) {\n                result[method] = _protectMethod(component, service[method]);\n            }\n            return result;\n        }\n    }\n    if (toRaw(service) !== service) {\n        return useState(service);\n    }\n    return service;\n}\n\n// -----------------------------------------------------------------------------\n// useSpellCheck\n// -----------------------------------------------------------------------------\n\n/**\n * To avoid elements to keep their spellcheck appearance when they are no\n * longer in focus. We only add this attribute when needed. To disable this\n * behavior, use the spellcheck attribute on the element.\n */\nexport function useSpellCheck({ refName } = {}) {\n    const elements = [];\n    const ref = useRef(refName || \"spellcheck\");\n    function toggleSpellcheck(ev) {\n        ev.target.spellcheck = document.activeElement === ev.target;\n    }\n    useEffect(\n        (el) => {\n            if (el) {\n                const inputs =\n                    [\"INPUT\", \"TEXTAREA\"].includes(el.nodeName) || el.isContentEditable\n                        ? [el]\n                        : el.querySelectorAll(\"input, textarea, [contenteditable=true]\");\n                inputs.forEach((input) => {\n                    if (input.spellcheck !== false) {\n                        elements.push(input);\n                        input.addEventListener(\"focus\", toggleSpellcheck);\n                        input.addEventListener(\"blur\", toggleSpellcheck);\n                    }\n                });\n            }\n            return () => {\n                elements.forEach((input) => {\n                    input.removeEventListener(\"focus\", toggleSpellcheck);\n                    input.removeEventListener(\"blur\", toggleSpellcheck);\n                });\n            };\n        },\n        () => [ref.el]\n    );\n}\n\n/**\n * @typedef {Function} ForwardRef\n * @property {HTMLElement | undefined} el\n */\n\n/**\n * Use a ref that was forwarded by a child @see useForwardRefToParent\n *\n * @returns {ForwardRef} a ref that can be called to set its value to that of a\n *  child ref, but can otherwise be used as a normal ref object\n */\nexport function useChildRef() {\n    let defined = false;\n    let value;\n    return function ref(v) {\n        value = v;\n        if (defined) {\n            return;\n        }\n        Object.defineProperty(ref, \"el\", {\n            get() {\n                return value.el;\n            },\n        });\n        defined = true;\n    };\n}\n/**\n * Forwards the given refName to the parent by calling the corresponding\n * ForwardRef received as prop. @see useChildRef\n *\n * @param {string} refName name of the ref to forward\n * @returns {Ref} the same ref that is forwarded to the\n *  parent\n */\nexport function useForwardRefToParent(refName) {\n    const component = useComponent();\n    const ref = useRef(refName);\n    if (component.props[refName]) {\n        component.props[refName](ref);\n    }\n    return ref;\n}\n/**\n * Use the dialog service while also automatically closing the dialogs opened\n * by the current component when it is unmounted.\n *\n * @returns {import(\"@web/core/dialog/dialog_service\").DialogServiceInterface}\n */\nexport function useOwnedDialogs() {\n    const dialogService = useService(\"dialog\");\n    const cbs = [];\n    onWillUnmount(() => {\n        cbs.forEach((cb) => cb());\n    });\n    const addDialog = (...args) => {\n        const close = dialogService.add(...args);\n        cbs.push(close);\n        return close;\n    };\n    return addDialog;\n}\n/**\n * Manages an event listener on a ref. Useful for hooks that want to manage\n * event listeners, especially more than one. Prefer using t-on directly in\n * components. If your hook only needs a single event listener, consider simply\n * returning it from the hook and letting the user attach it with t-on.\n *\n * @param {Ref} ref\n * @param {Parameters<typeof EventTarget.prototype.addEventListener>} listener\n */\nexport function useRefListener(ref, ...listener) {\n    useEffect(\n        (el) => {\n            el?.addEventListener(...listener);\n            return () => el?.removeEventListener(...listener);\n        },\n        () => [ref.el]\n    );\n}\n", "import { htmlEscape, markup } from \"@odoo/owl\";\n\nimport { formatList, normalizedMatches } from \"@web/core/l10n/utils\";\nimport { unique } from \"@web/core/utils/arrays\";\nimport { escapeRegExp, sprintf } from \"@web/core/utils/strings\";\n\nconst Markup = markup().constructor;\n\n/**\n * Safely creates a Document fragment from content. If content was flagged as safe HTML using\n * `markup` it is parsed as HTML. Otherwise it is escaped and parsed as text.\n *\n * @param {string|ReturnType<markup>} content\n */\nexport function createDocumentFragmentFromContent(content) {\n    return new document.defaultView.DOMParser().parseFromString(htmlEscape(content), \"text/html\");\n}\n\n/**\n * Safely creates an element with the given content. If content was flagged as safe HTML using\n * `markup` it is set as innerHTML. Otherwise it is set as text.\n *\n * @param {string} elementName\n * @param {string|ReturnType<markup>} content\n * @returns {Element}\n */\nexport function createElementWithContent(elementName, content) {\n    const element = document.createElement(elementName);\n    setElementContent(element, content);\n    return element;\n}\n\n/**\n * Returns a markuped version of the input text where\n * the query is highlighted using the input classes\n * if it is part of the text. Will normalize the query\n * for advanced symbols matching\n *\n * @param {string | ReturnType<markup>} query\n * @param {string | ReturnType<markup>} text\n * @param {string | ReturnType<markup>} classes\n * @returns {string | ReturnType<markup>}\n */\nexport function highlightText(query, text, classes) {\n    if (!query) {\n        return text;\n    }\n    const matches = unique(\n        normalizedMatches(text, query).map((m) =>\n            // normalizedMatch will remove Markup and return string matches\n            // so it is necessary to restore the removed Markup when needed\n            query instanceof Markup ? markup(m.match.toLowerCase()) : m.match.toLowerCase()\n        )\n    );\n    let result = text;\n    for (const match of matches) {\n        const regex = new RegExp(\n            `(?<!&[^;]{0,5})(${escapeRegExp(htmlEscape(match))})(?=(?:[^>]*<[^<]*>)*[^<>]*$)`,\n            \"ig\"\n        );\n        result = htmlReplace(result, regex, (_, match) => {\n            /**\n             * markup: text is a Markup object (either escaped inside htmlReplace or\n             * flagged safe), `match` is directly coming from this value,\n             * and the regex doesn't do anything crazy to unescape it.\n             */\n            match = markup(match);\n            return markup`<span class=\"${classes}\">${match}</span>`;\n        });\n    }\n    return result;\n}\n\n/**\n * Same behavior as formatList, but produces safe HTML. If the values are flagged as safe HTML using\n * `markup()` they are set as it is. Otherwise they are escaped.\n *\n * @param {Array<string|ReturnType<markup>>} list The array of values to format into a list.\n * @param {Object} [param0]\n * @param {string} [param0.localeCode] The locale to use (e.g. en-US).\n * @param {\"standard\"|\"standard-short\"|\"or\"|\"or-short\"|\"unit\"|\"unit-short\"|\"unit-narrow\"} [param0.style=\"standard\"] The style to format the list with.\n * @returns {ReturnType<markup>} The formatted list.\n */\nexport function htmlFormatList(list, ...args) {\n    return markup(\n        formatList(\n            Array.from(list, (val) => htmlEscape(val).toString()),\n            ...args\n        )\n    );\n}\n\n/**\n * Applies string replace on content and returns a markup result built for HTML.\n *\n * @param {string|ReturnType<markup>} content\n * @param {string | RegExp} search\n * @param {string} replacement\n * @returns {ReturnType<markup>}\n */\nexport function htmlReplace(content, search, replacement) {\n    if (search instanceof RegExp && !(replacement instanceof Function)) {\n        throw new Error(\"htmlReplace: replacement must be a function when search is a RegExp.\");\n    }\n    content = htmlEscape(content);\n    if (typeof search === \"string\" || search instanceof String) {\n        search = htmlEscape(search);\n    }\n    const safeReplacement =\n        replacement instanceof Function\n            ? (...args) => htmlEscape(replacement(...args))\n            : htmlEscape(replacement);\n    // markup: content and replacement are escaped (or markup), replace is considered safe\n    return markup(content.replace(search, safeReplacement));\n}\n\n/**\n * Applies string replaceAll on content and returns a markup result built for HTML.\n *\n * @param {string|ReturnType<markup>} content\n * @param {string | RegExp} search\n * @param {string|(match: string) => string|ReturnType<markup>} replacement\n * @returns {ReturnType<markup>}\n */\nexport function htmlReplaceAll(content, search, replacement) {\n    if (search instanceof RegExp && !(replacement instanceof Function)) {\n        throw new Error(\"htmlReplaceAll: replacement must be a function when search is a RegExp.\");\n    }\n    content = htmlEscape(content);\n    if (typeof search === \"string\" || search instanceof String) {\n        search = htmlEscape(search);\n    }\n    const safeReplacement =\n        replacement instanceof Function\n            ? (...args) => htmlEscape(replacement(...args))\n            : htmlEscape(replacement);\n    // markup: content and replacement are escaped (or markup), replaceAll is considered safe\n    return markup(content.replaceAll(search, safeReplacement));\n}\n\n/**\n * Applies list join on content and returns a markup result built for HTML.\n *\n * @param {Array<string|ReturnType<markup>>} args\n * @returns {ReturnType<markup>}\n */\nexport function htmlJoin(list, separator = \"\") {\n    // markup: args and separator are escaped (or markup), join is considered safe\n    return markup(list.map((arg) => htmlEscape(arg)).join(htmlEscape(separator)));\n}\n\n/**\n * Same behavior as sprintf, but produces safe HTML. If the string or values are flagged as safe HTML\n * using `markup()` they are set as it is. Otherwise they are escaped.\n *\n * @param {string} str The string with placeholders (%s) to insert values into.\n * @param  {...any} values Primitive values to insert in place of placeholders.\n * @returns {string|Markup}\n */\nexport function htmlSprintf(str, ...values) {\n    const valuesDict = values[0];\n    if (\n        valuesDict &&\n        Object.prototype.toString.call(valuesDict) === \"[object Object]\" &&\n        !(valuesDict instanceof Markup)\n    ) {\n        // markup: escaped base string and values (assuming sprintf itself is safe)\n        return markup(\n            sprintf(\n                htmlEscape(str).toString(),\n                Object.fromEntries(\n                    Object.entries(valuesDict).map(([key, value]) => [\n                        key,\n                        htmlEscape(value).toString(),\n                    ])\n                )\n            )\n        );\n    }\n    // markup: escaped base string and values (assuming sprintf itself is safe)\n    return markup(\n        sprintf(\n            htmlEscape(str).toString(),\n            values.map((value) => htmlEscape(value).toString())\n        )\n    );\n}\n\n/**\n * Applies string trim on content and returns a markup result built for HTML.\n *\n * @param {string|ReturnType<markup>} content\n * @returns {string|ReturnType<markup>}\n */\nexport function htmlTrim(content) {\n    content = htmlEscape(content);\n    // markup: content is escaped (or markup), trim is considered safe\n    return markup(content.trim());\n}\n\n/**\n * Checks if a html content is empty. If there are only formatting tags\n * with style attributes or a void content. Famous use case is\n * '<p style=\"...\" class=\"..\"><br></p>' added by some web editor(s).\n * Note that because the use of this method is limited, we ignore the cases\n * like there's one <img> tag in the content. In such case, even if it's the\n * actual content, we consider it empty.\n *\n * @param {string|ReturnType<markup>} content\n * @returns {boolean} true if no content found or if containing only formatting tags\n */\nexport function isHtmlEmpty(content = \"\") {\n    return createElementWithContent(\"div\", content).textContent.trim() === \"\";\n}\n\n/**\n * Format the text as follow:\n *      \\*\\*text\\*\\* => Put the text in bold.\n *      --text-- => Put the text in muted.\n *      \\`text\\` => Put the text in a rounded badge (bg-primary).\n *      \\n => Insert a breakline.\n *      \\t => Insert 4 spaces.\n *\n * @param {string|ReturnType<markup>} text\n * @returns {string|ReturnType<markup>} the formatted text\n */\nexport function odoomark(text) {\n    const replacements = [\n        [/\\n/g, () => markup`<br/>`],\n        [/\\t/g, () => markup`<span style=\"margin-left: 2em\"></span>`],\n        [\n            /\\*\\*(.+?)\\*\\*/g,\n            (_, bold) => {\n                /**\n                 * markup: text is a Markup object (either escaped inside htmlReplace or\n                 * flagged safe), `bold` is directly coming from this value,\n                 * and the regex doesn't do anything crazy to unescape it.\n                 */\n                markup(bold);\n                return markup`<b>${bold}</b>`;\n            },\n        ],\n        [\n            /--(.+?)--/g,\n            (_, muted) => {\n                /**\n                 * markup: text is a Markup object (either escaped inside htmlReplace or\n                 * flagged safe), `muted` is directly coming from this value,\n                 * and the regex doesn't do anything crazy to unescape it.\n                 */\n                muted = markup(muted);\n                return markup`<span class='text-muted'>${muted}</span>`;\n            },\n        ],\n        [\n            /&#x60;(.+?)&#x60;/g,\n            (_, tag) => {\n                /**\n                 * markup: text is a Markup object (either escaped inside htmlReplace or\n                 * flagged safe), `tag` is directly coming from this value,\n                 * and the regex doesn't do anything crazy to unescape it.\n                 */\n                tag = markup(tag);\n                return markup`<span class=\"o_tag position-relative d-inline-flex align-items-center mw-100 o_badge badge rounded-pill lh-1 o_tag_color_0\">${tag}</span>`;\n            },\n        ],\n    ];\n    for (const replacement of replacements) {\n        text = htmlReplaceAll(text, replacement[0], replacement[1]);\n    }\n    return text;\n}\n\n/**\n * Safely sets content on element. If content was flagged as safe HTML using `markup` it is set as\n * innerHTML. Otherwise it is set as text.\n *\n * @param {Element} element\n * @param {string|ReturnType<markup>} content\n */\nexport function setElementContent(element, content) {\n    if (content instanceof Markup) {\n        // innerHTML: content is markup\n        element.innerHTML = content;\n    } else {\n        element.textContent = content;\n    }\n}\n\nexport function isMarkup(content) {\n    return content instanceof Markup;\n}\n", "import { Mutex } from \"./concurrency\";\n\nconst VERSION_TABLE = \"__DBVersion__\";\nconst VERSION_KEY = \"__version__\";\n\nexport class IDBQuotaExceededError extends Error {}\n\nfunction formatStorageSize(size) {\n    const units = [\"b\", \"Kb\", \"Mb\", \"Gb\"];\n    while (size >= 1000 && units.length > 1) {\n        size /= 1000;\n        units.splice(0, 1);\n    }\n    return `${size.toFixed(2)}${units[0]}`;\n}\n\nexport class IndexedDB {\n    constructor(name, version) {\n        this.name = name;\n        this._tables = new Set([VERSION_TABLE]);\n        this.mutex = new Mutex();\n        this.mutex.exec(() => this._checkVersion(version));\n    }\n\n    // -------------------------------------------------------------------------\n    // Public\n    // -------------------------------------------------------------------------\n\n    /**\n     * Reads data from a given table.\n     *\n     * @param {string} table\n     * @param {string} key\n     * @returns Promise\n     */\n    async read(table, key) {\n        this._tables.add(table);\n        return this.execute((db) => {\n            if (db) {\n                return this._read(db, table, key);\n            }\n        });\n    }\n\n    /**\n     * Write data into the given table\n     *\n     * @param {string} table\n     * @param {string} key\n     * @param  {any} value\n     * @returns Promise\n     */\n    async write(table, key, value) {\n        this._tables.add(table);\n        return this.execute((db) => {\n            if (db) {\n                return this._write(db, table, key, value);\n            }\n        });\n    }\n\n    /**\n     * Invalidates a table, or the whole database.\n     *\n     * @param {string|Array} [table=null] if not given, the whole database is invalidated\n     * @returns Promise\n     */\n    async invalidate(tables = null) {\n        return this.execute((db) => {\n            if (db) {\n                return this._invalidate(db, typeof tables === \"string\" ? [tables] : tables);\n            }\n        });\n    }\n\n    /**\n     * Delete the whole database\n     *\n     * @returns Promise\n     */\n    async deleteDatabase() {\n        return this.mutex.exec(() => this._deleteDatabase(() => {}));\n    }\n\n    /**\n     * open the database and execute the callback with the db as parameter.\n     *\n     * @params {Function} callback\n     * @returns Promise\n     */\n    async execute(callback) {\n        return this.mutex.exec(() => this._execute(callback));\n    }\n\n    // -------------------------------------------------------------------------\n    // Protected\n    // -------------------------------------------------------------------------\n\n    async _deleteDatabase(callback) {\n        return new Promise((resolve) => {\n            const request = indexedDB.deleteDatabase(this.name);\n            request.onsuccess = () => {\n                Promise.resolve(callback()).then(resolve);\n            };\n            request.onerror = (event) => {\n                console.error(`IndexedDB delete error: ${event.target.error?.message}`);\n                Promise.resolve(callback()).then(resolve);\n            };\n        });\n    }\n\n    async _checkVersion(version) {\n        return new Promise((resolve) => {\n            this._execute((db) => {\n                if (db) {\n                    return this._read(db, VERSION_TABLE, VERSION_KEY);\n                }\n            }).then((currentVersion) => {\n                if (!currentVersion) {\n                    this._execute((db) => {\n                        if (db) {\n                            this._write(db, VERSION_TABLE, VERSION_KEY, version);\n                        }\n                    }).then(resolve);\n                } else if (currentVersion !== version) {\n                    this._deleteDatabase(() => {\n                        this._execute((db) => {\n                            if (db) {\n                                this._write(db, VERSION_TABLE, VERSION_KEY, version);\n                            }\n                        });\n                    }).then(resolve);\n                } else {\n                    resolve();\n                }\n            });\n        });\n    }\n\n    async _execute(callback, idbVersion) {\n        return new Promise((resolve, reject) => {\n            const request = indexedDB.open(this.name, idbVersion);\n            request.onupgradeneeded = (event) => {\n                const db = event.target.result;\n                const dbTables = new Set(db.objectStoreNames);\n                const newTables = this._tables.difference(dbTables);\n                newTables.forEach((table) => db.createObjectStore(table));\n            };\n            request.onsuccess = (event) => {\n                const db = event.target.result;\n                const dbTables = new Set(db.objectStoreNames);\n                const newTables = this._tables.difference(dbTables);\n                if (newTables.size !== 0) {\n                    db.close();\n                    const version = db.version + 1;\n                    return this._execute(callback, version).then(resolve);\n                }\n                Promise.resolve(callback(db))\n                    .then(resolve)\n                    .catch(async (e) => {\n                        if (e.name === \"QuotaExceededError\") {\n                            const { quota, usage } = await navigator.storage.estimate();\n                            console.error(\n                                `IndexedDB error: Quota Exceeded (${formatStorageSize(\n                                    usage\n                                )} out of ${formatStorageSize(quota)} used)`\n                            );\n                            reject(new IDBQuotaExceededError());\n                        } else {\n                            reject(e);\n                        }\n                    })\n                    .finally(() => db.close());\n            };\n            request.onerror = (event) => {\n                console.error(`IndexedDB error: ${event.target.error?.message}`);\n                Promise.resolve(callback()).then(resolve);\n            };\n        });\n    }\n\n    async _write(db, table, key, record) {\n        return new Promise((resolve, reject) => {\n            // AAB: do we care about write performance?\n            // Relaxed durability improves the write performances\n            // https://nolanlawson.com/2021/08/22/speeding-up-indexeddb-reads-and-writes/\n            // https://developer.mozilla.org/en-US/docs/Web/API/IDBTransaction/durability\n            const transaction = db.transaction(table, \"readwrite\", { durability: \"relaxed\" });\n            transaction.objectStore(table).put(record, key); // put to allow updates\n            transaction.onerror = (ev) => reject(ev.target.error); // firefox (DOMException)\n            transaction.onabort = (ev) => reject(ev.target.error); // chrome (QuotaExceededError)\n            transaction.oncomplete = resolve;\n\n            // Force the changes to be committed to the database asap\n            // https://developer.mozilla.org/en-US/docs/Web/API/IDBTransaction/commit\n            transaction.commit();\n        });\n    }\n\n    async _invalidate(db, tables) {\n        return new Promise((resolve, reject) => {\n            const objectStoreNames = [...db.objectStoreNames].filter(\n                (table) => table !== VERSION_TABLE\n            );\n            tables = tables ? objectStoreNames.filter((t) => tables.includes(t)) : objectStoreNames;\n\n            if (tables.length === 0) {\n                return resolve();\n            }\n            // Relaxed durability improves the write performances\n            // https://nolanlawson.com/2021/08/22/speeding-up-indexeddb-reads-and-writes/\n            // https://developer.mozilla.org/en-US/docs/Web/API/IDBTransaction/durability\n            const transaction = db.transaction(tables, \"readwrite\", { durability: \"relaxed\" });\n            const proms = tables.map(\n                (table) =>\n                    new Promise((resolve) => {\n                        const objectStore = transaction.objectStore(table);\n                        const request = objectStore.clear();\n                        request.onsuccess = resolve;\n                    })\n            );\n            transaction.onerror = () => reject(transaction.error);\n            Promise.all(proms).then(resolve);\n\n            // Force the changes to be committed to the database asap\n            // https://developer.mozilla.org/en-US/docs/Web/API/IDBTransaction/commit\n            transaction.commit();\n        });\n    }\n\n    async _read(db, table, key) {\n        return new Promise((resolve, reject) => {\n            const transaction = db.transaction(table, \"readonly\");\n            const objectStore = transaction.objectStore(table);\n            const r = objectStore.get(key);\n            r.onsuccess = () => resolve(r.result);\n            transaction.onerror = () => reject(transaction.error);\n        });\n    }\n}\n", "const eventHandledWeakMap = new WeakMap();\n/**\n * Returns whether the given event has been handled with the given markName.\n *\n * @param {Event} ev\n * @param {string} markName\n * @returns {boolean}\n */\nexport function isEventHandled(ev, markName) {\n    if (!eventHandledWeakMap.get(ev)) {\n        return false;\n    }\n    return eventHandledWeakMap.get(ev).includes(markName);\n}\n/**\n * Marks the given event as handled by the given markName. Useful to allow\n * handlers in the propagation chain to make a decision based on what has\n * already been done.\n *\n * @param {Event} ev\n * @param {string} markName\n */\nexport function markEventHandled(ev, markName) {\n    if (!eventHandledWeakMap.get(ev)) {\n        eventHandledWeakMap.set(ev, []);\n    }\n    eventHandledWeakMap.get(ev).push(markName);\n}\n", "import { localization } from \"@web/core/l10n/localization\";\nimport { makeDraggableHook } from \"@web/core/utils/draggable_hook_builder_owl\";\n\n/** @typedef {import(\"@web/core/utils/draggable_hook_builder\").DraggableHandlerParams} DraggableHandlerParams */\n/** @typedef {DraggableHandlerParams & { group: HTMLElement | null }} NestedSortableHandlerParams */\n\n/**\n * @typedef {import(\"./sortable\").SortableParams} NestedSortableParams\n *\n * OPTIONAL\n *\n * @property {(HTMLElement) => boolean} [preventDrag] function receiving a\n *  the current target for dragging (element) and returning a boolean, whether\n *  the element can be effectively dragged or not.\n * @property {boolean | () => boolean} [nest] whether elements are nested or not.\n * @property {string | () => string} [listTagName] type of lists (\"ul\" or \"ol\").\n * @property {number | () => number} [nestInterval] Horizontal distance needed to trigger\n * a change in the list hierarchy (i.e. changing parent when moving horizontally)\n * @property {number | () => number} [maxLevels] The maximum depth of nested items\n * the list can accept. If set to '0' the levels are unlimited. Default: 0\n * @property {(DraggableHookContext) => boolean} [isAllowed] You can specify a custom function\n * to verify if a drop location is allowed. return True by default\n * @property {boolean} [useElementSize] The placeholder use the dragged element size instead\n * of the small 8px lines. Default:false\n *\n * HANDLERS (also optional)\n *\n * @property {(params: MoveParams) => any} [onMove] called when the element has moved\n * (changed position) (@see MoveParams).\n */\n\n/**\n * @typedef MoveParams\n * @property {HTMLElement} element\n * @property {HTMLElement | null} group\n * @property {HTMLElement | null} previous\n * @property {HTMLElement | null} next\n * @property {HTMLElement | null} newGroup\n * @property {HTMLElement | null} parent\n * @property {HTMLElement} placeholder\n */\n\n/**\n * @typedef SortableState\n * @property {boolean} dragging\n */\n\n/** @type {(params: NestedSortableParams) => SortableState} */\nexport const useNestedSortable = makeDraggableHook({\n    name: \"useNestedSortable\",\n    acceptedParams: {\n        groups: [String, Function],\n        connectGroups: [Boolean, Function],\n        nest: [Boolean],\n        listTagName: [String],\n        nestInterval: [Number],\n        maxLevels: [Number],\n        isAllowed: [Function],\n        useElementSize: [Boolean],\n    },\n    defaultParams: {\n        connectGroups: false,\n        currentGroup: null,\n        cursor: \"grabbing\",\n        edgeScrolling: { speed: 20, threshold: 60 },\n        elements: \"li\",\n        groupSelector: null,\n        nest: false,\n        listTagName: \"ul\",\n        nestInterval: 15,\n        maxLevels: 0,\n        isAllowed: (ctx) => true,\n        useElementSize: false,\n    },\n\n    // Set the parameters.\n    onComputeParams({ ctx, params }) {\n        // Group selector\n        ctx.groupSelector = params.groups || null;\n        if (ctx.groupSelector) {\n            ctx.fullSelector = [ctx.groupSelector, ctx.fullSelector].join(\" \");\n        }\n        // Connection across groups\n        ctx.connectGroups = params.connectGroups;\n        // Nested elements\n        ctx.nest = params.nest;\n        // List tag name\n        ctx.listTagName = params.listTagName;\n        // Horizontal distance needed to trigger a change in the list hierarchy\n        // (i.e. changing parent when moving horizontally)\n        ctx.nestInterval = params.nestInterval;\n        ctx.isRTL = localization.direction === \"rtl\";\n        ctx.maxLevels = params.maxLevels || 0;\n        ctx.isAllowed = params.isAllowed ?? (() => true);\n        ctx.useElementSize = params.useElementSize;\n    },\n\n    // Set the current group and create the placeholder row that will take the\n    // place of the moving row.\n    onWillStartDrag({ ctx, addCleanup }) {\n        if (ctx.groupSelector) {\n            ctx.currentGroup = ctx.current.element.closest(ctx.groupSelector);\n            if (!ctx.connectGroups) {\n                ctx.current.container = ctx.currentGroup;\n            }\n        }\n\n        if (ctx.nest) {\n            ctx.prevNestX = ctx.pointer.x;\n        }\n        ctx.current.placeHolder = ctx.current.element.cloneNode(false);\n        ctx.current.placeHolder.removeAttribute(\"id\");\n        ctx.current.placeHolder.classList.add(\"w-100\", \"d-block\");\n        if (ctx.useElementSize) {\n            ctx.current.placeHolder.style.height = getComputedStyle(ctx.current.element).height;\n            ctx.current.placeHolder.classList.add(\"o_nested_sortable_placeholder_realsize\");\n        } else {\n            ctx.current.placeHolder.classList.add(\"o_nested_sortable_placeholder\");\n        }\n        addCleanup(() => ctx.current.placeHolder.remove());\n    },\n\n    // Make the placeholder take the place of the moving row, and add style on\n    // different elements to provide feedback that there is an ongoing dragging\n    // sequence.\n    onDragStart({ ctx, addStyle }) {\n        // Horizontal position which will be used to detect row changes when moving vertically, so that\n        // we do not need to be on the row to trigger row changes (only the vertical position matters).\n        // Nested rows are shorter than \"root\" rows, and do not start at the same horizontal position.\n        // However, every row ends at the same horizontal position. Therefore, we use the end of the\n        // current element - 1 as horizontal position.\n        ctx.selectorX = ctx.isRTL\n            ? ctx.current.elementRect.left + 1\n            : ctx.current.elementRect.right - 1;\n\n        // Placeholder is initially added right after the current element.\n        ctx.current.element.after(ctx.current.placeHolder);\n        addStyle(ctx.current.element, { opacity: 0.5 });\n\n        // Remove pointer-events style added by draggable_hook_builder and set\n        // it on the view elements instead as in our case we want to show the\n        // ctx.cursor style on the whole screen, not only in the ref el.\n        addStyle(document.body, { \"pointer-events\": \"auto\" });\n        addStyle(document.querySelector(\".o_navbar\"), { \"pointer-events\": \"none\" });\n        addStyle(document.querySelector(\".o_action_manager\"), { \"pointer-events\": \"none\" });\n        addStyle(ctx.current.container, { \"pointer-events\": \"auto\" });\n\n        // Calls \"onDragStart\" handler\n        return {\n            element: ctx.current.element,\n            group: ctx.currentGroup,\n        };\n    },\n    _getDeepestChildLevel(ctx, node, depth = 0) {\n        let result = 0;\n        const childSelector = `${ctx.listTagName} ${ctx.elementSelector}`;\n        for (const childNode of node.querySelectorAll(childSelector)) {\n            result = Math.max(this._getDeepestChildLevel(ctx, childNode, depth + 1), result);\n        }\n        return depth ? result + 1 : result;\n    },\n    _hasReachMaxAllowedLevel(ctx) {\n        if (!ctx.nest || ctx.maxLevels < 1) {\n            return false;\n        }\n        let level = this._getDeepestChildLevel(ctx, ctx.current.element);\n        let list = ctx.current.placeHolder.closest(ctx.listTagName);\n        while (list) {\n            level++;\n            list = list.parentNode.closest(ctx.listTagName);\n        }\n        return level > ctx.maxLevels;\n    },\n    _isAllowedNodeMove(ctx) {\n        return (\n            !this._hasReachMaxAllowedLevel(ctx) && ctx.isAllowed(ctx.current, ctx.elementSelector)\n        );\n    },\n    // Check if the cursor moved enough to trigger a move. If it did, move the\n    // placeholder accordingly.\n    onDrag({ ctx, callHandler }) {\n        const onMove = (prevPos) => {\n            if (!ctx.isAllowed(ctx.current, ctx.elementSelector)) {\n                ctx.current.placeHolder.classList.add(\"d-none\");\n                return;\n            } else if (this._hasReachMaxAllowedLevel(ctx)) {\n                // If the placeholder has reached its max allowed level, it is\n                // moved back to its previous position.\n                const previousSiblingEl = ctx.current.placeHolder\n                    .closest(ctx.listTagName)\n                    .closest(ctx.elementSelector);\n                previousSiblingEl.after(ctx.current.placeHolder);\n                return;\n            }\n            ctx.current.placeHolder.classList.remove(\"d-none\");\n            callHandler(\"onMove\", {\n                element: ctx.current.element,\n                previous: ctx.current.placeHolder.previousElementSibling,\n                next: ctx.current.placeHolder.nextElementSibling,\n                parent: ctx.nest\n                    ? ctx.current.placeHolder.parentElement.closest(ctx.elementSelector)\n                    : false,\n                group: ctx.currentGroup,\n                newGroup: ctx.connectGroups\n                    ? ctx.current.placeHolder.closest(ctx.groupSelector)\n                    : ctx.currentGroup,\n                prevPos,\n                placeholder: ctx.current.placeHolder,\n            });\n        };\n        /**\n         * Get the list element inside an element, or create one if it does not\n         * exists.\n         * @param {HTMLElement} el\n         * @return {HTMLElement} list\n         */\n        const getChildList = (el) => {\n            let list = el.querySelector(ctx.listTagName);\n            if (!list) {\n                list = document.createElement(ctx.listTagName);\n                el.appendChild(list);\n            }\n            return list;\n        };\n\n        const getPosition = (el) => {\n            return {\n                previous: el.previousElementSibling,\n                next: el.nextElementSibling,\n                parent: el.parentElement?.closest(ctx.elementSelector) || null,\n                group: ctx.groupSelector ? el.closest(ctx.groupSelector) : false,\n            };\n        };\n        const position = getPosition(ctx.current.placeHolder);\n\n        /** If nesting elements is allowed, horizontal moves may change the\n         * parent of the placeholder element (the placeholder does not move\n         * above or under an element, but it changes parent):\n         *\n         * - Moving to the left makes the placeholder a child of the previous\n         *   element up in the nested hierarchy, only if the placeholder is the\n         *   last child of its current parent:\n         *\n         *                    Allowed:\n         *    el                           el\n         *     \u2523 parent                     \u2523 parent\n         *     \u2503  \u2523 child           -->     \u2503  \u2517 child\n         *     \u2503  \u2517 placeholder             \u2523 placeholder\n         *     \u2517 el                         \u2517 el\n         *\n         *                  Not Allowed:\n         *    el                           el\n         *     \u2523 parent                     \u2523 parent\n         *     \u2503  \u2523 placeholder     -->     \u2523 p\u2503laceholder   <-- error\n         *     \u2503  \u2517 child                   \u2503  \u2517 child\n         *     \u2517 el                         \u2517 el\n         *\n         *\n         * - Moving to the right makes the placeholder the last child of the\n         * next element down in the nested hierarchy:\n         *\n         *    el                           el\n         *     \u2523 parent                    \u2523 parent\n         *     \u2503  \u2517 child           -->    \u2503  \u2523 child\n         *     \u2523 placeholder               \u2503  \u2517 placeholder\n         *     \u2517 el                        \u2517 el\n         */\n        if (ctx.nest) {\n            const xInterval = ctx.prevNestX - ctx.pointer.x;\n            if (ctx.nestInterval - (-1) ** ctx.isRTL * xInterval < 1) {\n                // Place placeholder after its parent in its parent's list only\n                // if the placeholder is the last child of its parent\n                // (ignoring the current element which is in the dom)\n                let nextElement = position.next;\n                if (nextElement === ctx.current.element) {\n                    nextElement = nextElement.nextElementSibling;\n                }\n                if (!nextElement) {\n                    const newSibling = position.parent;\n                    if (newSibling) {\n                        newSibling.after(ctx.current.placeHolder);\n                        onMove(position);\n                    }\n                }\n                // Recenter the pointer coordinates to this step\n                ctx.prevNestX = ctx.pointer.x;\n                return;\n            } else if (ctx.nestInterval + (-1) ** ctx.isRTL * xInterval < 1) {\n                // Place placeholder as the last child of its previous sibling,\n                // (ignoring the current element which is in the dom)\n                let parent = position.previous;\n                if (parent === ctx.current.element) {\n                    parent = parent.previousElementSibling;\n                }\n                if (parent && parent.matches(ctx.elementSelector)) {\n                    getChildList(parent).appendChild(ctx.current.placeHolder);\n                    onMove(position);\n                }\n                // Recenter the pointer coordinates to this step\n                ctx.prevNestX = ctx.pointer.x;\n                return;\n            }\n        }\n        const currentTop = ctx.pointer.y - ctx.current.offset.y;\n        const closestEl = document.elementFromPoint(ctx.selectorX, currentTop);\n        if (!closestEl) {\n            // Cursor outside of viewport\n            return;\n        }\n        const element = closestEl.closest(ctx.elementSelector);\n        // Vertical moves should move the placeholder element up or down.\n        if (element && element !== ctx.current.placeHolder) {\n            const elementPosition = getPosition(element);\n            const eRect = element.getBoundingClientRect();\n            const pos = ctx.current.placeHolder.compareDocumentPosition(element);\n            // Place placeholder before the hovered element in its parent's\n            // list. If the cursor is in the upper part of the element and\n            // if the placeholder is currently after or inside the hovered\n            // element. If the position is not allowed but nesting is allowed,\n            // place the placeholder as the last child of the previous sibling\n            // instead.\n            if (currentTop - eRect.y < 10) {\n                if (\n                    pos & Node.DOCUMENT_POSITION_PRECEDING &&\n                    (ctx.nest || elementPosition.parent === position.parent)\n                ) {\n                    element.before(ctx.current.placeHolder);\n                    onMove(position);\n                    // Recenter the pointer coordinates to this step\n                    ctx.prevNestX = ctx.pointer.x;\n                }\n            } else if (currentTop - eRect.y > 15 && pos === Node.DOCUMENT_POSITION_FOLLOWING) {\n                // Place placeholder after the hovered element in its parent's\n                // list if the cursor is not in the upper part of the\n                // element and if the placeholder is currently before the\n                // hovered element.\n                // If nesting is allowed and if the element has at least one\n                // child, place the placeholder above the first child of the\n                // hovered element instead.\n                if (ctx.nest) {\n                    const elementChildList = getChildList(element);\n                    if (elementChildList.querySelector(ctx.elementSelector)) {\n                        elementChildList.prepend(ctx.current.placeHolder);\n                        onMove(position);\n                    } else {\n                        element.after(ctx.current.placeHolder);\n                        onMove(position);\n                    }\n                    // Recenter the pointer coordinates to this step\n                    ctx.prevNestX = ctx.pointer.x;\n                } else if (elementPosition.parent === position.parent) {\n                    element.after(ctx.current.placeHolder);\n                    onMove(position);\n                }\n            }\n        } else {\n            const group = closestEl.closest(ctx.groupSelector);\n            if (group && group !== position.group && (ctx.nest || !position.parent)) {\n                if (\n                    group.compareDocumentPosition(position.group) ===\n                    Node.DOCUMENT_POSITION_PRECEDING\n                ) {\n                    getChildList(group).prepend(ctx.current.placeHolder);\n                    onMove(position);\n                } else {\n                    getChildList(group).appendChild(ctx.current.placeHolder);\n                    onMove(position);\n                }\n                // Recenter the pointer coordinates to this step\n                ctx.prevNestX = ctx.pointer.x;\n                callHandler(\"onGroupEnter\", { group, placeholder: ctx.current.placeHolder });\n                callHandler(\"onGroupLeave\", {\n                    group: position.group,\n                    placeholder: ctx.current.placeHolder,\n                });\n            }\n        }\n    },\n    // If the drop position is different from the starting position, run the\n    // onDrop handler from the parameters.\n    onDrop({ ctx }) {\n        if (!this._isAllowedNodeMove(ctx)) {\n            return;\n        }\n        const previous = ctx.current.placeHolder.previousElementSibling;\n        const next = ctx.current.placeHolder.nextElementSibling;\n        if (previous !== ctx.current.element && next !== ctx.current.element) {\n            return {\n                element: ctx.current.element,\n                group: ctx.currentGroup,\n                previous,\n                next,\n                newGroup: ctx.groupSelector && ctx.current.placeHolder.closest(ctx.groupSelector),\n                parent: ctx.current.placeHolder.parentElement.closest(ctx.elementSelector),\n                placeholder: ctx.current.placeHolder,\n            };\n        }\n    },\n    // Run the onDragEnd handler from the parameters.\n    onDragEnd({ ctx }) {\n        return {\n            element: ctx.current.element,\n            group: ctx.currentGroup,\n        };\n    },\n});\n", "import { localization as l10n } from \"@web/core/l10n/localization\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { intersperse } from \"@web/core/utils/strings\";\n\n/**\n * Returns value clamped to the inclusive range of min and max.\n *\n * @param {number} num\n * @param {number} min\n * @param {number} max\n * @returns {number}\n */\nexport function clamp(num, min, max) {\n    return Math.max(Math.min(num, max), min);\n}\n\n/**\n * A function to create flexibly-numbered lists of integers, handy for each and map loops.\n * step defaults to 1.\n * Returns a list of integers from start (inclusive) to stop (exclusive), incremented (or decremented) by step.\n * @param {number} start default 0\n * @param {number} stop\n * @param {number} step default 1\n * @returns {number[]}\n */\nexport function range(start, stop, step = 1) {\n    const array = [];\n    const nsteps = Math.floor((stop - start) / step);\n    for (let i = 0; i < nsteps; i++) {\n        array.push(start + step * i);\n    }\n    return array;\n}\n\n/**\n * Returns `value` rounded with `precision`, minimizing IEEE-754 floating point\n * representation errors, and applying the tie-breaking rule selected with\n * `method`, by default \"HALF-UP\" (away from zero).\n *\n * @param {number} value the value to be rounded\n * @param {number} precision a precision parameter. eg: 0.01 rounds to two digits.\n * @param {\"HALF-UP\" | \"HALF-DOWN\" | \"HALF-EVEN\" | \"UP\" | \"DOWN\"} [method=\"HALF-UP\"] the rounding method used:\n *    - \"HALF-UP\" rounds to the closest number with ties going away from zero.\n *    - \"HALF-DOWN\" rounds to the closest number with ties going towards zero.\n *    - \"HALF-EVEN\" rounds to the closest number with ties going to the closest even number.\n *    - \"UP\" always rounds away from 0.\n *    - \"DOWN\" always rounds towards 0.\n */\nexport function roundPrecision(value, precision, method = \"HALF-UP\") {\n    if (!value) {\n        return 0;\n    } else if (!precision || precision < 0) {\n        precision = 1;\n    }\n    let roundingFactor = precision;\n    let normalize = (val) => val / roundingFactor;\n    let denormalize = (val) => val * roundingFactor;\n    // inverting small rounding factors reduces rounding errors\n    if (roundingFactor < 1) {\n        roundingFactor = invertFloat(roundingFactor);\n        [normalize, denormalize] = [denormalize, normalize];\n    }\n    const normalizedValue = normalize(value);\n    const sign = Math.sign(normalizedValue);\n    const epsilonMagnitude = Math.log2(Math.abs(normalizedValue));\n    const epsilon = Math.pow(2, epsilonMagnitude - 50);\n    let roundedValue;\n\n    switch (method) {\n        case \"DOWN\": {\n            roundedValue = Math.trunc(normalizedValue + sign * epsilon);\n            break;\n        }\n        case \"HALF-DOWN\": {\n            roundedValue = Math.round(normalizedValue - sign * epsilon);\n            break;\n        }\n        case \"HALF-UP\": {\n            roundedValue = Math.round(normalizedValue + sign * epsilon);\n            break;\n        }\n        case \"HALF-EVEN\": {\n            const integral = Math.floor(normalizedValue);\n            const remainder = Math.abs(normalizedValue - integral);\n            const isHalf = Math.abs(0.5 - remainder) < epsilon;\n            roundedValue = isHalf ? integral + (integral & 1) : Math.round(normalizedValue);\n            break;\n        }\n        case \"UP\": {\n            roundedValue = Math.trunc(normalizedValue + sign * (1 - epsilon));\n            break;\n        }\n        default: {\n            throw new Error(`Unknown rounding method: ${method}`);\n        }\n    }\n\n    return denormalize(roundedValue);\n}\n\nfunction formatFixedDecimals(value, decimals) {\n    const rounded = roundDecimals(value, decimals);\n    const [intPart, decPart = \"\"] = rounded.toString().split(\".\");\n    const paddedDecimals = decPart.padEnd(decimals, \"0\").slice(0, decimals);\n    return decimals === 0 ? intPart : `${intPart}.${paddedDecimals}`;\n}\n\nexport function roundDecimals(value, decimals) {\n    /**\n     * The following decimals introduce numerical errors:\n     * Math.pow(10, -4) = 0.00009999999999999999\n     * Math.pow(10, -5) = 0.000009999999999999999\n     *\n     * Such errors will propagate in roundPrecision and lead to inconsistencies between Python\n     * and JavaScript. To avoid this, we parse the scientific notation.\n     */\n    return roundPrecision(value, parseFloat(\"1e\" + -decimals));\n}\n\n/**\n * @param {number} value\n * @param {integer} decimals\n * @returns {boolean}\n */\nexport function floatIsZero(value, decimals) {\n    return value === 0 || roundDecimals(value, decimals) === 0;\n}\n\n/**\n * Inserts \"thousands\" separators in the provided number.\n *\n * @param {string} string representing integer number\n * @param {string} [thousandsSep=\",\"] the separator to insert\n * @param {number[]} [grouping=[]]\n *   array of relative offsets at which to insert `thousandsSep`.\n *   See `strings.intersperse` method.\n * @returns {string}\n */\nexport function insertThousandsSep(number, thousandsSep = \",\", grouping = []) {\n    const negative = number[0] === \"-\";\n    number = negative ? number.slice(1) : number;\n    return (negative ? \"-\" : \"\") + intersperse(number, grouping, thousandsSep);\n}\n\n/**\n * Format a number to a human readable format. For example, 3000 could become 3k.\n * Or massive number can use the scientific exponential notation.\n *\n * @param {number} number to format\n * @param {Object} [options] Options to format\n * @param {number} [options.decimals=0] number of decimals to use\n *    if minDigits > 1 is used and effective on the number then decimals\n *    will be shrunk to zero, to avoid displaying irrelevant figures ( 0.01 compared to 1000 )\n * @param {number} [options.minDigits=1]\n *    the minimum number of digits to preserve when switching to another\n *    level of thousands (e.g. with a value of '2', 4321 will still be\n *    represented as 4321 otherwise it will be down to one digit (4k))\n * @returns {string}\n */\nexport function humanNumber(number, options = { decimals: 0, minDigits: 1 }) {\n    const decimals = options.decimals || 0;\n    const minDigits = options.minDigits || 1;\n    const d2 = Math.pow(10, decimals);\n    const numberMagnitude = +number.toExponential().split(\"e+\")[1];\n    number = Math.round(number * d2) / d2;\n    // the case numberMagnitude >= 21 corresponds to a number\n    // better expressed in the scientific format.\n    if (numberMagnitude >= 21) {\n        // we do not use number.toExponential(decimals) because we want to\n        // avoid the possible useless O decimals: 1e.+24 preferred to 1.0e+24\n        number = Math.round(number * Math.pow(10, decimals - numberMagnitude)) / d2;\n        return `${number}e+${numberMagnitude}`;\n    }\n    // note: we need to call toString here to make sure we manipulate the resulting\n    // string, not an object with a toString method.\n    const unitSymbols = _t(\"kMGTPE\").toString();\n    const sign = Math.sign(number);\n    number = Math.abs(number);\n    let symbol = \"\";\n    for (let i = unitSymbols.length; i > 0; i--) {\n        const s = Math.pow(10, i * 3);\n        if (s <= number / Math.pow(10, minDigits - 1)) {\n            number = Math.round((number * d2) / s) / d2;\n            symbol = unitSymbols[i - 1];\n            break;\n        }\n    }\n    const { decimalPoint, grouping, thousandsSep } = l10n;\n\n    // determine if we should keep the decimals (we don't want to display 1,020.02k for 1020020)\n    const decimalsToKeep = number >= 1000 ? 0 : decimals;\n    number = sign * number;\n    const [integerPart, decimalPart] = formatFixedDecimals(number, decimalsToKeep).split(\".\");\n    const int = insertThousandsSep(integerPart, thousandsSep, grouping);\n    if (!decimalPart) {\n        return int + symbol;\n    }\n    return int + decimalPoint + decimalPart + symbol;\n}\n\n/**\n * Returns a string representing a float.  The result takes into account the\n * user settings (to display the correct decimal separator).\n *\n * @param {number} value the value that should be formatted\n * @param {Object} [options]\n * @param {number[]} [options.digits] the number of digits that should be used,\n *   instead of the default digits precision in the field.\n * @param {boolean} [options.humanReadable] if true, large numbers are formatted\n *   to a human readable format.\n * @param {string} [options.decimalPoint] decimal separating character\n * @param {string} [options.thousandsSep] thousands separator to insert\n * @param {number[]} [options.grouping] array of relative offsets at which to\n *   insert `thousandsSep`. See `insertThousandsSep` method.\n * @param {number} [options.decimals] used for humanNumber formmatter\n * @param {boolean} [options.trailingZeros=true] if false, the decimal part\n *   won't contain unnecessary trailing zeros.\n * @returns {string}\n */\nexport function formatFloat(value, options = {}) {\n    let precision;\n    if (options.digits && options.digits[1] !== undefined) {\n        precision = options.digits[1];\n    } else {\n        precision = 2;\n    }\n    if (floatIsZero(value, precision)) {\n        value = 0.0;\n    }\n    if (options.humanReadable) {\n        return humanNumber(value, options);\n    }\n    const grouping = options.grouping || l10n.grouping;\n    const thousandsSep = \"thousandsSep\" in options ? options.thousandsSep : l10n.thousandsSep;\n    const decimalPoint = \"decimalPoint\" in options ? options.decimalPoint : l10n.decimalPoint;\n    const formatted = formatFixedDecimals(value, precision).split(\".\");\n    formatted[0] = insertThousandsSep(formatted[0], thousandsSep, grouping);\n    if (options.trailingZeros === false && formatted[1]) {\n        formatted[1] = formatted[1].replace(/0+$/, \"\");\n    }\n    return formatted[1] ? formatted.join(decimalPoint) : formatted[0];\n}\n\nconst _INVERTDICT = Object.freeze({\n    1e-1: 1e1,\n    1e-2: 1e2,\n    1e-3: 1e3,\n    1e-4: 1e4,\n    1e-5: 1e5,\n    1e-6: 1e6,\n    1e-7: 1e7,\n    1e-8: 1e8,\n    1e-9: 1e9,\n    1e-10: 1e10,\n    2e-1: 5,\n    2e-2: 5e1,\n    2e-3: 5e2,\n    2e-4: 5e3,\n    2e-5: 5e4,\n    2e-6: 5e5,\n    2e-7: 5e6,\n    2e-8: 5e7,\n    2e-9: 5e8,\n    2e-10: 5e9,\n    5e-1: 2,\n    5e-2: 2e1,\n    5e-3: 2e2,\n    5e-4: 2e3,\n    5e-5: 2e4,\n    5e-6: 2e5,\n    5e-7: 2e6,\n    5e-8: 2e7,\n    5e-9: 2e8,\n    5e-10: 2e9,\n});\n\n/**\n * Invert a number with increased accuracy.\n *\n * @param {number} value\n * @returns {number}\n */\nexport function invertFloat(value) {\n    let res = _INVERTDICT[value];\n    if (res === undefined) {\n        const [coeff, expt] = value.toExponential().split(\"e\").map(Number.parseFloat);\n        res = Number.parseFloat(`${coeff}e${-expt}`) / Math.pow(coeff, 2);\n    }\n    return res;\n}\n", "/**\n * Shallow compares two objects.\n *\n * @template {unknown} T\n * @param {T} obj1\n * @param {T} obj2\n * @param {(a: T[keyof T], b: T[keyof T]) => boolean} [comparisonFn]\n */\nexport function shallowEqual(obj1, obj2, comparisonFn = (a, b) => a === b) {\n    if (obj1 !== Object(obj1) || obj2 !== Object(obj2)) {\n        return obj1 === obj2;\n    }\n    const obj1Keys = Reflect.ownKeys(obj1);\n    return (\n        obj1Keys.length === Reflect.ownKeys(obj2).length &&\n        obj1Keys.every((key) => comparisonFn(obj1[key], obj2[key]))\n    );\n}\n\n/**\n * Deeply compares two objects.\n *\n * @template {unknown} T\n * @param {T} obj1\n * @param {T} obj2\n */\nexport const deepEqual = (obj1, obj2) => shallowEqual(obj1, obj2, deepEqual);\n\n/**\n * Deep copies an object. As it relies on JSON this function as some limitations\n * - no support for circular objects\n * - no support for specific classes, that will at best be lost and at worst crash (Map, Set etc...)\n * @template T\n * @param {T} object An object that is fully JSON stringifiable\n * @return {T}\n */\nexport function deepCopy(object) {\n    return object && JSON.parse(JSON.stringify(object));\n}\n\n/**\n * Returns whether the given value is an object, i.e. an instance of the `Object`\n * class or of one of its direct subclass.\n *\n * Note: this may wrongly validate any object implementing a modified `toString`\n * explicitly returning `\"[object Object]\"`.\n *\n * @param {unknown} value\n * @returns {boolean}\n * @example\n *  // true\n *  isObject({ a: 1 });\n *  isObject(Object.create(null));\n * @example\n *  // false\n *  isObject([1, 2, 3]);\n *  isObject(new Map([[\"a\", 1]]));\n */\nexport function isObject(value) {\n    return Object.prototype.toString.call(value) === \"[object Object]\";\n}\n\n/**\n * Returns a shallow copy of object with every property in properties removed\n * if present in object.\n *\n * @template T\n * @template {keyof T} K\n * @param {T} object\n * @param {...(K)} properties\n */\nexport function omit(object, ...properties) {\n    /** @type {Omit<T, K>} */\n    const result = {};\n    const propertiesSet = new Set(properties);\n    for (const key in object) {\n        if (!propertiesSet.has(key)) {\n            result[key] = object[key];\n        }\n    }\n    return result;\n}\n\n/**\n * @template T\n * @template {keyof T} K\n * @param {T} object\n * @param {...(K)} properties\n * @returns {Pick<T, K>}\n */\nexport function pick(object, ...properties) {\n    return Object.fromEntries(\n        properties.filter((prop) => prop in object).map((prop) => [prop, object[prop]])\n    );\n}\n\n/**\n * Deeply merges two objects, recursively combining properties.\n * Works like the spread operator but will merge nested objects.\n *\n * This function doesn't merge arrays.\n *\n * @param {Object} target - The target object to merge into.\n * @param {Object} extension - The extension to apply.\n * @returns {Object} - The merged object.\n *\n * @example\n * const target = { a: 1, b: { c: 2 } };\n * const source = { a: 2, b: { d: 3 } };\n * const output = deepMerge(target, source);\n * // output => { a: 2, b: { c: 2, d: 3 } }\n */\nexport function deepMerge(target, extension) {\n    if (!isObject(target) && !isObject(extension)) {\n        return;\n    }\n\n    target = target || {};\n    const output = Object.assign({}, target);\n    if (isObject(extension)) {\n        for (const key of Reflect.ownKeys(extension)) {\n            if (\n                key in target &&\n                isObject(extension[key]) &&\n                !Array.isArray(extension[key]) &&\n                typeof extension[key] !== \"function\"\n            ) {\n                output[key] = deepMerge(target[key], extension[key]);\n            } else {\n                Object.assign(output, { [key]: extension[key] });\n            }\n        }\n    }\n\n    return output;\n}\n", "/**\n *  @typedef {{\n *      originalProperties: Map<string, PropertyDescriptor>;\n *      skeleton: object;\n *      extensions: Set<object>;\n *  }} PatchDescription\n */\n\n/** @type {WeakMap<object, PatchDescription>} */\nconst patchDescriptions = new WeakMap();\n\n/**\n * Create or get the patch description for the given `objToPatch`.\n * @param {object} objToPatch\n * @returns {PatchDescription}\n */\nfunction getPatchDescription(objToPatch) {\n    if (!patchDescriptions.has(objToPatch)) {\n        patchDescriptions.set(objToPatch, {\n            originalProperties: new Map(),\n            skeleton: Object.create(Object.getPrototypeOf(objToPatch)),\n            extensions: new Set(),\n        });\n    }\n    return patchDescriptions.get(objToPatch);\n}\n\n/**\n * @param {object} objToPatch\n * @returns {boolean}\n */\nfunction isClassPrototype(objToPatch) {\n    // class A {}\n    // isClassPrototype(A) === false\n    // isClassPrototype(A.prototype) === true\n    // isClassPrototype(new A()) === false\n    // isClassPrototype({}) === false\n    return (\n        Object.hasOwn(objToPatch, \"constructor\") && objToPatch.constructor?.prototype === objToPatch\n    );\n}\n\n/**\n * Traverse the prototype chain to find a potential property.\n * @param {object} objToPatch\n * @param {string} key\n * @returns {object}\n */\nfunction findAncestorPropertyDescriptor(objToPatch, key) {\n    let descriptor = null;\n    let prototype = objToPatch;\n    do {\n        descriptor = Object.getOwnPropertyDescriptor(prototype, key);\n        prototype = Object.getPrototypeOf(prototype);\n    } while (!descriptor && prototype);\n    return descriptor;\n}\n\n/**\n * Patch an object\n *\n * If the intent is to patch a class, don't forget to patch the prototype, unless\n * you want to patch static properties/methods.\n *\n * @template T\n * @template {Partial<T>} U\n * @param {T} objToPatch The object to patch\n * @param {U} extension The object containing the patched properties\n * @returns {() => void} Returns an unpatch function\n */\nexport function patch(objToPatch, extension) {\n    if (typeof extension === \"string\") {\n        throw new Error(\n            `Patch \"${extension}\": Second argument is not the patch name anymore, it should be the object containing the patched properties`\n        );\n    }\n\n    const description = getPatchDescription(objToPatch);\n    description.extensions.add(extension);\n\n    const properties = Object.getOwnPropertyDescriptors(extension);\n    for (const [key, newProperty] of Object.entries(properties)) {\n        const oldProperty = Object.getOwnPropertyDescriptor(objToPatch, key);\n        if (oldProperty) {\n            // Store the old property on the skeleton.\n            Object.defineProperty(description.skeleton, key, oldProperty);\n        }\n\n        if (!description.originalProperties.has(key)) {\n            // Keep a trace of original property (prop before first patch), useful for unpatching.\n            description.originalProperties.set(key, oldProperty);\n        }\n\n        if (isClassPrototype(objToPatch)) {\n            // A property is enumerable on POJO ({ prop: 1 }) but not on classes (class A {}).\n            // Here, we only check if we patch a class prototype.\n            newProperty.enumerable = false;\n        }\n\n        if ((newProperty.get && 1) ^ (newProperty.set && 1)) {\n            // get and set are defined together. If they are both defined\n            // in the previous descriptor but only one in the new descriptor\n            // then the other will be undefined so we need to apply the\n            // previous descriptor in the new one.\n            const ancestorProperty = findAncestorPropertyDescriptor(objToPatch, key);\n            newProperty.get = newProperty.get ?? ancestorProperty?.get;\n            newProperty.set = newProperty.set ?? ancestorProperty?.set;\n        }\n\n        // Replace the old property by the new one.\n        Object.defineProperty(objToPatch, key, newProperty);\n    }\n\n    // Sets the current skeleton as the extension's prototype to make\n    // `super` keyword working and then set extension as the new skeleton.\n    description.skeleton = Object.setPrototypeOf(extension, description.skeleton);\n\n    return () => {\n        // Remove the description to start with a fresh base.\n        patchDescriptions.delete(objToPatch);\n\n        for (const [key, property] of description.originalProperties) {\n            if (property) {\n                // Restore the original property on the `objToPatch` object.\n                Object.defineProperty(objToPatch, key, property);\n            } else {\n                // Or remove the property if it did not exist at first.\n                delete objToPatch[key];\n            }\n        }\n\n        // Re-apply the patches without the current one.\n        description.extensions.delete(extension);\n        for (const extension of description.extensions) {\n            patch(objToPatch, extension);\n        }\n    };\n}\n", "import { isMobileOS } from \"@web/core/browser/feature_detection\";\nimport { loadJS } from \"@web/core/assets\";\n\n/**\n * Until we have our own implementation of the /web/static/lib/pdfjs/web/viewer.{html,js,css}\n * (currently based on Firefox), this method allows us to hide the buttons that we do not want:\n * * All edit buttons\n * * \"Open File\"\n * * \"Current Page\" (\"#viewBookmark\")\n * * \"Download\" (Hidden on mobile device like Android, iOS, ... or via option)\n * * \"Print\" (Hidden on mobile device like Android, iOS, ... or via option)\n * * \"Presentation\" (via options)\n * * \"Rotation\" (via options)\n *\n * @link https://mozilla.github.io/pdf.js/getting_started/\n *\n * @param {Element} rootElement IFRAME DOM element of PDF.js viewer\n * @param {Object} options options to hide additional buttons\n * @param {boolean} options.hideDownload hide download button\n * @param {boolean} options.hidePrint hide print button\n * @param {boolean} options.hidePresentation hide presentation button\n * @param {boolean} options.hideRotation hide rotation button\n */\nexport function hidePDFJSButtons(rootElement, options = {}) {\n    const hiddenElements = [\n        \"#editorModeButtons\",\n        \"button#openFile\",\n        \"button#secondaryOpenFile\",\n        \"a#viewBookmark\",\n        \"a#secondaryViewBookmark\",\n    ];\n    if (options.hideDownload || isMobileOS()) {\n        hiddenElements.push([\"button#downloadButton\", \"button#secondaryDownload\"]);\n    }\n    if (options.hidePrint || isMobileOS()) {\n        hiddenElements.push([\"button#printButton\", \"button#secondaryPrint\"]);\n    }\n    if (options.hidePresentation) {\n        hiddenElements.push(\"button#presentationMode\");\n    }\n    if (options.hideRotation) {\n        hiddenElements.push(\"button#pageRotateCw\");\n        hiddenElements.push(\"button#pageRotateCcw\");\n    }\n    const cssStyle = document.createElement(\"style\");\n    cssStyle.rel = \"stylesheet\";\n    cssStyle.textContent = `${hiddenElements.join(\", \")} {\n    display: none !important;\n}`;\n    const iframe =\n        rootElement.tagName === \"IFRAME\" ? rootElement : rootElement.querySelector(\"iframe\");\n    if (iframe) {\n        if (!iframe.dataset.hideButtons) {\n            iframe.dataset.hideButtons = \"true\";\n            iframe.addEventListener(\"load\", (event) => {\n                if (iframe.contentDocument && iframe.contentDocument.head) {\n                    iframe.contentDocument.head.appendChild(cssStyle);\n                }\n            });\n        }\n    } else {\n        console.warn(\"No IFRAME found\");\n    }\n}\n\nexport async function loadPDFJSAssets() {\n    return Promise.all([\n        loadJS(\"/web/static/lib/pdfjs/build/pdf.js\"),\n        loadJS(\"/web/static/lib/pdfjs/build/pdf.worker.js\"),\n    ]);\n}\n", "import { reactive } from \"@odoo/owl\";\n\n/**\n * This class should be used as a base when creating a class that is intended to\n * be used within the reactivity system, it avoids a specific class of bug where\n * callbacks that capture `this` declared in the constructor would escape the\n * reactivity system and prevent the observers from being notified:\n *\n * const bus = new EventBus();\n * class MyClass {\n *   constructor() {\n *     this.counter = 0;\n *     bus.addEventListener(\"change\", () => this.counter++);\n *     //                                   ^ Will never be reactive, this mutation will be missed\n *   }\n * }\n * const myObj = reactive(new MyClass(bus), () => console.log(myObj.counter));\n * myObj.counter++; // logs 0;\n * bus.trigger(\"change\"); // logs nothing!\n * myObj.counter++; // logs 2. counter == 1 was missed.\n */\nexport class Reactive {\n    constructor() {\n        return reactive(this);\n    }\n}\n\n/**\n * Creates a side-effect that runs based on the content of reactive objects.\n *\n * @template {object[]} T\n * @param {(...args: [...T]) => X} cb callback for the effect\n * @param {[...T]} deps the reactive objects that the effect depends on\n */\nexport function effect(cb, deps) {\n    const reactiveDeps = reactive(deps, () => {\n        cb(...reactiveDeps);\n    });\n    cb(...reactiveDeps);\n}\n\n/**\n * Adds computed properties to a reactive object derived from multiples sources.\n *\n * @template {object} T\n * @template {object[]} U\n * @template {{[key: string]: (this: T, ...rest: [...U]) => unknown}} V\n * @param {T} obj the reactive object on which to add the computed\n * properties\n * @param {[...U]} sources the reactive objects which are needed to compute\n * the properties\n * @param {V} descriptor the object containing methods to compute the\n * properties\n * @returns {T & {[key in keyof V]: ReturnType<V[key]>}}\n */\nexport function withComputedProperties(obj, sources, descriptor) {\n    for (const [key, compute] of Object.entries(descriptor)) {\n        effect(\n            (obj, sources) => {\n                obj[key] = compute.call(obj, ...sources);\n            },\n            [obj, sources]\n        );\n    }\n    return obj;\n}\n", "import { App, blockDom, Component, markup } from \"@odoo/owl\";\nimport { getTemplate } from \"@web/core/templates\";\nimport { appTranslateFn } from \"@web/core/l10n/translation\";\n\nexport function renderToElement(template, context = {}) {\n    const el = render(template, context).firstElementChild;\n    if (el?.nextElementSibling) {\n        throw new Error(\n            `The rendered template '${template}' contains multiple root ` +\n                `nodes that will be ignored using renderToElement, you should ` +\n                `consider using renderToFragment or refactoring the template.`\n        );\n    }\n    el?.remove();\n    return el;\n}\n\nexport function renderToFragment(template, context = {}) {\n    const frag = document.createDocumentFragment();\n    for (const el of [...render(template, context).children]) {\n        frag.appendChild(el);\n    }\n    return frag;\n}\n\n/**\n * renders a template with an (optional) context and outputs it as a string\n *\n * @param {string} template\n * @param {Object} context\n * @returns string: the html of the template\n */\nexport function renderToString(template, context = {}) {\n    return render(template, context).innerHTML;\n}\nlet app;\nObject.defineProperty(renderToString, \"app\", {\n    get: () => {\n        if (!app) {\n            app = new App(Component, {\n                name: \"renderToString\",\n                getTemplate,\n                translatableAttributes: [\"data-tooltip\"],\n                translateFn: appTranslateFn,\n            });\n        }\n        return app;\n    },\n});\n\nfunction render(template, context = {}) {\n    const app = renderToString.app;\n    const templateFn = app.getTemplate(template);\n    const bdom = templateFn(context, {});\n    const div = document.createElement(\"div\");\n    blockDom.mount(bdom, div);\n    return div;\n}\n\n/**\n * renders a template with an (optional) context and returns a Markup string,\n * suitable to be inserted in a template with a t-out directive\n *\n * @param {string} template\n * @param {Object} context\n * @returns {ReturnType<markup>} the html of the template, as a markup string\n */\nexport function renderToMarkup(template, context = {}) {\n    return markup(renderToString(template, context));\n}\n", "export function isScrollableX(el) {\n    if (el.scrollWidth > el.clientWidth && el.clientWidth > 0) {\n        return couldBeScrollableX(el);\n    }\n    return false;\n}\n\nexport function couldBeScrollableX(el) {\n    if (el) {\n        const overflow = getComputedStyle(el).getPropertyValue(\"overflow-x\");\n        if (/\\bauto\\b|\\bscroll\\b/.test(overflow)) {\n            return true;\n        }\n    }\n    return false;\n}\n\n/**\n * Get the closest horizontally scrollable for a given element.\n *\n * @param {HTMLElement} el\n * @returns {HTMLElement | null}\n */\nexport function closestScrollableX(el) {\n    if (!el) {\n        return null;\n    }\n    if (isScrollableX(el)) {\n        return el;\n    }\n    return closestScrollableX(el.parentElement);\n}\n\nexport function isScrollableY(el) {\n    if (el && el.scrollHeight > el.clientHeight && el.clientHeight > 0) {\n        return couldBeScrollableY(el);\n    }\n    return false;\n}\n\nexport function couldBeScrollableY(el) {\n    if (el) {\n        const overflow = getComputedStyle(el).getPropertyValue(\"overflow-y\");\n        if (/\\bauto\\b|\\bscroll\\b/.test(overflow)) {\n            return true;\n        }\n    }\n    return false;\n}\n\n/**\n * Get the closest vertically scrollable for a given element.\n *\n * @param {HTMLElement} el\n * @returns {HTMLElement | null}\n */\nexport function closestScrollableY(el) {\n    if (!el) {\n        return null;\n    }\n    if (isScrollableY(el)) {\n        return el;\n    }\n    return closestScrollableY(el.parentElement);\n}\n\n/**\n * Ensures that `element` will be visible in its `scrollable`.\n *\n * @param {HTMLElement} element\n * @param {object} options\n * @param {HTMLElement} [options.scrollable] a scrollable area\n * @param {boolean} [options.isAnchor] states if the scroll is to an anchor\n * @param {string} [options.behavior] \"smooth\", \"instant\", \"auto\" <=> undefined\n *        @url https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollTo#behavior\n * @param {number} [options.offset] applies a vertical offset\n */\nexport function scrollTo(element, options = {}) {\n    const { behavior = \"auto\", isAnchor = false, offset = 0 } = options;\n    const scrollable = closestScrollableY(options.scrollable || element.parentElement);\n    if (!scrollable) {\n        return;\n    }\n\n    const scrollBottom = scrollable.getBoundingClientRect().bottom;\n    const scrollTop = scrollable.getBoundingClientRect().top;\n    const elementBottom = element.getBoundingClientRect().bottom;\n    const elementTop = element.getBoundingClientRect().top;\n\n    const scrollPromises = [];\n\n    if (elementBottom > scrollBottom && !isAnchor) {\n        // The scroll place the element at the bottom border of the scrollable\n        scrollPromises.push(\n            new Promise((resolve) => {\n                scrollable.addEventListener(\"scrollend\", () => resolve(), { once: true });\n            })\n        );\n\n        scrollable.scrollTo({\n            top:\n                scrollable.scrollTop +\n                elementTop -\n                scrollBottom +\n                Math.ceil(element.getBoundingClientRect().height) +\n                offset,\n            behavior,\n        });\n    } else if (elementTop < scrollTop || isAnchor) {\n        // The scroll place the element at the top of the scrollable\n        scrollPromises.push(\n            new Promise((resolve) => {\n                scrollable.addEventListener(\"scrollend\", () => resolve(), { once: true });\n            })\n        );\n\n        scrollable.scrollTo({\n            top: scrollable.scrollTop - scrollTop + elementTop + offset,\n            behavior,\n        });\n\n        if (options.isAnchor) {\n            // If the scrollable is within a scrollable, another scroll should be done\n            const parentScrollable = closestScrollableY(scrollable.parentElement);\n            if (parentScrollable) {\n                scrollPromises.push(\n                    scrollTo(scrollable, {\n                        behavior,\n                        isAnchor: true,\n                        scrollable: parentScrollable,\n                    })\n                );\n            }\n        }\n    }\n\n    return Promise.all(scrollPromises);\n}\n\nexport function compensateScrollbar(\n    el,\n    add = true,\n    isScrollElement = true,\n    cssProperty = \"padding-right\"\n) {\n    if (!el) {\n        return;\n    }\n    // Compensate scrollbar\n    const scrollableEl = isScrollElement ? el : closestScrollableY(el.parentElement);\n    if (!scrollableEl) {\n        return;\n    }\n    const isRTL = scrollableEl.classList.contains(\".o_rtl\");\n    if (isRTL) {\n        cssProperty = cssProperty.replace(\"right\", \"left\");\n    }\n    el.style.removeProperty(cssProperty);\n    if (!add) {\n        return;\n    }\n    const style = window.getComputedStyle(el);\n    // Round up to the nearest integer to be as close as possible to\n    // the correct value in case of browser zoom.\n    const borderLeftWidth = Math.ceil(parseFloat(style.borderLeftWidth.replace(\"px\", \"\")));\n    const borderRightWidth = Math.ceil(parseFloat(style.borderRightWidth.replace(\"px\", \"\")));\n    const bordersWidth = borderLeftWidth + borderRightWidth;\n    const newValue =\n        parseInt(style[cssProperty]) +\n        scrollableEl.offsetWidth -\n        scrollableEl.clientWidth -\n        bordersWidth;\n    el.style.setProperty(cssProperty, `${newValue}px`, \"important\");\n}\n\nexport function getScrollingElement(document = window.document) {\n    const baseScrollingElement = document.scrollingElement;\n    if (isScrollableY(baseScrollingElement)) {\n        return baseScrollingElement;\n    }\n    const bodyHeight = window.getComputedStyle(document.body).height;\n    for (const el of document.body.children) {\n        // Search for a body child which is at least as tall as the body\n        // and which has the ability to scroll if enough content in it. If\n        // found, suppose this is the top scrolling element.\n        if (bodyHeight - el.scrollHeight > 1.5) {\n            continue;\n        }\n        if (isScrollableY(el)) {\n            return el;\n        }\n    }\n    return baseScrollingElement;\n}\n\nexport function getScrollingTarget(scrollingElement = window.document) {\n    const document = scrollingElement.ownerDocument;\n    return scrollingElement === document.scrollingElement ? document.defaultView : scrollingElement;\n}\n", "import { normalize } from \"@web/core/l10n/utils\";\n\n/**\n * @param {string} pattern\n * @param {string|string[]} strs\n * @returns {number}\n */\nfunction match(pattern, strs) {\n    if (!Array.isArray(strs)) {\n        strs = [strs];\n    }\n    let globalScore = 0;\n    for (const str of strs) {\n        globalScore = Math.max(globalScore, _match(pattern, str));\n    }\n    return globalScore;\n}\n\n/**\n * This private function computes a score that represent the fact that the\n * string contains the pattern, or not\n *\n * - If the score is 0, the string does not contain the letters of the pattern in\n *   the correct order.\n * - if the score is > 0, it actually contains the letters.\n *\n * Better matches will get a higher score: consecutive letters are better,\n * and a match closer to the beginning of the string is also scored higher.\n *\n * @param {string} pattern\n * @param {string} str\n * @returns {number}\n */\nfunction _match(pattern, str) {\n    let totalScore = 0;\n    let currentScore = 0;\n    let patternIndex = 0;\n\n    pattern = normalize(pattern);\n    str = normalize(str);\n\n    const len = str.length;\n\n    for (let i = 0; i < len; i++) {\n        if (str[i] === pattern[patternIndex]) {\n            patternIndex++;\n            currentScore += 100 + currentScore - i / 200;\n        } else {\n            currentScore = 0;\n        }\n        totalScore = totalScore + currentScore;\n    }\n\n    return patternIndex === pattern.length ? totalScore : 0;\n}\n\n/**\n * Return a list of things that matches a pattern, ordered by their 'score' (\n * higher score first). An higher score means that the match is better. For\n * example, consecutive letters are considered a better match.\n *\n * @template T\n * @param {string} pattern\n * @param {T[]} list\n * @param {(element: T) => (string|string[])} fn\n * @returns {T[]}\n */\nexport function fuzzyLookup(pattern, list, fn) {\n    const results = [];\n    list.forEach((data) => {\n        const score = match(pattern, fn(data));\n        if (score > 0) {\n            results.push({ score, elem: data });\n        }\n    });\n\n    // we want better matches first\n    results.sort((a, b) => b.score - a.score);\n\n    return results.map((r) => r.elem);\n}\n\n// Does `pattern` fuzzy match `string`?\n/**\n * @param {string} pattern\n * @param {string} string\n * @returns {boolean}\n */\nexport function fuzzyTest(pattern, string) {\n    return _match(pattern, string) !== 0;\n}\n\n/**\n * Performs fuzzy matching using a Levenshtein distance algorithm\n * to find matches within an error margin between a pattern\n * and a list of words.\n *\n * If the pattern is found directly inside an item,\n * it's treated as a perfect match (score 0).\n * Otherwise, the `getScore` function calculates the distance\n * between the pattern and each candidate\n *\n * @param {string} pattern - The string to match.\n * @param {string[]} list - The list of strings to compare against the pattern.\n * @param {number} errorRatio - Controls how many errors can a word have depending of its length.\n * @returns {string[]} The list of the words that matches within a defined number of errors.\n */\nexport function fuzzyLevenshteinLookup(pattern, list, errorRatio = 3) {\n    // We limit the maximum number of errors depending on the word length\n    // to not have \"overcorrections\" into words that doesn't have anything\n    // in common with what the user typed\n    const maxNbrCorrection = Math.round(pattern.length / errorRatio);\n    const results = [];\n    list.forEach((candidate) => {\n        let score = -1;\n        if (candidate.includes(pattern)) {\n            score = 0;\n            results.push({ score, elem: pattern });\n        } else {\n            score = getLevenshteinScore(pattern, candidate);\n            if (score >= 0 && score <= maxNbrCorrection) {\n                results.push({ score, elem: candidate });\n            }\n        }\n    });\n    results.sort((a, b) => a.score - b.score);\n    return results.map((r) => r.elem);\n}\n\n\n/**\n * Computes the Levenshtein distance between two strings.\n *\n * @param {string} a\n * @param {string} b\n * @returns {number} The Levenshtein distance between `a` and `b`.\n */\nfunction getLevenshteinScore(a, b) {\n    let aLength = a.length;\n    let bLength = b.length;\n\n    let distanceMatrix = [];\n    for (let i = 0; i <= aLength; i++) {\n        distanceMatrix[i] = [];\n        for (let j = 0; j <= bLength; j++) {\n            distanceMatrix[i][j] = 0;\n        }\n    }\n\n    for (let i = 0; i <= aLength; i++) {\n        for (let j = 0; j <= bLength; j++) {\n            if (Math.min(i, j) === 0) {\n                distanceMatrix[i][j] = Math.max(i, j);\n            } else {\n                if (a[i - 1] === b[j - 1]) {\n                    distanceMatrix[i][j] = distanceMatrix[i - 1][j - 1];\n                } else {\n                    distanceMatrix[i][j] = Math.min(\n                        distanceMatrix[i - 1][j] + 1,\n                        distanceMatrix[i][j - 1] + 1,\n                        distanceMatrix[i - 1][j - 1] + 1\n                    );\n                }\n            }\n        }\n    }\n    return distanceMatrix[aLength][bLength];\n}\n", "import {\n    DRAGGED_CLASS,\n    makeDraggableHook as nativeMakeDraggableHook,\n} from \"@web/core/utils/draggable_hook_builder\";\nimport { pick } from \"@web/core/utils/objects\";\n\n/** @typedef {import(\"@web/core/utils/draggable_hook_builder\").DraggableHandlerParams} DraggableHandlerParams */\n/** @typedef {DraggableHandlerParams & { group: HTMLElement | null }} SortableHandlerParams */\n\n/**\n * @typedef SortableParams\n *\n * MANDATORY\n *\n * @property {{ el: HTMLElement | null }} ref\n * @property {string} elements defines sortable elements\n *\n * OPTIONAL\n *\n * @property {boolean | (() => boolean)} [enable] whether the sortable system should\n *  be enabled.\n * @property {number} [delay] delay before starting a sequence after a \"pointerdown\".\n * @property {number} [touchDelay] same as \"delay\", but specific to touch environments.\n * @property {string | (() => string)} [groups] defines parent groups of sortable\n *  elements. This allows to add `onGroupEnter` and `onGroupLeave` callbacks to\n *  work on group elements during the dragging sequence.\n * @property {string | (() => string)} [handle] additional selector for when the\n *  dragging sequence must be initiated when dragging on a certain part of the element.\n * @property {string | (() => string)} [ignore] selector targetting elements that\n *  must initiate a drag.\n * @property {boolean | (() => boolean)} [connectGroups] whether elements can be\n *  dragged accross different parent groups. Note that it requires a `groups` param to work.\n * @property {string | (() => string)} [cursor] cursor style during the dragging\n *  sequence.\n * @property {boolean} [clone] the placeholder is a clone of the drag element.\n * @property {string[]} [placeholderClasses] array of classes added to the placeholder\n *  element.\n * @property {boolean} [applyChangeOnDrop] on drop the change is applied to the DOM.\n * @property {string[]} [followingElementClasses] array of classes added to the\n *  element that follow the pointer.\n *\n * HANDLERS (also optional)\n *\n * @property {(params: SortableHandlerParams) => any} [onDragStart]\n *  called when a dragging sequence is initiated.\n * @property {(params: DraggableHandlerParams) => any} [onElementEnter] called when\n *  the cursor enters another sortable element.\n * @property {(params: DraggableHandlerParams) => any} [onElementLeave] called when\n *  the cursor leaves another sortable element.\n * @property {(params: SortableHandlerParams) => any} [onGroupEnter] (if a `groups`\n *  is specified): will be called when the cursor enters another group element.\n * @property {(params: SortableHandlerParams) => any} [onGroupLeave] (if a `groups`\n *  is specified): will be called when the cursor leaves another group element.\n * @property {(params: SortableHandlerParams) => any} [onDragEnd]\n *  called when the dragging sequence ends, regardless of the reason.\n * @property {(params: DropParams) => any} [onDrop] called when the dragging sequence\n *  ends on a pointerup action AND the dragged element has been moved elsewhere.\n *  The callback will be given an object with any useful element regarding the new\n *  position of the dragged element (@see DropParams ).\n */\n\n/**\n * @typedef DropParams\n * @property {HTMLElement} element\n * @property {HTMLElement | null} group\n * @property {HTMLElement | null} previous\n * @property {HTMLElement | null} next\n * @property {HTMLElement | null} parent\n */\n\n/**\n * @typedef SortableState\n * @property {boolean} dragging\n */\n\n/** @type SortableParams */\nconst hookParams = {\n    name: \"useSortable\",\n    acceptedParams: {\n        groups: [String, Function],\n        connectGroups: [Boolean, Function],\n        clone: [Boolean],\n        placeholderClasses: [Object],\n        applyChangeOnDrop: [Boolean],\n        followingElementClasses: [Object],\n    },\n    defaultParams: {\n        connectGroups: false,\n        edgeScrolling: { speed: 20, threshold: 60 },\n        groupSelector: null,\n        clone: true,\n        placeholderClasses: [],\n        applyChangeOnDrop: false,\n        followingElementClasses: [],\n    },\n\n    // Build steps\n    onComputeParams({ ctx, params }) {\n        // Group selector\n        ctx.groupSelector = params.groups || null;\n        if (ctx.groupSelector) {\n            ctx.fullSelector = [ctx.groupSelector, ctx.fullSelector].join(\" \");\n        }\n\n        // Connection accross groups\n        ctx.connectGroups = params.connectGroups;\n\n        ctx.placeholderClone = params.clone;\n        ctx.placeholderClasses = params.placeholderClasses;\n        ctx.applyChangeOnDrop = params.applyChangeOnDrop;\n        ctx.followingElementClasses = params.followingElementClasses;\n    },\n\n    // Runtime steps\n    onDragStart({ ctx, addListener, addStyle, callHandler }) {\n        /**\n         * Element \"pointerenter\" event handler.\n         * @param {PointerEvent} ev\n         */\n        const onElementPointerEnter = (ev) => {\n            const element = ev.currentTarget;\n            if (\n                connectGroups ||\n                !groupSelector ||\n                current.group === element.closest(groupSelector)\n            ) {\n                const pos = current.placeHolder.compareDocumentPosition(element);\n                if (pos === Node.DOCUMENT_POSITION_PRECEDING) {\n                    element.before(current.placeHolder);\n                } else if (pos === Node.DOCUMENT_POSITION_FOLLOWING) {\n                    element.after(current.placeHolder);\n                }\n            }\n            callHandler(\"onElementEnter\", { element });\n        };\n\n        /**\n         * Element \"pointerleave\" event handler.\n         * @param {PointerEvent} ev\n         */\n        const onElementPointerLeave = (ev) => {\n            const element = ev.currentTarget;\n            callHandler(\"onElementLeave\", { element });\n        };\n\n        const onElementComplexPointerEnter = (ev) => {\n            if (ctx.haveAlreadyChanged) {\n                return;\n            }\n            const element = ev.currentTarget;\n\n            const siblingArray = [...element.parentElement.children].filter(\n                (el) =>\n                    el === current.placeHolder ||\n                    (el.matches(elementSelector) && !el.classList.contains(DRAGGED_CLASS))\n            );\n            const elementIndex = siblingArray.indexOf(element);\n            const placeholderIndex = siblingArray.indexOf(current.placeHolder);\n            const isDirectSibling = Math.abs(elementIndex - placeholderIndex) === 1;\n            if (\n                connectGroups ||\n                !groupSelector ||\n                current.group === element.closest(groupSelector)\n            ) {\n                const pos = current.placeHolder.compareDocumentPosition(element);\n                if (isDirectSibling) {\n                    if (pos === Node.DOCUMENT_POSITION_PRECEDING) {\n                        element.before(current.placeHolder);\n                        ctx.haveAlreadyChanged = true;\n                    } else if (pos === Node.DOCUMENT_POSITION_FOLLOWING) {\n                        element.after(current.placeHolder);\n                        ctx.haveAlreadyChanged = true;\n                    }\n                } else {\n                    if (pos === Node.DOCUMENT_POSITION_FOLLOWING) {\n                        element.before(current.placeHolder);\n                        ctx.haveAlreadyChanged = true;\n                    } else if (pos === Node.DOCUMENT_POSITION_PRECEDING) {\n                        element.after(current.placeHolder);\n                        ctx.haveAlreadyChanged = true;\n                    }\n                }\n            }\n            callHandler(\"onElementEnter\", { element });\n        };\n\n        /**\n         * Element \"pointerleave\" event handler.\n         * @param {PointerEvent} ev\n         */\n        const onElementComplexPointerLeave = (ev) => {\n            if (ctx.haveAlreadyChanged) {\n                return;\n            }\n            const element = ev.currentTarget;\n            const elementRect = element.getBoundingClientRect();\n\n            const relatedElement = ev.relatedTarget;\n            const relatedElementRect = element.getBoundingClientRect();\n\n            const siblingArray = [...element.parentElement.children].filter(\n                (el) =>\n                    el === current.placeHolder ||\n                    (el.matches(elementSelector) && !el.classList.contains(DRAGGED_CLASS))\n            );\n            const pointerOnSiblings = siblingArray.indexOf(relatedElement) > -1;\n            const elementIndex = siblingArray.indexOf(element);\n            const isFirst = elementIndex === 0;\n            const isAbove = relatedElementRect.top <= elementRect.top;\n            const isLast = elementIndex === siblingArray.length - 1;\n            const isBelow = relatedElementRect.bottom >= elementRect.bottom;\n            const pos = current.placeHolder.compareDocumentPosition(element);\n            if (!pointerOnSiblings) {\n                if (isFirst && isAbove && pos === Node.DOCUMENT_POSITION_PRECEDING) {\n                    element.before(current.placeHolder);\n                    ctx.haveAlreadyChanged = true;\n                } else if (isLast && isBelow && pos === Node.DOCUMENT_POSITION_FOLLOWING) {\n                    element.after(current.placeHolder);\n                    ctx.haveAlreadyChanged = true;\n                }\n            }\n            callHandler(\"onElementLeave\", { element });\n        };\n\n        /**\n         * Group \"pointerenter\" event handler.\n         * @param {PointerEvent} ev\n         */\n        const onGroupPointerEnter = (ev) => {\n            const group = ev.currentTarget;\n            group.appendChild(current.placeHolder);\n            callHandler(\"onGroupEnter\", { group });\n        };\n\n        /**\n         * Group \"pointerleave\" event handler.\n         * @param {PointerEvent} ev\n         */\n        const onGroupPointerLeave = (ev) => {\n            const group = ev.currentTarget;\n            callHandler(\"onGroupLeave\", { group });\n        };\n\n        const { connectGroups, current, elementSelector, groupSelector, ref } = ctx;\n        if (ctx.placeholderClone) {\n            const { width, height } = current.elementRect;\n\n            // Adjusts size for the placeholder element\n            addStyle(current.placeHolder, {\n                visibility: \"hidden\",\n                display: \"block\",\n                width: `${width}px`,\n                height: `${height}px`,\n            });\n        }\n\n        // Binds handlers on eligible groups, if the elements are not confined to\n        // their parents and a 'groupSelector' has been provided.\n        if (connectGroups && groupSelector) {\n            for (const siblingGroup of ref.el.querySelectorAll(groupSelector)) {\n                addListener(siblingGroup, \"pointerenter\", onGroupPointerEnter);\n                addListener(siblingGroup, \"pointerleave\", onGroupPointerLeave);\n            }\n        }\n\n        // Binds handlers on eligible elements\n        for (const siblingEl of ref.el.querySelectorAll(elementSelector)) {\n            if (siblingEl !== current.element && siblingEl !== current.placeHolder) {\n                if (ctx.placeholderClone) {\n                    addListener(siblingEl, \"pointerenter\", onElementPointerEnter);\n                    addListener(siblingEl, \"pointerleave\", onElementPointerLeave);\n                } else {\n                    addListener(siblingEl, \"pointerenter\", onElementComplexPointerEnter);\n                    addListener(siblingEl, \"pointerleave\", onElementComplexPointerLeave);\n                }\n            }\n        }\n\n        // Placeholder is initially added right after the current element.\n        current.element.after(current.placeHolder);\n\n        return pick(current, \"element\", \"group\");\n    },\n    onDrag({ ctx }) {\n        ctx.haveAlreadyChanged = false;\n    },\n    onDragEnd({ ctx }) {\n        return pick(ctx.current, \"element\", \"group\");\n    },\n    onDrop({ ctx }) {\n        const { current, groupSelector } = ctx;\n        const previous = current.placeHolder.previousElementSibling;\n        const next = current.placeHolder.nextElementSibling;\n        if (previous !== current.element && next !== current.element) {\n            const element = current.element;\n            if (ctx.applyChangeOnDrop) {\n                // Apply to the DOM the result of sortable()\n                if (previous) {\n                    previous.after(element);\n                } else if (next) {\n                    next.before(element);\n                }\n            }\n            return {\n                element,\n                group: current.group,\n                previous,\n                next,\n                parent: groupSelector && current.placeHolder.closest(groupSelector),\n            };\n        }\n    },\n    onWillStartDrag({ ctx, addCleanup }) {\n        const { connectGroups, current, groupSelector } = ctx;\n\n        if (groupSelector) {\n            current.group = current.element.closest(groupSelector);\n            if (!connectGroups) {\n                current.container = current.group;\n            }\n        }\n\n        if (ctx.placeholderClone) {\n            current.placeHolder = current.element.cloneNode(false);\n        } else {\n            current.placeHolder = document.createElement(\"div\");\n        }\n        current.placeHolder.classList.add(...ctx.placeholderClasses);\n        current.element.classList.add(...ctx.followingElementClasses);\n\n        addCleanup(() => current.element.classList.remove(...ctx.followingElementClasses));\n        addCleanup(() => current.placeHolder.remove());\n\n        return pick(current, \"element\", \"group\");\n    },\n};\n\n/** @type {(params: SortableParams) => SortableState} */\nexport const useSortable = (sortableParams) => {\n    const { setupHooks } = sortableParams;\n    delete sortableParams.setupHooks;\n    return nativeMakeDraggableHook({ ...hookParams, setupHooks })(sortableParams);\n};\n", "import { onWillUnmount, reactive, useEffect, useExternalListener } from \"@odoo/owl\";\nimport { useThrottleForAnimation } from \"./timing\";\nimport { useSortable as nativeUseSortable } from \"@web/core/utils/sortable\";\n\n/**\n * Set of default `useSortable` setup hooks that makes use of Owl lifecycle\n * and reactivity hooks to properly set up, update and tear down the elements and\n * listeners added by the draggable hook builder.\n *\n * @see {nativeUseSortable}\n * @type {typeof nativeUseSortable}\n */\nexport function useSortable(params) {\n    return nativeUseSortable({\n        ...params,\n        setupHooks: {\n            addListener: useExternalListener,\n            setup: useEffect,\n            teardown: onWillUnmount,\n            throttle: useThrottleForAnimation,\n            wrapState: reactive,\n        },\n    });\n}\n", "import { registry } from \"../registry\";\nimport { useSortable } from \"@web/core/utils/sortable\";\nimport { throttleForAnimation } from \"@web/core/utils/timing\";\nimport { reactive } from \"@odoo/owl\";\n\n/**\n * @typedef SortableServiceHookParams\n * @extends SortableParams\n * @property {{el: HTMLElement} | ReturnType<typeof import(\"@odoo/owl\").useRef>} [ref] container of sortable\n * @property {string | Symbol} [sortableId] identifier when multiple sortable on the same container\n */\n\nconst DEFAULT_SORTABLE_ID = Symbol.for(\"defaultSortable\");\nexport const sortableService = {\n    start() {\n        /**\n         * Map to avoid to setup/enable twice or more time the same element\n         * @type {Map<Element, Object>}\n         */\n        const boundElements = new Map();\n        return {\n            /**\n             * @param {SortableServiceHookParams} hookParams\n             */\n            create: (hookParams) => {\n                const element = hookParams.ref.el;\n                const sortableId = hookParams.sortableId ?? DEFAULT_SORTABLE_ID;\n                if (boundElements.has(element)) {\n                    const boundElement = boundElements.get(element);\n                    if (sortableId in boundElement) {\n                        return {\n                            enable() {\n                                return {\n                                    cleanup: boundElement[sortableId],\n                                };\n                            },\n                        };\n                    }\n                }\n                /**\n                 * @type {Map<Function, function():Array>}\n                 */\n                const setupFunctions = new Map();\n                /**\n                 * @type {Array<Function>}\n                 */\n                const cleanupFunctions = [];\n\n                const cleanup = () => {\n                    const boundElement = boundElements.get(element);\n                    if (sortableId in boundElement) {\n                        delete boundElement[sortableId];\n                        if (boundElement.length === 0) {\n                            boundElements.delete(element);\n                        }\n                    }\n                    cleanupFunctions.forEach((fn) => fn());\n                };\n\n                // Setup hookParam\n                const setupHooks = {\n                    wrapState: reactive,\n                    throttle: throttleForAnimation,\n                    addListener: (el, type, listener) => {\n                        el.addEventListener(type, listener);\n                        cleanupFunctions.push(() => el.removeEventListener(type, listener));\n                    },\n                    setup: (setupFn, dependenciesFn) => setupFunctions.set(setupFn, dependenciesFn),\n                    teardown: (fn) => cleanupFunctions.push(fn),\n                };\n\n                useSortable({ setupHooks, ...hookParams });\n\n                const boundElement = boundElements.get(element);\n                if (boundElement) {\n                    boundElement[sortableId] = cleanup;\n                } else {\n                    boundElements.set(element, { [sortableId]: cleanup });\n                }\n\n                return {\n                    enable() {\n                        setupFunctions.forEach((dependenciesFn, setupFn) =>\n                            setupFn(...dependenciesFn())\n                        );\n                        return {\n                            cleanup,\n                        };\n                    },\n                };\n            },\n        };\n    },\n};\n\nregistry.category(\"services\").add(\"sortable\", sortableService);\n", "import { isObject } from \"./objects\";\n\nexport const nbsp = \"\\u00a0\";\n\n/**\n * Escapes a string for HTML.\n *\n * @param {string | number} [str] the string to escape\n * @returns {string} an escaped string\n */\nexport function escape(str) {\n    if (str === undefined) {\n        return \"\";\n    }\n    if (typeof str === \"number\") {\n        return String(str);\n    }\n    [\n        [\"&\", \"&amp;\"],\n        [\"<\", \"&lt;\"],\n        [\">\", \"&gt;\"],\n        [\"'\", \"&#x27;\"],\n        ['\"', \"&quot;\"],\n        [\"`\", \"&#x60;\"],\n    ].forEach((pairs) => {\n        str = String(str).replaceAll(pairs[0], pairs[1]);\n    });\n    return str;\n}\n\n/**\n * Escapes a string to use as a RegExp.\n * @url https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#Escaping\n *\n * @param {string} str\n * @returns {string} escaped string to use as a RegExp\n */\nexport function escapeRegExp(str) {\n    return str.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n}\n\n/**\n * Intersperses ``separator`` in ``str`` at the positions indicated by\n * ``indices``.\n *\n * ``indices`` is an array of relative offsets (from the previous insertion\n * position, starting from the end of the string) at which to insert\n * ``separator``.\n *\n * There are two special values:\n *\n * ``-1``\n *   indicates the insertion should end now\n * ``0``\n *   indicates that the previous section pattern should be repeated (until all\n *   of ``str`` is consumed)\n *\n * @param {string} str\n * @param {number[]} indices\n * @param {string} separator\n * @returns {string}\n */\nexport function intersperse(str, indices, separator = \"\") {\n    separator = separator || \"\";\n    const result = [];\n    let last = str.length;\n    for (let i = 0; i < indices.length; ++i) {\n        let section = indices[i];\n        if (section === -1 || last <= 0) {\n            // Done with string, or -1 (stops formatting string)\n            break;\n        } else if (section === 0 && i === 0) {\n            // repeats previous section, which there is none => stop\n            break;\n        } else if (section === 0) {\n            // repeat previous section forever\n            //noinspection AssignmentToForLoopParameterJS\n            section = indices[--i];\n        }\n        result.push(str.substring(last - section, last));\n        last -= section;\n    }\n    const s = str.substring(0, last);\n    if (s) {\n        result.push(s);\n    }\n    return result.reverse().join(separator);\n}\n\n/**\n * Returns a string formatted using given values.\n * If the value is an object, its keys will replace `%(key)s` expressions.\n * If the values are a set of strings, they will replace `%s` expressions.\n * If no value is given, the string will not be formatted.\n *\n * @param {string} s\n * @param {any[]} values\n * @returns {string}\n */\nexport function sprintf(s, ...values) {\n    if (values.length === 1 && isObject(values[0])) {\n        const valuesDict = values[0];\n        s = s.replace(/%\\(([^)]+)\\)s/g, (match, value) => valuesDict[value]);\n    } else if (values.length > 0) {\n        s = s.replace(/%s/g, () => values.shift());\n    }\n    return s;\n}\n\n/**\n * Capitalizes a string: \"abc def\" => \"Abc def\"\n *\n * @param {string} s the input string\n * @returns {string}\n */\nexport function capitalize(s) {\n    return s ? s[0].toUpperCase() + s.slice(1) : \"\";\n}\n\n/**\n * @param {string} value\n * @returns boolean\n */\nexport function isEmail(value) {\n    // http://stackoverflow.com/questions/46155/validate-email-address-in-javascript\n    const re =\n        // eslint-disable-next-line no-useless-escape\n        /^(([^<>()\\[\\]\\.,;:\\s@\\\"]+(\\.[^<>()\\[\\]\\.,;:\\s@\\\"]+)*)|(\\\".+\\\"))@(([^<>()[\\]\\.,;:\\s@\\\"]+\\.)+[^<>()[\\]\\.,;:\\s@\\\"]{2,})$/i;\n    return re.test(value);\n}\n\n/**\n * Return true if the string is composed of only digits\n *\n * @param {string} value\n * @returns boolean\n */\n\nexport function isNumeric(value) {\n    return Boolean(value?.match(/^\\d+$/));\n}\n\n/**\n * Parse the string to check if the value is true or false\n * If the string is empty, 0, False or false it's considered as false\n * The rest is considered as true\n *\n * @param {string} str\n * @param {boolean} [trueIfEmpty=false]\n * @returns {boolean}\n */\nexport function exprToBoolean(str, trueIfEmpty = false) {\n    return str ? !/^false|0$/i.test(str) : trueIfEmpty;\n}\n\n/**\n * Generate a unique identifier (64 bits) in hexadecimal.\n *\n * @returns {string}\n */\nexport function uuid() {\n    const array = new Uint8Array(8);\n    window.crypto.getRandomValues(array);\n    // Uint8Array to hex\n    return [...array].map((b) => b.toString(16).padStart(2, \"0\")).join(\"\");\n}\n\n/**\n * Generate a hash, also known as a 'digest', for the given string.\n * This algorithm is based on the Java hashString method\n * (see: https://docs.oracle.com/javase/7/docs/api/java/lang/String.html#hashCode()).\n * Please note that this hash function is non-cryptographic and does not exhibit collision resistance.\n *\n * If a cryptographic hash function is required, the digest() function of the SubtleCrypto\n * interface makes various hash functions available:\n * https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/digest\n *\n * @param {string} str\n * @returns {string}\n */\nexport function hashCode(...strings) {\n    const str = strings.join(\"\\x1C\");\n\n    let hash = 0;\n    for (let i = 0; i < str.length; i++) {\n        hash = (hash << 5) - hash + str.charCodeAt(i);\n        hash |= 0;\n    }\n\n    // Convert the possibly negative number hash code into an 8 character\n    // hexadecimal string\n    return (hash + 16 ** 8).toString(16).slice(-8);\n}\n", "import { browser } from \"@web/core/browser/browser\";\nimport { onWillUnmount, useComponent } from \"@odoo/owl\";\n\n/**\n * Creates a batched version of a callback so that all calls to it in the same\n * time frame will only call the original callback once.\n * @param callback the callback to batch\n * @param synchronize this function decides the granularity of the batch (a microtick by default)\n * @returns a batched version of the original callback\n */\nexport function batched(callback, synchronize = () => Promise.resolve()) {\n    let scheduled = false;\n    return async (...args) => {\n        if (!scheduled) {\n            scheduled = true;\n            await synchronize();\n            scheduled = false;\n            callback(...args);\n        }\n    };\n}\n\n/**\n * Creates and returns a new debounced version of the passed function (func)\n * which will postpone its execution until after 'delay' milliseconds\n * have elapsed since the last time it was invoked. The debounced function\n * will return a Promise that will be resolved when the function (func)\n * has been fully executed.\n *\n * If both `options.trailing` and `options.leading` are true, the function\n * will only be invoked at the trailing edge if the debounced function was\n * called at least once more during the wait time.\n *\n * @template {Function} T the return type of the original function\n * @param {T} func the function to debounce\n * @param {number | \"animationFrame\"} delay how long should elapse before the function\n *      is called. If 'animationFrame' is given instead of a number, 'requestAnimationFrame'\n *      will be used instead of 'setTimeout'.\n * @param {boolean} [options] if true, equivalent to exclusive leading. If false, equivalent to exclusive trailing.\n * @param {object} [options]\n * @param {boolean} [options.leading=false] whether the function should be invoked at the leading edge of the timeout\n * @param {boolean} [options.trailing=true] whether the function should be invoked at the trailing edge of the timeout\n * @returns {T & { cancel: () => void }} the debounced function\n */\nexport function debounce(func, delay, options) {\n    let handle;\n    const funcName = func.name ? func.name + \" (debounce)\" : \"debounce\";\n    const useAnimationFrame = delay === \"animationFrame\";\n    const setFnName = useAnimationFrame ? \"requestAnimationFrame\" : \"setTimeout\";\n    const clearFnName = useAnimationFrame ? \"cancelAnimationFrame\" : \"clearTimeout\";\n    let lastArgs;\n    let leading = false;\n    let trailing = true;\n    if (typeof options === \"boolean\") {\n        leading = options;\n        trailing = !options;\n    } else if (options) {\n        leading = options.leading ?? leading;\n        trailing = options.trailing ?? trailing;\n    }\n\n    return Object.assign(\n        {\n            /** @type {any} */\n            [funcName](...args) {\n                return new Promise((resolve) => {\n                    if (leading && !handle) {\n                        Promise.resolve(func.apply(this, args)).then(resolve);\n                    } else {\n                        lastArgs = args;\n                    }\n                    browser[clearFnName](handle);\n                    handle = browser[setFnName](() => {\n                        handle = null;\n                        if (trailing && lastArgs) {\n                            Promise.resolve(func.apply(this, lastArgs)).then(resolve);\n                            lastArgs = null;\n                        }\n                    }, delay);\n                });\n            },\n        }[funcName],\n        {\n            cancel(execNow = false) {\n                browser[clearFnName](handle);\n                if (execNow && lastArgs) {\n                    func.apply(this, lastArgs);\n                }\n            },\n        }\n    );\n}\n\n/**\n * Function that calls recursively a request to an animation frame.\n * Useful to call a function repetitively, until asked to stop, that needs constant rerendering.\n * The provided callback gets as argument the time the last frame took.\n * @param {(deltaTime: number) => void} callback\n * @returns {() => void} stop function\n */\nexport function setRecurringAnimationFrame(callback) {\n    const handler = (timestamp) => {\n        callback(timestamp - lastTimestamp);\n        lastTimestamp = timestamp;\n        handle = browser.requestAnimationFrame(handler);\n    };\n\n    const stop = () => {\n        browser.cancelAnimationFrame(handle);\n    };\n\n    let lastTimestamp = browser.performance.now();\n    let handle = browser.requestAnimationFrame(handler);\n\n    return stop;\n}\n\n/**\n * Creates a version of the function where only the last call between two\n * animation frames is executed before the browser's next repaint. This\n * effectively throttles the function to the display's refresh rate.\n * Note that the throttled function can be any callback. It is not\n * specifically an event handler, no assumption is made about its\n * signature.\n * NB: The first call is always called immediately (leading edge).\n *\n * @template {Function} T\n * @param {T} func the function to throttle\n * @returns {T & { cancel: () => void }} the throttled function\n */\nexport function throttleForAnimation(func) {\n    let handle = null;\n    const calls = new Set();\n    const funcName = func.name ? `${func.name} (throttleForAnimation)` : \"throttleForAnimation\";\n    const pending = () => {\n        if (calls.size) {\n            handle = browser.requestAnimationFrame(pending);\n            const { args, resolve } = [...calls].pop();\n            calls.clear();\n            Promise.resolve(func.apply(this, args)).then(resolve);\n        } else {\n            handle = null;\n        }\n    };\n    return Object.assign(\n        {\n            /** @type {any} */\n            [funcName](...args) {\n                return new Promise((resolve) => {\n                    const isNew = handle === null;\n                    if (isNew) {\n                        handle = browser.requestAnimationFrame(pending);\n                        Promise.resolve(func.apply(this, args)).then(resolve);\n                    } else {\n                        calls.add({ args, resolve });\n                    }\n                });\n            },\n        }[funcName],\n        {\n            cancel() {\n                browser.cancelAnimationFrame(handle);\n                calls.clear();\n                handle = null;\n            },\n        }\n    );\n}\n\n// ----------------------------------- HOOKS -----------------------------------\n\n/**\n * Hook that returns a debounced version of the given function, and cancels\n * the potential pending execution on willUnmount.\n * @see debounce\n * @template {Function} T\n * @param {T} callback\n * @param {number | \"animationFrame\"} delay\n * @param {Object} [options]\n * @param {string} [options.execBeforeUnmount=false] executes the callback if the debounced function\n *      has been called and not resolved before destroying the component.\n * @param {boolean} [options.immediate=false] whether the function should be called on\n *      the leading edge of the timeout.\n * @param {boolean} [options.trailing=!options.immediate] whether the function should be called on\n *      the trailing edge of the timeout.\n * @returns {T & { cancel: () => void }}\n */\nexport function useDebounced(\n    callback,\n    delay,\n    { execBeforeUnmount = false, immediate = false, trailing = !immediate } = {}\n) {\n    const component = useComponent();\n    const debounced = debounce(callback.bind(component), delay, { leading: immediate, trailing });\n    onWillUnmount(() => debounced.cancel(execBeforeUnmount));\n    return debounced;\n}\n\n/**\n * Hook that returns a throttled for animation version of the given function,\n * and cancels the potential pending execution on willUnmount.\n * @see throttleForAnimation\n * @template {Function} T\n * @param {T} func the function to throttle\n * @returns {T & { cancel: () => void }} the throttled function\n */\nexport function useThrottleForAnimation(func) {\n    const component = useComponent();\n    const throttledForAnimation = throttleForAnimation(func.bind(component));\n    onWillUnmount(() => throttledForAnimation.cancel());\n    return throttledForAnimation;\n}\n", "/**\n * @typedef Position\n * @property {number} x\n * @property {number} y\n */\n\n/**\n * @param {Iterable<HTMLElement>} elements\n * @param {Position} targetPos\n * @returns {HTMLElement | null}\n */\nexport function closest(elements, targetPos) {\n    let closestEl = null;\n    let closestDistance = Infinity;\n    for (const el of elements) {\n        const rect = el.getBoundingClientRect();\n        const distance = getQuadrance(rect, targetPos);\n        if (!closestEl || distance < closestDistance) {\n            closestEl = el;\n            closestDistance = distance;\n        }\n    }\n    return closestEl;\n}\n\n/**\n * rough approximation of a visible element. not perfect (does not take into\n * account opacity = 0 for example), but good enough for our purpose\n *\n * @param {Element} el\n * @returns {boolean}\n */\nexport function isVisible(el) {\n    if (el === document || el === window) {\n        return true;\n    }\n    if (!el) {\n        return false;\n    }\n    let _isVisible = false;\n    if (\"offsetWidth\" in el && \"offsetHeight\" in el) {\n        _isVisible = el.offsetWidth > 0 && el.offsetHeight > 0;\n    } else if (\"getBoundingClientRect\" in el) {\n        // for example, svgelements\n        const rect = el.getBoundingClientRect();\n        _isVisible = rect.width > 0 && rect.height > 0;\n    }\n    if (!_isVisible && getComputedStyle(el).display === \"contents\") {\n        for (const child of el.children) {\n            if (isVisible(child)) {\n                return true;\n            }\n        }\n    }\n    return _isVisible;\n}\n\n/**\n * @param {DOMRect} rect\n * @param {Position} pos\n * @returns {number}\n */\nexport function getQuadrance(rect, pos) {\n    let q = 0;\n    if (pos.x < rect.x) {\n        q += (rect.x - pos.x) ** 2;\n    } else if (rect.x + rect.width < pos.x) {\n        q += (pos.x - (rect.x + rect.width)) ** 2;\n    }\n    if (pos.y < rect.y) {\n        q += (rect.y - pos.y) ** 2;\n    } else if (rect.y + rect.height < pos.y) {\n        q += (pos.y - (rect.y + rect.height)) ** 2;\n    }\n    return q;\n}\n\n/**\n * @param {Element} activeElement\n * @param {String} selector\n * @returns all selected and visible elements present in the activeElement\n */\nexport function getVisibleElements(activeElement, selector) {\n    const visibleElements = [];\n    /** @type {NodeListOf<HTMLElement>} */\n    const elements = activeElement.querySelectorAll(selector);\n    for (const el of elements) {\n        if (isVisible(el)) {\n            visibleElements.push(el);\n        }\n    }\n    return visibleElements;\n}\n\n/**\n * @param {Iterable<HTMLElement>} elements\n * @param {Partial<DOMRect>} targetRect\n * @returns {HTMLElement[]}\n */\nexport function touching(elements, targetRect) {\n    const r1 = { x: 0, y: 0, width: 0, height: 0, ...targetRect };\n    return [...elements].filter((el) => {\n        const r2 = el.getBoundingClientRect();\n        return (\n            r2.x + r2.width >= r1.x &&\n            r2.x <= r1.x + r1.width &&\n            r2.y + r2.height >= r1.y &&\n            r2.y <= r1.y + r1.height\n        );\n    });\n}\n\n// -----------------------------------------------------------------------------\n// Get Tabable Elements\n// -----------------------------------------------------------------------------\n// TODISCUSS:\n//  - leave the following in this file ?\n//  - redefine this selector in tests env with \":not(#qunit *)\" ?\n\n// Following selector is based on this spec: https://html.spec.whatwg.org/multipage/interaction.html#dom-tabindex\nconst FOCUSABLE_SELECTORS = [\n    \"[tabindex]\",\n    \"a\",\n    \"area\",\n    \"button\",\n    \"frame\",\n    \"iframe\",\n    \"input\",\n    \"object\",\n    \"select\",\n    \"textarea\",\n    \"details > summary:nth-child(1)\",\n].map((sel) => `${sel}:not(:disabled)`);\nconst TABABLE_SELECTORS = FOCUSABLE_SELECTORS.map((sel) => `${sel}:not([tabindex=\"-1\"])`);\n\n/**\n * Check if an element is focusable\n *\n * @param {HTMLElement} element\n */\nexport function isFocusable(el) {\n    return el.matches(FOCUSABLE_SELECTORS.join(\",\")) && isVisible(el) && !el.closest(\"[inert]\");\n}\n\n/**\n * Returns all focusable elements in the given container.\n *\n * @param {HTMLElement} [container=document.body]\n */\nexport function getTabableElements(container = document.body) {\n    const elements = [...container.querySelectorAll(TABABLE_SELECTORS.join(\",\"))].filter(\n        (el) => isVisible(el) && !el.closest(\"[inert]\")\n    );\n    /** @type {Record<number, HTMLElement[]>} */\n    const byTabIndex = {};\n    for (const el of [...elements]) {\n        if (!byTabIndex[el.tabIndex]) {\n            byTabIndex[el.tabIndex] = [];\n        }\n        byTabIndex[el.tabIndex].push(el);\n    }\n\n    const withTabIndexZero = byTabIndex[0] || [];\n    delete byTabIndex[0];\n    return [...Object.values(byTabIndex).flat(), ...withTabIndexZero];\n}\n\nexport function getNextTabableElement(container = document.body) {\n    const tabableElements = getTabableElements(container);\n    const index = tabableElements.indexOf(document.activeElement);\n    return index === -1 ? tabableElements[0] : tabableElements[index + 1] || null;\n}\n\nexport function getPreviousTabableElement(container = document.body) {\n    const tabableElements = getTabableElements(container);\n    const index = tabableElements.indexOf(document.activeElement);\n    return index === -1\n        ? tabableElements[tabableElements.length - 1]\n        : tabableElements[index - 1] || null;\n}\n\n/**\n * Gives the button a loading effect by disabling it and adding a `fa` spinner\n * icon. The existing button `fa` icons will be hidden through css.\n *\n * @param {HTMLElement} btnEl - the button to disable/load\n * @return {function} a callback function that will restore the button to its\n *         initial state\n */\nexport function addLoadingEffect(btnEl) {\n    // Note that pe-none is used alongside \"disabled\" so that the behavior is\n    // the same on links not using the \"btn\" class -> pointer-events disabled.\n    btnEl.classList.add(\"o_btn_loading\", \"disabled\", \"pe-none\");\n    btnEl.disabled = true;\n    const loaderEl = document.createElement(\"span\");\n    loaderEl.classList.add(\"fa\", \"fa-circle-o-notch\", \"fa-spin\", \"me-2\");\n    btnEl.prepend(loaderEl);\n    return () => {\n        btnEl.classList.remove(\"o_btn_loading\", \"disabled\", \"pe-none\");\n        btnEl.disabled = false;\n        loaderEl.remove();\n    };\n}\n", "import { session } from \"@web/session\";\nimport { browser } from \"../browser/browser\";\nimport { shallowEqual } from \"@web/core/utils/objects\";\nconst { DateTime } = luxon;\n\nexport class RedirectionError extends Error {}\n\n/**\n * Transforms a key value mapping to a string formatted as url hash, e.g.\n * {a: \"x\", b: 2} -> \"a=x&b=2\"\n *\n * @param {Object} obj\n * @returns {string}\n */\nexport function objectToUrlEncodedString(obj) {\n    return Object.entries(obj)\n        .map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v || \"\")}`)\n        .join(\"&\");\n}\n\n/**\n * Gets the origin url of the page, or cleans a given one\n *\n * @param {string} [origin]: a given origin url\n * @return {string} a cleaned origin url\n */\nexport function getOrigin(origin) {\n    if (origin) {\n        // remove trailing slashes\n        origin = origin.replace(/\\/+$/, \"\");\n    } else {\n        const { host, protocol } = browser.location;\n        origin = `${protocol}//${host}`;\n    }\n    return origin;\n}\n\n/**\n * @param {string} route: the relative route, or absolute in the case of cors urls\n * @param {object} [queryParams]: parameters to be appended as the url's queryString\n * @param {object} [options]\n * @param {string} [options.origin]: a precomputed origin\n */\nexport function url(route, queryParams, options = {}) {\n    const origin = getOrigin(options.origin ?? session.origin);\n    if (!route) {\n        return origin;\n    }\n\n    let queryString = objectToUrlEncodedString(queryParams || {});\n    queryString = queryString.length > 0 ? `?${queryString}` : queryString;\n\n    // Compare the wanted url against the current origin\n    let prefix = [\"http://\", \"https://\", \"//\"].some(\n        (el) => route.length >= el.length && route.slice(0, el.length) === el\n    );\n    prefix = prefix ? \"\" : origin;\n    return `${prefix}${route}${queryString}`;\n}\n\n/**\n * @param {string} model\n * @param {number} id\n * @param {string} field\n * @param {Object} [options]\n * @param {string} [options.crop]\n * @param {string} [options.filename]\n * @param {number} [options.height]\n * @param {string|import('luxon').DateTime} [options.unique]\n * @param {number} [options.width]\n */\nexport function imageUrl(\n    model,\n    id,\n    field,\n    { access_token, crop, filename, height, unique, width } = {}\n) {\n    let route = `/web/image/${model}/${id}/${field}`;\n    if (width && height) {\n        route = `${route}/${width}x${height}`;\n    }\n    if (filename) {\n        route = `${route}/${filename}`;\n    }\n    const urlParams = {};\n    if (access_token) {\n        Object.assign(urlParams, { access_token });\n    }\n    if (crop) {\n        Object.assign(urlParams, { crop });\n    }\n    if (unique) {\n        if (unique instanceof DateTime) {\n            urlParams.unique = unique.ts;\n        } else {\n            const dateTimeFromUnique = DateTime.fromSQL(unique);\n            if (dateTimeFromUnique.isValid) {\n                urlParams.unique = dateTimeFromUnique.ts;\n            } else if (typeof unique === \"string\" && unique.length > 0) {\n                urlParams.unique = unique;\n            }\n        }\n    }\n    return url(route, urlParams);\n}\n\n/**\n * Gets dataURL (base64 data) from the given file or blob.\n * Technically wraps FileReader.readAsDataURL in Promise.\n *\n * @param {Blob | File} file\n * @returns {Promise} resolved with the dataURL, or rejected if the file is\n *  empty or if an error occurs.\n */\nexport function getDataURLFromFile(file) {\n    if (!file) {\n        return Promise.reject();\n    }\n    return new Promise((resolve, reject) => {\n        const reader = new FileReader();\n        reader.addEventListener(\"load\", () => {\n            // Handle Chrome bug that creates invalid data URLs for empty files\n            if (reader.result === \"data:\") {\n                resolve(`data:${file.type};base64,`);\n            } else {\n                resolve(reader.result);\n            }\n        });\n        reader.addEventListener(\"abort\", reject);\n        reader.addEventListener(\"error\", reject);\n        reader.readAsDataURL(file);\n    });\n}\n\n/**\n * Safely redirects to the given url within the same origin.\n *\n * @param {string} url\n * @throws {RedirectionError} if the given url has a different origin\n */\nexport function redirect(url) {\n    const { origin, pathname } = browser.location;\n    const _url = new URL(url, `${origin}${pathname}`);\n    if (_url.origin !== origin) {\n        throw new RedirectionError(\"Can't redirect to another origin\");\n    }\n    browser.location.assign(_url.href);\n}\n\n/**\n * This function compares two URLs. It doesn't care about the order of the search parameters.\n *\n * @param {string} _url1\n * @param {string} _url2\n * @returns {boolean} true if the urls are identical, false otherwise\n */\nexport function compareUrls(_url1, _url2) {\n    const url1 = new URL(_url1);\n    const url2 = new URL(_url2);\n    return (\n        url1.origin === url2.origin &&\n        url1.pathname === url2.pathname &&\n        shallowEqual(\n            Object.fromEntries(url1.searchParams),\n            Object.fromEntries(url2.searchParams)\n        ) &&\n        url1.hash === url2.hash\n    );\n}\n", "import { isIterable } from \"./arrays\";\n\n/**\n * XML document to create new elements from. The fact that this is a \"text/xml\"\n * document ensures that tagNames and attribute names are case sensitive.\n */\nconst serializer = new XMLSerializer();\nconst parser = new DOMParser();\nconst xmlDocument = parser.parseFromString(\"<templates/>\", \"text/xml\");\n\nfunction hasParsingError(parsedDocument) {\n    return parsedDocument.getElementsByTagName(\"parsererror\").length > 0;\n}\n\n/**\n * @param {string} str\n * @returns {Element}\n */\nexport function parseXML(str) {\n    const xml = parser.parseFromString(str, \"text/xml\");\n    if (hasParsingError(xml)) {\n        throw new Error(\n            `An error occured while parsing ${str}: ${xml.getElementsByTagName(\"parsererror\")}`\n        );\n    }\n    return xml.documentElement;\n}\n\n/**\n * @param {Element} xml\n * @returns {string}\n */\nexport function serializeXML(xml) {\n    return serializer.serializeToString(xml);\n}\n\n/**\n * @param {Element | string} xml\n * @param {(el: Element, visitChildren: () => any) => any} callback\n */\nexport function visitXML(xml, callback) {\n    const visit = (el) => {\n        if (el) {\n            let didVisitChildren = false;\n            const visitChildren = () => {\n                for (const child of el.children) {\n                    visit(child);\n                }\n                didVisitChildren = true;\n            };\n            const shouldVisitChildren = callback(el, visitChildren);\n            if (shouldVisitChildren !== false && !didVisitChildren) {\n                visitChildren();\n            }\n        }\n    };\n    const xmlDoc = typeof xml === \"string\" ? parseXML(xml) : xml;\n    visit(xmlDoc);\n}\n\n/**\n * @param {Element} parent\n * @param {Node | Node[] | void} node\n */\nexport function append(parent, node) {\n    const nodes = Array.isArray(node) ? node : [node];\n    parent.append(...nodes.filter(Boolean));\n    return parent;\n}\n\n/**\n * Combines the existing value of a node attribute with new given parts. The glue\n * is the string used to join the parts.\n *\n * @param {Element} el\n * @param {string} attr\n * @param {string | string[]} parts\n * @param {string} [glue=\" \"]\n */\nexport function combineAttributes(el, attr, parts, glue = \" \") {\n    const allValues = [];\n    if (el.hasAttribute(attr)) {\n        allValues.push(el.getAttribute(attr));\n    }\n    parts = Array.isArray(parts) ? parts : [parts];\n    parts = parts.filter((part) => !!part);\n    allValues.push(...parts);\n    el.setAttribute(attr, allValues.join(glue));\n}\n\n/**\n * XML equivalent of `document.createElement`.\n *\n * @param {string} tagName\n * @param {...(Iterable<Element> | Record<string, string>)} args\n * @returns {Element}\n */\nexport function createElement(tagName, ...args) {\n    const el = xmlDocument.createElement(tagName);\n    for (const arg of args) {\n        if (!arg) {\n            continue;\n        }\n        if (isIterable(arg)) {\n            // Children list\n            el.append(...arg);\n        } else if (typeof arg === \"object\") {\n            // Attributes\n            for (const name in arg) {\n                el.setAttribute(name, arg[name]);\n            }\n        }\n    }\n    return el;\n}\n\n/**\n * XML equivalent of `document.createTextNode`.\n *\n * @param {string} data\n * @returns {Text}\n */\nexport function createTextNode(data) {\n    return xmlDocument.createTextNode(data);\n}\n\n/**\n * Removes the given attributes on the given element and returns them as a dictionnary.\n * @param {Element} el\n * @param {string[]} attributes\n * @returns {Record<string, string>}\n */\nexport function extractAttributes(el, attributes) {\n    const attrs = Object.create(null);\n    for (const attr of attributes) {\n        attrs[attr] = el.getAttribute(attr) || \"\";\n        el.removeAttribute(attr);\n    }\n    return attrs;\n}\n\n/**\n * @param {Node} [node]\n * @param {boolean} [lower=false]\n * @returns {string}\n */\nexport function getTag(node, lower = false) {\n    const tag = (node && node.nodeName) || \"\";\n    return lower ? tag.toLowerCase() : tag;\n}\n\n/**\n * @param {Node} node\n * @param {Object} attributes\n */\nexport function setAttributes(node, attributes) {\n    for (const [name, value] of Object.entries(attributes)) {\n        node.setAttribute(name, value);\n    }\n}\n", "import { useComponent, useEffect, useExternalListener } from \"@odoo/owl\";\nimport { pick, shallowEqual } from \"@web/core/utils/objects\";\nimport { useThrottleForAnimation } from \"@web/core/utils/timing\";\n\n/**\n * @template T\n * @typedef VirtualGridParams\n * @property {ReturnType<typeof import(\"@odoo/owl\").useRef>} scrollableRef\n *  a ref to the scrollable element\n * @property {ScrollPosition} [initialScroll={ left: 0, top: 0 }]\n *  the initial scroll position of the scrollable element\n * @property {(changed: Partial<VirtualGridIndexes>) => void} [onChange=() => this.render()]\n *  a callback called when the visible items change, i.e. when on scroll or resize.\n *  the default implementation is to re-render the component.\n * @property {number} [bufferCoef=1]\n *  the coefficient to calculate the buffer size around the visible area.\n *  The buffer size is equal to bufferCoef * windowSize.\n *  The default value is 1: it means that the buffer size takes one more window size on each side.\n *  So the whole area that will be rendered is 3 times the window size.\n *  If you use each direction, it could be up to 9 times the window size (3x3).\n *  Consider lowering this value if you have a costful rendering.\n *  A value of 0 means no buffer.\n */\n\n/**\n * @typedef VirtualGridIndexes\n * @property {[number, number] | undefined} columnsIndexes\n * @property {[number, number] | undefined} rowsIndexes\n */\n\n/**\n * @typedef VirtualGridSetters\n * @property {(widths: number[]) => void} setColumnsWidths\n *  Use it to set the width of each column.\n *  Indexes should match the indexes of the columns.\n * @property {(heights: number[]) => void} setRowsHeights\n *  Use it to set the height of each row.\n *  Indexes should match the indexes of the rows.\n */\n\n/**\n * @typedef ScrollPosition\n * @property {number} left\n * @property {number} top\n */\n\nconst BUFFER_COEFFICIENT = 1;\n\n/**\n * @typedef GetIndexesParams\n * @property {number[]} sizes contains the sizes of the items. Each size is the sum of the sizes of the previous items and the size of the current item.\n * @property {number} start it is the start position of the visible area, here it is the scroll position.\n * @property {number} span it is the size of the visible area, here it is the window size.\n * @property {number} [prevStartIndex] the previous start index, it is used to optimize the calculation.\n * @property {number} [bufferCoef=BUFFER_COEFFICIENT] the coefficient to calculate the buffer size.\n */\n\n/**\n * This function calculates the indexes of the visible items in a virtual list.\n *\n * @param {GetIndexesParams} param0\n * @returns {[number, number] | undefined} the indexes of the visible items with a surrounding buffer of totalSize on each side.\n */\nfunction getIndexes({ sizes, start, span, prevStartIndex, bufferCoef = BUFFER_COEFFICIENT }) {\n    if (!sizes || !sizes.length) {\n        return [];\n    }\n    if (sizes.at(-1) < span) {\n        // all items could be displayed\n        return [0, sizes.length - 1];\n    }\n    const bufferSize = Math.round(span * bufferCoef);\n    const bufferStart = start - bufferSize;\n    const bufferEnd = start + span + bufferSize;\n\n    let startIndex = prevStartIndex ?? 0;\n    // we search the first index such that sizes[index] > bufferStart\n    while (startIndex > 0 && sizes[startIndex] > bufferStart) {\n        startIndex--;\n    }\n    while (startIndex < sizes.length - 1 && sizes[startIndex] <= bufferStart) {\n        startIndex++;\n    }\n\n    let endIndex = startIndex;\n    // we search the last index such that (sizes[index - 1] ?? 0) < bufferEnd\n    while (endIndex < sizes.length - 1 && (sizes[endIndex - 1] ?? 0) < bufferEnd) {\n        endIndex++;\n    }\n    while (endIndex > startIndex && (sizes[endIndex - 1] ?? 0) >= bufferEnd) {\n        endIndex--;\n    }\n    return [startIndex, endIndex];\n}\n\n/**\n * Calculates the displayed items in a virtual grid.\n *\n * Requirements:\n *  - the scrollable area has a fixed height and width.\n *  - the items are rendered with a proper offset inside the scrollable area.\n *    This can be achieved e.g. with a css grid or an absolute positioning.\n *\n * @template T\n * @param {VirtualGridParams<T>} params\n * @returns {VirtualGridIndexes & VirtualGridSetters}\n */\nexport function useVirtualGrid({ scrollableRef, initialScroll, onChange, bufferCoef }) {\n    const comp = useComponent();\n    onChange ||= () => comp.render();\n\n    const current = { scroll: { left: 0, top: 0, ...initialScroll } };\n    const computeColumnsIndexes = () => {\n        return getIndexes({\n            sizes: current.summedColumnsWidths,\n            start: Math.abs(current.scroll.left),\n            span: window.innerWidth,\n            prevStartIndex: current.columnsIndexes?.[0],\n            bufferCoef,\n        });\n    };\n    const computeRowsIndexes = () => {\n        return getIndexes({\n            sizes: current.summedRowsHeights,\n            start: current.scroll.top,\n            span: window.innerHeight,\n            prevStartIndex: current.rowsIndexes?.[0],\n            bufferCoef,\n        });\n    };\n    const throttledCompute = useThrottleForAnimation(() => {\n        const changed = [];\n        const columnsVisibleIndexes = computeColumnsIndexes();\n        if (!shallowEqual(columnsVisibleIndexes, current.columnsIndexes)) {\n            current.columnsIndexes = columnsVisibleIndexes;\n            changed.push(\"columnsIndexes\");\n        }\n        const rowsVisibleIndexes = computeRowsIndexes();\n        if (!shallowEqual(rowsVisibleIndexes, current.rowsIndexes)) {\n            current.rowsIndexes = rowsVisibleIndexes;\n            changed.push(\"rowsIndexes\");\n        }\n        if (changed.length) {\n            onChange(pick(current, ...changed));\n        }\n    });\n    const scrollListener = (/** @type {Event & { target: Element }} */ ev) => {\n        current.scroll.left = ev.target.scrollLeft;\n        current.scroll.top = ev.target.scrollTop;\n        throttledCompute();\n    };\n    useEffect(\n        (el) => {\n            el?.addEventListener(\"scroll\", scrollListener);\n            return () => el?.removeEventListener(\"scroll\", scrollListener);\n        },\n        () => [scrollableRef.el]\n    );\n    useExternalListener(window, \"resize\", () => throttledCompute());\n    return {\n        get columnsIndexes() {\n            return current.columnsIndexes;\n        },\n        get rowsIndexes() {\n            return current.rowsIndexes;\n        },\n        setColumnsWidths(widths) {\n            let acc = 0;\n            current.summedColumnsWidths = widths.map((w) => (acc += w));\n            delete current.columnsIndexes;\n            current.columnsIndexes = computeColumnsIndexes();\n        },\n        setRowsHeights(heights) {\n            let acc = 0;\n            current.summedRowsHeights = heights.map((h) => (acc += h));\n            delete current.rowsIndexes;\n            current.rowsIndexes = computeRowsIndexes();\n        },\n    };\n}\n", "class ClipboardItemImpl {\n    constructor(items, options = {}) {\n        this.items = items;\n        this.options = options;\n    }\n    get presentationStyle() {\n        return this.options.presentationStyle;\n    }\n    get types() {\n        return Object.keys(this.items);\n    }\n    getType(type) {\n        return this.items[type];\n    }\n}\n\nfunction blobToStr(blob) {\n    return new Promise((resolve, reject) => {\n        const reader = new FileReader();\n        reader.addEventListener(\"load\", () => {\n            const { result } = reader;\n            if (typeof result === \"string\") {\n                resolve(result);\n            } else {\n                reject(\"Cannot read Blob as String\");\n            }\n        });\n        reader.addEventListener(\"error\", () => {\n            reject(\"Cannot read Blob\");\n        });\n        reader.readAsText(blob);\n    });\n}\n\nasync function stringify(item) {\n    const strItem = {};\n    for (const type of item.types) {\n        strItem[type] = await blobToStr(item.getType(type));\n    }\n    return strItem;\n}\n\nasync function write(items) {\n    if (!items[0].getType(\"text/plain\")) {\n        throw new Error(\n            `Calling clipboard.write() without a \"text/plain\" type may result in an empty clipboard on some platforms.`\n        );\n    }\n    const strItem = await stringify(items[0]);\n\n    const stubContainer = document.createElement(\"div\");\n    const shadowContainer = stubContainer.attachShadow({ mode: \"open\" });\n    const stub = document.createElement(\"span\");\n    stub.innerText = strItem[\"text/plain\"];\n    shadowContainer.appendChild(stub);\n    document.body.appendChild(stubContainer);\n\n    const selection = document.getSelection();\n    const range = document.createRange();\n    range.selectNodeContents(stub);\n    selection.removeAllRanges();\n    selection.addRange(range);\n\n    const onCopy = (ev) => {\n        for (const type in strItem) {\n            ev.clipboardData.setData(type, strItem[type]);\n        }\n        ev.preventDefault();\n    };\n    document.addEventListener(\"copy\", onCopy);\n    let result;\n    try {\n        result = document.execCommand(\"copy\");\n    } finally {\n        document.removeEventListener(\"copy\", onCopy);\n    }\n\n    selection.removeAllRanges();\n    document.body.removeChild(stubContainer);\n\n    return result;\n}\n\n/**\n * Only attempt to polyfill browsers that partially implement\n * the Clipboard API (aka. Firefox with `clipboard.write()` and\n * `ClipboardItem` behind a feature flag)\n *\n * Spec: https://w3c.github.io/clipboard-apis/\n */\nif (window.navigator.clipboard) {\n    if (!window.navigator.clipboard.write) {\n        window.navigator.clipboard.write = write.bind(window);\n    }\n    if (!window.ClipboardItem) {\n        window.ClipboardItem = ClipboardItemImpl;\n    }\n}\n", "/**\n * @popperjs/core v2.11.8 - MIT License\n */\n\n(function (global, factory) {\n  typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :\n  typeof define === 'function' && define.amd ? define(['exports'], factory) :\n  (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.Popper = {}));\n}(this, (function (exports) { 'use strict';\n\n  function getWindow(node) {\n    if (node == null) {\n      return window;\n    }\n\n    if (node.toString() !== '[object Window]') {\n      var ownerDocument = node.ownerDocument;\n      return ownerDocument ? ownerDocument.defaultView || window : window;\n    }\n\n    return node;\n  }\n\n  function isElement(node) {\n    var OwnElement = getWindow(node).Element;\n    return node instanceof OwnElement || node instanceof Element;\n  }\n\n  function isHTMLElement(node) {\n    var OwnElement = getWindow(node).HTMLElement;\n    return node instanceof OwnElement || node instanceof HTMLElement;\n  }\n\n  function isShadowRoot(node) {\n    // IE 11 has no ShadowRoot\n    if (typeof ShadowRoot === 'undefined') {\n      return false;\n    }\n\n    var OwnElement = getWindow(node).ShadowRoot;\n    return node instanceof OwnElement || node instanceof ShadowRoot;\n  }\n\n  var max = Math.max;\n  var min = Math.min;\n  var round = Math.round;\n\n  function getUAString() {\n    var uaData = navigator.userAgentData;\n\n    if (uaData != null && uaData.brands && Array.isArray(uaData.brands)) {\n      return uaData.brands.map(function (item) {\n        return item.brand + \"/\" + item.version;\n      }).join(' ');\n    }\n\n    return navigator.userAgent;\n  }\n\n  function isLayoutViewport() {\n    return !/^((?!chrome|android).)*safari/i.test(getUAString());\n  }\n\n  function getBoundingClientRect(element, includeScale, isFixedStrategy) {\n    if (includeScale === void 0) {\n      includeScale = false;\n    }\n\n    if (isFixedStrategy === void 0) {\n      isFixedStrategy = false;\n    }\n\n    var clientRect = element.getBoundingClientRect();\n    var scaleX = 1;\n    var scaleY = 1;\n\n    if (includeScale && isHTMLElement(element)) {\n      scaleX = element.offsetWidth > 0 ? round(clientRect.width) / element.offsetWidth || 1 : 1;\n      scaleY = element.offsetHeight > 0 ? round(clientRect.height) / element.offsetHeight || 1 : 1;\n    }\n\n    var _ref = isElement(element) ? getWindow(element) : window,\n        visualViewport = _ref.visualViewport;\n\n    var addVisualOffsets = !isLayoutViewport() && isFixedStrategy;\n    var x = (clientRect.left + (addVisualOffsets && visualViewport ? visualViewport.offsetLeft : 0)) / scaleX;\n    var y = (clientRect.top + (addVisualOffsets && visualViewport ? visualViewport.offsetTop : 0)) / scaleY;\n    var width = clientRect.width / scaleX;\n    var height = clientRect.height / scaleY;\n    return {\n      width: width,\n      height: height,\n      top: y,\n      right: x + width,\n      bottom: y + height,\n      left: x,\n      x: x,\n      y: y\n    };\n  }\n\n  function getWindowScroll(node) {\n    var win = getWindow(node);\n    var scrollLeft = win.pageXOffset;\n    var scrollTop = win.pageYOffset;\n    return {\n      scrollLeft: scrollLeft,\n      scrollTop: scrollTop\n    };\n  }\n\n  function getHTMLElementScroll(element) {\n    return {\n      scrollLeft: element.scrollLeft,\n      scrollTop: element.scrollTop\n    };\n  }\n\n  function getNodeScroll(node) {\n    if (node === getWindow(node) || !isHTMLElement(node)) {\n      return getWindowScroll(node);\n    } else {\n      return getHTMLElementScroll(node);\n    }\n  }\n\n  function getNodeName(element) {\n    return element ? (element.nodeName || '').toLowerCase() : null;\n  }\n\n  function getDocumentElement(element) {\n    // $FlowFixMe[incompatible-return]: assume body is always available\n    return ((isElement(element) ? element.ownerDocument : // $FlowFixMe[prop-missing]\n    element.document) || window.document).documentElement;\n  }\n\n  function getWindowScrollBarX(element) {\n    // If <html> has a CSS width greater than the viewport, then this will be\n    // incorrect for RTL.\n    // Popper 1 is broken in this case and never had a bug report so let's assume\n    // it's not an issue. I don't think anyone ever specifies width on <html>\n    // anyway.\n    // Browsers where the left scrollbar doesn't cause an issue report `0` for\n    // this (e.g. Edge 2019, IE11, Safari)\n    return getBoundingClientRect(getDocumentElement(element)).left + getWindowScroll(element).scrollLeft;\n  }\n\n  function getComputedStyle(element) {\n    return getWindow(element).getComputedStyle(element);\n  }\n\n  function isScrollParent(element) {\n    // Firefox wants us to check `-x` and `-y` variations as well\n    var _getComputedStyle = getComputedStyle(element),\n        overflow = _getComputedStyle.overflow,\n        overflowX = _getComputedStyle.overflowX,\n        overflowY = _getComputedStyle.overflowY;\n\n    return /auto|scroll|overlay|hidden/.test(overflow + overflowY + overflowX);\n  }\n\n  function isElementScaled(element) {\n    var rect = element.getBoundingClientRect();\n    var scaleX = round(rect.width) / element.offsetWidth || 1;\n    var scaleY = round(rect.height) / element.offsetHeight || 1;\n    return scaleX !== 1 || scaleY !== 1;\n  } // Returns the composite rect of an element relative to its offsetParent.\n  // Composite means it takes into account transforms as well as layout.\n\n\n  function getCompositeRect(elementOrVirtualElement, offsetParent, isFixed) {\n    if (isFixed === void 0) {\n      isFixed = false;\n    }\n\n    var isOffsetParentAnElement = isHTMLElement(offsetParent);\n    var offsetParentIsScaled = isHTMLElement(offsetParent) && isElementScaled(offsetParent);\n    var documentElement = getDocumentElement(offsetParent);\n    var rect = getBoundingClientRect(elementOrVirtualElement, offsetParentIsScaled, isFixed);\n    var scroll = {\n      scrollLeft: 0,\n      scrollTop: 0\n    };\n    var offsets = {\n      x: 0,\n      y: 0\n    };\n\n    if (isOffsetParentAnElement || !isOffsetParentAnElement && !isFixed) {\n      if (getNodeName(offsetParent) !== 'body' || // https://github.com/popperjs/popper-core/issues/1078\n      isScrollParent(documentElement)) {\n        scroll = getNodeScroll(offsetParent);\n      }\n\n      if (isHTMLElement(offsetParent)) {\n        offsets = getBoundingClientRect(offsetParent, true);\n        offsets.x += offsetParent.clientLeft;\n        offsets.y += offsetParent.clientTop;\n      } else if (documentElement) {\n        offsets.x = getWindowScrollBarX(documentElement);\n      }\n    }\n\n    return {\n      x: rect.left + scroll.scrollLeft - offsets.x,\n      y: rect.top + scroll.scrollTop - offsets.y,\n      width: rect.width,\n      height: rect.height\n    };\n  }\n\n  // means it doesn't take into account transforms.\n\n  function getLayoutRect(element) {\n    var clientRect = getBoundingClientRect(element); // Use the clientRect sizes if it's not been transformed.\n    // Fixes https://github.com/popperjs/popper-core/issues/1223\n\n    var width = element.offsetWidth;\n    var height = element.offsetHeight;\n\n    if (Math.abs(clientRect.width - width) <= 1) {\n      width = clientRect.width;\n    }\n\n    if (Math.abs(clientRect.height - height) <= 1) {\n      height = clientRect.height;\n    }\n\n    return {\n      x: element.offsetLeft,\n      y: element.offsetTop,\n      width: width,\n      height: height\n    };\n  }\n\n  function getParentNode(element) {\n    if (getNodeName(element) === 'html') {\n      return element;\n    }\n\n    return (// this is a quicker (but less type safe) way to save quite some bytes from the bundle\n      // $FlowFixMe[incompatible-return]\n      // $FlowFixMe[prop-missing]\n      element.assignedSlot || // step into the shadow DOM of the parent of a slotted node\n      element.parentNode || ( // DOM Element detected\n      isShadowRoot(element) ? element.host : null) || // ShadowRoot detected\n      // $FlowFixMe[incompatible-call]: HTMLElement is a Node\n      getDocumentElement(element) // fallback\n\n    );\n  }\n\n  function getScrollParent(node) {\n    if (['html', 'body', '#document'].indexOf(getNodeName(node)) >= 0) {\n      // $FlowFixMe[incompatible-return]: assume body is always available\n      return node.ownerDocument.body;\n    }\n\n    if (isHTMLElement(node) && isScrollParent(node)) {\n      return node;\n    }\n\n    return getScrollParent(getParentNode(node));\n  }\n\n  /*\n  given a DOM element, return the list of all scroll parents, up the list of ancesors\n  until we get to the top window object. This list is what we attach scroll listeners\n  to, because if any of these parent elements scroll, we'll need to re-calculate the\n  reference element's position.\n  */\n\n  function listScrollParents(element, list) {\n    var _element$ownerDocumen;\n\n    if (list === void 0) {\n      list = [];\n    }\n\n    var scrollParent = getScrollParent(element);\n    var isBody = scrollParent === ((_element$ownerDocumen = element.ownerDocument) == null ? void 0 : _element$ownerDocumen.body);\n    var win = getWindow(scrollParent);\n    var target = isBody ? [win].concat(win.visualViewport || [], isScrollParent(scrollParent) ? scrollParent : []) : scrollParent;\n    var updatedList = list.concat(target);\n    return isBody ? updatedList : // $FlowFixMe[incompatible-call]: isBody tells us target will be an HTMLElement here\n    updatedList.concat(listScrollParents(getParentNode(target)));\n  }\n\n  function isTableElement(element) {\n    return ['table', 'td', 'th'].indexOf(getNodeName(element)) >= 0;\n  }\n\n  function getTrueOffsetParent(element) {\n    if (!isHTMLElement(element) || // https://github.com/popperjs/popper-core/issues/837\n    getComputedStyle(element).position === 'fixed') {\n      return null;\n    }\n\n    return element.offsetParent;\n  } // `.offsetParent` reports `null` for fixed elements, while absolute elements\n  // return the containing block\n\n\n  function getContainingBlock(element) {\n    var isFirefox = /firefox/i.test(getUAString());\n    var isIE = /Trident/i.test(getUAString());\n\n    if (isIE && isHTMLElement(element)) {\n      // In IE 9, 10 and 11 fixed elements containing block is always established by the viewport\n      var elementCss = getComputedStyle(element);\n\n      if (elementCss.position === 'fixed') {\n        return null;\n      }\n    }\n\n    var currentNode = getParentNode(element);\n\n    if (isShadowRoot(currentNode)) {\n      currentNode = currentNode.host;\n    }\n\n    while (isHTMLElement(currentNode) && ['html', 'body'].indexOf(getNodeName(currentNode)) < 0) {\n      var css = getComputedStyle(currentNode); // This is non-exhaustive but covers the most common CSS properties that\n      // create a containing block.\n      // https://developer.mozilla.org/en-US/docs/Web/CSS/Containing_block#identifying_the_containing_block\n\n      if (css.transform !== 'none' || css.perspective !== 'none' || css.contain === 'paint' || ['transform', 'perspective'].indexOf(css.willChange) !== -1 || isFirefox && css.willChange === 'filter' || isFirefox && css.filter && css.filter !== 'none') {\n        return currentNode;\n      } else {\n        currentNode = currentNode.parentNode;\n      }\n    }\n\n    return null;\n  } // Gets the closest ancestor positioned element. Handles some edge cases,\n  // such as table ancestors and cross browser bugs.\n\n\n  function getOffsetParent(element) {\n    var window = getWindow(element);\n    var offsetParent = getTrueOffsetParent(element);\n\n    while (offsetParent && isTableElement(offsetParent) && getComputedStyle(offsetParent).position === 'static') {\n      offsetParent = getTrueOffsetParent(offsetParent);\n    }\n\n    if (offsetParent && (getNodeName(offsetParent) === 'html' || getNodeName(offsetParent) === 'body' && getComputedStyle(offsetParent).position === 'static')) {\n      return window;\n    }\n\n    return offsetParent || getContainingBlock(element) || window;\n  }\n\n  var top = 'top';\n  var bottom = 'bottom';\n  var right = 'right';\n  var left = 'left';\n  var auto = 'auto';\n  var basePlacements = [top, bottom, right, left];\n  var start = 'start';\n  var end = 'end';\n  var clippingParents = 'clippingParents';\n  var viewport = 'viewport';\n  var popper = 'popper';\n  var reference = 'reference';\n  var variationPlacements = /*#__PURE__*/basePlacements.reduce(function (acc, placement) {\n    return acc.concat([placement + \"-\" + start, placement + \"-\" + end]);\n  }, []);\n  var placements = /*#__PURE__*/[].concat(basePlacements, [auto]).reduce(function (acc, placement) {\n    return acc.concat([placement, placement + \"-\" + start, placement + \"-\" + end]);\n  }, []); // modifiers that need to read the DOM\n\n  var beforeRead = 'beforeRead';\n  var read = 'read';\n  var afterRead = 'afterRead'; // pure-logic modifiers\n\n  var beforeMain = 'beforeMain';\n  var main = 'main';\n  var afterMain = 'afterMain'; // modifier with the purpose to write to the DOM (or write into a framework state)\n\n  var beforeWrite = 'beforeWrite';\n  var write = 'write';\n  var afterWrite = 'afterWrite';\n  var modifierPhases = [beforeRead, read, afterRead, beforeMain, main, afterMain, beforeWrite, write, afterWrite];\n\n  function order(modifiers) {\n    var map = new Map();\n    var visited = new Set();\n    var result = [];\n    modifiers.forEach(function (modifier) {\n      map.set(modifier.name, modifier);\n    }); // On visiting object, check for its dependencies and visit them recursively\n\n    function sort(modifier) {\n      visited.add(modifier.name);\n      var requires = [].concat(modifier.requires || [], modifier.requiresIfExists || []);\n      requires.forEach(function (dep) {\n        if (!visited.has(dep)) {\n          var depModifier = map.get(dep);\n\n          if (depModifier) {\n            sort(depModifier);\n          }\n        }\n      });\n      result.push(modifier);\n    }\n\n    modifiers.forEach(function (modifier) {\n      if (!visited.has(modifier.name)) {\n        // check for visited object\n        sort(modifier);\n      }\n    });\n    return result;\n  }\n\n  function orderModifiers(modifiers) {\n    // order based on dependencies\n    var orderedModifiers = order(modifiers); // order based on phase\n\n    return modifierPhases.reduce(function (acc, phase) {\n      return acc.concat(orderedModifiers.filter(function (modifier) {\n        return modifier.phase === phase;\n      }));\n    }, []);\n  }\n\n  function debounce(fn) {\n    var pending;\n    return function () {\n      if (!pending) {\n        pending = new Promise(function (resolve) {\n          Promise.resolve().then(function () {\n            pending = undefined;\n            resolve(fn());\n          });\n        });\n      }\n\n      return pending;\n    };\n  }\n\n  function mergeByName(modifiers) {\n    var merged = modifiers.reduce(function (merged, current) {\n      var existing = merged[current.name];\n      merged[current.name] = existing ? Object.assign({}, existing, current, {\n        options: Object.assign({}, existing.options, current.options),\n        data: Object.assign({}, existing.data, current.data)\n      }) : current;\n      return merged;\n    }, {}); // IE11 does not support Object.values\n\n    return Object.keys(merged).map(function (key) {\n      return merged[key];\n    });\n  }\n\n  function getViewportRect(element, strategy) {\n    var win = getWindow(element);\n    var html = getDocumentElement(element);\n    var visualViewport = win.visualViewport;\n    var width = html.clientWidth;\n    var height = html.clientHeight;\n    var x = 0;\n    var y = 0;\n\n    if (visualViewport) {\n      width = visualViewport.width;\n      height = visualViewport.height;\n      var layoutViewport = isLayoutViewport();\n\n      if (layoutViewport || !layoutViewport && strategy === 'fixed') {\n        x = visualViewport.offsetLeft;\n        y = visualViewport.offsetTop;\n      }\n    }\n\n    return {\n      width: width,\n      height: height,\n      x: x + getWindowScrollBarX(element),\n      y: y\n    };\n  }\n\n  // of the `<html>` and `<body>` rect bounds if horizontally scrollable\n\n  function getDocumentRect(element) {\n    var _element$ownerDocumen;\n\n    var html = getDocumentElement(element);\n    var winScroll = getWindowScroll(element);\n    var body = (_element$ownerDocumen = element.ownerDocument) == null ? void 0 : _element$ownerDocumen.body;\n    var width = max(html.scrollWidth, html.clientWidth, body ? body.scrollWidth : 0, body ? body.clientWidth : 0);\n    var height = max(html.scrollHeight, html.clientHeight, body ? body.scrollHeight : 0, body ? body.clientHeight : 0);\n    var x = -winScroll.scrollLeft + getWindowScrollBarX(element);\n    var y = -winScroll.scrollTop;\n\n    if (getComputedStyle(body || html).direction === 'rtl') {\n      x += max(html.clientWidth, body ? body.clientWidth : 0) - width;\n    }\n\n    return {\n      width: width,\n      height: height,\n      x: x,\n      y: y\n    };\n  }\n\n  function contains(parent, child) {\n    var rootNode = child.getRootNode && child.getRootNode(); // First, attempt with faster native method\n\n    if (parent.contains(child)) {\n      return true;\n    } // then fallback to custom implementation with Shadow DOM support\n    else if (rootNode && isShadowRoot(rootNode)) {\n        var next = child;\n\n        do {\n          if (next && parent.isSameNode(next)) {\n            return true;\n          } // $FlowFixMe[prop-missing]: need a better way to handle this...\n\n\n          next = next.parentNode || next.host;\n        } while (next);\n      } // Give up, the result is false\n\n\n    return false;\n  }\n\n  function rectToClientRect(rect) {\n    return Object.assign({}, rect, {\n      left: rect.x,\n      top: rect.y,\n      right: rect.x + rect.width,\n      bottom: rect.y + rect.height\n    });\n  }\n\n  function getInnerBoundingClientRect(element, strategy) {\n    var rect = getBoundingClientRect(element, false, strategy === 'fixed');\n    rect.top = rect.top + element.clientTop;\n    rect.left = rect.left + element.clientLeft;\n    rect.bottom = rect.top + element.clientHeight;\n    rect.right = rect.left + element.clientWidth;\n    rect.width = element.clientWidth;\n    rect.height = element.clientHeight;\n    rect.x = rect.left;\n    rect.y = rect.top;\n    return rect;\n  }\n\n  function getClientRectFromMixedType(element, clippingParent, strategy) {\n    return clippingParent === viewport ? rectToClientRect(getViewportRect(element, strategy)) : isElement(clippingParent) ? getInnerBoundingClientRect(clippingParent, strategy) : rectToClientRect(getDocumentRect(getDocumentElement(element)));\n  } // A \"clipping parent\" is an overflowable container with the characteristic of\n  // clipping (or hiding) overflowing elements with a position different from\n  // `initial`\n\n\n  function getClippingParents(element) {\n    var clippingParents = listScrollParents(getParentNode(element));\n    var canEscapeClipping = ['absolute', 'fixed'].indexOf(getComputedStyle(element).position) >= 0;\n    var clipperElement = canEscapeClipping && isHTMLElement(element) ? getOffsetParent(element) : element;\n\n    if (!isElement(clipperElement)) {\n      return [];\n    } // $FlowFixMe[incompatible-return]: https://github.com/facebook/flow/issues/1414\n\n\n    return clippingParents.filter(function (clippingParent) {\n      return isElement(clippingParent) && contains(clippingParent, clipperElement) && getNodeName(clippingParent) !== 'body';\n    });\n  } // Gets the maximum area that the element is visible in due to any number of\n  // clipping parents\n\n\n  function getClippingRect(element, boundary, rootBoundary, strategy) {\n    var mainClippingParents = boundary === 'clippingParents' ? getClippingParents(element) : [].concat(boundary);\n    var clippingParents = [].concat(mainClippingParents, [rootBoundary]);\n    var firstClippingParent = clippingParents[0];\n    var clippingRect = clippingParents.reduce(function (accRect, clippingParent) {\n      var rect = getClientRectFromMixedType(element, clippingParent, strategy);\n      accRect.top = max(rect.top, accRect.top);\n      accRect.right = min(rect.right, accRect.right);\n      accRect.bottom = min(rect.bottom, accRect.bottom);\n      accRect.left = max(rect.left, accRect.left);\n      return accRect;\n    }, getClientRectFromMixedType(element, firstClippingParent, strategy));\n    clippingRect.width = clippingRect.right - clippingRect.left;\n    clippingRect.height = clippingRect.bottom - clippingRect.top;\n    clippingRect.x = clippingRect.left;\n    clippingRect.y = clippingRect.top;\n    return clippingRect;\n  }\n\n  function getBasePlacement(placement) {\n    return placement.split('-')[0];\n  }\n\n  function getVariation(placement) {\n    return placement.split('-')[1];\n  }\n\n  function getMainAxisFromPlacement(placement) {\n    return ['top', 'bottom'].indexOf(placement) >= 0 ? 'x' : 'y';\n  }\n\n  function computeOffsets(_ref) {\n    var reference = _ref.reference,\n        element = _ref.element,\n        placement = _ref.placement;\n    var basePlacement = placement ? getBasePlacement(placement) : null;\n    var variation = placement ? getVariation(placement) : null;\n    var commonX = reference.x + reference.width / 2 - element.width / 2;\n    var commonY = reference.y + reference.height / 2 - element.height / 2;\n    var offsets;\n\n    switch (basePlacement) {\n      case top:\n        offsets = {\n          x: commonX,\n          y: reference.y - element.height\n        };\n        break;\n\n      case bottom:\n        offsets = {\n          x: commonX,\n          y: reference.y + reference.height\n        };\n        break;\n\n      case right:\n        offsets = {\n          x: reference.x + reference.width,\n          y: commonY\n        };\n        break;\n\n      case left:\n        offsets = {\n          x: reference.x - element.width,\n          y: commonY\n        };\n        break;\n\n      default:\n        offsets = {\n          x: reference.x,\n          y: reference.y\n        };\n    }\n\n    var mainAxis = basePlacement ? getMainAxisFromPlacement(basePlacement) : null;\n\n    if (mainAxis != null) {\n      var len = mainAxis === 'y' ? 'height' : 'width';\n\n      switch (variation) {\n        case start:\n          offsets[mainAxis] = offsets[mainAxis] - (reference[len] / 2 - element[len] / 2);\n          break;\n\n        case end:\n          offsets[mainAxis] = offsets[mainAxis] + (reference[len] / 2 - element[len] / 2);\n          break;\n      }\n    }\n\n    return offsets;\n  }\n\n  function getFreshSideObject() {\n    return {\n      top: 0,\n      right: 0,\n      bottom: 0,\n      left: 0\n    };\n  }\n\n  function mergePaddingObject(paddingObject) {\n    return Object.assign({}, getFreshSideObject(), paddingObject);\n  }\n\n  function expandToHashMap(value, keys) {\n    return keys.reduce(function (hashMap, key) {\n      hashMap[key] = value;\n      return hashMap;\n    }, {});\n  }\n\n  function detectOverflow(state, options) {\n    if (options === void 0) {\n      options = {};\n    }\n\n    var _options = options,\n        _options$placement = _options.placement,\n        placement = _options$placement === void 0 ? state.placement : _options$placement,\n        _options$strategy = _options.strategy,\n        strategy = _options$strategy === void 0 ? state.strategy : _options$strategy,\n        _options$boundary = _options.boundary,\n        boundary = _options$boundary === void 0 ? clippingParents : _options$boundary,\n        _options$rootBoundary = _options.rootBoundary,\n        rootBoundary = _options$rootBoundary === void 0 ? viewport : _options$rootBoundary,\n        _options$elementConte = _options.elementContext,\n        elementContext = _options$elementConte === void 0 ? popper : _options$elementConte,\n        _options$altBoundary = _options.altBoundary,\n        altBoundary = _options$altBoundary === void 0 ? false : _options$altBoundary,\n        _options$padding = _options.padding,\n        padding = _options$padding === void 0 ? 0 : _options$padding;\n    var paddingObject = mergePaddingObject(typeof padding !== 'number' ? padding : expandToHashMap(padding, basePlacements));\n    var altContext = elementContext === popper ? reference : popper;\n    var popperRect = state.rects.popper;\n    var element = state.elements[altBoundary ? altContext : elementContext];\n    var clippingClientRect = getClippingRect(isElement(element) ? element : element.contextElement || getDocumentElement(state.elements.popper), boundary, rootBoundary, strategy);\n    var referenceClientRect = getBoundingClientRect(state.elements.reference);\n    var popperOffsets = computeOffsets({\n      reference: referenceClientRect,\n      element: popperRect,\n      strategy: 'absolute',\n      placement: placement\n    });\n    var popperClientRect = rectToClientRect(Object.assign({}, popperRect, popperOffsets));\n    var elementClientRect = elementContext === popper ? popperClientRect : referenceClientRect; // positive = overflowing the clipping rect\n    // 0 or negative = within the clipping rect\n\n    var overflowOffsets = {\n      top: clippingClientRect.top - elementClientRect.top + paddingObject.top,\n      bottom: elementClientRect.bottom - clippingClientRect.bottom + paddingObject.bottom,\n      left: clippingClientRect.left - elementClientRect.left + paddingObject.left,\n      right: elementClientRect.right - clippingClientRect.right + paddingObject.right\n    };\n    var offsetData = state.modifiersData.offset; // Offsets can be applied only to the popper element\n\n    if (elementContext === popper && offsetData) {\n      var offset = offsetData[placement];\n      Object.keys(overflowOffsets).forEach(function (key) {\n        var multiply = [right, bottom].indexOf(key) >= 0 ? 1 : -1;\n        var axis = [top, bottom].indexOf(key) >= 0 ? 'y' : 'x';\n        overflowOffsets[key] += offset[axis] * multiply;\n      });\n    }\n\n    return overflowOffsets;\n  }\n\n  var DEFAULT_OPTIONS = {\n    placement: 'bottom',\n    modifiers: [],\n    strategy: 'absolute'\n  };\n\n  function areValidElements() {\n    for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {\n      args[_key] = arguments[_key];\n    }\n\n    return !args.some(function (element) {\n      return !(element && typeof element.getBoundingClientRect === 'function');\n    });\n  }\n\n  function popperGenerator(generatorOptions) {\n    if (generatorOptions === void 0) {\n      generatorOptions = {};\n    }\n\n    var _generatorOptions = generatorOptions,\n        _generatorOptions$def = _generatorOptions.defaultModifiers,\n        defaultModifiers = _generatorOptions$def === void 0 ? [] : _generatorOptions$def,\n        _generatorOptions$def2 = _generatorOptions.defaultOptions,\n        defaultOptions = _generatorOptions$def2 === void 0 ? DEFAULT_OPTIONS : _generatorOptions$def2;\n    return function createPopper(reference, popper, options) {\n      if (options === void 0) {\n        options = defaultOptions;\n      }\n\n      var state = {\n        placement: 'bottom',\n        orderedModifiers: [],\n        options: Object.assign({}, DEFAULT_OPTIONS, defaultOptions),\n        modifiersData: {},\n        elements: {\n          reference: reference,\n          popper: popper\n        },\n        attributes: {},\n        styles: {}\n      };\n      var effectCleanupFns = [];\n      var isDestroyed = false;\n      var instance = {\n        state: state,\n        setOptions: function setOptions(setOptionsAction) {\n          var options = typeof setOptionsAction === 'function' ? setOptionsAction(state.options) : setOptionsAction;\n          cleanupModifierEffects();\n          state.options = Object.assign({}, defaultOptions, state.options, options);\n          state.scrollParents = {\n            reference: isElement(reference) ? listScrollParents(reference) : reference.contextElement ? listScrollParents(reference.contextElement) : [],\n            popper: listScrollParents(popper)\n          }; // Orders the modifiers based on their dependencies and `phase`\n          // properties\n\n          var orderedModifiers = orderModifiers(mergeByName([].concat(defaultModifiers, state.options.modifiers))); // Strip out disabled modifiers\n\n          state.orderedModifiers = orderedModifiers.filter(function (m) {\n            return m.enabled;\n          });\n          runModifierEffects();\n          return instance.update();\n        },\n        // Sync update \u2013 it will always be executed, even if not necessary. This\n        // is useful for low frequency updates where sync behavior simplifies the\n        // logic.\n        // For high frequency updates (e.g. `resize` and `scroll` events), always\n        // prefer the async Popper#update method\n        forceUpdate: function forceUpdate() {\n          if (isDestroyed) {\n            return;\n          }\n\n          var _state$elements = state.elements,\n              reference = _state$elements.reference,\n              popper = _state$elements.popper; // Don't proceed if `reference` or `popper` are not valid elements\n          // anymore\n\n          if (!areValidElements(reference, popper)) {\n            return;\n          } // Store the reference and popper rects to be read by modifiers\n\n\n          state.rects = {\n            reference: getCompositeRect(reference, getOffsetParent(popper), state.options.strategy === 'fixed'),\n            popper: getLayoutRect(popper)\n          }; // Modifiers have the ability to reset the current update cycle. The\n          // most common use case for this is the `flip` modifier changing the\n          // placement, which then needs to re-run all the modifiers, because the\n          // logic was previously ran for the previous placement and is therefore\n          // stale/incorrect\n\n          state.reset = false;\n          state.placement = state.options.placement; // On each update cycle, the `modifiersData` property for each modifier\n          // is filled with the initial data specified by the modifier. This means\n          // it doesn't persist and is fresh on each update.\n          // To ensure persistent data, use `${name}#persistent`\n\n          state.orderedModifiers.forEach(function (modifier) {\n            return state.modifiersData[modifier.name] = Object.assign({}, modifier.data);\n          });\n\n          for (var index = 0; index < state.orderedModifiers.length; index++) {\n            if (state.reset === true) {\n              state.reset = false;\n              index = -1;\n              continue;\n            }\n\n            var _state$orderedModifie = state.orderedModifiers[index],\n                fn = _state$orderedModifie.fn,\n                _state$orderedModifie2 = _state$orderedModifie.options,\n                _options = _state$orderedModifie2 === void 0 ? {} : _state$orderedModifie2,\n                name = _state$orderedModifie.name;\n\n            if (typeof fn === 'function') {\n              state = fn({\n                state: state,\n                options: _options,\n                name: name,\n                instance: instance\n              }) || state;\n            }\n          }\n        },\n        // Async and optimistically optimized update \u2013 it will not be executed if\n        // not necessary (debounced to run at most once-per-tick)\n        update: debounce(function () {\n          return new Promise(function (resolve) {\n            instance.forceUpdate();\n            resolve(state);\n          });\n        }),\n        destroy: function destroy() {\n          cleanupModifierEffects();\n          isDestroyed = true;\n        }\n      };\n\n      if (!areValidElements(reference, popper)) {\n        return instance;\n      }\n\n      instance.setOptions(options).then(function (state) {\n        if (!isDestroyed && options.onFirstUpdate) {\n          options.onFirstUpdate(state);\n        }\n      }); // Modifiers have the ability to execute arbitrary code before the first\n      // update cycle runs. They will be executed in the same order as the update\n      // cycle. This is useful when a modifier adds some persistent data that\n      // other modifiers need to use, but the modifier is run after the dependent\n      // one.\n\n      function runModifierEffects() {\n        state.orderedModifiers.forEach(function (_ref) {\n          var name = _ref.name,\n              _ref$options = _ref.options,\n              options = _ref$options === void 0 ? {} : _ref$options,\n              effect = _ref.effect;\n\n          if (typeof effect === 'function') {\n            var cleanupFn = effect({\n              state: state,\n              name: name,\n              instance: instance,\n              options: options\n            });\n\n            var noopFn = function noopFn() {};\n\n            effectCleanupFns.push(cleanupFn || noopFn);\n          }\n        });\n      }\n\n      function cleanupModifierEffects() {\n        effectCleanupFns.forEach(function (fn) {\n          return fn();\n        });\n        effectCleanupFns = [];\n      }\n\n      return instance;\n    };\n  }\n\n  var passive = {\n    passive: true\n  };\n\n  function effect$2(_ref) {\n    var state = _ref.state,\n        instance = _ref.instance,\n        options = _ref.options;\n    var _options$scroll = options.scroll,\n        scroll = _options$scroll === void 0 ? true : _options$scroll,\n        _options$resize = options.resize,\n        resize = _options$resize === void 0 ? true : _options$resize;\n    var window = getWindow(state.elements.popper);\n    var scrollParents = [].concat(state.scrollParents.reference, state.scrollParents.popper);\n\n    if (scroll) {\n      scrollParents.forEach(function (scrollParent) {\n        scrollParent.addEventListener('scroll', instance.update, passive);\n      });\n    }\n\n    if (resize) {\n      window.addEventListener('resize', instance.update, passive);\n    }\n\n    return function () {\n      if (scroll) {\n        scrollParents.forEach(function (scrollParent) {\n          scrollParent.removeEventListener('scroll', instance.update, passive);\n        });\n      }\n\n      if (resize) {\n        window.removeEventListener('resize', instance.update, passive);\n      }\n    };\n  } // eslint-disable-next-line import/no-unused-modules\n\n\n  var eventListeners = {\n    name: 'eventListeners',\n    enabled: true,\n    phase: 'write',\n    fn: function fn() {},\n    effect: effect$2,\n    data: {}\n  };\n\n  function popperOffsets(_ref) {\n    var state = _ref.state,\n        name = _ref.name;\n    // Offsets are the actual position the popper needs to have to be\n    // properly positioned near its reference element\n    // This is the most basic placement, and will be adjusted by\n    // the modifiers in the next step\n    state.modifiersData[name] = computeOffsets({\n      reference: state.rects.reference,\n      element: state.rects.popper,\n      strategy: 'absolute',\n      placement: state.placement\n    });\n  } // eslint-disable-next-line import/no-unused-modules\n\n\n  var popperOffsets$1 = {\n    name: 'popperOffsets',\n    enabled: true,\n    phase: 'read',\n    fn: popperOffsets,\n    data: {}\n  };\n\n  var unsetSides = {\n    top: 'auto',\n    right: 'auto',\n    bottom: 'auto',\n    left: 'auto'\n  }; // Round the offsets to the nearest suitable subpixel based on the DPR.\n  // Zooming can change the DPR, but it seems to report a value that will\n  // cleanly divide the values into the appropriate subpixels.\n\n  function roundOffsetsByDPR(_ref, win) {\n    var x = _ref.x,\n        y = _ref.y;\n    var dpr = win.devicePixelRatio || 1;\n    return {\n      x: round(x * dpr) / dpr || 0,\n      y: round(y * dpr) / dpr || 0\n    };\n  }\n\n  function mapToStyles(_ref2) {\n    var _Object$assign2;\n\n    var popper = _ref2.popper,\n        popperRect = _ref2.popperRect,\n        placement = _ref2.placement,\n        variation = _ref2.variation,\n        offsets = _ref2.offsets,\n        position = _ref2.position,\n        gpuAcceleration = _ref2.gpuAcceleration,\n        adaptive = _ref2.adaptive,\n        roundOffsets = _ref2.roundOffsets,\n        isFixed = _ref2.isFixed;\n    var _offsets$x = offsets.x,\n        x = _offsets$x === void 0 ? 0 : _offsets$x,\n        _offsets$y = offsets.y,\n        y = _offsets$y === void 0 ? 0 : _offsets$y;\n\n    var _ref3 = typeof roundOffsets === 'function' ? roundOffsets({\n      x: x,\n      y: y\n    }) : {\n      x: x,\n      y: y\n    };\n\n    x = _ref3.x;\n    y = _ref3.y;\n    var hasX = offsets.hasOwnProperty('x');\n    var hasY = offsets.hasOwnProperty('y');\n    var sideX = left;\n    var sideY = top;\n    var win = window;\n\n    if (adaptive) {\n      var offsetParent = getOffsetParent(popper);\n      var heightProp = 'clientHeight';\n      var widthProp = 'clientWidth';\n\n      if (offsetParent === getWindow(popper)) {\n        offsetParent = getDocumentElement(popper);\n\n        if (getComputedStyle(offsetParent).position !== 'static' && position === 'absolute') {\n          heightProp = 'scrollHeight';\n          widthProp = 'scrollWidth';\n        }\n      } // $FlowFixMe[incompatible-cast]: force type refinement, we compare offsetParent with window above, but Flow doesn't detect it\n\n\n      offsetParent = offsetParent;\n\n      if (placement === top || (placement === left || placement === right) && variation === end) {\n        sideY = bottom;\n        var offsetY = isFixed && offsetParent === win && win.visualViewport ? win.visualViewport.height : // $FlowFixMe[prop-missing]\n        offsetParent[heightProp];\n        y -= offsetY - popperRect.height;\n        y *= gpuAcceleration ? 1 : -1;\n      }\n\n      if (placement === left || (placement === top || placement === bottom) && variation === end) {\n        sideX = right;\n        var offsetX = isFixed && offsetParent === win && win.visualViewport ? win.visualViewport.width : // $FlowFixMe[prop-missing]\n        offsetParent[widthProp];\n        x -= offsetX - popperRect.width;\n        x *= gpuAcceleration ? 1 : -1;\n      }\n    }\n\n    var commonStyles = Object.assign({\n      position: position\n    }, adaptive && unsetSides);\n\n    var _ref4 = roundOffsets === true ? roundOffsetsByDPR({\n      x: x,\n      y: y\n    }, getWindow(popper)) : {\n      x: x,\n      y: y\n    };\n\n    x = _ref4.x;\n    y = _ref4.y;\n\n    if (gpuAcceleration) {\n      var _Object$assign;\n\n      return Object.assign({}, commonStyles, (_Object$assign = {}, _Object$assign[sideY] = hasY ? '0' : '', _Object$assign[sideX] = hasX ? '0' : '', _Object$assign.transform = (win.devicePixelRatio || 1) <= 1 ? \"translate(\" + x + \"px, \" + y + \"px)\" : \"translate3d(\" + x + \"px, \" + y + \"px, 0)\", _Object$assign));\n    }\n\n    return Object.assign({}, commonStyles, (_Object$assign2 = {}, _Object$assign2[sideY] = hasY ? y + \"px\" : '', _Object$assign2[sideX] = hasX ? x + \"px\" : '', _Object$assign2.transform = '', _Object$assign2));\n  }\n\n  function computeStyles(_ref5) {\n    var state = _ref5.state,\n        options = _ref5.options;\n    var _options$gpuAccelerat = options.gpuAcceleration,\n        gpuAcceleration = _options$gpuAccelerat === void 0 ? true : _options$gpuAccelerat,\n        _options$adaptive = options.adaptive,\n        adaptive = _options$adaptive === void 0 ? true : _options$adaptive,\n        _options$roundOffsets = options.roundOffsets,\n        roundOffsets = _options$roundOffsets === void 0 ? true : _options$roundOffsets;\n    var commonStyles = {\n      placement: getBasePlacement(state.placement),\n      variation: getVariation(state.placement),\n      popper: state.elements.popper,\n      popperRect: state.rects.popper,\n      gpuAcceleration: gpuAcceleration,\n      isFixed: state.options.strategy === 'fixed'\n    };\n\n    if (state.modifiersData.popperOffsets != null) {\n      state.styles.popper = Object.assign({}, state.styles.popper, mapToStyles(Object.assign({}, commonStyles, {\n        offsets: state.modifiersData.popperOffsets,\n        position: state.options.strategy,\n        adaptive: adaptive,\n        roundOffsets: roundOffsets\n      })));\n    }\n\n    if (state.modifiersData.arrow != null) {\n      state.styles.arrow = Object.assign({}, state.styles.arrow, mapToStyles(Object.assign({}, commonStyles, {\n        offsets: state.modifiersData.arrow,\n        position: 'absolute',\n        adaptive: false,\n        roundOffsets: roundOffsets\n      })));\n    }\n\n    state.attributes.popper = Object.assign({}, state.attributes.popper, {\n      'data-popper-placement': state.placement\n    });\n  } // eslint-disable-next-line import/no-unused-modules\n\n\n  var computeStyles$1 = {\n    name: 'computeStyles',\n    enabled: true,\n    phase: 'beforeWrite',\n    fn: computeStyles,\n    data: {}\n  };\n\n  // and applies them to the HTMLElements such as popper and arrow\n\n  function applyStyles(_ref) {\n    var state = _ref.state;\n    Object.keys(state.elements).forEach(function (name) {\n      var style = state.styles[name] || {};\n      var attributes = state.attributes[name] || {};\n      var element = state.elements[name]; // arrow is optional + virtual elements\n\n      if (!isHTMLElement(element) || !getNodeName(element)) {\n        return;\n      } // Flow doesn't support to extend this property, but it's the most\n      // effective way to apply styles to an HTMLElement\n      // $FlowFixMe[cannot-write]\n\n\n      Object.assign(element.style, style);\n      Object.keys(attributes).forEach(function (name) {\n        var value = attributes[name];\n\n        if (value === false) {\n          element.removeAttribute(name);\n        } else {\n          element.setAttribute(name, value === true ? '' : value);\n        }\n      });\n    });\n  }\n\n  function effect$1(_ref2) {\n    var state = _ref2.state;\n    var initialStyles = {\n      popper: {\n        position: state.options.strategy,\n        left: '0',\n        top: '0',\n        margin: '0'\n      },\n      arrow: {\n        position: 'absolute'\n      },\n      reference: {}\n    };\n    Object.assign(state.elements.popper.style, initialStyles.popper);\n    state.styles = initialStyles;\n\n    if (state.elements.arrow) {\n      Object.assign(state.elements.arrow.style, initialStyles.arrow);\n    }\n\n    return function () {\n      Object.keys(state.elements).forEach(function (name) {\n        var element = state.elements[name];\n        var attributes = state.attributes[name] || {};\n        var styleProperties = Object.keys(state.styles.hasOwnProperty(name) ? state.styles[name] : initialStyles[name]); // Set all values to an empty string to unset them\n\n        var style = styleProperties.reduce(function (style, property) {\n          style[property] = '';\n          return style;\n        }, {}); // arrow is optional + virtual elements\n\n        if (!isHTMLElement(element) || !getNodeName(element)) {\n          return;\n        }\n\n        Object.assign(element.style, style);\n        Object.keys(attributes).forEach(function (attribute) {\n          element.removeAttribute(attribute);\n        });\n      });\n    };\n  } // eslint-disable-next-line import/no-unused-modules\n\n\n  var applyStyles$1 = {\n    name: 'applyStyles',\n    enabled: true,\n    phase: 'write',\n    fn: applyStyles,\n    effect: effect$1,\n    requires: ['computeStyles']\n  };\n\n  function distanceAndSkiddingToXY(placement, rects, offset) {\n    var basePlacement = getBasePlacement(placement);\n    var invertDistance = [left, top].indexOf(basePlacement) >= 0 ? -1 : 1;\n\n    var _ref = typeof offset === 'function' ? offset(Object.assign({}, rects, {\n      placement: placement\n    })) : offset,\n        skidding = _ref[0],\n        distance = _ref[1];\n\n    skidding = skidding || 0;\n    distance = (distance || 0) * invertDistance;\n    return [left, right].indexOf(basePlacement) >= 0 ? {\n      x: distance,\n      y: skidding\n    } : {\n      x: skidding,\n      y: distance\n    };\n  }\n\n  function offset(_ref2) {\n    var state = _ref2.state,\n        options = _ref2.options,\n        name = _ref2.name;\n    var _options$offset = options.offset,\n        offset = _options$offset === void 0 ? [0, 0] : _options$offset;\n    var data = placements.reduce(function (acc, placement) {\n      acc[placement] = distanceAndSkiddingToXY(placement, state.rects, offset);\n      return acc;\n    }, {});\n    var _data$state$placement = data[state.placement],\n        x = _data$state$placement.x,\n        y = _data$state$placement.y;\n\n    if (state.modifiersData.popperOffsets != null) {\n      state.modifiersData.popperOffsets.x += x;\n      state.modifiersData.popperOffsets.y += y;\n    }\n\n    state.modifiersData[name] = data;\n  } // eslint-disable-next-line import/no-unused-modules\n\n\n  var offset$1 = {\n    name: 'offset',\n    enabled: true,\n    phase: 'main',\n    requires: ['popperOffsets'],\n    fn: offset\n  };\n\n  var hash$1 = {\n    left: 'right',\n    right: 'left',\n    bottom: 'top',\n    top: 'bottom'\n  };\n  function getOppositePlacement(placement) {\n    return placement.replace(/left|right|bottom|top/g, function (matched) {\n      return hash$1[matched];\n    });\n  }\n\n  var hash = {\n    start: 'end',\n    end: 'start'\n  };\n  function getOppositeVariationPlacement(placement) {\n    return placement.replace(/start|end/g, function (matched) {\n      return hash[matched];\n    });\n  }\n\n  function computeAutoPlacement(state, options) {\n    if (options === void 0) {\n      options = {};\n    }\n\n    var _options = options,\n        placement = _options.placement,\n        boundary = _options.boundary,\n        rootBoundary = _options.rootBoundary,\n        padding = _options.padding,\n        flipVariations = _options.flipVariations,\n        _options$allowedAutoP = _options.allowedAutoPlacements,\n        allowedAutoPlacements = _options$allowedAutoP === void 0 ? placements : _options$allowedAutoP;\n    var variation = getVariation(placement);\n    var placements$1 = variation ? flipVariations ? variationPlacements : variationPlacements.filter(function (placement) {\n      return getVariation(placement) === variation;\n    }) : basePlacements;\n    var allowedPlacements = placements$1.filter(function (placement) {\n      return allowedAutoPlacements.indexOf(placement) >= 0;\n    });\n\n    if (allowedPlacements.length === 0) {\n      allowedPlacements = placements$1;\n    } // $FlowFixMe[incompatible-type]: Flow seems to have problems with two array unions...\n\n\n    var overflows = allowedPlacements.reduce(function (acc, placement) {\n      acc[placement] = detectOverflow(state, {\n        placement: placement,\n        boundary: boundary,\n        rootBoundary: rootBoundary,\n        padding: padding\n      })[getBasePlacement(placement)];\n      return acc;\n    }, {});\n    return Object.keys(overflows).sort(function (a, b) {\n      return overflows[a] - overflows[b];\n    });\n  }\n\n  function getExpandedFallbackPlacements(placement) {\n    if (getBasePlacement(placement) === auto) {\n      return [];\n    }\n\n    var oppositePlacement = getOppositePlacement(placement);\n    return [getOppositeVariationPlacement(placement), oppositePlacement, getOppositeVariationPlacement(oppositePlacement)];\n  }\n\n  function flip(_ref) {\n    var state = _ref.state,\n        options = _ref.options,\n        name = _ref.name;\n\n    if (state.modifiersData[name]._skip) {\n      return;\n    }\n\n    var _options$mainAxis = options.mainAxis,\n        checkMainAxis = _options$mainAxis === void 0 ? true : _options$mainAxis,\n        _options$altAxis = options.altAxis,\n        checkAltAxis = _options$altAxis === void 0 ? true : _options$altAxis,\n        specifiedFallbackPlacements = options.fallbackPlacements,\n        padding = options.padding,\n        boundary = options.boundary,\n        rootBoundary = options.rootBoundary,\n        altBoundary = options.altBoundary,\n        _options$flipVariatio = options.flipVariations,\n        flipVariations = _options$flipVariatio === void 0 ? true : _options$flipVariatio,\n        allowedAutoPlacements = options.allowedAutoPlacements;\n    var preferredPlacement = state.options.placement;\n    var basePlacement = getBasePlacement(preferredPlacement);\n    var isBasePlacement = basePlacement === preferredPlacement;\n    var fallbackPlacements = specifiedFallbackPlacements || (isBasePlacement || !flipVariations ? [getOppositePlacement(preferredPlacement)] : getExpandedFallbackPlacements(preferredPlacement));\n    var placements = [preferredPlacement].concat(fallbackPlacements).reduce(function (acc, placement) {\n      return acc.concat(getBasePlacement(placement) === auto ? computeAutoPlacement(state, {\n        placement: placement,\n        boundary: boundary,\n        rootBoundary: rootBoundary,\n        padding: padding,\n        flipVariations: flipVariations,\n        allowedAutoPlacements: allowedAutoPlacements\n      }) : placement);\n    }, []);\n    var referenceRect = state.rects.reference;\n    var popperRect = state.rects.popper;\n    var checksMap = new Map();\n    var makeFallbackChecks = true;\n    var firstFittingPlacement = placements[0];\n\n    for (var i = 0; i < placements.length; i++) {\n      var placement = placements[i];\n\n      var _basePlacement = getBasePlacement(placement);\n\n      var isStartVariation = getVariation(placement) === start;\n      var isVertical = [top, bottom].indexOf(_basePlacement) >= 0;\n      var len = isVertical ? 'width' : 'height';\n      var overflow = detectOverflow(state, {\n        placement: placement,\n        boundary: boundary,\n        rootBoundary: rootBoundary,\n        altBoundary: altBoundary,\n        padding: padding\n      });\n      var mainVariationSide = isVertical ? isStartVariation ? right : left : isStartVariation ? bottom : top;\n\n      if (referenceRect[len] > popperRect[len]) {\n        mainVariationSide = getOppositePlacement(mainVariationSide);\n      }\n\n      var altVariationSide = getOppositePlacement(mainVariationSide);\n      var checks = [];\n\n      if (checkMainAxis) {\n        checks.push(overflow[_basePlacement] <= 0);\n      }\n\n      if (checkAltAxis) {\n        checks.push(overflow[mainVariationSide] <= 0, overflow[altVariationSide] <= 0);\n      }\n\n      if (checks.every(function (check) {\n        return check;\n      })) {\n        firstFittingPlacement = placement;\n        makeFallbackChecks = false;\n        break;\n      }\n\n      checksMap.set(placement, checks);\n    }\n\n    if (makeFallbackChecks) {\n      // `2` may be desired in some cases \u2013 research later\n      var numberOfChecks = flipVariations ? 3 : 1;\n\n      var _loop = function _loop(_i) {\n        var fittingPlacement = placements.find(function (placement) {\n          var checks = checksMap.get(placement);\n\n          if (checks) {\n            return checks.slice(0, _i).every(function (check) {\n              return check;\n            });\n          }\n        });\n\n        if (fittingPlacement) {\n          firstFittingPlacement = fittingPlacement;\n          return \"break\";\n        }\n      };\n\n      for (var _i = numberOfChecks; _i > 0; _i--) {\n        var _ret = _loop(_i);\n\n        if (_ret === \"break\") break;\n      }\n    }\n\n    if (state.placement !== firstFittingPlacement) {\n      state.modifiersData[name]._skip = true;\n      state.placement = firstFittingPlacement;\n      state.reset = true;\n    }\n  } // eslint-disable-next-line import/no-unused-modules\n\n\n  var flip$1 = {\n    name: 'flip',\n    enabled: true,\n    phase: 'main',\n    fn: flip,\n    requiresIfExists: ['offset'],\n    data: {\n      _skip: false\n    }\n  };\n\n  function getAltAxis(axis) {\n    return axis === 'x' ? 'y' : 'x';\n  }\n\n  function within(min$1, value, max$1) {\n    return max(min$1, min(value, max$1));\n  }\n  function withinMaxClamp(min, value, max) {\n    var v = within(min, value, max);\n    return v > max ? max : v;\n  }\n\n  function preventOverflow(_ref) {\n    var state = _ref.state,\n        options = _ref.options,\n        name = _ref.name;\n    var _options$mainAxis = options.mainAxis,\n        checkMainAxis = _options$mainAxis === void 0 ? true : _options$mainAxis,\n        _options$altAxis = options.altAxis,\n        checkAltAxis = _options$altAxis === void 0 ? false : _options$altAxis,\n        boundary = options.boundary,\n        rootBoundary = options.rootBoundary,\n        altBoundary = options.altBoundary,\n        padding = options.padding,\n        _options$tether = options.tether,\n        tether = _options$tether === void 0 ? true : _options$tether,\n        _options$tetherOffset = options.tetherOffset,\n        tetherOffset = _options$tetherOffset === void 0 ? 0 : _options$tetherOffset;\n    var overflow = detectOverflow(state, {\n      boundary: boundary,\n      rootBoundary: rootBoundary,\n      padding: padding,\n      altBoundary: altBoundary\n    });\n    var basePlacement = getBasePlacement(state.placement);\n    var variation = getVariation(state.placement);\n    var isBasePlacement = !variation;\n    var mainAxis = getMainAxisFromPlacement(basePlacement);\n    var altAxis = getAltAxis(mainAxis);\n    var popperOffsets = state.modifiersData.popperOffsets;\n    var referenceRect = state.rects.reference;\n    var popperRect = state.rects.popper;\n    var tetherOffsetValue = typeof tetherOffset === 'function' ? tetherOffset(Object.assign({}, state.rects, {\n      placement: state.placement\n    })) : tetherOffset;\n    var normalizedTetherOffsetValue = typeof tetherOffsetValue === 'number' ? {\n      mainAxis: tetherOffsetValue,\n      altAxis: tetherOffsetValue\n    } : Object.assign({\n      mainAxis: 0,\n      altAxis: 0\n    }, tetherOffsetValue);\n    var offsetModifierState = state.modifiersData.offset ? state.modifiersData.offset[state.placement] : null;\n    var data = {\n      x: 0,\n      y: 0\n    };\n\n    if (!popperOffsets) {\n      return;\n    }\n\n    if (checkMainAxis) {\n      var _offsetModifierState$;\n\n      var mainSide = mainAxis === 'y' ? top : left;\n      var altSide = mainAxis === 'y' ? bottom : right;\n      var len = mainAxis === 'y' ? 'height' : 'width';\n      var offset = popperOffsets[mainAxis];\n      var min$1 = offset + overflow[mainSide];\n      var max$1 = offset - overflow[altSide];\n      var additive = tether ? -popperRect[len] / 2 : 0;\n      var minLen = variation === start ? referenceRect[len] : popperRect[len];\n      var maxLen = variation === start ? -popperRect[len] : -referenceRect[len]; // We need to include the arrow in the calculation so the arrow doesn't go\n      // outside the reference bounds\n\n      var arrowElement = state.elements.arrow;\n      var arrowRect = tether && arrowElement ? getLayoutRect(arrowElement) : {\n        width: 0,\n        height: 0\n      };\n      var arrowPaddingObject = state.modifiersData['arrow#persistent'] ? state.modifiersData['arrow#persistent'].padding : getFreshSideObject();\n      var arrowPaddingMin = arrowPaddingObject[mainSide];\n      var arrowPaddingMax = arrowPaddingObject[altSide]; // If the reference length is smaller than the arrow length, we don't want\n      // to include its full size in the calculation. If the reference is small\n      // and near the edge of a boundary, the popper can overflow even if the\n      // reference is not overflowing as well (e.g. virtual elements with no\n      // width or height)\n\n      var arrowLen = within(0, referenceRect[len], arrowRect[len]);\n      var minOffset = isBasePlacement ? referenceRect[len] / 2 - additive - arrowLen - arrowPaddingMin - normalizedTetherOffsetValue.mainAxis : minLen - arrowLen - arrowPaddingMin - normalizedTetherOffsetValue.mainAxis;\n      var maxOffset = isBasePlacement ? -referenceRect[len] / 2 + additive + arrowLen + arrowPaddingMax + normalizedTetherOffsetValue.mainAxis : maxLen + arrowLen + arrowPaddingMax + normalizedTetherOffsetValue.mainAxis;\n      var arrowOffsetParent = state.elements.arrow && getOffsetParent(state.elements.arrow);\n      var clientOffset = arrowOffsetParent ? mainAxis === 'y' ? arrowOffsetParent.clientTop || 0 : arrowOffsetParent.clientLeft || 0 : 0;\n      var offsetModifierValue = (_offsetModifierState$ = offsetModifierState == null ? void 0 : offsetModifierState[mainAxis]) != null ? _offsetModifierState$ : 0;\n      var tetherMin = offset + minOffset - offsetModifierValue - clientOffset;\n      var tetherMax = offset + maxOffset - offsetModifierValue;\n      var preventedOffset = within(tether ? min(min$1, tetherMin) : min$1, offset, tether ? max(max$1, tetherMax) : max$1);\n      popperOffsets[mainAxis] = preventedOffset;\n      data[mainAxis] = preventedOffset - offset;\n    }\n\n    if (checkAltAxis) {\n      var _offsetModifierState$2;\n\n      var _mainSide = mainAxis === 'x' ? top : left;\n\n      var _altSide = mainAxis === 'x' ? bottom : right;\n\n      var _offset = popperOffsets[altAxis];\n\n      var _len = altAxis === 'y' ? 'height' : 'width';\n\n      var _min = _offset + overflow[_mainSide];\n\n      var _max = _offset - overflow[_altSide];\n\n      var isOriginSide = [top, left].indexOf(basePlacement) !== -1;\n\n      var _offsetModifierValue = (_offsetModifierState$2 = offsetModifierState == null ? void 0 : offsetModifierState[altAxis]) != null ? _offsetModifierState$2 : 0;\n\n      var _tetherMin = isOriginSide ? _min : _offset - referenceRect[_len] - popperRect[_len] - _offsetModifierValue + normalizedTetherOffsetValue.altAxis;\n\n      var _tetherMax = isOriginSide ? _offset + referenceRect[_len] + popperRect[_len] - _offsetModifierValue - normalizedTetherOffsetValue.altAxis : _max;\n\n      var _preventedOffset = tether && isOriginSide ? withinMaxClamp(_tetherMin, _offset, _tetherMax) : within(tether ? _tetherMin : _min, _offset, tether ? _tetherMax : _max);\n\n      popperOffsets[altAxis] = _preventedOffset;\n      data[altAxis] = _preventedOffset - _offset;\n    }\n\n    state.modifiersData[name] = data;\n  } // eslint-disable-next-line import/no-unused-modules\n\n\n  var preventOverflow$1 = {\n    name: 'preventOverflow',\n    enabled: true,\n    phase: 'main',\n    fn: preventOverflow,\n    requiresIfExists: ['offset']\n  };\n\n  var toPaddingObject = function toPaddingObject(padding, state) {\n    padding = typeof padding === 'function' ? padding(Object.assign({}, state.rects, {\n      placement: state.placement\n    })) : padding;\n    return mergePaddingObject(typeof padding !== 'number' ? padding : expandToHashMap(padding, basePlacements));\n  };\n\n  function arrow(_ref) {\n    var _state$modifiersData$;\n\n    var state = _ref.state,\n        name = _ref.name,\n        options = _ref.options;\n    var arrowElement = state.elements.arrow;\n    var popperOffsets = state.modifiersData.popperOffsets;\n    var basePlacement = getBasePlacement(state.placement);\n    var axis = getMainAxisFromPlacement(basePlacement);\n    var isVertical = [left, right].indexOf(basePlacement) >= 0;\n    var len = isVertical ? 'height' : 'width';\n\n    if (!arrowElement || !popperOffsets) {\n      return;\n    }\n\n    var paddingObject = toPaddingObject(options.padding, state);\n    var arrowRect = getLayoutRect(arrowElement);\n    var minProp = axis === 'y' ? top : left;\n    var maxProp = axis === 'y' ? bottom : right;\n    var endDiff = state.rects.reference[len] + state.rects.reference[axis] - popperOffsets[axis] - state.rects.popper[len];\n    var startDiff = popperOffsets[axis] - state.rects.reference[axis];\n    var arrowOffsetParent = getOffsetParent(arrowElement);\n    var clientSize = arrowOffsetParent ? axis === 'y' ? arrowOffsetParent.clientHeight || 0 : arrowOffsetParent.clientWidth || 0 : 0;\n    var centerToReference = endDiff / 2 - startDiff / 2; // Make sure the arrow doesn't overflow the popper if the center point is\n    // outside of the popper bounds\n\n    var min = paddingObject[minProp];\n    var max = clientSize - arrowRect[len] - paddingObject[maxProp];\n    var center = clientSize / 2 - arrowRect[len] / 2 + centerToReference;\n    var offset = within(min, center, max); // Prevents breaking syntax highlighting...\n\n    var axisProp = axis;\n    state.modifiersData[name] = (_state$modifiersData$ = {}, _state$modifiersData$[axisProp] = offset, _state$modifiersData$.centerOffset = offset - center, _state$modifiersData$);\n  }\n\n  function effect(_ref2) {\n    var state = _ref2.state,\n        options = _ref2.options;\n    var _options$element = options.element,\n        arrowElement = _options$element === void 0 ? '[data-popper-arrow]' : _options$element;\n\n    if (arrowElement == null) {\n      return;\n    } // CSS selector\n\n\n    if (typeof arrowElement === 'string') {\n      arrowElement = state.elements.popper.querySelector(arrowElement);\n\n      if (!arrowElement) {\n        return;\n      }\n    }\n\n    if (!contains(state.elements.popper, arrowElement)) {\n      return;\n    }\n\n    state.elements.arrow = arrowElement;\n  } // eslint-disable-next-line import/no-unused-modules\n\n\n  var arrow$1 = {\n    name: 'arrow',\n    enabled: true,\n    phase: 'main',\n    fn: arrow,\n    effect: effect,\n    requires: ['popperOffsets'],\n    requiresIfExists: ['preventOverflow']\n  };\n\n  function getSideOffsets(overflow, rect, preventedOffsets) {\n    if (preventedOffsets === void 0) {\n      preventedOffsets = {\n        x: 0,\n        y: 0\n      };\n    }\n\n    return {\n      top: overflow.top - rect.height - preventedOffsets.y,\n      right: overflow.right - rect.width + preventedOffsets.x,\n      bottom: overflow.bottom - rect.height + preventedOffsets.y,\n      left: overflow.left - rect.width - preventedOffsets.x\n    };\n  }\n\n  function isAnySideFullyClipped(overflow) {\n    return [top, right, bottom, left].some(function (side) {\n      return overflow[side] >= 0;\n    });\n  }\n\n  function hide(_ref) {\n    var state = _ref.state,\n        name = _ref.name;\n    var referenceRect = state.rects.reference;\n    var popperRect = state.rects.popper;\n    var preventedOffsets = state.modifiersData.preventOverflow;\n    var referenceOverflow = detectOverflow(state, {\n      elementContext: 'reference'\n    });\n    var popperAltOverflow = detectOverflow(state, {\n      altBoundary: true\n    });\n    var referenceClippingOffsets = getSideOffsets(referenceOverflow, referenceRect);\n    var popperEscapeOffsets = getSideOffsets(popperAltOverflow, popperRect, preventedOffsets);\n    var isReferenceHidden = isAnySideFullyClipped(referenceClippingOffsets);\n    var hasPopperEscaped = isAnySideFullyClipped(popperEscapeOffsets);\n    state.modifiersData[name] = {\n      referenceClippingOffsets: referenceClippingOffsets,\n      popperEscapeOffsets: popperEscapeOffsets,\n      isReferenceHidden: isReferenceHidden,\n      hasPopperEscaped: hasPopperEscaped\n    };\n    state.attributes.popper = Object.assign({}, state.attributes.popper, {\n      'data-popper-reference-hidden': isReferenceHidden,\n      'data-popper-escaped': hasPopperEscaped\n    });\n  } // eslint-disable-next-line import/no-unused-modules\n\n\n  var hide$1 = {\n    name: 'hide',\n    enabled: true,\n    phase: 'main',\n    requiresIfExists: ['preventOverflow'],\n    fn: hide\n  };\n\n  var defaultModifiers$1 = [eventListeners, popperOffsets$1, computeStyles$1, applyStyles$1];\n  var createPopper$1 = /*#__PURE__*/popperGenerator({\n    defaultModifiers: defaultModifiers$1\n  }); // eslint-disable-next-line import/no-unused-modules\n\n  var defaultModifiers = [eventListeners, popperOffsets$1, computeStyles$1, applyStyles$1, offset$1, flip$1, preventOverflow$1, arrow$1, hide$1];\n  var createPopper = /*#__PURE__*/popperGenerator({\n    defaultModifiers: defaultModifiers\n  }); // eslint-disable-next-line import/no-unused-modules\n\n  exports.applyStyles = applyStyles$1;\n  exports.arrow = arrow$1;\n  exports.computeStyles = computeStyles$1;\n  exports.createPopper = createPopper;\n  exports.createPopperLite = createPopper$1;\n  exports.defaultModifiers = defaultModifiers;\n  exports.detectOverflow = detectOverflow;\n  exports.eventListeners = eventListeners;\n  exports.flip = flip$1;\n  exports.hide = hide$1;\n  exports.offset = offset$1;\n  exports.popperGenerator = popperGenerator;\n  exports.popperOffsets = popperOffsets$1;\n  exports.preventOverflow = preventOverflow$1;\n\n  Object.defineProperty(exports, '__esModule', { value: true });\n\n})));\n//# sourceMappingURL=popper.js.map\n", "/*!\n  * Bootstrap index.js v5.3.3 (https://getbootstrap.com/)\n  * Copyright 2011-2024 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors)\n  * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n  */\n(function (global, factory) {\n  typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :\n  typeof define === 'function' && define.amd ? define(['exports'], factory) :\n  (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.Index = {}));\n})(this, (function (exports) { 'use strict';\n\n  /**\n   * --------------------------------------------------------------------------\n   * Bootstrap util/index.js\n   * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n   * --------------------------------------------------------------------------\n   */\n\n  const MAX_UID = 1000000;\n  const MILLISECONDS_MULTIPLIER = 1000;\n  const TRANSITION_END = 'transitionend';\n\n  /**\n   * Properly escape IDs selectors to handle weird IDs\n   * @param {string} selector\n   * @returns {string}\n   */\n  const parseSelector = selector => {\n    if (selector && window.CSS && window.CSS.escape) {\n      // document.querySelector needs escaping to handle IDs (html5+) containing for instance /\n      selector = selector.replace(/#([^\\s\"#']+)/g, (match, id) => `#${CSS.escape(id)}`);\n    }\n    return selector;\n  };\n\n  // Shout-out Angus Croll (https://goo.gl/pxwQGp)\n  const toType = object => {\n    if (object === null || object === undefined) {\n      return `${object}`;\n    }\n    return Object.prototype.toString.call(object).match(/\\s([a-z]+)/i)[1].toLowerCase();\n  };\n\n  /**\n   * Public Util API\n   */\n\n  const getUID = prefix => {\n    do {\n      prefix += Math.floor(Math.random() * MAX_UID);\n    } while (document.getElementById(prefix));\n    return prefix;\n  };\n  const getTransitionDurationFromElement = element => {\n    if (!element) {\n      return 0;\n    }\n\n    // Get transition-duration of the element\n    let {\n      transitionDuration,\n      transitionDelay\n    } = window.getComputedStyle(element);\n    const floatTransitionDuration = Number.parseFloat(transitionDuration);\n    const floatTransitionDelay = Number.parseFloat(transitionDelay);\n\n    // Return 0 if element or transition duration is not found\n    if (!floatTransitionDuration && !floatTransitionDelay) {\n      return 0;\n    }\n\n    // If multiple durations are defined, take the first\n    transitionDuration = transitionDuration.split(',')[0];\n    transitionDelay = transitionDelay.split(',')[0];\n    return (Number.parseFloat(transitionDuration) + Number.parseFloat(transitionDelay)) * MILLISECONDS_MULTIPLIER;\n  };\n  const triggerTransitionEnd = element => {\n    element.dispatchEvent(new Event(TRANSITION_END));\n  };\n  const isElement = object => {\n    if (!object || typeof object !== 'object') {\n      return false;\n    }\n    if (typeof object.jquery !== 'undefined') {\n      object = object[0];\n    }\n    return typeof object.nodeType !== 'undefined';\n  };\n  const getElement = object => {\n    // it's a jQuery object or a node element\n    if (isElement(object)) {\n      return object.jquery ? object[0] : object;\n    }\n    if (typeof object === 'string' && object.length > 0) {\n      return document.querySelector(parseSelector(object));\n    }\n    return null;\n  };\n  const isVisible = element => {\n    if (!isElement(element) || element.getClientRects().length === 0) {\n      return false;\n    }\n    const elementIsVisible = getComputedStyle(element).getPropertyValue('visibility') === 'visible';\n    // Handle `details` element as its content may falsie appear visible when it is closed\n    const closedDetails = element.closest('details:not([open])');\n    if (!closedDetails) {\n      return elementIsVisible;\n    }\n    if (closedDetails !== element) {\n      const summary = element.closest('summary');\n      if (summary && summary.parentNode !== closedDetails) {\n        return false;\n      }\n      if (summary === null) {\n        return false;\n      }\n    }\n    return elementIsVisible;\n  };\n  const isDisabled = element => {\n    if (!element || element.nodeType !== Node.ELEMENT_NODE) {\n      return true;\n    }\n    if (element.classList.contains('disabled')) {\n      return true;\n    }\n    if (typeof element.disabled !== 'undefined') {\n      return element.disabled;\n    }\n    return element.hasAttribute('disabled') && element.getAttribute('disabled') !== 'false';\n  };\n  const findShadowRoot = element => {\n    if (!document.documentElement.attachShadow) {\n      return null;\n    }\n\n    // Can find the shadow root otherwise it'll return the document\n    if (typeof element.getRootNode === 'function') {\n      const root = element.getRootNode();\n      return root instanceof ShadowRoot ? root : null;\n    }\n    if (element instanceof ShadowRoot) {\n      return element;\n    }\n\n    // when we don't find a shadow root\n    if (!element.parentNode) {\n      return null;\n    }\n    return findShadowRoot(element.parentNode);\n  };\n  const noop = () => {};\n\n  /**\n   * Trick to restart an element's animation\n   *\n   * @param {HTMLElement} element\n   * @return void\n   *\n   * @see https://www.charistheo.io/blog/2021/02/restart-a-css-animation-with-javascript/#restarting-a-css-animation\n   */\n  const reflow = element => {\n    element.offsetHeight; // eslint-disable-line no-unused-expressions\n  };\n  const getjQuery = () => {\n    if (window.jQuery && !document.body.hasAttribute('data-bs-no-jquery')) {\n      return window.jQuery;\n    }\n    return null;\n  };\n  const DOMContentLoadedCallbacks = [];\n  const onDOMContentLoaded = callback => {\n    if (document.readyState === 'loading') {\n      // add listener on the first call when the document is in loading state\n      if (!DOMContentLoadedCallbacks.length) {\n        document.addEventListener('DOMContentLoaded', () => {\n          for (const callback of DOMContentLoadedCallbacks) {\n            callback();\n          }\n        });\n      }\n      DOMContentLoadedCallbacks.push(callback);\n    } else {\n      callback();\n    }\n  };\n  const isRTL = () => document.documentElement.dir === 'rtl';\n  const defineJQueryPlugin = plugin => {\n    onDOMContentLoaded(() => {\n      const $ = getjQuery();\n      /* istanbul ignore if */\n      if ($) {\n        const name = plugin.NAME;\n        const JQUERY_NO_CONFLICT = $.fn[name];\n        $.fn[name] = plugin.jQueryInterface;\n        $.fn[name].Constructor = plugin;\n        $.fn[name].noConflict = () => {\n          $.fn[name] = JQUERY_NO_CONFLICT;\n          return plugin.jQueryInterface;\n        };\n      }\n    });\n  };\n  const execute = (possibleCallback, args = [], defaultValue = possibleCallback) => {\n    return typeof possibleCallback === 'function' ? possibleCallback(...args) : defaultValue;\n  };\n  const executeAfterTransition = (callback, transitionElement, waitForTransition = true) => {\n    if (!waitForTransition) {\n      execute(callback);\n      return;\n    }\n    const durationPadding = 5;\n    const emulatedDuration = getTransitionDurationFromElement(transitionElement) + durationPadding;\n    let called = false;\n    const handler = ({\n      target\n    }) => {\n      if (target !== transitionElement) {\n        return;\n      }\n      called = true;\n      transitionElement.removeEventListener(TRANSITION_END, handler);\n      execute(callback);\n    };\n    transitionElement.addEventListener(TRANSITION_END, handler);\n    setTimeout(() => {\n      if (!called) {\n        triggerTransitionEnd(transitionElement);\n      }\n    }, emulatedDuration);\n  };\n\n  /**\n   * Return the previous/next element of a list.\n   *\n   * @param {array} list    The list of elements\n   * @param activeElement   The active element\n   * @param shouldGetNext   Choose to get next or previous element\n   * @param isCycleAllowed\n   * @return {Element|elem} The proper element\n   */\n  const getNextActiveElement = (list, activeElement, shouldGetNext, isCycleAllowed) => {\n    const listLength = list.length;\n    let index = list.indexOf(activeElement);\n\n    // if the element does not exist in the list return an element\n    // depending on the direction and if cycle is allowed\n    if (index === -1) {\n      return !shouldGetNext && isCycleAllowed ? list[listLength - 1] : list[0];\n    }\n    index += shouldGetNext ? 1 : -1;\n    if (isCycleAllowed) {\n      index = (index + listLength) % listLength;\n    }\n    return list[Math.max(0, Math.min(index, listLength - 1))];\n  };\n\n  exports.defineJQueryPlugin = defineJQueryPlugin;\n  exports.execute = execute;\n  exports.executeAfterTransition = executeAfterTransition;\n  exports.findShadowRoot = findShadowRoot;\n  exports.getElement = getElement;\n  exports.getNextActiveElement = getNextActiveElement;\n  exports.getTransitionDurationFromElement = getTransitionDurationFromElement;\n  exports.getUID = getUID;\n  exports.getjQuery = getjQuery;\n  exports.isDisabled = isDisabled;\n  exports.isElement = isElement;\n  exports.isRTL = isRTL;\n  exports.isVisible = isVisible;\n  exports.noop = noop;\n  exports.onDOMContentLoaded = onDOMContentLoaded;\n  exports.parseSelector = parseSelector;\n  exports.reflow = reflow;\n  exports.toType = toType;\n  exports.triggerTransitionEnd = triggerTransitionEnd;\n\n  Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });\n\n}));\n//# sourceMappingURL=index.js.map\n", "/*!\n  * Bootstrap data.js v5.3.3 (https://getbootstrap.com/)\n  * Copyright 2011-2024 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors)\n  * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n  */\n(function (global, factory) {\n  typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :\n  typeof define === 'function' && define.amd ? define(factory) :\n  (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.Data = factory());\n})(this, (function () { 'use strict';\n\n  /**\n   * --------------------------------------------------------------------------\n   * Bootstrap dom/data.js\n   * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n   * --------------------------------------------------------------------------\n   */\n\n  /**\n   * Constants\n   */\n\n  const elementMap = new Map();\n  const data = {\n    set(element, key, instance) {\n      if (!elementMap.has(element)) {\n        elementMap.set(element, new Map());\n      }\n      const instanceMap = elementMap.get(element);\n\n      // make it clear we only want one instance per element\n      // can be removed later when multiple key/instances are fine to be used\n      if (!instanceMap.has(key) && instanceMap.size !== 0) {\n        // eslint-disable-next-line no-console\n        console.error(`Bootstrap doesn't allow more than one instance per element. Bound instance: ${Array.from(instanceMap.keys())[0]}.`);\n        return;\n      }\n      instanceMap.set(key, instance);\n    },\n    get(element, key) {\n      if (elementMap.has(element)) {\n        return elementMap.get(element).get(key) || null;\n      }\n      return null;\n    },\n    remove(element, key) {\n      if (!elementMap.has(element)) {\n        return;\n      }\n      const instanceMap = elementMap.get(element);\n      instanceMap.delete(key);\n\n      // free up element references if there are no instances left for an element\n      if (instanceMap.size === 0) {\n        elementMap.delete(element);\n      }\n    }\n  };\n\n  return data;\n\n}));\n//# sourceMappingURL=data.js.map\n", "/*!\n  * Bootstrap event-handler.js v5.3.3 (https://getbootstrap.com/)\n  * Copyright 2011-2024 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors)\n  * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n  */\n(function (global, factory) {\n  typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('../util/index.js')) :\n  typeof define === 'function' && define.amd ? define(['../util/index'], factory) :\n  (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.EventHandler = factory(global.Index));\n})(this, (function (index_js) { 'use strict';\n\n  /**\n   * --------------------------------------------------------------------------\n   * Bootstrap dom/event-handler.js\n   * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n   * --------------------------------------------------------------------------\n   */\n\n\n  /**\n   * Constants\n   */\n\n  const namespaceRegex = /[^.]*(?=\\..*)\\.|.*/;\n  const stripNameRegex = /\\..*/;\n  const stripUidRegex = /::\\d+$/;\n  const eventRegistry = {}; // Events storage\n  let uidEvent = 1;\n  const customEvents = {\n    mouseenter: 'mouseover',\n    mouseleave: 'mouseout'\n  };\n  const nativeEvents = new Set(['click', 'dblclick', 'mouseup', 'mousedown', 'contextmenu', 'mousewheel', 'DOMMouseScroll', 'mouseover', 'mouseout', 'mousemove', 'selectstart', 'selectend', 'keydown', 'keypress', 'keyup', 'orientationchange', 'touchstart', 'touchmove', 'touchend', 'touchcancel', 'pointerdown', 'pointermove', 'pointerup', 'pointerleave', 'pointercancel', 'gesturestart', 'gesturechange', 'gestureend', 'focus', 'blur', 'change', 'reset', 'select', 'submit', 'focusin', 'focusout', 'load', 'unload', 'beforeunload', 'resize', 'move', 'DOMContentLoaded', 'readystatechange', 'error', 'abort', 'scroll']);\n\n  /**\n   * Private methods\n   */\n\n  function makeEventUid(element, uid) {\n    return uid && `${uid}::${uidEvent++}` || element.uidEvent || uidEvent++;\n  }\n  function getElementEvents(element) {\n    const uid = makeEventUid(element);\n    element.uidEvent = uid;\n    eventRegistry[uid] = eventRegistry[uid] || {};\n    return eventRegistry[uid];\n  }\n  function bootstrapHandler(element, fn) {\n    return function handler(event) {\n      hydrateObj(event, {\n        delegateTarget: element\n      });\n      if (handler.oneOff) {\n        EventHandler.off(element, event.type, fn);\n      }\n      return fn.apply(element, [event]);\n    };\n  }\n  function bootstrapDelegationHandler(element, selector, fn) {\n    return function handler(event) {\n      const domElements = element.querySelectorAll(selector);\n      for (let {\n        target\n      } = event; target && target !== this; target = target.parentNode) {\n        for (const domElement of domElements) {\n          if (domElement !== target) {\n            continue;\n          }\n          hydrateObj(event, {\n            delegateTarget: target\n          });\n          if (handler.oneOff) {\n            EventHandler.off(element, event.type, selector, fn);\n          }\n          return fn.apply(target, [event]);\n        }\n      }\n    };\n  }\n  function findHandler(events, callable, delegationSelector = null) {\n    return Object.values(events).find(event => event.callable === callable && event.delegationSelector === delegationSelector);\n  }\n  function normalizeParameters(originalTypeEvent, handler, delegationFunction) {\n    const isDelegated = typeof handler === 'string';\n    // TODO: tooltip passes `false` instead of selector, so we need to check\n    const callable = isDelegated ? delegationFunction : handler || delegationFunction;\n    let typeEvent = getTypeEvent(originalTypeEvent);\n    if (!nativeEvents.has(typeEvent)) {\n      typeEvent = originalTypeEvent;\n    }\n    return [isDelegated, callable, typeEvent];\n  }\n  function addHandler(element, originalTypeEvent, handler, delegationFunction, oneOff) {\n    if (typeof originalTypeEvent !== 'string' || !element) {\n      return;\n    }\n    let [isDelegated, callable, typeEvent] = normalizeParameters(originalTypeEvent, handler, delegationFunction);\n\n    // in case of mouseenter or mouseleave wrap the handler within a function that checks for its DOM position\n    // this prevents the handler from being dispatched the same way as mouseover or mouseout does\n    if (originalTypeEvent in customEvents) {\n      const wrapFunction = fn => {\n        return function (event) {\n          if (!event.relatedTarget || event.relatedTarget !== event.delegateTarget && !event.delegateTarget.contains(event.relatedTarget)) {\n            return fn.call(this, event);\n          }\n        };\n      };\n      callable = wrapFunction(callable);\n    }\n    const events = getElementEvents(element);\n    const handlers = events[typeEvent] || (events[typeEvent] = {});\n    const previousFunction = findHandler(handlers, callable, isDelegated ? handler : null);\n    if (previousFunction) {\n      previousFunction.oneOff = previousFunction.oneOff && oneOff;\n      return;\n    }\n    const uid = makeEventUid(callable, originalTypeEvent.replace(namespaceRegex, ''));\n    const fn = isDelegated ? bootstrapDelegationHandler(element, handler, callable) : bootstrapHandler(element, callable);\n    fn.delegationSelector = isDelegated ? handler : null;\n    fn.callable = callable;\n    fn.oneOff = oneOff;\n    fn.uidEvent = uid;\n    handlers[uid] = fn;\n    element.addEventListener(typeEvent, fn, isDelegated);\n  }\n  function removeHandler(element, events, typeEvent, handler, delegationSelector) {\n    const fn = findHandler(events[typeEvent], handler, delegationSelector);\n    if (!fn) {\n      return;\n    }\n    element.removeEventListener(typeEvent, fn, Boolean(delegationSelector));\n    delete events[typeEvent][fn.uidEvent];\n  }\n  function removeNamespacedHandlers(element, events, typeEvent, namespace) {\n    const storeElementEvent = events[typeEvent] || {};\n    for (const [handlerKey, event] of Object.entries(storeElementEvent)) {\n      if (handlerKey.includes(namespace)) {\n        removeHandler(element, events, typeEvent, event.callable, event.delegationSelector);\n      }\n    }\n  }\n  function getTypeEvent(event) {\n    // allow to get the native events from namespaced events ('click.bs.button' --> 'click')\n    event = event.replace(stripNameRegex, '');\n    return customEvents[event] || event;\n  }\n  const EventHandler = {\n    on(element, event, handler, delegationFunction) {\n      addHandler(element, event, handler, delegationFunction, false);\n    },\n    one(element, event, handler, delegationFunction) {\n      addHandler(element, event, handler, delegationFunction, true);\n    },\n    off(element, originalTypeEvent, handler, delegationFunction) {\n      if (typeof originalTypeEvent !== 'string' || !element) {\n        return;\n      }\n      const [isDelegated, callable, typeEvent] = normalizeParameters(originalTypeEvent, handler, delegationFunction);\n      const inNamespace = typeEvent !== originalTypeEvent;\n      const events = getElementEvents(element);\n      const storeElementEvent = events[typeEvent] || {};\n      const isNamespace = originalTypeEvent.startsWith('.');\n      if (typeof callable !== 'undefined') {\n        // Simplest case: handler is passed, remove that listener ONLY.\n        if (!Object.keys(storeElementEvent).length) {\n          return;\n        }\n        removeHandler(element, events, typeEvent, callable, isDelegated ? handler : null);\n        return;\n      }\n      if (isNamespace) {\n        for (const elementEvent of Object.keys(events)) {\n          removeNamespacedHandlers(element, events, elementEvent, originalTypeEvent.slice(1));\n        }\n      }\n      for (const [keyHandlers, event] of Object.entries(storeElementEvent)) {\n        const handlerKey = keyHandlers.replace(stripUidRegex, '');\n        if (!inNamespace || originalTypeEvent.includes(handlerKey)) {\n          removeHandler(element, events, typeEvent, event.callable, event.delegationSelector);\n        }\n      }\n    },\n    trigger(element, event, args) {\n      if (typeof event !== 'string' || !element) {\n        return null;\n      }\n      const $ = index_js.getjQuery();\n      const typeEvent = getTypeEvent(event);\n      const inNamespace = event !== typeEvent;\n      let jQueryEvent = null;\n      let bubbles = true;\n      let nativeDispatch = true;\n      let defaultPrevented = false;\n      if (inNamespace && $) {\n        jQueryEvent = $.Event(event, args);\n        $(element).trigger(jQueryEvent);\n        bubbles = !jQueryEvent.isPropagationStopped();\n        nativeDispatch = !jQueryEvent.isImmediatePropagationStopped();\n        defaultPrevented = jQueryEvent.isDefaultPrevented();\n      }\n      const evt = hydrateObj(new Event(event, {\n        bubbles,\n        cancelable: true\n      }), args);\n      if (defaultPrevented) {\n        evt.preventDefault();\n      }\n      if (nativeDispatch) {\n        element.dispatchEvent(evt);\n      }\n      if (evt.defaultPrevented && jQueryEvent) {\n        jQueryEvent.preventDefault();\n      }\n      return evt;\n    }\n  };\n  function hydrateObj(obj, meta = {}) {\n    for (const [key, value] of Object.entries(meta)) {\n      try {\n        obj[key] = value;\n      } catch (_unused) {\n        Object.defineProperty(obj, key, {\n          configurable: true,\n          get() {\n            return value;\n          }\n        });\n      }\n    }\n    return obj;\n  }\n\n  return EventHandler;\n\n}));\n//# sourceMappingURL=event-handler.js.map\n", "/*!\n  * Bootstrap manipulator.js v5.3.3 (https://getbootstrap.com/)\n  * Copyright 2011-2024 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors)\n  * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n  */\n(function (global, factory) {\n  typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :\n  typeof define === 'function' && define.amd ? define(factory) :\n  (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.Manipulator = factory());\n})(this, (function () { 'use strict';\n\n  /**\n   * --------------------------------------------------------------------------\n   * Bootstrap dom/manipulator.js\n   * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n   * --------------------------------------------------------------------------\n   */\n\n  function normalizeData(value) {\n    if (value === 'true') {\n      return true;\n    }\n    if (value === 'false') {\n      return false;\n    }\n    if (value === Number(value).toString()) {\n      return Number(value);\n    }\n    if (value === '' || value === 'null') {\n      return null;\n    }\n    if (typeof value !== 'string') {\n      return value;\n    }\n    try {\n      return JSON.parse(decodeURIComponent(value));\n    } catch (_unused) {\n      return value;\n    }\n  }\n  function normalizeDataKey(key) {\n    return key.replace(/[A-Z]/g, chr => `-${chr.toLowerCase()}`);\n  }\n  const Manipulator = {\n    setDataAttribute(element, key, value) {\n      element.setAttribute(`data-bs-${normalizeDataKey(key)}`, value);\n    },\n    removeDataAttribute(element, key) {\n      element.removeAttribute(`data-bs-${normalizeDataKey(key)}`);\n    },\n    getDataAttributes(element) {\n      if (!element) {\n        return {};\n      }\n      const attributes = {};\n      const bsKeys = Object.keys(element.dataset).filter(key => key.startsWith('bs') && !key.startsWith('bsConfig'));\n      for (const key of bsKeys) {\n        let pureKey = key.replace(/^bs/, '');\n        pureKey = pureKey.charAt(0).toLowerCase() + pureKey.slice(1, pureKey.length);\n        attributes[pureKey] = normalizeData(element.dataset[key]);\n      }\n      return attributes;\n    },\n    getDataAttribute(element, key) {\n      return normalizeData(element.getAttribute(`data-bs-${normalizeDataKey(key)}`));\n    }\n  };\n\n  return Manipulator;\n\n}));\n//# sourceMappingURL=manipulator.js.map\n", "/*!\n  * Bootstrap selector-engine.js v5.3.3 (https://getbootstrap.com/)\n  * Copyright 2011-2024 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors)\n  * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n  */\n(function (global, factory) {\n  typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('../util/index.js')) :\n  typeof define === 'function' && define.amd ? define(['../util/index'], factory) :\n  (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.SelectorEngine = factory(global.Index));\n})(this, (function (index_js) { 'use strict';\n\n  /**\n   * --------------------------------------------------------------------------\n   * Bootstrap dom/selector-engine.js\n   * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n   * --------------------------------------------------------------------------\n   */\n\n  const getSelector = element => {\n    let selector = element.getAttribute('data-bs-target');\n    if (!selector || selector === '#') {\n      let hrefAttribute = element.getAttribute('href');\n\n      // The only valid content that could double as a selector are IDs or classes,\n      // so everything starting with `#` or `.`. If a \"real\" URL is used as the selector,\n      // `document.querySelector` will rightfully complain it is invalid.\n      // See https://github.com/twbs/bootstrap/issues/32273\n      if (!hrefAttribute || !hrefAttribute.includes('#') && !hrefAttribute.startsWith('.')) {\n        return null;\n      }\n\n      // Just in case some CMS puts out a full URL with the anchor appended\n      if (hrefAttribute.includes('#') && !hrefAttribute.startsWith('#')) {\n        hrefAttribute = `#${hrefAttribute.split('#')[1]}`;\n      }\n      selector = hrefAttribute && hrefAttribute !== '#' ? hrefAttribute.trim() : null;\n    }\n    return selector ? selector.split(',').map(sel => index_js.parseSelector(sel)).join(',') : null;\n  };\n  const SelectorEngine = {\n    find(selector, element = document.documentElement) {\n      return [].concat(...Element.prototype.querySelectorAll.call(element, selector));\n    },\n    findOne(selector, element = document.documentElement) {\n      return Element.prototype.querySelector.call(element, selector);\n    },\n    children(element, selector) {\n      return [].concat(...element.children).filter(child => child.matches(selector));\n    },\n    parents(element, selector) {\n      const parents = [];\n      let ancestor = element.parentNode.closest(selector);\n      while (ancestor) {\n        parents.push(ancestor);\n        ancestor = ancestor.parentNode.closest(selector);\n      }\n      return parents;\n    },\n    prev(element, selector) {\n      let previous = element.previousElementSibling;\n      while (previous) {\n        if (previous.matches(selector)) {\n          return [previous];\n        }\n        previous = previous.previousElementSibling;\n      }\n      return [];\n    },\n    // TODO: this is now unused; remove later along with prev()\n    next(element, selector) {\n      let next = element.nextElementSibling;\n      while (next) {\n        if (next.matches(selector)) {\n          return [next];\n        }\n        next = next.nextElementSibling;\n      }\n      return [];\n    },\n    focusableChildren(element) {\n      const focusables = ['a', 'button', 'input', 'textarea', 'select', 'details', '[tabindex]', '[contenteditable=\"true\"]'].map(selector => `${selector}:not([tabindex^=\"-\"])`).join(',');\n      return this.find(focusables, element).filter(el => !index_js.isDisabled(el) && index_js.isVisible(el));\n    },\n    getSelectorFromElement(element) {\n      const selector = getSelector(element);\n      if (selector) {\n        return SelectorEngine.findOne(selector) ? selector : null;\n      }\n      return null;\n    },\n    getElementFromSelector(element) {\n      const selector = getSelector(element);\n      return selector ? SelectorEngine.findOne(selector) : null;\n    },\n    getMultipleElementsFromSelector(element) {\n      const selector = getSelector(element);\n      return selector ? SelectorEngine.find(selector) : [];\n    }\n  };\n\n  return SelectorEngine;\n\n}));\n//# sourceMappingURL=selector-engine.js.map\n", "/*!\n  * Bootstrap config.js v5.3.3 (https://getbootstrap.com/)\n  * Copyright 2011-2024 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors)\n  * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n  */\n(function (global, factory) {\n  typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('../dom/manipulator.js'), require('./index.js')) :\n  typeof define === 'function' && define.amd ? define(['../dom/manipulator', './index'], factory) :\n  (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.Config = factory(global.Manipulator, global.Index));\n})(this, (function (Manipulator, index_js) { 'use strict';\n\n  /**\n   * --------------------------------------------------------------------------\n   * Bootstrap util/config.js\n   * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n   * --------------------------------------------------------------------------\n   */\n\n\n  /**\n   * Class definition\n   */\n\n  class Config {\n    // Getters\n    static get Default() {\n      return {};\n    }\n    static get DefaultType() {\n      return {};\n    }\n    static get NAME() {\n      throw new Error('You have to implement the static method \"NAME\", for each component!');\n    }\n    _getConfig(config) {\n      config = this._mergeConfigObj(config);\n      config = this._configAfterMerge(config);\n      this._typeCheckConfig(config);\n      return config;\n    }\n    _configAfterMerge(config) {\n      return config;\n    }\n    _mergeConfigObj(config, element) {\n      const jsonConfig = index_js.isElement(element) ? Manipulator.getDataAttribute(element, 'config') : {}; // try to parse\n\n      return {\n        ...this.constructor.Default,\n        ...(typeof jsonConfig === 'object' ? jsonConfig : {}),\n        ...(index_js.isElement(element) ? Manipulator.getDataAttributes(element) : {}),\n        ...(typeof config === 'object' ? config : {})\n      };\n    }\n    _typeCheckConfig(config, configTypes = this.constructor.DefaultType) {\n      for (const [property, expectedTypes] of Object.entries(configTypes)) {\n        const value = config[property];\n        const valueType = index_js.isElement(value) ? 'element' : index_js.toType(value);\n        if (!new RegExp(expectedTypes).test(valueType)) {\n          throw new TypeError(`${this.constructor.NAME.toUpperCase()}: Option \"${property}\" provided type \"${valueType}\" but expected type \"${expectedTypes}\".`);\n        }\n      }\n    }\n  }\n\n  return Config;\n\n}));\n//# sourceMappingURL=config.js.map\n", "/*!\n  * Bootstrap component-functions.js v5.3.3 (https://getbootstrap.com/)\n  * Copyright 2011-2024 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors)\n  * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n  */\n(function (global, factory) {\n  typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('../dom/event-handler.js'), require('../dom/selector-engine.js'), require('./index.js')) :\n  typeof define === 'function' && define.amd ? define(['exports', '../dom/event-handler', '../dom/selector-engine', './index'], factory) :\n  (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.ComponentFunctions = {}, global.EventHandler, global.SelectorEngine, global.Index));\n})(this, (function (exports, EventHandler, SelectorEngine, index_js) { 'use strict';\n\n  /**\n   * --------------------------------------------------------------------------\n   * Bootstrap util/component-functions.js\n   * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n   * --------------------------------------------------------------------------\n   */\n\n  const enableDismissTrigger = (component, method = 'hide') => {\n    const clickEvent = `click.dismiss${component.EVENT_KEY}`;\n    const name = component.NAME;\n    EventHandler.on(document, clickEvent, `[data-bs-dismiss=\"${name}\"]`, function (event) {\n      if (['A', 'AREA'].includes(this.tagName)) {\n        event.preventDefault();\n      }\n      if (index_js.isDisabled(this)) {\n        return;\n      }\n      const target = SelectorEngine.getElementFromSelector(this) || this.closest(`.${name}`);\n      const instance = component.getOrCreateInstance(target);\n\n      // Method argument is left, for Alert and only, as it doesn't implement the 'hide' method\n      instance[method]();\n    });\n  };\n\n  exports.enableDismissTrigger = enableDismissTrigger;\n\n  Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });\n\n}));\n//# sourceMappingURL=component-functions.js.map\n", "/*!\n  * Bootstrap backdrop.js v5.3.3 (https://getbootstrap.com/)\n  * Copyright 2011-2024 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors)\n  * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n  */\n(function (global, factory) {\n  typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('../dom/event-handler.js'), require('./config.js'), require('./index.js')) :\n  typeof define === 'function' && define.amd ? define(['../dom/event-handler', './config', './index'], factory) :\n  (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.Backdrop = factory(global.EventHandler, global.Config, global.Index));\n})(this, (function (EventHandler, Config, index_js) { 'use strict';\n\n  /**\n   * --------------------------------------------------------------------------\n   * Bootstrap util/backdrop.js\n   * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n   * --------------------------------------------------------------------------\n   */\n\n\n  /**\n   * Constants\n   */\n\n  const NAME = 'backdrop';\n  const CLASS_NAME_FADE = 'fade';\n  const CLASS_NAME_SHOW = 'show';\n  const EVENT_MOUSEDOWN = `mousedown.bs.${NAME}`;\n  const Default = {\n    className: 'modal-backdrop',\n    clickCallback: null,\n    isAnimated: false,\n    isVisible: true,\n    // if false, we use the backdrop helper without adding any element to the dom\n    rootElement: 'body' // give the choice to place backdrop under different elements\n  };\n  const DefaultType = {\n    className: 'string',\n    clickCallback: '(function|null)',\n    isAnimated: 'boolean',\n    isVisible: 'boolean',\n    rootElement: '(element|string)'\n  };\n\n  /**\n   * Class definition\n   */\n\n  class Backdrop extends Config {\n    constructor(config) {\n      super();\n      this._config = this._getConfig(config);\n      this._isAppended = false;\n      this._element = null;\n    }\n\n    // Getters\n    static get Default() {\n      return Default;\n    }\n    static get DefaultType() {\n      return DefaultType;\n    }\n    static get NAME() {\n      return NAME;\n    }\n\n    // Public\n    show(callback) {\n      if (!this._config.isVisible) {\n        index_js.execute(callback);\n        return;\n      }\n      this._append();\n      const element = this._getElement();\n      if (this._config.isAnimated) {\n        index_js.reflow(element);\n      }\n      element.classList.add(CLASS_NAME_SHOW);\n      this._emulateAnimation(() => {\n        index_js.execute(callback);\n      });\n    }\n    hide(callback) {\n      if (!this._config.isVisible) {\n        index_js.execute(callback);\n        return;\n      }\n      this._getElement().classList.remove(CLASS_NAME_SHOW);\n      this._emulateAnimation(() => {\n        this.dispose();\n        index_js.execute(callback);\n      });\n    }\n    dispose() {\n      if (!this._isAppended) {\n        return;\n      }\n      EventHandler.off(this._element, EVENT_MOUSEDOWN);\n      this._element.remove();\n      this._isAppended = false;\n    }\n\n    // Private\n    _getElement() {\n      if (!this._element) {\n        const backdrop = document.createElement('div');\n        backdrop.className = this._config.className;\n        if (this._config.isAnimated) {\n          backdrop.classList.add(CLASS_NAME_FADE);\n        }\n        this._element = backdrop;\n      }\n      return this._element;\n    }\n    _configAfterMerge(config) {\n      // use getElement() with the default \"body\" to get a fresh Element on each instantiation\n      config.rootElement = index_js.getElement(config.rootElement);\n      return config;\n    }\n    _append() {\n      if (this._isAppended) {\n        return;\n      }\n      const element = this._getElement();\n      this._config.rootElement.append(element);\n      EventHandler.on(element, EVENT_MOUSEDOWN, () => {\n        index_js.execute(this._config.clickCallback);\n      });\n      this._isAppended = true;\n    }\n    _emulateAnimation(callback) {\n      index_js.executeAfterTransition(callback, this._getElement(), this._config.isAnimated);\n    }\n  }\n\n  return Backdrop;\n\n}));\n//# sourceMappingURL=backdrop.js.map\n", "/*!\n  * Bootstrap focustrap.js v5.3.3 (https://getbootstrap.com/)\n  * Copyright 2011-2024 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors)\n  * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n  */\n(function (global, factory) {\n  typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('../dom/event-handler.js'), require('../dom/selector-engine.js'), require('./config.js')) :\n  typeof define === 'function' && define.amd ? define(['../dom/event-handler', '../dom/selector-engine', './config'], factory) :\n  (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.Focustrap = factory(global.EventHandler, global.SelectorEngine, global.Config));\n})(this, (function (EventHandler, SelectorEngine, Config) { 'use strict';\n\n  /**\n   * --------------------------------------------------------------------------\n   * Bootstrap util/focustrap.js\n   * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n   * --------------------------------------------------------------------------\n   */\n\n\n  /**\n   * Constants\n   */\n\n  const NAME = 'focustrap';\n  const DATA_KEY = 'bs.focustrap';\n  const EVENT_KEY = `.${DATA_KEY}`;\n  const EVENT_FOCUSIN = `focusin${EVENT_KEY}`;\n  const EVENT_KEYDOWN_TAB = `keydown.tab${EVENT_KEY}`;\n  const TAB_KEY = 'Tab';\n  const TAB_NAV_FORWARD = 'forward';\n  const TAB_NAV_BACKWARD = 'backward';\n  const Default = {\n    autofocus: true,\n    trapElement: null // The element to trap focus inside of\n  };\n  const DefaultType = {\n    autofocus: 'boolean',\n    trapElement: 'element'\n  };\n\n  /**\n   * Class definition\n   */\n\n  class FocusTrap extends Config {\n    constructor(config) {\n      super();\n      this._config = this._getConfig(config);\n      this._isActive = false;\n      this._lastTabNavDirection = null;\n    }\n\n    // Getters\n    static get Default() {\n      return Default;\n    }\n    static get DefaultType() {\n      return DefaultType;\n    }\n    static get NAME() {\n      return NAME;\n    }\n\n    // Public\n    activate() {\n      if (this._isActive) {\n        return;\n      }\n      if (this._config.autofocus) {\n        this._config.trapElement.focus();\n      }\n      EventHandler.off(document, EVENT_KEY); // guard against infinite focus loop\n      EventHandler.on(document, EVENT_FOCUSIN, event => this._handleFocusin(event));\n      EventHandler.on(document, EVENT_KEYDOWN_TAB, event => this._handleKeydown(event));\n      this._isActive = true;\n    }\n    deactivate() {\n      if (!this._isActive) {\n        return;\n      }\n      this._isActive = false;\n      EventHandler.off(document, EVENT_KEY);\n    }\n\n    // Private\n    _handleFocusin(event) {\n      const {\n        trapElement\n      } = this._config;\n      if (event.target === document || event.target === trapElement || trapElement.contains(event.target)) {\n        return;\n      }\n      const elements = SelectorEngine.focusableChildren(trapElement);\n      if (elements.length === 0) {\n        trapElement.focus();\n      } else if (this._lastTabNavDirection === TAB_NAV_BACKWARD) {\n        elements[elements.length - 1].focus();\n      } else {\n        elements[0].focus();\n      }\n    }\n    _handleKeydown(event) {\n      if (event.key !== TAB_KEY) {\n        return;\n      }\n      this._lastTabNavDirection = event.shiftKey ? TAB_NAV_BACKWARD : TAB_NAV_FORWARD;\n    }\n  }\n\n  return FocusTrap;\n\n}));\n//# sourceMappingURL=focustrap.js.map\n", "/*!\n  * Bootstrap sanitizer.js v5.3.3 (https://getbootstrap.com/)\n  * Copyright 2011-2024 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors)\n  * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n  */\n(function (global, factory) {\n  typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :\n  typeof define === 'function' && define.amd ? define(['exports'], factory) :\n  (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.Sanitizer = {}));\n})(this, (function (exports) { 'use strict';\n\n  /**\n   * --------------------------------------------------------------------------\n   * Bootstrap util/sanitizer.js\n   * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n   * --------------------------------------------------------------------------\n   */\n\n  // js-docs-start allow-list\n  const ARIA_ATTRIBUTE_PATTERN = /^aria-[\\w-]*$/i;\n  const DefaultAllowlist = {\n    // Global attributes allowed on any supplied element below.\n    '*': ['class', 'dir', 'id', 'lang', 'role', ARIA_ATTRIBUTE_PATTERN],\n    a: ['target', 'href', 'title', 'rel'],\n    area: [],\n    b: [],\n    br: [],\n    col: [],\n    code: [],\n    dd: [],\n    div: [],\n    dl: [],\n    dt: [],\n    em: [],\n    hr: [],\n    h1: [],\n    h2: [],\n    h3: [],\n    h4: [],\n    h5: [],\n    h6: [],\n    i: [],\n    img: ['src', 'srcset', 'alt', 'title', 'width', 'height'],\n    li: [],\n    ol: [],\n    p: [],\n    pre: [],\n    s: [],\n    small: [],\n    span: [],\n    sub: [],\n    sup: [],\n    strong: [],\n    u: [],\n    ul: []\n  };\n  // js-docs-end allow-list\n\n  const uriAttributes = new Set(['background', 'cite', 'href', 'itemtype', 'longdesc', 'poster', 'src', 'xlink:href']);\n\n  /**\n   * A pattern that recognizes URLs that are safe wrt. XSS in URL navigation\n   * contexts.\n   *\n   * Shout-out to Angular https://github.com/angular/angular/blob/15.2.8/packages/core/src/sanitization/url_sanitizer.ts#L38\n   */\n  // eslint-disable-next-line unicorn/better-regex\n  const SAFE_URL_PATTERN = /^(?!javascript:)(?:[a-z0-9+.-]+:|[^&:/?#]*(?:[/?#]|$))/i;\n  const allowedAttribute = (attribute, allowedAttributeList) => {\n    const attributeName = attribute.nodeName.toLowerCase();\n    if (allowedAttributeList.includes(attributeName)) {\n      if (uriAttributes.has(attributeName)) {\n        return Boolean(SAFE_URL_PATTERN.test(attribute.nodeValue));\n      }\n      return true;\n    }\n\n    // Check if a regular expression validates the attribute.\n    return allowedAttributeList.filter(attributeRegex => attributeRegex instanceof RegExp).some(regex => regex.test(attributeName));\n  };\n  function sanitizeHtml(unsafeHtml, allowList, sanitizeFunction) {\n    if (!unsafeHtml.length) {\n      return unsafeHtml;\n    }\n    if (sanitizeFunction && typeof sanitizeFunction === 'function') {\n      return sanitizeFunction(unsafeHtml);\n    }\n    const domParser = new window.DOMParser();\n    const createdDocument = domParser.parseFromString(unsafeHtml, 'text/html');\n    const elements = [].concat(...createdDocument.body.querySelectorAll('*'));\n    for (const element of elements) {\n      const elementName = element.nodeName.toLowerCase();\n      if (!Object.keys(allowList).includes(elementName)) {\n        element.remove();\n        continue;\n      }\n      const attributeList = [].concat(...element.attributes);\n      const allowedAttributes = [].concat(allowList['*'] || [], allowList[elementName] || []);\n      for (const attribute of attributeList) {\n        if (!allowedAttribute(attribute, allowedAttributes)) {\n          element.removeAttribute(attribute.nodeName);\n        }\n      }\n    }\n    return createdDocument.body.innerHTML;\n  }\n\n  exports.DefaultAllowlist = DefaultAllowlist;\n  exports.sanitizeHtml = sanitizeHtml;\n\n  Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });\n\n}));\n//# sourceMappingURL=sanitizer.js.map\n", "/*!\n  * Bootstrap scrollbar.js v5.3.3 (https://getbootstrap.com/)\n  * Copyright 2011-2024 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors)\n  * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n  */\n(function (global, factory) {\n  typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('../dom/manipulator.js'), require('../dom/selector-engine.js'), require('./index.js')) :\n  typeof define === 'function' && define.amd ? define(['../dom/manipulator', '../dom/selector-engine', './index'], factory) :\n  (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.Scrollbar = factory(global.Manipulator, global.SelectorEngine, global.Index));\n})(this, (function (Manipulator, SelectorEngine, index_js) { 'use strict';\n\n  /**\n   * --------------------------------------------------------------------------\n   * Bootstrap util/scrollBar.js\n   * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n   * --------------------------------------------------------------------------\n   */\n\n\n  /**\n   * Constants\n   */\n\n  const SELECTOR_FIXED_CONTENT = '.fixed-top, .fixed-bottom, .is-fixed, .sticky-top';\n  const SELECTOR_STICKY_CONTENT = '.sticky-top';\n  const PROPERTY_PADDING = 'padding-right';\n  const PROPERTY_MARGIN = 'margin-right';\n\n  /**\n   * Class definition\n   */\n\n  class ScrollBarHelper {\n    constructor() {\n      this._element = document.body;\n    }\n\n    // Public\n    getWidth() {\n      // https://developer.mozilla.org/en-US/docs/Web/API/Window/innerWidth#usage_notes\n      const documentWidth = document.documentElement.clientWidth;\n      return Math.abs(window.innerWidth - documentWidth);\n    }\n    hide() {\n      const width = this.getWidth();\n      this._disableOverFlow();\n      // give padding to element to balance the hidden scrollbar width\n      this._setElementAttributes(this._element, PROPERTY_PADDING, calculatedValue => calculatedValue + width);\n      // trick: We adjust positive paddingRight and negative marginRight to sticky-top elements to keep showing fullwidth\n      this._setElementAttributes(SELECTOR_FIXED_CONTENT, PROPERTY_PADDING, calculatedValue => calculatedValue + width);\n      this._setElementAttributes(SELECTOR_STICKY_CONTENT, PROPERTY_MARGIN, calculatedValue => calculatedValue - width);\n    }\n    reset() {\n      this._resetElementAttributes(this._element, 'overflow');\n      this._resetElementAttributes(this._element, PROPERTY_PADDING);\n      this._resetElementAttributes(SELECTOR_FIXED_CONTENT, PROPERTY_PADDING);\n      this._resetElementAttributes(SELECTOR_STICKY_CONTENT, PROPERTY_MARGIN);\n    }\n    isOverflowing() {\n      return this.getWidth() > 0;\n    }\n\n    // Private\n    _disableOverFlow() {\n      this._saveInitialAttribute(this._element, 'overflow');\n      this._element.style.overflow = 'hidden';\n    }\n    _setElementAttributes(selector, styleProperty, callback) {\n      const scrollbarWidth = this.getWidth();\n      const manipulationCallBack = element => {\n        if (element !== this._element && window.innerWidth > element.clientWidth + scrollbarWidth) {\n          return;\n        }\n        this._saveInitialAttribute(element, styleProperty);\n        const calculatedValue = window.getComputedStyle(element).getPropertyValue(styleProperty);\n        element.style.setProperty(styleProperty, `${callback(Number.parseFloat(calculatedValue))}px`);\n      };\n      this._applyManipulationCallback(selector, manipulationCallBack);\n    }\n    _saveInitialAttribute(element, styleProperty) {\n      const actualValue = element.style.getPropertyValue(styleProperty);\n      if (actualValue) {\n        Manipulator.setDataAttribute(element, styleProperty, actualValue);\n      }\n    }\n    _resetElementAttributes(selector, styleProperty) {\n      const manipulationCallBack = element => {\n        const value = Manipulator.getDataAttribute(element, styleProperty);\n        // We only want to remove the property if the value is `null`; the value can also be zero\n        if (value === null) {\n          element.style.removeProperty(styleProperty);\n          return;\n        }\n        Manipulator.removeDataAttribute(element, styleProperty);\n        element.style.setProperty(styleProperty, value);\n      };\n      this._applyManipulationCallback(selector, manipulationCallBack);\n    }\n    _applyManipulationCallback(selector, callBack) {\n      if (index_js.isElement(selector)) {\n        callBack(selector);\n        return;\n      }\n      for (const sel of SelectorEngine.find(selector, this._element)) {\n        callBack(sel);\n      }\n    }\n  }\n\n  return ScrollBarHelper;\n\n}));\n//# sourceMappingURL=scrollbar.js.map\n", "/*!\n  * Bootstrap swipe.js v5.3.3 (https://getbootstrap.com/)\n  * Copyright 2011-2024 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors)\n  * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n  */\n(function (global, factory) {\n  typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('../dom/event-handler.js'), require('./config.js'), require('./index.js')) :\n  typeof define === 'function' && define.amd ? define(['../dom/event-handler', './config', './index'], factory) :\n  (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.Swipe = factory(global.EventHandler, global.Config, global.Index));\n})(this, (function (EventHandler, Config, index_js) { 'use strict';\n\n  /**\n   * --------------------------------------------------------------------------\n   * Bootstrap util/swipe.js\n   * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n   * --------------------------------------------------------------------------\n   */\n\n\n  /**\n   * Constants\n   */\n\n  const NAME = 'swipe';\n  const EVENT_KEY = '.bs.swipe';\n  const EVENT_TOUCHSTART = `touchstart${EVENT_KEY}`;\n  const EVENT_TOUCHMOVE = `touchmove${EVENT_KEY}`;\n  const EVENT_TOUCHEND = `touchend${EVENT_KEY}`;\n  const EVENT_POINTERDOWN = `pointerdown${EVENT_KEY}`;\n  const EVENT_POINTERUP = `pointerup${EVENT_KEY}`;\n  const POINTER_TYPE_TOUCH = 'touch';\n  const POINTER_TYPE_PEN = 'pen';\n  const CLASS_NAME_POINTER_EVENT = 'pointer-event';\n  const SWIPE_THRESHOLD = 40;\n  const Default = {\n    endCallback: null,\n    leftCallback: null,\n    rightCallback: null\n  };\n  const DefaultType = {\n    endCallback: '(function|null)',\n    leftCallback: '(function|null)',\n    rightCallback: '(function|null)'\n  };\n\n  /**\n   * Class definition\n   */\n\n  class Swipe extends Config {\n    constructor(element, config) {\n      super();\n      this._element = element;\n      if (!element || !Swipe.isSupported()) {\n        return;\n      }\n      this._config = this._getConfig(config);\n      this._deltaX = 0;\n      this._supportPointerEvents = Boolean(window.PointerEvent);\n      this._initEvents();\n    }\n\n    // Getters\n    static get Default() {\n      return Default;\n    }\n    static get DefaultType() {\n      return DefaultType;\n    }\n    static get NAME() {\n      return NAME;\n    }\n\n    // Public\n    dispose() {\n      EventHandler.off(this._element, EVENT_KEY);\n    }\n\n    // Private\n    _start(event) {\n      if (!this._supportPointerEvents) {\n        this._deltaX = event.touches[0].clientX;\n        return;\n      }\n      if (this._eventIsPointerPenTouch(event)) {\n        this._deltaX = event.clientX;\n      }\n    }\n    _end(event) {\n      if (this._eventIsPointerPenTouch(event)) {\n        this._deltaX = event.clientX - this._deltaX;\n      }\n      this._handleSwipe();\n      index_js.execute(this._config.endCallback);\n    }\n    _move(event) {\n      this._deltaX = event.touches && event.touches.length > 1 ? 0 : event.touches[0].clientX - this._deltaX;\n    }\n    _handleSwipe() {\n      const absDeltaX = Math.abs(this._deltaX);\n      if (absDeltaX <= SWIPE_THRESHOLD) {\n        return;\n      }\n      const direction = absDeltaX / this._deltaX;\n      this._deltaX = 0;\n      if (!direction) {\n        return;\n      }\n      index_js.execute(direction > 0 ? this._config.rightCallback : this._config.leftCallback);\n    }\n    _initEvents() {\n      if (this._supportPointerEvents) {\n        EventHandler.on(this._element, EVENT_POINTERDOWN, event => this._start(event));\n        EventHandler.on(this._element, EVENT_POINTERUP, event => this._end(event));\n        this._element.classList.add(CLASS_NAME_POINTER_EVENT);\n      } else {\n        EventHandler.on(this._element, EVENT_TOUCHSTART, event => this._start(event));\n        EventHandler.on(this._element, EVENT_TOUCHMOVE, event => this._move(event));\n        EventHandler.on(this._element, EVENT_TOUCHEND, event => this._end(event));\n      }\n    }\n    _eventIsPointerPenTouch(event) {\n      return this._supportPointerEvents && (event.pointerType === POINTER_TYPE_PEN || event.pointerType === POINTER_TYPE_TOUCH);\n    }\n\n    // Static\n    static isSupported() {\n      return 'ontouchstart' in document.documentElement || navigator.maxTouchPoints > 0;\n    }\n  }\n\n  return Swipe;\n\n}));\n//# sourceMappingURL=swipe.js.map\n", "/*!\n  * Bootstrap template-factory.js v5.3.3 (https://getbootstrap.com/)\n  * Copyright 2011-2024 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors)\n  * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n  */\n(function (global, factory) {\n  typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('../dom/selector-engine.js'), require('./config.js'), require('./sanitizer.js'), require('./index.js')) :\n  typeof define === 'function' && define.amd ? define(['../dom/selector-engine', './config', './sanitizer', './index'], factory) :\n  (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.TemplateFactory = factory(global.SelectorEngine, global.Config, global.Sanitizer, global.Index));\n})(this, (function (SelectorEngine, Config, sanitizer_js, index_js) { 'use strict';\n\n  /**\n   * --------------------------------------------------------------------------\n   * Bootstrap util/template-factory.js\n   * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n   * --------------------------------------------------------------------------\n   */\n\n\n  /**\n   * Constants\n   */\n\n  const NAME = 'TemplateFactory';\n  const Default = {\n    allowList: sanitizer_js.DefaultAllowlist,\n    content: {},\n    // { selector : text ,  selector2 : text2 , }\n    extraClass: '',\n    html: false,\n    sanitize: true,\n    sanitizeFn: null,\n    template: '<div></div>'\n  };\n  const DefaultType = {\n    allowList: 'object',\n    content: 'object',\n    extraClass: '(string|function)',\n    html: 'boolean',\n    sanitize: 'boolean',\n    sanitizeFn: '(null|function)',\n    template: 'string'\n  };\n  const DefaultContentType = {\n    entry: '(string|element|function|null)',\n    selector: '(string|element)'\n  };\n\n  /**\n   * Class definition\n   */\n\n  class TemplateFactory extends Config {\n    constructor(config) {\n      super();\n      this._config = this._getConfig(config);\n    }\n\n    // Getters\n    static get Default() {\n      return Default;\n    }\n    static get DefaultType() {\n      return DefaultType;\n    }\n    static get NAME() {\n      return NAME;\n    }\n\n    // Public\n    getContent() {\n      return Object.values(this._config.content).map(config => this._resolvePossibleFunction(config)).filter(Boolean);\n    }\n    hasContent() {\n      return this.getContent().length > 0;\n    }\n    changeContent(content) {\n      this._checkContent(content);\n      this._config.content = {\n        ...this._config.content,\n        ...content\n      };\n      return this;\n    }\n    toHtml() {\n      const templateWrapper = document.createElement('div');\n      templateWrapper.innerHTML = this._maybeSanitize(this._config.template);\n      for (const [selector, text] of Object.entries(this._config.content)) {\n        this._setContent(templateWrapper, text, selector);\n      }\n      const template = templateWrapper.children[0];\n      const extraClass = this._resolvePossibleFunction(this._config.extraClass);\n      if (extraClass) {\n        template.classList.add(...extraClass.split(' '));\n      }\n      return template;\n    }\n\n    // Private\n    _typeCheckConfig(config) {\n      super._typeCheckConfig(config);\n      this._checkContent(config.content);\n    }\n    _checkContent(arg) {\n      for (const [selector, content] of Object.entries(arg)) {\n        super._typeCheckConfig({\n          selector,\n          entry: content\n        }, DefaultContentType);\n      }\n    }\n    _setContent(template, content, selector) {\n      const templateElement = SelectorEngine.findOne(selector, template);\n      if (!templateElement) {\n        return;\n      }\n      content = this._resolvePossibleFunction(content);\n      if (!content) {\n        templateElement.remove();\n        return;\n      }\n      if (index_js.isElement(content)) {\n        this._putElementInTemplate(index_js.getElement(content), templateElement);\n        return;\n      }\n      if (this._config.html) {\n        templateElement.innerHTML = this._maybeSanitize(content);\n        return;\n      }\n      templateElement.textContent = content;\n    }\n    _maybeSanitize(arg) {\n      return this._config.sanitize ? sanitizer_js.sanitizeHtml(arg, this._config.allowList, this._config.sanitizeFn) : arg;\n    }\n    _resolvePossibleFunction(arg) {\n      return index_js.execute(arg, [this]);\n    }\n    _putElementInTemplate(element, templateElement) {\n      if (this._config.html) {\n        templateElement.innerHTML = '';\n        templateElement.append(element);\n        return;\n      }\n      templateElement.textContent = element.textContent;\n    }\n  }\n\n  return TemplateFactory;\n\n}));\n//# sourceMappingURL=template-factory.js.map\n", "/*!\n  * Bootstrap base-component.js v5.3.3 (https://getbootstrap.com/)\n  * Copyright 2011-2024 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors)\n  * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n  */\n(function (global, factory) {\n  typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('./dom/data.js'), require('./dom/event-handler.js'), require('./util/config.js'), require('./util/index.js')) :\n  typeof define === 'function' && define.amd ? define(['./dom/data', './dom/event-handler', './util/config', './util/index'], factory) :\n  (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.BaseComponent = factory(global.Data, global.EventHandler, global.Config, global.Index));\n})(this, (function (Data, EventHandler, Config, index_js) { 'use strict';\n\n  /**\n   * --------------------------------------------------------------------------\n   * Bootstrap base-component.js\n   * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n   * --------------------------------------------------------------------------\n   */\n\n\n  /**\n   * Constants\n   */\n\n  const VERSION = '5.3.3';\n\n  /**\n   * Class definition\n   */\n\n  class BaseComponent extends Config {\n    constructor(element, config) {\n      super();\n      element = index_js.getElement(element);\n      if (!element) {\n        return;\n      }\n      this._element = element;\n      this._config = this._getConfig(config);\n      Data.set(this._element, this.constructor.DATA_KEY, this);\n    }\n\n    // Public\n    dispose() {\n      Data.remove(this._element, this.constructor.DATA_KEY);\n      EventHandler.off(this._element, this.constructor.EVENT_KEY);\n      for (const propertyName of Object.getOwnPropertyNames(this)) {\n        this[propertyName] = null;\n      }\n    }\n    _queueCallback(callback, element, isAnimated = true) {\n      index_js.executeAfterTransition(callback, element, isAnimated);\n    }\n    _getConfig(config) {\n      config = this._mergeConfigObj(config, this._element);\n      config = this._configAfterMerge(config);\n      this._typeCheckConfig(config);\n      return config;\n    }\n\n    // Static\n    static getInstance(element) {\n      return Data.get(index_js.getElement(element), this.DATA_KEY);\n    }\n    static getOrCreateInstance(element, config = {}) {\n      return this.getInstance(element) || new this(element, typeof config === 'object' ? config : null);\n    }\n    static get VERSION() {\n      return VERSION;\n    }\n    static get DATA_KEY() {\n      return `bs.${this.NAME}`;\n    }\n    static get EVENT_KEY() {\n      return `.${this.DATA_KEY}`;\n    }\n    static eventName(name) {\n      return `${name}${this.EVENT_KEY}`;\n    }\n  }\n\n  return BaseComponent;\n\n}));\n//# sourceMappingURL=base-component.js.map\n", "/*!\n  * Bootstrap alert.js v5.3.3 (https://getbootstrap.com/)\n  * Copyright 2011-2024 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors)\n  * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n  */\n(function (global, factory) {\n  typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('./base-component.js'), require('./dom/event-handler.js'), require('./util/component-functions.js'), require('./util/index.js')) :\n  typeof define === 'function' && define.amd ? define(['./base-component', './dom/event-handler', './util/component-functions', './util/index'], factory) :\n  (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.Alert = factory(global.BaseComponent, global.EventHandler, global.ComponentFunctions, global.Index));\n})(this, (function (BaseComponent, EventHandler, componentFunctions_js, index_js) { 'use strict';\n\n  /**\n   * --------------------------------------------------------------------------\n   * Bootstrap alert.js\n   * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n   * --------------------------------------------------------------------------\n   */\n\n\n  /**\n   * Constants\n   */\n\n  const NAME = 'alert';\n  const DATA_KEY = 'bs.alert';\n  const EVENT_KEY = `.${DATA_KEY}`;\n  const EVENT_CLOSE = `close${EVENT_KEY}`;\n  const EVENT_CLOSED = `closed${EVENT_KEY}`;\n  const CLASS_NAME_FADE = 'fade';\n  const CLASS_NAME_SHOW = 'show';\n\n  /**\n   * Class definition\n   */\n\n  class Alert extends BaseComponent {\n    // Getters\n    static get NAME() {\n      return NAME;\n    }\n\n    // Public\n    close() {\n      const closeEvent = EventHandler.trigger(this._element, EVENT_CLOSE);\n      if (closeEvent.defaultPrevented) {\n        return;\n      }\n      this._element.classList.remove(CLASS_NAME_SHOW);\n      const isAnimated = this._element.classList.contains(CLASS_NAME_FADE);\n      this._queueCallback(() => this._destroyElement(), this._element, isAnimated);\n    }\n\n    // Private\n    _destroyElement() {\n      this._element.remove();\n      EventHandler.trigger(this._element, EVENT_CLOSED);\n      this.dispose();\n    }\n\n    // Static\n    static jQueryInterface(config) {\n      return this.each(function () {\n        const data = Alert.getOrCreateInstance(this);\n        if (typeof config !== 'string') {\n          return;\n        }\n        if (data[config] === undefined || config.startsWith('_') || config === 'constructor') {\n          throw new TypeError(`No method named \"${config}\"`);\n        }\n        data[config](this);\n      });\n    }\n  }\n\n  /**\n   * Data API implementation\n   */\n\n  componentFunctions_js.enableDismissTrigger(Alert, 'close');\n\n  /**\n   * jQuery\n   */\n\n  index_js.defineJQueryPlugin(Alert);\n\n  return Alert;\n\n}));\n//# sourceMappingURL=alert.js.map\n", "/*!\n  * Bootstrap button.js v5.3.3 (https://getbootstrap.com/)\n  * Copyright 2011-2024 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors)\n  * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n  */\n(function (global, factory) {\n  typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('./base-component.js'), require('./dom/event-handler.js'), require('./util/index.js')) :\n  typeof define === 'function' && define.amd ? define(['./base-component', './dom/event-handler', './util/index'], factory) :\n  (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.Button = factory(global.BaseComponent, global.EventHandler, global.Index));\n})(this, (function (BaseComponent, EventHandler, index_js) { 'use strict';\n\n  /**\n   * --------------------------------------------------------------------------\n   * Bootstrap button.js\n   * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n   * --------------------------------------------------------------------------\n   */\n\n\n  /**\n   * Constants\n   */\n\n  const NAME = 'button';\n  const DATA_KEY = 'bs.button';\n  const EVENT_KEY = `.${DATA_KEY}`;\n  const DATA_API_KEY = '.data-api';\n  const CLASS_NAME_ACTIVE = 'active';\n  const SELECTOR_DATA_TOGGLE = '[data-bs-toggle=\"button\"]';\n  const EVENT_CLICK_DATA_API = `click${EVENT_KEY}${DATA_API_KEY}`;\n\n  /**\n   * Class definition\n   */\n\n  class Button extends BaseComponent {\n    // Getters\n    static get NAME() {\n      return NAME;\n    }\n\n    // Public\n    toggle() {\n      // Toggle class and sync the `aria-pressed` attribute with the return value of the `.toggle()` method\n      this._element.setAttribute('aria-pressed', this._element.classList.toggle(CLASS_NAME_ACTIVE));\n    }\n\n    // Static\n    static jQueryInterface(config) {\n      return this.each(function () {\n        const data = Button.getOrCreateInstance(this);\n        if (config === 'toggle') {\n          data[config]();\n        }\n      });\n    }\n  }\n\n  /**\n   * Data API implementation\n   */\n\n  EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, event => {\n    event.preventDefault();\n    const button = event.target.closest(SELECTOR_DATA_TOGGLE);\n    const data = Button.getOrCreateInstance(button);\n    data.toggle();\n  });\n\n  /**\n   * jQuery\n   */\n\n  index_js.defineJQueryPlugin(Button);\n\n  return Button;\n\n}));\n//# sourceMappingURL=button.js.map\n", "/*!\n  * Bootstrap carousel.js v5.3.3 (https://getbootstrap.com/)\n  * Copyright 2011-2024 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors)\n  * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n  */\n(function (global, factory) {\n  typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('./base-component.js'), require('./dom/event-handler.js'), require('./dom/manipulator.js'), require('./dom/selector-engine.js'), require('./util/index.js'), require('./util/swipe.js')) :\n  typeof define === 'function' && define.amd ? define(['./base-component', './dom/event-handler', './dom/manipulator', './dom/selector-engine', './util/index', './util/swipe'], factory) :\n  (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.Carousel = factory(global.BaseComponent, global.EventHandler, global.Manipulator, global.SelectorEngine, global.Index, global.Swipe));\n})(this, (function (BaseComponent, EventHandler, Manipulator, SelectorEngine, index_js, Swipe) { 'use strict';\n\n  /**\n   * --------------------------------------------------------------------------\n   * Bootstrap carousel.js\n   * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n   * --------------------------------------------------------------------------\n   */\n\n\n  /**\n   * Constants\n   */\n\n  const NAME = 'carousel';\n  const DATA_KEY = 'bs.carousel';\n  const EVENT_KEY = `.${DATA_KEY}`;\n  const DATA_API_KEY = '.data-api';\n  const ARROW_LEFT_KEY = 'ArrowLeft';\n  const ARROW_RIGHT_KEY = 'ArrowRight';\n  const TOUCHEVENT_COMPAT_WAIT = 500; // Time for mouse compat events to fire after touch\n\n  const ORDER_NEXT = 'next';\n  const ORDER_PREV = 'prev';\n  const DIRECTION_LEFT = 'left';\n  const DIRECTION_RIGHT = 'right';\n  const EVENT_SLIDE = `slide${EVENT_KEY}`;\n  const EVENT_SLID = `slid${EVENT_KEY}`;\n  const EVENT_KEYDOWN = `keydown${EVENT_KEY}`;\n  const EVENT_MOUSEENTER = `mouseenter${EVENT_KEY}`;\n  const EVENT_MOUSELEAVE = `mouseleave${EVENT_KEY}`;\n  const EVENT_DRAG_START = `dragstart${EVENT_KEY}`;\n  const EVENT_LOAD_DATA_API = `load${EVENT_KEY}${DATA_API_KEY}`;\n  const EVENT_CLICK_DATA_API = `click${EVENT_KEY}${DATA_API_KEY}`;\n  const CLASS_NAME_CAROUSEL = 'carousel';\n  const CLASS_NAME_ACTIVE = 'active';\n  const CLASS_NAME_SLIDE = 'slide';\n  const CLASS_NAME_END = 'carousel-item-end';\n  const CLASS_NAME_START = 'carousel-item-start';\n  const CLASS_NAME_NEXT = 'carousel-item-next';\n  const CLASS_NAME_PREV = 'carousel-item-prev';\n  const SELECTOR_ACTIVE = '.active';\n  const SELECTOR_ITEM = '.carousel-item';\n  const SELECTOR_ACTIVE_ITEM = SELECTOR_ACTIVE + SELECTOR_ITEM;\n  const SELECTOR_ITEM_IMG = '.carousel-item img';\n  const SELECTOR_INDICATORS = '.carousel-indicators';\n  const SELECTOR_DATA_SLIDE = '[data-bs-slide], [data-bs-slide-to]';\n  const SELECTOR_DATA_RIDE = '[data-bs-ride=\"carousel\"]';\n  const KEY_TO_DIRECTION = {\n    [ARROW_LEFT_KEY]: DIRECTION_RIGHT,\n    [ARROW_RIGHT_KEY]: DIRECTION_LEFT\n  };\n  const Default = {\n    interval: 5000,\n    keyboard: true,\n    pause: 'hover',\n    ride: false,\n    touch: true,\n    wrap: true\n  };\n  const DefaultType = {\n    interval: '(number|boolean)',\n    // TODO:v6 remove boolean support\n    keyboard: 'boolean',\n    pause: '(string|boolean)',\n    ride: '(boolean|string)',\n    touch: 'boolean',\n    wrap: 'boolean'\n  };\n\n  /**\n   * Class definition\n   */\n\n  class Carousel extends BaseComponent {\n    constructor(element, config) {\n      super(element, config);\n      this._interval = null;\n      this._activeElement = null;\n      this._isSliding = false;\n      this.touchTimeout = null;\n      this._swipeHelper = null;\n      this._indicatorsElement = SelectorEngine.findOne(SELECTOR_INDICATORS, this._element);\n      this._addEventListeners();\n      if (this._config.ride === CLASS_NAME_CAROUSEL) {\n        this.cycle();\n      }\n    }\n\n    // Getters\n    static get Default() {\n      return Default;\n    }\n    static get DefaultType() {\n      return DefaultType;\n    }\n    static get NAME() {\n      return NAME;\n    }\n\n    // Public\n    next() {\n      this._slide(ORDER_NEXT);\n    }\n    nextWhenVisible() {\n      // FIXME TODO use `document.visibilityState`\n      // Don't call next when the page isn't visible\n      // or the carousel or its parent isn't visible\n      if (!document.hidden && index_js.isVisible(this._element)) {\n        this.next();\n      }\n    }\n    prev() {\n      this._slide(ORDER_PREV);\n    }\n    pause() {\n      if (this._isSliding) {\n        index_js.triggerTransitionEnd(this._element);\n      }\n      this._clearInterval();\n    }\n    cycle() {\n      this._clearInterval();\n      this._updateInterval();\n      this._interval = setInterval(() => this.nextWhenVisible(), this._config.interval);\n    }\n    _maybeEnableCycle() {\n      if (!this._config.ride) {\n        return;\n      }\n      if (this._isSliding) {\n        EventHandler.one(this._element, EVENT_SLID, () => this.cycle());\n        return;\n      }\n      this.cycle();\n    }\n    to(index) {\n      const items = this._getItems();\n      if (index > items.length - 1 || index < 0) {\n        return;\n      }\n      if (this._isSliding) {\n        EventHandler.one(this._element, EVENT_SLID, () => this.to(index));\n        return;\n      }\n      const activeIndex = this._getItemIndex(this._getActive());\n      if (activeIndex === index) {\n        return;\n      }\n      const order = index > activeIndex ? ORDER_NEXT : ORDER_PREV;\n      this._slide(order, items[index]);\n    }\n    dispose() {\n      if (this._swipeHelper) {\n        this._swipeHelper.dispose();\n      }\n      super.dispose();\n    }\n\n    // Private\n    _configAfterMerge(config) {\n      config.defaultInterval = config.interval;\n      return config;\n    }\n    _addEventListeners() {\n      if (this._config.keyboard) {\n        EventHandler.on(this._element, EVENT_KEYDOWN, event => this._keydown(event));\n      }\n      if (this._config.pause === 'hover') {\n        EventHandler.on(this._element, EVENT_MOUSEENTER, () => this.pause());\n        EventHandler.on(this._element, EVENT_MOUSELEAVE, () => this._maybeEnableCycle());\n      }\n      if (this._config.touch && Swipe.isSupported()) {\n        this._addTouchEventListeners();\n      }\n    }\n    _addTouchEventListeners() {\n      for (const img of SelectorEngine.find(SELECTOR_ITEM_IMG, this._element)) {\n        EventHandler.on(img, EVENT_DRAG_START, event => event.preventDefault());\n      }\n      const endCallBack = () => {\n        if (this._config.pause !== 'hover') {\n          return;\n        }\n\n        // If it's a touch-enabled device, mouseenter/leave are fired as\n        // part of the mouse compatibility events on first tap - the carousel\n        // would stop cycling until user tapped out of it;\n        // here, we listen for touchend, explicitly pause the carousel\n        // (as if it's the second time we tap on it, mouseenter compat event\n        // is NOT fired) and after a timeout (to allow for mouse compatibility\n        // events to fire) we explicitly restart cycling\n\n        this.pause();\n        if (this.touchTimeout) {\n          clearTimeout(this.touchTimeout);\n        }\n        this.touchTimeout = setTimeout(() => this._maybeEnableCycle(), TOUCHEVENT_COMPAT_WAIT + this._config.interval);\n      };\n      const swipeConfig = {\n        leftCallback: () => this._slide(this._directionToOrder(DIRECTION_LEFT)),\n        rightCallback: () => this._slide(this._directionToOrder(DIRECTION_RIGHT)),\n        endCallback: endCallBack\n      };\n      this._swipeHelper = new Swipe(this._element, swipeConfig);\n    }\n    _keydown(event) {\n      if (/input|textarea/i.test(event.target.tagName)) {\n        return;\n      }\n      const direction = KEY_TO_DIRECTION[event.key];\n      if (direction) {\n        event.preventDefault();\n        this._slide(this._directionToOrder(direction));\n      }\n    }\n    _getItemIndex(element) {\n      return this._getItems().indexOf(element);\n    }\n    _setActiveIndicatorElement(index) {\n      if (!this._indicatorsElement) {\n        return;\n      }\n      const activeIndicator = SelectorEngine.findOne(SELECTOR_ACTIVE, this._indicatorsElement);\n      activeIndicator.classList.remove(CLASS_NAME_ACTIVE);\n      activeIndicator.removeAttribute('aria-current');\n      const newActiveIndicator = SelectorEngine.findOne(`[data-bs-slide-to=\"${index}\"]`, this._indicatorsElement);\n      if (newActiveIndicator) {\n        newActiveIndicator.classList.add(CLASS_NAME_ACTIVE);\n        newActiveIndicator.setAttribute('aria-current', 'true');\n      }\n    }\n    _updateInterval() {\n      const element = this._activeElement || this._getActive();\n      if (!element) {\n        return;\n      }\n      const elementInterval = Number.parseInt(element.getAttribute('data-bs-interval'), 10);\n      this._config.interval = elementInterval || this._config.defaultInterval;\n    }\n    _slide(order, element = null) {\n      if (this._isSliding) {\n        return;\n      }\n      const activeElement = this._getActive();\n      const isNext = order === ORDER_NEXT;\n      const nextElement = element || index_js.getNextActiveElement(this._getItems(), activeElement, isNext, this._config.wrap);\n      if (nextElement === activeElement) {\n        return;\n      }\n      const nextElementIndex = this._getItemIndex(nextElement);\n      const triggerEvent = eventName => {\n        return EventHandler.trigger(this._element, eventName, {\n          relatedTarget: nextElement,\n          direction: this._orderToDirection(order),\n          from: this._getItemIndex(activeElement),\n          to: nextElementIndex\n        });\n      };\n      const slideEvent = triggerEvent(EVENT_SLIDE);\n      if (slideEvent.defaultPrevented) {\n        return;\n      }\n      if (!activeElement || !nextElement) {\n        // Some weirdness is happening, so we bail\n        // TODO: change tests that use empty divs to avoid this check\n        return;\n      }\n      const isCycling = Boolean(this._interval);\n      this.pause();\n      this._isSliding = true;\n      this._setActiveIndicatorElement(nextElementIndex);\n      this._activeElement = nextElement;\n      const directionalClassName = isNext ? CLASS_NAME_START : CLASS_NAME_END;\n      const orderClassName = isNext ? CLASS_NAME_NEXT : CLASS_NAME_PREV;\n      nextElement.classList.add(orderClassName);\n      index_js.reflow(nextElement);\n      activeElement.classList.add(directionalClassName);\n      nextElement.classList.add(directionalClassName);\n      const completeCallBack = () => {\n        nextElement.classList.remove(directionalClassName, orderClassName);\n        nextElement.classList.add(CLASS_NAME_ACTIVE);\n        activeElement.classList.remove(CLASS_NAME_ACTIVE, orderClassName, directionalClassName);\n        this._isSliding = false;\n        triggerEvent(EVENT_SLID);\n      };\n      this._queueCallback(completeCallBack, activeElement, this._isAnimated());\n      if (isCycling) {\n        this.cycle();\n      }\n    }\n    _isAnimated() {\n      return this._element.classList.contains(CLASS_NAME_SLIDE);\n    }\n    _getActive() {\n      return SelectorEngine.findOne(SELECTOR_ACTIVE_ITEM, this._element);\n    }\n    _getItems() {\n      return SelectorEngine.find(SELECTOR_ITEM, this._element);\n    }\n    _clearInterval() {\n      if (this._interval) {\n        clearInterval(this._interval);\n        this._interval = null;\n      }\n    }\n    _directionToOrder(direction) {\n      if (index_js.isRTL()) {\n        return direction === DIRECTION_LEFT ? ORDER_PREV : ORDER_NEXT;\n      }\n      return direction === DIRECTION_LEFT ? ORDER_NEXT : ORDER_PREV;\n    }\n    _orderToDirection(order) {\n      if (index_js.isRTL()) {\n        return order === ORDER_PREV ? DIRECTION_LEFT : DIRECTION_RIGHT;\n      }\n      return order === ORDER_PREV ? DIRECTION_RIGHT : DIRECTION_LEFT;\n    }\n\n    // Static\n    static jQueryInterface(config) {\n      return this.each(function () {\n        const data = Carousel.getOrCreateInstance(this, config);\n        if (typeof config === 'number') {\n          data.to(config);\n          return;\n        }\n        if (typeof config === 'string') {\n          if (data[config] === undefined || config.startsWith('_') || config === 'constructor') {\n            throw new TypeError(`No method named \"${config}\"`);\n          }\n          data[config]();\n        }\n      });\n    }\n  }\n\n  /**\n   * Data API implementation\n   */\n\n  EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_SLIDE, function (event) {\n    const target = SelectorEngine.getElementFromSelector(this);\n    if (!target || !target.classList.contains(CLASS_NAME_CAROUSEL)) {\n      return;\n    }\n    event.preventDefault();\n    const carousel = Carousel.getOrCreateInstance(target);\n    const slideIndex = this.getAttribute('data-bs-slide-to');\n    if (slideIndex) {\n      carousel.to(slideIndex);\n      carousel._maybeEnableCycle();\n      return;\n    }\n    if (Manipulator.getDataAttribute(this, 'slide') === 'next') {\n      carousel.next();\n      carousel._maybeEnableCycle();\n      return;\n    }\n    carousel.prev();\n    carousel._maybeEnableCycle();\n  });\n  EventHandler.on(window, EVENT_LOAD_DATA_API, () => {\n    const carousels = SelectorEngine.find(SELECTOR_DATA_RIDE);\n    for (const carousel of carousels) {\n      Carousel.getOrCreateInstance(carousel);\n    }\n  });\n\n  /**\n   * jQuery\n   */\n\n  index_js.defineJQueryPlugin(Carousel);\n\n  return Carousel;\n\n}));\n//# sourceMappingURL=carousel.js.map\n", "/*!\n  * Bootstrap collapse.js v5.3.3 (https://getbootstrap.com/)\n  * Copyright 2011-2024 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors)\n  * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n  */\n(function (global, factory) {\n  typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('./base-component.js'), require('./dom/event-handler.js'), require('./dom/selector-engine.js'), require('./util/index.js')) :\n  typeof define === 'function' && define.amd ? define(['./base-component', './dom/event-handler', './dom/selector-engine', './util/index'], factory) :\n  (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.Collapse = factory(global.BaseComponent, global.EventHandler, global.SelectorEngine, global.Index));\n})(this, (function (BaseComponent, EventHandler, SelectorEngine, index_js) { 'use strict';\n\n  /**\n   * --------------------------------------------------------------------------\n   * Bootstrap collapse.js\n   * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n   * --------------------------------------------------------------------------\n   */\n\n\n  /**\n   * Constants\n   */\n\n  const NAME = 'collapse';\n  const DATA_KEY = 'bs.collapse';\n  const EVENT_KEY = `.${DATA_KEY}`;\n  const DATA_API_KEY = '.data-api';\n  const EVENT_SHOW = `show${EVENT_KEY}`;\n  const EVENT_SHOWN = `shown${EVENT_KEY}`;\n  const EVENT_HIDE = `hide${EVENT_KEY}`;\n  const EVENT_HIDDEN = `hidden${EVENT_KEY}`;\n  const EVENT_CLICK_DATA_API = `click${EVENT_KEY}${DATA_API_KEY}`;\n  const CLASS_NAME_SHOW = 'show';\n  const CLASS_NAME_COLLAPSE = 'collapse';\n  const CLASS_NAME_COLLAPSING = 'collapsing';\n  const CLASS_NAME_COLLAPSED = 'collapsed';\n  const CLASS_NAME_DEEPER_CHILDREN = `:scope .${CLASS_NAME_COLLAPSE} .${CLASS_NAME_COLLAPSE}`;\n  const CLASS_NAME_HORIZONTAL = 'collapse-horizontal';\n  const WIDTH = 'width';\n  const HEIGHT = 'height';\n  const SELECTOR_ACTIVES = '.collapse.show, .collapse.collapsing';\n  const SELECTOR_DATA_TOGGLE = '[data-bs-toggle=\"collapse\"]';\n  const Default = {\n    parent: null,\n    toggle: true\n  };\n  const DefaultType = {\n    parent: '(null|element)',\n    toggle: 'boolean'\n  };\n\n  /**\n   * Class definition\n   */\n\n  class Collapse extends BaseComponent {\n    constructor(element, config) {\n      super(element, config);\n      this._isTransitioning = false;\n      this._triggerArray = [];\n      const toggleList = SelectorEngine.find(SELECTOR_DATA_TOGGLE);\n      for (const elem of toggleList) {\n        const selector = SelectorEngine.getSelectorFromElement(elem);\n        const filterElement = SelectorEngine.find(selector).filter(foundElement => foundElement === this._element);\n        if (selector !== null && filterElement.length) {\n          this._triggerArray.push(elem);\n        }\n      }\n      this._initializeChildren();\n      if (!this._config.parent) {\n        this._addAriaAndCollapsedClass(this._triggerArray, this._isShown());\n      }\n      if (this._config.toggle) {\n        this.toggle();\n      }\n    }\n\n    // Getters\n    static get Default() {\n      return Default;\n    }\n    static get DefaultType() {\n      return DefaultType;\n    }\n    static get NAME() {\n      return NAME;\n    }\n\n    // Public\n    toggle() {\n      if (this._isShown()) {\n        this.hide();\n      } else {\n        this.show();\n      }\n    }\n    show() {\n      if (this._isTransitioning || this._isShown()) {\n        return;\n      }\n      let activeChildren = [];\n\n      // find active children\n      if (this._config.parent) {\n        activeChildren = this._getFirstLevelChildren(SELECTOR_ACTIVES).filter(element => element !== this._element).map(element => Collapse.getOrCreateInstance(element, {\n          toggle: false\n        }));\n      }\n      if (activeChildren.length && activeChildren[0]._isTransitioning) {\n        return;\n      }\n      const startEvent = EventHandler.trigger(this._element, EVENT_SHOW);\n      if (startEvent.defaultPrevented) {\n        return;\n      }\n      for (const activeInstance of activeChildren) {\n        activeInstance.hide();\n      }\n      const dimension = this._getDimension();\n      this._element.classList.remove(CLASS_NAME_COLLAPSE);\n      this._element.classList.add(CLASS_NAME_COLLAPSING);\n      this._element.style[dimension] = 0;\n      this._addAriaAndCollapsedClass(this._triggerArray, true);\n      this._isTransitioning = true;\n      const complete = () => {\n        this._isTransitioning = false;\n        this._element.classList.remove(CLASS_NAME_COLLAPSING);\n        this._element.classList.add(CLASS_NAME_COLLAPSE, CLASS_NAME_SHOW);\n        this._element.style[dimension] = '';\n        EventHandler.trigger(this._element, EVENT_SHOWN);\n      };\n      const capitalizedDimension = dimension[0].toUpperCase() + dimension.slice(1);\n      const scrollSize = `scroll${capitalizedDimension}`;\n      this._queueCallback(complete, this._element, true);\n      this._element.style[dimension] = `${this._element[scrollSize]}px`;\n    }\n    hide() {\n      if (this._isTransitioning || !this._isShown()) {\n        return;\n      }\n      const startEvent = EventHandler.trigger(this._element, EVENT_HIDE);\n      if (startEvent.defaultPrevented) {\n        return;\n      }\n      const dimension = this._getDimension();\n      this._element.style[dimension] = `${this._element.getBoundingClientRect()[dimension]}px`;\n      index_js.reflow(this._element);\n      this._element.classList.add(CLASS_NAME_COLLAPSING);\n      this._element.classList.remove(CLASS_NAME_COLLAPSE, CLASS_NAME_SHOW);\n      for (const trigger of this._triggerArray) {\n        const element = SelectorEngine.getElementFromSelector(trigger);\n        if (element && !this._isShown(element)) {\n          this._addAriaAndCollapsedClass([trigger], false);\n        }\n      }\n      this._isTransitioning = true;\n      const complete = () => {\n        this._isTransitioning = false;\n        this._element.classList.remove(CLASS_NAME_COLLAPSING);\n        this._element.classList.add(CLASS_NAME_COLLAPSE);\n        EventHandler.trigger(this._element, EVENT_HIDDEN);\n      };\n      this._element.style[dimension] = '';\n      this._queueCallback(complete, this._element, true);\n    }\n    _isShown(element = this._element) {\n      return element.classList.contains(CLASS_NAME_SHOW);\n    }\n\n    // Private\n    _configAfterMerge(config) {\n      config.toggle = Boolean(config.toggle); // Coerce string values\n      config.parent = index_js.getElement(config.parent);\n      return config;\n    }\n    _getDimension() {\n      return this._element.classList.contains(CLASS_NAME_HORIZONTAL) ? WIDTH : HEIGHT;\n    }\n    _initializeChildren() {\n      if (!this._config.parent) {\n        return;\n      }\n      const children = this._getFirstLevelChildren(SELECTOR_DATA_TOGGLE);\n      for (const element of children) {\n        const selected = SelectorEngine.getElementFromSelector(element);\n        if (selected) {\n          this._addAriaAndCollapsedClass([element], this._isShown(selected));\n        }\n      }\n    }\n    _getFirstLevelChildren(selector) {\n      const children = SelectorEngine.find(CLASS_NAME_DEEPER_CHILDREN, this._config.parent);\n      // remove children if greater depth\n      return SelectorEngine.find(selector, this._config.parent).filter(element => !children.includes(element));\n    }\n    _addAriaAndCollapsedClass(triggerArray, isOpen) {\n      if (!triggerArray.length) {\n        return;\n      }\n      for (const element of triggerArray) {\n        element.classList.toggle(CLASS_NAME_COLLAPSED, !isOpen);\n        element.setAttribute('aria-expanded', isOpen);\n      }\n    }\n\n    // Static\n    static jQueryInterface(config) {\n      const _config = {};\n      if (typeof config === 'string' && /show|hide/.test(config)) {\n        _config.toggle = false;\n      }\n      return this.each(function () {\n        const data = Collapse.getOrCreateInstance(this, _config);\n        if (typeof config === 'string') {\n          if (typeof data[config] === 'undefined') {\n            throw new TypeError(`No method named \"${config}\"`);\n          }\n          data[config]();\n        }\n      });\n    }\n  }\n\n  /**\n   * Data API implementation\n   */\n\n  EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (event) {\n    // preventDefault only for <a> elements (which change the URL) not inside the collapsible element\n    if (event.target.tagName === 'A' || event.delegateTarget && event.delegateTarget.tagName === 'A') {\n      event.preventDefault();\n    }\n    for (const element of SelectorEngine.getMultipleElementsFromSelector(this)) {\n      Collapse.getOrCreateInstance(element, {\n        toggle: false\n      }).toggle();\n    }\n  });\n\n  /**\n   * jQuery\n   */\n\n  index_js.defineJQueryPlugin(Collapse);\n\n  return Collapse;\n\n}));\n//# sourceMappingURL=collapse.js.map\n", "/*!\n  * Bootstrap dropdown.js v5.3.3 (https://getbootstrap.com/)\n  * Copyright 2011-2024 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors)\n  * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n  */\n(function (global, factory) {\n  typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('@popperjs/core'), require('./base-component.js'), require('./dom/event-handler.js'), require('./dom/manipulator.js'), require('./dom/selector-engine.js'), require('./util/index.js')) :\n  typeof define === 'function' && define.amd ? define(['@popperjs/core', './base-component', './dom/event-handler', './dom/manipulator', './dom/selector-engine', './util/index'], factory) :\n  (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.Dropdown = factory(global.Popper, global.BaseComponent, global.EventHandler, global.Manipulator, global.SelectorEngine, global.Index));\n})(this, (function (Popper, BaseComponent, EventHandler, Manipulator, SelectorEngine, index_js) { 'use strict';\n\n  function _interopNamespaceDefault(e) {\n    const n = Object.create(null, { [Symbol.toStringTag]: { value: 'Module' } });\n    if (e) {\n      for (const k in e) {\n        if (k !== 'default') {\n          const d = Object.getOwnPropertyDescriptor(e, k);\n          Object.defineProperty(n, k, d.get ? d : {\n            enumerable: true,\n            get: () => e[k]\n          });\n        }\n      }\n    }\n    n.default = e;\n    return Object.freeze(n);\n  }\n\n  const Popper__namespace = /*#__PURE__*/_interopNamespaceDefault(Popper);\n\n  /**\n   * --------------------------------------------------------------------------\n   * Bootstrap dropdown.js\n   * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n   * --------------------------------------------------------------------------\n   */\n\n\n  /**\n   * Constants\n   */\n\n  const NAME = 'dropdown';\n  const DATA_KEY = 'bs.dropdown';\n  const EVENT_KEY = `.${DATA_KEY}`;\n  const DATA_API_KEY = '.data-api';\n  const ESCAPE_KEY = 'Escape';\n  const TAB_KEY = 'Tab';\n  const ARROW_UP_KEY = 'ArrowUp';\n  const ARROW_DOWN_KEY = 'ArrowDown';\n  const RIGHT_MOUSE_BUTTON = 2; // MouseEvent.button value for the secondary button, usually the right button\n\n  const EVENT_HIDE = `hide${EVENT_KEY}`;\n  const EVENT_HIDDEN = `hidden${EVENT_KEY}`;\n  const EVENT_SHOW = `show${EVENT_KEY}`;\n  const EVENT_SHOWN = `shown${EVENT_KEY}`;\n  const EVENT_CLICK_DATA_API = `click${EVENT_KEY}${DATA_API_KEY}`;\n  const EVENT_KEYDOWN_DATA_API = `keydown${EVENT_KEY}${DATA_API_KEY}`;\n  const EVENT_KEYUP_DATA_API = `keyup${EVENT_KEY}${DATA_API_KEY}`;\n  const CLASS_NAME_SHOW = 'show';\n  const CLASS_NAME_DROPUP = 'dropup';\n  const CLASS_NAME_DROPEND = 'dropend';\n  const CLASS_NAME_DROPSTART = 'dropstart';\n  const CLASS_NAME_DROPUP_CENTER = 'dropup-center';\n  const CLASS_NAME_DROPDOWN_CENTER = 'dropdown-center';\n  const SELECTOR_DATA_TOGGLE = '[data-bs-toggle=\"dropdown\"]:not(.disabled):not(:disabled)';\n  const SELECTOR_DATA_TOGGLE_SHOWN = `${SELECTOR_DATA_TOGGLE}.${CLASS_NAME_SHOW}`;\n  const SELECTOR_MENU = '.dropdown-menu:not(.o-dropdown--menu)'; // Odoo fix task-2764821\n  const SELECTOR_NAVBAR = '.navbar';\n  const SELECTOR_MENU_NOT_SUB = '.dropdown-menu:not(.o-dropdown--menu):not(.o_wysiwyg_submenu)';\n  const SELECTOR_NAVBAR_NAV = '.navbar-nav';\n  const SELECTOR_VISIBLE_ITEMS = '.dropdown-menu .dropdown-item:not(.disabled):not(:disabled)';\n  const PLACEMENT_TOP = index_js.isRTL() ? 'top-end' : 'top-start';\n  const PLACEMENT_TOPEND = index_js.isRTL() ? 'top-start' : 'top-end';\n  const PLACEMENT_BOTTOM = index_js.isRTL() ? 'bottom-end' : 'bottom-start';\n  const PLACEMENT_BOTTOMEND = index_js.isRTL() ? 'bottom-start' : 'bottom-end';\n  const PLACEMENT_RIGHT = index_js.isRTL() ? 'left-start' : 'right-start';\n  const PLACEMENT_LEFT = index_js.isRTL() ? 'right-start' : 'left-start';\n  const PLACEMENT_TOPCENTER = 'top';\n  const PLACEMENT_BOTTOMCENTER = 'bottom';\n  const Default = {\n    autoClose: true,\n    boundary: 'clippingParents',\n    display: 'dynamic',\n    offset: [0, 2],\n    popperConfig: null,\n    reference: 'toggle'\n  };\n  const DefaultType = {\n    autoClose: '(boolean|string)',\n    boundary: '(string|element)',\n    display: 'string',\n    offset: '(array|string|function)',\n    popperConfig: '(null|object|function)',\n    reference: '(string|element|object)'\n  };\n\n  /**\n   * Class definition\n   */\n\n  class Dropdown extends BaseComponent {\n    constructor(element, config) {\n      super(element, config);\n      this._popper = null;\n      this._parent = this._element.parentNode; // dropdown wrapper\n      // TODO: v6 revert #37011 & change markup https://getbootstrap.com/docs/5.3/forms/input-group/\n      this._menu = SelectorEngine.next(this._element, SELECTOR_MENU)[0] || SelectorEngine.prev(this._element, SELECTOR_MENU)[0] || SelectorEngine.findOne(SELECTOR_MENU, this._parent);\n      this._inNavbar = this._detectNavbar();\n    }\n\n    // Getters\n    static get Default() {\n      return Default;\n    }\n    static get DefaultType() {\n      return DefaultType;\n    }\n    static get NAME() {\n      return NAME;\n    }\n\n    // Public\n    toggle() {\n      return this._isShown() ? this.hide() : this.show();\n    }\n    show() {\n      if (index_js.isDisabled(this._element) || this._isShown()) {\n        return;\n      }\n      const relatedTarget = {\n        relatedTarget: this._element\n      };\n      const showEvent = EventHandler.trigger(this._element, EVENT_SHOW, relatedTarget);\n      if (showEvent.defaultPrevented) {\n        return;\n      }\n      this._createPopper();\n\n      // If this is a touch-enabled device we add extra\n      // empty mouseover listeners to the body's immediate children;\n      // only needed because of broken event delegation on iOS\n      // https://www.quirksmode.org/blog/archives/2014/02/mouse_event_bub.html\n      if ('ontouchstart' in document.documentElement && !this._parent.closest(SELECTOR_NAVBAR_NAV)) {\n        for (const element of [].concat(...document.body.children)) {\n          EventHandler.on(element, 'mouseover', index_js.noop);\n        }\n      }\n      this._element.focus();\n      this._element.setAttribute('aria-expanded', true);\n      this._menu.classList.add(CLASS_NAME_SHOW);\n      this._element.classList.add(CLASS_NAME_SHOW);\n      EventHandler.trigger(this._element, EVENT_SHOWN, relatedTarget);\n    }\n    hide() {\n      if (index_js.isDisabled(this._element) || !this._isShown()) {\n        return;\n      }\n      const relatedTarget = {\n        relatedTarget: this._element\n      };\n      this._completeHide(relatedTarget);\n    }\n    dispose() {\n      if (this._popper) {\n        this._popper.destroy();\n      }\n      super.dispose();\n    }\n    update() {\n      this._inNavbar = this._detectNavbar();\n      if (this._popper) {\n        this._popper.update();\n      }\n    }\n\n    // Private\n    _completeHide(relatedTarget) {\n      const hideEvent = EventHandler.trigger(this._element, EVENT_HIDE, relatedTarget);\n      if (hideEvent.defaultPrevented) {\n        return;\n      }\n\n      // If this is a touch-enabled device we remove the extra\n      // empty mouseover listeners we added for iOS support\n      if ('ontouchstart' in document.documentElement) {\n        for (const element of [].concat(...document.body.children)) {\n          EventHandler.off(element, 'mouseover', index_js.noop);\n        }\n      }\n      if (this._popper) {\n        this._popper.destroy();\n      }\n      this._menu.classList.remove(CLASS_NAME_SHOW);\n      this._element.classList.remove(CLASS_NAME_SHOW);\n      this._element.setAttribute('aria-expanded', 'false');\n      Manipulator.removeDataAttribute(this._menu, 'popper');\n      EventHandler.trigger(this._element, EVENT_HIDDEN, relatedTarget);\n    }\n    _getConfig(config) {\n      config = super._getConfig(config);\n      if (typeof config.reference === 'object' && !index_js.isElement(config.reference) && typeof config.reference.getBoundingClientRect !== 'function') {\n        // Popper virtual elements require a getBoundingClientRect method\n        throw new TypeError(`${NAME.toUpperCase()}: Option \"reference\" provided type \"object\" without a required \"getBoundingClientRect\" method.`);\n      }\n      return config;\n    }\n    _createPopper() {\n      if (typeof Popper__namespace === 'undefined') {\n        throw new TypeError('Bootstrap\\'s dropdowns require Popper (https://popper.js.org)');\n      }\n      let referenceElement = this._element;\n      if (this._config.reference === 'parent') {\n        referenceElement = this._parent;\n      } else if (index_js.isElement(this._config.reference)) {\n        referenceElement = index_js.getElement(this._config.reference);\n      } else if (typeof this._config.reference === 'object') {\n        referenceElement = this._config.reference;\n      }\n      const popperConfig = this._getPopperConfig();\n      this._popper = Popper__namespace.createPopper(referenceElement, this._menu, popperConfig);\n    }\n    _isShown() {\n      return this._menu.classList.contains(CLASS_NAME_SHOW);\n    }\n    _getPlacement() {\n      const parentDropdown = this._parent;\n      if (parentDropdown.classList.contains(CLASS_NAME_DROPEND)) {\n        return PLACEMENT_RIGHT;\n      }\n      if (parentDropdown.classList.contains(CLASS_NAME_DROPSTART)) {\n        return PLACEMENT_LEFT;\n      }\n      if (parentDropdown.classList.contains(CLASS_NAME_DROPUP_CENTER)) {\n        return PLACEMENT_TOPCENTER;\n      }\n      if (parentDropdown.classList.contains(CLASS_NAME_DROPDOWN_CENTER)) {\n        return PLACEMENT_BOTTOMCENTER;\n      }\n\n      // We need to trim the value because custom properties can also include spaces\n      const isEnd = getComputedStyle(this._menu).getPropertyValue('--bs-position').trim() === 'end';\n      if (parentDropdown.classList.contains(CLASS_NAME_DROPUP)) {\n        return isEnd ? PLACEMENT_TOPEND : PLACEMENT_TOP;\n      }\n      return isEnd ? PLACEMENT_BOTTOMEND : PLACEMENT_BOTTOM;\n    }\n    _detectNavbar() {\n      return this._element.closest(SELECTOR_NAVBAR) !== null;\n    }\n    _getOffset() {\n      const {\n        offset\n      } = this._config;\n      if (typeof offset === 'string') {\n        return offset.split(',').map(value => Number.parseInt(value, 10));\n      }\n      if (typeof offset === 'function') {\n        return popperData => offset(popperData, this._element);\n      }\n      return offset;\n    }\n    _getPopperConfig() {\n      const defaultBsPopperConfig = {\n        placement: this._getPlacement(),\n        modifiers: [{\n          name: 'preventOverflow',\n          options: {\n            boundary: this._config.boundary\n          }\n        }, {\n          name: 'offset',\n          options: {\n            offset: this._getOffset()\n          }\n        }]\n      };\n\n      // Disable Popper if we have a static display or Dropdown is in Navbar\n      if (this._inNavbar || this._config.display === 'static') {\n        Manipulator.setDataAttribute(this._menu, 'popper', 'static'); // TODO: v6 remove\n        defaultBsPopperConfig.modifiers = [{\n          name: 'applyStyles',\n          enabled: false\n        }];\n      }\n      return {\n        ...defaultBsPopperConfig,\n        ...index_js.execute(this._config.popperConfig, [defaultBsPopperConfig])\n      };\n    }\n    _selectMenuItem({\n      key,\n      target\n    }) {\n      const items = SelectorEngine.find(SELECTOR_VISIBLE_ITEMS, this._menu).filter(element => index_js.isVisible(element));\n      if (!items.length) {\n        return;\n      }\n\n      // if target isn't included in items (e.g. when expanding the dropdown)\n      // allow cycling to get the last item in case key equals ARROW_UP_KEY\n      index_js.getNextActiveElement(items, target, key === ARROW_DOWN_KEY, !items.includes(target)).focus();\n    }\n\n    // Static\n    static jQueryInterface(config) {\n      return this.each(function () {\n        const data = Dropdown.getOrCreateInstance(this, config);\n        if (typeof config !== 'string') {\n          return;\n        }\n        if (typeof data[config] === 'undefined') {\n          throw new TypeError(`No method named \"${config}\"`);\n        }\n        data[config]();\n      });\n    }\n    static clearMenus(event) {\n      if (event.button === RIGHT_MOUSE_BUTTON || event.type === 'keyup' && event.key !== TAB_KEY) {\n        return;\n      }\n      const openToggles = SelectorEngine.find(SELECTOR_DATA_TOGGLE_SHOWN);\n      for (const toggle of openToggles) {\n        const context = Dropdown.getInstance(toggle);\n        if (!context || context._config.autoClose === false) {\n          continue;\n        }\n        const composedPath = event.composedPath();\n        const isMenuTarget = composedPath.includes(context._menu);\n        if (composedPath.includes(context._element) || context._config.autoClose === 'inside' && !isMenuTarget || context._config.autoClose === 'outside' && isMenuTarget) {\n          continue;\n        }\n\n        // Tab navigation through the dropdown menu or events from contained inputs shouldn't close the menu\n        if (context._menu.contains(event.target) && (event.type === 'keyup' && event.key === TAB_KEY || /input|select|option|textarea|form/i.test(event.target.tagName))) {\n          continue;\n        }\n        const relatedTarget = {\n          relatedTarget: context._element\n        };\n        if (event.type === 'click') {\n          relatedTarget.clickEvent = event;\n        }\n        context._completeHide(relatedTarget);\n      }\n    }\n    static dataApiKeydownHandler(event) {\n      // If not an UP | DOWN | ESCAPE key => not a dropdown command\n      // If input/textarea && if key is other than ESCAPE => not a dropdown command\n\n      const isInput = /input|textarea/i.test(event.target.tagName);\n      const isEscapeEvent = event.key === ESCAPE_KEY;\n      const isUpOrDownEvent = [ARROW_UP_KEY, ARROW_DOWN_KEY].includes(event.key);\n      if (!isUpOrDownEvent && !isEscapeEvent) {\n        return;\n      }\n      if (isInput && !isEscapeEvent) {\n        return;\n      }\n      event.preventDefault();\n\n      // TODO: v6 revert #37011 & change markup https://getbootstrap.com/docs/5.3/forms/input-group/\n      const getToggleButton = this.matches(SELECTOR_DATA_TOGGLE) ? this : SelectorEngine.prev(this, SELECTOR_DATA_TOGGLE)[0] || SelectorEngine.next(this, SELECTOR_DATA_TOGGLE)[0] || SelectorEngine.findOne(SELECTOR_DATA_TOGGLE, event.delegateTarget.parentNode);\n      const instance = Dropdown.getOrCreateInstance(getToggleButton);\n      if (isUpOrDownEvent) {\n        event.stopPropagation();\n        instance.show();\n        instance._selectMenuItem(event);\n        return;\n      }\n      if (instance._isShown()) {\n        // else is escape and we check if it is shown\n        event.stopPropagation();\n        instance.hide();\n        getToggleButton.focus();\n      }\n    }\n  }\n\n  /**\n   * Data API implementation\n   */\n\n  EventHandler.on(document, EVENT_KEYDOWN_DATA_API, SELECTOR_DATA_TOGGLE, Dropdown.dataApiKeydownHandler);\n  EventHandler.on(document, EVENT_KEYDOWN_DATA_API, SELECTOR_MENU_NOT_SUB, Dropdown.dataApiKeydownHandler);\n  EventHandler.on(document, EVENT_CLICK_DATA_API, Dropdown.clearMenus);\n  EventHandler.on(document, EVENT_KEYUP_DATA_API, Dropdown.clearMenus);\n  EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (event) {\n    event.preventDefault();\n    Dropdown.getOrCreateInstance(this).toggle();\n  });\n\n  /**\n   * jQuery\n   */\n\n  index_js.defineJQueryPlugin(Dropdown);\n\n  return Dropdown;\n\n}));\n//# sourceMappingURL=dropdown.js.map\n", "/*!\n  * Bootstrap modal.js v5.3.3 (https://getbootstrap.com/)\n  * Copyright 2011-2024 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors)\n  * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n  */\n(function (global, factory) {\n  typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('./base-component.js'), require('./dom/event-handler.js'), require('./dom/selector-engine.js'), require('./util/backdrop.js'), require('./util/component-functions.js'), require('./util/focustrap.js'), require('./util/index.js'), require('./util/scrollbar.js')) :\n  typeof define === 'function' && define.amd ? define(['./base-component', './dom/event-handler', './dom/selector-engine', './util/backdrop', './util/component-functions', './util/focustrap', './util/index', './util/scrollbar'], factory) :\n  (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.Modal = factory(global.BaseComponent, global.EventHandler, global.SelectorEngine, global.Backdrop, global.ComponentFunctions, global.Focustrap, global.Index, global.Scrollbar));\n})(this, (function (BaseComponent, EventHandler, SelectorEngine, Backdrop, componentFunctions_js, FocusTrap, index_js, ScrollBarHelper) { 'use strict';\n\n  /**\n   * --------------------------------------------------------------------------\n   * Bootstrap modal.js\n   * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n   * --------------------------------------------------------------------------\n   */\n\n\n  /**\n   * Constants\n   */\n\n  const NAME = 'modal';\n  const DATA_KEY = 'bs.modal';\n  const EVENT_KEY = `.${DATA_KEY}`;\n  const DATA_API_KEY = '.data-api';\n  const ESCAPE_KEY = 'Escape';\n  const EVENT_HIDE = `hide${EVENT_KEY}`;\n  const EVENT_HIDE_PREVENTED = `hidePrevented${EVENT_KEY}`;\n  const EVENT_HIDDEN = `hidden${EVENT_KEY}`;\n  const EVENT_SHOW = `show${EVENT_KEY}`;\n  const EVENT_SHOWN = `shown${EVENT_KEY}`;\n  const EVENT_RESIZE = `resize${EVENT_KEY}`;\n  const EVENT_CLICK_DISMISS = `click.dismiss${EVENT_KEY}`;\n  const EVENT_MOUSEDOWN_DISMISS = `mousedown.dismiss${EVENT_KEY}`;\n  const EVENT_KEYDOWN_DISMISS = `keydown.dismiss${EVENT_KEY}`;\n  const EVENT_CLICK_DATA_API = `click${EVENT_KEY}${DATA_API_KEY}`;\n  const CLASS_NAME_OPEN = 'modal-open';\n  const CLASS_NAME_FADE = 'fade';\n  const CLASS_NAME_SHOW = 'show';\n  const CLASS_NAME_STATIC = 'modal-static';\n  const OPEN_SELECTOR = '.modal.show';\n  const SELECTOR_DIALOG = '.modal-dialog';\n  const SELECTOR_MODAL_BODY = '.modal-body';\n  const SELECTOR_DATA_TOGGLE = '[data-bs-toggle=\"modal\"]';\n  const Default = {\n    backdrop: true,\n    focus: true,\n    keyboard: true\n  };\n  const DefaultType = {\n    backdrop: '(boolean|string)',\n    focus: 'boolean',\n    keyboard: 'boolean'\n  };\n\n  /**\n   * Class definition\n   */\n\n  class Modal extends BaseComponent {\n    constructor(element, config) {\n      super(element, config);\n      this._dialog = SelectorEngine.findOne(SELECTOR_DIALOG, this._element);\n      this._backdrop = this._initializeBackDrop();\n      this._focustrap = this._initializeFocusTrap();\n      this._isShown = false;\n      this._isTransitioning = false;\n      this._scrollBar = new ScrollBarHelper();\n      this._addEventListeners();\n    }\n\n    // Getters\n    static get Default() {\n      return Default;\n    }\n    static get DefaultType() {\n      return DefaultType;\n    }\n    static get NAME() {\n      return NAME;\n    }\n\n    // Public\n    toggle(relatedTarget) {\n      return this._isShown ? this.hide() : this.show(relatedTarget);\n    }\n    show(relatedTarget) {\n      if (this._isShown || this._isTransitioning) {\n        return;\n      }\n      const showEvent = EventHandler.trigger(this._element, EVENT_SHOW, {\n        relatedTarget\n      });\n      if (showEvent.defaultPrevented) {\n        return;\n      }\n      this._isShown = true;\n      this._isTransitioning = true;\n      this._scrollBar.hide();\n      document.body.classList.add(CLASS_NAME_OPEN);\n      this._adjustDialog();\n      this._backdrop.show(() => this._showElement(relatedTarget));\n    }\n    hide() {\n      if (!this._isShown || this._isTransitioning) {\n        return;\n      }\n      const hideEvent = EventHandler.trigger(this._element, EVENT_HIDE);\n      if (hideEvent.defaultPrevented) {\n        return;\n      }\n      this._isShown = false;\n      this._isTransitioning = true;\n      this._focustrap.deactivate();\n      this._element.classList.remove(CLASS_NAME_SHOW);\n      this._queueCallback(() => this._hideModal(), this._element, this._isAnimated());\n    }\n    dispose() {\n      EventHandler.off(window, EVENT_KEY);\n      EventHandler.off(this._dialog, EVENT_KEY);\n      this._backdrop.dispose();\n      this._focustrap.deactivate();\n      super.dispose();\n    }\n    handleUpdate() {\n      this._adjustDialog();\n    }\n\n    // Private\n    _initializeBackDrop() {\n      return new Backdrop({\n        isVisible: Boolean(this._config.backdrop),\n        // 'static' option will be translated to true, and booleans will keep their value,\n        isAnimated: this._isAnimated()\n      });\n    }\n    _initializeFocusTrap() {\n      return new FocusTrap({\n        trapElement: this._element\n      });\n    }\n    _showElement(relatedTarget) {\n      // try to append dynamic modal\n      if (!document.body.contains(this._element)) {\n        document.body.append(this._element);\n      }\n      this._element.style.display = 'block';\n      this._element.removeAttribute('aria-hidden');\n      this._element.setAttribute('aria-modal', true);\n      this._element.setAttribute('role', 'dialog');\n      this._element.scrollTop = 0;\n      const modalBody = SelectorEngine.findOne(SELECTOR_MODAL_BODY, this._dialog);\n      if (modalBody) {\n        modalBody.scrollTop = 0;\n      }\n      index_js.reflow(this._element);\n      this._element.classList.add(CLASS_NAME_SHOW);\n      const transitionComplete = () => {\n        if (this._config?.focus) {\n          this._focustrap.activate();\n        }\n        this._isTransitioning = false;\n        EventHandler.trigger(this._element, EVENT_SHOWN, {\n          relatedTarget\n        });\n      };\n      this._queueCallback(transitionComplete, this._dialog, this._isAnimated());\n    }\n    _addEventListeners() {\n      EventHandler.on(this._element, EVENT_KEYDOWN_DISMISS, event => {\n        if (event.key !== ESCAPE_KEY) {\n          return;\n        }\n        if (this._config.keyboard) {\n          this.hide();\n          return;\n        }\n        this._triggerBackdropTransition();\n      });\n      EventHandler.on(window, EVENT_RESIZE, () => {\n        if (this._isShown && !this._isTransitioning) {\n          this._adjustDialog();\n        }\n      });\n      EventHandler.on(this._element, EVENT_MOUSEDOWN_DISMISS, event => {\n        // a bad trick to segregate clicks that may start inside dialog but end outside, and avoid listen to scrollbar clicks\n        EventHandler.one(this._element, EVENT_CLICK_DISMISS, event2 => {\n          if (this._element !== event.target || this._element !== event2.target) {\n            return;\n          }\n          if (this._config.backdrop === 'static') {\n            this._triggerBackdropTransition();\n            return;\n          }\n          if (this._config.backdrop) {\n            this.hide();\n          }\n        });\n      });\n    }\n    _hideModal() {\n      this._element.style.display = 'none';\n      this._element.setAttribute('aria-hidden', true);\n      this._element.removeAttribute('aria-modal');\n      this._element.removeAttribute('role');\n      this._isTransitioning = false;\n      this._backdrop.hide(() => {\n        document.body.classList.remove(CLASS_NAME_OPEN);\n        this._resetAdjustments();\n        this._scrollBar.reset();\n        EventHandler.trigger(this._element, EVENT_HIDDEN);\n      });\n    }\n    _isAnimated() {\n      return this._element.classList.contains(CLASS_NAME_FADE);\n    }\n    _triggerBackdropTransition() {\n      const hideEvent = EventHandler.trigger(this._element, EVENT_HIDE_PREVENTED);\n      if (hideEvent.defaultPrevented) {\n        return;\n      }\n      const isModalOverflowing = this._element.scrollHeight > document.documentElement.clientHeight;\n      const initialOverflowY = this._element.style.overflowY;\n      // return if the following background transition hasn't yet completed\n      if (initialOverflowY === 'hidden' || this._element.classList.contains(CLASS_NAME_STATIC)) {\n        return;\n      }\n      if (!isModalOverflowing) {\n        this._element.style.overflowY = 'hidden';\n      }\n      this._element.classList.add(CLASS_NAME_STATIC);\n      this._queueCallback(() => {\n        this._element.classList.remove(CLASS_NAME_STATIC);\n        this._queueCallback(() => {\n          this._element.style.overflowY = initialOverflowY;\n        }, this._dialog);\n      }, this._dialog);\n      this._element.focus();\n    }\n\n    /**\n     * The following methods are used to handle overflowing modals\n     */\n\n    _adjustDialog() {\n      const isModalOverflowing = this._element.scrollHeight > document.documentElement.clientHeight;\n      const scrollbarWidth = this._scrollBar.getWidth();\n      const isBodyOverflowing = scrollbarWidth > 0;\n      if (isBodyOverflowing && !isModalOverflowing) {\n        const property = index_js.isRTL() ? 'paddingLeft' : 'paddingRight';\n        this._element.style[property] = `${scrollbarWidth}px`;\n      }\n      if (!isBodyOverflowing && isModalOverflowing) {\n        const property = index_js.isRTL() ? 'paddingRight' : 'paddingLeft';\n        this._element.style[property] = `${scrollbarWidth}px`;\n      }\n    }\n    _resetAdjustments() {\n      this._element.style.paddingLeft = '';\n      this._element.style.paddingRight = '';\n    }\n\n    // Static\n    static jQueryInterface(config, relatedTarget) {\n      return this.each(function () {\n        const data = Modal.getOrCreateInstance(this, config);\n        if (typeof config !== 'string') {\n          return;\n        }\n        if (typeof data[config] === 'undefined') {\n          throw new TypeError(`No method named \"${config}\"`);\n        }\n        data[config](relatedTarget);\n      });\n    }\n  }\n\n  /**\n   * Data API implementation\n   */\n\n  EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (event) {\n    const target = SelectorEngine.getElementFromSelector(this);\n    if (['A', 'AREA'].includes(this.tagName)) {\n      event.preventDefault();\n    }\n    EventHandler.one(target, EVENT_SHOW, showEvent => {\n      if (showEvent.defaultPrevented) {\n        // only register focus restorer if modal will actually get shown\n        return;\n      }\n      EventHandler.one(target, EVENT_HIDDEN, () => {\n        if (index_js.isVisible(this)) {\n          this.focus();\n        }\n      });\n    });\n\n    // avoid conflict when clicking modal toggler while another one is open\n    const alreadyOpen = SelectorEngine.findOne(OPEN_SELECTOR);\n    if (alreadyOpen) {\n      Modal.getInstance(alreadyOpen).hide();\n    }\n    const data = Modal.getOrCreateInstance(target);\n    data.toggle(this);\n  });\n  componentFunctions_js.enableDismissTrigger(Modal);\n\n  /**\n   * jQuery\n   */\n\n  index_js.defineJQueryPlugin(Modal);\n\n  return Modal;\n\n}));\n//# sourceMappingURL=modal.js.map\n", "/*!\n  * Bootstrap offcanvas.js v5.3.3 (https://getbootstrap.com/)\n  * Copyright 2011-2024 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors)\n  * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n  */\n(function (global, factory) {\n  typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('./base-component.js'), require('./dom/event-handler.js'), require('./dom/selector-engine.js'), require('./util/backdrop.js'), require('./util/component-functions.js'), require('./util/focustrap.js'), require('./util/index.js'), require('./util/scrollbar.js')) :\n  typeof define === 'function' && define.amd ? define(['./base-component', './dom/event-handler', './dom/selector-engine', './util/backdrop', './util/component-functions', './util/focustrap', './util/index', './util/scrollbar'], factory) :\n  (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.Offcanvas = factory(global.BaseComponent, global.EventHandler, global.SelectorEngine, global.Backdrop, global.ComponentFunctions, global.Focustrap, global.Index, global.Scrollbar));\n})(this, (function (BaseComponent, EventHandler, SelectorEngine, Backdrop, componentFunctions_js, FocusTrap, index_js, ScrollBarHelper) { 'use strict';\n\n  /**\n   * --------------------------------------------------------------------------\n   * Bootstrap offcanvas.js\n   * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n   * --------------------------------------------------------------------------\n   */\n\n\n  /**\n   * Constants\n   */\n\n  const NAME = 'offcanvas';\n  const DATA_KEY = 'bs.offcanvas';\n  const EVENT_KEY = `.${DATA_KEY}`;\n  const DATA_API_KEY = '.data-api';\n  const EVENT_LOAD_DATA_API = `load${EVENT_KEY}${DATA_API_KEY}`;\n  const ESCAPE_KEY = 'Escape';\n  const CLASS_NAME_SHOW = 'show';\n  const CLASS_NAME_SHOWING = 'showing';\n  const CLASS_NAME_HIDING = 'hiding';\n  const CLASS_NAME_BACKDROP = 'offcanvas-backdrop';\n  const OPEN_SELECTOR = '.offcanvas.show';\n  const EVENT_SHOW = `show${EVENT_KEY}`;\n  const EVENT_SHOWN = `shown${EVENT_KEY}`;\n  const EVENT_HIDE = `hide${EVENT_KEY}`;\n  const EVENT_HIDE_PREVENTED = `hidePrevented${EVENT_KEY}`;\n  const EVENT_HIDDEN = `hidden${EVENT_KEY}`;\n  const EVENT_RESIZE = `resize${EVENT_KEY}`;\n  const EVENT_CLICK_DATA_API = `click${EVENT_KEY}${DATA_API_KEY}`;\n  const EVENT_KEYDOWN_DISMISS = `keydown.dismiss${EVENT_KEY}`;\n  const SELECTOR_DATA_TOGGLE = '[data-bs-toggle=\"offcanvas\"]';\n  const Default = {\n    backdrop: true,\n    keyboard: true,\n    scroll: false\n  };\n  const DefaultType = {\n    backdrop: '(boolean|string)',\n    keyboard: 'boolean',\n    scroll: 'boolean'\n  };\n\n  /**\n   * Class definition\n   */\n\n  class Offcanvas extends BaseComponent {\n    constructor(element, config) {\n      super(element, config);\n      this._isShown = false;\n      this._backdrop = this._initializeBackDrop();\n      this._focustrap = this._initializeFocusTrap();\n      this._addEventListeners();\n    }\n\n    // Getters\n    static get Default() {\n      return Default;\n    }\n    static get DefaultType() {\n      return DefaultType;\n    }\n    static get NAME() {\n      return NAME;\n    }\n\n    // Public\n    toggle(relatedTarget) {\n      return this._isShown ? this.hide() : this.show(relatedTarget);\n    }\n    show(relatedTarget) {\n      if (this._isShown) {\n        return;\n      }\n      const showEvent = EventHandler.trigger(this._element, EVENT_SHOW, {\n        relatedTarget\n      });\n      if (showEvent.defaultPrevented) {\n        return;\n      }\n      this._isShown = true;\n      this._backdrop.show();\n      if (!this._config.scroll) {\n        new ScrollBarHelper().hide();\n      }\n      this._element.setAttribute('aria-modal', true);\n      this._element.setAttribute('role', 'dialog');\n      this._element.classList.add(CLASS_NAME_SHOWING);\n      const completeCallBack = () => {\n        if (!this._config.scroll || this._config.backdrop) {\n          this._focustrap.activate();\n        }\n        this._element.classList.add(CLASS_NAME_SHOW);\n        this._element.classList.remove(CLASS_NAME_SHOWING);\n        EventHandler.trigger(this._element, EVENT_SHOWN, {\n          relatedTarget\n        });\n      };\n      this._queueCallback(completeCallBack, this._element, true);\n    }\n    hide() {\n      if (!this._isShown) {\n        return;\n      }\n      const hideEvent = EventHandler.trigger(this._element, EVENT_HIDE);\n      if (hideEvent.defaultPrevented) {\n        return;\n      }\n      this._focustrap.deactivate();\n      this._element.blur();\n      this._isShown = false;\n      this._element.classList.add(CLASS_NAME_HIDING);\n      this._backdrop.hide();\n      const completeCallback = () => {\n        this._element.classList.remove(CLASS_NAME_SHOW, CLASS_NAME_HIDING);\n        this._element.removeAttribute('aria-modal');\n        this._element.removeAttribute('role');\n        if (!this._config.scroll) {\n          new ScrollBarHelper().reset();\n        }\n        EventHandler.trigger(this._element, EVENT_HIDDEN);\n      };\n      this._queueCallback(completeCallback, this._element, true);\n    }\n    dispose() {\n      this._backdrop.dispose();\n      this._focustrap.deactivate();\n      super.dispose();\n    }\n\n    // Private\n    _initializeBackDrop() {\n      const clickCallback = () => {\n        if (this._config.backdrop === 'static') {\n          EventHandler.trigger(this._element, EVENT_HIDE_PREVENTED);\n          return;\n        }\n        this.hide();\n      };\n\n      // 'static' option will be translated to true, and booleans will keep their value\n      const isVisible = Boolean(this._config.backdrop);\n      return new Backdrop({\n        className: CLASS_NAME_BACKDROP,\n        isVisible,\n        isAnimated: true,\n        rootElement: this._element.parentNode,\n        clickCallback: isVisible ? clickCallback : null\n      });\n    }\n    _initializeFocusTrap() {\n      return new FocusTrap({\n        trapElement: this._element\n      });\n    }\n    _addEventListeners() {\n      EventHandler.on(this._element, EVENT_KEYDOWN_DISMISS, event => {\n        if (event.key !== ESCAPE_KEY) {\n          return;\n        }\n        if (this._config.keyboard) {\n          this.hide();\n          return;\n        }\n        EventHandler.trigger(this._element, EVENT_HIDE_PREVENTED);\n      });\n    }\n\n    // Static\n    static jQueryInterface(config) {\n      return this.each(function () {\n        const data = Offcanvas.getOrCreateInstance(this, config);\n        if (typeof config !== 'string') {\n          return;\n        }\n        if (data[config] === undefined || config.startsWith('_') || config === 'constructor') {\n          throw new TypeError(`No method named \"${config}\"`);\n        }\n        data[config](this);\n      });\n    }\n  }\n\n  /**\n   * Data API implementation\n   */\n\n  EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (event) {\n    const target = SelectorEngine.getElementFromSelector(this);\n    if (['A', 'AREA'].includes(this.tagName)) {\n      event.preventDefault();\n    }\n    if (index_js.isDisabled(this)) {\n      return;\n    }\n    EventHandler.one(target, EVENT_HIDDEN, () => {\n      // focus on trigger when it is closed\n      if (index_js.isVisible(this)) {\n        this.focus();\n      }\n    });\n\n    // avoid conflict when clicking a toggler of an offcanvas, while another is open\n    const alreadyOpen = SelectorEngine.findOne(OPEN_SELECTOR);\n    if (alreadyOpen && alreadyOpen !== target) {\n      Offcanvas.getInstance(alreadyOpen).hide();\n    }\n    const data = Offcanvas.getOrCreateInstance(target);\n    data.toggle(this);\n  });\n  EventHandler.on(window, EVENT_LOAD_DATA_API, () => {\n    for (const selector of SelectorEngine.find(OPEN_SELECTOR)) {\n      Offcanvas.getOrCreateInstance(selector).show();\n    }\n  });\n  EventHandler.on(window, EVENT_RESIZE, () => {\n    for (const element of SelectorEngine.find('[aria-modal][class*=show][class*=offcanvas-]')) {\n      if (getComputedStyle(element).position !== 'fixed') {\n        Offcanvas.getOrCreateInstance(element).hide();\n      }\n    }\n  });\n  componentFunctions_js.enableDismissTrigger(Offcanvas);\n\n  /**\n   * jQuery\n   */\n\n  index_js.defineJQueryPlugin(Offcanvas);\n\n  return Offcanvas;\n\n}));\n//# sourceMappingURL=offcanvas.js.map\n", "/*!\n  * Bootstrap tooltip.js v5.3.3 (https://getbootstrap.com/)\n  * Copyright 2011-2024 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors)\n  * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n  */\n(function (global, factory) {\n  typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('@popperjs/core'), require('./base-component.js'), require('./dom/event-handler.js'), require('./dom/manipulator.js'), require('./util/index.js'), require('./util/sanitizer.js'), require('./util/template-factory.js')) :\n  typeof define === 'function' && define.amd ? define(['@popperjs/core', './base-component', './dom/event-handler', './dom/manipulator', './util/index', './util/sanitizer', './util/template-factory'], factory) :\n  (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.Tooltip = factory(global.Popper, global.BaseComponent, global.EventHandler, global.Manipulator, global.Index, global.Sanitizer, global.TemplateFactory));\n})(this, (function (Popper, BaseComponent, EventHandler, Manipulator, index_js, sanitizer_js, TemplateFactory) { 'use strict';\n\n  function _interopNamespaceDefault(e) {\n    const n = Object.create(null, { [Symbol.toStringTag]: { value: 'Module' } });\n    if (e) {\n      for (const k in e) {\n        if (k !== 'default') {\n          const d = Object.getOwnPropertyDescriptor(e, k);\n          Object.defineProperty(n, k, d.get ? d : {\n            enumerable: true,\n            get: () => e[k]\n          });\n        }\n      }\n    }\n    n.default = e;\n    return Object.freeze(n);\n  }\n\n  const Popper__namespace = /*#__PURE__*/_interopNamespaceDefault(Popper);\n\n  /**\n   * --------------------------------------------------------------------------\n   * Bootstrap tooltip.js\n   * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n   * --------------------------------------------------------------------------\n   */\n\n\n  /**\n   * Constants\n   */\n\n  const NAME = 'tooltip';\n  const DISALLOWED_ATTRIBUTES = new Set(['sanitize', 'allowList', 'sanitizeFn']);\n  const CLASS_NAME_FADE = 'fade';\n  const CLASS_NAME_MODAL = 'modal';\n  const CLASS_NAME_SHOW = 'show';\n  const SELECTOR_TOOLTIP_INNER = '.tooltip-inner';\n  const SELECTOR_MODAL = `.${CLASS_NAME_MODAL}`;\n  const EVENT_MODAL_HIDE = 'hide.bs.modal';\n  const TRIGGER_HOVER = 'hover';\n  const TRIGGER_FOCUS = 'focus';\n  const TRIGGER_CLICK = 'click';\n  const TRIGGER_MANUAL = 'manual';\n  const EVENT_HIDE = 'hide';\n  const EVENT_HIDDEN = 'hidden';\n  const EVENT_SHOW = 'show';\n  const EVENT_SHOWN = 'shown';\n  const EVENT_INSERTED = 'inserted';\n  const EVENT_CLICK = 'click';\n  const EVENT_FOCUSIN = 'focusin';\n  const EVENT_FOCUSOUT = 'focusout';\n  const EVENT_MOUSEENTER = 'mouseenter';\n  const EVENT_MOUSELEAVE = 'mouseleave';\n  const AttachmentMap = {\n    AUTO: 'auto',\n    TOP: 'top',\n    RIGHT: index_js.isRTL() ? 'left' : 'right',\n    BOTTOM: 'bottom',\n    LEFT: index_js.isRTL() ? 'right' : 'left'\n  };\n  const Default = {\n    allowList: sanitizer_js.DefaultAllowlist,\n    animation: true,\n    boundary: 'clippingParents',\n    container: false,\n    customClass: '',\n    delay: 0,\n    fallbackPlacements: ['top', 'right', 'bottom', 'left'],\n    html: false,\n    offset: [0, 6],\n    placement: 'top',\n    popperConfig: null,\n    sanitize: true,\n    sanitizeFn: null,\n    selector: false,\n    template: '<div class=\"tooltip\" role=\"tooltip\">' + '<div class=\"tooltip-arrow\"></div>' + '<div class=\"tooltip-inner\"></div>' + '</div>',\n    title: '',\n    trigger: 'hover focus'\n  };\n  const DefaultType = {\n    allowList: 'object',\n    animation: 'boolean',\n    boundary: '(string|element)',\n    container: '(string|element|boolean)',\n    customClass: '(string|function)',\n    delay: '(number|object)',\n    fallbackPlacements: 'array',\n    html: 'boolean',\n    offset: '(array|string|function)',\n    placement: '(string|function)',\n    popperConfig: '(null|object|function)',\n    sanitize: 'boolean',\n    sanitizeFn: '(null|function)',\n    selector: '(string|boolean)',\n    template: 'string',\n    title: '(string|element|function)',\n    trigger: 'string'\n  };\n\n  /**\n   * Class definition\n   */\n\n  class Tooltip extends BaseComponent {\n    constructor(element, config) {\n      if (typeof Popper__namespace === 'undefined') {\n        throw new TypeError('Bootstrap\\'s tooltips require Popper (https://popper.js.org)');\n      }\n      super(element, config);\n\n      // Private\n      this._isEnabled = true;\n      this._timeout = 0;\n      this._isHovered = null;\n      this._activeTrigger = {};\n      this._popper = null;\n      this._templateFactory = null;\n      this._newContent = null;\n\n      // Protected\n      this.tip = null;\n      this._setListeners();\n      if (!this._config.selector) {\n        this._fixTitle();\n      }\n    }\n\n    // Getters\n    static get Default() {\n      return Default;\n    }\n    static get DefaultType() {\n      return DefaultType;\n    }\n    static get NAME() {\n      return NAME;\n    }\n\n    // Public\n    enable() {\n      this._isEnabled = true;\n    }\n    disable() {\n      this._isEnabled = false;\n    }\n    toggleEnabled() {\n      this._isEnabled = !this._isEnabled;\n    }\n    toggle() {\n      if (!this._isEnabled) {\n        return;\n      }\n      this._activeTrigger.click = !this._activeTrigger.click;\n      if (this._isShown()) {\n        this._leave();\n        return;\n      }\n      this._enter();\n    }\n    dispose() {\n      clearTimeout(this._timeout);\n      EventHandler.off(this._element.closest(SELECTOR_MODAL), EVENT_MODAL_HIDE, this._hideModalHandler);\n      if (this._element.getAttribute('data-bs-original-title')) {\n        this._element.setAttribute('title', this._element.getAttribute('data-bs-original-title'));\n      }\n      this._disposePopper();\n      super.dispose();\n    }\n    show() {\n      if (this._element.style.display === 'none') {\n        throw new Error('Please use show on visible elements');\n      }\n      if (!(this._isWithContent() && this._isEnabled)) {\n        return;\n      }\n      const showEvent = EventHandler.trigger(this._element, this.constructor.eventName(EVENT_SHOW));\n      const shadowRoot = index_js.findShadowRoot(this._element);\n      const isInTheDom = (shadowRoot || this._element.ownerDocument.documentElement).contains(this._element);\n      if (showEvent.defaultPrevented || !isInTheDom) {\n        return;\n      }\n\n      // TODO: v6 remove this or make it optional\n      this._disposePopper();\n      const tip = this._getTipElement();\n      this._element.setAttribute('aria-describedby', tip.getAttribute('id'));\n      const {\n        container\n      } = this._config;\n      if (!this._element.ownerDocument.documentElement.contains(this.tip)) {\n        container.append(tip);\n        EventHandler.trigger(this._element, this.constructor.eventName(EVENT_INSERTED));\n      }\n      this._popper = this._createPopper(tip);\n      tip.classList.add(CLASS_NAME_SHOW);\n\n      // If this is a touch-enabled device we add extra\n      // empty mouseover listeners to the body's immediate children;\n      // only needed because of broken event delegation on iOS\n      // https://www.quirksmode.org/blog/archives/2014/02/mouse_event_bub.html\n      if ('ontouchstart' in document.documentElement) {\n        for (const element of [].concat(...document.body.children)) {\n          EventHandler.on(element, 'mouseover', index_js.noop);\n        }\n      }\n      const complete = () => {\n        EventHandler.trigger(this._element, this.constructor.eventName(EVENT_SHOWN));\n        if (this._isHovered === false) {\n          this._leave();\n        }\n        this._isHovered = false;\n      };\n      this._queueCallback(complete, this.tip, this._isAnimated());\n    }\n    hide() {\n      if (!this._isShown()) {\n        return;\n      }\n      const hideEvent = EventHandler.trigger(this._element, this.constructor.eventName(EVENT_HIDE));\n      if (hideEvent.defaultPrevented) {\n        return;\n      }\n      const tip = this._getTipElement();\n      tip.classList.remove(CLASS_NAME_SHOW);\n\n      // If this is a touch-enabled device we remove the extra\n      // empty mouseover listeners we added for iOS support\n      if ('ontouchstart' in document.documentElement) {\n        for (const element of [].concat(...document.body.children)) {\n          EventHandler.off(element, 'mouseover', index_js.noop);\n        }\n      }\n      this._activeTrigger[TRIGGER_CLICK] = false;\n      this._activeTrigger[TRIGGER_FOCUS] = false;\n      this._activeTrigger[TRIGGER_HOVER] = false;\n      this._isHovered = null; // it is a trick to support manual triggering\n\n      const complete = () => {\n        if (this._isWithActiveTrigger()) {\n          return;\n        }\n        if (!this._isHovered) {\n          this._disposePopper();\n        }\n        this._element.removeAttribute('aria-describedby');\n        EventHandler.trigger(this._element, this.constructor.eventName(EVENT_HIDDEN));\n      };\n      this._queueCallback(complete, this.tip, this._isAnimated());\n    }\n    update() {\n      if (this._popper) {\n        this._popper.update();\n      }\n    }\n\n    // Protected\n    _isWithContent() {\n      return Boolean(this._getTitle());\n    }\n    _getTipElement() {\n      if (!this.tip) {\n        this.tip = this._createTipElement(this._newContent || this._getContentForTemplate());\n      }\n      return this.tip;\n    }\n    _createTipElement(content) {\n      const tip = this._getTemplateFactory(content).toHtml();\n\n      // TODO: remove this check in v6\n      if (!tip) {\n        return null;\n      }\n      tip.classList.remove(CLASS_NAME_FADE, CLASS_NAME_SHOW);\n      // TODO: v6 the following can be achieved with CSS only\n      tip.classList.add(`bs-${this.constructor.NAME}-auto`);\n      const tipId = index_js.getUID(this.constructor.NAME).toString();\n      tip.setAttribute('id', tipId);\n      if (this._isAnimated()) {\n        tip.classList.add(CLASS_NAME_FADE);\n      }\n      return tip;\n    }\n    setContent(content) {\n      this._newContent = content;\n      if (this._isShown()) {\n        this._disposePopper();\n        this.show();\n      }\n    }\n    _getTemplateFactory(content) {\n      if (this._templateFactory) {\n        this._templateFactory.changeContent(content);\n      } else {\n        this._templateFactory = new TemplateFactory({\n          ...this._config,\n          // the `content` var has to be after `this._config`\n          // to override config.content in case of popover\n          content,\n          extraClass: this._resolvePossibleFunction(this._config.customClass)\n        });\n      }\n      return this._templateFactory;\n    }\n    _getContentForTemplate() {\n      return {\n        [SELECTOR_TOOLTIP_INNER]: this._getTitle()\n      };\n    }\n    _getTitle() {\n      return this._resolvePossibleFunction(this._config.title) || this._element.getAttribute('data-bs-original-title');\n    }\n\n    // Private\n    _initializeOnDelegatedTarget(event) {\n      return this.constructor.getOrCreateInstance(event.delegateTarget, this._getDelegateConfig());\n    }\n    _isAnimated() {\n      return this._config.animation || this.tip && this.tip.classList.contains(CLASS_NAME_FADE);\n    }\n    _isShown() {\n      return this.tip && this.tip.classList.contains(CLASS_NAME_SHOW);\n    }\n    _createPopper(tip) {\n      const placement = index_js.execute(this._config.placement, [this, tip, this._element]);\n      const attachment = AttachmentMap[placement.toUpperCase()];\n      return Popper__namespace.createPopper(this._element, tip, this._getPopperConfig(attachment));\n    }\n    _getOffset() {\n      const {\n        offset\n      } = this._config;\n      if (typeof offset === 'string') {\n        return offset.split(',').map(value => Number.parseInt(value, 10));\n      }\n      if (typeof offset === 'function') {\n        return popperData => offset(popperData, this._element);\n      }\n      return offset;\n    }\n    _resolvePossibleFunction(arg) {\n      return index_js.execute(arg, [this._element]);\n    }\n    _getPopperConfig(attachment) {\n      const defaultBsPopperConfig = {\n        placement: attachment,\n        modifiers: [{\n          name: 'flip',\n          options: {\n            fallbackPlacements: this._config.fallbackPlacements\n          }\n        }, {\n          name: 'offset',\n          options: {\n            offset: this._getOffset()\n          }\n        }, {\n          name: 'preventOverflow',\n          options: {\n            boundary: this._config.boundary\n          }\n        }, {\n          name: 'arrow',\n          options: {\n            element: `.${this.constructor.NAME}-arrow`\n          }\n        }, {\n          name: 'preSetPlacement',\n          enabled: true,\n          phase: 'beforeMain',\n          fn: data => {\n            // Pre-set Popper's placement attribute in order to read the arrow sizes properly.\n            // Otherwise, Popper mixes up the width and height dimensions since the initial arrow style is for top placement\n            this._getTipElement().setAttribute('data-popper-placement', data.state.placement);\n          }\n        }]\n      };\n      return {\n        ...defaultBsPopperConfig,\n        ...index_js.execute(this._config.popperConfig, [defaultBsPopperConfig])\n      };\n    }\n    _setListeners() {\n      const triggers = this._config.trigger.split(' ');\n      for (const trigger of triggers) {\n        if (trigger === 'click') {\n          EventHandler.on(this._element, this.constructor.eventName(EVENT_CLICK), this._config.selector, event => {\n            const context = this._initializeOnDelegatedTarget(event);\n            context.toggle();\n          });\n        } else if (trigger !== TRIGGER_MANUAL) {\n          const eventIn = trigger === TRIGGER_HOVER ? this.constructor.eventName(EVENT_MOUSEENTER) : this.constructor.eventName(EVENT_FOCUSIN);\n          const eventOut = trigger === TRIGGER_HOVER ? this.constructor.eventName(EVENT_MOUSELEAVE) : this.constructor.eventName(EVENT_FOCUSOUT);\n          EventHandler.on(this._element, eventIn, this._config.selector, event => {\n            const context = this._initializeOnDelegatedTarget(event);\n            context._activeTrigger[event.type === 'focusin' ? TRIGGER_FOCUS : TRIGGER_HOVER] = true;\n            context._enter();\n          });\n          EventHandler.on(this._element, eventOut, this._config.selector, event => {\n            const context = this._initializeOnDelegatedTarget(event);\n            context._activeTrigger[event.type === 'focusout' ? TRIGGER_FOCUS : TRIGGER_HOVER] = context._element.contains(event.relatedTarget);\n            context._leave();\n          });\n        }\n      }\n      this._hideModalHandler = () => {\n        if (this._element) {\n          this.hide();\n        }\n      };\n      EventHandler.on(this._element.closest(SELECTOR_MODAL), EVENT_MODAL_HIDE, this._hideModalHandler);\n    }\n    _fixTitle() {\n      const title = this._element.getAttribute('title');\n      if (!title) {\n        return;\n      }\n      if (!this._element.getAttribute('aria-label') && !this._element.textContent.trim()) {\n        this._element.setAttribute('aria-label', title);\n      }\n      this._element.setAttribute('data-bs-original-title', title); // DO NOT USE IT. Is only for backwards compatibility\n      this._element.removeAttribute('title');\n    }\n    _enter() {\n      if (this._isShown() || this._isHovered) {\n        this._isHovered = true;\n        return;\n      }\n      this._isHovered = true;\n      this._setTimeout(() => {\n        if (this._isHovered) {\n          this.show();\n        }\n      }, this._config.delay.show);\n    }\n    _leave() {\n      if (this._isWithActiveTrigger()) {\n        return;\n      }\n      this._isHovered = false;\n      this._setTimeout(() => {\n        if (!this._isHovered) {\n          this.hide();\n        }\n      }, this._config.delay.hide);\n    }\n    _setTimeout(handler, timeout) {\n      clearTimeout(this._timeout);\n      this._timeout = setTimeout(handler, timeout);\n    }\n    _isWithActiveTrigger() {\n      return Object.values(this._activeTrigger).includes(true);\n    }\n    _getConfig(config) {\n      const dataAttributes = Manipulator.getDataAttributes(this._element);\n      for (const dataAttribute of Object.keys(dataAttributes)) {\n        if (DISALLOWED_ATTRIBUTES.has(dataAttribute)) {\n          delete dataAttributes[dataAttribute];\n        }\n      }\n      config = {\n        ...dataAttributes,\n        ...(typeof config === 'object' && config ? config : {})\n      };\n      config = this._mergeConfigObj(config);\n      config = this._configAfterMerge(config);\n      this._typeCheckConfig(config);\n      return config;\n    }\n    _configAfterMerge(config) {\n      config.container = config.container === false ? document.body : index_js.getElement(config.container);\n      if (typeof config.delay === 'number') {\n        config.delay = {\n          show: config.delay,\n          hide: config.delay\n        };\n      }\n      if (typeof config.title === 'number') {\n        config.title = config.title.toString();\n      }\n      if (typeof config.content === 'number') {\n        config.content = config.content.toString();\n      }\n      return config;\n    }\n    _getDelegateConfig() {\n      const config = {};\n      for (const [key, value] of Object.entries(this._config)) {\n        if (this.constructor.Default[key] !== value) {\n          config[key] = value;\n        }\n      }\n      config.selector = false;\n      config.trigger = 'manual';\n\n      // In the future can be replaced with:\n      // const keysWithDifferentValues = Object.entries(this._config).filter(entry => this.constructor.Default[entry[0]] !== this._config[entry[0]])\n      // `Object.fromEntries(keysWithDifferentValues)`\n      return config;\n    }\n    _disposePopper() {\n      if (this._popper) {\n        this._popper.destroy();\n        this._popper = null;\n      }\n      if (this.tip) {\n        this.tip.remove();\n        this.tip = null;\n      }\n    }\n\n    // Static\n    static jQueryInterface(config) {\n      return this.each(function () {\n        const data = Tooltip.getOrCreateInstance(this, config);\n        if (typeof config !== 'string') {\n          return;\n        }\n        if (typeof data[config] === 'undefined') {\n          throw new TypeError(`No method named \"${config}\"`);\n        }\n        data[config]();\n      });\n    }\n  }\n\n  /**\n   * jQuery\n   */\n\n  index_js.defineJQueryPlugin(Tooltip);\n\n  return Tooltip;\n\n}));\n//# sourceMappingURL=tooltip.js.map\n", "/*!\n  * Bootstrap popover.js v5.3.3 (https://getbootstrap.com/)\n  * Copyright 2011-2024 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors)\n  * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n  */\n(function (global, factory) {\n  typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('./tooltip.js'), require('./util/index.js')) :\n  typeof define === 'function' && define.amd ? define(['./tooltip', './util/index'], factory) :\n  (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.Popover = factory(global.Tooltip, global.Index));\n})(this, (function (Tooltip, index_js) { 'use strict';\n\n  /**\n   * --------------------------------------------------------------------------\n   * Bootstrap popover.js\n   * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n   * --------------------------------------------------------------------------\n   */\n\n\n  /**\n   * Constants\n   */\n\n  const NAME = 'popover';\n  const SELECTOR_TITLE = '.popover-header';\n  const SELECTOR_CONTENT = '.popover-body';\n  const Default = {\n    ...Tooltip.Default,\n    content: '',\n    offset: [0, 8],\n    placement: 'right',\n    template: '<div class=\"popover\" role=\"tooltip\">' + '<div class=\"popover-arrow\"></div>' + '<h3 class=\"popover-header\"></h3>' + '<div class=\"popover-body\"></div>' + '</div>',\n    trigger: 'click'\n  };\n  const DefaultType = {\n    ...Tooltip.DefaultType,\n    content: '(null|string|element|function)'\n  };\n\n  /**\n   * Class definition\n   */\n\n  class Popover extends Tooltip {\n    // Getters\n    static get Default() {\n      return Default;\n    }\n    static get DefaultType() {\n      return DefaultType;\n    }\n    static get NAME() {\n      return NAME;\n    }\n\n    // Overrides\n    _isWithContent() {\n      return this._getTitle() || this._getContent();\n    }\n\n    // Private\n    _getContentForTemplate() {\n      return {\n        [SELECTOR_TITLE]: this._getTitle(),\n        [SELECTOR_CONTENT]: this._getContent()\n      };\n    }\n    _getContent() {\n      return this._resolvePossibleFunction(this._config.content);\n    }\n\n    // Static\n    static jQueryInterface(config) {\n      return this.each(function () {\n        const data = Popover.getOrCreateInstance(this, config);\n        if (typeof config !== 'string') {\n          return;\n        }\n        if (typeof data[config] === 'undefined') {\n          throw new TypeError(`No method named \"${config}\"`);\n        }\n        data[config]();\n      });\n    }\n  }\n\n  /**\n   * jQuery\n   */\n\n  index_js.defineJQueryPlugin(Popover);\n\n  return Popover;\n\n}));\n//# sourceMappingURL=popover.js.map\n", "/*!\n  * Bootstrap scrollspy.js v5.3.3 (https://getbootstrap.com/)\n  * Copyright 2011-2024 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors)\n  * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n  */\n(function (global, factory) {\n  typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('./base-component.js'), require('./dom/event-handler.js'), require('./dom/selector-engine.js'), require('./util/index.js')) :\n  typeof define === 'function' && define.amd ? define(['./base-component', './dom/event-handler', './dom/selector-engine', './util/index'], factory) :\n  (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.ScrollSpy = factory(global.BaseComponent, global.EventHandler, global.SelectorEngine, global.Index));\n})(this, (function (BaseComponent, EventHandler, SelectorEngine, index_js) { 'use strict';\n\n  /**\n   * --------------------------------------------------------------------------\n   * Bootstrap scrollspy.js\n   * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n   * --------------------------------------------------------------------------\n   */\n\n\n  /**\n   * Constants\n   */\n\n  const NAME = 'scrollspy';\n  const DATA_KEY = 'bs.scrollspy';\n  const EVENT_KEY = `.${DATA_KEY}`;\n  const DATA_API_KEY = '.data-api';\n  const EVENT_ACTIVATE = `activate${EVENT_KEY}`;\n  const EVENT_CLICK = `click${EVENT_KEY}`;\n  const EVENT_LOAD_DATA_API = `load${EVENT_KEY}${DATA_API_KEY}`;\n  const CLASS_NAME_DROPDOWN_ITEM = 'dropdown-item';\n  const CLASS_NAME_ACTIVE = 'active';\n  const SELECTOR_DATA_SPY = '[data-bs-spy=\"scroll\"]';\n  const SELECTOR_TARGET_LINKS = '[href]';\n  const SELECTOR_NAV_LIST_GROUP = '.nav, .list-group';\n  const SELECTOR_NAV_LINKS = '.nav-link';\n  const SELECTOR_NAV_ITEMS = '.nav-item';\n  const SELECTOR_LIST_ITEMS = '.list-group-item';\n  const SELECTOR_LINK_ITEMS = `${SELECTOR_NAV_LINKS}, ${SELECTOR_NAV_ITEMS} > ${SELECTOR_NAV_LINKS}, ${SELECTOR_LIST_ITEMS}`;\n  const SELECTOR_DROPDOWN = '.dropdown';\n  const SELECTOR_DROPDOWN_TOGGLE = '.dropdown-toggle';\n  const Default = {\n    offset: null,\n    // TODO: v6 @deprecated, keep it for backwards compatibility reasons\n    rootMargin: '0px 0px -25%',\n    smoothScroll: false,\n    target: null,\n    threshold: [0.1, 0.5, 1]\n  };\n  const DefaultType = {\n    offset: '(number|null)',\n    // TODO v6 @deprecated, keep it for backwards compatibility reasons\n    rootMargin: 'string',\n    smoothScroll: 'boolean',\n    target: 'element',\n    threshold: 'array'\n  };\n\n  /**\n   * Class definition\n   */\n\n  class ScrollSpy extends BaseComponent {\n    constructor(element, config) {\n      super(element, config);\n\n      // this._element is the observablesContainer and config.target the menu links wrapper\n      this._targetLinks = new Map();\n      this._observableSections = new Map();\n      this._rootElement = getComputedStyle(this._element).overflowY === 'visible' ? null : this._element;\n      this._activeTarget = null;\n      this._observer = null;\n      this._previousScrollData = {\n        visibleEntryTop: 0,\n        parentScrollTop: 0\n      };\n      this.refresh(); // initialize\n    }\n\n    // Getters\n    static get Default() {\n      return Default;\n    }\n    static get DefaultType() {\n      return DefaultType;\n    }\n    static get NAME() {\n      return NAME;\n    }\n\n    // Public\n    refresh() {\n      this._initializeTargetsAndObservables();\n      this._maybeEnableSmoothScroll();\n      if (this._observer) {\n        this._observer.disconnect();\n      } else {\n        this._observer = this._getNewObserver();\n      }\n      for (const section of this._observableSections.values()) {\n        this._observer.observe(section);\n      }\n    }\n    dispose() {\n      this._observer.disconnect();\n      super.dispose();\n    }\n\n    // Private\n    _configAfterMerge(config) {\n      // TODO: on v6 target should be given explicitly & remove the {target: 'ss-target'} case\n      config.target = index_js.getElement(config.target) || document.body;\n\n      // TODO: v6 Only for backwards compatibility reasons. Use rootMargin only\n      config.rootMargin = config.offset ? `${config.offset}px 0px -30%` : config.rootMargin;\n      if (typeof config.threshold === 'string') {\n        config.threshold = config.threshold.split(',').map(value => Number.parseFloat(value));\n      }\n      return config;\n    }\n    _maybeEnableSmoothScroll() {\n      if (!this._config.smoothScroll) {\n        return;\n      }\n\n      // unregister any previous listeners\n      EventHandler.off(this._config.target, EVENT_CLICK);\n      EventHandler.on(this._config.target, EVENT_CLICK, SELECTOR_TARGET_LINKS, event => {\n        const observableSection = this._observableSections.get(event.target.hash);\n        if (observableSection) {\n          event.preventDefault();\n          const root = this._rootElement || window;\n          const height = observableSection.offsetTop - this._element.offsetTop;\n          if (root.scrollTo) {\n            root.scrollTo({\n              top: height,\n              behavior: 'smooth'\n            });\n            return;\n          }\n\n          // Chrome 60 doesn't support `scrollTo`\n          root.scrollTop = height;\n        }\n      });\n    }\n    _getNewObserver() {\n      const options = {\n        root: this._rootElement,\n        threshold: this._config.threshold,\n        rootMargin: this._config.rootMargin\n      };\n      return new IntersectionObserver(entries => this._observerCallback(entries), options);\n    }\n\n    // The logic of selection\n    _observerCallback(entries) {\n      const targetElement = entry => this._targetLinks.get(`#${entry.target.id}`);\n      const activate = entry => {\n        this._previousScrollData.visibleEntryTop = entry.target.offsetTop;\n        this._process(targetElement(entry));\n      };\n      const parentScrollTop = (this._rootElement || document.documentElement).scrollTop;\n      const userScrollsDown = parentScrollTop >= this._previousScrollData.parentScrollTop;\n      this._previousScrollData.parentScrollTop = parentScrollTop;\n      for (const entry of entries) {\n        if (!entry.isIntersecting) {\n          this._activeTarget = null;\n          this._clearActiveClass(targetElement(entry));\n          continue;\n        }\n        const entryIsLowerThanPrevious = entry.target.offsetTop >= this._previousScrollData.visibleEntryTop;\n        // if we are scrolling down, pick the bigger offsetTop\n        if (userScrollsDown && entryIsLowerThanPrevious) {\n          activate(entry);\n          // if parent isn't scrolled, let's keep the first visible item, breaking the iteration\n          if (!parentScrollTop) {\n            return;\n          }\n          continue;\n        }\n\n        // if we are scrolling up, pick the smallest offsetTop\n        if (!userScrollsDown && !entryIsLowerThanPrevious) {\n          activate(entry);\n        }\n      }\n    }\n    _initializeTargetsAndObservables() {\n      this._targetLinks = new Map();\n      this._observableSections = new Map();\n      const targetLinks = SelectorEngine.find(SELECTOR_TARGET_LINKS, this._config.target);\n      for (const anchor of targetLinks) {\n        // ensure that the anchor has an id and is not disabled\n        if (!anchor.hash || index_js.isDisabled(anchor)) {\n          continue;\n        }\n        const observableSection = SelectorEngine.findOne(decodeURI(anchor.hash), this._element);\n\n        // ensure that the observableSection exists & is visible\n        if (index_js.isVisible(observableSection)) {\n          this._targetLinks.set(decodeURI(anchor.hash), anchor);\n          this._observableSections.set(anchor.hash, observableSection);\n        }\n      }\n    }\n    _process(target) {\n      if (this._activeTarget === target) {\n        return;\n      }\n      this._clearActiveClass(this._config.target);\n      this._activeTarget = target;\n      target.classList.add(CLASS_NAME_ACTIVE);\n      this._activateParents(target);\n      EventHandler.trigger(this._element, EVENT_ACTIVATE, {\n        relatedTarget: target\n      });\n    }\n    _activateParents(target) {\n      // Activate dropdown parents\n      if (target.classList.contains(CLASS_NAME_DROPDOWN_ITEM)) {\n        SelectorEngine.findOne(SELECTOR_DROPDOWN_TOGGLE, target.closest(SELECTOR_DROPDOWN)).classList.add(CLASS_NAME_ACTIVE);\n        return;\n      }\n      for (const listGroup of SelectorEngine.parents(target, SELECTOR_NAV_LIST_GROUP)) {\n        // Set triggered links parents as active\n        // With both <ul> and <nav> markup a parent is the previous sibling of any nav ancestor\n        for (const item of SelectorEngine.prev(listGroup, SELECTOR_LINK_ITEMS)) {\n          item.classList.add(CLASS_NAME_ACTIVE);\n        }\n      }\n    }\n    _clearActiveClass(parent) {\n      parent.classList.remove(CLASS_NAME_ACTIVE);\n      const activeNodes = SelectorEngine.find(`${SELECTOR_TARGET_LINKS}.${CLASS_NAME_ACTIVE}`, parent);\n      for (const node of activeNodes) {\n        node.classList.remove(CLASS_NAME_ACTIVE);\n      }\n    }\n\n    // Static\n    static jQueryInterface(config) {\n      return this.each(function () {\n        const data = ScrollSpy.getOrCreateInstance(this, config);\n        if (typeof config !== 'string') {\n          return;\n        }\n        if (data[config] === undefined || config.startsWith('_') || config === 'constructor') {\n          throw new TypeError(`No method named \"${config}\"`);\n        }\n        data[config]();\n      });\n    }\n  }\n\n  /**\n   * Data API implementation\n   */\n\n  EventHandler.on(window, EVENT_LOAD_DATA_API, () => {\n    for (const spy of SelectorEngine.find(SELECTOR_DATA_SPY)) {\n      ScrollSpy.getOrCreateInstance(spy);\n    }\n  });\n\n  /**\n   * jQuery\n   */\n\n  index_js.defineJQueryPlugin(ScrollSpy);\n\n  return ScrollSpy;\n\n}));\n//# sourceMappingURL=scrollspy.js.map\n", "/*!\n  * Bootstrap tab.js v5.3.3 (https://getbootstrap.com/)\n  * Copyright 2011-2024 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors)\n  * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n  */\n(function (global, factory) {\n  typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('./base-component.js'), require('./dom/event-handler.js'), require('./dom/selector-engine.js'), require('./util/index.js')) :\n  typeof define === 'function' && define.amd ? define(['./base-component', './dom/event-handler', './dom/selector-engine', './util/index'], factory) :\n  (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.Tab = factory(global.BaseComponent, global.EventHandler, global.SelectorEngine, global.Index));\n})(this, (function (BaseComponent, EventHandler, SelectorEngine, index_js) { 'use strict';\n\n  /**\n   * --------------------------------------------------------------------------\n   * Bootstrap tab.js\n   * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n   * --------------------------------------------------------------------------\n   */\n\n\n  /**\n   * Constants\n   */\n\n  const NAME = 'tab';\n  const DATA_KEY = 'bs.tab';\n  const EVENT_KEY = `.${DATA_KEY}`;\n  const EVENT_HIDE = `hide${EVENT_KEY}`;\n  const EVENT_HIDDEN = `hidden${EVENT_KEY}`;\n  const EVENT_SHOW = `show${EVENT_KEY}`;\n  const EVENT_SHOWN = `shown${EVENT_KEY}`;\n  const EVENT_CLICK_DATA_API = `click${EVENT_KEY}`;\n  const EVENT_KEYDOWN = `keydown${EVENT_KEY}`;\n  const EVENT_LOAD_DATA_API = `load${EVENT_KEY}`;\n  const ARROW_LEFT_KEY = 'ArrowLeft';\n  const ARROW_RIGHT_KEY = 'ArrowRight';\n  const ARROW_UP_KEY = 'ArrowUp';\n  const ARROW_DOWN_KEY = 'ArrowDown';\n  const HOME_KEY = 'Home';\n  const END_KEY = 'End';\n  const CLASS_NAME_ACTIVE = 'active';\n  const CLASS_NAME_FADE = 'fade';\n  const CLASS_NAME_SHOW = 'show';\n  const CLASS_DROPDOWN = 'dropdown';\n  const SELECTOR_DROPDOWN_TOGGLE = '.dropdown-toggle';\n  const SELECTOR_DROPDOWN_MENU = '.dropdown-menu';\n  const NOT_SELECTOR_DROPDOWN_TOGGLE = `:not(${SELECTOR_DROPDOWN_TOGGLE})`;\n  const SELECTOR_TAB_PANEL = '.list-group, .nav, [role=\"tablist\"]';\n  const SELECTOR_OUTER = '.nav-item, .list-group-item';\n  const SELECTOR_INNER = `.nav-link${NOT_SELECTOR_DROPDOWN_TOGGLE}, .list-group-item${NOT_SELECTOR_DROPDOWN_TOGGLE}, [role=\"tab\"]${NOT_SELECTOR_DROPDOWN_TOGGLE}`;\n  const SELECTOR_DATA_TOGGLE = '[data-bs-toggle=\"tab\"], [data-bs-toggle=\"pill\"], [data-bs-toggle=\"list\"]'; // TODO: could only be `tab` in v6\n  const SELECTOR_INNER_ELEM = `${SELECTOR_INNER}, ${SELECTOR_DATA_TOGGLE}`;\n  const SELECTOR_DATA_TOGGLE_ACTIVE = `.${CLASS_NAME_ACTIVE}[data-bs-toggle=\"tab\"], .${CLASS_NAME_ACTIVE}[data-bs-toggle=\"pill\"], .${CLASS_NAME_ACTIVE}[data-bs-toggle=\"list\"]`;\n\n  /**\n   * Class definition\n   */\n\n  class Tab extends BaseComponent {\n    constructor(element) {\n      super(element);\n      this._parent = this._element.closest(SELECTOR_TAB_PANEL);\n      if (!this._parent) {\n        return;\n        // TODO: should throw exception in v6\n        // throw new TypeError(`${element.outerHTML} has not a valid parent ${SELECTOR_INNER_ELEM}`)\n      }\n\n      // Set up initial aria attributes\n      this._setInitialAttributes(this._parent, this._getChildren());\n      EventHandler.on(this._element, EVENT_KEYDOWN, event => this._keydown(event));\n    }\n\n    // Getters\n    static get NAME() {\n      return NAME;\n    }\n\n    // Public\n    show() {\n      // Shows this elem and deactivate the active sibling if exists\n      const innerElem = this._element;\n      if (this._elemIsActive(innerElem)) {\n        return;\n      }\n\n      // Search for active tab on same parent to deactivate it\n      const active = this._getActiveElem();\n      const hideEvent = active ? EventHandler.trigger(active, EVENT_HIDE, {\n        relatedTarget: innerElem\n      }) : null;\n      const showEvent = EventHandler.trigger(innerElem, EVENT_SHOW, {\n        relatedTarget: active\n      });\n      if (showEvent.defaultPrevented || hideEvent && hideEvent.defaultPrevented) {\n        return;\n      }\n      this._deactivate(active, innerElem);\n      this._activate(innerElem, active);\n    }\n\n    // Private\n    _activate(element, relatedElem) {\n      if (!element) {\n        return;\n      }\n      element.classList.add(CLASS_NAME_ACTIVE);\n      this._activate(SelectorEngine.getElementFromSelector(element)); // Search and activate/show the proper section\n\n      const complete = () => {\n        if (element.getAttribute('role') !== 'tab') {\n          element.classList.add(CLASS_NAME_SHOW);\n          return;\n        }\n        element.removeAttribute('tabindex');\n        element.setAttribute('aria-selected', true);\n        this._toggleDropDown(element, true);\n        EventHandler.trigger(element, EVENT_SHOWN, {\n          relatedTarget: relatedElem\n        });\n      };\n      this._queueCallback(complete, element, element.classList.contains(CLASS_NAME_FADE));\n    }\n    _deactivate(element, relatedElem) {\n      if (!element) {\n        return;\n      }\n      element.classList.remove(CLASS_NAME_ACTIVE);\n      element.blur();\n      this._deactivate(SelectorEngine.getElementFromSelector(element)); // Search and deactivate the shown section too\n\n      const complete = () => {\n        if (element.getAttribute('role') !== 'tab') {\n          element.classList.remove(CLASS_NAME_SHOW);\n          return;\n        }\n        element.setAttribute('aria-selected', false);\n        element.setAttribute('tabindex', '-1');\n        this._toggleDropDown(element, false);\n        EventHandler.trigger(element, EVENT_HIDDEN, {\n          relatedTarget: relatedElem\n        });\n      };\n      this._queueCallback(complete, element, element.classList.contains(CLASS_NAME_FADE));\n    }\n    _keydown(event) {\n      if (![ARROW_LEFT_KEY, ARROW_RIGHT_KEY, ARROW_UP_KEY, ARROW_DOWN_KEY, HOME_KEY, END_KEY].includes(event.key)) {\n        return;\n      }\n      event.stopPropagation(); // stopPropagation/preventDefault both added to support up/down keys without scrolling the page\n      event.preventDefault();\n      const children = this._getChildren().filter(element => !index_js.isDisabled(element));\n      let nextActiveElement;\n      if ([HOME_KEY, END_KEY].includes(event.key)) {\n        nextActiveElement = children[event.key === HOME_KEY ? 0 : children.length - 1];\n      } else {\n        const isNext = [ARROW_RIGHT_KEY, ARROW_DOWN_KEY].includes(event.key);\n        nextActiveElement = index_js.getNextActiveElement(children, event.target, isNext, true);\n      }\n      if (nextActiveElement) {\n        nextActiveElement.focus({\n          preventScroll: true\n        });\n        Tab.getOrCreateInstance(nextActiveElement).show();\n      }\n    }\n    _getChildren() {\n      // collection of inner elements\n      return SelectorEngine.find(SELECTOR_INNER_ELEM, this._parent);\n    }\n    _getActiveElem() {\n      return this._getChildren().find(child => this._elemIsActive(child)) || null;\n    }\n    _setInitialAttributes(parent, children) {\n      this._setAttributeIfNotExists(parent, 'role', 'tablist');\n      for (const child of children) {\n        this._setInitialAttributesOnChild(child);\n      }\n    }\n    _setInitialAttributesOnChild(child) {\n      child = this._getInnerElement(child);\n      const isActive = this._elemIsActive(child);\n      const outerElem = this._getOuterElement(child);\n      child.setAttribute('aria-selected', isActive);\n      if (outerElem !== child) {\n        this._setAttributeIfNotExists(outerElem, 'role', 'presentation');\n      }\n      if (!isActive) {\n        child.setAttribute('tabindex', '-1');\n      }\n      this._setAttributeIfNotExists(child, 'role', 'tab');\n\n      // set attributes to the related panel too\n      this._setInitialAttributesOnTargetPanel(child);\n    }\n    _setInitialAttributesOnTargetPanel(child) {\n      const target = SelectorEngine.getElementFromSelector(child);\n      if (!target) {\n        return;\n      }\n      this._setAttributeIfNotExists(target, 'role', 'tabpanel');\n      if (child.id) {\n        this._setAttributeIfNotExists(target, 'aria-labelledby', `${child.id}`);\n      }\n    }\n    _toggleDropDown(element, open) {\n      const outerElem = this._getOuterElement(element);\n      if (!outerElem.classList.contains(CLASS_DROPDOWN)) {\n        return;\n      }\n      const toggle = (selector, className) => {\n        const element = SelectorEngine.findOne(selector, outerElem);\n        if (element) {\n          element.classList.toggle(className, open);\n        }\n      };\n      toggle(SELECTOR_DROPDOWN_TOGGLE, CLASS_NAME_ACTIVE);\n      toggle(SELECTOR_DROPDOWN_MENU, CLASS_NAME_SHOW);\n      outerElem.setAttribute('aria-expanded', open);\n    }\n    _setAttributeIfNotExists(element, attribute, value) {\n      if (!element.hasAttribute(attribute)) {\n        element.setAttribute(attribute, value);\n      }\n    }\n    _elemIsActive(elem) {\n      return elem.classList.contains(CLASS_NAME_ACTIVE);\n    }\n\n    // Try to get the inner element (usually the .nav-link)\n    _getInnerElement(elem) {\n      return elem.matches(SELECTOR_INNER_ELEM) ? elem : SelectorEngine.findOne(SELECTOR_INNER_ELEM, elem);\n    }\n\n    // Try to get the outer element (usually the .nav-item)\n    _getOuterElement(elem) {\n      return elem.closest(SELECTOR_OUTER) || elem;\n    }\n\n    // Static\n    static jQueryInterface(config) {\n      return this.each(function () {\n        const data = Tab.getOrCreateInstance(this);\n        if (typeof config !== 'string') {\n          return;\n        }\n        if (data[config] === undefined || config.startsWith('_') || config === 'constructor') {\n          throw new TypeError(`No method named \"${config}\"`);\n        }\n        data[config]();\n      });\n    }\n  }\n\n  /**\n   * Data API implementation\n   */\n\n  EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (event) {\n    if (['A', 'AREA'].includes(this.tagName)) {\n      event.preventDefault();\n    }\n    if (index_js.isDisabled(this)) {\n      return;\n    }\n    Tab.getOrCreateInstance(this).show();\n  });\n\n  /**\n   * Initialize on focus\n   */\n  EventHandler.on(window, EVENT_LOAD_DATA_API, () => {\n    for (const element of SelectorEngine.find(SELECTOR_DATA_TOGGLE_ACTIVE)) {\n      Tab.getOrCreateInstance(element);\n    }\n  });\n  /**\n   * jQuery\n   */\n\n  index_js.defineJQueryPlugin(Tab);\n\n  return Tab;\n\n}));\n//# sourceMappingURL=tab.js.map\n", "/*!\n  * Bootstrap toast.js v5.3.3 (https://getbootstrap.com/)\n  * Copyright 2011-2024 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors)\n  * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n  */\n(function (global, factory) {\n  typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('./base-component.js'), require('./dom/event-handler.js'), require('./util/component-functions.js'), require('./util/index.js')) :\n  typeof define === 'function' && define.amd ? define(['./base-component', './dom/event-handler', './util/component-functions', './util/index'], factory) :\n  (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.Toast = factory(global.BaseComponent, global.EventHandler, global.ComponentFunctions, global.Index));\n})(this, (function (BaseComponent, EventHandler, componentFunctions_js, index_js) { 'use strict';\n\n  /**\n   * --------------------------------------------------------------------------\n   * Bootstrap toast.js\n   * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n   * --------------------------------------------------------------------------\n   */\n\n\n  /**\n   * Constants\n   */\n\n  const NAME = 'toast';\n  const DATA_KEY = 'bs.toast';\n  const EVENT_KEY = `.${DATA_KEY}`;\n  const EVENT_MOUSEOVER = `mouseover${EVENT_KEY}`;\n  const EVENT_MOUSEOUT = `mouseout${EVENT_KEY}`;\n  const EVENT_FOCUSIN = `focusin${EVENT_KEY}`;\n  const EVENT_FOCUSOUT = `focusout${EVENT_KEY}`;\n  const EVENT_HIDE = `hide${EVENT_KEY}`;\n  const EVENT_HIDDEN = `hidden${EVENT_KEY}`;\n  const EVENT_SHOW = `show${EVENT_KEY}`;\n  const EVENT_SHOWN = `shown${EVENT_KEY}`;\n  const CLASS_NAME_FADE = 'fade';\n  const CLASS_NAME_HIDE = 'hide'; // @deprecated - kept here only for backwards compatibility\n  const CLASS_NAME_SHOW = 'show';\n  const CLASS_NAME_SHOWING = 'showing';\n  const DefaultType = {\n    animation: 'boolean',\n    autohide: 'boolean',\n    delay: 'number'\n  };\n  const Default = {\n    animation: true,\n    autohide: true,\n    delay: 5000\n  };\n\n  /**\n   * Class definition\n   */\n\n  class Toast extends BaseComponent {\n    constructor(element, config) {\n      super(element, config);\n      this._timeout = null;\n      this._hasMouseInteraction = false;\n      this._hasKeyboardInteraction = false;\n      this._setListeners();\n    }\n\n    // Getters\n    static get Default() {\n      return Default;\n    }\n    static get DefaultType() {\n      return DefaultType;\n    }\n    static get NAME() {\n      return NAME;\n    }\n\n    // Public\n    show() {\n      const showEvent = EventHandler.trigger(this._element, EVENT_SHOW);\n      if (showEvent.defaultPrevented) {\n        return;\n      }\n      this._clearTimeout();\n      if (this._config.animation) {\n        this._element.classList.add(CLASS_NAME_FADE);\n      }\n      const complete = () => {\n        this._element.classList.remove(CLASS_NAME_SHOWING);\n        EventHandler.trigger(this._element, EVENT_SHOWN);\n        this._maybeScheduleHide();\n      };\n      this._element.classList.remove(CLASS_NAME_HIDE); // @deprecated\n      index_js.reflow(this._element);\n      this._element.classList.add(CLASS_NAME_SHOW, CLASS_NAME_SHOWING);\n      this._queueCallback(complete, this._element, this._config.animation);\n    }\n    hide() {\n      if (!this.isShown()) {\n        return;\n      }\n      const hideEvent = EventHandler.trigger(this._element, EVENT_HIDE);\n      if (hideEvent.defaultPrevented) {\n        return;\n      }\n      const complete = () => {\n        this._element.classList.add(CLASS_NAME_HIDE); // @deprecated\n        this._element.classList.remove(CLASS_NAME_SHOWING, CLASS_NAME_SHOW);\n        EventHandler.trigger(this._element, EVENT_HIDDEN);\n      };\n      this._element.classList.add(CLASS_NAME_SHOWING);\n      this._queueCallback(complete, this._element, this._config.animation);\n    }\n    dispose() {\n      this._clearTimeout();\n      if (this.isShown()) {\n        this._element.classList.remove(CLASS_NAME_SHOW);\n      }\n      super.dispose();\n    }\n    isShown() {\n      return this._element.classList.contains(CLASS_NAME_SHOW);\n    }\n\n    // Private\n\n    _maybeScheduleHide() {\n      if (!this._config.autohide) {\n        return;\n      }\n      if (this._hasMouseInteraction || this._hasKeyboardInteraction) {\n        return;\n      }\n      this._timeout = setTimeout(() => {\n        this.hide();\n      }, this._config.delay);\n    }\n    _onInteraction(event, isInteracting) {\n      switch (event.type) {\n        case 'mouseover':\n        case 'mouseout':\n          {\n            this._hasMouseInteraction = isInteracting;\n            break;\n          }\n        case 'focusin':\n        case 'focusout':\n          {\n            this._hasKeyboardInteraction = isInteracting;\n            break;\n          }\n      }\n      if (isInteracting) {\n        this._clearTimeout();\n        return;\n      }\n      const nextElement = event.relatedTarget;\n      if (this._element === nextElement || this._element.contains(nextElement)) {\n        return;\n      }\n      this._maybeScheduleHide();\n    }\n    _setListeners() {\n      EventHandler.on(this._element, EVENT_MOUSEOVER, event => this._onInteraction(event, true));\n      EventHandler.on(this._element, EVENT_MOUSEOUT, event => this._onInteraction(event, false));\n      EventHandler.on(this._element, EVENT_FOCUSIN, event => this._onInteraction(event, true));\n      EventHandler.on(this._element, EVENT_FOCUSOUT, event => this._onInteraction(event, false));\n    }\n    _clearTimeout() {\n      clearTimeout(this._timeout);\n      this._timeout = null;\n    }\n\n    // Static\n    static jQueryInterface(config) {\n      return this.each(function () {\n        const data = Toast.getOrCreateInstance(this, config);\n        if (typeof config === 'string') {\n          if (typeof data[config] === 'undefined') {\n            throw new TypeError(`No method named \"${config}\"`);\n          }\n          data[config](this);\n        }\n      });\n    }\n  }\n\n  /**\n   * Data API implementation\n   */\n\n  componentFunctions_js.enableDismissTrigger(Toast);\n\n  /**\n   * jQuery\n   */\n\n  index_js.defineJQueryPlugin(Toast);\n\n  return Toast;\n\n}));\n//# sourceMappingURL=toast.js.map\n", "import { compensateScrollbar, getScrollingElement } from \"@web/core/utils/scrolling\";\n\n/**\n * The bootstrap library extensions and fixes should be done here to avoid\n * patching in place.\n */\n\n/**\n * Review Bootstrap Sanitization: leave it enabled by default but extend it to\n * accept more common tag names like tables and buttons, and common attributes\n * such as style or data-. If a specific tooltip or popover must accept custom\n * tags or attributes, they must be supplied through the whitelist BS\n * parameter explicitely.\n *\n * We cannot disable sanitization because bootstrap uses tooltip/popover\n * DOM attributes in an \"unsafe\" way.\n */\nconst bsSanitizeAllowList = Tooltip.Default.allowList;\n\nbsSanitizeAllowList[\"*\"].push(\"title\", \"style\", /^data-[\\w-]+/);\n\nbsSanitizeAllowList.header = [];\nbsSanitizeAllowList.main = [];\nbsSanitizeAllowList.footer = [];\n\nbsSanitizeAllowList.caption = [];\nbsSanitizeAllowList.col = [\"span\"];\nbsSanitizeAllowList.colgroup = [\"span\"];\nbsSanitizeAllowList.table = [];\nbsSanitizeAllowList.thead = [];\nbsSanitizeAllowList.tbody = [];\nbsSanitizeAllowList.tfooter = [];\nbsSanitizeAllowList.tr = [];\nbsSanitizeAllowList.th = [\"colspan\", \"rowspan\"];\nbsSanitizeAllowList.td = [\"colspan\", \"rowspan\"];\n\nbsSanitizeAllowList.address = [];\nbsSanitizeAllowList.article = [];\nbsSanitizeAllowList.aside = [];\nbsSanitizeAllowList.blockquote = [];\nbsSanitizeAllowList.section = [];\n\nbsSanitizeAllowList.button = [\"type\"];\nbsSanitizeAllowList.del = [];\n\n/* Bootstrap tooltip defaults overwrite */\nTooltip.Default.placement = \"auto\";\nTooltip.Default.fallbackPlacement = [\"bottom\", \"right\", \"left\", \"top\"];\nTooltip.Default.html = true;\nTooltip.Default.trigger = \"hover\";\nTooltip.Default.container = \"body\";\nTooltip.Default.boundary = \"window\";\nTooltip.Default.delay = { show: 1000, hide: 0 };\n\nconst bootstrapShowFunction = Tooltip.prototype.show;\nTooltip.prototype.show = function () {\n    // Overwrite bootstrap tooltip method to prevent showing 2 tooltip at the\n    // same time\n    document.querySelectorAll(\".tooltip\").forEach((el) => el.remove());\n    const errorsToIgnore = [\"Please use show on visible elements\"];\n    try {\n        return bootstrapShowFunction.call(this);\n    } catch (error) {\n        if (errorsToIgnore.includes(error.message)) {\n            return 0;\n        }\n        throw error;\n    }\n};\n\n/**\n * Bootstrap disables dynamic dropdown positioning when it is in a navbar. Here\n * we make this patch to activate this dynamic navbar's dropdown positioning\n * which is useful to avoid that the elements of the website sub-menus overflow\n * the page.\n */\nDropdown.prototype._detectNavbar = function () {\n    return false;\n};\n\n/* Bootstrap modal scrollbar compensation on non-body */\nconst bsAdjustDialogFunction = Modal.prototype._adjustDialog;\nModal.prototype._adjustDialog = function () {\n    const document = this._element.ownerDocument;\n\n    this._scrollBar.reset();\n    document.body.classList.remove(\"modal-open\");\n\n    const scrollable = getScrollingElement(document);\n    if (document.body.contains(scrollable)) {\n        compensateScrollbar(scrollable, true);\n    }\n\n    this._scrollBar.hide();\n    document.body.classList.add(\"modal-open\");\n\n    return bsAdjustDialogFunction.apply(this, arguments);\n};\n\nconst bsResetAdjustmentsFunction = Modal.prototype._resetAdjustments;\nModal.prototype._resetAdjustments = function () {\n    const document = this._element.ownerDocument;\n\n    this._scrollBar.reset();\n    document.body.classList.remove(\"modal-open\");\n\n    const scrollable = getScrollingElement(document);\n    if (document.body.contains(scrollable)) {\n        compensateScrollbar(scrollable, false);\n    }\n    return bsResetAdjustmentsFunction.apply(this, arguments);\n};\n", "/*! @license DOMPurify 3.1.7 | (c) Cure53 and other contributors | Released under the Apache license 2.0 and Mozilla Public License 2.0 | github.com/cure53/DOMPurify/blob/3.1.7/LICENSE */\n\n(function (global, factory) {\n  typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :\n  typeof define === 'function' && define.amd ? define(factory) :\n  (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.DOMPurify = factory());\n})(this, (function () { 'use strict';\n\n  const {\n    entries,\n    setPrototypeOf,\n    isFrozen,\n    getPrototypeOf,\n    getOwnPropertyDescriptor\n  } = Object;\n  let {\n    freeze,\n    seal,\n    create\n  } = Object; // eslint-disable-line import/no-mutable-exports\n  let {\n    apply,\n    construct\n  } = typeof Reflect !== 'undefined' && Reflect;\n  if (!freeze) {\n    freeze = function freeze(x) {\n      return x;\n    };\n  }\n  if (!seal) {\n    seal = function seal(x) {\n      return x;\n    };\n  }\n  if (!apply) {\n    apply = function apply(fun, thisValue, args) {\n      return fun.apply(thisValue, args);\n    };\n  }\n  if (!construct) {\n    construct = function construct(Func, args) {\n      return new Func(...args);\n    };\n  }\n  const arrayForEach = unapply(Array.prototype.forEach);\n  const arrayPop = unapply(Array.prototype.pop);\n  const arrayPush = unapply(Array.prototype.push);\n  const stringToLowerCase = unapply(String.prototype.toLowerCase);\n  const stringToString = unapply(String.prototype.toString);\n  const stringMatch = unapply(String.prototype.match);\n  const stringReplace = unapply(String.prototype.replace);\n  const stringIndexOf = unapply(String.prototype.indexOf);\n  const stringTrim = unapply(String.prototype.trim);\n  const objectHasOwnProperty = unapply(Object.prototype.hasOwnProperty);\n  const regExpTest = unapply(RegExp.prototype.test);\n  const typeErrorCreate = unconstruct(TypeError);\n\n  /**\n   * Creates a new function that calls the given function with a specified thisArg and arguments.\n   *\n   * @param {Function} func - The function to be wrapped and called.\n   * @returns {Function} A new function that calls the given function with a specified thisArg and arguments.\n   */\n  function unapply(func) {\n    return function (thisArg) {\n      for (var _len = arguments.length, args = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {\n        args[_key - 1] = arguments[_key];\n      }\n      return apply(func, thisArg, args);\n    };\n  }\n\n  /**\n   * Creates a new function that constructs an instance of the given constructor function with the provided arguments.\n   *\n   * @param {Function} func - The constructor function to be wrapped and called.\n   * @returns {Function} A new function that constructs an instance of the given constructor function with the provided arguments.\n   */\n  function unconstruct(func) {\n    return function () {\n      for (var _len2 = arguments.length, args = new Array(_len2), _key2 = 0; _key2 < _len2; _key2++) {\n        args[_key2] = arguments[_key2];\n      }\n      return construct(func, args);\n    };\n  }\n\n  /**\n   * Add properties to a lookup table\n   *\n   * @param {Object} set - The set to which elements will be added.\n   * @param {Array} array - The array containing elements to be added to the set.\n   * @param {Function} transformCaseFunc - An optional function to transform the case of each element before adding to the set.\n   * @returns {Object} The modified set with added elements.\n   */\n  function addToSet(set, array) {\n    let transformCaseFunc = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : stringToLowerCase;\n    if (setPrototypeOf) {\n      // Make 'in' and truthy checks like Boolean(set.constructor)\n      // independent of any properties defined on Object.prototype.\n      // Prevent prototype setters from intercepting set as a this value.\n      setPrototypeOf(set, null);\n    }\n    let l = array.length;\n    while (l--) {\n      let element = array[l];\n      if (typeof element === 'string') {\n        const lcElement = transformCaseFunc(element);\n        if (lcElement !== element) {\n          // Config presets (e.g. tags.js, attrs.js) are immutable.\n          if (!isFrozen(array)) {\n            array[l] = lcElement;\n          }\n          element = lcElement;\n        }\n      }\n      set[element] = true;\n    }\n    return set;\n  }\n\n  /**\n   * Clean up an array to harden against CSPP\n   *\n   * @param {Array} array - The array to be cleaned.\n   * @returns {Array} The cleaned version of the array\n   */\n  function cleanArray(array) {\n    for (let index = 0; index < array.length; index++) {\n      const isPropertyExist = objectHasOwnProperty(array, index);\n      if (!isPropertyExist) {\n        array[index] = null;\n      }\n    }\n    return array;\n  }\n\n  /**\n   * Shallow clone an object\n   *\n   * @param {Object} object - The object to be cloned.\n   * @returns {Object} A new object that copies the original.\n   */\n  function clone(object) {\n    const newObject = create(null);\n    for (const [property, value] of entries(object)) {\n      const isPropertyExist = objectHasOwnProperty(object, property);\n      if (isPropertyExist) {\n        if (Array.isArray(value)) {\n          newObject[property] = cleanArray(value);\n        } else if (value && typeof value === 'object' && value.constructor === Object) {\n          newObject[property] = clone(value);\n        } else {\n          newObject[property] = value;\n        }\n      }\n    }\n    return newObject;\n  }\n\n  /**\n   * This method automatically checks if the prop is function or getter and behaves accordingly.\n   *\n   * @param {Object} object - The object to look up the getter function in its prototype chain.\n   * @param {String} prop - The property name for which to find the getter function.\n   * @returns {Function} The getter function found in the prototype chain or a fallback function.\n   */\n  function lookupGetter(object, prop) {\n    while (object !== null) {\n      const desc = getOwnPropertyDescriptor(object, prop);\n      if (desc) {\n        if (desc.get) {\n          return unapply(desc.get);\n        }\n        if (typeof desc.value === 'function') {\n          return unapply(desc.value);\n        }\n      }\n      object = getPrototypeOf(object);\n    }\n    function fallbackValue() {\n      return null;\n    }\n    return fallbackValue;\n  }\n\n  const html$1 = freeze(['a', 'abbr', 'acronym', 'address', 'area', 'article', 'aside', 'audio', 'b', 'bdi', 'bdo', 'big', 'blink', 'blockquote', 'body', 'br', 'button', 'canvas', 'caption', 'center', 'cite', 'code', 'col', 'colgroup', 'content', 'data', 'datalist', 'dd', 'decorator', 'del', 'details', 'dfn', 'dialog', 'dir', 'div', 'dl', 'dt', 'element', 'em', 'fieldset', 'figcaption', 'figure', 'font', 'footer', 'form', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'head', 'header', 'hgroup', 'hr', 'html', 'i', 'img', 'input', 'ins', 'kbd', 'label', 'legend', 'li', 'main', 'map', 'mark', 'marquee', 'menu', 'menuitem', 'meter', 'nav', 'nobr', 'ol', 'optgroup', 'option', 'output', 'p', 'picture', 'pre', 'progress', 'q', 'rp', 'rt', 'ruby', 's', 'samp', 'section', 'select', 'shadow', 'small', 'source', 'spacer', 'span', 'strike', 'strong', 'style', 'sub', 'summary', 'sup', 'table', 'tbody', 'td', 'template', 'textarea', 'tfoot', 'th', 'thead', 'time', 'tr', 'track', 'tt', 'u', 'ul', 'var', 'video', 'wbr']);\n\n  // SVG\n  const svg$1 = freeze(['svg', 'a', 'altglyph', 'altglyphdef', 'altglyphitem', 'animatecolor', 'animatemotion', 'animatetransform', 'circle', 'clippath', 'defs', 'desc', 'ellipse', 'filter', 'font', 'g', 'glyph', 'glyphref', 'hkern', 'image', 'line', 'lineargradient', 'marker', 'mask', 'metadata', 'mpath', 'path', 'pattern', 'polygon', 'polyline', 'radialgradient', 'rect', 'stop', 'style', 'switch', 'symbol', 'text', 'textpath', 'title', 'tref', 'tspan', 'view', 'vkern']);\n  const svgFilters = freeze(['feBlend', 'feColorMatrix', 'feComponentTransfer', 'feComposite', 'feConvolveMatrix', 'feDiffuseLighting', 'feDisplacementMap', 'feDistantLight', 'feDropShadow', 'feFlood', 'feFuncA', 'feFuncB', 'feFuncG', 'feFuncR', 'feGaussianBlur', 'feImage', 'feMerge', 'feMergeNode', 'feMorphology', 'feOffset', 'fePointLight', 'feSpecularLighting', 'feSpotLight', 'feTile', 'feTurbulence']);\n\n  // List of SVG elements that are disallowed by default.\n  // We still need to know them so that we can do namespace\n  // checks properly in case one wants to add them to\n  // allow-list.\n  const svgDisallowed = freeze(['animate', 'color-profile', 'cursor', 'discard', 'font-face', 'font-face-format', 'font-face-name', 'font-face-src', 'font-face-uri', 'foreignobject', 'hatch', 'hatchpath', 'mesh', 'meshgradient', 'meshpatch', 'meshrow', 'missing-glyph', 'script', 'set', 'solidcolor', 'unknown', 'use']);\n  const mathMl$1 = freeze(['math', 'menclose', 'merror', 'mfenced', 'mfrac', 'mglyph', 'mi', 'mlabeledtr', 'mmultiscripts', 'mn', 'mo', 'mover', 'mpadded', 'mphantom', 'mroot', 'mrow', 'ms', 'mspace', 'msqrt', 'mstyle', 'msub', 'msup', 'msubsup', 'mtable', 'mtd', 'mtext', 'mtr', 'munder', 'munderover', 'mprescripts']);\n\n  // Similarly to SVG, we want to know all MathML elements,\n  // even those that we disallow by default.\n  const mathMlDisallowed = freeze(['maction', 'maligngroup', 'malignmark', 'mlongdiv', 'mscarries', 'mscarry', 'msgroup', 'mstack', 'msline', 'msrow', 'semantics', 'annotation', 'annotation-xml', 'mprescripts', 'none']);\n  const text = freeze(['#text']);\n\n  const html = freeze(['accept', 'action', 'align', 'alt', 'autocapitalize', 'autocomplete', 'autopictureinpicture', 'autoplay', 'background', 'bgcolor', 'border', 'capture', 'cellpadding', 'cellspacing', 'checked', 'cite', 'class', 'clear', 'color', 'cols', 'colspan', 'controls', 'controlslist', 'coords', 'crossorigin', 'datetime', 'decoding', 'default', 'dir', 'disabled', 'disablepictureinpicture', 'disableremoteplayback', 'download', 'draggable', 'enctype', 'enterkeyhint', 'face', 'for', 'headers', 'height', 'hidden', 'high', 'href', 'hreflang', 'id', 'inputmode', 'integrity', 'ismap', 'kind', 'label', 'lang', 'list', 'loading', 'loop', 'low', 'max', 'maxlength', 'media', 'method', 'min', 'minlength', 'multiple', 'muted', 'name', 'nonce', 'noshade', 'novalidate', 'nowrap', 'open', 'optimum', 'pattern', 'placeholder', 'playsinline', 'popover', 'popovertarget', 'popovertargetaction', 'poster', 'preload', 'pubdate', 'radiogroup', 'readonly', 'rel', 'required', 'rev', 'reversed', 'role', 'rows', 'rowspan', 'spellcheck', 'scope', 'selected', 'shape', 'size', 'sizes', 'span', 'srclang', 'start', 'src', 'srcset', 'step', 'style', 'summary', 'tabindex', 'title', 'translate', 'type', 'usemap', 'valign', 'value', 'width', 'wrap', 'xmlns', 'slot']);\n  const svg = freeze(['accent-height', 'accumulate', 'additive', 'alignment-baseline', 'amplitude', 'ascent', 'attributename', 'attributetype', 'azimuth', 'basefrequency', 'baseline-shift', 'begin', 'bias', 'by', 'class', 'clip', 'clippathunits', 'clip-path', 'clip-rule', 'color', 'color-interpolation', 'color-interpolation-filters', 'color-profile', 'color-rendering', 'cx', 'cy', 'd', 'dx', 'dy', 'diffuseconstant', 'direction', 'display', 'divisor', 'dur', 'edgemode', 'elevation', 'end', 'exponent', 'fill', 'fill-opacity', 'fill-rule', 'filter', 'filterunits', 'flood-color', 'flood-opacity', 'font-family', 'font-size', 'font-size-adjust', 'font-stretch', 'font-style', 'font-variant', 'font-weight', 'fx', 'fy', 'g1', 'g2', 'glyph-name', 'glyphref', 'gradientunits', 'gradienttransform', 'height', 'href', 'id', 'image-rendering', 'in', 'in2', 'intercept', 'k', 'k1', 'k2', 'k3', 'k4', 'kerning', 'keypoints', 'keysplines', 'keytimes', 'lang', 'lengthadjust', 'letter-spacing', 'kernelmatrix', 'kernelunitlength', 'lighting-color', 'local', 'marker-end', 'marker-mid', 'marker-start', 'markerheight', 'markerunits', 'markerwidth', 'maskcontentunits', 'maskunits', 'max', 'mask', 'media', 'method', 'mode', 'min', 'name', 'numoctaves', 'offset', 'operator', 'opacity', 'order', 'orient', 'orientation', 'origin', 'overflow', 'paint-order', 'path', 'pathlength', 'patterncontentunits', 'patterntransform', 'patternunits', 'points', 'preservealpha', 'preserveaspectratio', 'primitiveunits', 'r', 'rx', 'ry', 'radius', 'refx', 'refy', 'repeatcount', 'repeatdur', 'restart', 'result', 'rotate', 'scale', 'seed', 'shape-rendering', 'slope', 'specularconstant', 'specularexponent', 'spreadmethod', 'startoffset', 'stddeviation', 'stitchtiles', 'stop-color', 'stop-opacity', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', 'stroke', 'stroke-width', 'style', 'surfacescale', 'systemlanguage', 'tabindex', 'tablevalues', 'targetx', 'targety', 'transform', 'transform-origin', 'text-anchor', 'text-decoration', 'text-rendering', 'textlength', 'type', 'u1', 'u2', 'unicode', 'values', 'viewbox', 'visibility', 'version', 'vert-adv-y', 'vert-origin-x', 'vert-origin-y', 'width', 'word-spacing', 'wrap', 'writing-mode', 'xchannelselector', 'ychannelselector', 'x', 'x1', 'x2', 'xmlns', 'y', 'y1', 'y2', 'z', 'zoomandpan']);\n  const mathMl = freeze(['accent', 'accentunder', 'align', 'bevelled', 'close', 'columnsalign', 'columnlines', 'columnspan', 'denomalign', 'depth', 'dir', 'display', 'displaystyle', 'encoding', 'fence', 'frame', 'height', 'href', 'id', 'largeop', 'length', 'linethickness', 'lspace', 'lquote', 'mathbackground', 'mathcolor', 'mathsize', 'mathvariant', 'maxsize', 'minsize', 'movablelimits', 'notation', 'numalign', 'open', 'rowalign', 'rowlines', 'rowspacing', 'rowspan', 'rspace', 'rquote', 'scriptlevel', 'scriptminsize', 'scriptsizemultiplier', 'selection', 'separator', 'separators', 'stretchy', 'subscriptshift', 'supscriptshift', 'symmetric', 'voffset', 'width', 'xmlns']);\n  const xml = freeze(['xlink:href', 'xml:id', 'xlink:title', 'xml:space', 'xmlns:xlink']);\n\n  // eslint-disable-next-line unicorn/better-regex\n  const MUSTACHE_EXPR = seal(/\\{\\{[\\w\\W]*|[\\w\\W]*\\}\\}/gm); // Specify template detection regex for SAFE_FOR_TEMPLATES mode\n  const ERB_EXPR = seal(/<%[\\w\\W]*|[\\w\\W]*%>/gm);\n  const TMPLIT_EXPR = seal(/\\${[\\w\\W]*}/gm);\n  const DATA_ATTR = seal(/^data-[\\-\\w.\\u00B7-\\uFFFF]/); // eslint-disable-line no-useless-escape\n  const ARIA_ATTR = seal(/^aria-[\\-\\w]+$/); // eslint-disable-line no-useless-escape\n  const IS_ALLOWED_URI = seal(/^(?:(?:(?:f|ht)tps?|mailto|tel|callto|sms|cid|xmpp):|[^a-z]|[a-z+.\\-]+(?:[^a-z+.\\-:]|$))/i // eslint-disable-line no-useless-escape\n  );\n  const IS_SCRIPT_OR_DATA = seal(/^(?:\\w+script|data):/i);\n  const ATTR_WHITESPACE = seal(/[\\u0000-\\u0020\\u00A0\\u1680\\u180E\\u2000-\\u2029\\u205F\\u3000]/g // eslint-disable-line no-control-regex\n  );\n  const DOCTYPE_NAME = seal(/^html$/i);\n  const CUSTOM_ELEMENT = seal(/^[a-z][.\\w]*(-[.\\w]+)+$/i);\n\n  var EXPRESSIONS = /*#__PURE__*/Object.freeze({\n    __proto__: null,\n    MUSTACHE_EXPR: MUSTACHE_EXPR,\n    ERB_EXPR: ERB_EXPR,\n    TMPLIT_EXPR: TMPLIT_EXPR,\n    DATA_ATTR: DATA_ATTR,\n    ARIA_ATTR: ARIA_ATTR,\n    IS_ALLOWED_URI: IS_ALLOWED_URI,\n    IS_SCRIPT_OR_DATA: IS_SCRIPT_OR_DATA,\n    ATTR_WHITESPACE: ATTR_WHITESPACE,\n    DOCTYPE_NAME: DOCTYPE_NAME,\n    CUSTOM_ELEMENT: CUSTOM_ELEMENT\n  });\n\n  // https://developer.mozilla.org/en-US/docs/Web/API/Node/nodeType\n  const NODE_TYPE = {\n    element: 1,\n    attribute: 2,\n    text: 3,\n    cdataSection: 4,\n    entityReference: 5,\n    // Deprecated\n    entityNode: 6,\n    // Deprecated\n    progressingInstruction: 7,\n    comment: 8,\n    document: 9,\n    documentType: 10,\n    documentFragment: 11,\n    notation: 12 // Deprecated\n  };\n  const getGlobal = function getGlobal() {\n    return typeof window === 'undefined' ? null : window;\n  };\n\n  /**\n   * Creates a no-op policy for internal use only.\n   * Don't export this function outside this module!\n   * @param {TrustedTypePolicyFactory} trustedTypes The policy factory.\n   * @param {HTMLScriptElement} purifyHostElement The Script element used to load DOMPurify (to determine policy name suffix).\n   * @return {TrustedTypePolicy} The policy created (or null, if Trusted Types\n   * are not supported or creating the policy failed).\n   */\n  const _createTrustedTypesPolicy = function _createTrustedTypesPolicy(trustedTypes, purifyHostElement) {\n    if (typeof trustedTypes !== 'object' || typeof trustedTypes.createPolicy !== 'function') {\n      return null;\n    }\n\n    // Allow the callers to control the unique policy name\n    // by adding a data-tt-policy-suffix to the script element with the DOMPurify.\n    // Policy creation with duplicate names throws in Trusted Types.\n    let suffix = null;\n    const ATTR_NAME = 'data-tt-policy-suffix';\n    if (purifyHostElement && purifyHostElement.hasAttribute(ATTR_NAME)) {\n      suffix = purifyHostElement.getAttribute(ATTR_NAME);\n    }\n    const policyName = 'dompurify' + (suffix ? '#' + suffix : '');\n    try {\n      return trustedTypes.createPolicy(policyName, {\n        createHTML(html) {\n          return html;\n        },\n        createScriptURL(scriptUrl) {\n          return scriptUrl;\n        }\n      });\n    } catch (_) {\n      // Policy creation failed (most likely another DOMPurify script has\n      // already run). Skip creating the policy, as this will only cause errors\n      // if TT are enforced.\n      console.warn('TrustedTypes policy ' + policyName + ' could not be created.');\n      return null;\n    }\n  };\n  function createDOMPurify() {\n    let window = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : getGlobal();\n    const DOMPurify = root => createDOMPurify(root);\n\n    /**\n     * Version label, exposed for easier checks\n     * if DOMPurify is up to date or not\n     */\n    DOMPurify.version = '3.1.7';\n\n    /**\n     * Array of elements that DOMPurify removed during sanitation.\n     * Empty if nothing was removed.\n     */\n    DOMPurify.removed = [];\n    if (!window || !window.document || window.document.nodeType !== NODE_TYPE.document) {\n      // Not running in a browser, provide a factory function\n      // so that you can pass your own Window\n      DOMPurify.isSupported = false;\n      return DOMPurify;\n    }\n    let {\n      document\n    } = window;\n    const originalDocument = document;\n    const currentScript = originalDocument.currentScript;\n    const {\n      DocumentFragment,\n      HTMLTemplateElement,\n      Node,\n      Element,\n      NodeFilter,\n      NamedNodeMap = window.NamedNodeMap || window.MozNamedAttrMap,\n      HTMLFormElement,\n      DOMParser,\n      trustedTypes\n    } = window;\n    const ElementPrototype = Element.prototype;\n    const cloneNode = lookupGetter(ElementPrototype, 'cloneNode');\n    const remove = lookupGetter(ElementPrototype, 'remove');\n    const getNextSibling = lookupGetter(ElementPrototype, 'nextSibling');\n    const getChildNodes = lookupGetter(ElementPrototype, 'childNodes');\n    const getParentNode = lookupGetter(ElementPrototype, 'parentNode');\n\n    // As per issue #47, the web-components registry is inherited by a\n    // new document created via createHTMLDocument. As per the spec\n    // (http://w3c.github.io/webcomponents/spec/custom/#creating-and-passing-registries)\n    // a new empty registry is used when creating a template contents owner\n    // document, so we use that as our parent document to ensure nothing\n    // is inherited.\n    if (typeof HTMLTemplateElement === 'function') {\n      const template = document.createElement('template');\n      if (template.content && template.content.ownerDocument) {\n        document = template.content.ownerDocument;\n      }\n    }\n    let trustedTypesPolicy;\n    let emptyHTML = '';\n    const {\n      implementation,\n      createNodeIterator,\n      createDocumentFragment,\n      getElementsByTagName\n    } = document;\n    const {\n      importNode\n    } = originalDocument;\n    let hooks = {};\n\n    /**\n     * Expose whether this browser supports running the full DOMPurify.\n     */\n    DOMPurify.isSupported = typeof entries === 'function' && typeof getParentNode === 'function' && implementation && implementation.createHTMLDocument !== undefined;\n    const {\n      MUSTACHE_EXPR,\n      ERB_EXPR,\n      TMPLIT_EXPR,\n      DATA_ATTR,\n      ARIA_ATTR,\n      IS_SCRIPT_OR_DATA,\n      ATTR_WHITESPACE,\n      CUSTOM_ELEMENT\n    } = EXPRESSIONS;\n    let {\n      IS_ALLOWED_URI: IS_ALLOWED_URI$1\n    } = EXPRESSIONS;\n\n    /**\n     * We consider the elements and attributes below to be safe. Ideally\n     * don't add any new ones but feel free to remove unwanted ones.\n     */\n\n    /* allowed element names */\n    let ALLOWED_TAGS = null;\n    const DEFAULT_ALLOWED_TAGS = addToSet({}, [...html$1, ...svg$1, ...svgFilters, ...mathMl$1, ...text]);\n\n    /* Allowed attribute names */\n    let ALLOWED_ATTR = null;\n    const DEFAULT_ALLOWED_ATTR = addToSet({}, [...html, ...svg, ...mathMl, ...xml]);\n\n    /*\n     * Configure how DOMPUrify should handle custom elements and their attributes as well as customized built-in elements.\n     * @property {RegExp|Function|null} tagNameCheck one of [null, regexPattern, predicate]. Default: `null` (disallow any custom elements)\n     * @property {RegExp|Function|null} attributeNameCheck one of [null, regexPattern, predicate]. Default: `null` (disallow any attributes not on the allow list)\n     * @property {boolean} allowCustomizedBuiltInElements allow custom elements derived from built-ins if they pass CUSTOM_ELEMENT_HANDLING.tagNameCheck. Default: `false`.\n     */\n    let CUSTOM_ELEMENT_HANDLING = Object.seal(create(null, {\n      tagNameCheck: {\n        writable: true,\n        configurable: false,\n        enumerable: true,\n        value: null\n      },\n      attributeNameCheck: {\n        writable: true,\n        configurable: false,\n        enumerable: true,\n        value: null\n      },\n      allowCustomizedBuiltInElements: {\n        writable: true,\n        configurable: false,\n        enumerable: true,\n        value: false\n      }\n    }));\n\n    /* Explicitly forbidden tags (overrides ALLOWED_TAGS/ADD_TAGS) */\n    let FORBID_TAGS = null;\n\n    /* Explicitly forbidden attributes (overrides ALLOWED_ATTR/ADD_ATTR) */\n    let FORBID_ATTR = null;\n\n    /* Decide if ARIA attributes are okay */\n    let ALLOW_ARIA_ATTR = true;\n\n    /* Decide if custom data attributes are okay */\n    let ALLOW_DATA_ATTR = true;\n\n    /* Decide if unknown protocols are okay */\n    let ALLOW_UNKNOWN_PROTOCOLS = false;\n\n    /* Decide if self-closing tags in attributes are allowed.\n     * Usually removed due to a mXSS issue in jQuery 3.0 */\n    let ALLOW_SELF_CLOSE_IN_ATTR = true;\n\n    /* Output should be safe for common template engines.\n     * This means, DOMPurify removes data attributes, mustaches and ERB\n     */\n    let SAFE_FOR_TEMPLATES = false;\n\n    /* Output should be safe even for XML used within HTML and alike.\n     * This means, DOMPurify removes comments when containing risky content.\n     */\n    let SAFE_FOR_XML = true;\n\n    /* Decide if document with <html>... should be returned */\n    let WHOLE_DOCUMENT = false;\n\n    /* Track whether config is already set on this instance of DOMPurify. */\n    let SET_CONFIG = false;\n\n    /* Decide if all elements (e.g. style, script) must be children of\n     * document.body. By default, browsers might move them to document.head */\n    let FORCE_BODY = false;\n\n    /* Decide if a DOM `HTMLBodyElement` should be returned, instead of a html\n     * string (or a TrustedHTML object if Trusted Types are supported).\n     * If `WHOLE_DOCUMENT` is enabled a `HTMLHtmlElement` will be returned instead\n     */\n    let RETURN_DOM = false;\n\n    /* Decide if a DOM `DocumentFragment` should be returned, instead of a html\n     * string  (or a TrustedHTML object if Trusted Types are supported) */\n    let RETURN_DOM_FRAGMENT = false;\n\n    /* Try to return a Trusted Type object instead of a string, return a string in\n     * case Trusted Types are not supported  */\n    let RETURN_TRUSTED_TYPE = false;\n\n    /* Output should be free from DOM clobbering attacks?\n     * This sanitizes markups named with colliding, clobberable built-in DOM APIs.\n     */\n    let SANITIZE_DOM = true;\n\n    /* Achieve full DOM Clobbering protection by isolating the namespace of named\n     * properties and JS variables, mitigating attacks that abuse the HTML/DOM spec rules.\n     *\n     * HTML/DOM spec rules that enable DOM Clobbering:\n     *   - Named Access on Window (\u00a77.3.3)\n     *   - DOM Tree Accessors (\u00a73.1.5)\n     *   - Form Element Parent-Child Relations (\u00a74.10.3)\n     *   - Iframe srcdoc / Nested WindowProxies (\u00a74.8.5)\n     *   - HTMLCollection (\u00a74.2.10.2)\n     *\n     * Namespace isolation is implemented by prefixing `id` and `name` attributes\n     * with a constant string, i.e., `user-content-`\n     */\n    let SANITIZE_NAMED_PROPS = false;\n    const SANITIZE_NAMED_PROPS_PREFIX = 'user-content-';\n\n    /* Keep element content when removing element? */\n    let KEEP_CONTENT = true;\n\n    /* If a `Node` is passed to sanitize(), then performs sanitization in-place instead\n     * of importing it into a new Document and returning a sanitized copy */\n    let IN_PLACE = false;\n\n    /* Allow usage of profiles like html, svg and mathMl */\n    let USE_PROFILES = {};\n\n    /* Tags to ignore content of when KEEP_CONTENT is true */\n    let FORBID_CONTENTS = null;\n    const DEFAULT_FORBID_CONTENTS = addToSet({}, ['annotation-xml', 'audio', 'colgroup', 'desc', 'foreignobject', 'head', 'iframe', 'math', 'mi', 'mn', 'mo', 'ms', 'mtext', 'noembed', 'noframes', 'noscript', 'plaintext', 'script', 'style', 'svg', 'template', 'thead', 'title', 'video', 'xmp']);\n\n    /* Tags that are safe for data: URIs */\n    let DATA_URI_TAGS = null;\n    const DEFAULT_DATA_URI_TAGS = addToSet({}, ['audio', 'video', 'img', 'source', 'image', 'track']);\n\n    /* Attributes safe for values like \"javascript:\" */\n    let URI_SAFE_ATTRIBUTES = null;\n    const DEFAULT_URI_SAFE_ATTRIBUTES = addToSet({}, ['alt', 'class', 'for', 'id', 'label', 'name', 'pattern', 'placeholder', 'role', 'summary', 'title', 'value', 'style', 'xmlns']);\n    const MATHML_NAMESPACE = 'http://www.w3.org/1998/Math/MathML';\n    const SVG_NAMESPACE = 'http://www.w3.org/2000/svg';\n    const HTML_NAMESPACE = 'http://www.w3.org/1999/xhtml';\n    /* Document namespace */\n    let NAMESPACE = HTML_NAMESPACE;\n    let IS_EMPTY_INPUT = false;\n\n    /* Allowed XHTML+XML namespaces */\n    let ALLOWED_NAMESPACES = null;\n    const DEFAULT_ALLOWED_NAMESPACES = addToSet({}, [MATHML_NAMESPACE, SVG_NAMESPACE, HTML_NAMESPACE], stringToString);\n\n    /* Parsing of strict XHTML documents */\n    let PARSER_MEDIA_TYPE = null;\n    const SUPPORTED_PARSER_MEDIA_TYPES = ['application/xhtml+xml', 'text/html'];\n    const DEFAULT_PARSER_MEDIA_TYPE = 'text/html';\n    let transformCaseFunc = null;\n\n    /* Keep a reference to config to pass to hooks */\n    let CONFIG = null;\n\n    /* Ideally, do not touch anything below this line */\n    /* ______________________________________________ */\n\n    const formElement = document.createElement('form');\n    const isRegexOrFunction = function isRegexOrFunction(testValue) {\n      return testValue instanceof RegExp || testValue instanceof Function;\n    };\n\n    /**\n     * _parseConfig\n     *\n     * @param  {Object} cfg optional config literal\n     */\n    // eslint-disable-next-line complexity\n    const _parseConfig = function _parseConfig() {\n      let cfg = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};\n      if (CONFIG && CONFIG === cfg) {\n        return;\n      }\n\n      /* Shield configuration object from tampering */\n      if (!cfg || typeof cfg !== 'object') {\n        cfg = {};\n      }\n\n      /* Shield configuration object from prototype pollution */\n      cfg = clone(cfg);\n      PARSER_MEDIA_TYPE =\n      // eslint-disable-next-line unicorn/prefer-includes\n      SUPPORTED_PARSER_MEDIA_TYPES.indexOf(cfg.PARSER_MEDIA_TYPE) === -1 ? DEFAULT_PARSER_MEDIA_TYPE : cfg.PARSER_MEDIA_TYPE;\n\n      // HTML tags and attributes are not case-sensitive, converting to lowercase. Keeping XHTML as is.\n      transformCaseFunc = PARSER_MEDIA_TYPE === 'application/xhtml+xml' ? stringToString : stringToLowerCase;\n\n      /* Set configuration parameters */\n      ALLOWED_TAGS = objectHasOwnProperty(cfg, 'ALLOWED_TAGS') ? addToSet({}, cfg.ALLOWED_TAGS, transformCaseFunc) : DEFAULT_ALLOWED_TAGS;\n      ALLOWED_ATTR = objectHasOwnProperty(cfg, 'ALLOWED_ATTR') ? addToSet({}, cfg.ALLOWED_ATTR, transformCaseFunc) : DEFAULT_ALLOWED_ATTR;\n      ALLOWED_NAMESPACES = objectHasOwnProperty(cfg, 'ALLOWED_NAMESPACES') ? addToSet({}, cfg.ALLOWED_NAMESPACES, stringToString) : DEFAULT_ALLOWED_NAMESPACES;\n      URI_SAFE_ATTRIBUTES = objectHasOwnProperty(cfg, 'ADD_URI_SAFE_ATTR') ? addToSet(clone(DEFAULT_URI_SAFE_ATTRIBUTES),\n      // eslint-disable-line indent\n      cfg.ADD_URI_SAFE_ATTR,\n      // eslint-disable-line indent\n      transformCaseFunc // eslint-disable-line indent\n      ) // eslint-disable-line indent\n      : DEFAULT_URI_SAFE_ATTRIBUTES;\n      DATA_URI_TAGS = objectHasOwnProperty(cfg, 'ADD_DATA_URI_TAGS') ? addToSet(clone(DEFAULT_DATA_URI_TAGS),\n      // eslint-disable-line indent\n      cfg.ADD_DATA_URI_TAGS,\n      // eslint-disable-line indent\n      transformCaseFunc // eslint-disable-line indent\n      ) // eslint-disable-line indent\n      : DEFAULT_DATA_URI_TAGS;\n      FORBID_CONTENTS = objectHasOwnProperty(cfg, 'FORBID_CONTENTS') ? addToSet({}, cfg.FORBID_CONTENTS, transformCaseFunc) : DEFAULT_FORBID_CONTENTS;\n      FORBID_TAGS = objectHasOwnProperty(cfg, 'FORBID_TAGS') ? addToSet({}, cfg.FORBID_TAGS, transformCaseFunc) : {};\n      FORBID_ATTR = objectHasOwnProperty(cfg, 'FORBID_ATTR') ? addToSet({}, cfg.FORBID_ATTR, transformCaseFunc) : {};\n      USE_PROFILES = objectHasOwnProperty(cfg, 'USE_PROFILES') ? cfg.USE_PROFILES : false;\n      ALLOW_ARIA_ATTR = cfg.ALLOW_ARIA_ATTR !== false; // Default true\n      ALLOW_DATA_ATTR = cfg.ALLOW_DATA_ATTR !== false; // Default true\n      ALLOW_UNKNOWN_PROTOCOLS = cfg.ALLOW_UNKNOWN_PROTOCOLS || false; // Default false\n      ALLOW_SELF_CLOSE_IN_ATTR = cfg.ALLOW_SELF_CLOSE_IN_ATTR !== false; // Default true\n      SAFE_FOR_TEMPLATES = cfg.SAFE_FOR_TEMPLATES || false; // Default false\n      SAFE_FOR_XML = cfg.SAFE_FOR_XML !== false; // Default true\n      WHOLE_DOCUMENT = cfg.WHOLE_DOCUMENT || false; // Default false\n      RETURN_DOM = cfg.RETURN_DOM || false; // Default false\n      RETURN_DOM_FRAGMENT = cfg.RETURN_DOM_FRAGMENT || false; // Default false\n      RETURN_TRUSTED_TYPE = cfg.RETURN_TRUSTED_TYPE || false; // Default false\n      FORCE_BODY = cfg.FORCE_BODY || false; // Default false\n      SANITIZE_DOM = cfg.SANITIZE_DOM !== false; // Default true\n      SANITIZE_NAMED_PROPS = cfg.SANITIZE_NAMED_PROPS || false; // Default false\n      KEEP_CONTENT = cfg.KEEP_CONTENT !== false; // Default true\n      IN_PLACE = cfg.IN_PLACE || false; // Default false\n      IS_ALLOWED_URI$1 = cfg.ALLOWED_URI_REGEXP || IS_ALLOWED_URI;\n      NAMESPACE = cfg.NAMESPACE || HTML_NAMESPACE;\n      CUSTOM_ELEMENT_HANDLING = cfg.CUSTOM_ELEMENT_HANDLING || {};\n      if (cfg.CUSTOM_ELEMENT_HANDLING && isRegexOrFunction(cfg.CUSTOM_ELEMENT_HANDLING.tagNameCheck)) {\n        CUSTOM_ELEMENT_HANDLING.tagNameCheck = cfg.CUSTOM_ELEMENT_HANDLING.tagNameCheck;\n      }\n      if (cfg.CUSTOM_ELEMENT_HANDLING && isRegexOrFunction(cfg.CUSTOM_ELEMENT_HANDLING.attributeNameCheck)) {\n        CUSTOM_ELEMENT_HANDLING.attributeNameCheck = cfg.CUSTOM_ELEMENT_HANDLING.attributeNameCheck;\n      }\n      if (cfg.CUSTOM_ELEMENT_HANDLING && typeof cfg.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements === 'boolean') {\n        CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements = cfg.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements;\n      }\n      if (SAFE_FOR_TEMPLATES) {\n        ALLOW_DATA_ATTR = false;\n      }\n      if (RETURN_DOM_FRAGMENT) {\n        RETURN_DOM = true;\n      }\n\n      /* Parse profile info */\n      if (USE_PROFILES) {\n        ALLOWED_TAGS = addToSet({}, text);\n        ALLOWED_ATTR = [];\n        if (USE_PROFILES.html === true) {\n          addToSet(ALLOWED_TAGS, html$1);\n          addToSet(ALLOWED_ATTR, html);\n        }\n        if (USE_PROFILES.svg === true) {\n          addToSet(ALLOWED_TAGS, svg$1);\n          addToSet(ALLOWED_ATTR, svg);\n          addToSet(ALLOWED_ATTR, xml);\n        }\n        if (USE_PROFILES.svgFilters === true) {\n          addToSet(ALLOWED_TAGS, svgFilters);\n          addToSet(ALLOWED_ATTR, svg);\n          addToSet(ALLOWED_ATTR, xml);\n        }\n        if (USE_PROFILES.mathMl === true) {\n          addToSet(ALLOWED_TAGS, mathMl$1);\n          addToSet(ALLOWED_ATTR, mathMl);\n          addToSet(ALLOWED_ATTR, xml);\n        }\n      }\n\n      /* Merge configuration parameters */\n      if (cfg.ADD_TAGS) {\n        if (ALLOWED_TAGS === DEFAULT_ALLOWED_TAGS) {\n          ALLOWED_TAGS = clone(ALLOWED_TAGS);\n        }\n        addToSet(ALLOWED_TAGS, cfg.ADD_TAGS, transformCaseFunc);\n      }\n      if (cfg.ADD_ATTR) {\n        if (ALLOWED_ATTR === DEFAULT_ALLOWED_ATTR) {\n          ALLOWED_ATTR = clone(ALLOWED_ATTR);\n        }\n        addToSet(ALLOWED_ATTR, cfg.ADD_ATTR, transformCaseFunc);\n      }\n      if (cfg.ADD_URI_SAFE_ATTR) {\n        addToSet(URI_SAFE_ATTRIBUTES, cfg.ADD_URI_SAFE_ATTR, transformCaseFunc);\n      }\n      if (cfg.FORBID_CONTENTS) {\n        if (FORBID_CONTENTS === DEFAULT_FORBID_CONTENTS) {\n          FORBID_CONTENTS = clone(FORBID_CONTENTS);\n        }\n        addToSet(FORBID_CONTENTS, cfg.FORBID_CONTENTS, transformCaseFunc);\n      }\n\n      /* Add #text in case KEEP_CONTENT is set to true */\n      if (KEEP_CONTENT) {\n        ALLOWED_TAGS['#text'] = true;\n      }\n\n      /* Add html, head and body to ALLOWED_TAGS in case WHOLE_DOCUMENT is true */\n      if (WHOLE_DOCUMENT) {\n        addToSet(ALLOWED_TAGS, ['html', 'head', 'body']);\n      }\n\n      /* Add tbody to ALLOWED_TAGS in case tables are permitted, see #286, #365 */\n      if (ALLOWED_TAGS.table) {\n        addToSet(ALLOWED_TAGS, ['tbody']);\n        delete FORBID_TAGS.tbody;\n      }\n      if (cfg.TRUSTED_TYPES_POLICY) {\n        if (typeof cfg.TRUSTED_TYPES_POLICY.createHTML !== 'function') {\n          throw typeErrorCreate('TRUSTED_TYPES_POLICY configuration option must provide a \"createHTML\" hook.');\n        }\n        if (typeof cfg.TRUSTED_TYPES_POLICY.createScriptURL !== 'function') {\n          throw typeErrorCreate('TRUSTED_TYPES_POLICY configuration option must provide a \"createScriptURL\" hook.');\n        }\n\n        // Overwrite existing TrustedTypes policy.\n        trustedTypesPolicy = cfg.TRUSTED_TYPES_POLICY;\n\n        // Sign local variables required by `sanitize`.\n        emptyHTML = trustedTypesPolicy.createHTML('');\n      } else {\n        // Uninitialized policy, attempt to initialize the internal dompurify policy.\n        if (trustedTypesPolicy === undefined) {\n          trustedTypesPolicy = _createTrustedTypesPolicy(trustedTypes, currentScript);\n        }\n\n        // If creating the internal policy succeeded sign internal variables.\n        if (trustedTypesPolicy !== null && typeof emptyHTML === 'string') {\n          emptyHTML = trustedTypesPolicy.createHTML('');\n        }\n      }\n\n      // Prevent further manipulation of configuration.\n      // Not available in IE8, Safari 5, etc.\n      if (freeze) {\n        freeze(cfg);\n      }\n      CONFIG = cfg;\n    };\n    const MATHML_TEXT_INTEGRATION_POINTS = addToSet({}, ['mi', 'mo', 'mn', 'ms', 'mtext']);\n    const HTML_INTEGRATION_POINTS = addToSet({}, ['annotation-xml']);\n\n    // Certain elements are allowed in both SVG and HTML\n    // namespace. We need to specify them explicitly\n    // so that they don't get erroneously deleted from\n    // HTML namespace.\n    const COMMON_SVG_AND_HTML_ELEMENTS = addToSet({}, ['title', 'style', 'font', 'a', 'script']);\n\n    /* Keep track of all possible SVG and MathML tags\n     * so that we can perform the namespace checks\n     * correctly. */\n    const ALL_SVG_TAGS = addToSet({}, [...svg$1, ...svgFilters, ...svgDisallowed]);\n    const ALL_MATHML_TAGS = addToSet({}, [...mathMl$1, ...mathMlDisallowed]);\n\n    /**\n     * @param  {Element} element a DOM element whose namespace is being checked\n     * @returns {boolean} Return false if the element has a\n     *  namespace that a spec-compliant parser would never\n     *  return. Return true otherwise.\n     */\n    const _checkValidNamespace = function _checkValidNamespace(element) {\n      let parent = getParentNode(element);\n\n      // In JSDOM, if we're inside shadow DOM, then parentNode\n      // can be null. We just simulate parent in this case.\n      if (!parent || !parent.tagName) {\n        parent = {\n          namespaceURI: NAMESPACE,\n          tagName: 'template'\n        };\n      }\n      const tagName = stringToLowerCase(element.tagName);\n      const parentTagName = stringToLowerCase(parent.tagName);\n      if (!ALLOWED_NAMESPACES[element.namespaceURI]) {\n        return false;\n      }\n      if (element.namespaceURI === SVG_NAMESPACE) {\n        // The only way to switch from HTML namespace to SVG\n        // is via <svg>. If it happens via any other tag, then\n        // it should be killed.\n        if (parent.namespaceURI === HTML_NAMESPACE) {\n          return tagName === 'svg';\n        }\n\n        // The only way to switch from MathML to SVG is via`\n        // svg if parent is either <annotation-xml> or MathML\n        // text integration points.\n        if (parent.namespaceURI === MATHML_NAMESPACE) {\n          return tagName === 'svg' && (parentTagName === 'annotation-xml' || MATHML_TEXT_INTEGRATION_POINTS[parentTagName]);\n        }\n\n        // We only allow elements that are defined in SVG\n        // spec. All others are disallowed in SVG namespace.\n        return Boolean(ALL_SVG_TAGS[tagName]);\n      }\n      if (element.namespaceURI === MATHML_NAMESPACE) {\n        // The only way to switch from HTML namespace to MathML\n        // is via <math>. If it happens via any other tag, then\n        // it should be killed.\n        if (parent.namespaceURI === HTML_NAMESPACE) {\n          return tagName === 'math';\n        }\n\n        // The only way to switch from SVG to MathML is via\n        // <math> and HTML integration points\n        if (parent.namespaceURI === SVG_NAMESPACE) {\n          return tagName === 'math' && HTML_INTEGRATION_POINTS[parentTagName];\n        }\n\n        // We only allow elements that are defined in MathML\n        // spec. All others are disallowed in MathML namespace.\n        return Boolean(ALL_MATHML_TAGS[tagName]);\n      }\n      if (element.namespaceURI === HTML_NAMESPACE) {\n        // The only way to switch from SVG to HTML is via\n        // HTML integration points, and from MathML to HTML\n        // is via MathML text integration points\n        if (parent.namespaceURI === SVG_NAMESPACE && !HTML_INTEGRATION_POINTS[parentTagName]) {\n          return false;\n        }\n        if (parent.namespaceURI === MATHML_NAMESPACE && !MATHML_TEXT_INTEGRATION_POINTS[parentTagName]) {\n          return false;\n        }\n\n        // We disallow tags that are specific for MathML\n        // or SVG and should never appear in HTML namespace\n        return !ALL_MATHML_TAGS[tagName] && (COMMON_SVG_AND_HTML_ELEMENTS[tagName] || !ALL_SVG_TAGS[tagName]);\n      }\n\n      // For XHTML and XML documents that support custom namespaces\n      if (PARSER_MEDIA_TYPE === 'application/xhtml+xml' && ALLOWED_NAMESPACES[element.namespaceURI]) {\n        return true;\n      }\n\n      // The code should never reach this place (this means\n      // that the element somehow got namespace that is not\n      // HTML, SVG, MathML or allowed via ALLOWED_NAMESPACES).\n      // Return false just in case.\n      return false;\n    };\n\n    /**\n     * _forceRemove\n     *\n     * @param  {Node} node a DOM node\n     */\n    const _forceRemove = function _forceRemove(node) {\n      arrayPush(DOMPurify.removed, {\n        element: node\n      });\n      try {\n        // eslint-disable-next-line unicorn/prefer-dom-node-remove\n        getParentNode(node).removeChild(node);\n      } catch (_) {\n        remove(node);\n      }\n    };\n\n    /**\n     * _removeAttribute\n     *\n     * @param  {String} name an Attribute name\n     * @param  {Node} node a DOM node\n     */\n    const _removeAttribute = function _removeAttribute(name, node) {\n      try {\n        arrayPush(DOMPurify.removed, {\n          attribute: node.getAttributeNode(name),\n          from: node\n        });\n      } catch (_) {\n        arrayPush(DOMPurify.removed, {\n          attribute: null,\n          from: node\n        });\n      }\n      node.removeAttribute(name);\n\n      // We void attribute values for unremovable \"is\"\" attributes\n      if (name === 'is' && !ALLOWED_ATTR[name]) {\n        if (RETURN_DOM || RETURN_DOM_FRAGMENT) {\n          try {\n            _forceRemove(node);\n          } catch (_) {}\n        } else {\n          try {\n            node.setAttribute(name, '');\n          } catch (_) {}\n        }\n      }\n    };\n\n    /**\n     * _initDocument\n     *\n     * @param  {String} dirty a string of dirty markup\n     * @return {Document} a DOM, filled with the dirty markup\n     */\n    const _initDocument = function _initDocument(dirty) {\n      /* Create a HTML document */\n      let doc = null;\n      let leadingWhitespace = null;\n      if (FORCE_BODY) {\n        dirty = '<remove></remove>' + dirty;\n      } else {\n        /* If FORCE_BODY isn't used, leading whitespace needs to be preserved manually */\n        const matches = stringMatch(dirty, /^[\\r\\n\\t ]+/);\n        leadingWhitespace = matches && matches[0];\n      }\n      if (PARSER_MEDIA_TYPE === 'application/xhtml+xml' && NAMESPACE === HTML_NAMESPACE) {\n        // Root of XHTML doc must contain xmlns declaration (see https://www.w3.org/TR/xhtml1/normative.html#strict)\n        dirty = '<html xmlns=\"http://www.w3.org/1999/xhtml\"><head></head><body>' + dirty + '</body></html>';\n      }\n      const dirtyPayload = trustedTypesPolicy ? trustedTypesPolicy.createHTML(dirty) : dirty;\n      /*\n       * Use the DOMParser API by default, fallback later if needs be\n       * DOMParser not work for svg when has multiple root element.\n       */\n      if (NAMESPACE === HTML_NAMESPACE) {\n        try {\n          doc = new DOMParser().parseFromString(dirtyPayload, PARSER_MEDIA_TYPE);\n        } catch (_) {}\n      }\n\n      /* Use createHTMLDocument in case DOMParser is not available */\n      if (!doc || !doc.documentElement) {\n        doc = implementation.createDocument(NAMESPACE, 'template', null);\n        try {\n          doc.documentElement.innerHTML = IS_EMPTY_INPUT ? emptyHTML : dirtyPayload;\n        } catch (_) {\n          // Syntax error if dirtyPayload is invalid xml\n        }\n      }\n      const body = doc.body || doc.documentElement;\n      if (dirty && leadingWhitespace) {\n        body.insertBefore(document.createTextNode(leadingWhitespace), body.childNodes[0] || null);\n      }\n\n      /* Work on whole document or just its body */\n      if (NAMESPACE === HTML_NAMESPACE) {\n        return getElementsByTagName.call(doc, WHOLE_DOCUMENT ? 'html' : 'body')[0];\n      }\n      return WHOLE_DOCUMENT ? doc.documentElement : body;\n    };\n\n    /**\n     * Creates a NodeIterator object that you can use to traverse filtered lists of nodes or elements in a document.\n     *\n     * @param  {Node} root The root element or node to start traversing on.\n     * @return {NodeIterator} The created NodeIterator\n     */\n    const _createNodeIterator = function _createNodeIterator(root) {\n      return createNodeIterator.call(root.ownerDocument || root, root,\n      // eslint-disable-next-line no-bitwise\n      NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_COMMENT | NodeFilter.SHOW_TEXT | NodeFilter.SHOW_PROCESSING_INSTRUCTION | NodeFilter.SHOW_CDATA_SECTION, null);\n    };\n\n    /**\n     * _isClobbered\n     *\n     * @param  {Node} elm element to check for clobbering attacks\n     * @return {Boolean} true if clobbered, false if safe\n     */\n    const _isClobbered = function _isClobbered(elm) {\n      return elm instanceof HTMLFormElement && (typeof elm.nodeName !== 'string' || typeof elm.textContent !== 'string' || typeof elm.removeChild !== 'function' || !(elm.attributes instanceof NamedNodeMap) || typeof elm.removeAttribute !== 'function' || typeof elm.setAttribute !== 'function' || typeof elm.namespaceURI !== 'string' || typeof elm.insertBefore !== 'function' || typeof elm.hasChildNodes !== 'function');\n    };\n\n    /**\n     * Checks whether the given object is a DOM node.\n     *\n     * @param  {Node} object object to check whether it's a DOM node\n     * @return {Boolean} true is object is a DOM node\n     */\n    const _isNode = function _isNode(object) {\n      return typeof Node === 'function' && object instanceof Node;\n    };\n\n    /**\n     * _executeHook\n     * Execute user configurable hooks\n     *\n     * @param  {String} entryPoint  Name of the hook's entry point\n     * @param  {Node} currentNode node to work on with the hook\n     * @param  {Object} data additional hook parameters\n     */\n    const _executeHook = function _executeHook(entryPoint, currentNode, data) {\n      if (!hooks[entryPoint]) {\n        return;\n      }\n      arrayForEach(hooks[entryPoint], hook => {\n        hook.call(DOMPurify, currentNode, data, CONFIG);\n      });\n    };\n\n    /**\n     * _sanitizeElements\n     *\n     * @protect nodeName\n     * @protect textContent\n     * @protect removeChild\n     *\n     * @param   {Node} currentNode to check for permission to exist\n     * @return  {Boolean} true if node was killed, false if left alive\n     */\n    const _sanitizeElements = function _sanitizeElements(currentNode) {\n      let content = null;\n\n      /* Execute a hook if present */\n      _executeHook('beforeSanitizeElements', currentNode, null);\n\n      /* Check if element is clobbered or can clobber */\n      if (_isClobbered(currentNode)) {\n        _forceRemove(currentNode);\n        return true;\n      }\n\n      /* Now let's check the element's type and name */\n      const tagName = transformCaseFunc(currentNode.nodeName);\n\n      /* Execute a hook if present */\n      _executeHook('uponSanitizeElement', currentNode, {\n        tagName,\n        allowedTags: ALLOWED_TAGS\n      });\n\n      /* Detect mXSS attempts abusing namespace confusion */\n      if (currentNode.hasChildNodes() && !_isNode(currentNode.firstElementChild) && regExpTest(/<[/\\w]/g, currentNode.innerHTML) && regExpTest(/<[/\\w]/g, currentNode.textContent)) {\n        _forceRemove(currentNode);\n        return true;\n      }\n\n      /* Remove any occurrence of processing instructions */\n      if (currentNode.nodeType === NODE_TYPE.progressingInstruction) {\n        _forceRemove(currentNode);\n        return true;\n      }\n\n      /* Remove any kind of possibly harmful comments */\n      if (SAFE_FOR_XML && currentNode.nodeType === NODE_TYPE.comment && regExpTest(/<[/\\w]/g, currentNode.data)) {\n        _forceRemove(currentNode);\n        return true;\n      }\n\n      /* Remove element if anything forbids its presence */\n      if (!ALLOWED_TAGS[tagName] || FORBID_TAGS[tagName]) {\n        /* Check if we have a custom element to handle */\n        if (!FORBID_TAGS[tagName] && _isBasicCustomElement(tagName)) {\n          if (CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof RegExp && regExpTest(CUSTOM_ELEMENT_HANDLING.tagNameCheck, tagName)) {\n            return false;\n          }\n          if (CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof Function && CUSTOM_ELEMENT_HANDLING.tagNameCheck(tagName)) {\n            return false;\n          }\n        }\n\n        /* Keep content except for bad-listed elements */\n        if (KEEP_CONTENT && !FORBID_CONTENTS[tagName]) {\n          const parentNode = getParentNode(currentNode) || currentNode.parentNode;\n          const childNodes = getChildNodes(currentNode) || currentNode.childNodes;\n          if (childNodes && parentNode) {\n            const childCount = childNodes.length;\n            for (let i = childCount - 1; i >= 0; --i) {\n              const childClone = cloneNode(childNodes[i], true);\n              childClone.__removalCount = (currentNode.__removalCount || 0) + 1;\n              parentNode.insertBefore(childClone, getNextSibling(currentNode));\n            }\n          }\n        }\n        _forceRemove(currentNode);\n        return true;\n      }\n\n      /* Check whether element has a valid namespace */\n      if (currentNode instanceof Element && !_checkValidNamespace(currentNode)) {\n        _forceRemove(currentNode);\n        return true;\n      }\n\n      /* Make sure that older browsers don't get fallback-tag mXSS */\n      if ((tagName === 'noscript' || tagName === 'noembed' || tagName === 'noframes') && regExpTest(/<\\/no(script|embed|frames)/i, currentNode.innerHTML)) {\n        _forceRemove(currentNode);\n        return true;\n      }\n\n      /* Sanitize element content to be template-safe */\n      if (SAFE_FOR_TEMPLATES && currentNode.nodeType === NODE_TYPE.text) {\n        /* Get the element's text content */\n        content = currentNode.textContent;\n        arrayForEach([MUSTACHE_EXPR, ERB_EXPR, TMPLIT_EXPR], expr => {\n          content = stringReplace(content, expr, ' ');\n        });\n        if (currentNode.textContent !== content) {\n          arrayPush(DOMPurify.removed, {\n            element: currentNode.cloneNode()\n          });\n          currentNode.textContent = content;\n        }\n      }\n\n      /* Execute a hook if present */\n      _executeHook('afterSanitizeElements', currentNode, null);\n      return false;\n    };\n\n    /**\n     * _isValidAttribute\n     *\n     * @param  {string} lcTag Lowercase tag name of containing element.\n     * @param  {string} lcName Lowercase attribute name.\n     * @param  {string} value Attribute value.\n     * @return {Boolean} Returns true if `value` is valid, otherwise false.\n     */\n    // eslint-disable-next-line complexity\n    const _isValidAttribute = function _isValidAttribute(lcTag, lcName, value) {\n      /* Make sure attribute cannot clobber */\n      if (SANITIZE_DOM && (lcName === 'id' || lcName === 'name') && (value in document || value in formElement)) {\n        return false;\n      }\n\n      /* Allow valid data-* attributes: At least one character after \"-\"\n          (https://html.spec.whatwg.org/multipage/dom.html#embedding-custom-non-visible-data-with-the-data-*-attributes)\n          XML-compatible (https://html.spec.whatwg.org/multipage/infrastructure.html#xml-compatible and http://www.w3.org/TR/xml/#d0e804)\n          We don't need to check the value; it's always URI safe. */\n      if (ALLOW_DATA_ATTR && !FORBID_ATTR[lcName] && regExpTest(DATA_ATTR, lcName)) ; else if (ALLOW_ARIA_ATTR && regExpTest(ARIA_ATTR, lcName)) ; else if (!ALLOWED_ATTR[lcName] || FORBID_ATTR[lcName]) {\n        if (\n        // First condition does a very basic check if a) it's basically a valid custom element tagname AND\n        // b) if the tagName passes whatever the user has configured for CUSTOM_ELEMENT_HANDLING.tagNameCheck\n        // and c) if the attribute name passes whatever the user has configured for CUSTOM_ELEMENT_HANDLING.attributeNameCheck\n        _isBasicCustomElement(lcTag) && (CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof RegExp && regExpTest(CUSTOM_ELEMENT_HANDLING.tagNameCheck, lcTag) || CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof Function && CUSTOM_ELEMENT_HANDLING.tagNameCheck(lcTag)) && (CUSTOM_ELEMENT_HANDLING.attributeNameCheck instanceof RegExp && regExpTest(CUSTOM_ELEMENT_HANDLING.attributeNameCheck, lcName) || CUSTOM_ELEMENT_HANDLING.attributeNameCheck instanceof Function && CUSTOM_ELEMENT_HANDLING.attributeNameCheck(lcName)) ||\n        // Alternative, second condition checks if it's an `is`-attribute, AND\n        // the value passes whatever the user has configured for CUSTOM_ELEMENT_HANDLING.tagNameCheck\n        lcName === 'is' && CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements && (CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof RegExp && regExpTest(CUSTOM_ELEMENT_HANDLING.tagNameCheck, value) || CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof Function && CUSTOM_ELEMENT_HANDLING.tagNameCheck(value))) ; else {\n          return false;\n        }\n        /* Check value is safe. First, is attr inert? If so, is safe */\n      } else if (URI_SAFE_ATTRIBUTES[lcName]) ; else if (regExpTest(IS_ALLOWED_URI$1, stringReplace(value, ATTR_WHITESPACE, ''))) ; else if ((lcName === 'src' || lcName === 'xlink:href' || lcName === 'href') && lcTag !== 'script' && stringIndexOf(value, 'data:') === 0 && DATA_URI_TAGS[lcTag]) ; else if (ALLOW_UNKNOWN_PROTOCOLS && !regExpTest(IS_SCRIPT_OR_DATA, stringReplace(value, ATTR_WHITESPACE, ''))) ; else if (value) {\n        return false;\n      } else ;\n      return true;\n    };\n\n    /**\n     * _isBasicCustomElement\n     * checks if at least one dash is included in tagName, and it's not the first char\n     * for more sophisticated checking see https://github.com/sindresorhus/validate-element-name\n     *\n     * @param {string} tagName name of the tag of the node to sanitize\n     * @returns {boolean} Returns true if the tag name meets the basic criteria for a custom element, otherwise false.\n     */\n    const _isBasicCustomElement = function _isBasicCustomElement(tagName) {\n      return tagName !== 'annotation-xml' && stringMatch(tagName, CUSTOM_ELEMENT);\n    };\n\n    /**\n     * _sanitizeAttributes\n     *\n     * @protect attributes\n     * @protect nodeName\n     * @protect removeAttribute\n     * @protect setAttribute\n     *\n     * @param  {Node} currentNode to sanitize\n     */\n    const _sanitizeAttributes = function _sanitizeAttributes(currentNode) {\n      /* Execute a hook if present */\n      _executeHook('beforeSanitizeAttributes', currentNode, null);\n      const {\n        attributes\n      } = currentNode;\n\n      /* Check if we have attributes; if not we might have a text node */\n      if (!attributes) {\n        return;\n      }\n      const hookEvent = {\n        attrName: '',\n        attrValue: '',\n        keepAttr: true,\n        allowedAttributes: ALLOWED_ATTR\n      };\n      let l = attributes.length;\n\n      /* Go backwards over all attributes; safely remove bad ones */\n      while (l--) {\n        const attr = attributes[l];\n        const {\n          name,\n          namespaceURI,\n          value: attrValue\n        } = attr;\n        const lcName = transformCaseFunc(name);\n        let value = name === 'value' ? attrValue : stringTrim(attrValue);\n\n        /* Execute a hook if present */\n        hookEvent.attrName = lcName;\n        hookEvent.attrValue = value;\n        hookEvent.keepAttr = true;\n        hookEvent.forceKeepAttr = undefined; // Allows developers to see this is a property they can set\n        _executeHook('uponSanitizeAttribute', currentNode, hookEvent);\n        value = hookEvent.attrValue;\n\n        /* Did the hooks approve of the attribute? */\n        if (hookEvent.forceKeepAttr) {\n          continue;\n        }\n\n        /* Remove attribute */\n        _removeAttribute(name, currentNode);\n\n        /* Did the hooks approve of the attribute? */\n        if (!hookEvent.keepAttr) {\n          continue;\n        }\n\n        /* Work around a security issue in jQuery 3.0 */\n        if (!ALLOW_SELF_CLOSE_IN_ATTR && regExpTest(/\\/>/i, value)) {\n          _removeAttribute(name, currentNode);\n          continue;\n        }\n\n        /* Sanitize attribute content to be template-safe */\n        if (SAFE_FOR_TEMPLATES) {\n          arrayForEach([MUSTACHE_EXPR, ERB_EXPR, TMPLIT_EXPR], expr => {\n            value = stringReplace(value, expr, ' ');\n          });\n        }\n\n        /* Is `value` valid for this attribute? */\n        const lcTag = transformCaseFunc(currentNode.nodeName);\n        if (!_isValidAttribute(lcTag, lcName, value)) {\n          continue;\n        }\n\n        /* Full DOM Clobbering protection via namespace isolation,\n         * Prefix id and name attributes with `user-content-`\n         */\n        if (SANITIZE_NAMED_PROPS && (lcName === 'id' || lcName === 'name')) {\n          // Remove the attribute with this value\n          _removeAttribute(name, currentNode);\n\n          // Prefix the value and later re-create the attribute with the sanitized value\n          value = SANITIZE_NAMED_PROPS_PREFIX + value;\n        }\n\n        /* Work around a security issue with comments inside attributes */\n        if (SAFE_FOR_XML && regExpTest(/((--!?|])>)|<\\/(style|title)/i, value)) {\n          _removeAttribute(name, currentNode);\n          continue;\n        }\n\n        /* Handle attributes that require Trusted Types */\n        if (trustedTypesPolicy && typeof trustedTypes === 'object' && typeof trustedTypes.getAttributeType === 'function') {\n          if (namespaceURI) ; else {\n            switch (trustedTypes.getAttributeType(lcTag, lcName)) {\n              case 'TrustedHTML':\n                {\n                  value = trustedTypesPolicy.createHTML(value);\n                  break;\n                }\n              case 'TrustedScriptURL':\n                {\n                  value = trustedTypesPolicy.createScriptURL(value);\n                  break;\n                }\n            }\n          }\n        }\n\n        /* Handle invalid data-* attribute set by try-catching it */\n        try {\n          if (namespaceURI) {\n            currentNode.setAttributeNS(namespaceURI, name, value);\n          } else {\n            /* Fallback to setAttribute() for browser-unrecognized namespaces e.g. \"x-schema\". */\n            currentNode.setAttribute(name, value);\n          }\n          if (_isClobbered(currentNode)) {\n            _forceRemove(currentNode);\n          } else {\n            arrayPop(DOMPurify.removed);\n          }\n        } catch (_) {}\n      }\n\n      /* Execute a hook if present */\n      _executeHook('afterSanitizeAttributes', currentNode, null);\n    };\n\n    /**\n     * _sanitizeShadowDOM\n     *\n     * @param  {DocumentFragment} fragment to iterate over recursively\n     */\n    const _sanitizeShadowDOM = function _sanitizeShadowDOM(fragment) {\n      let shadowNode = null;\n      const shadowIterator = _createNodeIterator(fragment);\n\n      /* Execute a hook if present */\n      _executeHook('beforeSanitizeShadowDOM', fragment, null);\n      while (shadowNode = shadowIterator.nextNode()) {\n        /* Execute a hook if present */\n        _executeHook('uponSanitizeShadowNode', shadowNode, null);\n\n        /* Sanitize tags and elements */\n        if (_sanitizeElements(shadowNode)) {\n          continue;\n        }\n\n        /* Deep shadow DOM detected */\n        if (shadowNode.content instanceof DocumentFragment) {\n          _sanitizeShadowDOM(shadowNode.content);\n        }\n\n        /* Check attributes, sanitize if necessary */\n        _sanitizeAttributes(shadowNode);\n      }\n\n      /* Execute a hook if present */\n      _executeHook('afterSanitizeShadowDOM', fragment, null);\n    };\n\n    /**\n     * Sanitize\n     * Public method providing core sanitation functionality\n     *\n     * @param {String|Node} dirty string or DOM node\n     * @param {Object} cfg object\n     */\n    // eslint-disable-next-line complexity\n    DOMPurify.sanitize = function (dirty) {\n      let cfg = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};\n      let body = null;\n      let importedNode = null;\n      let currentNode = null;\n      let returnNode = null;\n      /* Make sure we have a string to sanitize.\n        DO NOT return early, as this will return the wrong type if\n        the user has requested a DOM object rather than a string */\n      IS_EMPTY_INPUT = !dirty;\n      if (IS_EMPTY_INPUT) {\n        dirty = '<!-->';\n      }\n\n      /* Stringify, in case dirty is an object */\n      if (typeof dirty !== 'string' && !_isNode(dirty)) {\n        if (typeof dirty.toString === 'function') {\n          dirty = dirty.toString();\n          if (typeof dirty !== 'string') {\n            throw typeErrorCreate('dirty is not a string, aborting');\n          }\n        } else {\n          throw typeErrorCreate('toString is not a function');\n        }\n      }\n\n      /* Return dirty HTML if DOMPurify cannot run */\n      if (!DOMPurify.isSupported) {\n        return dirty;\n      }\n\n      /* Assign config vars */\n      if (!SET_CONFIG) {\n        _parseConfig(cfg);\n      }\n\n      /* Clean up removed elements */\n      DOMPurify.removed = [];\n\n      /* Check if dirty is correctly typed for IN_PLACE */\n      if (typeof dirty === 'string') {\n        IN_PLACE = false;\n      }\n      if (IN_PLACE) {\n        /* Do some early pre-sanitization to avoid unsafe root nodes */\n        if (dirty.nodeName) {\n          const tagName = transformCaseFunc(dirty.nodeName);\n          if (!ALLOWED_TAGS[tagName] || FORBID_TAGS[tagName]) {\n            throw typeErrorCreate('root node is forbidden and cannot be sanitized in-place');\n          }\n        }\n      } else if (dirty instanceof Node) {\n        /* If dirty is a DOM element, append to an empty document to avoid\n           elements being stripped by the parser */\n        body = _initDocument('<!---->');\n        importedNode = body.ownerDocument.importNode(dirty, true);\n        if (importedNode.nodeType === NODE_TYPE.element && importedNode.nodeName === 'BODY') {\n          /* Node is already a body, use as is */\n          body = importedNode;\n        } else if (importedNode.nodeName === 'HTML') {\n          body = importedNode;\n        } else {\n          // eslint-disable-next-line unicorn/prefer-dom-node-append\n          body.appendChild(importedNode);\n        }\n      } else {\n        /* Exit directly if we have nothing to do */\n        if (!RETURN_DOM && !SAFE_FOR_TEMPLATES && !WHOLE_DOCUMENT &&\n        // eslint-disable-next-line unicorn/prefer-includes\n        dirty.indexOf('<') === -1) {\n          return trustedTypesPolicy && RETURN_TRUSTED_TYPE ? trustedTypesPolicy.createHTML(dirty) : dirty;\n        }\n\n        /* Initialize the document to work on */\n        body = _initDocument(dirty);\n\n        /* Check we have a DOM node from the data */\n        if (!body) {\n          return RETURN_DOM ? null : RETURN_TRUSTED_TYPE ? emptyHTML : '';\n        }\n      }\n\n      /* Remove first element node (ours) if FORCE_BODY is set */\n      if (body && FORCE_BODY) {\n        _forceRemove(body.firstChild);\n      }\n\n      /* Get node iterator */\n      const nodeIterator = _createNodeIterator(IN_PLACE ? dirty : body);\n\n      /* Now start iterating over the created document */\n      while (currentNode = nodeIterator.nextNode()) {\n        /* Sanitize tags and elements */\n        if (_sanitizeElements(currentNode)) {\n          continue;\n        }\n\n        /* Shadow DOM detected, sanitize it */\n        if (currentNode.content instanceof DocumentFragment) {\n          _sanitizeShadowDOM(currentNode.content);\n        }\n\n        /* Check attributes, sanitize if necessary */\n        _sanitizeAttributes(currentNode);\n      }\n\n      /* If we sanitized `dirty` in-place, return it. */\n      if (IN_PLACE) {\n        return dirty;\n      }\n\n      /* Return sanitized string or DOM */\n      if (RETURN_DOM) {\n        if (RETURN_DOM_FRAGMENT) {\n          returnNode = createDocumentFragment.call(body.ownerDocument);\n          while (body.firstChild) {\n            // eslint-disable-next-line unicorn/prefer-dom-node-append\n            returnNode.appendChild(body.firstChild);\n          }\n        } else {\n          returnNode = body;\n        }\n        if (ALLOWED_ATTR.shadowroot || ALLOWED_ATTR.shadowrootmode) {\n          /*\n            AdoptNode() is not used because internal state is not reset\n            (e.g. the past names map of a HTMLFormElement), this is safe\n            in theory but we would rather not risk another attack vector.\n            The state that is cloned by importNode() is explicitly defined\n            by the specs.\n          */\n          returnNode = importNode.call(originalDocument, returnNode, true);\n        }\n        return returnNode;\n      }\n      let serializedHTML = WHOLE_DOCUMENT ? body.outerHTML : body.innerHTML;\n\n      /* Serialize doctype if allowed */\n      if (WHOLE_DOCUMENT && ALLOWED_TAGS['!doctype'] && body.ownerDocument && body.ownerDocument.doctype && body.ownerDocument.doctype.name && regExpTest(DOCTYPE_NAME, body.ownerDocument.doctype.name)) {\n        serializedHTML = '<!DOCTYPE ' + body.ownerDocument.doctype.name + '>\\n' + serializedHTML;\n      }\n\n      /* Sanitize final string template-safe */\n      if (SAFE_FOR_TEMPLATES) {\n        arrayForEach([MUSTACHE_EXPR, ERB_EXPR, TMPLIT_EXPR], expr => {\n          serializedHTML = stringReplace(serializedHTML, expr, ' ');\n        });\n      }\n      return trustedTypesPolicy && RETURN_TRUSTED_TYPE ? trustedTypesPolicy.createHTML(serializedHTML) : serializedHTML;\n    };\n\n    /**\n     * Public method to set the configuration once\n     * setConfig\n     *\n     * @param {Object} cfg configuration object\n     */\n    DOMPurify.setConfig = function () {\n      let cfg = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};\n      _parseConfig(cfg);\n      SET_CONFIG = true;\n    };\n\n    /**\n     * Public method to remove the configuration\n     * clearConfig\n     *\n     */\n    DOMPurify.clearConfig = function () {\n      CONFIG = null;\n      SET_CONFIG = false;\n    };\n\n    /**\n     * Public method to check if an attribute value is valid.\n     * Uses last set config, if any. Otherwise, uses config defaults.\n     * isValidAttribute\n     *\n     * @param  {String} tag Tag name of containing element.\n     * @param  {String} attr Attribute name.\n     * @param  {String} value Attribute value.\n     * @return {Boolean} Returns true if `value` is valid. Otherwise, returns false.\n     */\n    DOMPurify.isValidAttribute = function (tag, attr, value) {\n      /* Initialize shared config vars if necessary. */\n      if (!CONFIG) {\n        _parseConfig({});\n      }\n      const lcTag = transformCaseFunc(tag);\n      const lcName = transformCaseFunc(attr);\n      return _isValidAttribute(lcTag, lcName, value);\n    };\n\n    /**\n     * AddHook\n     * Public method to add DOMPurify hooks\n     *\n     * @param {String} entryPoint entry point for the hook to add\n     * @param {Function} hookFunction function to execute\n     */\n    DOMPurify.addHook = function (entryPoint, hookFunction) {\n      if (typeof hookFunction !== 'function') {\n        return;\n      }\n      hooks[entryPoint] = hooks[entryPoint] || [];\n      arrayPush(hooks[entryPoint], hookFunction);\n    };\n\n    /**\n     * RemoveHook\n     * Public method to remove a DOMPurify hook at a given entryPoint\n     * (pops it from the stack of hooks if more are present)\n     *\n     * @param {String} entryPoint entry point for the hook to remove\n     * @return {Function} removed(popped) hook\n     */\n    DOMPurify.removeHook = function (entryPoint) {\n      if (hooks[entryPoint]) {\n        return arrayPop(hooks[entryPoint]);\n      }\n    };\n\n    /**\n     * RemoveHooks\n     * Public method to remove all DOMPurify hooks at a given entryPoint\n     *\n     * @param  {String} entryPoint entry point for the hooks to remove\n     */\n    DOMPurify.removeHooks = function (entryPoint) {\n      if (hooks[entryPoint]) {\n        hooks[entryPoint] = [];\n      }\n    };\n\n    /**\n     * RemoveAllHooks\n     * Public method to remove all DOMPurify hooks\n     */\n    DOMPurify.removeAllHooks = function () {\n      hooks = {};\n    };\n    return DOMPurify;\n  }\n  var purify = createDOMPurify();\n\n  return purify;\n\n}));\n//# sourceMappingURL=purify.js.map\n", "import { RPCError } from \"@web/core/network/rpc\";\nimport { user } from \"@web/core/user\";\nimport { Deferred, Race } from \"@web/core/utils/concurrency\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { useSetupAction } from \"@web/search/action_hook\";\nimport { SEARCH_KEYS } from \"@web/search/with_search/with_search\";\nimport { buildSampleORM } from \"./sample_server\";\n\nimport {\n    EventBus,\n    onWillStart,\n    onWillUnmount,\n    onWillUpdateProps,\n    status,\n    useComponent,\n} from \"@odoo/owl\";\n\n/**\n * @typedef {import(\"@web/env\").OdooEnv} OdooEnv\n * @typedef {import(\"@web/search/search_model\").SearchParams} SearchParams\n * @typedef {import(\"services\").ServiceFactories} Services\n */\n\nexport class Model {\n    static services = [];\n\n    /**\n     * @param {OdooEnv} env\n     * @param {SearchParams} params\n     * @param {Services} services\n     */\n    constructor(env, params, services) {\n        this.env = env;\n        this.orm = services.orm;\n        this.bus = new EventBus();\n        this.isReady = false;\n        this.whenReady = new Deferred();\n        this.whenReady.then(() => {\n            this.isReady = true;\n            this.notify();\n        });\n        this.setup(params, services);\n    }\n\n    /**\n     * @param {SearchParams} params\n     * @param {Services} services\n     */\n    setup(/* params, services */) {}\n\n    /**\n     * @param {Partial<SearchParams>} _params\n     */\n    async load(_params) {}\n\n    /**\n     * This function is meant to be overriden by models that want to implement\n     * the sample data feature. It should return true iff the last loaded state\n     * actually contains data. If not, another load will be done (if the sample\n     * feature is enabled) with the orm service substituted by another using the\n     * SampleServer, to have sample data to display instead of an empty screen.\n     *\n     * @returns {boolean}\n     */\n    hasData() {\n        return true;\n    }\n\n    /**\n     * This function is meant to be overriden by models that want to combine\n     * sample data with real groups that exist on the server.\n     *\n     * @returns {boolean}\n     */\n    getGroups() {\n        return null;\n    }\n\n    notify() {\n        this.bus.trigger(\"update\");\n    }\n}\n\n/**\n * @param {Record<string, unknown>} props\n * @returns {SearchParams}\n */\nfunction getSearchParams(props) {\n    const params = {};\n    for (const key of SEARCH_KEYS) {\n        params[key] = props[key];\n    }\n    return params;\n}\n\n/**\n * @template {typeof Model} T\n * @param {T} ModelClass\n * @param {Object} params\n * @param {Object} [options]\n * @param {Function} [options.beforeFirstLoad]\n * @returns {InstanceType<T>}\n */\nexport function useModel(ModelClass, params, options = {}) {\n    const component = useComponent();\n    const services = {};\n    for (const key of ModelClass.services) {\n        services[key] = useService(key);\n    }\n    services.orm = services.orm || useService(\"orm\");\n    const model = new ModelClass(component.env, params, services);\n    onWillStart(async () => {\n        await options.beforeFirstLoad?.();\n        await model.load(getSearchParams(component.props));\n        model.whenReady.resolve();\n    });\n    onWillUpdateProps((nextProps) => model.load(getSearchParams(nextProps)));\n    return model;\n}\n\n/**\n * @template {typeof Model} T\n * @param {T} ModelClass\n * @param {Object} params\n * @param {Object} [options]\n * @param {Function} [options.lazy=false]\n * @returns {InstanceType<T>}\n */\nexport function useModelWithSampleData(ModelClass, params, options = {}) {\n    const component = useComponent();\n    if (!(ModelClass.prototype instanceof Model)) {\n        throw new Error(`the model class should extend Model`);\n    }\n    const services = {};\n    for (const key of ModelClass.services) {\n        services[key] = useService(key);\n    }\n    services.orm = services.orm || useService(\"orm\");\n\n    if (!(\"isAlive\" in params)) {\n        params.isAlive = () => status(component) !== \"destroyed\";\n    }\n\n    const model = new ModelClass(component.env, params, services);\n\n    const onUpdate = () => component.render(true);\n    model.bus.addEventListener(\"update\", onUpdate);\n    onWillUnmount(() => model.bus.removeEventListener(\"update\", onUpdate));\n\n    const globalState = component.props.globalState || {};\n    const localState = component.props.state || {};\n    let useSampleModel =\n        component.props.useSampleModel &&\n        (!(\"useSampleModel\" in globalState) || globalState.useSampleModel);\n    model.useSampleModel = false;\n    const orm = model.orm;\n    let sampleORM = localState.sampleORM;\n\n    /**\n     * @param {Record<string, unknown>} props\n     */\n    async function _load(props) {\n        const searchParams = getSearchParams(props);\n        await model.load(searchParams);\n        if (useSampleModel && !model.hasData()) {\n            sampleORM =\n                sampleORM || buildSampleORM(component.props.resModel, component.props.fields, user);\n            // Load data with sampleORM then restore real ORM.\n            model.orm = sampleORM;\n            await model.load(searchParams);\n            model.orm = orm;\n            model.useSampleModel = true;\n        } else {\n            useSampleModel = false;\n            model.useSampleModel = useSampleModel;\n        }\n        model.whenReady.resolve(); // resolve after the first successful load\n        if (status(component) === \"mounted\") {\n            model.notify();\n        }\n    }\n    const race = new Race();\n    const load = (props) => race.add(_load(props));\n    onWillStart(() => {\n        const prom = load(component.props);\n        if (options.lazy) {\n            // in-house error handling as we're out of willStart\n            prom.catch((e) => {\n                if (e instanceof RPCError) {\n                    component.env.config.historyBack();\n                }\n                throw e;\n            });\n        } else {\n            return prom;\n        }\n    });\n    onWillUpdateProps((nextProps) => {\n        useSampleModel = false;\n        load(nextProps);\n    });\n\n    useSetupAction({\n        getGlobalState() {\n            if (component.props.useSampleModel) {\n                return { useSampleModel };\n            }\n        },\n        getLocalState: () => ({ sampleORM }),\n    });\n\n    return model;\n}\n\nexport function _makeFieldFromPropertyDefinition(name, definition, relatedPropertyField) {\n    return {\n        ...definition,\n        name,\n        propertyName: definition.name,\n        relation: definition.comodel,\n        relatedPropertyField,\n    };\n}\n\nexport async function addPropertyFieldDefs(orm, resModel, context, fields, groupBy) {\n    const proms = [];\n    for (const gb of groupBy) {\n        if (gb in fields) {\n            continue;\n        }\n        const [fieldName] = gb.split(\".\");\n        const field = fields[fieldName];\n        if (field?.type === \"properties\") {\n            proms.push(\n                orm\n                    .call(resModel, \"get_property_definition\", [gb], {\n                        context,\n                    })\n                    .then((definition) => {\n                        fields[gb] = _makeFieldFromPropertyDefinition(gb, definition, field);\n                    })\n                    .catch(() => {\n                        fields[gb] = _makeFieldFromPropertyDefinition(gb, {}, field);\n                    })\n            );\n        }\n    }\n    return Promise.all(proms);\n}\n", "import { useService } from \"@web/core/utils/hooks\";\nimport { isObject, pick } from \"@web/core/utils/objects\";\nimport { RelationalModel } from \"@web/model/relational_model/relational_model\";\nimport { getFieldsSpec } from \"@web/model/relational_model/utils\";\nimport { Component, xml, onWillStart, onWillUpdateProps, useState } from \"@odoo/owl\";\n\nconst defaultActiveField = { attrs: {}, options: {}, domain: \"[]\", string: \"\" };\n\nclass StandaloneRelationalModel extends RelationalModel {\n    load(params = {}) {\n        if (params.values) {\n            const data = params.values;\n            const config = this._getNextConfig(this.config, params);\n            this.root = this._createRoot(config, data);\n            this.config = config;\n            this.hooks.onRootLoaded(this.root);\n            return Promise.resolve();\n        }\n        return super.load(params);\n    }\n}\n\nclass _Record extends Component {\n    static template = xml`<t t-slot=\"default\" record=\"model.root\"/>`;\n    static props = [\"slots\", \"info\", \"fields\", \"values?\"];\n    setup() {\n        this.orm = useService(\"orm\");\n        const resModel = this.props.info.resModel;\n        const activeFields = this.getActiveFields();\n        const modelParams = {\n            config: {\n                resModel,\n                fields: this.props.fields,\n                isMonoRecord: true,\n                activeFields,\n                resId: this.props.info.resId,\n                mode: this.props.info.mode,\n                context: this.props.info.context,\n            },\n            hooks: this.props.info.hooks,\n        };\n        const modelServices = Object.fromEntries(\n            StandaloneRelationalModel.services.map((servName) => [servName, useService(servName)])\n        );\n        modelServices.orm = this.orm;\n        this.model = useState(new StandaloneRelationalModel(this.env, modelParams, modelServices));\n\n        const prepareLoadWithValues = async (values) => {\n            values = pick(values, ...Object.keys(modelParams.config.activeFields));\n            const proms = [];\n            for (const fieldName in values) {\n                if ([\"one2many\", \"many2many\"].includes(this.props.fields[fieldName].type)) {\n                    if (values[fieldName].length && typeof values[fieldName][0] === \"number\") {\n                        const resModel = this.props.fields[fieldName].relation;\n                        const resIds = values[fieldName];\n                        const activeField = modelParams.config.activeFields[fieldName];\n                        if (activeField.related) {\n                            const { activeFields, fields } = activeField.related;\n                            const fieldSpec = getFieldsSpec(activeFields, fields, {});\n                            const kwargs = {\n                                context: activeField.context || {},\n                                specification: fieldSpec,\n                            };\n                            proms.push(\n                                this.orm.webRead(resModel, resIds, kwargs).then((records) => {\n                                    values[fieldName] = records;\n                                })\n                            );\n                        }\n                    }\n                }\n                if (this.props.fields[fieldName].type === \"many2one\") {\n                    const loadDisplayName = async (resId) => {\n                        const resModel = this.props.fields[fieldName].relation;\n                        const activeField = modelParams.config.activeFields[fieldName];\n                        const kwargs = {\n                            context: activeField.context || {},\n                            specification: { display_name: {} },\n                        };\n                        const records = await this.orm.webRead(resModel, [resId], kwargs);\n                        return records[0].display_name;\n                    };\n                    if (typeof values[fieldName] === \"number\") {\n                        const prom = loadDisplayName(values[fieldName]);\n                        prom.then((displayName) => {\n                            values[fieldName] = {\n                                id: values[fieldName],\n                                display_name: displayName,\n                            };\n                        });\n                        proms.push(prom);\n                    } else if (Array.isArray(values[fieldName])) {\n                        if (values[fieldName][1] === undefined) {\n                            const prom = loadDisplayName(values[fieldName][0]);\n                            prom.then((displayName) => {\n                                values[fieldName] = {\n                                    id: values[fieldName][0],\n                                    display_name: displayName,\n                                };\n                            });\n                            proms.push(prom);\n                        }\n                        values[fieldName] = {\n                            id: values[fieldName][0],\n                            display_name: values[fieldName][1],\n                        };\n                    } else if (isObject(values[fieldName])) {\n                        if (values[fieldName].display_name === undefined) {\n                            const prom = loadDisplayName(values[fieldName].id);\n                            prom.then((displayName) => {\n                                values[fieldName] = {\n                                    id: values[fieldName].id,\n                                    display_name: displayName,\n                                };\n                            });\n                            proms.push(prom);\n                        }\n                        values[fieldName] = {\n                            id: values[fieldName].id,\n                            display_name: values[fieldName].display_name,\n                        };\n                    }\n                }\n                await Promise.all(proms);\n            }\n            return values;\n        };\n        onWillStart(async () => {\n            if (this.props.values) {\n                const values = await prepareLoadWithValues(this.props.values);\n                await this.model.load({ values });\n            } else {\n                await this.model.load();\n            }\n            this.model.whenReady.resolve();\n        });\n        onWillUpdateProps(async (nextProps) => {\n            const params = {};\n            if (nextProps.info.resId !== this.model.root.resId) {\n                params.resId = nextProps.info.resId;\n            }\n            if (nextProps.values) {\n                params.values = await prepareLoadWithValues(nextProps.values);\n            }\n            if (Object.keys(params).length) {\n                return this.model.load(params);\n            }\n        });\n    }\n\n    getActiveFields() {\n        if (this.props.info.activeFields) {\n            const activeFields = {};\n            for (const [fName, fInfo] of Object.entries(this.props.info.activeFields)) {\n                activeFields[fName] = { ...defaultActiveField, ...fInfo };\n            }\n            return activeFields;\n        }\n        return Object.fromEntries(\n            this.props.info.fieldNames.map((f) => [f, { ...defaultActiveField }])\n        );\n    }\n}\n\nexport class Record extends Component {\n    static template = xml`<_Record fields=\"fields\" slots=\"props.slots\" values=\"props.values\" info=\"props\" />`;\n    static components = { _Record };\n    static props = [\n        \"slots\",\n        \"resModel?\",\n        \"fieldNames?\",\n        \"activeFields?\",\n        \"fields?\",\n        \"resId?\",\n        \"mode?\",\n        \"values?\",\n        \"context?\",\n        \"hooks?\",\n    ];\n    static defaultProps = {\n        context: {},\n    };\n    setup() {\n        const { activeFields, fieldNames, fields, resModel } = this.props;\n        if (!activeFields && !fieldNames) {\n            throw Error(\n                `Record props should have either a \"activeFields\" key or a \"fieldNames\" key`\n            );\n        }\n        if (!fields && (!fieldNames || !resModel)) {\n            throw Error(\n                `Record props should have either a \"fields\" key or a \"fieldNames\" and a \"resModel\" key`\n            );\n        }\n        if (fields) {\n            this.fields = fields;\n        } else {\n            const fieldService = useService(\"field\");\n            onWillStart(async () => {\n                this.fields = await fieldService.loadFields(resModel, { fieldNames });\n            });\n        }\n    }\n}\n", "import { markRaw } from \"@odoo/owl\";\nimport { Reactive } from \"@web/core/utils/reactive\";\nimport { getId } from \"./utils\";\n\n/**\n * @typedef {import(\"@web/search/search_model\").Field} Field\n * @typedef {import(\"@web/search/search_model\").FieldInfo} FieldInfo\n * @typedef {import(\"./relational_model\").RelationalModel} RelationalModel\n * @typedef {import(\"./relational_model\").RelationalModelConfig} RelationalModelConfig\n */\n\nexport class DataPoint extends Reactive {\n    /**\n     * @param {RelationalModel} model\n     * @param {RelationalModelConfig} config\n     * @param {Record<string, unknown>} data\n     * @param {unknown} [options]\n     */\n    constructor(model, config, data, options) {\n        super(...arguments);\n        this.id = getId(\"datapoint\");\n        this.model = model;\n        markRaw(config.activeFields);\n        markRaw(config.fields);\n        /** @type {RelationalModelConfig} */\n        this._config = config;\n        this.setup(config, data, options);\n    }\n\n    /**\n     * @abstract\n     * @template [O={}]\n     * @param {RelationalModelConfig} _config\n     * @param {Record<string, unknown>} _data\n     * @param {O | undefined} _options\n     */\n    setup(_config, _data, _options) {}\n\n    get activeFields() {\n        return this.config.activeFields;\n    }\n\n    get fields() {\n        return this.config.fields;\n    }\n\n    get fieldNames() {\n        return Object.keys(this.activeFields).filter(\n            (fieldName) => !this.fields[fieldName].relatedPropertyField\n        );\n    }\n\n    get resModel() {\n        return this.config.resModel;\n    }\n\n    get config() {\n        return this._config;\n    }\n\n    get context() {\n        return this.config.context;\n    }\n}\n", "//@ts-check\n\nimport { Domain } from \"@web/core/domain\";\nimport { DynamicList } from \"./dynamic_list\";\nimport { getGroupServerValue } from \"./utils\";\n\nexport const MOVABLE_RECORD_TYPES = [\"char\", \"boolean\", \"integer\", \"selection\", \"many2one\"];\n\n/**\n * @typedef {import(\"./record\").Record} RelationalRecord\n */\n\nexport class DynamicGroupList extends DynamicList {\n    static type = \"DynamicGroupList\";\n\n    /**\n     * @type {DynamicList[\"setup\"]}\n     */\n    setup(_config, data) {\n        super.setup(...arguments);\n\n        this.isGrouped = true;\n        this._nbRecordsMatchingDomain = null;\n        this._setData(data);\n    }\n\n    /**\n     * @param {Record<string, unknown>} data\n     */\n    _setData(data) {\n        /** @type {import(\"./group\").Group[]} */\n        this.groups = data.groups.map((g) => this._createGroupDatapoint(g));\n        this.count = data.length;\n        this._selectDomain(this.isDomainSelected);\n    }\n\n    // -------------------------------------------------------------------------\n    // Getters\n    // -------------------------------------------------------------------------\n\n    get groupBy() {\n        return this.config.groupBy;\n    }\n\n    get groupByField() {\n        return this.fields[this.groupBy[0].split(\":\")[0]];\n    }\n\n    get hasData() {\n        return this.groups.some((group) => group.hasData);\n    }\n\n    get isRecordCountTrustable() {\n        return this.count <= this.limit || this._nbRecordsMatchingDomain !== null;\n    }\n\n    /**\n     * List of loaded records inside groups.\n     * @returns {RelationalRecord[]}\n     */\n    get records() {\n        return this.groups\n            .filter((group) => !group.isFolded)\n            .map((group) => group.records)\n            .flat();\n    }\n\n    /**\n     * @returns {number}\n     */\n    get recordCount() {\n        if (this._nbRecordsMatchingDomain !== null) {\n            return this._nbRecordsMatchingDomain;\n        }\n        return this.groups.reduce((acc, group) => acc + group.count, 0);\n    }\n\n    // -------------------------------------------------------------------------\n    // Public\n    // -------------------------------------------------------------------------\n\n    /**\n     * @param {string} groupName\n     * @param {string} [foldField] if given, will write true on this field to\n     *   make the group folded by default\n     */\n    async createGroup(groupName, foldField) {\n        if (!this.groupByField || this.groupByField.type !== \"many2one\") {\n            throw new Error(\"Cannot create a group on a non many2one group field\");\n        }\n\n        await this.model.mutex.exec(() => this._createGroup(groupName, foldField));\n    }\n\n    async deleteGroups(groups) {\n        await this.model.mutex.exec(() => this._deleteGroups(groups));\n    }\n\n    /**\n     * @param {string} dataRecordId\n     * @param {string} dataGroupId\n     * @param {string} refId\n     * @param {string} targetGroupId\n     */\n    async moveRecord(dataRecordId, dataGroupId, refId, targetGroupId) {\n        const targetGroup = this.groups.find((g) => g.id === targetGroupId);\n        if (dataGroupId === targetGroupId) {\n            // move a record inside the same group\n            await targetGroup.list._resequence(\n                targetGroup.list.records,\n                this.resModel,\n                dataRecordId,\n                refId\n            );\n            return;\n        }\n\n        // move record from a group to another group\n        const sourceGroup = this.groups.find((g) => g.id === dataGroupId);\n        const recordIndex = sourceGroup.list.records.findIndex((r) => r.id === dataRecordId);\n        const record = sourceGroup.list.records[recordIndex];\n        // step 1: move record to correct position\n        const refIndex = targetGroup.list.records.findIndex((r) => r.id === refId);\n        const oldIndex = sourceGroup.list.records.findIndex((r) => r.id === dataRecordId);\n\n        const sourceList = sourceGroup.list;\n        // if the source contains more records than what's loaded, reload it after moving the record\n        const mustReloadSourceList = sourceList.count > sourceList.offset + sourceList.limit;\n\n        sourceGroup._removeRecords([record.id]);\n        targetGroup._addRecord(record, refIndex + 1);\n        // step 2: update record value\n        let value = targetGroup.value;\n        if (targetGroup.groupByField.type === \"many2one\") {\n            value = value ? { id: value, display_name: targetGroup.displayName } : false;\n        }\n\n        const revert = () => {\n            targetGroup._removeRecords([record.id]);\n            sourceGroup._addRecord(record, oldIndex);\n        };\n        try {\n            const changes = { [targetGroup.groupByField.name]: value };\n            const res = await record.update(changes, { save: true });\n            if (!res) {\n                return revert();\n            }\n        } catch (e) {\n            // revert changes\n            revert();\n            throw e;\n        }\n\n        const proms = [];\n        if (mustReloadSourceList) {\n            const { offset, limit, orderBy, domain } = sourceGroup.list;\n            proms.push(sourceGroup.list._load(offset, limit, orderBy, domain));\n        }\n        if (!targetGroup.isFolded) {\n            const targetList = targetGroup.list;\n            const records = targetList.records;\n            proms.push(targetList._resequence(records, this.resModel, dataRecordId, refId));\n        }\n        return Promise.all(proms);\n    }\n\n    async resequence(movedGroupId, targetGroupId) {\n        if (!this.groupByField || this.groupByField.type !== \"many2one\") {\n            throw new Error(\"Cannot resequence a group on a non many2one group field\");\n        }\n\n        return this.model.mutex.exec(async () => {\n            await this._resequence(\n                this.groups,\n                this.groupByField.relation,\n                movedGroupId,\n                targetGroupId\n            );\n        });\n    }\n\n    async selectDomain(value) {\n        return this.model.mutex.exec(async () => {\n            await this._ensureCorrectRecordCount();\n            this._selectDomain(value);\n        });\n    }\n\n    async sortBy(fieldName) {\n        if (!this.groups.length) {\n            return;\n        }\n        if (this.groups.every((group) => group.isFolded)) {\n            // all groups are folded\n            if (this.groupByField.name !== fieldName) {\n                // grouped by another field than fieldName\n                if (!(fieldName in this.groups[0].aggregates)) {\n                    // fieldName has no aggregate values\n                    return;\n                }\n            }\n        }\n        return super.sortBy(fieldName);\n    }\n\n    // -------------------------------------------------------------------------\n    // Protected\n    // -------------------------------------------------------------------------\n\n    async _createGroup(groupName, foldField = false) {\n        const [id] = await this.model.orm.call(\n            this.groupByField.relation,\n            \"name_create\",\n            [groupName],\n            { context: this.context }\n        );\n        if (foldField) {\n            await this.model.orm.write(\n                this.groupByField.relation,\n                [id],\n                { [foldField]: true },\n                { context: this.context }\n            );\n        }\n        const lastGroup = this.groups.at(-1);\n\n        // This is almost a copy/past of the code in relational_model.js\n        // Maybe we can create an addGroup method in relational_model.js\n        // and call it from here and from relational_model.js\n        const commonConfig = {\n            resModel: this.config.resModel,\n            fields: this.config.fields,\n            activeFields: this.config.activeFields,\n            fieldsToAggregate: this.config.fieldsToAggregate,\n        };\n        const context = {\n            ...this.context,\n            [`default_${this.groupByField.name}`]: id,\n        };\n        const nextConfigGroups = { ...this.config.groups };\n        const domain = Domain.and([this.domain, [[this.groupByField.name, \"=\", id]]]).toList();\n        const groupBy = this.groupBy.slice(1);\n        nextConfigGroups[id] = {\n            ...commonConfig,\n            context,\n            groupByFieldName: this.groupByField.name,\n            isFolded: Boolean(foldField),\n            value: id,\n            extraDomain: false,\n            initialDomain: domain,\n            list: {\n                ...commonConfig,\n                context,\n                domain: domain,\n                groupBy,\n                orderBy: this.orderBy,\n                limit: this.model.initialLimit,\n                offset: 0,\n            },\n        };\n        this.model._updateConfig(this.config, { groups: nextConfigGroups }, { reload: false });\n\n        const data = {\n            aggregates: {},\n            count: 0,\n            length: 0,\n            __domain: domain,\n            [this.groupByField.name]: [id, groupName],\n            value: id,\n            serverValue: getGroupServerValue(this.groupByField, id),\n            displayName: groupName,\n            rawValue: [id, groupName],\n        };\n        if (groupBy.length) {\n            data.groups = [];\n        } else {\n            data.records = [];\n        }\n\n        const group = this._createGroupDatapoint(data);\n        if (lastGroup) {\n            const groups = [...this.groups, group];\n            await this._resequence(groups, this.groupByField.relation, group.id, lastGroup.id);\n            this.groups = groups;\n        } else {\n            this.groups.push(group);\n        }\n    }\n\n    _createGroupDatapoint(data) {\n        return new this.model.constructor.Group(this.model, this.config.groups[data.value], data);\n    }\n\n    async _deleteGroups(groups) {\n        const shouldReload = groups.some((g) => g.count > 0);\n        await this._unlinkGroups(groups);\n        const configGroups = { ...this.config.groups };\n        for (const group of groups) {\n            delete configGroups[group.value];\n        }\n        if (shouldReload) {\n            await this.model._updateConfig(\n                this.config,\n                { groups: configGroups },\n                { commit: this._setData.bind(this) }\n            );\n        } else {\n            for (const group of groups) {\n                this._removeGroup(group);\n            }\n            this.model._updateConfig(this.config, { groups: configGroups }, { reload: false });\n        }\n    }\n\n    async _ensureCorrectRecordCount() {\n        if (!this.isRecordCountTrustable) {\n            this._nbRecordsMatchingDomain = await this.model.orm.searchCount(\n                this.resModel,\n                this.domain,\n                { limit: this.model.initialCountLimit }\n            );\n        }\n    }\n\n    _getDPresId(group) {\n        return group.value;\n    }\n\n    _getDPFieldValue(group, handleField) {\n        return group[handleField];\n    }\n\n    async _load(offset, limit, orderBy, domain) {\n        await this.model._updateConfig(\n            this.config,\n            { offset, limit, orderBy, domain },\n            { commit: this._setData.bind(this) }\n        );\n        if (this.isDomainSelected) {\n            await this._ensureCorrectRecordCount();\n        }\n    }\n\n    _removeGroup(group) {\n        const index = this.groups.findIndex((g) => g.id === group.id);\n        this.groups.splice(index, 1);\n        this.count--;\n    }\n\n    _removeRecords(recordIds) {\n        const proms = [];\n        for (const group of this.groups) {\n            proms.push(group._removeRecords(recordIds));\n        }\n        return Promise.all(proms);\n    }\n\n    _selectDomain(value) {\n        for (const group of this.groups) {\n            group.list._selectDomain(value);\n        }\n        super._selectDomain(value);\n    }\n\n    async _toggleSelection() {\n        if (!this.records.length) {\n            // all groups are folded, so there's no visible records => select all domain\n            if (!this.isDomainSelected) {\n                await this._ensureCorrectRecordCount();\n                this._selectDomain(true);\n            } else {\n                this._selectDomain(false);\n            }\n        } else {\n            super._toggleSelection();\n        }\n    }\n\n    _unlinkGroups(groups) {\n        const groupResIds = groups.map((g) => g.value);\n        return this.model.orm.unlink(this.groupByField.relation, groupResIds, {\n            context: this.context,\n        });\n    }\n}\n", "import { ConfirmationDialog } from \"@web/core/confirmation_dialog/confirmation_dialog\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { x2ManyCommands } from \"@web/core/orm_service\";\nimport { unique } from \"@web/core/utils/arrays\";\nimport { DataPoint } from \"./datapoint\";\nimport { Operation } from \"./operation\";\nimport { Record as RelationalRecord } from \"./record\";\nimport { getFieldsSpec, resequence } from \"./utils\";\n\n/**\n * @typedef {import(\"./record\").Record} RelationalRecord\n */\n\nconst DEFAULT_HANDLE_FIELD = \"sequence\";\n\n/**\n * @abstract\n */\nexport class DynamicList extends DataPoint {\n    /**\n     * @type {DataPoint[\"setup\"]}\n     */\n    setup() {\n        super.setup(...arguments);\n        this.handleField = Object.keys(this.activeFields).find(\n            (fieldName) => this.activeFields[fieldName].isHandle\n        );\n        if (!this.handleField && DEFAULT_HANDLE_FIELD in this.fields) {\n            this.handleField = DEFAULT_HANDLE_FIELD;\n        }\n        this.isDomainSelected = false;\n        this.evalContext = this.context;\n    }\n\n    // -------------------------------------------------------------------------\n    // Getters\n    // -------------------------------------------------------------------------\n\n    get groupBy() {\n        return [];\n    }\n\n    get orderBy() {\n        return this.config.orderBy;\n    }\n\n    get domain() {\n        return this.config.domain;\n    }\n\n    /**\n     * Be careful that this getter is costly, as it iterates over the whole list\n     * of records. This property should not be accessed in a loop.\n     */\n    get editedRecord() {\n        return this.records.find((record) => record.isInEdition);\n    }\n\n    get isRecordCountTrustable() {\n        return true;\n    }\n\n    get limit() {\n        return this.config.limit;\n    }\n\n    get offset() {\n        return this.config.offset;\n    }\n\n    /**\n     * Be careful that this getter is costly, as it iterates over the whole list\n     * of records. This property should not be accessed in a loop.\n     */\n    get selection() {\n        return this.records.filter((record) => record.selected);\n    }\n\n    // -------------------------------------------------------------------------\n    // Public\n    // -------------------------------------------------------------------------\n\n    archive(isSelected) {\n        return this.model.mutex.exec(() => this._toggleArchive(isSelected, true));\n    }\n\n    canResequence() {\n        return !!this.handleField;\n    }\n\n    deleteRecords(records = []) {\n        return this.model.mutex.exec(() => this._deleteRecords(records));\n    }\n\n    duplicateRecords(records = []) {\n        return this.model.mutex.exec(() => this._duplicateRecords(records));\n    }\n\n    async enterEditMode(record) {\n        if (this.editedRecord === record) {\n            return true;\n        }\n        const canProceed = await this.leaveEditMode();\n        if (canProceed) {\n            record._checkValidity();\n            this.model._updateConfig(record.config, { mode: \"edit\" }, { reload: false });\n        }\n        return canProceed;\n    }\n\n    /**\n     * @param {boolean} [isSelected]\n     * @returns {Promise<number[]>}\n     */\n    async getResIds(isSelected) {\n        let resIds;\n        if (isSelected) {\n            if (this.isDomainSelected) {\n                resIds = await this.model.orm.search(this.resModel, this.domain, {\n                    limit: this.model.activeIdsLimit,\n                    context: this.context,\n                });\n            } else {\n                resIds = this.selection.map((r) => r.resId);\n            }\n        } else {\n            resIds = this.records.map((r) => r.resId);\n        }\n        return unique(resIds);\n    }\n\n    async leaveEditMode({ discard } = {}) {\n        let editedRecord = this.editedRecord;\n        if (editedRecord) {\n            let canProceed = true;\n            if (discard) {\n                this._recordToDiscard = editedRecord;\n                await editedRecord.discard();\n                this._recordToDiscard = null;\n                editedRecord = this.editedRecord;\n                if (editedRecord && editedRecord.isNew) {\n                    this._removeRecords([editedRecord.id]);\n                }\n            } else {\n                let isValid = true;\n                if (!this.model._urgentSave) {\n                    isValid = await editedRecord.checkValidity();\n                    editedRecord = this.editedRecord;\n                    if (!editedRecord) {\n                        return true;\n                    }\n                }\n                if (editedRecord.isNew && !editedRecord.dirty) {\n                    this._removeRecords([editedRecord.id]);\n                } else if (isValid || editedRecord.dirty) {\n                    canProceed = await editedRecord.save();\n                }\n            }\n\n            editedRecord = this.editedRecord;\n            if (canProceed && editedRecord) {\n                this.model._updateConfig(\n                    editedRecord.config,\n                    { mode: \"readonly\" },\n                    { reload: false }\n                );\n            } else {\n                return canProceed;\n            }\n        }\n        return true;\n    }\n\n    load(params = {}) {\n        const limit = params.limit === undefined ? this.limit : params.limit;\n        const offset = params.offset === undefined ? this.offset : params.offset;\n        const orderBy = params.orderBy === undefined ? this.orderBy : params.orderBy;\n        const domain = params.domain === undefined ? this.domain : params.domain;\n        return this.model.mutex.exec(() => this._load(offset, limit, orderBy, domain));\n    }\n\n    async multiSave(record, changes) {\n        return this.model.mutex.exec(() => this._multiSave(record, changes));\n    }\n\n    selectDomain(value) {\n        return this.model.mutex.exec(() => this._selectDomain(value));\n    }\n\n    sortBy(fieldName) {\n        return this.model.mutex.exec(() => {\n            let orderBy = [...this.orderBy];\n            if (orderBy.length && orderBy[0].name === fieldName) {\n                if (orderBy[0].asc) {\n                    orderBy[0] = { name: orderBy[0].name, asc: false };\n                } else {\n                    orderBy = [];\n                }\n            } else {\n                orderBy = orderBy.filter((o) => o.name !== fieldName);\n                orderBy.unshift({\n                    name: fieldName,\n                    asc: true,\n                });\n            }\n            return this._load(this.offset, this.limit, orderBy, this.domain);\n        });\n    }\n\n    toggleSelection() {\n        return this.model.mutex.exec(() => this._toggleSelection());\n    }\n\n    unarchive(isSelected) {\n        return this.model.mutex.exec(() => this._toggleArchive(isSelected, false));\n    }\n\n    toggleArchiveWithConfirmation(archive, dialogProps = {}) {\n        const isSelected = this.isDomainSelected || this.selection.length > 0;\n        if (archive) {\n            const defaultProps = {\n                body: _t(\"Are you sure that you want to archive all the selected records?\"),\n                cancel: () => {},\n                confirm: () => this.archive(isSelected),\n                confirmLabel: _t(\"Archive\"),\n            };\n            this.model.dialog.add(ConfirmationDialog, { ...defaultProps, ...dialogProps });\n        } else {\n            this.unarchive(isSelected);\n        }\n    }\n\n    // -------------------------------------------------------------------------\n    // Protected\n    // -------------------------------------------------------------------------\n\n    async _duplicateRecords(records) {\n        let resIds;\n        if (records.length) {\n            resIds = unique(records.map((r) => r.resId));\n        } else {\n            resIds = await this.getResIds(true);\n        }\n\n        const copy = async (resIds) => {\n            const copiedRecords = await this.model.orm.call(this.resModel, \"copy\", [resIds], {\n                context: this.context,\n            });\n\n            if (resIds.length > copiedRecords.length) {\n                this.model.notification.add(_t(\"Some records could not be duplicated\"));\n            }\n            return this.model.load();\n        };\n\n        if (resIds.length > 1) {\n            this.model.dialog.add(ConfirmationDialog, {\n                body: _t(\"Are you sure that you want to duplicate all the selected records?\"),\n                confirm: () => copy(resIds),\n                cancel: () => {},\n                confirmLabel: _t(\"Confirm\"),\n            });\n        } else {\n            await copy(resIds);\n        }\n    }\n\n    async _deleteRecords(records) {\n        let resIds;\n        if (records.length) {\n            resIds = unique(records.map((r) => r.resId));\n        } else {\n            resIds = await this.getResIds(true);\n            records = this.records.filter((r) => resIds.includes(r.resId));\n        }\n        const unlinked = await this.model.orm.unlink(this.resModel, resIds, {\n            context: this.context,\n        });\n        if (!unlinked) {\n            return false;\n        }\n        if (\n            this.isDomainSelected &&\n            resIds.length === this.model.activeIdsLimit &&\n            resIds.length < this.count\n        ) {\n            const msg = _t(\n                \"Only the first %(count)s records have been deleted (out of %(total)s selected)\",\n                { count: resIds.length, total: this.count }\n            );\n            this.model.notification.add(msg);\n        }\n        await this.model.load();\n        return unlinked;\n    }\n\n    async _leaveSampleMode() {\n        if (this.model.useSampleModel) {\n            await this._load(this.offset, this.limit, this.orderBy, this.domain);\n            this.model.useSampleModel = false;\n        }\n    }\n\n    async _multiSave(editedRecord, changes) {\n        if (!Object.keys(changes).length || editedRecord === this._recordToDiscard) {\n            return;\n        }\n        let canProceed = await this.model.hooks.onWillSaveMulti(editedRecord, changes);\n        if (canProceed === false) {\n            return false;\n        }\n\n        const selectedRecords = this.selection; // costly getter => compute it once\n\n        // special treatment for x2manys: apply commands on all selected record's static lists\n        const proms = [];\n        for (const fieldName in changes) {\n            if ([\"one2many\", \"many2many\"].includes(this.fields[fieldName].type)) {\n                const list = editedRecord.data[fieldName];\n                const commands = list._getCommands();\n                if (\"display_name\" in list.activeFields) {\n                    // add display_name to LINK commands to prevent a web_read by selected record\n                    for (const command of commands) {\n                        if (command[0] === x2ManyCommands.LINK) {\n                            const relRecord = list._cache[command[1]];\n                            command[2] = { display_name: relRecord.data.display_name };\n                        }\n                    }\n                }\n                for (const record of selectedRecords) {\n                    if (record !== editedRecord) {\n                        proms.push(record.data[fieldName]._applyCommands(commands));\n                    }\n                }\n            }\n        }\n        await Promise.all(proms);\n        // apply changes on all selected records (for x2manys, the change is the static list itself)\n        selectedRecords.forEach((record) => {\n            const _changes = Object.assign({}, changes);\n            for (const fieldName in _changes) {\n                if ([\"one2many\", \"many2many\"].includes(this.fields[fieldName].type)) {\n                    _changes[fieldName] = record.data[fieldName];\n                }\n            }\n            record._applyChanges(_changes);\n        });\n\n        // determine valid and invalid records\n        const validRecords = [];\n        const invalidRecords = [];\n        for (const record of selectedRecords) {\n            const isEditedRecord = record === editedRecord;\n            if (\n                Object.keys(changes).every((fieldName) => !record._isReadonly(fieldName)) &&\n                record._checkValidity({ silent: !isEditedRecord })\n            ) {\n                validRecords.push(record);\n            } else {\n                invalidRecords.push(record);\n            }\n        }\n        const discardInvalidRecords = () => invalidRecords.forEach((record) => record._discard());\n\n        if (validRecords.length === 0) {\n            editedRecord._displayInvalidFieldNotification();\n            discardInvalidRecords();\n            return false;\n        }\n\n        // generate the save callback with the values to save (must be done before discarding\n        // invalid records, in case the editedRecord is itself invalid)\n        const resIds = unique(validRecords.map((r) => r.resId));\n        const kwargs = {\n            context: this.context,\n            specification: getFieldsSpec(editedRecord.activeFields, editedRecord.fields),\n        };\n        let save;\n        if (Object.values(changes).some((v) => v instanceof Operation)) {\n            // \"changes\" contains a Field Operation => we must call the web_save_multi method to\n            // save each record individually\n            const changesById = {};\n            for (const record of validRecords) {\n                changesById[record.resId] = changesById[record.resId] || record._getChanges();\n            }\n            const valsList = resIds.map((resId) => changesById[resId]);\n            save = () => this.model.orm.webSaveMulti(this.resModel, resIds, valsList, kwargs);\n        } else {\n            const vals = editedRecord._getChanges();\n            save = () => this.model.orm.webSave(this.resModel, resIds, vals, kwargs);\n        }\n\n        const _changes = Object.assign(changes);\n        for (const fieldName in changes) {\n            if (this.fields[fieldName].type === \"many2many\") {\n                const list = changes[fieldName];\n                _changes[fieldName] = {\n                    add: list._commands\n                        .filter((command) => command[0] === x2ManyCommands.LINK)\n                        .map((command) => list._cache[command[1]]),\n                    remove: list._commands\n                        .filter((command) => command[0] === x2ManyCommands.UNLINK)\n                        .map((command) => list._cache[command[1]]),\n                };\n            }\n        }\n        discardInvalidRecords();\n\n        // ask confirmation\n        canProceed = await this.model.hooks.onAskMultiSaveConfirmation(_changes, validRecords);\n        if (canProceed === false) {\n            selectedRecords.forEach((record) => record._discard());\n            this.leaveEditMode({ discard: true });\n            return false;\n        }\n\n        // save changes\n        let records = [];\n        try {\n            records = await save();\n        } catch (e) {\n            selectedRecords.forEach((record) => record._discard());\n            this.model._updateConfig(editedRecord.config, { mode: \"readonly\" }, { reload: false });\n            throw e;\n        }\n        const serverValuesById = Object.fromEntries(records.map((record) => [record.id, record]));\n        for (const record of validRecords) {\n            const serverValues = serverValuesById[record.resId];\n            record._setData(serverValues);\n            this.model._updateSimilarRecords(record, serverValues);\n        }\n        this.model._updateConfig(editedRecord.config, { mode: \"readonly\" }, { reload: false });\n        this.model.hooks.onSavedMulti(validRecords);\n        return true;\n    }\n\n    async _resequence(originalList, resModel, movedId, targetId) {\n        if (this.resModel === resModel && !this.canResequence()) {\n            return;\n        }\n        const handleField = this.resModel === resModel ? this.handleField : DEFAULT_HANDLE_FIELD;\n        const order = this.orderBy.find((o) => o.name === handleField);\n        const getSequence = (dp) => dp && this._getDPFieldValue(dp, handleField);\n        const getResId = (dp) => this._getDPresId(dp);\n        const resequencedRecords = await resequence({\n            records: originalList,\n            resModel,\n            movedId,\n            targetId,\n            fieldName: handleField,\n            asc: order?.asc,\n            context: this.context,\n            orm: this.model.orm,\n            getSequence,\n            getResId,\n        });\n        for (const dpData of resequencedRecords) {\n            const dp = originalList.find((d) => getResId(d) === dpData.id);\n            if (dp instanceof RelationalRecord) {\n                dp._applyValues(dpData);\n            } else {\n                dp[handleField] = dpData[handleField];\n            }\n        }\n    }\n\n    _selectDomain(value) {\n        this.isDomainSelected = value;\n    }\n\n    async _toggleArchive(isSelected, state) {\n        const method = state ? \"action_archive\" : \"action_unarchive\";\n        const context = this.context;\n        const resIds = await this.getResIds(isSelected);\n        const action = await this.model.orm.call(this.resModel, method, [resIds], { context });\n        if (\n            this.isDomainSelected &&\n            resIds.length === this.model.activeIdsLimit &&\n            resIds.length < this.count\n        ) {\n            const msg = _t(\n                \"Of the %(selectedRecord)s selected records, only the first %(firstRecords)s have been archived/unarchived.\",\n                {\n                    selectedRecords: resIds.length,\n                    firstRecords: this.count,\n                }\n            );\n            this.model.notification.add(msg);\n        }\n        const reload = () => this.model.load();\n        if (action && Object.keys(action).length) {\n            this.model.action.doAction(action, {\n                onClose: reload,\n            });\n        } else {\n            return reload();\n        }\n    }\n\n    async _toggleSelection() {\n        if (this.selection.length === this.records.length) {\n            this.records.forEach((record) => {\n                record._toggleSelection(false);\n            });\n            this._selectDomain(false);\n        } else {\n            this.records.forEach((record) => {\n                record._toggleSelection(true);\n            });\n        }\n    }\n}\n", "import { DynamicList } from \"./dynamic_list\";\n\n/**\n * @typedef {import(\"./record\").Record} RelationalRecord\n */\n\nexport class DynamicRecordList extends DynamicList {\n    static type = \"DynamicRecordList\";\n\n    /**\n     * @param {import(\"./relational_model\").Config} config\n     * @param {Object} data\n     */\n    setup(config, data) {\n        super.setup(config);\n        this._setData(data);\n    }\n\n    _setData(data) {\n        /** @type {RelationalRecord[]} */\n        this.records = data.records.map((r) => this._createRecordDatapoint(r));\n        this._updateCount(data);\n        this._selectDomain(this.isDomainSelected);\n    }\n\n    // -------------------------------------------------------------------------\n    // Getter\n    // -------------------------------------------------------------------------\n\n    get hasData() {\n        return this.count > 0;\n    }\n\n    // -------------------------------------------------------------------------\n    // Public\n    // -------------------------------------------------------------------------\n\n    /**\n     * @param {number} resId\n     * @param {boolean} [atFirstPosition]\n     * @returns {Promise<Record>} the newly created record\n     */\n    addExistingRecord(resId, atFirstPosition) {\n        return this.model.mutex.exec(async () => {\n            const record = this._createRecordDatapoint({});\n            await record._load({ resId });\n            this._addRecord(record, atFirstPosition ? 0 : this.records.length);\n            return record;\n        });\n    }\n\n    /**\n     * @param {boolean} [atFirstPosition=false]\n     * @returns {Promise<Record>}\n     */\n    addNewRecord(atFirstPosition = false) {\n        return this.model.mutex.exec(async () => {\n            await this._leaveSampleMode();\n            return this._addNewRecord(atFirstPosition);\n        });\n    }\n\n    /**\n     * Performs a search_count with the current domain to set the count. This is\n     * useful as web_search_read limits the count for performance reasons, so it\n     * might sometimes be less than the real number of records matching the domain.\n     **/\n    async fetchCount() {\n        this.count = await this.model._updateCount(this.config);\n        this.hasLimitedCount = false;\n        return this.count;\n    }\n\n    moveRecord(dataRecordId, _dataGroupId, refId, _targetGroupId) {\n        return this.resequence(dataRecordId, refId);\n    }\n\n    removeRecord(record) {\n        if (!record.isNew) {\n            throw new Error(\"removeRecord can't be called on an existing record\");\n        }\n        const index = this.records.findIndex((r) => r === record);\n        if (index < 0) {\n            return;\n        }\n        this.records.splice(index, 1);\n        this.count--;\n        return record;\n    }\n\n    async resequence(movedRecordId, targetRecordId) {\n        return this.model.mutex.exec(\n            async () =>\n                await this._resequence(this.records, this.resModel, movedRecordId, targetRecordId)\n        );\n    }\n\n    // -------------------------------------------------------------------------\n    // Protected\n    // -------------------------------------------------------------------------\n\n    async _addNewRecord(atFirstPosition) {\n        const values = await this.model._loadNewRecord({\n            resModel: this.resModel,\n            activeFields: this.activeFields,\n            fields: this.fields,\n            context: this.context,\n        });\n        const record = this._createRecordDatapoint(values, \"edit\");\n        this._addRecord(record, atFirstPosition ? 0 : this.records.length);\n        return record;\n    }\n\n    _addRecord(record, index) {\n        this.records.splice(Number.isInteger(index) ? index : this.records.length, 0, record);\n        this.count++;\n    }\n\n    _createRecordDatapoint(data, mode = \"readonly\") {\n        return new this.model.constructor.Record(\n            this.model,\n            {\n                context: this.context,\n                activeFields: this.activeFields,\n                resModel: this.resModel,\n                fields: this.fields,\n                resId: data.id || false,\n                resIds: data.id ? [data.id] : [],\n                isMonoRecord: true,\n                mode,\n            },\n            data,\n            { manuallyAdded: !data.id }\n        );\n    }\n\n    _getDPresId(record) {\n        return record.resId;\n    }\n\n    _getDPFieldValue(record, handleField) {\n        return record.data[handleField];\n    }\n\n    async _load(offset, limit, orderBy, domain) {\n        await this.model._updateConfig(\n            this.config,\n            { offset, limit, orderBy, domain },\n            { commit: this._setData.bind(this) }\n        );\n    }\n\n    _removeRecords(recordIds) {\n        const keptRecords = this.records.filter((r) => !recordIds.includes(r.id));\n        this.count -= this.records.length - keptRecords.length;\n        this.records = keptRecords;\n        if (this.offset && !this.records.length) {\n            // we weren't on the first page, and we removed all records of the current page\n            const offset = Math.max(this.offset - this.limit, 0);\n            this.model._updateConfig(this.config, { offset }, { reload: false });\n        }\n    }\n\n    _selectDomain(value) {\n        if (value) {\n            this.records.forEach((r) => (r.selected = true));\n        }\n        super._selectDomain(value);\n    }\n\n    _updateCount(data) {\n        const length = data.length;\n        if (length >= this.config.countLimit + 1) {\n            this.hasLimitedCount = true;\n            this.count = this.config.countLimit;\n        } else {\n            this.hasLimitedCount = false;\n            this.count = length;\n        }\n    }\n}\n", "import { registry } from \"@web/core/registry\";\nimport { _t } from \"@web/core/l10n/translation\";\n\nexport class FetchRecordError extends Error {\n    constructor(resIds) {\n        super(\n            _t(\n                \"It seems the records with IDs %s cannot be found. They might have been deleted.\",\n                resIds\n            )\n        );\n        this.resIds = resIds;\n    }\n}\nfunction fetchRecordErrorHandler(env, error, originalError) {\n    if (originalError instanceof FetchRecordError) {\n        env.services.notification.add(originalError.message, { sticky: true, type: \"danger\" });\n        return true;\n    }\n}\nconst errorHandlerRegistry = registry.category(\"error_handlers\");\nerrorHandlerRegistry.add(\"fetchRecordErrorHandler\", fetchRecordErrorHandler);\n", "import { Domain } from \"@web/core/domain\";\nimport { DataPoint } from \"./datapoint\";\n\n/**\n * @typedef Params\n * @property {string[]} groupBy\n */\n\nexport class Group extends DataPoint {\n    static type = \"Group\";\n\n    /**\n     * @param {import(\"./relational_model\").Config} config\n     */\n    setup(config, data) {\n        super.setup(...arguments);\n        this.groupByField = this.fields[config.groupByFieldName];\n        this.range = data.range;\n        this._rawValue = data.rawValue;\n        /** @type {number} */\n        this.count = data.count;\n        this.value = data.value;\n        this.serverValue = data.serverValue;\n        this.displayName = data.displayName;\n        this.aggregates = data.aggregates;\n        let List;\n        if (config.list.groupBy.length) {\n            List = this.model.constructor.DynamicGroupList;\n        } else {\n            List = this.model.constructor.DynamicRecordList;\n        }\n        /** @type {import(\"./dynamic_group_list\").DynamicGroupList | import(\"./dynamic_record_list\").DynamicRecordList} */\n        this.list = new List(this.model, config.list, data);\n        this._useGroupCountForList();\n        if (config.record) {\n            config.record.context = { ...config.record.context, ...config.context };\n            this.record = new this.model.constructor.Record(this.model, config.record, data.values);\n        }\n    }\n\n    // -------------------------------------------------------------------------\n    // Getters\n    // -------------------------------------------------------------------------\n\n    get groupDomain() {\n        return this.config.initialDomain;\n    }\n    get hasData() {\n        return this.count > 0;\n    }\n    get isFolded() {\n        return this.config.isFolded;\n    }\n    get records() {\n        return this.list.records;\n    }\n\n    // -------------------------------------------------------------------------\n    // Public\n    // -------------------------------------------------------------------------\n\n    async addExistingRecord(resId, atFirstPosition = false) {\n        const record = await this.list.addExistingRecord(resId, atFirstPosition);\n        this.count++;\n        return record;\n    }\n\n    async addNewRecord(_unused, atFirstPosition = false) {\n        const canProceed = await this.model.root.leaveEditMode();\n        if (canProceed) {\n            const record = await this.list.addNewRecord(atFirstPosition);\n            if (record) {\n                this.count++;\n            }\n        }\n    }\n\n    async applyFilter(filter) {\n        if (filter) {\n            await this.list.load({\n                domain: Domain.and([this.groupDomain, filter]).toList(),\n            });\n        } else {\n            await this.list.load({ domain: this.groupDomain });\n            this.count = this.list.isGrouped ? this.list.recordCount : this.list.count;\n        }\n        this.model._updateConfig(this.config, { extraDomain: filter }, { reload: false });\n    }\n\n    deleteRecords(records) {\n        return this.model.mutex.exec(() => this._deleteRecords(records));\n    }\n\n    async toggle() {\n        if (this.config.isFolded) {\n            await this.list.load();\n        }\n        this._useGroupCountForList();\n        this.model._updateConfig(\n            this.config,\n            { isFolded: !this.config.isFolded },\n            { reload: false }\n        );\n    }\n\n    // -------------------------------------------------------------------------\n    // Protected\n    // -------------------------------------------------------------------------\n\n    _addRecord(record, index) {\n        this.list._addRecord(record, index);\n        this.count++;\n    }\n\n    async _deleteRecords(records) {\n        await this.list._deleteRecords(records);\n        this.count -= records.length;\n    }\n\n    /**\n     * The count returned by web_search_read is limited (see DEFAULT_COUNT_LIMIT). However, the one\n     * returned by formatted_read_group, for each group, isn't. So in the grouped case, it might happen\n     * that the group count is more accurate than the list one. It that case, we use it on the list.\n     */\n    _useGroupCountForList() {\n        if (!this.list.isGrouped && this.list.count === this.list.config.countLimit) {\n            this.list.count = this.count;\n        }\n    }\n\n    async _removeRecords(recordIds) {\n        const idsToRemove = recordIds.filter((id) => this.list.records.some((r) => r.id === id));\n        this.list._removeRecords(idsToRemove);\n        this.count -= idsToRemove.length;\n    }\n}\n", "export class Operation {\n    constructor(operator, operand) {\n        this.operator = operator;\n        this.operand = operand;\n    }\n\n    compute(value) {\n        switch (this.operator) {\n            case \"+\":\n                return value + this.operand;\n            case \"-\":\n                return value - this.operand;\n            case \"*\":\n                return value * this.operand;\n            case \"/\":\n                return value / this.operand;\n            default:\n                throw new Error(`Unsupported operator: ${this.operator}`);\n        }\n    }\n}\n", "import { markRaw, markup, toRaw } from \"@odoo/owl\";\nimport { serializeDate, serializeDateTime } from \"@web/core/l10n/dates\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { x2ManyCommands } from \"@web/core/orm_service\";\nimport { evaluateBooleanExpr } from \"@web/core/py_js/py\";\nimport { DataPoint } from \"./datapoint\";\nimport { Operation } from \"./operation\";\nimport { FetchRecordError } from \"./errors\";\nimport {\n    createPropertyActiveField,\n    getBasicEvalContext,\n    getFieldContext,\n    getFieldsSpec,\n    parseServerValue,\n} from \"./utils\";\n\n/**\n * Redefine default 'Record' type\n * TODO: rename 'Record' to 'RelationalRecord'?\n * @template {keyof any} K\n * @template T\n * @typedef {{ [P in K]: T }} RecordType\n */\n\n/**\n * @typedef {{\n *  currentValues?: RecordType<string, unknown>;\n *  orderBys?: RecordType<string, unknown>;\n *  withInvisible?: boolean;\n *  withReadonly?: boolean;\n * }} FieldSpecifications\n *\n * @typedef {\"edit\" | \"readonly\"} Mode\n */\n\nexport class Record extends DataPoint {\n    static type = \"Record\";\n\n    /**\n     * @type {typeof DataPoint.prototype.setup<{\n     *  manuallyAdded?: boolean;\n     *  onUpdate?: () => unknown;\n     *  parentRecord?: Record;\n     *  virtualId?: string;\n     * }>}\n     */\n    setup(_config, data, options = {}) {\n        this._manuallyAdded = options.manuallyAdded === true;\n        this._onUpdate = options.onUpdate || (() => {});\n        this._parentRecord = options.parentRecord;\n        this.canSaveOnUpdate = !options.parentRecord;\n        this._virtualId = options.virtualId || false;\n        this._isEvalContextReady = false;\n\n        // Be careful that pending changes might not have been notified yet, so the \"dirty\" flag may\n        // be false even though there are changes in a field. Consider calling \"isDirty()\" instead.\n        this.dirty = false;\n        this.selected = false;\n\n        /** @type {Set<string>} */\n        this._invalidFields = new Set();\n        /** @type {Set<string>} */\n        this._unsetRequiredFields = markRaw(new Set());\n        this._closeInvalidFieldsNotification = () => {};\n\n        const parentRecord = this._parentRecord;\n        if (parentRecord) {\n            this.evalContext = {\n                get parent() {\n                    return parentRecord.evalContext;\n                },\n            };\n            this.evalContextWithVirtualIds = {\n                get parent() {\n                    return parentRecord.evalContextWithVirtualIds;\n                },\n            };\n        } else {\n            this.evalContext = {};\n            this.evalContextWithVirtualIds = {};\n        }\n        const missingFields = this.fieldNames.filter((fieldName) => !(fieldName in data));\n        data = { ...this._getDefaultValues(missingFields), ...data };\n        // In db, char, text and html fields can be not set (NULL) and set to the empty string. In\n        // the UI, there's no difference, but in the eval context, it's not the same. The next\n        // structure keeps track of the server values we received for those fields (which can thus\n        // be false or a string). This allows us to properly build the eval context, and to always\n        // expose string values (false fallbacks on the empty string) in this.data.\n        this._textValues = markRaw({});\n        this._setData(data);\n    }\n\n    /**\n     * @param {RecordType<string, unknown>} data\n     * @param {FieldSpecifications} [params]\n     */\n    _setData(data, { orderBys, keepChanges } = {}) {\n        this._isEvalContextReady = false;\n        if (this.resId) {\n            this._values = this._parseServerValues(data, { orderBys });\n            Object.assign(this._textValues, this._getTextValues(data));\n        } else {\n            const allVals = { ...this._getDefaultValues(), ...data };\n            this._values = markRaw(this._parseServerValues(allVals, { orderBys }));\n            Object.assign(this._textValues, this._getTextValues(allVals));\n        }\n        if (!keepChanges) {\n            this._changes = markRaw({});\n        }\n        this.dirty = false;\n        this.data = { ...this._values, ...this._changes };\n        this._setEvalContext();\n        this._initialTextValues = { ...this._textValues };\n\n        this._invalidFields.clear();\n        if (!this.isNew && this.isInEdition && !this._parentRecord) {\n            this._checkValidity();\n        }\n        this._savePoint = undefined;\n    }\n\n    // -------------------------------------------------------------------------\n    // Getter\n    // -------------------------------------------------------------------------\n\n    get canBeAbandoned() {\n        return this.isNew && !this.dirty && this._manuallyAdded;\n    }\n\n    get hasData() {\n        return true;\n    }\n\n    /** @type {boolean} */\n    get isActive() {\n        if (\"active\" in this.activeFields) {\n            return this.data.active;\n        } else if (\"x_active\" in this.activeFields) {\n            return this.data.x_active;\n        }\n        return true;\n    }\n\n    get isInEdition() {\n        if (this.config.mode === \"readonly\") {\n            return false;\n        } else {\n            return this.config.mode === \"edit\" || !this.resId;\n        }\n    }\n\n    get isNew() {\n        return !this.resId;\n    }\n\n    get isValid() {\n        return !this._invalidFields.size;\n    }\n\n    get resId() {\n        return this.config.resId;\n    }\n\n    get resIds() {\n        return this.config.resIds;\n    }\n\n    // -------------------------------------------------------------------------\n    // Public\n    // -------------------------------------------------------------------------\n\n    archive() {\n        return this.model.mutex.exec(() => this._toggleArchive(true));\n    }\n\n    async checkValidity({ displayNotification } = {}) {\n        if (!this._urgentSave) {\n            await this.model._askChanges();\n        }\n        return this._checkValidity({ displayNotification });\n    }\n\n    delete() {\n        return this.model.mutex.exec(async () => {\n            const unlinked = await this.model.orm.unlink(this.resModel, [this.resId], {\n                context: this.context,\n            });\n            if (!unlinked) {\n                return false;\n            }\n            const resIds = this.resIds.slice();\n            const index = resIds.indexOf(this.resId);\n            resIds.splice(index, 1);\n            const resId = resIds[Math.min(index, resIds.length - 1)] || false;\n            if (resId) {\n                await this.model.load({ resId, resIds });\n            } else {\n                this.model._updateConfig(this.config, { resId: false }, { reload: false });\n                this.dirty = false;\n                this._changes = markRaw({});\n                this._values = markRaw(this._parseServerValues(this._getDefaultValues()));\n                this._textValues = markRaw({});\n                this.data = { ...this._values };\n                this._setEvalContext();\n            }\n        });\n    }\n\n    async discard() {\n        if (this.model._closeUrgentSaveNotification) {\n            this.model._closeUrgentSaveNotification();\n        }\n        await this.model._askChanges();\n        return this.model.mutex.exec(() => this._discard());\n    }\n\n    duplicate() {\n        return this.model.mutex.exec(async () => {\n            const kwargs = { context: this.context };\n            const index = this.resIds.indexOf(this.resId);\n            const [resId] = await this.model.orm.call(\n                this.resModel,\n                \"copy\",\n                [[this.resId]],\n                kwargs\n            );\n            const resIds = this.resIds.slice();\n            resIds.splice(index + 1, 0, resId);\n            await this.model.load({ resId, resIds, mode: \"edit\" });\n        });\n    }\n\n    /**\n     * @param {FieldSpecifications} [params]\n     */\n    async getChanges({ withReadonly } = {}) {\n        await this.model._askChanges();\n        return this.model.mutex.exec(() => this._getChanges(this._changes, { withReadonly }));\n    }\n\n    async isDirty() {\n        await this.model._askChanges();\n        return this.dirty;\n    }\n\n    /**\n     * @param {string} fieldName\n     */\n    isFieldInvalid(fieldName) {\n        return this._invalidFields.has(fieldName);\n    }\n\n    load() {\n        if (arguments.length > 0) {\n            throw new Error(\"Record.load() does not accept arguments\");\n        }\n        return this.model.mutex.exec(() => this._load());\n    }\n\n    /**\n     * @param {Parameters<Record[\"_save\"]>[0]} options\n     */\n    async save(options) {\n        await this.model._askChanges();\n        return this.model.mutex.exec(() => this._save(options));\n    }\n\n    /**\n     * @param {string} fieldName\n     */\n    async setInvalidField(fieldName) {\n        this.dirty = true;\n        return this._setInvalidField(fieldName);\n    }\n\n    /**\n     * @param {string} fieldName\n     */\n    async resetFieldValidity(fieldName) {\n        this.dirty = true;\n        return this._resetFieldValidity(fieldName);\n    }\n\n    /**\n     * @param {Mode} mode\n     */\n    switchMode(mode) {\n        return this.model.mutex.exec(() => this._switchMode(mode));\n    }\n\n    toggleSelection(selected) {\n        return this.model.mutex.exec(() => {\n            this._toggleSelection(selected);\n        });\n    }\n\n    unarchive() {\n        return this.model.mutex.exec(() => this._toggleArchive(false));\n    }\n\n    update(changes, { save } = {}) {\n        if (this.model._urgentSave) {\n            return this._update(changes);\n        }\n        return this.model.mutex.exec(async () => {\n            await this._update(changes, { withoutOnchange: save });\n            if (save && this.canSaveOnUpdate) {\n                return this._save();\n            }\n        });\n    }\n\n    async urgentSave() {\n        this.model._urgentSave = true;\n        this.model.bus.trigger(\"WILL_SAVE_URGENTLY\");\n        const succeeded = await this._save({ reload: false });\n        this.model._urgentSave = false;\n        return succeeded;\n    }\n\n    // -------------------------------------------------------------------------\n    // Protected\n    // -------------------------------------------------------------------------\n\n    _addSavePoint() {\n        this._savePoint = markRaw({\n            dirty: this.dirty,\n            textValues: { ...this._textValues },\n            changes: { ...this._changes },\n        });\n        for (const fieldName in this._changes) {\n            if ([\"one2many\", \"many2many\"].includes(this.fields[fieldName].type)) {\n                this._changes[fieldName]._addSavePoint();\n            }\n        }\n    }\n\n    _applyChanges(changes, serverChanges = {}) {\n        // We need to generate the undo function before applying the changes\n        const initialTextValues = { ...this._textValues };\n        const initialChanges = { ...this._changes };\n        const initialData = { ...toRaw(this.data) };\n        const invalidFields = [...toRaw(this._invalidFields)];\n        const undoChanges = () => {\n            for (const fieldName of invalidFields) {\n                this.setInvalidField(fieldName);\n            }\n            Object.assign(this.data, initialData);\n            this._changes = markRaw(initialChanges);\n            Object.assign(this._textValues, initialTextValues);\n            this._setEvalContext();\n        };\n\n        // Apply changes\n        for (const fieldName in changes) {\n            let change = changes[fieldName];\n            if (change instanceof Operation) {\n                change = change.compute(this.data[fieldName]);\n            }\n            this._changes[fieldName] = change;\n            this.data[fieldName] = change;\n            if (this.fields[fieldName].type === \"html\") {\n                this._textValues[fieldName] = change === false ? false : change.toString();\n            } else if ([\"char\", \"text\"].includes(this.fields[fieldName].type)) {\n                this._textValues[fieldName] = change;\n            }\n        }\n\n        // Apply server changes\n        const parsedChanges = this._parseServerValues(serverChanges, { currentValues: this.data });\n        for (const fieldName in parsedChanges) {\n            this._changes[fieldName] = parsedChanges[fieldName];\n            this.data[fieldName] = parsedChanges[fieldName];\n        }\n        Object.assign(this._textValues, this._getTextValues(serverChanges));\n\n        this._setEvalContext();\n\n        // mark changed fields as valid if they were not, and re-evaluate required attributes\n        // for all fields, as some of them might still be unset but become valid with those changes\n        this._removeInvalidFields(...Object.keys(changes), ...Object.keys(serverChanges));\n        this._checkValidity({ removeInvalidOnly: true });\n        return undoChanges;\n    }\n\n    _applyDefaultValues() {\n        const fieldNames = this.fieldNames.filter((fieldName) => !(fieldName in this.data));\n        const defaultValues = this._getDefaultValues(fieldNames);\n        if (this.isNew) {\n            this._applyChanges({}, defaultValues);\n        } else {\n            this._applyValues(defaultValues);\n        }\n    }\n\n    _applyValues(values) {\n        const newValues = this._parseServerValues(values);\n        Object.assign(this._values, newValues);\n        for (const fieldName in newValues) {\n            if (fieldName in this._changes) {\n                if ([\"one2many\", \"many2many\"].includes(this.fields[fieldName].type)) {\n                    this._changes[fieldName] = newValues[fieldName];\n                }\n            }\n        }\n        Object.assign(this.data, this._values, this._changes);\n        const textValues = this._getTextValues(values);\n        Object.assign(this._initialTextValues, textValues);\n        Object.assign(this._textValues, textValues, this._getTextValues(this._changes));\n        this._setEvalContext();\n    }\n\n    _checkValidity({ silent, displayNotification, removeInvalidOnly } = {}) {\n        const unsetRequiredFields = new Set();\n        for (const fieldName in this.activeFields) {\n            const fieldType = this.fields[fieldName].type;\n            if (this._isInvisible(fieldName) || this.fields[fieldName].relatedPropertyField) {\n                continue;\n            }\n            switch (fieldType) {\n                case \"boolean\":\n                case \"float\":\n                case \"integer\":\n                case \"monetary\":\n                    continue;\n                case \"html\":\n                    if (this._isRequired(fieldName) && this.data[fieldName].length === 0) {\n                        unsetRequiredFields.add(fieldName);\n                    }\n                    break;\n                case \"one2many\":\n                case \"many2many\": {\n                    const list = this.data[fieldName];\n                    if (\n                        (this._isRequired(fieldName) && !list.count) ||\n                        !list.records.every(\n                            (r) => !r.dirty || r._checkValidity({ silent, removeInvalidOnly })\n                        )\n                    ) {\n                        unsetRequiredFields.add(fieldName);\n                    }\n                    break;\n                }\n                case \"properties\": {\n                    const value = this.data[fieldName];\n                    if (value) {\n                        const ok = value.every(\n                            (propertyDefinition) =>\n                                propertyDefinition.name &&\n                                propertyDefinition.name.length &&\n                                propertyDefinition.string &&\n                                propertyDefinition.string.length\n                        );\n                        if (!ok) {\n                            unsetRequiredFields.add(fieldName);\n                        }\n                    }\n                    break;\n                }\n                case \"json\": {\n                    if (\n                        this._isRequired(fieldName) &&\n                        (!this.data[fieldName] || !Object.keys(this.data[fieldName]).length)\n                    ) {\n                        unsetRequiredFields.add(fieldName);\n                    }\n                    break;\n                }\n                default:\n                    if (!this.data[fieldName] && this._isRequired(fieldName)) {\n                        unsetRequiredFields.add(fieldName);\n                    }\n            }\n        }\n\n        if (silent) {\n            return !unsetRequiredFields.size;\n        }\n\n        if (removeInvalidOnly) {\n            for (const fieldName of Array.from(this._unsetRequiredFields)) {\n                if (!unsetRequiredFields.has(fieldName)) {\n                    this._unsetRequiredFields.delete(fieldName);\n                    this._invalidFields.delete(fieldName);\n                }\n            }\n        } else {\n            for (const fieldName of Array.from(this._unsetRequiredFields)) {\n                this._invalidFields.delete(fieldName);\n            }\n            this._unsetRequiredFields.clear();\n            for (const fieldName of unsetRequiredFields) {\n                this._unsetRequiredFields.add(fieldName);\n                this._invalidFields.add(fieldName);\n            }\n        }\n        const isValid = !this._invalidFields.size;\n        if (!isValid && displayNotification) {\n            this._closeInvalidFieldsNotification = this._displayInvalidFieldNotification();\n        }\n        return isValid;\n    }\n\n    /**\n     * Given a possibily incomplete value for a many2one field (i.e. a object { id, display_name } but\n     * with id and/or display_name being undefined), return the complete value as follows:\n     *  - if a display_name is given but no id, perform a name_create to get an id\n     *  - if an id is given but display_name is undefined, call web_read to get the display_name\n     *  - if both id and display_name are given, return the value as is\n     *  - in any other cases, return false\n     *\n     * @param {{ id?: number; display_name?: string }} value\n     * @param {string} fieldName\n     * @param {string} resModel\n     * @returns {Promise<false | { id: number; display_name: string; }>} the completed record { id, display_name } or false\n     */\n    async _completeMany2OneValue(value, fieldName, resModel) {\n        const resId = value.id;\n        const displayName = value.display_name;\n        if (!resId && !displayName) {\n            return false;\n        }\n        const context = getFieldContext(this, fieldName);\n        if (!resId && displayName !== undefined) {\n            const pair = await this.model.orm.call(resModel, \"name_create\", [displayName], {\n                context,\n            });\n            return pair && { id: pair[0], display_name: pair[1] };\n        }\n        if (resId && displayName === undefined) {\n            const fieldSpec = { display_name: {} };\n            if (this.activeFields[fieldName].related) {\n                Object.assign(\n                    fieldSpec,\n                    getFieldsSpec(\n                        this.activeFields[fieldName].related.activeFields,\n                        this.activeFields[fieldName].related.fields,\n                        getBasicEvalContext(this.config)\n                    )\n                );\n            }\n            const kwargs = {\n                context,\n                specification: fieldSpec,\n            };\n            const records = await this.model.orm.webRead(resModel, [resId], kwargs);\n            return records[0];\n        }\n        return value;\n    }\n\n    _computeDataContext() {\n        const dataContext = {};\n        const x2manyDataContext = {\n            withVirtualIds: {},\n            withoutVirtualIds: {},\n        };\n        const data = toRaw(this.data);\n        for (const fieldName in data) {\n            const value = data[fieldName];\n            const field = this.fields[fieldName];\n            if (field.relatedPropertyField) {\n                continue;\n            }\n            if ([\"char\", \"text\", \"html\"].includes(field.type)) {\n                dataContext[fieldName] = this._textValues[fieldName];\n            } else if (field.type === \"one2many\" || field.type === \"many2many\") {\n                x2manyDataContext.withVirtualIds[fieldName] = value.currentIds;\n                x2manyDataContext.withoutVirtualIds[fieldName] = value.currentIds.filter(\n                    (id) => typeof id === \"number\"\n                );\n            } else if (value && field.type === \"date\") {\n                dataContext[fieldName] = serializeDate(value);\n            } else if (value && field.type === \"datetime\") {\n                dataContext[fieldName] = serializeDateTime(value);\n            } else if (value && field.type === \"many2one\") {\n                dataContext[fieldName] = value.id;\n            } else if (value && field.type === \"reference\") {\n                dataContext[fieldName] = `${value.resModel},${value.resId}`;\n            } else if (field.type === \"properties\") {\n                dataContext[fieldName] = value.filter(\n                    (property) => !property.definition_deleted !== false\n                );\n            } else {\n                dataContext[fieldName] = value;\n            }\n        }\n        dataContext.id = this.resId || false;\n        return {\n            withVirtualIds: { ...dataContext, ...x2manyDataContext.withVirtualIds },\n            withoutVirtualIds: { ...dataContext, ...x2manyDataContext.withoutVirtualIds },\n        };\n    }\n\n    /**\n     * @param {RecordType<string, unknown>} data\n     * @param {string} fieldName\n     * @param {FieldSpecifications} [params]\n     */\n    _createStaticListDatapoint(data, fieldName, { orderBys } = {}) {\n        const { related, limit, defaultOrderBy } = this.activeFields[fieldName];\n        const relatedActiveFields = (related && related.activeFields) || {};\n        const config = {\n            resModel: this.fields[fieldName].relation,\n            activeFields: relatedActiveFields,\n            fields: (related && related.fields) || {},\n            relationField: this.fields[fieldName].relation_field || false,\n            offset: 0,\n            resIds: data.map((r) => r.id),\n            orderBy: orderBys?.[fieldName] || defaultOrderBy || [],\n            limit: limit || (Object.keys(relatedActiveFields).length ? Number.MAX_SAFE_INTEGER : 1),\n            context: {}, // will be set afterwards, see \"_updateContext\" in \"_setEvalContext\"\n        };\n        const options = {\n            onUpdate: ({ withoutOnchange } = {}) =>\n                this._update({ [fieldName]: [] }, { withoutOnchange }),\n            parent: this,\n        };\n        return new this.model.constructor.StaticList(this.model, config, data, options);\n    }\n\n    _discard() {\n        for (const fieldName in this._changes) {\n            if ([\"one2many\", \"many2many\"].includes(this.fields[fieldName].type)) {\n                this._changes[fieldName]._discard();\n            }\n        }\n        if (this._savePoint) {\n            this.dirty = this._savePoint.dirty;\n            this._changes = markRaw({ ...this._savePoint.changes });\n            this._textValues = markRaw({ ...this._savePoint.textValues });\n        } else {\n            this.dirty = false;\n            this._changes = markRaw({});\n            this._textValues = markRaw({ ...this._initialTextValues });\n        }\n        this.data = { ...this._values, ...this._changes };\n        this._savePoint = undefined;\n        this._setEvalContext();\n        this._invalidFields.clear();\n        if (!this.isNew) {\n            this._checkValidity();\n        }\n        this._closeInvalidFieldsNotification();\n        this._closeInvalidFieldsNotification = () => {};\n        this._restoreActiveFields();\n    }\n\n    _displayInvalidFieldNotification() {\n        return this.model.notification.add(_t(\"Missing required fields\"), { type: \"danger\" });\n    }\n\n    _formatServerValue(fieldType, value) {\n        if (fieldType === \"date\") {\n            return value ? serializeDate(value) : false;\n        } else if (fieldType === \"datetime\") {\n            return value ? serializeDateTime(value) : false;\n        } else if (fieldType === \"char\" || fieldType === \"text\") {\n            return value !== \"\" ? value : false;\n        } else if (fieldType === \"html\") {\n            return value && value.length ? value : false;\n        } else if (fieldType === \"many2one\") {\n            return value ? value.id : false;\n        } else if (fieldType === \"many2one_reference\") {\n            return value ? value.resId : 0;\n        } else if (fieldType === \"reference\") {\n            return value && value.resModel && value.resId\n                ? `${value.resModel},${value.resId}`\n                : false;\n        } else if (fieldType === \"properties\") {\n            return value.map((property) => {\n                property = { ...property };\n                for (const key of [\"value\", \"default\"]) {\n                    let value;\n                    if (property.type === \"many2one\") {\n                        value = property[key] && [property[key].id, property[key].display_name];\n                    } else if (\n                        (property.type === \"date\" || property.type === \"datetime\") &&\n                        typeof property[key] === \"string\"\n                    ) {\n                        // TO REMOVE: need refactoring PropertyField to use the same format as the server\n                        value = property[key];\n                    } else if (property[key] !== undefined) {\n                        value = this._formatServerValue(property.type, property[key]);\n                    }\n                    property[key] = value;\n                }\n                return property;\n            });\n        }\n        return value;\n    }\n\n    /**\n     * @param {RecordType<string, unknown>} [changes]\n     * @param {FieldSpecifications} [params]\n     */\n    _getChanges(changes = this._changes, { withReadonly } = {}) {\n        if (!this.resId) {\n            // Apply the initial changes when the record is new\n            changes = { ...this._values, ...changes };\n        }\n        const result = {};\n        for (const [fieldName, value] of Object.entries(changes)) {\n            const field = this.fields[fieldName];\n            if (fieldName === \"id\") {\n                continue;\n            }\n            if (\n                !withReadonly &&\n                fieldName in this.activeFields &&\n                this._isReadonly(fieldName) &&\n                !this.activeFields[fieldName].forceSave\n            ) {\n                continue;\n            }\n            if (field.relatedPropertyField) {\n                continue;\n            }\n            if (field.type === \"one2many\" || field.type === \"many2many\") {\n                const commands = value._getCommands({ withReadonly });\n                if (!this.isNew && !commands.length && !withReadonly) {\n                    continue;\n                }\n                result[fieldName] = commands;\n            } else {\n                result[fieldName] = this._formatServerValue(field.type, value);\n            }\n        }\n        return result;\n    }\n\n    _getDefaultValues(fieldNames = this.fieldNames) {\n        const defaultValues = {};\n        for (const fieldName of fieldNames) {\n            switch (this.fields[fieldName].type) {\n                case \"integer\":\n                case \"float\":\n                case \"monetary\":\n                    defaultValues[fieldName] = fieldName === \"id\" ? false : 0;\n                    break;\n                case \"one2many\":\n                case \"many2many\":\n                    defaultValues[fieldName] = [];\n                    break;\n                default:\n                    defaultValues[fieldName] = false;\n            }\n        }\n        return defaultValues;\n    }\n\n    /**\n     * @param {RecordType<string, unknown>} values\n     */\n    _getTextValues(values) {\n        const textValues = {};\n        for (const fieldName in values) {\n            if (!this.activeFields[fieldName]) {\n                continue;\n            }\n            if ([\"char\", \"text\", \"html\"].includes(this.fields[fieldName].type)) {\n                textValues[fieldName] = values[fieldName];\n            }\n        }\n        return textValues;\n    }\n\n    /**\n     * @param {string} fieldName\n     */\n    _isInvisible(fieldName) {\n        const invisible = this.activeFields[fieldName].invisible;\n        return invisible ? evaluateBooleanExpr(invisible, this.evalContextWithVirtualIds) : false;\n    }\n\n    /**\n     * @param {string} fieldName\n     */\n    _isReadonly(fieldName) {\n        const readonly = this.activeFields[fieldName].readonly;\n        return readonly ? evaluateBooleanExpr(readonly, this.evalContextWithVirtualIds) : false;\n    }\n\n    /**\n     * @param {string} fieldName\n     */\n    _isRequired(fieldName) {\n        const required = this.activeFields[fieldName].required;\n        return required ? evaluateBooleanExpr(required, this.evalContextWithVirtualIds) : false;\n    }\n\n    async _load(nextConfig = {}) {\n        if (\"resId\" in nextConfig && this.resId) {\n            throw new Error(\"Cannot change resId of a record\");\n        }\n        await this.model._updateConfig(this.config, nextConfig, {\n            commit: (values) => {\n                if (this.resId) {\n                    this.model._updateSimilarRecords(this, values);\n                }\n                this._setData(values);\n            },\n        });\n    }\n\n    /**\n     * This function extracts all properties and adds them to fields and activeFields.\n     *\n     * @param {Object[]} properties the list of properties to be extracted\n     * @param {string} fieldName name of the field containing the properties\n     * @param {Array} parent Array with ['id, 'display_name'], representing the record to which the definition of properties is linked\n     * @param {Object} currentValues current values of the record\n     * @returns An object containing as key `${fieldName}.${property.name}` and as value the value of the property\n     */\n    _processProperties(properties, fieldName, parent, currentValues = {}) {\n        const data = {};\n\n        const hasCurrentValues = Object.keys(currentValues).length > 0;\n        for (const property of properties) {\n            const propertyFieldName = `${fieldName}.${property.name}`;\n\n            // Add Unknown Property Field and ActiveField\n            if (hasCurrentValues || !this.fields[propertyFieldName]) {\n                this.fields[propertyFieldName] = {\n                    ...property,\n                    name: propertyFieldName,\n                    relatedPropertyField: {\n                        name: fieldName,\n                    },\n                    propertyName: property.name,\n                    relation: property.comodel,\n                    sortable: ![\"many2one\", \"many2many\", \"tags\"].includes(property.type),\n                };\n            }\n            if (hasCurrentValues || !this.activeFields[propertyFieldName]) {\n                this.activeFields[propertyFieldName] = createPropertyActiveField(property);\n            }\n\n            if (!this.activeFields[propertyFieldName].relatedPropertyField) {\n                this.activeFields[propertyFieldName].relatedPropertyField = {\n                    name: fieldName,\n                    id: parent?.id,\n                    displayName: parent?.display_name,\n                };\n            }\n\n            // Extract property data\n            if (property.type === \"many2many\") {\n                let staticList = currentValues[propertyFieldName];\n                if (!staticList) {\n                    staticList = this._createStaticListDatapoint(\n                        (property.value || []).map((record) => ({\n                            id: record[0],\n                            display_name: record[1],\n                        })),\n                        propertyFieldName\n                    );\n                }\n                data[propertyFieldName] = staticList;\n            } else if (property.type === \"many2one\") {\n                data[propertyFieldName] =\n                    property.value && property.value.display_name === null\n                        ? { id: property.value.id, display_name: _t(\"No Access\") }\n                        : property.value;\n            } else {\n                data[propertyFieldName] = property.value ?? false;\n            }\n        }\n\n        return data;\n    }\n\n    /**\n     * @param {RecordType<string, unknown>} serverValues\n     * @param {FieldSpecifications} [params]\n     */\n    _parseServerValues(serverValues, { currentValues, orderBys } = {}) {\n        const parsedValues = {};\n        if (!serverValues) {\n            return parsedValues;\n        }\n        for (const fieldName in serverValues) {\n            const value = serverValues[fieldName];\n            if (!this.activeFields[fieldName]) {\n                continue;\n            }\n            const field = this.fields[fieldName];\n            if (field.type === \"one2many\" || field.type === \"many2many\") {\n                let staticList = currentValues?.[fieldName];\n                let valueIsCommandList = true;\n                // value can be a list of records or a list of commands (new record)\n                valueIsCommandList = value.length > 0 && Array.isArray(value[0]);\n                if (!staticList) {\n                    let data = valueIsCommandList ? [] : value;\n                    if (data.length > 0 && typeof data[0] === \"number\") {\n                        data = data.map((resId) => ({ id: resId }));\n                    }\n                    staticList = this._createStaticListDatapoint(data, fieldName, { orderBys });\n                    if (valueIsCommandList) {\n                        staticList._applyInitialCommands(value);\n                    }\n                } else if (valueIsCommandList) {\n                    staticList._applyCommands(value);\n                }\n                parsedValues[fieldName] = staticList;\n            } else {\n                parsedValues[fieldName] = parseServerValue(field, value);\n                if (field.type === \"properties\") {\n                    const parent = serverValues[field.definition_record];\n                    Object.assign(\n                        parsedValues,\n                        this._processProperties(\n                            parsedValues[fieldName],\n                            fieldName,\n                            parent,\n                            currentValues\n                        )\n                    );\n                }\n            }\n        }\n        return parsedValues;\n    }\n\n    async _preprocessMany2oneChanges(changes) {\n        const proms = Object.entries(changes)\n            .filter(([fieldName]) => this.fields[fieldName].type === \"many2one\")\n            .map(async ([fieldName, value]) => {\n                if (!value) {\n                    changes[fieldName] = false;\n                } else if (!this.activeFields[fieldName]) {\n                    changes[fieldName] = value;\n                } else {\n                    const relation = this.fields[fieldName].relation;\n                    return this._completeMany2OneValue(value, fieldName, relation).then((v) => {\n                        changes[fieldName] = v;\n                    });\n                }\n            });\n        return Promise.all(proms);\n    }\n\n    async _preprocessMany2OneReferenceChanges(changes) {\n        const proms = Object.entries(changes)\n            .filter(([fieldName]) => this.fields[fieldName].type === \"many2one_reference\")\n            .map(async ([fieldName, value]) => {\n                if (!value) {\n                    changes[fieldName] = false;\n                } else if (typeof value === \"number\") {\n                    // Many2OneReferenceInteger field only manipulates the id\n                    changes[fieldName] = { resId: value };\n                } else {\n                    const relation = this.data[this.fields[fieldName].model_field];\n                    return this._completeMany2OneValue(\n                        { id: value.resId, display_name: value.displayName },\n                        fieldName,\n                        relation\n                    ).then((v) => {\n                        changes[fieldName] = { resId: v.id, displayName: v.display_name };\n                    });\n                }\n            });\n        return Promise.all(proms);\n    }\n\n    async _preprocessReferenceChanges(changes) {\n        const proms = Object.entries(changes)\n            .filter(([fieldName]) => this.fields[fieldName].type === \"reference\")\n            .map(async ([fieldName, value]) => {\n                if (!value) {\n                    changes[fieldName] = false;\n                } else {\n                    return this._completeMany2OneValue(\n                        { id: value.resId, display_name: value.displayName },\n                        fieldName,\n                        value.resModel\n                    ).then((v) => {\n                        changes[fieldName] = {\n                            resId: v.id,\n                            resModel: value.resModel,\n                            displayName: v.display_name,\n                        };\n                    });\n                }\n            });\n        return Promise.all(proms);\n    }\n\n    async _preprocessX2manyChanges(changes) {\n        for (const [fieldName, value] of Object.entries(changes)) {\n            if (\n                this.fields[fieldName].type !== \"one2many\" &&\n                this.fields[fieldName].type !== \"many2many\"\n            ) {\n                continue;\n            }\n            const list = this.data[fieldName];\n            for (const command of value) {\n                switch (command[0]) {\n                    case x2ManyCommands.SET:\n                        await list._replaceWith(command[2]);\n                        break;\n                    default:\n                        await list._applyCommands([command]);\n                }\n            }\n            changes[fieldName] = list;\n        }\n    }\n\n    _preprocessPropertiesChanges(changes) {\n        for (const [fieldName, value] of Object.entries(changes)) {\n            const field = this.fields[fieldName];\n            if (field.type === \"properties\") {\n                const parent =\n                    changes[field.definition_record] || this.data[field.definition_record];\n                Object.assign(\n                    changes,\n                    this._processProperties(value, fieldName, parent, this.data)\n                );\n            } else if (field && field.relatedPropertyField) {\n                const [propertyFieldName, propertyName] = field.name.split(\".\");\n                const propertiesData = this.data[propertyFieldName] || [];\n                if (!propertiesData.find((property) => property.name === propertyName)) {\n                    // try to change the value of a properties that has a different parent\n                    this.model.notification.add(\n                        _t(\n                            \"This record belongs to a different parent so you can not change this property.\"\n                        ),\n                        { type: \"warning\" }\n                    );\n                    return;\n                }\n                changes[propertyFieldName] = propertiesData.map((property) =>\n                    property.name === propertyName ? { ...property, value } : property\n                );\n            }\n        }\n    }\n\n    _preprocessHtmlChanges(changes) {\n        for (const [fieldName, value] of Object.entries(changes)) {\n            if (this.fields[fieldName].type === \"html\") {\n                changes[fieldName] = value === false ? false : markup(value);\n            }\n        }\n    }\n\n    /**\n     * @param {...string} fieldNames\n     */\n    _removeInvalidFields(...fieldNames) {\n        for (const fieldName of fieldNames) {\n            this._invalidFields.delete(fieldName);\n        }\n    }\n\n    _restoreActiveFields() {\n        if (!this._activeFieldsToRestore) {\n            return;\n        }\n        this.model._updateConfig(\n            this.config,\n            {\n                activeFields: { ...this._activeFieldsToRestore },\n            },\n            { reload: false }\n        );\n        this._activeFieldsToRestore = undefined;\n    }\n\n    async _save({ reload = true, onError, nextId } = {}) {\n        if (this.model._closeUrgentSaveNotification) {\n            this.model._closeUrgentSaveNotification();\n        }\n        const creation = !this.resId;\n        if (nextId) {\n            if (creation) {\n                throw new Error(\"Cannot set nextId on a new record\");\n            }\n            reload = true;\n        }\n        // before saving, abandon new invalid, untouched records in x2manys\n        for (const fieldName in this.activeFields) {\n            const field = this.fields[fieldName];\n            if ([\"one2many\", \"many2many\"].includes(field.type) && !field.relatedPropertyField) {\n                this.data[fieldName]._abandonRecords();\n            }\n        }\n        if (!this._checkValidity({ displayNotification: true })) {\n            return false;\n        }\n        const changes = this._getChanges();\n        delete changes.id; // id never changes, and should not be written\n        if (!creation && !Object.keys(changes).length) {\n            if (nextId) {\n                return this.model.load({ resId: nextId });\n            }\n            this._changes = markRaw({});\n            this.data = { ...this._values };\n            this.dirty = false;\n            return true;\n        }\n        if (\n            this.model._urgentSave &&\n            this.model.useSendBeaconToSaveUrgently &&\n            !this.model.env.inDialog\n        ) {\n            // We are trying to save urgently because the user is closing the page. To\n            // ensure that the save succeeds, we can't do a classic rpc, as these requests\n            // can be cancelled (payload too heavy, network too slow, computer too fast...).\n            // We instead use sendBeacon, which isn't cancellable. However, it has limited\n            // payload (typically < 64k). So we try to save with sendBeacon, and if it\n            // doesn't work, we will prevent the page from unloading.\n            const route = `/web/dataset/call_kw/${this.resModel}/web_save`;\n            const params = {\n                model: this.resModel,\n                method: \"web_save\",\n                args: [this.resId ? [this.resId] : [], changes],\n                kwargs: { context: this.context, specification: {} },\n            };\n            const data = { jsonrpc: \"2.0\", method: \"call\", params };\n            const blob = new Blob([JSON.stringify(data)], { type: \"application/json\" });\n            const succeeded = navigator.sendBeacon(route, blob);\n            if (succeeded) {\n                this._changes = markRaw({});\n                this.dirty = false;\n            } else {\n                this.model._closeUrgentSaveNotification = this.model.notification.add(\n                    _t(\n                        `Heads up! Your recent changes are too large to save automatically. Please click the %(upload_icon)s button now to ensure your work is saved before you exit this tab.`,\n                        { upload_icon: markup`<i class=\"fa fa-cloud-upload fa-fw\"></i>` }\n                    ),\n                    { sticky: true }\n                );\n            }\n            return succeeded;\n        }\n        const canProceed = await this.model.hooks.onWillSaveRecord(this, changes);\n        if (canProceed === false) {\n            return false;\n        }\n        // keep x2many orderBy if we stay on the same record\n        const orderBys = {};\n        if (!nextId) {\n            for (const fieldName of this.fieldNames) {\n                if ([\"one2many\", \"many2many\"].includes(this.fields[fieldName].type)) {\n                    orderBys[fieldName] = this.data[fieldName].orderBy;\n                }\n            }\n        }\n        let fieldSpec = {};\n        if (reload) {\n            fieldSpec = getFieldsSpec(\n                this.activeFields,\n                this.fields,\n                getBasicEvalContext(this.config),\n                { orderBys }\n            );\n        }\n        const kwargs = {\n            context: this.context,\n            specification: fieldSpec,\n            next_id: nextId,\n        };\n        let records = [];\n        try {\n            records = await this.model.orm.webSave(\n                this.resModel,\n                this.resId ? [this.resId] : [],\n                changes,\n                kwargs\n            );\n        } catch (e) {\n            if (onError) {\n                return onError(e, {\n                    discard: () => this._discard(),\n                    retry: () => this._save(...arguments),\n                });\n            }\n            if (!this.isInEdition) {\n                await this._load({});\n            }\n            throw e;\n        }\n        if (reload && !records.length) {\n            throw new FetchRecordError([nextId || this.resId]);\n        }\n        if (creation) {\n            const resId = records[0].id;\n            const resIds = this.resIds.concat([resId]);\n            this.model._updateConfig(this.config, { resId, resIds }, { reload: false });\n        }\n        await this.model.hooks.onRecordSaved(this, changes);\n        if (reload) {\n            if (this.resId) {\n                this.model._updateSimilarRecords(this, records[0]);\n            }\n            if (nextId) {\n                this.model._updateConfig(this.config, { resId: nextId }, { reload: false });\n            }\n            if (this.config.isRoot) {\n                this.model.hooks.onWillLoadRoot(this.config);\n            }\n            this._setData(records[0], { orderBys });\n        } else {\n            this._values = markRaw({ ...this._values, ...this._changes });\n            if (\"id\" in this.activeFields) {\n                this._values.id = records[0].id;\n            }\n            for (const fieldName in this.activeFields) {\n                const field = this.fields[fieldName];\n                if ([\"one2many\", \"many2many\"].includes(field.type) && !field.relatedPropertyField) {\n                    this._changes[fieldName]?._clearCommands();\n                }\n            }\n            this._changes = markRaw({});\n            this.data = { ...this._values };\n            this.dirty = false;\n        }\n        return true;\n    }\n\n    /**\n     * For owl reactivity, it's better to only update the keys inside the evalContext\n     * instead of replacing the evalContext itself, because a lot of components are\n     * registered to the evalContext (but not necessarily keys inside it), and would\n     * be uselessly re-rendered if we replace it by a brand new object.\n     */\n    _setEvalContext() {\n        const evalContext = getBasicEvalContext(this.config);\n        const dataContext = this._computeDataContext();\n        Object.assign(this.evalContext, evalContext, dataContext.withoutVirtualIds);\n        Object.assign(this.evalContextWithVirtualIds, evalContext, dataContext.withVirtualIds);\n        this._isEvalContextReady = true;\n\n        if (!this._parentRecord || this._parentRecord._isEvalContextReady) {\n            for (const [fieldName, value] of Object.entries(toRaw(this.data))) {\n                if ([\"one2many\", \"many2many\"].includes(this.fields[fieldName].type)) {\n                    value._updateContext(getFieldContext(this, fieldName));\n                }\n            }\n        }\n    }\n\n    /**\n     * @param {string} fieldName\n     */\n    async _setInvalidField(fieldName) {\n        const canProceed = this.model.hooks.onWillSetInvalidField(this, fieldName);\n        if (canProceed === false) {\n            return;\n        }\n        if (toRaw(this._invalidFields).has(fieldName)) {\n            return;\n        }\n        this._invalidFields.add(fieldName);\n        if (this.selected && this.model.multiEdit && this.model.root._recordToDiscard !== this) {\n            this._displayInvalidFieldNotification();\n            await this.discard();\n            this.switchMode(\"readonly\");\n        }\n    }\n\n    _resetFieldValidity(fieldName) {\n        this._invalidFields.delete(fieldName);\n    }\n\n    /**\n     * @param {Mode} mode\n     */\n    _switchMode(mode) {\n        this.model._updateConfig(this.config, { mode }, { reload: false });\n        if (mode === \"readonly\") {\n            this._noUpdateParent = false;\n            this._invalidFields.clear();\n        }\n    }\n\n    /**\n     * @param {boolean} state archive the records if true, otherwise unarchive them\n     */\n    async _toggleArchive(state) {\n        const method = state ? \"action_archive\" : \"action_unarchive\";\n        const action = await this.model.orm.call(this.resModel, method, [[this.resId]], {\n            context: this.context,\n        });\n        if (action && Object.keys(action).length) {\n            this.model.action.doAction(action, { onClose: () => this._load() });\n        } else {\n            return this._load();\n        }\n    }\n\n    _toggleSelection(selected) {\n        if (typeof selected === \"boolean\") {\n            this.selected = selected;\n        } else {\n            this.selected = !this.selected;\n        }\n        if (!this.selected && this.model.root.isDomainSelected) {\n            this.model.root._selectDomain(false);\n        }\n    }\n\n    async _getOnchangeValues(changes) {\n        for (const fieldName in changes) {\n            if (changes[fieldName] instanceof Operation) {\n                changes[fieldName] = changes[fieldName].compute(this.data[fieldName]);\n            }\n        }\n        const onChangeFields = Object.keys(changes).filter(\n            (fieldName) => this.activeFields[fieldName] && this.activeFields[fieldName].onChange\n        );\n        if (!onChangeFields.length) {\n            return {};\n        }\n\n        const localChanges = this._getChanges(\n            { ...this._changes, ...changes },\n            { withReadonly: true }\n        );\n        if (this.config.relationField) {\n            const parentRecord = this._parentRecord;\n            localChanges[this.config.relationField] = parentRecord._getChanges(\n                parentRecord._changes,\n                { withReadonly: true }\n            );\n            if (!this._parentRecord.isNew) {\n                localChanges[this.config.relationField].id = this._parentRecord.resId;\n            }\n        }\n        return this.model._onchange(this.config, {\n            changes: localChanges,\n            fieldNames: onChangeFields,\n            evalContext: toRaw(this.evalContext),\n            onError: (e) => {\n                // We apply changes and revert them after to force a render of the Field components\n                const undoChanges = this._applyChanges(changes);\n                undoChanges();\n                throw e;\n            },\n        });\n    }\n\n    async _update(changes, { withoutOnchange, withoutParentUpdate } = {}) {\n        this.dirty = true;\n        const prom = Promise.all([\n            this._preprocessMany2oneChanges(changes),\n            this._preprocessMany2OneReferenceChanges(changes),\n            this._preprocessReferenceChanges(changes),\n            this._preprocessX2manyChanges(changes),\n            this._preprocessPropertiesChanges(changes),\n            this._preprocessHtmlChanges(changes),\n        ]);\n        if (!this.model._urgentSave) {\n            await prom;\n        }\n        if (this.selected && this.model.multiEdit) {\n            return this.model.root._multiSave(this, changes);\n        }\n        let onchangeServerValues = {};\n        if (!this.model._urgentSave && !withoutOnchange) {\n            onchangeServerValues = await this._getOnchangeValues(changes);\n        }\n        // changes inside the record set as value for a many2one field must trigger the onchange,\n        // but can't be considered as changes on the parent record, so here we detect if many2one\n        // fields really changed, and if not, we delete them from changes\n        for (const fieldName in changes) {\n            if (this.fields[fieldName].type === \"many2one\") {\n                const curVal = toRaw(this.data[fieldName]);\n                const nextVal = changes[fieldName];\n                if (\n                    curVal &&\n                    nextVal &&\n                    curVal.id === nextVal.id &&\n                    curVal.display_name === nextVal.display_name\n                ) {\n                    delete changes[fieldName];\n                }\n            }\n        }\n        const undoChanges = this._applyChanges(changes, onchangeServerValues);\n        if (Object.keys(changes).length > 0 || Object.keys(onchangeServerValues).length > 0) {\n            try {\n                await this._onUpdate({ withoutParentUpdate });\n            } catch (e) {\n                undoChanges();\n                throw e;\n            }\n            await this.model.hooks.onRecordChanged(this, this._getChanges());\n        }\n    }\n}\n", "// @ts-check\n\nimport { EventBus, markRaw, toRaw } from \"@odoo/owl\";\nimport { makeContext } from \"@web/core/context\";\nimport { Domain } from \"@web/core/domain\";\nimport { WarningDialog } from \"@web/core/errors/error_dialogs\";\nimport { rpcBus } from \"@web/core/network/rpc\";\nimport { shallowEqual } from \"@web/core/utils/arrays\";\nimport { deepCopy, pick } from \"@web/core/utils/objects\";\nimport { Deferred, KeepLast, Mutex } from \"@web/core/utils/concurrency\";\nimport { orderByToString } from \"@web/search/utils/order_by\";\nimport { Model } from \"../model\";\nimport { DynamicGroupList } from \"./dynamic_group_list\";\nimport { DynamicRecordList } from \"./dynamic_record_list\";\nimport { Group } from \"./group\";\nimport { Record as RelationalRecord } from \"./record\";\nimport { StaticList } from \"./static_list\";\nimport {\n    extractInfoFromGroupData,\n    getAggregateSpecifications,\n    getBasicEvalContext,\n    getFieldsSpec,\n    getGroupServerValue,\n    getId,\n    makeActiveField,\n} from \"./utils\";\nimport { FetchRecordError } from \"./errors\";\n\n/**\n * @typedef {import(\"@web/core/context\").Context} Context\n * @typedef {import(\"./datapoint\").DataPoint} DataPoint\n * @typedef {import(\"@web/core/domain\").DomainListRepr} DomainListRepr\n * @typedef {import(\"@web/search/search_model\").Field} Field\n * @typedef {import(\"@web/search/search_model\").FieldInfo} FieldInfo\n * @typedef {import(\"@web/search/search_model\").SearchParams} SearchParams\n * @typedef {import(\"services\").ServiceFactories} Services\n *\n * @typedef {{\n *  changes?: Record<string, unknown>;\n *  fieldNames?: string[];\n *  evalContext?: Context;\n *  onError?: (error: unknown) => unknown;\n *  cache?: Object;\n * }} OnChangeParams\n *\n * @typedef {SearchParams & {\n *  fields: Record<string, Field>;\n *  activeFields: Record<string, FieldInfo>;\n *  fieldsToAggregate: string[];\n *  isMonoRecord: boolean;\n *  isRoot: boolean;\n *  resIds?: number[];\n *  mode?: \"edit\" | \"readonly\";\n *  loadId?: string;\n *  limit?: number;\n *  offset?: number;\n *  countLimit?: number;\n *  groupsLimit?: number;\n *  groups?: Record<string, unknown>;\n *  currentGroups?: Record<string, unknown>; // FIXME: could be cleaned: Object\n *  openGroupsByDefault?: boolean;\n * }} RelationalModelConfig\n *\n * @typedef {{\n *  config: RelationalModelConfig;\n *  state?: RelationalModelState;\n *  hooks?: Partial<typeof DEFAULT_HOOKS>;\n *  limit?: number;\n *  countLimit?: number;\n *  groupsLimit?: number;\n *  defaultOrderBy?: string[];\n *  maxGroupByDepth?: number;\n *  multiEdit?: boolean;\n *  groupByInfo?: Record<string, unknown>;\n *  activeIdsLimit?: number;\n *  useSendBeaconToSaveUrgently?: boolean;\n * }} RelationalModelParams\n *\n * @typedef {{\n *  config: RelationalModelConfig;\n *  specialDataCaches: Record<string, unknown>;\n * }} RelationalModelState\n */\n\nconst DEFAULT_HOOKS = {\n    /** @type {(config: RelationalModelConfig) => any} */\n    onWillLoadRoot: () => {},\n    /** @type {(root: DataPoint) => any} */\n    onRootLoaded: () => {},\n    /** @type {(record: RelationalRecord) => any} */\n    onWillSaveRecord: () => {},\n    /** @type {(record: RelationalRecord) => any} */\n    onRecordSaved: () => {},\n    /** @type {(record: RelationalRecord, changes: Object) => any} */\n    onWillSaveMulti: () => {},\n    /** @type {(records: RelationalRecord[]) => any} */\n    onSavedMulti: () => {},\n    /** @type {(record: RelationalRecord, fieldName: string) => any} */\n    onWillSetInvalidField: () => {},\n    /** @type {(record: RelationalRecord) => any} */\n    onRecordChanged: () => {},\n    /** @type {(warning: Object) => any} */\n    onWillDisplayOnchangeWarning: () => {},\n    /** @type {(changes: Object, validRecords: RelationalRecord[]) => any} */\n    onAskMultiSaveConfirmation: () => true,\n};\n\nrpcBus.addEventListener(\"RPC:RESPONSE\", (ev) => {\n    if (ev.detail.data.params?.method === \"unlink\") {\n        rpcBus.trigger(\"CLEAR-CACHES\", [\"web_read\", \"web_search_read\", \"web_read_group\"]);\n    }\n});\n\nexport class RelationalModel extends Model {\n    static services = [\"action\", \"dialog\", \"notification\", \"orm\"];\n    static Record = RelationalRecord;\n    static Group = Group;\n    static DynamicRecordList = DynamicRecordList;\n    static DynamicGroupList = DynamicGroupList;\n    static StaticList = StaticList;\n    static DEFAULT_LIMIT = 80;\n    static DEFAULT_COUNT_LIMIT = 10000;\n    static DEFAULT_GROUP_LIMIT = 80;\n    static DEFAULT_OPEN_GROUP_LIMIT = 10; // TODO: remove ?\n    static withCache = true;\n\n    /**\n     * @param {RelationalModelParams} params\n     * @param {Services} services\n     */\n    setup(params, { action, dialog, notification }) {\n        this.action = action;\n        this.dialog = dialog;\n        this.notification = notification;\n\n        this.bus = new EventBus();\n\n        this.keepLast = markRaw(new KeepLast());\n        this.mutex = markRaw(new Mutex());\n\n        /** @type {RelationalModelConfig} */\n        this.config = {\n            isMonoRecord: false,\n            context: {},\n            fieldsToAggregate: Object.keys(params.config.activeFields), // active fields by default\n            ...params.config,\n            isRoot: true,\n        };\n\n        this.hooks = Object.assign({}, DEFAULT_HOOKS, params.hooks);\n\n        this.initialLimit = params.limit || this.constructor.DEFAULT_LIMIT;\n        this.initialGroupsLimit = params.groupsLimit;\n        this.initialCountLimit = params.countLimit || this.constructor.DEFAULT_COUNT_LIMIT;\n        this.defaultOrderBy = params.defaultOrderBy;\n        this.maxGroupByDepth = params.maxGroupByDepth;\n        this.groupByInfo = params.groupByInfo || {};\n        this.multiEdit = params.multiEdit;\n        this.activeIdsLimit = params.activeIdsLimit || Number.MAX_SAFE_INTEGER;\n        this.specialDataCaches = markRaw(params.state?.specialDataCaches || {});\n        this.useSendBeaconToSaveUrgently = params.useSendBeaconToSaveUrgently || false;\n        this.withCache = this.constructor.withCache && this.env.config?.cache;\n        this.initialSampleGroups = undefined; // real groups to populate with sample records\n\n        this._urgentSave = false;\n    }\n\n    // -------------------------------------------------------------------------\n    // Public\n    // -------------------------------------------------------------------------\n\n    exportState() {\n        const config = { ...toRaw(this.config) };\n        delete config.currentGroups;\n        return {\n            config,\n            specialDataCaches: this.specialDataCaches,\n        };\n    }\n\n    /**\n     * @override\n     * @type {Model[\"hasData\"]}\n     */\n    hasData() {\n        return this.root.hasData;\n    }\n\n    /**\n     * @override\n     * @type {Model[\"load\"]}\n     */\n    async load(params = {}) {\n        if (this.orm.isSample && this.initialSampleGroups?.length) {\n            this.orm.setGroups(this.initialSampleGroups);\n        }\n        const config = this._getNextConfig(this.config, params);\n        if (!this.isReady) {\n            // We want the control panel to be displayed directly, without waiting for data to be\n            // loaded, for instance to be able to interact with the search view. For that reason, we\n            // create an empty root, without data, s.t. controllers can make the assumption that the\n            // root is set when they are rendered. The root is replaced later on by the real root,\n            // when data are loaded.\n            this.root = this._createEmptyRoot(config);\n            this.config = config;\n        }\n        this.hooks.onWillLoadRoot(config);\n        const rootLoadDef = new Deferred();\n        const cache = this._getCacheParams(config, rootLoadDef);\n        const data = await this.keepLast.add(this._loadData(config, cache));\n        this.root = this._createRoot(config, data);\n        rootLoadDef.resolve({ root: this.root, loadId: config.loadId });\n        this.config = config;\n        await this.hooks.onRootLoaded(this.root);\n    }\n\n    // -------------------------------------------------------------------------\n    // Protected\n    // -------------------------------------------------------------------------\n\n    /**\n     * If we group by default based on a property, the property might not be loaded in `fields`.\n     *\n     * @param {RelationalModelConfig} config\n     * @param {string} propertyFullName\n     */\n    async _getPropertyDefinition(config, propertyFullName) {\n        // dynamically load the property and add the definition in the fields attribute\n        const result = await this.orm.call(\n            config.resModel,\n            \"get_property_definition\",\n            [propertyFullName],\n            { context: config.context }\n        );\n        if (!result) {\n            // the property might have been removed\n            config.groupBy = null;\n        } else {\n            result.propertyName = result.name;\n            result.name = propertyFullName; // \"xxxxx\" -> \"property.xxxxx\"\n            // needed for _applyChanges\n            result.relatedPropertyField = { fieldName: propertyFullName.split(\".\")[0] };\n            result.relation = result.comodel; // match name on field\n            config.fields[propertyFullName] = result;\n        }\n    }\n\n    async _askChanges() {\n        const proms = [];\n        this.bus.trigger(\"NEED_LOCAL_CHANGES\", { proms });\n        await Promise.all([...proms, this.mutex.getUnlockedDef()]);\n    }\n\n    /**\n     * Creates a root datapoint without data. Supported root types are DynamicRecordList and\n     * DynamicGroupList.\n     *\n     * @param {RelationalModelConfig} config\n     * @returns {DataPoint | undefined}\n     */\n    _createEmptyRoot(config) {\n        if (!config.isMonoRecord) {\n            if (config.groupBy.length) {\n                return this._createRoot(config, { groups: [], length: 0 });\n            }\n            return this._createRoot(config, { records: [], length: 0 });\n        }\n    }\n\n    /**\n     * @param {RelationalModelConfig} config\n     * @param {Record<string, unknown>} data\n     * @returns {DataPoint}\n     */\n    _createRoot(config, data) {\n        if (config.isMonoRecord) {\n            return new this.constructor.Record(this, config, data);\n        }\n        if (config.groupBy.length) {\n            return new this.constructor.DynamicGroupList(this, config, data);\n        }\n        return new this.constructor.DynamicRecordList(this, config, data);\n    }\n\n    _getCacheParams(config, rootLoadDef) {\n        if (!this.withCache) {\n            return;\n        }\n        if (\n            !this.isReady || // first load of the model\n            // monorecord, loading a different id, or creating a new record (onchange)\n            (config.isMonoRecord && (this.root.config.resId !== config.resId || !config.resId))\n        ) {\n            return {\n                type: \"disk\",\n                update: \"always\",\n                callback: async (result, hasChanged) => {\n                    if (!hasChanged) {\n                        return;\n                    }\n                    const { root, loadId } = await rootLoadDef;\n                    if (root.id !== this.root.id) {\n                        // The root id might have changed, either because:\n                        //  1) the user already changed the domain and a second load has been done\n                        //  2) there was no data, so we reloaded directly with the sample orm\n                        // In the first case, there's nothing to do, we can ignore this update. We\n                        // have to deal with the second case:\n                        if (this.useSampleModel) {\n                            // We displayed sample data from the cache, but the rpc returned records\n                            // or groups => leave sample mode, forget previous groups and update\n                            this.useSampleModel = false;\n                            if (this.root.config.groupBy.length) {\n                                delete this.root.config.currentGroups;\n                                result = await this._postprocessReadGroup(this.root.config, result);\n                            }\n                            this.root._setData(result);\n                        }\n                        return;\n                    }\n                    if (loadId !== this.root.config.loadId) {\n                        // Avoid updating if another load was already done (e.g. a sort in a list)\n                        return;\n                    }\n                    if (root.config.isMonoRecord) {\n                        if (!root.config.resId) {\n                            // result is the response of the onchange rpc\n                            return root._setData(result.value, { keepChanges: true });\n                        }\n                        // result is the response of a web_read rpc\n                        if (!result.length) {\n                            // we read a record that no longer exists\n                            throw new FetchRecordError([root.config.resId]);\n                        }\n                        return root._setData(result[0], { keepChanges: true });\n                    }\n\n                    // multi record case: either grouped or ungrouped\n                    if (root.config.groupBy.length) {\n                        // result is the response of a web_read_group rpc\n                        // in case there're less groups, we don't want to keep displaying groups\n                        // that are no longer there => forget previous groups\n                        delete this.root.config.currentGroups;\n                        // in case that the config of the groups changed (e.g. group is now folded)\n                        // we want to update the groups.\n                        this.root.config.groups = [];\n                        result = await this._postprocessReadGroup(root.config, result);\n                    }\n                    root._setData(result);\n                },\n            };\n        }\n    }\n\n    /**\n     * @param {RelationalModelConfig} currentConfig\n     * @param {Partial<SearchParams>} params\n     * @returns {RelationalModelConfig}\n     */\n    _getNextConfig(currentConfig, params) {\n        const currentGroupBy = currentConfig.groupBy;\n        const config = Object.assign({}, currentConfig);\n\n        config.context = \"context\" in params ? params.context : config.context;\n        config.context = { ...config.context };\n        if (currentConfig.isMonoRecord) {\n            config.resId = \"resId\" in params ? params.resId : config.resId;\n            config.resIds = \"resIds\" in params ? params.resIds : config.resIds;\n            if (!config.resIds) {\n                config.resIds = config.resId ? [config.resId] : [];\n            }\n            if (!config.resId && config.mode !== \"edit\") {\n                config.mode = \"edit\";\n            }\n        } else {\n            config.domain = \"domain\" in params ? params.domain : config.domain;\n\n            // groupBy\n            config.groupBy = \"groupBy\" in params ? params.groupBy : config.groupBy;\n            // restrict the number of groupbys if requested\n            if (this.maxGroupByDepth) {\n                config.groupBy = config.groupBy.slice(0, this.maxGroupByDepth);\n            }\n            // apply month granularity if none explicitly given\n            // TODO: accept only explicit granularity\n            config.groupBy = config.groupBy.map((g) => {\n                if (g in config.fields && [\"date\", \"datetime\"].includes(config.fields[g].type)) {\n                    return `${g}:month`;\n                }\n                return g;\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\n            // keep current root config if any, if the groupBy parameter is the same\n            if (!shallowEqual(config.groupBy || [], currentGroupBy || [])) {\n                delete config.groups;\n            }\n            if (!config.groupBy.length) {\n                config.orderBy = config.orderBy.filter((order) => order.name !== \"__count\");\n            }\n        }\n        if (!config.isMonoRecord && params.domain) {\n            // always reset the offset to 0 when reloading from above with a domain\n            const resetOffset = (config) => {\n                config.offset = 0;\n                for (const group of Object.values(config.groups || {})) {\n                    resetOffset(group.list);\n                }\n            };\n            if (this.root) {\n                resetOffset(config);\n            }\n            if (!!config.groupBy.length !== !!currentGroupBy?.length) {\n                // from grouped to ungrouped or the other way around -> force the limit to be reset\n                delete config.limit;\n            }\n        }\n\n        return config;\n    }\n\n    /**\n     *\n     * @param {RelationalModelConfig} config\n     * @param {Object} [cache]\n     */\n    async _loadData(config, cache) {\n        config.loadId = getId(\"load\");\n        if (config.isMonoRecord) {\n            const evalContext = getBasicEvalContext(config);\n            if (!config.resId) {\n                return this._loadNewRecord(config, { evalContext, cache });\n            }\n            const records = await this._loadRecords(config, evalContext, cache);\n            return records[0];\n        }\n        if (config.resIds) {\n            // static list\n            const resIds = config.resIds.slice(config.offset, config.offset + config.limit);\n            return this._loadRecords({ ...config, resIds });\n        }\n        if (config.groupBy.length) {\n            return this._loadGroupedList(config, cache);\n        }\n        Object.assign(config, {\n            limit: config.limit || this.initialLimit,\n            countLimit: \"countLimit\" in config ? config.countLimit : this.initialCountLimit,\n            offset: config.offset || 0,\n        });\n        if (config.countLimit !== Number.MAX_SAFE_INTEGER) {\n            config.countLimit = Math.max(config.countLimit, config.offset + config.limit);\n        }\n        const { records, length } = await this._loadUngroupedList(config, cache);\n        if (config.offset && !records.length) {\n            config.offset = 0;\n            return this._loadData(config, cache);\n        }\n        return { records, length };\n    }\n\n    /**\n     * @param {RelationalModelConfig} config\n     * @param {Object} [cache]\n     */\n    async _loadGroupedList(config, cache) {\n        config.offset = config.offset || 0;\n        config.limit = config.limit || this.initialGroupsLimit;\n        if (!config.limit) {\n            config.limit = config.openGroupsByDefault\n                ? this.constructor.DEFAULT_OPEN_GROUP_LIMIT\n                : this.constructor.DEFAULT_GROUP_LIMIT;\n        }\n        config.groups = config.groups || {};\n\n        const response = await this._webReadGroup(config, cache);\n        return this._postprocessReadGroup(config, response);\n    }\n\n    async _postprocessReadGroup(config, { groups, length }) {\n        const commonConfig = {\n            resModel: config.resModel,\n            fields: config.fields,\n            activeFields: config.activeFields,\n            fieldsToAggregate: config.fieldsToAggregate,\n            offset: 0,\n        };\n        const extractGroups = async (currentConfig, groupsData) => {\n            const groupByFieldName = currentConfig.groupBy[0].split(\":\")[0];\n            if (groupByFieldName.includes(\".\")) {\n                if (!config.fields[groupByFieldName]) {\n                    await this._getPropertyDefinition(config, groupByFieldName);\n                }\n                const propertiesFieldName = groupByFieldName.split(\".\")[0];\n                if (!config.activeFields[propertiesFieldName]) {\n                    // add the properties field so we load its data when reading the records\n                    // so when we drag and drop we don't need to fetch the value of the record\n                    config.activeFields[propertiesFieldName] = makeActiveField();\n                }\n            }\n            const nextLevelGroupBy = currentConfig.groupBy.slice(1);\n            const groups = [];\n\n            let groupRecordConfig;\n            if (this.groupByInfo[groupByFieldName]) {\n                groupRecordConfig = {\n                    ...this.groupByInfo[groupByFieldName],\n                    resModel: currentConfig.fields[groupByFieldName].relation,\n                    context: {},\n                };\n            }\n\n            for (const groupData of groupsData) {\n                const group = extractInfoFromGroupData(\n                    groupData,\n                    currentConfig.groupBy,\n                    currentConfig.fields,\n                    currentConfig.domain\n                );\n                if (!currentConfig.groups[group.value]) {\n                    const isFolded =\n                        !Object.hasOwn(groupData, \"__records\") &&\n                        !Object.hasOwn(groupData, \"__groups\");\n                    currentConfig.groups[group.value] = {\n                        ...commonConfig,\n                        groupByFieldName,\n                        isFolded: isFolded,\n                        extraDomain: false,\n                        value: group.value,\n                        list: {\n                            ...commonConfig,\n                            groupBy: nextLevelGroupBy,\n                            groups: {},\n                            limit:\n                                nextLevelGroupBy.length === 0\n                                    ? this.initialLimit\n                                    : this.initialGroupsLimit ||\n                                      this.constructor.DEFAULT_GROUP_LIMIT,\n                        },\n                    };\n                }\n\n                const groupConfig = currentConfig.groups[group.value];\n                groupConfig.list.orderBy = currentConfig.orderBy;\n                groupConfig.initialDomain = group.domain;\n                if (groupConfig.extraDomain) {\n                    groupConfig.list.domain = Domain.and([\n                        group.domain,\n                        groupConfig.extraDomain,\n                    ]).toList();\n                } else {\n                    groupConfig.list.domain = group.domain;\n                }\n                const context = {\n                    ...currentConfig.context,\n                    [`default_${groupByFieldName}`]: group.serverValue,\n                };\n                groupConfig.list.context = context;\n                groupConfig.context = context;\n                if (nextLevelGroupBy.length) {\n                    if (!groupConfig.isFolded) {\n                        const { groups, length } = groupData.__groups;\n                        group.groups = await extractGroups(groupConfig.list, groups);\n                        group.length = length;\n                    } else {\n                        group.groups = [];\n                    }\n                } else {\n                    if (!groupConfig.isFolded) {\n                        group.records = groupData.__records;\n                        group.length = groupData.__count;\n                    } else {\n                        group.records = [];\n                    }\n                }\n                if (Object.hasOwn(groupData, \"__offset\")) {\n                    groupConfig.list.offset = groupData.__offset;\n                }\n                if (groupRecordConfig) {\n                    groupConfig.record = {\n                        ...groupRecordConfig,\n                        resId: group.value ?? false,\n                    };\n                }\n                groups.push(group);\n            }\n\n            return groups;\n        };\n\n        groups = await extractGroups(config, groups);\n\n        const params = JSON.stringify([\n            config.domain,\n            config.groupBy,\n            config.offset,\n            config.limit,\n            config.orderBy,\n        ]);\n        if (config.currentGroups && config.currentGroups.params === params) {\n            const currentGroups = config.currentGroups.groups;\n            currentGroups.forEach((group, index) => {\n                if (\n                    config.groups[group.value] &&\n                    !groups.some((g) => JSON.stringify(g.value) === JSON.stringify(group.value))\n                ) {\n                    const aggregates = Object.assign({}, group.aggregates);\n                    for (const key in aggregates) {\n                        // the `array_agg_distinct` aggregator's value is an array\n                        aggregates[key] = Array.isArray(aggregates[key]) ? [] : 0;\n                    }\n                    groups.splice(\n                        index,\n                        0,\n                        Object.assign({}, group, { count: 0, length: 0, records: [], aggregates })\n                    );\n                }\n            });\n        }\n        config.currentGroups = { params, groups };\n\n        return { groups, length };\n    }\n\n    /**\n     * @param {RelationalModelConfig} config\n     * @param {Partial<RelationalModelParams>} [params={}]\n     * @returns {Promise<Record<string, unknown>>}\n     */\n    async _loadNewRecord(config, params = {}) {\n        return this._onchange(config, params);\n    }\n\n    /**\n     * @param {RelationalModelConfig} config\n     * @param {Context} evalContext\n     * @param {Object} [cache]\n     */\n    async _loadRecords(config, evalContext = config.context, cache) {\n        const { resModel, activeFields, fields, context } = config;\n        const resIds = config.resId ? [config.resId] : config.resIds;\n        if (!resIds.length) {\n            return [];\n        }\n        const fieldSpec = getFieldsSpec(activeFields, fields, evalContext);\n        if (Object.keys(fieldSpec).length > 0) {\n            const kwargs = {\n                context: { bin_size: true, ...context },\n                specification: fieldSpec,\n            };\n            const orm = cache ? this.orm.cache(cache) : this.orm;\n            const records = await orm.webRead(resModel, resIds, kwargs);\n            if (!records.length) {\n                throw new FetchRecordError(resIds);\n            }\n\n            return records;\n        } else {\n            return resIds.map((resId) => ({ id: resId }));\n        }\n    }\n\n    /**\n     * Load records from the server for an ungrouped list. Return the result\n     * of unity read RPC.\n     *\n     * @param {RelationalModelConfig} config\n     * @param {Object} [cache]\n     */\n    async _loadUngroupedList(config, cache) {\n        const orderBy = config.orderBy.filter((o) => o.name !== \"__count\");\n        const kwargs = {\n            specification: getFieldsSpec(config.activeFields, config.fields, config.context),\n            offset: config.offset,\n            order: orderByToString(orderBy),\n            limit: config.limit,\n            context: { bin_size: true, ...config.context },\n            count_limit:\n                config.countLimit !== Number.MAX_SAFE_INTEGER ? config.countLimit + 1 : undefined,\n        };\n        const orm = cache ? this.orm.cache(cache) : this.orm;\n        return orm.webSearchRead(config.resModel, config.domain, kwargs);\n    }\n\n    /**\n     * @param {RelationalModelConfig} config\n     * @param {OnChangeParams} params\n     * @returns {Promise<Record<string, unknown>>}\n     */\n    async _onchange(\n        config,\n        { changes = {}, fieldNames = [], evalContext = config.context, onError, cache }\n    ) {\n        const { fields, activeFields, resModel, resId } = config;\n        let context = config.context;\n        if (fieldNames.length === 1) {\n            const fieldContext = config.activeFields[fieldNames[0]].context;\n            context = makeContext([context, fieldContext], evalContext);\n        }\n        const spec = getFieldsSpec(activeFields, fields, evalContext, { withInvisible: true });\n        const args = [resId ? [resId] : [], changes, fieldNames, spec];\n        let response;\n        try {\n            const orm = cache ? this.orm.cache(cache) : this.orm;\n            response = await orm.call(resModel, \"onchange\", args, { context });\n        } catch (e) {\n            if (onError) {\n                return void onError(e);\n            }\n            throw e;\n        }\n        if (response.warning) {\n            Promise.resolve(this.hooks.onWillDisplayOnchangeWarning(response.warning)).then(() => {\n                const { type, title, message, className, sticky } = response.warning;\n                if (type === \"dialog\") {\n                    this.dialog.add(WarningDialog, { title, message });\n                } else {\n                    this.notification.add(message, {\n                        className,\n                        sticky,\n                        title,\n                        type: \"warning\",\n                    });\n                }\n            });\n        }\n        return response.value;\n    }\n\n    /**\n     * @param {RelationalModelConfig} config\n     * @param {Partial<RelationalModelConfig>} patch\n     * @param {{\n     *  commit?: (data: Record<string, unknown>) => unknown;\n     *  reload?: boolean;\n     * }} [options]\n     */\n    async _updateConfig(config, patch, { reload = true, commit } = {}) {\n        const tmpConfig = { ...config, ...patch };\n        markRaw(tmpConfig.activeFields);\n        markRaw(tmpConfig.fields);\n\n        let data;\n        if (reload) {\n            if (tmpConfig.isRoot) {\n                this.hooks.onWillLoadRoot(tmpConfig);\n            }\n            data = await this._loadData(tmpConfig);\n        }\n        Object.assign(config, tmpConfig);\n        if (data && commit) {\n            commit(data);\n        }\n        if (reload && config.isRoot) {\n            await this.hooks.onRootLoaded(this.root);\n        }\n    }\n\n    /**\n     *\n     * @param {RelationalModelConfig} config\n     * @returns {Promise<number>}\n     */\n    async _updateCount(config) {\n        const count = await this.keepLast.add(\n            this.orm.searchCount(config.resModel, config.domain, { context: config.context })\n        );\n        config.countLimit = Number.MAX_SAFE_INTEGER;\n        return count;\n    }\n\n    /**\n     * When grouped by a many2many field, the same record may be displayed in\n     * several groups. When one of these records is edited, we want all other\n     * occurrences to be updated. The purpose of this function is to find and\n     * update all occurrences of a record that has been reloaded, in a grouped\n     * list view.\n     *\n     * @param {RelationalRecord} reloadedRecord\n     * @param {Record<string, unknown>} serverValues\n     */\n    _updateSimilarRecords(reloadedRecord, serverValues) {\n        if (this.config.isMonoRecord || !this.config.groupBy.length) {\n            return;\n        }\n        for (const record of this.root.records) {\n            if (record === reloadedRecord) {\n                continue;\n            }\n            if (record.resId === reloadedRecord.resId) {\n                record._applyValues(serverValues);\n            }\n        }\n    }\n\n    /**\n     * @param {RelationalModelConfig} config\n     * @param {Object} cache\n     */\n    async _webReadGroup(config, cache) {\n        function getGroupInfo(groups) {\n            return Object.values(groups).map((group) => {\n                const field = group.fields[group.groupByFieldName];\n                const value =\n                    field.type !== \"many2many\"\n                        ? getGroupServerValue(field, group.value)\n                        : group.value;\n                if (group.isFolded) {\n                    return { value, folded: group.isFolded };\n                } else {\n                    return {\n                        value,\n                        folded: group.isFolded,\n                        limit: group.list.limit,\n                        offset: group.list.offset,\n                        progressbar_domain: group.extraDomain,\n                        groups: group.list.groups && getGroupInfo(group.list.groups),\n                    };\n                }\n            });\n        }\n        const aggregates = getAggregateSpecifications(\n            pick(config.fields, ...config.fieldsToAggregate)\n        );\n        const currentGroupInfos = getGroupInfo(config.groups);\n        const { activeFields, fields } = config;\n        const evalContext = getBasicEvalContext(config);\n        const unfoldReadSpecification = getFieldsSpec(activeFields, fields, evalContext);\n\n        const groupByReadSpecification = {};\n        for (const groupBy of config.groupBy) {\n            const groupInfo = this.groupByInfo[groupBy];\n            if (groupInfo) {\n                const { activeFields, fields } = this.groupByInfo[groupBy];\n                groupByReadSpecification[groupBy] = getFieldsSpec(\n                    activeFields,\n                    fields,\n                    evalContext\n                );\n            }\n        }\n\n        const params = {\n            limit: config.limit !== Number.MAX_SAFE_INTEGER ? config.limit : undefined,\n            offset: config.offset,\n            order: orderByToString(config.orderBy),\n            auto_unfold: config.openGroupsByDefault,\n            opening_info: currentGroupInfos,\n            unfold_read_specification: unfoldReadSpecification,\n            unfold_read_default_limit: this.initialLimit,\n            groupby_read_specification: groupByReadSpecification,\n            context: { read_group_expand: true, ...config.context },\n        };\n        const orm = cache ? this.orm.cache(cache) : this.orm;\n        const result = await orm.webReadGroup(\n            config.resModel,\n            config.domain,\n            config.groupBy,\n            aggregates,\n            params\n        );\n        if (!this.initialSampleGroups) {\n            this.initialSampleGroups = deepCopy(result.groups);\n        }\n        return result;\n    }\n}\n", "import { x2ManyCommands } from \"@web/core/orm_service\";\nimport { intersection } from \"@web/core/utils/arrays\";\nimport { omit, pick } from \"@web/core/utils/objects\";\nimport { completeActiveFields } from \"@web/model/relational_model/utils\";\nimport { DataPoint } from \"./datapoint\";\nimport { fromUnityToServerValues, getBasicEvalContext, getId, patchActiveFields } from \"./utils\";\n\nimport { markRaw } from \"@odoo/owl\";\n\n/**\n * @typedef {import(\"./record\").Record} RelationalRecord\n */\n\nfunction compareFieldValues(v1, v2, fieldType) {\n    if (fieldType === \"many2one\") {\n        v1 = v1 ? v1.display_name : \"\";\n        v2 = v2 ? v2.display_name : \"\";\n    }\n    return v1 < v2;\n}\n\nfunction compareRecords(r1, r2, orderBy, fields) {\n    const { name, asc } = orderBy[0];\n    function getValue(record, fieldName) {\n        return fieldName === \"id\" ? record.resId : record.data[fieldName];\n    }\n    const v1 = asc ? getValue(r1, name) : getValue(r2, name);\n    const v2 = asc ? getValue(r2, name) : getValue(r1, name);\n    if (compareFieldValues(v1, v2, fields[name].type)) {\n        return -1;\n    }\n    if (compareFieldValues(v2, v1, fields[name].type)) {\n        return 1;\n    }\n    if (orderBy.length > 1) {\n        return compareRecords(r1, r2, orderBy.slice(1), fields);\n    }\n    return 0;\n}\n\nfunction copyRecordData(record, copyFields = []) {\n    const data = {};\n    for (const [name, value] of Object.entries(record.data)) {\n        if (\n            ![...copyFields, \"display_name\"].includes(name) &&\n            (record._isReadonly(name) || record._isInvisible(name)) &&\n            !record._isRequired(name)\n        ) {\n            continue;\n        }\n        switch (record.fields[name].type) {\n            case \"many2many\": {\n                const list = record.data[name];\n                data[name] = list.currentIds.map((id) => {\n                    let data;\n                    if (list._cache[id]) {\n                        data = copyRecordData(list._cache[id]);\n                    }\n                    return [x2ManyCommands.LINK, id, data];\n                });\n                break;\n            }\n            case \"many2one\":\n            case \"many2one_reference\":\n            case \"reference\":\n                data[name] = value && Object.assign({}, value);\n                break;\n            case \"one2many\":\n                // Not supported => that field is left empty\n                break;\n            default:\n                data[name] = value;\n        }\n    }\n    return data;\n}\n\nexport class StaticList extends DataPoint {\n    static type = \"StaticList\";\n\n    /**\n     * @type {typeof DataPoint.prototype.setup<{\n     *  onUpdate?: () => unknown;\n     *  parent?: RelationalRecord;\n     * }>}\n     */\n    setup(_config, data, options = {}) {\n        this._parent = options.parent;\n        this._onUpdate = options.onUpdate;\n\n        this._cache = markRaw({});\n        this._commands = [];\n        this._initialCommands = [];\n        this._savePoint = undefined;\n        this._unknownRecordCommands = {}; // tracks update commands on records we haven't fetched yet\n        this._currentIds = [...this.resIds];\n        this._initialCurrentIds = [...this.currentIds];\n        this._needsReordering = false;\n        this._tmpIncreaseLimit = 0;\n        // In kanban and non editable list views, x2many records can be opened in a form view in\n        // dialog, which may contain other fields than the kanban or list view. The next set keeps\n        // tracks of records we already opened in dialog and thus for which we already modified the\n        // config to add the form view's fields in activeFields.\n        this._extendedRecords = new Set();\n\n        /** @type {RelationalRecord[]} */\n        this.records = data\n            .slice(this.offset, this.limit)\n            .map((r) => this._createRecordDatapoint(r));\n        this.count = this.resIds.length;\n        this.handleField = Object.keys(this.activeFields).find(\n            (fieldName) => this.activeFields[fieldName].isHandle\n        );\n    }\n\n    // -------------------------------------------------------------------------\n    // Getters\n    // -------------------------------------------------------------------------\n\n    get currentIds() {\n        return this._currentIds;\n    }\n\n    get editedRecord() {\n        return this.records.find((record) => record.isInEdition);\n    }\n\n    get evalContext() {\n        const evalContext = getBasicEvalContext(this.config);\n        evalContext.parent = this._parent.evalContext;\n        return evalContext;\n    }\n\n    get limit() {\n        return this.config.limit;\n    }\n\n    get offset() {\n        return this.config.offset;\n    }\n\n    get orderBy() {\n        return this.config.orderBy;\n    }\n\n    get resIds() {\n        return this.config.resIds;\n    }\n\n    get selection() {\n        return [];\n    }\n\n    // -------------------------------------------------------------------------\n    // Public\n    // -------------------------------------------------------------------------\n\n    /**\n     * Adds a new record to an x2many relation. If params.record is given, adds\n     * given record (use case: after saving the form dialog in a, e.g., non\n     * editable x2many list). Otherwise, do an onchange to get the initial\n     * values and create a new Record (e.g. after clicking on Add a line in an\n     * editable x2many list).\n     *\n     * @param {Object} params\n     * @param {\"top\"|\"bottom\"} [params.position]\n     * @param {Object} [params.activeFields=this.activeFields]\n     * @param {boolean} [params.withoutParent=false]\n     */\n    addNewRecord(params) {\n        return this.model.mutex.exec(async () => {\n            const { activeFields, context, mode, position, withoutParent } = params;\n            const record = await this._createNewRecordDatapoint({\n                activeFields,\n                context,\n                position,\n                withoutParent,\n                manuallyAdded: true,\n                mode,\n            });\n            await this._addRecord(record, { position });\n            await this._onUpdate({ withoutOnchange: !record._checkValidity({ silent: true }) });\n            return record;\n        });\n    }\n\n    /**\n     * @param {number} index\n     * @param {Object} [options]\n     * @param {Object} [options.context]\n     * @param {\"edit\" | \"readonly\"} [options.mode]\n     */\n    addNewRecordAtIndex(index, options = {}) {\n        return this.model.mutex.exec(async () => {\n            const newRecord = await this._addNewRecordAtIndex(index, options);\n            await this._onUpdate();\n            return newRecord;\n        });\n    }\n\n    /**\n     * @param {[number, any, any][]} commands\n     * @param {Object} [options]\n     * @param {boolean} [options.canAddOverLimit]\n     * @param {boolean} [options.sort]\n     * @returns {Promise<void>}\n     */\n    applyCommands(commands, options = {}) {\n        return this.model.mutex.exec(async () => {\n            await this._applyCommands(commands, omit(options, \"sort\"));\n            if (options.sort) {\n                await this._sort();\n            }\n            await this._onUpdate();\n        });\n    }\n\n    canResequence() {\n        return this.handleField && this.orderBy.length && this.orderBy[0].name === this.handleField;\n    }\n\n    delete(record) {\n        return this.model.mutex.exec(async () => {\n            await this._applyCommands([[x2ManyCommands.DELETE, record.resId || record._virtualId]]);\n            // All records of last page are deleted => reload the new last page\n            if (this.count === this.offset) {\n                await this._load({ offset: Math.max(this.offset - this.limit, 0) });\n            }\n            await this._onUpdate();\n        });\n    }\n\n    /**\n     * @param {RelationalRecord[]} records\n     * @param {Object} [options={}]\n     * @param {number} [options.targetIndex]\n     * @returns {Promise<void>}\n     */\n    duplicateRecords(records = [], options = {}) {\n        return this.model.mutex.exec(async () => {\n            await this._duplicateRecords(records, options);\n            await this._onUpdate();\n        });\n    }\n\n    async enterEditMode(record) {\n        const canProceed = await this.leaveEditMode();\n        if (canProceed) {\n            await record.switchMode(\"edit\");\n        }\n        return canProceed;\n    }\n\n    /**\n     * This method is meant to be used in a very specific usecase: when an x2many record is viewed\n     * or edited through a form view dialog (e.g. x2many kanban or non editable list). In this case,\n     * the form typically contains different fields than the kanban or list, so we need to \"extend\"\n     * the fields and activeFields. If the record opened in a form view dialog already exists, we\n     * modify it's config to add the new fields. If it is a new record, we create it with the\n     * extended config.\n     *\n     * @param {Object} params\n     * @param {Object} params.activeFields\n     * @param {Object} params.fields\n     * @param {Object} [params.context]\n     * @param {boolean} [params.withoutParent]\n     * @param {string} [params.mode]\n     * @param {RelationalRecord} [record]\n     * @returns {RelationalRecord}\n     */\n    extendRecord(params, record) {\n        return this.model.mutex.exec(async () => {\n            // extend fields and activeFields of the list with those given in params\n            completeActiveFields(this.config.activeFields, params.activeFields);\n            Object.assign(this.fields, params.fields);\n            const activeFields = { ...params.activeFields };\n            for (const fieldName in this.activeFields) {\n                if (fieldName in activeFields) {\n                    patchActiveFields(activeFields[fieldName], this.activeFields[fieldName]);\n                } else {\n                    activeFields[fieldName] = this.activeFields[fieldName];\n                }\n            }\n\n            if (record) {\n                record._noUpdateParent = true;\n                record._activeFieldsToRestore = { ...this.config.activeFields };\n                const config = {\n                    ...record.config,\n                    ...params,\n                    activeFields,\n                };\n\n                // case 1: the record already exists\n                if (this._extendedRecords.has(record.id)) {\n                    // case 1.1: the record has already been extended\n                    // -> simply store a savepoint\n                    this.model._updateConfig(record.config, config, { reload: false });\n                    record._addSavePoint();\n                    return record;\n                }\n                // case 1.2: the record is extended for the first time, and it now potentially has\n                // more fields than before (or x2many fields displayed differently)\n                // -> if it isn't a new record, load it to retrieve the values of new fields\n                // -> generate default values for new fields\n                // -> recursively update the config of the record and it's sub datapoints\n                // -> apply the loaded values in the case of a not new record\n                // -> store a savepoint\n                // These operations must be done in that specific order to ensure that the model is\n                // mutated only once (in a tick), and that datapoints have the correct config to\n                // handle field values they receive.\n                let data = {};\n                if (!record.isNew) {\n                    const evalContext = Object.assign({}, record.evalContext, config.context);\n                    const resIds = [record.resId];\n                    [data] = await this.model._loadRecords({ ...config, resIds }, evalContext);\n                }\n                this.model._updateConfig(record.config, config, { reload: false });\n                record._applyDefaultValues();\n                for (const fieldName in record.activeFields) {\n                    if ([\"one2many\", \"many2many\"].includes(record.fields[fieldName].type)) {\n                        const list = record.data[fieldName];\n                        const patch = {\n                            activeFields: activeFields[fieldName].related.activeFields,\n                            fields: activeFields[fieldName].related.fields,\n                        };\n                        for (const subRecord of Object.values(list._cache)) {\n                            this.model._updateConfig(subRecord.config, patch, {\n                                reload: false,\n                            });\n                        }\n                        this.model._updateConfig(list.config, patch, { reload: false });\n                    }\n                }\n                record._applyValues(data);\n                const commands = this._unknownRecordCommands[record.resId];\n                delete this._unknownRecordCommands[record.resId];\n                if (commands) {\n                    this._applyCommands(commands);\n                }\n                record._addSavePoint();\n            } else {\n                // case 2: the record is a new record\n                // -> simply create one with the extended config\n                record = await this._createNewRecordDatapoint({\n                    activeFields,\n                    context: params.context,\n                    withoutParent: params.withoutParent,\n                    manuallyAdded: true,\n                });\n                record._activeFieldsToRestore = { ...this.config.activeFields };\n                record._noUpdateParent = true;\n            }\n            // mark the record as being extended, to go through case 1.1 next time\n            this._extendedRecords.add(record.id);\n\n            return record;\n        });\n    }\n\n    forget(record) {\n        return this.model.mutex.exec(async () => {\n            await this._applyCommands([[x2ManyCommands.UNLINK, record.resId]]);\n            await this._onUpdate();\n        });\n    }\n\n    async leaveEditMode({ discard, canAbandon, validate } = {}) {\n        if (this.editedRecord) {\n            await this.model._askChanges(false);\n        }\n        return this.model.mutex.exec(async () => {\n            let editedRecord = this.editedRecord;\n            if (editedRecord) {\n                const isValid = editedRecord._checkValidity();\n                if (!isValid && validate) {\n                    return false;\n                }\n                if (canAbandon !== false && !validate) {\n                    this._abandonRecords([editedRecord], { force: true });\n                }\n                // if we still have an editedRecord, it means it hasn't been abandonned\n                editedRecord = this.editedRecord;\n                if (editedRecord) {\n                    if (isValid && !editedRecord.dirty && discard) {\n                        return false;\n                    }\n                    if (isValid || (!editedRecord.dirty && !editedRecord._manuallyAdded)) {\n                        editedRecord._switchMode(\"readonly\");\n                    }\n                }\n            }\n            return !this.editedRecord;\n        });\n    }\n\n    linkTo(resId, serverData) {\n        return this.model.mutex.exec(async () => {\n            await this._applyCommands([[x2ManyCommands.LINK, resId, serverData]]);\n            await this._onUpdate();\n        });\n    }\n\n    unlinkFrom(resId, serverData) {\n        return this.model.mutex.exec(async () => {\n            await this._applyCommands([[x2ManyCommands.UNLINK, resId, serverData]]);\n            await this._onUpdate();\n        });\n    }\n\n    load({ limit, offset, orderBy } = {}) {\n        return this.model.mutex.exec(async () => {\n            const editedRecord = this.editedRecord;\n            if (editedRecord && !(await editedRecord.checkValidity())) {\n                return;\n            }\n            limit = limit !== undefined ? limit : this.limit;\n            offset = offset !== undefined ? offset : this.offset;\n            orderBy = orderBy !== undefined ? orderBy : this.orderBy;\n            return this._load({ limit, offset, orderBy });\n        });\n    }\n\n    moveRecord(dataRecordId, _dataGroupId, refId, _targetGroupId) {\n        return this.resequence(dataRecordId, refId);\n    }\n\n    sortBy(fieldName) {\n        return this.model.mutex.exec(() => this._sortBy(fieldName));\n    }\n\n    async addAndRemove({ add, remove } = {}) {\n        return this.model.mutex.exec(async () => {\n            const commands = [\n                ...(add || []).map((id) => [x2ManyCommands.LINK, id]),\n                ...(remove || []).map((id) => [x2ManyCommands.UNLINK, id]),\n            ];\n            await this._applyCommands(commands, { canAddOverLimit: true });\n            await this._onUpdate();\n        });\n    }\n\n    async resequence(movedId, targetId) {\n        return this.model.mutex.exec(() => this._resequence(movedId, targetId));\n    }\n\n    /**\n     * This method is meant to be called when a record, which has previously been extended to be\n     * displayed in a form view dialog (see @extendRecord) is saved. In this case, we may need to\n     * add this record to the list (if it is a new one), and to notify the parent record of the\n     * update. We may also want to sort the list.\n     *\n     * @param {RelationalRecord} record\n     */\n    validateExtendedRecord(record) {\n        return this.model.mutex.exec(async () => {\n            if (!this._currentIds.includes(record.isNew ? record._virtualId : record.resId)) {\n                // new record created, not yet in the list\n                await this._addRecord(record);\n            } else if (!record.dirty) {\n                return;\n            }\n            await this._onUpdate();\n            record._restoreActiveFields();\n            record._savePoint = undefined;\n        });\n    }\n\n    // -------------------------------------------------------------------------\n    // Protected\n    // -------------------------------------------------------------------------\n\n    _abandonRecords(records = this.records, { force } = {}) {\n        for (const record of records) {\n            if (record.canBeAbandoned && (force || !record._checkValidity())) {\n                const virtualId = record._virtualId;\n                const index = this._currentIds.findIndex((id) => id === virtualId);\n                this._currentIds.splice(index, 1);\n                this.records.splice(\n                    this.records.findIndex((r) => r === record),\n                    1\n                );\n                this._commands = this._commands.filter((c) => c[1] !== virtualId);\n                this.count--;\n                if (this._tmpIncreaseLimit > 0) {\n                    this.model._updateConfig(\n                        this.config,\n                        { limit: this.limit - 1 },\n                        { reload: false }\n                    );\n                    this._tmpIncreaseLimit--;\n                }\n            }\n        }\n    }\n\n    async _addRecord(record, { position, sort = true } = {}) {\n        const command = [x2ManyCommands.CREATE, record._virtualId];\n        if (position === \"top\") {\n            this.records.unshift(record);\n            if (this.records.length > this.limit) {\n                this.records.pop();\n            }\n            this._currentIds.splice(this.offset, 0, record._virtualId);\n            this._commands.unshift(command);\n        } else if (position === \"bottom\") {\n            this.records.push(record);\n            this._currentIds.splice(this.offset + this.limit, 0, record._virtualId);\n            if (this.records.length > this.limit) {\n                this._tmpIncreaseLimit++;\n                const nextLimit = this.limit + 1;\n                this.model._updateConfig(this.config, { limit: nextLimit }, { reload: false });\n            }\n            this._commands.push(command);\n        } else {\n            const currentIds = [...this._currentIds, record._virtualId];\n            if (this.orderBy.length && sort) {\n                await this._sort(currentIds);\n            } else {\n                if (this.records.length < this.limit) {\n                    this.records.push(record);\n                }\n            }\n            this._currentIds = currentIds;\n            this._commands.push(command);\n        }\n        this.count++;\n        this._needsReordering = true;\n    }\n\n    async _addNewRecordAtIndex(index, options = {}) {\n        const newRecord = await this._createNewRecordDatapoint({\n            context: options.context,\n            manuallyAdded: true,\n            mode: options.mode || \"edit\",\n        });\n        if (this.records.length === this.limit) {\n            this._tmpIncreaseLimit++;\n            const nextLimit = this.limit + 1;\n            this.model._updateConfig(this.config, { limit: nextLimit }, { reload: false });\n        }\n        await this._addRecord(newRecord);\n        await this._resequence(newRecord.id, this.records[index].id);\n        newRecord.dirty = false;\n        return newRecord;\n    }\n\n    _addSavePoint() {\n        for (const id in this._cache) {\n            this._cache[id]._addSavePoint();\n        }\n        this._savePoint = markRaw({\n            _commands: [...this._commands],\n            _currentIds: [...this._currentIds],\n            count: this.count,\n        });\n    }\n\n    _applyCommands(commands, { canAddOverLimit } = {}) {\n        const { CREATE, UPDATE, DELETE, UNLINK, LINK, SET } = x2ManyCommands;\n\n        // For performance reasons, we split commands by record ids, such that we have quick access\n        // to all commands concerning a given record. At the end, we re-build the list of commands\n        // from this structure.\n        let lastCommandIndex = -1;\n        const commandsByIds = {};\n        function addOwnCommand(command) {\n            commandsByIds[command[1]] = commandsByIds[command[1]] || [];\n            commandsByIds[command[1]].push({ command, index: ++lastCommandIndex });\n        }\n        function getOwnCommands(id) {\n            commandsByIds[id] = commandsByIds[id] || [];\n            return commandsByIds[id];\n        }\n        for (const command of this._commands) {\n            addOwnCommand(command);\n        }\n\n        // For performance reasons, we accumulate removed ids (commands DELETE and UNLINK), and at\n        // the end, we filter once this.records and this._currentIds to remove them.\n        const removedIds = {};\n        const recordsToLoad = [];\n        for (const command of commands) {\n            switch (command[0]) {\n                case CREATE: {\n                    const virtualId = getId(\"virtual\");\n                    const record = this._createRecordDatapoint(command[2], { virtualId });\n                    this.records.push(record);\n                    addOwnCommand([CREATE, virtualId]);\n                    const index = this.offset + this.limit + this._tmpIncreaseLimit;\n                    this._currentIds.splice(index, 0, virtualId);\n                    this._tmpIncreaseLimit = Math.max(this.records.length - this.limit, 0);\n                    const nextLimit = this.limit + this._tmpIncreaseLimit;\n                    this.model._updateConfig(this.config, { limit: nextLimit }, { reload: false });\n                    this.count++;\n                    break;\n                }\n                case UPDATE: {\n                    const existingCommand = getOwnCommands(command[1]).some(\n                        (x) => x.command[0] === CREATE || x.command[0] === UPDATE\n                    );\n                    if (!existingCommand) {\n                        addOwnCommand([UPDATE, command[1]]);\n                    }\n                    const record = this._cache[command[1]];\n                    if (!record) {\n                        // the record isn't in the cache, it means it is on a page we haven't loaded\n                        // so we say the record is \"unknown\", and store all update commands we\n                        // receive about it in a separated structure, s.t. we can easily apply them\n                        // later on after loading the record, if we ever load it.\n                        if (!(command[1] in this._unknownRecordCommands)) {\n                            this._unknownRecordCommands[command[1]] = [];\n                        }\n                        this._unknownRecordCommands[command[1]].push(command);\n                    } else if (command[1] in this._unknownRecordCommands) {\n                        // this case is more tricky: the record is in the cache, but it isn't loaded\n                        // yet, as we are currently loading it (see below, where we load missing\n                        // records for the current page)\n                        this._unknownRecordCommands[command[1]].push(command);\n                    } else {\n                        const changes = {};\n                        for (const fieldName in command[2]) {\n                            if ([\"one2many\", \"many2many\"].includes(this.fields[fieldName].type)) {\n                                const invisible = record.activeFields[fieldName]?.invisible;\n                                if (\n                                    invisible === \"True\" ||\n                                    invisible === \"1\" ||\n                                    !(fieldName in record.activeFields) // this record hasn't been extended\n                                ) {\n                                    if (!(command[1] in this._unknownRecordCommands)) {\n                                        this._unknownRecordCommands[command[1]] = [];\n                                    }\n                                    this._unknownRecordCommands[command[1]].push(command);\n                                    continue;\n                                }\n                            }\n                            changes[fieldName] = command[2][fieldName];\n                        }\n                        record._applyChanges(\n                            record._parseServerValues(changes, { currentValues: record.data })\n                        );\n                    }\n                    break;\n                }\n                case DELETE:\n                case UNLINK: {\n                    // If we receive an UNLINK command and we already have a SET command\n                    // containing the record to unlink, we just remove it from the SET command.\n                    // If there's a SET command, we know it's the first one (see @_replaceWith).\n                    if (command[0] === UNLINK) {\n                        const firstCommand = this._commands[0];\n                        const hasReplaceWithCommand = firstCommand && firstCommand[0] === SET;\n                        if (hasReplaceWithCommand && firstCommand[2].includes(command[1])) {\n                            firstCommand[2] = firstCommand[2].filter((id) => id !== command[1]);\n                            break;\n                        }\n                    }\n                    const ownCommands = getOwnCommands(command[1]);\n                    if (command[0] === DELETE) {\n                        const hasCreateCommand = ownCommands.some((x) => x.command[0] === CREATE);\n                        ownCommands.splice(0); // reset to the empty list\n                        if (!hasCreateCommand) {\n                            addOwnCommand([DELETE, command[1]]);\n                        }\n                    } else {\n                        const linkToIndex = ownCommands.findIndex((x) => x.command[0] === LINK);\n                        if (linkToIndex >= 0) {\n                            ownCommands.splice(linkToIndex, 1);\n                        } else {\n                            addOwnCommand([UNLINK, command[1]]);\n                        }\n                    }\n                    removedIds[command[1]] = true;\n                    break;\n                }\n                case LINK: {\n                    let record;\n                    if (command[1] in this._cache) {\n                        record = this._cache[command[1]];\n                    } else {\n                        record = this._createRecordDatapoint({ ...command[2], id: command[1] });\n                    }\n                    if (this._currentIds.includes(record.resId) && !removedIds[record.resId]) {\n                        break;\n                    }\n                    if (!this.limit || this.records.length < this.limit || canAddOverLimit) {\n                        if (!command[2]) {\n                            recordsToLoad.push(record);\n                        }\n                        this.records.push(record);\n                        if (this.records.length > this.limit) {\n                            this._tmpIncreaseLimit = this.records.length - this.limit;\n                            const nextLimit = this.limit + this._tmpIncreaseLimit;\n                            this.model._updateConfig(\n                                this.config,\n                                { limit: nextLimit },\n                                { reload: false }\n                            );\n                        }\n                    }\n                    this._currentIds.push(record.resId);\n                    addOwnCommand([command[0], command[1]]);\n                    this.count++;\n                    break;\n                }\n            }\n        }\n\n        // Re-generate the new list of commands\n        this._commands = Object.values(commandsByIds)\n            .flat()\n            .sort((x, y) => x.index - y.index)\n            .map((x) => x.command);\n\n        // Filter out removed records and ids from this.records and this._currentIds\n        if (Object.keys(removedIds).length) {\n            let removeCommandsByIdsCopy = Object.assign({}, removedIds);\n            this.records = this.records.filter((r) => {\n                const id = r.resId || r._virtualId;\n                if (removeCommandsByIdsCopy[id]) {\n                    delete removeCommandsByIdsCopy[id];\n                    return false;\n                }\n                return true;\n            });\n            const nextCurrentIds = [];\n            removeCommandsByIdsCopy = Object.assign({}, removedIds);\n            for (const id of this._currentIds) {\n                if (removeCommandsByIdsCopy[id]) {\n                    delete removeCommandsByIdsCopy[id];\n                } else {\n                    nextCurrentIds.push(id);\n                }\n            }\n            this._currentIds = nextCurrentIds;\n            this.count = this._currentIds.length;\n        }\n\n        // Fill the page if it isn't full w.r.t. the limit. This may happen if we aren't on the last\n        // page and records of the current have been removed, or if we applied commands to remove\n        // some records and to add others, but we were on the limit.\n        const nbMissingRecords = this.limit - this.records.length;\n        if (nbMissingRecords > 0) {\n            const lastRecordIndex = this.limit + this.offset;\n            const firstRecordIndex = lastRecordIndex - nbMissingRecords;\n            const nextRecordIds = this._currentIds.slice(firstRecordIndex, lastRecordIndex);\n            for (const id of this._getResIdsToLoad(nextRecordIds)) {\n                const record = this._createRecordDatapoint({ id }, { dontApplyCommands: true });\n                recordsToLoad.push(record);\n            }\n            for (const id of nextRecordIds) {\n                this.records.push(this._cache[id]);\n            }\n        }\n        if (recordsToLoad.length) {\n            const resIds = recordsToLoad.map((r) => r.resId);\n            return this.model._loadRecords({ ...this.config, resIds }).then((recordValues) => {\n                for (let i = 0; i < recordsToLoad.length; i++) {\n                    const record = recordsToLoad[i];\n                    record._applyValues(recordValues[i]);\n                    const commands = this._unknownRecordCommands[record.resId];\n                    if (commands) {\n                        delete this._unknownRecordCommands[record.resId];\n                        this._applyCommands(commands);\n                    }\n                }\n            });\n        }\n    }\n\n    _applyInitialCommands(commands) {\n        this._applyCommands(commands);\n        this._initialCommands = [...commands];\n        this._initialCurrentIds = [...this._currentIds];\n    }\n\n    async _createNewRecordDatapoint(params = {}) {\n        const changes = {};\n        if (!params.withoutParent && this.config.relationField) {\n            changes[this.config.relationField] = this._parent._getChanges();\n            if (!this._parent.isNew) {\n                changes[this.config.relationField].id = this._parent.resId;\n            }\n        }\n        const values = await this.model._loadNewRecord(\n            {\n                resModel: this.resModel,\n                activeFields: params.activeFields || this.activeFields,\n                fields: this.fields,\n                context: Object.assign({}, this.context, params.context),\n            },\n            { changes, evalContext: this.evalContext }\n        );\n\n        if (this.canResequence() && this.records.length) {\n            const position = params.position || \"bottom\";\n            const order = this.orderBy[0];\n            const asc = !order || order.asc;\n            let value;\n            if (position === \"top\") {\n                const isOnFirstPage = this.offset === 0;\n                value = this.records[0].data[this.handleField];\n                if (isOnFirstPage) {\n                    if (asc) {\n                        value = value > 0 ? value - 1 : 0;\n                    } else {\n                        value = value + 1;\n                    }\n                }\n            } else if (position === \"bottom\") {\n                value = this.records[this.records.length - 1].data[this.handleField];\n                const isOnLastPage = this.limit + this.offset >= this.count;\n                if (isOnLastPage) {\n                    if (asc) {\n                        value = value + 1;\n                    } else {\n                        value = value > 0 ? value - 1 : 0;\n                    }\n                }\n            }\n            values[this.handleField] = value;\n        }\n        return this._createRecordDatapoint(values, {\n            mode: params.mode || \"edit\",\n            virtualId: getId(\"virtual\"),\n            activeFields: params.activeFields,\n            manuallyAdded: params.manuallyAdded,\n        });\n    }\n\n    _createRecordDatapoint(data, params = {}) {\n        const resId = data.id || false;\n        if (!resId && !params.virtualId) {\n            throw new Error(\"You must provide a virtualId if the record has no id\");\n        }\n        const id = resId || params.virtualId;\n        const config = {\n            context: this.context,\n            activeFields: Object.assign({}, params.activeFields || this.activeFields),\n            resModel: this.resModel,\n            fields: params.fields || this.fields,\n            relationField: this.config.relationField,\n            resId,\n            resIds: resId ? [resId] : [],\n            mode: params.mode || \"readonly\",\n            isMonoRecord: true,\n        };\n        const { CREATE, UPDATE } = x2ManyCommands;\n        const options = {\n            parentRecord: this._parent,\n            onUpdate: async ({ withoutParentUpdate }) => {\n                const id = record.isNew ? record._virtualId : record.resId;\n                if (!this.currentIds.includes(id)) {\n                    // the record hasn't been added to the list yet (we're currently creating it\n                    // from a dialog)\n                    return;\n                }\n                const hasCommand = this._commands.some(\n                    (c) => (c[0] === CREATE || c[0] === UPDATE) && c[1] === id\n                );\n                if (!hasCommand) {\n                    this._commands.push([UPDATE, id]);\n                }\n                if (record._noUpdateParent) {\n                    // the record is edited from a dialog, so we don't want to notify the parent\n                    // record to be notified at each change inside the dialog (it will be notified\n                    // at the end when the dialog is saved)\n                    return;\n                }\n                if (!withoutParentUpdate) {\n                    await this._onUpdate({\n                        withoutOnchange: !record._checkValidity({ silent: true }),\n                    });\n                }\n            },\n            virtualId: params.virtualId,\n            manuallyAdded: params.manuallyAdded,\n        };\n        const record = new this.model.constructor.Record(this.model, config, data, options);\n        this._cache[id] = record;\n        if (!params.dontApplyCommands) {\n            const commands = this._unknownRecordCommands[id];\n            if (commands) {\n                delete this._unknownRecordCommands[id];\n                this._applyCommands(commands);\n            }\n        }\n        return record;\n    }\n\n    _clearCommands() {\n        this._commands = [];\n        this._unknownRecordCommands = {};\n    }\n\n    _discard() {\n        for (const id in this._cache) {\n            this._cache[id]._discard();\n        }\n        if (this._savePoint) {\n            this._commands = this._savePoint._commands;\n            this._currentIds = this._savePoint._currentIds;\n            this.count = this._savePoint.count;\n        } else {\n            this._commands = [];\n            this._currentIds = [...this.resIds];\n            this.count = this.resIds.length;\n        }\n        this._unknownRecordCommands = {};\n        const limit = this.limit - this._tmpIncreaseLimit;\n        this._tmpIncreaseLimit = 0;\n        this.model._updateConfig(this.config, { limit }, { reload: false });\n        this.records = this._currentIds\n            .slice(this.offset, this.limit)\n            .map((resId) => this._cache[resId]);\n        if (!this._savePoint) {\n            this._applyCommands(this._initialCommands);\n        }\n        this._savePoint = undefined;\n    }\n\n    /**\n     * @fixme: this method is naive and ineffective (it triggers a lot of onchange rpcs)\n     */\n    async _duplicateRecords(records, options) {\n        const targetIndex = options.targetIndex ?? this.records.indexOf(records.at(-1)) + 1;\n        const copyFields = options.copyFields || [];\n        let sequence = this.records[targetIndex - 1].data[this.handleField] + 1;\n        const newRecords = await Promise.all(\n            records.map(async () =>\n                this._createNewRecordDatapoint({\n                    mode: \"readonly\",\n                })\n            )\n        );\n        await Promise.all(\n            records.map((record, index) =>\n                newRecords[index]._update({\n                    ...copyRecordData(record, copyFields),\n                    [this.handleField]: sequence++,\n                })\n            )\n        );\n\n        const localIncreaseLimit = this.records.length + records.length - this.limit;\n        if (localIncreaseLimit > 0) {\n            this._tmpIncreaseLimit += localIncreaseLimit;\n            const nextLimit = this.limit + localIncreaseLimit;\n            this.model._updateConfig(this.config, { limit: nextLimit }, { reload: false });\n        }\n\n        const commands = [];\n        // `this.records.slice(targetIndex)` is wrong\n        // we need to iterate on ALL the next records even the ones on the next pages..\n        for (const record of this.records.slice(targetIndex)) {\n            commands.push(\n                x2ManyCommands.update(record.resId || record._virtualId, {\n                    [this.handleField]: sequence++,\n                })\n            );\n        }\n        await this._applyCommands(commands);\n\n        await Promise.all(newRecords.map((record) => this._addRecord(record, { sort: false })));\n\n        await this._sort();\n    }\n\n    _getCommands({ withReadonly } = {}) {\n        const { CREATE, UPDATE, LINK } = x2ManyCommands;\n        const commands = [];\n        for (const command of this._commands) {\n            if (command[0] === UPDATE && command[1] in this._unknownRecordCommands) {\n                // the record has never been loaded, but we received update commands from the\n                // server for it, so we need to sanitize them (as they contained unity values)\n                const uCommands = this._unknownRecordCommands[command[1]];\n                for (const uCommand of uCommands) {\n                    const values = fromUnityToServerValues(\n                        uCommand[2],\n                        this.fields,\n                        this.activeFields,\n                        { withReadonly, context: this.context }\n                    );\n                    commands.push([uCommand[0], uCommand[1], values]);\n                }\n            } else if (command[0] === CREATE || command[0] === UPDATE) {\n                const record = this._cache[command[1]];\n                if (command[0] === CREATE && record.resId) {\n                    // we created a new record, but it has already been saved (e.g. because we clicked\n                    // on a view button in the x2many dialog), so replace the CREATE command by a\n                    // LINK\n                    commands.push([LINK, record.resId]);\n                } else {\n                    const values = record._getChanges(record._changes, { withReadonly });\n                    if (command[0] === CREATE || Object.keys(values).length) {\n                        commands.push([command[0], command[1], values]);\n                    }\n                }\n            } else {\n                commands.push(command);\n            }\n        }\n        return commands;\n    }\n\n    _getResIdsToLoad(resIds, fieldNames = this.fieldNames) {\n        return resIds.filter((resId) => {\n            if (typeof resId === \"string\") {\n                // this is a virtual id, we don't want to read it\n                return false;\n            }\n            const record = this._cache[resId];\n            if (!record) {\n                // record hasn't been loaded yet\n                return true;\n            }\n            // record has already been loaded -> check if we already read all orderBy fields\n            fieldNames = fieldNames.filter((fieldName) => fieldName !== \"id\");\n            return intersection(fieldNames, record.fieldNames).length !== fieldNames.length;\n        });\n    }\n\n    async _load({\n        limit = this.limit,\n        offset = this.offset,\n        orderBy = this.orderBy,\n        nextCurrentIds = this._currentIds,\n    } = {}) {\n        const currentIds = nextCurrentIds.slice(offset, offset + limit);\n        const resIds = this._getResIdsToLoad(currentIds);\n        if (resIds.length) {\n            const records = await this.model._loadRecords(\n                { ...this.config, resIds },\n                this.evalContext\n            );\n            for (const record of records) {\n                this._createRecordDatapoint(record);\n            }\n        }\n        this.records = currentIds.map((id) => this._cache[id]);\n        this._currentIds = nextCurrentIds;\n        await this.model._updateConfig(this.config, { limit, offset, orderBy }, { reload: false });\n    }\n\n    async _replaceWith(ids, { reload = false } = {}) {\n        const resIds = reload ? ids : ids.filter((id) => !this._cache[id]);\n        if (resIds.length) {\n            const records = await this.model._loadRecords({\n                ...this.config,\n                resIds,\n                context: this.context,\n            });\n            for (const record of records) {\n                this._createRecordDatapoint(record);\n            }\n        }\n        this.records = ids.map((id) => this._cache[id]);\n        const updateCommandsToKeep = this._commands.filter(\n            (c) => c[0] === x2ManyCommands.UPDATE && ids.includes(c[1])\n        );\n        this._commands = [x2ManyCommands.set(ids)].concat(updateCommandsToKeep);\n        this._currentIds = [...ids];\n        this.count = this._currentIds.length;\n        if (this._currentIds.length > this.limit) {\n            this._tmpIncreaseLimit = this._currentIds.length - this.limit;\n            const nextLimit = this.limit + this._tmpIncreaseLimit;\n            this.model._updateConfig(this.config, { limit: nextLimit }, { reload: false });\n        }\n    }\n\n    async _resequence(movedId, targetId) {\n        const records = [...this.records];\n        const order = this.orderBy.find((o) => o.name === this.handleField);\n        const asc = !order || order.asc;\n\n        // Find indices\n        const fromIndex = records.findIndex((r) => r.id === movedId);\n        let toIndex = 0;\n        if (targetId !== null) {\n            const targetIndex = records.findIndex((r) => r.id === targetId);\n            toIndex = fromIndex > targetIndex ? targetIndex + 1 : targetIndex;\n        }\n\n        const getSequence = (rec) => rec && rec.data[this.handleField];\n\n        // Determine what records need to be modified\n        const firstIndex = Math.min(fromIndex, toIndex);\n        const lastIndex = Math.max(fromIndex, toIndex) + 1;\n        let reorderAll = false;\n        let lastSequence = (asc ? -1 : 1) * Infinity;\n        for (let index = 0; index < records.length; index++) {\n            const sequence = getSequence(records[index]);\n            if ((asc && lastSequence >= sequence) || (!asc && lastSequence <= sequence)) {\n                reorderAll = true;\n                break;\n            }\n            lastSequence = sequence;\n        }\n\n        // Perform the resequence in the list of records\n        const [record] = records.splice(fromIndex, 1);\n        records.splice(toIndex, 0, record);\n\n        // Creates the list of to modify\n        let toReorder = records;\n        if (!reorderAll) {\n            toReorder = toReorder.slice(firstIndex, lastIndex).filter((r) => r.id !== movedId);\n            if (fromIndex < toIndex) {\n                toReorder.push(record);\n            } else {\n                toReorder.unshift(record);\n            }\n        }\n        if (!asc) {\n            toReorder.reverse();\n        }\n\n        const sequences = toReorder.map(getSequence);\n        const offset = sequences.length && Math.min(...sequences);\n\n        const proms = [];\n        for (const [i, record] of Object.entries(toReorder)) {\n            proms.push(\n                record._update(\n                    { [this.handleField]: offset + Number(i) },\n                    { withoutParentUpdate: true }\n                )\n            );\n        }\n        await Promise.all(proms);\n\n        await this._sort();\n        await this._onUpdate();\n    }\n\n    async _sort(currentIds = this.currentIds, orderBy = this.orderBy) {\n        const fieldNames = orderBy.map((o) => o.name);\n        const resIds = this._getResIdsToLoad(currentIds, fieldNames);\n        if (resIds.length) {\n            const activeFields = pick(this.activeFields, ...fieldNames);\n            const config = { ...this.config, resIds, activeFields };\n            const records = await this.model._loadRecords(config);\n            for (const record of records) {\n                this._createRecordDatapoint(record, { activeFields });\n            }\n        }\n        const allRecords = currentIds.map((id) => this._cache[id]);\n        const sortedRecords = allRecords.sort((r1, r2) =>\n            compareRecords(r1, r2, orderBy, this.fields)\n        );\n        await this._load({\n            orderBy,\n            nextCurrentIds: sortedRecords.map((r) => r.resId || r._virtualId),\n        });\n        this._needsReordering = false;\n    }\n\n    async _sortBy(fieldName) {\n        let orderBy = [...this.orderBy];\n        if (fieldName) {\n            if (orderBy.length && orderBy[0].name === fieldName) {\n                if (!this._needsReordering) {\n                    if (orderBy[0].asc) {\n                        orderBy[0] = { name: orderBy[0].name, asc: false };\n                    } else {\n                        orderBy = [{ name: \"id\", asc: true }];\n                    }\n                }\n            } else {\n                orderBy = orderBy.filter((o) => o.name !== fieldName);\n                orderBy.unshift({\n                    name: fieldName,\n                    asc: true,\n                });\n            }\n        }\n        return this._sort(this._currentIds, orderBy);\n    }\n\n    _updateContext(context) {\n        Object.assign(this.context, context);\n        for (const record of Object.values(this._cache)) {\n            record._setEvalContext();\n        }\n    }\n}\n", "import { markup, onWillDestroy, onWillStart, onWillUpdateProps, useComponent } from \"@odoo/owl\";\nimport { evalPartialContext, makeContext } from \"@web/core/context\";\nimport { Domain } from \"@web/core/domain\";\nimport {\n    deserializeDate,\n    deserializeDateTime,\n    serializeDate,\n    serializeDateTime,\n} from \"@web/core/l10n/dates\";\nimport { x2ManyCommands } from \"@web/core/orm_service\";\nimport { evaluateExpr } from \"@web/core/py_js/py\";\nimport { Deferred } from \"@web/core/utils/concurrency\";\nimport { omit } from \"@web/core/utils/objects\";\nimport { effect } from \"@web/core/utils/reactive\";\nimport { batched } from \"@web/core/utils/timing\";\nimport { orderByToString } from \"@web/search/utils/order_by\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { user } from \"@web/core/user\";\nimport { uniqueId } from \"@web/core/utils/functions\";\nimport { unique } from \"@web/core/utils/arrays\";\n\nconst granularityToInterval = {\n    hour: { hours: 1 },\n    day: { days: 1 },\n    week: { days: 7 },\n    month: { month: 1 },\n    quarter: { month: 4 },\n    year: { year: 1 },\n};\n\n/**\n * @param {boolean || string} value boolean or string encoding a python expression\n * @returns {string} string encoding a python expression\n */\nfunction convertBoolToPyExpr(value) {\n    if (value === true || value === false) {\n        return value ? \"True\" : \"False\";\n    }\n    return value;\n}\n\nexport function makeActiveField({\n    context,\n    invisible,\n    readonly,\n    required,\n    onChange,\n    forceSave,\n    isHandle,\n} = {}) {\n    return {\n        context: context || \"{}\",\n        invisible: convertBoolToPyExpr(invisible || false),\n        readonly: convertBoolToPyExpr(readonly || false),\n        required: convertBoolToPyExpr(required || false),\n        onChange: onChange || false,\n        forceSave: forceSave || false,\n        isHandle: isHandle || false,\n    };\n}\n\nexport const AGGREGATABLE_FIELD_TYPES = [\"float\", \"integer\", \"monetary\"]; // types that can be aggregated in grouped views\n\nexport function addFieldDependencies(activeFields, fields, fieldDependencies = []) {\n    for (const field of fieldDependencies) {\n        if (!(\"readonly\" in field)) {\n            field.readonly = true;\n        }\n        if (field.name in activeFields) {\n            patchActiveFields(activeFields[field.name], makeActiveField(field));\n        } else {\n            activeFields[field.name] = makeActiveField(field);\n            if ([\"one2many\", \"many2many\"].includes(field.type)) {\n                activeFields[field.name].related = { activeFields: {}, fields: {} };\n            }\n        }\n        if (!fields[field.name]) {\n            const newField = omit(field, [\n                \"context\",\n                \"invisible\",\n                \"required\",\n                \"readonly\",\n                \"onChange\",\n            ]);\n            fields[field.name] = newField;\n            if (newField.type === \"selection\" && !Array.isArray(newField.selection)) {\n                newField.selection = [];\n            }\n        }\n    }\n}\n\nfunction completeActiveField(activeField, extra) {\n    if (extra.related) {\n        for (const fieldName in extra.related.activeFields) {\n            if (fieldName in activeField.related.activeFields) {\n                completeActiveField(\n                    activeField.related.activeFields[fieldName],\n                    extra.related.activeFields[fieldName]\n                );\n            } else {\n                activeField.related.activeFields[fieldName] = {\n                    ...extra.related.activeFields[fieldName],\n                };\n            }\n        }\n        Object.assign(activeField.related.fields, extra.related.fields);\n    }\n}\n\nexport function completeActiveFields(activeFields, extraActiveFields) {\n    for (const fieldName in extraActiveFields) {\n        const extraActiveField = {\n            ...extraActiveFields[fieldName],\n            invisible: \"True\",\n        };\n        if (fieldName in activeFields) {\n            completeActiveField(activeFields[fieldName], extraActiveField);\n        } else {\n            activeFields[fieldName] = extraActiveField;\n        }\n    }\n}\n\nexport function createPropertyActiveField(property) {\n    const { type } = property;\n\n    const activeField = makeActiveField();\n    if (type === \"one2many\" || type === \"many2many\") {\n        activeField.related = {\n            fields: {\n                id: { name: \"id\", type: \"integer\" },\n                display_name: { name: \"display_name\", type: \"char\" },\n            },\n            activeFields: {\n                id: makeActiveField({ readonly: true }),\n                display_name: makeActiveField(),\n            },\n        };\n    }\n    return activeField;\n}\n\nexport function combineModifiers(mod1, mod2, operator) {\n    if (operator === \"AND\") {\n        if (!mod1 || mod1 === \"False\" || !mod2 || mod2 === \"False\") {\n            return \"False\";\n        }\n        if (mod1 === \"True\") {\n            return mod2;\n        }\n        if (mod2 === \"True\") {\n            return mod1;\n        }\n        return \"(\" + mod1 + \") and (\" + mod2 + \")\";\n    } else if (operator === \"OR\") {\n        if (mod1 === \"True\" || mod2 === \"True\") {\n            return \"True\";\n        }\n        if (!mod1 || mod1 === \"False\") {\n            return mod2;\n        }\n        if (!mod2 || mod2 === \"False\") {\n            return mod1;\n        }\n        return \"(\" + mod1 + \") or (\" + mod2 + \")\";\n    }\n    throw new Error(\n        `Operator provided to \"combineModifiers\" must be \"AND\" or \"OR\", received ${operator}`\n    );\n}\n\nexport function patchActiveFields(activeField, patch) {\n    activeField.invisible = combineModifiers(activeField.invisible, patch.invisible, \"AND\");\n    activeField.readonly = combineModifiers(activeField.readonly, patch.readonly, \"AND\");\n    activeField.required = combineModifiers(activeField.required, patch.required, \"OR\");\n    activeField.onChange = activeField.onChange || patch.onChange;\n    activeField.forceSave = activeField.forceSave || patch.forceSave;\n    activeField.isHandle = activeField.isHandle || patch.isHandle;\n    // x2manys\n    if (patch.related) {\n        const related = activeField.related;\n        for (const fieldName in patch.related.activeFields) {\n            if (fieldName in related.activeFields) {\n                patchActiveFields(\n                    related.activeFields[fieldName],\n                    patch.related.activeFields[fieldName]\n                );\n            } else {\n                related.activeFields[fieldName] = { ...patch.related.activeFields[fieldName] };\n            }\n        }\n        Object.assign(related.fields, patch.related.fields);\n    }\n    if (\"limit\" in patch) {\n        activeField.limit = patch.limit;\n    }\n    if (patch.defaultOrderBy) {\n        activeField.defaultOrderBy = patch.defaultOrderBy;\n    }\n}\n\nexport function extractFieldsFromArchInfo({ fieldNodes, widgetNodes }, fields) {\n    const activeFields = {};\n    for (const fieldNode of Object.values(fieldNodes)) {\n        const fieldName = fieldNode.name;\n        const activeField = makeActiveField({\n            context: fieldNode.context,\n            invisible: combineModifiers(fieldNode.invisible, fieldNode.column_invisible, \"OR\"),\n            readonly: fieldNode.readonly,\n            required: fieldNode.required,\n            onChange: fieldNode.onChange,\n            forceSave: fieldNode.forceSave,\n            isHandle: fieldNode.isHandle,\n        });\n        if ([\"one2many\", \"many2many\"].includes(fields[fieldName].type)) {\n            activeField.related = {\n                activeFields: {},\n                fields: {},\n            };\n            if (fieldNode.views) {\n                const viewDescr = fieldNode.views[fieldNode.viewMode];\n                if (viewDescr) {\n                    activeField.related = extractFieldsFromArchInfo(viewDescr, viewDescr.fields);\n                    activeField.limit = viewDescr.limit;\n                    activeField.defaultOrderBy = viewDescr.defaultOrder;\n                    if (fieldNode.views.form) {\n                        // we already know the form view (it is inline), so add its fields (in invisible)\n                        // s.t. they will be sent in the spec for onchange, and create commands returned\n                        // by the onchange could return values for those fields (that would be displayed\n                        // later if the user opens the form view)\n                        const formArchInfo = extractFieldsFromArchInfo(\n                            fieldNode.views.form,\n                            fieldNode.views.form.fields\n                        );\n                        completeActiveFields(\n                            activeField.related.activeFields,\n                            formArchInfo.activeFields\n                        );\n                        Object.assign(activeField.related.fields, formArchInfo.fields);\n                    }\n\n                    if (fieldNode.viewMode !== \"default\" && fieldNode.views.default) {\n                        const defaultArchInfo = extractFieldsFromArchInfo(\n                            fieldNode.views.default,\n                            fieldNode.views.default.fields\n                        );\n                        for (const fieldName in defaultArchInfo.activeFields) {\n                            if (fieldName in activeField.related.activeFields) {\n                                patchActiveFields(\n                                    activeField.related.activeFields[fieldName],\n                                    defaultArchInfo.activeFields[fieldName]\n                                );\n                            } else {\n                                activeField.related.activeFields[fieldName] = {\n                                    ...defaultArchInfo.activeFields[fieldName],\n                                };\n                            }\n                        }\n                        activeField.related.fields = Object.assign(\n                            {},\n                            defaultArchInfo.fields,\n                            activeField.related.fields\n                        );\n                    }\n                }\n            }\n            if (fieldNode.field?.useSubView) {\n                activeField.required = \"False\";\n            }\n        }\n        if (\n            [\"many2one\", \"many2one_reference\"].includes(fields[fieldName].type) &&\n            fieldNode.views\n        ) {\n            const viewDescr = fieldNode.views.default;\n            activeField.related = extractFieldsFromArchInfo(viewDescr, viewDescr.fields);\n        }\n\n        if (fieldName in activeFields) {\n            patchActiveFields(activeFields[fieldName], activeField);\n        } else {\n            activeFields[fieldName] = activeField;\n        }\n\n        if (fieldNode.field) {\n            let fieldDependencies = fieldNode.field.fieldDependencies;\n            if (typeof fieldDependencies === \"function\") {\n                fieldDependencies = fieldDependencies(fieldNode);\n            }\n            addFieldDependencies(activeFields, fields, fieldDependencies);\n        }\n    }\n\n    for (const widgetInfo of Object.values(widgetNodes || {})) {\n        let fieldDependencies = widgetInfo.widget.fieldDependencies;\n        if (typeof fieldDependencies === \"function\") {\n            fieldDependencies = fieldDependencies(widgetInfo);\n        }\n        addFieldDependencies(activeFields, fields, fieldDependencies);\n    }\n    return { activeFields, fields };\n}\n\nexport function getFieldContext(\n    record,\n    fieldName,\n    rawContext = record.activeFields[fieldName].context\n) {\n    const context = {};\n    for (const key in record.context) {\n        if (\n            !key.startsWith(\"default_\") &&\n            !key.startsWith(\"search_default_\") &&\n            !key.endsWith(\"_view_ref\")\n        ) {\n            context[key] = record.context[key];\n        }\n    }\n\n    return {\n        ...context,\n        ...record.fields[fieldName].context,\n        ...makeContext([rawContext], record.evalContext),\n    };\n}\n\nexport function getFieldDomain(record, fieldName, domain) {\n    if (typeof domain === \"function\") {\n        domain = domain();\n        domain = typeof domain === \"function\" ? domain() : domain;\n    }\n    if (domain) {\n        return domain;\n    }\n    // Fallback to the domain defined in the field definition in python\n    domain = record.fields[fieldName].domain;\n    return typeof domain === \"string\"\n        ? new Domain(evaluateExpr(domain, record.evalContext)).toList()\n        : domain || [];\n}\n\nexport function getBasicEvalContext(config) {\n    const { uid, allowed_company_ids } = config.context;\n    return {\n        context: config.context,\n        uid,\n        allowed_company_ids,\n        current_company_id: user.activeCompany?.id,\n    };\n}\n\nfunction getFieldContextForSpec(activeFields, fields, fieldName, evalContext) {\n    let context = activeFields[fieldName].context;\n    if (!context || context === \"{}\") {\n        context = fields[fieldName].context || {};\n    } else {\n        context = evalPartialContext(context, evalContext);\n    }\n    if (Object.keys(context).length > 0) {\n        return context;\n    }\n}\n\nexport function getFieldsSpec(activeFields, fields, evalContext, { orderBys, withInvisible } = {}) {\n    const fieldsSpec = {};\n    const properties = [];\n    for (const fieldName in activeFields) {\n        if (fields[fieldName].relatedPropertyField) {\n            continue;\n        }\n        const { related, limit, defaultOrderBy, invisible } = activeFields[fieldName];\n        const isAlwaysInvisible = invisible === \"True\" || invisible === \"1\";\n        fieldsSpec[fieldName] = {};\n        switch (fields[fieldName].type) {\n            case \"one2many\":\n            case \"many2many\": {\n                if (related && (withInvisible || !isAlwaysInvisible)) {\n                    fieldsSpec[fieldName].fields = getFieldsSpec(\n                        related.activeFields,\n                        related.fields,\n                        evalContext,\n                        { withInvisible }\n                    );\n                    fieldsSpec[fieldName].context = getFieldContextForSpec(\n                        activeFields,\n                        fields,\n                        fieldName,\n                        evalContext\n                    );\n                    fieldsSpec[fieldName].limit = limit;\n                    const orderBy = orderBys?.[fieldName] || defaultOrderBy || [];\n                    if (orderBy.length) {\n                        fieldsSpec[fieldName].order = orderByToString(orderBy);\n                    }\n                }\n                break;\n            }\n            case \"many2one\":\n            case \"reference\": {\n                fieldsSpec[fieldName].fields = {};\n                if (!isAlwaysInvisible) {\n                    if (related) {\n                        fieldsSpec[fieldName].fields = getFieldsSpec(\n                            related.activeFields,\n                            related.fields,\n                            evalContext\n                        );\n                    }\n                    fieldsSpec[fieldName].fields.display_name = {};\n                    fieldsSpec[fieldName].context = getFieldContextForSpec(\n                        activeFields,\n                        fields,\n                        fieldName,\n                        evalContext\n                    );\n                }\n                break;\n            }\n            case \"many2one_reference\": {\n                if (related && !isAlwaysInvisible) {\n                    fieldsSpec[fieldName].fields = getFieldsSpec(\n                        related.activeFields,\n                        related.fields,\n                        evalContext\n                    );\n                    fieldsSpec[fieldName].context = getFieldContextForSpec(\n                        activeFields,\n                        fields,\n                        fieldName,\n                        evalContext\n                    );\n                }\n                break;\n            }\n            case \"properties\": {\n                properties.push(fieldName);\n                break;\n            }\n        }\n    }\n\n    for (const fieldName of properties) {\n        const fieldSpec = fieldsSpec[fields[fieldName].definition_record];\n        if (fieldSpec) {\n            if (!fieldSpec.fields) {\n                fieldSpec.fields = {};\n            }\n            fieldSpec.fields.display_name = {};\n        }\n    }\n    return fieldsSpec;\n}\n\nlet nextId = 0;\n/**\n * @param {string} [prefix]\n * @returns {string}\n */\nexport function getId(prefix = \"\") {\n    return `${prefix}_${++nextId}`;\n}\n\n/**\n * @protected\n * @param {Field | false} field\n * @param {any} value\n * @returns {any}\n */\nexport function parseServerValue(field, value) {\n    switch (field.type) {\n        case \"char\":\n        case \"text\": {\n            return value || \"\";\n        }\n        case \"html\": {\n            return markup(value || \"\");\n        }\n        case \"date\": {\n            return value ? deserializeDate(value) : false;\n        }\n        case \"datetime\": {\n            return value ? deserializeDateTime(value) : false;\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.find((option) => option[0] === 0);\n                return hasKey0 ? 0 : value;\n            }\n            return value;\n        }\n        case \"reference\": {\n            if (value === false) {\n                return false;\n            }\n            return {\n                resId: value.id.id,\n                resModel: value.id.model,\n                displayName: value.display_name,\n            };\n        }\n        case \"many2one_reference\": {\n            if (value === 0) {\n                // unset many2one_reference fields' value is 0\n                return false;\n            }\n            if (typeof value === \"number\") {\n                // many2one_reference fetched without \"fields\" key in spec -> only returns the id\n                return { resId: value };\n            }\n            return {\n                resId: value.id,\n                displayName: value.display_name,\n            };\n        }\n        case \"many2one\": {\n            if (Array.isArray(value)) {\n                // Used for web_read_group, where the value is an array of [id, display_name]\n                value = { id: value[0], display_name: value[1] };\n            }\n            return value;\n        }\n        case \"properties\": {\n            return value\n                ? value.map((property) => {\n                      if (property.value !== undefined) {\n                          property.value = parseServerValue(property, property.value ?? false);\n                      }\n                      if (property.default !== undefined) {\n                          property.default = parseServerValue(property, property.default ?? false);\n                      }\n                      return property;\n                  })\n                : [];\n        }\n    }\n    return value;\n}\n\nexport function getAggregateSpecifications(fields) {\n    const aggregatableFields = Object.values(fields)\n        .filter((field) => field.aggregator && AGGREGATABLE_FIELD_TYPES.includes(field.type))\n        .map((field) => `${field.name}:${field.aggregator}`);\n    const currencyFields = unique(\n        Object.values(fields)\n            .filter((field) => field.aggregator && field.currency_field)\n            .map((field) => [\n                `${field.currency_field}:array_agg_distinct`,\n                `${field.name}:sum_currency`,\n            ])\n            .flat()\n    );\n    return aggregatableFields.concat(currencyFields);\n}\n\n/**\n * Extract useful information from a group data returned by a call to webReadGroup.\n *\n * @param {Object} groupData\n * @param {string[]} groupBy\n * @param {Object} fields\n * @returns {Object}\n */\nexport function extractInfoFromGroupData(groupData, groupBy, fields, domain) {\n    const info = {};\n    const groupByField = fields[groupBy[0].split(\":\")[0]];\n    info.count = groupData.__count;\n    info.length = info.count; // TODO: remove but still used in DynamicRecordList._updateCount\n    info.domain = Domain.and([domain, groupData.__extra_domain]).toList();\n    info.rawValue = groupData[groupBy[0]];\n    info.value = getValueFromGroupData(groupByField, info.rawValue);\n    if ([\"date\", \"datetime\"].includes(groupByField.type) && info.value) {\n        const granularity = groupBy[0].split(\":\")[1];\n        info.range = {\n            from: info.value,\n            to: info.value.plus(granularityToInterval[granularity]),\n        };\n    }\n    info.displayName = getDisplayNameFromGroupData(groupByField, info.rawValue);\n    info.serverValue = getGroupServerValue(groupByField, info.value);\n    info.aggregates = getAggregatesFromGroupData(groupData, fields);\n    info.values = groupData.__values; // Extra data of the relational groupby field record\n    return info;\n}\n\n/**\n * @param {Object} groupData\n * @returns {Object}\n */\nfunction getAggregatesFromGroupData(groupData, fields) {\n    const aggregates = {};\n    for (const keyAggregate of getAggregateSpecifications(fields)) {\n        if (keyAggregate in groupData) {\n            const [fieldName, aggregate] = keyAggregate.split(\":\");\n            if (aggregate === \"sum_currency\") {\n                const currencies =\n                    groupData[fields[fieldName].currency_field + \":array_agg_distinct\"];\n                if (currencies.length === 1) {\n                    continue;\n                }\n            }\n            aggregates[fieldName] = groupData[keyAggregate];\n        }\n    }\n    return aggregates;\n}\n\n/**\n * @param {import(\"./datapoint\").Field} field\n * @param {any} rawValue\n * @returns {string}\n */\nfunction getDisplayNameFromGroupData(field, rawValue) {\n    switch (field.type) {\n        case \"selection\": {\n            return Object.fromEntries(field.selection)[rawValue];\n        }\n        case \"boolean\": {\n            return rawValue ? _t(\"Yes\") : _t(\"No\");\n        }\n        case \"integer\": {\n            return rawValue ? String(rawValue) : \"0\";\n        }\n        case \"many2one\":\n        case \"many2many\":\n        case \"date\":\n        case \"datetime\":\n        case \"tags\": {\n            return (rawValue && rawValue[1]) || field.falsy_value_label || _t(\"None\");\n        }\n    }\n    return rawValue ? String(rawValue) : field.falsy_value_label || _t(\"None\");\n}\n\n/**\n * @param {import(\"./datapoint\").Field} field\n * @param {any} value\n * @returns {any}\n */\nexport function getGroupServerValue(field, value) {\n    switch (field.type) {\n        case \"many2many\": {\n            return value ? [value] : false;\n        }\n        case \"datetime\": {\n            return value ? serializeDateTime(value) : false;\n        }\n        case \"date\": {\n            return value ? serializeDate(value) : false;\n        }\n        default: {\n            return value || false;\n        }\n    }\n}\n\n/**\n * @param {import(\"./datapoint\").Field} field\n * @param {any} rawValue\n * @param {object} [range]\n * @returns {any}\n */\nfunction getValueFromGroupData(field, rawValue) {\n    if ([\"date\", \"datetime\"].includes(field.type)) {\n        if (!rawValue) {\n            return false;\n        }\n        return parseServerValue(field, rawValue[0]);\n    }\n    const value = parseServerValue(field, rawValue);\n    if (field.type === \"many2one\") {\n        return value && value.id;\n    }\n    if (field.type === \"many2many\") {\n        return value ? value[0] : false;\n    }\n    if (field.type === \"tags\") {\n        return value ? value[0] : false;\n    }\n    return value;\n}\n\n/**\n * Onchanges sometimes return update commands for records we don't know (e.g. if\n * they are on a page we haven't loaded yet). We may actually never load them.\n * When this happens, we must still be able to send back those commands to the\n * server when saving. However, we can't send the commands exactly as we received\n * them, since the values they contain have been \"unity read\". The purpose of this\n * function is to transform field values from the unity format to the format\n * expected by the server for a write.\n * For instance, for a many2one: { id: 3, display_name: \"Marc\" } => 3.\n */\nexport function fromUnityToServerValues(\n    values,\n    fields,\n    activeFields,\n    { withReadonly, context } = {}\n) {\n    const { CREATE, UPDATE } = x2ManyCommands;\n    const serverValues = {};\n    for (const fieldName in values) {\n        let value = values[fieldName];\n        const field = fields[fieldName];\n        const activeField = activeFields[fieldName];\n        if (!withReadonly) {\n            if (field.readonly) {\n                continue;\n            }\n            try {\n                if (evaluateExpr(activeField.readonly, context)) {\n                    continue;\n                }\n            } catch {\n                // if the readonly expression depends on other fields, we can't evaluate it as we\n                // didn't read the record, so we simply ignore it\n            }\n        }\n        switch (fields[fieldName].type) {\n            case \"one2many\":\n            case \"many2many\":\n                value = value.map((c) => {\n                    if (c[0] === CREATE || c[0] === UPDATE) {\n                        const _fields = activeField.related.fields;\n                        const _activeFields = activeField.related.activeFields;\n                        return [\n                            c[0],\n                            c[1],\n                            fromUnityToServerValues(c[2], _fields, _activeFields, { withReadonly }),\n                        ];\n                    }\n                    return [c[0], c[1]];\n                });\n                break;\n            case \"many2one\":\n                value = value ? value.id : false;\n                break;\n            // case \"reference\":\n            //     // TODO\n            //     break;\n        }\n        serverValues[fieldName] = value;\n    }\n    return serverValues;\n}\n\n/**\n * @param {any} field\n * @returns {boolean}\n */\nexport function isRelational(field) {\n    return field && [\"one2many\", \"many2many\", \"many2one\"].includes(field.type);\n}\n\n/**\n * This hook should only be used in a component field because it\n * depends on the record props.\n * The callback will be executed once during setup and each time\n * a record value read in the callback changes.\n * @param {(record) => void} callback\n */\nexport function useRecordObserver(callback) {\n    const component = useComponent();\n    let currentId;\n    const observeRecord = (props) => {\n        currentId = uniqueId();\n        if (!props.record) {\n            return;\n        }\n        const def = new Deferred();\n        const effectId = currentId;\n        let firstCall = true;\n        effect(\n            (record) => {\n                if (firstCall) {\n                    firstCall = false;\n                    return Promise.resolve(callback(record, props))\n                        .then(def.resolve)\n                        .catch(def.reject);\n                } else {\n                    return batched(\n                        (record) => {\n                            if (effectId !== currentId) {\n                                // effect doesn't clean up when the component is unmounted.\n                                // We must do it manually.\n                                return;\n                            }\n                            return Promise.resolve(callback(record, props))\n                                .then(def.resolve)\n                                .catch(def.reject);\n                        },\n                        () => new Promise((resolve) => window.requestAnimationFrame(resolve))\n                    )(record);\n                }\n            },\n            [props.record]\n        );\n        return def;\n    };\n    onWillDestroy(() => {\n        currentId = uniqueId();\n    });\n    onWillStart(() => observeRecord(component.props));\n    onWillUpdateProps((nextProps) => {\n        if (nextProps.record !== component.props.record) {\n            return observeRecord(nextProps);\n        }\n    });\n}\n\n/**\n * Resequence records based on provided parameters.\n *\n * @param {Object} params\n * @param {Array} params.records - The list of records to resequence.\n * @param {string} params.resModel - The model to be used for resequencing.\n * @param {Object} params.orm\n * @param {string} params.fieldName - The field used to handle the sequence.\n * @param {number} params.movedId - The id of the record being moved.\n * @param {number} [params.targetId] - The id of the target position, the record will be resequenced\n *                                     after the target. If undefined, the record will be resequenced\n *                                     as the first record.\n * @param {Boolean} [params.asc] - Resequence in ascending or descending order\n * @param {Function} [params.getSequence] - Function to get the sequence of a record.\n * @param {Function} [params.getResId] - Function to get the resID of the record.\n * @param {Object} [params.context]\n * @returns {Promise<any>} - The list of the resequenced fieldName\n */\nexport async function resequence({\n    records,\n    resModel,\n    orm,\n    fieldName,\n    movedId,\n    targetId,\n    asc = true,\n    getSequence = (record) => record[fieldName],\n    getResId = (record) => record.id,\n    context,\n}) {\n    // Find indices\n    const fromIndex = records.findIndex((d) => d.id === movedId);\n    let toIndex = 0;\n    if (targetId !== null) {\n        const targetIndex = records.findIndex((d) => d.id === targetId);\n        toIndex = fromIndex > targetIndex ? targetIndex + 1 : targetIndex;\n    }\n\n    // Determine which records/groups need to be modified\n    const firstIndex = Math.min(fromIndex, toIndex);\n    const lastIndex = Math.max(fromIndex, toIndex) + 1;\n    let reorderAll = records.some((record) => getSequence(record) === undefined);\n    if (!reorderAll) {\n        let lastSequence = (asc ? -1 : 1) * Infinity;\n        for (let index = 0; index < records.length; index++) {\n            const sequence = getSequence(records[index]);\n            if ((asc && lastSequence >= sequence) || (!asc && lastSequence <= sequence)) {\n                reorderAll = true;\n                break;\n            }\n            lastSequence = sequence;\n        }\n    }\n\n    // Save the original list in case of error\n    const originalOrder = [...records];\n    // Perform the resequence in the list of records/groups\n    const record = records[fromIndex];\n    if (fromIndex !== toIndex) {\n        records.splice(fromIndex, 1);\n        records.splice(toIndex, 0, record);\n    }\n\n    // Creates the list of records/groups to modify\n    let toReorder = records;\n    if (!reorderAll) {\n        toReorder = toReorder.slice(firstIndex, lastIndex).filter((r) => r.id !== movedId);\n        if (fromIndex < toIndex) {\n            toReorder.push(record);\n        } else {\n            toReorder.unshift(record);\n        }\n    }\n    if (!asc) {\n        toReorder.reverse();\n    }\n\n    const resIds = toReorder.map((d) => getResId(d)).filter((id) => id && !isNaN(id));\n    const sequences = toReorder.map(getSequence);\n    const offset = Math.min(...sequences) || 0;\n\n    // Try to write new sequences on the affected records/groups\n    try {\n        return await orm.webResequence(resModel, resIds, {\n            field_name: fieldName,\n            offset,\n            context,\n            specification: { [fieldName]: {} },\n        });\n    } catch (error) {\n        // If the server fails to resequence, rollback the original list\n        records.splice(0, records.length, ...originalOrder);\n        throw error;\n    }\n}\n", "import {\n    deserializeDate,\n    deserializeDateTime,\n    serializeDate,\n    serializeDateTime,\n} from \"@web/core/l10n/dates\";\nimport { ORM } from \"@web/core/orm_service\";\nimport { registry } from \"@web/core/registry\";\nimport { cartesian, sortBy as arraySortBy, unique } from \"@web/core/utils/arrays\";\nimport { parseServerValue } from \"./relational_model/utils\";\n\nclass UnimplementedRouteError extends Error {}\n\n/**\n * Helper function returning the value from a list of sample strings\n * corresponding to the given ID.\n * @param {number} id\n * @param {string[]} sampleTexts\n * @returns {string}\n */\nfunction getSampleFromId(id, sampleTexts) {\n    return sampleTexts[(id - 1) % sampleTexts.length];\n}\n\nfunction serializeGroupDateValue(range, field) {\n    if (!range) {\n        return false;\n    }\n    const dateValue = parseServerValue(field, range[0]);\n    return field.type === \"date\" ? serializeDate(dateValue) : serializeDateTime(dateValue);\n}\n\n/**\n * Helper function returning a regular expression specifically matching\n * a given 'term' in a fieldName. For example `fieldNameRegex('abc')`:\n * will match:\n * - \"abc\"\n * - \"field_abc__def\"\n * will not match:\n * - \"aabc\"\n * - \"abcd_ef\"\n * @param {...string} term\n * @returns {RegExp}\n */\nfunction fieldNameRegex(...terms) {\n    return new RegExp(`\\\\b((\\\\w+)?_)?(${terms.join(\"|\")})(_(\\\\w+)?)?\\\\b`);\n}\n\nconst MEASURE_SPEC_REGEX = /(?<fieldName>\\w+):(?<func>\\w+)/;\nconst DESCRIPTION_REGEX = fieldNameRegex(\"description\", \"label\", \"title\", \"subject\", \"message\");\nconst EMAIL_REGEX = fieldNameRegex(\"email\");\nconst PHONE_REGEX = fieldNameRegex(\"phone\");\nconst URL_REGEX = fieldNameRegex(\"url\");\nconst MAX_NUMBER_OPENED_GROUPS = 10;\n\n/**\n * Sample server class\n *\n * Represents a static instance of the server used when a RPC call sends\n * empty values/groups while the attribute 'sample' is set to true on the\n * view.\n *\n * This server will generate fake data and send them in the adequate format\n * according to the route/method used in the RPC.\n */\nexport class SampleServer {\n    /**\n     * @param {string} modelName\n     * @param {Object} fields\n     */\n    constructor(modelName, fields) {\n        this.mainModel = modelName;\n        this.data = {};\n        this.data[modelName] = {\n            fields,\n            records: [],\n        };\n        // Generate relational fields' co models\n        for (const fieldName in fields) {\n            const field = fields[fieldName];\n            if ([\"many2one\", \"one2many\", \"many2many\"].includes(field.type)) {\n                this.data[field.relation] = this.data[field.relation] || {\n                    fields: {\n                        display_name: { type: \"char\" },\n                        id: { type: \"integer\" },\n                        color: { type: \"integer\" },\n                    },\n                    records: [],\n                };\n            }\n        }\n        // On some models, empty grouped Kanban or List view still contain\n        // real (empty) groups. In this case, we re-use the result of the\n        // web_read_group rpc to tweak sample data s.t. those real groups\n        // contain sample records.\n        this.existingGroups = null;\n        // Sample records generation is only done if necessary, so we delay\n        // it to the first \"mockRPC\" call. These flags allow us to know if\n        // the records have been generated or not.\n        this.populated = false;\n        this.existingGroupsPopulated = false;\n    }\n\n    //---------------------------------------------------------------------\n    // Public\n    //---------------------------------------------------------------------\n\n    /**\n     * This is the main entry point of the SampleServer. Mocks a request to\n     * the server with sample data.\n     * @param {Object} params\n     * @returns {any} the result obtained with the sample data\n     * @throws {Error} If called on a route/method we do not handle\n     */\n    mockRpc(params) {\n        if (!(params.model in this.data)) {\n            throw new Error(`SampleServer: unknown model ${params.model}`);\n        }\n        this._populateModels();\n        switch (params.method || params.route) {\n            case \"web_search_read\":\n                return this._mockWebSearchReadUnity(params);\n            case \"web_read_group\":\n                return this._mockWebReadGroup(params);\n            case \"formatted_read_group\":\n                return this._mockFormattedReadGroup(params);\n            case \"formatted_read_grouping_sets\":\n                return this._mockFormattedReadGroupingSets(params);\n            case \"read_progress_bar\":\n                return this._mockReadProgressBar(params);\n            case \"read\":\n                return this._mockRead(params);\n        }\n        // this rpc can't be mocked by the SampleServer itself, so check if there is an handler\n        // in the registry: either specific for this model (with key 'model/method'), or\n        // global (with key 'method')\n        const method = params.method || params.route;\n        // This allows to register mock version of methods or routes,\n        // for all models:\n        // registry.category(\"sample_server\").add('some_route', () => \"abcd\");\n        // for a specific model (e.g. 'res.partner'):\n        // registry.category(\"sample_server\").add('res.partner/some_method', () => 23);\n        const mockFunction =\n            registry.category(\"sample_server\").get(`${params.model}/${method}`, null) ||\n            registry.category(\"sample_server\").get(method, null);\n        if (mockFunction) {\n            return mockFunction.call(this, params);\n        }\n        console.log(`SampleServer: unimplemented route \"${params.method || params.route}\"`);\n        throw new SampleServer.UnimplementedRouteError();\n    }\n\n    setExistingGroups(groups) {\n        this.existingGroups = groups;\n    }\n\n    //---------------------------------------------------------------------\n    // Private\n    //---------------------------------------------------------------------\n\n    /**\n     * @param {Object[]} measures, each measure has the form { fieldName, type }\n     * @param {Object[]} records\n     * @returns {Object}\n     */\n    _aggregateFields(measures, records) {\n        const group = {};\n        for (const { fieldName, func, name } of measures) {\n            if ([\"sum\", \"sum_currency\", \"avg\", \"max\", \"min\"].includes(func)) {\n                if (!records.length) {\n                    group[name] = false;\n                } else {\n                    group[name] = 0;\n                    for (const record of records) {\n                        group[name] += record[fieldName];\n                    }\n                }\n                group[name] = this._sanitizeNumber(group[name]);\n            } else if (func === \"array_agg\") {\n                group[name] = records.map((r) => r[fieldName]);\n            } else if (func === \"__count\") {\n                group[name] = records.length;\n            } else if (func === \"count_distinct\") {\n                group[name] = unique(records.map((r) => r[fieldName])).filter(Boolean).length;\n            } else if (func === \"bool_or\") {\n                group[name] = records.some((r) => Boolean(r[fieldName]));\n            } else if (func === \"bool_and\") {\n                group[name] = records.every((r) => Boolean(r[fieldName]));\n            } else if (func === \"array_agg_distinct\") {\n                group[name] = unique(records.map((r) => r[fieldName]));\n            } else {\n                throw new Error(`Aggregate \"${func}\" not implemented in SampleServer`);\n            }\n        }\n        return group;\n    }\n\n    /**\n     * @param {any} value\n     * @param {Object} options\n     * @param {string} [options.interval]\n     * @param {string} [options.relation]\n     * @param {string} [options.type]\n     * @returns {any}\n     */\n    _formatValue(value, options) {\n        if (!value) {\n            return false;\n        }\n        const { type, interval, relation } = options;\n        if ([\"date\", \"datetime\"].includes(type) && value) {\n            const deserialize = type === \"date\" ? deserializeDate : deserializeDateTime;\n            const serialize = type === \"date\" ? serializeDate : serializeDateTime;\n            const from = deserialize(value).startOf(interval);\n            const fmt = SampleServer.FORMATS[interval];\n            return [serialize(from), from.toFormat(fmt)];\n        } else if ([\"many2one\", \"many2many\"].includes(type)) {\n            const rec = this.data[relation].records.find(({ id }) => id === value);\n            return [value, rec.display_name];\n        } else {\n            return value;\n        }\n    }\n\n    /**\n     * Generates field values based on heuristics according to field types\n     * and names.\n     *\n     * @private\n     * @param {string} modelName\n     * @param {string} fieldName\n     * @param {number} id the record id\n     * @returns {any} the field value\n     */\n    _generateFieldValue(modelName, fieldName, id) {\n        const field = this.data[modelName].fields[fieldName];\n        switch (field.type) {\n            case \"boolean\":\n                return fieldName === \"active\" ? true : this._getRandomBool();\n            case \"char\":\n            case \"text\":\n                if ([\"display_name\", \"name\"].includes(fieldName)) {\n                    if (SampleServer.PEOPLE_MODELS.includes(modelName)) {\n                        return getSampleFromId(id, SampleServer.SAMPLE_PEOPLE);\n                    } else if (modelName === \"res.country\") {\n                        return getSampleFromId(id, SampleServer.SAMPLE_COUNTRIES);\n                    }\n                }\n                if (fieldName === \"display_name\") {\n                    return getSampleFromId(id, SampleServer.SAMPLE_TEXTS);\n                } else if ([\"name\", \"reference\"].includes(fieldName)) {\n                    return `REF${String(id).padStart(4, \"0\")}`;\n                } else if (DESCRIPTION_REGEX.test(fieldName)) {\n                    return getSampleFromId(id, SampleServer.SAMPLE_TEXTS);\n                } else if (EMAIL_REGEX.test(fieldName)) {\n                    const emailName = getSampleFromId(id, SampleServer.SAMPLE_PEOPLE)\n                        .replace(/ /, \".\")\n                        .toLowerCase();\n                    return `${emailName}@sample.demo`;\n                } else if (PHONE_REGEX.test(fieldName)) {\n                    return `+1 555 754 ${String(id).padStart(4, \"0\")}`;\n                } else if (URL_REGEX.test(fieldName)) {\n                    return `http://sample${id}.com`;\n                }\n                return false;\n            case \"date\":\n            case \"datetime\": {\n                const datetime = this._getRandomDate();\n                return field.type === \"date\"\n                    ? serializeDate(datetime)\n                    : serializeDateTime(datetime);\n            }\n            case \"float\":\n                return this._getRandomFloat(SampleServer.MAX_FLOAT);\n            case \"integer\": {\n                let max = SampleServer.MAX_INTEGER;\n                if (fieldName.includes(\"color\")) {\n                    max = this._getRandomBool() ? SampleServer.MAX_COLOR_INT : 0;\n                }\n                return this._getRandomInt(max);\n            }\n            case \"monetary\":\n                return this._getRandomInt(SampleServer.MAX_MONETARY);\n            case \"many2one\":\n                if (field.relation === \"res.currency\") {\n                    /** @todo return session.company_currency_id */\n                    return 1;\n                }\n                if (field.relation === \"ir.attachment\") {\n                    return false;\n                }\n                return this._getRandomSubRecordId();\n            case \"one2many\":\n            case \"many2many\": {\n                const ids = [this._getRandomSubRecordId(), this._getRandomSubRecordId()];\n                return [...new Set(ids)];\n            }\n            case \"selection\": {\n                return this._getRandomSelectionValue(modelName, field);\n            }\n            default:\n                return false;\n        }\n    }\n\n    /**\n     * @private\n     * @param {any[]} array\n     * @returns {any}\n     */\n    _getRandomArrayEl(array) {\n        return array[Math.floor(Math.random() * array.length)];\n    }\n\n    /**\n     * @private\n     * @returns {boolean}\n     */\n    _getRandomBool() {\n        return Math.random() < 0.5;\n    }\n\n    /**\n     * @private\n     * @returns {DateTime}\n     */\n    _getRandomDate() {\n        const delta = Math.floor((Math.random() - Math.random()) * SampleServer.DATE_DELTA);\n        return luxon.DateTime.local().plus({ hours: delta });\n    }\n\n    /**\n     * @private\n     * @param {number} max\n     * @returns {number} float in [O, max[\n     */\n    _getRandomFloat(max) {\n        return this._sanitizeNumber(Math.random() * max);\n    }\n\n    /**\n     * @private\n     * @param {number} max\n     * @returns {number} int in [0, max[\n     */\n    _getRandomInt(max) {\n        return Math.floor(Math.random() * max);\n    }\n\n    /**\n     * @private\n     * @returns {string}\n     */\n    _getRandomSelectionValue(modelName, field) {\n        if (field.selection.length > 0) {\n            return this._getRandomArrayEl(field.selection)[0];\n        }\n        return false;\n    }\n\n    /**\n     * @private\n     * @returns {number} id in [1, SUB_RECORDSET_SIZE]\n     */\n    _getRandomSubRecordId() {\n        return Math.floor(Math.random() * SampleServer.SUB_RECORDSET_SIZE) + 1;\n    }\n\n    /**\n     * Mocks calls to the read method.\n     * @private\n     * @param {Object} params\n     * @param {string} params.model\n     * @param {Array[]} params.args (args[0] is the list of ids, args[1] is\n     *   the list of fields)\n     * @returns {Object[]}\n     */\n    _mockRead(params) {\n        const model = this.data[params.model];\n        const ids = params.args[0];\n        const fieldNames = params.args[1];\n        const records = [];\n        for (const r of model.records) {\n            if (!ids.includes(r.id)) {\n                continue;\n            }\n            const record = { id: r.id };\n            for (const fieldName of fieldNames) {\n                const field = model.fields[fieldName];\n                if (!field) {\n                    record[fieldName] = false; // unknown field\n                } else if (field.type === \"many2one\") {\n                    const relModel = this.data[field.relation];\n                    const relRecord = relModel.records.find((relR) => r[fieldName] === relR.id);\n                    record[fieldName] = relRecord ? [relRecord.id, relRecord.display_name] : false;\n                } else {\n                    record[fieldName] = r[fieldName];\n                }\n            }\n            records.push(record);\n        }\n        return records;\n    }\n\n    /**\n     * Mocks calls to the base method of formatted_read_group method.\n     *\n     * @param {Object} params\n     * @param {string} params.model\n     * @param {string[]} params.groupBy\n     * @param {string[]} params.aggregates\n     * @returns {Object[]} Object with keys groups and length\n     */\n    _mockFormattedReadGroup(params) {\n        const model = params.model;\n        const groupBy = params.groupBy;\n        const fields = this.data[model].fields;\n        const records = this.data[model].records;\n        const normalizedGroupBys = [];\n\n        for (const groupBySpec of groupBy) {\n            const [fieldName, interval] = groupBySpec.split(\":\");\n            const { type, relation } = fields[fieldName];\n            if (type) {\n                const gb = { fieldName, type, interval, relation, alias: groupBySpec };\n                normalizedGroupBys.push(gb);\n            }\n        }\n\n        const groupsFromRecord = (record) => {\n            const values = [];\n            for (const gb of normalizedGroupBys) {\n                const { fieldName, type, alias } = gb;\n                let fieldVals;\n                if ([\"date\", \"datetime\"].includes(type)) {\n                    fieldVals = [this._formatValue(record[fieldName], gb)];\n                } else if (type === \"many2many\") {\n                    fieldVals = record[fieldName].length ? record[fieldName] : [false];\n                } else {\n                    fieldVals = [record[fieldName]];\n                }\n                values.push(fieldVals.map((val) => ({ [alias]: val })));\n            }\n            const cart = cartesian(...values);\n            return cart.map((tuple) => {\n                if (!Array.isArray(tuple)) {\n                    tuple = [tuple];\n                }\n                return Object.assign({}, ...tuple);\n            });\n        };\n\n        const groups = {};\n        for (const record of records) {\n            const recordGroups = groupsFromRecord(record);\n            for (const group of recordGroups) {\n                const groupId = JSON.stringify(group);\n                if (!(groupId in groups)) {\n                    groups[groupId] = [];\n                }\n                groups[groupId].push(record);\n            }\n        }\n\n        const aggregates = params.aggregates || [];\n        const measures = [];\n        for (const measureSpec of aggregates) {\n            if (measureSpec === \"__count\") {\n                measures.push({ fieldName: \"__count\", func: \"__count\", name: measureSpec });\n                continue;\n            }\n            const matches = measureSpec.match(MEASURE_SPEC_REGEX);\n            if (!matches) {\n                throw new Error(`Invalidate Aggregate \"${measureSpec}\" in SampleServer`);\n            }\n            const { fieldName, func } = matches.groups;\n            measures.push({ fieldName, func, name: measureSpec });\n        }\n\n        let result = [];\n        for (const id in groups) {\n            const records = groups[id];\n            const group = { __extra_domain: [] };\n            const firstElem = records[0];\n            const parsedId = JSON.parse(id);\n            for (const gb of normalizedGroupBys) {\n                const { alias, fieldName, type } = gb;\n                if (type === \"many2many\") {\n                    group[alias] = this._formatValue(parsedId[fieldName], gb);\n                } else {\n                    group[alias] = this._formatValue(firstElem[fieldName], gb);\n                }\n            }\n            Object.assign(group, this._aggregateFields(measures, records));\n            result.push(group);\n        }\n        if (normalizedGroupBys.length > 0) {\n            const { alias, type } = normalizedGroupBys[0];\n            result = arraySortBy(result, (group) => {\n                const val = group[alias];\n                if (type === \"datetime\") {\n                    return deserializeDateTime(val);\n                } else if (type === \"date\") {\n                    return deserializeDate(val);\n                }\n                return val;\n            });\n        }\n        return result;\n    }\n\n    /**\n     * Mocks calls to the base method of formatted_read_grouping_sets method.\n     *\n     * @param {Object} params\n     * @param {string} params.model\n     * @param {string[][]} params.grouping_sets\n     * @param {string[]} params.aggregates\n     * @returns {Object[]} Object with keys groups and length\n     */\n    _mockFormattedReadGroupingSets(params) {\n        const res = [];\n        for (const groupBy of params.grouping_sets) {\n            res.push(this._mockFormattedReadGroup({ ...params, groupBy }));\n        }\n        return res;\n    }\n\n    /**\n     * Mocks calls to the read_progress_bar method.\n     * @private\n     * @param {Object} params\n     * @param {string} params.model\n     * @param {string} params.group_by\n     * @param {Object} params.progress_bar\n     * @return {Object}\n     */\n    _mockReadProgressBar(params) {\n        const groupBy = params.group_by;\n        const progressBar = params.progress_bar;\n        const groups = this._mockFormattedReadGroup({\n            model: params.model,\n            domain: params.domain,\n            groupBy: [groupBy, progressBar.field],\n            aggregates: [\"__count\"],\n        });\n        const data = {};\n        for (const group of groups) {\n            let groupByValue = group[groupBy];\n            if (Array.isArray(groupByValue)) {\n                groupByValue = groupByValue[0];\n            }\n\n            // special case for bool values: rpc call response with capitalized strings\n            if (!(groupByValue in data)) {\n                if (groupByValue === true) {\n                    groupByValue = \"True\";\n                } else if (groupByValue === false) {\n                    groupByValue = \"False\";\n                }\n            }\n\n            if (!(groupByValue in data)) {\n                data[groupByValue] = {};\n                for (const key in progressBar.colors) {\n                    data[groupByValue][key] = 0;\n                }\n            }\n            data[groupByValue][group[progressBar.field]] += group.__count;\n        }\n        return data;\n    }\n\n    _mockWebSearchReadUnity(params) {\n        const fields = Object.keys(params.specification);\n        const model = this.data[params.model];\n        let rawRecords = model.records;\n        if (\"recordIds\" in params) {\n            rawRecords = model.records.filter((record) => params.recordIds.includes(record.id));\n        } else {\n            rawRecords = rawRecords.slice(0, SampleServer.SEARCH_READ_LIMIT);\n        }\n        const records = this._mockRead({\n            model: params.model,\n            args: [rawRecords.map((r) => r.id), fields],\n        });\n        const result = { records, length: records.length };\n        // populate many2one and x2many values\n        for (const fieldName in params.specification) {\n            const field = this.data[params.model].fields[fieldName];\n            if (field.type === \"many2one\") {\n                for (const record of result.records) {\n                    record[fieldName] = record[fieldName]\n                        ? {\n                              id: record[fieldName][0],\n                              display_name: record[fieldName][1],\n                          }\n                        : false;\n                }\n            }\n            if (field.type === \"one2many\" || field.type === \"many2many\") {\n                const relFields = Object.keys(params.specification[fieldName].fields || {});\n                if (relFields.length) {\n                    const relIds = result.records.map((r) => r[fieldName]).flat();\n                    const relRecords = {};\n                    const _relRecords = this._mockRead({\n                        model: field.relation,\n                        args: [relIds, relFields],\n                    });\n                    for (const relRecord of _relRecords) {\n                        relRecords[relRecord.id] = relRecord;\n                    }\n                    for (const record of result.records) {\n                        record[fieldName] = record[fieldName].map((resId) => relRecords[resId]);\n                    }\n                }\n            }\n        }\n        return result;\n    }\n\n    /**\n     * Mocks calls to the web_read_group method to return groups populated\n     * with sample records. Only handles the case where the real call to\n     * web_read_group returned groups, but none of these groups contain\n     * records. In this case, we keep the real groups, and populate them\n     * with sample records.\n     * @private\n     * @param {Object} params\n     * @param {Object} [result] the result of a real call to web_read_group\n     * @returns {{ groups: Object[], length: number }}\n     */\n    _mockWebReadGroup(params) {\n        const aggregates = [...params.aggregates, \"__count\"];\n        if (params.unfold_read_specification) {\n            aggregates.push(\"id:array_agg\");\n        }\n        let groups;\n        if (this.existingGroups) {\n            this._tweakExistingGroups({ ...params, aggregates });\n            groups = this.existingGroups;\n        } else {\n            groups = this._mockFormattedReadGroup({ ...params, aggregates });\n        }\n        // Don't care another params - and no subgroup:\n        // order / opening_info / unfold_read_default_limit / groupby_read_specification\n        let nbOpenedGroup = 0;\n        if (params.unfold_read_specification) {\n            for (const group of groups) {\n                if (params.auto_unfold || \"__records\" in group) {\n                    // if group has a \"__records\" key, it means that it is an existing group, and\n                    // that the real webReadGroup returned a \"__records\" key for that group (which\n                    // is empty, otherwise we wouldn't be here), i.e. that group is opened.\n                    if (nbOpenedGroup < MAX_NUMBER_OPENED_GROUPS) {\n                        nbOpenedGroup++;\n                        group[\"__records\"] = this._mockWebSearchReadUnity({\n                            model: params.model,\n                            specification: params.unfold_read_specification,\n                            recordIds: group[\"id:array_agg\"],\n                        }).records;\n                    }\n                }\n                delete group[\"id:array_agg\"];\n            }\n        }\n\n        return {\n            groups,\n            length: groups.length,\n        };\n    }\n\n    /**\n     * Updates the sample data such that the existing groups (in database)\n     * also exists in the sample, and such that there are sample records in\n     * those groups.\n     * @private\n     * @param {Object[]} groups empty groups returned by the server\n     * @param {Object} params\n     * @param {string} params.model\n     * @param {string[]} params.groupBy\n     */\n    _populateExistingGroups(params) {\n        const groups = this.existingGroups;\n        const groupBy = params.groupBy[0].split(\":\")[0];\n        const groupByField = this.data[params.model].fields[groupBy];\n        const groupedByM2O = groupByField.type === \"many2one\";\n        if (groupedByM2O) {\n            // re-populate co model with relevant records\n            this.data[groupByField.relation].records = groups.map((g) => ({\n                id: g[groupBy][0],\n                display_name: g[groupBy][1],\n            }));\n        }\n        for (const r of this.data[params.model].records) {\n            const group = getSampleFromId(r.id, groups);\n            if ([\"date\", \"datetime\"].includes(groupByField.type)) {\n                r[groupBy] = serializeGroupDateValue(group[params.groupBy[0]], groupByField);\n            } else if (groupByField.type === \"many2one\") {\n                r[groupBy] = group[params.groupBy[0]] ? group[params.groupBy[0]][0] : false;\n            } else {\n                r[groupBy] = group[params.groupBy[0]];\n            }\n        }\n    }\n\n    /**\n     * Generates sample records for the models in this.data. Records will be\n     * generated once, and subsequent calls to this function will be skipped.\n     * @private\n     */\n    _populateModels() {\n        if (!this.populated) {\n            for (const modelName in this.data) {\n                const model = this.data[modelName];\n                const fieldNames = Object.keys(model.fields).filter((f) => f !== \"id\");\n                const size =\n                    modelName === this.mainModel\n                        ? SampleServer.MAIN_RECORDSET_SIZE\n                        : SampleServer.SUB_RECORDSET_SIZE;\n                for (let id = 1; id <= size; id++) {\n                    const record = { id };\n                    for (const fieldName of fieldNames) {\n                        record[fieldName] = this._generateFieldValue(modelName, fieldName, id);\n                    }\n                    model.records.push(record);\n                }\n            }\n            this.populated = true;\n        }\n    }\n\n    /**\n     * Rounds the given number value according to the configured precision.\n     * @private\n     * @param {number} value\n     * @returns {number}\n     */\n    _sanitizeNumber(value) {\n        return parseFloat(value.toFixed(SampleServer.FLOAT_PRECISION));\n    }\n\n    /**\n     * A real (web_)read_group call has been done, and it has returned groups,\n     * but they are all empty. This function updates the sample data such\n     * that those group values exist and those groups contain sample records.\n     * @private\n     * @param {Object[]} groups empty groups returned by the server\n     * @param {Object} params\n     * @param {string} params.model\n     * @param {string[]} params.aggregates\n     * @param {string[]} params.groupBy\n     * @returns {Object[]} groups with count and aggregate values updated\n     *\n     * TODO: rename\n     */\n    _tweakExistingGroups(params) {\n        const groups = this.existingGroups;\n        this._populateExistingGroups(params);\n\n        // update count and aggregates for each group\n        const fullGroupBy = params.groupBy[0];\n        const groupBy = fullGroupBy.split(\":\")[0];\n        const groupByField = this.data[params.model].fields[groupBy];\n        const records = this.data[params.model].records;\n        for (const g of groups) {\n            const recordsInGroup = records.filter((r) => {\n                if ([\"date\", \"datetime\"].includes(groupByField.type)) {\n                    return r[groupBy] === serializeGroupDateValue(g[fullGroupBy], groupByField);\n                } else if (groupByField.type === \"many2one\") {\n                    return (!r[groupBy] && !g[fullGroupBy]) || r[groupBy] === g[fullGroupBy][0];\n                }\n                return r[groupBy] === g[fullGroupBy];\n            });\n            for (const aggregateSpec of params.aggregates || []) {\n                if (aggregateSpec === \"__count\") {\n                    g.__count = recordsInGroup.length;\n                    continue;\n                }\n                const [fieldName, func] = aggregateSpec.split(\":\");\n                if (func === \"array_agg\") {\n                    g[aggregateSpec] = recordsInGroup.map((r) => r[fieldName]);\n                } else if (\n                    [\"integer, float\", \"monetary\"].includes(\n                        this.data[params.model].fields[fieldName].type\n                    )\n                ) {\n                    g[aggregateSpec] = recordsInGroup.reduce((acc, r) => acc + r[fieldName], 0);\n                }\n            }\n        }\n    }\n}\n\nSampleServer.FORMATS = {\n    day: \"yyyy-MM-dd\",\n    week: \"'W'WW kkkk\",\n    month: \"MMMM yyyy\",\n    quarter: \"'Q'q yyyy\",\n    year: \"y\",\n};\nSampleServer.INTERVALS = {\n    day: (dt) => dt.plus({ days: 1 }),\n    week: (dt) => dt.plus({ weeks: 1 }),\n    month: (dt) => dt.plus({ months: 1 }),\n    quarter: (dt) => dt.plus({ months: 3 }),\n    year: (dt) => dt.plus({ years: 1 }),\n};\nSampleServer.DISPLAY_FORMATS = Object.assign({}, SampleServer.FORMATS, { day: \"dd MMM yyyy\" });\n\nSampleServer.MAIN_RECORDSET_SIZE = 16;\nSampleServer.SUB_RECORDSET_SIZE = 5;\nSampleServer.SEARCH_READ_LIMIT = 10;\n\nSampleServer.MAX_FLOAT = 100;\nSampleServer.MAX_INTEGER = 50;\nSampleServer.MAX_COLOR_INT = 7;\nSampleServer.MAX_MONETARY = 100000;\nSampleServer.DATE_DELTA = 24 * 60; // in hours -> 60 days\nSampleServer.FLOAT_PRECISION = 2;\n\nSampleServer.SAMPLE_COUNTRIES = [\"Belgium\", \"France\", \"Portugal\", \"Singapore\", \"Australia\"];\nSampleServer.SAMPLE_PEOPLE = [\n    \"John Miller\",\n    \"Henry Campbell\",\n    \"Carrie Helle\",\n    \"Wendi Baltz\",\n    \"Thomas Passot\",\n];\nSampleServer.SAMPLE_TEXTS = [\n    \"Laoreet id\",\n    \"Volutpat blandit\",\n    \"Integer vitae\",\n    \"Viverra nam\",\n    \"In massa\",\n];\nSampleServer.PEOPLE_MODELS = [\n    \"res.users\",\n    \"res.partner\",\n    \"hr.employee\",\n    \"mail.followers\",\n    \"mailing.contact\",\n];\n\nSampleServer.UnimplementedRouteError = UnimplementedRouteError;\n\nexport function buildSampleORM(resModel, fields, user) {\n    const sampleServer = new SampleServer(resModel, fields);\n    const fakeRPC = async (_, params) => {\n        const { args, kwargs, method, model } = params;\n        const { groupby: groupBy } = kwargs;\n        return sampleServer.mockRpc({ method, model, args, ...kwargs, groupBy });\n    };\n    const sampleORM = new ORM(user);\n    sampleORM.rpc = fakeRPC;\n    sampleORM.isSample = true;\n    sampleORM.cache = () => sampleORM;\n    sampleORM.setGroups = (groups) => sampleServer.setExistingGroups(groups);\n    return sampleORM;\n}\n", "import { onMounted, useComponent, useEffect, useExternalListener } from \"@odoo/owl\";\n\nexport const scrollSymbol = Symbol(\"scroll\");\n\nexport class CallbackRecorder {\n    constructor() {\n        this.setup();\n    }\n    setup() {\n        this._callbacks = [];\n    }\n    /**\n     * @returns {Function[]}\n     */\n    get callbacks() {\n        return this._callbacks.map(({ callback }) => callback);\n    }\n    /**\n     * @param {any} owner\n     * @param {Function} callback\n     */\n    add(owner, callback) {\n        if (!callback) {\n            throw new Error(\"Missing callback\");\n        }\n        this._callbacks.push({ owner, callback });\n    }\n    /**\n     * @param {any} owner\n     */\n    remove(owner) {\n        this._callbacks = this._callbacks.filter((s) => s.owner !== owner);\n    }\n}\n\n/**\n * @param {CallbackRecorder} callbackRecorder\n * @param {Function} callback\n */\nexport function useCallbackRecorder(callbackRecorder, callback) {\n    const component = useComponent();\n    useEffect(\n        () => {\n            callbackRecorder.add(component, callback);\n            return () => callbackRecorder.remove(component);\n        },\n        () => []\n    );\n}\n\n/**\n */\nexport function useSetupAction(params = {}) {\n    const component = useComponent();\n    const {\n        __beforeLeave__,\n        __getGlobalState__,\n        __getLocalState__,\n        __getContext__,\n        __getOrderBy__,\n    } = component.env;\n\n    const {\n        beforeVisibilityChange,\n        beforeUnload,\n        beforeLeave,\n        getGlobalState,\n        getLocalState,\n        rootRef,\n    } = params;\n\n    if (beforeVisibilityChange) {\n        useExternalListener(document, \"visibilitychange\", beforeVisibilityChange);\n    }\n\n    if (beforeUnload) {\n        useExternalListener(window, \"beforeunload\", beforeUnload);\n    }\n    if (__beforeLeave__ && beforeLeave) {\n        useCallbackRecorder(__beforeLeave__, beforeLeave);\n    }\n    if (__getGlobalState__ && (getGlobalState || rootRef)) {\n        useCallbackRecorder(__getGlobalState__, () => {\n            const state = {};\n            if (getGlobalState) {\n                Object.assign(state, getGlobalState());\n            }\n            return state;\n        });\n    }\n\n    function setScrollFromState() {\n        const { state } = component.props;\n        const scrolling = state && state[scrollSymbol];\n        if (scrolling) {\n            if (component.env.isSmall) {\n                rootRef.el.scrollTop = (scrolling.root && scrolling.root.top) || 0;\n                rootRef.el.scrollLeft = (scrolling.root && scrolling.root.left) || 0;\n            } else if (scrolling.content) {\n                const contentEl =\n                    rootRef.el.querySelector(\n                        \".o_component_with_search_panel > .o_renderer_with_searchpanel,\" +\n                            \".o_component_with_search_panel > .o_renderer\"\n                    ) || rootRef.el.querySelector(\".o_content\");\n                if (contentEl) {\n                    contentEl.scrollTop = scrolling.content.top || 0;\n                    contentEl.scrollLeft = scrolling.content.left || 0;\n                }\n            }\n        }\n    }\n    if (__getLocalState__ && (getLocalState || rootRef)) {\n        useCallbackRecorder(__getLocalState__, () => {\n            const state = {};\n            if (getLocalState) {\n                Object.assign(state, getLocalState());\n            }\n            if (rootRef) {\n                if (component.env.isSmall) {\n                    state[scrollSymbol] = {\n                        root: { left: rootRef.el.scrollLeft, top: rootRef.el.scrollTop },\n                    };\n                } else {\n                    const contentEl =\n                        rootRef.el.querySelector(\n                            \".o_component_with_search_panel > .o_renderer_with_searchpanel,\" +\n                                \".o_component_with_search_panel > .o_renderer\"\n                        ) || rootRef.el.querySelector(\".o_content\");\n                    if (contentEl) {\n                        state[scrollSymbol] = {\n                            content: { left: contentEl.scrollLeft, top: contentEl.scrollTop },\n                        };\n                    }\n                }\n            }\n            return state;\n        });\n\n        if (rootRef) {\n            onMounted(() => setScrollFromState());\n        }\n    }\n    if (__getContext__ && params.getContext) {\n        useCallbackRecorder(__getContext__, params.getContext);\n    }\n    if (__getOrderBy__ && params.getOrderBy) {\n        useCallbackRecorder(__getOrderBy__, params.getOrderBy);\n    }\n\n    return {\n        setScrollFromState,\n    };\n}\n", "import { browser } from \"@web/core/browser/browser\";\nimport { makeContext } from \"@web/core/context\";\nimport { session } from \"@web/session\";\nimport { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { useService } from \"@web/core/utils/hooks\";\n\nimport { Component, onWillStart, onWillUpdateProps, useState } from \"@odoo/owl\";\n\nexport const STATIC_ACTIONS_GROUP_NUMBER = 1;\nexport const ACTIONS_GROUP_NUMBER = 100;\n\n/**\n * Action menus (or Action/Print bar, previously called 'Sidebar')\n *\n * The side bar is the group of dropdown menus located on the left side of the\n * control panel. Its role is to display a list of items depending on the view\n * type and selected records and to execute a set of actions on active records.\n * It is made out of 2 dropdown: Print and Action.\n *\n * @extends Component\n */\nexport class ActionMenus extends Component {\n    static template = \"web.ActionMenus\";\n    static components = {\n        Dropdown,\n        DropdownItem,\n    };\n    static props = {\n        getActiveIds: Function,\n        context: Object,\n        resModel: String,\n        printDropdownTitle: { type: String, optional: true },\n        domain: { type: Array, optional: true },\n        isDomainSelected: { type: Boolean, optional: true },\n        items: {\n            type: Object,\n            shape: {\n                action: { type: Array, optional: true },\n                print: { type: Array, optional: true },\n            },\n        },\n        onActionExecuted: { type: Function, optional: true },\n        shouldExecuteAction: { type: Function, optional: true },\n        loadExtraPrintItems: { type: Function, optional: true },\n    };\n    static defaultProps = {\n        printDropdownTitle: _t(\"Print\"),\n        onActionExecuted: () => {},\n        shouldExecuteAction: () => true,\n        loadExtraPrintItems: () => [],\n    };\n\n    setup() {\n        this.orm = useService(\"orm\");\n        this.actionService = useService(\"action\");\n        this.state = useState({ printItems: []})\n        onWillStart(async () => {\n            this.actionItems = await this.getActionItems(this.props);\n        });\n        onWillUpdateProps(async (nextProps) => {\n            this.actionItems = await this.getActionItems(nextProps);\n        });\n    }\n\n    //---------------------------------------------------------------------\n    // Private\n    //---------------------------------------------------------------------\n\n    async getActionItems(props) {\n        return (props.items.action || []).map((action) => {\n            if (action.callback) {\n                return Object.assign(\n                    { key: `action-${action.description}`, groupNumber: ACTIONS_GROUP_NUMBER },\n                    action\n                );\n            } else {\n                return {\n                    action,\n                    description: action.name,\n                    key: action.id,\n                    groupNumber: action.groupNumber || ACTIONS_GROUP_NUMBER,\n                };\n            }\n        });\n    }\n\n    //---------------------------------------------------------------------\n    // Handlers\n    //---------------------------------------------------------------------\n\n    async executeAction(action) {\n        let activeIds = this.props.getActiveIds();\n        if (this.props.isDomainSelected) {\n            activeIds = await this.orm.search(this.props.resModel, this.props.domain, {\n                limit: session.active_ids_limit,\n                context: this.props.context,\n            });\n        }\n        const activeIdsContext = {\n            active_id: activeIds[0],\n            active_ids: activeIds,\n            active_model: this.props.resModel,\n        };\n        if (this.props.domain) {\n            // keep active_domain in context for backward compatibility\n            // reasons, and to allow actions to bypass the active_ids_limit\n            activeIdsContext.active_domain = this.props.domain;\n        }\n        const context = makeContext([this.props.context, activeIdsContext]);\n        return this.actionService.doAction(action.id, {\n            additionalContext: context,\n            onClose: this.props.onActionExecuted,\n        });\n    }\n\n    /**\n     * Handler used to determine which way must be used to execute a selected\n     * action: it will be either:\n     * - a callback (function given by the view controller);\n     * - an action ID (string);\n     * - an URL (string).\n     * @private\n     * @param {Object} item\n     */\n    async onItemSelected(item) {\n        if (!(await this.props.shouldExecuteAction(item))) {\n            return;\n        }\n        if (item.callback) {\n            item.callback([item]);\n        } else if (item.action) {\n            this.executeAction(item.action);\n        } else if (item.url) {\n            // Event has been prevented at its source: we need to redirect manually.\n            browser.location = item.url;\n        }\n    }\n\n    async loadAvailablePrintItems() {\n        const printActions = this.props.items.print || [];\n        const actionWithDomainIds = [];\n        const validActionIds = [];\n        for (const action of printActions) {\n            \"domain\" in action\n                ? actionWithDomainIds.push(action.id)\n                : validActionIds.push(action.id);\n        }\n        if (actionWithDomainIds.length) {\n            const validActionsWithDomainIds = await this.orm.call(\n                \"ir.actions.report\",\n                \"get_valid_action_reports\",\n                [actionWithDomainIds, this.props.resModel, this.props.getActiveIds()]\n            );\n            validActionIds.push(...validActionsWithDomainIds);\n        }\n        return printActions\n            .filter((action) => validActionIds.includes(action.id))\n            .map((action) => ({\n                action,\n                class: \"o_menu_item\",\n                description: action.name,\n                key: action.id,\n            }));\n    }\n\n    async loadPrintItems() {\n        if (!this.props.items.print?.length) {\n            return;\n        }\n        const [items, extraItems] = await Promise.all([\n            this.loadAvailablePrintItems(),\n            this.props.loadExtraPrintItems(),\n        ]);\n        const allItems = [...extraItems, ...items];\n        if (!allItems.length) {\n            allItems.push({\n                description: _t(\"No report available.\"),\n                class: \"o_menu_item disabled\",\n                key: \"nothing_to_display\",\n            });\n        }\n        this.state.printItems = allItems;\n    }\n}\n", "import { Component } 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\";\n\nexport class Breadcrumbs extends Component {\n    static template = \"web.Breadcrumbs\";\n    static components = { Dropdown, DropdownItem };\n    static props = {\n        breadcrumbs: Array,\n        slots: { type: Object, optional: true },\n    };\n\n    getBreadcrumbTooltip({ isFormView, name }) {\n        if (isFormView) {\n            return _t(\"Back to \u201c%s\u201d form\", name);\n        }\n        return _t(\"Back to \u201c%s\u201d\", name);\n    }\n}\n", "import { registry } from \"@web/core/registry\";\nimport { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { ActionMenus } from \"@web/search/action_menus/action_menus\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { onWillStart, onWillUpdateProps } from \"@odoo/owl\";\n\nconst cogMenuRegistry = registry.category(\"cogMenu\");\n\n/**\n * Combined Action menus (or Action/Print bar, previously called 'Sidebar')\n *\n * This is a variation of the ActionMenus, combined into a single DropDown.\n *\n * The side bar is the group of dropdown menus located on the left side of the\n * control panel. Its role is to display a list of items depending on the view\n * type and selected records and to execute a set of actions on active records.\n * It is made out of 2 dropdown: Print and Action.\n *\n * @extends ActionMenus\n */\nexport class CogMenu extends ActionMenus {\n    static template = \"web.CogMenu\";\n    static components = {\n        ...ActionMenus.components,\n        Dropdown,\n    };\n    static props = {\n        ...ActionMenus.props,\n        getActiveIds: { type: ActionMenus.props.getActiveIds, optional: true },\n        context: { type: ActionMenus.props.context, optional: true },\n        resModel: { type: ActionMenus.props.resModel, optional: true },\n        items: { ...ActionMenus.props.items, optional: true },\n        slots: { type: Object, optional: true },\n    };\n    static defaultProps = {\n        ...ActionMenus.defaultProps,\n        items: {},\n    };\n\n    setup() {\n        super.setup();\n        onWillStart(async () => {\n            this.registryItems = await this._registryItems();\n        });\n        onWillUpdateProps(async () => {\n            this.registryItems = await this._registryItems();\n        });\n    }\n\n    get hasItems() {\n        return this.cogItems.length || this.props.items.print?.length;\n    }\n\n    async _registryItems() {\n        const items = [];\n        for (const item of cogMenuRegistry.getAll()) {\n            if (\"isDisplayed\" in item ? await item.isDisplayed(this.env) : true) {\n                items.push({\n                    Component: item.Component,\n                    groupNumber: item.groupNumber,\n                    key: item.Component.name,\n                });\n            }\n        }\n        return items;\n    }\n\n    get cogItems() {\n        return [...this.registryItems, ...this.actionItems].sort(\n            (item1, item2) => (item1.groupNumber || 0) - (item2.groupNumber || 0)\n        );\n    }\n\n    getPrintItemAriaLabel(item) {\n        return _t(\"Print report: %s\", item.description);\n    }\n}\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { browser } from \"@web/core/browser/browser\";\nimport { getActiveHotkey } from \"@web/core/hotkeys/hotkey_service\";\nimport { Pager } from \"@web/core/pager/pager\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { useCommand } from \"@web/core/commands/command_hook\";\nimport { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\nimport { useHotkey } from \"@web/core/hotkeys/hotkey_hook\";\nimport { useSortable } from \"@web/core/utils/sortable_owl\";\nimport { user } from \"@web/core/user\";\nimport { AccordionItem } from \"@web/core/dropdown/accordion_item\";\nimport { CheckBox } from \"@web/core/checkbox/checkbox\";\nimport { makeContext } from \"@web/core/context\";\nimport { ConfirmationDialog } from \"@web/core/confirmation_dialog/confirmation_dialog\";\nimport { Transition } from \"@web/core/transition\";\nimport { Breadcrumbs } from \"../breadcrumbs/breadcrumbs\";\nimport { SearchBar } from \"../search_bar/search_bar\";\n\nimport { Component, useState, onMounted, useRef, useEffect } from \"@odoo/owl\";\n\nconst STICKY_CLASS = \"o_mobile_sticky\";\n\n/**\n * @typedef EmbeddedAction\n * @property {number} id\n * @property {[number, string]} parent_action_id\n * @property {string} name\n * @property {number} sequence\n * @property {number} parent_res_id\n * @property {string} parent_res_model\n * @property {[number, string]} action_id\n * @property {string} python_method\n * @property {number} user_id\n * @property {boolean} is_deletable\n * @property {string} default_view_mode\n * @property {string} filter_ids\n * @property {string} domain\n * @property {string} context\n */\n\nclass EmbeddedActionsConfigHandler {\n    constructor(parentActionId, currentActiveId, parentResModel, ormService) {\n        this.parentActionId = parentActionId;\n        this.currentActiveId = currentActiveId;\n        this.parentResModel = parentResModel;\n        this.embeddedActionsKey = `${this.parentActionId}+${this.currentActiveId || \"\"}`;\n        this.embeddedActionsConfig = user.settings.embedded_actions_config_ids || {};\n        this.orm = ormService;\n    }\n\n    setEmbeddedActionsConfig(config) {\n        if (this.embeddedActionsKey in this.embeddedActionsConfig) {\n            Object.assign(this.embeddedActionsConfig[this.embeddedActionsKey], config);\n        } else {\n            this.embeddedActionsConfig[this.embeddedActionsKey] = config;\n        }\n        this.orm.call(\"res.users.settings\", \"set_embedded_actions_setting\", [\n            user.settings.id,\n            this.parentActionId,\n            this.currentActiveId,\n            config,\n        ]);\n    }\n\n    getEmbeddedActionsConfig(key) {\n        return this.embeddedActionsConfig[this.embeddedActionsKey]?.[key];\n    }\n\n    hasEmbeddedActionsConfig() {\n        return this.embeddedActionsKey in this.embeddedActionsConfig;\n    }\n\n    async fetchEmbeddedActionsConfig() {\n        return await this.orm.call(\n            \"res.users.settings\",\n            \"get_embedded_actions_settings\",\n            [user.settings.id],\n            { context: { res_model: this.parentResModel, res_id: this.currentActiveId } }\n        );\n    }\n\n    updateEmbeddedActionsConfig(newSettings) {\n        for (const key in newSettings) {\n            this.embeddedActionsConfig[key] = newSettings[key];\n        }\n    }\n}\n\nexport class ControlPanel extends Component {\n    static template = \"web.ControlPanel\";\n    static components = {\n        Pager,\n        SearchBar,\n        Dropdown,\n        DropdownItem,\n        Breadcrumbs,\n        AccordionItem,\n        CheckBox,\n        Transition,\n    };\n    static props = {\n        display: { type: Object, optional: true },\n        slots: { type: Object, optional: true },\n    };\n\n    setup() {\n        this.actionService = useService(\"action\");\n        this.pagerProps = this.env.config.pagerProps\n            ? useState(this.env.config.pagerProps)\n            : undefined;\n        this.notificationService = useService(\"notification\");\n        this.breadcrumbs = useState(this.env.config.breadcrumbs);\n        this.orm = useService(\"orm\");\n        this.dialogService = useService(\"dialog\");\n\n        this.root = useRef(\"root\");\n        this.newActionNameRef = useRef(\"newActionNameRef\");\n        this.defaultEmbeddedActions = this.env.config.embeddedActions;\n        if (this.env.config.embeddedActions?.length > 0 && !this.env.config.parentActionId) {\n            const { parent_res_model, parent_action_id } = this.env.config.embeddedActions[0];\n            this.defaultEmbeddedActions = [\n                {\n                    id: false,\n                    name: this.env.config?.actionName,\n                    parent_action_id,\n                    parent_res_model,\n                    action_id: parent_action_id,\n                    user_id: false,\n                    context: {},\n                },\n                ...this.env.config.embeddedActions,\n            ];\n            this.env.config.setEmbeddedActions(this.defaultEmbeddedActions);\n        }\n\n        const parentActionId =\n            this.env.config.parentActionId ||\n            this.env.config.embeddedActions?.[0]?.parent_action_id[0] ||\n            this.env.config.embeddedActions?.[0]?.parent_action_id ||\n            \"\";\n        const currentActiveId = this.env.searchModel?.globalContext.active_id || false;\n        this.embeddedActionsConfigHandler = new EmbeddedActionsConfigHandler(\n            parentActionId,\n            currentActiveId,\n            this.currentEmbeddedAction?.parent_res_model,\n            this.orm\n        );\n\n        this.state = useState({\n            embeddedInfos: {\n                showEmbedded:\n                    !!this.embeddedActionsConfigHandler.getEmbeddedActionsConfig(\n                        \"embedded_visibility\"\n                    ),\n                embeddedActions: this.defaultEmbeddedActions || [],\n                newActionIsShared: false,\n                newActionName: this.newActionNameGetter,\n                visibleEmbeddedActions:\n                    this.embeddedActionsConfigHandler.getEmbeddedActionsConfig(\n                        \"embedded_actions_visibility\"\n                    ) || [],\n                currentEmbeddedAction: this.currentEmbeddedAction,\n            },\n        });\n\n        this.onScrollThrottledBound = this.onScrollThrottled.bind(this);\n\n        const { viewSwitcherEntries, viewType } = this.env.config;\n        for (const view of viewSwitcherEntries || []) {\n            useCommand(_t(\"Show %s view\", view.name), () => this.switchView(view.type), {\n                category: \"view_switcher\",\n                isAvailable: () => view.type !== viewType,\n            });\n        }\n\n        if (viewSwitcherEntries?.length > 1) {\n            useHotkey(\n                \"alt+shift+v\",\n                () => {\n                    this.cycleThroughViews();\n                },\n                {\n                    bypassEditableProtection: true,\n                    withOverlay: () => this.root.el.querySelector(\"nav.o_cp_switch_buttons\"),\n                }\n            );\n        }\n\n        useEffect(() => {\n            if (\n                !this.env.isSmall ||\n                (\"adaptToScroll\" in this.display && !this.display.adaptToScroll)\n            ) {\n                return;\n            }\n            const scrollingEl = this.getScrollingElement();\n            this.scrollingElementResizeObserver.observe(scrollingEl);\n            scrollingEl.addEventListener(\"scroll\", this.onScrollThrottledBound);\n            this.root.el.style.top = \"0px\";\n            this.scrollingElementHeight = scrollingEl.scrollHeight;\n            return () => {\n                this.scrollingElementResizeObserver.unobserve(scrollingEl);\n                scrollingEl.removeEventListener(\"scroll\", this.onScrollThrottledBound);\n            };\n        });\n\n        // The goal is to automatically open the dropdown menu of embedded actions if there is only one visible embedded action\n        // We use a timer to delay the display of that dropdown menu to avoid flicker issues\n        useEffect(\n            (el, showEmbedded) => {\n                const timer = setTimeout(() => {\n                    if (\n                        showEmbedded &&\n                        this.state.embeddedInfos.visibleEmbeddedActions.length === 1\n                    ) {\n                        el.querySelector(\".btn[name='openEmbeddedActions']\")?.click();\n                    }\n                }, 100);\n                return () => clearTimeout(timer);\n            },\n            () => [this.root.el, this.state.embeddedInfos.showEmbedded]\n        );\n\n        onMounted(() => {\n            if (this.state.embeddedInfos.embeddedActions?.length > 0) {\n                const embeddedOrder =\n                    this.embeddedActionsConfigHandler.getEmbeddedActionsConfig(\n                        \"embedded_actions_order\"\n                    );\n                if (embeddedOrder) {\n                    this._sortEmbeddedActions(embeddedOrder);\n                }\n            }\n            if (\n                !this.env.isSmall ||\n                (\"adaptToScroll\" in this.display && !this.display.adaptToScroll)\n            ) {\n                return;\n            }\n            this.oldScrollTop = 0;\n            this.lastScrollTop = 0;\n            this.initialScrollTop = this.getScrollingElement().scrollTop;\n        });\n\n        useSortable({\n            enable: true,\n            ref: this.root,\n            elements: \".o_draggable\",\n            cursor: \"move\",\n            delay: 200,\n            tolerance: 10,\n            onWillStartDrag: (params) => this._sortEmbeddedActionStart(params),\n            onDrop: (params) => this._sortEmbeddedActionDrop(params),\n        });\n    }\n\n    scrollingElementResizeObserver = new ResizeObserver((entries) => {\n        for (const entry of entries) {\n            if (this.scrollingElementHeight !== entry.target.scrollingElementHeight) {\n                this.oldScrollTop +=\n                    entry.target.scrollingElementHeight - this.scrollingElementHeight;\n                this.scrollingElementHeight = entry.target.scrollingElementHeight;\n            }\n        }\n    });\n\n    getDropdownClass(action) {\n        return (!this.env.isSmall && this._isEmbeddedActionVisible(action)) ||\n            (this.env.isSmall && this.state.embeddedInfos.currentEmbeddedAction?.id === action.id)\n            ? \"selected\"\n            : \"\";\n    }\n\n    getScrollingElement() {\n        return this.root.el.parentElement;\n    }\n\n    /**\n     * @returns {EmbeddedAction}\n     */\n    get currentEmbeddedAction() {\n        if (!this.env.config) {\n            return {};\n        }\n        const { currentEmbeddedActionId } = this.env.config;\n        return (\n            this.defaultEmbeddedActions?.find(({ id }) => id === currentEmbeddedActionId) ||\n            this.defaultEmbeddedActions?.[0]\n        );\n    }\n\n    get newActionNameGetter() {\n        if (this.currentEmbeddedAction?.name) {\n            return _t(\"Custom %s\", this.currentEmbeddedAction.name);\n        } else {\n            return _t(\"Custom Embedded Action\");\n        }\n    }\n\n    /**\n     * @returns {Object}\n     */\n    get display() {\n        return {\n            layoutActions: true,\n            ...this.props.display,\n        };\n    }\n\n    async onClickShowEmbedded() {\n        if (\n            !this.state.embeddedInfos.showEmbedded &&\n            !this.embeddedActionsConfigHandler.hasEmbeddedActionsConfig()\n        ) {\n            // If there are embedded actions and no config has been found in the settings, we will fetch it from DB\n            // We need to fetch because it's possible that the config from DB was changed while it wasn't in the browser user settings\n            // We then need to keep the browser user settings up to date with the DB\n            const embeddedSettings =\n                await this.embeddedActionsConfigHandler.fetchEmbeddedActionsConfig();\n            if (this.embeddedActionsConfigHandler.embeddedActionsKey in embeddedSettings) {\n                this.embeddedActionsConfigHandler.updateEmbeddedActionsConfig(embeddedSettings);\n                this.state.embeddedInfos.visibleEmbeddedActions =\n                    this.embeddedActionsConfigHandler.getEmbeddedActionsConfig(\n                        \"embedded_actions_visibility\"\n                    ) || [];\n                const embeddedOrder =\n                    this.embeddedActionsConfigHandler.getEmbeddedActionsConfig(\n                        \"embedded_actions_order\"\n                    );\n                if (embeddedOrder) {\n                    this._sortEmbeddedActions(embeddedOrder);\n                }\n                this.embeddedActionsConfigHandler.setEmbeddedActionsConfig({\n                    embedded_visibility: true,\n                });\n            } else {\n                // Store a new embedded actions config if still not found in the settings\n                const config = {\n                    res_model: this.state.embeddedInfos.currentEmbeddedAction.parent_res_model,\n                    embedded_actions_visibility: [],\n                    embedded_visibility: true,\n                    embedded_actions_order: [],\n                };\n                // If there is no visible embedded actions, the current action (if it exists) is put by default\n                if (this.state.embeddedInfos.embeddedActions?.length > 0) {\n                    const embeddedActionKey =\n                        this.state.embeddedInfos.currentEmbeddedAction?.id || false;\n                    if (\n                        !this.state.embeddedInfos.visibleEmbeddedActions.includes(embeddedActionKey)\n                    ) {\n                        this.state.embeddedInfos.visibleEmbeddedActions.push(embeddedActionKey);\n                        config.embedded_actions_visibility =\n                            this.state.embeddedInfos.visibleEmbeddedActions;\n                    }\n                }\n                this.embeddedActionsConfigHandler.setEmbeddedActionsConfig(config);\n            }\n        } else {\n            this.embeddedActionsConfigHandler.setEmbeddedActionsConfig({\n                embedded_visibility: !this.state.embeddedInfos.showEmbedded,\n            });\n        }\n        this.state.embeddedInfos.showEmbedded = !this.state.embeddedInfos.showEmbedded;\n    }\n\n    /**\n     * Show or hide the control panel on the top screen.\n     * The function is throttled to avoid refreshing the scroll position more\n     * often than necessary.\n     */\n    onScrollThrottled() {\n        if (this.isScrolling) {\n            return;\n        }\n        this.isScrolling = true;\n        browser.requestAnimationFrame(() => (this.isScrolling = false));\n\n        const scrollTop = this.getScrollingElement().scrollTop;\n        const delta = Math.round(scrollTop - this.oldScrollTop);\n\n        if (scrollTop > this.initialScrollTop) {\n            // Beneath initial position => sticky display\n            this.root.el.classList.add(STICKY_CLASS);\n            if (delta <= 0) {\n                // Going up | not moving\n                this.lastScrollTop = Math.min(0, this.lastScrollTop - delta);\n            } else {\n                // Going down\n                this.lastScrollTop = Math.max(\n                    -this.root.el.offsetHeight,\n                    -this.root.el.offsetTop - delta\n                );\n            }\n            this.root.el.style.top = `${this.lastScrollTop}px`;\n        } else {\n            // Above initial position => standard display\n            this.root.el.classList.remove(STICKY_CLASS);\n            this.lastScrollTop = 0;\n        }\n\n        this.oldScrollTop = scrollTop;\n    }\n\n    /**\n     * Allow to switch from the current view to another.\n     * Called when a view is clicked in the view switcher\n     * and reset mobile search state on switch view.\n     *\n     * @param {import(\"@web/views/view\").ViewType} viewType\n     */\n    switchView(viewType, newWindow) {\n        this.actionService.switchView(viewType, {}, { newWindow });\n    }\n\n    cycleThroughViews() {\n        const currentViewType = this.env.config.viewType;\n        const viewSwitcherEntries = this.env.config.viewSwitcherEntries;\n        const currentIndex = viewSwitcherEntries.findIndex(\n            (entry) => entry.type === currentViewType\n        );\n        const nextIndex = (currentIndex + 1) % viewSwitcherEntries.length;\n        this.switchView(viewSwitcherEntries[nextIndex].type);\n    }\n\n    /**\n     * @param {KeyboardEvent} ev\n     */\n    onMainButtonsKeydown(ev) {\n        const hotkey = getActiveHotkey(ev);\n        if (hotkey === \"arrowdown\") {\n            this.env.searchModel.trigger(\"focus-view\");\n            ev.preventDefault();\n            ev.stopPropagation();\n        }\n    }\n\n    /**\n     * @param {EmbeddedAction} action\n     */\n    _isEmbeddedActionVisible(action) {\n        return this.state.embeddedInfos.visibleEmbeddedActions.includes(action.id);\n    }\n\n    /**\n     * The selected action is put into (or removed from) the user settings and its visibility changes.\n     * The state variable visibleEmbeddedActions keeps track of the visible actions to avoid  having to parse\n     * the user settings values every time we want to access them.\n     * @param {EmbeddedAction} action\n     */\n    _setVisibility(actionId) {\n        if (this.state.embeddedInfos.visibleEmbeddedActions.includes(actionId)) {\n            const embeddedActionIndex =\n                this.state.embeddedInfos.visibleEmbeddedActions.indexOf(actionId);\n            if (embeddedActionIndex !== -1) {\n                this.state.embeddedInfos.visibleEmbeddedActions.splice(embeddedActionIndex, 1);\n            }\n        } else {\n            this.state.embeddedInfos.visibleEmbeddedActions.push(actionId);\n        }\n        this.embeddedActionsConfigHandler.setEmbeddedActionsConfig({\n            embedded_actions_visibility: this.state.embeddedInfos.visibleEmbeddedActions,\n        });\n    }\n\n    _onShareCheckboxChange() {\n        this.state.embeddedInfos.newActionIsShared = !this.state.embeddedInfos.newActionIsShared;\n    }\n\n    /**\n     * @param {Event} ev\n     */\n    async _saveNewAction(ev) {\n        const {\n            newActionName,\n            newActionIsShared,\n            embeddedActions,\n            currentEmbeddedAction,\n            visibleEmbeddedActions,\n        } = this.state.embeddedInfos;\n        if (!newActionName) {\n            this.notificationService.add(_t(\"A name for your new action is required.\"), {\n                type: \"danger\",\n            });\n            ev.stopPropagation();\n            return this.newActionNameRef.el.focus();\n        }\n        const duplicateName = embeddedActions.some(({ name }) => name === newActionName);\n        if (duplicateName) {\n            this.notificationService.add(_t(\"An action with the same name already exists.\"), {\n                type: \"danger\",\n            });\n            ev.stopPropagation();\n            return this.newActionNameRef.el.focus();\n        }\n        const userId = newActionIsShared ? false : user.userId;\n\n        const {\n            parent_action_id,\n            action_id,\n            parent_res_model,\n            python_method,\n            domain,\n            context,\n            groups_ids,\n        } = currentEmbeddedAction;\n        const values = {\n            parent_action_id: parent_action_id[0],\n            parent_res_model,\n            parent_res_id: this.env.searchModel.globalContext.active_id,\n            user_id: userId,\n            is_deletable: true,\n            default_view_mode: this.env.config.viewType,\n            domain,\n            context,\n            groups_ids,\n            name: newActionName,\n        };\n        if (python_method) {\n            values.python_method = python_method;\n        } else {\n            values.action_id = action_id[0] || this.env.config.actionId;\n        }\n        const embeddedActionId = await this.orm.create(\"ir.embedded.actions\", [values]);\n        const description = `${newActionName}`;\n        this.env.searchModel.createNewFavorite({\n            description,\n            isDefault: true,\n            isShared: newActionIsShared,\n            embeddedActionId: embeddedActionId[0],\n        });\n        Object.assign(this.state.embeddedInfos, {\n            newActionName: \"\",\n            newActionIsShared: false,\n        });\n        const enrichedNewEmbeddedAction = {\n            ...values,\n            parent_action_id,\n            action_id,\n            id: embeddedActionId[0],\n        };\n        this.state.embeddedInfos.embeddedActions.push(enrichedNewEmbeddedAction);\n        const embeddedActionResId = embeddedActionId[0];\n        visibleEmbeddedActions.push(embeddedActionResId);\n        const order = this.state.embeddedInfos.embeddedActions.map((el) => el.id);\n        this.embeddedActionsConfigHandler.setEmbeddedActionsConfig({\n            embedded_actions_visibility: visibleEmbeddedActions,\n            embedded_actions_order: order,\n        });\n        this.env.config.setCurrentEmbeddedAction(embeddedActionId);\n        this.state.embeddedInfos.currentEmbeddedAction = enrichedNewEmbeddedAction;\n        this.state.embeddedInfos.newActionName = `${newActionName} Custom`;\n    }\n\n    /**\n     * @param {EmbeddedAction} action\n     */\n    openConfirmationDialog(action) {\n        const dialogProps = {\n            title: _t(\"Warning\"),\n            body: action.user_id\n                ? _t(\"Are you sure that you want to remove this embedded action?\")\n                : _t(\"This embedded action is global and will be removed for everyone.\"),\n            confirmLabel: _t(\"Delete\"),\n            confirm: async () => await this._deleteEmbeddedAction(action),\n            cancel: () => {},\n        };\n        this.dialogService.add(ConfirmationDialog, dialogProps);\n    }\n\n    /**\n     * @param {EmbeddedAction} action\n     */\n    async _deleteEmbeddedAction(action) {\n        const { visibleEmbeddedActions, embeddedActions, currentEmbeddedAction } =\n            this.state.embeddedInfos;\n        const embeddedActionIndex = visibleEmbeddedActions.indexOf(action.id);\n        if (embeddedActionIndex !== -1) {\n            visibleEmbeddedActions.splice(embeddedActionIndex, 1);\n        }\n        this.state.embeddedInfos.embeddedActions = embeddedActions.filter(\n            ({ id }) => id !== action.id\n        );\n        const order = this.state.embeddedInfos.embeddedActions.map((el) => el.id);\n        this.embeddedActionsConfigHandler.setEmbeddedActionsConfig({\n            embedded_actions_visibility: visibleEmbeddedActions,\n            embedded_actions_order: order,\n        });\n        await this.orm.unlink(\"ir.embedded.actions\", [action.id]);\n        if (action.id === currentEmbeddedAction?.id) {\n            const { active_id, active_model } = this.env.searchModel.globalContext;\n            const actionContext = action.context ? makeContext([action.context]) : {};\n            const additionalContext = {\n                ...actionContext,\n                active_id,\n                active_model,\n            };\n            this.actionService.doAction(action.parent_action_id[0] || action.parent_action_id, {\n                additionalContext,\n                stackPosition: \"replaceCurrentAction\",\n            });\n        }\n    }\n\n    /**\n     * @param {EmbeddedAction} action\n     */\n    async onEmbeddedActionClick(action) {\n        this.env.config.setEmbeddedActions(this.state.embeddedInfos.embeddedActions);\n        const { active_id, active_model } = this.env.searchModel.globalContext;\n        const actionContext = action.context ? makeContext([action.context]) : {};\n        const context = {\n            ...actionContext,\n            active_id,\n            active_model,\n            current_embedded_action_id: action.id,\n            parent_action_embedded_actions: this.state.embeddedInfos.embeddedActions,\n            parent_action_id: action.parent_action_id[0] || action.parent_action_id,\n        };\n        this.actionService.doActionButton(\n            {\n                type: action.python_method ? \"object\" : \"action\",\n                resId: this.env.searchModel?.globalContext.active_id,\n                name: action.python_method || action.action_id[0] || action.action_id,\n                resModel: action.parent_res_model,\n                context,\n                stackPosition: \"replaceCurrentAction\",\n                viewType: action.default_view_mode,\n            },\n            { isEmbeddedAction: true }\n        );\n    }\n\n    /**\n     * @param {number[]} order\n     */\n    _sortEmbeddedActions(order) {\n        this.state.embeddedInfos.embeddedActions = this.state.embeddedInfos.embeddedActions.sort(\n            (a, b) => {\n                const indexA = order.indexOf(a.id);\n                if (!indexA) {\n                    return -1;\n                }\n                const indexB = order.indexOf(b.id);\n                if (!indexB) {\n                    return 1;\n                }\n                return indexA - indexB;\n            }\n        );\n    }\n\n    /**\n     * @param {Object} params\n     * @param {HTMLElement} params.element\n     */\n    _sortEmbeddedActionStart({ element, addClass }) {\n        addClass(element, \"o_dragged_embedded_action\");\n    }\n\n    /**\n     * @param {Object} params\n     * @param {HTMLElement} params.element\n     * @param {HTMLElement} params.previous\n     */\n    _sortEmbeddedActionDrop({ element, previous }) {\n        const order = this.state.embeddedInfos.embeddedActions.map((el) => el.id);\n        const elementId = Number(element.dataset.id) || false;\n        const elementIndex = order.indexOf(elementId);\n        order.splice(elementIndex, 1);\n        if (previous) {\n            const prevIndex = order.indexOf(Number(previous.dataset.id) || false);\n            order.splice(prevIndex + 1, 0, elementId);\n        } else {\n            order.splice(0, 0, elementId);\n        }\n        this._sortEmbeddedActions(order);\n        this.embeddedActionsConfigHandler.setEmbeddedActionsConfig({\n            embedded_actions_order: order,\n        });\n    }\n\n    dropdownifyButtons() {\n        const adaptiveMenu = document.querySelector(\n            \".o-control-panel-adaptive-dropdown.dropdown-menu\"\n        );\n        const meaningfulElements = this.getBoxedElements(adaptiveMenu.children);\n        for (const el of meaningfulElements) {\n            el.classList.add(\"dropdown-item\");\n            el.classList.remove(\"btn\");\n        }\n    }\n\n    getBoxedElements(elements) {\n        const boxed = [];\n        for (const el of [...elements]) {\n            const elStyles = el.ownerDocument.defaultView.getComputedStyle(el);\n            if (elStyles.getPropertyValue(\"display\") === \"contents\") {\n                boxed.push(...this.getBoxedElements(el.children));\n            } else if (elStyles.getPropertyValue(\"display\") === \"none\") {\n                continue;\n            } else {\n                boxed.push(el);\n            }\n        }\n        return boxed;\n    }\n}\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { AccordionItem } from \"@web/core/dropdown/accordion_item\";\nimport { CheckBox } from \"@web/core/checkbox/checkbox\";\nimport { registry } from \"@web/core/registry\";\nimport { useService } from \"@web/core/utils/hooks\";\n\nimport { Component, useRef, useState } from \"@odoo/owl\";\n\nconst favoriteMenuRegistry = registry.category(\"favoriteMenu\");\n\nexport class CustomFavoriteItem extends Component {\n    static template = \"web.CustomFavoriteItem\";\n    static components = { CheckBox, AccordionItem };\n    static props = {};\n\n    setup() {\n        this.actionService = useService(\"action\");\n        this.notificationService = useService(\"notification\");\n        this.descriptionRef = useRef(\"description\");\n        this.state = useState({\n            description: this.env.config.getDisplayName(),\n            isDefault: false,\n        });\n    }\n\n    /**\n     * @param {Event} ev\n     */\n    async saveFavorite(ev, isShared = false) {\n        if (!this.state.description) {\n            this.notificationService.add(_t(\"A name for your favorite filter is required.\"), {\n                type: \"danger\",\n            });\n            ev.stopPropagation();\n            this.descriptionRef.el.focus();\n            return false;\n        }\n        const { description, isDefault } = this.state;\n        const embeddedActionId = this.env.config.currentEmbeddedActionId || false;\n        const serverSideId = await this.env.searchModel.createNewFavorite({\n            description,\n            isDefault,\n            isShared,\n            embeddedActionId,\n        });\n\n        Object.assign(this.state, {\n            description: this.env.config.getDisplayName(),\n            isDefault: false,\n        });\n        return serverSideId;\n    }\n\n    /**\n     * @param {Event} ev\n     */\n    async editFavorite(ev) {\n        const serverSideId = await this.saveFavorite(ev);\n        if (!serverSideId) {\n            return;\n        }\n        this.actionService.doAction({\n            type: \"ir.actions.act_window\",\n            res_model: \"ir.filters\",\n            views: [[false, \"form\"]],\n            context: {\n                form_view_ref: \"base.ir_filters_view_edit_form\",\n            },\n            res_id: serverSideId,\n        });\n    }\n\n    /**\n     * @param {KeyboardEvent} ev\n     */\n    onInputKeydown(ev) {\n        switch (ev.key) {\n            case \"Enter\":\n                ev.preventDefault();\n                this.saveFavorite(ev);\n                break;\n            case \"Escape\":\n                // Gives the focus back to the component.\n                ev.preventDefault();\n                ev.target.blur();\n                break;\n        }\n    }\n}\n\nfavoriteMenuRegistry.add(\n    \"custom-favorite-item\",\n    { Component: CustomFavoriteItem, groupNumber: 3 },\n    { sequence: 0 }\n);\n", "import { Component } from \"@odoo/owl\";\n\nexport class CustomGroupByItem extends Component {\n    static template = \"web.CustomGroupByItem\";\n    static props = {\n        fields: Array,\n        onAddCustomGroup: Function,\n    };\n\n    get choices() {\n        return this.props.fields.map((f) => ({ label: f.string, value: f.name }));\n    }\n\n    onSelected(ev) {\n        if (ev.target.value) {\n            this.props.onAddCustomGroup(ev.target.value);\n            // reset the placeholder\n            ev.target.value = \"\";\n        }\n    }\n}\n", "import { Component, useRef } from \"@odoo/owl\";\nimport { ControlPanel } from \"@web/search/control_panel/control_panel\";\nimport { SearchPanel } from \"@web/search/search_panel/search_panel\";\n\n/**\n * @param {Object} params\n * @returns {Object}\n */\nexport function extractLayoutComponents(params) {\n    const layoutComponents = {\n        ControlPanel: params.ControlPanel || ControlPanel,\n        SearchPanel: params.SearchPanel || SearchPanel,\n    };\n    return layoutComponents;\n}\n\nexport class Layout extends Component {\n    static template = \"web.Layout\";\n    static props = {\n        className: { type: String, optional: true },\n        display: { type: Object, optional: true },\n        slots: { type: Object, optional: true },\n    };\n    static defaultProps = {\n        display: {},\n    };\n    setup() {\n        this.components = extractLayoutComponents(this.env.config);\n        this.contentRef = useRef(\"content\");\n    }\n    get controlPanelSlots() {\n        const slots = { ...this.props.slots };\n        if (this.env.inDialog) {\n            delete slots[\"layout-buttons\"];\n        }\n        delete slots.default;\n        return slots;\n    }\n}\n", "import { useEnv, useSubEnv, useState, onWillRender } from \"@odoo/owl\";\n\n/**\n * @typedef PagerUpdateParams\n * @property {number} offset\n * @property {number} limit\n */\n\n/**\n * @typedef PagerProps\n * @property {number} offset\n * @property {number} limit\n * @property {number} total\n * @property {(params: PagerUpdateParams) => any} onUpdate\n * @property {boolean} [isEditable]\n * @property {boolean} [withAccessKey]\n */\n\n/**\n * @param {() => PagerProps} getProps\n */\nexport function usePager(getProps) {\n    const env = useEnv();\n    const pagerState = useState({});\n\n    useSubEnv({\n        config: {\n            ...env.config,\n            pagerProps: pagerState,\n        },\n    });\n    onWillRender(() => {\n        Object.assign(pagerState, getProps() || { total: 0 });\n    });\n}\n", "import { AccordionItem, ACCORDION } from \"@web/core/dropdown/accordion_item\";\nimport { CheckboxItem } from \"@web/core/dropdown/checkbox_item\";\nimport { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\nimport { Component, useState, useChildSubEnv } from \"@odoo/owl\";\n\nexport class PropertiesGroupByItem extends Component {\n    static template = \"web.PropertiesGroupByItem\";\n    static components = { AccordionItem, CheckboxItem, DropdownItem };\n    static props = {\n        item: Object,\n        onGroup: Function,\n    };\n\n    setup() {\n        this.state = useState({ groupByItems: [] });\n        useChildSubEnv({\n            [ACCORDION]: {\n                accordionStateChanged: this.beforeOpen.bind(this),\n            },\n        });\n    }\n\n    /**\n     * The properties field is considered as active if one of its property is active.\n     */\n    get isActive() {\n        return this.state.groupByItems.some((item) => item.isActive);\n    }\n\n    /**\n     * True if all group items come from the same definition record.\n     */\n    get isSingleParent() {\n        const uniqueNames = new Set(this.state.groupByItems.map((item) => item.definitionRecordId));\n        return uniqueNames.size < 2;\n    }\n\n    /**\n     * Dynamically load the definition, only when needed (if we open the dropdown).\n     */\n    async beforeOpen() {\n        if (this.definitionLoaded) {\n            return;\n        }\n        this.definitionLoaded = true;\n\n        await this.env.searchModel.fillSearchViewItemsProperty();\n        this._updateGroupByItems();\n    }\n\n    /**\n     * Callback to group records per one property.\n     */\n    onGroup(ids) {\n        this.props.onGroup(ids);\n        this._updateGroupByItems(); // isActive state might have changed\n    }\n\n    /**\n     * Update the component state to sync it with the search model group item.\n     */\n    _updateGroupByItems() {\n        this.state.groupByItems = this.env.searchModel.getSearchItems(\n            (searchItem) =>\n                [\"groupBy\", \"dateGroupBy\"].includes(searchItem.type) &&\n                searchItem.isProperty &&\n                searchItem.propertyFieldName === this.props.item.fieldName\n        );\n    }\n}\n", "import { makeContext } from \"@web/core/context\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { evaluateBooleanExpr, evaluateExpr } from \"@web/core/py_js/py\";\nimport { clamp } from \"@web/core/utils/numbers\";\nimport { visitXML } from \"@web/core/utils/xml\";\nimport { DEFAULT_INTERVAL, toGeneratorId } from \"@web/search/utils/dates\";\n\nconst ALL = _t(\"All\");\nconst DEFAULT_LIMIT = 200;\nconst DEFAULT_VIEWS_WITH_SEARCH_PANEL = [\"kanban\", \"list\"];\n\n/**\n * Returns the split 'group_by' key from the given context attribute.\n * This helper accepts any invalid context or one that does not have\n * a valid 'group_by' key, and falls back to an empty list.\n * @param {string} context\n * @returns {string[]}\n */\nfunction getContextGroupBy(context) {\n    try {\n        return makeContext([context]).group_by?.split(\":\") || [];\n    } catch {\n        return [];\n    }\n}\n\nfunction reduceType(type) {\n    if (type === \"dateFilter\") {\n        return \"filter\";\n    }\n    if (type === \"dateGroupBy\") {\n        return \"groupBy\";\n    }\n    return type;\n}\n\nexport class SearchArchParser {\n    constructor(searchViewDescription, fields, searchDefaults = {}, searchPanelDefaults = {}) {\n        const { irFilters, arch } = searchViewDescription;\n\n        this.fields = fields || {};\n        this.irFilters = irFilters || [];\n        this.arch = arch || \"<search/>\";\n\n        this.labels = [];\n        this.preSearchItems = [];\n        this.searchPanelInfo = {\n            className: \"\",\n            viewTypes: DEFAULT_VIEWS_WITH_SEARCH_PANEL,\n        };\n        this.sections = [];\n\n        this.searchDefaults = searchDefaults;\n        this.searchPanelDefaults = searchPanelDefaults;\n\n        this.currentGroup = [];\n        this.currentTag = null;\n        this.groupNumber = 0;\n        this.pregroupOfGroupBys = [];\n\n        this.optionsParams = null;\n    }\n\n    parse() {\n        visitXML(this.arch, (node, visitChildren) => {\n            switch (node.tagName) {\n                case \"search\":\n                    this.visitSearch(node, visitChildren);\n                    break;\n                case \"searchpanel\":\n                    return this.visitSearchPanel(node);\n                case \"group\":\n                    this.visitGroup(node, visitChildren);\n                    break;\n                case \"separator\":\n                    this.visitSeparator();\n                    break;\n                case \"field\":\n                    this.visitField(node);\n                    break;\n                case \"filter\":\n                    if (this.optionsParams) {\n                        this.visitDateOption(node);\n                    } else {\n                        this.visitFilter(node, visitChildren);\n                    }\n                    break;\n            }\n        });\n\n        return {\n            labels: this.labels,\n            preSearchItems: this.preSearchItems,\n            searchPanelInfo: this.searchPanelInfo,\n            sections: this.sections,\n        };\n    }\n\n    pushGroup(tag = null) {\n        if (this.currentGroup.length) {\n            if (this.currentTag === \"groupBy\") {\n                this.pregroupOfGroupBys.push(...this.currentGroup);\n            } else {\n                this.preSearchItems.push(this.currentGroup);\n            }\n        }\n        this.currentTag = tag;\n        this.currentGroup = [];\n        this.groupNumber++;\n    }\n\n    visitField(node) {\n        this.pushGroup(\"field\");\n        const preField = { type: \"field\" };\n        if (node.hasAttribute(\"invisible\")) {\n            preField.invisible = node.getAttribute(\"invisible\");\n        }\n        if (node.hasAttribute(\"domain\")) {\n            preField.domain = node.getAttribute(\"domain\");\n        }\n        if (node.hasAttribute(\"filter_domain\")) {\n            preField.filterDomain = node.getAttribute(\"filter_domain\");\n        } else if (node.hasAttribute(\"operator\")) {\n            preField.operator = node.getAttribute(\"operator\");\n        }\n        if (node.hasAttribute(\"context\")) {\n            preField.context = node.getAttribute(\"context\");\n        }\n        if (node.hasAttribute(\"name\")) {\n            const name = node.getAttribute(\"name\");\n            if (!this.fields[name]) {\n                throw Error(`Unknown field ${name}`);\n            }\n            const fieldType = this.fields[name].type;\n            preField.fieldName = name;\n            preField.fieldType = fieldType;\n            if (fieldType !== \"properties\" && name in this.searchDefaults) {\n                preField.isDefault = true;\n                const val = this.searchDefaults[name];\n                const value = Array.isArray(val) ? val[0] : val;\n                let operator = preField.operator;\n                if (!operator) {\n                    let type = fieldType;\n                    if (node.hasAttribute(\"widget\")) {\n                        type = node.getAttribute(\"widget\");\n                    }\n                    // Note: many2one as a default filter will have a\n                    // numeric value instead of a string => we want \"=\"\n                    // instead of \"ilike\".\n                    if ([\"char\", \"html\", \"many2many\", \"one2many\", \"text\"].includes(type)) {\n                        operator = \"ilike\";\n                    } else {\n                        operator = \"=\";\n                    }\n                }\n                preField.defaultRank = -10;\n                const { selection, context, relation } = this.fields[name];\n                preField.defaultAutocompleteValue = { label: `${value}`, operator, value };\n                if (fieldType === \"selection\") {\n                    const option = selection.find((sel) => sel[0] === value);\n                    if (!option) {\n                        throw Error();\n                    }\n                    preField.defaultAutocompleteValue.label = option[1];\n                } else if (fieldType === \"many2one\") {\n                    this.labels.push((orm) =>\n                        orm\n                            .call(relation, \"read\", [value, [\"display_name\"]], { context })\n                            .then((results) => {\n                                preField.defaultAutocompleteValue.label =\n                                    results[0][\"display_name\"];\n                            })\n                    );\n                } else if (\n                    [\"many2many\", \"one2many\"].includes(fieldType) &&\n                    Array.isArray(val) &&\n                    val.every((v) => Number.isInteger(v) && v > 0)\n                ) {\n                    preField.defaultAutocompleteValue.operator = \"in\";\n                    preField.defaultAutocompleteValue.value = val;\n                    this.labels.push((orm) =>\n                        orm\n                            .call(relation, \"read\", [val, [\"display_name\"]], { context })\n                            .then((results) => {\n                                preField.defaultAutocompleteValue.label = `${results\n                                    .map((r) => r[\"display_name\"])\n                                    .join(\" or \")}`;\n                            })\n                    );\n                }\n            }\n        } else {\n            throw Error(); //but normally this should have caught earlier with view arch validation server side\n        }\n        if (node.hasAttribute(\"string\")) {\n            preField.description = node.getAttribute(\"string\");\n        } else if (preField.fieldName) {\n            preField.description = this.fields[preField.fieldName].string;\n        } else {\n            preField.description = \"\u03a9\";\n        }\n        this.currentGroup.push(preField);\n    }\n\n    visitFilter(node, visitChildren) {\n        const preSearchItem = { type: \"filter\" };\n        if (node.hasAttribute(\"context\")) {\n            const context = node.getAttribute(\"context\");\n            const [fieldName, defaultInterval] = getContextGroupBy(context);\n            const groupByField = this.fields[fieldName];\n            if (groupByField) {\n                preSearchItem.type = \"groupBy\";\n                preSearchItem.fieldName = fieldName;\n                preSearchItem.fieldType = groupByField.type;\n                if ([\"date\", \"datetime\"].includes(groupByField.type)) {\n                    preSearchItem.type = \"dateGroupBy\";\n                    preSearchItem.defaultIntervalId = defaultInterval || DEFAULT_INTERVAL;\n                }\n            } else {\n                preSearchItem.context = context;\n            }\n        }\n        if (reduceType(preSearchItem.type) !== this.currentTag) {\n            this.pushGroup(reduceType(preSearchItem.type));\n        }\n        if (preSearchItem.type === \"filter\") {\n            if (node.hasAttribute(\"date\")) {\n                const fieldName = node.getAttribute(\"date\");\n                preSearchItem.type = \"dateFilter\";\n                preSearchItem.fieldName = fieldName;\n                preSearchItem.fieldType = this.fields[fieldName].type;\n                const optionsParams = {\n                    startYear: Number(node.getAttribute(\"start_year\") || -2),\n                    endYear: Number(node.getAttribute(\"end_year\") || 0),\n                    startMonth: Number(node.getAttribute(\"start_month\") || -2),\n                    endMonth: Number(node.getAttribute(\"end_month\") || 0),\n                    customOptions: [],\n                };\n                const defaultOffset = clamp(optionsParams.startMonth, optionsParams.endMonth, 0);\n                preSearchItem.defaultGeneratorIds = [toGeneratorId(\"month\", defaultOffset)];\n                if (node.hasAttribute(\"default_period\")) {\n                    preSearchItem.defaultGeneratorIds = node\n                        .getAttribute(\"default_period\")\n                        .split(\",\");\n                }\n                this.optionsParams = optionsParams;\n                visitChildren();\n                preSearchItem.optionsParams = optionsParams;\n                this.optionsParams = null;\n            }\n            preSearchItem.domain = node.getAttribute(\"domain\") || \"[]\";\n        }\n        if (node.hasAttribute(\"invisible\")) {\n            preSearchItem.invisible = node.getAttribute(\"invisible\");\n            const fieldName = preSearchItem.fieldName;\n            if (fieldName && !this.fields[fieldName]) {\n                // In some case when a field is limited to specific groups\n                // on the model, we need to ensure to discard related filter\n                // as it may still be present in the view (in 'invisible' state)\n                return;\n            }\n        }\n        preSearchItem.groupNumber = this.groupNumber;\n        if (node.hasAttribute(\"name\")) {\n            const name = node.getAttribute(\"name\");\n            preSearchItem.name = name;\n            if (name in this.searchDefaults) {\n                preSearchItem.isDefault = true;\n                const value = this.searchDefaults[name];\n                if ([\"groupBy\", \"dateGroupBy\"].includes(preSearchItem.type)) {\n                    preSearchItem.defaultRank = typeof value === \"number\" ? value : 100;\n                } else {\n                    preSearchItem.defaultRank = -5;\n                }\n                if (\n                    preSearchItem.type === \"dateFilter\" &&\n                    typeof value === \"string\" &&\n                    !/^(true|1)$/i.test(value)\n                ) {\n                    preSearchItem.defaultGeneratorIds = value.split(\",\");\n                }\n            }\n        }\n        if (node.hasAttribute(\"string\")) {\n            preSearchItem.description = node.getAttribute(\"string\");\n        } else if (preSearchItem.fieldName) {\n            preSearchItem.description = this.fields[preSearchItem.fieldName].string;\n        } else if (node.hasAttribute(\"help\")) {\n            preSearchItem.description = node.getAttribute(\"help\");\n        } else if (node.hasAttribute(\"name\")) {\n            preSearchItem.description = node.getAttribute(\"name\");\n        } else {\n            preSearchItem.description = \"\u03a9\";\n        }\n        this.currentGroup.push(preSearchItem);\n    }\n\n    visitDateOption(node) {\n        const preDateOption = { type: \"dateOption\" };\n        for (const attribute of [\"name\", \"string\", \"domain\"]) {\n            if (!node.getAttribute(attribute)) {\n                throw new Error(`Attribute \"${attribute}\" is missing.`);\n            }\n        }\n        preDateOption.id = `custom_${node.getAttribute(\"name\")}`;\n        preDateOption.description = node.getAttribute(\"string\");\n        preDateOption.domain = node.getAttribute(\"domain\");\n        this.optionsParams.customOptions.push(preDateOption);\n    }\n\n    visitGroup(node, visitChildren) {\n        this.pushGroup();\n        visitChildren();\n        this.pushGroup();\n    }\n\n    visitSearch(node, visitChildren) {\n        visitChildren();\n        this.pushGroup();\n        if (this.pregroupOfGroupBys.length) {\n            this.preSearchItems.push(this.pregroupOfGroupBys);\n        }\n    }\n\n    visitSearchPanel(searchPanelNode) {\n        let hasCategoryWithCounters = false;\n        let hasFilterWithDomain = false;\n        let nextSectionId = 1;\n\n        if (searchPanelNode.hasAttribute(\"class\")) {\n            this.searchPanelInfo.className = searchPanelNode.getAttribute(\"class\");\n        }\n        if (searchPanelNode.hasAttribute(\"view_types\")) {\n            this.searchPanelInfo.viewTypes = searchPanelNode.getAttribute(\"view_types\").split(\",\");\n        }\n\n        for (const node of searchPanelNode.children) {\n            if (node.nodeType !== 1 || node.tagName !== \"field\") {\n                continue;\n            }\n            if (\n                node.getAttribute(\"invisible\") === \"True\" ||\n                node.getAttribute(\"invisible\") === \"1\"\n            ) {\n                continue;\n            }\n            const attrs = {};\n            for (const attrName of node.getAttributeNames()) {\n                attrs[attrName] = node.getAttribute(attrName);\n            }\n\n            const type = attrs.select === \"multi\" ? \"filter\" : \"category\";\n            const section = {\n                color: attrs.color || null,\n                description: attrs.string || this.fields[attrs.name].string,\n                enableCounters: evaluateBooleanExpr(attrs.enable_counters),\n                expand: evaluateBooleanExpr(attrs.expand),\n                fieldName: attrs.name,\n                icon: attrs.icon || null,\n                id: nextSectionId++,\n                limit: evaluateExpr(attrs.limit || String(DEFAULT_LIMIT)),\n                type,\n                values: new Map(),\n            };\n            if (type === \"category\") {\n                section.activeValueId = this.searchPanelDefaults[attrs.name];\n                section.icon = section.icon || \"fa-folder\";\n                section.hierarchize = evaluateBooleanExpr(attrs.hierarchize || \"1\");\n                section.depth = attrs.depth ? parseInt(attrs.depth) : 0;\n                section.values.set(false, {\n                    childrenIds: [],\n                    display_name: ALL.toString(),\n                    id: false,\n                    bold: true,\n                    parentId: false,\n                });\n                hasCategoryWithCounters = hasCategoryWithCounters || section.enableCounters;\n            } else {\n                section.domain = attrs.domain || \"[]\";\n                section.groupBy = attrs.groupby || null;\n                section.icon = section.icon || \"fa-filter\";\n                hasFilterWithDomain = hasFilterWithDomain || section.domain !== \"[]\";\n            }\n            this.sections.push([section.id, section]);\n        }\n\n        /**\n         * Category counters are automatically disabled if a filter domain is found\n         * to avoid inconsistencies with the counters. The underlying problem could\n         * actually be solved by reworking the search panel and the way the\n         * counters are computed, though this is not the current priority\n         * considering the time it would take, hence this quick \"fix\".\n         */\n        if (hasCategoryWithCounters && hasFilterWithDomain) {\n            // If incompatibilities are found -> disables all category counters\n            for (const section of this.sections) {\n                if (section.type === \"category\") {\n                    section.enableCounters = false;\n                }\n            }\n            // ... and triggers a warning\n            console.warn(\n                \"Warning: categories with counters are incompatible with filters having a domain attribute.\",\n                \"All category counters have been disabled to avoid inconsistencies.\"\n            );\n        }\n\n        return false; // we do not want to let the parser keep visiting children\n    }\n\n    visitSeparator() {\n        this.pushGroup();\n    }\n}\n", "import { Domain } from \"@web/core/domain\";\nimport { serializeDate, serializeDateTime } from \"@web/core/l10n/dates\";\nimport { registry } from \"@web/core/registry\";\nimport { KeepLast } from \"@web/core/utils/concurrency\";\nimport { useAutofocus, useBus, useChildRef, useService } from \"@web/core/utils/hooks\";\nimport { DomainSelectorDialog } from \"@web/core/domain_selector_dialog/domain_selector_dialog\";\nimport { fuzzyTest } from \"@web/core/utils/search\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { SearchBarMenu } from \"../search_bar_menu/search_bar_menu\";\nimport { Component, status, useRef, useState } from \"@odoo/owl\";\nimport { useDropdownState } from \"@web/core/dropdown/dropdown_hooks\";\nimport { hasTouch } from \"@web/core/browser/feature_detection\";\nimport { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\nimport { useNavigation } from \"@web/core/navigation/navigation\";\n\nconst parsers = registry.category(\"parsers\");\n\nconst parseValue = (value, fieldType) => {\n    const parser = parsers.contains(fieldType) ? parsers.get(fieldType) : (str) => str;\n    switch (fieldType) {\n        case \"date\": {\n            return serializeDate(parser(value));\n        }\n        case \"datetime\": {\n            return serializeDateTime(parser(value));\n        }\n        case \"many2one\": {\n            return value;\n        }\n        default: {\n            return parser(value);\n        }\n    }\n};\n\nconst CHAR_FIELDS = [\"char\", \"html\", \"many2many\", \"many2one\", \"one2many\", \"text\", \"properties\"];\nconst FOLDABLE_TYPES = [\"properties\", \"many2one\", \"many2many\"];\n\nlet nextItemId = 1;\nconst SUB_ITEMS_DEFAULT_LIMIT = 8;\n\nexport class SearchBar extends Component {\n    static template = \"web.SearchBar\";\n    static components = {\n        SearchBarMenu,\n        Dropdown,\n        DropdownItem,\n    };\n    static props = {\n        autofocus: { type: Boolean, optional: true },\n        slots: {\n            type: Object,\n            optional: true,\n            shape: {\n                default: { optional: true },\n                \"search-bar-additional-menu\": { optional: true },\n            },\n        },\n        toggler: {\n            type: Object,\n            optional: true,\n        },\n    };\n    static defaultProps = {\n        autofocus: true,\n    };\n\n    setup() {\n        this.dialogService = useService(\"dialog\");\n        this.fields = this.env.searchModel.searchViewFields;\n        this.searchItemsFields = this.env.searchModel.getSearchItems((f) => f.type === \"field\");\n        this.root = useRef(\"root\");\n        this.ui = useService(\"ui\");\n\n        this.visibilityState = useState(this.props.toggler?.state || { showSearchBar: true });\n\n        // core state\n        this.state = useState({\n            expanded: [],\n            query: \"\",\n            subItemsLimits: {},\n        });\n\n        // derived state\n        this.items = useState([]);\n        this.subItems = {};\n\n        this.facetContainerRef = useRef(\"facetContainerRef\");\n        this.menuRef = useChildRef();\n        this.setupFacetNavigation();\n        this.inputDropdownState = useDropdownState();\n        this.inputDropdownNavOptions = this.getDropdownNavigation();\n\n        this.searchBarDropdownState = useDropdownState();\n\n        this.orm = useService(\"orm\");\n\n        this.keepLast = new KeepLast();\n\n        this.inputRef =\n            this.env.config.disableSearchBarAutofocus || !this.props.autofocus\n                ? useRef(\"autofocus\")\n                : useAutofocus({ mobile: this.ui.isSmall }); // only force the focus on touch devices on small screens\n\n        useBus(this.env.searchModel, \"focus-search\", () => {\n            this.inputRef.el.focus();\n        });\n\n        useBus(this.env.searchModel, \"update\", this.render);\n    }\n\n    /**\n     * @param {number} id\n     * @param {Object}\n     */\n    getSearchItem(id) {\n        return this.env.searchModel.searchItems[id];\n    }\n\n    /**\n     * @param {Object} [options={}]\n     * @param {number[]} [options.expanded]\n     * @param {string} [options.query]\n     * @param {Object[]} [options.subItems]\n     * @returns {Object[]}\n     */\n    async computeState(options = {}) {\n        const query = \"query\" in options ? options.query : this.state.query;\n        const expanded = \"expanded\" in options ? options.expanded : this.state.expanded;\n        const subItems = \"subItems\" in options ? options.subItems : this.subItems;\n\n        const tasks = [];\n        for (const id of expanded) {\n            const searchItem = this.getSearchItem(id);\n            if (searchItem.type === \"field\" && searchItem.fieldType === \"properties\") {\n                tasks.push({ id, prom: this.getSearchItemsProperties(searchItem) });\n            } else if (!subItems[id]) {\n                if (!this.state.subItemsLimits[id]) {\n                    this.state.subItemsLimits[id] = SUB_ITEMS_DEFAULT_LIMIT;\n                }\n                tasks.push({ id, prom: this.computeSubItems(searchItem, query) });\n            }\n        }\n\n        const prom = this.keepLast.add(Promise.all(tasks.map((task) => task.prom)));\n\n        if (tasks.length) {\n            const taskResults = await prom;\n            tasks.forEach((task, index) => {\n                subItems[task.id] = taskResults[index];\n            });\n        }\n\n        this.state.expanded = expanded;\n        this.state.query = query;\n        this.subItems = subItems;\n\n        this.inputRef.el.value = query;\n\n        const trimmedQuery = this.state.query.trim();\n\n        this.items.length = 0;\n        if (!trimmedQuery) {\n            return;\n        }\n\n        for (const searchItem of this.searchItemsFields) {\n            this.items.push(...this.getItems(searchItem, trimmedQuery));\n        }\n\n        this.items.push({\n            title: _t(\"Add a custom filter\"),\n            isAddCustomFilterButton: true,\n        });\n    }\n\n    /**\n     * @param {Object} searchItem\n     * @param {string} trimmedQuery\n     * @returns {Object[]}\n     */\n    getItems(searchItem, trimmedQuery) {\n        const items = [];\n\n        const isFieldProperty = searchItem.type === \"field_property\";\n        const fieldType = this.getFieldType(searchItem);\n\n        /** @todo do something with respect to localization (rtl) */\n        let preposition = this.getPreposition(searchItem);\n\n        if ((isFieldProperty && FOLDABLE_TYPES.includes(fieldType)) || fieldType === \"properties\") {\n            // Do not chose preposition for foldable properties\n            // or the properties item itself\n            preposition = null;\n        }\n        if (\n            [\"boolean\", \"tags\"].includes(fieldType) ||\n            (isFieldProperty && fieldType === \"selection\")\n        ) {\n            const booleanOptions = [\n                [true, _t(\"Yes\")],\n                [false, _t(\"No\")],\n            ];\n            let options;\n            if (isFieldProperty) {\n                const { selection, tags } = searchItem.propertyFieldDefinition || {};\n                options = selection || tags || booleanOptions;\n            } else {\n                options = booleanOptions;\n            }\n            for (const [value, label] of options) {\n                if (fuzzyTest(trimmedQuery.toLowerCase(), label.toLowerCase())) {\n                    items.push({\n                        id: nextItemId++,\n                        fieldType,\n                        searchItemDescription: searchItem.description,\n                        preposition,\n                        searchItemId: searchItem.id,\n                        label,\n                        /** @todo check if searchItem.operator is fine (here and elsewhere) */\n                        operator: searchItem.operator || \"=\",\n                        value,\n                        isFieldProperty,\n                    });\n                }\n            }\n            return items;\n        }\n\n        let value;\n        try {\n            value = parseValue(trimmedQuery, fieldType);\n        } catch {\n            return [];\n        }\n\n        const item = {\n            id: nextItemId++,\n            fieldType,\n            searchItemDescription: searchItem.description,\n            preposition,\n            searchItemId: searchItem.id,\n            label: this.state.query,\n            operator: searchItem.operator || (CHAR_FIELDS.includes(fieldType) ? \"ilike\" : \"=\"),\n            value,\n            isFieldProperty,\n        };\n\n        if (isFieldProperty) {\n            item.isParent = FOLDABLE_TYPES.includes(fieldType);\n            item.unselectable = FOLDABLE_TYPES.includes(fieldType);\n            item.propertyItemId = searchItem.propertyItemId;\n        } else if (fieldType === \"properties\") {\n            item.isParent = true;\n            item.unselectable = true;\n        } else if (fieldType === \"many2one\" || fieldType === \"selection\") {\n            item.isParent = true;\n        }\n\n        if (item.isParent) {\n            item.isExpanded = this.state.expanded.includes(item.searchItemId);\n        }\n\n        items.push(item);\n\n        if (item.isExpanded) {\n            if (searchItem.type === \"field\" && searchItem.fieldType === \"properties\") {\n                for (const subItem of this.subItems[searchItem.id]) {\n                    items.push(...this.getItems(subItem, trimmedQuery));\n                }\n            } else {\n                items.push(...this.subItems[searchItem.id]);\n            }\n        }\n\n        return items;\n    }\n\n    getPreposition(searchItem) {\n        const fieldType = this.getFieldType(searchItem);\n        return [\"date\", \"datetime\"].includes(fieldType) ? _t(\"at\") : _t(\"for\");\n    }\n\n    getFieldType(searchItem) {\n        const { type } =\n            searchItem.type === \"field_property\"\n                ? searchItem.propertyFieldDefinition\n                : this.fields[searchItem.fieldName];\n        const fieldType = type === \"reference\" ? \"char\" : type;\n\n        return fieldType;\n    }\n\n    /**\n     * @param {Object} searchItem\n     * @returns {Object[]}\n     */\n    getSearchItemsProperties(searchItem) {\n        return this.env.searchModel.getSearchItemsProperties(searchItem);\n    }\n\n    /**\n     * @param {Object} searchItem\n     * @param {string} query\n     * @returns {Object[]}\n     */\n    async computeSubItems(searchItem, query) {\n        const field = this.fields[searchItem.fieldName];\n        let options = [];\n        let showLoadMore = false;\n        if (searchItem.fieldType === \"selection\") {\n            options = field.selection.filter(([_, label]) =>\n                fuzzyTest(query.toLowerCase(), label.toLowerCase())\n            );\n        } else {\n            let domain = [];\n            if (searchItem.domain) {\n                const domainEvalContext = {\n                    ...this.env.searchModel.domainEvalContext,\n                    ...field.context,\n                };\n                domain = new Domain(searchItem.domain).toList(domainEvalContext);\n            }\n            const relation =\n                searchItem.type === \"field_property\"\n                    ? searchItem.propertyFieldDefinition.comodel\n                    : field.relation;\n\n            const limitToFetch = this.state.subItemsLimits[searchItem.id] + 1;\n            options = await this.orm.call(relation, \"name_search\", [], {\n                domain: domain,\n                context: { ...this.env.searchModel.globalContext, ...field.context },\n                limit: limitToFetch,\n                name: query.trim(),\n            });\n\n            if (options.length === limitToFetch) {\n                options.pop();\n                showLoadMore = true;\n            }\n        }\n\n        const subItems = [];\n        if (options.length) {\n            const operator = searchItem.operator || \"=\";\n            for (const [value, label] of options) {\n                subItems.push({\n                    id: nextItemId++,\n                    isChild: true,\n                    searchItemId: searchItem.id,\n                    value,\n                    label,\n                    operator,\n                });\n            }\n            if (showLoadMore) {\n                subItems.push({\n                    id: nextItemId++,\n                    isChild: true,\n                    searchItemId: searchItem.id,\n                    label: _t(\"Load more\"),\n                    unselectable: true,\n                    loadMore: () => {\n                        this.state.subItemsLimits[searchItem.id] += SUB_ITEMS_DEFAULT_LIMIT;\n                        const newSubItems = [...this.subItems];\n                        newSubItems[searchItem.id] = undefined;\n                        this.computeState({ subItems: newSubItems });\n                    },\n                });\n            }\n        } else {\n            subItems.push({\n                id: nextItemId++,\n                isChild: true,\n                searchItemId: searchItem.id,\n                label: _t(\"(no result)\"),\n                unselectable: true,\n            });\n        }\n        return subItems;\n    }\n\n    /**\n     * @param {number} [index]\n     */\n    focusFacet(index) {\n        const facets = this.root.el.getElementsByClassName(\"o_searchview_facet\");\n        if (facets.length) {\n            if (index === undefined) {\n                facets[facets.length - 1].focus();\n            } else {\n                facets[index].focus();\n            }\n        }\n    }\n\n    /**\n     * @param {Object} facet\n     */\n    removeFacet(facet) {\n        this.env.searchModel.deactivateGroup(facet.groupId);\n        this.inputRef.el.focus();\n    }\n\n    resetState(options = { focus: true }) {\n        this.state.subItemsLimits = {};\n        this.computeState({ expanded: [], query: \"\", subItems: [] });\n        if (options.focus) {\n            this.inputRef.el.focus();\n        }\n    }\n\n    /**\n     * @param {Object} item\n     */\n    selectItem(item) {\n        if (item.isAddCustomFilterButton) {\n            return this.env.searchModel.spawnCustomFilterDialog();\n        }\n\n        const searchItem = this.getSearchItem(item.searchItemId);\n        if (\n            (searchItem.fieldType === \"selection\" && !item.isChild) ||\n            (searchItem.type === \"field\" && searchItem.fieldType === \"properties\") ||\n            (searchItem.type === \"field_property\" && item.unselectable)\n        ) {\n            this.toggleItem(item, !item.isExpanded);\n            return;\n        }\n\n        if (!item.unselectable) {\n            const { searchItemId, fieldType, operator } = item;\n            let { label, value } = item;\n            if (\n                ![\"selection\", \"boolean\", \"tags\"].includes(fieldType) &&\n                this.state.query !== label &&\n                !item.isChild\n            ) {\n                // The query (the search input) changed but it hasn't been reflected yet in the\n                // items (a rendering is scheduled but hasn't been applied to the DOM yet), so select\n                // the item but use the current query. Typical usecase is when scanning a barcode,\n                // as the keystrokes are closer than when a user uses a regular keyboard.\n                label = this.state.query;\n                value = parseValue(this.state.query.trim(), fieldType);\n            }\n            this.env.searchModel.addAutoCompletionValues(searchItemId, { label, operator, value });\n        }\n\n        if (item.loadMore) {\n            item.loadMore();\n        } else {\n            this.inputDropdownState.close();\n            this.resetState();\n        }\n    }\n\n    /**\n     * @param {Object} item\n     * @param {boolean} shouldExpand\n     */\n    toggleItem(item, shouldExpand) {\n        const id = item.searchItemId;\n        const expanded = [...this.state.expanded];\n        const index = expanded.findIndex((id0) => id0 === id);\n        if (shouldExpand === true) {\n            if (index < 0) {\n                expanded.push(id);\n            }\n        } else {\n            if (index >= 0) {\n                expanded.splice(index, 1);\n            }\n        }\n        this.computeState({ expanded });\n    }\n\n    setupFacetNavigation() {\n        const isFacet = (target) => target && target.classList.contains(\"o_searchview_facet\");\n\n        useNavigation(this.facetContainerRef, {\n            shouldFocusChildInput: false,\n            getItems: () => {\n                if (this.root.el && this.inputRef.el) {\n                    return [\n                        ...this.root.el.querySelectorAll(\":scope .o_searchview_facet\"),\n                        this.inputRef.el,\n                    ];\n                }\n                return [];\n            },\n            hotkeys: {\n                enter: {\n                    isAvailable: () => !this.inputDropdownState.isOpen,\n                    callback: () => this.env.searchModel.search() /** @todo keep this thing ?*/,\n                },\n                arrowdown: {\n                    callback: () => this.env.searchModel.trigger(\"focus-view\"),\n                },\n                backspace: {\n                    bypassEditableProtection: true,\n                    allowRepeat: false,\n                    isAvailable: ({ target }) =>\n                        isFacet(target) ||\n                        (target.selectionStart === 0 && target.selectionEnd === 0),\n                    callback: (navigator) => {\n                        const facets = this.env.searchModel.facets;\n                        if (isFacet(navigator.activeItem.el)) {\n                            this.removeFacet(facets[navigator.activeItemIndex]);\n                        } else if (facets.length > 0) {\n                            this.removeFacet(facets[facets.length - 1]);\n                        }\n                    },\n                },\n                arrowright: {\n                    bypassEditableProtection: true,\n                    allowRepeat: false,\n                    isAvailable: ({ target }) =>\n                        isFacet(target) || target.selectionStart === this.state.query.length,\n                    callback: (navigator) => {\n                        navigator.next();\n                        if (navigator.activeItem.el === this.inputRef.el) {\n                            this.inputRef.el.setSelectionRange(0, 0);\n                        }\n                    },\n                },\n                arrowleft: {\n                    bypassEditableProtection: true,\n                    isAvailable: ({ target }) => isFacet(target) || target.selectionStart === 0,\n                    callback: (navigator) => {\n                        navigator.previous();\n                        if (navigator.activeItem.el === this.inputRef.el) {\n                            const inputLength = this.inputRef.el.value.length;\n                            this.inputRef.el.setSelectionRange(inputLength, inputLength);\n                        }\n                    },\n                },\n            },\n        });\n    }\n\n    /**\n     * @returns {import(\"@web/core/navigation/navigation\").NavigationOptions}\n     */\n    getDropdownNavigation() {\n        const isExpansible = (index) => {\n            const item = this.items[index];\n            return item && item.isParent;\n        };\n\n        const isCollapsible = (index) => {\n            const item = this.items[index];\n            return (\n                item && ((item.isParent && item.isExpanded) || item.isChild || item.isFieldProperty)\n            );\n        };\n\n        return {\n            virtualFocus: true,\n            getItems: () => this.menuRef.el?.querySelectorAll(\":scope .o-dropdown-item\") ?? [],\n            isNavigationAvailable: ({ navigator, target }) =>\n                this.inputDropdownState.isOpen &&\n                (this.facetContainerRef.el?.contains(target) || navigator.contains(target)),\n            onUpdated: (navigator) => (this.navigator = navigator),\n            onItemActivated: (itemEl) => (this.lastActiveItemId = parseInt(itemEl.id, 10)),\n            hotkeys: {\n                escape: {\n                    callback: () => {\n                        this.inputDropdownState.close();\n                        this.resetState();\n                    },\n                },\n                arrowright: {\n                    bypassEditableProtection: true,\n                    allowRepeat: false,\n                    isAvailable: ({ navigator }) => isExpansible(navigator.activeItemIndex),\n                    callback: (navigator) => {\n                        const item = this.items[navigator.activeItemIndex];\n                        if (item.isParent) {\n                            if (item.isExpanded) {\n                                navigator.next();\n                            } else {\n                                this.toggleItem(item, true);\n                            }\n                        }\n                    },\n                },\n                arrowleft: {\n                    bypassEditableProtection: true,\n                    isAvailable: ({ navigator }) => isCollapsible(navigator.activeItemIndex),\n                    callback: (navigator) => {\n                        const item = this.items[navigator.activeItemIndex];\n\n                        const findIndex = (id) =>\n                            this.items.findIndex(\n                                (item) => item.isParent && item.searchItemId === id\n                            );\n                        if (item && item.isParent && item.isExpanded) {\n                            this.toggleItem(item, false);\n                        } else if (item && item.isChild) {\n                            navigator.items[findIndex(item.searchItemId)]?.setActive();\n                        } else if (item && item.isFieldProperty) {\n                            navigator.items[findIndex(item.propertyItemId)]?.setActive();\n                        } else if (this.inputRef.el.selectionStart === 0) {\n                            navigator.items[this.env.searchModel.facets.length - 1]?.setActive();\n                        }\n                    },\n                },\n            },\n        };\n    }\n\n    //---------------------------------------------------------------------\n    // Handlers\n    //---------------------------------------------------------------------\n\n    onFacetLabelClick(target, facet) {\n        const { domain, groupId } = facet;\n        if (this.env.searchModel.canOrderByCount && facet.type === \"groupBy\") {\n            this.env.searchModel.switchGroupBySort();\n            return;\n        } else if (!domain) {\n            return;\n        }\n        const { resModel } = this.env.searchModel;\n        this.dialogService.add(DomainSelectorDialog, {\n            resModel,\n            domain,\n            context: this.env.searchModel.domainEvalContext,\n            onConfirm: (nextDomain) => {\n                if (nextDomain !== domain) {\n                    this.env.searchModel.splitAndAddDomain(nextDomain, groupId);\n                }\n            },\n            disableConfirmButton: (domain) => domain === `[]`,\n            title: _t(\"Custom Filter\"),\n            confirmButtonText: _t(\"Search\"),\n            discardButtonText: _t(\"Discard\"),\n            isDebugMode: this.env.searchModel.isDebugMode,\n        });\n    }\n\n    /**\n     * @param {Object} facet\n     */\n    onFacetRemove(facet) {\n        this.removeFacet(facet);\n    }\n\n    onSearchClick() {\n        if (!hasTouch()) {\n            if (!this.inputRef.el.value.length) {\n                this.searchBarDropdownState.open();\n            } else {\n                this.inputDropdownState.open();\n            }\n        }\n    }\n\n    /**\n     * @param {InputEvent} ev\n     */\n    onSearchInput(ev) {\n        if (!hasTouch()) {\n            this.searchBarDropdownState.close();\n        }\n        const query = ev.target.value;\n        if (query.trim()) {\n            if (!ev.isComposing) {\n                // Protection for IME input\n                this.inputDropdownState.open();\n            }\n            this.computeState({ query, expanded: [], subItems: [] });\n        } else if (this.items.length) {\n            this.inputDropdownState.close();\n            this.resetState();\n        }\n    }\n\n    /**\n     * @param {CompositionEvent} ev\n     */\n    onCompositionEnd(ev) {\n        const query = ev.target.value;\n        if (query.trim()) {\n            // Open dropdown after IME composition is complete\n            this.inputDropdownState.open();\n        }\n    }\n\n    onClickSearchIcon() {\n        if (!this.state.query.length) {\n            this.env.searchModel.search();\n        } else {\n            const item = this.items.find((item) => item.id === this.lastActiveItemId);\n            if (item) {\n                this.selectItem(item);\n            }\n        }\n    }\n\n    onToggleSearchBar() {\n        this.state.showSearchBar = !this.state.showSearchBar;\n    }\n\n    onInputDropdownChanged(isOpen) {\n        if (!isOpen && status(this) === \"mounted\") {\n            this.resetState({ focus: false });\n        } else if (this.navigator) {\n            this.navigator.items[0]?.setActive();\n        }\n    }\n}\n", "import { Component, useEffect, useState } from \"@odoo/owl\";\nimport { browser } from \"@web/core/browser/browser\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { useDebounced } from \"@web/core/utils/timing\";\n\nexport class SearchBarToggler extends Component {\n    static template = \"web.SearchBar.Toggler\";\n    static props = {\n        isSmall: Boolean,\n        showSearchBar: Boolean,\n        toggleSearchBar: Function,\n    };\n}\n\nexport function useSearchBarToggler() {\n    const ui = useService(\"ui\");\n\n    let isToggled = false;\n    const state = useState({\n        isSmall: ui.isSmall,\n        showSearchBar: false,\n    });\n    const updateState = () => {\n        state.isSmall = ui.isSmall;\n        state.showSearchBar = !ui.isSmall || isToggled;\n    };\n    updateState();\n\n    function toggleSearchBar() {\n        isToggled = !isToggled;\n        updateState();\n    }\n\n    const onResize = useDebounced(updateState, 200);\n    useEffect(\n        () => {\n            browser.addEventListener(\"resize\", onResize);\n            return () => browser.removeEventListener(\"resize\", onResize);\n        },\n        () => []\n    );\n\n    return {\n        state,\n        component: SearchBarToggler,\n        get props() {\n            return {\n                isSmall: state.isSmall,\n                showSearchBar: state.showSearchBar,\n                toggleSearchBar,\n            };\n        },\n    };\n}\n", "import { Component, useState } from \"@odoo/owl\";\nimport { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { PropertiesGroupByItem } from \"@web/search/properties_group_by_item/properties_group_by_item\";\nimport { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\nimport { registry } from \"@web/core/registry\";\nimport { sortBy } from \"@web/core/utils/arrays\";\nimport { useBus, useService } from \"@web/core/utils/hooks\";\nimport { AccordionItem } from \"@web/core/dropdown/accordion_item\";\nimport { CustomGroupByItem } from \"@web/search/custom_group_by_item/custom_group_by_item\";\nimport { CheckboxItem } from \"@web/core/dropdown/checkbox_item\";\nimport { FACET_ICONS, GROUPABLE_TYPES } from \"@web/search/utils/misc\";\n\nconst favoriteMenuRegistry = registry.category(\"favoriteMenu\");\n\nexport class SearchBarMenu extends Component {\n    static template = \"web.SearchBarMenu\";\n    static components = {\n        Dropdown,\n        DropdownItem,\n        CheckboxItem,\n        CustomGroupByItem,\n        AccordionItem,\n        PropertiesGroupByItem,\n    };\n    static props = {\n        slots: {\n            type: Object,\n            optional: true,\n            shape: {\n                default: { optional: true },\n            },\n        },\n        dropdownState: { ...Dropdown.props.state },\n    };\n\n    setup() {\n        this.facet_icons = FACET_ICONS;\n        // Filter\n        this.actionService = useService(\"action\");\n        // 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        // Favorite\n        this.state = useState({ sharedFavoritesExpanded: false });\n        useBus(this.env.searchModel, \"update\", this.render);\n    }\n\n    // Filter Panel\n    get filterItems() {\n        return this.env.searchModel.getSearchItems((searchItem) =>\n            [\"filter\", \"dateFilter\"].includes(searchItem.type)\n        );\n    }\n\n    async onAddCustomFilterClick() {\n        this.env.searchModel.spawnCustomFilterDialog();\n    }\n\n    /**\n     * @param {Object} param0\n     * @param {number} param0.itemId\n     * @param {number} [param0.optionId]\n     */\n    onFilterSelected({ itemId, optionId }) {\n        if (optionId) {\n            this.env.searchModel.toggleDateFilter(itemId, optionId);\n        } else {\n            this.env.searchModel.toggleSearchItem(itemId);\n        }\n    }\n\n    // GroupBy Panel\n    /**\n     * @returns {boolean}\n     */\n    get hideCustomGroupBy() {\n        return this.env.searchModel.hideCustomGroupBy || false;\n    }\n\n    /**\n     * @returns {Object[]}\n     */\n    get groupByItems() {\n        return this.env.searchModel.getSearchItems(\n            (searchItem) =>\n                [\"groupBy\", \"dateGroupBy\"].includes(searchItem.type) && !searchItem.isProperty\n        );\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 groupable && fieldName !== \"id\" && GROUPABLE_TYPES.includes(type);\n    }\n\n    /**\n     * @param {Object} param0\n     * @param {number} param0.itemId\n     * @param {number} [param0.optionId]\n     */\n    onGroupBySelected({ itemId, optionId }) {\n        if (optionId) {\n            this.env.searchModel.toggleDateGroupBy(itemId, optionId);\n        } else {\n            this.env.searchModel.toggleSearchItem(itemId);\n        }\n    }\n\n    /**\n     * @param {string} fieldName\n     */\n    onAddCustomGroup(fieldName) {\n        this.env.searchModel.createNewGroupBy(fieldName);\n    }\n\n    // Favorite Panel\n\n    get favorites() {\n        return this.env.searchModel.getSearchItems(\n            (searchItem) => searchItem.type === \"favorite\" && searchItem.userIds.length === 1\n        );\n    }\n\n    get sharedFavorites() {\n        const sharedFavorites = this.env.searchModel.getSearchItems(\n            (searchItem) => searchItem.type === \"favorite\" && searchItem.userIds.length !== 1\n        );\n        if (sharedFavorites.length <= 4 || this.state.sharedFavoritesExpanded) {\n            this.state.sharedFavoritesExpanded = true;\n        } else {\n            sharedFavorites.length = 3;\n        }\n        return sharedFavorites;\n    }\n\n    get otherItems() {\n        const registryMenus = [];\n        for (const item of favoriteMenuRegistry.getAll()) {\n            if (\"isDisplayed\" in item ? item.isDisplayed(this.env) : true) {\n                registryMenus.push({\n                    Component: item.Component,\n                    groupNumber: item.groupNumber,\n                    key: item.Component.name,\n                });\n            }\n        }\n        return registryMenus;\n    }\n\n    onFavoriteSelected(itemId) {\n        this.env.searchModel.toggleSearchItem(itemId);\n    }\n\n    editFavorite(itemId) {\n        this.actionService.doAction({\n            type: \"ir.actions.act_window\",\n            res_model: \"ir.filters\",\n            views: [[false, \"form\"]],\n            context: {\n                form_view_ref: \"base.ir_filters_view_edit_form\",\n            },\n            res_id: this.env.searchModel.searchItems[itemId].serverSideId,\n        });\n    }\n}\n", "import { EventBus, toRaw } from \"@odoo/owl\";\nimport { makeContext } from \"@web/core/context\";\nimport { Domain } from \"@web/core/domain\";\nimport { getDefaultDomain } from \"@web/core/domain_selector/utils\";\nimport { DomainSelectorDialog } from \"@web/core/domain_selector_dialog/domain_selector_dialog\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { rpcBus } from \"@web/core/network/rpc\";\nimport { evaluateExpr } from \"@web/core/py_js/py\";\nimport { domainFromTree } from \"@web/core/tree_editor/domain_from_tree\";\nimport { user } from \"@web/core/user\";\nimport { groupBy, sortBy } from \"@web/core/utils/arrays\";\nimport { deepCopy } from \"@web/core/utils/objects\";\nimport { SearchArchParser } from \"./search_arch_parser\";\nimport {\n    constructDateDomain,\n    DEFAULT_INTERVAL,\n    getIntervalOptions,\n    getPeriodOptions,\n    INTERVAL_OPTIONS,\n    rankInterval,\n    yearSelected,\n} from \"./utils/dates\";\nimport { FACET_COLORS, FACET_ICONS } from \"./utils/misc\";\n\nconst { DateTime } = luxon;\n\n/**\n * @typedef {import(\"@web/core/context\").Context} Context\n * @typedef {import(\"@web/core/domain\").DomainListRepr} DomainListRepr\n * @typedef {import(\"@web/search/utils/order_by\").OrderTerm} OrderTerm\n *\n * @typedef {{\n *  name: string;\n *  type: string;\n *  selection: [string, string][];\n * }} Field\n *\n * @typedef {{\n *  context: Context;\n *  forceSave?: boolean;\n *  invisible: string;\n *  isHandle?: boolean;\n *  onChange: boolean;\n *  readonly: string;\n *  required: string;\n * }} FieldInfo\n *\n * @typedef {{\n *  context: Context;\n *  domain: DomainListRepr;\n *  groupBy: string[];\n *  orderBy: OrderTerm[];\n *  resModel: string;\n *  resId?: number | false;\n *  useSampleModel?: boolean;\n * }} SearchParams\n */\n\nconst SPECIAL = Symbol(\"special\");\n\n/** @todo rework doc */\n// interface SectionCommon { // check optional keys\n//     color: string;\n//     description: string;\n//     errorMsg: [string];\n//     enableCounters: boolean;\n//     expand: boolean;\n//     fieldName: string;\n//     icon: string;\n//     id: number;\n//     limit: number;\n//     values: Map<any,any>;\n//   }\n\n//   export interface Category extends SectionCommon {\n//     type: \"category\";\n//     hierarchize: boolean;\n//   }\n\n//   export interface Filter extends SectionCommon {\n//     type: \"filter\";\n//     domain: string;\n//     groupBy: string;\n//     groups: Map<any,any>;\n//   }\n\n//   export type Section = Category | Filter;\n\n//   export type SectionPredicate = (section: Section) => boolean;\n\n/**\n * @param {Section} section\n * @returns {boolean}\n */\nfunction hasValues(section) {\n    const { errorMsg, type, values } = section;\n    if (errorMsg) {\n        return true;\n    }\n    switch (type) {\n        case \"category\": {\n            return values && values.size > 1; // false item ignored\n        }\n        case \"filter\": {\n            return values && values.size > 0;\n        }\n    }\n}\n\n/**\n * Returns a serialised array of the given map with its values being the\n * shallow copies of the original values.\n * @param {Map<any, Object>} map\n * @return {Array[]}\n */\nfunction mapToArray(map) {\n    const result = [];\n    for (const [key, val] of map) {\n        const valCopy = Object.assign({}, val);\n        result.push([key, valCopy]);\n    }\n    return result;\n}\n/**\n * @param {Array[]}\n * @returns {Map<any, Object>} map\n */\nfunction arraytoMap(array) {\n    return new Map(array);\n}\n\n/**\n * @param {Function} op\n * @param {Object} source\n * @param {Object} target\n */\nfunction execute(op, source, target) {\n    const { query, nextId, nextGroupId, nextGroupNumber, searchItems, searchPanelInfo, sections } =\n        source;\n\n    target.nextGroupId = nextGroupId;\n    target.nextGroupNumber = nextGroupNumber;\n    target.nextId = nextId;\n\n    target.query = query;\n    target.searchItems = searchItems;\n\n    target.searchPanelInfo = searchPanelInfo;\n\n    target.sections = op(sections);\n    for (const [, section] of target.sections) {\n        section.values = op(section.values);\n        if (section.groups) {\n            section.groups = op(section.groups);\n            for (const [, group] of section.groups) {\n                group.values = op(group.values);\n            }\n        }\n    }\n}\n\n//--------------------------------------------------------------------------\n// Global constants/variables\n//--------------------------------------------------------------------------\n\nconst FAVORITE_PRIVATE_GROUP = 1;\nconst FAVORITE_SHARED_GROUP = 2;\n\nexport class SearchModel extends EventBus {\n    constructor(env, services, args) {\n        super();\n        this.env = env;\n        this.setup(services, args);\n    }\n\n    setup(services) {\n        // services\n        const { field: fieldService, orm, view, dialog, treeProcessor } = services;\n        this.orm = orm;\n        this.fieldService = fieldService;\n        this.viewService = view;\n        this.treeProcessor = treeProcessor;\n        this.dialog = dialog;\n        this.orderByCount = false;\n\n        // used to manage search items related to date/datetime fields\n        this.referenceMoment = DateTime.local();\n        this.intervalOptions = getIntervalOptions();\n        this.categoriesLoadId = 0;\n        this.filtersLoadId = 0;\n    }\n\n    /**\n     *\n     * @param {Object} config\n     * @param {string} config.resModel\n     *\n     * @param {string} [config.searchViewArch=\"<search/>\"]\n     * @param {Object} [config.searchViewFields={}]\n     * @param {number|false} [config.searchViewId=false]\n     * @param {Object[]} [config.irFilters=[]]\n     *\n     * @param {boolean} [config.activateFavorite=true]\n     * @param {Object} [config.context={}]\n     * @param {Array} [config.domain=[]]\n     * @param {Array} [config.dynamicFilters=[]]\n     * @param {string[]} [config.groupBy=[]]\n     * @param {boolean} [config.loadIrFilters=false]\n     * @param {boolean} [config.display.searchPanel=true]\n     * @param {OrderTerm[]} [config.orderBy=[]]\n     * @param {string[]} [config.searchMenuTypes=[\"filter\", \"groupBy\", \"favorite\"]]\n     * @param {Object} [config.state]\n     */\n    async load(config) {\n        const { resModel } = config;\n        if (!resModel) {\n            throw Error(`SearchModel config should have a \"resModel\" key`);\n        }\n        this.resModel = resModel;\n\n        // used to avoid useless recomputations\n        this._reset();\n\n        const { context, domain, groupBy, hideCustomGroupBy, orderBy } = config;\n\n        this.globalContext = toRaw(Object.assign({}, context));\n        this.globalDomain = domain || [];\n        this.globalGroupBy = groupBy || [];\n        this.globalOrderBy = orderBy || [];\n        this.hideCustomGroupBy = hideCustomGroupBy;\n\n        this.searchMenuTypes = new Set(config.searchMenuTypes || [\"filter\", \"groupBy\", \"favorite\"]);\n        this.canOrderByCount = config.canOrderByCount;\n        this.defaultGroupBy = config.defaultGroupBy;\n\n        let { irFilters, loadIrFilters, searchViewArch, searchViewFields, searchViewId } = config;\n        const loadSearchView =\n            searchViewId !== undefined &&\n            (!searchViewArch || !searchViewFields || (!irFilters && loadIrFilters));\n\n        const searchViewDescription = {};\n        if (loadSearchView) {\n            const result = await this.viewService.loadViews(\n                {\n                    context: this.globalContext,\n                    resModel,\n                    views: [[searchViewId, \"search\"]],\n                },\n                {\n                    actionId: this.env.config.actionId,\n                    embeddedActionId: this.env.config.currentEmbeddedActionId,\n                    loadIrFilters: loadIrFilters || false,\n                }\n            );\n            Object.assign(searchViewDescription, result.views.search);\n            searchViewFields = searchViewFields || result.fields;\n        }\n        if (searchViewArch) {\n            searchViewDescription.arch = searchViewArch;\n        }\n        if (irFilters) {\n            searchViewDescription.irFilters = irFilters;\n        }\n        if (searchViewId !== undefined) {\n            searchViewDescription.viewId = searchViewId;\n        }\n        this.searchViewArch = searchViewDescription.arch || \"<search/>\";\n        this.searchViewFields = searchViewFields || {};\n        if (searchViewDescription.irFilters) {\n            this.irFilters = searchViewDescription.irFilters;\n        }\n        if (searchViewDescription.viewId !== undefined) {\n            this.searchViewId = searchViewDescription.viewId;\n        }\n\n        const { searchDefaults, searchPanelDefaults } =\n            this._extractSearchDefaultsFromGlobalContext();\n\n        if (config.state) {\n            this._importState(config.state);\n            this.__legacyParseSearchPanelArchAnyway(searchViewDescription, searchViewFields);\n            this.display = this._getDisplay(config.display);\n            this._reconciliateFavorites();\n            if (!this.searchPanelInfo.loaded) {\n                return this._reloadSections();\n            }\n            return;\n        }\n\n        this.blockNotification = true;\n\n        this.searchItems = {};\n        this.query = [];\n\n        this.nextId = 1;\n        this.nextGroupId = 1;\n        this.nextGroupNumber = 1;\n\n        const parser = new SearchArchParser(\n            searchViewDescription,\n            searchViewFields,\n            searchDefaults,\n            searchPanelDefaults\n        );\n        const { labels, preSearchItems, searchPanelInfo, sections } = parser.parse();\n\n        this.searchPanelInfo = { ...searchPanelInfo, loaded: false, shouldReload: false };\n\n        await Promise.all(labels.map((cb) => cb(this.orm)));\n\n        // prepare search items (populate this.searchItems)\n        for (const preGroup of preSearchItems || []) {\n            this._createGroupOfSearchItems(preGroup);\n        }\n        this.nextGroupNumber =\n            1 + Math.max(...Object.values(this.searchItems).map((i) => i.groupNumber || 0), 0);\n\n        const { dynamicFilters } = config;\n        if (dynamicFilters) {\n            this._createGroupOfDynamicFilters(dynamicFilters);\n        }\n\n        const defaultFavoriteId = this._createGroupOfFavorites(this.irFilters || []);\n        const activateFavorite = \"activateFavorite\" in config ? config.activateFavorite : true;\n\n        // activate default search items (populate this.query)\n        this._activateDefaultSearchItems(activateFavorite ? defaultFavoriteId : null);\n\n        // prepare search panel sections\n\n        /** @type Map<number,Section> */\n        this.sections = new Map(sections || []);\n        this.display = this._getDisplay(config.display);\n\n        if (this.display.searchPanel) {\n            /** @type DomainListRepr */\n            this.searchDomain = this._getDomain({ withSearchPanel: false });\n            this.sectionsPromise = this._fetchSections(this.categories, this.filters).then(() => {\n                for (const { fieldName, values } of this.filters) {\n                    const filterDefaults = searchPanelDefaults[fieldName] || [];\n                    for (const valueId of filterDefaults) {\n                        const value = values.get(valueId);\n                        if (value) {\n                            value.checked = true;\n                        }\n                    }\n                }\n            });\n            if (Object.keys(searchPanelDefaults).length || this._shouldWaitForData(false)) {\n                await this.sectionsPromise;\n            }\n        }\n\n        this.blockNotification = false;\n    }\n\n    /**\n     * @param {Object} [config={}]\n     * @param {Object} [config.context={}]\n     * @param {Array} [config.domain=[]]\n     * @param {string[]} [config.groupBy=[]]\n     * @param {OrderTerm[]} [config.orderBy=[]]\n     */\n    async reload(config = {}) {\n        this._reset();\n\n        const { context, domain, groupBy, orderBy } = config;\n\n        this.globalContext = Object.assign({}, context);\n        this.globalDomain = domain || [];\n        this.globalGroupBy = groupBy || [];\n        this.globalOrderBy = orderBy || [];\n\n        this._extractSearchDefaultsFromGlobalContext();\n\n        await this._reloadSections();\n    }\n\n    //--------------------------------------------------------------------------\n    // Getters\n    //--------------------------------------------------------------------------\n\n    /**\n     * @returns {Category[]}\n     */\n    get categories() {\n        return [...this.sections.values()].filter((s) => s.type === \"category\");\n    }\n\n    /**\n     * @returns {Context} should be imported from context.js?\n     */\n    get context() {\n        if (!this._context) {\n            this._context = makeContext([this.globalContext, this._getContext()]);\n        }\n        return deepCopy(this._context);\n    }\n\n    /**\n     * @returns {DomainListRepr}\n     */\n    get domain() {\n        if (!this._domain) {\n            this._domain = this._getDomain();\n        }\n        return deepCopy(this._domain);\n    }\n\n    /**\n     * @returns {string}\n     */\n    get domainString() {\n        return this._getDomain({ raw: true }).toString();\n    }\n\n    get domainEvalContext() {\n        return Object.assign({}, this.globalContext, user.context);\n    }\n\n    get facets() {\n        const facets = [];\n        for (const facet of this._getFacets()) {\n            if (facet.type === \"groupBy\" && !this.searchMenuTypes.has(facet.type)) {\n                continue;\n            }\n            facets.push(facet);\n        }\n        return facets;\n    }\n\n    /**\n     * @returns {Filter[]}\n     */\n    get filters() {\n        return [...this.sections.values()].filter((s) => s.type === \"filter\");\n    }\n\n    /**\n     * @returns {string[]}\n     */\n    get groupBy() {\n        if (!this.searchMenuTypes.has(\"groupBy\")) {\n            return [];\n        }\n        if (!this._groupBy) {\n            this._groupBy = this._getGroupBy();\n        }\n        return deepCopy(this._groupBy);\n    }\n\n    /**\n     * @returns {OrderTerm[]}\n     */\n    get orderBy() {\n        if (!this._orderBy) {\n            this._orderBy = this._getOrderBy();\n        }\n        return deepCopy(this._orderBy);\n    }\n\n    get isDebugMode() {\n        return !!this.env.debug;\n    }\n    //--------------------------------------------------------------------------\n    // Public\n    //--------------------------------------------------------------------------\n\n    /**\n     * Activate a filter of type 'field' with given filterId with\n     * 'autocompleteValues' value, label, and operator.\n     * @param {Object}\n     */\n    addAutoCompletionValues(searchItemId, autocompleteValue) {\n        const searchItem = this.searchItems[searchItemId];\n        if (![\"field\", \"field_property\"].includes(searchItem.type)) {\n            return;\n        }\n        const { label, value, operator } = autocompleteValue;\n        const queryElem = this.query.find(\n            (queryElem) =>\n                queryElem.searchItemId === searchItemId &&\n                \"autocompleteValue\" in queryElem &&\n                queryElem.autocompleteValue.value === value &&\n                queryElem.autocompleteValue.operator === operator\n        );\n        if (!queryElem) {\n            this.query.push({ searchItemId, autocompleteValue });\n        } else {\n            queryElem.autocompleteValue.label = label; // seems related to old stuff --> should be useless now\n        }\n        this._notify();\n    }\n\n    /**\n     * Remove all the query elements from query.\n     */\n    clearQuery() {\n        this.query = [];\n        this.orderByCount = false;\n        this._notify();\n    }\n\n    /**\n     * Removes filter, field and favorite facets but keeps groupBy ones\n     */\n    clearFilters() {\n        this.blockNotification = true;\n        this.facets.forEach((facet) => {\n            if (facet.type !== \"groupBy\") {\n                this.deactivateGroup(facet.groupId);\n            }\n        });\n        this.blockNotification = false;\n        this._notify();\n    }\n\n    /**\n     * Create a new filter of type 'favorite' and activate it.\n     * A new group containing only that filter is created.\n     * The query is emptied before activating the new favorite.\n     * @param {Object} params\n     * @returns {Promise}\n     */\n    async createNewFavorite(params) {\n        const { preFavorite, irFilter } = this._getIrFilterDescription(params);\n        const serverSideId = await this._createIrFilters(irFilter);\n\n        // before the filter cache was cleared!\n        this.blockNotification = true;\n        this.clearQuery();\n        const favorite = {\n            ...preFavorite,\n            type: \"favorite\",\n            id: this.nextId,\n            groupId: this.nextGroupId,\n            groupNumber:\n                preFavorite.userIds.length === 1 ? FAVORITE_PRIVATE_GROUP : FAVORITE_SHARED_GROUP,\n            removable: true,\n            serverSideId,\n        };\n        this.searchItems[this.nextId] = favorite;\n        this.query.push({ searchItemId: this.nextId });\n        this.nextGroupId++;\n        this.nextId++;\n        this.blockNotification = false;\n        this._notify();\n        return serverSideId;\n    }\n\n    async _createIrFilters(irFilter) {\n        const serverSideIds = await this.orm.call(\"ir.filters\", \"create_filter\", [irFilter]);\n        rpcBus.trigger(\"CLEAR-CACHES\", \"get_views\");\n        return serverSideIds[0];\n    }\n\n    /**\n     * Create new search items of type 'filter' and activate them.\n     * A new group containing only those filters is created.\n     */\n    createNewFilters(prefilters) {\n        if (!prefilters.length) {\n            return [];\n        }\n        prefilters.forEach((preFilter) => {\n            const filter = Object.assign(preFilter, {\n                groupId: this.nextGroupId,\n                groupNumber: this.nextGroupNumber,\n                id: this.nextId,\n                type: \"filter\",\n            });\n            this.searchItems[this.nextId] = filter;\n            this.query.push({ searchItemId: this.nextId });\n            this.nextId++;\n        });\n        this.nextGroupId++;\n        this.nextGroupNumber++;\n        this._notify();\n    }\n\n    /**\n     * Create a new filter of type 'groupBy' or 'dateGroupBy' and activate it.\n     * It is added to the unique group of groupbys.\n     * @param {string} fieldName\n     * @param {Object} [param]\n     * @param {string} [param.interval=DEFAULT_INTERVAL]\n     * @param {boolean} [param.invisible=false]\n     */\n    createNewGroupBy(fieldName, { interval, invisible } = {}) {\n        const field = this.searchViewFields[fieldName];\n        const { string, type: fieldType } = field;\n        const firstGroupBy = Object.values(this.searchItems).find((f) => f.type === \"groupBy\");\n        const preSearchItem = {\n            description: string || fieldName,\n            fieldName,\n            fieldType,\n            groupId: firstGroupBy ? firstGroupBy.groupId : this.nextGroupId++,\n            groupNumber: this.nextGroupNumber,\n            id: this.nextId,\n            custom: true,\n        };\n        if (invisible) {\n            preSearchItem.invisible = \"True\";\n        }\n        if ([\"date\", \"datetime\"].includes(fieldType)) {\n            this.searchItems[this.nextId] = Object.assign(\n                { type: \"dateGroupBy\", defaultIntervalId: interval || DEFAULT_INTERVAL },\n                preSearchItem\n            );\n            this.toggleDateGroupBy(this.nextId);\n        } else {\n            this.searchItems[this.nextId] = Object.assign({ type: \"groupBy\" }, preSearchItem);\n            this.toggleSearchItem(this.nextId);\n        }\n        this.nextGroupNumber++; // FIXME: with this, all subsequent added groups are in different groups (visually)\n        this.nextId++;\n        this._notify();\n    }\n\n    /**\n     * Deactivate a group with provided groupId, i.e. delete the query elements\n     * with given groupId.\n     */\n    deactivateGroup(groupId) {\n        if (groupId === SPECIAL) {\n            delete this.defaultGroupBy;\n            this._notify();\n            return;\n        }\n        this.query = this.query.filter((queryElem) => {\n            const searchItem = this.searchItems[queryElem.searchItemId];\n            return searchItem.groupId !== groupId;\n        });\n        this._checkOrderByCountStatus();\n        this._notify();\n    }\n\n    /**\n     * @returns {Object}\n     */\n    exportState() {\n        const state = {};\n        execute(mapToArray, this, state);\n        return state;\n    }\n\n    getIrFilterValues(params) {\n        const { irFilter } = this._getIrFilterDescription(params);\n        return irFilter;\n    }\n\n    getPreFavoriteValues(params) {\n        const { preFavorite } = this._getIrFilterDescription(params);\n        return preFavorite;\n    }\n\n    /**\n     * Return an array containing enriched copies of all searchElements or of those\n     * satifying the given predicate if any\n     * @param {Function} [predicate]\n     * @returns {Object[]}\n     */\n    getSearchItems(predicate) {\n        const searchItems = [];\n        for (const searchItem of Object.values(this.searchItems)) {\n            const enrichedSearchitem = this._enrichItem(searchItem);\n            if (enrichedSearchitem) {\n                const isInvisible =\n                    \"invisible\" in searchItem &&\n                    evaluateExpr(searchItem.invisible, this.domainEvalContext);\n                if (!isInvisible && (!predicate || predicate(enrichedSearchitem))) {\n                    searchItems.push(enrichedSearchitem);\n                }\n            }\n        }\n        if (searchItems.some((f) => f.type === \"favorite\")) {\n            searchItems.sort((f1, f2) => f1.groupNumber - f2.groupNumber);\n        }\n        return searchItems;\n    }\n\n    /**\n     * Returns a sorted list of a copy of all sections. This list can be\n     * filtered by a given predicate.\n     * @param {SectionPredicate} [predicate] used to determine\n     *      which subsets of sections is wanted\n     * @returns {Section[]}\n     */\n    getSections(predicate) {\n        let sections = [...this.sections.values()].map((section) =>\n            Object.assign({}, section, { empty: !hasValues(section) })\n        );\n        if (predicate) {\n            sections = sections.filter(predicate);\n        }\n        return sections.sort((s1, s2) => s1.index - s2.index);\n    }\n\n    search() {\n        this.trigger(\"update\");\n    }\n\n    async splitAndAddDomain(domain, groupId) {\n        const group = groupId ? this._getGroups().find((g) => g.id === groupId) : null;\n        let context;\n        if (group) {\n            const contexts = [];\n            for (const activeItem of group.activeItems) {\n                const context = this._getSearchItemContext(activeItem);\n                if (context) {\n                    contexts.push(context);\n                }\n            }\n            context = makeContext(contexts);\n        }\n\n        const tree = await this.treeProcessor.treeFromDomain(\n            this.resModel,\n            domain,\n            !this.isDebugMode\n        );\n        const trees =\n            !tree.negate &&\n            tree.type === \"connector\" &&\n            tree.value === \"&\" &&\n            tree.children.length > 0\n                ? tree.children\n                : [tree];\n        const promises = trees.map(async (tree) => {\n            const [description, tooltip] = await Promise.all([\n                this.treeProcessor.getDomainTreeDescription(this.resModel, tree),\n                this.treeProcessor.getDomainTreeTooltip(this.resModel, tree),\n            ]);\n            const preFilter = {\n                description,\n                tooltip,\n                domain: domainFromTree(tree),\n                invisible: \"True\",\n                type: \"filter\",\n            };\n            if (context) {\n                preFilter.context = context;\n            }\n            return preFilter;\n        });\n\n        const preFilters = await Promise.all(promises);\n\n        this.blockNotification = true;\n\n        let queryItemIndex;\n        if (group) {\n            const firstActiveItem = group.activeItems[0];\n            const firstSearchItem = this.searchItems[firstActiveItem.searchItemId];\n            queryItemIndex = this.query.findIndex(\n                (queryElem) => queryElem.searchItemId === firstActiveItem.searchItemId\n            );\n            const { type } = firstSearchItem;\n            if (type === \"favorite\") {\n                const activeItemGroupBys = this._getSearchItemGroupBys(firstActiveItem);\n                let createNewGroupBys = Boolean(activeItemGroupBys.length);\n                if (\n                    createNewGroupBys &&\n                    this.defaultGroupBy &&\n                    this.env.config.viewType === \"kanban\"\n                ) {\n                    const currentGroupBy = this._getGroupBy({ fallbackOnDefault: false });\n                    if (JSON.stringify(currentGroupBy) === JSON.stringify(this.defaultGroupBy)) {\n                        createNewGroupBys = false;\n                    }\n                }\n                if (createNewGroupBys) {\n                    for (const activeItemGroupBy of activeItemGroupBys) {\n                        const [fieldName, interval] = activeItemGroupBy.split(\":\");\n                        this.createNewGroupBy(fieldName, { interval, invisible: true });\n                    }\n                    const index = this.query.length - activeItemGroupBys.length;\n                    this.query = [...this.query.slice(index), ...this.query.slice(0, index)];\n                }\n            }\n            this.deactivateGroup(groupId);\n        }\n\n        const queryLength = this.query.length;\n        for (const preFilter of preFilters) {\n            this.createNewFilters([preFilter]);\n        }\n        const queryElems = this.query.slice(queryLength);\n\n        if (queryItemIndex !== undefined) {\n            this.query = [\n                ...this.query.slice(0, queryItemIndex),\n                ...queryElems,\n                ...this.query.slice(queryItemIndex, queryLength),\n            ];\n        }\n\n        this.blockNotification = false;\n\n        this._notify();\n    }\n\n    /**\n     * Set the active value id of a given category.\n     * @param {number} sectionId\n     * @param {number} valueId\n     */\n    toggleCategoryValue(sectionId, valueId) {\n        const category = this.sections.get(sectionId);\n        category.activeValueId = valueId;\n        this._notify();\n    }\n\n    /**\n     * Toggle a filter value of a given section. The value will be set\n     * to \"forceTo\" if provided, else it will be its own opposed value.\n     * @param {number} sectionId\n     * @param {number[]} valueIds\n     * @param {boolean} [forceTo=null]\n     */\n    toggleFilterValues(sectionId, valueIds, forceTo = null) {\n        const filter = this.sections.get(sectionId);\n        for (const valueId of valueIds) {\n            const value = filter.values.get(valueId);\n            value.checked = forceTo === null ? !value.checked : forceTo;\n        }\n        this._notify();\n    }\n\n    /**\n     * Clears all values from the provided sections\n     * @param {array} sectionIds\n     */\n    clearSections(sectionIds) {\n        for (const sectionId of sectionIds) {\n            const section = this.sections.get(sectionId);\n            if (section.type === \"category\") {\n                section.activeValueId = false;\n            } else {\n                for (const [, value] of section.values) {\n                    value.checked = false;\n                }\n            }\n        }\n        this._notify();\n    }\n\n    /**\n     * Activate or deactivate the simple filter with given filterId, i.e.\n     * add or remove a corresponding query element.\n     */\n    toggleSearchItem(searchItemId) {\n        const searchItem = this.searchItems[searchItemId];\n        switch (searchItem.type) {\n            case \"dateFilter\":\n            case \"dateGroupBy\":\n            case \"field_property\":\n            case \"field\": {\n                return;\n            }\n        }\n        const index = this.query.findIndex((queryElem) => queryElem.searchItemId === searchItemId);\n        if (index >= 0) {\n            this.query.splice(index, 1);\n            this._checkOrderByCountStatus();\n        } else {\n            if (searchItem.type === \"favorite\") {\n                this.query = [];\n            }\n            this.query.push({ searchItemId });\n        }\n        this._notify();\n    }\n\n    /**\n     * Used to toggle a query element.\n     * This can impact the query in various form, e.g. add/remove other query elements\n     * in case the filter is of type 'filter'.\n     */\n    toggleDateFilter(searchItemId, generatorId) {\n        const searchItem = this.searchItems[searchItemId];\n        if (searchItem.type !== \"dateFilter\") {\n            return;\n        }\n        const generatorIds = generatorId ? [generatorId] : searchItem.defaultGeneratorIds;\n        for (const generatorId of generatorIds) {\n            const index = this.query.findIndex(\n                (queryElem) =>\n                    queryElem.searchItemId === searchItemId &&\n                    \"generatorId\" in queryElem &&\n                    queryElem.generatorId === generatorId\n            );\n            if (index >= 0) {\n                this.query.splice(index, 1);\n                if (!yearSelected(this._getSelectedGeneratorIds(searchItemId))) {\n                    // This is the case where generatorId was the last option\n                    // of type 'year' to be there before being removed above.\n                    // Since other options of type 'month' or 'quarter' do\n                    // not make sense without a year we deactivate all options.\n                    this.query = this.query.filter(\n                        (queryElem) => queryElem.searchItemId !== searchItemId\n                    );\n                }\n            } else {\n                if (generatorId.startsWith(\"custom\")) {\n                    this.query = this.query.filter(\n                        (queryElem) => searchItemId !== queryElem.searchItemId\n                    );\n                    this.query.push({ searchItemId, generatorId });\n                    continue;\n                }\n                this.query = this.query.filter(\n                    (queryElem) =>\n                        queryElem.searchItemId !== searchItemId ||\n                        !queryElem.generatorId.startsWith(\"custom\")\n                );\n                this.query.push({ searchItemId, generatorId });\n                if (!yearSelected(this._getSelectedGeneratorIds(searchItemId))) {\n                    // Here we add 'year' as options if no option of type\n                    // year is already selected.\n                    const { defaultYearId } = getPeriodOptions(\n                        this.referenceMoment,\n                        searchItem.optionsParams\n                    ).find((o) => o.id === generatorId);\n                    this.query.push({ searchItemId, generatorId: defaultYearId });\n                }\n            }\n        }\n        this._notify();\n    }\n\n    toggleDateGroupBy(searchItemId, intervalId) {\n        const searchItem = this.searchItems[searchItemId];\n        if (searchItem.type !== \"dateGroupBy\") {\n            return;\n        }\n        intervalId = intervalId || searchItem.defaultIntervalId;\n        const index = this.query.findIndex(\n            (queryElem) =>\n                queryElem.searchItemId === searchItemId &&\n                \"intervalId\" in queryElem &&\n                queryElem.intervalId === intervalId\n        );\n        if (index >= 0) {\n            this.query.splice(index, 1);\n            this._checkOrderByCountStatus();\n        } else {\n            this.query.push({ searchItemId, intervalId });\n        }\n        this._notify();\n    }\n\n    async spawnCustomFilterDialog() {\n        const domain = getDefaultDomain(this.searchViewFields);\n        this.dialog.add(DomainSelectorDialog, {\n            resModel: this.resModel,\n            defaultConnector: \"|\",\n            domain,\n            context: this.globalContext,\n            onConfirm: (domain) => this.splitAndAddDomain(domain),\n            disableConfirmButton: (domain) => domain === `[]`,\n            title: _t(\"Custom Filter\"),\n            confirmButtonText: _t(\"Search\"),\n            discardButtonText: _t(\"Discard\"),\n            isDebugMode: this.isDebugMode,\n        });\n    }\n\n    switchGroupBySort() {\n        if (this.orderByCount === \"Desc\") {\n            this.orderByCount = \"Asc\";\n        } else {\n            this.orderByCount = \"Desc\";\n        }\n        this._notify();\n    }\n\n    /**\n     * Generate the searchItems corresponding to the properties.\n     * @param {Object} searchItem\n     * @returns {Object[]}\n     */\n    async getSearchItemsProperties(searchItem) {\n        if (searchItem.type !== \"field\" || searchItem.fieldType !== \"properties\") {\n            return [];\n        }\n        const field = this.searchViewFields[searchItem.fieldName];\n        const definitionRecord = field.definition_record;\n        const result = await this._fetchPropertiesDefinition(this.resModel, searchItem.fieldName);\n\n        const searchItemIds = new Set();\n        const existingFieldProperties = {};\n        for (const item of Object.values(this.searchItems)) {\n            if (item.type === \"field_property\" && item.propertyItemId === searchItem.id) {\n                existingFieldProperties[item.propertyFieldDefinition.name] = item;\n            }\n        }\n\n        for (const { definitionRecordId, definitionRecordName, definitions } of result) {\n            for (const definition of definitions) {\n                if (definition.type === \"separator\") {\n                    continue;\n                }\n                const existingSearchItem = existingFieldProperties[definition.name];\n                if (existingSearchItem) {\n                    // already in the list, can happen if we unfold the properties field\n                    // open a form view, edit the property and then go back to the search view\n                    // the label of the property might have been changed\n                    existingSearchItem.description = `${definition.string} (${definitionRecordName})`;\n                    searchItemIds.add(existingSearchItem.id);\n                    continue;\n                }\n                const id = this.nextId++;\n                const newSearchItem = {\n                    id,\n                    type: \"field_property\",\n                    fieldName: searchItem.fieldName,\n                    propertyDomain: [definitionRecord, \"=\", definitionRecordId],\n                    propertyFieldDefinition: definition,\n                    propertyItemId: searchItem.id,\n                    description: definitionRecordName\n                        ? `${definition.string} (${definitionRecordName})`\n                        : definition.string,\n                    groupId: this.nextGroupId++,\n                };\n                if ([\"many2many\", \"tags\"].includes(definition.type)) {\n                    newSearchItem.operator = \"in\";\n                }\n                this.searchItems[id] = newSearchItem;\n                searchItemIds.add(id);\n            }\n        }\n\n        return this.getSearchItems((searchItem) => searchItemIds.has(searchItem.id));\n    }\n\n    //--------------------------------------------------------------------------\n    // Private methods\n    //--------------------------------------------------------------------------\n\n    /**\n     * Because it require a RPC to get the properties search views items,\n     * it's done lazily, only when we need them.\n     */\n    async fillSearchViewItemsProperty() {\n        if (!this.searchViewFields) {\n            return;\n        }\n\n        const fields = Object.values(this.searchViewFields);\n\n        for (const field of fields) {\n            if (field.type !== \"properties\") {\n                continue;\n            }\n\n            const result = await this._fetchPropertiesDefinition(this.resModel, field.name);\n\n            const searchItemsNames = Object.values(this.searchItems)\n                .filter((item) => item.isProperty && [\"groupBy\", \"dateGroupBy\"].includes(item.type))\n                .map((item) => item.fieldName);\n\n            for (const { definitionRecordId, definitionRecordName, definitions } of result) {\n                // some properties might have been deleted\n                const groupNames = definitions.map(\n                    (definition) => `group_by_${field.name}.${definition.name}`\n                );\n                Object.values(this.searchItems).forEach((searchItem) => {\n                    if (\n                        searchItem.isProperty &&\n                        searchItem.definitionRecordId === definitionRecordId &&\n                        [\"groupBy\", \"dateGroupBy\"].includes(searchItem.type) &&\n                        !groupNames.includes(searchItem.name)\n                    ) {\n                        // we can not just remove the element from the list because index are used as id\n                        // so we use a different type to hide it everywhere (until the user refresh his\n                        // browser and the item won't be created again)\n                        searchItem.type = \"group_by_property_deleted\";\n                    }\n                });\n\n                for (const definition of definitions) {\n                    // we need the definition of the \"field\" (fake field, property) to be\n                    // in searchViewFields to be able to have the type, it's description, etc\n                    // the name of the property is stored as \"<properties field name>.<property name>\"\n                    const fullName = `${field.name}.${definition.name}`;\n                    this.searchViewFields[fullName] = {\n                        name: fullName,\n                        readonly: false,\n                        relation: definition.comodel,\n                        required: false,\n                        searchable: false,\n                        selection: definition.selection,\n                        sortable: true,\n                        store: true,\n                        string: definition.string,\n                        type: definition.type,\n                        relatedPropertyField: field,\n                    };\n\n                    if (\n                        !searchItemsNames.includes(fullName) &&\n                        ![\"html\", \"separator\"].includes(definition.type)\n                    ) {\n                        const groupByItem = {\n                            description: definition.string,\n                            definitionRecordId,\n                            definitionRecordName,\n                            fieldName: fullName,\n                            fieldType: definition.type,\n                            isProperty: true,\n                            name: `group_by_${field.name}.${definition.name}`,\n                            propertyFieldName: field.name,\n                            type: [\"datetime\", \"date\"].includes(definition.type)\n                                ? \"dateGroupBy\"\n                                : \"groupBy\",\n                        };\n                        this._createGroupOfSearchItems([groupByItem]);\n                    }\n                }\n            }\n        }\n    }\n\n    /**\n     * Fetch the properties definitions.\n     *\n     * @param {string} definitionRecordModel\n     * @param {string} definitionRecordField\n     * @return {Object[]} A list of objects of the form\n     *      {\n     *          definitionRecordId: <id of the parent record>\n     *          definitionRecordName: <display name of the parent record>\n     *          definitions: <list of properties definitions>\n     *      }\n     */\n    async _fetchPropertiesDefinition(resModel, fieldName) {\n        const domain = [];\n        if (this.context.active_id) {\n            // assume the active id is the definition record\n            // and show only its properties\n            domain.push([\"id\", \"=\", this.context.active_id]);\n        }\n\n        const definitions = await this.fieldService.loadPropertyDefinitions(\n            resModel,\n            fieldName,\n            domain\n        );\n        const result = groupBy(Object.values(definitions), (definition) => definition.record_id);\n        return Object.entries(result).map(([recordId, definitions]) => ({\n            definitionRecordId: parseInt(recordId),\n            definitionRecordName: definitions[0]?.record_name,\n            definitions,\n        }));\n    }\n\n    /**\n     * Activate the default favorite (if any) or all default filters.\n     */\n    _activateDefaultSearchItems(defaultFavoriteId) {\n        if (defaultFavoriteId) {\n            // Activate default favorite\n            this.toggleSearchItem(defaultFavoriteId);\n        } else {\n            // Activate default filters\n            Object.values(this.searchItems)\n                .filter((f) => f.isDefault && f.type !== \"favorite\")\n                .sort((f1, f2) => (f1.defaultRank || 100) - (f2.defaultRank || 100))\n                .forEach((f) => {\n                    if (f.type === \"dateFilter\") {\n                        this.toggleDateFilter(f.id);\n                    } else if (f.type === \"dateGroupBy\") {\n                        this.toggleDateGroupBy(f.id);\n                    } else if (f.type === \"field\") {\n                        this.addAutoCompletionValues(f.id, f.defaultAutocompleteValue);\n                    } else {\n                        this.toggleSearchItem(f.id);\n                    }\n                });\n        }\n    }\n\n    _checkOrderByCountStatus() {\n        if (\n            this.orderByCount &&\n            !this.query.some((item) =>\n                [\"dateGroupBy\", \"groupBy\"].includes(this.searchItems[item.searchItemId].type)\n            )\n        ) {\n            this.orderByCount = false;\n        }\n    }\n\n    /**\n     * @param {string} sectionId\n     * @param {Object} result\n     */\n    _createCategoryTree(sectionId, result) {\n        const category = this.sections.get(sectionId);\n\n        let { error_msg, parent_field: parentField, values } = result;\n        if (error_msg) {\n            category.errorMsg = error_msg;\n            values = [];\n        }\n        if (category.hierarchize) {\n            category.parentField = parentField;\n        }\n        for (const value of values) {\n            category.values.set(\n                value.id,\n                Object.assign({}, value, {\n                    childrenIds: [],\n                    parentId: value[parentField] || false,\n                })\n            );\n        }\n        for (const value of values) {\n            const { parentId } = category.values.get(value.id);\n            if (parentId && category.values.has(parentId)) {\n                category.values.get(parentId).childrenIds.push(value.id);\n            }\n        }\n        // collect rootIds\n        category.rootIds = [false];\n        for (const value of values) {\n            const { parentId } = category.values.get(value.id);\n            if (!parentId) {\n                category.rootIds.push(value.id);\n            }\n        }\n        // Set active value from context\n        const valueIds = [false, ...values.map((val) => val.id)];\n        this._ensureCategoryValue(category, valueIds);\n    }\n\n    /**\n     * @param {string} sectionId\n     * @param {Object} result\n     */\n    _createFilterTree(sectionId, result) {\n        const filter = this.sections.get(sectionId);\n\n        let { error_msg, values } = result;\n        if (error_msg) {\n            filter.errorMsg = error_msg;\n            values = [];\n        }\n\n        // restore checked property\n        values.forEach((value) => {\n            const oldValue = filter.values.get(value.id);\n            value.checked = oldValue ? oldValue.checked : false;\n        });\n\n        filter.values = new Map();\n        const groupIds = [];\n        if (filter.groupBy) {\n            const groups = new Map();\n            for (const value of values) {\n                const groupId = value.group_id;\n                if (!groups.has(groupId)) {\n                    if (groupId) {\n                        groupIds.push(groupId);\n                    }\n                    groups.set(groupId, {\n                        id: groupId,\n                        name: value.group_name,\n                        values: new Map(),\n                        tooltip: value.group_tooltip,\n                        sequence: value.group_sequence,\n                        color_index: value.color_index,\n                    });\n                    // restore former checked state\n                    const oldGroup = filter.groups && filter.groups.get(groupId);\n                    groups.get(groupId).state = (oldGroup && oldGroup.state) || false;\n                }\n                groups.get(groupId).values.set(value.id, value);\n            }\n            filter.groups = groups;\n            filter.sortedGroupIds = sortBy(\n                groupIds,\n                (id) => groups.get(id).sequence || groups.get(id).name\n            );\n            for (const group of filter.groups.values()) {\n                for (const [valueId, value] of group.values) {\n                    filter.values.set(valueId, value);\n                }\n            }\n        } else {\n            for (const value of values) {\n                filter.values.set(value.id, value);\n            }\n        }\n    }\n\n    /**\n     * Add filters of type 'filter' determined by the key array dynamicFilters.\n     */\n    _createGroupOfDynamicFilters(dynamicFilters) {\n        const pregroup = dynamicFilters.map((filter) => ({\n            groupNumber: this.nextGroupNumber,\n            description: filter.description,\n            domain: filter.domain,\n            isDefault: \"is_default\" in filter ? filter.is_default : true,\n            type: \"filter\",\n        }));\n        this.nextGroupNumber++;\n        this._createGroupOfSearchItems(pregroup);\n    }\n\n    /**\n     * Add filters of type 'favorite' determined by the array this.favoriteFilters.\n     */\n    _createGroupOfFavorites(irFilters) {\n        let defaultFavoriteId = null;\n        irFilters.forEach((irFilter) => {\n            const favorite = this._irFilterToFavorite(irFilter);\n            this._createGroupOfSearchItems([favorite]);\n            if (favorite.isDefault) {\n                defaultFavoriteId = favorite.id;\n            }\n        });\n        return defaultFavoriteId;\n    }\n\n    /**\n     * Using a list (a 'pregroup') of 'prefilters', create new filters in `searchItems`\n     * for each prefilter. The new filters belong to a same new group.\n     */\n    _createGroupOfSearchItems(pregroup) {\n        pregroup.forEach((preSearchItem) => {\n            const searchItem = Object.assign(preSearchItem, {\n                groupId: this.nextGroupId,\n                id: this.nextId,\n            });\n            this.searchItems[this.nextId] = searchItem;\n            this.nextId++;\n        });\n        this.nextGroupId++;\n    }\n\n    /**\n     * Returns null or a copy of the provided filter with additional information\n     * used only outside of the control panel model, like in search bar or in the\n     * various menus. The value null is returned if the filter should not appear\n     * for some reason.\n     */\n    _enrichItem(searchItem) {\n        if (searchItem.type === \"field\" && searchItem.fieldType === \"properties\") {\n            return { ...searchItem };\n        }\n        const queryElements = this.query.filter(\n            (queryElem) => queryElem.searchItemId === searchItem.id\n        );\n        const isActive = Boolean(queryElements.length);\n        const enrichSearchItem = Object.assign({ isActive }, searchItem);\n        function _enrichOptions(options, selectedIds) {\n            return options.map((o) => {\n                const { description, id, groupNumber } = o;\n                const isActive = selectedIds.some((optionId) => optionId === id);\n                return { description, id, groupNumber, isActive };\n            });\n        }\n        switch (searchItem.type) {\n            case \"dateFilter\":\n                enrichSearchItem.options = _enrichOptions(\n                    getPeriodOptions(this.referenceMoment, searchItem.optionsParams),\n                    queryElements.map((queryElem) => queryElem.generatorId)\n                );\n                break;\n            case \"dateGroupBy\":\n                enrichSearchItem.options = _enrichOptions(\n                    this.intervalOptions,\n                    queryElements.map((queryElem) => queryElem.intervalId)\n                );\n                break;\n            case \"field\":\n            case \"field_property\":\n                enrichSearchItem.autocompleteValues = queryElements.map(\n                    (queryElem) => queryElem.autocompleteValue\n                );\n                break;\n        }\n        return enrichSearchItem;\n    }\n\n    /**\n     * Ensures that the active value of a category is one of its own\n     * existing values.\n     * @param {Category} category\n     * @param {number[]} valueIds\n     */\n    _ensureCategoryValue(category, valueIds) {\n        if (!valueIds.includes(category.activeValueId)) {\n            category.activeValueId = valueIds[0];\n        }\n    }\n\n    _extractSearchDefaultsFromGlobalContext() {\n        const searchDefaults = {};\n        const searchPanelDefaults = {};\n        for (const key in this.globalContext) {\n            const defaultValue = this.globalContext[key];\n            const searchDefaultMatch = /^search_default_(.*)$/.exec(key);\n            if (searchDefaultMatch) {\n                if (defaultValue) {\n                    searchDefaults[searchDefaultMatch[1]] = defaultValue;\n                }\n                delete this.globalContext[key];\n                continue;\n            }\n            const searchPanelDefaultMatch = /^searchpanel_default_(.*)$/.exec(key);\n            if (searchPanelDefaultMatch) {\n                searchPanelDefaults[searchPanelDefaultMatch[1]] = defaultValue;\n                delete this.globalContext[key];\n            }\n        }\n        return { searchDefaults, searchPanelDefaults };\n    }\n\n    /**\n     * Fetches values for each category at startup. At reload a category is\n     * only fetched if needed.\n     * @param {Category[]} categories\n     * @returns {Promise} resolved when all categories have been fetched\n     */\n    async _fetchCategories(categories) {\n        const filterDomain = this._getFilterDomain();\n        const searchDomain = this.searchDomain;\n        const categoriesLoadId = ++this.categoriesLoadId;\n        await Promise.all(\n            categories.map(async (category) => {\n                const result = await this.orm\n                    .cache({\n                        type: \"disk\",\n                        update: \"always\",\n                        callback: (result, hasChanged) => {\n                            if (!hasChanged || categoriesLoadId !== this.categoriesLoadId) {\n                                return;\n                            }\n                            this._createCategoryTree(category.id, result);\n                            this._reset();\n                            this.trigger(\"update\");\n                        },\n                    })\n                    .call(this.resModel, \"search_panel_select_range\", [category.fieldName], {\n                        category_domain: this._getCategoryDomain(category.id),\n                        context: this.globalContext,\n                        enable_counters: category.enableCounters,\n                        expand: category.expand,\n                        filter_domain: filterDomain,\n                        hierarchize: category.hierarchize,\n                        limit: category.limit,\n                        search_domain: searchDomain,\n                    });\n                this._createCategoryTree(category.id, result);\n            })\n        );\n    }\n\n    /**\n     * Fetches values for each filter. This is done at startup and at each\n     * reload if needed.\n     * @param {Filter[]} filters\n     * @returns {Promise} resolved when all filters have been fetched\n     */\n    async _fetchFilters(filters) {\n        const evalContext = {};\n        for (const category of this.categories) {\n            evalContext[category.fieldName] = category.activeValueId;\n        }\n        const categoryDomain = this._getCategoryDomain();\n        const searchDomain = this.searchDomain;\n        const filtersLoadId = ++this.filtersLoadId;\n        await Promise.all(\n            filters.map(async (filter) => {\n                const result = await this.orm\n                    .cache({\n                        type: \"disk\",\n                        update: \"always\",\n                        callback: (result, hasChanged) => {\n                            if (!hasChanged || filtersLoadId !== this.filtersLoadId) {\n                                return;\n                            }\n                            this._createFilterTree(filter.id, result);\n                            this._reset();\n                            this.trigger(\"update\");\n                        },\n                    })\n                    .call(this.resModel, \"search_panel_select_multi_range\", [filter.fieldName], {\n                        category_domain: categoryDomain,\n                        comodel_domain: new Domain(filter.domain).toList(evalContext),\n                        context: this.globalContext,\n                        enable_counters: filter.enableCounters,\n                        filter_domain: this._getFilterDomain(filter.id),\n                        expand: filter.expand,\n                        group_by: filter.groupBy || false,\n                        group_domain: this._getGroupDomain(filter),\n                        limit: filter.limit,\n                        search_domain: searchDomain,\n                    });\n                this._createFilterTree(filter.id, result);\n            })\n        );\n    }\n\n    /**\n     * Fetches values for the given categories and filters.\n     * @param {Category[]} categoriesToLoad\n     * @param {Filter[]} filtersToLoad\n     * @returns {Promise} resolved when all categories have been fetched\n     */\n    async _fetchSections(categoriesToLoad, filtersToLoad) {\n        await this._fetchCategories(categoriesToLoad);\n        await this._fetchFilters(filtersToLoad);\n        this.searchPanelInfo.loaded = true;\n    }\n\n    /**\n     * Computes and returns the domain based on the current active\n     * categories. If \"excludedCategoryId\" is provided, the category with\n     * that id is not taken into account in the domain computation.\n     * @param {string} [excludedCategoryId]\n     * @returns {Array[]}\n     */\n    _getCategoryDomain(excludedCategoryId) {\n        const domain = [];\n        for (const category of this.categories) {\n            if (category.id === excludedCategoryId || !category.activeValueId) {\n                continue;\n            }\n            const field = this.searchViewFields[category.fieldName];\n            const operator = field.type === \"many2one\" && category.parentField ? \"child_of\" : \"=\";\n            domain.push([category.fieldName, operator, category.activeValueId]);\n        }\n        return domain;\n    }\n\n    /**\n     * Construct a single context from the contexts of\n     * filters of type 'filter', 'favorite', and 'field'.\n     * @returns {Object}\n     */\n    _getContext() {\n        const groups = this._getGroups();\n        const contexts = [user.context];\n        for (const group of groups) {\n            for (const activeItem of group.activeItems) {\n                const context = this._getSearchItemContext(activeItem);\n                if (context) {\n                    contexts.push(context);\n                }\n            }\n        }\n        return makeContext(contexts);\n    }\n\n    /**\n     * Compute the string representation or the description of the current domain associated\n     * with a date filter starting from its corresponding query elements.\n     */\n    _getDateFilterDomain(dateFilter, generatorIds, key = \"domain\") {\n        const dateFilterRange = constructDateDomain(this.referenceMoment, dateFilter, generatorIds);\n        return dateFilterRange[key];\n    }\n\n    /**\n     * Returns which components are displayed in the current action. Components\n     * are opt-out, meaning that they will be displayed as long as a falsy\n     * value is not provided. With the search panel, the view type must also\n     * match the given (or default) search panel view types if the search model\n     * is instanciated in a view (this doesn't apply for any other action type).\n     * @private\n     * @param {Object} [display={}]\n     * @returns {{ controlPanel: Object | false, searchPanel: boolean, banner: boolean }}\n     */\n    _getDisplay(display = {}) {\n        const { viewTypes } = this.searchPanelInfo;\n        const { viewType } = this.env.config;\n        return {\n            controlPanel: \"controlPanel\" in display ? display.controlPanel : {},\n            searchPanel:\n                this.sections.size &&\n                (!viewType || viewTypes.includes(viewType)) &&\n                (\"searchPanel\" in display ? display.searchPanel : true),\n        };\n    }\n\n    /**\n     * Return a domain created by combinining appropriately (with an 'AND') the domains\n     * coming from the active groups of type 'filter', 'dateFilter', 'favorite', and 'field'.\n     * @param {Object} [params]\n     * @param {boolean} [params.raw=false]\n     * @param {boolean} [params.withSearchPanel=true]\n     * @param {boolean} [params.withGlobal=true]\n     * @returns {DomainListRepr | Domain} Domain instance if 'raw', else the evaluated list domain\n     */\n    _getDomain(params = {}) {\n        const withSearchPanel = \"withSearchPanel\" in params ? params.withSearchPanel : true;\n        const withGlobal = \"withGlobal\" in params ? params.withGlobal : true;\n\n        const groups = this._getGroups();\n        const domains = [];\n        if (withGlobal) {\n            domains.push(this.globalDomain);\n        }\n        for (const group of groups) {\n            const groupActiveItemDomains = [];\n            for (const activeItem of group.activeItems) {\n                const domain = this._getSearchItemDomain(activeItem);\n                if (domain) {\n                    groupActiveItemDomains.push(domain);\n                }\n            }\n            const groupDomain = Domain.or(groupActiveItemDomains);\n            domains.push(groupDomain);\n        }\n\n        // we need to manage (optional) facets, deactivateGroup, clearQuery,...\n\n        if (this.display.searchPanel && withSearchPanel) {\n            domains.push(this._getSearchPanelDomain());\n        }\n\n        const domain = Domain.and(domains);\n        return params.raw ? domain : domain.toList(this.domainEvalContext);\n    }\n\n    _getFacets() {\n        const facets = [];\n        const groups = this._getGroups();\n        for (const group of groups) {\n            const groupActiveItemDomains = [];\n            const values = [];\n            let title;\n            let type;\n            let tooltip;\n            for (const activeItem of group.activeItems) {\n                const domain = this._getSearchItemDomain(activeItem);\n                if (domain) {\n                    groupActiveItemDomains.push(domain);\n                }\n                const searchItem = this.searchItems[activeItem.searchItemId];\n                tooltip = searchItem.tooltip;\n                switch (searchItem.type) {\n                    case \"field_property\":\n                    case \"field\": {\n                        type = \"field\";\n                        title = searchItem.description;\n                        for (const autocompleteValue of activeItem.autocompleteValues) {\n                            values.push(autocompleteValue.label);\n                        }\n                        break;\n                    }\n                    case \"groupBy\": {\n                        type = \"groupBy\";\n                        values.push(searchItem.description);\n                        break;\n                    }\n                    case \"dateGroupBy\": {\n                        type = \"groupBy\";\n                        for (const intervalId of activeItem.intervalIds) {\n                            const { description } = INTERVAL_OPTIONS[intervalId];\n                            values.push(`${searchItem.description}: ${description}`);\n                        }\n                        break;\n                    }\n                    case \"dateFilter\": {\n                        type = \"filter\";\n                        const periodDescription = this._getDateFilterDomain(\n                            searchItem,\n                            activeItem.generatorIds,\n                            \"description\"\n                        );\n                        values.push(`${searchItem.description}: ${periodDescription}`);\n\n                        break;\n                    }\n                    default: {\n                        type = searchItem.type;\n                        values.push(searchItem.description);\n                    }\n                }\n            }\n            const facet = {\n                groupId: group.id,\n                type,\n                values,\n                separator: type === \"groupBy\" ? \">\" : _t(\"or\"),\n            };\n            if (type === \"field\") {\n                facet.title = title;\n            } else {\n                if (type === \"groupBy\" && this.orderByCount) {\n                    facet.icon =\n                        FACET_ICONS[this.orderByCount === \"Asc\" ? \"groupByAsc\" : \"groupByDesc\"];\n                } else {\n                    facet.icon = FACET_ICONS[type];\n                }\n                facet.color = FACET_COLORS[type];\n            }\n            if (tooltip) {\n                facet.tooltip = tooltip;\n            }\n            if (groupActiveItemDomains.length) {\n                facet.domain = Domain.or(groupActiveItemDomains).toString();\n            }\n            facets.push(facet);\n        }\n        const hasAGroupByFacet = facets.some((f) => f.type === \"groupBy\");\n        if (\n            !hasAGroupByFacet &&\n            !this.globalGroupBy.length &&\n            this.defaultGroupBy &&\n            this.env.config.viewType !== \"kanban\"\n        ) {\n            facets.unshift({\n                groupId: SPECIAL,\n                type: \"groupBy\",\n                values: this.defaultGroupBy.map((gb) => {\n                    const [fieldName, interval] = gb.split(\":\");\n                    const { string } = this.searchViewFields[fieldName];\n                    if (interval) {\n                        const { description } = INTERVAL_OPTIONS[interval];\n                        return `${string}:${description}`;\n                    }\n                    return string;\n                }),\n                separator: \">\",\n                icon: FACET_ICONS.groupBy,\n                color: FACET_COLORS.groupBy,\n            });\n        }\n        return facets;\n    }\n\n    /**\n     * Return the domain resulting from the combination of the autocomplete values\n     * of a search item of type 'field'.\n     */\n    _getFieldDomain(field, autocompleteValues) {\n        const domains = autocompleteValues.map(({ label, value, operator }) => {\n            let domain;\n            if (field.filterDomain) {\n                domain = new Domain(field.filterDomain).toList({\n                    self: label.trim(),\n                    raw_value: value,\n                });\n            } else if (field.type === \"field\") {\n                domain = [[field.fieldName, operator, value]];\n            } else if (field.type === \"field_property\") {\n                domain = [\n                    field.propertyDomain,\n                    [`${field.fieldName}.${field.propertyFieldDefinition.name}`, operator, value],\n                ];\n            }\n            return new Domain(domain);\n        });\n        return Domain.or(domains);\n    }\n\n    /**\n     * Computes and returns the domain based on the current checked\n     * filters. The values of a single filter are combined using a simple\n     * rule: checked values within a same group are combined with an \"OR\"\n     * operator (this is expressed as single condition using a list) and\n     * groups are combined with an \"AND\" operator (expressed by\n     * concatenation of conditions).\n     * If a filter has no group, its checked values are implicitely\n     * considered as forming a group (and grouped using an \"OR\").\n     * If excludedFilterId is provided, the filter with that id is not\n     * taken into account in the domain computation.\n     * @param {string} [excludedFilterId]\n     * @returns {Array[]}\n     */\n    _getFilterDomain(excludedFilterId) {\n        const domain = [];\n\n        function addCondition(fieldName, valueMap) {\n            const ids = [];\n            for (const [valueId, value] of valueMap) {\n                if (value.checked) {\n                    ids.push(valueId);\n                }\n            }\n            if (ids.length) {\n                domain.push([fieldName, \"in\", ids]);\n            }\n        }\n\n        for (const filter of this.filters) {\n            if (filter.id === excludedFilterId) {\n                continue;\n            }\n            const { fieldName, groups, values } = filter;\n            if (groups) {\n                for (const group of groups.values()) {\n                    addCondition(fieldName, group.values);\n                }\n            } else {\n                addCondition(fieldName, values);\n            }\n        }\n        return domain;\n    }\n\n    /**\n     * Return the concatenation of groupBys comming from the active filters of\n     * type 'favorite' and 'groupBy'.\n     * The result respects the appropriate logic: the groupBys\n     * coming from an active favorite (if any) come first, then come the\n     * groupBys comming from the active filters of type 'groupBy' in the order\n     * defined in this.query. If no groupBys are found, one tries to\n     * find some groupBys in this.globalGroupBy or this.defaultGroupBy.\n     * @param {Object} [options={}]\n     * @param {boolean} [options.fallbackOnDefault=true]\n     * @returns {string[]}\n     */\n    _getGroupBy(options = {}) {\n        const fallbackOnDefault = \"fallbackOnDefault\" in options ? options.fallbackOnDefault : true;\n        const groups = this._getGroups();\n        const groupBys = [];\n        for (const group of groups) {\n            for (const activeItem of group.activeItems) {\n                const activeItemGroupBys = this._getSearchItemGroupBys(activeItem);\n                if (activeItemGroupBys) {\n                    groupBys.push(...activeItemGroupBys);\n                }\n            }\n        }\n        const groupBy = groupBys.length\n            ? groupBys\n            : this.globalGroupBy.length\n            ? this.globalGroupBy.slice()\n            : (fallbackOnDefault && this.defaultGroupBy?.slice()) || [];\n        return typeof groupBy === \"string\" ? [groupBy] : groupBy;\n    }\n\n    /**\n     * Returns a domain or an object of domains used to complement\n     * the filter domains to accurately describe the constrains on\n     * records when computing record counts associated to the filter\n     * values (if a groupBy is provided). The idea is that the checked\n     * values within a group should not impact the counts for the other\n     * values in the same group.\n     * @param {Filter} filter\n     * @returns {Object<string, Array[]> | Array[] | null}\n     */\n    _getGroupDomain(filter) {\n        const { fieldName, groups, enableCounters } = filter;\n        const { type: fieldType } = this.searchViewFields[fieldName];\n\n        if (!enableCounters || !groups) {\n            return {\n                many2one: [],\n                many2many: {},\n            }[fieldType];\n        }\n        let groupDomain = null;\n        if (fieldType === \"many2one\") {\n            for (const group of groups.values()) {\n                const valueIds = [];\n                let active = false;\n                for (const [valueId, value] of group.values) {\n                    const { checked } = value;\n                    valueIds.push(valueId);\n                    if (checked) {\n                        active = true;\n                    }\n                }\n                if (active) {\n                    if (groupDomain) {\n                        groupDomain = [[0, \"=\", 1]];\n                        break;\n                    } else {\n                        groupDomain = [[fieldName, \"in\", valueIds]];\n                    }\n                }\n            }\n        } else if (fieldType === \"many2many\") {\n            const checkedValueIds = new Map();\n            groups.forEach(({ values }, groupId) => {\n                values.forEach(({ checked }, valueId) => {\n                    if (checked) {\n                        if (!checkedValueIds.has(groupId)) {\n                            checkedValueIds.set(groupId, []);\n                        }\n                        checkedValueIds.get(groupId).push(valueId);\n                    }\n                });\n            });\n            groupDomain = {};\n            for (const [gId, ids] of checkedValueIds.entries()) {\n                for (const groupId of groups.keys()) {\n                    if (gId !== groupId) {\n                        const key = JSON.stringify(groupId);\n                        if (!groupDomain[key]) {\n                            groupDomain[key] = [];\n                        }\n                        groupDomain[key].push([fieldName, \"in\", ids]);\n                    }\n                }\n            }\n        }\n        return groupDomain;\n    }\n\n    /**\n     * Reconstruct the (active) groups from the query elements.\n     * @returns {Object[]}\n     */\n    _getGroups() {\n        const preGroups = [];\n        for (const queryElem of this.query) {\n            const { searchItemId } = queryElem;\n            const { groupId } = this.searchItems[searchItemId];\n            let preGroup = preGroups.find((group) => group.id === groupId);\n            if (!preGroup) {\n                preGroup = { id: groupId, queryElements: [] };\n                preGroups.push(preGroup);\n            }\n            preGroup.queryElements.push(queryElem);\n        }\n        const groups = [];\n        for (const preGroup of preGroups) {\n            const { queryElements, id } = preGroup;\n            const activeItems = [];\n            for (const queryElem of queryElements) {\n                const { searchItemId } = queryElem;\n                let activeItem = activeItems.find(({ searchItemId: id }) => id === searchItemId);\n                if (\"generatorId\" in queryElem) {\n                    if (!activeItem) {\n                        activeItem = { searchItemId, generatorIds: [] };\n                        activeItems.push(activeItem);\n                    }\n                    activeItem.generatorIds.push(queryElem.generatorId);\n                } else if (\"intervalId\" in queryElem) {\n                    if (!activeItem) {\n                        activeItem = { searchItemId, intervalIds: [] };\n                        activeItems.push(activeItem);\n                    }\n                    activeItem.intervalIds.push(queryElem.intervalId);\n                } else if (\"autocompleteValue\" in queryElem) {\n                    if (!activeItem) {\n                        activeItem = { searchItemId, autocompleteValues: [] };\n                        activeItems.push(activeItem);\n                    }\n                    activeItem.autocompleteValues.push(queryElem.autocompleteValue);\n                } else {\n                    if (!activeItem) {\n                        activeItem = { searchItemId };\n                        activeItems.push(activeItem);\n                    }\n                }\n            }\n            for (const activeItem of activeItems) {\n                if (\"intervalIds\" in activeItem) {\n                    activeItem.intervalIds.sort((g1, g2) => rankInterval(g1) - rankInterval(g2));\n                }\n            }\n            groups.push({ id, activeItems });\n        }\n        return groups;\n    }\n\n    /**\n     *\n     * @private\n     * @param {Object} [params={}]\n     * @returns {{ preFavorite: Object, irFilter: Object }}\n     */\n    _getIrFilterDescription(params = {}) {\n        const { description, isDefault, isShared, embeddedActionId } = params;\n        const fns = this.env.__getContext__.callbacks;\n        const localContext = Object.assign({}, ...fns.map((fn) => fn()));\n        const gs = this.env.__getOrderBy__.callbacks;\n        let localOrderBy;\n        if (gs.length) {\n            localOrderBy = gs.flatMap((g) => g());\n        }\n        const context = makeContext([this._getContext(), localContext]);\n        const userContext = user.context;\n        for (const key in context) {\n            if (key in userContext || /^search(panel)?_default_/.test(key)) {\n                // clean search defaults and user context keys\n                delete context[key];\n            }\n        }\n        const domain = this._getDomain({ raw: true, withGlobal: false }).toString();\n        const groupBys = this._getGroupBy();\n        const orderBy = localOrderBy || this._getOrderBy();\n        const userIds = isShared ? [] : [user.userId];\n\n        const preFavorite = {\n            description,\n            isDefault,\n            domain,\n            context,\n            groupBys,\n            orderBy,\n            userIds,\n        };\n        const irFilter = {\n            name: description,\n            action_id: this.env.config.actionId,\n            model_id: this.resModel,\n            domain,\n            embedded_action_id: embeddedActionId,\n            embedded_parent_res_id: this.globalContext.active_id || false,\n            is_default: isDefault,\n            sort: JSON.stringify(orderBy.map((o) => `${o.name}${o.asc === false ? \" desc\" : \"\"}`)),\n            user_ids: userIds,\n            context: { group_by: groupBys, ...context },\n        };\n\n        return { preFavorite, irFilter };\n    }\n\n    /**\n     * @returns {OrderTerm[]}\n     */\n    _getOrderBy() {\n        const groups = this._getGroups();\n        const orderBy = [];\n        if (this.groupBy.length && this.orderByCount) {\n            orderBy.push({ name: \"__count\", asc: this.orderByCount === \"Asc\" });\n        }\n        for (const group of groups) {\n            for (const activeItem of group.activeItems) {\n                const { searchItemId } = activeItem;\n                const searchItem = this.searchItems[searchItemId];\n                if (searchItem.type === \"favorite\") {\n                    orderBy.push(...searchItem.orderBy);\n                }\n            }\n        }\n        return orderBy.length ? orderBy : this.globalOrderBy;\n    }\n\n    /**\n     * Return the context of the provided (active) filter.\n     */\n    _getSearchItemContext(activeItem) {\n        const { searchItemId } = activeItem;\n        const searchItem = this.searchItems[searchItemId];\n        switch (searchItem.type) {\n            case \"field\": {\n                // for <field> nodes, a dynamic context (like context=\"{'field1': self}\")\n                // should set {'field1': [value1, value2]} in the context\n                let context = {};\n                if (searchItem.context) {\n                    const self = activeItem.autocompleteValues.map(\n                        (autocompleValue) => autocompleValue.value\n                    );\n                    context = evaluateExpr(searchItem.context, { self });\n                    if (typeof context !== \"object\") {\n                        throw new Error(\n                            _t(\"Failed to evaluate the context: %(context)s.\", {\n                                context: searchItem.context,\n                            })\n                        );\n                    }\n                }\n                // the following code aims to remodel this:\n                // https://github.com/odoo/odoo/blob/12.0/addons/web/static/src/js/views/search/search_inputs.js#L498\n                // this is required for the helpdesk tour to pass\n                // this seems weird to only do that for m2o fields, but a test fails if\n                // we do it for other fields (my guess being that the test should simply\n                // be adapted)\n                if (searchItem.isDefault && searchItem.fieldType === \"many2one\") {\n                    context[`default_${searchItem.fieldName}`] =\n                        searchItem.defaultAutocompleteValue.value;\n                }\n                return context;\n            }\n            case \"favorite\":\n            case \"filter\": {\n                //Return a deep copy of the filter/favorite to avoid the view to modify the context\n                return makeContext([searchItem.context && deepCopy(searchItem.context)]);\n            }\n            default: {\n                return null;\n            }\n        }\n    }\n\n    /**\n     * Return the domain of the provided filter.\n     */\n    _getSearchItemDomain(activeItem) {\n        const { searchItemId } = activeItem;\n        const searchItem = this.searchItems[searchItemId];\n        switch (searchItem.type) {\n            case \"field_property\":\n            case \"field\": {\n                return this._getFieldDomain(searchItem, activeItem.autocompleteValues);\n            }\n            case \"dateFilter\": {\n                return this._getDateFilterDomain(searchItem, activeItem.generatorIds);\n            }\n            case \"filter\":\n            case \"favorite\": {\n                return searchItem.domain;\n            }\n            default: {\n                return null;\n            }\n        }\n    }\n\n    _getSearchItemGroupBys(activeItem) {\n        const { searchItemId } = activeItem;\n        const searchItem = this.searchItems[searchItemId];\n        switch (searchItem.type) {\n            case \"dateGroupBy\": {\n                const { fieldName } = searchItem;\n                return activeItem.intervalIds.map((intervalId) => `${fieldName}:${intervalId}`);\n            }\n            case \"groupBy\": {\n                return [searchItem.fieldName];\n            }\n            case \"favorite\": {\n                return searchItem.groupBys;\n            }\n            default: {\n                return null;\n            }\n        }\n    }\n\n    /**\n     * Starting from a date filter id, returns the array of option ids currently selected\n     * for the corresponding date filter.\n     */\n    _getSelectedGeneratorIds(dateFilterId) {\n        const selectedOptionIds = [];\n        for (const queryElem of this.query) {\n            if (queryElem.searchItemId === dateFilterId && \"generatorId\" in queryElem) {\n                selectedOptionIds.push(queryElem.generatorId);\n            }\n        }\n        return selectedOptionIds;\n    }\n\n    /**\n     * @returns {Domain}\n     */\n    _getSearchPanelDomain() {\n        return Domain.and([this._getCategoryDomain(), this._getFilterDomain()]);\n    }\n\n    /**\n     * @param {Object} state\n     */\n    _importState(state) {\n        execute(arraytoMap, state, this);\n    }\n\n    /**\n     * @param {Object} irFilter\n     */\n    _irFilterToFavorite(irFilter) {\n        const userIds = irFilter.user_ids;\n        const groupNumber = userIds.length === 1 ? FAVORITE_PRIVATE_GROUP : FAVORITE_SHARED_GROUP;\n        const context = evaluateExpr(irFilter.context, user.context);\n        let groupBys = [];\n        if (context.group_by) {\n            groupBys = context.group_by;\n            delete context.group_by;\n        }\n        let sort;\n        try {\n            sort = JSON.parse(irFilter.sort);\n        } catch (err) {\n            if (err instanceof SyntaxError) {\n                sort = [];\n            } else {\n                throw err;\n            }\n        }\n        const orderBy = sort.map((order) => {\n            let fieldName;\n            let asc;\n            const sqlNotation = order.split(\" \");\n            if (sqlNotation.length > 1) {\n                // regex: \\fieldName (asc|desc)?\\\n                fieldName = sqlNotation[0];\n                asc = sqlNotation[1] === \"asc\";\n            } else {\n                // legacy notation -- regex: \\-?fieldName\\\n                fieldName = order[0] === \"-\" ? order.slice(1) : order;\n                asc = order[0] === \"-\" ? false : true;\n            }\n            return {\n                asc: asc,\n                name: fieldName,\n            };\n        });\n        const favorite = {\n            context,\n            description: irFilter.name,\n            domain: irFilter.domain,\n            groupBys,\n            groupNumber,\n            orderBy,\n            removable: true,\n            serverSideId: irFilter.id,\n            type: \"favorite\",\n            userIds,\n        };\n        if (irFilter.is_default) {\n            favorite.isDefault = irFilter.is_default;\n        }\n        return favorite;\n    }\n\n    async _notify() {\n        if (this.blockNotification) {\n            return;\n        }\n\n        this._reset();\n\n        await this._reloadSections();\n\n        this.trigger(\"update\");\n    }\n\n    /**\n     * Reconciliate the search items with the ir.filters.\n     * @private\n     */\n    _reconciliateFavorites() {\n        const irFilters = this.irFilters || [];\n        const mapping = Object.fromEntries(irFilters.map((i) => [i.id, i]));\n        for (const item of Object.values(this.searchItems)) {\n            if (item.type !== \"favorite\") {\n                continue;\n            }\n            const irFilter = mapping[item.serverSideId];\n            if (irFilter) {\n                Object.assign(item, this._irFilterToFavorite(irFilter));\n                delete mapping[item.serverSideId];\n            } else {\n                const queryIndex = this.query.findIndex((q) => q.searchItemId === item.id);\n                if (queryIndex !== -1) {\n                    this.query.splice(queryIndex, 1);\n                }\n                delete this.searchItems[item.id];\n            }\n        }\n        this._createGroupOfFavorites(Object.values(mapping));\n    }\n\n    /**\n     * Updates the search domain and reloads sections if:\n     * - the current search domain is different from the previous, or...\n     * - a `shouldReload` flag has been set to true on the searchPanelInfo.\n     * The latter means that the search domain has been modified while the\n     * search panel was not displayed (and thus not reloaded) and the reload\n     * should occur as soon as the search panel is visible again.\n     * @private\n     * @returns {Promise<void>}\n     */\n    async _reloadSections() {\n        this.blockNotification = true;\n\n        // Check whether the search domain changed\n        const searchDomain = this._getDomain({ withSearchPanel: false });\n        const searchDomainChanged =\n            this.searchPanelInfo.shouldReload ||\n            JSON.stringify(this.searchDomain) !== JSON.stringify(searchDomain);\n        this.searchDomain = searchDomain;\n\n        // Check whether categories/filters will force a reload of the sections\n        const toFetch = (section) =>\n            section.enableCounters || (searchDomainChanged && !section.expand);\n        const categoriesToFetch = this.categories.filter(toFetch);\n        const filtersToFetch = this.filters.filter(toFetch);\n\n        if (searchDomainChanged || Boolean(categoriesToFetch.length + filtersToFetch.length)) {\n            if (this.display.searchPanel) {\n                this.sectionsPromise = this._fetchSections(categoriesToFetch, filtersToFetch);\n                if (this._shouldWaitForData(searchDomainChanged)) {\n                    await this.sectionsPromise;\n                }\n            }\n            // If no current search panel: will try to reload on next model update\n            this.searchPanelInfo.shouldReload = !this.display.searchPanel;\n        }\n\n        this.blockNotification = false;\n    }\n\n    _reset() {\n        this._context = null;\n        this._domain = null;\n        this._groupBy = null;\n        this._orderBy = null;\n    }\n\n    /**\n     * Returns whether the query informations should be considered as ready\n     * before or after having (re-)fetched the sections data.\n     * @param {boolean} searchDomainChanged\n     * @returns {boolean}\n     */\n    _shouldWaitForData(searchDomainChanged) {\n        if (this.categories.length && this.filters.some((filter) => filter.domain !== \"[]\")) {\n            // Selected category value might affect the filter values\n            return true;\n        }\n        if (!this.searchDomain.length) {\n            // No search domain -> no need to check for expand\n            return false;\n        }\n        return [...this.sections.values()].some(\n            (section) => !section.expand && searchDomainChanged\n        );\n    }\n\n    /**\n     * Legacy compatibility: the imported state of a legacy search panel model\n     * extension doesn't include the arch information, i.e. the class name and\n     * view types. We have to extract those if they are not given.\n     * @param {Object} searchViewDescription\n     * @param {Object} searchViewFields\n     */\n    __legacyParseSearchPanelArchAnyway(searchViewDescription, searchViewFields) {\n        if (this.searchPanelInfo) {\n            return;\n        }\n\n        const parser = new SearchArchParser(searchViewDescription, searchViewFields);\n        const { searchPanelInfo } = parser.parse();\n\n        this.searchPanelInfo = { ...searchPanelInfo, loaded: false, shouldReload: false };\n    }\n}\n", "import { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { useBus } from \"@web/core/utils/hooks\";\n\nimport {\n    Component,\n    onMounted,\n    onWillStart,\n    onWillUpdateProps,\n    reactive,\n    useEffect,\n    useRef,\n    useState,\n} from \"@odoo/owl\";\nimport { browser } from \"@web/core/browser/browser\";\nimport { exprToBoolean } from \"@web/core/utils/strings\";\nimport { useSetupAction } from \"@web/search/action_hook\";\n\n//-------------------------------------------------------------------------\n// Helpers\n//-------------------------------------------------------------------------\n\nconst isFilter = (s) => s.type === \"filter\";\nconst isActiveCategory = (s) => s.type === \"category\" && s.activeValueId;\n\n/**\n * @param {Map<string | false, Object>} values\n * @returns {Object[]}\n */\nconst nameOfCheckedValues = (values) => {\n    const names = [];\n    for (const [, value] of values) {\n        if (value.checked) {\n            names.push(value.display_name);\n        }\n    }\n    return names;\n};\n\n/**\n * Search panel\n *\n * Represent an extension of the search interface located on the left side of\n * the view. It is divided in sections defined in a \"<searchpanel>\" node located\n * inside of a \"<search>\" arch. Each section is represented by a list of different\n * values (categories or ungrouped filters) or groups of values (grouped filters).\n * Its state is directly affected by its model (@see SearchModel).\n */\nexport class SearchPanel extends Component {\n    static template = \"web.SearchPanel\";\n    static props = {};\n    static components = {\n        Dropdown,\n    };\n    static subTemplates = {\n        section: \"web.SearchPanel.Section\",\n        category: \"web.SearchPanel.Category\",\n        filtersGroup: \"web.SearchPanel.FiltersGroup\",\n    };\n\n    setup() {\n        this.keyExpandSidebar = `search_panel_expanded,${this.env.config.viewId},${this.env.config.actionId}`;\n        this.state = useState({\n            active: {},\n            expanded: {},\n            sidebarExpanded: true,\n        });\n        this.hasImportedState = false;\n        this.root = useRef(\"root\");\n        this.scrollTop = 0;\n        this.dropdownStates = {};\n        this.width = \"10px\";\n\n        this.importState(this.env.searchPanelState);\n        const sidebarExpandedPreference = browser.localStorage.getItem(this.keyExpandSidebar);\n        if (sidebarExpandedPreference !== null) {\n            this.state.sidebarExpanded = exprToBoolean(sidebarExpandedPreference);\n        }\n\n        useBus(this.env.searchModel, \"update\", async () => {\n            await this.env.searchModel.sectionsPromise;\n            this.updateActiveValues();\n            this.render();\n        });\n\n        useEffect(\n            (el) => {\n                if (el && this.hasImportedState) {\n                    el.style[\"min-width\"] = this.width;\n                    el.scroll({ top: this.scrollTop });\n                }\n            },\n            () => [this.root.el]\n        );\n\n        useSetupAction({\n            getGlobalState: () => ({\n                searchPanel: this.exportState(),\n            }),\n        });\n\n        onWillStart(async () => {\n            await this.env.searchModel.sectionsPromise;\n            this.expandDefaultValue();\n            this.expandValues();\n            this.updateActiveValues();\n        });\n\n        onWillUpdateProps(async () => {\n            await this.env.searchModel.sectionsPromise;\n            this.updateActiveValues();\n        });\n\n        onMounted(() => {\n            this.updateGroupHeadersChecked();\n        });\n    }\n\n    //---------------------------------------------------------------------\n    // Getters\n    //---------------------------------------------------------------------\n\n    get sections() {\n        return this.env.searchModel.getSections((s) => !s.empty);\n    }\n\n    //---------------------------------------------------------------------\n    // Public\n    //---------------------------------------------------------------------\n\n    exportState() {\n        const exported = {\n            expanded: this.state.expanded,\n            scrollTop: this.root.el?.scrollTop || 0,\n            sidebarExpanded: this.state.sidebarExpanded,\n            width: this.width,\n        };\n        return JSON.stringify(exported);\n    }\n\n    importState(state) {\n        this.hasImportedState = Boolean(state);\n        if (this.hasImportedState) {\n            this.state.expanded = state.expanded;\n            this.scrollTop = state.scrollTop;\n            this.state.sidebarExpanded = state.sidebarExpanded;\n            this.width = state.width;\n        }\n    }\n\n    //---------------------------------------------------------------------\n    // Protected\n    //---------------------------------------------------------------------\n\n    getDropdownState(sectionId) {\n        if (!this.dropdownStates[sectionId]) {\n            const state = reactive({\n                isOpen: false,\n                open: () => (state.isOpen = true),\n                close: () => (state.isOpen = false),\n            });\n            this.dropdownStates[sectionId] = reactive(state);\n        }\n        return this.dropdownStates[sectionId];\n    }\n\n    /**\n     * Expands category values holding the default value of a category.\n     */\n    expandDefaultValue() {\n        if (this.hasImportedState) {\n            return;\n        }\n        const categories = this.env.searchModel.getSections((s) => s.type === \"category\");\n        for (const category of categories) {\n            this.state.expanded[category.id] = {};\n            if (category.activeValueId) {\n                const ancestorIds = this.getAncestorValueIds(category, category.activeValueId);\n                for (const ancestorId of ancestorIds) {\n                    this.state.expanded[category.id][ancestorId] = true;\n                }\n            }\n        }\n    }\n\n    expandValues() {\n        if (this.hasImportedState) {\n            return;\n        }\n        const categories = this.env.searchModel.getSections((s) => s.type === \"category\");\n        for (const category of categories) {\n            if (category.depth === 0) {\n                continue;\n            }\n\n            this.state.expanded[category.id] ||= {};\n            const expand = (id, level) => {\n                if (!level) {\n                    return;\n                }\n                this.state.expanded[category.id][id] = true;\n                const { childrenIds } = category.values.get(id);\n                level -= 1;\n                for (const childId of childrenIds) {\n                    expand(childId, level);\n                }\n            };\n\n            for (const rootId of category.rootIds) {\n                expand(rootId, category.depth);\n            }\n        }\n    }\n\n    /**\n     * @param {Object} category\n     * @param {number} categoryValueId\n     * @returns {number[]} list of ids of the ancestors of the given value in\n     *   the given category.\n     */\n    getAncestorValueIds(category, categoryValueId) {\n        const { parentId } = category.values.get(categoryValueId);\n        return parentId ? [...this.getAncestorValueIds(category, parentId), parentId] : [];\n    }\n\n    /**\n     * Returns a formatted version of the active categories to populate\n     * the selection banner of the control panel summary.\n     * @returns {Object[]}\n     */\n    getCategorySelection() {\n        const activeCategories = this.env.searchModel.getSections(isActiveCategory);\n        const selection = [];\n        for (const category of activeCategories) {\n            const parentIds = this.getAncestorValueIds(category, category.activeValueId);\n            const orderedCategoryNames = [...parentIds, category.activeValueId].map(\n                (valueId) => category.values.get(valueId).display_name\n            );\n            selection.push({\n                values: orderedCategoryNames,\n                icon: category.icon,\n                color: category.color,\n            });\n        }\n        return selection;\n    }\n\n    /**\n     * Returns a formatted version of the active filters to populate\n     * the selection banner of the control panel summary.\n     * @returns {Object[]}\n     */\n    getFilterSelection() {\n        const filters = this.env.searchModel.getSections(isFilter);\n        const selection = [];\n        for (const { groups, values, icon, color } of filters) {\n            let filterValues;\n            if (groups) {\n                filterValues = Object.keys(groups)\n                    .map((groupId) => nameOfCheckedValues(groups[groupId].values))\n                    .flat();\n            } else if (values) {\n                filterValues = nameOfCheckedValues(values);\n            }\n            if (filterValues.length) {\n                selection.push({ values: filterValues, icon, color });\n            }\n        }\n        return selection;\n    }\n\n    /**\n     * Checks if the section matching the provided id has at least one active selection.\n     * If no id is provided, checks if at least one section has an active selection.\n     * @param {Number} sectionId\n     */\n    hasSelection(sectionId = 0) {\n        if (sectionId) {\n            const sectionState = this.state.active[sectionId];\n            if (sectionState instanceof Object) {\n                return Object.values(sectionState).some((val) => val);\n            }\n            return Boolean(sectionState);\n        }\n        return Object.keys(this.state.active).some((key) => this.hasSelection(key));\n    }\n\n    /**\n     * Clears all active selection in the section which id was provided.\n     * If no id is provided, clears the selection of all sections.\n     * @param {Number} sectionId\n     */\n    clearSelection(sectionId = 0) {\n        const sectionIds = sectionId ? [sectionId] : Object.keys(this.state.active).map(Number);\n        this.env.searchModel.clearSections(sectionIds);\n    }\n\n    /**\n     * Prevent unnecessary calls to the model by ensuring a different category\n     * is clicked.\n     * @param {Object} category\n     * @param {Object} value\n     */\n    async toggleCategory(category, value) {\n        if (value.childrenIds.length) {\n            const categoryState = this.state.expanded[category.id];\n            if (categoryState[value.id] && category.activeValueId === value.id) {\n                delete categoryState[value.id];\n            } else {\n                categoryState[value.id] = true;\n            }\n        } else {\n            this.getDropdownState(category.id).close();\n        }\n        if (category.activeValueId !== value.id) {\n            this.env.searchModel.toggleCategoryValue(category.id, value.id);\n        }\n    }\n\n    toggleSidebar() {\n        this.state.sidebarExpanded = !this.state.sidebarExpanded;\n        browser.localStorage.setItem(this.keyExpandSidebar, this.state.sidebarExpanded);\n    }\n\n    /**\n     * @param {number} filterId\n     * @param {{ values: Map<Object> }} group\n     */\n    toggleFilterGroup(filterId, { values }) {\n        const valueIds = [];\n        const checked = [...values.values()].every(\n            (value) => this.state.active[filterId][value.id]\n        );\n        values.forEach(({ id }) => {\n            valueIds.push(id);\n            this.state.active[filterId][id] = !checked;\n        });\n        this.env.searchModel.toggleFilterValues(filterId, valueIds, !checked);\n    }\n\n    /**\n     * @param {number} filterId\n     * @param {Object} [group]\n     * @param {number} valueId\n     * @param {MouseEvent} ev\n     */\n    toggleFilterValue(filterId, valueId, { currentTarget }) {\n        this.state.active[filterId][valueId] = currentTarget.checked;\n        this.updateGroupHeadersChecked();\n        this.env.searchModel.toggleFilterValues(filterId, [valueId]);\n    }\n\n    updateActiveValues() {\n        if (this.sections.length === 0) {\n            this.state.sidebarExpanded = false;\n        }\n        for (const section of this.sections) {\n            if (section.type === \"category\") {\n                this.state.active[section.id] = section.activeValueId;\n            } else {\n                this.state.active[section.id] = {};\n                if (section.groups) {\n                    for (const group of section.groups.values()) {\n                        for (const value of group.values.values()) {\n                            this.state.active[section.id][value.id] = value.checked;\n                        }\n                    }\n                }\n                if (section && section.values) {\n                    for (const value of section.values.values()) {\n                        this.state.active[section.id][value.id] = value.checked;\n                    }\n                }\n            }\n        }\n    }\n\n    /**\n     * Updates the \"checked\" or \"indeterminate\" state of each of the group\n     * headers according to the state of their values.\n     */\n    updateGroupHeadersChecked() {\n        const groups = document.querySelectorAll(\".o_search_panel_filter_group\");\n        for (const group of groups) {\n            const header = group.querySelector(\":scope .o_search_panel_group_header input\");\n            const vals = [...group.querySelectorAll(\":scope .o_search_panel_filter_value input\")];\n            header.checked = false;\n            header.indeterminate = false;\n            if (vals.every((v) => v.checked)) {\n                header.checked = true;\n            } else if (vals.some((v) => v.checked)) {\n                header.indeterminate = true;\n            }\n        }\n    }\n\n    /**\n     * Handles the resize feature on the sidebar\n     *\n     * @private\n     * @param {PointerEvent} ev\n     */\n    _onStartResize(ev) {\n        // Only triggered by left mouse button\n        if (ev.button !== 0) {\n            return;\n        }\n\n        const initialX = ev.pageX;\n        const initialWidth = this.root.el.offsetWidth;\n        const resizeStoppingEvents = [\"keydown\", \"pointerdown\", \"pointerup\"];\n\n        // Pointermove event : resize header\n        const resizePanel = (ev) => {\n            ev.preventDefault();\n            ev.stopPropagation();\n            const maxWidth = Math.max(0.5 * window.innerWidth, initialWidth);\n            const delta = ev.pageX - initialX;\n            const newWidth = Math.min(maxWidth, Math.max(10, initialWidth + delta));\n            this.width = `${newWidth}px`;\n            this.root.el.style[\"min-width\"] = this.width;\n        };\n        document.addEventListener(\"pointermove\", resizePanel, true);\n\n        // Pointer or keyboard events : stop resize\n        const stopResize = (ev) => {\n            // Ignores the initial 'left mouse button down' event in order\n            // to not instantly remove the listener\n            if (ev.type === \"pointerdown\" && ev.button === 0) {\n                return;\n            }\n            ev.preventDefault();\n            ev.stopPropagation();\n\n            document.removeEventListener(\"pointermove\", resizePanel, true);\n            resizeStoppingEvents.forEach((stoppingEvent) => {\n                document.removeEventListener(stoppingEvent, stopResize, true);\n            });\n            // we remove the focus to make sure that the there is no focus inside\n            // the panel. If that is the case, there is some css to darken the whole\n            // thead, and it looks quite weird with the small css hover effect.\n            document.activeElement.blur();\n        };\n        // We have to listen to several events to properly stop the resizing function. Those are:\n        // - pointerdown (e.g. pressing right click)\n        // - pointerup : logical flow of the resizing feature (drag & drop)\n        // - keydown : (e.g. pressing 'Alt' + 'Tab' or 'Windows' key)\n        resizeStoppingEvents.forEach((stoppingEvent) => {\n            document.addEventListener(stoppingEvent, stopResize, true);\n        });\n    }\n}\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { Domain } from \"@web/core/domain\";\nimport { serializeDate, serializeDateTime } from \"@web/core/l10n/dates\";\nimport { localization } from \"@web/core/l10n/localization\";\nimport { clamp } from \"@web/core/utils/numbers\";\nimport { pick } from \"@web/core/utils/objects\";\n\nexport const QUARTERS = {\n    1: { description: _t(\"Q1\"), coveredMonths: [1, 2, 3] },\n    2: { description: _t(\"Q2\"), coveredMonths: [4, 5, 6] },\n    3: { description: _t(\"Q3\"), coveredMonths: [7, 8, 9] },\n    4: { description: _t(\"Q4\"), coveredMonths: [10, 11, 12] },\n};\n\nexport const QUARTER_OPTIONS = {\n    fourth_quarter: {\n        id: \"fourth_quarter\",\n        groupNumber: 1,\n        description: QUARTERS[4].description,\n        setParam: { quarter: 4 },\n        granularity: \"quarter\",\n    },\n    third_quarter: {\n        id: \"third_quarter\",\n        groupNumber: 1,\n        description: QUARTERS[3].description,\n        setParam: { quarter: 3 },\n        granularity: \"quarter\",\n    },\n    second_quarter: {\n        id: \"second_quarter\",\n        groupNumber: 1,\n        description: QUARTERS[2].description,\n        setParam: { quarter: 2 },\n        granularity: \"quarter\",\n    },\n    first_quarter: {\n        id: \"first_quarter\",\n        groupNumber: 1,\n        description: QUARTERS[1].description,\n        setParam: { quarter: 1 },\n        granularity: \"quarter\",\n    },\n};\n\nexport const DEFAULT_INTERVAL = \"month\";\n\n/**\n * Time interval options that users can select in the views.\n */\nexport const INTERVAL_OPTIONS = {\n    year: { description: _t(\"Year\"), id: \"year\", groupNumber: 1 },\n    quarter: { description: _t(\"Quarter\"), id: \"quarter\", groupNumber: 1 },\n    month: { description: _t(\"Month\"), id: \"month\", groupNumber: 1 },\n    week: { description: _t(\"Week\"), id: \"week\", groupNumber: 1 },\n    day: { description: _t(\"Day\"), id: \"day\", groupNumber: 1 },\n};\n\n/**\n * Time interval options supported by the backend.\n * These options are not available in the views UI, but can be used in dashboards.\n */\nexport const BACKEND_INTERVAL_OPTIONS = {\n    ...INTERVAL_OPTIONS,\n    hour: { description: _t(\"Hour\"), id: \"hour\" },\n};\n\n//-------------------------------------------------------------------------\n// Functions\n//-------------------------------------------------------------------------\n\n/**\n * Constructs the string representation of a domain and its description. The\n * domain is of the form:\n *      ['|', d_1 ,..., '|', d_n]\n * where d_i is a time range of the form\n *      ['&', [fieldName, >=, leftBound_i], [fieldName, <=, rightBound_i]]\n * where leftBound_i and rightBound_i are date or datetime computed accordingly\n * to the given options and reference moment.\n */\nexport function constructDateDomain(referenceMoment, searchItem, selectedOptionIds) {\n    let plusParam;\n    const selectedOptions = getSelectedOptions(referenceMoment, searchItem, selectedOptionIds);\n    if (\"withDomain\" in selectedOptions) {\n        return {\n            description: selectedOptions.withDomain[0].description,\n            domain: Domain.and([selectedOptions.withDomain[0].domain, searchItem.domain]),\n        };\n    }\n    const yearOptions = selectedOptions.year;\n    const otherOptions = [...(selectedOptions.quarter || []), ...(selectedOptions.month || [])];\n    sortPeriodOptions(yearOptions);\n    sortPeriodOptions(otherOptions);\n    const ranges = [];\n    const { fieldName, fieldType } = searchItem;\n    for (const yearOption of yearOptions) {\n        const constructRangeParams = {\n            referenceMoment,\n            fieldName,\n            fieldType,\n            plusParam,\n        };\n        if (otherOptions.length) {\n            for (const option of otherOptions) {\n                const setParam = Object.assign(\n                    {},\n                    yearOption.setParam,\n                    option ? option.setParam : {}\n                );\n                const { granularity } = option;\n                const range = constructDateRange(\n                    Object.assign({ granularity, setParam }, constructRangeParams)\n                );\n                ranges.push(range);\n            }\n        } else {\n            const { granularity, setParam } = yearOption;\n            const range = constructDateRange(\n                Object.assign({ granularity, setParam }, constructRangeParams)\n            );\n            ranges.push(range);\n        }\n    }\n    let domain = Domain.combine(\n        ranges.map((range) => range.domain),\n        \"OR\"\n    );\n    domain = Domain.and([domain, searchItem.domain]);\n    const description = ranges.map((range) => range.description).join(\"/\");\n    return { domain, description };\n}\n\n/**\n * Constructs the string representation of a domain and its description. The\n * domain is a time range of the form:\n *      ['&', [fieldName, >=, leftBound],[fieldName, <=, rightBound]]\n * where leftBound and rightBound are some date or datetime determined by setParam,\n * plusParam, granularity and the reference moment.\n */\nexport function constructDateRange(params) {\n    const { referenceMoment, fieldName, fieldType, granularity, setParam, plusParam } = params;\n    if (\"quarter\" in setParam) {\n        // Luxon does not consider quarter key in setParam (like moment did)\n        setParam.month = QUARTERS[setParam.quarter].coveredMonths[0];\n        delete setParam.quarter;\n    }\n    const date = referenceMoment.set(setParam).plus(plusParam || {});\n    // compute domain\n    const leftDate = date.startOf(granularity);\n    const rightDate = date.endOf(granularity);\n    let leftBound;\n    let rightBound;\n    if (fieldType === \"date\") {\n        leftBound = serializeDate(leftDate);\n        rightBound = serializeDate(rightDate);\n    } else {\n        leftBound = serializeDateTime(leftDate);\n        rightBound = serializeDateTime(rightDate);\n    }\n    const domain = new Domain([\"&\", [fieldName, \">=\", leftBound], [fieldName, \"<=\", rightBound]]);\n    // compute description\n    const descriptions = [date.toFormat(\"yyyy\")];\n    const method = localization.direction === \"rtl\" ? \"push\" : \"unshift\";\n    if (granularity === \"month\") {\n        descriptions[method](date.toFormat(\"MMMM\"));\n    } else if (granularity === \"quarter\") {\n        const quarter = date.quarter;\n        descriptions[method](QUARTERS[quarter].description.toString());\n    }\n    const description = descriptions.join(\" \");\n    return { domain, description };\n}\n\n/**\n * Returns a version of the options in INTERVAL_OPTIONS with translated descriptions.\n * @see getOptionsWithDescriptions\n */\nexport function getIntervalOptions() {\n    return getOptionsWithDescriptions(INTERVAL_OPTIONS);\n}\n\n/**\n * Returns a version of the options in OPTIONS with translated descriptions (if any).\n * @param {Object{}} OPTIONS\n * @returns {Object[]}\n */\nexport function getOptionsWithDescriptions(OPTIONS) {\n    const options = [];\n    for (const option of Object.values(OPTIONS)) {\n        options.push(Object.assign({}, option, { description: option.description.toString() }));\n    }\n    return options;\n}\n\n/**\n * Returns the period options relative to the referenceMoment for a date filter, with translated\n * descriptions and a key defautlYearId used in the control panel model when toggling a period option.\n */\nexport function getPeriodOptions(referenceMoment, optionsParams) {\n    return [\n        ...getMonthPeriodOptions(referenceMoment, optionsParams),\n        ...getQuarterPeriodOptions(optionsParams),\n        ...getYearPeriodOptions(referenceMoment, optionsParams),\n        ...getCustomPeriodOptions(optionsParams),\n    ];\n}\n\nexport function toGeneratorId(unit, offset) {\n    if (!offset) {\n        return unit;\n    }\n    const sep = offset > 0 ? \"+\" : \"-\";\n    const val = Math.abs(offset);\n    return `${unit}${sep}${val}`;\n}\n\nfunction getMonthPeriodOptions(referenceMoment, optionsParams) {\n    const { startYear, endYear, startMonth, endMonth } = optionsParams;\n    return [...Array(endMonth - startMonth + 1).keys()]\n        .map((i) => {\n            const monthOffset = startMonth + i;\n            const date = referenceMoment.plus({\n                months: monthOffset,\n                years: clamp(0, startYear, endYear),\n            });\n            const yearOffset = date.year - referenceMoment.year;\n            return {\n                id: toGeneratorId(\"month\", monthOffset),\n                defaultYearId: toGeneratorId(\"year\", clamp(yearOffset, startYear, endYear)),\n                description: date.toFormat(\"MMMM\"),\n                granularity: \"month\",\n                groupNumber: 1,\n                plusParam: { months: monthOffset },\n            };\n        })\n        .reverse();\n}\n\nfunction getQuarterPeriodOptions(optionsParams) {\n    const { startYear, endYear } = optionsParams;\n    const defaultYearId = toGeneratorId(\"year\", clamp(0, startYear, endYear));\n    return Object.values(QUARTER_OPTIONS).map((quarter) => ({\n        ...quarter,\n        defaultYearId,\n    }));\n}\n\nfunction getYearPeriodOptions(referenceMoment, optionsParams) {\n    const { startYear, endYear } = optionsParams;\n    return [...Array(endYear - startYear + 1).keys()]\n        .map((i) => {\n            const offset = startYear + i;\n            const date = referenceMoment.plus({ years: offset });\n            return {\n                id: toGeneratorId(\"year\", offset),\n                description: date.toFormat(\"yyyy\"),\n                granularity: \"year\",\n                groupNumber: 2,\n                plusParam: { years: offset },\n            };\n        })\n        .reverse();\n}\n\nfunction getCustomPeriodOptions(optionsParams) {\n    const { customOptions } = optionsParams;\n    return customOptions.map((option) => ({\n        id: option.id,\n        description: option.description,\n        granularity: \"withDomain\",\n        groupNumber: 3,\n        domain: option.domain,\n    }));\n}\n\n/**\n * Returns a partial version of the period options whose ids are in selectedOptionIds\n * partitioned by granularity.\n */\nexport function getSelectedOptions(referenceMoment, searchItem, selectedOptionIds) {\n    const selectedOptions = { year: [] };\n    const periodOptions = getPeriodOptions(referenceMoment, searchItem.optionsParams);\n    for (const optionId of selectedOptionIds) {\n        const option = periodOptions.find((option) => option.id === optionId);\n        const granularity = option.granularity;\n        if (!selectedOptions[granularity]) {\n            selectedOptions[granularity] = [];\n        }\n        if (option.domain) {\n            selectedOptions[granularity].push(pick(option, \"domain\", \"description\"));\n        } else {\n            const setParam = getSetParam(option, referenceMoment);\n            selectedOptions[granularity].push({ granularity, setParam });\n        }\n    }\n    return selectedOptions;\n}\n\n/**\n * Returns the setParam object associated with the given periodOption and\n * referenceMoment.\n */\nexport function getSetParam(periodOption, referenceMoment) {\n    if (periodOption.granularity === \"quarter\") {\n        return periodOption.setParam;\n    }\n    const date = referenceMoment.plus(periodOption.plusParam);\n    const granularity = periodOption.granularity;\n    const setParam = { [granularity]: date[granularity] };\n    return setParam;\n}\n\nexport function rankInterval(intervalOptionId) {\n    return Object.keys(INTERVAL_OPTIONS).indexOf(intervalOptionId);\n}\n\n/**\n * Sorts in place an array of 'period' options.\n */\nexport function sortPeriodOptions(options) {\n    options.sort((o1, o2) => {\n        var _a, _b;\n        const granularity1 = o1.granularity;\n        const granularity2 = o2.granularity;\n        if (granularity1 === granularity2) {\n            return (\n                ((_a = o1.setParam[granularity1]) !== null && _a !== void 0 ? _a : 0) -\n                ((_b = o2.setParam[granularity1]) !== null && _b !== void 0 ? _b : 0)\n            );\n        }\n        return granularity1 < granularity2 ? -1 : 1;\n    });\n}\n\n/**\n * Checks if a year id is among the given array of period option ids.\n */\nexport function yearSelected(selectedOptionIds) {\n    return selectedOptionIds.some((optionId) => optionId.startsWith(\"year\"));\n}\n", "import { DEFAULT_INTERVAL, BACKEND_INTERVAL_OPTIONS } from \"./dates\";\n\n/**\n * @param {string} descr\n */\nfunction errorMsg(descr) {\n    return `Invalid groupBy description: ${descr}`;\n}\n\n/**\n * @param {string} descr\n * @param {Object} fields\n * @returns {Object}\n */\nexport function getGroupBy(descr, fields) {\n    let fieldName;\n    let interval;\n    let spec;\n    [fieldName, interval] = descr.split(\":\");\n    if (!fieldName) {\n        throw Error();\n    }\n    if (fields) {\n        if (!fields[fieldName] && !fieldName.includes(\".\")) {\n            throw Error(errorMsg(descr));\n        }\n        const fieldType = fields[fieldName]?.type;\n        if ([\"date\", \"datetime\"].includes(fieldType)) {\n            if (!interval) {\n                interval = DEFAULT_INTERVAL;\n            } else if (!Object.keys(BACKEND_INTERVAL_OPTIONS).includes(interval)) {\n                throw Error(errorMsg(descr));\n            }\n            spec = `${fieldName}:${interval}`;\n        } else if (interval) {\n            throw Error(errorMsg(descr));\n        } else {\n            spec = fieldName;\n            interval = null;\n        }\n    } else {\n        if (interval) {\n            if (!Object.keys(BACKEND_INTERVAL_OPTIONS).includes(interval)) {\n                throw Error(errorMsg(descr));\n            }\n            spec = `${fieldName}:${interval}`;\n        } else {\n            spec = fieldName;\n            interval = null;\n        }\n    }\n    return {\n        fieldName,\n        interval,\n        spec,\n        toJSON() {\n            return spec;\n        },\n    };\n}\n", "export const FACET_ICONS = {\n    filter: \"fa fa-filter\",\n    groupBy: \"oi oi-group\",\n    groupByAsc: \"fa fa-sort-numeric-asc\",\n    groupByDesc: \"fa fa-sort-numeric-desc\",\n    favorite: \"fa fa-star\",\n};\n\nexport const FACET_COLORS = {\n    filter: \"primary\",\n    groupBy: \"action\",\n    favorite: \"warning\",\n};\n\nexport const GROUPABLE_TYPES = [\n    \"boolean\",\n    \"char\",\n    \"date\",\n    \"datetime\",\n    \"integer\",\n    \"many2one\",\n    \"many2many\",\n    \"selection\",\n    \"tags\",\n];\n", "/**\n * @typedef {{\n *  name: string;\n *  asc?: boolean;\n * }} OrderTerm\n */\n\n/**\n * @param {OrderTerm[]} orderBy\n * @returns {string}\n */\nexport function orderByToString(orderBy) {\n    return orderBy.map((o) => `${o.name} ${o.asc ? \"ASC\" : \"DESC\"}`).join(\", \");\n}\n\n/**\n * @param {any} string\n * @return {OrderTerm[]}\n */\nexport function stringToOrderBy(string) {\n    if (!string) {\n        return [];\n    }\n    return string.split(\",\").map((order) => {\n        const splitOrder = order.trim().split(\" \");\n        if (splitOrder.length === 2) {\n            return {\n                name: splitOrder[0],\n                asc: splitOrder[1].toLowerCase() === \"asc\",\n            };\n        } else {\n            return {\n                name: splitOrder[0],\n                asc: true,\n            };\n        }\n    });\n}\n", "import { Component, onWillStart, onWillUpdateProps, toRaw, useSubEnv } from \"@odoo/owl\";\nimport { CallbackRecorder, useSetupAction } from \"@web/search/action_hook\";\nimport { SearchModel } from \"@web/search/search_model\";\nimport { useBus, useService } from \"@web/core/utils/hooks\";\n\nexport const SEARCH_KEYS = [\"context\", \"domain\", \"groupBy\", \"orderBy\"];\n\nexport class WithSearch extends Component {\n    static template = \"web.WithSearch\";\n    static props = {\n        slots: Object,\n        SearchModel: { type: Function, optional: true },\n\n        resModel: String,\n\n        globalState: { type: Object, optional: true },\n        searchModelArgs: { type: Object, optional: true },\n\n        display: { type: Object, optional: true },\n\n        // search query elements\n        context: { type: Object, optional: true },\n        domain: { type: Array, element: [String, Array], optional: true },\n        groupBy: { type: Array, element: String, optional: true },\n        orderBy: { type: Array, element: Object, optional: true },\n\n        // search view description\n        searchViewArch: { type: String, optional: true },\n        searchViewFields: { type: Object, optional: true },\n        searchViewId: { type: [Number, Boolean], optional: true },\n\n        irFilters: { type: Array, element: Object, optional: true },\n        loadIrFilters: { type: Boolean, optional: true },\n\n        // extra options\n        activateFavorite: { type: Boolean, optional: true },\n        dynamicFilters: { type: Array, element: Object, optional: true },\n        hideCustomGroupBy: { type: Boolean, optional: true },\n        searchMenuTypes: { type: Array, element: String, optional: true },\n        canOrderByCount: { type: Boolean, optional: true },\n        defaultGroupBy: { type: Array, element: String, optional: true },\n    };\n\n    setup() {\n        if (!this.env.__getContext__) {\n            useSubEnv({ __getContext__: new CallbackRecorder() });\n        }\n        if (!this.env.__getOrderBy__) {\n            useSubEnv({ __getOrderBy__: new CallbackRecorder() });\n        }\n\n        const SearchModelClass = this.props.SearchModel || SearchModel;\n        this.searchModel = new SearchModelClass(\n            this.env,\n            {\n                orm: useService(\"orm\"),\n                view: useService(\"view\"),\n                field: useService(\"field\"),\n                name: useService(\"name\"),\n                dialog: useService(\"dialog\"),\n                treeProcessor: useService(\"tree_processor\"),\n            },\n            this.props.searchModelArgs\n        );\n\n        const searchPanelState = this.props.globalState?.searchPanel\n            ? JSON.parse(this.props.globalState?.searchPanel)\n            : null;\n        useSubEnv({ searchModel: this.searchModel, searchPanelState });\n\n        useBus(this.searchModel, \"update\", this.render);\n        useSetupAction({\n            getGlobalState: () => ({\n                searchModel: JSON.stringify(this.searchModel.exportState()),\n            }),\n        });\n\n        onWillStart(async () => {\n            const config = { ...toRaw(this.props) };\n            if (config.globalState && config.globalState.searchModel) {\n                config.state = JSON.parse(config.globalState.searchModel);\n                delete config.globalState;\n            }\n            await this.searchModel.load(config);\n        });\n\n        onWillUpdateProps(async (nextProps) => {\n            const config = {};\n            for (const key of SEARCH_KEYS) {\n                if (nextProps[key] !== undefined) {\n                    config[key] = nextProps[key];\n                }\n            }\n            await this.searchModel.reload(config);\n        });\n    }\n}\n", "import { Component } from \"@odoo/owl\";\nimport { Widget } from \"@web/views/widgets/widget\";\nimport { RibbonWidget } from \"@web/views/widgets/ribbon/ribbon\";\n\nexport class ActionHelper extends Component {\n    static template = \"web.ActionHelper\";\n    static components = { Widget, RibbonWidget };\n    static props = {\n        showRibbon: { type: Boolean, optional: true, default: false },\n        noContentHelp: { type: String, optional: true },\n    };\n\n    get showDefaultHelper() {\n        return !this.props.noContentHelp;\n    }\n}\n", "import { evaluateExpr } from \"@web/core/py_js/py\";\nimport { exprToBoolean } from \"@web/core/utils/strings\";\nimport { visitXML } from \"@web/core/utils/xml\";\nimport { Field } from \"@web/views/fields/field\";\n\nconst FIELD_ATTRIBUTE_NAMES = [\n    \"date_start\",\n    \"date_delay\",\n    \"date_stop\",\n    \"all_day\",\n    \"create_name_field\",\n    \"color\",\n];\nconst SCALES = [\"day\", \"week\", \"month\", \"year\"];\n\nexport class CalendarParseArchError extends Error {}\n\nexport class CalendarArchParser {\n    parse(xmlDoc, models, modelName) {\n        const fields = models[modelName].fields;\n        const fieldNames = new Set(fields.display_name ? [\"display_name\"] : []);\n        const fieldMapping = {};\n        for (const fieldAttrName of FIELD_ATTRIBUTE_NAMES) {\n            if (xmlDoc.hasAttribute(fieldAttrName)) {\n                const fieldName = xmlDoc.getAttribute(fieldAttrName);\n                fieldNames.add(fieldName);\n                fieldMapping[fieldAttrName] = fieldName;\n            }\n        }\n        const aggregate = xmlDoc.getAttribute(\"aggregate\") || null;\n        if (aggregate) {\n            fieldNames.add(aggregate.split(\":\")[0]);\n        }\n\n        let scales = [...SCALES];\n        const scalesAttr = xmlDoc.getAttribute(\"scales\");\n        if (scalesAttr) {\n            scales = scalesAttr.split(\",\").filter((scale) => SCALES.includes(scale));\n        }\n        let scale = scales.includes(\"week\") ? \"week\" : scales[0];\n        if (xmlDoc.hasAttribute(\"mode\")) {\n            scale = xmlDoc.getAttribute(\"mode\");\n        }\n\n        const canCreate = exprToBoolean(xmlDoc.getAttribute(\"create\"), true);\n        const canDelete = exprToBoolean(xmlDoc.getAttribute(\"delete\"), true);\n        const canEdit = exprToBoolean(xmlDoc.getAttribute(\"edit\"), true);\n\n        const eventLimit = xmlDoc.hasAttribute(\"event_limit\")\n            ? evaluateExpr(xmlDoc.getAttribute(\"event_limit\"))\n            : 5;\n        const formViewId = parseInt(xmlDoc.getAttribute(\"form_view_id\"), 10) || false;\n        const hasEditDialog = exprToBoolean(xmlDoc.getAttribute(\"event_open_popup\"));\n        const isDateHidden = exprToBoolean(xmlDoc.getAttribute(\"hide_date\"));\n        const isTimeHidden = exprToBoolean(xmlDoc.getAttribute(\"hide_time\"));\n        const jsClass = xmlDoc.getAttribute(\"js_class\") || null;\n        const monthOverflow = exprToBoolean(xmlDoc.getAttribute(\"month_overflow\"), true);\n        const multiCreateView = xmlDoc.getAttribute(\"multi_create_view\");\n        const quickCreate = exprToBoolean(xmlDoc.getAttribute(\"quick_create\"), true);\n        const quickCreateViewId =\n            (quickCreate && parseInt(xmlDoc.getAttribute(\"quick_create_view_id\"), 10)) || null;\n        const showDatePicker = exprToBoolean(xmlDoc.getAttribute(\"show_date_picker\"), true);\n        const showUnusualDays = exprToBoolean(xmlDoc.getAttribute(\"show_unusual_days\"));\n\n        const popoverFieldNodes = {};\n        const filtersInfo = {};\n        visitXML(xmlDoc, (node) => {\n            switch (node.tagName) {\n                case \"field\": {\n                    const fieldName = node.getAttribute(\"name\");\n                    fieldNames.add(fieldName);\n\n                    const fieldInfo = Field.parseFieldNode(\n                        node,\n                        models,\n                        modelName,\n                        \"calendar\",\n                        jsClass\n                    );\n                    popoverFieldNodes[fieldName] = fieldInfo;\n\n                    if (!node.hasAttribute(\"invisible\") || node.hasAttribute(\"filters\")) {\n                        if (\n                            node.hasAttribute(\"avatar_field\") ||\n                            node.hasAttribute(\"write_model\") ||\n                            node.hasAttribute(\"write_field\") ||\n                            node.hasAttribute(\"color\") ||\n                            node.hasAttribute(\"filters\")\n                        ) {\n                            const field = fields[fieldName];\n                            filtersInfo[fieldName] = filtersInfo[fieldName] || {\n                                avatarFieldName: null,\n                                colorFieldName: null,\n                                context: fieldInfo.context || \"{}\",\n                                fieldName,\n                                filterFieldName: null,\n                                label: field.string,\n                                resModel: field.relation,\n                                writeFieldName: null,\n                                writeResModel: null,\n                            };\n                            const filterInfo = filtersInfo[fieldName];\n                            filterInfo.avatarFieldName = node.getAttribute(\"avatar_field\") || null;\n                            filterInfo.colorFieldName =\n                                (node.hasAttribute(\"filters\") && node.getAttribute(\"color\")) ||\n                                null;\n                            filterInfo.filterFieldName = node.getAttribute(\"filter_field\") || null;\n                            filterInfo.writeFieldName = node.getAttribute(\"write_field\") || null;\n                            filterInfo.writeResModel = node.getAttribute(\"write_model\") || null;\n                        }\n                    }\n                    break;\n                }\n            }\n        });\n\n        if (!fieldMapping.date_start) {\n            throw new CalendarParseArchError(`Calendar view must define \"date_start\" attribute.`);\n        }\n        if (!scales.includes(scale)) {\n            throw new CalendarParseArchError(`Calendar view cannot display mode: ${scale}`);\n        }\n        if (!Number.isInteger(eventLimit)) {\n            throw new CalendarParseArchError(`Calendar view's event limit should be a number`);\n        }\n\n        return {\n            aggregate,\n            canCreate,\n            canDelete,\n            canEdit,\n            eventLimit,\n            fieldMapping,\n            fieldNames: [...fieldNames],\n            filtersInfo,\n            formViewId,\n            hasEditDialog,\n            multiCreateView,\n            quickCreate,\n            quickCreateViewId,\n            isDateHidden,\n            isTimeHidden,\n            monthOverflow,\n            popoverFieldNodes,\n            scale,\n            scales,\n            showUnusualDays,\n            showDatePicker,\n        };\n    }\n}\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { Dialog } from \"@web/core/dialog/dialog\";\nimport { evaluateBooleanExpr } from \"@web/core/py_js/py\";\nimport { is24HourFormat } from \"@web/core/l10n/time\";\nimport { registry } from \"@web/core/registry\";\nimport { Field } from \"@web/views/fields/field\";\nimport { Record } from \"@web/model/record\";\nimport { getFormattedDateSpan } from \"@web/views/calendar/utils\";\n\nimport { Component, useExternalListener } from \"@odoo/owl\";\n\nexport class CalendarCommonPopover extends Component {\n    static template = \"web.CalendarCommonPopover\";\n    static subTemplates = {\n        popover: \"web.CalendarCommonPopover.popover\",\n        body: \"web.CalendarCommonPopover.body\",\n        footer: \"web.CalendarCommonPopover.footer\",\n    };\n    static components = {\n        Dialog,\n        Field,\n        Record,\n    };\n    static props = {\n        close: Function,\n        record: Object,\n        model: Object,\n        createRecord: Function,\n        deleteRecord: Function,\n        editRecord: Function,\n    };\n\n    setup() {\n        this.time = null;\n        this.timeDuration = null;\n        this.date = null;\n        this.dateDuration = null;\n\n        useExternalListener(window, \"pointerdown\", (e) => e.preventDefault(), { capture: true });\n\n        this.computeDateTimeAndDuration();\n    }\n\n    get activeFields() {\n        return this.props.model.activeFields;\n    }\n    get isEventEditable() {\n        return this.props.model.canEdit;\n    }\n    get isEventDeletable() {\n        return this.props.model.canDelete;\n    }\n    get isEventViewable() {\n        return true;\n    }\n    get hasFooter() {\n        return this.isEventEditable || this.isEventDeletable || this.isEventViewable;\n    }\n\n    isInvisible(fieldNode, record) {\n        return evaluateBooleanExpr(fieldNode.invisible, record.evalContextWithVirtualIds);\n    }\n\n    getFormattedValue(fieldName, record) {\n        const fieldInfo = this.props.model.popoverFieldNodes[fieldName];\n        const field = this.props.model.fields[fieldName];\n        let format;\n        const formattersRegistry = registry.category(\"formatters\");\n        if (fieldInfo.widget && formattersRegistry.contains(fieldInfo.widget)) {\n            format = formattersRegistry.get(fieldInfo.widget);\n        } else {\n            format = formattersRegistry.get(field.type);\n        }\n        return format(record.data[fieldName]);\n    }\n\n    computeDateTimeAndDuration() {\n        const record = this.props.record;\n        const { start, end } = record;\n        const isSameDay = start.hasSame(end, \"day\");\n\n        if (!record.isTimeHidden && !record.isAllDay && isSameDay) {\n            const timeFormat = is24HourFormat() ? \"HH:mm\" : \"hh:mm a\";\n            this.time = `${start.toFormat(timeFormat)} - ${end.toFormat(timeFormat)}`;\n\n            const duration = end.diff(start, [\"hours\", \"minutes\"]);\n            const formatParts = [];\n            if (duration.hours > 0) {\n                const hourString = duration.hours === 1 ? _t(\"hour\") : _t(\"hours\");\n                formatParts.push(`h '${hourString}'`);\n            }\n            if (duration.minutes > 0) {\n                const minuteStr = duration.minutes === 1 ? _t(\"minute\") : _t(\"minutes\");\n                formatParts.push(`m '${minuteStr}'`);\n            }\n            this.timeDuration = duration.toFormat(formatParts.join(\", \"));\n        }\n\n        if (!this.props.model.isDateHidden) {\n            this.date = getFormattedDateSpan(start, end);\n\n            if (record.isAllDay) {\n                if (isSameDay) {\n                    this.dateDuration = _t(\"All day\");\n                } else {\n                    const duration = end.plus({ day: 1 }).diff(start, \"days\");\n                    this.dateDuration = duration.toFormat(`d '${_t(\"days\")}'`);\n                }\n            }\n        }\n    }\n\n    onEditEvent() {\n        this.props.editRecord(this.props.record);\n        this.props.close();\n    }\n    onDeleteEvent() {\n        this.props.deleteRecord(this.props.record);\n        this.props.close();\n    }\n}\n", "import { browser } from \"@web/core/browser/browser\";\nimport { getLocalYearAndWeek } from \"@web/core/l10n/dates\";\nimport { localization } from \"@web/core/l10n/localization\";\nimport { is24HourFormat } from \"@web/core/l10n/time\";\nimport { useBus } from \"@web/core/utils/hooks\";\nimport { renderToFragment, renderToString } from \"@web/core/utils/render\";\nimport { useDebounced } from \"@web/core/utils/timing\";\nimport { makeWeekColumn } from \"@web/views/calendar/calendar_common/calendar_common_week_column\";\nimport { CalendarCommonPopover } from \"@web/views/calendar/calendar_common/calendar_common_popover\";\nimport { convertRecordToEvent, getColor } from \"@web/views/calendar/utils\";\nimport { useCalendarPopover } from \"@web/views/calendar/hooks/calendar_popover_hook\";\nimport { useFullCalendar } from \"@web/views/calendar/hooks/full_calendar_hook\";\nimport { useSquareSelection } from \"@web/views/calendar/hooks/square_selection_hook\";\n\nimport { Component, useEffect } from \"@odoo/owl\";\n\nconst SCALE_TO_FC_VIEW = {\n    day: \"timeGridDay\",\n    week: \"timeGridWeek\",\n    month: \"dayGridMonth\",\n};\nconst SCALE_TO_HEADER_FORMAT = {\n    day: \"DDD\",\n    week: \"EEE d\",\n    month: \"EEEE\",\n};\nconst SHORT_SCALE_TO_HEADER_FORMAT = {\n    ...SCALE_TO_HEADER_FORMAT,\n    day: \"D\",\n    month: \"EEE\",\n};\nconst HOUR_FORMATS = {\n    12: {\n        hour: \"numeric\",\n        minute: \"2-digit\",\n        omitZeroMinute: true,\n        meridiem: \"short\",\n    },\n    24: {\n        hour: \"numeric\",\n        minute: \"2-digit\",\n        hour12: false,\n    },\n};\n\nconst { DateTime } = luxon;\n\nexport class CalendarCommonRenderer extends Component {\n    static components = {\n        Popover: CalendarCommonPopover,\n    };\n    static template = \"web.CalendarCommonRenderer\";\n    static eventTemplate = \"web.CalendarCommonRenderer.event\";\n    static headerTemplate = \"web.CalendarCommonRendererHeader\";\n    static props = {\n        model: Object,\n        isWeekendVisible: { type: Boolean, optional: true },\n        createRecord: Function,\n        editRecord: Function,\n        deleteRecord: Function,\n        setDate: { type: Function, optional: true },\n        callbackRecorder: Object,\n        onSquareSelection: Function,\n        cleanSquareSelection: Function,\n    };\n\n    setup() {\n        this.fc = useFullCalendar(\"fullCalendar\", this.options);\n        this.clickTimeoutId = null;\n        this.popover = useCalendarPopover(this.constructor.components.Popover);\n        this.timeFormat = is24HourFormat() ? \"HH:mm\" : \"hh:mm a\";\n        useBus(this.props.model.bus, \"SCROLL_TO_CURRENT_HOUR\", () =>\n            this.fc.api.scrollToTime(`${luxon.DateTime.local().hour - 2}:00:00`)\n        );\n\n        const fullCalendarRenderDebounced = useDebounced(() => this.fc.api.updateSize(), 100, {\n            immediate: true,\n            trailing: true,\n        });\n        const fullCalendarResizeObserver = new ResizeObserver(fullCalendarRenderDebounced);\n        useEffect(\n            (el) => {\n                fullCalendarResizeObserver.observe(el);\n                return () => fullCalendarResizeObserver.unobserve(el);\n            },\n            () => [this.fc.el]\n        );\n        useSquareSelection({\n            cellIsSelectable: this.constructor.cellIsSelectable,\n        });\n    }\n\n    get options() {\n        return {\n            allDaySlot: true,\n            allDayContent: \"\",\n            dayHeaderFormat: this.env.isSmall\n                ? SHORT_SCALE_TO_HEADER_FORMAT[this.props.model.scale]\n                : SCALE_TO_HEADER_FORMAT[this.props.model.scale],\n            // we must handle clicks differently in multicreate mode:\n            // fc is blocked by safePrevent in onPointerDown (draggable_hook_builder.js)\n            dateClick: this.props.model.hasMultiCreate ? () => {} : this.onDateClick,\n            dayCellClassNames: this.getDayCellClassNames,\n            initialDate: this.props.model.date.toISO(),\n            initialView: SCALE_TO_FC_VIEW[this.props.model.scale],\n            direction: localization.direction,\n            droppable: true,\n            editable: this.props.model.canEdit,\n            eventClick: this.onEventClick,\n            eventDragStart: this.onEventDragStart,\n            eventDrop: this.onEventDrop,\n            dayMaxEventRows: this.props.model.eventLimit,\n            moreLinkClick: this.onEventLimitClick,\n            eventMouseEnter: this.onEventMouseEnter,\n            eventMouseLeave: this.onEventMouseLeave,\n            eventClassNames: this.eventClassNames,\n            eventDidMount: this.onEventDidMount,\n            eventContent: this.onEventContent,\n            eventResizableFromStart: true,\n            eventResize: this.onEventResize,\n            eventResizeStart: this.onEventResizeStart,\n            events: (_, successCb) => successCb(this.mapRecordsToEvents()),\n            firstDay: this.props.model.firstDayOfWeek,\n            headerToolbar: false,\n            height: \"100%\",\n            locale: luxon.Settings.defaultLocale,\n            longPressDelay: 500,\n            navLinks: false,\n            nowIndicator: true,\n            nowIndicatorContent: {\n                html: `\n                    <div class=\"o_calendar_time_indicator_now\"></div>\n                `,\n            },\n            select: this.onSelect,\n            selectAllow: this.isSelectionAllowed,\n            selectMinDistance: 5, // needed to not trigger select when click\n            selectMirror: true,\n            selectable: !this.props.model.hasMultiCreate && this.props.model.canCreate,\n            showNonCurrentDates: this.props.model.monthOverflow,\n            slotLabelFormat: is24HourFormat() ? HOUR_FORMATS[24] : HOUR_FORMATS[12],\n            snapDuration: { minutes: 15 },\n            timeZone: luxon.Settings.defaultZone.name,\n            unselectAuto: false,\n            weekNumberFormat: {\n                week: this.props.model.scale === \"month\" || this.env.isSmall ? \"numeric\" : \"long\",\n            },\n            weekends: this.props.isWeekendVisible,\n            weekNumberCalculation: (date) => getLocalYearAndWeek(date).week,\n            weekNumbers: true,\n            dayHeaderContent: this.getHeaderHtml,\n            eventDisplay: \"block\", // Restore old render in daygrid view for single-day timed events\n            eventTimeFormat: is24HourFormat() ? HOUR_FORMATS[24] : HOUR_FORMATS[12],\n            viewDidMount: this.viewDidMount,\n            moreLinkDidMount: this.wrapMoreLink,\n            fixedWeekCount: false,\n        };\n    }\n\n    get customOptions() {\n        return {\n            weekNumbersWithinDays: !this.env.isSmall,\n        };\n    }\n\n    viewDidMount({ el, view }) {\n        const showWeek = view.calendar.currentData.options.weekNumbers;\n        const weekText = view.calendar.currentData.options.weekText;\n        const weekColumn = !this.customOptions.weekNumbersWithinDays;\n        if (showWeek && weekColumn) {\n            makeWeekColumn({ el, weekText });\n        }\n    }\n\n    getStartTime(record) {\n        return record.start.toFormat(this.timeFormat);\n    }\n\n    getEndTime(record) {\n        return record.end.toFormat(this.timeFormat);\n    }\n\n    computeEventSelector(event) {\n        return `[data-event-id=\"${event.id}\"]`;\n    }\n    highlightEvent(event, className) {\n        for (const el of this.fc.el.querySelectorAll(this.computeEventSelector(event))) {\n            el.classList.add(className);\n        }\n    }\n    unhighlightEvent(event, className) {\n        for (const el of this.fc.el.querySelectorAll(this.computeEventSelector(event))) {\n            el.classList.remove(className);\n        }\n    }\n    mapRecordsToEvents() {\n        return Object.values(this.props.model.records).map((r) => this.convertRecordToEvent(r));\n    }\n    convertRecordToEvent(record) {\n        return convertRecordToEvent(record);\n    }\n    getPopoverProps(record) {\n        return {\n            record,\n            model: this.props.model,\n            createRecord: this.props.createRecord,\n            deleteRecord: this.props.deleteRecord,\n            editRecord: this.props.editRecord,\n        };\n    }\n    openPopover(target, record) {\n        const color = getColor(record.colorIndex);\n        this.popover.open(\n            target,\n            this.getPopoverProps(record),\n            `o_cw_popover card o_calendar_color_${typeof color === \"number\" ? color : 0}`\n        );\n    }\n\n    onClick(info) {\n        this.openPopover(info.el, this.props.model.records[info.event.id]);\n        this.highlightEvent(info.event, \"o_cw_custom_highlight\");\n    }\n    onDateClick(info) {\n        if (info.jsEvent.defaultPrevented) {\n            return;\n        }\n        this.props.createRecord(this.fcEventToRecord(info));\n    }\n    getDayCellClassNames(info) {\n        const date = luxon.DateTime.fromJSDate(info.date).toISODate();\n        if (this.props.model.unusualDays.includes(date)) {\n            return [\"o_calendar_disabled\"];\n        }\n        return [];\n    }\n    onDblClick(info) {\n        this.props.editRecord(this.props.model.records[info.event.id]);\n    }\n    onEventClick(info) {\n        if (this.clickTimeoutId) {\n            this.onDblClick(info);\n            browser.clearTimeout(this.clickTimeoutId);\n            this.clickTimeoutId = null;\n        } else {\n            this.clickTimeoutId = browser.setTimeout(() => {\n                this.onClick(info);\n                this.clickTimeoutId = null;\n            }, 250);\n        }\n    }\n    onEventContent(arg) {\n        const { event } = arg;\n        if (event.start && event.end) {\n            const dateFmt = (date) =>\n                luxon.DateTime.fromJSDate(date).toFormat(this.timeFormat);\n            arg.timeText = `${dateFmt(event.start)} - ${dateFmt(event.end)}`;\n        }\n        const record = this.props.model.records[event.id];\n        if (record) {\n            // This is needed in order to give the possibility to change the event template.\n            const fragment = renderToFragment(this.constructor.eventTemplate, {\n                ...record,\n                startTime: this.getStartTime(record),\n                endTime: this.getEndTime(record),\n            });\n            return { domNodes: fragment.children };\n        }\n        return true;\n    }\n    eventClassNames({ el, event }) {\n        const classesToAdd = [];\n        classesToAdd.push(\"o_event\");\n        const record = this.props.model.records[event.id];\n\n        if (record) {\n            const color = getColor(record.colorIndex);\n            if (typeof color === \"number\") {\n                classesToAdd.push(`o_calendar_color_${color}`);\n            } else if (typeof color !== \"string\") {\n                classesToAdd.push(\"o_calendar_color_0\");\n            }\n\n            if (record.isHatched) {\n                classesToAdd.push(\"o_event_hatched\");\n            }\n            if (record.isStriked) {\n                classesToAdd.push(\"o_event_striked\");\n            }\n            if (record.duration <= 0.25) {\n                classesToAdd.push(\"o_event_oneliner\");\n            }\n            if (DateTime.now() >= record.end) {\n                classesToAdd.push(\"o_past_event\");\n            }\n\n            if (!record.isAllDay && !record.isTimeHidden && record.isMonth) {\n                classesToAdd.push(\"o_event_dot\");\n            } else if (record.isAllDay) {\n                classesToAdd.push(\"o_event_allday\");\n            }\n        }\n        return classesToAdd;\n    }\n    onEventDidMount({ el, event }) {\n        el.dataset.eventId = event.id;\n        const record = this.props.model.records[event.id];\n\n        if (record) {\n            if (record.isMonth) {\n                el.querySelector(\".fc-event-main\").classList.add(\n                    \"d-flex\",\n                    \"gap-1\",\n                    \"text-truncate\"\n                );\n            }\n            const color = getColor(record.colorIndex);\n            if (typeof color === \"string\") {\n                el.style.backgroundColor = color;\n            }\n\n            if (!el.classList.contains(\"fc-bg\")) {\n                const bg = document.createElement(\"div\");\n                bg.classList.add(\"fc-bg\");\n                el.appendChild(bg);\n            }\n        }\n    }\n    async onSelect(info) {\n        info.jsEvent.preventDefault();\n        this.popover.close();\n        await this.props.createRecord(this.fcEventToRecord(info));\n        this.fc.api.unselect();\n    }\n    isSelectionAllowed(event) {\n        return event.end.getDate() === event.start.getDate() || event.allDay;\n    }\n    onEventDrop(info) {\n        this.fc.api.unselect();\n        this.props.model.updateRecord(this.fcEventToRecord(info.event), { moved: true });\n    }\n    onEventResize(info) {\n        this.fc.api.unselect();\n        this.props.model.updateRecord(this.fcEventToRecord(info.event));\n    }\n    fcEventToRecord(event) {\n        const { id, allDay, date, start, end } = event;\n        const res = {\n            start: luxon.DateTime.fromJSDate(date || start),\n            isAllDay: allDay,\n        };\n        if (end) {\n            res.end = luxon.DateTime.fromJSDate(end);\n            if ([\"week\", \"month\"].includes(this.props.model.scale) && allDay) {\n                res.end = res.end.minus({ days: 1 });\n            }\n        }\n        if (id) {\n            const existingRecord = this.props.model.records[id];\n            if (this.props.model.scale === \"month\") {\n                res.start = res.start?.set({\n                    hour: existingRecord.start.hour,\n                    minute: existingRecord.start.minute,\n                });\n                if (existingRecord.end) {\n                    res.end = res.end?.set({\n                        hour: existingRecord.end.hour,\n                        minute: existingRecord.end.minute,\n                    });\n                }\n            }\n            res.id = existingRecord.id;\n        }\n        return res;\n    }\n    onEventMouseEnter(info) {\n        this.highlightEvent(info.event, \"o_cw_custom_highlight\");\n    }\n    onEventMouseLeave(info) {\n        if (!info.event.id) {\n            return;\n        }\n        this.unhighlightEvent(info.event, \"o_cw_custom_highlight\");\n    }\n    onEventDragStart(info) {\n        this.props.cleanSquareSelection();\n        info.el.classList.add(info.view.type);\n        this.fc.api.unselect();\n        this.highlightEvent(info.event, \"o_cw_custom_highlight\");\n    }\n    onEventResizeStart(info) {\n        this.props.cleanSquareSelection();\n        this.fc.api.unselect();\n        this.highlightEvent(info.event, \"o_cw_custom_highlight\");\n    }\n    onEventLimitClick() {\n        this.fc.api.unselect();\n        return \"popover\";\n    }\n    onWindowResize() {\n        this.updateSize();\n    }\n\n    getHeaderHtml({ date }) {\n        return {\n            html: renderToString(this.constructor.headerTemplate, this.headerTemplateProps(date)),\n        };\n    }\n\n    headerTemplateProps(date) {\n        const scale = this.props.model.scale;\n        // when rendering months, FullCalendar uses a date w/out tz\n        // so use UTC instead of local tz when converting to DateTime\n        const options = scale === \"month\" ? { zone: \"UTC\" } : {};\n        const { weekdayShort, weekdayLong, day } = DateTime.fromJSDate(date, options);\n        return {\n            weekdayShort,\n            weekdayLong,\n            day,\n            scale,\n        };\n    }\n\n    wrapMoreLink({ el }) {\n        const wrapper = document.createElement(\"div\");\n        wrapper.classList.add(\"fc-more-cell\");\n        el.parentNode.insertBefore(wrapper, el);\n        wrapper.appendChild(el);\n    }\n}\n", "export function makeWeekColumn({ el, showWeek, weekColumn, weekText }) {\n    const firstRows = el.querySelectorAll(\".fc-col-header-cell:nth-child(1), .fc-day:nth-child(1)\");\n    for (const element of firstRows) {\n        const newElement = document.createElement(\"th\");\n        if (element.classList.contains(\"fc-col-header-cell\")) {\n            newElement.classList.add(\"o-fc-week-header\");\n            newElement.innerText = weekText;\n        } else {\n            newElement.classList.add(\"o-fc-week\");\n            const weekElement = element.querySelector(\".fc-daygrid-week-number\");\n            weekElement.classList.remove(\"fc-daygrid-week-number\");\n            newElement.append(weekElement);\n        }\n        element.parentElement.insertBefore(newElement, element);\n    }\n}\n", "import {\n    deleteConfirmationMessage,\n    ConfirmationDialog,\n} from \"@web/core/confirmation_dialog/confirmation_dialog\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { useBus, useOwnedDialogs, useService } from \"@web/core/utils/hooks\";\nimport { Layout } from \"@web/search/layout\";\nimport { useModelWithSampleData } from \"@web/model/model\";\nimport { FormViewDialog } from \"@web/views/view_dialogs/form_view_dialog\";\nimport { CallbackRecorder, useSetupAction } from \"@web/search/action_hook\";\nimport { CalendarMobileFilterPanel } from \"./mobile_filter_panel/calendar_mobile_filter_panel\";\nimport { CalendarQuickCreate } from \"./quick_create/calendar_quick_create\";\nimport { CalendarSidePanel } from \"@web/views/calendar/calendar_side_panel/calendar_side_panel\";\nimport { SearchBar } from \"@web/search/search_bar/search_bar\";\nimport { useSearchBarToggler } from \"@web/search/search_bar/search_bar_toggler\";\nimport { ViewScaleSelector } from \"@web/views/view_components/view_scale_selector\";\nimport { CogMenu } from \"@web/search/cog_menu/cog_menu\";\nimport { browser } from \"@web/core/browser/browser\";\nimport { standardViewProps } from \"@web/views/standard_view_props\";\nimport { MultiSelectionButtons } from \"@web/views/view_components/multi_selection_buttons\";\nimport { getLocalYearAndWeek } from \"@web/core/l10n/dates\";\n\nimport { Component, reactive, useState } from \"@odoo/owl\";\n\nconst { DateTime } = luxon;\n\nexport const SCALE_LABELS = {\n    day: _t(\"Day\"),\n    week: _t(\"Week\"),\n    month: _t(\"Month\"),\n    year: _t(\"Year\"),\n};\n\nfunction useUniqueDialog() {\n    const displayDialog = useOwnedDialogs();\n    let close = null;\n    return (...args) => {\n        if (close) {\n            close();\n        }\n        close = displayDialog(...args);\n    };\n}\n\nexport class CalendarController extends Component {\n    static components = {\n        MobileFilterPanel: CalendarMobileFilterPanel,\n        QuickCreate: CalendarQuickCreate,\n        QuickCreateFormView: FormViewDialog,\n        Layout,\n        SearchBar,\n        ViewScaleSelector,\n        CogMenu,\n        CalendarSidePanel,\n        MultiSelectionButtons,\n    };\n    static template = \"web.CalendarController\";\n    static props = {\n        ...standardViewProps,\n        Model: Function,\n        Renderer: Function,\n        archInfo: Object,\n        buttonTemplate: String,\n        itemCalendarProps: { type: Object, optional: true },\n    };\n\n    setup() {\n        this.action = useService(\"action\");\n        this.orm = useService(\"orm\");\n        this.displayDialog = useUniqueDialog();\n\n        this.model = useModelWithSampleData(this.props.Model, this.modelParams);\n\n        useSetupAction({\n            getLocalState: () => this.model.exportedState,\n        });\n\n        const sessionShowSidebar = browser.sessionStorage.getItem(\"calendar.showSideBar\");\n        this.state = useState({\n            isWeekendVisible:\n                browser.localStorage.getItem(\"calendar.isWeekendVisible\") != null\n                    ? JSON.parse(browser.localStorage.getItem(\"calendar.isWeekendVisible\"))\n                    : true,\n            showSideBar:\n                !this.env.isSmall &&\n                Boolean(sessionShowSidebar != null ? JSON.parse(sessionShowSidebar) : true),\n        });\n\n        this.searchBarToggler = useSearchBarToggler();\n\n        this._baseRendererProps = {\n            createRecord: this.createRecord.bind(this),\n            deleteRecord: this.deleteRecord.bind(this),\n            editRecord: this.editRecord.bind(this),\n            setDate: this.setDate.bind(this),\n        };\n\n        this.prepareSelectionFeature();\n    }\n\n    get modelParams() {\n        return {\n            ...this.props.archInfo,\n            resModel: this.props.resModel,\n            domain: this.props.domain,\n            fields: this.props.fields,\n            date: this.props.state?.date,\n        };\n    }\n\n    get currentDate() {\n        const meta = this.model.meta;\n        const scale = meta.scale;\n        if (this.env.isSmall && [\"week\", \"month\"].includes(scale)) {\n            const date = meta.date || DateTime.now();\n            let text = \"\";\n            if (scale === \"week\") {\n                const startMonth = date.startOf(\"week\");\n                const endMonth = date.endOf(\"week\");\n                if (startMonth.toFormat(\"LLL\") !== endMonth.toFormat(\"LLL\")) {\n                    text = `${startMonth.toFormat(\"LLL\")}-${endMonth.toFormat(\"LLL\")}`;\n                } else {\n                    text = startMonth.toFormat(\"LLLL\");\n                }\n            } else if (scale === \"month\") {\n                text = date.toFormat(\"LLLL\");\n            }\n            return ` - ${text} ${date.year}`;\n        } else {\n            return \"\";\n        }\n    }\n\n    get date() {\n        return this.model.meta.date || DateTime.now();\n    }\n\n    get today() {\n        return DateTime.now().toFormat(\"d\");\n    }\n\n    get currentYear() {\n        return this.date.toFormat(\"y\");\n    }\n\n    get dayHeader() {\n        return `${this.date.toFormat(\"d\")} ${this.date.toFormat(\"MMMM\")} ${this.date.year}`;\n    }\n\n    get weekHeader() {\n        const { rangeStart, rangeEnd } = this.model;\n        if (rangeStart.year != rangeEnd.year) {\n            return `${rangeStart.toFormat(\"MMMM\")} ${rangeStart.year} - ${rangeEnd.toFormat(\n                \"MMMM\"\n            )} ${rangeEnd.year}`;\n        } else if (rangeStart.month != rangeEnd.month) {\n            return `${rangeStart.toFormat(\"MMMM\")} - ${rangeEnd.toFormat(\"MMMM\")} ${\n                rangeStart.year\n            }`;\n        }\n        return `${rangeStart.toFormat(\"MMMM\")} ${rangeStart.year}`;\n    }\n\n    get currentMonth() {\n        return `${this.date.toFormat(\"MMMM\")} ${this.date.year}`;\n    }\n\n    get currentWeek() {\n        return getLocalYearAndWeek(this.model.rangeStart).week;\n    }\n\n    get rendererProps() {\n        return {\n            ...this._baseRendererProps,\n            model: this.model,\n            isWeekendVisible: this.model.scale === \"day\" || this.state.isWeekendVisible,\n        };\n    }\n\n    get mobileFilterPanelProps() {\n        return {\n            model: this.model,\n            sideBarShown: this.state.showSideBar,\n            toggleSideBar: () => {\n                this.state.showSideBar = !this.state.showSideBar;\n            },\n        };\n    }\n\n    get sidePanelProps() {\n        return { model: this.model };\n    }\n\n    toggleSideBar() {\n        this.state.showSideBar = !this.state.showSideBar;\n        browser.sessionStorage.setItem(\"calendar.showSideBar\", this.state.showSideBar);\n    }\n\n    get showCalendar() {\n        return !this.env.isSmall || !this.state.showSideBar;\n    }\n\n    get hasSideBar() {\n        return this.model.showDatePicker || this.model.filterSections.length > 0;\n    }\n\n    get showSideBar() {\n        return this.state.showSideBar;\n    }\n\n    get className() {\n        return this.props.className;\n    }\n\n    get editRecordDefaultDisplayText() {\n        return _t(\"New Event\");\n    }\n\n    prepareMultiSelectionButtonsReactive() {\n        return reactive({\n            onCancel: this.cleanSquareSelection.bind(this),\n            onAdd: (multiCreateData) => {\n                this.onMultiCreate(multiCreateData, this.selectedCells);\n                this.cleanSquareSelection();\n            },\n            onDelete: () => {\n                this.onMultiDelete(this.selectedCells);\n                this.cleanSquareSelection();\n            },\n            nbSelected: 0,\n            multiCreateView: this.model.meta.multiCreateView || \"\",\n            resModel: this.model.meta.resModel,\n            multiCreateValues: this.props.state?.multiCreateValues,\n            showMultiCreateTimeRange: this.model.showMultiCreateTimeRange,\n            visible: false,\n            context: this.props.context,\n        });\n    }\n\n    prepareSelectionFeature() {\n        this.selectedCells = null;\n        this.multiSelectionButtonsReactive = this.prepareMultiSelectionButtonsReactive();\n        this.callbackRecorder = new CallbackRecorder();\n        this._baseRendererProps.callbackRecorder = this.callbackRecorder;\n        this._baseRendererProps.onSquareSelection = this.updateMultiSelection.bind(this);\n        this._baseRendererProps.cleanSquareSelection = this.cleanSquareSelection.bind(this);\n\n        useBus(this.model.bus, \"update\", this.cleanSquareSelection.bind(this));\n    }\n\n    updateMultiSelection(selectedCells) {\n        if (selectedCells.length) {\n            this.selectedCells = selectedCells;\n            this.multiSelectionButtonsReactive.visible = true;\n            this.multiSelectionButtonsReactive.nbSelected = this.getSelectedRecordIds(\n                this.selectedCells\n            ).length;\n        } else {\n            this.selectedCells = null;\n            this.multiSelectionButtonsReactive.visible = false;\n            this.multiSelectionButtonsReactive.nbSelected = 0;\n        }\n    }\n\n    cleanSquareSelection() {\n        this.selectedCells = null;\n        this.multiSelectionButtonsReactive.visible = false;\n        this.callbackRecorder.callbacks.forEach((fn) => fn());\n    }\n\n    getQuickCreateProps(record) {\n        return {\n            record,\n            model: this.model,\n            editRecord: this.editRecordInCreation.bind(this),\n            title: this.props.context.default_name,\n        };\n    }\n\n    getQuickCreateFormViewProps(record) {\n        const rawRecord = this.model.buildRawRecord(record);\n        const context = this.model.makeContextDefaults(rawRecord);\n        return {\n            resModel: this.model.resModel,\n            viewId: this.model.quickCreateFormViewId,\n            title: _t(\"New Event\"),\n            context,\n        };\n    }\n\n    createRecord(record) {\n        if (!this.model.canCreate) {\n            return;\n        }\n        if (this.model.hasQuickCreate) {\n            if (this.model.quickCreateFormViewId) {\n                return new Promise((resolve) => {\n                    this.displayDialog(\n                        this.constructor.components.QuickCreateFormView,\n                        this.getQuickCreateFormViewProps(record),\n                        {\n                            onClose: () => resolve(),\n                        }\n                    );\n                });\n            }\n\n            return new Promise((resolve) => {\n                this.displayDialog(\n                    this.constructor.components.QuickCreate,\n                    this.getQuickCreateProps(record),\n                    {\n                        onClose: () => resolve(),\n                    }\n                );\n            });\n        } else {\n            return this.editRecordInCreation(record);\n        }\n    }\n    async editRecord(record, context = {}) {\n        if (this.model.hasEditDialog) {\n            return new Promise((resolve) => {\n                this.displayDialog(\n                    FormViewDialog,\n                    {\n                        resModel: this.model.resModel,\n                        resId: record.id || false,\n                        context,\n                        title: record.id\n                            ? _t(\"Open: %s\", record.title)\n                            : this.editRecordDefaultDisplayText,\n                        viewId: this.model.formViewId,\n                        onRecordSaved: () => this.model.load(),\n                    },\n                    { onClose: () => resolve() }\n                );\n            });\n        } else {\n            const action = {\n                type: \"ir.actions.act_window\",\n                res_model: this.model.resModel,\n                views: [[this.model.formViewId || false, \"form\"]],\n                target: \"current\",\n                context,\n            };\n            if (record.id) {\n                action.res_id = record.id;\n            }\n            this.action.doAction(action);\n        }\n    }\n    editRecordInCreation(record) {\n        const rawRecord = this.model.buildRawRecord(record);\n        const context = this.model.makeContextDefaults(rawRecord);\n        return this.editRecord(record, context);\n    }\n\n    deleteConfirmationDialogProps(record) {\n        return {\n            title: _t(\"Bye-bye, record!\"),\n            body: deleteConfirmationMessage,\n            confirm: () => {\n                this.model.unlinkRecord(record.id);\n            },\n            confirmLabel: _t(\"Delete\"),\n            cancel: () => {\n                // `ConfirmationDialog` needs this prop to display the cancel\n                // button but we do nothing on cancel.\n            },\n            cancelLabel: _t(\"No, keep it\"),\n        };\n    }\n\n    deleteRecord(record) {\n        this.displayDialog(ConfirmationDialog, this.deleteConfirmationDialogProps(record));\n    }\n\n    getDates(selectedCells) {\n        const dates = [];\n        for (const element of selectedCells) {\n            const date = luxon.DateTime.fromISO(element.dataset.date);\n            if (!date.invalid) {\n                dates.push(date);\n            }\n        }\n        return dates;\n    }\n\n    onMultiCreate(multiCreateData, selectedCells) {\n        const dates = this.getDates(selectedCells);\n        return this.model.multiCreateRecords(multiCreateData, dates);\n    }\n\n    getSelectedRecordIds(selectedCells) {\n        const ids = [];\n        for (const element of selectedCells) {\n            for (const event of [...element.querySelectorAll(\".fc-event\")]) {\n                ids.push(parseInt(event.dataset.eventId, 10));\n            }\n        }\n        return ids;\n    }\n\n    onMultiDelete(selectedCells) {\n        const ids = this.getSelectedRecordIds(selectedCells);\n        return this.model.unlinkRecords(ids);\n    }\n\n    async setDate(move) {\n        let date = null;\n        switch (move) {\n            case \"next\":\n                date = this.model.date.plus({ [`${this.model.scale}s`]: 1 });\n                break;\n            case \"previous\":\n                date = this.model.date.minus({ [`${this.model.scale}s`]: 1 });\n                break;\n            case \"today\":\n                date = luxon.DateTime.local().startOf(\"day\");\n                if (date.ts === this.date.startOf(\"day\").ts) {\n                    this.model.bus.trigger(\"SCROLL_TO_CURRENT_HOUR\", false);\n                }\n                break;\n        }\n        await this.model.load({ date });\n    }\n\n    get scales() {\n        return Object.fromEntries(\n            this.model.scales.map((s) => [s, { description: SCALE_LABELS[s] }])\n        );\n    }\n\n    async setScale(scale) {\n        await this.model.load({ scale });\n    }\n\n    toggleWeekendVisibility() {\n        this.state.isWeekendVisible = !this.state.isWeekendVisible;\n        browser.localStorage.setItem(\"calendar.isWeekendVisible\", this.state.isWeekendVisible);\n    }\n}\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { AutoComplete } from \"@web/core/autocomplete/autocomplete\";\nimport { Transition } from \"@web/core/transition\";\nimport { useOwnedDialogs, useService } from \"@web/core/utils/hooks\";\nimport { SelectCreateDialog } from \"@web/views/view_dialogs/select_create_dialog\";\nimport { getColor } from \"../utils\";\nimport { Component, useState } from \"@odoo/owl\";\n\nlet nextId = 1;\n\nexport class CalendarFilterSection extends Component {\n    static components = {\n        AutoComplete,\n        Transition,\n    };\n    static template = \"web.CalendarFilterSection\";\n    static subTemplates = {\n        filter: \"web.CalendarFilterSection.filter\",\n    };\n    static props = {\n        model: Object,\n        section: Object,\n    };\n\n    setup() {\n        this.state = useState({\n            collapsed: false,\n            fieldRev: 1,\n        });\n        this.addDialog = useOwnedDialogs();\n        this.orm = useService(\"orm\");\n    }\n\n    get autoCompleteProps() {\n        return {\n            autoSelect: true,\n            resetOnSelect: true,\n            placeholder: _t(\"+ Add %s\", this.section.label),\n            sources: [\n                {\n                    placeholder: _t(\"Loading...\"),\n                    options: (request) => this.loadSource(request),\n                    optionSlot: \"option\",\n                },\n            ],\n            value: \"\",\n            class: \"mt-1\",\n        };\n    }\n\n    get isAllActive() {\n        return this.section.filters.length && this.section.filters.every((filter) => filter.active);\n    }\n\n    get nextFilterId() {\n        nextId += 1;\n        return nextId;\n    }\n\n    get section() {\n        return this.props.section;\n    }\n\n    getFilterColor(filter) {\n        return filter.colorIndex !== null ? \"o_cw_filter_color_\" + getColor(filter.colorIndex) : \"\";\n    }\n\n    getSortedFilters() {\n        const types = [\"user\", \"record\", \"dynamic\"];\n        return this.section.filters.slice().sort((a, b) => {\n            if (a.type === b.type) {\n                const va = a.value ? -1 : 0;\n                const vb = b.value ? -1 : 0;\n                //Condition to put unvaluable item (eg: Open Shifts) at the end of the sorted list.\n                if (a.type === \"dynamic\" && va !== vb) {\n                    return va - vb;\n                }\n                return a.label.localeCompare(b.label, undefined, {\n                    numeric: true,\n                    sensitivity: \"base\",\n                    ignorePunctuation: true,\n                });\n            } else {\n                return types.indexOf(a.type) - types.indexOf(b.type);\n            }\n        });\n    }\n\n    async loadSource(request) {\n        const resModel = this.props.model.fields[this.section.fieldName].relation;\n        const activeIds = this.section.filters.map((f) => f.value);\n        const domain = [[\"id\", \"not in\", activeIds]];\n        const records = await this.orm.call(resModel, \"name_search\", [], {\n            name: request,\n            operator: \"ilike\",\n            domain: domain,\n            limit: 8,\n            context: this.section.context,\n        });\n\n        const options = records.map((result) => ({\n            data: {\n                id: result[0],\n            },\n            label: result[1],\n            onSelect: () => {\n                return this.props.model.createFilter(this.section.fieldName, result[0]);\n            },\n        }));\n\n        if (records.length > 7) {\n            options.push({\n                cssClass: \"o_calendar_dropdown_option\",\n                label: _t(\"Search More...\"),\n                onSelect: () => this.onSearchMore(resModel, domain, request),\n            });\n        }\n\n        if (records.length === 0) {\n            options.push({\n                cssClass: \"o_m2o_no_result\",\n                label: _t(\"No records\"),\n            });\n        }\n\n        return options;\n    }\n\n    toggleSection() {\n        this.state.collapsed = !this.state.collapsed;\n    }\n\n    onFilterInputChange(filter, ev) {\n        this.props.model.updateFilters(this.section.fieldName, [filter], ev.target.checked);\n        this.render();\n    }\n\n    onAllFilterInputChange(ev) {\n        const { fieldName, filters } = this.section;\n        this.props.model.updateFilters(fieldName, filters, ev.target.checked);\n        this.render();\n    }\n\n    onFilterRemoveBtnClick(filter, ev) {\n        if (!ev.currentTarget.dataset.unlinked) {\n            ev.currentTarget.dataset.unlinked = true;\n            this.props.model.unlinkFilter(this.section.fieldName, filter.recordId);\n            this.render();\n        }\n    }\n\n    async onSearchMore(resModel, domain, request) {\n        const dynamicFilters = [];\n        if (request.length) {\n            const nameGets = await this.orm.call(resModel, \"name_search\", [], {\n                name: request,\n                domain: domain,\n                operator: \"ilike\",\n                context: this.section.context,\n            });\n            dynamicFilters.push({\n                description: _t(\"Quick search: %s\", request),\n                domain: [[\"id\", \"in\", nameGets.map((nameGet) => nameGet[0])]],\n            });\n        }\n        this.addDialog(SelectCreateDialog, {\n            title: _t(\"Search: %s\", this.section.label),\n            noCreate: true,\n            multiSelect: true,\n            resModel,\n            context: this.section.context,\n            domain,\n            onSelected: (resId) => this.props.model.createFilter(this.section.fieldName, resId),\n            dynamicFilters,\n        });\n    }\n}\n", "import {\n    serializeDate,\n    serializeDateTime,\n    deserializeDate,\n    deserializeDateTime,\n} from \"@web/core/l10n/dates\";\nimport { localization } from \"@web/core/l10n/localization\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { user } from \"@web/core/user\";\nimport { KeepLast } from \"@web/core/utils/concurrency\";\nimport { Model } from \"@web/model/model\";\nimport { extractFieldsFromArchInfo } from \"@web/model/relational_model/utils\";\nimport { browser } from \"@web/core/browser/browser\";\nimport { makeContext } from \"@web/core/context\";\nimport { groupBy, intersection } from \"@web/core/utils/arrays\";\nimport { Cache } from \"@web/core/utils/cache\";\nimport { formatFloat } from \"@web/core/utils/numbers\";\nimport { useDebounced } from \"@web/core/utils/timing\";\nimport { computeAggregatedValue } from \"@web/views/utils\";\n\nconst { DateTime } = luxon;\n\nexport class CalendarModel extends Model {\n    static DEBOUNCED_LOAD_DELAY = 600;\n    static services = [\"notification\"];\n\n    setup(params, { notification }) {\n        /** @protected */\n        this.keepLast = new KeepLast();\n        this.notification = notification;\n\n        const formViewFromConfig = (this.env.config.views || []).find((view) => view[1] === \"form\");\n        const formViewIdFromConfig = formViewFromConfig ? formViewFromConfig[0] : false;\n        const fieldNodes = params.popoverFieldNodes;\n        const { activeFields, fields } = extractFieldsFromArchInfo({ fieldNodes }, params.fields);\n        this.meta = {\n            ...params,\n            activeFields,\n            fields,\n            firstDayOfWeek: (localization.weekStart || 0) % 7,\n            formViewId: params.formViewId || formViewIdFromConfig,\n        };\n        if (this.meta.aggregate?.split(\":\").length === 1) {\n            const aggregator = this.fields[this.meta.aggregate].aggregator || \"sum\";\n            this.meta.aggregate = `${this.meta.aggregate}:${aggregator}`;\n        }\n        this.meta.scale = this.getLocalStorageScale();\n        this.data = {\n            filterSections: {},\n            range: null,\n            records: {},\n            unusualDays: [],\n        };\n\n        const debouncedLoadDelay = this.constructor.DEBOUNCED_LOAD_DELAY;\n        this.debouncedLoad = useDebounced((params) => this.load(params), debouncedLoadDelay);\n\n        this._unusualDaysCache = new Cache(\n            (data) => this.fetchUnusualDays(data),\n            (data) => `${serializeDateTime(data.range.start)},${serializeDateTime(data.range.end)}`\n        );\n    }\n    async load(params = {}) {\n        Object.assign(this.meta, params);\n        if (!this.meta.date) {\n            this.meta.date =\n                params.context && params.context.initial_date\n                    ? deserializeDateTime(params.context.initial_date).startOf(\"day\")\n                    : DateTime.local().startOf(\"day\");\n        }\n        // Prevent picking a scale that is not supported by the view\n        if (!this.meta.scales.includes(this.meta.scale)) {\n            this.meta.scale = this.meta.scales[0];\n        }\n        browser.localStorage.setItem(this.storageKey, this.meta.scale);\n        const data = { ...this.data };\n        await this.keepLast.add(this.updateData(data));\n        this.data = data;\n        this.notify();\n    }\n\n    //--------------------------------------------------------------------------\n    // Public\n    //--------------------------------------------------------------------------\n\n    get aggregate() {\n        return this.meta.aggregate;\n    }\n    get date() {\n        return this.meta.date;\n    }\n    get canCreate() {\n        return this.meta.canCreate;\n    }\n    get canDelete() {\n        return this.meta.canDelete;\n    }\n    get canEdit() {\n        return this.meta.canEdit && !this.meta.fields[this.meta.fieldMapping.date_start].readonly;\n    }\n    get dateStartType() {\n        return this.fields[this.fieldMapping.date_start].type;\n    }\n    get dateStopType() {\n        if (this.fieldMapping.date_stop) {\n            return this.fields[this.fieldMapping.date_stop].type;\n        }\n        return null;\n    }\n    get eventLimit() {\n        return this.meta.eventLimit;\n    }\n    get exportedState() {\n        return { date: this.meta.date };\n    }\n    get fieldMapping() {\n        return this.meta.fieldMapping;\n    }\n    get fields() {\n        return this.meta.fields;\n    }\n    get filterSections() {\n        return Object.values(this.data.filterSections);\n    }\n    get firstDayOfWeek() {\n        return this.meta.firstDayOfWeek;\n    }\n    get formViewId() {\n        return this.meta.formViewId;\n    }\n    get hasAllDaySlot() {\n        return (\n            this.meta.fieldMapping.all_day ||\n            this.meta.fields[this.meta.fieldMapping.date_start].type === \"date\"\n        );\n    }\n    get hasEditDialog() {\n        return this.meta.hasEditDialog;\n    }\n    get hasMultiCreate() {\n        return !!this.meta.multiCreateView && !this.env.isSmall && this.meta.scale === \"month\";\n    }\n    get hasQuickCreate() {\n        return this.meta.quickCreate;\n    }\n    get isDateHidden() {\n        return this.meta.isDateHidden;\n    }\n    get isTimeHidden() {\n        return this.meta.isTimeHidden;\n    }\n    get monthOverflow() {\n        return this.meta.monthOverflow;\n    }\n    get popoverFieldNodes() {\n        return this.meta.popoverFieldNodes;\n    }\n    get activeFields() {\n        return this.meta.activeFields;\n    }\n    get rangeEnd() {\n        return this.data.range.end;\n    }\n    get rangeStart() {\n        return this.data.range.start;\n    }\n    get records() {\n        return this.data.records;\n    }\n    get resModel() {\n        return this.meta.resModel;\n    }\n    get scale() {\n        return this.meta.scale;\n    }\n    get scales() {\n        return this.meta.scales;\n    }\n    get showDatePicker() {\n        return this.meta.showDatePicker;\n    }\n    get showMultiCreateTimeRange() {\n        return this.dateStartType === \"datetime\" && this.dateStopType === \"datetime\";\n    }\n    get storageKey() {\n        return `scaleOf-viewId-${this.env.config.viewId}`;\n    }\n    get unusualDays() {\n        return this.data.unusualDays;\n    }\n    get quickCreateFormViewId() {\n        return this.meta.quickCreateViewId;\n    }\n    get defaultFilterLabel() {\n        return _t(\"Undefined\");\n    }\n\n    //--------------------------------------------------------------------------\n\n    async createFilter(fieldName, filterValue) {\n        const info = this.meta.filtersInfo[fieldName];\n        if (!info || !info.writeFieldName || !info.writeResModel) {\n            return;\n        }\n\n        const normalizedFilterValue = Array.isArray(filterValue) ? filterValue : [filterValue];\n        const dataArray = normalizedFilterValue.map((value) => {\n            const data = {\n                user_id: user.userId,\n                [info.writeFieldName]: value,\n            };\n            if (info.filterFieldName) {\n                data[info.filterFieldName] = true;\n            }\n            return data;\n        });\n\n        await this.orm.create(info.writeResModel, dataArray);\n        await this.load();\n    }\n    async createRecord(record) {\n        const rawRecord = this.buildRawRecord(record);\n        const context = this.makeContextDefaults(rawRecord);\n        await this.orm.create(this.meta.resModel, [rawRecord], { context });\n        await this.load();\n    }\n\n    /**\n     * Create multi records of the specify dates and values.\n     * Optionally time range can be specified to set the start and end time.\n     * Also, if there is a filter section, the first filter section will be chosen as additional value for the record.\n     *\n     * @param {Object} multiCreateData\n     * @param {DateTime[]} dates array of Date\n     * @returns {Promise<*>}\n     */\n    async multiCreateRecords(multiCreateData, dates) {\n        const records = [];\n        const values = await multiCreateData.record.getChanges();\n        const timeRange = multiCreateData.timeRange;\n\n        // we deliberately only use the values of the first filter section, to avoid combinatorial explosion\n        const [section] = this.filterSections;\n        for (const date of dates) {\n            const initialRecordValue = {};\n            if (this.showMultiCreateTimeRange) {\n                initialRecordValue.start = date.plus(timeRange.start.toObject());\n                initialRecordValue.end = date.plus(timeRange.end.toObject());\n            } else {\n                initialRecordValue.start = date;\n            }\n            const rawRecord = this.buildRawRecord(initialRecordValue);\n            if (!section) {\n                records.push({\n                    ...rawRecord,\n                    ...values,\n                });\n                continue;\n            }\n            for (const filter of section.filters) {\n                if (filter.active && filter.type === \"record\") {\n                    records.push({\n                        ...rawRecord,\n                        ...values,\n                        [section.fieldName]: filter.value,\n                    });\n                }\n            }\n        }\n        if (records.length) {\n            const createdRecords = await this.orm.create(this.meta.resModel, records, {\n                context: this.meta.context,\n            });\n            await this.load();\n            return createdRecords;\n        }\n        return [];\n    }\n\n    async unlinkFilter(fieldName, recordId) {\n        const info = this.meta.filtersInfo[fieldName];\n        const section = this.data.filterSections[fieldName];\n        if (section) {\n            // remove the filter directly, to provide a direct feedback to the user\n            this.keepLast.add(Promise.resolve());\n            section.filters = section.filters.filter((f) => f.recordId !== recordId);\n        }\n        if (info && info.writeResModel) {\n            await this.orm.unlink(info.writeResModel, [recordId]);\n            await this.debouncedLoad();\n        }\n    }\n    async unlinkRecord(recordId) {\n        await this.orm.unlink(this.meta.resModel, [recordId]);\n        await this.load();\n    }\n\n    async unlinkRecords(recordsId) {\n        if (recordsId.length) {\n            await this.orm.unlink(this.meta.resModel, recordsId);\n            await this.load();\n        }\n    }\n\n    async updateFilters(fieldName, filters, active) {\n        // update filters directly, to provide a direct feedback to the user\n        this.keepLast.add(Promise.resolve());\n        for (const filter of filters) {\n            filter.active = active;\n        }\n        const info = this.meta.filtersInfo[fieldName];\n        if (info && info.writeFieldName && info.writeResModel && info.filterFieldName) {\n            const userFilter = filters.find((f) => f.type === \"user\");\n            if (userFilter) {\n                userFilter.active = active;\n            }\n            const filterIds = filters.filter((f) => f.type === \"record\").map((f) => f.recordId);\n            if (filterIds) {\n                const data = {\n                    [info.filterFieldName]: active,\n                };\n                const context = this.meta.context;\n                await this.orm.write(info.writeResModel, filterIds, data, { context });\n            }\n        }\n        await this.debouncedLoad();\n    }\n    async updateRecord(record, options = {}) {\n        const rawRecord = this.buildRawRecord(record, options);\n        delete rawRecord.name; // name is immutable.\n        await this.orm.write(this.meta.resModel, [record.id], rawRecord, {\n            context: this.meta.context,\n        });\n        await this.load();\n    }\n\n    //--------------------------------------------------------------------------\n    getAllDayDates(start, end) {\n        return [start.set({ hours: 7 }), end.set({ hours: 19 })];\n    }\n\n    buildRawRecord(partialRecord, options = {}) {\n        const data = {};\n        data[this.meta.fieldMapping.create_name_field || \"name\"] = partialRecord.title;\n\n        let start = partialRecord.start;\n        let end = partialRecord.end;\n\n        if (!end || !end.isValid) {\n            // Set end date if not existing\n            if (partialRecord.isAllDay) {\n                end = start;\n            } else {\n                // in week mode or day mode, convert allday event to event\n                end = start.plus({ hours: options.duration_hour || 1 });\n            }\n        }\n\n        const isDateEvent = this.dateStartType === \"date\";\n        // An \"all day\" event without the \"all_day\" option is not considered\n        // as a 24h day. It's just a part of the day (by default: 7h-19h).\n        if (partialRecord.isAllDay) {\n            if (!this.hasAllDaySlot && !isDateEvent && !partialRecord.id) {\n                // default hours in the user's timezone\n                [start, end] = this.getAllDayDates(start, end);\n            }\n        }\n\n        if (this.meta.fieldMapping.all_day) {\n            data[this.meta.fieldMapping.all_day] = partialRecord.isAllDay;\n        }\n\n        data[this.meta.fieldMapping.date_start] =\n            (partialRecord.isAllDay && this.hasAllDaySlot ? \"date\" : this.dateStartType) === \"date\"\n                ? serializeDate(start)\n                : serializeDateTime(start);\n\n        if (this.meta.fieldMapping.date_stop) {\n            data[this.meta.fieldMapping.date_stop] =\n                (partialRecord.isAllDay && this.hasAllDaySlot ? \"date\" : this.dateStartType) ===\n                \"date\"\n                    ? serializeDate(end)\n                    : serializeDateTime(end);\n        }\n\n        if (this.meta.fieldMapping.date_delay) {\n            if (this.meta.scale !== \"month\" || !options.moved) {\n                data[this.meta.fieldMapping.date_delay] = end.diff(start, \"hours\").hours;\n            }\n        }\n        return data;\n    }\n    makeContextDefaults(rawRecord) {\n        const { fieldMapping, scale } = this.meta;\n\n        const context = { ...this.meta.context };\n        const fieldNames = [\n            fieldMapping.create_name_field || \"name\",\n            fieldMapping.date_start,\n            fieldMapping.date_stop,\n            fieldMapping.date_delay,\n            fieldMapping.all_day || \"allday\",\n        ];\n        for (const fieldName of fieldNames) {\n            // fieldName could be in rawRecord but not defined\n            if (rawRecord[fieldName] !== undefined) {\n                context[`default_${fieldName}`] = rawRecord[fieldName];\n            }\n        }\n        if ([\"month\", \"year\"].includes(scale)) {\n            context[`default_${fieldMapping.all_day || \"allday\"}`] = true;\n        }\n\n        return context;\n    }\n\n    //--------------------------------------------------------------------------\n    // Protected\n    //--------------------------------------------------------------------------\n\n    /**\n     * @protected\n     */\n    async updateData(data) {\n        data.range = this.computeRange();\n        let unusualDaysProm;\n        if (this.meta.showUnusualDays) {\n            unusualDaysProm = this.loadUnusualDays(data).then((unusualDays) => {\n                data.unusualDays = unusualDays;\n            });\n        }\n\n        const { sections, dynamicFiltersInfo } = await this.loadFilters(data);\n\n        // Load records and dynamic filters only with fresh filters\n        data.filterSections = sections;\n        data.records = await this.loadRecords(data);\n        const dynamicSections = await this.loadDynamicFilters(data, dynamicFiltersInfo);\n\n        // Apply newly computed filter sections\n        Object.assign(data.filterSections, dynamicSections);\n\n        // Remove records that don't match dynamic filters\n        for (const [fieldName, filterInfo] of Object.entries(dynamicSections)) {\n            const field = this.meta.fields[fieldName];\n            if (!field) {\n                continue;\n            }\n            const inactiveFilters = filterInfo.filters.filter((f) => !f.active);\n            if (!inactiveFilters.length) {\n                continue;\n            }\n            for (const [recordId, record] of Object.entries(data.records)) {\n                const rawValue = record.rawRecord[fieldName];\n                let remove = false;\n                if ([\"many2many\", \"one2many\"].includes(field.type)) {\n                    const inactiveFilterVals = inactiveFilters.map((filter) => filter.value);\n                    remove = intersection(rawValue, inactiveFilterVals).length === rawValue.length;\n                } else {\n                    const recordValue = Array.isArray(rawValue) ? rawValue[0] : rawValue;\n                    remove = inactiveFilters.some((filter) => filter.value === recordValue);\n                }\n                if (remove) {\n                    delete data.records[recordId];\n                }\n            }\n        }\n\n        await unusualDaysProm;\n\n        // Compute aggregate values\n        if (this.aggregate) {\n            for (const [fieldName, { filters }] of Object.entries(data.filterSections)) {\n                const aggregates = this.computeAggregatedValues(fieldName, data);\n                for (const filter of filters) {\n                    filter.aggregatedValue = aggregates[filter.value] || 0;\n                }\n            }\n        }\n    }\n\n    //--------------------------------------------------------------------------\n\n    /**\n     * @protected\n     */\n    computeRange() {\n        const { scale, date, firstDayOfWeek } = this.meta;\n        let start = date;\n        let end = date;\n\n        if (scale !== \"week\") {\n            // startOf(\"week\") does not depend on locale and will always give the\n            // \"Monday\" of the week...\n            start = start.startOf(scale);\n            end = end.endOf(scale);\n        }\n\n        if (scale === \"week\" || (scale === \"month\" && this.monthOverflow)) {\n            const currentWeekOffset = (start.weekday - firstDayOfWeek + 7) % 7;\n            start = start.minus({ days: currentWeekOffset });\n            end = start.plus({ weeks: scale === \"week\" ? 1 : 6, days: -1 });\n        }\n\n        start = start.startOf(\"day\");\n        end = end.endOf(\"day\");\n\n        return { start, end };\n    }\n\n    //--------------------------------------------------------------------------\n\n    /**\n     * @param {string} fieldName\n     * @param {Object} [data=this.data]\n     * @returns Object\n     */\n    computeAggregatedValues(fieldName, data = this.data) {\n        const records = Object.values(data.records);\n        const fieldType = this.meta.fields[fieldName].type;\n        const groups = groupBy(records, ({ rawRecord }) => {\n            const rawValue = rawRecord[fieldName];\n            // FIXME: many2many not supported, but not supported for filters either\n            return fieldType === \"many2one\" ? rawValue?.[0] || false : rawValue;\n        });\n        const aggregates = {};\n        const [aggregateField, aggregator] = this.aggregate.split(\":\");\n        for (const group in groups) {\n            const values = groups[group].map(({ rawRecord }) => rawRecord[aggregateField]);\n            aggregates[group] = formatFloat(computeAggregatedValue(values, aggregator), {\n                trailingZeros: false,\n            });\n        }\n        return aggregates;\n    }\n    /**\n     * @protected\n     */\n    computeDomain(data) {\n        return [\n            ...this.meta.domain,\n            ...this.computeRangeDomain(data),\n            ...this.computeFiltersDomain(data),\n        ];\n    }\n    /**\n     * @protected\n     */\n    computeFiltersDomain(data) {\n        // List authorized values for every field\n        // fields with an active \"all\" filter are skipped\n        const authorizedValues = {};\n        const avoidValues = {};\n\n        for (const [fieldName, filterSection] of Object.entries(data.filterSections)) {\n            const filterSectionInfo = this.meta.filtersInfo[fieldName];\n            // Loop over subfilters to complete authorizedValues\n            for (const filter of filterSection.filters) {\n                if (filterSectionInfo.writeResModel) {\n                    if (!authorizedValues[fieldName]) {\n                        authorizedValues[fieldName] = [];\n                    }\n                    if (filter.active) {\n                        authorizedValues[fieldName].push(filter.value);\n                    }\n                } else {\n                    if (!filter.active) {\n                        if (!avoidValues[fieldName]) {\n                            avoidValues[fieldName] = [];\n                        }\n                        avoidValues[fieldName].push(filter.value);\n                    }\n                }\n            }\n        }\n\n        // Compute the domain\n        const domain = [];\n        for (const field in authorizedValues) {\n            domain.push([field, \"in\", authorizedValues[field]]);\n        }\n        for (const field in avoidValues) {\n            if (avoidValues[field].length > 0) {\n                domain.push([field, \"not in\", avoidValues[field]]);\n            }\n        }\n        return domain;\n    }\n    /**\n     * @protected\n     */\n    computeRangeDomain(data) {\n        const { fieldMapping } = this.meta;\n        const serializeFn = this.dateStartType === \"date\" ? serializeDate : serializeDateTime;\n        const formattedEnd = serializeFn(data.range.end);\n        const formattedStart = serializeFn(data.range.start);\n\n        const domain = [[fieldMapping.date_start, \"<=\", formattedEnd]];\n        if (fieldMapping.date_stop) {\n            domain.push([fieldMapping.date_stop, \">=\", formattedStart]);\n        } else if (!fieldMapping.date_delay) {\n            domain.push([fieldMapping.date_start, \">=\", formattedStart]);\n        }\n        return domain;\n    }\n\n    //--------------------------------------------------------------------------\n\n    /**\n     * @protected\n     */\n    fetchUnusualDays(data) {\n        return this.orm.call(this.meta.resModel, \"get_unusual_days\", [\n            serializeDateTime(data.range.start),\n            serializeDateTime(data.range.end),\n        ]);\n    }\n    /**\n     * @protected\n     */\n    async loadUnusualDays(data) {\n        const unusualDays = await this._unusualDaysCache.read(data);\n        return Object.entries(unusualDays)\n            .filter((entry) => entry[1])\n            .map((entry) => entry[0]);\n    }\n\n    //--------------------------------------------------------------------------\n\n    /**\n     * @protected\n     */\n    fetchRecords(data) {\n        const { context, fieldNames, resModel } = this.meta;\n        return this.orm.searchRead(\n            resModel,\n            this.computeDomain(data),\n            [...new Set([...fieldNames, ...Object.keys(this.meta.activeFields)])],\n            { context }\n        );\n    }\n    /**\n     * @protected\n     */\n    async loadRecords(data) {\n        const rawRecords = await this.fetchRecords(data);\n        const records = {};\n        for (const rawRecord of rawRecords) {\n            records[rawRecord.id] = this.normalizeRecord(rawRecord);\n        }\n        return records;\n    }\n    /**\n     * @protected\n     * @param {Record<string, any>} rawRecord\n     */\n    normalizeRecord(rawRecord) {\n        const { fields, fieldMapping, isTimeHidden } = this.meta;\n\n        const startType = fields[fieldMapping.date_start].type;\n        const isAllDay =\n            startType === \"date\" ||\n            (fieldMapping.all_day && rawRecord[fieldMapping.all_day]) ||\n            false;\n        let start = isAllDay\n            ? deserializeDate(rawRecord[fieldMapping.date_start])\n            : deserializeDateTime(rawRecord[fieldMapping.date_start]);\n\n        let end = start;\n        let endType = startType;\n        if (fieldMapping.date_stop) {\n            endType = fields[fieldMapping.date_stop].type;\n            end = isAllDay\n                ? deserializeDate(rawRecord[fieldMapping.date_stop])\n                : deserializeDateTime(rawRecord[fieldMapping.date_stop]);\n        }\n\n        const duration = rawRecord[fieldMapping.date_delay] || 1;\n\n        if (isAllDay) {\n            start = start.startOf(\"day\");\n            end = end.startOf(\"day\");\n        }\n        if (!fieldMapping.date_stop && duration) {\n            end = start.plus({ hours: duration });\n        }\n\n        const showTime =\n            !(fieldMapping.all_day && rawRecord[fieldMapping.all_day]) &&\n            startType !== \"date\" &&\n            start.day === end.day;\n\n        const colorValue = rawRecord[fieldMapping.color];\n        const colorIndex = Array.isArray(colorValue) ? colorValue[0] : colorValue;\n\n        const title = rawRecord[fieldMapping.create_name_field || \"display_name\"];\n\n        return {\n            id: rawRecord.id,\n            title,\n            isAllDay,\n            start,\n            startType,\n            end,\n            endType,\n            duration,\n            colorIndex,\n            isHatched: rawRecord[\"is_hatched\"] || false,\n            isStriked: rawRecord[\"is_striked\"] || false,\n            isTimeHidden: isTimeHidden || !showTime,\n            isMonth: this.meta.scale === \"month\",\n            isSmall: this.env.isSmall,\n            rawRecord,\n        };\n    }\n\n    /**\n     * @protected\n     */\n    addFilterFields(record, filterInfo) {\n        return {\n            colorIndex: record.colorIndex,\n        };\n    }\n    //--------------------------------------------------------------------------\n\n    /**\n     * @protected\n     */\n    fetchFilters(resModel, fieldNames) {\n        return this.orm.searchRead(resModel, [[\"user_id\", \"=\", user.userId]], fieldNames);\n    }\n\n    getLocalStorageScale() {\n        const localScaleId = browser.localStorage.getItem(this.storageKey);\n        return this.meta.scales.includes(localScaleId) ? localScaleId : this.meta.scale;\n    }\n\n    /**\n     * @protected\n     */\n    async loadFilters(data) {\n        const previousSections = data.filterSections;\n        const sections = {};\n        const dynamicFiltersInfo = {};\n        const proms = [];\n        for (const [fieldName, filterInfo] of Object.entries(this.meta.filtersInfo)) {\n            const previousSection = previousSections[fieldName];\n            if (filterInfo.writeResModel) {\n                const prom = this.loadFilterSection(fieldName, filterInfo, previousSection).then(\n                    (result) => {\n                        sections[fieldName] = result;\n                    }\n                );\n                proms.push(prom);\n            } else {\n                dynamicFiltersInfo[fieldName] = { filterInfo, previousSection };\n            }\n        }\n        await Promise.all(proms);\n        return { sections, dynamicFiltersInfo };\n    }\n    /**\n     * @protected\n     */\n    async loadFilterSection(fieldName, filterInfo, previousSection) {\n        const { filterFieldName, writeFieldName, writeResModel } = filterInfo;\n        const fields = [writeFieldName, filterFieldName].filter(Boolean);\n        const rawFilters = await this.fetchFilters(writeResModel, fields);\n        const previousFilters = previousSection ? previousSection.filters : [];\n\n        const filters = rawFilters.map((rawFilter) => {\n            const previousRecordFilter = previousFilters.find(\n                (f) => f.type === \"record\" && f.recordId === rawFilter.id\n            );\n            return this.makeFilterRecord(filterInfo, previousRecordFilter, rawFilter);\n        });\n\n        const field = this.meta.fields[fieldName];\n        const isUserOrPartner = [\"res.users\", \"res.partner\"].includes(field.relation);\n        if (isUserOrPartner) {\n            const previousUserFilter = previousFilters.find((f) => f.type === \"user\");\n            filters.push(\n                this.makeFilterUser(filterInfo, previousUserFilter, fieldName, rawFilters)\n            );\n        }\n\n        return {\n            label: filterInfo.label,\n            fieldName,\n            filters,\n            avatar: {\n                field: filterInfo.avatarFieldName,\n                model: filterInfo.resModel,\n            },\n            hasAvatar: !!filterInfo.avatarFieldName,\n            write: {\n                field: writeFieldName,\n                model: writeResModel,\n            },\n            canAddFilter: !!filterInfo.writeResModel,\n            context: makeContext([filterInfo.context, this.meta.context]),\n        };\n    }\n    /**\n     * @protected\n     */\n    async loadDynamicFilters(data, filtersInfo) {\n        const sections = {};\n        const proms = [];\n        for (const [fieldName, { filterInfo, previousSection }] of Object.entries(filtersInfo)) {\n            const prom = this.loadDynamicFilterSection(\n                data,\n                fieldName,\n                filterInfo,\n                previousSection\n            ).then((result) => {\n                sections[fieldName] = result;\n            });\n            proms.push(prom);\n        }\n        await Promise.all(proms);\n        return sections;\n    }\n    /**\n     * @protected\n     */\n    async loadDynamicFilterSection(data, fieldName, filterInfo, previousSection) {\n        const { fields, fieldMapping } = this.meta;\n        const field = fields[fieldName];\n        const previousFilters = previousSection ? previousSection.filters : [];\n\n        const rawFilters = Object.values(data.records).reduce((filters, record) => {\n            const rawValues = [\"many2many\", \"one2many\"].includes(field.type)\n                ? record.rawRecord[fieldName]\n                : [record.rawRecord[fieldName]];\n\n            for (const rawValue of rawValues) {\n                const value = Array.isArray(rawValue) ? rawValue[0] : rawValue;\n                if (!filters.find((f) => f.id === value)) {\n                    filters.push({\n                        id: value,\n                        [fieldName]: rawValue,\n                        ...this.addFilterFields(record, filterInfo),\n                    });\n                }\n            }\n            return filters;\n        }, []);\n\n        const isX2Many = [\"many2many\", \"one2many\"].includes(field.type);\n\n        const relatedIds = rawFilters.map((f) => f.id).filter((id) => id);\n        let rawColors = [];\n        if (relatedIds.length > 0 && field.relation) {\n            const fieldsToFetch = [];\n            const { colorFieldName } = filterInfo;\n            const shouldFetchColor =\n                colorFieldName &&\n                (!fieldMapping.color ||\n                    `${fieldName}.${colorFieldName}` !== fields[fieldMapping.color].related);\n            if (shouldFetchColor) {\n                fieldsToFetch.push(colorFieldName);\n            }\n            if (isX2Many) {\n                fieldsToFetch.push(\"display_name\");\n            }\n            if (fieldsToFetch.length > 0) {\n                const records = await this.orm.searchRead(\n                    field.relation,\n                    [[\"id\", \"in\", relatedIds]],\n                    fieldsToFetch\n                );\n                if (isX2Many) {\n                    const nameById = Object.fromEntries(records.map((r) => [r.id, r.display_name]));\n                    for (const rawFilter of rawFilters) {\n                        const id = rawFilter.id;\n                        if (!id || !nameById[id]) {\n                            continue;\n                        }\n                        rawFilter[fieldName] = [id, nameById[id]];\n                    }\n                }\n                if (shouldFetchColor) {\n                    rawColors = records;\n                }\n            }\n        }\n\n        const filters = rawFilters.map((rawFilter) => {\n            const previousDynamicFilter = previousFilters.find(\n                (f) => f.type === \"dynamic\" && f.value === rawFilter.id\n            );\n            return this.makeFilterDynamic(\n                filterInfo,\n                previousDynamicFilter,\n                fieldName,\n                rawFilter,\n                rawColors\n            );\n        });\n\n        return {\n            label: filterInfo.label,\n            fieldName,\n            filters,\n            avatar: {\n                field: filterInfo.avatarFieldName,\n                model: filterInfo.resModel,\n            },\n            hasAvatar: !!filterInfo.avatarFieldName,\n            write: {\n                field: filterInfo.writeFieldName,\n                model: filterInfo.writeResModel,\n            },\n            canAddFilter: !!filterInfo.writeResModel,\n        };\n    }\n    /**\n     * @protected\n     */\n    makeFilterDynamic(filterInfo, previousFilter, fieldName, rawFilter, rawColors) {\n        const { fieldMapping, fields } = this.meta;\n        const rawValue = rawFilter[fieldName];\n        const value = Array.isArray(rawValue) ? rawValue[0] : rawValue;\n        const field = fields[fieldName];\n        const isX2Many = [\"many2many\", \"one2many\"].includes(field.type);\n        const formatter = registry.category(\"formatters\").get(isX2Many ? \"many2one\" : field.type);\n\n        const { colorFieldName } = filterInfo;\n        const colorField = fields[fieldMapping.color];\n        const hasFilterColorAttr = !!colorFieldName;\n        const sameRelatedModel =\n            colorField &&\n            (colorField.relation === field.relation ||\n                (colorField.related && colorField.related.startsWith(`${fieldName}.`)));\n        let colorIndex = null;\n        if (hasFilterColorAttr || sameRelatedModel) {\n            colorIndex = rawFilter.colorIndex;\n        }\n        if (rawColors.length) {\n            const rawColor = rawColors.find(({ id }) => id === value);\n            colorIndex = rawColor ? rawColor[colorFieldName] : 0;\n        }\n\n        return {\n            type: \"dynamic\",\n            recordId: null,\n            value,\n            label: formatter(rawValue, { field }) || this.defaultFilterLabel,\n            active: previousFilter ? previousFilter.active : true,\n            canRemove: false,\n            colorIndex,\n            hasAvatar: !!value,\n        };\n    }\n    /**\n     * @protected\n     */\n    makeFilterRecord(filterInfo, previousFilter, rawRecord) {\n        const { colorFieldName, filterFieldName, writeFieldName } = filterInfo;\n        const { fields, fieldMapping } = this.meta;\n        const raw = rawRecord[writeFieldName];\n        const value = Array.isArray(raw) ? raw[0] : raw;\n        const field = fields[writeFieldName];\n        const isX2Many = [\"many2many\", \"one2many\"].includes(field.type);\n        const formatter = registry.category(\"formatters\").get(isX2Many ? \"many2one\" : field.type);\n\n        const colorField = fields[fieldMapping.color];\n        const colorValue =\n            colorField &&\n            (() => {\n                const sameRelatedModel = colorField.relation === field.relation;\n                const sameRelatedField =\n                    colorField.related === `${writeFieldName}.${colorFieldName}`;\n                const shouldHaveColor = sameRelatedModel || sameRelatedField;\n                const colorToUse = raw ? value : rawRecord[fieldMapping.color];\n                return shouldHaveColor ? colorToUse : null;\n            })();\n        const colorIndex = Array.isArray(colorValue) ? colorValue[0] : colorValue;\n\n        let active = false;\n        if (filterFieldName) {\n            active = rawRecord[filterFieldName];\n        } else if (previousFilter) {\n            active = previousFilter.active;\n        }\n        return {\n            type: \"record\",\n            recordId: rawRecord.id,\n            value,\n            label: formatter(raw),\n            active,\n            canRemove: true,\n            colorIndex,\n            hasAvatar: !!value,\n        };\n    }\n    /**\n     * @protected\n     */\n    makeFilterUser(filterInfo, previousFilter, fieldName, rawRecords) {\n        const field = this.meta.fields[fieldName];\n        const userFieldName = field.relation === \"res.partner\" ? \"partnerId\" : \"userId\";\n        const value = user[userFieldName];\n\n        let colorIndex = value;\n        const rawRecord = rawRecords.find((r) => r[filterInfo.writeFieldName][0] === value);\n        if (filterInfo.colorFieldName && rawRecord) {\n            const colorValue = rawRecord[filterInfo.colorFieldName];\n            colorIndex = Array.isArray(colorValue) ? colorValue[0] : colorValue;\n        }\n\n        return {\n            type: \"user\",\n            recordId: null,\n            value,\n            label: user.name,\n            active: previousFilter ? previousFilter.active : true,\n            canRemove: false,\n            colorIndex,\n            hasAvatar: !!value,\n        };\n    }\n}\n", "import { ActionSwiper } from \"@web/core/action_swiper/action_swiper\";\nimport { CalendarCommonRenderer } from \"./calendar_common/calendar_common_renderer\";\nimport { CalendarYearRenderer } from \"./calendar_year/calendar_year_renderer\";\n\nimport { Component } from \"@odoo/owl\";\n\nexport class CalendarRenderer extends Component {\n    static template = \"web.CalendarRenderer\";\n    static components = {\n        day: CalendarCommonRenderer,\n        week: CalendarCommonRenderer,\n        month: CalendarCommonRenderer,\n        year: CalendarYearRenderer,\n        ActionSwiper,\n    };\n    static props = {\n        model: Object,\n        isWeekendVisible: Boolean,\n        createRecord: Function,\n        editRecord: Function,\n        deleteRecord: Function,\n        setDate: Function,\n        callbackRecorder: Object,\n        onSquareSelection: Function,\n        cleanSquareSelection: Function,\n    };\n    get concreteRenderer() {\n        return this.constructor.components[this.props.model.scale];\n    }\n    get concreteRendererProps() {\n        if (this.props.model.scale === \"year\") {\n            return {\n                model: this.props.model,\n                isWeekendVisible: this.props.isWeekendVisible,\n                createRecord: this.props.createRecord,\n                editRecord: this.props.editRecord,\n                deleteRecord: this.props.deleteRecord,\n            };\n        }\n        return this.props;\n    }\n    get calendarKey() {\n        return `${this.props.model.scale}_${this.props.model.date.valueOf()}`;\n    }\n    get actionSwiperProps() {\n        return {\n            onLeftSwipe: this.env.isSmall\n                ? { action: () => this.props.setDate(\"next\") }\n                : undefined,\n            onRightSwipe: this.env.isSmall\n                ? { action: () => this.props.setDate(\"previous\") }\n                : undefined,\n            animationOnMove: false,\n            animationType: \"forwards\",\n            swipeDistanceRatio: 6,\n            swipeInvalid: () => Boolean(document.querySelector(\".o_event.fc-mirror\")),\n        };\n    }\n}\n", "import { Component } from \"@odoo/owl\";\nimport { DateTimePicker } from \"@web/core/datetime/datetime_picker\";\nimport { CalendarFilterSection } from \"@web/views/calendar/calendar_filter_section/calendar_filter_section\";\n\nexport class CalendarSidePanel extends Component {\n    static components = {\n        DatePicker: DateTimePicker,\n        FilterSection: CalendarFilterSection,\n    };\n    static template = \"web.CalendarSidePanel\";\n    static props = [\"model\"];\n\n    get datePickerProps() {\n        return {\n            type: \"date\",\n            showWeekNumbers: false,\n            maxPrecision: \"days\",\n            daysOfWeekFormat: \"narrow\",\n            onSelect: (date) => {\n                let scale = \"week\";\n\n                if (this.props.model.date.hasSame(date, \"day\")) {\n                    const scales = [\"month\", \"week\", \"day\"];\n                    scale = scales[(scales.indexOf(this.props.model.scale) + 1) % scales.length];\n                } else {\n                    // Check if dates are on the same week\n                    // As a.hasSame(b, \"week\") does not depend on locale and week always starts on Monday,\n                    // we are comparing derivated dates instead to take this into account.\n                    const currentDate =\n                        this.props.model.date.weekday === 7\n                            ? this.props.model.date.plus({ day: 1 })\n                            : this.props.model.date;\n                    const pickedDate = date.weekday === 7 ? date.plus({ day: 1 }) : date;\n\n                    // a.hasSame(b, \"week\") does not depend on locale and week alway starts on Monday\n                    if (currentDate.hasSame(pickedDate, \"week\")) {\n                        scale = \"day\";\n                    }\n                }\n\n                this.props.model.load({ scale, date });\n            },\n            value: this.props.model.date,\n        };\n    }\n    get filterPanelProps() {\n        return {\n            model: this.props.model,\n        };\n    }\n\n    get showDatePicker() {\n        return this.props.model.showDatePicker && !this.env.isSmall;\n    }\n}\n", "import { registry } from \"@web/core/registry\";\nimport { CalendarRenderer } from \"./calendar_renderer\";\nimport { CalendarArchParser } from \"./calendar_arch_parser\";\nimport { CalendarModel } from \"./calendar_model\";\nimport { CalendarController } from \"./calendar_controller\";\n\nexport const calendarView = {\n    type: \"calendar\",\n\n    searchMenuTypes: [\"filter\", \"favorite\"],\n\n    ArchParser: CalendarArchParser,\n    Controller: CalendarController,\n    Model: CalendarModel,\n    Renderer: CalendarRenderer,\n\n    buttonTemplate: \"web.CalendarController.controlButtons\",\n\n    props: (props, view) => {\n        const { ArchParser } = view;\n        const { arch, relatedModels, resModel } = props;\n        const archInfo = new ArchParser().parse(arch, relatedModels, resModel);\n        return {\n            ...props,\n            Model: view.Model,\n            Renderer: view.Renderer,\n            buttonTemplate: view.buttonTemplate,\n            archInfo,\n        };\n    },\n};\n\nregistry.category(\"views\").add(\"calendar\", calendarView);\n", "import { Dialog } from \"@web/core/dialog/dialog\";\nimport { formatDate } from \"@web/core/l10n/dates\";\nimport { getColor } from \"../utils\";\nimport { getFormattedDateSpan } from \"@web/views/calendar/utils\";\n\nimport { Component } from \"@odoo/owl\";\n\nexport class CalendarYearPopover extends Component {\n    static components = { Dialog };\n    static template = \"web.CalendarYearPopover\";\n    static subTemplates = {\n        popover: \"web.CalendarYearPopover.popover\",\n        body: \"web.CalendarYearPopover.body\",\n        footer: \"web.CalendarYearPopover.footer\",\n        record: \"web.CalendarYearPopover.record\",\n    };\n    static props = {\n        close: Function,\n        date: true,\n        model: Object,\n        records: Array,\n        createRecord: Function,\n        deleteRecord: Function,\n        editRecord: Function,\n    };\n\n    get recordGroups() {\n        return this.computeRecordGroups();\n    }\n\n    get dialogTitle() {\n        return formatDate(this.props.date, { format: \"DDD\" });\n    }\n\n    computeRecordGroups() {\n        const recordGroups = this.groupRecords();\n        return this.getSortedRecordGroups(recordGroups);\n    }\n    groupRecords() {\n        const recordGroups = {};\n        for (const record of this.props.records) {\n            const start = record.start;\n            const end = record.end;\n\n            const duration = end.diff(start, \"days\").days;\n            const modifiedRecord = Object.create(record);\n            modifiedRecord.startHour =\n                !record.isAllDay && duration < 1 ? start.toFormat(\"HH:mm\") : \"\";\n\n            const formattedDate = getFormattedDateSpan(start, end);\n            if (!(formattedDate in recordGroups)) {\n                recordGroups[formattedDate] = {\n                    title: formattedDate,\n                    start,\n                    end,\n                    records: [],\n                };\n            }\n            recordGroups[formattedDate].records.push(modifiedRecord);\n        }\n        return Object.values(recordGroups);\n    }\n    getRecordClass(record) {\n        const { colorIndex } = record;\n        const color = getColor(colorIndex);\n        if (color && typeof color === \"number\") {\n            return `o_calendar_color_${color}`;\n        }\n        return \"\";\n    }\n    getRecordStyle(record) {\n        const { colorIndex } = record;\n        const color = getColor(colorIndex);\n        if (color && typeof color === \"string\") {\n            return `background-color: ${color};`;\n        }\n        return \"\";\n    }\n    getSortedRecordGroups(recordGroups) {\n        return recordGroups.sort((a, b) => {\n            if (a.start.hasSame(a.end, \"days\")) {\n                return Number.MIN_SAFE_INTEGER;\n            } else if (b.start.hasSame(b.end, \"days\")) {\n                return Number.MAX_SAFE_INTEGER;\n            } else if (a.start.toMillis() - b.start.toMillis() === 0) {\n                return a.end.toMillis() - b.end.toMillis();\n            }\n            return a.start.toMillis() - b.start.toMillis();\n        });\n    }\n\n    onCreateButtonClick() {\n        this.props.createRecord({\n            start: this.props.date,\n            isAllDay: true,\n        });\n        this.props.close();\n    }\n    onRecordClick(record) {\n        this.props.editRecord(record);\n        this.props.close();\n    }\n}\n", "import { getLocalYearAndWeek } from \"@web/core/l10n/dates\";\nimport { localization } from \"@web/core/l10n/localization\";\nimport { convertRecordToEvent, getColor } from \"@web/views/calendar/utils\";\nimport { useCalendarPopover } from \"@web/views/calendar/hooks/calendar_popover_hook\";\nimport { useFullCalendar } from \"@web/views/calendar/hooks/full_calendar_hook\";\nimport { makeWeekColumn } from \"@web/views/calendar/calendar_common/calendar_common_week_column\";\nimport { CalendarYearPopover } from \"@web/views/calendar/calendar_year/calendar_year_popover\";\n\nimport { Component, useEffect, useRef } from \"@odoo/owl\";\n\nexport class CalendarYearRenderer extends Component {\n    static components = {\n        Popover: CalendarYearPopover,\n    };\n    static template = \"web.CalendarYearRenderer\";\n    static props = {\n        model: Object,\n        createRecord: Function,\n        editRecord: Function,\n        deleteRecord: Function,\n        isWeekendVisible: { type: Boolean, optional: true },\n    };\n\n    setup() {\n        this.months = luxon.Info.months();\n        this.fcs = {};\n        for (const month of this.months) {\n            this.fcs[month] = useFullCalendar(\n                `fullCalendar-${month}`,\n                this.getOptionsForMonth(month)\n            );\n        }\n        this.popover = useCalendarPopover(this.constructor.components.Popover);\n        this.rootRef = useRef(\"root\");\n\n        useEffect(() => {\n            this.updateSize();\n        });\n    }\n\n    get options() {\n        return {\n            dayHeaderFormat: \"EEEEE\",\n            dateClick: this.onDateClick,\n            dayCellClassNames: this.getDayCellClassNames,\n            initialDate: this.props.model.date.toISO(),\n            initialView: \"dayGridMonth\",\n            direction: localization.direction,\n            droppable: true,\n            editable: this.props.model.canEdit,\n            dayMaxEventRows: this.props.model.eventLimit,\n            eventClassNames: this.eventClassNames,\n            eventDidMount: this.onEventDidMount,\n            eventResizableFromStart: true,\n            events: (_, successCb) => successCb(this.mapRecordsToEvents()),\n            firstDay: this.props.model.firstDayOfWeek,\n            headerToolbar: { start: false, center: \"title\", end: false },\n            height: \"auto\",\n            locale: luxon.Settings.defaultLocale,\n            longPressDelay: 500,\n            navLinks: false,\n            nowIndicator: true,\n            select: this.onSelect,\n            selectMinDistance: 5, // needed to not trigger select when click\n            selectMirror: true,\n            selectable: this.props.model.canCreate,\n            showNonCurrentDates: false,\n            timeZone: luxon.Settings.defaultZone.name,\n            titleFormat: { month: \"long\", year: \"numeric\" },\n            unselectAuto: false,\n            weekNumberCalculation: (date) => getLocalYearAndWeek(date).week,\n            weekNumbers: false,\n            weekNumberFormat: { week: \"numeric\" },\n            windowResize: this.onWindowResize,\n            eventContent: this.onEventContent,\n            viewDidMount: this.viewDidMount,\n            weekends: this.props.isWeekendVisible,\n            fixedWeekCount: false,\n        };\n    }\n\n    get customOptions() {\n        return {\n            weekNumbersWithinDays: true,\n        };\n    }\n\n    viewDidMount({ el, view }) {\n        const showWeek = view.calendar.currentData.options.weekNumbers;\n        const weekText = view.calendar.currentData.options.weekText;\n        const weekColumn = !this.customOptions.weekNumbersWithinDays;\n        if (showWeek && weekColumn) {\n            makeWeekColumn({ el, weekText });\n        }\n    }\n\n    mapRecordsToEvents() {\n        return Object.values(this.props.model.records).map((r) => this.convertRecordToEvent(r));\n    }\n    convertRecordToEvent(record) {\n        return {\n            ...convertRecordToEvent(record, true),\n            display: \"background\",\n        };\n    }\n    getDateWithMonth(month) {\n        return this.props.model.date.set({ month: this.months.indexOf(month) + 1 }).toISO();\n    }\n    getOptionsForMonth(month) {\n        return {\n            ...this.options,\n            initialDate: this.getDateWithMonth(month),\n        };\n    }\n    getPopoverProps(date, records) {\n        return {\n            date,\n            records,\n            model: this.props.model,\n            createRecord: this.props.createRecord,\n            deleteRecord: this.props.deleteRecord,\n            editRecord: this.props.editRecord,\n        };\n    }\n    openPopover(target, date, records) {\n        this.popover.open(target, this.getPopoverProps(date, records), \"o_cw_popover\");\n    }\n    unselect() {\n        for (const fc of Object.values(this.fcs)) {\n            fc.api.unselect();\n        }\n    }\n    updateSize() {\n        const height = window.innerHeight - this.rootRef.el.getBoundingClientRect().top;\n        this.rootRef.el.style.height = `${height}px`;\n    }\n\n    onDateClick(info) {\n        if (this.env.isSmall) {\n            this.props.model.load({\n                date: luxon.DateTime.fromISO(info.dateStr),\n                scale: \"day\",\n            });\n            return;\n        }\n\n        // With date value we don't want to change the time, we need the exact date\n        const date = luxon.DateTime.fromISO(info.dateStr);\n        const records = Object.values(this.props.model.records).filter((r) =>\n            luxon.Interval.fromDateTimes(r.start.startOf(\"day\"), r.end.endOf(\"day\")).contains(date)\n        );\n\n        this.popover.close();\n        if (records.length) {\n            const target = info.dayEl;\n            this.openPopover(target, date, records);\n        } else if (this.props.model.canCreate) {\n            this.props.createRecord({\n                // With date value we don't want to change the time, we need the exact date\n                start: luxon.DateTime.fromISO(info.dateStr),\n                isAllDay: true,\n            });\n        }\n    }\n    getDayCellClassNames(info) {\n        const date = luxon.DateTime.fromJSDate(info.date).toISODate();\n        if (this.props.model.unusualDays.includes(date)) {\n            return [\"o_calendar_disabled\"];\n        }\n        return [];\n    }\n    eventClassNames({ event }) {\n        const classesToAdd = [];\n        classesToAdd.push(\"o_event\");\n        const record = this.props.model.records[event.id];\n        if (record) {\n            const color = getColor(record.colorIndex);\n            if (typeof color === \"number\") {\n                classesToAdd.push(`o_calendar_color_${color}`);\n            } else if (typeof color !== \"string\") {\n                classesToAdd.push(\"o_calendar_color_0\");\n            }\n\n            if (record.isHatched) {\n                classesToAdd.push(\"o_event_hatched\");\n            }\n            if (record.isStriked) {\n                classesToAdd.push(\"o_event_striked\");\n            }\n        }\n        return classesToAdd;\n    }\n    onEventDidMount(info) {\n        const { el, event } = info;\n        el.dataset.eventId = event.id;\n        const record = this.props.model.records[event.id];\n        if (record) {\n            const color = getColor(record.colorIndex);\n            if (typeof color === \"string\") {\n                el.style.backgroundColor = color;\n            }\n        }\n    }\n    async onSelect(info) {\n        this.popover.close();\n        await this.props.createRecord({\n            // With date value we don't want to change the time, we need the exact date\n            start: luxon.DateTime.fromISO(info.startStr),\n            end: luxon.DateTime.fromISO(info.endStr).minus({ days: 1 }),\n            isAllDay: true,\n        });\n        this.unselect();\n    }\n    onWindowResize() {\n        this.updateSize();\n    }\n\n    onEventContent(info) {\n        // Remove the title on the background event like in FCv4\n        if (info.event.display?.includes(\"background\")) {\n            return null;\n        }\n    }\n}\n", "import { usePopover } from \"@web/core/popover/popover_hook\";\nimport { useService } from \"@web/core/utils/hooks\";\n\nimport { useComponent, useExternalListener } from \"@odoo/owl\";\n\nexport function useCalendarPopover(component) {\n    const owner = useComponent();\n    let popoverClass = \"\";\n    const popoverOptions = { extendedFlipping: true, position: \"right\", onClose: cleanup };\n    Object.defineProperty(popoverOptions, \"popoverClass\", { get: () => popoverClass });\n    const popover = usePopover(component, popoverOptions);\n    const dialog = useService(\"dialog\");\n    let removeDialog = null;\n    let fcPopover;\n    useExternalListener(\n        window,\n        \"mousedown\",\n        (ev) => {\n            if (fcPopover) {\n                // do not let fullcalendar popover close when our own popover is open\n                ev.stopPropagation();\n            }\n        },\n        { capture: true }\n    );\n    function cleanup() {\n        fcPopover = null;\n        removeDialog = null;\n    }\n    function close() {\n        removeDialog?.();\n        popover.close();\n        cleanup();\n    }\n    return {\n        close,\n        open(target, props, popoverClassToUse) {\n            fcPopover = target.closest(\".fc-popover\");\n            if (owner.env.isSmall) {\n                close();\n                removeDialog = dialog.add(component, props, { onClose: cleanup });\n            } else {\n                popoverClass = popoverClassToUse;\n                popover.open(target, props);\n            }\n        },\n    };\n}\n", "import { loadBundle } from \"@web/core/assets\";\n\nimport { onMounted, onPatched, onWillStart, onWillUnmount, useComponent, useRef } from \"@odoo/owl\";\n\nexport function useFullCalendar(refName, params) {\n    const component = useComponent();\n    const ref = useRef(refName);\n    let instance = null;\n\n    function boundParams() {\n        const newParams = {};\n        for (const key in params) {\n            const value = params[key];\n            newParams[key] = typeof value === \"function\" ? value.bind(component) : value;\n        }\n        return newParams;\n    }\n\n    onWillStart(async () => await loadBundle(\"web.fullcalendar_lib\"));\n\n    onMounted(() => {\n        try {\n            instance = new FullCalendar.Calendar(ref.el, boundParams());\n            instance.render();\n        } catch (e) {\n            throw new Error(`Cannot instantiate FullCalendar\\n${e.message}`);\n        }\n    });\n\n    onPatched(() => {\n        instance.refetchEvents();\n        instance.setOption(\"weekends\", component.props.isWeekendVisible);\n        if (params.weekNumbers && component.props.model.scale === \"year\") {\n            instance.destroy();\n            instance.render();\n        }\n    });\n    onWillUnmount(() => {\n        instance.destroy();\n    });\n\n    return {\n        get api() {\n            return instance;\n        },\n        get el() {\n            return ref.el;\n        },\n    };\n}\n", "import { useComponent, useEffect, useExternalListener, useRef } from \"@odoo/owl\";\nimport { makeDraggableHook } from \"@web/core/utils/draggable_hook_builder_owl\";\nimport { shallowEqual } from \"@web/core/utils/objects\";\nimport { closest } from \"@web/core/utils/ui\";\nimport { useCallbackRecorder } from \"@web/search/action_hook\";\n\nconst CELL_SELECTOR = `.fc-day:not(.fc-col-header-cell)`;\nconst ROW_SELECTOR = `tr[role=\"row\"]`;\nconst EVENT_CONTAINER_SELECTOR = \".fc-daygrid-event-harness\";\nconst IGNORE_SELECTOR = [\".fc-event\", \".fc-more-cell\", \".fc-more-popover\"].join(\",\");\n\nfunction getClosestCell(ctx) {\n    const { pointer, ref } = ctx;\n    return closest(ref.el.querySelectorAll(CELL_SELECTOR), pointer);\n}\n\nfunction getElementIndex(element) {\n    return [].indexOf.call(element?.parentNode.children || [], element);\n}\n\nfunction getCoordinates(cell) {\n    const colIndex = getElementIndex(cell);\n    const rowIndex = getElementIndex(cell.closest(ROW_SELECTOR));\n    return { colIndex, rowIndex };\n}\n\nfunction getBlockBounds({ initCoord, coord }) {\n    const [startColIndex, endColIndex] = [initCoord.colIndex, coord.colIndex].sort((a, b) => a - b);\n    const [startRowIndex, endRowIndex] = [initCoord.rowIndex, coord.rowIndex].sort((a, b) => a - b);\n    return { startColIndex, endColIndex, startRowIndex, endRowIndex };\n}\n\nfunction getSelectedCellsInBlock(ctx) {\n    const { cellIsSelectable, current, ref } = ctx;\n    const { startColIndex, endColIndex, startRowIndex, endRowIndex } = getBlockBounds(current);\n    const selectedCells = [];\n    for (const cell of ref.el.querySelectorAll(`tbody tr[role=\"row\"] .fc-day`)) {\n        const { colIndex, rowIndex } = getCoordinates(cell);\n        if (\n            startColIndex <= colIndex &&\n            colIndex <= endColIndex &&\n            startRowIndex <= rowIndex &&\n            rowIndex <= endRowIndex &&\n            cellIsSelectable(cell)\n        ) {\n            selectedCells.push(cell);\n        }\n    }\n    return { selectedCells };\n}\n\nfunction getSelectedCellsBetween2Cells(ctx, prevCell, cellClicked) {\n    const { cellIsSelectable, ref } = ctx;\n    const cells = [...ref.el.querySelectorAll(`tbody tr[role=\"row\"] .fc-day`)];\n    const index1 = cells.indexOf(prevCell);\n    if (index1 === -1) {\n        return new Set([cellClicked]);\n    }\n    const index2 = cells.indexOf(cellClicked);\n    const [startIndex, endIndex] = [index1, index2].sort((a, b) => a - b);\n    return new Set(cells.slice(startIndex, endIndex + 1).filter((cell) => cellIsSelectable(cell)));\n}\n\n// @ts-ignore\nconst useBlockSelection = makeDraggableHook({\n    name: \"useBlockSelection\",\n    acceptedParams: {\n        cellIsSelectable: [Function],\n    },\n    onComputeParams({ ctx, params }) {\n        ctx.followCursor = false;\n        ctx.cellIsSelectable = params.cellIsSelectable;\n    },\n    onWillStartDrag({ addClass, ctx }) {\n        const { current, ref } = ctx;\n        addClass(ref.el, \"pe-auto\");\n        const cell = getClosestCell(ctx);\n        addClass(cell, \"pe-auto\");\n        const coord = getCoordinates(cell);\n        current.initCoord = coord;\n        current.coord = coord;\n        return getSelectedCellsInBlock(ctx);\n    },\n    onDragStart({ ctx }) {\n        return getSelectedCellsInBlock(ctx);\n    },\n    onDrag({ ctx }) {\n        const { current } = ctx;\n        const cell = getClosestCell(ctx);\n        const coord = getCoordinates(cell);\n        if (shallowEqual(current.coord, coord)) {\n            return;\n        }\n        current.coord = coord;\n        return getSelectedCellsInBlock(ctx);\n    },\n    onDrop({ ctx }) {\n        return getSelectedCellsInBlock(ctx);\n    },\n});\n\nexport function useSquareSelection(params = {}) {\n    const cellIsSelectable = params.cellIsSelectable || (() => true);\n    const component = useComponent();\n    const ref = useRef(\"fullCalendar\");\n    const highlightClass = \"o-highlight\";\n\n    const removeHighlight = () => {\n        ref.el.querySelectorAll(`.${highlightClass}`).forEach((node) => {\n            node.classList.remove(highlightClass);\n        });\n    };\n\n    let allSelectedCells = new Set();\n    const getAllCells = (cells, action) => {\n        cells = new Set(cells);\n        switch (action) {\n            case \"add\":\n                return allSelectedCells.union(cells);\n            case \"toggle\":\n                return allSelectedCells.symmetricDifference(cells);\n            case \"replace\":\n                return cells;\n        }\n    };\n\n    const highlight = ({ selectedCells }) => {\n        removeHighlight();\n        selectedCells.forEach((node) => {\n            node.classList.add(highlightClass);\n        });\n    };\n\n    useCallbackRecorder(component.props.callbackRecorder, () => {\n        allSelectedCells = new Set();\n        prevSelectedCell = null;\n        removeHighlight();\n    });\n\n    let action = null;\n    let prevSelectedCell = null;\n    const update = ({ selectedCells }) => {\n        const allSelectedCells = getAllCells(selectedCells, action);\n        highlight({ selectedCells: allSelectedCells });\n    };\n\n    const selectState = useBlockSelection({\n        enable: () => component.props.model.hasMultiCreate,\n        ignore: EVENT_CONTAINER_SELECTOR,\n        elements: CELL_SELECTOR,\n        ref,\n        edgeScrolling: { speed: 40, threshold: 150 },\n        cellIsSelectable,\n        onDragStart: ({ selectedCells }) => {\n            prevSelectedCell = null;\n            action = ctrlPressed ? \"add\" : \"replace\";\n            update({ selectedCells });\n        },\n        onDrag: update,\n        onDrop: ({ selectedCells }) => {\n            allSelectedCells = getAllCells(selectedCells, action);\n            action = null;\n            highlight({ selectedCells: allSelectedCells });\n            component.props.onSquareSelection([...allSelectedCells]);\n        },\n    });\n\n    const onClick = (ev) => {\n        if (selectState.dragging) {\n            return;\n        }\n        const ignoreElement = ev.target.closest(IGNORE_SELECTOR);\n        if (ignoreElement) {\n            return;\n        }\n        const eventContainer = ev.target.closest(EVENT_CONTAINER_SELECTOR);\n        if (eventContainer) {\n            return;\n        }\n        const cell = ev.target.closest(CELL_SELECTOR);\n        if (!cell) {\n            return;\n        }\n        const coord = getCoordinates(cell);\n        const current = { initCoord: coord, coord };\n        const pseudoCtx = { current, ref, cellIsSelectable };\n        const { selectedCells } = getSelectedCellsInBlock(pseudoCtx);\n        const selectedCell = selectedCells[0];\n        if (prevSelectedCell && shiftPressed) {\n            allSelectedCells = getSelectedCellsBetween2Cells(\n                pseudoCtx,\n                prevSelectedCell,\n                selectedCell\n            );\n        } else {\n            const action = ctrlPressed ? \"toggle\" : \"replace\";\n            allSelectedCells = getAllCells(selectedCells, action);\n        }\n        if (!prevSelectedCell || !shiftPressed) {\n            prevSelectedCell = selectedCell;\n        }\n        highlight({ selectedCells: allSelectedCells });\n        component.props.onSquareSelection([...allSelectedCells]);\n    };\n\n    useEffect(\n        (el, hasMultiCreate) => {\n            if (!hasMultiCreate) {\n                return;\n            }\n            el && el.addEventListener(\"click\", onClick);\n            return () => {\n                el && el.removeEventListener(\"click\", onClick);\n            };\n        },\n        () => [ref.el, component.props.model.hasMultiCreate]\n    );\n\n    let ctrlPressed = false;\n    let shiftPressed = false;\n    function onWindowKeyDown(ev) {\n        if (ev.key === \"Control\") {\n            ctrlPressed = true;\n        } else if (ev.key === \"Shift\") {\n            shiftPressed = true;\n        }\n    }\n\n    function onWindowKeyUp(ev) {\n        if (ev.key === \"Control\") {\n            ctrlPressed = false;\n        } else if (ev.key === \"Shift\") {\n            shiftPressed = false;\n        }\n    }\n\n    useExternalListener(window, \"keydown\", onWindowKeyDown);\n    useExternalListener(window, \"keyup\", onWindowKeyUp);\n}\n", "import { Component } from \"@odoo/owl\";\nimport { getColor } from \"../utils\";\n\nexport class CalendarMobileFilterPanel extends Component {\n    static components = {};\n    static template = \"web.CalendarMobileFilterPanel\";\n    static props = {\n        model: Object,\n        sideBarShown: Boolean,\n        toggleSideBar: Function,\n    };\n    get caretDirection() {\n        return this.props.sideBarShown ? \"down\" : \"left\";\n    }\n    getFilterColor(filter) {\n        return `o_color_${getColor(filter.colorIndex)}`;\n    }\n    getFilterTypePriority(type) {\n        return [\"user\", \"record\", \"dynamic\", \"all\"].indexOf(type);\n    }\n    getSortedFilters(section) {\n        return section.filters.slice().sort((a, b) => {\n            if (a.type === b.type) {\n                const va = a.value ? -1 : 0;\n                const vb = b.value ? -1 : 0;\n                //Condition to put unvaluable item (eg: Open Shifts) at the end of the sorted list.\n                if (a.type === \"dynamic\" && va !== vb) {\n                    return va - vb;\n                }\n                return a.label.localeCompare(b.label, undefined, {\n                    numeric: true,\n                    sensitivity: \"base\",\n                    ignorePunctuation: true,\n                });\n            } else {\n                return this.getFilterTypePriority(a.type) - this.getFilterTypePriority(b.type);\n            }\n        });\n    }\n}\n", "import { useAutofocus, useService } from \"@web/core/utils/hooks\";\nimport { Dialog } from \"@web/core/dialog/dialog\";\nimport { _t } from \"@web/core/l10n/translation\";\n\nimport { Component } from \"@odoo/owl\";\n\nexport class CalendarQuickCreate extends Component {\n    static template = \"web.CalendarQuickCreate\";\n    static components = {\n        Dialog,\n    };\n    static props = {\n        title: { type: String, optional: true },\n        close: Function,\n        record: Object,\n        model: Object,\n        editRecord: Function,\n    };\n\n    setup() {\n        this.titleRef = useAutofocus({ refName: \"title\" });\n        this.notification = useService(\"notification\");\n        this.creatingRecord = false;\n    }\n\n    get dialogTitle() {\n        return _t(\"New Event\");\n    }\n\n    get recordTitle() {\n        return this.titleRef.el.value.trim();\n    }\n    get record() {\n        return {\n            ...this.props.record,\n            title: this.recordTitle,\n        };\n    }\n\n    editRecord() {\n        this.props.editRecord(this.record);\n        this.props.close();\n    }\n    async createRecord() {\n        if (this.creatingRecord) {\n            return;\n        }\n\n        if (this.recordTitle) {\n            try {\n                this.creatingRecord = true;\n                await this.props.model.createRecord(this.record);\n                this.props.close();\n            } catch {\n                this.editRecord();\n            }\n        } else {\n            this.titleRef.el.classList.add(\"o_field_invalid\");\n            this.notification.add(_t(\"Meeting Subject\"), {\n                title: _t(\"Invalid fields\"),\n                type: \"danger\",\n            });\n        }\n    }\n\n    onInputKeyup(ev) {\n        switch (ev.key) {\n            case \"Enter\":\n                this.createRecord();\n                break;\n            case \"Escape\":\n                this.props.close();\n                break;\n        }\n    }\n    onCreateBtnClick() {\n        this.createRecord();\n    }\n    onEditBtnClick() {\n        this.editRecord();\n    }\n    onCancelBtnClick() {\n        this.props.close();\n    }\n}\n", "export function convertRecordToEvent(record, forceAllDay = false) {\n    const allDay =\n        forceAllDay || record.isAllDay || record.end.diff(record.start, \"hours\").hours >= 24;\n    let end = record.end;\n    if (record.isAllDay || (allDay && end.toMillis() !== end.startOf(\"day\").toMillis())) {\n        end = end.plus({ days: 1 });\n    }\n    return {\n        id: record.id,\n        title: record.title,\n        start: record.start.toISO(),\n        end: end.toISO(),\n        allDay,\n    };\n}\n\nconst CSS_COLOR_REGEX =\n    /^((#[A-F0-9]{3})|(#[A-F0-9]{6})|((hsl|rgb)a?\\(\\s*(?:(\\s*\\d{1,3}%?\\s*),?){3}(\\s*,[0-9.]{1,4})?\\))|)$/i;\nconst colorMap = new Map();\nexport function getColor(key) {\n    if (!key) {\n        return false;\n    }\n    if (colorMap.has(key)) {\n        return colorMap.get(key);\n    }\n\n    // check if the key is a css color\n    if (typeof key === \"string\" && key.match(CSS_COLOR_REGEX)) {\n        colorMap.set(key, key);\n    } else if (typeof key === \"number\") {\n        colorMap.set(key, ((key - 1) % 55) + 1);\n    } else {\n        colorMap.set(key, (((colorMap.size + 1) * 5) % 24) + 1);\n    }\n\n    return colorMap.get(key);\n}\n\nexport function getFormattedDateSpan(start, end) {\n    const isSameDay = start.hasSame(end, \"days\");\n\n    if (!isSameDay && start.hasSame(end, \"month\")) {\n        // Simplify date-range if an event occurs into the same month (eg. \"August 4-5, 2019\")\n        return start.toFormat(\"LLLL d\") + \"-\" + end.toFormat(\"d, y\");\n    } else {\n        return isSameDay\n            ? start.toFormat(\"DDD\")\n            : start.toFormat(\"DDD\") + \" - \" + end.toFormat(\"DDD\");\n    }\n}\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { Dialog } from \"@web/core/dialog/dialog\";\nimport { evaluateBooleanExpr } from \"@web/core/py_js/py\";\nimport { editModelDebug } from \"@web/core/debug/debug_utils\";\nimport { formatDateTime, deserializeDateTime } from \"@web/core/l10n/dates\";\nimport { registry } from \"@web/core/registry\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { formatMany2one } from \"@web/views/fields/formatters\";\nimport { FormViewDialog } from \"@web/views/view_dialogs/form_view_dialog\";\n\nimport { Component, onWillStart, useState, xml } from \"@odoo/owl\";\nimport { serializeDate, serializeDateTime } from \"../core/l10n/dates\";\n\nconst debugRegistry = registry.category(\"debug\");\n\n//------------------------------------------------------------------------------\n// Get view\n//------------------------------------------------------------------------------\n\nclass GetViewDialog extends Component {\n    static template = \"web.DebugMenu.GetViewDialog\";\n    static components = { Dialog };\n    static props = {\n        arch: { type: String },\n        close: { type: Function },\n    };\n}\n\nexport function getView({ component, env }) {\n    return {\n        type: \"item\",\n        description: _t(\"Computed Arch\"),\n        callback: () => {\n            env.services.dialog.add(GetViewDialog, { arch: component.env.config.rawArch });\n        },\n        sequence: 270,\n        section: \"ui\",\n    };\n}\n\ndebugRegistry.category(\"view\").add(\"getView\", getView);\n\n//------------------------------------------------------------------------------\n// Edit View\n//------------------------------------------------------------------------------\n\nexport function editView({ accessRights, component, env }) {\n    if (!accessRights.canEditView) {\n        return null;\n    }\n    const { viewId, viewType: type } = component.env.config;\n    if (!type) {\n        return;\n    }\n    const displayName = type[0].toUpperCase() + type.slice(1);\n    const description = _t(\"View: %(displayName)s\", { displayName });\n    return {\n        type: \"item\",\n        description,\n        callback: () => {\n            editModelDebug(env, description, \"ir.ui.view\", viewId);\n        },\n        sequence: 240,\n        section: \"ui\",\n    };\n}\n\ndebugRegistry.category(\"view\").add(\"editView\", editView);\n\n//------------------------------------------------------------------------------\n// Edit SearchView\n//------------------------------------------------------------------------------\n\nexport function editSearchView({ accessRights, component, env }) {\n    if (!accessRights.canEditView) {\n        return null;\n    }\n    const { searchViewId } = component.componentProps.info;\n    if (searchViewId === undefined) {\n        return null;\n    }\n    const description = _t(\"SearchView\");\n    return {\n        type: \"item\",\n        description,\n        callback: () => {\n            editModelDebug(env, description, \"ir.ui.view\", searchViewId);\n        },\n        sequence: 230,\n        section: \"ui\",\n    };\n}\n\ndebugRegistry.category(\"view\").add(\"editSearchView\", editSearchView);\n\n// -----------------------------------------------------------------------------\n// View Metadata\n// -----------------------------------------------------------------------------\n\nclass GetMetadataDialog extends Component {\n    static template = \"web.DebugMenu.GetMetadataDialog\";\n    static components = { Dialog };\n    static props = {\n        resModel: String,\n        resId: Number,\n        close: Function,\n    };\n    setup() {\n        this.orm = useService(\"orm\");\n        this.dialogService = useService(\"dialog\");\n        this.title = _t(\"View Metadata\");\n        this.state = useState({});\n        onWillStart(() => this.loadMetadata());\n    }\n\n    onClickCreateXmlid() {\n        const context = Object.assign({}, this.context, {\n            default_module: \"__custom__\",\n            default_res_id: this.state.id,\n            default_model: this.props.resModel,\n        });\n        this.dialogService.add(FormViewDialog, {\n            context,\n            onRecordSaved: () => this.loadMetadata(),\n            resModel: \"ir.model.data\",\n        });\n    }\n\n    async toggleNoupdate() {\n        await this.env.services.orm.call(\"ir.model.data\", \"toggle_noupdate\", [\n            this.props.resModel,\n            this.state.id,\n        ]);\n        await this.loadMetadata();\n    }\n\n    async loadMetadata() {\n        const args = [[this.props.resId]];\n        const result = await this.orm.call(this.props.resModel, \"get_metadata\", args);\n        const metadata = result[0];\n        this.state.id = metadata.id;\n        this.state.xmlid = metadata.xmlid;\n        this.state.xmlids = metadata.xmlids;\n        this.state.noupdate = metadata.noupdate;\n        this.state.creator = formatMany2one(metadata.create_uid && { display_name: metadata.create_uid[1] });\n        this.state.lastModifiedBy = formatMany2one(metadata.write_uid && { display_name: metadata.write_uid[1] });\n        this.state.createDate = formatDateTime(deserializeDateTime(metadata.create_date));\n        this.state.writeDate = formatDateTime(deserializeDateTime(metadata.write_date));\n    }\n}\n\nexport function viewMetadata({ component, env }) {\n    const resId = component.model.root.resId;\n    if (!resId) {\n        return null; // No record\n    }\n    return {\n        type: \"item\",\n        description: _t(\"Metadata\"),\n        callback: () => {\n            env.services.dialog.add(GetMetadataDialog, {\n                resModel: component.props.resModel,\n                resId,\n            });\n        },\n        sequence: 110,\n        section: \"record\",\n    };\n}\n\ndebugRegistry.category(\"form\").add(\"viewMetadata\", viewMetadata);\n\nfunction sortKeysDeep(obj) {\n    if (Array.isArray(obj)) {\n        return obj.map(sortKeysDeep);\n    } else if (obj && typeof obj === \"object\") {\n        return Object.keys(obj)\n            .sort()\n            .reduce((result, key) => {\n                result[key] = sortKeysDeep(obj[key]);\n                return result;\n            }, {});\n    }\n    return obj;\n}\n\n// -----------------------------------------------------------------------------\n// View Raw Record Data\n// -----------------------------------------------------------------------------\n\nclass RawRecordDialog extends Component {\n    static template = xml`\n        <Dialog title=\"props.title\">\n            <pre t-esc=\"content\"/>\n        </Dialog>\n    `;\n    static components = { Dialog };\n    static props = {\n        record: { type: Object },\n        title: { type: String },\n        close: { type: Function },\n    };\n    get content() {\n        const record = this.props.record;\n        return JSON.stringify(sortKeysDeep(record), null, 2);\n    }\n}\n\nexport function viewRawRecord({ component, env }) {\n    const { resId, resModel, fields } = component.model.config;\n    if (!resId) {\n        return null;\n    }\n    const description = _t(\"Data\");\n    return {\n        type: \"item\",\n        description,\n        callback: async () => {\n            const serializableFields = Object.entries(fields).reduce(\n                (acc, [k, v]) => (v.type !== \"binary\" && !v.propertyName ? acc.concat(k) : acc),\n                []\n            );\n            const records = await component.model.orm.read(resModel, [resId], serializableFields);\n            env.services.dialog.add(RawRecordDialog, {\n                title: _t(\"Data: %(model)s(%(id)s)\", { model: resModel, id: resId }),\n                record: records[0],\n            });\n        },\n        sequence: 120,\n        section: \"record\",\n    };\n}\n\ndebugRegistry.category(\"form\").add(\"viewRawRecord\", viewRawRecord);\n\n// -----------------------------------------------------------------------------\n// Set Defaults\n// -----------------------------------------------------------------------------\n\nclass SetDefaultDialog extends Component {\n    static template = \"web.DebugMenu.SetDefaultDialog\";\n    static components = { Dialog };\n    static props = {\n        record: { type: Object },\n        fieldNodes: { type: Object },\n        close: { type: Function },\n    };\n\n    setup() {\n        this.orm = useService(\"orm\");\n        this.state = {\n            fieldToSet: \"\",\n            condition: \"\",\n            scope: \"self\",\n        };\n        this.fields = this.props.record.fields;\n        this.activeFields = this.props.record.activeFields;\n        this.fieldNamesInView = this.props.record.fieldNames;\n        this.fieldNamesBlackList = [\"message_attachment_count\"];\n        this.fieldsValues = this.props.record.data;\n        this.modifierDatas = {};\n        this.defaultFields = this.getDefaultFields();\n        this.conditions = this.getConditions();\n    }\n\n    getDefaultFields() {\n        return this.fieldNamesInView\n            .filter((fieldName) => !this.fieldNamesBlackList.includes(fieldName))\n            .map((fieldName) => {\n                const fieldInfo = this.fields[fieldName];\n                const valueDisplayed = this.display(fieldInfo, this.fieldsValues[fieldName]);\n                const value = valueDisplayed[0];\n                const displayed = valueDisplayed[1];\n                const evalContext = this.props.record.evalContextWithVirtualIds;\n                // ignore fields which are empty, invisible, readonly, o2m or m2m\n                if (\n                    !value ||\n                    evaluateBooleanExpr(this.activeFields[fieldName].invisible, evalContext) ||\n                    evaluateBooleanExpr(this.activeFields[fieldName].readonly, evalContext) ||\n                    fieldInfo.type === \"one2many\" ||\n                    fieldInfo.type === \"many2many\" ||\n                    fieldInfo.type === \"binary\" ||\n                    Object.entries(this.props.fieldNodes)\n                        .filter(([key, value]) => value.name === fieldName)\n                        .some(([key, value]) => value.options.isPassword)\n                ) {\n                    return false;\n                }\n                return {\n                    name: fieldName,\n                    string: fieldInfo.string,\n                    value,\n                    displayed,\n                };\n            })\n            .filter((val) => val)\n            .sort((field) => field.string);\n    }\n\n    getConditions() {\n        return this.fieldNamesInView\n            .filter((fieldName) => {\n                const fieldInfo = this.fields[fieldName];\n                return fieldInfo.change_default;\n            })\n            .map((fieldName) => {\n                const fieldInfo = this.fields[fieldName];\n                const valueDisplayed = this.display(fieldInfo, this.fieldsValues[fieldName]);\n                const value = valueDisplayed[0];\n                const displayed = valueDisplayed[1];\n                return {\n                    name: fieldName,\n                    string: fieldInfo.string,\n                    value: value,\n                    displayed: displayed,\n                };\n            });\n    }\n\n    display(fieldInfo, value) {\n        let displayed = value;\n        if (value && fieldInfo.type === \"many2one\") {\n            displayed = value.display_name;\n            value = value.id;\n        } else if (value && fieldInfo.type === \"selection\") {\n            displayed = fieldInfo.selection.find((option) => {\n                return option[0] === value;\n            })[1];\n        }\n        if (\n            (typeof displayed === \"string\" || displayed instanceof String) &&\n            displayed.length > 60\n        ) {\n            displayed = displayed.slice(0, 57) + \"...\";\n        }\n        return [value, displayed];\n    }\n\n    async saveDefault() {\n        if (!this.state.fieldToSet) {\n            return;\n        }\n        let fieldToSet = this.defaultFields.find((field) => {\n            return field.name === this.state.fieldToSet;\n        }).value;\n\n        if (fieldToSet.constructor.name.toLowerCase() === \"date\") {\n            fieldToSet = serializeDate(fieldToSet);\n        } else if (fieldToSet.constructor.name.toLowerCase() === \"datetime\") {\n            fieldToSet = serializeDateTime(fieldToSet);\n        }\n        await this.orm.call(\"ir.default\", \"set\", [\n            this.props.record.resModel,\n            this.state.fieldToSet,\n            fieldToSet,\n            this.state.scope === \"self\",\n            true,\n            this.state.condition || false,\n        ]);\n        this.props.close();\n    }\n}\n\nexport function setDefaults({ component, env }) {\n    return {\n        type: \"item\",\n        description: _t(\"Set Default Values\"),\n        callback: () => {\n            env.services.dialog.add(SetDefaultDialog, {\n                record: component.model.root,\n                fieldNodes: component.props.archInfo.fieldNodes,\n            });\n        },\n        sequence: 150,\n        section: \"record\",\n    };\n}\ndebugRegistry.category(\"form\").add(\"setDefaults\", setDefaults);\n\n//------------------------------------------------------------------------------\n// Manage Attachments\n//------------------------------------------------------------------------------\n\nexport function manageAttachments({ component, env }) {\n    const resId = component.model.root.resId;\n    if (!resId) {\n        return null; // No record\n    }\n    const description = _t(\"Attachments\");\n    return {\n        type: \"item\",\n        description,\n        callback: () => {\n            env.services.action.doAction({\n                res_model: \"ir.attachment\",\n                name: description,\n                views: [\n                    [false, \"list\"],\n                    [false, \"form\"],\n                ],\n                type: \"ir.actions.act_window\",\n                domain: [\n                    [\"res_model\", \"=\", component.props.resModel],\n                    [\"res_id\", \"=\", resId],\n                ],\n                context: {\n                    default_res_model: component.props.resModel,\n                    default_res_id: resId,\n                    skip_res_field_check: true,\n                },\n            });\n        },\n        sequence: 140,\n        section: \"record\",\n    };\n}\n\ndebugRegistry.category(\"form\").add(\"manageAttachments\", manageAttachments);\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { useBus } from \"@web/core/utils/hooks\";\nimport { standardFieldProps } from \"../standard_field_props\";\n\nimport { CodeEditor } from \"@web/core/code_editor/code_editor\";\nimport { Component, useState } from \"@odoo/owl\";\nimport { useRecordObserver } from \"@web/model/relational_model/utils\";\nimport { formatText } from \"@web/views/fields/formatters\";\nimport { cookie } from \"@web/core/browser/cookie\";\n\nexport class AceField extends Component {\n    static template = \"web.AceField\";\n    static props = {\n        ...standardFieldProps,\n        mode: { type: String, optional: true },\n    };\n    static defaultProps = {\n        mode: \"qweb\",\n    };\n    static components = { CodeEditor };\n\n    setup() {\n        this.state = useState({});\n        useRecordObserver((record) => {\n            this.state.initialValue = formatText(record.data[this.props.name]);\n        });\n\n        this.isDirty = false;\n\n        const { model } = this.props.record;\n        useBus(model.bus, \"WILL_SAVE_URGENTLY\", () => this.commitChanges());\n        useBus(model.bus, \"NEED_LOCAL_CHANGES\", ({ detail }) =>\n            detail.proms.push(this.commitChanges())\n        );\n    }\n\n    get mode() {\n        return this.props.mode === \"xml\" ? \"qweb\" : this.props.mode;\n    }\n    get theme() {\n        return cookie.get(\"color_scheme\") === \"dark\" ? \"monokai\" : \"\";\n    }\n\n    handleChange(editedValue) {\n        if (this.state.initialValue !== editedValue) {\n            this.isDirty = true;\n        } else {\n            this.isDirty = false;\n        }\n        this.props.record.model.bus.trigger(\"FIELD_IS_DIRTY\", this.isDirty);\n        this.editedValue = editedValue;\n    }\n\n    async commitChanges() {\n        if (!this.props.readonly && this.isDirty) {\n            if (this.state.initialValue !== this.editedValue) {\n                await this.props.record.update({ [this.props.name]: this.editedValue });\n            }\n            this.isDirty = false;\n            this.props.record.model.bus.trigger(\"FIELD_IS_DIRTY\", false);\n        }\n    }\n}\n\nexport const aceField = {\n    component: AceField,\n    displayName: _t(\"Ace Editor\"),\n    supportedOptions: [\n        {\n            label: _t(\"Mode\"),\n            name: \"mode\",\n            type: \"string\",\n        },\n    ],\n    supportedTypes: [\"text\", \"html\"],\n    extractProps: ({ options }) => ({\n        mode: options.mode,\n    }),\n};\n\nregistry.category(\"fields\").add(\"ace\", aceField);\nregistry.category(\"fields\").add(\"code\", aceField);\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\n\nimport { Component } from \"@odoo/owl\";\nimport { standardFieldProps } from \"@web/views/fields/standard_field_props\";\n\nexport class AttachmentImageField extends Component {\n    static template = \"web.AttachmentImageField\";\n    static props = { ...standardFieldProps };\n}\n\nexport const attachmentImageField = {\n    component: AttachmentImageField,\n    displayName: _t(\"Attachment Image\"),\n    supportedTypes: [\"many2one\"],\n};\n\nregistry.category(\"fields\").add(\"attachment_image\", attachmentImageField);\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { evaluateBooleanExpr } from \"@web/core/py_js/py\";\nimport { registry } from \"@web/core/registry\";\nimport { standardFieldProps } from \"@web/views/fields/standard_field_props\";\n\nimport { Component } from \"@odoo/owl\";\nconst formatters = registry.category(\"formatters\");\n\nexport class BadgeField extends Component {\n    static template = \"web.BadgeField\";\n    static props = {\n        ...standardFieldProps,\n        decorations: { type: Object, optional: true },\n        colorField: { type: String, optional: true },\n    };\n    static defaultProps = {\n        decorations: {},\n    };\n\n    get formattedValue() {\n        const formatter = formatters.get(this.props.record.fields[this.props.name].type);\n        return formatter(this.props.record.data[this.props.name], {\n            selection: this.props.record.fields[this.props.name].selection,\n        });\n    }\n\n    get badgeClass() {\n        if (this.props.colorField) {\n            return `o_badge_color_${this.props.record.data[this.props.colorField]}`;\n        }\n        const evalContext = this.props.record.evalContextWithVirtualIds;\n        for (const decorationName in this.props.decorations) {\n            if (evaluateBooleanExpr(this.props.decorations[decorationName], evalContext)) {\n                // fallback case for text-bg-muted\n                if (decorationName === \"muted\") {\n                    return \"text-bg-300\";\n                }\n                return `text-bg-${decorationName}`;\n            }\n        }\n        return \"text-bg-300\";\n    }\n}\n\nexport const badgeField = {\n    component: BadgeField,\n    displayName: _t(\"Badge\"),\n    supportedTypes: [\"selection\", \"many2one\", \"char\"],\n    supportedOptions: [\n        {\n            label: _t(\"Color field\"),\n            name: \"color_field\",\n            type: \"field\",\n            availableTypes: [\"integer\"],\n            help: _t(\"Set an integer field to use colors with the badge.\"),\n        },\n    ],\n    extractProps: ({ decorations, options }) => {\n        return {\n            decorations,\n            colorField: options.color_field,\n        };\n    },\n};\n\nregistry.category(\"fields\").add(\"badge\", badgeField);\n", "import { Component } from \"@odoo/owl\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { getFieldDomain } from \"@web/model/relational_model/utils\";\nimport { useSpecialData } from \"@web/views/fields/relational_utils\";\nimport { standardFieldProps } from \"../standard_field_props\";\n\nexport class BadgeSelectionField extends Component {\n    static template = \"web.BadgeSelectionField\";\n    static props = {\n        ...standardFieldProps,\n        domain: { type: [Array, Function], optional: true },\n        size: {\n            type: String,\n            optional: true,\n            validate: (s) => [\"sm\", \"md\", \"lg\"].includes(s),\n            default: \"md\",\n        },\n    };\n\n    setup() {\n        this.type = this.props.record.fields[this.props.name].type;\n        if (this.type === \"many2one\") {\n            this.specialData = useSpecialData((orm, props) => {\n                const domain = getFieldDomain(props.record, props.name, props.domain);\n                const { relation } = props.record.fields[props.name];\n                return orm.call(relation, \"name_search\", [\"\", domain]);\n            });\n        }\n    }\n\n    get options() {\n        switch (this.type) {\n            case \"many2one\":\n                return this.specialData.data;\n            case \"selection\":\n                return this.props.record.fields[this.props.name].selection;\n            default:\n                return [];\n        }\n    }\n\n    get string() {\n        switch (this.type) {\n            case \"many2one\":\n                return this.props.record.data[this.props.name]\n                    ? this.props.record.data[this.props.name].display_name\n                    : \"\";\n            case \"selection\":\n                return this.props.record.data[this.props.name] !== false\n                    ? this.options.find((o) => o[0] === this.props.record.data[this.props.name])[1]\n                    : \"\";\n            default:\n                return \"\";\n        }\n    }\n    get value() {\n        const rawValue = this.props.record.data[this.props.name];\n        return this.type === \"many2one\" && rawValue ? rawValue.id : rawValue;\n    }\n\n    stringify(value) {\n        return JSON.stringify(value);\n    }\n\n    /**\n     * @param {string | number | false} value\n     */\n    onChange(value) {\n        switch (this.type) {\n            case \"many2one\":\n                if (value === false) {\n                    this.props.record.update({ [this.props.name]: false });\n                } else {\n                    const option = this.options.find((option) => option[0] === value);\n                    this.props.record.update({\n                        [this.props.name]: { id: option[0], display_name: option[1] },\n                    });\n                }\n                break;\n            case \"selection\":\n                if (value === this.value) {\n                    const { required } = this.props.record.fields[this.props.name];\n                    if (!required) {\n                        this.props.record.update({ [this.props.name]: false });\n                    }\n                } else {\n                    this.props.record.update({ [this.props.name]: value });\n                }\n                break;\n        }\n    }\n}\n\nexport const badgeSelectionField = {\n    component: BadgeSelectionField,\n    displayName: _t(\"Badges\"),\n    supportedTypes: [\"many2one\", \"selection\"],\n    supportedOptions: [\n        {\n            label: \"Size\",\n            name: \"size\",\n            type: \"selection\",\n            choices: [\n                { label: \"Small\", value: \"sm\" },\n                { label: \"Medium\", value: \"md\" },\n                { label: \"Large\", value: \"lg\" },\n            ],\n            default: \"md\",\n        },\n    ],\n    isEmpty: (record, fieldName) => record.data[fieldName] === false,\n    extractProps: (fieldInfo, dynamicInfo) => ({\n        domain: dynamicInfo.domain,\n        size: fieldInfo.options.size,\n    }),\n};\n\nregistry.category(\"fields\").add(\"selection_badge\", badgeSelectionField);\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { mergeClasses } from \"@web/core/utils/classname\";\nimport { badgeSelectionField, BadgeSelectionField } from \"./badge_selection_field\";\n\nexport class ListBadgeSelectionField extends BadgeSelectionField {\n    static template = \"web.ListBadgeSelectionField\";\n    static props = {\n        ...BadgeSelectionField.props,\n        colorField: { type: String, optional: true },\n    };\n    getBadgeClassNames(option = false) {\n        if (this.props.readonly) {\n            if (\n                this.props.colorField &&\n                Number.isInteger(this.props.record.data[this.props.colorField])\n            ) {\n                return `o_badge_color_${this.props.record.data[this.props.colorField]}`;\n            }\n            return mergeClasses({ \"btn btn-secondary\": this.value });\n        }\n        return mergeClasses({\n            \"active o_badge_border\": this.value === option[0],\n            \"btn-sm\": this.props.size === \"sm\",\n            \"btn-lg\": this.props.size === \"lg\",\n        });\n    }\n}\n\nexport const listBadgeSelectionField = {\n    ...badgeSelectionField,\n    component: ListBadgeSelectionField,\n    supportedOptions: [\n        ...badgeSelectionField.supportedOptions,\n        {\n            label: _t(\"Color field\"),\n            name: \"color_field\",\n            type: \"field\",\n            availableTypes: [\"integer\"],\n            help: _t(\"Set an integer field to use colors with the badge.\"),\n        },\n    ],\n    extractProps: (fieldInfo, dynamicInfo) => ({\n        ...badgeSelectionField.extractProps(fieldInfo, dynamicInfo),\n        colorField: fieldInfo.options.color_field,\n    }),\n};\n\nregistry.category(\"fields\").add(\"list.selection_badge\", listBadgeSelectionField);\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport {\n    BadgeSelectionField,\n    badgeSelectionField,\n} from \"@web/views/fields/badge_selection/badge_selection_field\";\n\nexport class BadgeSelectionWithFilterField extends BadgeSelectionField {\n    static props = {\n        ...BadgeSelectionField.props,\n        allowedSelectionField: { type: String },\n    };\n\n    get options() {\n        const allowedSelection = this.props.record.data[this.props.allowedSelectionField];\n        return super.options.filter(([value, _]) => allowedSelection.includes(value));\n    }\n}\n\nexport const badgeSelectionFieldWithFilter = {\n    ...badgeSelectionField,\n    component: BadgeSelectionWithFilterField,\n    displayName: _t(\"Badges for Selection With Filter\"),\n    supportedTypes: [\"selection\"],\n    extractProps({ options }) {\n        return {\n            ...badgeSelectionField.extractProps(...arguments),\n            allowedSelectionField: options.allowed_selection_field,\n        };\n    },\n};\n\nregistry.category(\"fields\").add(\"selection_badge_with_filter\", badgeSelectionFieldWithFilter);\n", "import { registry } from \"@web/core/registry\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { isBinarySize, toBase64Length } from \"@web/core/utils/binary\";\nimport { download } from \"@web/core/network/download\";\nimport { standardFieldProps } from \"../standard_field_props\";\nimport { FileUploader } from \"../file_handler\";\nimport { _t } from \"@web/core/l10n/translation\";\n\nimport { Component } from \"@odoo/owl\";\n\nexport const MAX_FILENAME_SIZE_BYTES = 0xFF;  // filenames do not exceed 255 bytes on Linux/Windows/MacOS\n\nexport class BinaryField extends Component {\n    static template = \"web.BinaryField\";\n    static components = {\n        FileUploader,\n    };\n    static props = {\n        ...standardFieldProps,\n        acceptedFileExtensions: { type: String, optional: true },\n        // See https://www.iana.org/assignments/media-types/media-types.xhtml\n        allowedMIMETypes: { type: String, optional: true },\n        fileNameField: { type: String, optional: true },\n    };\n    static defaultProps = {\n        acceptedFileExtensions: \"*\",\n    };\n\n    setup() {\n        this.notification = useService(\"notification\");\n    }\n\n    get fileName() {\n        let value = this.props.record.data[this.props.name];\n        value = value && typeof value === \"string\" ? value : false;\n        return (this.props.record.data[this.props.fileNameField] || value || \"\").slice(\n            0,\n            toBase64Length(MAX_FILENAME_SIZE_BYTES)\n        );\n    }\n\n    update({ data, name }) {\n        const { fileNameField, record } = this.props;\n        const changes = { [this.props.name]: data || false };\n        if (fileNameField in record.fields && record.data[fileNameField] !== name) {\n            changes[fileNameField] = name || '';\n        }\n        return this.props.record.update(changes);\n    }\n\n    getDownloadData() {\n        return {\n            model: this.props.record.resModel,\n            id: this.props.record.resId,\n            field: this.props.name,\n            filename_field: this.fileName,\n            filename: this.fileName || \"\",\n            download: true,\n            data: isBinarySize(this.props.record.data[this.props.name])\n                ? null\n                : this.props.record.data[this.props.name],\n        };\n    }\n\n    async onFileDownload() {\n        await download({\n            data: this.getDownloadData(),\n            url: \"/web/content\",\n        });\n    }\n}\n\nexport class ListBinaryField extends BinaryField {\n    static template = \"web.ListBinaryField\";\n}\n\nexport const binaryField = {\n    component: BinaryField,\n    displayName: _t(\"File\"),\n    supportedOptions: [\n        {\n            label: _t(\"Accepted file extensions\"),\n            name: \"accepted_file_extensions\",\n            type: \"string\",\n        },\n        {\n            label: _t(\"Allowed file mimetype\"),\n            name: \"allowed_mime_type\",\n            type: \"string\",\n        },\n    ],\n    supportedTypes: [\"binary\"],\n    extractProps: ({ attrs, options }) => ({\n        acceptedFileExtensions: options.accepted_file_extensions,\n        allowedMIMETypes: options.allowed_mime_type,\n        fileNameField: attrs.filename,\n    }),\n};\n\nexport const listBinaryField = {\n    ...binaryField,\n    component: ListBinaryField,\n};\n\nregistry.category(\"fields\").add(\"binary\", binaryField);\nregistry.category(\"fields\").add(\"list.binary\", listBinaryField);\n", "import { Component, useState } from \"@odoo/owl\";\nimport { CheckBox } from \"@web/core/checkbox/checkbox\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { useRecordObserver } from \"@web/model/relational_model/utils\";\nimport { standardFieldProps } from \"../standard_field_props\";\n\nexport class BooleanField extends Component {\n    static template = \"web.BooleanField\";\n    static components = { CheckBox };\n    static props = {\n        ...standardFieldProps,\n    };\n\n    setup() {\n        this.state = useState({});\n        useRecordObserver((record) => {\n            this.state.value = record.data[this.props.name];\n        });\n    }\n\n    /**\n     * @param {boolean} newValue\n     */\n    onChange(newValue) {\n        this.state.value = newValue;\n        this.props.record.update({ [this.props.name]: newValue });\n    }\n}\n\nexport const booleanField = {\n    component: BooleanField,\n    displayName: _t(\"Checkbox\"),\n    supportedTypes: [\"boolean\"],\n    isEmpty: () => false,\n};\n\nregistry.category(\"fields\").add(\"boolean\", booleanField);\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { exprToBoolean } from \"@web/core/utils/strings\";\nimport { standardFieldProps } from \"../standard_field_props\";\n\nimport { Component } from \"@odoo/owl\";\n\nexport class BooleanFavoriteField extends Component {\n    static template = \"web.BooleanFavoriteField\";\n    static props = {\n        ...standardFieldProps,\n        noLabel: { type: Boolean, optional: true },\n        autosave: { type: Boolean, optional: true },\n    };\n    static defaultProps = {\n        noLabel: false,\n    };\n\n    get iconClass() {\n        return this.props.record.data[this.props.name] ? \"fa fa-star me-1\" : \"fa fa-star-o me-1\";\n    }\n\n    get label() {\n        return this.props.record.data[this.props.name]\n            ? _t(\"Remove from Favorites\")\n            : _t(\"Add to Favorites\");\n    }\n\n    async update() {\n        if (this.props.readonly) {\n            return;\n        }\n        const changes = { [this.props.name]: !this.props.record.data[this.props.name] };\n        await this.props.record.update(changes, { save: this.props.autosave });\n    }\n}\n\nexport const booleanFavoriteField = {\n    component: BooleanFavoriteField,\n    displayName: _t(\"Favorite\"),\n    supportedTypes: [\"boolean\"],\n    isEmpty: () => false,\n    listViewWidth: ({ hasLabel }) => (!hasLabel ? 20 : false),\n    supportedOptions: [\n        {\n            label: _t(\"Autosave\"),\n            name: \"autosave\",\n            type: \"boolean\",\n            default: true,\n            help: _t(\n                \"If checked, the record will be saved immediately when the field is modified.\"\n            ),\n        },\n    ],\n    extractProps: ({ attrs, options }, dynamicInfo) => ({\n        noLabel: exprToBoolean(attrs.nolabel),\n        autosave: \"autosave\" in options ? Boolean(options.autosave) : true,\n        readonly: dynamicInfo.readonly,\n    }),\n};\n\nregistry.category(\"fields\").add(\"boolean_favorite\", booleanFavoriteField);\n", "import { Component } from \"@odoo/owl\";\nimport { registry } from \"@web/core/registry\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { standardFieldProps } from \"../standard_field_props\";\n\nexport class BooleanIconField extends Component {\n    static template = \"web.BooleanIconField\";\n    static props = {\n        ...standardFieldProps,\n        icon: { type: String, optional: true },\n        label: { type: String, optional: true },\n    };\n    static defaultProps = {\n        icon: \"fa-check-square-o\",\n    };\n\n    update() {\n        this.props.record.update({ [this.props.name]: !this.props.record.data[this.props.name] });\n    }\n}\n\nexport const booleanIconField = {\n    component: BooleanIconField,\n    displayName: _t(\"Boolean Icon\"),\n    supportedOptions: [\n        {\n            label: _t(\"Icon\"),\n            name: \"icon\",\n            type: \"string\",\n        },\n    ],\n    supportedTypes: [\"boolean\"],\n    extractProps: ({ options, string }) => ({\n        icon: options.icon,\n        label: string,\n    }),\n};\n\nregistry.category(\"fields\").add(\"boolean_icon\", booleanIconField);\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { booleanField, BooleanField } from \"../boolean/boolean_field\";\n\nexport class BooleanToggleField extends BooleanField {\n    static template = \"web.BooleanToggleField\";\n    static props = {\n        ...BooleanField.props,\n        autosave: { type: Boolean, optional: true },\n    };\n\n    async onChange(newValue) {\n        this.state.value = newValue;\n        const changes = { [this.props.name]: newValue };\n        await this.props.record.update(changes, { save: this.props.autosave });\n    }\n}\n\nexport const booleanToggleField = {\n    ...booleanField,\n    component: BooleanToggleField,\n    displayName: _t(\"Toggle\"),\n    supportedOptions: [\n        {\n            label: _t(\"Autosave\"),\n            name: \"autosave\",\n            type: \"boolean\",\n            default: true,\n            help: _t(\n                \"If checked, the record will be saved immediately when the field is modified.\"\n            ),\n        },\n    ],\n    extractProps({ options }, dynamicInfo) {\n        return {\n            autosave: \"autosave\" in options ? Boolean(options.autosave) : true,\n            readonly: dynamicInfo.readonly,\n        };\n    },\n};\n\nregistry.category(\"fields\").add(\"boolean_toggle\", booleanToggleField);\n", "import { registry } from \"@web/core/registry\";\nimport { booleanToggleField, BooleanToggleField } from \"./boolean_toggle_field\";\n\nexport class ListBooleanToggleField extends BooleanToggleField {\n    static template = \"web.ListBooleanToggleField\";\n\n    async onClick() {\n        if (!this.props.readonly && this.props.record.isInEdition) {\n            const changes = { [this.props.name]: !this.props.record.data[this.props.name] };\n            await this.props.record.update(changes, { save: this.props.autosave });\n        }\n    }\n}\n\nexport const listBooleanToggleField = {\n    ...booleanToggleField,\n    component: ListBooleanToggleField,\n};\n\nregistry.category(\"fields\").add(\"list.boolean_toggle\", listBooleanToggleField);\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { exprToBoolean } from \"@web/core/utils/strings\";\nimport { useDynamicPlaceholder } from \"../dynamic_placeholder_hook\";\nimport { formatChar } from \"../formatters\";\nimport { useInputField } from \"../input_field_hook\";\nimport { standardFieldProps } from \"../standard_field_props\";\nimport { TranslationButton } from \"../translation_button\";\n\nimport { Component, useEffect, useExternalListener, useRef } from \"@odoo/owl\";\n\nexport class CharField extends Component {\n    static template = \"web.CharField\";\n    static components = {\n        TranslationButton,\n    };\n    static props = {\n        ...standardFieldProps,\n        autocomplete: { type: String, optional: true },\n        isPassword: { type: Boolean, optional: true },\n        placeholder: { type: String, optional: true },\n        dynamicPlaceholder: { type: Boolean, optional: true },\n        dynamicPlaceholderModelReferenceField: { type: String, optional: true },\n    };\n    static defaultProps = { dynamicPlaceholder: false };\n\n    setup() {\n        this.input = useRef(\"input\");\n        if (this.props.dynamicPlaceholder) {\n            this.dynamicPlaceholder = useDynamicPlaceholder(this.input);\n            useExternalListener(document, \"keydown\", this.dynamicPlaceholder.onKeydown);\n            useEffect(() =>\n                this.dynamicPlaceholder.updateModel(\n                    this.props.dynamicPlaceholderModelReferenceField\n                )\n            );\n        }\n        useInputField({\n            getValue: () => this.props.record.data[this.props.name] || \"\",\n            parse: (v) => this.parse(v),\n        });\n\n        this.selectionStart = this.props.record.data[this.props.name]?.length || 0;\n    }\n\n    get shouldTrim() {\n        return this.props.record.fields[this.props.name].trim && !this.props.isPassword;\n    }\n    get maxLength() {\n        return this.props.record.fields[this.props.name].size;\n    }\n    get isTranslatable() {\n        return this.props.record.fields[this.props.name].translate;\n    }\n    get formattedValue() {\n        return formatChar(this.props.record.data[this.props.name], {\n            isPassword: this.props.isPassword,\n        });\n    }\n    get hasDynamicPlaceholder() {\n        return this.props.dynamicPlaceholder && !this.props.readonly;\n    }\n\n    parse(value) {\n        if (this.shouldTrim) {\n            return value.trim();\n        }\n        return value;\n    }\n\n    onBlur() {\n        this.selectionStart = this.input.el.selectionStart;\n    }\n\n    async onDynamicPlaceholderOpen() {\n        await this.dynamicPlaceholder.open({\n            validateCallback: this.onDynamicPlaceholderValidate.bind(this),\n        });\n    }\n\n    async onDynamicPlaceholderValidate(chain, defaultValue) {\n        if (chain) {\n            this.input.el.focus();\n            const dynamicPlaceholder = ` {{object.${chain}${\n                defaultValue?.length ? ` ||| ${defaultValue}` : \"\"\n            }}}`;\n            this.input.el.setRangeText(\n                dynamicPlaceholder,\n                this.selectionStart,\n                this.selectionStart,\n                \"end\"\n            );\n            // trigger events to make the field dirty\n            this.input.el.dispatchEvent(new InputEvent(\"input\"));\n            this.input.el.dispatchEvent(new KeyboardEvent(\"keydown\"));\n            this.input.el.focus();\n        }\n    }\n}\n\nexport const charField = {\n    component: CharField,\n    displayName: _t(\"Text\"),\n    supportedTypes: [\"char\", \"text\"],\n    supportedOptions: [\n        {\n            label: _t(\"Dynamic Placeholder\"),\n            name: \"placeholder_field\",\n            type: \"field\",\n            availableTypes: [\"char\", \"text\"],\n            help: _t(\n                \"Displays the value of the selected field as a textual hint. If the selected field is empty, the static placeholder attribute is displayed instead.\"\n            ),\n        },\n    ],\n    extractProps: ({ attrs, options, placeholder }) => ({\n        isPassword: exprToBoolean(attrs.password),\n        dynamicPlaceholder: options.dynamic_placeholder || false,\n        dynamicPlaceholderModelReferenceField:\n            options.dynamic_placeholder_model_reference_field || \"\",\n        autocomplete: attrs.autocomplete,\n        placeholder,\n    }),\n};\n\nregistry.category(\"fields\").add(\"char\", charField);\n", "import { Component } from \"@odoo/owl\";\nimport { registry } from \"@web/core/registry\";\nimport { standardFieldProps } from \"../standard_field_props\";\n\nexport class ColorField extends Component {\n    static template = \"web.ColorField\";\n    static props = {\n        ...standardFieldProps,\n    };\n\n    get color() {\n        return this.props.record.data[this.props.name] || \"\";\n    }\n}\n\nexport const colorField = {\n    component: ColorField,\n    supportedTypes: [\"char\"],\n    extractProps(fieldInfo, dynamicInfo) {\n        return {\n            readonly: dynamicInfo.readonly,\n        };\n    },\n};\n\nregistry.category(\"fields\").add(\"color\", colorField);\n", "import { ColorList } from \"@web/core/colorlist/colorlist\";\nimport { registry } from \"@web/core/registry\";\nimport { standardFieldProps } from \"../standard_field_props\";\n\nimport { Component } from \"@odoo/owl\";\n\nexport class ColorPickerField extends Component {\n    static template = \"web.ColorPickerField\";\n    static components = {\n        ColorList,\n    };\n    static props = {\n        ...standardFieldProps,\n        canToggle: { type: Boolean },\n    };\n\n    static RECORD_COLORS = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11];\n\n    get isExpanded() {\n        return !this.props.canToggle && !this.props.readonly;\n    }\n\n    switchColor(colorIndex) {\n        this.props.record.update({ [this.props.name]: colorIndex });\n    }\n}\n\nexport const colorPickerField = {\n    component: ColorPickerField,\n    supportedTypes: [\"integer\"],\n    extractProps: ({ viewType }) => ({\n        canToggle: viewType !== \"list\",\n    }),\n};\n\nregistry.category(\"fields\").add(\"color_picker\", colorPickerField);\n", "import { isBinarySize } from \"@web/core/utils/binary\";\nimport { registry } from \"@web/core/registry\";\nimport { imageUrl } from \"@web/core/utils/urls\";\nimport { ImageField, imageField } from \"@web/views/fields/image/image_field\";\n\nexport class ContactImageField extends ImageField {\n    static template = \"web.ContactImageField\";\n\n    getUrl(imageFieldName) {\n        if (\n            this.props.previewImage &&\n            (!this.props.record.data[this.props.name] || !this.state.isValid)\n        ) {\n            if (isBinarySize(this.props.record.data[imageFieldName])) {\n                this.lastURL = imageUrl(\n                    this.props.record.resModel,\n                    this.props.record.resId,\n                    imageFieldName,\n                    { unique: this.rawCacheKey }\n                );\n            } else {\n                this.lastURL = `data:image/png;base64,${this.props.record.data[imageFieldName]}`;\n            }\n            return this.lastURL;\n        }\n        return super.getUrl(imageFieldName);\n    }\n\n    get imgClass() {\n        let classes = super.imgClass;\n        if (!this.props.record.data[this.props.name] || !this.state.isValid) {\n            classes += \" opacity-100 opacity-25-hover\";\n        }\n        return classes;\n    }\n\n    get containsValidImage() {\n        return this.props.record.data[this.props.name] && this.state.isValid;\n    }\n}\n\nexport const contactImageField = {\n    ...imageField,\n    component: ContactImageField,\n};\n\nregistry.category(\"fields\").add(\"contact_image\", contactImageField);\n", "import { registry } from \"@web/core/registry\";\nimport { standardFieldProps } from \"../standard_field_props\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { Component } from \"@odoo/owl\";\n\nexport class ContactStatisticsField extends Component {\n    static template = \"web.ContactStatisticsField\";\n    static props = {\n        ...standardFieldProps,\n    };\n\n    get list() {\n        return this.props.record.data[this.props.name] || [];\n    }\n}\n\nexport const contactStatisticsField = {\n    component: ContactStatisticsField,\n    displayName: _t(\"Contact Statistics\"),\n    supportedTypes: [\"json\"],\n};\n\nregistry.category(\"fields\").add(\"contact_statistics\", contactStatisticsField);\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { evaluateBooleanExpr } from \"@web/core/py_js/py\";\nimport { registry } from \"@web/core/registry\";\nimport { omit } from \"@web/core/utils/objects\";\n\nimport { CopyButton } from \"@web/core/copy_button/copy_button\";\nimport { CharField } from \"../char/char_field\";\nimport { standardFieldProps } from \"../standard_field_props\";\nimport { UrlField } from \"../url/url_field\";\n\nimport { Component } from \"@odoo/owl\";\n\nclass CopyClipboardField extends Component {\n    static template = \"web.CopyClipboardField\";\n    static props = {\n        ...standardFieldProps,\n        string: { type: String, optional: true },\n        disabledExpr: { type: String, optional: true },\n    };\n\n    setup() {\n        this.copyText = this.props.string || _t(\"Copy\");\n        this.successText = _t(\"Copied\");\n    }\n\n    get copyButtonClassName() {\n        return `o_btn_${this.type}_copy btn-sm`;\n    }\n    get fieldProps() {\n        return omit(this.props, \"string\", \"disabledExpr\");\n    }\n    get type() {\n        return this.props.record.fields[this.props.name].type;\n    }\n    get disabled() {\n        return this.props.disabledExpr\n            ? evaluateBooleanExpr(\n                  this.props.disabledExpr,\n                  this.props.record.evalContextWithVirtualIds\n              )\n            : false;\n    }\n}\n\nexport class CopyClipboardButtonField extends CopyClipboardField {\n    static template = \"web.CopyClipboardButtonField\";\n    static components = { CopyButton };\n    static props = {\n        ...CopyClipboardField.props,\n        btnClass: { type: String, optional: true },\n    };\n    static defaultProps = {\n        ...CopyClipboardField.defaultProps,\n        btnClass: \"primary\",\n    };\n\n    get copyButtonClassName() {\n        return `o_btn_${this.type}_copy btn-${this.props.btnClass} rounded-2`;\n    }\n}\n\nexport class CopyClipboardCharField extends CopyClipboardField {\n    static components = { Field: CharField, CopyButton };\n\n    get copyButtonIcon() {\n        return \"fa-clipboard\";\n    }\n}\n\nexport class CopyClipboardURLField extends CopyClipboardField {\n    static components = { Field: UrlField, CopyButton };\n\n    get copyButtonIcon() {\n        return \"fa-link\";\n    }\n}\n\n// ----------------------------------------------------------------------------\n\nfunction extractProps({ string, attrs }) {\n    return {\n        string,\n        disabledExpr: attrs.disabled,\n    };\n}\n\nexport const copyClipboardButtonField = {\n    component: CopyClipboardButtonField,\n    displayName: _t(\"Copy to Clipboard\"),\n    extractProps: (fieldInfo) => ({\n        ...extractProps(fieldInfo),\n        btnClass: fieldInfo.options.btn_class,\n    }),\n};\n\nregistry.category(\"fields\").add(\"CopyClipboardButton\", copyClipboardButtonField);\n\nexport const copyClipboardCharField = {\n    component: CopyClipboardCharField,\n    displayName: _t(\"Copy Text to Clipboard\"),\n    supportedTypes: [\"char\"],\n    extractProps,\n};\n\nregistry.category(\"fields\").add(\"CopyClipboardChar\", copyClipboardCharField);\n\nexport const copyClipboardURLField = {\n    component: CopyClipboardURLField,\n    displayName: _t(\"Copy URL to Clipboard\"),\n    supportedTypes: [\"char\"],\n    extractProps,\n};\n\nregistry.category(\"fields\").add(\"CopyClipboardURL\", copyClipboardURLField);\n", "import { Component, onWillRender, useEffect, useRef, useState } from \"@odoo/owl\";\nimport { useDateTimePicker } from \"@web/core/datetime/datetime_picker_hook\";\nimport { areDatesEqual, deserializeDate, deserializeDateTime, today } from \"@web/core/l10n/dates\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { evaluateBooleanExpr } from \"@web/core/py_js/py\";\nimport { registry } from \"@web/core/registry\";\nimport { ensureArray } from \"@web/core/utils/arrays\";\nimport { exprToBoolean } from \"@web/core/utils/strings\";\nimport { FIELD_WIDTHS } from \"@web/views/list/column_width_hook\";\nimport { formatDate, formatDateTime } from \"../formatters\";\nimport { standardFieldProps } from \"../standard_field_props\";\n\nconst { DateTime } = luxon;\n\nfunction getFormattedPlaceholder(value, type, options) {\n    if (value instanceof luxon.DateTime) {\n        return type === \"date\" ? formatDate(value, options) : formatDateTime(value, options);\n    }\n    return value || \"\";\n}\n\n/**\n * @typedef {luxon.DateTime} DateTime\n *\n * @typedef {import(\"../standard_field_props\").StandardFieldProps & {\n *  endDateField?: string;\n *  maxDate?: string;\n *  minDate?: string;\n *  placeholder?: string;\n *  required?: boolean;\n *  rounding?: number;\n *  startDateField?: string;\n *  warnFuture?: boolean;\n *  showSeconds?: boolean;\n *  showTime?: boolean;\n *  numeric?: boolean;\n *  minPrecision?: string;\n *  maxPrecision?: string;\n * }} DateTimeFieldProps\n *\n * @typedef {import(\"@web/core/datetime/datetime_picker\").DateTimePickerProps} DateTimePickerProps\n */\n\n/** @extends {Component<DateTimeFieldProps>} */\nexport class DateTimeField extends Component {\n    static props = {\n        ...standardFieldProps,\n        endDateField: { type: String, optional: true },\n        maxDate: { type: String, optional: true },\n        minDate: { type: String, optional: true },\n        alwaysRange: { type: Boolean, optional: true },\n        placeholder: { type: String, optional: true },\n        required: { type: Boolean, optional: true },\n        rounding: { type: Number, optional: true },\n        startDateField: { type: String, optional: true },\n        numeric: { type: Boolean, optional: true },\n        warnFuture: { type: Boolean, optional: true },\n        showSeconds: { type: Boolean, optional: true },\n        showTime: { type: Boolean, optional: true },\n        minPrecision: {\n            type: String,\n            optional: true,\n            validate: (props) => [\"days\", \"months\", \"years\", \"decades\"].includes(props),\n        },\n        maxPrecision: {\n            type: String,\n            optional: true,\n            validate: (props) => [\"days\", \"months\", \"years\", \"decades\"].includes(props),\n        },\n    };\n    static defaultProps = {\n        showSeconds: false,\n        showTime: true,\n        numeric: false,\n    };\n\n    static template = \"web.DateTimeField\";\n\n    //-------------------------------------------------------------------------\n    // Getters\n    //-------------------------------------------------------------------------\n\n    get endDateField() {\n        return this.relatedField ? this.props.endDateField || this.props.name : null;\n    }\n\n    get field() {\n        return this.props.record.fields[this.props.name];\n    }\n\n    get relatedField() {\n        return this.props.startDateField || this.props.endDateField;\n    }\n\n    get startDateField() {\n        return this.props.startDateField || this.props.name;\n    }\n\n    get values() {\n        return ensureArray(this.state.value);\n    }\n\n    //-------------------------------------------------------------------------\n    // Lifecycle\n    //-------------------------------------------------------------------------\n\n    setup() {\n        const getPickerProps = () => {\n            const value = this.getRecordValue();\n            /** @type {DateTimePickerProps} */\n            const pickerProps = {\n                value,\n                type: this.field.type,\n                range: this.isRange(value),\n                showRangeToggler:\n                    this.relatedField && !this.props.required && !this.props.alwaysRange,\n                onToggleRange,\n            };\n            if (this.props.maxDate) {\n                pickerProps.maxDate = this.parseLimitDate(this.props.maxDate);\n            }\n            if (this.props.minDate) {\n                pickerProps.minDate = this.parseLimitDate(this.props.minDate);\n            }\n            if (!isNaN(this.props.rounding)) {\n                pickerProps.rounding = this.props.rounding;\n            } else if (this.props.showSeconds) {\n                pickerProps.rounding = 0;\n            }\n            if (this.props.maxPrecision) {\n                pickerProps.maxPrecision = this.props.maxPrecision;\n            }\n            if (this.props.minPrecision) {\n                pickerProps.minPrecision = this.props.minPrecision;\n            }\n            return pickerProps;\n        };\n\n        const onToggleRange = () => {\n            this.state.range = !this.state.range;\n\n            if (this.state.range) {\n                let values = this.values;\n                const optionalFieldIndex = values[0] ? 1 : 0;\n\n                if (!values[0] && !values[1]) {\n                    values = [DateTime.local(), DateTime.local()];\n                }\n                values[optionalFieldIndex] = optionalFieldIndex\n                    ? values[0].plus({ hours: 1 })\n                    : values[1].minus({ hours: 1 });\n\n                this.state.focusedDateIndex = 0;\n                this.state.value = values;\n            } else {\n                const mainFieldIndex = this.props.name === this.startDateField ? 0 : 1;\n\n                this.state.focusedDateIndex = mainFieldIndex;\n                this.state.value[mainFieldIndex ? 0 : 1] = false;\n            }\n        };\n\n        const dateTimePicker = useDateTimePicker({\n            target: \"root\",\n            showSeconds: this.props.showSeconds,\n            get pickerProps() {\n                return getPickerProps();\n            },\n            onChange: () => {\n                this.state.range = this.isRange(this.state.value);\n            },\n            onClose: () => {\n                this.picker.activeInput = \"\";\n                this.state.value = this.getRecordValue();\n            },\n            onApply: async () => {\n                const toUpdate = {};\n                if (Array.isArray(this.state.value)) {\n                    // Value is already a range\n                    [toUpdate[this.startDateField], toUpdate[this.endDateField]] = this.state.value;\n                } else {\n                    toUpdate[this.props.name] = this.state.value;\n                }\n\n                // If startDateField or endDateField are not set, delete unchanged fields\n                for (const fieldName in toUpdate) {\n                    if (areDatesEqual(toUpdate[fieldName], this.props.record.data[fieldName])) {\n                        delete toUpdate[fieldName];\n                    }\n                }\n\n                if (Object.keys(toUpdate).length) {\n                    await this.props.record.update(toUpdate);\n                }\n            },\n        });\n        // Subscribes to changes made on the picker state\n        this.state = useState(dateTimePicker.state);\n        this.picker = useState({ activeInput: \"\" });\n        this.openPicker = dateTimePicker.open;\n\n        this.startDate = useRef(\"start-date\");\n        this.endDate = useRef(\"end-date\");\n\n        useEffect(\n            () => {\n                [this.startDate, this.endDate].forEach((ref, index) => {\n                    if (ref.el?.getAttribute(\"data-field\") === this.picker.activeInput) {\n                        ref.el.focus();\n                        this.openPicker(index);\n                    }\n                });\n            },\n            () => [this.startDate.el?.tagName, this.endDate.el?.tagName, this.picker.activeInput]\n        );\n\n        onWillRender(() => this.triggerIsDirty());\n\n        this.futureWarningMsg = _t(\"This date is in the future\");\n    }\n\n    //-------------------------------------------------------------------------\n    // Methods\n    //-------------------------------------------------------------------------\n\n    /**\n     * @param {number} valueIndex\n     * @param {boolean} [numeric=this.props.numeric]\n     * @returns formatted date string\n     */\n    getFormattedValue(valueIndex, numeric = this.props.numeric) {\n        const values = this.values;\n        const value = values[valueIndex];\n        if (!value) {\n            return \"\";\n        }\n        const { showSeconds, showTime } = this.props;\n        if (this.field.type === \"date\") {\n            return formatDate(value, { numeric });\n        } else {\n            const showDate =\n                !showTime || valueIndex !== 1 || !values[0] || !values[0].hasSame(value, \"day\");\n            return formatDateTime(value, {\n                numeric,\n                showSeconds,\n                showTime,\n                showDate,\n            });\n        }\n    }\n\n    /**\n     * @returns {DateTimePickerProps[\"value\"]}\n     */\n    getRecordValue() {\n        if (this.relatedField) {\n            return [\n                this.props.record.data[this.startDateField],\n                this.props.record.data[this.endDateField],\n            ];\n        } else {\n            return this.props.record.data[this.props.name];\n        }\n    }\n\n    /**\n     * @param {number} index\n     */\n    isDateInTheFuture(index) {\n        return this.values[index] > today();\n    }\n\n    /**\n     * @param {string} fieldName\n     */\n    isEmpty(fieldName) {\n        return fieldName === this.startDateField ? !this.values[0] : !this.values[1];\n    }\n\n    /**\n     * @param {DateTimePickerProps[\"value\"]} value\n     * @returns {boolean}\n     */\n    isRange(value) {\n        if (!this.relatedField) {\n            return false;\n        }\n        return (\n            this.props.alwaysRange ||\n            this.props.required ||\n            ensureArray(value).filter(Boolean).length === 2\n        );\n    }\n\n    /**\n     * @param {string} value\n     */\n    parseLimitDate(value) {\n        if (value === \"today\") {\n            return value;\n        }\n        return this.field.type === \"date\" ? deserializeDate(value) : deserializeDateTime(value);\n    }\n\n    /**\n     * @return {boolean}\n     */\n    shouldShowSeparator() {\n        return (\n            (this.props.alwaysRange &&\n                (this.props.readonly\n                    ? !this.isEmpty(this.startDateField) || !this.isEmpty(this.endDateField)\n                    : true)) ||\n            (this.state.range &&\n                (this.props.required ||\n                    (!this.isEmpty(this.startDateField) && !this.isEmpty(this.endDateField))))\n        );\n    }\n\n    /**\n     * The given props are used to compute the current value and compare it to\n     * the state handled by the datetime hook.\n     *\n     * @param {boolean} [isDirty]\n     */\n    triggerIsDirty(isDirty) {\n        this.props.record.model.bus.trigger(\n            \"FIELD_IS_DIRTY\",\n            isDirty ?? !areDatesEqual(this.getRecordValue(), this.state.value)\n        );\n    }\n\n    //-------------------------------------------------------------------------\n    // Handlers\n    //-------------------------------------------------------------------------\n\n    onInput() {\n        this.triggerIsDirty(true);\n    }\n}\n\nconst START_DATE_FIELD_OPTION = \"start_date_field\";\nconst END_DATE_FIELD_OPTION = \"end_date_field\";\n\nexport const dateField = {\n    component: DateTimeField,\n    displayName: _t(\"Date\"),\n    supportedOptions: [\n        {\n            label: _t(\"Earliest accepted date\"),\n            name: \"min_date\",\n            type: \"string\",\n            help: _t(`ISO-formatted date (e.g. \"2018-12-31\") or \"%s\".`, \"today\"),\n        },\n        {\n            label: _t(\"Latest accepted date\"),\n            name: \"max_date\",\n            type: \"string\",\n            help: _t(`ISO-formatted date (e.g. \"2018-12-31\") or \"%s\".`, \"today\"),\n        },\n        {\n            label: _t(\"Warning for future dates\"),\n            name: \"warn_future\",\n            type: \"boolean\",\n            help: _t(`Displays a warning icon if the input dates are in the future.`),\n        },\n        {\n            label: _t(\"Minimal precision\"),\n            name: \"min_precision\",\n            type: \"selection\",\n            help: _t(\n                `Choose which minimal precision (days, months, ...) you want in the datetime picker.`\n            ),\n            choices: [\n                { label: _t(\"Days\"), value: \"days\" },\n                { label: _t(\"Months\"), value: \"months\" },\n                { label: _t(\"Years\"), value: \"years\" },\n                { label: _t(\"Decades\"), value: \"decades\" },\n            ],\n        },\n        {\n            label: _t(\"Maximal precision\"),\n            name: \"max_precision\",\n            type: \"selection\",\n            help: _t(\n                `Choose which maximal precision (days, months, ...) you want in the datetime picker.`\n            ),\n            choices: [\n                { label: _t(\"Days\"), value: \"days\" },\n                { label: _t(\"Months\"), value: \"months\" },\n                { label: _t(\"Years\"), value: \"years\" },\n                { label: _t(\"Decades\"), value: \"decades\" },\n            ],\n        },\n        {\n            label: _t(\"Date Format\"),\n            name: \"numeric\",\n            type: \"selection\",\n            help: _t(\"Displays the date either in 31/01/%(year)s or in Jan 31, %(year)s\", {\n                year: today().year,\n            }),\n            placeholder: _t(\"Jan 31, %s\", today().year),\n            choices: [\n                { label: _t(\"Jan 31, %s\", today().year), value: false },\n                { label: _t(\"31/01/%s\", today().year), value: true },\n            ],\n        },\n        {\n            label: _t(\"Dynamic Placeholder\"),\n            name: \"placeholder_field\",\n            type: \"field\",\n            availableTypes: [\"date\", \"char\"],\n        },\n    ],\n    supportedTypes: [\"date\"],\n    extractProps: ({ options, placeholder, type }, dynamicInfo) => ({\n        endDateField: options[END_DATE_FIELD_OPTION],\n        maxDate: options.max_date,\n        minDate: options.min_date,\n        alwaysRange: exprToBoolean(options.always_range),\n        placeholder: getFormattedPlaceholder(placeholder, type, { numeric: options.numeric }),\n        required: dynamicInfo.required,\n        rounding: options.rounding && parseInt(options.rounding, 10),\n        startDateField: options[START_DATE_FIELD_OPTION],\n        numeric: options.numeric,\n        warnFuture: exprToBoolean(options.warn_future),\n        minPrecision: options.min_precision,\n        maxPrecision: options.max_precision,\n    }),\n    listViewWidth: ({ options }) =>\n        options.numeric ? FIELD_WIDTHS.numeric_date : FIELD_WIDTHS.date,\n    fieldDependencies: ({ type, attrs, options }) => {\n        const deps = [];\n        if (options[START_DATE_FIELD_OPTION]) {\n            deps.push({\n                name: options[START_DATE_FIELD_OPTION],\n                type,\n                readonly: false,\n                ...attrs,\n            });\n            if (options[END_DATE_FIELD_OPTION]) {\n                console.warn(\n                    `A field cannot have both ${START_DATE_FIELD_OPTION} and ${END_DATE_FIELD_OPTION} options at the same time`\n                );\n            }\n        } else if (options[END_DATE_FIELD_OPTION]) {\n            deps.push({\n                name: options[END_DATE_FIELD_OPTION],\n                type,\n                readonly: false,\n                ...attrs,\n            });\n        }\n        return deps;\n    },\n};\n\nexport const dateTimeField = {\n    ...dateField,\n    displayName: _t(\"Date & Time\"),\n    supportedOptions: [\n        ...dateField.supportedOptions.filter((o) => o.name !== \"placeholder_field\"),\n        {\n            label: _t(\"Time interval\"),\n            name: \"rounding\",\n            type: \"number\",\n            default: 5,\n            help: _t(\n                `Control the number of minutes in the time selection. E.g. set it to 15 to work in quarters.`\n            ),\n        },\n        {\n            label: _t(\"Show time\"),\n            name: \"show_time\",\n            type: \"boolean\",\n            default: true,\n            help: _t(`Displays or hides the time in the datetime value.`),\n        },\n        {\n            label: _t(\"Show seconds\"),\n            name: \"show_seconds\",\n            type: \"boolean\",\n            default: false,\n            help: _t(\n                `Displays or hides the seconds in the datetime value. Affect only the readable datetime format.`\n            ),\n        },\n        {\n            label: _t(\"Dynamic Placeholder\"),\n            name: \"placeholder_field\",\n            type: \"field\",\n            availableTypes: [\"datetime\", \"char\"],\n        },\n    ],\n    extractProps: ({ attrs, options, placeholder, type }, dynamicInfo) => {\n        const showSeconds = exprToBoolean(options.show_seconds ?? false);\n        const showTime = exprToBoolean(options.show_time ?? true);\n        const numeric = exprToBoolean(options.numeric ?? false);\n        return {\n            ...dateField.extractProps({ attrs, options, placeholder, type }, dynamicInfo),\n            placeholder: getFormattedPlaceholder(placeholder, type, {\n                numeric,\n                showSeconds,\n                showTime,\n            }),\n            numeric,\n            showSeconds,\n            showTime,\n        };\n    },\n    supportedTypes: [\"datetime\"],\n    listViewWidth: ({ options }) => {\n        if (!exprToBoolean(options.show_time ?? true)) {\n            return dateField.listViewWidth({ options });\n        }\n        return options.numeric ? FIELD_WIDTHS.numeric_datetime : FIELD_WIDTHS.datetime;\n    },\n};\n\nexport const dateRangeField = {\n    ...dateTimeField,\n    displayName: _t(\"Date Range\"),\n    supportedOptions: [\n        ...dateTimeField.supportedOptions.filter((o) => o.name !== \"placeholder_field\"),\n        {\n            label: _t(\"Start date field\"),\n            name: START_DATE_FIELD_OPTION,\n            type: \"field\",\n            availableTypes: [\"date\", \"datetime\"],\n        },\n        {\n            label: _t(\"End date field\"),\n            name: END_DATE_FIELD_OPTION,\n            type: \"field\",\n            availableTypes: [\"date\", \"datetime\"],\n        },\n        {\n            label: _t(\"Always range\"),\n            name: \"always_range\",\n            type: \"boolean\",\n            default: false,\n            help: _t(\n                `Set to true the full range input has to be display by default, even if empty.`\n            ),\n        },\n        {\n            label: _t(\"Dynamic Placeholder\"),\n            name: \"placeholder_field\",\n            type: \"field\",\n            availableTypes: [\"date\", \"datetime\", \"char\"],\n        },\n    ],\n    supportedTypes: [\"date\", \"datetime\"],\n    listViewWidth: ({ type, options }) => {\n        const width =\n            type === \"datetime\"\n                ? dateTimeField.listViewWidth({ options })\n                : dateField.listViewWidth({ options });\n        return 2 * width + 30; // 30px for the arrow and the gaps\n    },\n    isValid: (record, fieldname, fieldInfo) => {\n        if (fieldInfo.widget === \"daterange\") {\n            if (\n                !record.data[fieldInfo.options[END_DATE_FIELD_OPTION]] !==\n                    !record.data[fieldname] &&\n                evaluateBooleanExpr(\n                    record.activeFields[fieldInfo.options[END_DATE_FIELD_OPTION]]?.required,\n                    record.evalContextWithVirtualIds\n                )\n            ) {\n                return false;\n            }\n            if (\n                !record.data[fieldInfo.options[START_DATE_FIELD_OPTION]] !==\n                    !record.data[fieldname] &&\n                evaluateBooleanExpr(\n                    record.activeFields[fieldInfo.options[START_DATE_FIELD_OPTION]]?.required,\n                    record.evalContextWithVirtualIds\n                )\n            ) {\n                return false;\n            }\n        }\n        return !record.isFieldInvalid(fieldname);\n    },\n};\n\nregistry\n    .category(\"fields\")\n    .add(\"date\", dateField)\n    .add(\"daterange\", dateRangeField)\n    .add(\"datetime\", dateTimeField);\n", "import { useRef } from \"@odoo/owl\";\nimport { registry } from \"@web/core/registry\";\nimport { useAutoresize } from \"@web/core/utils/autoresize\";\nimport { DateTimeField, dateField, dateRangeField, dateTimeField } from \"./datetime_field\";\n\nexport class ListDateTimeField extends DateTimeField {\n    setup() {\n        super.setup();\n        const startDateRef = useRef(\"start-date\");\n        useAutoresize(startDateRef, { offset: -5, ignoreIfEmpty: true });\n    }\n}\n\nexport const listDateField = { ...dateField, component: ListDateTimeField };\nexport const listDateRangeField = { ...dateRangeField, component: ListDateTimeField };\nexport const listDateTimeField = { ...dateTimeField, component: ListDateTimeField };\n\nregistry\n    .category(\"fields\")\n    .add(\"list.date\", listDateField)\n    .add(\"list.daterange\", listDateRangeField)\n    .add(\"list.datetime\", listDateTimeField);\n", "import { Component, useState } from \"@odoo/owl\";\nimport { Domain, InvalidDomainError } from \"@web/core/domain\";\nimport { DomainSelector } from \"@web/core/domain_selector/domain_selector\";\nimport { useGetDefaultLeafDomain } from \"@web/core/domain_selector/utils\";\nimport { DomainSelectorDialog } from \"@web/core/domain_selector_dialog/domain_selector_dialog\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { EvaluationError } from \"@web/core/py_js/py_builtin\";\nimport { registry } from \"@web/core/registry\";\nimport { domainContainsExpressions } from \"@web/core/tree_editor/domain_contains_expressions\";\nimport { useBus, useOwnedDialogs, useService } from \"@web/core/utils/hooks\";\nimport { useRecordObserver } from \"@web/model/relational_model/utils\";\nimport { SelectCreateDialog } from \"@web/views/view_dialogs/select_create_dialog\";\nimport { standardFieldProps } from \"../standard_field_props\";\n\nexport class DomainField extends Component {\n    static template = \"web.DomainField\";\n    static components = {\n        DomainSelector,\n    };\n    static props = {\n        ...standardFieldProps,\n        context: { type: Object, optional: true },\n        editInDialog: { type: Boolean, optional: true },\n        resModel: { type: String, optional: true },\n        isFoldable: { type: Boolean, optional: true },\n        countLimit: { type: Number, optional: true },\n        allowExpressions: { type: Boolean, optional: true },\n    };\n    static defaultProps = {\n        editInDialog: false,\n        isFoldable: false,\n        countLimit: 10000,\n        allowExpressions: false,\n    };\n\n    setup() {\n        this.orm = useService(\"orm\");\n        this.notification = useService(\"notification\");\n        this.treeProcessor = useService(\"tree_processor\");\n        this.getDefaultLeafDomain = useGetDefaultLeafDomain();\n        this.addDialog = useOwnedDialogs();\n\n        this.state = useState({\n            isValid: null,\n            recordCount: null,\n            hasLimitedCount: null,\n            folded: this.props.isFoldable,\n            facets: [],\n        });\n\n        this.debugDomain = null;\n        useRecordObserver(async (record, nextProps) => {\n            nextProps = { ...nextProps, record };\n            if (this.debugDomain && this.props.readonly !== nextProps.readonly) {\n                this.debugDomain = null;\n            }\n            if (this.debugDomain) {\n                this.state.isValid = await this.quickValidityCheck(nextProps);\n                if (!this.state.isValid) {\n                    this.state.recordCount = 0;\n                    nextProps.record.setInvalidField(nextProps.name);\n                }\n            } else {\n                this.checkProps(nextProps); // not awaited\n            }\n            if (nextProps.isFoldable) {\n                this.loadFacets(nextProps);\n            }\n        });\n\n        useBus(this.props.record.model.bus, \"NEED_LOCAL_CHANGES\", async (ev) => {\n            if (this.debugDomain) {\n                const props = this.props;\n                const handleChanges = async () => {\n                    await props.record.update({ [props.name]: this.debugDomain });\n                    const isValid = await this.quickValidityCheck(props);\n                    if (isValid) {\n                        this.debugDomain = null; // will allow the count to be loaded if needed\n                    } else {\n                        this.state.isValid = false;\n                        this.state.recordCount = 0;\n                        props.record.setInvalidField(props.name);\n                    }\n                };\n                ev.detail.proms.push(handleChanges());\n            }\n        });\n    }\n\n    allowExpressions(props) {\n        return props.allowExpressions;\n    }\n\n    getContext(props = this.props) {\n        return props.context;\n    }\n\n    getDomain(props = this.props) {\n        return props.record.data[props.name] || \"[]\";\n    }\n\n    getEvaluatedDomain(props = this.props) {\n        const domainStringRepr = this.getDomain(props);\n        const evalContext = this.getContext(props);\n        if (domainContainsExpressions(domainStringRepr)) {\n            const allowExpressions = this.allowExpressions(props);\n            if (domainStringRepr !== this.lastDomainChecked) {\n                this.lastDomainChecked = domainStringRepr;\n                this.notification.add(\n                    allowExpressions\n                        ? _t(\"The domain involves non-literals. Their evaluation might fail.\")\n                        : _t(\"The domain should not involve non-literals\")\n                );\n            }\n            if (!allowExpressions) {\n                return { isInvalid: true };\n            }\n        }\n        try {\n            const domain = new Domain(domainStringRepr).toList(evalContext);\n            // Here, there is still some incertitude on the domain validity.\n            // we could improve this check but a complete (async) check is done\n            // when loading the record count associated with the domain.\n            return domain;\n        } catch (error) {\n            if (error instanceof InvalidDomainError || error instanceof EvaluationError) {\n                return { isInvalid: true };\n            }\n            throw error;\n        }\n    }\n\n    getResModel(props = this.props) {\n        let resModel = props.resModel;\n        if (props.record.fieldNames.includes(resModel)) {\n            resModel = props.record.data[resModel];\n        }\n        return resModel;\n    }\n\n    async addCondition() {\n        const defaultDomain = await this.getDefaultLeafDomain(this.getResModel());\n        this.update(defaultDomain);\n        this.state.folded = false;\n    }\n\n    async loadFacets(props = this.props) {\n        const resModel = this.getResModel(props);\n\n        if (!resModel) {\n            this.state.facets = [];\n            this.state.folded = false;\n            return;\n        }\n\n        if (typeof resModel !== \"string\") {\n            // we don't want to support invalid models\n            throw new Error(`Invalid model: ${resModel}`);\n        }\n\n        let promises = [];\n        const domain = this.getDomain(props);\n        try {\n            const tree = await this.treeProcessor.treeFromDomain(resModel, domain, !this.env.debug);\n            const trees = !tree.negate && tree.value === \"&\" ? tree.children : [tree];\n            promises = trees.map((tree) =>\n                this.treeProcessor.getDomainTreeDescription(resModel, tree)\n            );\n        } catch (error) {\n            if (error.data?.name === \"builtins.KeyError\" && error.data.message === resModel) {\n                // we don't want to support invalid models\n                throw new Error(`Invalid model: ${resModel}`);\n            }\n            this.state.facets = [];\n            this.state.folded = false;\n        }\n        this.state.facets = await Promise.all(promises);\n    }\n\n    async checkProps(props = this.props) {\n        const resModel = this.getResModel(props);\n        if (!resModel) {\n            this.updateState({});\n            return;\n        }\n\n        if (typeof resModel !== \"string\") {\n            // we don't want to support invalid models\n            throw new Error(`Invalid model: ${resModel}`);\n        }\n\n        const domain = this.getEvaluatedDomain(props);\n        if (domain.isInvalid) {\n            this.updateState({ isValid: false, recordCount: 0, hasLimitedCount: false });\n            return;\n        }\n\n        let recordCount;\n        let hasLimitedCount = false;\n        const context = this.getContext(props);\n        let limit;\n        if (props.countLimit !== Number.MAX_SAFE_INTEGER) {\n            limit = props.countLimit + 1;\n        }\n        try {\n            recordCount = await this.orm.silent.searchCount(resModel, domain, { context, limit });\n        } catch (error) {\n            if (error.data?.name === \"builtins.KeyError\" && error.data.message === resModel) {\n                // we don't want to support invalid models\n                throw new Error(`Invalid model: ${resModel}`);\n            }\n            this.updateState({ isValid: false, recordCount: 0, hasLimitedCount: false });\n            return;\n        }\n        if (limit && recordCount >= limit) {\n            hasLimitedCount = true;\n            recordCount = props.countLimit;\n        }\n        this.updateState({ isValid: true, recordCount, hasLimitedCount });\n    }\n\n    onButtonClick() {\n        // resModel, domain, and context are assumed to be valid here.\n        this.addDialog(\n            SelectCreateDialog,\n            {\n                title: _t(\"Selected records\"),\n                noCreate: true,\n                multiSelect: false,\n                resModel: this.getResModel(),\n                domain: this.getEvaluatedDomain(),\n                context: this.getContext(),\n            },\n            {\n                // The counter is reloaded \"on close\" because some modal allows\n                // to modify data that can impact the counter\n                onClose: () => this.checkProps(),\n            }\n        );\n    }\n\n    onEditDialogBtnClick() {\n        // resModel is assumed to be valid here\n        this.addDialog(DomainSelectorDialog, {\n            resModel: this.getResModel(),\n            domain: this.getDomain(),\n            isDebugMode: !!this.env.debug,\n            onConfirm: this.update.bind(this),\n        });\n    }\n\n    async quickValidityCheck(props) {\n        const resModel = this.getResModel(props);\n        if (!resModel) {\n            return false;\n        }\n        const domain = this.getEvaluatedDomain(props);\n        if (domain.isInvalid) {\n            return false;\n        }\n        return rpc(\"/web/domain/validate\", { model: resModel, domain });\n    }\n\n    update(domain, isDebugEdited = false) {\n        if (!isDebugEdited) {\n            this.debugDomain = null;\n        }\n        this.props.record.update({ [this.props.name]: domain });\n        this.props.record.model.bus.trigger(\"FIELD_IS_DIRTY\", false);\n    }\n\n    debugUpdate(domain) {\n        const isDirty = domain !== this.getDomain();\n        this.debugDomain = isDirty ? domain : null;\n        this.props.record.model.bus.trigger(\"FIELD_IS_DIRTY\", isDirty);\n        if (!this.props.record.isValid) {\n            this.props.record.resetFieldValidity(this.props.name);\n        }\n    }\n\n    fold() {\n        this.state.folded = true;\n    }\n\n    updateState(params = {}) {\n        Object.assign(this.state, {\n            isValid: \"isValid\" in params ? params.isValid : null,\n            recordCount: \"recordCount\" in params ? params.recordCount : null,\n            hasLimitedCount: \"hasLimitedCount\" in params ? params.hasLimitedCount : null,\n        });\n    }\n}\n\nexport const domainField = {\n    component: DomainField,\n    displayName: _t(\"Domain\"),\n    supportedOptions: [\n        {\n            label: _t(\"Edit in dialog\"),\n            name: \"in_dialog\",\n            type: \"boolean\",\n        },\n        {\n            label: _t(\"Foldable\"),\n            name: \"foldable\",\n            type: \"boolean\",\n            help: _t(\"Display the domain using facets\"),\n        },\n        {\n            label: _t(\"Allow expressions\"),\n            name: \"allow_expressions\",\n            type: \"boolean\",\n            help: _t(\"If true, non-literals are accepted\"),\n        },\n        {\n            label: _t(\"Model\"),\n            name: \"model\",\n            type: \"string\",\n        },\n        {\n            label: _t(\"Count Limit\"),\n            name: \"count_limit\",\n            type: \"number\",\n        },\n    ],\n    supportedTypes: [\"char\", \"text\"],\n    isEmpty: () => false,\n    extractProps({ options }, dynamicInfo) {\n        return {\n            editInDialog: options.in_dialog,\n            isFoldable: options.foldable,\n            allowExpressions: options.allow_expressions,\n            resModel: options.model,\n            countLimit: options.count_limit,\n            context: dynamicInfo.context,\n        };\n    },\n};\n\nregistry.category(\"fields\").add(\"domain\", domainField);\n", "import { usePopover } from \"@web/core/popover/popover_hook\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { useComponent } from \"@odoo/owl\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { DynamicPlaceholderPopover } from \"./dynamic_placeholder_popover\";\n\nexport function useDynamicPlaceholder(elementRef) {\n    const TRIGGER_KEY = \"#\";\n    const ownerField = useComponent();\n    const triggerKeyReplaceRegex = new RegExp(`${TRIGGER_KEY}$`);\n    let closeCallback;\n    let positionCallback;\n    const popover = usePopover(DynamicPlaceholderPopover, {\n        onClose: () => closeCallback?.(),\n        onPositioned: (popper, position) => positionCallback?.(popper, position),\n    });\n    const notification = useService(\"notification\");\n\n    let model = null;\n\n    const onDynamicPlaceholderValidate = function (path, defaultValue) {\n        const element = elementRef?.el;\n        if (!element) {\n            return;\n        }\n        let rangeIndex = parseInt(element.getAttribute(\"data-oe-dynamic-placeholder-range-index\"));\n        // When the user cancel/close the popover, the path is empty.\n        if (path) {\n            defaultValue = defaultValue.replace(\"|||\", \"\");\n            const dynamicPlaceholder = ` {{object.${path}${\n                defaultValue?.length ? ` ||| ${defaultValue}` : \"\"\n            }}}`;\n            const baseValue = element.value;\n            const splitedValue = [baseValue.slice(0, rangeIndex), baseValue.slice(rangeIndex)];\n            const newValue =\n                splitedValue[0].replace(triggerKeyReplaceRegex, \"\") +\n                dynamicPlaceholder +\n                splitedValue[1];\n            const changes = { [ownerField.props.name]: newValue };\n            ownerField.props.record.update(changes);\n            element.value = newValue;\n\n            // -1 to take the removal of the trigger key char into account\n            rangeIndex += dynamicPlaceholder.length - 1;\n            element.setSelectionRange(rangeIndex, rangeIndex);\n            element.removeAttribute(\"data-oe-dynamic-placeholder-range-index\");\n        }\n    };\n    const onDynamicPlaceholderClose = function () {\n        elementRef?.el.focus();\n    };\n\n    /**\n     * Open a Model Field Selector which can select fields to create a dynamic\n     * placeholder string in the Input with or without a default text value.\n     *\n     * @public\n     * @param {Object} opts\n     * @param {function} opts.validateCallback\n     * @param {function} opts.closeCallback\n     * @param {function} [opts.positionCallback]\n     */\n    async function open(opts) {\n        if (!model) {\n            return notification.add(\n                _t(\"You need to select a model before opening the dynamic placeholder selector.\"),\n                { type: \"danger\" }\n            );\n        }\n        closeCallback = opts.closeCallback;\n        positionCallback = opts.positionCallback;\n        popover.open(elementRef?.el, {\n            resModel: model,\n            validate: opts.validateCallback,\n        });\n    }\n    async function onKeydown(ev) {\n        const element = elementRef?.el;\n        if (ev.target === element && ev.key === TRIGGER_KEY) {\n            const currentRangeIndex = element.selectionStart;\n            // +1 to take the trigger key char into account\n            element.setAttribute(\"data-oe-dynamic-placeholder-range-index\", currentRangeIndex + 1);\n            await open({\n                validateCallback: onDynamicPlaceholderValidate,\n                closeCallback: onDynamicPlaceholderClose,\n            });\n        }\n    }\n    function updateModel(model_name_location) {\n        const recordData = ownerField.props.record.data;\n        model = recordData[model_name_location] || recordData.model;\n    }\n\n    return {\n        updateModel: updateModel,\n        onKeydown: onKeydown,\n        setElementRef: (er) => (elementRef = er),\n        open: open,\n    };\n}\n", "import { useAutofocus } from \"@web/core/utils/hooks\";\nimport { ModelFieldSelectorPopover } from \"@web/core/model_field_selector/model_field_selector_popover\";\nimport { Component, onWillStart, useState } from \"@odoo/owl\";\nimport { user } from \"@web/core/user\";\nimport { registry } from \"@web/core/registry\";\n\nconst allowedQwebExpressionsService = {\n    dependencies: [\"orm\"],\n    start(env, { orm }) {\n        const cache = new Map();\n        return (resModel) => {\n            if (cache.has(resModel)) {\n                return cache.get(resModel);\n            }\n            const prom = orm.call(resModel, \"mail_allowed_qweb_expressions\").catch((e) => {\n                cache.delete(resModel);\n                return Promise.reject(e);\n            });\n            cache.set(resModel, prom);\n            return prom;\n        };\n    },\n};\nregistry.category(\"services\").add(\"allowed_qweb_expressions\", allowedQwebExpressionsService);\n\nexport class DynamicPlaceholderPopover extends Component {\n    static template = \"web.DynamicPlaceholderPopover\";\n    static components = {\n        ModelFieldSelectorPopover,\n    };\n    static props = [\"resModel\", \"validate\", \"close\"];\n\n    setup() {\n        useAutofocus();\n        this.state = useState({\n            path: \"\",\n            isPathSelected: false,\n            defaultValue: \"\",\n        });\n        onWillStart(() => this._loadAllowedExpressions());\n    }\n\n    async _loadAllowedExpressions() {\n        const getAllowedQwebExpressions = this.env.services[\"allowed_qweb_expressions\"];\n        [this.isTemplateEditor, this.allowedQwebExpressions] = await Promise.all([\n            user.hasGroup(\"mail.group_mail_template_editor\"),\n            getAllowedQwebExpressions(this.props.resModel),\n        ]);\n    }\n\n    filter(fieldDef, path) {\n        const fullPath = `object${path ? `.${path}` : \"\"}.${fieldDef.name}`;\n        if (!this.isTemplateEditor && !this.allowedQwebExpressions.includes(fullPath)) {\n            return false;\n        }\n        if (fieldDef.is_property && fieldDef.type === \"separator\") {\n            return false;\n        }\n        return ![\"one2many\", \"boolean\", \"many2many\"].includes(fieldDef.type) && fieldDef.searchable;\n    }\n    closeFieldSelector(isPathSelected = false) {\n        if (isPathSelected) {\n            this.state.isPathSelected = true;\n            return;\n        }\n        this.props.close();\n    }\n    setPath(path, fieldInfo) {\n        this.state.path = path;\n        this.state.fieldName = fieldInfo?.string;\n        this.fieldType = fieldInfo?.type\n    }\n    setDefaultValue(value) {\n        this.state.defaultValue = value;\n    }\n    validate() {\n        this.props.validate(this.state.path, this.state.defaultValue, this.fieldType);\n        this.props.close();\n    }\n\n    onBack() {\n        this.state.defaultValue = \"\";\n        this.state.isPathSelected = false;\n        this.state.path = \"\";\n    }\n\n    // @TODO should rework this to use hotkeys\n    async onInputKeydown(ev) {\n        switch (ev.key) {\n            case \"Enter\": {\n                this.validate();\n                ev.stopPropagation();\n                ev.preventDefault();\n                break;\n            }\n            case \"Escape\": {\n                this.props.close();\n                break;\n            }\n        }\n    }\n}\n", "import { registry } from \"@web/core/registry\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { useInputField } from \"../input_field_hook\";\nimport { standardFieldProps } from \"../standard_field_props\";\n\nimport { Component } from \"@odoo/owl\";\n\nexport class EmailField extends Component {\n    static template = \"web.EmailField\";\n    static props = {\n        ...standardFieldProps,\n        placeholder: { type: String, optional: true },\n    };\n\n    setup() {\n        useInputField({ getValue: () => this.props.record.data[this.props.name] || \"\" });\n    }\n}\n\nexport const emailField = {\n    component: EmailField,\n    displayName: _t(\"Email\"),\n    supportedOptions: [\n        {\n            label: _t(\"Dynamic Placeholder\"),\n            name: \"placeholder_field\",\n            type: \"field\",\n            availableTypes: [\"char\"],\n        },\n    ],\n    supportedTypes: [\"char\"],\n    extractProps: ({ placeholder }) => ({\n        placeholder,\n    }),\n};\n\nregistry.category(\"fields\").add(\"email\", emailField);\n\nclass FormEmailField extends EmailField {\n    static template = \"web.FormEmailField\";\n}\n\nexport const formEmailField = {\n    ...emailField,\n    component: FormEmailField,\n};\n\nregistry.category(\"fields\").add(\"form.email\", formEmailField);\n", "import { Domain } from \"@web/core/domain\";\nimport { evaluateBooleanExpr, evaluateExpr } from \"@web/core/py_js/py\";\nimport { registry } from \"@web/core/registry\";\nimport { utils } from \"@web/core/ui/ui_service\";\nimport { exprToBoolean } from \"@web/core/utils/strings\";\nimport { getFieldContext } from \"@web/model/relational_model/utils\";\nimport { X2M_TYPES, getClassNameFromDecoration } from \"@web/views/utils\";\nimport { getTooltipInfo } from \"./field_tooltip\";\n\nimport { Component, xml } from \"@odoo/owl\";\n\nconst isSmall = utils.isSmall;\n\nconst viewRegistry = registry.category(\"views\");\nconst fieldRegistry = registry.category(\"fields\");\n\nconst validFieldTypes = [\n    \"binary\",\n    \"boolean\",\n    \"json\",\n    \"integer\",\n    \"float\",\n    \"monetary\",\n    \"properties\",\n    \"properties_definition\",\n    \"reference\",\n    \"many2one_reference\",\n    \"many2one\",\n    \"one2many\",\n    \"many2many\",\n    \"selection\",\n    \"date\",\n    \"datetime\",\n    \"char\",\n    \"text\",\n    \"html\",\n];\n\nconst supportedInfoValidation = {\n    type: Array,\n    element: Object,\n    shape: {\n        label: String,\n        name: String,\n        type: String,\n        availableTypes: { type: Array, element: String, optional: true },\n        default: { type: String, optional: true },\n        help: { type: String, optional: true },\n        choices: /* choices if type == selection */ {\n            type: Array,\n            element: Object,\n            shape: { label: String, value: String },\n            optional: true,\n        },\n        /**\n         * If true, the listed fields come from the relation.\n         * e.g.: the field is a relational one like many2many_tags, so\n         * property 'field' will search on the relation.\n         * */\n        isRelationalField: { type: Boolean, optional: false },\n    },\n    optional: true,\n};\n\nfieldRegistry.addValidation({\n    component: { validate: (c) => c.prototype instanceof Component },\n    displayName: { type: String, optional: true },\n    supportedAttributes: supportedInfoValidation,\n    supportedOptions: supportedInfoValidation,\n    supportedTypes: {\n        type: Array,\n        element: String,\n        optional: true,\n        validate: (array) => array.every((x) => validFieldTypes.includes(x)),\n    },\n    extractProps: { type: Function, optional: true },\n    isEmpty: { type: Function, optional: true },\n    isValid: { type: Function, optional: true }, // Override the validation for the validation visual feedbacks\n    additionalClasses: { type: Array, element: String, optional: true },\n    fieldDependencies: {\n        type: [Function, { type: Array, element: Object, shape: { name: String, type: String } }],\n        optional: true,\n    },\n    relatedFields: {\n        type: [\n            Function,\n            {\n                type: Array,\n                element: Object,\n                shape: {\n                    name: String,\n                    type: String,\n                    readonly: { type: Boolean, optional: true },\n                    selection: { type: Array, element: { type: Array, element: String } },\n                    optional: true,\n                },\n            },\n        ],\n        optional: true,\n    },\n    useSubView: { type: Boolean, optional: true },\n    label: { type: [String, { value: false }], optional: true },\n    listViewWidth: {\n        type: [\n            Number,\n            {\n                type: Array,\n                element: Number,\n                validate: (array) => array.length === 1 || array.length === 2,\n            },\n            Function,\n        ],\n        optional: true,\n    },\n});\n\nclass DefaultField extends Component {\n    static template = xml``;\n    static props = [\"*\"];\n}\n\nexport function getFieldFromRegistry(fieldType, widget, viewType, jsClass) {\n    const prefixes = jsClass ? [jsClass, viewType, \"\"] : [viewType, \"\"];\n    const findInRegistry = (key) => {\n        for (const prefix of prefixes) {\n            const _key = prefix ? `${prefix}.${key}` : key;\n            if (fieldRegistry.contains(_key)) {\n                return fieldRegistry.get(_key);\n            }\n        }\n    };\n    if (widget) {\n        const field = findInRegistry(widget);\n        if (field) {\n            if (field.supportedTypes && !field.supportedTypes?.includes(fieldType)) {\n                console.warn(`The widget: ${widget} don't support the type ${fieldType}`);\n            }\n            return field;\n        }\n        console.warn(`Missing widget: ${widget} for field of type ${fieldType}`);\n    }\n    return findInRegistry(fieldType) || { component: DefaultField };\n}\n\nexport function fieldVisualFeedback(field, record, fieldName, fieldInfo) {\n    const readonly = evaluateBooleanExpr(fieldInfo.readonly, record.evalContextWithVirtualIds);\n    const required = evaluateBooleanExpr(fieldInfo.required, record.evalContextWithVirtualIds);\n    const inEdit = record.isInEdition;\n\n    let empty = !record.isNew;\n    if (\"isEmpty\" in field) {\n        empty = empty && field.isEmpty(record, fieldName);\n    } else {\n        empty = empty && !record.data[fieldName];\n    }\n    empty = inEdit ? empty && readonly : empty;\n    return {\n        readonly,\n        required,\n        invalid: field.isValid\n            ? !field.isValid(record, fieldName, fieldInfo)\n            : record.isFieldInvalid(fieldName),\n        empty,\n    };\n}\n\nexport function getPropertyFieldInfo(propertyField) {\n    const { name, relatedPropertyField, string, type, widget } = propertyField;\n\n    const fieldInfo = {\n        name,\n        string,\n        type,\n        widget: widget || type,\n        options: {},\n        column_invisible: \"False\",\n        invisible: \"False\",\n        readonly: \"False\",\n        required: \"False\",\n        attrs: {},\n        relatedPropertyField,\n\n        // ??? We don t use it ? But it s in the fieldInfo of the field\n        context: \"{}\",\n        help: undefined,\n        onChange: false,\n        forceSave: false,\n        decorations: {},\n        // ???\n    };\n\n    if (type === \"many2one\" || type === \"many2many\") {\n        const { domain, relation } = propertyField;\n        fieldInfo.relation = relation;\n        fieldInfo.domain = domain;\n\n        if (relation === \"res.users\" || relation === \"res.partner\") {\n            fieldInfo.widget =\n                propertyField.type === \"many2one\" ? \"many2one_avatar\" : \"many2many_tags_avatar\";\n        } else {\n            fieldInfo.widget = propertyField.type === \"many2one\" ? type : \"many2many_tags\";\n        }\n    } else if (type === \"tags\") {\n        fieldInfo.tags = propertyField.tags;\n        fieldInfo.widget = `property_tags`;\n    } else if (type === \"selection\") {\n        fieldInfo.selection = propertyField.selection;\n    }\n\n    fieldInfo.field = getFieldFromRegistry(propertyField.type, fieldInfo.widget);\n    let { relatedFields } = fieldInfo.field;\n    if (relatedFields) {\n        if (relatedFields instanceof Function) {\n            relatedFields = relatedFields({ options: {}, attrs: {} });\n        }\n        fieldInfo.relatedFields = Object.fromEntries(relatedFields.map((f) => [f.name, f]));\n    }\n\n    return fieldInfo;\n}\nexport class Field extends Component {\n    static template = \"web.Field\";\n    static props = [\"fieldInfo?\", \"*\"];\n    static parseFieldNode = function (node, models, modelName, viewType, jsClass) {\n        const name = node.getAttribute(\"name\");\n        const widget = node.getAttribute(\"widget\");\n        const fields = models[modelName].fields;\n        if (!fields[name]) {\n            throw new Error(`\"${modelName}\".\"${name}\" field is undefined.`);\n        }\n        const field = getFieldFromRegistry(fields[name].type, widget, viewType, jsClass);\n        const fieldInfo = {\n            name,\n            type: fields[name].type,\n            viewType,\n            widget,\n            field,\n            context: \"{}\",\n            string: fields[name].string,\n            help: undefined,\n            onChange: false,\n            forceSave: false,\n            options: {},\n            decorations: {},\n            attrs: {},\n            domain: undefined,\n        };\n\n        for (const attr of [\"invisible\", \"column_invisible\", \"readonly\", \"required\"]) {\n            fieldInfo[attr] = node.getAttribute(attr);\n            if (fieldInfo[attr] === \"True\") {\n                if (attr === \"column_invisible\") {\n                    fieldInfo.invisible = \"True\";\n                }\n            } else if (fieldInfo[attr] === null && fields[name][attr]) {\n                fieldInfo[attr] = \"True\";\n            }\n        }\n\n        for (const { name, value } of node.attributes) {\n            if ([\"name\", \"widget\"].includes(name)) {\n                // avoid adding name and widget to attrs\n                continue;\n            }\n            if ([\"context\", \"string\", \"help\", \"domain\"].includes(name)) {\n                fieldInfo[name] = value;\n            } else if (name === \"on_change\") {\n                fieldInfo.onChange = exprToBoolean(value);\n            } else if (name === \"options\") {\n                fieldInfo.options = evaluateExpr(value);\n            } else if (name === \"force_save\") {\n                fieldInfo.forceSave = exprToBoolean(value);\n            } else if (name.startsWith(\"decoration-\")) {\n                // prepare field decorations\n                fieldInfo.decorations[name.replace(\"decoration-\", \"\")] = value;\n            } else if (!name.startsWith(\"t-att\")) {\n                // all other (non dynamic) attributes\n                fieldInfo.attrs[name] = value;\n            }\n        }\n        if (name === \"id\") {\n            fieldInfo.readonly = \"True\";\n        }\n\n        if (widget === \"handle\") {\n            fieldInfo.isHandle = true;\n        }\n\n        if (X2M_TYPES.includes(fields[name].type)) {\n            const views = {};\n            let relatedFields = fieldInfo.field.relatedFields;\n            if (relatedFields) {\n                if (relatedFields instanceof Function) {\n                    relatedFields = relatedFields(fieldInfo);\n                }\n                for (const relatedField of relatedFields) {\n                    if (!(\"readonly\" in relatedField)) {\n                        relatedField.readonly = true;\n                    }\n                }\n                relatedFields = Object.fromEntries(relatedFields.map((f) => [f.name, f]));\n                views.default = { fieldNodes: relatedFields, fields: relatedFields };\n                if (!fieldInfo.field.useSubView) {\n                    fieldInfo.viewMode = \"default\";\n                }\n            }\n            for (const child of node.children) {\n                const viewType = child.tagName;\n                const { ArchParser } = viewRegistry.get(viewType);\n                // We copy and hence isolate the subview from the main view's tree\n                // This way, the subview's tree is autonomous and CSS selectors will work normally\n                const childCopy = child.cloneNode(true);\n                const archInfo = new ArchParser().parse(childCopy, models, fields[name].relation);\n                views[viewType] = {\n                    ...archInfo,\n                    limit: archInfo.limit || 40,\n                    fields: models[fields[name].relation].fields,\n                };\n            }\n\n            let viewMode = node.getAttribute(\"mode\");\n            if (viewMode) {\n                if (viewMode.split(\",\").length !== 1) {\n                    viewMode = isSmall() ? \"kanban\" : \"list\";\n                }\n            } else {\n                if (views.list && !views.kanban) {\n                    viewMode = \"list\";\n                } else if (!views.list && views.kanban) {\n                    viewMode = \"kanban\";\n                } else if (views.list && views.kanban) {\n                    viewMode = isSmall() ? \"kanban\" : \"list\";\n                }\n            }\n            if (viewMode) {\n                fieldInfo.viewMode = viewMode;\n            }\n            if (Object.keys(views).length) {\n                fieldInfo.relatedFields = models[fields[name].relation]?.fields;\n                fieldInfo.views = views;\n            }\n        }\n        if ([\"many2one\", \"many2one_reference\"].includes(fields[name].type)) {\n            let relatedFields = fieldInfo.field.relatedFields;\n            if (relatedFields) {\n                relatedFields = Object.fromEntries(relatedFields.map((f) => [f.name, f]));\n                fieldInfo.viewMode = \"default\";\n                fieldInfo.views = {\n                    default: { fieldNodes: relatedFields, fields: relatedFields },\n                };\n            }\n        }\n\n        return fieldInfo;\n    };\n\n    setup() {\n        if (this.props.fieldInfo) {\n            this.field = this.props.fieldInfo.field;\n        } else {\n            const fieldType = this.props.record.fields[this.props.name].type;\n            this.field = getFieldFromRegistry(fieldType, this.props.type);\n        }\n    }\n\n    get classNames() {\n        const { class: _class, fieldInfo, name, record } = this.props;\n        const { readonly, required, invalid, empty } = fieldVisualFeedback(\n            this.field,\n            record,\n            name,\n            fieldInfo || {}\n        );\n        const classNames = {\n            o_field_widget: true,\n            o_readonly_modifier: readonly,\n            o_required_modifier: required,\n            o_field_invalid: invalid,\n            o_field_empty: empty,\n            [`o_field_${this.type}`]: true,\n            [_class]: Boolean(_class),\n        };\n        if (this.field.additionalClasses) {\n            for (const cls of this.field.additionalClasses) {\n                classNames[cls] = true;\n            }\n        }\n\n        // generate field decorations classNames (only if field-specific decorations\n        // have been defined in an attribute, e.g. decoration-danger=\"other_field = 5\")\n        // only handle the text-decoration.\n        if (fieldInfo && fieldInfo.decorations) {\n            const { decorations } = fieldInfo;\n            for (const decoName in decorations) {\n                const value = evaluateBooleanExpr(\n                    decorations[decoName],\n                    record.evalContextWithVirtualIds\n                );\n                classNames[getClassNameFromDecoration(decoName)] = value;\n            }\n        }\n\n        return classNames;\n    }\n\n    get type() {\n        return this.props.type || this.props.record.fields[this.props.name].type;\n    }\n\n    get fieldComponentProps() {\n        const record = this.props.record;\n        let readonly = this.props.readonly || false;\n\n        let propsFromNode = {};\n        if (this.props.fieldInfo) {\n            let fieldInfo = this.props.fieldInfo;\n            readonly =\n                readonly ||\n                evaluateBooleanExpr(fieldInfo.readonly, record.evalContextWithVirtualIds);\n\n            if (this.field.extractProps) {\n                if (this.props.attrs) {\n                    fieldInfo = {\n                        ...fieldInfo,\n                        attrs: { ...fieldInfo.attrs, ...this.props.attrs },\n                    };\n                }\n                if (fieldInfo.attrs.placeholder || fieldInfo.options.placeholder_field) {\n                    fieldInfo.placeholder =\n                        record.data[fieldInfo.options.placeholder_field] ||\n                        fieldInfo.attrs.placeholder;\n                }\n\n                const dynamicInfo = {\n                    get context() {\n                        return getFieldContext(record, fieldInfo.name, fieldInfo.context);\n                    },\n                    domain() {\n                        const evalContext = record.evalContext;\n                        if (fieldInfo.domain) {\n                            return new Domain(evaluateExpr(fieldInfo.domain, evalContext)).toList();\n                        }\n                    },\n                    required: evaluateBooleanExpr(\n                        fieldInfo.required,\n                        record.evalContextWithVirtualIds\n                    ),\n                    readonly: readonly,\n                };\n                propsFromNode = this.field.extractProps(fieldInfo, dynamicInfo);\n            }\n        }\n\n        const props = { ...this.props };\n        delete props.style;\n        delete props.class;\n        delete props.showTooltip;\n        delete props.fieldInfo;\n        delete props.attrs;\n        delete props.type;\n        delete props.readonly;\n\n        return {\n            readonly: readonly || !record.isInEdition || false,\n            ...propsFromNode,\n            ...props,\n        };\n    }\n\n    get tooltip() {\n        if (this.props.showTooltip) {\n            const tooltip = getTooltipInfo({\n                field: this.props.record.fields[this.props.name],\n                fieldInfo: this.props.fieldInfo || {},\n            });\n            if (Boolean(odoo.debug) || (tooltip && JSON.parse(tooltip).field.help)) {\n                return tooltip;\n            }\n        }\n        return false;\n    }\n}\n", "import { Component } from \"@odoo/owl\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { ModelFieldSelector } from \"@web/core/model_field_selector/model_field_selector\";\nimport { registry } from \"@web/core/registry\";\nimport { exprToBoolean } from \"@web/core/utils/strings\";\nimport { standardFieldProps } from \"../standard_field_props\";\nimport { formatChar } from \"../formatters\";\n\nexport class FieldSelectorField extends Component {\n    static template = \"web.FieldSelectorField\";\n    static components = { ModelFieldSelector };\n    static props = {\n        ...standardFieldProps,\n        resModel: { type: String, optional: true },\n        onlySearchable: { type: Boolean, optional: true },\n        allowProperties: { type: Boolean, optional: true },\n        followRelations: { type: Boolean, optional: true },\n    };\n\n    filter(fieldDef) {\n        if (fieldDef.type === \"separator\") {\n            // Don't show properties separator\n            return false;\n        }\n        if (!this.props.allowProperties && fieldDef.type === \"properties\") {\n            return false;\n        }\n        return !this.props.onlySearchable || fieldDef.searchable;\n    }\n\n    async update(value) {\n        await this.props.record.update({ [this.props.name]: value });\n    }\n\n    //---- Getters ----\n    get formattedValue() {\n        return formatChar(this.props.record.data[this.props.name]);\n    }\n\n    get resModel() {\n        return this.props.record.data[this.props.resModel] || this.props.record.resModel;\n    }\n\n    get selectorProps() {\n        return {\n            allowEmpty: !this.props.required,\n            path: this.props.record.data[this.props.name],\n            resModel: this.resModel,\n            readonly: this.props.readonly,\n            update: this.update.bind(this),\n            isDebugMode: !!this.env.debug,\n            filter: this.filter.bind(this),\n            followRelations: this.props.followRelations,\n        };\n    }\n}\n\nexport const fieldSelectorField = {\n    component: FieldSelectorField,\n    displayName: _t(\"Field Selector\"),\n    supportedTypes: [\"char\"],\n    supportedOptions: [\n        {\n            label: _t(\"Follow relations\"),\n            name: \"follow_relations\",\n            type: \"boolean\",\n            default: true,\n        },\n        {\n            label: _t(\"Model\"),\n            name: \"model\",\n            type: \"string\",\n        },\n        {\n            label: _t(\"Only searchable\"),\n            name: \"only_searchable\",\n            type: \"string\",\n        },\n    ],\n    extractProps({ options }, dynamicInfo) {\n        return {\n            allowProperties: options.allow_properties ?? true,\n            followRelations: options.follow_relations ?? true,\n            onlySearchable: exprToBoolean(options.only_searchable),\n            resModel: options.model,\n        };\n    },\n};\n\nregistry.category(\"fields\").add(\"field_selector\", fieldSelectorField);\n", "export function getTooltipInfo(params) {\n    let widgetDescription = undefined;\n    if (params.fieldInfo.widget) {\n        widgetDescription = params.fieldInfo.field.displayName;\n    }\n\n    const info = {\n        viewMode: params.viewMode,\n        resModel: params.resModel,\n        debug: Boolean(odoo.debug),\n        field: {\n            name: params.field.name,\n            label: params.field.string,\n            help: params.fieldInfo.help ?? params.field.help,\n            type: params.field.type,\n            widget: params.fieldInfo.widget,\n            widgetDescription,\n            context: params.fieldInfo.context,\n            domain: params.fieldInfo.domain || params.field.domain,\n            invisible: params.fieldInfo.invisible,\n            column_invisible: params.fieldInfo.column_invisible,\n            readonly: params.fieldInfo.readonly,\n            required: params.fieldInfo.required,\n            changeDefault: params.field.change_default,\n            relation: params.field.relation,\n            model_field: params.field.model_field,\n            selection: params.field.selection,\n            default: params.field.default,\n        },\n    };\n    return JSON.stringify(info);\n}\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { getDataURLFromFile } from \"@web/core/utils/urls\";\nimport { checkFileSize } from \"@web/core/utils/files\";\n\nimport { Component, useRef, useState } from \"@odoo/owl\";\n\nexport class FileUploader extends Component {\n    static template = \"web.FileUploader\";\n    static props = {\n        onClick: { type: Function, optional: true },\n        onUploaded: Function,\n        onUploadComplete: { type: Function, optional: true },\n        multiUpload: { type: Boolean, optional: true },\n        checkSize: { type: Boolean, optional: true },\n        inputName: { type: String, optional: true },\n        fileUploadClass: { type: String, optional: true },\n        acceptedFileExtensions: { type: String, optional: true },\n        slots: { type: Object, optional: true },\n        showUploadingText: { type: Boolean, optional: true },\n        // See https://www.iana.org/assignments/media-types/media-types.xhtml\n        allowedMIMETypes: { type: String, optional: true },\n    };\n    static defaultProps = {\n        checkSize: true,\n        showUploadingText: true,\n    };\n\n    setup() {\n        this.notification = useService(\"notification\");\n        this.fileInputRef = useRef(\"fileInput\");\n        this.state = useState({\n            isUploading: false,\n        });\n    }\n\n    /**\n     * @param {Event} ev\n     */\n    async onFileChange(ev) {\n        const files = [...ev.target.files].filter(file => this.validFileType(file));\n        if (!files. length) {\n            return;\n        }\n        const { target } = ev;\n        for (const file of files) {\n            if (this.props.checkSize && !checkFileSize(file.size, this.notification)) {\n                return null;\n            }\n            this.state.isUploading = true;\n            const data = await getDataURLFromFile(file);\n            if (!file.size) {\n                console.warn(`Error while uploading file : ${file.name}`);\n                this.notification.add(_t(\"There was a problem while uploading your file.\"), {\n                    type: \"danger\",\n                });\n            }\n            try {\n                await this.props.onUploaded({\n                    name: file.name,\n                    size: file.size,\n                    type: file.type,\n                    data: data.split(\",\")[1],\n                    objectUrl: file.type === \"application/pdf\" ? URL.createObjectURL(file) : null,\n                });\n            } finally {\n                this.state.isUploading = false;\n            }\n        }\n        target.value = null;\n        if (this.props.multiUpload && this.props.onUploadComplete) {\n            this.props.onUploadComplete({});\n        }\n    }\n\n    /**\n     * The `allowedMIMETypes` props can restrict the file types users are guided to select.\n     * However, the `acceptedFileExtensions` attribute doesn't enforce strict validation;\n     * it only suggests file types for browsers.\n     *\n     * @param {File} file\n     * @returns Whether the upload file's type is in the whitelist (`allowedMIMETypes`).\n     */\n     validFileType(file) {\n        if (this.props.allowedMIMETypes && !this.props.allowedMIMETypes.includes(file.type)) {\n            this.notification.add(\n                _t(`Oops! '%(fileName)s' didn\u2019t upload since its format isn\u2019t allowed.`, {\n                    fileName: file.name,\n                }),\n                {\n                    type: \"danger\",\n                }\n            );\n            return false;\n        }\n        return true;\n    }\n\n    async onSelectFileButtonClick(ev) {\n        if (this.props.onClick) {\n            const ok = await this.props.onClick(ev);\n            if (ok !== undefined && !ok) {\n                return;\n            }\n        }\n        this.fileInputRef.el.click();\n    }\n}\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { useInputField } from \"../input_field_hook\";\nimport { useNumpadDecimal } from \"../numpad_decimal_hook\";\nimport { formatFloat } from \"../formatters\";\nimport { parseFloat } from \"../parsers\";\nimport { standardFieldProps } from \"../standard_field_props\";\n\nimport { Component, useState } from \"@odoo/owl\";\n\nexport class FloatField extends Component {\n    static template = \"web.FloatField\";\n    static props = {\n        ...standardFieldProps,\n        formatNumber: { type: Boolean, optional: true },\n        inputType: { type: String, optional: true },\n        step: { type: Number, optional: true },\n        digits: { type: Array, optional: true },\n        humanReadable: { type: Boolean, optional: true },\n        decimals: { type: Number, optional: true },\n        trailingZeros: { type: Boolean, optional: true },\n    };\n    static defaultProps = {\n        formatNumber: true,\n        inputType: \"text\",\n        humanReadable: false,\n        decimals: 0,\n        trailingZeros: true,\n    };\n\n    setup() {\n        this.state = useState({\n            hasFocus: false,\n        });\n        this.inputRef = useInputField({\n            getValue: () => this.formattedValue,\n            refName: \"numpadDecimal\",\n            parse: (v) => this.parse(v),\n        });\n        useNumpadDecimal();\n    }\n\n    onFocusIn() {\n        this.state.hasFocus = true;\n    }\n\n    onFocusOut() {\n        this.state.hasFocus = false;\n    }\n\n    parse(value) {\n        return this.props.inputType === \"number\"\n            ? Number(value)\n            : parseFloat(value, { allowOperation: true });\n    }\n\n    get formattedValue() {\n        if (\n            !this.props.formatNumber ||\n            (this.props.inputType === \"number\" && !this.props.readonly && this.value)\n        ) {\n            return this.value;\n        }\n        const options = {\n            digits: this.props.digits,\n            field: this.props.record.fields[this.props.name],\n            trailingZeros: this.props.trailingZeros,\n        };\n        if (this.props.humanReadable && !this.state.hasFocus) {\n            return formatFloat(this.value, {\n                ...options,\n                humanReadable: true,\n                decimals: this.props.decimals,\n            });\n        } else {\n            return formatFloat(this.value, { ...options, humanReadable: false });\n        }\n    }\n\n    get value() {\n        return this.props.record.data[this.props.name];\n    }\n}\n\nexport const floatField = {\n    component: FloatField,\n    displayName: _t(\"Float\"),\n    supportedOptions: [\n        {\n            label: _t(\"Format number\"),\n            name: \"enable_formatting\",\n            type: \"boolean\",\n            help: _t(\n                \"Format the value according to your language setup - e.g. thousand separators, rounding, etc.\"\n            ),\n            default: true,\n        },\n        {\n            label: _t(\"Digits\"),\n            name: \"digits\",\n            type: \"digits\",\n        },\n        {\n            label: _t(\"Type\"),\n            name: \"type\",\n            type: \"string\",\n        },\n        {\n            label: _t(\"Step\"),\n            name: \"step\",\n            type: \"number\",\n        },\n        {\n            label: _t(\"User-friendly format\"),\n            name: \"human_readable\",\n            type: \"boolean\",\n            help: _t(\"Use a human readable format (e.g.: 500G instead of 500,000,000,000).\"),\n        },\n        {\n            label: _t(\"Hide trailing zeros\"),\n            name: \"hide_trailing_zeros\",\n            type: \"boolean\",\n            help: _t(\"Hide zeros to the right of the last non-zero digit, e.g. 1.20 becomes 1.2\"),\n        },\n        {\n            label: _t(\"Decimals\"),\n            name: \"decimals\",\n            type: \"number\",\n            default: 0,\n            help: _t(\"Use it with the 'User-friendly format' option to customize the formatting.\"),\n        },\n    ],\n    supportedTypes: [\"float\", \"monetary\"],\n    isEmpty: (record, fieldName) => record.data[fieldName] === false,\n    extractProps: ({ attrs, options }) => {\n        // Sadly, digits param was available as an option and an attr.\n        // The option version could be removed with some xml refactoring.\n        let digits;\n        if (attrs.digits) {\n            digits = JSON.parse(attrs.digits);\n        } else if (options.digits) {\n            digits = options.digits;\n        }\n\n        return {\n            formatNumber:\n                options?.enable_formatting !== undefined\n                    ? Boolean(options.enable_formatting)\n                    : true,\n            inputType: options.type,\n            humanReadable: !!options.human_readable,\n            step: options.step,\n            digits,\n            decimals: options.decimals || 0,\n            trailingZeros: !options.hide_trailing_zeros,\n        };\n    },\n};\n\nregistry.category(\"fields\").add(\"float\", floatField);\n", "import { registry } from \"@web/core/registry\";\nimport { floatField, FloatField } from \"../float/float_field\";\nimport { _t } from \"@web/core/l10n/translation\";\n\nexport class FloatFactorField extends FloatField {\n    static props = {\n        ...FloatField.props,\n        factor: { type: Number, optional: true },\n    };\n    static defaultProps = {\n        ...FloatField.defaultProps,\n        factor: 1,\n    };\n\n    parse(value) {\n        return super.parse(value) / this.props.factor;\n    }\n\n    get value() {\n        return this.props.record.data[this.props.name] * this.props.factor;\n    }\n}\n\nexport const floatFactorField = {\n    ...floatField,\n    component: FloatFactorField,\n    supportedOptions: [\n        ...floatField.supportedOptions,\n        {\n            label: _t(\"Factor\"),\n            name: \"factor\",\n            type: \"number\",\n        },\n    ],\n    extractProps({ options }) {\n        const props = floatField.extractProps(...arguments);\n        props.factor = options.factor;\n        return props;\n    },\n};\n\nregistry.category(\"fields\").add(\"float_factor\", floatFactorField);\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { formatFloatTime } from \"../formatters\";\nimport { parseFloatTime } from \"../parsers\";\nimport { useInputField } from \"../input_field_hook\";\nimport { standardFieldProps } from \"../standard_field_props\";\nimport { useNumpadDecimal } from \"../numpad_decimal_hook\";\n\nimport { Component } from \"@odoo/owl\";\n\nexport class FloatTimeField extends Component {\n    static template = \"web.FloatTimeField\";\n    static props = {\n        ...standardFieldProps,\n        inputType: { type: String, optional: true },\n        displaySeconds: { type: Boolean, optional: true },\n    };\n    static defaultProps = {\n        inputType: \"text\",\n    };\n\n    setup() {\n        this.inputFloatTimeRef = useInputField({\n            getValue: () => this.formattedValue,\n            refName: \"numpadDecimal\",\n            parse: (v) => parseFloatTime(v),\n        });\n        useNumpadDecimal();\n    }\n\n    get formattedValue() {\n        return formatFloatTime(this.props.record.data[this.props.name], {\n            displaySeconds: this.props.displaySeconds,\n        });\n    }\n}\n\nexport const floatTimeField = {\n    component: FloatTimeField,\n    displayName: _t(\"Time\"),\n    supportedOptions: [\n        {\n            label: _t(\"Display seconds\"),\n            name: \"display_seconds\",\n            type: \"boolean\",\n        },\n        {\n            label: _t(\"Type\"),\n            name: \"type\",\n            type: \"string\",\n            default: \"text\",\n        },\n    ],\n    supportedTypes: [\"float\"],\n    isEmpty: () => false,\n    extractProps: ({ options }) => ({\n        displaySeconds: options.displaySeconds,\n        inputType: options.type,\n    }),\n};\n\nregistry.category(\"fields\").add(\"float_time\", floatTimeField);\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { formatFloatFactor } from \"../formatters\";\nimport { standardFieldProps } from \"../standard_field_props\";\n\nimport { Component } from \"@odoo/owl\";\n\nexport class FloatToggleField extends Component {\n    static template = \"web.FloatToggleField\";\n    static props = {\n        ...standardFieldProps,\n        digits: { type: Array, optional: true },\n        range: { type: Array, optional: true },\n        factor: { type: Number, optional: true },\n        disableReadOnly: { type: Boolean, optional: true },\n    };\n    static defaultProps = {\n        range: [0.0, 0.5, 1.0],\n        factor: 1,\n        disableReadOnly: false,\n    };\n\n    // TODO perf issue (because of update round trip)\n    // we probably want to have a state and a useEffect or onWillUpateProps\n    onChange() {\n        let currentIndex = this.props.range.indexOf(\n            this.props.record.data[this.props.name] * this.factor\n        );\n        currentIndex++;\n        if (currentIndex > this.props.range.length - 1) {\n            currentIndex = 0;\n        }\n        this.props.record.update({\n            [this.props.name]: this.props.range[currentIndex] / this.factor,\n        });\n    }\n\n    // This property has been created in order to allow overrides in other modules.\n    get factor() {\n        return this.props.factor;\n    }\n\n    get formattedValue() {\n        return formatFloatFactor(this.props.record.data[this.props.name], {\n            digits: this.props.digits,\n            factor: this.factor,\n            field: this.props.record.fields[this.props.name],\n        });\n    }\n}\n\nexport const floatToggleField = {\n    component: FloatToggleField,\n    supportedOptions: [\n        {\n            label: _t(\"Digits\"),\n            name: \"digits\",\n            type: \"digits\",\n        },\n        {\n            label: _t(\"Type\"),\n            name: \"type\",\n            type: \"string\",\n        },\n        {\n            label: _t(\"Range\"),\n            name: \"range\",\n            type: \"string\",\n        },\n        {\n            label: _t(\"Factor\"),\n            name: \"factor\",\n            type: \"number\",\n        },\n        {\n            label: _t(\"Disable readonly\"),\n            name: \"force_button\",\n            type: \"boolean\",\n        },\n    ],\n    supportedTypes: [\"float\"],\n    isEmpty: () => false,\n    extractProps: ({ attrs, options }) => {\n        // Sadly, digits param was available as an option and an attr.\n        // The option version could be removed with some xml refactoring.\n        let digits;\n        if (attrs.digits) {\n            digits = JSON.parse(attrs.digits);\n        } else if (options.digits) {\n            digits = options.digits;\n        }\n\n        return {\n            digits,\n            range: options.range,\n            factor: options.factor,\n            disableReadOnly: options.force_button || false,\n        };\n    },\n};\n\nregistry.category(\"fields\").add(\"float_toggle\", floatToggleField);\n", "import {\n    formatDate as _formatDate,\n    formatDateTime as _formatDateTime,\n    toLocaleDateString,\n    toLocaleDateTimeString,\n} from \"@web/core/l10n/dates\";\nimport { localization as l10n } from \"@web/core/l10n/localization\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { isBinarySize } from \"@web/core/utils/binary\";\nimport {\n    formatFloat as formatFloatNumber,\n    humanNumber,\n    insertThousandsSep,\n} from \"@web/core/utils/numbers\";\nimport { exprToBoolean } from \"@web/core/utils/strings\";\n\nimport { markup } from \"@odoo/owl\";\nimport { formatCurrency } from \"@web/core/currency\";\n\n// -----------------------------------------------------------------------------\n// Helpers\n// -----------------------------------------------------------------------------\n\nfunction humanSize(value) {\n    if (!value) {\n        return \"\";\n    }\n    const suffix = value < 1024 ? \" \" + _t(\"Bytes\") : \"b\";\n    return (\n        humanNumber(value, {\n            decimals: 2,\n        }) + suffix\n    );\n}\n\n// -----------------------------------------------------------------------------\n// Exports\n// -----------------------------------------------------------------------------\n\n/**\n * @param {string} [value] base64 representation of the binary\n * @returns {string}\n */\nexport function formatBinary(value) {\n    if (!isBinarySize(value)) {\n        // Computing approximate size out of base64 encoded string\n        // http://en.wikipedia.org/wiki/Base64#MIME\n        return humanSize(value.length / 1.37);\n    }\n    // already bin_size\n    return value;\n}\n\n/**\n * @param {boolean} value\n * @returns {string}\n */\nexport function formatBoolean(value) {\n    return markup`\n        <div class=\"o-checkbox d-inline-block me-2\">\n            <input id=\"boolean_checkbox\" type=\"checkbox\" class=\"form-check-input\" disabled ${\n                value ? \"checked\" : \"\"\n            }/>\n            <label for=\"boolean_checkbox\" class=\"form-check-label\"/>\n        </div>`;\n}\n\n/**\n * @param {string} value\n * @param {Object} [options] additional options\n * @param {boolean} [options.isPassword=false] if true, returns '********'\n *   instead of the formatted value\n * @returns {string}\n */\nexport function formatChar(value, options) {\n    if (options && options.isPassword) {\n        return \"*\".repeat(value ? value.length : 0);\n    }\n    return value || \"\";\n}\nformatChar.extractOptions = ({ attrs }) => ({\n    isPassword: exprToBoolean(attrs.password),\n});\n\nexport function formatDate(value, options = {}) {\n    if (options.numeric) {\n        return _formatDate(value, options);\n    } else {\n        return toLocaleDateString(value);\n    }\n}\nformatDate.extractOptions = ({ options }) => ({\n    numeric: exprToBoolean(options.numeric ?? false),\n});\n\nexport function formatDateTime(value, options = {}) {\n    if (options.numeric) {\n        if (options.showTime === false) {\n            return _formatDate(value, options);\n        }\n        return _formatDateTime(value, options);\n    } else {\n        return toLocaleDateTimeString(value, options);\n    }\n}\nformatDateTime.extractOptions = ({ attrs, options }) => ({\n    ...formatDate.extractOptions({ attrs, options }),\n    showSeconds: exprToBoolean(options.show_seconds ?? false),\n    showTime: exprToBoolean(options.show_time ?? true),\n    showDate: exprToBoolean(options.show_date ?? true),\n});\n\n/**\n * Returns a string representing a float.  The result takes into account the\n * user settings (to display the correct decimal separator).\n *\n * @param {number | false} value the value that should be formatted\n * @param {Object} [options]\n * @param {number[]} [options.digits] the number of digits that should be used,\n *   instead of the default digits precision in the field.\n * @param {boolean} [options.humanReadable] if true, large numbers are formatted\n *   to a human readable format.\n * @param {string} [options.decimalPoint] decimal separating character\n * @param {string} [options.thousandsSep] thousands separator to insert\n * @param {number[]} [options.grouping] array of relative offsets at which to\n *   insert `thousandsSep`. See `insertThousandsSep` method.\n * @param {number} [options.decimals] used for humanNumber formmatter\n * @param {boolean} [options.trailingZeros=true] if false, the decimal part\n *   won't contain unnecessary trailing zeros.\n * @returns {string}\n */\nexport function formatFloat(value, options = {}) {\n    if (value === false) {\n        return \"\";\n    }\n    if (!options.digits && options.field) {\n        options.digits = options.field.digits;\n    }\n    return formatFloatNumber(value, options);\n}\nformatFloat.extractOptions = ({ attrs, options }) => {\n    // Sadly, digits param was available as an option and an attr.\n    // The option version could be removed with some xml refactoring.\n    let digits;\n    if (attrs.digits) {\n        digits = JSON.parse(attrs.digits);\n    } else if (options.digits) {\n        digits = options.digits;\n    }\n    const humanReadable = !!options.human_readable;\n    const decimals = options.decimals || 0;\n    const trailingZeros = !options.hide_trailing_zeros;\n    return { decimals, digits, humanReadable, trailingZeros };\n};\n\n/**\n * Returns a string representing a float value, from a float converted with a\n * factor.\n *\n * @param {number | false} value\n * @param {Object} [options]\n * @param {number} [options.factor=1.0] conversion factor\n * @returns {string}\n */\nexport function formatFloatFactor(value, options = {}) {\n    if (value === false) {\n        return \"\";\n    }\n    const factor = options.factor || 1;\n    if (!options.digits && options.field) {\n        options.digits = options.field.digits;\n    }\n    return formatFloatNumber(value * factor, options);\n}\nformatFloatFactor.extractOptions = ({ attrs, options }) => ({\n    ...formatFloat.extractOptions({ attrs, options }),\n    factor: options.factor,\n});\n\n/**\n * Returns a string representing a time value, from a float.  The idea is that\n * we sometimes want to display something like 1:45 instead of 1.75, or 0:15\n * instead of 0.25.\n *\n * @param {number | false} value\n * @param {Object} [options]\n * @param {boolean} [options.noLeadingZeroHour] if true, format like 1:30 otherwise, format like 01:30\n * @param {boolean} [options.displaySeconds] if true, format like ?1:30:00 otherwise, format like ?1:30\n * @returns {string}\n */\nexport function formatFloatTime(value, options = {}) {\n    if (value === false) {\n        return \"\";\n    }\n    const isNegative = value < 0;\n    value = Math.abs(value);\n\n    let hour = Math.floor(value);\n    const milliSecLeft = Math.round(value * 3600000) - hour * 3600000;\n    // Although looking quite overkill, the following lines ensures that we do\n    // not have float issues while still considering that 59s is 00:00.\n    let min = milliSecLeft / 60000;\n    if (options.displaySeconds) {\n        min = Math.floor(min);\n    } else {\n        min = Math.round(min);\n    }\n    if (min === 60) {\n        min = 0;\n        hour = hour + 1;\n    }\n    min = String(min).padStart(2, \"0\");\n    if (!options.noLeadingZeroHour) {\n        hour = String(hour).padStart(2, \"0\");\n    }\n    let sec = \"\";\n    if (options.displaySeconds) {\n        sec = \":\" + String(Math.floor((milliSecLeft % 60000) / 1000)).padStart(2, \"0\");\n    }\n    return `${isNegative ? \"-\" : \"\"}${hour}:${min}${sec}`;\n}\nformatFloatTime.extractOptions = ({ options }) => ({\n    displaySeconds: options.displaySeconds,\n});\n\n/**\n * Returns a string representing an integer.  If the value is false, then we\n * return an empty string.\n *\n * @param {number | false | null} value\n * @param {Object} [options]\n * @param {boolean} [options.humanReadable] if true, large numbers are formatted\n *   to a human readable format.\n * @param {boolean} [options.isPassword=false] if returns true, acts like\n * @param {string} [options.thousandsSep] thousands separator to insert\n * @param {number[]} [options.grouping] array of relative offsets at which to\n * @param {number} [options.decimals] used for humanNumber formmatter\n *   insert `thousandsSep`. See `insertThousandsSep` method.\n * @returns {string}\n */\nexport function formatInteger(value, options = {}) {\n    if (value === false || value === null) {\n        return \"\";\n    }\n    if (options.isPassword) {\n        return \"*\".repeat(value.length);\n    }\n    if (options.humanReadable) {\n        return humanNumber(value, options);\n    }\n    const grouping = options.grouping || l10n.grouping;\n    const thousandsSep = \"thousandsSep\" in options ? options.thousandsSep : l10n.thousandsSep;\n    return insertThousandsSep(value.toFixed(0), thousandsSep, grouping);\n}\nformatInteger.extractOptions = ({ attrs, options }) => ({\n    decimals: options.decimals || 0,\n    humanReadable: !!options.human_readable,\n    isPassword: exprToBoolean(attrs.password),\n});\n\n/**\n * Returns a string representing a many2one value. The value is expected to be\n * either `false` or an array in the form [id, display_name] or an object\n * containing at least the key \"display_name\". The returned value will then be\n * the display name of the given value, or an empty string if the value is false.\n *\n * @param {[number, string] | { display_name: string } | false} value\n * @param {Object} [options] additional options\n * @param {boolean} [options.escape=false] if true, escapes the formatted value\n * @returns {string}\n */\nexport function formatMany2one(value, options) {\n    if (!value) {\n        value = \"\";\n    } else if (\"display_name\" in value ? value.display_name : value[1]) {\n        value = \"display_name\" in value ? value.display_name : value[1];\n    } else {\n        value = _t(\"Unnamed\");\n    }\n    if (options && options.escape) {\n        value = encodeURIComponent(value);\n    }\n    return value;\n}\n\n/**\n * Returns a string representing a one2many or many2many value. The value is\n * expected to be either `false` or an array of ids. The returned value will\n * then be the count of ids in the given value in the form \"x record(s)\".\n *\n * @param {number[] | false} value\n * @returns {string}\n */\nexport function formatX2many(value) {\n    const count = value.currentIds.length;\n    if (count === 0) {\n        return _t(\"No records\");\n    } else if (count === 1) {\n        return _t(\"1 record\");\n    } else {\n        return _t(\"%s records\", count);\n    }\n}\n\n/**\n * Returns a string representing a monetary value. The result takes into account\n * the user settings (to display the correct decimal separator, currency, ...).\n *\n * @param {number | false} value the value that should be formatted\n * @param {Object} [options]\n *   additional options to override the values in the python description of the\n *   field.\n * @param {number} [options.currencyId] the id of the 'res.currency' to use\n * @param {string} [options.currencyField] the name of the field whose value is\n *   the currency id (ignored if options.currency_id).\n *   Note: if not given it will default to the field \"currency_field\" value or\n *   on \"currency_id\".\n * @param {Object} [options.data] a mapping of field names to field values,\n *   required with options.currencyField\n * @param {boolean} [options.noSymbol] this currency has not a sympbol\n * @param {boolean} [options.humanReadable] if true, large numbers are formatted\n *   to a human readable format.\n * @param {[number, number]} [options.digits] the number of digits that should\n *   be used, instead of the default digits precision in the field.  The first\n *   number is always ignored (legacy constraint)\n * @returns {string}\n */\nexport function formatMonetary(value, options = {}) {\n    // Monetary fields want to display nothing when the value is unset.\n    // You wouldn't want a value of 0 euro if nothing has been provided.\n    if (value === false) {\n        return \"\";\n    }\n\n    let currencyId = options.currencyId;\n    if (!currencyId && options.data) {\n        const currencyField =\n            options.currencyField ||\n            (options.field && options.field.currency_field) ||\n            \"currency_id\";\n        const dataValue = options.data[currencyField];\n        currencyId = dataValue?.id ?? dataValue;\n    }\n    return formatCurrency(value, currencyId, options);\n}\nformatMonetary.extractOptions = ({ options }) => ({\n    noSymbol: options.no_symbol,\n    currencyField: options.currency_field,\n    trailingZeros: !options.hide_trailing_zeros,\n});\n\n/**\n * Returns a string representing the given value (multiplied by 100)\n * concatenated with '%'.\n *\n * @param {number | false} value\n * @param {Object} [options]\n * @param {boolean} [options.noSymbol] if true, doesn't concatenate with \"%\"\n * @returns {string}\n */\nexport function formatPercentage(value, options = {}) {\n    value = value || 0;\n    options = Object.assign({ trailingZeros: false, thousandsSep: \"\" }, options);\n    if (!options.digits && options.field) {\n        options.digits = options.field.digits;\n    }\n    const formatted = formatFloatNumber(value * 100, options);\n    return `${formatted}${options.noSymbol ? \"\" : \"%\"}`;\n}\nformatPercentage.extractOptions = formatFloat.extractOptions;\n\n/**\n * Returns a string representing the value of the python properties field\n * or a properties definition field (see fields.py@Properties).\n *\n * @param {array|false} value\n * @param {Object} [field]\n *        a description of the field (note: this parameter is ignored)\n */\nfunction formatProperties(value, field) {\n    if (!value || !value.length) {\n        return \"\";\n    }\n    return value.map((property) => property[\"string\"]).join(\", \");\n}\n\n/**\n * Returns a string representing the value of the reference field.\n *\n * @param {Object|false} value Object with keys \"resId\" and \"displayName\"\n * @param {Object} [options={}]\n * @returns {string}\n */\nexport function formatReference(value, options) {\n    return formatMany2one(\n        value ? { id: value.resId, display_name: value.displayName } : false,\n        options\n    );\n}\n\n/**\n * Returns a string representing the value of the many2one_reference field.\n *\n * @param {Object|false} value Object with keys \"resId\" and \"displayName\"\n * @returns {string}\n */\nexport function formatMany2oneReference(value) {\n    return value ? formatMany2one({ id: value.resId, display_name: value.displayName }) : \"\";\n}\n\n/**\n * Returns a string of the value of the selection.\n *\n * @param {Object} [options={}]\n * @param {[string, string][]} [options.selection]\n * @param {Object} [options.field]\n * @returns {string}\n */\nexport function formatSelection(value, options = {}) {\n    const selection = options.selection || (options.field && options.field.selection) || [];\n    const option = selection.find((option) => option[0] === value);\n    return option ? option[1] : \"\";\n}\n\n/**\n * Returns the value or an empty string if it's falsy.\n *\n * @param {string | false} value\n * @returns {string}\n */\nexport function formatText(value) {\n    return value ? value.toString() : \"\";\n}\n\n/**\n * Returns the value.\n * Note that, this function is added to be coherent with the rest of the formatters.\n *\n * @param {html} value\n * @returns {html}\n */\nexport function formatHtml(value) {\n    return value || \"\";\n}\n\nexport function formatJson(value) {\n    return (value && JSON.stringify(value)) || \"\";\n}\n\nregistry\n    .category(\"formatters\")\n    .add(\"binary\", formatBinary)\n    .add(\"boolean\", formatBoolean)\n    .add(\"char\", formatChar)\n    .add(\"date\", formatDate)\n    .add(\"datetime\", formatDateTime)\n    .add(\"float\", formatFloat)\n    .add(\"float_factor\", formatFloatFactor)\n    .add(\"float_time\", formatFloatTime)\n    .add(\"html\", formatHtml)\n    .add(\"integer\", formatInteger)\n    .add(\"json\", formatJson)\n    .add(\"many2one\", formatMany2one)\n    .add(\"many2one_reference\", formatMany2oneReference)\n    .add(\"one2many\", formatX2many)\n    .add(\"many2many\", formatX2many)\n    .add(\"monetary\", formatMonetary)\n    .add(\"percentage\", formatPercentage)\n    .add(\"properties\", formatProperties)\n    .add(\"properties_definition\", formatProperties)\n    .add(\"reference\", formatReference)\n    .add(\"selection\", formatSelection)\n    .add(\"text\", formatText);\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { loadBundle } from \"@web/core/assets\";\nimport { registry } from \"@web/core/registry\";\nimport { formatFloat } from \"@web/views/fields/formatters\";\nimport { standardFieldProps } from \"@web/views/fields/standard_field_props\";\n\nimport { Component, onWillStart, useEffect, useRef } from \"@odoo/owl\";\n\nexport class GaugeField extends Component {\n    static template = \"web.GaugeField\";\n    static props = {\n        ...standardFieldProps,\n        maxValueField: { type: String, optional: true },\n        maxValue: { type: Number, optional: true },\n        title: { type: String, optional: true },\n    };\n    static defaultProps = {\n        maxValue: 100,\n    };\n\n    setup() {\n        this.chart = null;\n        this.canvasRef = useRef(\"canvas\");\n\n        onWillStart(async () => await loadBundle(\"web.chartjs_lib\"));\n\n        useEffect(() => {\n            this.renderChart();\n            return () => {\n                if (this.chart) {\n                    this.chart.destroy();\n                }\n            };\n        });\n    }\n\n    get title() {\n        return this.props.title || this.props.record.fields[this.props.name].string || \"\";\n    }\n\n    get formattedValue() {\n        return formatFloat(this.props.record.data[this.props.name], {\n            humanReadable: true,\n            decimals: 1,\n        });\n    }\n\n    renderChart() {\n        const gaugeValue = this.props.record.data[this.props.name];\n        let maxValue = this.props.maxValueField ? this.props.record.data[this.props.maxValueField] : this.props.maxValue;\n        maxValue = Math.max(gaugeValue, maxValue);\n        let maxLabel = maxValue;\n        if (gaugeValue === 0 && maxValue === 0) {\n            maxValue = 1;\n            maxLabel = 0;\n        }\n        const config = {\n            type: \"doughnut\",\n            data: {\n                datasets: [\n                    {\n                        data: [gaugeValue, maxValue - gaugeValue],\n                        backgroundColor: [\"#1f77b4\", \"#dddddd\"],\n                        label: this.title,\n                    },\n                ],\n            },\n            options: {\n                circumference: 180,\n                rotation: 270,\n                responsive: true,\n                maintainAspectRatio: false,\n                cutout: \"70%\",\n                layout: {\n                    padding: 5,\n                },\n                plugins: {\n                    title: {\n                        display: true,\n                        text: this.title,\n                        padding: 4,\n                    },\n                    tooltip: {\n                        displayColors: false,\n                        callbacks: {\n                            label: function (tooltipItem) {\n                                if (tooltipItem.dataIndex === 0) {\n                                    return _t(\"Value: %(value)s\", { value: gaugeValue });\n                                }\n                                return _t(\"Max: %(max)s\", { max: maxLabel });\n                            },\n                        },\n                    },\n                },\n                aspectRatio: 2,\n            },\n        };\n        this.chart = new Chart(this.canvasRef.el, config);\n    }\n}\n\nexport const gaugeField = {\n    component: GaugeField,\n    supportedOptions: [\n        {\n            label: _t(\"Title\"),\n            name: \"title\",\n            type: \"string\",\n        },\n        {\n            label: _t(\"Max value field\"),\n            name: \"max_value_field\",\n            type: \"field\",\n            availableTypes: [\"integer\", \"float\"],\n        },\n        {\n            label: _t(\"Max value\"),\n            name: \"max_value\",\n            type: \"string\",\n        },\n    ],\n    extractProps: ({ options }) => ({\n        maxValueField: options.max_field,\n        maxValue: options.max_value,\n        title: options.title,\n    }),\n};\n\nregistry.category(\"fields\").add(\"gauge\", gaugeField);\n", "/** @odoo-module **/\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { CharField, charField } from \"@web/views/fields/char/char_field\";\n\nexport function getGoogleSlideUrl(value, page) {\n    let url = false;\n    const googleRegExp = /(^https:\\/\\/docs.google.com).*(\\/d\\/e\\/|\\/d\\/)([A-Za-z0-9-_]+)/;\n    const google = value.match(googleRegExp);\n    if (google && google[3]) {\n        url = `https://docs.google.com/presentation${google[2]}${google[3]}/preview?slide=${page}`;\n    }\n    return url;\n}\n\nexport class GoogleSlideViewer extends CharField {\n    static template = \"web.GoogleSlideViewer\";\n    setup() {\n        super.setup();\n        this.notification = useService(\"notification\");\n        this.page = 1;\n    }\n\n    _get_slide_page() {\n        return this.props.record.data[this.props.name + \"_page\"]\n            ? this.props.record.data[this.props.name + \"_page\"]\n            : this.page;\n    }\n\n    get url() {\n        let url = this.props.value;\n        if (this.props.record.data[this.props.name]) {\n            url = getGoogleSlideUrl(\n                this.props.record.data[this.props.name],\n                this._get_slide_page()\n            );\n        }\n        return url;\n    }\n\n    onLoadFailed() {\n        this.notification.add(_t(\"Could not display the selected spreadsheet\"), { type: \"danger\" });\n    }\n}\n\nexport const googleSlideViewer = {\n    ...charField,\n    component: GoogleSlideViewer,\n    displayName: _t(\"Google Slide Viewer\"),\n};\n\nregistry.category(\"fields\").add(\"google_slide_viewer\", googleSlideViewer);\n", "import { registry } from \"@web/core/registry\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { standardFieldProps } from \"../standard_field_props\";\n\nimport { Component } from \"@odoo/owl\";\n\nexport class HandleField extends Component {\n    static template = \"web.HandleField\";\n    static props = {\n        ...standardFieldProps,\n    };\n}\n\nexport const handleField = {\n    component: HandleField,\n    displayName: _t(\"Handle\"),\n    supportedTypes: [\"integer\"],\n    isEmpty: () => false,\n    listViewWidth: 20,\n    extractProps(_, dynamicInfo) {\n        return {\n            readonly: dynamicInfo.readonly,\n        };\n    },\n};\n\nregistry.category(\"fields\").add(\"handle\", handleField);\n", "import { registry } from \"@web/core/registry\";\nimport { TextField, textField } from \"../text/text_field\";\n\nexport class HtmlField extends TextField {\n    static template = \"web.HtmlField\";\n}\n\nexport const htmlField = {\n    ...textField,\n    component: HtmlField,\n};\n\nregistry.category(\"fields\").add(\"html\", htmlField);\n", "import { registry } from \"@web/core/registry\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { standardFieldProps } from \"../standard_field_props\";\nimport { Component, useEffect, useRef } from \"@odoo/owl\";\n\nexport class IframeWrapperField extends Component {\n    static template = \"web.IframeWrapperField\";\n    static props = {\n        ...standardFieldProps,\n    };\n\n    setup() {\n        this.iframeRef = useRef(\"iframe\");\n\n        useEffect(\n            (value) => {\n                /**\n                 * The document.write is not recommended. It is better to manipulate the DOM through $.appendChild and\n                 * others. In our case though, we deal with an iframe without src attribute and with metadata to put in\n                 * head tag. If we use the usual dom methods, the iframe is automatically created with its document\n                 * component containing html > head & body. Therefore, if we want to make it work that way, we would\n                 * need to receive each piece at a time to  append it to this document (with this.record.data and extra\n                 * model fields or with an rpc). It also cause other difficulties getting attribute on the most parent\n                 * nodes, parsing to HTML complex elements, etc.\n                 * Therefore, document.write makes it much more trivial in our situation.\n                 */\n                const iframeDoc = this.iframeRef.el.contentDocument;\n                iframeDoc.open();\n                iframeDoc.write(value);\n                iframeDoc.close();\n            },\n            () => [this.props.record.data[this.props.name]]\n        );\n    }\n}\n\nexport const iframeWrapperField = {\n    component: IframeWrapperField,\n    displayName: _t(\"Wrap raw html within an iframe\"),\n    // If HTML, don't forget to adjust the sanitize options to avoid stripping most of the metadata\n    supportedTypes: [\"text\", \"html\"],\n};\n\nregistry.category(\"fields\").add(\"iframe_wrapper\", iframeWrapperField);\n", "import { isMobileOS } from \"@web/core/browser/feature_detection\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { imageUrl } from \"@web/core/utils/urls\";\nimport { isBinarySize } from \"@web/core/utils/binary\";\nimport { FileUploader } from \"../file_handler\";\nimport { standardFieldProps } from \"../standard_field_props\";\n\nimport { Component, useState, onWillRender } from \"@odoo/owl\";\nconst { DateTime } = luxon;\n\nexport const fileTypeMagicWordMap = {\n    \"/\": \"jpg\",\n    R: \"gif\",\n    i: \"png\",\n    P: \"svg+xml\",\n    U: \"webp\",\n};\nconst placeholder = \"/web/static/img/placeholder.png\";\n\nexport class ImageField extends Component {\n    static template = \"web.ImageField\";\n    static components = {\n        FileUploader,\n    };\n    static props = {\n        ...standardFieldProps,\n        alt: { type: String, optional: true },\n        enableZoom: { type: Boolean, optional: true },\n        imgClass: { type: String, optional: true },\n        zoomDelay: { type: Number, optional: true },\n        previewImage: { type: String, optional: true },\n        acceptedFileExtensions: { type: String, optional: true },\n        width: { type: Number, optional: true },\n        height: { type: Number, optional: true },\n        reload: { type: Boolean, optional: true },\n        convertToWebp: { type: Boolean, optional: true },\n    };\n    static defaultProps = {\n        acceptedFileExtensions: \"image/*\",\n        alt: _t(\"Binary file\"),\n        imgClass: \"\",\n        reload: true,\n    };\n\n    setup() {\n        this.notification = useService(\"notification\");\n        this.orm = useService(\"orm\");\n        this.isMobile = isMobileOS();\n        this.state = useState({\n            isValid: true,\n        });\n        this.lastURL = undefined;\n\n        if (this.fieldType === \"many2one\" && !this.props.previewImage) {\n            throw new Error(\n                \"ImageField: previewImage must be provided when set on a many2one field\"\n            );\n        }\n        const field = this.props.record.fields[this.props.name];\n        if (field.related?.includes(\".\")) {\n            this.uniqueId = DateTime.now();\n            let key = this.props.record.data[this.props.name];\n            onWillRender(() => {\n                const nextKey = this.props.record.data[this.props.name];\n                if (key !== nextKey) {\n                    this.uniqueId = DateTime.now();\n                }\n\n                key = nextKey;\n            });\n        }\n    }\n\n    get imgAlt() {\n        if (this.fieldType === \"many2one\" && this.props.record.data[this.props.name]) {\n            return this.props.record.data[this.props.name].display_name;\n        }\n        return this.props.alt;\n    }\n\n    get imgClass() {\n        return [\"img\", \"img-fluid\"].concat(this.props.imgClass.split(\" \")).join(\" \");\n    }\n\n    get fieldType() {\n        return this.props.record.fields[this.props.name].type;\n    }\n\n    get rawCacheKey() {\n        return this.uniqueId || this.props.record.data.write_date;\n    }\n\n    get sizeStyle() {\n        let style = \"\";\n        if (this.props.width) {\n            style += `max-width: ${this.props.width}px;`;\n            if (!this.props.height) {\n                style += `height: auto; max-height: 100%;`;\n            }\n        }\n        if (this.props.height) {\n            style += `max-height: ${this.props.height}px;`;\n            if (!this.props.width) {\n                style += `width: auto; max-width: 100%;`;\n            }\n        }\n        return style;\n    }\n    get hasTooltip() {\n        return this.props.enableZoom && this.props.record.data[this.props.name];\n    }\n    get tooltipAttributes() {\n        const fieldName = this.fieldType === \"many2one\" ? this.props.previewImage : this.props.name;\n        return {\n            template: \"web.ImageZoomTooltip\",\n            info: JSON.stringify({ url: this.getUrl(fieldName) }),\n        };\n    }\n\n    getUrl(imageFieldName) {\n        if (!this.props.reload && this.lastURL) {\n            return this.lastURL;\n        }\n        if (!this.props.record.data[this.props.name] || !this.state.isValid) {\n            return placeholder;\n        }\n        if (this.fieldType === \"many2one\") {\n            this.lastURL = imageUrl(\n                this.props.record.fields[this.props.name].relation,\n                this.props.record.data[this.props.name].id,\n                imageFieldName,\n                { unique: this.rawCacheKey }\n            );\n        } else if (isBinarySize(this.props.record.data[this.props.name])) {\n            this.lastURL = imageUrl(\n                this.props.record.resModel,\n                this.props.record.resId,\n                imageFieldName,\n                { unique: this.rawCacheKey }\n            );\n        } else {\n            // Use magic-word technique for detecting image type\n            const magic = fileTypeMagicWordMap[this.props.record.data[this.props.name][0]] || \"png\";\n            this.lastURL = `data:image/${magic};base64,${this.props.record.data[this.props.name]}`;\n        }\n        return this.lastURL;\n    }\n    onFileRemove() {\n        this.state.isValid = true;\n        this.props.record.update({ [this.props.name]: false });\n    }\n    async onFileUploaded(info) {\n        this.state.isValid = true;\n        if (\n            this.props.convertToWebp &&\n            ![\"image/gif\", \"image/svg+xml\", \"image/webp\"].includes(info.type)\n        ) {\n            const image = document.createElement(\"img\");\n            image.src = `data:${info.type};base64,${info.data}`;\n            await new Promise((resolve) => image.addEventListener(\"load\", resolve));\n\n            const canvas = document.createElement(\"canvas\");\n            canvas.width = image.width;\n            canvas.height = image.height;\n            const ctx = canvas.getContext(\"2d\");\n            ctx.drawImage(image, 0, 0);\n\n            info.data = canvas.toDataURL(\"image/webp\").split(\",\")[1];\n            info.type = \"image/webp\";\n            info.name = info.name.replace(/\\.[^/.]+$/, \".webp\");\n        }\n        if (info.type === \"image/webp\") {\n            // Generate alternate sizes and format for reports.\n            const image = document.createElement(\"img\");\n            image.src = `data:image/webp;base64,${info.data}`;\n            await new Promise((resolve) => image.addEventListener(\"load\", resolve));\n            const originalSize = Math.max(image.width, image.height);\n            const smallerSizes = [1920, 1024, 512, 256, 128].filter((size) => size < originalSize);\n            let referenceId = undefined;\n            for (const size of [originalSize, ...smallerSizes]) {\n                const ratio = size / originalSize;\n                const canvas = document.createElement(\"canvas\");\n                canvas.width = image.width * ratio;\n                canvas.height = image.height * ratio;\n                const ctx = canvas.getContext(\"2d\");\n                ctx.fillStyle = \"transparent\";\n                ctx.fillRect(0, 0, canvas.width, canvas.height);\n                ctx.imageSmoothingEnabled = true;\n                ctx.imageSmoothingQuality = \"high\";\n                ctx.drawImage(\n                    image,\n                    0,\n                    0,\n                    image.width,\n                    image.height,\n                    0,\n                    0,\n                    canvas.width,\n                    canvas.height\n                );\n                const [resizedId] = await this.orm.call(\"ir.attachment\", \"create_unique\", [\n                    [\n                        {\n                            name: info.name,\n                            description: size === originalSize ? \"\" : `resize: ${size}`,\n                            datas:\n                                size === originalSize\n                                    ? info.data\n                                    : canvas.toDataURL(\"image/webp\").split(\",\")[1],\n                            res_id: referenceId,\n                            res_model: \"ir.attachment\",\n                            mimetype: \"image/webp\",\n                        },\n                    ],\n                ]);\n                referenceId = referenceId || resizedId; // Keep track of original.\n                // Converted to JPEG for use in PDF files, alpha values will default to white\n                await this.orm.call(\"ir.attachment\", \"create_unique\", [\n                    [\n                        {\n                            name: info.name.replace(/\\.webp$/, \".jpg\"),\n                            description: \"format: jpeg\",\n                            datas: canvas.toDataURL(\"image/jpeg\").split(\",\")[1],\n                            res_id: resizedId,\n                            res_model: \"ir.attachment\",\n                            mimetype: \"image/jpeg\",\n                        },\n                    ],\n                ]);\n            }\n        }\n        this.props.record.update({ [this.props.name]: info.data });\n    }\n    onLoadFailed() {\n        this.state.isValid = false;\n    }\n}\n\nexport const imageField = {\n    component: ImageField,\n    displayName: _t(\"Image\"),\n    supportedAttributes: [\n        {\n            label: _t(\"Alternative text\"),\n            name: \"alt\",\n            type: \"string\",\n        },\n    ],\n    supportedOptions: [\n        {\n            label: _t(\"Reload\"),\n            name: \"reload\",\n            type: \"boolean\",\n            default: true,\n        },\n        {\n            label: _t(\"Enable zoom\"),\n            name: \"zoom\",\n            type: \"boolean\",\n        },\n        {\n            label: _t(\"Convert to webp\"),\n            name: \"convert_to_webp\",\n            type: \"boolean\",\n        },\n        {\n            label: _t(\"Zoom delay\"),\n            name: \"zoom_delay\",\n            type: \"number\",\n            help: _t(\"Delay the apparition of the zoomed image with a value in milliseconds\"),\n        },\n        {\n            label: _t(\"Accepted file extensions\"),\n            name: \"accepted_file_extensions\",\n            type: \"string\",\n        },\n        {\n            label: _t(\"Size\"),\n            name: \"size\",\n            type: \"selection\",\n            choices: [\n                { label: _t(\"Small\"), value: \"[0,90]\" },\n                { label: _t(\"Medium\"), value: \"[0,180]\" },\n                { label: _t(\"Large\"), value: \"[0,270]\" },\n            ],\n        },\n        {\n            label: _t(\"Preview image\"),\n            name: \"preview_image\",\n            type: \"field\",\n            availableTypes: [\"binary\"],\n        },\n    ],\n    supportedTypes: [\"binary\", \"many2one\"],\n    fieldDependencies: [{ name: \"write_date\", type: \"datetime\" }],\n    isEmpty: () => false,\n    extractProps: ({ attrs, options }) => ({\n        alt: attrs.alt,\n        enableZoom: options.zoom,\n        convertToWebp: options.convert_to_webp,\n        imgClass: options.img_class,\n        zoomDelay: options.zoom_delay,\n        previewImage: options.preview_image,\n        acceptedFileExtensions: options.accepted_file_extensions,\n        width: options.size && Boolean(options.size[0]) ? options.size[0] : undefined,\n        height: options.size && Boolean(options.size[1]) ? options.size[1] : undefined,\n        reload: \"reload\" in options ? Boolean(options.reload) : true,\n    }),\n};\n\nregistry.category(\"fields\").add(\"image\", imageField);\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { standardFieldProps } from \"../standard_field_props\";\n\nimport { Component, useState } from \"@odoo/owl\";\nimport { useRecordObserver } from \"@web/model/relational_model/utils\";\n\nexport class ImageUrlField extends Component {\n    static template = \"web.ImageUrlField\";\n    static props = {\n        ...standardFieldProps,\n        width: { type: Number, optional: true },\n        height: { type: Number, optional: true },\n    };\n\n    static fallbackSrc = \"/web/static/img/placeholder.png\";\n\n    setup() {\n        this.notification = useService(\"notification\");\n        this.state = useState({\n            src: this.props.record.data[this.props.name],\n        });\n\n        useRecordObserver((record) => {\n            this.state.src = record.data[this.props.name];\n        });\n    }\n\n    get sizeStyle() {\n        let style = \"\";\n        if (this.props.width) {\n            style += `max-width: ${this.props.width}px;`;\n        }\n        if (this.props.height) {\n            style += `max-height: ${this.props.height}px;`;\n        }\n        return style;\n    }\n\n    onLoadFailed() {\n        this.state.src = this.constructor.fallbackSrc;\n    }\n}\n\nexport const imageUrlField = {\n    component: ImageUrlField,\n    displayName: _t(\"Image\"),\n    supportedOptions: [\n        {\n            label: _t(\"Size\"),\n            name: \"size\",\n            type: \"selection\",\n            choices: [\n                { label: _t(\"Small\"), value: \"[0,90]\" },\n                { label: _t(\"Medium\"), value: \"[0,180]\" },\n                { label: _t(\"Large\"), value: \"[0,270]\" },\n            ],\n        },\n    ],\n    supportedTypes: [\"char\"],\n    extractProps: ({ attrs, options }) => ({\n        width: options.size ? options.size[0] : attrs.width,\n        height: options.size ? options.size[1] : attrs.height,\n    }),\n};\n\nregistry.category(\"fields\").add(\"image_url\", imageUrlField);\n", "import { getActiveHotkey } from \"@web/core/hotkeys/hotkey_service\";\nimport { useBus } from \"@web/core/utils/hooks\";\n\nimport { useComponent, useEffect, useRef } from \"@odoo/owl\";\n\n/**\n * This hook is meant to be used by field components that use an input or\n * textarea to edit their value. Its purpose is to prevent that value from being\n * erased by an update of the model (typically coming from an onchange) when the\n * user is currently editing it.\n *\n * @param {Object} params\n * @param {() => string} params.getValue a function that returns the value to write in\n *   the input, if the user isn't currently editing it\n * @param {(value: string) => any} [params.parse] a function that parses the value of the input.\n * @param {Ref<HTMLInputElement | HTMLTextAreaElement>} [params.ref] a ref containing the input/textarea\n * @param {string} [params.refName=\"input\"] the ref name of the input/textarea\n * @param {boolean} [params.preventLineBreaks] Prevent line breaks in input when set\n * @param {string} [params.fieldName]\n * @param {() => boolean} [params.shouldSave] if true, save the record with the new value\n */\nexport function useInputField(params) {\n    const inputRef = params.ref || useRef(params.refName || \"input\");\n    const component = useComponent();\n    const fieldName = params.fieldName || component.props.name;\n    const shouldSave = params.shouldSave ?? (() => false);\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 thefore 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 commited to the model.\n     * Not changed in case of invalid field value.\n     */\n    let lastSetValue = null;\n\n    /**\n     * Track the fact that there is a change sent to the model that hasn't been acknowledged yet\n     * (e.g. because the onchange is still pending). This is necessary if we must do an urgent save,\n     * as we have to re-send that change for the write that will be done directly.\n     * FIXME: this could/should be handled by the model itself, when it will be rewritten\n     */\n    let pendingUpdate = false;\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.preventLineBreaks && ev.inputType === \"insertFromPaste\") {\n            ev.target.value = ev.target.value.replace(/[\\r\\n]+/g, \" \");\n        }\n        component.props.record.model.bus.trigger(\"FIELD_IS_DIRTY\", isDirty);\n        if (!component.props.record.isValid) {\n            component.props.record.resetFieldValidity(fieldName);\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    async 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                    component.props.record.setInvalidField(fieldName);\n                    isInvalid = true;\n                }\n            }\n\n            if (!isInvalid) {\n                if (val !== component.props.record.data[fieldName]) {\n                    lastSetValue = inputRef.el.value;\n                    pendingUpdate = true;\n                    await component.props.record.update({ [fieldName]: val }, { save: shouldSave() });\n                    pendingUpdate = false;\n                    component.props.record.model.bus.trigger(\"FIELD_IS_DIRTY\", isDirty);\n                } else {\n                    inputRef.el.value = params.getValue();\n                }\n            }\n        }\n    }\n    function onKeydown(ev) {\n        const hotkey = getActiveHotkey(ev);\n        const keys = [\"tab\", \"shift+tab\"];\n        if (ev.target.tagName.toLowerCase() !== \"textarea\") {\n            keys.push(\"enter\");\n        }\n        if (keys.includes(hotkey)) {\n            commitChanges(false);\n        }\n        if (params.preventLineBreaks && [\"enter\", \"shift+enter\"].includes(hotkey)) {\n            ev.preventDefault();\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        // We need to call getValue before the condition to always observe\n        // the corresponding value in the record. Otherwise, in some cases,\n        // if the value in the record change the useEffect isn't triggered.\n        const value = params.getValue();\n        if (\n            inputRef.el &&\n            !isDirty &&\n            !component.props.record.isFieldInvalid(fieldName)\n        ) {\n            inputRef.el.value = value;\n            lastSetValue = inputRef.el.value;\n        }\n    });\n\n    const { model } = component.props.record;\n    useBus(model.bus, \"WILL_SAVE_URGENTLY\", () => commitChanges(true));\n    useBus(model.bus, \"NEED_LOCAL_CHANGES\", (ev) => ev.detail.proms.push(commitChanges()));\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 || (urgent && pendingUpdate)) {\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                        component.props.record.setInvalidField(fieldName);\n                    }\n                }\n            }\n\n            if (isInvalid) {\n                return;\n            }\n\n            if ((val || false) !== (component.props.record.data[fieldName] || false)) {\n                lastSetValue = inputRef.el.value;\n                await component.props.record.update({ [fieldName]: val }, { save: shouldSave() });\n                component.props.record.model.bus.trigger(\"FIELD_IS_DIRTY\", false);\n            } else {\n                inputRef.el.value = params.getValue();\n            }\n        }\n    }\n\n    return inputRef;\n}\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { formatInteger } from \"../formatters\";\nimport { parseInteger } from \"../parsers\";\nimport { useInputField } from \"../input_field_hook\";\nimport { standardFieldProps } from \"../standard_field_props\";\nimport { useNumpadDecimal } from \"../numpad_decimal_hook\";\n\nimport { Component, useState } from \"@odoo/owl\";\n\nexport class IntegerField extends Component {\n    static template = \"web.IntegerField\";\n    static props = {\n        ...standardFieldProps,\n        formatNumber: { type: Boolean, optional: true },\n        humanReadable: { type: Boolean, optional: true },\n        decimals: { type: Number, optional: true },\n        inputType: { type: String, optional: true },\n        min: { type: Number, optional: true },\n        max: { type: Number, optional: true },\n        step: { type: Number, optional: true },\n    };\n    static defaultProps = {\n        formatNumber: true,\n        humanReadable: false,\n        inputType: \"text\",\n        decimals: 0,\n    };\n\n    setup() {\n        this.state = useState({\n            hasFocus: false,\n        });\n        useInputField({\n            getValue: () => this.formattedValue,\n            refName: \"numpadDecimal\",\n            parse: (v) => parseInteger(v, { allowOperation: true }),\n        });\n        useNumpadDecimal();\n    }\n\n    onFocusIn() {\n        this.state.hasFocus = true;\n    }\n\n    onFocusOut() {\n        this.state.hasFocus = false;\n    }\n\n    get formattedValue() {\n        if (\n            !this.props.formatNumber ||\n            (!this.props.readonly && this.props.inputType === \"number\")\n        ) {\n            if (this.value === false) {\n                return \"\";\n            }\n            return this.value;\n        }\n        if (this.props.humanReadable && !this.state.hasFocus) {\n            return formatInteger(this.value, {\n                humanReadable: true,\n                decimals: this.props.decimals,\n            });\n        } else {\n            return formatInteger(this.value, { humanReadable: false });\n        }\n    }\n\n    get value() {\n        return this.props.record.data[this.props.name];\n    }\n}\n\nexport const integerField = {\n    component: IntegerField,\n    displayName: _t(\"Integer\"),\n    supportedOptions: [\n        {\n            label: _t(\"Format number\"),\n            name: \"enable_formatting\",\n            type: \"boolean\",\n            help: _t(\n                \"Format the value\u00a0according to your language setup - e.g. thousand separators, rounding, etc.\"\n            ),\n            default: true,\n        },\n        {\n            label: _t(\"Type\"),\n            name: \"type\",\n            type: \"string\",\n        },\n        {\n            label: _t(\"Step\"),\n            name: \"step\",\n            type: \"number\",\n        },\n        {\n            label: _t(\"User-friendly format\"),\n            name: \"human_readable\",\n            type: \"boolean\",\n            help: _t(\"Use a human readable format (e.g.: 500G instead of 500,000,000,000).\"),\n        },\n        {\n            label: _t(\"Decimals\"),\n            name: \"decimals\",\n            type: \"number\",\n            default: 0,\n            help: _t(\"Use it with the 'User-friendly format' option to customize the formatting.\"),\n        },\n    ],\n    supportedTypes: [\"integer\"],\n    isEmpty: (record, fieldName) => record.data[fieldName] === false,\n    extractProps: ({ options }) => ({\n        formatNumber:\n            options?.enable_formatting !== undefined ? Boolean(options.enable_formatting) : true,\n        humanReadable: !!options.human_readable,\n        inputType: options.type,\n        min: options.min,\n        max: options.max,\n        step: options.step,\n        decimals: options.decimals || 0,\n    }),\n};\n\nregistry.category(\"fields\").add(\"integer\", integerField);\n", "/** @odoo-module **/\nimport { registry } from \"@web/core/registry\";\nimport { aceField, AceField } from \"@web/views/fields/ace/ace_field\";\nimport { IrUiViewCodeEditor } from \"@web/core/ir_ui_view_code_editor/code_editor\";\n\nexport class IrUiViewAceField extends AceField {\n    static template = \"web.IrUIViewAceField\";\n    static components = { IrUiViewCodeEditor };\n}\n\nexport const irUiViewAceField = {\n    ...aceField,\n    component: IrUiViewAceField,\n    additionalClasses: [\"o_field_ace\"],\n};\n\nregistry.category(\"fields\").add(\"code_ir_ui_view\", irUiViewAceField);\n", "import { loadBundle } from \"@web/core/assets\";\nimport { registry } from \"@web/core/registry\";\nimport { getColor, hexToRGBA, getCustomColor } from \"@web/core/colors/colors\";\nimport { standardFieldProps } from \"../standard_field_props\";\n\nimport { Component, onWillStart, useEffect, useRef } from \"@odoo/owl\";\nimport { cookie } from \"@web/core/browser/cookie\";\n\nconst colorScheme = cookie.get(\"color_scheme\");\nconst GRAPH_GRID_COLOR = getCustomColor(colorScheme, \"#d8dadd\", \"#3C3E4B\");\nconst GRAPH_LABEL_COLOR = getCustomColor(colorScheme, \"#111827\", \"#E4E4E4\");\nexport class JournalDashboardGraphField extends Component {\n    static template = \"web.JournalDashboardGraphField\";\n    static props = {\n        ...standardFieldProps,\n        graphType: String,\n    };\n\n    setup() {\n        this.chart = null;\n        this.canvasRef = useRef(\"canvas\");\n        this.data = JSON.parse(this.props.record.data[this.props.name]);\n\n        onWillStart(async () => await loadBundle(\"web.chartjs_lib\"));\n\n        useEffect(() => {\n            this.renderChart();\n            return () => {\n                if (this.chart) {\n                    this.chart.destroy();\n                }\n            };\n        });\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        let config;\n        if (this.props.graphType === \"line\") {\n            config = this.getLineChartConfig();\n        } else if (this.props.graphType === \"bar\") {\n            config = this.getBarChartConfig();\n        }\n        this.chart = new Chart(this.canvasRef.el, config);\n    }\n    getLineChartConfig() {\n        const labels = this.data[0].values.map(function (pt) {\n            return pt.x;\n        });\n        const color10 = getColor(3, cookie.get(\"color_scheme\"), \"odoo\");\n        const borderColor = this.data[0].is_sample_data ? hexToRGBA(color10, 0.1) : color10;\n        const backgroundColor = this.data[0].is_sample_data\n            ? hexToRGBA(color10, 0.05)\n            : hexToRGBA(color10, 0.2);\n        return {\n            type: \"line\",\n            data: {\n                labels,\n                datasets: [\n                    {\n                        backgroundColor,\n                        borderColor,\n                        data: this.data[0].values,\n                        fill: \"start\",\n                        label: this.data[0].key,\n                        borderWidth: 2,\n                    },\n                ],\n            },\n            options: {\n                plugins: {\n                    legend: { display: false },\n                    tooltip: {\n                        enabled: !this.data[0].is_sample_data,\n                        intersect: false,\n                        position: \"nearest\",\n                        caretSize: 0,\n                    },\n                },\n                scales: {\n                    y: {\n                        display: false,\n                    },\n                    x: {\n                        display: false,\n                    },\n                },\n                maintainAspectRatio: false,\n                elements: {\n                    line: {\n                        tension: 0.000001,\n                    },\n                },\n            },\n        };\n    }\n\n    getBarChartConfig() {\n        const data = [];\n        const labels = [];\n        const backgroundColor = [];\n\n        const color13 = getColor(2, cookie.get(\"color_scheme\"), \"odoo\");\n        const color19 = getColor(1, cookie.get(\"color_scheme\"), \"odoo\");\n        this.data[0].values.forEach((pt) => {\n            data.push(pt.value);\n            labels.push(pt.label);\n            if (pt.type === \"past\") {\n                backgroundColor.push(color13);\n            } else if (pt.type === \"future\") {\n                backgroundColor.push(color19);\n            } else {\n                backgroundColor.push(getCustomColor(colorScheme, \"#ebebeb\", \"#3C3E4B\"));\n            }\n        });\n        return {\n            type: \"bar\",\n            data: {\n                labels,\n                datasets: [\n                    {\n                        backgroundColor,\n                        data,\n                        fill: \"start\",\n                        label: this.data[0].key,\n                    },\n                ],\n            },\n            options: {\n                plugins: {\n                    legend: { display: false },\n                    tooltip: {\n                        enabled: !this.data[0].is_sample_data,\n                        intersect: false,\n                        position: \"nearest\",\n                        caretSize: 0,\n                    },\n                },\n                scales: {\n                    y: {\n                        display: false,\n                    },\n                    x: {\n                        grid: {\n                            color: GRAPH_GRID_COLOR,\n                        },\n                        ticks: {\n                            color: GRAPH_LABEL_COLOR,\n                        },\n                        border: {\n                            color: GRAPH_GRID_COLOR,\n                        },\n                    },\n                },\n                maintainAspectRatio: false,\n                elements: {\n                    line: {\n                        tension: 0.000001,\n                    },\n                },\n            },\n        };\n    }\n}\n\nexport const journalDashboardGraphField = {\n    component: JournalDashboardGraphField,\n    supportedTypes: [\"text\"],\n    extractProps: ({ attrs }) => ({\n        graphType: attrs.graph_type,\n    }),\n};\n\nregistry.category(\"fields\").add(\"dashboard_graph\", journalDashboardGraphField);\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { standardFieldProps } from \"../standard_field_props\";\nimport { formatJson } from \"@web/views/fields/formatters\";\n\nimport { Component } from \"@odoo/owl\";\n\nexport class JsonField extends Component {\n    static template = \"web.JsonField\";\n    static props = {\n        ...standardFieldProps,\n    };\n    get formattedValue() {\n        return formatJson(this.props.record.data[this.props.name]);\n    }\n}\n\nexport const jsonField = {\n    component: JsonField,\n    displayName: _t(\"Json\"),\n    supportedTypes: [\"json\"],\n};\n\nregistry.category(\"fields\").add(\"json\", jsonField);\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { Component, useState } from \"@odoo/owl\";\nimport { CheckBox } from \"@web/core/checkbox/checkbox\";\nimport { registry } from \"@web/core/registry\";\nimport { standardFieldProps } from \"@web/views/fields/standard_field_props\";\nimport { debounce } from \"@web/core/utils/timing\";\nimport { useRecordObserver } from \"@web/model/relational_model/utils\";\n\nexport class JsonCheckboxes extends Component {\n    static template = \"account.JsonCheckboxes\";\n    static components = { CheckBox };\n    static props = {\n        ...standardFieldProps,\n        stacked: { type: Boolean, optional: true },\n    };\n\n    setup() {\n        this.checkboxes = useState(this.props.record.data[this.props.name]);\n        this.debouncedCommitChanges = debounce(this.commitChanges.bind(this), 100);\n\n        useRecordObserver((record) => {\n            Object.assign(this.checkboxes, record.data[this.props.name]);\n        });\n    }\n\n    commitChanges() {\n        this.props.record.update({ [this.props.name]: this.checkboxes });\n    }\n\n    onChange(key, checked) {\n        this.checkboxes[key].checked = checked;\n        this.debouncedCommitChanges();\n    }\n}\n\nexport const jsonCheckboxes = {\n    component: JsonCheckboxes,\n    supportedOptions: [\n        {\n            label: _t(\"Stacked\"),\n            name: \"stacked\",\n            type: \"boolean\",\n            help: _t(\n                \"If checked, the checkboxes will be displayed in a column. Otherwise, they will be inlined.\"\n            ),\n        },\n    ],\n    supportedTypes: [\"json\"],\n    extractProps({ options }) {\n        const stacked = Boolean(options.stacked);\n        return {\n            stacked,\n        };\n    },\n};\n\nregistry.category(\"fields\").add(\"json_checkboxes\", jsonCheckboxes);\nregistry.category(\"fields\").add(\"account_json_checkboxes\", jsonCheckboxes); // TODO: remove in saas~19.1\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { ColorList } from \"@web/core/colorlist/colorlist\";\nimport { registry } from \"@web/core/registry\";\nimport { standardFieldProps } from \"../standard_field_props\";\n\nimport { Component } from \"@odoo/owl\";\n\nclass KanbanColorPickerField extends Component {\n    static template = \"web.KanbanColorPickerField\";\n    static props = standardFieldProps;\n\n    get colors() {\n        return ColorList.COLORS;\n    }\n\n    selectColor(colorIndex) {\n        return this.props.record.update({ [this.props.name]: colorIndex }, { save: true });\n    }\n}\n\nexport const kanbanColorPickerField = {\n    component: KanbanColorPickerField,\n    displayName: _t(\"Color Picker\"),\n    extractProps(fieldInfo, dynamicInfo) {\n        return {\n            readonly: dynamicInfo.readonly,\n        };\n    },\n};\n\nregistry.category(\"fields\").add(\"kanban_color_picker\", kanbanColorPickerField);\n", "import { registry } from \"@web/core/registry\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { standardFieldProps } from \"../standard_field_props\";\nimport { formatSelection } from \"../formatters\";\n\nimport { Component } from \"@odoo/owl\";\n\nexport class LabelSelectionField extends Component {\n    static template = \"web.LabelSelectionField\";\n    static props = {\n        ...standardFieldProps,\n        classesObj: { type: Object, optional: true },\n    };\n    static defaultProps = {\n        classesObj: {},\n    };\n\n    get className() {\n        return this.props.classesObj[this.props.record.data[this.props.name]] || \"primary\";\n    }\n    get string() {\n        return formatSelection(this.props.record.data[this.props.name], {\n            selection: Array.from(this.props.record.fields[this.props.name].selection),\n        });\n    }\n}\n\nexport const labelSelectionField = {\n    component: LabelSelectionField,\n    displayName: _t(\"Label Selection\"),\n    supportedOptions: [\n        {\n            label: _t(\"Classes\"),\n            name: \"classes\",\n            type: \"string\",\n        },\n    ],\n    supportedTypes: [\"selection\"],\n    extractProps: ({ options }) => ({\n        classesObj: options.classes,\n    }),\n};\n\nregistry.category(\"fields\").add(\"label_selection\", labelSelectionField);\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { standardFieldProps } from \"../standard_field_props\";\nimport { FileInput } from \"@web/core/file_input/file_input\";\nimport { useX2ManyCrud } from \"@web/views/fields/relational_utils\";\n\nimport { Component } from \"@odoo/owl\";\n\nexport class Many2ManyBinaryField extends Component {\n    static template = \"web.Many2ManyBinaryField\";\n    static components = {\n        FileInput,\n    };\n    static props = {\n        ...standardFieldProps,\n        acceptedFileExtensions: { type: String, optional: true },\n        className: { type: String, optional: true },\n        numberOfFiles: { type: Number, optional: true },\n    };\n\n    setup() {\n        this.orm = useService(\"orm\");\n        this.notification = useService(\"notification\");\n        this.operations = useX2ManyCrud(() => this.props.record.data[this.props.name], true);\n    }\n\n    get uploadText() {\n        return this.props.record.fields[this.props.name].string;\n    }\n    get files() {\n        return this.props.record.data[this.props.name].records.map((record) => {\n            return {\n                ...record.data,\n                id: record.resId,\n            };\n        });\n    }\n\n    getUrl(id) {\n        return \"/web/content/\" + id + \"?download=true\";\n    }\n\n    getExtension(file) {\n        return file.name.replace(/^.*\\./, \"\");\n    }\n\n    isImage(file) {\n        return file.mimetype.startsWith(\"image/\");\n    }\n\n    async onFileUploaded(files) {\n        for (const file of files) {\n            if (file.error) {\n                return this.notification.add(file.error, {\n                    title: _t(\"Uploading error\"),\n                    type: \"danger\",\n                });\n            }\n            await this.operations.saveRecord([file.id]);\n        }\n    }\n\n    async onFileRemove(deleteId) {\n        const record = this.props.record.data[this.props.name].records.find(\n            (record) => record.resId === deleteId\n        );\n        this.operations.removeRecord(record);\n    }\n}\n\nexport const many2ManyBinaryField = {\n    component: Many2ManyBinaryField,\n    supportedOptions: [\n        {\n            label: _t(\"Accepted file extensions\"),\n            name: \"accepted_file_extensions\",\n            type: \"string\",\n        },\n        {\n            label: _t(\"Number of files\"),\n            name: \"number_of_files\",\n            type: \"integer\",\n        },\n    ],\n    supportedTypes: [\"many2many\"],\n    isEmpty: () => false,\n    relatedFields: [\n        { name: \"name\", type: \"char\" },\n        { name: \"mimetype\", type: \"char\" },\n    ],\n    extractProps: ({ attrs, options }) => ({\n        acceptedFileExtensions: options.accepted_file_extensions,\n        className: attrs.class,\n        numberOfFiles: options.number_of_files,\n    }),\n};\n\nregistry.category(\"fields\").add(\"many2many_binary\", many2ManyBinaryField);\n", "import { Component, onWillUnmount } from \"@odoo/owl\";\nimport { CheckBox } from \"@web/core/checkbox/checkbox\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { useBus } from \"@web/core/utils/hooks\";\nimport { debounce } from \"@web/core/utils/timing\";\nimport { getFieldDomain } from \"@web/model/relational_model/utils\";\nimport { useSpecialData } from \"@web/views/fields/relational_utils\";\nimport { standardFieldProps } from \"../standard_field_props\";\n\nexport class Many2ManyCheckboxesField extends Component {\n    static template = \"web.Many2ManyCheckboxesField\";\n    static components = { CheckBox };\n    static props = {\n        ...standardFieldProps,\n        domain: { type: [Array, Function], optional: true },\n        context: { type: Object, optional: true },\n    };\n\n    setup() {\n        this.specialData = useSpecialData((orm, props) => {\n            const { relation } = props.record.fields[props.name];\n            const domain = getFieldDomain(props.record, props.name, props.domain);\n            return orm.call(relation, \"name_search\", [\"\", domain], {\n                context: this.props.context || {},\n            });\n        });\n        // these two sets track pending changes in the relation, and allow us to\n        // batch consecutive changes into a single replaceWith, thus saving\n        // unnecessary potential intermediate onchanges\n        this.idsToAdd = new Set();\n        this.idsToRemove = new Set();\n        this.debouncedCommitChanges = debounce(this.commitChanges.bind(this), 500);\n        useBus(this.props.record.model.bus, \"NEED_LOCAL_CHANGES\", this.commitChanges.bind(this));\n        onWillUnmount(this.commitChanges.bind(this));\n    }\n\n    get items() {\n        return this.specialData.data;\n    }\n\n    isSelected(item) {\n        return this.props.record.data[this.props.name].currentIds.includes(item[0]);\n    }\n\n    commitChanges() {\n        if (this.idsToAdd.size === 0 && this.idsToRemove.size === 0) {\n            return;\n        }\n        const result = this.props.record.data[this.props.name].addAndRemove({\n            add: [...this.idsToAdd],\n            remove: [...this.idsToRemove],\n        });\n        this.idsToAdd.clear();\n        this.idsToRemove.clear();\n        return result;\n    }\n\n    onChange(resId, checked) {\n        if (checked) {\n            if (this.idsToRemove.has(resId)) {\n                this.idsToRemove.delete(resId);\n            } else {\n                this.idsToAdd.add(resId);\n            }\n        } else {\n            if (this.idsToAdd.has(resId)) {\n                this.idsToAdd.delete(resId);\n            } else {\n                this.idsToRemove.add(resId);\n            }\n        }\n        this.debouncedCommitChanges();\n    }\n}\n\nexport const many2ManyCheckboxesField = {\n    component: Many2ManyCheckboxesField,\n    displayName: _t(\"Checkboxes\"),\n    supportedTypes: [\"many2many\"],\n    isEmpty: () => false,\n    extractProps(fieldInfo, dynamicInfo) {\n        return {\n            domain: dynamicInfo.domain,\n            context: dynamicInfo.context,\n        };\n    },\n};\n\nregistry.category(\"fields\").add(\"many2many_checkboxes\", many2ManyCheckboxesField);\n", "import { registry } from \"@web/core/registry\";\nimport { Many2ManyTagsField, many2ManyTagsField } from \"./many2many_tags_field\";\n\nexport class KanbanMany2ManyTagsField extends Many2ManyTagsField {\n    static template = \"web.KanbanMany2ManyTagsField\";\n\n    get tags() {\n        return super.tags.reduce((kanbanTags, tag) => {\n            if (tag.colorIndex !== 0) {\n                delete tag.onClick;\n                kanbanTags.push(tag);\n            }\n            return kanbanTags;\n        }, []);\n    }\n}\n\nexport const kanbanMany2ManyTagsField = {\n    ...many2ManyTagsField,\n    component: KanbanMany2ManyTagsField,\n};\n\nregistry.category(\"fields\").add(\"kanban.many2many_tags\", kanbanMany2ManyTagsField);\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { CheckBox } from \"@web/core/checkbox/checkbox\";\nimport { ColorList } from \"@web/core/colorlist/colorlist\";\nimport { Domain } from \"@web/core/domain\";\nimport { evaluateBooleanExpr } from \"@web/core/py_js/py\";\nimport {\n    Many2XAutocomplete,\n    useActiveActions,\n    useX2ManyCrud,\n    useOpenMany2XRecord,\n} from \"@web/views/fields/relational_utils\";\nimport { registry } from \"@web/core/registry\";\nimport { Mutex } from \"@web/core/utils/concurrency\";\nimport { standardFieldProps } from \"../standard_field_props\";\nimport { TagsList } from \"@web/core/tags_list/tags_list\";\nimport { usePopover } from \"@web/core/popover/popover_hook\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { useTagNavigation } from \"@web/core/record_selectors/tag_navigation_hook\";\n\nimport { Component, useRef } from \"@odoo/owl\";\nimport { getFieldDomain } from \"@web/model/relational_model/utils\";\n\nclass Many2ManyTagsFieldColorListPopover extends Component {\n    static template = \"web.Many2ManyTagsFieldColorListPopover\";\n    static components = {\n        CheckBox,\n        ColorList,\n    };\n    static props = {\n        colors: Array,\n        tag: Object,\n        switchTagColor: Function,\n        onTagVisibilityChange: Function,\n        close: Function,\n    };\n}\n\nexport class Many2ManyTagsField extends Component {\n    static template = \"web.Many2ManyTagsField\";\n    static components = {\n        TagsList,\n        Many2XAutocomplete,\n    };\n    static props = {\n        ...standardFieldProps,\n        canCreate: { type: Boolean, optional: true },\n        canQuickCreate: { type: Boolean, optional: true },\n        canCreateEdit: { type: Boolean, optional: true },\n        colorField: { type: String, optional: true },\n        createDomain: { type: [Array, Boolean], optional: true },\n        domain: { type: [Array, Function], optional: true },\n        context: { type: Object, optional: true },\n        placeholder: { type: String, optional: true },\n        nameCreateField: { type: String, optional: true },\n        searchThreshold: { type: Number, optional: true },\n        string: { type: String, optional: true },\n    };\n    static defaultProps = {\n        canCreate: true,\n        canQuickCreate: true,\n        canCreateEdit: true,\n        nameCreateField: \"name\",\n        context: {},\n    };\n\n    static RECORD_COLORS = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11];\n    static SEARCH_MORE_LIMIT = 320;\n\n    setup() {\n        this.orm = useService(\"orm\");\n        this.previousColorsMap = {};\n        this.popover = usePopover(this.constructor.components.Popover);\n        this.dialog = useService(\"dialog\");\n        this.dialogClose = [];\n        useTagNavigation(\"many2ManyTagsField\", {\n            isEnabled: () => !this.props.readonly,\n            delete: (index) => this.deleteTagByIndex(index),\n        });\n        this.autoCompleteRef = useRef(\"autoComplete\");\n        this.mutex = new Mutex();\n\n        const { saveRecord, removeRecord } = useX2ManyCrud(\n            () => this.props.record.data[this.props.name],\n            true\n        );\n\n        this.activeActions = useActiveActions({\n            fieldType: \"many2many\",\n            crudOptions: {\n                create: this.props.canCreate && this.props.createDomain,\n                createEdit: this.props.canCreateEdit,\n                onDelete: removeRecord,\n                edit: this.props.record.isInEdition,\n            },\n            getEvalParams: (props) => ({\n                evalContext: this.evalContext,\n                readonly: props.readonly,\n            }),\n        });\n\n        this.openMany2xRecord = useOpenMany2XRecord({\n            resModel: this.relation,\n            activeActions: {\n                create: false,\n                write: true,\n            },\n            onRecordSaved: (record) => {\n                const records = this.props.record.data[this.props.name].records;\n                return records.find((r) => r.resId === record.resId).load();\n            },\n        });\n\n        this.update = (recordlist) => {\n            recordlist = recordlist\n                ? recordlist.filter(\n                      (element) => !this.tags.some((record) => record.resId === element.id)\n                  )\n                : [];\n            if (!recordlist.length) {\n                return;\n            }\n            const resIds = recordlist.map((rec) => rec.id);\n            return saveRecord(resIds);\n        };\n\n        if (this.props.canQuickCreate) {\n            this.quickCreate = async (name) => {\n                const created = await this.orm.call(this.relation, \"name_create\", [name], {\n                    context: this.props.context,\n                });\n                return saveRecord([created[0]]);\n            };\n        }\n    }\n\n    get relation() {\n        return this.props.record.fields[this.props.name].relation;\n    }\n    get evalContext() {\n        return this.props.record.evalContext;\n    }\n    get string() {\n        return this.props.string || this.props.record.fields[this.props.name].string || \"\";\n    }\n\n    getTagProps(record) {\n        return {\n            id: record.id, // datapoint_X\n            resId: record.resId,\n            text: record.data.display_name,\n            colorIndex: record.data[this.props.colorField],\n            canEdit: this.props.canEditTags,\n            onDelete: !this.props.readonly ? () => this.deleteTag(record.id) : undefined,\n        };\n    }\n\n    get tags() {\n        return this.props.record.data[this.props.name].records.map((record) =>\n            this.getTagProps(record)\n        );\n    }\n\n    get showM2OSelectionField() {\n        return !this.props.readonly;\n    }\n\n    async deleteTagByIndex(index) {\n        this.mutex.exec(() => {\n            if (this.tags[index]) {\n                return this.deleteTag(this.tags[index].id);\n            }\n        });\n    }\n\n    async deleteTag(id) {\n        const tagRecord = this.props.record.data[this.props.name].records.find(\n            (record) => record.id === id\n        );\n        await this.props.record.data[this.props.name].forget(tagRecord);\n    }\n\n    getDomain() {\n        return Domain.and([\n            getFieldDomain(this.props.record, this.props.name, this.props.domain),\n        ]).toList(this.props.context);\n    }\n\n    isSelected(record) {\n        const records = this.props.record.data[this.props.name].records;\n        return records.some((r) => r.resId === record.id);\n    }\n}\n\nexport const many2ManyTagsField = {\n    component: Many2ManyTagsField,\n    displayName: _t(\"Tags\"),\n    supportedOptions: [\n        {\n            label: _t(\"Disable creation\"),\n            name: \"no_create\",\n            type: \"boolean\",\n            help: _t(\n                \"If checked, users won't be able to create records through the autocomplete dropdown at all.\"\n            ),\n        },\n        {\n            label: _t(\"Disable 'Create' option\"),\n            name: \"no_quick_create\",\n            type: \"boolean\",\n            help: _t(\n                \"If checked, users will not be able to create records based on the text input; they will still be able to create records via a popup form.\"\n            ),\n        },\n        {\n            label: _t(\"Disable 'Create and Edit' option\"),\n            name: \"no_create_edit\",\n            type: \"boolean\",\n            help: _t(\n                \"If checked, users will not be able to create records based through a popup form; they will still be able to create records based on the text input.\"\n            ),\n        },\n        {\n            label: _t(\"Can create\"),\n            name: \"create\",\n            type: \"string\",\n            help: _t(\"Write a domain to allow the creation of records conditionnally.\"),\n        },\n        {\n            label: _t(\"Color field\"),\n            name: \"color_field\",\n            type: \"field\",\n            isRelationalField: true,\n            availableTypes: [\"integer\"],\n            help: _t(\"Set an integer field to use colors with the tags.\"),\n        },\n        {\n            label: _t(\"Typeahead search\"),\n            name: \"search_threshold\",\n            type: \"number\",\n            help: _t(\n                \"Defines the minimum number of characters to perform the search. If not set, the search is performed on focus.\"\n            ),\n        },\n        {\n            label: _t(\"Dynamic Placeholder\"),\n            name: \"placeholder_field\",\n            type: \"field\",\n            availableTypes: [\"char\"],\n        },\n    ],\n    supportedTypes: [\"many2many\", \"one2many\"],\n    relatedFields: ({ options }) => {\n        const relatedFields = [{ name: \"display_name\", type: \"char\" }];\n        if (options.color_field) {\n            relatedFields.push({ name: options.color_field, type: \"integer\", readonly: false });\n        }\n        return relatedFields;\n    },\n    extractProps({ attrs, options, string, placeholder }, dynamicInfo) {\n        const hasCreatePermission = attrs.can_create ? evaluateBooleanExpr(attrs.can_create) : true;\n        const noCreate = Boolean(options.no_create);\n        const canCreate = noCreate ? false : hasCreatePermission;\n        const noQuickCreate = Boolean(options.no_quick_create);\n        const noCreateEdit = Boolean(options.no_create_edit);\n        return {\n            colorField: options.color_field,\n            nameCreateField: options.create_name_field,\n            canCreate,\n            canQuickCreate: canCreate && !noQuickCreate,\n            canCreateEdit: canCreate && !noCreateEdit,\n            createDomain: options.create,\n            context: dynamicInfo.context,\n            domain: dynamicInfo.domain,\n            placeholder,\n            searchThreshold: options.search_threshold,\n            string,\n        };\n    },\n};\n\nregistry.category(\"fields\").add(\"many2many_tags\", many2ManyTagsField);\nregistry.category(\"fields\").add(\"calendar.one2many\", many2ManyTagsField);\nregistry.category(\"fields\").add(\"calendar.many2many\", many2ManyTagsField);\n\n/**\n * A specialization that allows to edit the color with the colorpicker.\n * Used in form view.\n */\nexport class Many2ManyTagsFieldColorEditable extends Many2ManyTagsField {\n    static components = {\n        ...super.components,\n        Popover: Many2ManyTagsFieldColorListPopover,\n    };\n    static props = {\n        ...super.props,\n        canEditColor: { type: Boolean, optional: true },\n        canEditTags: { type: Boolean, optional: true },\n    };\n    static defaultProps = {\n        ...super.defaultProps,\n        canEditColor: true,\n        canEditTags: false,\n    };\n\n    getTagProps(record) {\n        const props = super.getTagProps(record);\n        props.onClick = (ev) => this.onTagClick(ev, record);\n        return props;\n    }\n\n    onTagClick(ev, record) {\n        if (this.props.canEditTags) {\n            return this.openMany2xRecord({\n                resId: record.resId,\n                context: this.props.context,\n                title: _t(\"Edit: %s\", record.data.display_name),\n            });\n        }\n        if (!this.props.canEditColor) {\n            return;\n        }\n        if (this.popover.isOpen) {\n            this.popover.close();\n        } else {\n            this.popover.open(ev.currentTarget, {\n                colors: this.constructor.RECORD_COLORS,\n                tag: {\n                    id: record.id,\n                    colorIndex: record.data[this.props.colorField],\n                },\n                switchTagColor: this.switchTagColor.bind(this),\n                onTagVisibilityChange: this.onTagVisibilityChange.bind(this),\n            });\n        }\n    }\n\n    async onTagVisibilityChange(isHidden, tag) {\n        const tagRecord = this.props.record.data[this.props.name].records.find(\n            (record) => record.id === tag.id\n        );\n        if (tagRecord.data[this.props.colorField] != 0) {\n            this.previousColorsMap[tagRecord.resId] = tagRecord.data[this.props.colorField];\n        }\n        const changes = {\n            [this.props.colorField]: isHidden ? 0 : this.previousColorsMap[tagRecord.resId] || 1,\n        };\n        await tagRecord.update(changes);\n        await tagRecord.save();\n        this.popover.close();\n    }\n\n    async switchTagColor(colorIndex, tag) {\n        const tagRecord = this.props.record.data[this.props.name].records.find(\n            (record) => record.id === tag.id\n        );\n        await tagRecord.update({ [this.props.colorField]: colorIndex });\n        await tagRecord.save();\n        this.popover.close();\n    }\n}\n\nexport const many2ManyTagsFieldColorEditable = {\n    ...many2ManyTagsField,\n    component: Many2ManyTagsFieldColorEditable,\n    supportedOptions: [\n        ...many2ManyTagsField.supportedOptions,\n        {\n            label: _t(\"Prevent color edition\"),\n            name: \"no_edit_color\",\n            type: \"boolean\",\n        },\n        {\n            label: _t(\"Edit Tags\"),\n            name: \"edit_tags\",\n            type: \"boolean\",\n            help: _t(\n                \"If checked, clicking on the tag will open the form that allows to directly edit it. Note that if a color field is also set, the tag edition will prevail. So, the color picker will not be displayed on click on the tag.\"\n            ),\n        },\n    ],\n    extractProps({ options, attrs }) {\n        const props = many2ManyTagsField.extractProps(...arguments);\n        const hasEditPermission = attrs.can_write ? evaluateBooleanExpr(attrs.can_write) : true;\n        props.canEditTags = options.edit_tags ? hasEditPermission : false;\n        props.canEditColor = !props.canEditTags && !options.no_edit_color && !!options.color_field;\n        return props;\n    },\n};\n\nregistry.category(\"fields\").add(\"form.many2many_tags\", many2ManyTagsFieldColorEditable);\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { usePopover } from \"@web/core/popover/popover_hook\";\nimport { registry } from \"@web/core/registry\";\nimport {\n    many2ManyTagsField,\n    Many2ManyTagsField,\n} from \"@web/views/fields/many2many_tags/many2many_tags_field\";\nimport { TagsList } from \"@web/core/tags_list/tags_list\";\nimport { imageUrl } from \"@web/core/utils/urls\";\n\nexport class Many2ManyTagsAvatarField extends Many2ManyTagsField {\n    static template = \"web.Many2ManyTagsAvatarField\";\n    static optionTemplate = \"web.Many2ManyTagsAvatarField.option\";\n    static props = {\n        ...Many2ManyTagsField.props,\n        withCommand: { type: Boolean, optional: true },\n    };\n\n    get specification() {\n        return {};\n    }\n\n    getTagProps(record) {\n        return {\n            ...super.getTagProps(record),\n            img: imageUrl(this.relation, record.resId, \"avatar_128\"),\n        };\n    }\n}\n\nexport const many2ManyTagsAvatarField = {\n    ...many2ManyTagsField,\n    component: Many2ManyTagsAvatarField,\n    extractProps({ viewType }, dynamicInfo) {\n        const props = many2ManyTagsField.extractProps(...arguments);\n        props.withCommand = viewType === \"form\" || viewType === \"list\";\n        props.domain = dynamicInfo.domain;\n        return props;\n    },\n};\n\nregistry.category(\"fields\").add(\"many2many_tags_avatar\", many2ManyTagsAvatarField);\n\nexport class ListMany2ManyTagsAvatarField extends Many2ManyTagsAvatarField {\n    visibleItemsLimit = 5;\n}\n\nexport const listMany2ManyTagsAvatarField = {\n    ...many2ManyTagsAvatarField,\n    component: ListMany2ManyTagsAvatarField,\n};\n\nregistry.category(\"fields\").add(\"list.many2many_tags_avatar\", listMany2ManyTagsAvatarField);\n\nexport class Many2ManyTagsAvatarFieldPopover extends Many2ManyTagsAvatarField {\n    static template = \"web.Many2ManyTagsAvatarFieldPopover\";\n    static props = {\n        ...Many2ManyTagsAvatarField.props,\n        close: { type: Function },\n    };\n\n    setup() {\n        super.setup();\n        const originalUpdate = this.update;\n        this.update = async (recordList) => {\n            await originalUpdate(recordList);\n            await this._saveUpdate();\n        };\n    }\n\n    async deleteTag(id) {\n        await super.deleteTag(id);\n        await this._saveUpdate();\n    }\n\n    async _saveUpdate() {\n        await this.props.record.save({ reload: false });\n        // manual render to dirty record\n        this.render();\n        // update dropdown\n        this.autoCompleteRef.el?.querySelector(\"input\")?.click();\n    }\n\n    get tags() {\n        return super.tags.reverse();\n    }\n}\n\nexport const many2ManyTagsAvatarFieldPopover = {\n    ...many2ManyTagsAvatarField,\n    component: Many2ManyTagsAvatarFieldPopover,\n};\nregistry.category(\"fields\").add(\"many2many_tags_avatar_popover\", many2ManyTagsAvatarFieldPopover);\n\nexport class KanbanMany2ManyTagsAvatarFieldTagsList extends TagsList {\n    static template = \"web.KanbanMany2ManyTagsAvatarFieldTagsList\";\n\n    static props = {\n        ...TagsList.props,\n        popoverProps: { type: Object },\n        readonly: { type: Boolean, optional: true },\n    };\n    setup() {\n        super.setup();\n        this.popover = usePopover(Many2ManyTagsAvatarFieldPopover, {\n            popoverClass: \"o_m2m_tags_avatar_field_popover\",\n            closeOnClickAway: (target) => !target.closest(\".modal\"),\n        });\n    }\n\n    openPopover(ev) {\n        if (this.props.readonly) {\n            return;\n        }\n        this.popover.open(ev.currentTarget.parentElement, {\n            ...this.props.popoverProps,\n            readonly: false,\n            canCreate: false,\n            canCreateEdit: false,\n            canQuickCreate: false,\n            placeholder: _t(\"Search users...\"),\n        });\n    }\n    get canDisplayQuickAssignAvatar() {\n        return !this.props.readonly;\n    }\n}\n\nexport class KanbanMany2ManyTagsAvatarField extends Many2ManyTagsAvatarField {\n    static template = \"web.KanbanMany2ManyTagsAvatarField\";\n    static components = {\n        ...Many2ManyTagsAvatarField.components,\n        TagsList: KanbanMany2ManyTagsAvatarFieldTagsList,\n    };\n    static props = {\n        ...Many2ManyTagsAvatarField.props,\n        isEditable: { type: Boolean, optional: true },\n    };\n    visibleItemsLimit = 3;\n\n    get popoverProps() {\n        const props = {\n            ...this.props,\n            readonly: false,\n        };\n        delete props.isEditable;\n        return props;\n    }\n    get tags() {\n        return super.tags.reverse();\n    }\n}\n\nexport const kanbanMany2ManyTagsAvatarField = {\n    ...many2ManyTagsAvatarField,\n    component: KanbanMany2ManyTagsAvatarField,\n    extractProps(fieldInfo, dynamicInfo) {\n        const props = many2ManyTagsAvatarField.extractProps(...arguments);\n        props.isEditable = !dynamicInfo.readonly;\n        return props;\n    },\n};\n\nregistry.category(\"fields\").add(\"kanban.many2many_tags_avatar\", kanbanMany2ManyTagsAvatarField);\n", "import { Component, toRaw, useRef, useState } from \"@odoo/owl\";\nimport * as BarcodeScanner from \"@web/core/barcode/barcode_dialog\";\nimport { isBarcodeScannerSupported } from \"@web/core/barcode/barcode_video_scanner\";\nimport { isMobileOS } from \"@web/core/browser/feature_detection\";\nimport { makeContext } from \"@web/core/context\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { usePopover } from \"@web/core/popover/popover_hook\";\nimport { evaluateBooleanExpr } from \"@web/core/py_js/py\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { getFieldDomain } from \"@web/model/relational_model/utils\";\nimport { Many2XAutocomplete, useOpenMany2XRecord } from \"../relational_utils\";\n\n///////////////////////////////////////////////////////////////////////////////\n// UTILS\n///////////////////////////////////////////////////////////////////////////////\n\nfunction extractData(record) {\n    let name;\n    if (\"display_name\" in record) {\n        name = record.display_name;\n    } else if (\"name\" in record) {\n        name = record.name.id ? record.name.display_name : record.name;\n    }\n    return { id: record.id, display_name: name };\n}\n\nexport function computeM2OProps(fieldProps) {\n    const computeLinkCssClass = () => {\n        const evalContext = fieldProps.record.evalContextWithVirtualIds;\n        for (const decorationName in fieldProps.decorations) {\n            if (evaluateBooleanExpr(fieldProps.decorations[decorationName], evalContext)) {\n                return `text-${decorationName}`;\n            }\n        }\n        return \"\";\n    };\n\n    return {\n        canCreate: fieldProps.canCreate,\n        canCreateEdit: fieldProps.canCreateEdit,\n        canOpen: fieldProps.canOpen,\n        canQuickCreate: fieldProps.canQuickCreate,\n        canScanBarcode: fieldProps.canScanBarcode,\n        canWrite: fieldProps.canWrite,\n        context: fieldProps.context,\n        domain: () => getFieldDomain(fieldProps.record, fieldProps.name, fieldProps.domain),\n        id: fieldProps.id,\n        linkCssClass: computeLinkCssClass(),\n        nameCreateField: fieldProps.nameCreateField,\n        openActionContext: () => {\n            const { context, name, openActionContext, record } = fieldProps;\n            return makeContext(\n                [openActionContext || context, record.fields[name].context],\n                record.evalContext\n            );\n        },\n        placeholder: fieldProps.placeholder,\n        readonly: fieldProps.readonly,\n        relation: fieldProps.record.fields[fieldProps.name].relation,\n        searchThreshold: fieldProps.searchThreshold,\n        string: fieldProps.string || fieldProps.record.fields[fieldProps.name].string || \"\",\n        update: (value, options = {}) =>\n            fieldProps.record.update({ [fieldProps.name]: value }, options),\n        value: toRaw(fieldProps.record.data[fieldProps.name]),\n    };\n}\n\n///////////////////////////////////////////////////////////////////////////////\n// Components\n///////////////////////////////////////////////////////////////////////////////\n\nexport class Many2One extends Component {\n    static template = \"web.Many2One\";\n    static components = { Many2XAutocomplete };\n    static props = {\n        canCreate: { type: Boolean, optional: true },\n        canCreateEdit: { type: Boolean, optional: true },\n        canOpen: { type: Boolean, optional: true },\n        canQuickCreate: { type: Boolean, optional: true },\n        canScanBarcode: { type: Boolean, optional: true },\n        canWrite: { type: Boolean, optional: true },\n        context: { type: Object, optional: true },\n        createAction: { type: Function, optional: true },\n        cssClass: { type: String, optional: true },\n        domain: { type: Function, optional: true },\n        id: { type: String, optional: true },\n        linkCssClass: { type: String, optional: true },\n        nameCreateField: { type: String, optional: true },\n        openActionContext: { type: Function, optional: true },\n        openRecordAction: { type: Function, optional: true },\n        otherSources: { type: Array, optional: true },\n        placeholder: { type: String, optional: true },\n        readonly: { type: Boolean, optional: true },\n        relation: { type: String },\n        searchMoreLabel: { type: String, optional: true },\n        searchThreshold: { type: Number, optional: true },\n        slots: { type: Object, optional: true },\n        specification: { type: Object, optional: true },\n        string: { type: String, optional: true },\n        update: { type: Function },\n        value: { type: [Array, Object, { value: false }], optional: true },\n    };\n    static defaultProps = {\n        canCreate: true,\n        canCreateEdit: true,\n        canOpen: true,\n        canQuickCreate: true,\n        canScanBarcode: false,\n        canWrite: true,\n        context: {},\n        domain: [],\n        linkCssClass: \"\",\n        nameCreateField: \"name\",\n        otherSources: [],\n        placeholder: \"\",\n        readonly: false,\n        string: \"\",\n    };\n\n    setup() {\n        this.rootRef = useRef(\"root\");\n\n        this.action = useService(\"action\");\n        this.notification = useService(\"notification\");\n        this.orm = useService(\"orm\");\n\n        this.state = useState({ isFloating: false });\n\n        this.recordDialog = {\n            open: useOpenMany2XRecord({\n                activeActions: this.activeActions,\n                fieldString: this.props.string,\n                isToMany: false,\n                onClose: () => {\n                    this.input.focus();\n                },\n                onRecordSaved: async () => {\n                    const resId = this.props.value?.id;\n                    const fieldNames = [\"display_name\"];\n                    // use unity read + relatedFields from Field Component\n                    const records = await this.orm.read(this.props.relation, [resId], fieldNames, {\n                        context: this.props.context,\n                    });\n                    await this.update(records[0] ? extractData(records[0]) : false);\n                },\n                onRecordDiscarded: () => {},\n                resModel: this.props.relation,\n            }),\n        };\n    }\n\n    get activeActions() {\n        return {\n            create: this.props.canCreate,\n            createEdit: this.props.canCreateEdit,\n            write: this.props.canWrite,\n        };\n    }\n\n    get many2XAutocompleteProps() {\n        return {\n            activeActions: this.activeActions,\n            autoSelect: true,\n            context: this.props.context,\n            createAction: this.props.createAction,\n            fieldString: this.props.string,\n            getDomain: this.props.domain,\n            id: this.props.id,\n            nameCreateField: this.props.nameCreateField,\n            otherSources: this.props.otherSources,\n            placeholder: this.props.placeholder,\n            quickCreate: this.props.canQuickCreate ? (name) => this.quickCreate(name) : null,\n            resModel: this.props.relation,\n            searchMoreLabel: this.props.searchMoreLabel,\n            searchThreshold: this.props.searchThreshold,\n            setInputFloats: (isFloating) => {\n                this.state.isFloating = isFloating;\n            },\n            slots: this.props.slots,\n            specification: this.props.specification,\n            update: (records) => {\n                const idNamePair = records && records[0] ? extractData(records[0]) : false;\n                return this.update(idNamePair);\n            },\n            value: this.displayName,\n        };\n    }\n\n    get displayName() {\n        if (this.props.value) {\n            if (this.props.value.display_name) {\n                return this.props.value.display_name.split(\"\\n\")[0];\n            } else {\n                return _t(\"Unnamed\");\n            }\n        } else {\n            return \"\";\n        }\n    }\n\n    get extraLines() {\n        const name = this.props.value?.display_name;\n        return name\n            ? name\n                  .split(\"\\n\")\n                  .map((line) => line.trim())\n                  .slice(1)\n            : [];\n    }\n\n    get hasBarcodeButton() {\n        const supported = isBarcodeScannerSupported();\n        return this.props.canScanBarcode && isMobileOS() && supported && !this.hasLinkButton;\n    }\n\n    get hasLinkButton() {\n        return this.props.canOpen && !!this.props.value && !this.state.isFloating;\n    }\n\n    get input() {\n        return this.rootRef.el?.querySelector(\"input\");\n    }\n\n    get linkHref() {\n        if (!this.props.value) {\n            return \"/\";\n        }\n        const relation = this.props.relation.includes(\".\")\n            ? this.props.relation\n            : `m-${this.props.relation}`;\n        return `/odoo/${relation}/${this.props.value.id}`;\n    }\n\n    async openBarcodeScanner() {\n        const barcode = await BarcodeScanner.scanBarcode(this.env);\n        if (barcode) {\n            await this.processScannedBarcode(barcode);\n            if (\"vibrate\" in navigator) {\n                navigator.vibrate(100);\n            }\n        } else {\n            /** @type {any} */\n            const message = _t(\"Please, scan again!\");\n            this.notification.add(message, { type: \"warning\" });\n        }\n    }\n\n    async openRecord(mode) {\n        if (this.props.openRecordAction) {\n            return this.props.openRecordAction(mode);\n        }\n\n        switch (mode) {\n            case \"action\": {\n                return this.openRecordInAction(false);\n            }\n            case \"dialog\": {\n                return this.openRecordInDialog();\n            }\n            case \"tab\": {\n                return this.openRecordInAction(true);\n            }\n        }\n    }\n\n    async openRecordInAction(newWindow) {\n        const action = await this.orm.call(\n            this.props.relation,\n            \"get_formview_action\",\n            [[this.props.value?.id]],\n            { context: this.props.openActionContext() }\n        );\n        await this.action.doAction(action, { newWindow });\n    }\n\n    async openRecordInDialog() {\n        return this.recordDialog.open({\n            resId: this.props.value?.id,\n            context: this.props.context,\n        });\n    }\n\n    async processScannedBarcode(barcode) {\n        const pairs = await this.orm.call(this.props.relation, \"name_search\", [], {\n            name: barcode,\n            domain: this.props.domain(),\n            operator: \"ilike\",\n            limit: 2, // If one result we set directly and if more than one we use normal flow so no need to search more\n            context: this.props.context,\n        });\n        const validPairs = pairs.filter(([id]) => !!id);\n        if (validPairs.length === 1) {\n            const pair = validPairs[0];\n            return this.update({ id: pair[0], display_name: pair[1] });\n        } else {\n            const input = this.input;\n            input.value = barcode;\n            input.dispatchEvent(new Event(\"input\"));\n            if (this.env.isSmall) {\n                input.dispatchEvent(new Event(\"barcode-search\"));\n            }\n        }\n    }\n\n    quickCreate(name) {\n        return this.update({ id: false, display_name: name });\n    }\n\n    update(idNamePair) {\n        this.state.isFloating = false;\n        return this.props.update(idNamePair);\n    }\n}\n\nclass KanbanMany2OneAssignPopover extends Many2One {\n    static props = {\n        ...super.props,\n        close: Function,\n    };\n\n    get many2XAutocompleteProps() {\n        return {\n            ...super.many2XAutocompleteProps,\n            dropdown: false,\n        };\n    }\n}\n\nexport class KanbanMany2One extends Component {\n    static template = \"web.KanbanMany2One\";\n    static props = { ...Many2One.props };\n\n    setup() {\n        this.assignPopover = usePopover(KanbanMany2OneAssignPopover, {\n            popoverClass: \"o_m2o_tags_avatar_field_popover\",\n        });\n    }\n\n    openAssignPopover(target) {\n        this.assignPopover.open(target, {\n            ...this.props,\n            canCreate: false,\n            canCreateEdit: false,\n            canQuickCreate: false,\n            placeholder: this.props.placeholder || _t(\"Search user...\"),\n            readonly: false,\n            update: async (value) => {\n                await this.props.update(value, { save: true });\n                this.assignPopover.close();\n            },\n        });\n    }\n}\n", "import { Component } from \"@odoo/owl\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { computeM2OProps, Many2One } from \"./many2one\";\nimport { standardFieldProps } from \"../standard_field_props\";\nimport { evaluateBooleanExpr } from \"@web/core/py_js/py\";\n\n/** @type {import(\"registries\").FieldsRegistryItemShape[\"supportedOptions\"]} */\nexport const m2oSupportedOptions = [\n    {\n        label: _t(\"Disable opening\"),\n        name: \"no_open\",\n        type: \"boolean\",\n    },\n    {\n        label: _t(\"Disable creation\"),\n        name: \"no_create\",\n        type: \"boolean\",\n        help: _t(\n            \"If checked, users won't be able to create records through the autocomplete dropdown at all.\"\n        ),\n    },\n    {\n        label: _t(\"Disable 'Create' option\"),\n        name: \"no_quick_create\",\n        type: \"boolean\",\n        help: _t(\n            \"If checked, users will not be able to create records based on the text input; they will still be able to create records via a popup form.\"\n        ),\n    },\n    {\n        label: _t(\"Disable 'Create and Edit' option\"),\n        name: \"no_create_edit\",\n        type: \"boolean\",\n        help: _t(\n            \"If checked, users will not be able to create records based through a popup form; they will still be able to create records based on the text input.\"\n        ),\n    },\n    {\n        label: _t(\"Typeahead search\"),\n        name: \"search_threshold\",\n        type: \"number\",\n        help: _t(\n            \"Defines the minimum number of characters to perform the search. If not set, the search is performed on focus.\"\n        ),\n    },\n    {\n        label: _t(\"Dynamic placeholder\"),\n        name: \"placeholder_field\",\n        type: \"field\",\n        availableTypes: [\"char\"],\n    },\n];\n/** @type {import(\"registries\").FieldsRegistryItemShape[\"supportedTypes\"]} */\nexport const m2oSupportedTypes = [\"many2one\"];\n\n/**\n * @param {typeof Component} component\n * @returns {import(\"registries\").FieldsRegistryItemShape}\n */\nexport function buildM2OFieldDescription(component) {\n    return {\n        component,\n        displayName: _t(\"Many2one\"),\n        extractProps: extractM2OFieldProps,\n        supportedOptions: m2oSupportedOptions,\n        supportedTypes: m2oSupportedTypes,\n    };\n}\n\nexport function extractM2OFieldProps(staticInfo, dynamicInfo) {\n    const { attrs, context, decorations, options, string, placeholder } = staticInfo;\n\n    const hasCreatePermission = attrs.can_create ? evaluateBooleanExpr(attrs.can_create) : true;\n    const hasWritePermission = attrs.can_write ? evaluateBooleanExpr(attrs.can_write) : true;\n    const canCreate = options.no_create ? false : hasCreatePermission;\n    return {\n        canCreate,\n        canCreateEdit: canCreate && !options.no_create_edit,\n        canOpen: !options.no_open,\n        canQuickCreate: canCreate && !options.no_quick_create,\n        canScanBarcode: !!options.can_scan_barcode,\n        canWrite: hasWritePermission,\n        context: dynamicInfo.context,\n        decorations,\n        domain: dynamicInfo.domain,\n        nameCreateField: options.create_name_field,\n        openActionContext: context || \"{}\",\n        placeholder,\n        searchThreshold: options.search_threshold,\n        string,\n    };\n}\n\nexport class Many2OneField extends Component {\n    static template = \"web.Many2OneField\";\n    static components = { Many2One };\n    static props = {\n        ...standardFieldProps,\n        canCreate: { type: Boolean, optional: true },\n        canCreateEdit: { type: Boolean, optional: true },\n        canOpen: { type: Boolean, optional: true },\n        canQuickCreate: { type: Boolean, optional: true },\n        canScanBarcode: { type: Boolean, optional: true },\n        canWrite: { type: Boolean, optional: true },\n        context: { type: Object, optional: true },\n        decorations: { type: Object, optional: true },\n        domain: { type: [Array, Function], optional: true },\n        nameCreateField: { type: String, optional: true },\n        openActionContext: { type: String, optional: true },\n        placeholder: { type: String, optional: true },\n        searchLimit: { type: Number, optional: true },\n        searchThreshold: { type: Number, optional: true },\n        string: { type: String, optional: true },\n    };\n\n    get m2oProps() {\n        return computeM2OProps(this.props);\n    }\n}\n\nregistry.category(\"fields\").add(\"many2one\", {\n    ...buildM2OFieldDescription(Many2OneField),\n});\n", "import { Component } from \"@odoo/owl\";\nimport { registry } from \"@web/core/registry\";\nimport { computeM2OProps, KanbanMany2One } from \"../many2one/many2one\";\nimport {\n    buildM2OFieldDescription,\n    extractM2OFieldProps,\n    Many2OneField,\n} from \"../many2one/many2one_field\";\n\nexport class KanbanMany2OneAvatarField extends Component {\n    static template = \"web.KanbanMany2OneAvatarField\";\n    static components = { KanbanMany2One };\n    static props = { ...Many2OneField.props };\n\n    get m2oProps() {\n        return computeM2OProps(this.props);\n    }\n}\n\nregistry.category(\"fields\").add(\"kanban.many2one_avatar\", {\n    ...buildM2OFieldDescription(KanbanMany2OneAvatarField),\n    additionalClasses: [\"o_field_many2one_avatar_kanban\"],\n    extractProps(staticInfo, dynamicInfo) {\n        return {\n            ...extractM2OFieldProps(staticInfo, dynamicInfo),\n            readonly: dynamicInfo.readonly,\n        };\n    },\n});\n", "import { Component } from \"@odoo/owl\";\nimport { registry } from \"@web/core/registry\";\nimport { computeM2OProps, Many2One } from \"../many2one/many2one\";\nimport {\n    buildM2OFieldDescription,\n    extractM2OFieldProps,\n    Many2OneField,\n} from \"../many2one/many2one_field\";\n\nexport class Many2OneAvatarField extends Component {\n    static template = \"web.Many2OneAvatarField\";\n    static components = { Many2One };\n    static props = { ...Many2OneField.props };\n\n    get m2oProps() {\n        return computeM2OProps(this.props);\n    }\n}\n\nexport const many2OneAvatarField = {\n    ...buildM2OFieldDescription(Many2OneAvatarField),\n    extractProps(staticInfo, dynamicInfo) {\n        return {\n            ...extractM2OFieldProps(staticInfo, dynamicInfo),\n            canOpen:\n                \"no_open\" in staticInfo.options\n                    ? !staticInfo.options.no_open\n                    : staticInfo.viewType === \"form\",\n        };\n    },\n};\n\nregistry.category(\"fields\").add(\"many2one_avatar\", many2OneAvatarField);\n", "import { Component } from \"@odoo/owl\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { computeM2OProps, Many2One } from \"../many2one/many2one\";\nimport {\n    buildM2OFieldDescription,\n    extractM2OFieldProps,\n    Many2OneField,\n} from \"../many2one/many2one_field\";\n\nexport class Many2OneBarcodeField extends Component {\n    static template = \"web.Many2OneBarcodeField\";\n    static components = { Many2One };\n    static props = { ...Many2OneField.props };\n\n    get m2oProps() {\n        return computeM2OProps(this.props);\n    }\n}\n\nexport const many2OneBarcodeField = {};\n\nregistry.category(\"fields\").add(\"many2one_barcode\", {\n    ...buildM2OFieldDescription(Many2OneBarcodeField),\n    displayName: _t(\"Many2OneBarcode\"),\n    extractProps(staticInfo, dynamicInfo) {\n        return {\n            ...extractM2OFieldProps(staticInfo, dynamicInfo),\n            canScanBarcode: true,\n        };\n    },\n});\n", "import { Component } from \"@odoo/owl\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { computeM2OProps, Many2One } from \"../many2one/many2one\";\nimport { extractM2OFieldProps, Many2OneField } from \"../many2one/many2one_field\";\n\nexport class Many2OneReferenceField extends Component {\n    static template = \"web.Many2OneReferenceField\";\n    static components = { Many2One };\n    static props = { ...Many2OneField.props };\n\n    get m2oProps() {\n        const props = computeM2OProps(this.props);\n\n        const relation = this.relation;\n        const value = this.props.record.data[this.props.name];\n\n        return {\n            ...props,\n            relation,\n            value: value ? { id: value.resId, display_name: value.displayName } : false,\n            readonly: this.props.readonly || !relation,\n            update: (changes) => this.update(changes),\n        };\n    }\n\n    get relation() {\n        const modelField = this.props.record.fields[this.props.name].model_field;\n        if (!(modelField in this.props.record.data)) {\n            throw new Error(`Many2OneReferenceField: model_field must be in view (${modelField})`);\n        }\n        return this.props.record.data[modelField];\n    }\n\n    update(record) {\n        const nextVal = record && { resId: record.id, displayName: record.display_name };\n        return this.props.record.update({ [this.props.name]: nextVal });\n    }\n}\n\nregistry.category(\"fields\").add(\"many2one_reference\", {\n    component: Many2OneReferenceField,\n    displayName: _t(\"Many2OneReference\"),\n    extractProps(staticInfo, dynamicInfo) {\n        return extractM2OFieldProps(staticInfo, dynamicInfo);\n    },\n    relatedFields: [{ name: \"display_name\", type: \"char\" }],\n    supportedTypes: [\"many2one_reference\"],\n});\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { IntegerField } from \"@web/views/fields/integer/integer_field\";\n\nexport class Many2OneReferenceIntegerField extends IntegerField {\n    get value() {\n        const value = this.props.record.data[this.props.name];\n        return value ? value.resId : false;\n    }\n}\n\nconst many2oneReferenceIntegerField = {\n    component: Many2OneReferenceIntegerField,\n    displayName: _t(\"Many2OneReferenceInteger\"),\n    supportedTypes: [\"many2one_reference\"],\n};\n\nregistry.category(\"fields\").add(\"many2one_reference_integer\", many2oneReferenceIntegerField);\n", "import { registry } from \"@web/core/registry\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { formatMonetary } from \"../formatters\";\nimport { parseMonetary } from \"../parsers\";\nimport { useInputField } from \"../input_field_hook\";\nimport { useNumpadDecimal } from \"../numpad_decimal_hook\";\nimport { standardFieldProps } from \"../standard_field_props\";\nimport { nbsp } from \"@web/core/utils/strings\";\n\nimport { Component, useState, useEffect } from \"@odoo/owl\";\nimport { getCurrency } from \"@web/core/currency\";\n\nexport class MonetaryField extends Component {\n    static template = \"web.MonetaryField\";\n    static props = {\n        ...standardFieldProps,\n        currencyField: { type: String, optional: true },\n        inputType: { type: String, optional: true },\n        useFieldDigits: { type: Boolean, optional: true },\n        hideSymbol: { type: Boolean, optional: true },\n        trailingZeros: { type: Boolean, optional: true },\n    };\n    static defaultProps = {\n        hideSymbol: false,\n        inputType: \"text\",\n        trailingZeros: true,\n    };\n\n    setup() {\n        this.inputRef = useInputField(this.inputOptions);\n        this.state = useState({ value: undefined });\n        this.nbsp = nbsp;\n        useNumpadDecimal();\n        useEffect(() => {\n            if (this.inputRef?.el) {\n                this.state.value = this.inputRef.el.value;\n            }\n        });\n    }\n\n    get inputOptions() {\n        return {\n            getValue: () => this.formattedValue,\n            refName: \"numpadDecimal\",\n            parse: (v) => parseMonetary(v, { allowOperation: true }),\n        };\n    }\n\n    get currencyId() {\n        const currencyField =\n            this.props.currencyField ||\n            this.props.record.fields[this.props.name].currency_field ||\n            \"currency_id\";\n        const currency = this.props.record.data[currencyField];\n        return currency && currency.id;\n    }\n    get currency() {\n        if (!isNaN(this.currencyId)) {\n            return getCurrency(this.currencyId) || null;\n        }\n        return null;\n    }\n\n    get currencySymbol() {\n        return this.currency ? this.currency.symbol : \"\";\n    }\n\n    get currencyDigits() {\n        if (this.props.useFieldDigits) {\n            return this.props.record.fields[this.props.name].digits;\n        }\n        if (!this.currency) {\n            return null;\n        }\n        return getCurrency(this.currencyId).digits;\n    }\n\n    get value() {\n        return this.props.record.data[this.props.name];\n    }\n\n    get formattedValue() {\n        if (this.props.inputType === \"number\" && !this.props.readonly && this.value) {\n            return this.value;\n        }\n        return formatMonetary(this.value, {\n            digits: this.currencyDigits,\n            currencyId: this.currencyId,\n            noSymbol: !this.props.readonly || this.props.hideSymbol,\n            trailingZeros: this.props.trailingZeros,\n        });\n    }\n\n    onInput(ev) {\n        this.state.value = ev.target.value;\n    }\n}\n\nexport const monetaryField = {\n    component: MonetaryField,\n    supportedOptions: [\n        {\n            label: _t(\"Hide symbol\"),\n            name: \"no_symbol\",\n            type: \"boolean\",\n        },\n        {\n            label: _t(\"Currency\"),\n            name: \"currency_field\",\n            type: \"field\",\n            availableTypes: [\"many2one\"],\n        },\n        {\n            label: _t(\"Hide trailing zeros\"),\n            name: \"hide_trailing_zeros\",\n            type: \"boolean\",\n            help: _t(\"Hide zeros to the right of the last non-zero digit, e.g. 1.20 becomes 1.2\"),\n        },\n    ],\n    supportedTypes: [\"monetary\", \"float\", \"integer\"],\n    displayName: _t(\"Monetary\"),\n    isEmpty: (record, fieldName) => record.data[fieldName] === false,\n    extractProps: ({ attrs, options }) => ({\n        currencyField: options.currency_field,\n        inputType: attrs.type,\n        useFieldDigits: options.field_digits,\n        hideSymbol: options.no_symbol,\n        trailingZeros: !options.hide_trailing_zeros,\n    }),\n};\n\nregistry.category(\"fields\").add(\"monetary\", monetaryField);\n", "import { localization } from \"@web/core/l10n/localization\";\nimport { isIOS } from \"@web/core/browser/feature_detection\";\n\nimport { useRef, useEffect } from \"@odoo/owl\";\n\nfunction onKeydown(ev) {\n    const decimalPoint = localization.decimalPoint;\n    if (\n        !([\".\", \",\"].includes(ev.key) && ev.code === \"NumpadDecimal\") ||\n        ev.key === decimalPoint ||\n        ev.target.type === \"number\"\n    ) {\n        return;\n    }\n    ev.preventDefault();\n    ev.target.setRangeText(decimalPoint, ev.target.selectionStart, ev.target.selectionEnd, \"end\");\n}\n\nfunction onFocus(ev) {\n    ev.target.select();\n}\n\n/**\n * This hook replaces the decimal separator of the numpad decimal key\n * by the decimal separator from the user's language setting when user\n * edits an input. The input is found using a t-ref=\"numpadDecimal\"\n * reference in the current component. It can be placed directly on an\n * input or an element containing multiple inputs that require the\n * behavior\n *\n * NOTE: Special consideration for the input type = \"number\". In this\n * case, whatever the user types, we let the browser's default behavior.\n *\n * NOTE: On IOS devices, the inputmode attribute prevents the user from\n * entering a negative number (the minus sign is not on the virtual keyboard),\n * so we need to remove it.\n */\nexport function useNumpadDecimal() {\n    const ref = useRef(\"numpadDecimal\");\n    const isIOSDevice = isIOS();\n    useEffect(() => {\n        let inputs = [];\n        const el = ref.el;\n        if (el) {\n            inputs = el.nodeName === \"INPUT\" ? [el] : el.querySelectorAll(\"input\");\n            inputs.forEach((input) => input.addEventListener(\"keydown\", onKeydown));\n            inputs.forEach((input) => input.addEventListener(\"focus\", onFocus));\n            if (isIOSDevice) {\n                inputs.forEach((input) => input.removeAttribute(\"inputmode\"));\n            }\n        }\n        return () => {\n            inputs.forEach((input) => input.removeEventListener(\"keydown\", onKeydown));\n            inputs.forEach((input) => input.removeEventListener(\"focus\", onFocus));\n        };\n    });\n}\n", "import { parseDate, parseDateTime } from \"@web/core/l10n/dates\";\nimport { localization } from \"@web/core/l10n/localization\";\nimport { evaluateExpr } from \"@web/core/py_js/py\";\nimport { registry } from \"@web/core/registry\";\nimport { escapeRegExp } from \"@web/core/utils/strings\";\nimport { Operation } from \"@web/model/relational_model/operation\";\n\n// -----------------------------------------------------------------------------\n// Helpers\n// -----------------------------------------------------------------------------\n\nfunction evaluateMathematicalExpression(expr, context = {}) {\n    // remove extra space\n    var val = expr.replace(new RegExp(/( )/g), \"\");\n    var safeEvalString = \"\";\n    for (let v of val.split(new RegExp(/([-+*/()^])/g))) {\n        if (![\"+\", \"-\", \"*\", \"/\", \"(\", \")\", \"^\"].includes(v) && v.length) {\n            // check if this is a float and take into account user delimiter preference\n            v = parseFloat(v);\n        }\n        if (v === \"^\") {\n            v = \"**\";\n        }\n        safeEvalString += v;\n    }\n    return evaluateExpr(safeEvalString, context);\n}\n\nfunction parseOperation(value, parseValueFn) {\n    const regex = new RegExp(\n        `^(?<operator>[+\\\\-*/])\\\\s*=\\\\s*(?<operand>-?\\\\d+(?:[${escapeRegExp(\n            localization.decimalPoint\n        )}]\\\\d+)?)$`\n    );\n    const match = value.match(regex);\n    if (match?.groups) {\n        const operand = parseValueFn(match.groups.operand);\n        const operator = match.groups.operator;\n        return new Operation(operator, operand);\n    }\n    return false;\n}\n\n/**\n * Parses a string into a number.\n *\n * @param {string} value\n * @param {Object} options - additional options\n * @param {string|RegExp} options.thousandsSep - the thousands separator used in the value\n * @param {string|RegExp} options.decimalPoint - the decimal point used in the value\n * @returns {number}\n */\nfunction parseNumber(value, options = {}) {\n    if (value.startsWith(\"=\")) {\n        value = evaluateMathematicalExpression(value.substring(1));\n        if (options.truncate) {\n            value = Math.trunc(value);\n        }\n    } else {\n        // A whitespace thousands separator is equivalent to any whitespace character.\n        // E.g. \"1  000 000\" should be parsed as 1000000 even if the\n        // thousands separator is nbsp.\n        const thousandsSepRegex = options.thousandsSep.match(/\\s+/)\n            ? /\\s+/g\n            : new RegExp(escapeRegExp(options.thousandsSep), \"g\") || \",\";\n\n        // a number can have the thousand separator multiple times. ex: 1,000,000.00\n        value = value.replaceAll(thousandsSepRegex, \"\");\n        // a number only have one decimal separator\n        value = value.replace(new RegExp(escapeRegExp(options.decimalPoint), \"g\") || \".\", \".\");\n    }\n\n    return Number(value);\n}\n\n// -----------------------------------------------------------------------------\n// Exports\n// -----------------------------------------------------------------------------\n\nexport class InvalidNumberError extends Error {}\n\n/**\n * Try to extract a float from a string. The localization is considered in the process.\n *\n * @param {string} value\n * @returns {number} a float\n */\nexport function parseFloat(value, { allowOperation = false } = {}) {\n    const operation = allowOperation ? parseOperation(value, parseFloat) : null;\n    if (operation instanceof Operation) {\n        return operation;\n    }\n    const thousandsSepRegex = localization.thousandsSep || \"\";\n    const decimalPointRegex = localization.decimalPoint;\n    let parsed = parseNumber(value, {\n        thousandsSep: thousandsSepRegex,\n        decimalPoint: decimalPointRegex,\n    });\n    if (isNaN(parsed)) {\n        parsed = parseNumber(value, {\n            thousandsSep: \",\",\n            decimalPoint: \".\",\n        });\n        if (isNaN(parsed)) {\n            throw new InvalidNumberError(`\"${value}\" is not a correct number`);\n        }\n    }\n    return parsed;\n}\n\n/**\n * Try to extract a float time from a string. The localization is considered in the process.\n * The float time can have two formats: float or integer:integer.\n *\n * @param {string} value\n * @returns {number} a float\n */\nexport function parseFloatTime(value) {\n    let sign = 1;\n    if (value[0] === \"-\") {\n        value = value.slice(1);\n        sign = -1;\n    }\n    const values = value.split(\":\");\n    if (values.length > 2) {\n        throw new InvalidNumberError(`\"${value}\" is not a correct number`);\n    }\n    if (values.length === 1) {\n        return sign * parseFloat(value);\n    }\n    const hours = parseInteger(values[0]);\n    const minutes = parseInteger(values[1]);\n    return sign * (hours + minutes / 60);\n}\n\n/**\n * Try to extract an integer from a string. The localization is considered in the process.\n *\n * @param {string} value\n * @returns {number} an integer\n */\nexport function parseInteger(value, { allowOperation = false } = {}) {\n    const operation = allowOperation ? parseOperation(value, parseInteger) : null;\n    if (operation instanceof Operation) {\n        return operation;\n    }\n    const thousandsSepRegex = localization.thousandsSep || \"\";\n    const decimalPointRegex = localization.decimalPoint;\n    let parsed = parseNumber(value, {\n        thousandsSep: thousandsSepRegex,\n        decimalPoint: decimalPointRegex,\n        truncate: true,\n    });\n    if (!Number.isInteger(parsed)) {\n        parsed = parseNumber(value, {\n            thousandsSep: \",\",\n            decimalPoint: \".\",\n            truncate: true,\n        });\n        if (!Number.isInteger(parsed)) {\n            throw new InvalidNumberError(`\"${value}\" is not a correct number`);\n        }\n    }\n    if (parsed < -2147483648 || parsed > 2147483647) {\n        throw new InvalidNumberError(\n            `\"${value}\" is out of bounds (integers should be between -2,147,483,648 and 2,147,483,647)`\n        );\n    }\n    return parsed;\n}\n\n/**\n * Try to extract a float from a string and unconvert it with a conversion factor of 100.\n * The localization is considered in the process.\n * The percentage can have two formats: float or float%.\n *\n * @param {string} value\n * @returns {number} float\n */\nexport function parsePercentage(value) {\n    if (value[value.length - 1] === \"%\") {\n        value = value.slice(0, value.length - 1);\n    }\n    return parseFloat(value) / 100;\n}\n\n/**\n * Try to extract a monetary value from a string. The localization is considered in the process.\n * This is a very lenient function such that it ignores everything before we encounter a substring consisting of either\n * - a sign (- or +)\n * - an equals sign (signaling the start of a mathematical expression)\n * - a decimal point\n * - a number\n * We then remove any non-numeric characters at the end\n *\n *\n * @param {string} value\n * @returns {number}\n */\nexport function parseMonetary(value, { allowOperation = false } = {}) {\n    const operation = allowOperation ? parseOperation(value, parseMonetary) : null;\n    if (operation instanceof Operation) {\n        return operation;\n    }\n    value = value.trim();\n    const startMatch = value.match(\n        new RegExp(`[\\\\d\\\\-+=]|${escapeRegExp(localization.decimalPoint)}`)\n    );\n    if (startMatch) {\n        value = value.substring(startMatch.index);\n    }\n    value = value.replace(/\\D*$/, \"\");\n    return parseFloat(value);\n}\n\nregistry\n    .category(\"parsers\")\n    .add(\"date\", parseDate)\n    .add(\"datetime\", parseDateTime)\n    .add(\"float\", parseFloat)\n    .add(\"float_time\", parseFloatTime)\n    .add(\"integer\", parseInteger)\n    .add(\"many2one_reference\", parseInteger)\n    .add(\"monetary\", parseMonetary)\n    .add(\"percentage\", parsePercentage);\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { url } from \"@web/core/utils/urls\";\nimport { standardFieldProps } from \"../standard_field_props\";\nimport { FileUploader } from \"../file_handler\";\n\nimport { Component, onWillUpdateProps, useEffect, useRef, useState } from \"@odoo/owl\";\nimport { hidePDFJSButtons } from \"@web/core/utils/pdfjs\";\n\nexport class PdfViewerField extends Component {\n    static template = \"web.PdfViewerField\";\n    static components = {\n        FileUploader,\n    };\n    static props = {\n        ...standardFieldProps,\n    };\n\n    setup() {\n        this.notification = useService(\"notification\");\n        this.action = useService(\"action\");\n        this.state = useState({\n            isValid: true,\n            objectUrl: \"\",\n        });\n        this.iframeViewerPdfRef = useRef(\"iframeViewerPdf\");\n        onWillUpdateProps((nextProps) => {\n            if (nextProps.readonly) {\n                this.state.objectUrl = \"\";\n            }\n        });\n        useEffect(\n            (el) => {\n                if (el) {\n                    hidePDFJSButtons(this.iframeViewerPdfRef.el, {\n                        hideDownload: true,\n                        hidePrint: true,\n                    });\n                }\n            },\n            () => [this.iframeViewerPdfRef.el]\n        );\n    }\n\n    get urlFile() {\n        return (\n            this.state.objectUrl ||\n            url(\"/web/content\", {\n                model: this.props.record.resModel,\n                field: this.props.name,\n                id: this.props.record.resId,\n            })\n        );\n    }\n\n    get url() {\n        if (!this.state.isValid || !this.props.record.data[this.props.name]) {\n            return null;\n        }\n        const page = this.props.record.data[`${this.props.name}_page`] || 1;\n        const file = encodeURIComponent(this.urlFile);\n        return `/web/static/lib/pdfjs/web/viewer.html?file=${file}#page=${page}`;\n    }\n\n    update({ data }) {\n        const changes = { [this.props.name]: data || false };\n        return this.props.record.update(changes);\n    }\n\n    onFileRemove() {\n        this.state.isValid = true;\n        this.update({});\n    }\n\n    onFileDownload() {\n        this.action.doAction({\n            type: \"ir.actions.act_url\",\n            url: this.urlFile,\n            target: \"new\",\n        });\n    }\n\n    onFileUploaded({ data, objectUrl }) {\n        this.state.isValid = true;\n        this.state.objectUrl = objectUrl;\n        this.update({ data });\n    }\n\n    onLoadFailed() {\n        this.state.isValid = false;\n        this.notification.add(_t(\"Could not display the selected pdf\"), {\n            type: \"danger\",\n        });\n    }\n}\n\nexport const pdfViewerField = {\n    component: PdfViewerField,\n    displayName: _t(\"PDF Viewer\"),\n    supportedOptions: [\n        {\n            label: _t(\"Preview image\"),\n            name: \"preview_image\",\n            type: \"field\",\n            availableTypes: [\"binary\"],\n        },\n    ],\n    supportedTypes: [\"binary\"],\n};\n\nregistry.category(\"fields\").add(\"pdf_viewer\", pdfViewerField);\n", "import { registry } from \"@web/core/registry\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { formatFloat } from \"../formatters\";\nimport { standardFieldProps } from \"../standard_field_props\";\n\nimport { Component } from \"@odoo/owl\";\n\nexport class PercentPieField extends Component {\n    static template = \"web.PercentPieField\";\n    static props = {\n        ...standardFieldProps,\n        string: { type: String, optional: true },\n    };\n\n    /**\n     * Format to 2 decimals without trailing zeros.\n     */\n    get formattedValue() {\n        return formatFloat(this.props.record.data[this.props.name], {\n            trailingZeros: false,\n        });\n    }\n}\n\nexport const percentPieField = {\n    component: PercentPieField,\n    displayName: _t(\"PercentPie\"),\n    supportedTypes: [\"float\", \"integer\"],\n    additionalClasses: [\"o_field_percent_pie\"],\n    extractProps: ({ string }) => ({ string }),\n};\n\nregistry.category(\"fields\").add(\"percentpie\", percentPieField);\n", "import { registry } from \"@web/core/registry\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { formatPercentage } from \"../formatters\";\nimport { parsePercentage } from \"../parsers\";\nimport { useInputField } from \"../input_field_hook\";\nimport { useNumpadDecimal } from \"../numpad_decimal_hook\";\nimport { standardFieldProps } from \"../standard_field_props\";\n\nimport { Component } from \"@odoo/owl\";\n\nexport class PercentageField extends Component {\n    static template = \"web.PercentageField\";\n    static props = {\n        ...standardFieldProps,\n        digits: { type: Array, optional: true },\n    };\n\n    setup() {\n        useInputField({\n            getValue: () =>\n                formatPercentage(this.props.record.data[this.props.name], {\n                    digits: this.props.digits,\n                    noSymbol: true,\n                    field: this.props.record.fields[this.props.name],\n                }),\n            refName: \"numpadDecimal\",\n            parse: (v) => parsePercentage(v),\n        });\n        useNumpadDecimal();\n    }\n\n    get formattedValue() {\n        return formatPercentage(this.props.record.data[this.props.name], {\n            digits: this.props.digits,\n            field: this.props.record.fields[this.props.name],\n        });\n    }\n}\n\nexport const percentageField = {\n    component: PercentageField,\n    displayName: _t(\"Percentage\"),\n    supportedTypes: [\"integer\", \"float\"],\n    extractProps: ({ attrs, options }) => {\n        // Sadly, digits param was available as an option and an attr.\n        // The option version could be removed with some xml refactoring.\n        let digits;\n        if (attrs.digits) {\n            digits = JSON.parse(attrs.digits);\n        } else if (options.digits) {\n            digits = options.digits;\n        }\n\n        return {\n            digits,\n        };\n    },\n};\n\nregistry.category(\"fields\").add(\"percentage\", percentageField);\n", "import { registry } from \"@web/core/registry\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { useInputField } from \"../input_field_hook\";\nimport { standardFieldProps } from \"../standard_field_props\";\n\nimport { Component } from \"@odoo/owl\";\n\nexport class PhoneField extends Component {\n    static template = \"web.PhoneField\";\n    static props = {\n        ...standardFieldProps,\n        placeholder: { type: String, optional: true },\n    };\n\n    setup() {\n        useInputField({ getValue: () => this.props.record.data[this.props.name] || \"\" });\n    }\n    get phoneHref() {\n        return \"tel:\" + this.props.record.data[this.props.name].replace(/\\s+/g, \"\");\n    }\n}\n\nexport const phoneField = {\n    component: PhoneField,\n    displayName: _t(\"Phone\"),\n    supportedOptions: [\n        {\n            label: _t(\"Dynamic Placeholder\"),\n            name: \"placeholder_field\",\n            type: \"field\",\n            availableTypes: [\"char\"],\n        },\n    ],\n    supportedTypes: [\"char\"],\n    extractProps: ({ placeholder }) => ({\n        placeholder,\n    }),\n};\n\nregistry.category(\"fields\").add(\"phone\", phoneField);\n\nclass FormPhoneField extends PhoneField {\n    static template = \"web.FormPhoneField\";\n}\n\nexport const formPhoneField = {\n    ...phoneField,\n    component: FormPhoneField,\n};\n\nregistry.category(\"fields\").add(\"form.phone\", formPhoneField);\n", "import { useCommand } from \"@web/core/commands/command_hook\";\nimport { registry } from \"@web/core/registry\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { standardFieldProps } from \"../standard_field_props\";\n\nimport { Component, useState } from \"@odoo/owl\";\n\nexport class PriorityField extends Component {\n    static template = \"web.PriorityField\";\n    static props = {\n        ...standardFieldProps,\n        withCommand: { type: Boolean, optional: true },\n        autosave: { type: Boolean, optional: true },\n    };\n\n    setup() {\n        this.state = useState({\n            index: -1,\n        });\n        if (this.props.withCommand) {\n            for (const command of this.commands) {\n                useCommand(...command);\n            }\n        }\n    }\n\n    get commands() {\n        const commandName = _t(\"Set priority...\");\n        return [\n            [\n                commandName,\n                () => {\n                    return {\n                        placeholder: commandName,\n                        providers: [\n                            {\n                                provide: () =>\n                                    this.options.map((value) => ({\n                                        name: value[1],\n                                        action: () => {\n                                            this.updateRecord(value[0]);\n                                        },\n                                    })),\n                            },\n                        ],\n                    };\n                },\n                { category: \"smart_action\", hotkey: \"alt+r\" },\n            ],\n        ];\n    }\n\n    get tooltipLabel() {\n        return this.props.record.fields[this.props.name].string;\n    }\n    get options() {\n        return Array.from(this.props.record.fields[this.props.name].selection);\n    }\n    get index() {\n        return this.state.index > -1\n            ? this.state.index\n            : this.options.findIndex((o) => o[0] === this.props.record.data[this.props.name]);\n    }\n\n    getTooltip(value) {\n        return this.tooltipLabel && this.tooltipLabel !== value\n            ? `${this.tooltipLabel}: ${value}`\n            : value;\n    }\n    /**\n     * @param {string} value\n     */\n    onStarClicked(value) {\n        if (this.props.record.data[this.props.name] === value) {\n            this.state.index = -1;\n            this.updateRecord(this.options[0][0]);\n        } else {\n            this.updateRecord(value);\n        }\n    }\n\n    async updateRecord(value) {\n        await this.props.record.update({ [this.props.name]: value }, { save: this.props.autosave });\n    }\n}\n\nexport const priorityField = {\n    component: PriorityField,\n    displayName: _t(\"Priority\"),\n    supportedOptions: [\n        {\n            label: _t(\"Autosave\"),\n            name: \"autosave\",\n            type: \"boolean\",\n            default: true,\n            help: _t(\n                \"If checked, the record will be saved immediately when the field is modified.\"\n            ),\n        },\n    ],\n    supportedTypes: [\"selection\"],\n    extractProps({ options, viewType }, dynamicInfo) {\n        return {\n            withCommand: viewType === \"form\",\n            readonly: dynamicInfo.readonly,\n            autosave: \"autosave\" in options ? !!options.autosave : true,\n        };\n    },\n};\n\nregistry.category(\"fields\").add(\"priority\", priorityField);\n", "import { registry } from \"@web/core/registry\";\nimport { progressBarField, ProgressBarField } from \"./progress_bar_field\";\n\nexport class KanbanProgressBarField extends ProgressBarField {\n    get isEditable() {\n        return this.props.isEditable;\n    }\n}\n\nexport const kanbanProgressBarField = {\n    ...progressBarField,\n    component: KanbanProgressBarField,\n};\n\nregistry.category(\"fields\").add(\"kanban.progressbar\", kanbanProgressBarField);\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { useNumpadDecimal } from \"../numpad_decimal_hook\";\nimport { parseFloat } from \"../parsers\";\nimport { useInputField } from \"@web/views/fields/input_field_hook\";\nimport { standardFieldProps } from \"../standard_field_props\";\n\nimport { Component, useRef, useState } from \"@odoo/owl\";\nconst formatters = registry.category(\"formatters\");\n\nexport class ProgressBarField extends Component {\n    static template = \"web.ProgressBarField\";\n    static props = {\n        ...standardFieldProps,\n        maxValueField: { type: [String, Number], optional: true },\n        currentValueField: { type: String, optional: true },\n        isEditable: { type: Boolean, optional: true },\n        isCurrentValueEditable: { type: Boolean, optional: true },\n        isMaxValueEditable: { type: Boolean, optional: true },\n        title: { type: String, optional: true },\n        overflowClass: { type: String, optional: true },\n    };\n\n    setup() {\n        useNumpadDecimal();\n        this.root = useRef(\"numpadDecimal\");\n\n        const { currentValueField, maxValueField, name } = this.props;\n        this.currentValueField = currentValueField ? currentValueField : name;\n        if (maxValueField) {\n            this.maxValueField = maxValueField;\n        }\n        this.currentValueRef = useInputField({\n            getValue: () => this.formatCurrentValue(),\n            parse: (v) => this.parseCurrentValue(v),\n            refName: \"currentValue\",\n            fieldName: this.currentValueField,\n            shouldSave: () => this.props.readonly,\n        });\n        this.maxValueRef = useInputField({\n            getValue: () => this.formatMaxValue(),\n            parse: (v) => this.parseMaxValue(v),\n            refName: \"maxValue\",\n            fieldName: this.maxValueField,\n            shouldSave: () => this.props.readonly,\n        });\n\n        this.state = useState({\n            isEditing: false,\n        });\n    }\n\n    get isEditable() {\n        return this.props.isEditable && !this.props.readonly;\n    }\n    get isPercentage() {\n        return !this.props.maxValueField || !isNaN(this.props.maxValueField);\n    }\n\n    get currentValue() {\n        return this.props.record.data[this.currentValueField] || 0;\n    }\n\n    get maxValue() {\n        return this.props.record.data[this.maxValueField] || 100;\n    }\n\n    get progressBarColorClass() {\n        return this.currentValue > this.maxValue ? this.props.overflowClass : \"bg-primary\";\n    }\n\n    formatCurrentValue(humanReadable = !this.state.isEditing) {\n        const formatter = formatters.get(this.props.record.fields[this.currentValueField].type);\n        return formatter(this.currentValue, { humanReadable });\n    }\n\n    formatMaxValue(humanReadable = !this.state.isEditing) {\n        const formatter = formatters.get(this.props.record.fields[this.maxValueField]?.type ?? \"integer\");\n        return formatter(this.maxValue, { humanReadable });\n    }\n\n    parseCurrentValue(value) {\n        let parsedValue = parseFloat(value);\n        if (this.props.record.fields[this.currentValueField].type === \"integer\") {\n            parsedValue = Math.floor(parsedValue);\n        }\n        return parsedValue;\n    }\n\n    parseMaxValue(value) {\n        let parsedValue = parseFloat(value);\n        if (this.props.record.fields[this.maxValueField].type === \"integer\") {\n            parsedValue = Math.floor(parsedValue);\n        }\n        return parsedValue;\n    }\n\n    onInputBlur() {\n        if (\n            document.activeElement !== this.maxValueRef.el &&\n            document.activeElement !== this.currentValueRef.el\n        ) {\n            this.state.isEditing = false;\n        }\n    }\n    onInputFocus() {\n        this.state.isEditing = true;\n    }\n}\n\nexport const progressBarField = {\n    component: ProgressBarField,\n    displayName: _t(\"Progress Bar\"),\n    supportedOptions: [\n        {\n            label: _t(\"Can edit value\"),\n            name: \"editable\",\n            type: \"boolean\",\n        },\n        {\n            label: _t(\"Can edit max value\"),\n            name: \"edit_max_value\",\n            type: \"boolean\",\n        },\n        {\n            label: _t(\"Current value field\"),\n            name: \"current_value\",\n            type: \"field\",\n            availableTypes: [\"integer\", \"float\"],\n            help: _t(\n                \"Use to override the display value (e.g. if your progress bar is a computed percentage but you want to display the actual field value instead).\"\n            ),\n        },\n        {\n            label: _t(\"Max value field\"),\n            name: \"max_value\",\n            type: \"field\",\n            availableTypes: [\"integer\", \"float\"],\n            help: _t(\n                \"Field that holds the maximum value of the progress bar. If set, will be displayed next to the progress bar (e.g. 10 / 200).\"\n            ),\n        },\n        {\n            label: _t(\"Overflow style\"),\n            name: \"overflow_class\",\n            type: \"string\",\n            availableTypes: [\"integer\", \"float\"],\n            help: _t(\n                \"Bootstrap classname to customize the style of the progress bar when the maximum value is exceeded\"\n            ),\n            default: \"bg-secondary\",\n        },\n    ],\n    supportedTypes: [\"integer\", \"float\"],\n    extractProps: ({ attrs, options }) => ({\n        maxValueField: options.max_value,\n        currentValueField: options.current_value,\n        isEditable: !options.readonly && options.editable,\n        isCurrentValueEditable: options.editable && !options.edit_max_value,\n        isMaxValueEditable: options.editable && options.edit_max_value,\n        title: attrs.title,\n        overflowClass: options.overflow_class || \"bg-secondary\",\n    }),\n};\n\nregistry.category(\"fields\").add(\"progressbar\", progressBarField);\n", "import { registry } from \"@web/core/registry\";\nimport { propertiesField, PropertiesField } from \"./properties_field\";\n\nexport class CalendarPropertiesField extends PropertiesField {\n    static template = \"web.CalendarPropertiesField\";\n    async checkDefinitionWriteAccess() {\n        return false;\n    }\n}\n\nexport const calendarPropertiesField = {\n    ...propertiesField,\n    component: CalendarPropertiesField,\n};\n\nregistry.category(\"fields\").add(\"calendar.properties\", calendarPropertiesField);\n", "import { registry } from \"@web/core/registry\";\nimport { propertiesField, PropertiesField } from \"./properties_field\";\n\nexport class CardPropertiesField extends PropertiesField {\n    static template = \"web.CardPropertiesField\";\n\n    async checkDefinitionWriteAccess() {\n        return false;\n    }\n}\n\nexport const cardPropertiesField = {\n    ...propertiesField,\n    component: CardPropertiesField,\n};\n\nregistry.category(\"fields\").add(\"kanban.properties\", cardPropertiesField);\nregistry.category(\"fields\").add(\"hierarchy.properties\", cardPropertiesField);\n", "import { ConfirmationDialog } from \"@web/core/confirmation_dialog/confirmation_dialog\";\nimport { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { usePopover } from \"@web/core/popover/popover_hook\";\nimport { reposition } from \"@web/core/position/utils\";\nimport { registry } from \"@web/core/registry\";\nimport { user } from \"@web/core/user\";\nimport { useBus, useService } from \"@web/core/utils/hooks\";\nimport { useSortable } from \"@web/core/utils/sortable_owl\";\nimport { exprToBoolean, uuid } from \"@web/core/utils/strings\";\nimport { useRecordObserver } from \"@web/model/relational_model/utils\";\nimport { standardFieldProps } from \"../standard_field_props\";\nimport { PropertyDefinition } from \"./property_definition\";\nimport { PropertyValue } from \"./property_value\";\n\nimport { Component, onWillStart, onWillUpdateProps, useEffect, useRef, useState } from \"@odoo/owl\";\n\nexport class PropertiesField extends Component {\n    static template = \"web.PropertiesField\";\n    static components = {\n        Dropdown,\n        DropdownItem,\n        PropertyDefinition,\n        PropertyValue,\n    };\n    static props = {\n        ...standardFieldProps,\n        context: { type: Object, optional: true },\n        columns: {\n            type: Number,\n            optional: true,\n            validate: (columns) => [1, 2].includes(columns),\n        },\n        editMode: { type: Boolean, optional: true },\n    };\n\n    setup() {\n        this.notification = useService(\"notification\");\n        this.orm = useService(\"orm\");\n        this.dialogService = useService(\"dialog\");\n        this.popover = usePopover(PropertyDefinition, {\n            closeOnClickAway: this.checkPopoverClose,\n            popoverClass: \"o_property_field_popover\",\n            position: \"right\",\n            onClose: () => this.onCloseCurrentPopover?.(),\n            fixedPosition: true,\n            arrow: false,\n            setActiveElement: false, // make tag navigation work when adding a tag property\n        });\n        this.propertiesRef = useRef(\"properties\");\n\n        let currentResId;\n        useRecordObserver((record) => {\n            if (currentResId !== record.resId) {\n                currentResId = record.resId;\n                this._saveInitialPropertiesValues();\n            }\n        });\n\n        const field = this.props.record.fields[this.props.name];\n        this.definitionRecordField = field.definition_record;\n\n        this.state = useState({\n            canChangeDefinition: false,\n            isInEditMode: false,\n            movedPropertyName: null,\n        });\n\n        // Properties can be added from the cog menu of the form controller\n        if (this.env.config?.viewType === \"form\") {\n            useBus(this.env.model.bus, \"PROPERTY_FIELD:EDIT\", async () => {\n                if (this.props.readonly || this.state.isInEditMode) {\n                    return;\n                }\n                let canChangeDefinition = this.state.canChangeDefinition;\n                if (!canChangeDefinition) {\n                    canChangeDefinition = await this.checkDefinitionWriteAccess();\n                    if (!canChangeDefinition) {\n                        this.notification.add(this._getPropertyEditWarningText(), {\n                            type: \"warning\",\n                        });\n                    }\n                }\n                const isInEditMode = canChangeDefinition && !this.props.readonly;\n                this.state.canChangeDefinition = !!canChangeDefinition;\n                this.state.isInEditMode = isInEditMode;\n                if (isInEditMode && this.propertiesList.length === 0) {\n                    this.onPropertyCreate();\n                }\n            });\n        }\n\n        onWillStart(async () => {\n            if (this.props.readonly || !this.props.editMode) {\n                return;\n            }\n            this.checkDefinitionWriteAccess().then((canChangeDefinition) => {\n                if (canChangeDefinition) {\n                    this.state.canChangeDefinition = true;\n                    this.state.isInEditMode = !this.props.readonly;\n                }\n            });\n        });\n\n        useEffect(\n            () => {\n                // when the field has a new definition record:\n                if (this.props.readonly || (!this.state.isInEditMode && !this.props.editMode)) {\n                    return;\n                }\n                this.checkDefinitionWriteAccess().then((canChangeDefinition) => {\n                    this.state.canChangeDefinition = !!canChangeDefinition;\n                    this.state.isInEditMode =\n                        canChangeDefinition &&\n                        !this.props.readonly &&\n                        (this.state.isInEditMode || this.props.editMode);\n                });\n            },\n            () => [this.props.record.data[this.definitionRecordField]]\n        );\n\n        onWillUpdateProps(async (nextProps) => {\n            if (nextProps.readonly && !this.props.readonly) {\n                this.state.isInEditMode = false;\n            }\n            if (\n                !nextProps.readonly &&\n                (this.props.readonly || (nextProps.editMode && !this.props.editMode))\n            ) {\n                let canChangeDefinition = this.state.canChangeDefinition;\n                if (!canChangeDefinition) {\n                    canChangeDefinition = await this.checkDefinitionWriteAccess();\n                }\n                this.state.canChangeDefinition = !!canChangeDefinition;\n                this.state.isInEditMode =\n                    canChangeDefinition &&\n                    !nextProps.readonly &&\n                    (this.state.isInEditMode || nextProps.editMode);\n            }\n        });\n\n        useEffect(\n            () => {\n                if (this.openPropertyDefinition) {\n                    const propertyName = this.openPropertyDefinition;\n                    const labels = this.propertiesRef.el.querySelectorAll(\n                        `.o_property_field[property-name=\"${propertyName}\"] .o_field_property_open_popover`\n                    );\n                    this.openPropertyDefinition = null;\n                    const lastLabel = labels[labels.length - 1];\n                    this._openPropertyDefinition(lastLabel, propertyName, true);\n                }\n            },\n            () => [this.openPropertyDefinition]\n        );\n\n        useEffect(() => this._movePopoverIfNeeded());\n\n        // sort properties\n        useSortable({\n            enable: () => !this.props.readonly && this.state.canChangeDefinition,\n            ref: this.propertiesRef,\n            handle: \".o_field_property_label .oi-draggable\",\n            // on mono-column layout, allow to move before a separator to make the usage more fluid\n            elements:\n                this.renderedColumnsCount === 1\n                    ? \"*:is(.o_property_field, .o_field_property_group_label)\"\n                    : \".o_property_field\",\n            groups: \".o_property_group\",\n            connectGroups: true,\n            cursor: \"grabbing\",\n            onDragStart: ({ element, group }) => {\n                this.propertiesRef.el.classList.add(\"o_property_dragging\");\n                element.classList.add(\"o_property_drag_item\");\n                group.classList.add(\"o_property_drag_group\");\n                // without this, if we edit a char property, move it,\n                // the change will be reset when we drop the property\n                document.activeElement.blur();\n            },\n            onDrop: async ({ parent, element, next, previous }) => {\n                const from = element.getAttribute(\"property-name\");\n                let to = previous && previous.getAttribute(\"property-name\");\n                let moveBefore = false;\n                if (!to && next) {\n                    // we move the element at the first position inside a group\n                    // or at the first position of a column\n                    if (next.classList.contains(\"o_field_property_group_label\")) {\n                        // mono-column layout, move before the separator\n                        next = next.closest(\".o_property_group\");\n                    }\n                    to = next.getAttribute(\"property-name\");\n                    moveBefore = !!to;\n                }\n                if (!to) {\n                    // we move in an empty group or outside of the DOM element\n                    // move the element at the end of the group\n                    const groupName = parent.getAttribute(\"property-name\");\n                    const group = this.groupedPropertiesList.find(\n                        (group) => group.name === groupName\n                    );\n                    if (!group) {\n                        to = null;\n                        moveBefore = false;\n                    } else {\n                        to = group.elements.length ? group.elements.at(-1).name : groupName;\n                    }\n                }\n                await this.onPropertyMoveTo(from, to, moveBefore);\n            },\n            onDragEnd: ({ element }) => {\n                this.propertiesRef.el.classList.remove(\"o_property_dragging\");\n                element.classList.remove(\"o_property_drag_item\");\n                const targetGroup = this.propertiesRef.el.querySelector(\".o_property_drag_group\");\n                if (targetGroup) {\n                    targetGroup.classList.remove(\"o_property_drag_group\");\n                }\n            },\n            onGroupEnter: ({ group }) => {\n                group.classList.add(\"o_property_drag_group\");\n                this._toggleSeparators([group.getAttribute(\"property-name\")], false);\n            },\n            onGroupLeave: ({ group }) => {\n                group.classList.remove(\"o_property_drag_group\");\n            },\n        });\n\n        // sort group of properties\n        useSortable({\n            enable: () => !this.props.readonly && this.state.canChangeDefinition,\n            ref: this.propertiesRef,\n            handle: \".o_field_property_group_label .oi-draggable\",\n            elements: \".o_property_group:not([property-name=''])\",\n            cursor: \"grabbing\",\n            onDragStart: ({ element }) => {\n                this.propertiesRef.el.classList.add(\"o_property_dragging\");\n                element.classList.add(\"o_property_drag_item\");\n                document.activeElement.blur();\n            },\n            onDrop: async ({ element, previous }) => {\n                const from = element.getAttribute(\"property-name\");\n                const to = previous && previous.getAttribute(\"property-name\");\n                await this.onGroupMoveTo(from, to);\n            },\n            onDragEnd: ({ element }) => {\n                this.propertiesRef.el.classList.remove(\"o_property_dragging\");\n                element.classList.remove(\"o_property_drag_item\");\n            },\n        });\n    }\n\n    /* --------------------------------------------------------\n     * Public methods / Getters\n     * -------------------------------------------------------- */\n\n    /**\n     * Return the number of columns we have to render\n     * (The properties can be split in many column,\n     * to follow the layout of the form view)\n     *\n     * @returns {object}\n     */\n    get renderedColumnsCount() {\n        return this.env.isSmall ? 1 : this.props.columns;\n    }\n\n    /**\n     * Return the current properties value.\n     *\n     * Make a deep copy of this properties values, so when we will modify it\n     * in the events, we won't re-use same object (can lead to issue, e.g. if we\n     * discard a form view, we should be able to restore the old props).\n     *\n     * @returns {array}\n     */\n    get propertiesList() {\n        return (this.props.record.data[this.props.name] || [])\n            .filter((definition) => !definition.definition_deleted)\n            .map((definition) => ({ ...definition }));\n    }\n\n    // for overrides\n    get additionalPropertyDefinitionProps() {\n        return {};\n    }\n\n    /**\n     * Return the current properties value splitted in multiple groups/columns.\n     * Each properties are splitted in groups, thanks to the separators, and\n     * groups are splitted in columns (the columns property is the number of groups\n     * we have on a row).\n     *\n     * The groups are created with the separators (special type of property) so\n     * the order mater in the group creation.\n     *\n     * @returns {Array<Array>}\n     */\n    get groupedPropertiesList() {\n        const propertiesList = this.propertiesList;\n        // default invisible group\n        const groupedProperties =\n            propertiesList[0]?.type !== \"separator\"\n                ? [{ title: null, name: null, elements: [], invisibleLabel: true }]\n                : [];\n\n        propertiesList.forEach((property) => {\n            if (property.type === \"separator\") {\n                groupedProperties.push({\n                    title: property.string,\n                    name: property.name,\n                    elements: [],\n                    isFolded: property.value ?? property.fold_by_default,\n                });\n            } else {\n                groupedProperties.at(-1).elements.push(property);\n            }\n        });\n\n        if (groupedProperties.length === 1) {\n            // only one group, split this group in the columns to take the entire width\n            const invisibleLabel = propertiesList[0]?.type !== \"separator\";\n            groupedProperties[0].elements = [];\n            groupedProperties[0].invisibleLabel = invisibleLabel;\n            for (let col = 1; col < this.renderedColumnsCount; ++col) {\n                groupedProperties.push({\n                    title: null,\n                    name: null,\n                    columnSeparator: true,\n                    elements: [],\n                    invisibleLabel: true,\n                });\n            }\n            const properties = propertiesList.filter((property) => property.type !== \"separator\");\n            properties.forEach((property, index) => {\n                const columnIndex = Math.floor(\n                    (index * this.renderedColumnsCount) / properties.length\n                );\n                groupedProperties[columnIndex].elements.push(property);\n            });\n        }\n\n        return groupedProperties;\n    }\n\n    /**\n     * Return the id of the definition record.\n     *\n     * @returns {integer}\n     */\n    get definitionRecordId() {\n        return this.props.record.data[this.definitionRecordField].id;\n    }\n\n    /**\n     * Return the model of the definition record.\n     *\n     * @returns {string}\n     */\n    get definitionRecordModel() {\n        return this.props.record.fields[this.definitionRecordField].relation;\n    }\n\n    /**\n     * Return true if we should close the popover containing the\n     * properties definition based on the target received.\n     *\n     * If we edit the datetime, it will open a popover with the date picker\n     * component, but this component won't be a child of the current popover.\n     * So when we will click on it to select a date, it will close the definition\n     * popover. It's the same for other similar components (many2one modal, etc).\n     *\n     * @param {HTMLElement} target\n     * @returns {boolean}\n     */\n    checkPopoverClose(target) {\n        if (target.closest(\".o_datetime_picker\")) {\n            // selected a datetime, do not close the definition popover\n            return false;\n        }\n\n        if (target.closest(\".modal\")) {\n            // close a many2one modal\n            return false;\n        }\n\n        if (target.closest(\".o_tag_popover\")) {\n            // tag color popover\n            return false;\n        }\n\n        if (target.closest(\".o_model_field_selector_popover\")) {\n            // domain selector\n            return false;\n        }\n\n        return true;\n    }\n\n    /**\n     * Generate an unique ID to be used in the DOM.\n     *\n     * @returns {string}\n     */\n    generateUniqueDomID() {\n        return `property_${uuid()}`;\n    }\n\n    /**\n     * Generate a new property name.\n     *\n     * @returns {string}\n     */\n    generatePropertyName(propertyType) {\n        let name = uuid();\n        if (propertyType === \"html\") {\n            name = `${name}_html`;\n        }\n        return name;\n    }\n\n    /* --------------------------------------------------------\n     * Event handlers\n     * -------------------------------------------------------- */\n\n    /**\n     * Move the given property up or down in the list.\n     *\n     * @param {string} propertyName\n     * @param {string} direction, either \"up\" or \"down\"\n     */\n    async onPropertyMove(propertyName, direction) {\n        const propertiesValues = this.propertiesList || [];\n        const propertyIndex = propertiesValues.findIndex(\n            (property) => property.name === propertyName\n        );\n\n        const targetIndex = propertyIndex + (direction === \"down\" ? 1 : -1);\n        if (targetIndex < 0 || targetIndex >= propertiesValues.length) {\n            this.notification.add(\n                direction === \"down\"\n                    ? _t(\"This field is already last\")\n                    : _t(\"This field is already first\"),\n                { type: \"warning\" }\n            );\n            return;\n        }\n        this.state.movedPropertyName = propertyName;\n\n        const prop = propertiesValues[targetIndex];\n        propertiesValues[targetIndex] = propertiesValues[propertyIndex];\n        propertiesValues[propertyIndex] = prop;\n        propertiesValues[propertyIndex].definition_changed = true;\n\n        await this.props.record.update({ [this.props.name]: propertiesValues });\n        await this._unfoldPropertyGroup(targetIndex, propertiesValues);\n\n        // move the popover once the DOM is updated\n        this.movePopoverToProperty = propertyName;\n    }\n\n    /**\n     * Move a property after the target property.\n     *\n     * @param {string} propertyName\n     * @param {string} toPropertyName, the target property\n     *  (null if we move the property to the first index)\n     */\n    async onPropertyMoveTo(propertyName, toPropertyName, moveBefore) {\n        const propertiesValues = this.propertiesList || [];\n\n        let fromIndex = propertiesValues.findIndex((property) => property.name === propertyName);\n        let toIndex = propertiesValues.findIndex((property) => property.name === toPropertyName);\n        const columnSize = Math.ceil(propertiesValues.length / this.renderedColumnsCount);\n\n        // if we have no separator at first, we might want to create some\n        // to keep the initial column separation (only if needed, if we move properties\n        // inside the same column we do nothing)\n        if (\n            this.renderedColumnsCount > 1 &&\n            !propertiesValues.some((p, index) => index !== 0 && p.type === \"separator\") &&\n            Math.floor(fromIndex / columnSize) !== Math.floor(toIndex / columnSize)\n        ) {\n            const newSeparators = [];\n            for (let col = 0; col < this.renderedColumnsCount; ++col) {\n                const separatorIndex = columnSize * col + newSeparators.length;\n\n                if (propertiesValues[separatorIndex]?.type === \"separator\") {\n                    newSeparators.push(propertiesValues[separatorIndex].name);\n                    continue;\n                }\n                const newSeparator = {\n                    type: \"separator\",\n                    string: _t(\"Group %s\", col + 1),\n                    name: this.generatePropertyName(\"separator\"),\n                };\n                newSeparators.push(newSeparator.name);\n                propertiesValues.splice(separatorIndex, 0, newSeparator);\n            }\n            await this._toggleSeparators(newSeparators, false);\n            toPropertyName = toPropertyName || propertiesValues.at(-1).name;\n\n            // indexes might have changed\n            fromIndex = propertiesValues.findIndex((property) => property.name === propertyName);\n            toIndex = propertiesValues.findIndex((property) => property.name === toPropertyName);\n        }\n\n        if (moveBefore) {\n            toIndex--;\n        }\n        if (toIndex < fromIndex) {\n            // the first splice operation will change the index\n            toIndex++;\n        }\n        propertiesValues.splice(toIndex, 0, propertiesValues.splice(fromIndex, 1)[0]);\n        propertiesValues[0].definition_changed = true;\n        this.props.record.update({ [this.props.name]: propertiesValues });\n    }\n\n    /**\n     * Move a group of properties after the target group.\n     *\n     * @param {string} propertyName\n     * @param {string} toPropertyName, the target group (separator)\n     *  (null if we move the group to the first index)\n     */\n    onGroupMoveTo(propertyName, toPropertyName) {\n        const propertiesValues = this.propertiesList || [];\n        const fromIndex = propertiesValues.findIndex((property) => property.name === propertyName);\n        const toIndex = propertiesValues.findIndex((property) => property.name === toPropertyName);\n        if (\n            propertiesValues[fromIndex].type !== \"separator\" ||\n            (toIndex >= 0 && propertiesValues[toIndex].type !== \"separator\")\n        ) {\n            throw new Error(\"Something went wrong\");\n        }\n\n        // find the next separator index\n        const getNextSeparatorIndex = (startIndex) => {\n            const nextSeparatorIndex = propertiesValues.findIndex(\n                (property, index) => property.type === \"separator\" && index > startIndex\n            );\n            return nextSeparatorIndex < 0 ? propertiesValues.length : nextSeparatorIndex;\n        };\n        const groupSize = getNextSeparatorIndex(fromIndex) - fromIndex;\n        let targetIndex = getNextSeparatorIndex(toIndex);\n        if (targetIndex > fromIndex) {\n            // the size of the array will change after the first splice\n            // so we need to correct the index\n            targetIndex -= groupSize;\n        }\n        propertiesValues.splice(targetIndex, 0, ...propertiesValues.splice(fromIndex, groupSize));\n        propertiesValues[0].definition_changed = true;\n        this.props.record.update({ [this.props.name]: propertiesValues });\n    }\n\n    /**\n     * The value / definition of the given property has been changed.\n     * `propertyValue` contains the definition of the property with the value.\n     *\n     * @param {string} propertyName\n     * @param {object} propertyValue\n     */\n    onPropertyValueChange(propertyName, propertyValue) {\n        const propertiesValues = this.propertiesList;\n        propertiesValues.find((property) => property.name === propertyName).value = propertyValue;\n        this.props.record.update({ [this.props.name]: propertiesValues });\n    }\n\n    /**\n     * Check if the definition is not already opened\n     * and if it's not the case, open the popover with the property definition.\n     *\n     * @param {event} event\n     * @param {string} propertyName\n     */\n    async onPropertyEdit(event, propertyName) {\n        event.stopPropagation();\n        event.preventDefault();\n\n        if (event.target.classList.contains(\"disabled\")) {\n            // remove the glitch if we click on the edit button\n            // while the popover is already opened\n            return;\n        }\n\n        event.target.classList.add(\"disabled\");\n        this._openPropertyDefinition(event.target, propertyName, false);\n    }\n\n    /**\n     * The property definition or value has been changed.\n     *\n     * @param {object} propertyDefinition\n     */\n    async onPropertyDefinitionChange(propertyDefinition) {\n        propertyDefinition[\"definition_changed\"] = true;\n        if (propertyDefinition.type === \"separator\") {\n            // remove all other keys\n            const separatorKeys = new Set([\n                \"definition_changed\",\n                \"fold_by_default\",\n                \"name\",\n                \"string\",\n                \"type\",\n                \"value\",\n            ]);\n            // remove all other keys in place, since propertyDefinition instance\n            // will be used as a PropertyDefinition component state value.\n            for (const key in propertyDefinition) {\n                if (!separatorKeys.has(key)) {\n                    delete propertyDefinition[key];\n                }\n            }\n        }\n        const propertiesValues = this.propertiesList;\n        const propertyIndex = this._getPropertyIndex(propertyDefinition.name);\n\n        const oldType = propertiesValues[propertyIndex].type;\n        const newType = propertyDefinition.type;\n\n        this._regeneratePropertyName(propertyDefinition, propertiesValues[propertyIndex]);\n\n        propertiesValues[propertyIndex] = propertyDefinition;\n        await this.props.record.update({ [this.props.name]: propertiesValues });\n\n        if (newType === \"separator\" && oldType !== \"separator\") {\n            // unfold automatically the new separator\n            await this._toggleSeparators([propertyDefinition.name], propertyDefinition.fold_by_default);\n            // layout has been changed, move the definition popover\n            this.movePopoverToProperty = propertyDefinition.name;\n        } else if (oldType === \"separator\" && newType !== \"separator\") {\n            // unfold automatically the previous separator\n            const previousSeperator = propertiesValues.findLast(\n                (property, index) => index < propertyIndex && property.type === \"separator\"\n            );\n            if (previousSeperator) {\n                await this._toggleSeparators([previousSeperator.name], propertyDefinition.fold_by_default);\n            }\n            // layout has been changed, move the definition popover\n            this.movePopoverToProperty = propertyDefinition.name;\n        }\n    }\n\n    /**\n     * Mark a property as \"to delete\".\n     *\n     * @param {string} propertyName\n     */\n    onPropertyDelete(propertyName) {\n        let message = _t(\"Are you sure you want to delete this property field?\") + \" \";\n        if (this.definitionRecordModel !== \"properties.base.definition\") {\n            const parentName = this.props.record.data[this.definitionRecordField].display_name;\n            const parentFieldLabel = this.props.record.fields[this.definitionRecordField].string;\n            message += _t(\n                'It will be removed for everyone using the \"%(parentName)s\" %(parentFieldLabel)s.',\n                { parentName, parentFieldLabel }\n            );\n        } else {\n            message += _t(\"It will be removed for everyone!\");\n        }\n        this.popover.close();\n        const dialogProps = {\n            title: _t(\"Delete Property Field\"),\n            body: message,\n            confirmLabel: _t(\"Delete Field\"),\n            cancelLabel: _t(\"Discard\"),\n            confirm: () => {\n                const propertiesDefinitions = this.propertiesList;\n                propertiesDefinitions.find(\n                    (property) => property.name === propertyName\n                ).definition_deleted = true;\n                this.props.record.update({ [this.props.name]: propertiesDefinitions });\n            },\n            cancel: () => {},\n        };\n        this.dialogService.add(ConfirmationDialog, dialogProps);\n    }\n\n    async onPropertyCreate() {\n        if (!this.definitionRecordId || !this.definitionRecordModel) {\n            this.notification.add(\n                _t(\n                    \"Oops! A %(parentFieldLabel)s is needed to add property fields.\",\n                    {\n                        parentFieldLabel:\n                            this.props.record.fields[this.definitionRecordField].string,\n                    },\n                    { type: \"warning\" }\n                )\n            );\n            return;\n        }\n        const propertiesDefinitions = this.propertiesList || [];\n\n        if (\n            propertiesDefinitions.length &&\n            propertiesDefinitions.some(\n                (prop) => prop.type !== \"separator\" && (!prop.string || !prop.string.length)\n            )\n        ) {\n            // do not allow to add new field until we set a label on the previous one\n            this.propertiesRef.el.closest(\".o_field_properties\").classList.add(\"o_field_invalid\");\n\n            this.notification.add(_t(\"Please complete your properties before adding a new one\"), {\n                type: \"warning\",\n            });\n            return;\n        }\n        const count = propertiesDefinitions.length;\n\n        this.propertiesRef.el.closest(\".o_field_properties\").classList.remove(\"o_field_invalid\");\n\n        const newName = this.generatePropertyName(\"char\");\n        propertiesDefinitions.push({\n            name: newName,\n            string: _t(\"Property %s\", count + 1),\n            type: \"char\",\n            definition_changed: true,\n        });\n        this.initialValues[newName] = { name: newName, type: \"char\" };\n        this.openPropertyDefinition = newName;\n        await this.props.record.update({ [this.props.name]: propertiesDefinitions });\n        await this._unfoldPropertyGroup(count - 1, propertiesDefinitions);\n    }\n\n    /**\n     * Fold / unfold the given separator property.\n     *\n     * @param {string} propertyName, Name of the separator property\n     */\n    onSeparatorClick(propertyName) {\n        if (propertyName) {\n            this._toggleSeparators([propertyName]);\n        }\n    }\n\n    /**\n     * Verify that we can write on properties, we can not change the definition\n     * if we don't have access for parent or if no parent is set.\n     */\n    async checkDefinitionWriteAccess() {\n        if (!this.definitionRecordId || !this.definitionRecordModel) {\n            return false;\n        }\n\n        return await user.checkAccessRight(\n            this.definitionRecordModel,\n            \"write\",\n            this.definitionRecordId\n        );\n    }\n\n    /**\n     * The tags list has been changed.\n     * If `newValue` is given, update the property value as well.\n     *\n     * @param {string} propertyName\n     * @param {array} newTags\n     * @param {array | null} newValue\n     */\n    onTagsChange(propertyName, newTags, newValue = null) {\n        const propertyDefinition = this.propertiesList.find(\n            (property) => property.name === propertyName\n        );\n        propertyDefinition.tags = newTags;\n        if (newValue !== null) {\n            propertyDefinition.value = newValue;\n        }\n        propertyDefinition.definition_changed = true;\n        this.onPropertyDefinitionChange(propertyDefinition);\n    }\n\n    /* --------------------------------------------------------\n     * Private methods\n     * -------------------------------------------------------- */\n\n    /**\n     * Switch the folded state of the given separators.\n     *\n     * @param {array} separatorNames, list of separator name to fold / unfold\n     * @param {boolean} [forceState] force the separator to be folded or open\n     */\n    _toggleSeparators(separatorNames, forceState) {\n        const propertiesValues = this.propertiesList;\n        for (const separatorName of separatorNames) {\n            const property = propertiesValues.find((prop) => prop.name === separatorName);\n            if (property) {\n                property.value = forceState ?? !(property.value ?? property.fold_by_default);\n            }\n        }\n        return this.props.record.update({ [this.props.name]: propertiesValues });\n    }\n\n    /**\n     * Move the popover to the given property id.\n     * Used when we change the position of the properties.\n     *\n     * We change the popover position after the DOM has been updated (see @useEffect)\n     * because if we update it after changing the component properties,\n     */\n    _movePopoverIfNeeded() {\n        if (!this.movePopoverToProperty) {\n            return;\n        }\n        const propertyName = this.movePopoverToProperty;\n        this.movePopoverToProperty = null;\n\n        const popover = document\n            .querySelector(\".o_field_property_definition\")\n            .closest(\".o_popover\");\n        const target = document.querySelector(\n            `*[property-name=\"${propertyName}\"] .o_field_property_open_popover`\n        );\n\n        reposition(popover, target, { position: \"top\", margin: 10 });\n    }\n\n    /**\n     * Regenerate a new name if needed or restore the original one.\n     * (see @_saveInitialPropertiesValues).\n     *\n     * If the type / model are the same, restore the original name to not reset the\n     * children otherwise, generate a new value so all value of the record are reset.\n     *\n     * @param {object} newDefinition\n     * @param {object} oldDefinition\n     */\n    _regeneratePropertyName(newDefinition, oldDefinition) {\n        const initialValues = this.initialValues[newDefinition.name];\n        if (\n            initialValues &&\n            newDefinition.type === initialValues.type &&\n            newDefinition.comodel === initialValues.comodel\n        ) {\n            // restore the original name (so the value on other records are not set to false)\n            newDefinition.name = initialValues.name;\n        } else if (\n            oldDefinition.type !== newDefinition.type ||\n            oldDefinition.model !== newDefinition.model\n        ) {\n            // Generate a new name to reset all values on other records.\n            // because the name has been changed on the definition,\n            // the old name on others record won't match the name on the definition\n            // and the python field will just ignore the old value.\n            // Store the new generated name to be able to restore it\n            // if needed.\n            const newName = this.generatePropertyName(newDefinition.type);\n            this.initialValues[newName] = initialValues;\n            newDefinition.name = newName;\n        }\n    }\n\n    /**\n     * Find the index of the given property in the list.\n     *\n     * Care about new name generation, if the name changed (because\n     * the type of the property, the model, etc changed), it will\n     * still find the index of the original property.\n     *\n     * @params {string} propertyName\n     * @returns {integer}\n     */\n    _getPropertyIndex(propertyName) {\n        const initialName = this.initialValues[propertyName]?.name || propertyName;\n        return this.propertiesList.findIndex((property) =>\n            [propertyName, initialName].includes(property.name)\n        );\n    }\n\n    /**\n     * If we change the type / model of a property, we will regenerate it's name\n     * (like if it was a new property) in order to reset the value of the children.\n     *\n     * But if we reset the old model / type, we want to be able to discard this\n     * modification (even if we save) and restore the original name.\n     *\n     * For that purpose, we save the original properties values.\n     */\n    _saveInitialPropertiesValues() {\n        // initial properties values, if the type or the model changed, the\n        // name will be regenerated in order to reset the value on the children\n        this.initialValues = {};\n        for (const propertiesValues of this.props.record.data[this.props.name] || []) {\n            this.initialValues[propertiesValues.name] = {\n                name: propertiesValues.name,\n                type: propertiesValues.type,\n                comodel: propertiesValues.comodel,\n            };\n        }\n    }\n\n    /**\n     * Open the popover with the property definition.\n     *\n     * @param {DomElement} target\n     * @param {string} propertyName\n     * @param {boolean} isNewlyCreated\n     */\n    _openPropertyDefinition(target, propertyName, isNewlyCreated = false) {\n        const propertiesList = this.propertiesList;\n        const propertyIndex = propertiesList.findIndex(\n            (property) => property.name === propertyName\n        );\n\n        // maybe the property has been renamed because the type / model\n        // changed, retrieve the new one\n        const currentName = (propertyName) => {\n            const propertiesList = this.propertiesList;\n            for (const [newName, initialValue] of Object.entries(this.initialValues)) {\n                if (initialValue.name === propertyName) {\n                    const prop = propertiesList.find((prop) => prop.name === newName);\n                    if (prop) {\n                        return newName;\n                    }\n                }\n            }\n            return propertyName;\n        };\n\n        this.onCloseCurrentPopover = () => {\n            this.onCloseCurrentPopover = null;\n            this.state.movedPropertyName = null;\n            target.classList.remove(\"disabled\");\n            if (isNewlyCreated) {\n                this._setDefaultPropertyValue(currentName(propertyName));\n            }\n        };\n\n        this.popover.open(target, {\n            fieldName: this.props.name,\n            readonly: this.props.readonly || !this.state.canChangeDefinition,\n            canChangeDefinition: this.state.canChangeDefinition,\n            propertyDefinition: this.propertiesList.find(\n                (property) => property.name === currentName(propertyName)\n            ),\n            context: this.props.context,\n            onChange: this.onPropertyDefinitionChange.bind(this),\n            onDelete: () => this.onPropertyDelete(currentName(propertyName)),\n            onPropertyMove: (direction) =>\n                this.onPropertyMove(currentName(propertyName), direction),\n            isNewlyCreated: isNewlyCreated,\n            propertyIndex: propertyIndex,\n            propertiesSize: propertiesList.length,\n            record: this.props.record,\n            ...this.additionalPropertyDefinitionProps,\n        });\n    }\n\n    /**\n     * Write the default value on the given property.\n     *\n     * @param {string} propertyName\n     */\n    _setDefaultPropertyValue(propertyName) {\n        const propertiesValues = this.propertiesList;\n        const newProperty = propertiesValues.find((property) => property.name === propertyName);\n        if (newProperty.default) {\n            newProperty.value = newProperty.default;\n        }\n        // it won't update the props, it's a trick because the onClose event of the popover\n        // is called not synchronously, and so if we click on \"create a property\", it will close\n        // the popover, calling this function, but the value will be overwritten because of onPropertyCreate\n        this.props.value = propertiesValues;\n        this.props.record.update({ [this.props.name]: propertiesValues });\n    }\n\n    /**\n     * Unfold the group of the given property.\n     *\n     * @param {integer} targetIndex\n     * @param {object} propertiesValues\n     */\n    _unfoldPropertyGroup(targetIndex, propertiesValues) {\n        const separator = propertiesValues.findLast(\n            (property, index) => property.type === \"separator\" && index <= targetIndex\n        );\n        if (separator) {\n            return this._toggleSeparators([separator.name], false);\n        }\n    }\n\n    /**\n     * Returns the text for the warning raised in the \"PROPERTY_FIELD:EDIT\"\n     * bus event, if the PropertiesField component cannot enter edit mode.\n     */\n    _getPropertyEditWarningText() {\n        return _t('Oops! You cannot edit the %(parentFieldLabel)s \"%(parentName)s\".', {\n            parentName: this.props.record.data[this.definitionRecordField].display_name,\n            parentFieldLabel: this.props.record.fields[this.definitionRecordField].string,\n        });\n    }\n}\n\nexport const propertiesField = {\n    component: PropertiesField,\n    displayName: _t(\"Properties\"),\n    supportedTypes: [\"properties\"],\n    extractProps({ attrs }, dynamicInfo) {\n        return {\n            context: dynamicInfo.context,\n            columns: parseInt(attrs.columns || \"1\"),\n            editMode: exprToBoolean(attrs.editMode),\n        };\n    },\n};\n\nregistry.category(\"fields\").add(\"properties\", propertiesField);\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { PropertyValue } from \"./property_value\";\nimport { CheckBox } from \"@web/core/checkbox/checkbox\";\nimport { DomainSelector } from \"@web/core/domain_selector/domain_selector\";\nimport { Domain } from \"@web/core/domain\";\nimport { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\nimport { ModelSelector } from \"@web/core/model_selector/model_selector\";\nimport { Many2XAutocomplete } from \"@web/views/fields/relational_utils\";\nimport { useService, useOwnedDialogs } from \"@web/core/utils/hooks\";\nimport { PropertyDefinitionSelection } from \"./property_definition_selection\";\nimport { PropertyTags } from \"./property_tags\";\nimport { SelectCreateDialog } from \"@web/views/view_dialogs/select_create_dialog\";\nimport { uuid } from \"@web/core/utils/strings\";\n\nimport { Component, useState, onWillUpdateProps, useEffect, useRef } from \"@odoo/owl\";\n\nexport class PropertyDefinition extends Component {\n    static template = \"web.PropertyDefinition\";\n    static components = {\n        CheckBox,\n        DomainSelector,\n        Dropdown,\n        DropdownItem,\n        PropertyValue,\n        Many2XAutocomplete,\n        ModelSelector,\n        PropertyDefinitionSelection,\n        PropertyTags,\n    };\n    static props = {\n        fieldName: { type: String },\n        readonly: { type: Boolean, optional: true },\n        canChangeDefinition: { type: Boolean, optional: true },\n        propertyDefinition: { optional: true },\n        context: { type: Object },\n        isNewlyCreated: { type: Boolean, optional: true },\n        // index and number of properties, to hide the move arrows when needed\n        propertyIndex: { type: Number },\n        propertiesSize: { type: Number },\n        // events\n        onChange: { type: Function, optional: true },\n        onDelete: { type: Function, optional: true },\n        onPropertyMove: { type: Function, optional: true },\n        // prop needed by the popover service\n        close: { type: Function, optional: true },\n        record: { type: Object, optional: true },\n    };\n    static _propertyParametersMap = new Map([\n        [\"comodel\", [\"many2one\", \"many2many\"]],\n        [\"currency_field\", [\"monetary\"]],\n        [\"domain\", [\"many2one\", \"many2many\"]],\n        [\"selection\", [\"selection\"]],\n        [\"tags\", [\"tags\"]],\n    ]);\n\n    setup() {\n        this.orm = useService(\"orm\");\n\n        this.propertyDefinitionRef = useRef(\"propertyDefinition\");\n        this.addDialog = useOwnedDialogs();\n\n        const defaultDefinition = {\n            name: false,\n            string: \"\",\n            type: \"char\",\n            default: \"\",\n        };\n        const propertyDefinition = {\n            ...defaultDefinition,\n            ...this.props.propertyDefinition,\n        };\n\n        this.state = useState({\n            propertyDefinition: propertyDefinition,\n            typeLabel: this._typeLabel(propertyDefinition.type),\n            resModel: \"\",\n            resModelDescription: \"\",\n            matchingRecordsCount: undefined,\n            propertyIndex: this.props.propertyIndex,\n        });\n\n        this._syncStateWithProps(propertyDefinition);\n\n        this._domInputIdPrefix = uuid();\n\n        // update the state and fetch needed information\n        onWillUpdateProps((newProps) => this._syncStateWithProps(newProps.value));\n\n        useEffect((event) => {\n            // focus the property label, when we open the property definition\n            if (this.labelFocused) {\n                // focus it only once\n                return;\n            }\n            this.labelFocused = true;\n            const labelInput = this.propertyDefinitionRef.el.querySelectorAll(\"input\")[0];\n            if (labelInput) {\n                if (this.props.isNewlyCreated) {\n                    labelInput.select();\n                } else {\n                    labelInput.focus();\n                }\n            }\n        });\n    }\n\n    /* --------------------------------------------------------\n     * Public methods / Getters\n     * -------------------------------------------------------- */\n\n    /**\n     * Return the list of property types with their labels.\n     *\n     * @returns {array}\n     */\n    get availablePropertyTypes() {\n        return [\n            [\"char\", _t(\"Text\")],\n            [\"text\", _t(\"Multiline Text\")],\n            [\"html\", _t(\"HTML\")],\n            [\"boolean\", _t(\"Checkbox\")],\n            [\"integer\", _t(\"Integer\")],\n            [\"float\", _t(\"Decimal\")],\n            [\"monetary\", _t(\"Monetary\")],\n            [\"date\", _t(\"Date\")],\n            [\"datetime\", _t(\"Date & Time\")],\n            [\"selection\", _t(\"Selection\")],\n            [\"tags\", _t(\"Tags\")],\n            [\"many2one\", _t(\"Many2one\")],\n            [\"many2many\", _t(\"Many2many\")],\n            [\"separator\", _t(\"Separator\")],\n        ];\n    }\n\n    get currencyFields() {\n        return Object\n            .values(this.props.record.fields)\n            .filter((fieldDef) => fieldDef.type === \"many2one\" && fieldDef.relation === \"res.currency\");\n    }\n\n    get defaultCurrencyField() {\n        const currencyFields = this.currencyFields.map((fieldDef) => fieldDef.name);\n        return currencyFields.includes(\"currency_id\") ? \"currency_id\" : currencyFields[0] || false;\n    }\n\n    /**\n     * Return True if the current properties is the first one in the list.\n     */\n    get isFirst() {\n        return this.state.propertyIndex === 0;\n    }\n\n    /**\n     * Return True if the current properties is the last one in the list.\n     */\n    get isLast() {\n        return this.state.propertyIndex === this.props.propertiesSize - 1;\n    }\n\n    /**\n     * Return the list of tag values, that will be selected by the PropertyTags\n     * component (all existing tags because we are editing the definition).\n     *\n     * @returns {array}\n     */\n    get propertyTagValues() {\n        return (this.state.propertyDefinition.tags || []).map((tag) => tag[0]);\n    }\n\n    /**\n     * Return an unique ID to be used in the DOM.\n     *\n     * @returns {string}\n     */\n    getUniqueDomID(suffix) {\n        return `property_definition_${this._domInputIdPrefix}_${suffix}`;\n    }\n\n    /* --------------------------------------------------------\n     * Event handlers\n     * -------------------------------------------------------- */\n\n    /**\n     * We changed the string of the property.\n     *\n     * @param {event} event\n     */\n    onPropertyLabelChange(event) {\n        const newString = event.target.value;\n        const propertyDefinition = {\n            ...this.state.propertyDefinition,\n            string: newString,\n        };\n        this.props.onChange(propertyDefinition);\n        this.state.propertyDefinition = propertyDefinition;\n    }\n\n    /**\n     * Pressed enter on the property label close the definition.\n     *\n     * @param {event} event\n     */\n    onPropertyLabelKeypress(event) {\n        if (event.key !== \"Enter\") {\n            return;\n        }\n        this.props.close();\n    }\n\n    /**\n     * We changed the default value of the property.\n     *\n     * @param {object} newDefault\n     */\n    onDefaultChange(newDefault) {\n        const propertyDefinition = {\n            ...this.state.propertyDefinition,\n            default: newDefault,\n        };\n        this.props.onChange(propertyDefinition);\n        this.state.propertyDefinition = propertyDefinition;\n    }\n\n    /**\n     * We selected a new property type.\n     *\n     * @param {string} newType\n     */\n    onPropertyTypeChange(newType) {\n        const propertyDefinition = {\n            ...this.state.propertyDefinition,\n            type: newType,\n        };\n        if ([\"integer\", \"float\", \"monetary\"].includes(newType)) {\n            propertyDefinition.value = 0;\n            propertyDefinition.default = 0;\n        } else {\n            propertyDefinition.value = false;\n            propertyDefinition.default = false;\n        }\n\n        if (newType === \"monetary\") {\n            propertyDefinition.currency_field = this.defaultCurrencyField;\n        }\n\n        if (newType === \"separator\") {\n            propertyDefinition.fold_by_default = true;\n        }\n\n        PropertyDefinition._propertyParametersMap.forEach((types, param) => {\n            if (!types.includes(propertyDefinition.type)) {\n                delete propertyDefinition[param];\n            }\n        });\n\n        this.props.onChange(propertyDefinition);\n        this.state.propertyDefinition = propertyDefinition;\n        if (!propertyDefinition.comodel) {\n            this.state.resModel = \"\";\n            this.state.resModelDescription = \"\";\n        }\n        this.state.typeLabel = this._typeLabel(newType);\n    }\n\n    /**\n     * The model of the relational property (many2one / many2many) has been changed.\n     *\n     * @param {string} newModel\n     */\n    async onModelChange(newModel) {\n        const { label, technical } = newModel;\n\n        // if we change the model, we should reset the default value and the domain\n        const modelChanged = technical !== this.state.resModel;\n\n        this.state.resModel = technical;\n        this.state.resModelDescription = label;\n\n        const propertyDefinition = {\n            ...this.state.propertyDefinition,\n            comodel: technical,\n            default: modelChanged ? false : this.state.propertyDefinition.default,\n            value: modelChanged ? false : this.state.propertyDefinition.value,\n            domain: modelChanged ? false : this.state.propertyDefinition.domain,\n        };\n        this.props.onChange(propertyDefinition);\n        this.state.propertyDefinition = propertyDefinition;\n        await this._updateMatchingRecordsCount();\n    }\n\n    /**\n     * The domain of the relational property has been changed.\n     *\n     * @param {string} newDomain\n     */\n    async onDomainChange(newDomain) {\n        const propertyDefinition = {\n            ...this.state.propertyDefinition,\n            domain: newDomain,\n            default: false,\n        };\n        this.props.onChange(propertyDefinition);\n        this.state.propertyDefinition = propertyDefinition;\n        await this._updateMatchingRecordsCount();\n    }\n\n    /**\n     * Open the list view of the records matching the current domain.\n     */\n    onButtonDomainClick() {\n        this.addDialog(SelectCreateDialog, {\n            title: _t(\"Selected records\"),\n            noCreate: true,\n            multiSelect: false,\n            resModel: this.state.propertyDefinition.comodel,\n            domain: new Domain(this.state.propertyDefinition.domain || \"[]\").toList(),\n            context: this.props.context || {},\n        });\n    }\n\n    /**\n     * Move the current property up or down.\n     *\n     * @param {string} direction, either 'up' or 'down'\n     */\n    onPropertyMove(direction) {\n        if (direction === \"up\") {\n            this.state.propertyIndex--;\n        } else {\n            this.state.propertyIndex++;\n        }\n        this.props.onPropertyMove(direction);\n    }\n\n    /**\n     * We renamed / created / removed a selection option.\n     *\n     * @param {array} newOptions\n     */\n    onSelectionOptionChange(newOptions) {\n        const propertyDefinition = {\n            ...this.state.propertyDefinition,\n            selection: newOptions,\n        };\n        this.props.onChange(propertyDefinition);\n        this.state.propertyDefinition = propertyDefinition;\n    }\n\n    /**\n     * @param {Event & { target: HTMLInputElement }} ev\n     */\n    onSuffixChange(ev) {\n        const propertyDefinition = {\n            ...this.state.propertyDefinition,\n            suffix: ev.target.value,\n        };\n        this.props.onChange(propertyDefinition);\n        this.state.propertyDefinition = propertyDefinition;\n    }\n\n    /**\n     * We renamed / created / removed tags.\n     *\n     * @param {array} newTags\n     */\n    onTagsChange(newTags) {\n        const propertyDefinition = {\n            ...this.state.propertyDefinition,\n            tags: newTags,\n        };\n        this.props.onChange(propertyDefinition);\n        this.state.propertyDefinition = propertyDefinition;\n    }\n\n    /**\n     * We activate / deactivate the property in the kanban view.\n     *\n     * @param {boolean} newValue\n     */\n    onViewInKanbanChange(newValue) {\n        const propertyDefinition = {\n            ...this.state.propertyDefinition,\n            view_in_cards: newValue,\n        };\n        this.props.onChange(propertyDefinition);\n        this.state.propertyDefinition = propertyDefinition;\n    }\n\n    /**\n     * Ensure the section below the separator is folded/unfolded by default\n     * @param {boolean} checked\n     */\n    onFoldByDefaultChange(checked) {\n        const propertyDefinition = {\n            ...this.state.propertyDefinition,\n            fold_by_default: checked,\n        };\n        this.props.onChange(propertyDefinition);\n        this.state.propertyDefinition = propertyDefinition;\n    }\n\n    onCurrencyFieldUpdate(path) {\n        const propertyDefinition = {\n            ...this.state.propertyDefinition,\n            currency_field: path,\n        };\n        this.props.onChange(propertyDefinition);\n        this.state.propertyDefinition = propertyDefinition;\n    }\n\n    /* --------------------------------------------------------\n     * Private methods\n     * -------------------------------------------------------- */\n\n    /**\n     * The property value changed (e.g. we discard a form view editing).\n     * Re-update the state with the new props.\n     *\n     * @param {object} propertyDefinition\n     */\n    async _syncStateWithProps(propertyDefinition) {\n        const newModel = propertyDefinition.comodel;\n        const currentModel = this.state.resModel;\n\n        this.state.propertyDefinition = propertyDefinition;\n        this.state.resModel = propertyDefinition.comodel;\n        this.state.typeLabel = this._typeLabel(propertyDefinition.type);\n        this.state.resModel = newModel;\n\n        if (newModel && newModel !== currentModel) {\n            // retrieve the model id and the model description from it's name\n            // \"res.partner\" => (5, \"Contact\")\n            try {\n                const result = await this.orm.call(\"ir.model\", \"display_name_for\", [[newModel]]);\n                if (!result || !result.length) {\n                    return;\n                }\n                this.state.resModelDescription = result[0].display_name;\n            } catch {\n                // can not read the ir.model\n                this.state.resModelDescription = _t(\n                    'You do not have access to the model \"%s\".',\n                    newModel\n                );\n            }\n\n            await this._updateMatchingRecordsCount();\n        } else if (!newModel) {\n            this.state.resModelDescription = \"\";\n        }\n    }\n\n    /**\n     * Update the number of records that match the current domain.\n     */\n    async _updateMatchingRecordsCount() {\n        if (this.state.resModel && this.state.resModel.length) {\n            const domainList = new Domain(this.state.propertyDefinition.domain || \"[]\").toList();\n\n            const result = await this.orm.call(\n                this.state.propertyDefinition.comodel,\n                \"search_count\",\n                [domainList]\n            );\n\n            this.state.matchingRecordsCount = result;\n        } else {\n            this.state.matchingRecordsCount = undefined;\n        }\n    }\n\n    /**\n     * Return the property label corresponding to the property type.\n     *\n     * @param {string} propertyType\n     * @returns {string}\n     */\n    _typeLabel(propertyType) {\n        const allTypes = this.availablePropertyTypes;\n        return allTypes.find((type) => type[0] === propertyType)[1];\n    }\n}\n", "import { useService } from \"@web/core/utils/hooks\";\nimport { uuid } from \"@web/core/utils/strings\";\n\nimport { Component, useState, useRef, useEffect } from \"@odoo/owl\";\nimport { useSortable } from \"@web/core/utils/sortable_owl\";\n\nexport class PropertyDefinitionSelection extends Component {\n    static template = \"web.PropertyDefinitionSelection\";\n    static props = {\n        default: { type: String, optional: true },\n        options: {},\n        readonly: { type: Boolean, optional: true },\n        canChangeDefinition: { type: Boolean, optional: true },\n        onOptionsChange: { type: Function, optional: true }, // we add / remove / rename an option\n        onDefaultOptionChange: { type: Function, optional: true }, // we select a default value\n    };\n\n    setup() {\n        this.notification = useService(\"notification\");\n\n        // when we create a new option, it's added in the state\n        // when we have finished to edit it (blur / enter) we propagate\n        // the new value in the props\n        this.state = useState({\n            newOption: null,\n        });\n\n        this.propertyDefinitionSelectionRef = useRef(\"propertyDefinitionSelection\");\n        this.addButtonRef = useRef(\"addButton\");\n\n        useEffect(() => {\n            // automatically give the focus to the new option if it is empty\n            if (!this.state.newOption) {\n                return;\n            }\n            const inputs = this.propertyDefinitionSelectionRef.el.querySelectorAll(\n                \".o_field_property_selection_option input\"\n            );\n            if (inputs && inputs.length && !inputs[this.state.newOption.index].value) {\n                inputs[this.state.newOption.index].focus();\n            }\n        });\n\n        useSortable({\n            enable: () => this.props.canChangeDefinition && !this.props.readonly,\n            ref: this.propertyDefinitionSelectionRef,\n            handle: \".o_field_property_selection_drag\",\n            elements: \".o_field_property_selection_option\",\n            cursor: \"grabbing\",\n            onDrop: async ({ element, previous }) => {\n                const movedOption = element.getAttribute(\"option-name\");\n                const destinationOption = previous && previous.getAttribute(\"option-name\");\n                await this.onOptionMoveTo(movedOption, destinationOption);\n            },\n        });\n    }\n\n    /* --------------------------------------------------------\n     * Public methods / Getters\n     * -------------------------------------------------------- */\n\n    /**\n     * Return the current available options.\n     *\n     * Make a deep copy to not change original object to be able to restore\n     * the old props if we discard the editing of the forma view.\n     *\n     * @returns {array}\n     */\n    get options() {\n        return JSON.parse(JSON.stringify(this.props.options || []));\n    }\n\n    /**\n     * Options visible by the UI, include the newly created option if needed.\n     *\n     * @returns {array}\n     */\n    get optionsVisible() {\n        const options = this.options || [];\n        const newOption = this.state.newOption;\n        if (newOption) {\n            options.splice(newOption.index, 0, [newOption.name, \"\"]);\n        }\n        return options;\n    }\n\n    /* --------------------------------------------------------\n     * Event handlers\n     * -------------------------------------------------------- */\n\n    /**\n     * Add a new empty selection option.\n     */\n    onOptionCreate(index) {\n        this.state.newOption = {\n            index: index,\n            name: uuid(),\n        };\n    }\n\n    /**\n     * We changed an option label.\n     *\n     * @param {event} event\n     * @param {integer} optionIndex\n     */\n    onOptionChange(event, optionIndex) {\n        const target = event.target;\n        const newLabel = target.value;\n\n        if (this.options[optionIndex] && this.options[optionIndex][1] === newLabel) {\n            // do not update the props if we are already up to date\n            // e.g. we pressed enter already and lost focus\n            return;\n        }\n\n        const options = this.optionsVisible;\n\n        if (!newLabel || !newLabel.length) {\n            // if the label is empty, remove the option\n            options.splice(optionIndex, 1);\n        } else {\n            options[optionIndex][1] = newLabel;\n        }\n\n        const nonEmptyOptions = options.filter((option) => option[1] && option[1].length);\n        this.props.onOptionsChange(nonEmptyOptions);\n\n        if (this.state.newOption) {\n            // the new option has been propagated in the props\n            this.state.newOption = null;\n        }\n    }\n\n    /**\n     * Loose focus on an option, should cancel the newly\n     * created option if we didn't write on it.\n     *\n     * The attribute `_ignoreBlur` can be set if we don't want to remove\n     * the option if it's empty (and it will re-gain the focus at the\n     * next `useEffect` call).\n     *\n     * @param {event} event\n     * @param {integer} optionIndex\n     */\n    onOptionBlur(event, optionIndex) {\n        if (event.target.value && event.target.value.length) {\n            // losing the focus on an non-empty option should have no effect\n            return;\n        } else if (this._ignoreBlur) {\n            this._ignoreBlur = false;\n            return;\n        }\n\n        if (event.relatedTarget === this.addButtonRef.el) {\n            // lost the focus because we click on the add button\n            // if the value is empty, just ignore and cancel the event\n            event.stopPropagation();\n            event.preventDefault();\n        } else if (optionIndex === this.state.newOption?.index) {\n            // we remove the focus from the new empty option, remove it\n            this.state.newOption = null;\n        }\n    }\n\n    /**\n     * We pressed Enter on an option, add it if it's not\n     * empty and automatically create a new one.\n     *\n     * Navigate using the up / down arrows.\n     *\n     * @param {event} event\n     * @param {integer} optionIndex\n     */\n    onOptionKeyDown(event, optionIndex) {\n        if (event.key === \"Enter\") {\n            const newLabel = event.target.value;\n\n            if (!newLabel || !newLabel.length) {\n                // press enter on an empty option, just ignore it, nothing to save\n                event.stopPropagation();\n                event.preventDefault();\n                return;\n            }\n\n            this.onOptionChange(event, optionIndex);\n            this.onOptionCreate(optionIndex + 1);\n        } else if ([\"ArrowUp\", \"ArrowDown\"].includes(event.key)) {\n            event.stopPropagation();\n            event.preventDefault();\n\n            if (event.key === \"ArrowUp\" && optionIndex > 0) {\n                const previousInput = event.target\n                    .closest(\".o_field_property_selection_option\")\n                    .previousElementSibling.querySelector(\"input\");\n                previousInput.focus();\n            } else if (event.key === \"ArrowDown\" && optionIndex < this.optionsVisible.length - 1) {\n                const nextInput = event.target\n                    .closest(\".o_field_property_selection_option\")\n                    .nextElementSibling.querySelector(\"input\");\n                nextInput.focus();\n            }\n        }\n    }\n\n    /**\n     * Change the default selection option.\n     *\n     * @param {integer} optionIndex\n     */\n    onOptionSetDefault(optionIndex) {\n        if (!this.props.canChangeDefinition) {\n            return;\n        }\n        const newValue = this.optionsVisible[optionIndex][0];\n        this.props.onDefaultOptionChange(newValue !== this.props.default ? newValue : false);\n    }\n\n    /**\n     * Ask to remove the selection option.\n     *\n     * @param {integer} optionIndex\n     */\n    onOptionDelete(optionIndex) {\n        const options = this.optionsVisible;\n        options.splice(optionIndex, 1);\n        this.props.onOptionsChange(options);\n    }\n\n    /**\n     * Move an option after an other one.\n     *\n     * @param {string} from, the option to move\n     * @param {string} to, the target option\n     *      (null if we move the option at the first index)\n     */\n    onOptionMoveTo(movedOption, destinationOption) {\n        this._ignoreBlur = true;\n\n        let options = this.optionsVisible;\n        // if destinationOption is null, destinationOptionIndex will be -1 which is intended\n        let destinationOptionIndex = options.findIndex((option) => option[0] == destinationOption);\n        const movedOptionIndex = options.findIndex((option) => option[0] == movedOption);\n        if (destinationOptionIndex < movedOptionIndex) {\n            // the first splice operation won't change the index (and we except it to decrease it)\n            // for example if we have [A, B, C], and we move C such that it becomes [A, C, B]\n            // destinationOption is A and the destination index is 0, but we need the index to be 1\n            // (if the destination is after the moved option, the first splice will fix it for us)\n            destinationOptionIndex++;\n        }\n\n        const activeEl = document.activeElement;\n        if (\n            activeEl &&\n            this.propertyDefinitionSelectionRef.el.contains(activeEl) &&\n            activeEl.tagName === \"INPUT\"\n        ) {\n            const optionName = activeEl\n                .closest(\".o_field_property_selection_option\")\n                .getAttribute(\"option-name\");\n            const editedOptionIndex = options.findIndex((option) => option[0] === optionName);\n            // we might be editing the value and drag and drop something else just after\n            options[editedOptionIndex][1] = activeEl.value;\n        }\n\n        options.splice(destinationOptionIndex, 0, options.splice(movedOptionIndex, 1)[0]);\n\n        if (this.state.newOption) {\n            const newOptionIndex = options.findIndex(\n                (option) => option[0] === this.state.newOption.name\n            );\n            if (!options[newOptionIndex][1]?.length) {\n                // if there's an empty option, fix it's index in the state\n                // and do not propagate it in the props\n                this.state.newOption = {\n                    ...this.state.newOption,\n                    index: newOptionIndex,\n                };\n                options = options.filter((option) => option[0] !== this.state.newOption.name);\n            } else {\n                this.state.newOption = null;\n            }\n        }\n\n        this.props.onOptionsChange(options);\n    }\n}\n", "import { AutoComplete } from \"@web/core/autocomplete/autocomplete\";\nimport { ColorList } from \"@web/core/colorlist/colorlist\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { usePopover } from \"@web/core/popover/popover_hook\";\nimport { registry } from \"@web/core/registry\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { TagsList } from \"@web/core/tags_list/tags_list\";\nimport { standardFieldProps } from \"@web/views/fields/standard_field_props\";\nimport { useTagNavigation } from \"@web/core/record_selectors/tag_navigation_hook\";\n\nimport { Component } from \"@odoo/owl\";\n\nclass PropertyTagsColorListPopover extends Component {\n    static template = \"web.PropertyTagsColorListPopover\";\n    static components = {\n        ColorList,\n    };\n    static props = {\n        colors: Array,\n        tag: Object,\n        switchTagColor: Function,\n        close: Function,\n    };\n}\n\nexport class PropertyTags extends Component {\n    static template = \"web.PropertyTags\";\n    static components = {\n        AutoComplete,\n        TagsList,\n        ColorList,\n        Popover: PropertyTagsColorListPopover,\n    };\n\n    static props = {\n        id: { type: String, optional: true },\n        selectedTags: {}, // Tags value visible in the tags list\n        tags: {}, // Tags definition visible in the dropdown\n        // Define the behavior of the delete button on the tags, either\n        // \"value\" or \"tags\". If \"value\", the delete button will unselect\n        // the value, if \"tags\" the value will be removed from the definition.\n        deleteAction: { type: String },\n        readonly: { type: Boolean, optional: true },\n        canChangeTags: { type: Boolean, optional: true },\n        // Select a new value\n        onValueChange: { type: Function, optional: true },\n        // Change the tags definition (can also receive a second\n        // argument to update the current selected value)\n        onTagsChange: { type: Function, optional: true },\n    };\n    setup() {\n        this.notification = useService(\"notification\");\n        this.popover = usePopover(this.constructor.components.Popover);\n        useTagNavigation(\"propertyTags\", {\n            delete: (index) => this.deleteTagByIndex(index),\n        });\n    }\n\n    /* --------------------------------------------------------\n     * Public methods / Getters\n     * -------------------------------------------------------- */\n\n    /**\n     * Return true if we should display the badges or just the tag label.\n     *\n     * @returns {array}\n     */\n    get displayBadge() {\n        return !this.env.config || this.env.config.viewType !== \"kanban\";\n    }\n\n    /**\n     * Return the list containing tags values and actions for the TagsList component.\n     *\n     * @returns {array}\n     */\n    get tagListItems() {\n        if (!this.props.selectedTags || !this.props.selectedTags.length) {\n            return [];\n        }\n\n        // Retrieve the tags label and color\n        // ['a', 'b'] =>  [['a', 'A', 5], ['b', 'B', 6]]\n        let value = this.props.tags.filter((tag) => this.props.selectedTags.indexOf(tag[0]) >= 0);\n\n        if (!this.displayBadge) {\n            // in kanban view e.g. to not show tag without color\n            value = value.filter((tag) => tag[2]);\n        }\n\n        const canDeleteTag =\n            !this.props.readonly &&\n            (this.props.canChangeTags || this.props.deleteAction === \"value\");\n\n        return value.map((tag) => {\n            const [tagId, tagLabel, tagColorIndex] = tag;\n            return {\n                id: tagId,\n                text: tagLabel,\n                className: this.props.canChangeTags ? \"\" : \"pe-none\",\n                colorIndex: tagColorIndex || 0,\n                onClick: (event) => this.onTagClick(event, tagId, tagColorIndex),\n                onDelete: canDeleteTag && (() => this.onTagDelete(tagId)),\n            };\n        });\n    }\n\n    /**\n     * Return the current selected tags.\n     * Make a deep copy to not make change on the original object\n     * and to be able to discard change.\n     *\n     * @returns {array}\n     */\n    get selectedTags() {\n        return JSON.parse(JSON.stringify(this.props.selectedTags || []));\n    }\n\n    /**\n     * Return the current tags that can be selected.\n     * Make a deep copy to not make change on the original object\n     * and to be able to discard change.\n     *\n     * @returns {array}\n     */\n    get availableTags() {\n        return JSON.parse(JSON.stringify(this.props.tags || []));\n    }\n\n    /**\n     * Options available in the autocomplete component.\n     *\n     * @returns {array}\n     */\n    get autocompleteSources() {\n        return [\n            {\n                options: (request) => {\n                    const tagsFiltered = this.props.tags.filter(\n                        (tag) =>\n                            (!this.props.selectedTags ||\n                                this.props.selectedTags.indexOf(tag[0]) < 0) &&\n                            (!request ||\n                                !request.length ||\n                                tag[1].toLocaleLowerCase().indexOf(request.toLocaleLowerCase()) >=\n                                    0)\n                    );\n                    if (!tagsFiltered || !tagsFiltered.length) {\n                        // no result, ask the user if he want to create a new tag\n                        if (!request || !request.length) {\n                            return [\n                                {\n                                    label: _t(\"Start typing...\"),\n                                    cssClass: \"fst-italic\",\n                                },\n                            ];\n                        } else if (!this.props.canChangeTags) {\n                            return [\n                                {\n                                    label: _t(\"No result\"),\n                                    cssClass: \"fst-italic\",\n                                },\n                            ];\n                        }\n\n                        return [\n                            {\n                                label: _t('Create \"%s\"', request),\n                                cssClass: \"o_field_property_dropdown_add\",\n                                onSelect: () => this.onTagCreate(request),\n                            },\n                        ];\n                    }\n                    return tagsFiltered.map((tag) => ({\n                        label: tag[1],\n                        onSelect: () => this.onOptionSelected(tag[0]),\n                    }));\n                },\n            },\n        ];\n    }\n\n    /* --------------------------------------------------------\n     * Event handlers\n     * -------------------------------------------------------- */\n\n    /**\n     * Add one value in the current tag list values.\n     *\n     * @param {string | object} tagValue\n     *      Either\n     *      - {toCreate: true, value: label}, to create a new value\n     *      - value, to select an existing value\n     */\n    onOptionSelected(tagValue) {\n        const selectedTags = this.selectedTags;\n        const newValue = [...selectedTags, tagValue];\n        this.props.onValueChange(newValue);\n    }\n\n    /**\n     * Ask to create a new tag that will be added in\n     * the definition and automatically selected.\n     *\n     * @param {string} newLabel\n     */\n    async onTagCreate(newLabel) {\n        if (!newLabel || !newLabel.length) {\n            return;\n        }\n\n        const newValue = newLabel ? newLabel.toLowerCase().replace(\" \", \"_\") : \"\";\n        const existingTag = this.props.tags.find((tag) => tag[0] === newValue);\n\n        if (existingTag) {\n            this.notification.add(_t(\"This tag is already available\"), {\n                type: \"warning\",\n            });\n            return;\n        }\n\n        // cycle trough colors\n        let tagColor =\n            this.props.tags && this.props.tags.length\n                ? (this.props.tags[this.props.tags.length - 1][2] + 1) % ColorList.COLORS.length\n                : parseInt(Math.random() * ColorList.COLORS.length);\n        tagColor = tagColor || 1; // never select white by default\n\n        const newTag = [newValue, newLabel, tagColor];\n        const updatedTags = [...this.availableTags, newTag];\n        // automatically select the newly created tag\n        const newValues = [...this.props.selectedTags, newTag[0]];\n        this.props.onTagsChange(updatedTags, newValues);\n    }\n\n    /**\n     * Click on the delete button on the tag pill.\n     * The behavior is defined by the prop \"deleteAction\".\n     *\n     * If we use the component for the tag configuration, clicking on \"delete\"\n     * will remove the tags from the available tags. If we use the component\n     * the tag selection, it will unselect the tag.\n     *\n     * @param {string} deleteTag, ID of the tag to delete\n     */\n    onTagDelete(deleteTag) {\n        if (this.props.deleteAction === \"value\") {\n            // remove the tag from the value (but keep it in the options list)\n            const selectedTags = this.selectedTags;\n            const newValue = selectedTags.filter((tag) => tag !== deleteTag);\n            this.props.onValueChange(newValue);\n        } else {\n            // remove the tag from the options\n            const availableTags = this.availableTags;\n            this.props.onTagsChange(availableTags.filter((tag) => tag[0] !== deleteTag));\n        }\n    }\n\n    /**\n     * Click on a tag pill, open the color popover if we can change the tag definition.\n     *\n     * @param {event} event\n     * @param {string} tagId\n     * @param {integer} tagColor\n     */\n    onTagClick(event, tagId, tagColor) {\n        if (!this.props.canChangeTags) {\n            event.currentTarget.blur();\n            return;\n        }\n        this.popover.open(event.currentTarget, {\n            colors: [...Array(ColorList.COLORS.length).keys()],\n            tag: { id: tagId, colorIndex: tagColor },\n            switchTagColor: this.onTagColorSwitch.bind(this),\n        });\n    }\n\n    /**\n     * Ask to change the color of a tag.\n     *\n     * @param {integer} colorIndex\n     * @param {object} currentTag\n     */\n    onTagColorSwitch(colorIndex, currentTag) {\n        const availableTags = this.availableTags;\n        availableTags.find((tag) => tag[0] === currentTag.id)[2] = colorIndex;\n        this.props.onTagsChange(availableTags);\n\n        // close the color popover\n        this.popover.close();\n    }\n\n    /**\n     * Delete tags by pressing backspace.\n     *\n     * @param {integer} index\n     */\n    deleteTagByIndex(index) {\n        this.onTagDelete(this.tagListItems[index].id);\n    }\n}\n\nexport class PropertyTagsField extends Component {\n    static template = \"web.PropertyTagsField\";\n    static components = { PropertyTags };\n    static props = { ...standardFieldProps };\n\n    get propertyTagsProps() {\n        return {\n            selectedTags: this.props.record.data[this.props.name] || [],\n            tags: this.props.record.fields[this.props.name].tags || [],\n            deleteAction: \"value\",\n            readonly: this.props.readonly,\n            canChangeTags: false,\n            onValueChange: (value) => {\n                this.props.record.update({ [this.props.name]: value });\n            },\n        };\n    }\n}\n\nexport const propertyTagsField = {\n    component: PropertyTagsField,\n};\n\nregistry.category(\"fields\").add(\"property_tags\", propertyTagsField);\n", "import { useAutoresize } from \"@web/core/utils/autoresize\";\n\nimport { Component, useRef } from \"@odoo/owl\";\n\nexport class PropertyText extends Component {\n    static template = \"web.PropertyText\";\n    static props = {\n        updateProperty: Function,\n        value: String,\n    };\n\n    setup() {\n        this.textareaRef = useRef(\"textarea\");\n        useAutoresize(this.textareaRef);\n    }\n}\n", "import { Component } from \"@odoo/owl\";\nimport { CheckBox } from \"@web/core/checkbox/checkbox\";\nimport { DateTimeInput } from \"@web/core/datetime/datetime_input\";\nimport { Domain } from \"@web/core/domain\";\nimport { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\nimport {\n    deserializeDate,\n    deserializeDateTime,\n    formatDate,\n    formatDateTime,\n    serializeDate,\n    serializeDateTime,\n} from \"@web/core/l10n/dates\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { TagsList } from \"@web/core/tags_list/tags_list\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { formatInteger, formatMany2one, formatMonetary } from \"@web/views/fields/formatters\";\nimport { formatFloat } from \"@web/core/utils/numbers\";\nimport { parseFloat, parseInteger, parseMonetary } from \"@web/views/fields/parsers\";\nimport { Many2XAutocomplete, useOpenMany2XRecord } from \"@web/views/fields/relational_utils\";\nimport { PropertyTags } from \"./property_tags\";\nimport { PropertyText } from \"./property_text\";\nimport { imageUrl } from \"@web/core/utils/urls\";\nimport { getCurrency } from \"@web/core/currency\";\nimport { nbsp } from \"@web/core/utils/strings\";\n\nfunction extractData(record) {\n    let name;\n    if (\"display_name\" in record) {\n        name = record.display_name;\n    } else if (\"name\" in record) {\n        name = record.name.id ? record.name.display_name : record.name;\n    }\n    return { id: record.id, display_name: name };\n}\n\n/**\n * Represent one property value.\n * Supports many types and instantiates the appropriate component for it.\n * - Text\n * - Integer\n * - Boolean\n * - Selection\n * - Datetime & Date\n * - Many2one\n * - Many2many\n * - Monetary\n * - Tags\n * - ...\n */\nexport class PropertyValue extends Component {\n    static template = \"web.PropertyValue\";\n    static components = {\n        Dropdown,\n        DropdownItem,\n        CheckBox,\n        DateTimeInput,\n        Many2XAutocomplete,\n        TagsList,\n        PropertyTags,\n        PropertyText,\n    };\n\n    static props = {\n        id: { type: String, optional: true },\n        type: { type: String, optional: true },\n        comodel: { type: String, optional: true },\n        currencyField: { type: String, optional: true },\n        domain: { type: String, optional: true },\n        string: { type: String, optional: true },\n        value: { optional: true },\n        context: { type: Object },\n        readonly: { type: Boolean, optional: true },\n        canChangeDefinition: { type: Boolean, optional: true },\n        selection: { type: Array, optional: true },\n        tags: { type: Array, optional: true },\n        onChange: { type: Function, optional: true },\n        onTagsChange: { type: Function, optional: true },\n        record: { type: Object, optional: true },\n    };\n\n    setup() {\n        this.nbsp = nbsp;\n\n        this.orm = useService(\"orm\");\n        this.action = useService(\"action\");\n\n        this.openMany2X = useOpenMany2XRecord({\n            resModel: this.props.model,\n            activeActions: {\n                create: false,\n                createEdit: false,\n                write: true,\n            },\n            isToMany: false,\n            onRecordSaved: async (record) => {\n                if (!record) {\n                    return;\n                }\n                // maybe the record display name has changed\n                await record.load();\n                const recordData = extractData(record.data);\n                await this.onValueChange([recordData]);\n            },\n            fieldString: this.props.string,\n        });\n    }\n\n    /* --------------------------------------------------------\n     * Public methods / Getters\n     * -------------------------------------------------------- */\n\n    get currency() {\n        if (!isNaN(this.currencyId)) {\n            return getCurrency(this.currencyId) || null;\n        }\n        return null;\n    }\n\n    get currencyId() {\n        const currency = this.props.record.data[this.props.currencyField];\n        return currency && currency.id;\n    }\n\n    /**\n     * Return the value of the current property,\n     * that will be used by the sub-components.\n     *\n     * @returns {object}\n     */\n    get propertyValue() {\n        const value = this.props.value;\n\n        if (this.props.type === \"float\") {\n            // force to show at least 1 digit, even for integers\n            return value;\n        } else if (this.props.type === \"datetime\") {\n            const datetimeValue = typeof value === \"string\" ? deserializeDateTime(value) : value;\n            return datetimeValue && !datetimeValue.invalid ? datetimeValue : false;\n        } else if (this.props.type === \"date\") {\n            const dateValue = typeof value === \"string\" ? deserializeDate(value) : value;\n            return dateValue && !dateValue.invalid ? dateValue : false;\n        } else if (this.props.type === \"boolean\") {\n            return !!value;\n        } else if (this.props.type === \"selection\") {\n            const options = this.props.selection || [];\n            const option = options.find((option) => option[0] === value);\n            return option && option.length === 2 && option[0] ? option[0] : \"\";\n        } else if (this.props.type === \"many2one\") {\n            return !value || !value.id || !value.display_name ? false : value;\n        } else if (this.props.type === \"many2many\") {\n            if (!value || !value.length) {\n                return [];\n            }\n\n            // Convert to TagsList component format\n            return value.map((many2manyValue) => {\n                const hasAccess = many2manyValue[1] !== null;\n                return {\n                    id: many2manyValue[0],\n                    comodel: this.props.comodel,\n                    text: hasAccess ? many2manyValue[1] : _t(\"No Access\"),\n                    onClick:\n                        hasAccess &&\n                        this.clickableRelational &&\n                        (async () => await this._openRecord(this.props.comodel, many2manyValue[0])),\n                    onDelete:\n                        !this.props.readonly &&\n                        hasAccess &&\n                        (() => this.onMany2manyDelete(many2manyValue[0])),\n                    colorIndex: 0,\n                    img:\n                        this.showAvatar && hasAccess\n                            ? imageUrl(this.props.comodel, many2manyValue[0], \"avatar_128\")\n                            : null,\n                };\n            });\n        } else if (this.props.type === \"tags\") {\n            return value || [];\n        }\n\n        return value;\n    }\n\n    /**\n     * Return the model domain (related to many2one and many2many properties).\n     *\n     * @returns {array}\n     */\n    get propertyDomain() {\n        if (!this.props.domain || !this.props.domain.length) {\n            return [];\n        }\n        let domain = new Domain(this.props.domain);\n        if (this.props.type === \"many2many\" && this.props.value) {\n            domain = Domain.and([\n                domain,\n                [[\"id\", \"not in\", this.props.value.map((rec) => rec[0])]],\n            ]);\n        }\n        return domain.toList();\n    }\n\n    /**\n     * Formatted value displayed in readonly mode.\n     *\n     * @returns {string}\n     */\n    get displayValue() {\n        const value = this.propertyValue;\n\n        if (this.props.type === \"many2one\" && value && value.length === 2) {\n            return formatMany2one(value);\n        } else if (this.props.type === \"integer\") {\n            return formatInteger(value || 0);\n        } else if (this.props.type === \"float\") {\n            return formatFloat(value || 0);\n        } else if (this.props.type === \"monetary\") {\n            return formatMonetary(value || 0, {\n                digits: this.currency?.digits,\n                currencyId: this.currencyId,\n                noSymbol: !this.props.readonly,\n            });\n        } else if (!value) {\n            return false;\n        } else if (this.props.type === \"datetime\" && value) {\n            return formatDateTime(value);\n        } else if (this.props.type === \"date\" && value) {\n            return formatDate(value);\n        } else if (this.props.type === \"selection\") {\n            return this.props.selection.find((option) => option[0] === value)[1];\n        }\n        return value.toString();\n    }\n\n    /**\n     * Return true if the relational properties are clickable.\n     *\n     * @returns {boolean}\n     */\n    get clickableRelational() {\n        return !this.env.config || this.env.config.viewType !== \"kanban\";\n    }\n\n    /**\n     * Return True if we need to display a avatar for the current property.\n     *\n     * @returns {boolean}\n     */\n    get showAvatar() {\n        return (\n            [\"many2one\", \"many2many\"].includes(this.props.type) &&\n            [\"res.users\", \"res.partner\"].includes(this.props.comodel)\n        );\n    }\n\n    /* --------------------------------------------------------\n     * Event handlers\n     * -------------------------------------------------------- */\n\n    /**\n     * Parse the value received by the sub-components and trigger an onChange event.\n     *\n     * @param {object} newValue\n     */\n    async onValueChange(newValue) {\n        if (this.props.type === \"datetime\") {\n            newValue = newValue && serializeDateTime(newValue);\n        } else if (this.props.type === \"date\") {\n            newValue = newValue && serializeDate(newValue);\n        } else if (this.props.type === \"integer\") {\n            try {\n                newValue = parseInteger(newValue) || 0;\n            } catch {\n                newValue = 0;\n            }\n        } else if (this.props.type === \"float\") {\n            try {\n                newValue = parseFloat(newValue) || 0;\n            } catch {\n                newValue = 0;\n            }\n        } else if ([\"many2one\", \"many2many\"].includes(this.props.type)) {\n            newValue = newValue[0];\n            if (newValue && newValue.id && newValue.display_name === undefined) {\n                // The \"Search more\" option in the Many2XAutocomplete component\n                // only return the record ID, and not the name. But we need to name\n                // in the component props to be able to display it.\n                // Make a RPC call to resolve the display name of the record.\n                newValue = await this._nameGet(newValue.id);\n            }\n\n            if (this.props.type === \"many2many\" && newValue) {\n                // add the record in the current many2many list\n                const currentValue = this.props.value || [];\n                const recordId = newValue.id;\n                const exists = currentValue.find((rec) => rec.id === recordId);\n                if (exists) {\n                    return;\n                }\n                newValue = [...currentValue, [newValue.id, newValue.display_name]];\n            }\n        } else if (this.props.type === \"monetary\") {\n            try {\n                newValue = parseMonetary(newValue) || 0;\n            } catch {\n                newValue = 0;\n            }\n        }\n\n        // trigger the onchange event to notify the parent component\n        this.props.onChange(newValue);\n    }\n\n    /**\n     * Open the form view of the current record.\n     *\n     * @param {event} event\n     */\n    async onMany2oneClick(event) {\n        if (this.props.readonly) {\n            event.stopPropagation();\n            await this._openRecord(this.props.comodel, this.propertyValue.id);\n        }\n    }\n\n    /**\n     * Open the current many2one record form view in a modal.\n     */\n    onExternalLinkClick() {\n        return this.openMany2X({\n            resId: this.propertyValue.id,\n            forceModel: this.props.comodel,\n            context: this.context,\n        });\n    }\n\n    /**\n     * Removed a record from the many2many list.\n     *\n     * @param {integer} many2manyId\n     */\n    onMany2manyDelete(many2manyId) {\n        // deep copy\n        const currentValue = JSON.parse(JSON.stringify(this.props.value || []));\n        const newValue = currentValue.filter((value) => value[0] !== many2manyId);\n        this.props.onChange(newValue);\n    }\n\n    /**\n     * Ask to create a record from a relational property.\n     *\n     * @param {string} name\n     */\n    async onQuickCreate(name) {\n        const result = await this.orm.call(this.props.comodel, \"name_create\", [name], {\n            context: this.props.context,\n        });\n        this.onValueChange([{ id: result[0], display_name: result[1] }]);\n    }\n\n    /* --------------------------------------------------------\n     * Private methods\n     * -------------------------------------------------------- */\n\n    /**\n     * Open the form view of the given record id / model.\n     *\n     * @param {string} recordModel\n     * @param {integer} recordId\n     */\n    async _openRecord(recordModel, recordId) {\n        const action = await this.orm.call(recordModel, \"get_formview_action\", [[recordId]], {\n            context: this.props.context,\n        });\n\n        this.action.doAction(action);\n    }\n\n    /**\n     * Get the display name of the given record.\n     * Model is taken from the current selected model.\n     *\n     * @param {string} recordId\n     * @returns {array} [record id, record name]\n     */\n    async _nameGet(recordId) {\n        const result = await this.orm.read(this.props.comodel, [recordId], [\"display_name\"], {\n            context: this.props.context,\n        });\n        return result[0];\n    }\n}\n", "import { Component } from \"@odoo/owl\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { getFieldDomain } from \"@web/model/relational_model/utils\";\nimport { useSpecialData } from \"@web/views/fields/relational_utils\";\nimport { standardFieldProps } from \"../standard_field_props\";\n\nlet nextId = 0;\nexport class RadioField extends Component {\n    static template = \"web.RadioField\";\n    static props = {\n        ...standardFieldProps,\n        orientation: { type: String, optional: true },\n        label: { type: String, optional: true },\n        domain: { type: [Array, Function], optional: true },\n    };\n    static defaultProps = {\n        orientation: \"vertical\",\n    };\n\n    setup() {\n        this.id = `radio_field_${nextId++}`;\n        this.type = this.props.record.fields[this.props.name].type;\n        if (this.type === \"many2one\") {\n            this.specialData = useSpecialData(async (orm, props) => {\n                const { relation } = props.record.fields[props.name];\n                const domain = getFieldDomain(props.record, props.name, props.domain);\n                const kwargs = {\n                    specification: { display_name: 1 },\n                    domain,\n                };\n                const { records } = await orm.call(relation, \"web_search_read\", [], kwargs);\n                return records.map((record) => [record.id, record.display_name]);\n            });\n        }\n    }\n\n    get items() {\n        switch (this.type) {\n            case \"selection\":\n                return this.props.record.fields[this.props.name].selection;\n            case \"many2one\": {\n                return this.specialData.data;\n            }\n            default:\n                return [];\n        }\n    }\n    get value() {\n        const value = this.props.record.data[this.props.name];\n        switch (this.type) {\n            case \"selection\":\n                return value;\n            case \"many2one\":\n                return value && value.id;\n            default:\n                return null;\n        }\n    }\n\n    /**\n     * @param {any} value\n     */\n    onChange(value) {\n        switch (this.type) {\n            case \"selection\":\n                this.props.record.update({ [this.props.name]: value[0] });\n                break;\n            case \"many2one\":\n                this.props.record.update({\n                    [this.props.name]: value && { id: value[0], display_name: value[1] },\n                });\n                break;\n        }\n    }\n}\n\nexport const radioField = {\n    component: RadioField,\n    displayName: _t(\"Radio\"),\n    supportedOptions: [\n        {\n            label: _t(\"Display horizontally\"),\n            name: \"horizontal\",\n            type: \"boolean\",\n        },\n    ],\n    supportedTypes: [\"many2one\", \"selection\"],\n    isEmpty: (record, fieldName) => record.data[fieldName] === false,\n    extractProps: ({ options, string }, dynamicInfo) => ({\n        orientation: options.horizontal ? \"horizontal\" : \"vertical\",\n        label: string,\n        domain: dynamicInfo.domain,\n    }),\n};\n\nregistry.category(\"fields\").add(\"radio\", radioField);\n", "import { Component, useState } from \"@odoo/owl\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { useRecordObserver } from \"@web/model/relational_model/utils\";\nimport { computeM2OProps, Many2One } from \"../many2one/many2one\";\nimport { extractM2OFieldProps, Many2OneField } from \"../many2one/many2one_field\";\n\n/**\n * @typedef ReferenceValue\n * @property {string} resModel\n * @property {number} resId\n * @property {string} displayName\n */\n\n/**\n * 1. Reference field is a char field\n * 2. Reference widget has model_field prop\n * 3. Standard case\n */\n\n/**\n * This class represents a reference field widget. It can be used to display\n * a reference field OR a char field.\n * The res_model of the relation is defined either by the reference field itself\n * or by the model_field prop.\n *\n * 1) Reference field is a char field\n * We have to fetch the display name (name_get) of the referenced record.\n *\n * 2) Reference widget has model_field prop\n * We have to fetch the technical name of the co model.\n *\n * 3) Standard case\n * The value is already in record.data[fieldName]\n */\nexport class ReferenceField extends Component {\n    static template = \"web.ReferenceField\";\n    static components = { Many2One };\n    static props = {\n        ...Many2OneField.props,\n        hideModel: { type: Boolean, optional: true },\n        modelField: { type: String, optional: true },\n    };\n\n    setup() {\n        /** @type {{formattedCharValue?: ReferenceValue, modelName?: string}} */\n        this.state = useState({\n            formattedCharValue: undefined, // Value extracted from reference char field\n            modelName: undefined, // Name get of the value of the model field\n            currentRelation: undefined,\n        });\n        if (this._isCharField(this.props)) {\n            /** Fetch the display name of the record referenced by the field */\n            let currentValue = undefined;\n            useRecordObserver(async (record, props) => {\n                if (currentValue !== record.data[props.name]) {\n                    this.state.formattedCharValue = await this._fetchReferenceCharData(props);\n                    currentValue = record.data[props.name];\n                }\n            });\n        } else if (this.props.modelField) {\n            /** Fetch the technical name of the co model */\n            useRecordObserver(async (record, props) => {\n                if (this.currentModelId !== record.data[props.modelField]?.id) {\n                    this.state.modelName = await this._fetchModelTechnicalName(props);\n                    if (this.currentModelId !== undefined) {\n                        record.update({ [props.name]: false });\n                    }\n                    this.currentModelId = record.data[props.modelField]?.id;\n                }\n            });\n        }\n    }\n\n    get m2oProps() {\n        const value = this.getValue();\n        return {\n            ...computeM2OProps(this.props),\n            relation: this.getRelation(),\n            value: value && { id: value.resId, display_name: value.displayName },\n            update: this.updateM2O.bind(this),\n        };\n    }\n    get selection() {\n        if (!this._isCharField(this.props) && !this.hideModelSelector) {\n            return this.props.record.fields[this.props.name].selection;\n        }\n        return [];\n    }\n\n    get hideModelSelector() {\n        return this.props.hideModel || this.props.modelField;\n    }\n\n    getRelation() {\n        const modelName = this.getModelName();\n        if (modelName) {\n            return modelName;\n        }\n\n        const value = this.getValue();\n        if (value && value.resModel) {\n            return value.resModel;\n        } else {\n            return this.state.currentRelation;\n        }\n    }\n\n    /**\n     * @returns {ReferenceValue|false}\n     */\n    getValue() {\n        if (this._isCharField(this.props)) {\n            return this.state.formattedCharValue;\n        } else {\n            return this.props.record.data[this.props.name];\n        }\n    }\n\n    /**\n     * @returns {string|undefined}\n     */\n    getModelName() {\n        return this.hideModelSelector && this.state.modelName;\n    }\n\n    updateModel(value) {\n        this.state.currentRelation = value;\n        this.props.record.update({ [this.props.name]: false });\n    }\n\n    updateM2O(value) {\n        const resModel = this.state.currentRelation || this.getRelation();\n        this.props.record.update({\n            [this.props.name]: value && {\n                resModel,\n                resId: value.id,\n                displayName: value.display_name,\n            },\n        });\n    }\n\n    /**\n     * Return true if the reference field is a char field.\n     */\n    _isCharField(props) {\n        return props.record.fields[props.name].type === \"char\";\n    }\n\n    /**\n     * Fetch special data if the reference field is a char field.\n     * It fetches the display name of the record.\n     *\n     * @returns {Promise<{ resId: number, resModel: string, displayName: string }|false>}\n     */\n    async _fetchReferenceCharData(props) {\n        const recordData = props.record.data[props.name];\n        if (!recordData) {\n            return false;\n        }\n        const [resModel, _resId] = recordData.split(\",\");\n        const resId = parseInt(_resId, 10);\n        if (resModel && resId) {\n            const { specialDataCaches, orm } = props.record.model;\n            const key = `__reference__name_get-${recordData}`;\n            if (!specialDataCaches[key]) {\n                specialDataCaches[key] = orm.read(resModel, [resId], [\"display_name\"]);\n            }\n            const result = await specialDataCaches[key];\n            return {\n                resId,\n                resModel,\n                displayName: result[0].display_name,\n            };\n        }\n        return false;\n    }\n\n    /**\n     * Ensure that the modelField is a many2one to ir.model\n     */\n    _assertMany2OneToIrModel(props) {\n        const field = props.modelField && props.record.fields[props.modelField];\n        if (field && (field.type !== \"many2one\" || field.relation !== \"ir.model\")) {\n            throw new Error(\n                `The model_field (${props.modelField}) of the reference field ${props.name} must be a many2one('ir.model').`\n            );\n        }\n    }\n\n    /**\n     * Fetch the technical name of the model which is selected in the modelField\n     * props\n     *\n     * @returns {Promise<string|false>}\n     */\n    async _fetchModelTechnicalName(props) {\n        this._assertMany2OneToIrModel(props);\n        const record = props.record;\n        const modelId = record.data[props.modelField]?.id;\n        if (!modelId) {\n            return false;\n        }\n        const { specialDataCaches, orm } = props.record.model;\n        const key = `__reference__ir_model-${modelId}`;\n        if (!specialDataCaches[key]) {\n            specialDataCaches[key] = orm.read(\"ir.model\", [modelId], [\"model\"]);\n        }\n        const result = await specialDataCaches[key];\n        return result[0].model;\n    }\n}\n\nexport const referenceField = {\n    component: ReferenceField,\n    displayName: _t(\"Reference\"),\n    supportedOptions: [\n        {\n            label: _t(\"Hide model\"),\n            name: \"hide_model\",\n            type: \"boolean\",\n        },\n        {\n            label: _t(\"Model field\"),\n            name: \"model_field\",\n            type: \"field\",\n            availableTypes: [\"many2one\"],\n        },\n    ],\n    supportedTypes: [\"reference\", \"char\"],\n    extractProps({ options }) {\n        /*\n        1 - <field name=\"ref\" options=\"{'model_field': 'model_id'}\" />\n        2 - <field name=\"ref\" options=\"{'hide_model': True}\" />\n        3 - <field name=\"ref\" options=\"{'model_field': 'model_id' 'hide_model': True}\" />\n        4 - <field name=\"ref\"/>\n\n        We want to display the model selector only in the 4th case.\n        */\n        const props = extractM2OFieldProps(...arguments);\n        props.hideModel = !!options.hide_model;\n        props.modelField = options.model_field;\n        return props;\n    },\n};\n\nregistry.category(\"fields\").add(\"reference\", referenceField);\n", "import { AutoComplete } from \"@web/core/autocomplete/autocomplete\";\nimport { makeContext } from \"@web/core/context\";\nimport { Dialog } from \"@web/core/dialog/dialog\";\nimport { Domain } from \"@web/core/domain\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { RPCError } from \"@web/core/network/rpc\";\nimport { evaluateBooleanExpr } from \"@web/core/py_js/py\";\nimport {\n    useBus,\n    useChildRef,\n    useForwardRefToParent,\n    useOwnedDialogs,\n    useService,\n} from \"@web/core/utils/hooks\";\nimport { createElement, parseXML } from \"@web/core/utils/xml\";\nimport { extractFieldsFromArchInfo, useRecordObserver } from \"@web/model/relational_model/utils\";\nimport { FormArchParser } from \"@web/views/form/form_arch_parser\";\nimport { loadSubViews, useFormViewInDialog } from \"@web/views/form/form_controller\";\nimport { FormRenderer } from \"@web/views/form/form_renderer\";\nimport { computeViewClassName, isNull } from \"@web/views/utils\";\nimport { ViewButton } from \"@web/views/view_button/view_button\";\nimport { executeButtonCallback, useViewButtons } from \"@web/views/view_button/view_button_hook\";\nimport { FormViewDialog } from \"@web/views/view_dialogs/form_view_dialog\";\nimport { SelectCreateDialog } from \"@web/views/view_dialogs/select_create_dialog\";\n\n/**\n * @typedef {Object} RelationalActiveActions {\n * @property {\"x2m\"} type\n * @property {boolean} create\n * @property {boolean} createEdit\n * @property {boolean} delete\n * @property {boolean} [link]\n * @property {boolean} [unlink]\n * @property {boolean} [write]\n * @property {Function | null} onDelete\n *\n * @typedef {import(\"services\").Services} Services\n */\n\nimport {\n    Component,\n    onWillUpdateProps,\n    status,\n    useComponent,\n    useEffect,\n    useEnv,\n    useState,\n    useSubEnv,\n} from \"@odoo/owl\";\nimport { KeepLast } from \"@web/core/utils/concurrency\";\nimport { highlightText, odoomark } from \"@web/core/utils/html\";\nimport { deepEqual } from \"@web/core/utils/objects\";\n\n//\n// Commons\n//\nexport function useSelectCreate({ resModel, activeActions, onSelected, onCreateEdit, onUnselect }) {\n    const addDialog = useOwnedDialogs();\n\n    function selectCreate({ domain, context, filters, title }) {\n        addDialog(SelectCreateDialog, {\n            title: title || _t(\"Select records\"),\n            noCreate: !activeActions.create,\n            multiSelect: \"link\" in activeActions ? activeActions.link : false, // LPE Fixme\n            resModel,\n            context,\n            domain,\n            onSelected,\n            onCreateEdit: () => onCreateEdit({ context }),\n            dynamicFilters: filters,\n            onUnselect,\n        });\n    }\n    return selectCreate;\n}\n\nconst STANDARD_ACTIVE_ACTIONS = [\"create\", \"createEdit\", \"delete\", \"link\", \"unlink\", \"write\"];\n\n/**\n * FIXME: this should somehow be merged with 'getActiveActions' (@web/views/utils.js)\n * Also I don't think storing a function in a collection of booleans is a good idea...\n *\n * @param {Object} params\n * @param {string} params.fieldType\n * @param {Record<string, boolean>} [params.subViewActiveActions={}]\n * @param {Object} [params.crudOptions={}]\n * @param {(props: Record<string, any>) => Record<any, any>} [params.getEvalParams=() => ({})]\n * @returns {RelationalActiveActions}\n */\nexport function useActiveActions({\n    fieldType,\n    subViewActiveActions = {},\n    crudOptions = {},\n    getEvalParams = () => ({}),\n}) {\n    const compute = ({ evalContext = {}, readonly = true }) => {\n        /** @type {RelationalActiveActions} */\n        const result = { type: fieldType, onDelete: null };\n        const evalAction = (actionName) => evals[actionName](evalContext);\n\n        // We need to take care of tags \"control\" and \"create\" to set create stuff\n        result.create = !readonly && evalAction(\"create\");\n        result.createEdit = !readonly && result.create && crudOptions.createEdit; // always a boolean\n        result.edit = crudOptions.edit; // always a boolean\n        result.delete = !readonly && evalAction(\"delete\");\n        result.write = (isMany2Many || !readonly) && evalAction(\"write\");\n\n        if (isMany2Many) {\n            result.link = !readonly && evalAction(\"link\");\n            result.unlink = !readonly && evalAction(\"unlink\");\n        }\n\n        if (result.unlink || (!isMany2Many && result.delete)) {\n            result.onDelete = crudOptions.onDelete;\n        }\n\n        return result;\n    };\n\n    const props = useComponent().props;\n    const isMany2Many = fieldType === \"many2many\";\n\n    // Define eval functions\n    const evals = {};\n    for (const actionName of STANDARD_ACTIVE_ACTIONS) {\n        let evalFn = () => true;\n        if (!isNull(crudOptions[actionName])) {\n            const action = crudOptions[actionName];\n            evalFn = (evalContext) => Boolean(action && new Domain(action).contains(evalContext));\n        }\n\n        if (actionName in subViewActiveActions) {\n            const viewActiveAction = subViewActiveActions[actionName];\n            evals[actionName] = (evalContext) => viewActiveAction && evalFn(evalContext);\n        } else {\n            evals[actionName] = evalFn;\n        }\n    }\n\n    // Compute active actions\n    const activeActions = compute(getEvalParams(props));\n    onWillUpdateProps((nextProps) => {\n        Object.assign(activeActions, compute(getEvalParams(nextProps)));\n    });\n\n    return activeActions;\n}\n\n/**\n * @template T, [Props=any], [Env=any]\n * @param {(orm: Services[\"orm\"], props: Component<Props, Env>[\"props\"]) => Promise<T>} loadFn\n */\nexport function useSpecialData(loadFn) {\n    const component = useComponent();\n    const record = component.props.record;\n    const { specialDataCaches } = record.model;\n    const orm = component.env.services.orm;\n    const ormWithCache = Object.create(orm);\n    ormWithCache.call = async (...args) => {\n        const key = JSON.stringify(args);\n        if (!specialDataCaches[key]) {\n            return await orm\n                .cache({\n                    type: \"disk\",\n                    update: \"always\",\n                    callback: (res, hasChanged) => {\n                        specialDataCaches[key] = Promise.resolve(res);\n                        if (status(component) !== \"destroyed\" && hasChanged) {\n                            loadFn(ormWithCache, component.props).then((res) => {\n                                result.data = res;\n                            });\n                        }\n                    },\n                })\n                .call(...args);\n        }\n        return specialDataCaches[key];\n    };\n\n    /** @type {{ data: Record<string, T> }} */\n    const result = useState({ data: {} });\n    useRecordObserver(async (record, props) => {\n        result.data = await loadFn(ormWithCache, { ...props, record });\n    });\n    onWillUpdateProps(async (props) => {\n        // useRecordObserver callback is not called when the record doesn't change\n        if (props.record.id === component.props.record.id) {\n            result.data = await loadFn(ormWithCache, props);\n        }\n    });\n    return result;\n}\n\n//\n// Many2X\n//\n\nexport class Many2XAutocomplete extends Component {\n    static template = \"web.Many2XAutocomplete\";\n    static components = { AutoComplete };\n    static props = {\n        activeActions: Object,\n        autoSelect: { type: Boolean, optional: true },\n        autocomplete_container: { type: Function, optional: true },\n        autofocus: { type: Boolean, optional: true },\n        context: { type: Object, optional: true },\n        createAction: { type: Function, optional: true },\n        dropdown: { type: Boolean, optional: true },\n        fieldString: String,\n        getDomain: Function,\n        id: { type: String, optional: true },\n        isToMany: { type: Boolean, optional: true },\n        nameCreateField: { type: String, optional: true },\n        otherSources: { type: Array, optional: true },\n        placeholder: { type: String, optional: true },\n        quickCreate: { type: [Function, { value: null }], optional: true },\n        resModel: String,\n        searchLimit: { type: Number, optional: true },\n        searchMoreLabel: { type: String, optional: true },\n        searchMoreLimit: { type: Number, optional: true },\n        searchThreshold: { type: Number, optional: true },\n        setInputFloats: { type: Function, optional: true },\n        slots: { optional: true },\n        specification: { type: Object, optional: true },\n        update: Function,\n        value: { type: String, optional: true },\n    };\n    static defaultProps = {\n        context: {},\n        dropdown: true,\n        nameCreateField: \"name\",\n        otherSources: [],\n        quickCreate: null,\n        searchLimit: 7,\n        searchThreshold: 0,\n        searchMoreLimit: 320,\n        setInputFloats: () => {},\n        specification: {},\n        value: \"\",\n    };\n    setup() {\n        this.orm = useService(\"orm\");\n\n        this.autoCompleteContainer = useForwardRefToParent(\"autocomplete_container\");\n        const { activeActions, resModel, update, isToMany, fieldString } = this.props;\n\n        this.keepLast = new KeepLast();\n\n        this.openMany2X =\n            this.props.createAction ??\n            useOpenMany2XRecord({\n                resModel,\n                activeActions,\n                isToMany,\n                onRecordSaved: (record) => update([{ ...record.data, id: record.resId }]),\n                onRecordDiscarded: () => {\n                    if (!isToMany) {\n                        this.props.update(false);\n                    }\n                },\n                fieldString,\n                onClose: () => {\n                    const autoCompleteInput = this.autoCompleteContainer.el.querySelector(\"input\");\n\n                    // There are two cases:\n                    // 1. Value is the same as the input: it means the autocomplete has re-rendered with the right value\n                    //    This is in case we saved the record, triggering all the interface to update.\n                    // 2. Value is different from the input: it means the input has a manually entered value and nothing\n                    //    happened, that is, we discarded the changes\n                    if (this.props.value !== autoCompleteInput.value) {\n                        autoCompleteInput.value = \"\";\n                    }\n                    autoCompleteInput.focus();\n                },\n                component: this.createDialog,\n                size: this.createDialogSize,\n            });\n\n        this.selectCreate = useSelectCreate({\n            resModel,\n            activeActions,\n            onSelected: (resId) => {\n                const resIds = Array.isArray(resId) ? resId : [resId];\n                const values = resIds.map((id) => ({ id }));\n                return update(values);\n            },\n            onCreateEdit: ({ context }) => this.openMany2X({ context }),\n            onUnselect: isToMany ? undefined : () => update(),\n        });\n    }\n\n    get autoCompleteProps() {\n        return {\n            autocomplete: \"off\",\n            autoSelect: this.props.autoSelect,\n            autofocus: this.props.autofocus,\n            dropdown: this.props.dropdown,\n            id: this.props.id,\n            onCancel: this.onCancel.bind(this),\n            onChange: this.onChange.bind(this),\n            onInput: this.onInput.bind(this),\n            placeholder: this.props.placeholder,\n            resetOnSelect: this.props.value === \"\",\n            sources: this.sources,\n            slots: this.props.slots,\n            value: this.props.value,\n        };\n    }\n\n    get sources() {\n        return [this.optionsSource, ...this.props.otherSources];\n    }\n\n    get optionsSource() {\n        return {\n            placeholder: _t(\"Loading...\"),\n            options: this.loadOptionsSource.bind(this),\n            optionSlot: \"option\",\n        };\n    }\n\n    get activeActions() {\n        return this.props.activeActions || {};\n    }\n\n    get createDialog() {\n        return FormViewDialog;\n    }\n\n    get createDialogSize() {\n        return \"lg\";\n    }\n\n    getCreationContext(value) {\n        return makeContext([\n            this.props.context,\n            value && { [`default_${this.props.nameCreateField}`]: value },\n        ]);\n    }\n    onInput({ inputValue }) {\n        if (!this.props.value || this.props.value !== inputValue) {\n            this.props.setInputFloats(true);\n        }\n    }\n    onCancel() {\n        this.props.setInputFloats(false);\n    }\n\n    get searchSpecification() {\n        return {\n            display_name: {},\n            ...this.props.specification,\n        };\n    }\n\n    async search(name) {\n        const domain = this.props.getDomain();\n        const context = this.props.context;\n        if (\n            this.lastEmptySearch &&\n            deepEqual(this.lastEmptySearch.domain, domain) &&\n            deepEqual(this.lastEmptySearch.context, context) &&\n            (name.startsWith(this.lastEmptySearch.name) || name.length < this.props.searchThreshold)\n        ) {\n            return [];\n        }\n        const records = await this.orm.call(this.props.resModel, \"web_name_search\", [], {\n            name,\n            operator: \"ilike\",\n            domain,\n            limit: this.props.searchLimit + 1,\n            context,\n            specification: this.searchSpecification,\n        });\n        if (!records.length) {\n            this.lastEmptySearch = {\n                context,\n                domain,\n                name,\n            };\n        }\n        return records;\n    }\n\n    slowCreate(request) {\n        return this.openMany2X({\n            context: this.getCreationContext(request),\n            nextRecordsContext: this.props.context,\n        });\n    }\n\n    onQuickCreateError(error, request) {\n        if (\n            error instanceof RPCError &&\n            error.exceptionName === \"odoo.exceptions.ValidationError\"\n        ) {\n            return this.slowCreate(request);\n        } else {\n            throw error;\n        }\n    }\n\n    async loadOptionsSource(request) {\n        await this.keepLast.add(Promise.resolve());\n        return this.suggest(request, (promise) => this.keepLast.add(promise));\n    }\n\n    async suggest(request, lock) {\n        const suggestions = [];\n        /** @type {Record<string, any>[] | null} */\n        let records = null;\n\n        if (request.length < this.props.searchThreshold) {\n            if (this.addStartTypingSuggestion({ request, records })) {\n                suggestions.push(this.buildStartTypingSuggestion());\n            }\n        } else {\n            records = await lock(this.search(request));\n            if (records.length) {\n                for (const record of records) {\n                    suggestions.push(this.buildRecordSuggestion(request, record));\n                }\n            } else if (this.addNoRecordsSuggestion({ request, records })) {\n                suggestions.push(this.buildNoRecordsSuggestion());\n            } else if (this.addStartTypingSuggestion({ request, records })) {\n                suggestions.push(this.buildStartTypingSuggestion());\n            }\n        }\n\n        for (const action of this.actionSuggestions) {\n            const enabled = action.enabled ?? (() => true);\n            if (enabled({ request, records })) {\n                suggestions.push(action.build(request));\n            }\n        }\n\n        return suggestions;\n    }\n\n    get actionSuggestions() {\n        return [\n            {\n                // create\n                enabled: this.addCreateSuggestion.bind(this),\n                build: this.buildCreateSuggestion.bind(this),\n            },\n            {\n                // create and edit\n                enabled: this.addCreateEditSuggestion.bind(this),\n                build: this.buildCreateEditSuggestion.bind(this),\n            },\n            {\n                // search more\n                enabled: this.addSearchMoreSuggestion.bind(this),\n                build: this.buildSearchMoreSuggestion.bind(this),\n            },\n        ];\n    }\n\n    addCreateSuggestion({ request }) {\n        return !!this.props.quickCreate && request.length > 0;\n    }\n\n    addCreateEditSuggestion({ records, request }) {\n        return (\n            (this.activeActions.createEdit ?? this.activeActions.create) &&\n            (request.length > 0 || records?.length === 0)\n        );\n    }\n\n    addNoRecordsSuggestion({ request, records }) {\n        return !this.activeActions.createEdit && !this.props.quickCreate;\n    }\n\n    addSearchMoreSuggestion({ records, request }) {\n        return request.length < this.props.searchThreshold || records?.length > 0;\n    }\n\n    addStartTypingSuggestion({ request, records }) {\n        return records !== null\n            ? request.length === 0 && !this.activeActions.createEdit\n            : !this.props.value;\n    }\n\n    buildCreateSuggestion(request) {\n        return {\n            cssClass: \"o_m2o_dropdown_option o_m2o_dropdown_option_create\",\n            data: { slotName: \"createItem\" },\n            label: _t('Create \"%s\"', request),\n            onSelect: async () => {\n                try {\n                    await this.props.quickCreate(request);\n                } catch (e) {\n                    this.onQuickCreateError(e, request);\n                }\n            },\n        };\n    }\n\n    buildCreateEditSuggestion(request) {\n        return {\n            cssClass: \"o_m2o_dropdown_option o_m2o_dropdown_option_create_edit\",\n            data: { slotName: \"createEditItem\" },\n            label: request.length > 0 ? _t(\"Create and edit...\") : _t(\"Create...\"),\n            onSelect: () => this.slowCreate(request),\n        };\n    }\n\n    buildNoRecordsSuggestion() {\n        return {\n            cssClass: \"o_m2o_no_result\",\n            data: { slotName: \"noRecordsItem\" },\n            label: _t(\"No records\"),\n        };\n    }\n\n    buildRecordSuggestion(request, record) {\n        const label = record.__formatted_display_name || record.display_name;\n        return {\n            data: { record, slotName: \"autoCompleteItem\" },\n            label: label\n                ? highlightText(request, odoomark(label), \"text-primary fw-bold\")\n                : _t(\"Unnamed\"),\n            onSelect: () => this.props.update([record]),\n        };\n    }\n\n    buildSearchMoreSuggestion(request) {\n        return {\n            cssClass: \"o_m2o_dropdown_option o_m2o_dropdown_option_search_more\",\n            data: { slotName: \"searchMoreItem\" },\n            label: this.SearchMoreButtonLabel,\n            onSelect: this.onSearchMore.bind(this, request),\n        };\n    }\n\n    buildStartTypingSuggestion() {\n        return {\n            cssClass: \"o_m2o_start_typing\",\n            data: { slotName: \"startTypingItem\" },\n            label:\n                this.props.searchThreshold > 1\n                    ? _t(\"Start typing %s characters\", this.props.searchThreshold)\n                    : _t(\"Start typing...\"),\n        };\n    }\n\n    get SearchMoreButtonLabel() {\n        return this.props.searchMoreLabel ?? _t(\"Search more...\");\n    }\n\n    async onBarcodeSearch() {\n        const autoCompleteInput = this.autoCompleteContainer.el.querySelector(\"input\");\n        return this.onSearchMore(autoCompleteInput.value);\n    }\n\n    async onSearchMore(request) {\n        const { resModel, getDomain, context, fieldString } = this.props;\n\n        const domain = getDomain();\n        let dynamicFilters = [];\n        if (request.length) {\n            const nameGets = await this.orm.call(resModel, \"name_search\", [], {\n                name: request,\n                domain: domain,\n                operator: \"ilike\",\n                limit: this.props.searchMoreLimit,\n                context,\n            });\n\n            dynamicFilters = [\n                {\n                    description: _t(\"Quick search: %s\", request),\n                    domain: [[\"id\", \"in\", nameGets.map((nameGet) => nameGet[0])]],\n                },\n            ];\n        }\n\n        const title = _t(\"Search: %s\", fieldString);\n        this.selectCreate({\n            domain,\n            context,\n            filters: dynamicFilters,\n            title,\n        });\n    }\n\n    onChange({ inputValue }) {\n        if (!inputValue.length) {\n            this.props.update(false);\n        }\n    }\n}\n\nexport function useOpenMany2XRecord({\n    resModel,\n    onRecordSaved,\n    onRecordDiscarded,\n    fieldString,\n    activeActions,\n    isToMany,\n    onClose = (isNew) => {},\n    component = FormViewDialog,\n    size = \"lg\",\n}) {\n    const addDialog = useOwnedDialogs();\n    const orm = useService(\"orm\");\n\n    return async function openDialog(\n        { resId = false, forceModel = null, title, context, nextRecordsContext },\n        immediate = false\n    ) {\n        const model = forceModel || resModel;\n        let viewId;\n        if (resId !== false) {\n            viewId = await orm.call(model, \"get_formview_id\", [[resId]], {\n                context,\n            });\n        }\n\n        let resolve = () => {};\n        if (!title) {\n            title = resId ? _t(\"Open: %s\", fieldString) : _t(\"Create %s\", fieldString);\n        }\n\n        const { create: canCreate, write: canWrite } = activeActions;\n        const readonly = !(resId ? canWrite : canCreate);\n\n        addDialog(\n            component,\n            {\n                preventCreate: !canCreate,\n                preventEdit: !canWrite,\n                title,\n                context,\n                nextRecordsContext,\n                readonly,\n                resId,\n                resModel: model,\n                viewId,\n                onRecordSaved,\n                onRecordDiscarded,\n                isToMany,\n                size,\n            },\n            {\n                onClose: () => {\n                    resolve();\n                    const isNew = !resId;\n                    onClose(isNew);\n                },\n            }\n        );\n\n        if (!immediate) {\n            return new Promise((_resolve) => {\n                resolve = _resolve;\n            });\n        }\n    };\n}\n\n//\n// X2Many\n//\n\nexport class X2ManyFieldDialog extends Component {\n    static template = \"web.X2ManyFieldDialog\";\n    static components = { Dialog, FormRenderer, ViewButton };\n    static props = {\n        archInfo: Object,\n        close: Function,\n        record: Object,\n        addNew: Function,\n        save: Function,\n        title: String,\n        delete: { optional: true },\n        deleteButtonLabel: { optional: true },\n        config: Object,\n        controls: { type: Array, optional: true },\n    };\n    static defaultProps = {\n        controls: [],\n    };\n    setup() {\n        this.actionService = useService(\"action\");\n        this.archInfo = this.props.archInfo;\n        this.record = this.props.record;\n        this.title = this.props.title;\n        this.contentClass = computeViewClassName(\"form\", this.archInfo.xmlDoc);\n        useSubEnv({ config: this.props.config });\n        this.env.dialogData.dismiss = () => this.discard();\n\n        useBus(this.record.model.bus, \"update\", () => this.render(true));\n\n        this.modalRef = useChildRef();\n\n        const reload = () => this.record.load();\n\n        useViewButtons(this.modalRef, {\n            reload,\n            beforeExecuteAction: this.beforeExecuteActionButton.bind(this),\n        }); // maybe pass the model directly in props\n\n        this.readonly = this.record.resId && !this.archInfo.activeActions.edit;\n        this.canCreate = !this.record.resId;\n\n        if (this.archInfo.xmlDoc.querySelector(\"footer:not(field footer)\")) {\n            this.archInfo = { ...this.archInfo, xmlDoc: this.archInfo.xmlDoc.cloneNode(true) };\n            this.footerArchInfo = Object.assign({}, this.archInfo);\n            this.footerArchInfo.xmlDoc = createElement(\"t\");\n            this.footerArchInfo.xmlDoc.append(\n                ...this.archInfo.xmlDoc.querySelectorAll(\"footer:not(field footer)\")\n            );\n            this.footerArchInfo.arch = this.footerArchInfo.xmlDoc.outerHTML;\n            this.archInfo.arch = this.archInfo.xmlDoc.outerHTML;\n        }\n\n        const { autofocusFieldIds, disableAutofocus } = this.archInfo;\n        if (!disableAutofocus) {\n            // to simplify\n            useEffect(\n                (isInEdition) => {\n                    let elementToFocus;\n                    if (isInEdition) {\n                        for (const id of autofocusFieldIds) {\n                            elementToFocus = this.modalRef.el.querySelector(`#${id}`);\n                            if (elementToFocus) {\n                                break;\n                            }\n                        }\n                        elementToFocus =\n                            elementToFocus ||\n                            this.modalRef.el.querySelector(\".o_field_widget input\");\n                    } else {\n                        elementToFocus = this.modalRef.el.querySelector(\"button.btn-primary\");\n                    }\n                    if (elementToFocus) {\n                        elementToFocus.focus();\n                    } else {\n                        this.modalRef.el.focus();\n                    }\n                },\n                () => [this.record.isInEdition]\n            );\n        }\n        useFormViewInDialog();\n    }\n\n    get dialogProps() {\n        const props = {\n            title: this.title,\n            withBodyPadding: false,\n            modalRef: this.modalRef,\n            contentClass: this.contentClass,\n        };\n        if (!this.record.isNew) {\n            props.onExpand = async () => {\n                await this.save({ saveAndNew: false });\n                this.actionService.doAction({\n                    type: \"ir.actions.act_window\",\n                    res_model: this.props.record.resModel,\n                    res_id: this.props.record.resId,\n                    views: [[false, \"form\"]],\n                });\n            };\n        }\n        return props;\n    }\n\n    get displayDeleteButton() {\n        const deleteControl = this.props.controls.find((control) => control.type === \"delete\");\n        return (\n            !deleteControl || !evaluateBooleanExpr(deleteControl.invisible, this.record.evalContext)\n        );\n    }\n\n    async beforeExecuteActionButton(clickParams) {\n        if (clickParams.special !== \"cancel\") {\n            return this.record.save();\n        }\n    }\n\n    async discard() {\n        if (this.record.isInEdition) {\n            await this.record.discard();\n        }\n        this.props.close();\n    }\n\n    save({ saveAndNew }) {\n        return executeButtonCallback(this.modalRef.el, async () => {\n            if (await this.record.checkValidity({ displayNotification: true })) {\n                await this.props.save(this.record);\n                if (saveAndNew) {\n                    await this.record.switchMode(\"readonly\");\n                    this.record = await this.props.addNew();\n                }\n            } else {\n                return false;\n            }\n            if (!saveAndNew) {\n                this.props.close();\n            }\n            return true;\n        });\n    }\n\n    async remove() {\n        await this.props.delete();\n        this.props.close();\n    }\n\n    async saveAndNew() {\n        const saved = await this.save({ saveAndNew: true });\n        if (saved) {\n            if (this.title) {\n                this.title = this.title.replace(_t(\"Open:\"), _t(\"New:\"));\n            }\n            this.render(true);\n        }\n    }\n}\n\nasync function getFormViewInfo({ list, context, activeField, viewService, env }) {\n    let formArchInfo = activeField.views.form;\n    let fields = activeField.fields;\n    const comodel = list.resModel;\n    if (!formArchInfo) {\n        const {\n            fields: formFields,\n            relatedModels,\n            views,\n        } = await viewService.loadViews({\n            context: makeContext([list.context, context]),\n            resModel: comodel,\n            views: [[false, \"form\"]],\n        });\n        const xmlDoc = parseXML(views.form.arch);\n        formArchInfo = new FormArchParser().parse(xmlDoc, relatedModels, comodel);\n        // Fields that need to be defined are the ones in the form view, this is natural,\n        // plus the ones that the list record has, that is, present in either the list arch\n        // or the kanban arch of the one2many field\n        fields = { ...list.fields, ...formFields }; // FIXME: update in place?\n    }\n\n    await loadSubViews(\n        formArchInfo.fieldNodes,\n        fields,\n        {}, // context\n        comodel,\n        viewService,\n        env.isSmall\n    );\n\n    return { archInfo: formArchInfo, fields };\n}\n\nexport function useAddInlineRecord({ addNew }) {\n    let creatingRecord = false;\n\n    async function addInlineRecord({ context, editable }) {\n        if (!creatingRecord) {\n            creatingRecord = true;\n            try {\n                await addNew({ context, mode: \"edit\", position: editable });\n            } finally {\n                creatingRecord = false;\n            }\n        }\n    }\n    return addInlineRecord;\n}\n\nexport function useOpenX2ManyRecord({\n    activeField, // TODO: this should be renamed (object with keys \"viewMode\", \"views\" and \"string\")\n    activeActions,\n    getList,\n    updateRecord,\n    saveRecord,\n    isMany2Many,\n}) {\n    const viewService = useService(\"view\");\n    const env = useEnv();\n    const component = useComponent();\n\n    const addDialog = useOwnedDialogs();\n    const viewMode = activeField.viewMode;\n\n    async function openRecord({ record, readonly, context, title, controls, onClose }) {\n        if (!title) {\n            title = record\n                ? _t(\"Open: %s\", activeField.string)\n                : _t(\"Create %s\", activeField.string);\n        }\n        const list = getList();\n        const { archInfo, fields: _fields } = await getFormViewInfo({\n            list,\n            context,\n            activeField,\n            viewService,\n            env,\n        });\n        if (!component.props.record.isInEdition) {\n            archInfo.activeActions.edit = false;\n        }\n\n        const { activeFields, fields } = extractFieldsFromArchInfo(archInfo, _fields);\n\n        let deleteRecord;\n        let deleteButtonLabel = undefined;\n        const isDuplicate = !!record;\n\n        const params = { activeFields, fields };\n        if (isMany2Many) {\n            params.mode = activeActions.write ? \"edit\" : \"readonly\";\n        } else {\n            params.mode = readonly || !activeActions.write ? \"readonly\" : \"edit\";\n        }\n        if (record) {\n            const { delete: canDelete, onDelete } = activeActions;\n            deleteRecord = viewMode === \"kanban\" && canDelete ? () => onDelete(record) : null;\n            deleteButtonLabel = activeActions.type === \"one2many\" ? _t(\"Delete\") : _t(\"Remove\");\n        } else {\n            params.context = makeContext([list.context, context]);\n            params.withoutParent = isMany2Many;\n        }\n        record = await list.extendRecord(params, record);\n\n        const _onClose = () => {\n            list.editedRecord?.switchMode(\"readonly\");\n            onClose?.();\n        };\n\n        addDialog(\n            X2ManyFieldDialog,\n            {\n                config: env.config,\n                archInfo,\n                record,\n                controls,\n                addNew: () => getList().extendRecord(params),\n                save: (rec) => {\n                    if (isDuplicate && rec.id === record.id) {\n                        return updateRecord(rec);\n                    } else {\n                        return saveRecord(rec);\n                    }\n                },\n                title,\n                delete: deleteRecord,\n                deleteButtonLabel: deleteButtonLabel,\n            },\n            { onClose: _onClose }\n        );\n    }\n\n    let recordIsOpen = false;\n    return (params) => {\n        if (recordIsOpen) {\n            return;\n        }\n        recordIsOpen = true;\n\n        const onClose = params.onClose;\n        params = {\n            ...params,\n            onClose: (...args) => {\n                recordIsOpen = false;\n                if (onClose) {\n                    return onClose(...args);\n                }\n            },\n        };\n\n        try {\n            return openRecord(params);\n        } catch (e) {\n            recordIsOpen = false;\n            throw e;\n        }\n    };\n}\n\nexport function useX2ManyCrud(getList, isMany2Many) {\n    let saveRecord; // FIXME: isn't this \"createRecord\" instead?\n    if (isMany2Many) {\n        saveRecord = async (object) => {\n            const list = getList();\n            if (Array.isArray(object)) {\n                return list.addAndRemove({ add: object });\n            } else {\n                // object instanceof Record\n                await object.save({ reload: false });\n                return list.linkTo(object.resId);\n            }\n        };\n    } else {\n        saveRecord = async (record) => getList().validateExtendedRecord(record);\n    }\n\n    const updateRecord = async (record) => {\n        if (isMany2Many) {\n            await record.save();\n        }\n        return getList().validateExtendedRecord(record);\n    };\n\n    const removeRecord = (record) => {\n        const list = getList();\n        if (isMany2Many) {\n            return list.forget(record);\n        }\n        return list.delete(record);\n    };\n\n    return {\n        saveRecord,\n        updateRecord,\n        removeRecord,\n    };\n}\n", "import { Component } from \"@odoo/owl\";\nimport { evaluateExpr } from \"@web/core/py_js/py\";\nimport { getClassNameFromDecoration } from \"@web/views/utils\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { DateTimeField } from \"../datetime/datetime_field\";\nimport { standardFieldProps } from \"../standard_field_props\";\nimport { capitalize } from \"@web/core/utils/strings\";\nimport { formatDate } from \"../formatters\";\n\nconst { DateTime } = luxon;\n\nexport class RemainingDaysField extends Component {\n    static components = { DateTimeField };\n\n    static props = {\n        ...standardFieldProps,\n        classes: { type: Object, optional: true },\n    };\n\n    static defaultProps = {\n        classes: {\n            bf: \"days <= 0\",\n            danger: \"days < 0\",\n            warning: \"days == 0\",\n        },\n    };\n\n    static template = \"web.RemainingDaysField\";\n\n    get diffDays() {\n        const { record, name } = this.props;\n        const value = record.data[name];\n        if (!value) {\n            return null;\n        }\n        const today = DateTime.local().startOf(\"day\");\n        const diff = value.startOf(\"day\").diff(today, \"days\");\n        return Math.floor(diff.days);\n    }\n\n    get diffString() {\n        if (this.diffDays === null) {\n            return \"\";\n        }\n        if (Math.abs(this.diffDays) > 99) {\n            return this.formattedValue;\n        }\n        const { record, name } = this.props;\n        const value = record.data[name];\n        return capitalize(value.toRelativeCalendar());\n    }\n\n    get formattedValue() {\n        const { record, name } = this.props;\n        return formatDate(record.data[name]);\n    }\n\n    get numericValue() {\n        const { record, name } = this.props;\n        return formatDate(record.data[name], { numeric: true });\n    }\n\n    get classNames() {\n        if (this.diffDays === null) {\n            return null;\n        }\n        if (!this.props.record.isActive) {\n            return null;\n        }\n        const classNames = {};\n        const evalContext = { days: this.diffDays, record: this.props.record.evalContext };\n        for (const decoration in this.props.classes) {\n            const value = evaluateExpr(this.props.classes[decoration], evalContext);\n            classNames[getClassNameFromDecoration(decoration)] = value;\n        }\n        return classNames;\n    }\n\n    get dateTimeFieldProps() {\n        return Object.fromEntries(\n            Object.entries(this.props).filter(([key]) => standardFieldProps[key])\n        );\n    }\n}\n\nexport const remainingDaysField = {\n    component: RemainingDaysField,\n    displayName: _t(\"Remaining Days\"),\n    supportedTypes: [\"date\", \"datetime\"],\n    extractProps: ({ options }) => ({\n        classes: options.classes,\n    }),\n};\n\nregistry.category(\"fields\").add(\"remaining_days\", remainingDaysField);\n", "import { registry } from \"@web/core/registry\";\nimport { SelectionField, selectionField } from \"@web/views/fields/selection/selection_field\";\n\n/**\n * The purpose of this field is to be able to define some values which should not be\n * displayed on our selection field, this way we can have multiple views for the same model\n * that uses different possible sets of values on the same selection field.\n */\nexport class FilterableSelectionField extends SelectionField {\n    static props = {\n        ...SelectionField.props,\n        whitelist_fname: { type: String, optional: true },\n        whitelisted_values: { type: Array, optional: true },\n        blacklisted_values: { type: Array, optional: true },\n    };\n\n    /**\n     * @override\n     */\n    get options() {\n        let options = super.options;\n        if (this.props.whitelist_fname) {\n            options = options.filter((option) => {\n                return (\n                    option[0] === this.props.record.data[this.props.name] ||\n                    this.props.record.data[this.props.whitelist_fname].includes(option[0])\n                );\n            });\n        } else if (this.props.whitelisted_values) {\n            options = options.filter((option) => {\n                return (\n                    option[0] === this.props.record.data[this.props.name] ||\n                    this.props.whitelisted_values.includes(option[0])\n                );\n            });\n        } else if (this.props.blacklisted_values) {\n            options = options.filter((option) => {\n                return (\n                    option[0] === this.props.record.data[this.props.name] ||\n                    !this.props.blacklisted_values.includes(option[0])\n                );\n            });\n        }\n        return options;\n    }\n}\n\nexport const filterableSelectionField = {\n    ...selectionField,\n    component: FilterableSelectionField,\n    supportedOptions: [\n        {\n            label: \"Whitelisted Values\",\n            name: \"whitelisted_values\",\n            type: \"string\",\n        },\n        {\n            label: \"Blacklisted Values\",\n            name: \"blacklisted_values\",\n            type: \"string\",\n        },\n        {\n            label: \"Whitelisted field name\",\n            name: \"whitelist_fname\",\n            type: \"string\",\n        },\n    ],\n    extractProps({ options }) {\n        const props = selectionField.extractProps(...arguments);\n        props.whitelist_fname = options.whitelist_fname;\n        props.whitelisted_values = options.whitelisted_values;\n        props.blacklisted_values = options.blacklisted_values;\n        return props;\n    },\n};\n\nregistry.category(\"fields\").add(\"filterable_selection\", filterableSelectionField);\n", "import { Component } from \"@odoo/owl\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { SelectMenu } from \"@web/core/select_menu/select_menu\";\nimport { getFieldDomain } from \"@web/model/relational_model/utils\";\nimport { useSpecialData } from \"@web/views/fields/relational_utils\";\nimport { hasTouch } from \"@web/core/browser/feature_detection\";\nimport { standardFieldProps } from \"../standard_field_props\";\n\nexport class SelectionField extends Component {\n    static components = {\n        SelectMenu,\n    };\n    static template = \"web.SelectionField\";\n    static props = {\n        ...standardFieldProps,\n        placeholder: { type: String, optional: true },\n        required: { type: Boolean, optional: true },\n        domain: { type: [Array, Function], optional: true },\n        autosave: { type: Boolean, optional: true },\n    };\n    static defaultProps = {\n        autosave: false,\n    };\n\n    setup() {\n        this.type = this.props.record.fields[this.props.name].type;\n        if (this.type === \"many2one\") {\n            this.specialData = useSpecialData((orm, props) => {\n                const { relation } = props.record.fields[props.name];\n                const domain = getFieldDomain(props.record, props.name, props.domain);\n                return orm.call(relation, \"name_search\", [\"\", domain]);\n            });\n        }\n    }\n\n    get choices() {\n        return this.options.map(([value, label]) => ({ value, label }));\n    }\n    get isBottomSheet() {\n        return this.env.isSmall && hasTouch();\n    }\n    get options() {\n        switch (this.type) {\n            case \"many2one\":\n                return [...this.specialData.data];\n            case \"selection\":\n                return this.props.record.fields[this.props.name].selection.filter(\n                    (option) => option[1] !== \"\"\n                );\n            default:\n                return [];\n        }\n    }\n    get string() {\n        switch (this.type) {\n            case \"many2one\":\n                return this.props.record.data[this.props.name]\n                    ? this.props.record.data[this.props.name].display_name\n                    : \"\";\n            case \"selection\":\n                return this.props.record.data[this.props.name] !== false\n                    ? this.options.find((o) => o[0] === this.props.record.data[this.props.name])[1]\n                    : \"\";\n            default:\n                return \"\";\n        }\n    }\n    get value() {\n        const rawValue = this.props.record.data[this.props.name];\n        return this.type === \"many2one\" && rawValue ? rawValue.id : rawValue;\n    }\n\n    stringify(value) {\n        return JSON.stringify(value);\n    }\n\n    onChange(value) {\n        switch (this.type) {\n            case \"many2one\":\n                if (value === null) {\n                    this.props.record.update(\n                        { [this.props.name]: false },\n                        { save: this.props.autosave }\n                    );\n                } else {\n                    const option = this.options.find((option) => option[0] === value);\n                    this.props.record.update(\n                        {\n                            [this.props.name]: { id: option[0], display_name: option[1] },\n                        },\n                        { save: this.props.autosave }\n                    );\n                }\n                break;\n            case \"selection\":\n                this.props.record.update(\n                    { [this.props.name]: value ?? false },\n                    { save: this.props.autosave }\n                );\n                break;\n        }\n    }\n}\n\nexport const selectionField = {\n    component: SelectionField,\n    displayName: _t(\"Selection\"),\n    supportedOptions: [\n        {\n            label: _t(\"Dynamic Placeholder\"),\n            name: \"placeholder_field\",\n            type: \"field\",\n            availableTypes: [\"char\"],\n        },\n    ],\n    supportedTypes: [\"many2one\", \"selection\"],\n    isEmpty: (record, fieldName) => record.data[fieldName] === false,\n    extractProps({ viewType, placeholder }, dynamicInfo) {\n        const props = {\n            autosave: viewType === \"kanban\",\n            placeholder,\n            required: dynamicInfo.required,\n            domain: dynamicInfo.domain,\n        };\n        if (viewType === \"kanban\") {\n            props.readonly = dynamicInfo.readonly;\n        }\n        return props;\n    },\n};\n\nregistry.category(\"fields\").add(\"selection\", selectionField);\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { SignatureDialog } from \"@web/core/signature/signature_dialog\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { imageUrl } from \"@web/core/utils/urls\";\nimport { isBinarySize } from \"@web/core/utils/binary\";\nimport { fileTypeMagicWordMap } from \"@web/views/fields/image/image_field\";\nimport { standardFieldProps } from \"@web/views/fields/standard_field_props\";\n\nimport { Component, useState } from \"@odoo/owl\";\n\nconst placeholder = \"/web/static/img/placeholder.png\";\n\nexport class SignatureField extends Component {\n    static template = \"web.SignatureField\";\n    static props = {\n        ...standardFieldProps,\n        defaultFont: { type: String },\n        fullName: { type: String, optional: true },\n        height: { type: Number, optional: true },\n        previewImage: { type: String, optional: true },\n        width: { type: Number, optional: true },\n        type: { validate: (t) => [\"initial\", \"signature\"].includes(t), optional: true },\n    };\n    static defaultProps = {\n        type: \"signature\",\n    };\n\n    setup() {\n        this.displaySignatureRatio = 3;\n\n        this.dialogService = useService(\"dialog\");\n        this.state = useState({\n            isValid: true,\n        });\n    }\n\n    get rawCacheKey() {\n        return this.props.record.data.write_date;\n    }\n\n    get getUrl() {\n        const { name, previewImage, record } = this.props;\n        if (this.state.isValid && this.value) {\n            if (isBinarySize(this.value)) {\n                return imageUrl(record.resModel, record.resId, previewImage || name, {\n                    unique: this.rawCacheKey,\n                });\n            } else {\n                // Use magic-word technique for detecting image type\n                const magic = fileTypeMagicWordMap[this.value[0]] || \"png\";\n                return `data:image/${magic};base64,${this.props.record.data[this.props.name]}`;\n            }\n        }\n        return placeholder;\n    }\n\n    get sizeStyle() {\n        let { width, height } = this.props;\n\n        if (!this.value) {\n            if (width && height) {\n                width = Math.min(width, this.displaySignatureRatio * height);\n                height = width / this.displaySignatureRatio;\n            } else if (width) {\n                height = width / this.displaySignatureRatio;\n            } else if (height) {\n                width = height * this.displaySignatureRatio;\n            }\n        }\n\n        let style = \"\";\n        if (width) {\n            style += `width:${width}px; max-width:${width}px;`;\n        }\n        if (height) {\n            style += `height:${height}px; max-height:${height}px;`;\n        }\n        return style;\n    }\n\n    get value() {\n        return this.props.record.data[this.props.name];\n    }\n\n    onClickSignature() {\n        if (!this.props.readonly) {\n            const nameAndSignatureProps = {\n                displaySignatureRatio: 3,\n                signatureType: this.props.type,\n                noInputName: true,\n            };\n            const { fullName, record } = this.props;\n            let defaultName = \"\";\n            if (fullName) {\n                let signName;\n                const fullNameData = record.data[fullName];\n                if (record.fields[fullName].type === \"many2one\") {\n                    // If m2o is empty, it will have falsy value in recordData\n                    signName = fullNameData && fullNameData.display_name;\n                } else {\n                    signName = fullNameData;\n                }\n                defaultName = signName === \"\" ? undefined : signName;\n            }\n\n            nameAndSignatureProps.defaultFont = this.props.defaultFont;\n\n            const dialogProps = {\n                defaultName,\n                nameAndSignatureProps,\n                uploadSignature: (signature) => this.uploadSignature(signature),\n            };\n            this.dialogService.add(SignatureDialog, dialogProps);\n        }\n    }\n\n    onLoadFailed() {\n        this.state.isValid = false;\n        this.notification.add(_t(\"Could not display the selected image\"), {\n            type: \"danger\",\n        });\n    }\n\n    /**\n     * Upload the signature image if valid and close the dialog.\n     *\n     * @private\n     */\n    uploadSignature({ signatureImage }) {\n        return this.props.record.update({\n            [this.props.name]: signatureImage.split(\",\")[1] || false,\n        });\n    }\n}\n\nexport const signatureField = {\n    component: SignatureField,\n    fieldDependencies: [{ name: \"write_date\", type: \"datetime\" }],\n    supportedTypes: [\"binary\"],\n    supportedOptions: [\n        {\n            label: _t(\"Prefill with\"),\n            name: \"full_name\",\n            type: \"field\",\n            availableTypes: [\"char\", \"many2one\"],\n            help: _t(\"The selected field will be used to pre-fill the signature\"),\n        },\n        {\n            label: _t(\"Default font\"),\n            name: \"default_font\",\n            type: \"string\",\n        },\n        {\n            label: _t(\"Size\"),\n            name: \"size\",\n            type: \"selection\",\n            choices: [\n                { label: _t(\"Small\"), value: \"[0,90]\" },\n                { label: _t(\"Medium\"), value: \"[0,180]\" },\n                { label: _t(\"Large\"), value: \"[0,270]\" },\n            ],\n        },\n        {\n            label: _t(\"Preview image field\"),\n            name: \"preview_image\",\n            type: \"field\",\n            availableTypes: [\"binary\"],\n        },\n    ],\n    extractProps: ({ attrs, options }) => ({\n        defaultFont: options.default_font || \"\",\n        fullName: options.full_name,\n        height: options.size ? options.size[1] || undefined : attrs.height,\n        previewImage: options.preview_image,\n        type: options.type,\n        width: options.size ? options.size[0] || undefined : attrs.width,\n    }),\n};\n\nregistry.category(\"fields\").add(\"signature\", signatureField);\n", "/**\n * @typedef StandardFieldProps\n * @property {string} [id]\n * @property {string} name\n * @property {boolean} [readonly]\n * @property {import(\"@web/model/relational_model/record\").Record} record\n */\n\nexport const standardFieldProps = {\n    id: { type: String, optional: true },\n    name: { type: String },\n    readonly: { type: Boolean, optional: true },\n    record: { type: Object },\n};\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { exprToBoolean } from \"@web/core/utils/strings\";\nimport { standardFieldProps } from \"../standard_field_props\";\n\nimport { Component } from \"@odoo/owl\";\nconst formatters = registry.category(\"formatters\");\n\nexport class StatInfoField extends Component {\n    static template = \"web.StatInfoField\";\n    static props = {\n        ...standardFieldProps,\n        labelField: { type: String, optional: true },\n        noLabel: { type: Boolean, optional: true },\n        digits: { type: Array, optional: true },\n        string: { type: String, optional: true },\n    };\n\n    get formattedValue() {\n        const field = this.props.record.fields[this.props.name];\n        const formatter = formatters.get(field.type);\n        return formatter(this.props.record.data[this.props.name], {\n            digits: this.props.digits,\n            field,\n        });\n    }\n    get label() {\n        return this.props.labelField\n            ? this.props.record.data[this.props.labelField]\n            : this.props.string;\n    }\n}\n\nexport const statInfoField = {\n    component: StatInfoField,\n    displayName: _t(\"Stat Info\"),\n    supportedOptions: [\n        {\n            label: _t(\"Label field\"),\n            name: \"label_field\",\n            type: \"field\",\n            availableTypes: [\"char\"],\n        },\n    ],\n    supportedTypes: [\"float\", \"integer\", \"monetary\", \"char\", \"one2many\", \"many2one\"],\n    isEmpty: () => false,\n    extractProps: ({ attrs, options, string }) => {\n        // Sadly, digits param was available as an option and an attr.\n        // The option version could be removed with some xml refactoring.\n        let digits;\n        if (attrs.digits) {\n            digits = JSON.parse(attrs.digits);\n        } else if (options.digits) {\n            digits = options.digits;\n        }\n\n        return {\n            digits,\n            labelField: options.label_field,\n            noLabel: exprToBoolean(attrs.nolabel),\n            string,\n        };\n    },\n};\n\nregistry.category(\"fields\").add(\"statinfo\", statInfoField);\n", "import { Component } from \"@odoo/owl\";\nimport { useCommand } from \"@web/core/commands/command_hook\";\nimport { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { CheckboxItem } from \"@web/core/dropdown/checkbox_item\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { formatSelection } from \"../formatters\";\nimport { standardFieldProps } from \"../standard_field_props\";\n\nexport class StateSelectionField extends Component {\n    static template = \"web.StateSelectionField\";\n    static components = {\n        Dropdown,\n        CheckboxItem,\n    };\n    static props = {\n        ...standardFieldProps,\n        showLabel: { type: Boolean, optional: true },\n        withCommand: { type: Boolean, optional: true },\n        autosave: { type: Boolean, optional: true },\n    };\n    static defaultProps = {\n        showLabel: true,\n    };\n\n    setup() {\n        this.colorPrefix = \"o_status_\";\n        this.colors = {\n            blocked: \"red\",\n            done: \"green\",\n        };\n        if (this.props.withCommand) {\n            const hotkeys = [\"D\", \"F\", \"G\"];\n            for (const [index, [value, label]] of this.options.entries()) {\n                useCommand(\n                    _t(\"Set kanban state as %s\", label),\n                    () => {\n                        this.updateRecord(value);\n                    },\n                    {\n                        category: \"smart_action\",\n                        hotkey: hotkeys[index] && \"alt+\" + hotkeys[index],\n                        isAvailable: () => this.props.record.data[this.props.name] !== value,\n                    }\n                );\n            }\n        }\n    }\n    get options() {\n        return this.props.record.fields[this.props.name].selection.map(([state, label]) => {\n            return [state, this.props.record.data[`legend_${state}`] || label];\n        });\n    }\n    get currentValue() {\n        return this.props.record.data[this.props.name] || this.options[0][0];\n    }\n    get label() {\n        if (\n            this.props.record.data[this.props.name] &&\n            this.props.record.data[`legend_${this.props.record.data[this.props.name][0]}`]\n        ) {\n            return this.props.record.data[`legend_${this.props.record.data[this.props.name][0]}`];\n        }\n        return formatSelection(this.currentValue, { selection: this.options });\n    }\n\n    statusColor(value) {\n        return this.colors[value] ? this.colorPrefix + this.colors[value] : \"\";\n    }\n\n    async updateRecord(value) {\n        await this.props.record.update({ [this.props.name]: value }, { save: this.props.autosave });\n    }\n}\n\nexport const stateSelectionField = {\n    component: StateSelectionField,\n    displayName: _t(\"Label Selection\"),\n    supportedOptions: [\n        {\n            label: _t(\"Autosave\"),\n            name: \"autosave\",\n            type: \"boolean\",\n            default: true,\n            help: _t(\n                \"If checked, the record will be saved immediately when the field is modified.\"\n            ),\n        },\n        {\n            label: _t(\"Hide label\"),\n            name: \"hide_label\",\n            type: \"boolean\",\n        },\n    ],\n    supportedTypes: [\"selection\"],\n    extractProps({ options, viewType }, dynamicInfo) {\n        return {\n            showLabel: 'hide_label' in options ? !options.hide_label : false,\n            withCommand: viewType === \"form\",\n            readonly: dynamicInfo.readonly,\n            autosave: \"autosave\" in options ? !!options.autosave : true,\n        };\n    },\n};\n\nregistry.category(\"fields\").add(\"state_selection\", stateSelectionField);\n", "import { Component, onWillRender, useEffect, useExternalListener, useRef } from \"@odoo/owl\";\nimport { useCommand } from \"@web/core/commands/command_hook\";\nimport { Domain } from \"@web/core/domain\";\nimport { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { groupBy } from \"@web/core/utils/arrays\";\nimport { throttleForAnimation } from \"@web/core/utils/timing\";\nimport { getFieldDomain } from \"@web/model/relational_model/utils\";\nimport { useSpecialData } from \"@web/views/fields/relational_utils\";\nimport { standardFieldProps } from \"../standard_field_props\";\n\n/**\n * @typedef {import(\"../standard_field_props\").StandardFieldProps & {\n *  domain?: [Array, Function];\n *  foldField?: string;\n *  isDisabled?: boolean;\n *  visibleSelection?: string[];\n *  withCommand?: boolean;\n * }} StatusBarFieldProps\n *\n * @typedef StatusBarItem\n * @property {number} value\n * @property {string} label\n * @property {boolean} isFolded\n * @property {boolean} isSelected\n *\n * @typedef StatusBarList\n * @property {string} label\n * @property {StatusBarItem[]} items\n */\n\n/**\n * @param {...HTMLElement} els\n */\nconst hide = (...els) => els.forEach((el) => el.classList.add(\"d-none\"));\n\n/**\n * @param {...HTMLElement} els\n */\nconst show = (...els) => els.forEach((el) => el.classList.remove(\"d-none\"));\n\n/** @extends {Component<StatusBarFieldProps>} */\nexport class StatusBarField extends Component {\n    static template = \"web.StatusBarField\";\n    static components = {\n        Dropdown,\n        DropdownItem,\n    };\n    static props = {\n        ...standardFieldProps,\n        domain: { type: [Array, Function], optional: true },\n        foldField: { type: String, optional: true },\n        isDisabled: { type: Boolean, optional: true },\n        visibleSelection: { type: Array, element: String, optional: true },\n        withCommand: { type: Boolean, optional: true },\n    };\n\n    setup() {\n        // Properties\n        this.items = {};\n        this.beforeRef = useRef(\"before\");\n        this.rootRef = useRef(\"root\");\n        this.afterRef = useRef(\"after\");\n        this.dropdownRef = useRef(\"dropdown\");\n\n        // Resize listeners\n        let status = \"idle\";\n        const adjust = () => {\n            status = \"adjusting\";\n            this.adjustVisibleItems();\n            this.render();\n        };\n\n        useEffect(() => {\n            if (status === \"shouldAdjust\") {\n                adjust();\n            }\n        });\n\n        let forceRecomputeItems = false;\n        onWillRender(() => {\n            if (status !== \"adjusting\" || forceRecomputeItems) {\n                Object.assign(this.items, this.getSortedItems());\n                status = \"shouldAdjust\";\n            } else {\n                status = \"idle\";\n            }\n            forceRecomputeItems = false;\n        });\n\n        useExternalListener(window, \"resize\", throttleForAnimation(adjust));\n\n        // Special data\n        if (this.field.type === \"many2one\") {\n            this.specialData = useSpecialData(async (orm, props) => {\n                const { foldField, name: fieldName, record } = props;\n                const { relation } = record.fields[fieldName];\n                const fieldNames = [\"display_name\"];\n                if (foldField) {\n                    fieldNames.push(foldField);\n                }\n                const value = record.data[fieldName];\n                let domain = getFieldDomain(record, fieldName, props.domain);\n                domain = Domain.and([this.getDomain(), domain]).toList();\n                if (domain.length && value) {\n                    domain = Domain.or([[[\"id\", \"=\", value.id]], domain]).toList(\n                        record.evalContext\n                    );\n                }\n                const res = orm.searchRead(relation, domain, fieldNames);\n                forceRecomputeItems = true;\n                return res;\n            });\n        }\n\n        // Command palette\n        if (this.props.withCommand) {\n            const moveToCommandName = _t(\"Move to %s...\", this.field.string);\n            useCommand(\n                moveToCommandName,\n                () => ({\n                    placeholder: moveToCommandName,\n                    providers: [\n                        {\n                            provide: () =>\n                                this.getAllItems().map((item) => ({\n                                    name: item.label,\n                                    action: () => this.selectItem(item),\n                                })),\n                        },\n                    ],\n                }),\n                {\n                    category: \"smart_action\",\n                    hotkey: \"alt+shift+x\",\n                    isAvailable: () => !this.props.isDisabled,\n                }\n            );\n            useCommand(\n                _t(\"Move to next %s\", this.field.string),\n                () => {\n                    const items = this.getAllItems();\n                    const nextIndex = items.findIndex((item) => item.isSelected) + 1;\n                    this.selectItem(items[nextIndex]);\n                },\n                {\n                    category: \"smart_action\",\n                    hotkey: \"alt+x\",\n                    isAvailable: () => {\n                        if (this.props.isDisabled) {\n                            return false;\n                        }\n                        const items = this.getAllItems();\n                        return items.length && !items.at(-1).isSelected;\n                    },\n                }\n            );\n        }\n    }\n\n    /**\n     * @returns {{ selection?: [string, string][], string: string, type: \"many2one\" | \"selection\" }}\n     */\n    get field() {\n        return this.props.record.fields[this.props.name];\n    }\n\n    /**\n     * Override this to force a dynamic domain on the records\n     */\n    getDomain() {\n        return [];\n    }\n\n    /**\n     * Determines what items must be visible and how they must be displayed.\n     * There are 4 main scenarios:\n     *\n     * 1. All items can be displayed inline, no modification in the UI;\n     *\n     * The following scenarios imply that the viewport is too small to display\n     * all items in one line. Adjustments are made incrementally:\n     *\n     * 2. Items up to 1 before the currently selected item are combined in a dropdown;\n     *\n     * 3. Items up to 1 after the currently selected item are combined in a dropdown,\n     * along with the initially folded items;\n     *\n     * 4. If that still doesn't suffice: all items are combined in a single dropdown.\n     */\n    adjustVisibleItems() {\n        // Get all visible buttons\n        const itemEls = [\n            ...this.rootRef.el.querySelectorAll(\".o_arrow_button:not(.dropdown-toggle)\"),\n        ];\n        const selectedIndex = itemEls.findIndex((el) =>\n            el.classList.contains(\"o_arrow_button_current\")\n        );\n        const itemsBefore = itemEls.slice(selectedIndex + 2).reverse();\n        const itemsAfter = itemEls.slice(0, Math.max(selectedIndex - 1, 0)).reverse();\n\n        // Reset hidden elements\n        show(...itemEls);\n        hide(this.dropdownRef.el, this.beforeRef.el);\n        if (this.items.folded.length) {\n            show(this.afterRef.el);\n            itemEls.forEach((el) => el.classList.remove(\"o_first\"));\n        } else {\n            hide(this.afterRef.el);\n            itemEls[0]?.classList.add(\"o_first\");\n        }\n\n        // Reset items variables\n        this.items.before = [];\n        this.items.after = [...this.items.folded];\n        const itemsToAssign = this.getAllItems().filter((item) => !item.isFolded);\n\n        if (this.env.isSmall && this.items.inline.length) {\n            // Small screen case: only a single dropdown\n            show(this.dropdownRef.el);\n            hide(this.beforeRef.el, this.afterRef.el, ...itemEls);\n            return;\n        }\n\n        while (this.areItemsWrapping()) {\n            if (itemsBefore.length) {\n                // Case 1: elements before can be hidden\n                show(this.beforeRef.el);\n                hide(itemsBefore.shift());\n                this.items.before.push(itemsToAssign.shift());\n            } else if (itemsAfter.length) {\n                // Case 2: elements before are hidden, elements after can be hidden\n                show(this.afterRef.el);\n                hide(itemsAfter.pop());\n                this.items.after.unshift(itemsToAssign.pop());\n            } else {\n                // Last resort: no elements can be hidden => fallback to single dropdown\n                show(this.dropdownRef.el);\n                hide(this.beforeRef.el, this.afterRef.el, ...itemEls);\n                break;\n            }\n        }\n    }\n\n    areItemsWrapping() {\n        const root = this.rootRef.el;\n        const firstItem = root.querySelector(\":scope > :not(.d-none)\");\n        if (!firstItem) {\n            return false;\n        }\n        const { height: currentHeight } = root.getBoundingClientRect();\n        const { height: targetHeight } = firstItem.getBoundingClientRect();\n        return currentHeight > targetHeight;\n    }\n\n    /**\n     * @returns {StatusBarItem[]}\n     */\n    getAllItems() {\n        const { foldField, name, record } = this.props;\n        const currentValue = record.data[name];\n        if (this.field.type === \"many2one\") {\n            // Many2one\n            return this.specialData.data.map((option) => ({\n                value: option.id,\n                label: option.display_name,\n                isFolded: option[foldField],\n                isSelected: Boolean(currentValue && option.id === currentValue.id),\n            }));\n        } else {\n            // Selection\n            let { selection } = this.field;\n            const { visibleSelection } = this.props;\n            if (visibleSelection?.length) {\n                selection = selection.filter(\n                    ([value]) => value === currentValue || visibleSelection.includes(value)\n                );\n            }\n            return selection.map(([value, label]) => ({\n                value,\n                label,\n                isFolded: false,\n                isSelected: value === currentValue,\n            }));\n        }\n    }\n\n    getCurrentLabel() {\n        return this.getAllItems().find((item) => item.isSelected)?.label || _t(\"More\");\n    }\n\n    /**\n     * @param {StatusBarItem} item\n     */\n    getDropdownItemClassNames(item) {\n        const classNames = [];\n        if (item.isSelected) {\n            classNames.push(\"active\");\n        }\n        if (item.isSelected || this.props.isDisabled) {\n            classNames.push(\"disabled\");\n        }\n        return classNames.join(\" \");\n    }\n\n    getSortedItems() {\n        const before = [];\n        const after = [];\n        const { true: inline = [], false: folded = [] } = groupBy(\n            this.getAllItems(),\n            (item) => item.isSelected || !item.isFolded\n        );\n        inline.reverse(); // CSS rules account for this list to be reversed\n        after.push(...folded);\n        return { inline, before, after, folded };\n    }\n\n    /**\n     * @param {StatusBarItem} item\n     */\n    async selectItem(item) {\n        const { name, record } = this.props;\n        const value =\n            this.field.type === \"many2one\"\n                ? { id: item.value, display_name: item.label }\n                : item.value;\n        await record.update({ [name]: value });\n        await record.save();\n    }\n\n    /**\n     * @param {CustomEvent<{ payload: StatusBarItem }>} ev\n     */\n    onDropdownItemSelected(ev) {\n        this.selectItem(ev.detail.payload);\n    }\n}\n\nexport const statusBarField = {\n    component: StatusBarField,\n    displayName: _t(\"Status\"),\n    supportedOptions: [\n        {\n            label: _t(\"Clickable\"),\n            name: \"clickable\",\n            type: \"boolean\",\n            default: true,\n        },\n        {\n            label: _t(\"Fold field\"),\n            name: \"fold_field\",\n            type: \"field\",\n            availableTypes: [\"boolean\"],\n            help: _t(\n                \"Boolean field from the model used in the relation, which indicates whether the state is folded or not.\"\n            ),\n        },\n    ],\n    supportedTypes: [\"many2one\", \"selection\"],\n    isEmpty: (record, fieldName) => !record.data[fieldName],\n    extractProps: ({ attrs, options, viewType }, dynamicInfo) => ({\n        isDisabled: !options.clickable || dynamicInfo.readonly,\n        visibleSelection: attrs.statusbar_visible?.trim().split(/\\s*,\\s*/g),\n        withCommand: viewType === \"form\",\n        foldField: options.fold_field,\n        domain: dynamicInfo.domain,\n    }),\n};\n\nregistry.category(\"fields\").add(\"statusbar\", statusBarField);\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { useAutoresize } from \"@web/core/utils/autoresize\";\nimport { useSpellCheck } from \"@web/core/utils/hooks\";\nimport { useDynamicPlaceholder } from \"../dynamic_placeholder_hook\";\nimport { useInputField } from \"../input_field_hook\";\nimport { parseInteger } from \"../parsers\";\nimport { standardFieldProps } from \"../standard_field_props\";\nimport { TranslationButton } from \"../translation_button\";\n\nimport { Component, useExternalListener, useEffect, useRef } from \"@odoo/owl\";\n\nexport class TextField extends Component {\n    static template = \"web.TextField\";\n    static components = {\n        TranslationButton,\n    };\n    static props = {\n        ...standardFieldProps,\n        lineBreaks: { type: Boolean, optional: true },\n        placeholder: { type: String, optional: true },\n        dynamicPlaceholder: { type: Boolean, optional: true },\n        dynamicPlaceholderModelReferenceField: { type: String, optional: true },\n        rowCount: { type: Number, optional: true },\n    };\n    static defaultProps = {\n        lineBreaks: true,\n        dynamicPlaceholder: false,\n        rowCount: 2,\n    };\n\n    setup() {\n        this.divRef = useRef(\"div\");\n        this.textareaRef = useRef(\"textarea\");\n        if (this.props.dynamicPlaceholder) {\n            this.dynamicPlaceholder = useDynamicPlaceholder(this.textareaRef);\n            useExternalListener(document, \"keydown\", this.dynamicPlaceholder.onKeydown);\n            useEffect(() =>\n                this.dynamicPlaceholder.updateModel(\n                    this.props.dynamicPlaceholderModelReferenceField\n                )\n            );\n        }\n        useInputField({\n            getValue: () => this.props.record.data[this.props.name] || \"\",\n            refName: \"textarea\",\n            preventLineBreaks: !this.props.lineBreaks,\n        });\n        useSpellCheck({ refName: \"textarea\" });\n\n        useAutoresize(this.textareaRef, { minimumHeight: this.minimumHeight });\n\n        this.selectionStart = this.props.record.data[this.props.name]?.length || 0;\n    }\n\n    async onBlur() {\n        this.selectionStart = this.textareaRef.el.selectionStart;\n    }\n\n    async onDynamicPlaceholderOpen() {\n        await this.dynamicPlaceholder.open({\n            validateCallback: this.onDynamicPlaceholderValidate.bind(this),\n        });\n    }\n\n    get isTranslatable() {\n        return this.props.record.fields[this.props.name].translate;\n    }\n    get minimumHeight() {\n        return this.props.lineBreaks ? 50 : 0;\n    }\n    get rowCount() {\n        return this.props.lineBreaks ? this.props.rowCount : 1;\n    }\n\n    async onDynamicPlaceholderValidate(chain, defaultValue) {\n        if (chain) {\n            this.textareaRef.el.focus();\n            const dynamicPlaceholder = ` {{object.${chain}${\n                defaultValue?.length ? ` ||| ${defaultValue}` : \"\"\n            }}}`;\n            this.textareaRef.el.setRangeText(\n                dynamicPlaceholder,\n                this.selectionStart,\n                this.selectionStart,\n                \"end\"\n            );\n            // trigger events to make the field dirty\n            this.textareaRef.el.dispatchEvent(new InputEvent(\"input\"));\n            this.textareaRef.el.dispatchEvent(new KeyboardEvent(\"keydown\"));\n            this.textareaRef.el.focus();\n        }\n    }\n}\n\nexport const textField = {\n    component: TextField,\n    displayName: _t(\"Multiline Text\"),\n    supportedOptions: [\n        {\n            label: _t(\"Enable line breaks\"),\n            name: \"line_breaks\",\n            type: \"boolean\",\n            default: true,\n        },\n        {\n            label: _t(\"Dynamic Placeholder\"),\n            name: \"placeholder_field\",\n            type: \"field\",\n            availableTypes: [\"char\"],\n        },\n    ],\n    supportedTypes: [\"html\", \"text\", \"char\"],\n    extractProps: ({ attrs, options, placeholder }) => ({\n        placeholder,\n        dynamicPlaceholder: options?.dynamic_placeholder || false,\n        dynamicPlaceholderModelReferenceField:\n            options?.dynamic_placeholder_model_reference_field || \"\",\n        rowCount: attrs.rows && parseInteger(attrs.rows),\n        lineBreaks: options?.line_breaks !== undefined ? Boolean(options.line_breaks) : true,\n    }),\n};\n\nregistry.category(\"fields\").add(\"text\", textField);\n\nexport class ListTextField extends TextField {\n    static defaultProps = {\n        ...super.defaultProps,\n        rowCount: 1,\n    };\n\n    get minimumHeight() {\n        return 0;\n    }\n    get rowCount() {\n        return this.props.rowCount;\n    }\n}\n\nexport const listTextField = {\n    ...textField,\n    component: ListTextField,\n};\n\nregistry.category(\"fields\").add(\"list.text\", listTextField);\n", "import { formatDateTime } from \"@web/core/l10n/dates\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { selectionField, SelectionField } from \"../selection/selection_field\";\n\nconst { DateTime } = luxon;\n\nexport class TimezoneMismatchField extends SelectionField {\n    static template = \"web.TimezoneMismatchField\";\n    static props = {\n        ...super.props,\n        tzOffsetField: { type: String, optional: true },\n        mismatchTitle: { type: String, optional: true },\n    };\n    static defaultProps = {\n        ...super.defaultProps,\n        tzOffsetField: \"tz_offset\",\n        mismatchTitle: _t(\n            \"Timezone Mismatch : This timezone is different from that of your browser.\\nPlease, set the same timezone as your browser's to avoid time discrepancies in your system.\"\n        ),\n    };\n\n    get mismatch() {\n        const userOffset = this.props.record.data[this.props.tzOffsetField];\n        if (userOffset && this.props.record.data[this.props.name]) {\n            const offset = -new Date().getTimezoneOffset();\n            let browserOffset = offset < 0 ? \"-\" : \"+\";\n            browserOffset += Math.floor(Math.abs(offset / 60))\n                .toFixed(0)\n                .padStart(2, \"0\");\n            browserOffset += Math.abs(offset % 60)\n                .toFixed(0)\n                .padStart(2, \"0\");\n            return browserOffset !== userOffset;\n        } else if (!this.props.record.data[this.props.name]) {\n            return true;\n        }\n        return false;\n    }\n    get mismatchTitle() {\n        if (!this.props.record.data[this.props.name]) {\n            return _t(\"Set a timezone on your user\");\n        }\n        return this.props.mismatchTitle;\n    }\n    get options() {\n        if (!this.mismatch) {\n            return super.options;\n        }\n        return super.options.map((option) => {\n            const [value, label] = option;\n            if (value === this.props.record.data[this.props.name]) {\n                const offset = this.props.record.data[this.props.tzOffsetField].match(\n                    /([+-])([0-9]{2})([0-9]{2})/\n                );\n                const sign = offset[1] === \"-\" ? -1 : 1;\n                const userOffset = sign * (parseInt(offset[2]) * 60 + parseInt(offset[3]));\n                const browserOffset = -new Date().getTimezoneOffset();\n                // UTC time of the user's selected timezone.\n                // E.g.\n                // - current time in UTC, say equal to 2021-01-01T00:00:00Z\n                // - userOffset of +0300 = 180 minutes\n                // - browserOffset of +0200 = -new Date().getTimezoneOffset() = 120 minutes\n                // - userUTCDatetime is then 2021-01-01T01:00:00Z\n                const userUTCDatetime = DateTime.utc().plus({\n                    minutes: userOffset - browserOffset,\n                });\n                return [value, `${label} (${formatDateTime(userUTCDatetime)})`];\n            }\n            return [value, label];\n        });\n    }\n}\n\nexport const timezoneMismatchField = {\n    ...selectionField,\n    component: TimezoneMismatchField,\n    additionalClasses: [\"d-flex\"],\n    supportedOptions: [\n        ...(selectionField.supportedOptions || []),\n        {\n            label: _t(\"Mismatch title\"),\n            name: \"mismatch_title\",\n            type: \"string\",\n        },\n        {\n            label: _t(\"Timezone offset field\"),\n            name: \"tz_offset_field\",\n            type: \"field\",\n            availableTypes: [\"char\"],\n        },\n    ],\n    extractProps({ options }) {\n        const props = selectionField.extractProps(...arguments);\n        props.tzOffsetField = options.tz_offset_field;\n        props.mismatchTitle = options.mismatch_title;\n        return props;\n    },\n};\n\nregistry.category(\"fields\").add(\"timezone_mismatch\", timezoneMismatchField);\n", "import { localization } from \"@web/core/l10n/localization\";\nimport { useOwnedDialogs } from \"@web/core/utils/hooks\";\nimport { user } from \"@web/core/user\";\nimport { TranslationDialog } from \"./translation_dialog\";\n\nimport { Component } from \"@odoo/owl\";\n\n/**\n * Prepares a function that will open the dialog that allows to edit translation\n * values for a given field.\n *\n * It is mainly a factorization of the feature that is also used\n * in legacy_fields. We expect it to be fully implemented in TranslationButton\n * when legacy code is removed.\n */\nexport function useTranslationDialog() {\n    const addDialog = useOwnedDialogs();\n\n    async function openTranslationDialog({ record, fieldName }) {\n        const saved = await record.save();\n        if (!saved) {\n            return;\n        }\n        const { resModel, resId } = record;\n\n        addDialog(TranslationDialog, {\n            fieldName: fieldName,\n            resId: resId,\n            resModel: resModel,\n            userLanguageValue: record.data[fieldName] || \"\",\n            isComingFromTranslationAlert: false,\n            onSave: async () => {\n                await record.load();\n            },\n        });\n    }\n\n    return openTranslationDialog;\n}\n\nexport class TranslationButton extends Component {\n    static template = \"web.TranslationButton\";\n    static props = {\n        fieldName: { type: String },\n        record: { type: Object },\n    };\n\n    setup() {\n        this.translationDialog = useTranslationDialog();\n    }\n\n    get isMultiLang() {\n        return localization.multiLang;\n    }\n    get lang() {\n        return new Intl.Locale(user.lang).language.toUpperCase();\n    }\n\n    onClick() {\n        const { fieldName, record } = this.props;\n        this.translationDialog({ fieldName, record });\n    }\n}\n", "import { Dialog } from \"@web/core/dialog/dialog\";\nimport { user } from \"@web/core/user\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { loadLanguages, _t } from \"@web/core/l10n/translation\";\nimport { jsToPyLocale } from \"@web/core/l10n/utils\";\n\nimport { Component, onWillStart } from \"@odoo/owl\";\n\nexport class TranslationDialog extends Component {\n    static template = \"web.TranslationDialog\";\n    static components = { Dialog };\n    static props = {\n        fieldName: String,\n        resId: Number,\n        resModel: String,\n        userLanguageValue: { type: String, optional: true },\n        isComingFromTranslationAlert: { type: Boolean, optional: true },\n        onSave: Function,\n        close: Function,\n        isText: { type: Boolean, optional: true },\n        showSource: { type: Boolean, optional: true },\n    };\n    setup() {\n        super.setup();\n        this.title = _t(\"Translate: %s\", this.props.fieldName);\n\n        this.user = user;\n        this.orm = useService(\"orm\");\n\n        this.terms = [];\n        this.updatedTerms = {};\n\n        onWillStart(async () => {\n            const languages = await loadLanguages(this.orm);\n            const [translations, context] = await this.loadTranslations(languages);\n            let id = 1;\n            translations.forEach((t) => (t.id = id++));\n            this.props.isText = context.translation_type === \"text\";\n            this.props.showSource = context.translation_show_source;\n\n            this.terms = translations.map((term) => {\n                const relatedLanguage = languages.find((l) => l[0] === term.lang);\n                const termInfo = {\n                    ...term,\n                    langName: relatedLanguage[1],\n                    value: term.value || \"\",\n                };\n                // we set the translation value coming from the database, except for the language\n                // the user is currently utilizing. Then we set the translation value coming\n                // from the value of the field in the form\n                if (\n                    term.lang === jsToPyLocale(user.lang) &&\n                    !this.props.showSource &&\n                    !this.props.isComingFromTranslationAlert\n                ) {\n                    this.updatedTerms[term.id] = this.props.userLanguageValue;\n                    termInfo.value = this.props.userLanguageValue;\n                }\n                return termInfo;\n            });\n            this.terms.sort((a, b) => a.langName.localeCompare(b.langName));\n        });\n    }\n\n    get domain() {\n        const domain = this.props.domain;\n        if (this.props.searchName) {\n            domain.push([\"name\", \"=\", `${this.props.searchName}`]);\n        }\n        return domain;\n    }\n\n    /**\n     * Load the translation terms for the installed language, for the current model and res_id\n     */\n    async loadTranslations(languages) {\n        return this.orm.call(this.props.resModel, \"get_field_translations\", [\n            [this.props.resId],\n            this.props.fieldName,\n        ]);\n    }\n\n    /**\n     * Save all the terms that have been updated\n     */\n    async onSave() {\n        const translations = {};\n\n        this.terms.map((term) => {\n            const updatedTermValue = this.updatedTerms[term.id];\n            if (term.id in this.updatedTerms && term.value !== updatedTermValue) {\n                if (this.props.showSource) {\n                    if (!translations[term.lang]) {\n                        translations[term.lang] = {};\n                    }\n                    translations[term.lang][term.source] = updatedTermValue || term.source;\n                } else {\n                    translations[term.lang] = updatedTermValue || false;\n                }\n            }\n        });\n\n        await this.orm.call(this.props.resModel, \"update_field_translations\", [\n            [this.props.resId],\n            this.props.fieldName,\n            translations,\n        ]);\n\n        await this.props.onSave();\n        this.props.close();\n    }\n}\n", "import { registry } from \"@web/core/registry\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { useInputField } from \"../input_field_hook\";\nimport { standardFieldProps } from \"../standard_field_props\";\n\nimport { Component } from \"@odoo/owl\";\n\nexport class UrlField extends Component {\n    static template = \"web.UrlField\";\n    static props = {\n        ...standardFieldProps,\n        placeholder: { type: String, optional: true },\n        text: { type: String, optional: true },\n        websitePath: { type: Boolean, optional: true },\n    };\n\n    setup() {\n        useInputField({ getValue: () => this.value });\n    }\n\n    get value() {\n        return this.props.record.data[this.props.name] || \"\";\n    }\n\n    get formattedHref() {\n        let value = this.props.record.data[this.props.name];\n        if (value && !this.props.websitePath) {\n            const regex = /^((ftp|http)s?:\\/)?\\//i; // http(s)://... ftp(s)://... /...\n            value = !regex.test(value) ? `http://${value}` : value;\n        }\n        return value;\n    }\n}\n\nexport const urlField = {\n    component: UrlField,\n    displayName: _t(\"URL\"),\n    supportedOptions: [\n        {\n            label: _t(\"Is a website path\"),\n            name: \"website_path\",\n            type: \"boolean\",\n            help: _t(\"If True, the url will be used as it is, without any prefix added to it.\"),\n        },\n        {\n            label: _t(\"Dynamic Placeholder\"),\n            name: \"placeholder_field\",\n            type: \"field\",\n            availableTypes: [\"char\"],\n        },\n    ],\n    supportedTypes: [\"char\"],\n    extractProps: ({ attrs, options, placeholder }) => ({\n        placeholder,\n        text: attrs.text,\n        websitePath: options.website_path,\n    }),\n};\n\nregistry.category(\"fields\").add(\"url\", urlField);\n\nclass FormUrlField extends UrlField {\n    static template = \"web.FormUrlField\";\n}\n\nexport const formUrlField = {\n    ...urlField,\n    component: FormUrlField,\n};\n\nregistry.category(\"fields\").add(\"form.url\", formUrlField);\n", "import { registry } from \"@web/core/registry\";\nimport { formatX2many } from \"../formatters\";\nimport { standardFieldProps } from \"../standard_field_props\";\n\nimport { Component } from \"@odoo/owl\";\n\nexport class ListX2ManyField extends Component {\n    static template = \"web.ListX2ManyField\";\n    static props = { ...standardFieldProps };\n\n    get formattedValue() {\n        return formatX2many(this.props.record.data[this.props.name]);\n    }\n}\n\nexport const listX2ManyField = {\n    component: ListX2ManyField,\n    useSubView: false,\n};\n\nregistry.category(\"fields\").add(\"list.one2many\", listX2ManyField);\nregistry.category(\"fields\").add(\"list.many2many\", listX2ManyField);\n", "import { makeContext } from \"@web/core/context\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { Pager } from \"@web/core/pager/pager\";\nimport { evaluateBooleanExpr } from \"@web/core/py_js/py\";\nimport { registry } from \"@web/core/registry\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { getFieldDomain } from \"@web/model/relational_model/utils\";\nimport {\n    useActiveActions,\n    useAddInlineRecord,\n    useOpenX2ManyRecord,\n    useSelectCreate,\n    useX2ManyCrud,\n} from \"@web/views/fields/relational_utils\";\nimport { standardFieldProps } from \"@web/views/fields/standard_field_props\";\nimport { KanbanCompiler } from \"@web/views/kanban/kanban_compiler\";\nimport { KanbanRenderer } from \"@web/views/kanban/kanban_renderer\";\nimport { ListRenderer } from \"@web/views/list/list_renderer\";\nimport { computeViewClassName } from \"@web/views/utils\";\nimport { ViewButton } from \"@web/views/view_button/view_button\";\nimport { symmetricalDifference } from \"@web/core/utils/arrays\";\nimport { x2ManyCommands } from \"@web/core/orm_service\";\n\nimport { Component } from \"@odoo/owl\";\n\nexport class X2ManyField extends Component {\n    static template = \"web.X2ManyField\";\n    static components = { Pager, KanbanRenderer, ListRenderer, ViewButton };\n    static props = {\n        ...standardFieldProps,\n        addLabel: { type: String, optional: true },\n        editable: { type: String, optional: true },\n        viewMode: { type: String, optional: true },\n        widget: { type: String, optional: true },\n        crudOptions: { type: Object, optional: true },\n        string: { type: String, optional: true },\n        relatedFields: { type: Object, optional: true },\n        views: { type: Object, optional: true },\n        domain: { type: [Array, Function], optional: true },\n        context: { type: Object },\n    };\n\n    setup() {\n        this.field = this.props.record.fields[this.props.name];\n        const { saveRecord, updateRecord, removeRecord } = useX2ManyCrud(\n            () => this.list,\n            this.isMany2Many\n        );\n\n        this.archInfo = this.props.views?.[this.props.viewMode] || {};\n        const classes = this.props.viewMode\n            ? [\"o_field_x2many\", `o_field_x2many_${this.props.viewMode}`]\n            : [\"o_field_x2many\"];\n        this.className = computeViewClassName(this.props.viewMode, this.archInfo.xmlDoc, classes);\n\n        const { activeActions, controls } = this.archInfo;\n        if (this.props.viewMode === \"kanban\") {\n            this.controls = controls || [];\n        }\n        const subViewActiveActions = activeActions;\n        this.activeActions = useActiveActions({\n            crudOptions: Object.assign({}, this.props.crudOptions, {\n                onDelete: removeRecord,\n                edit: this.props.record.isInEdition,\n            }),\n            fieldType: this.isMany2Many ? \"many2many\" : \"one2many\",\n            subViewActiveActions,\n            getEvalParams: (props) => ({\n                evalContext: props.record.evalContext,\n                readonly: props.readonly,\n            }),\n        });\n\n        this.addInLine = useAddInlineRecord({\n            addNew: (...args) => this.list.addNewRecord(...args),\n        });\n\n        const openRecord = useOpenX2ManyRecord({\n            resModel: this.list.resModel,\n            activeField: this.activeField,\n            activeActions: this.activeActions,\n            getList: () => this.list,\n            saveRecord,\n            updateRecord,\n            isMany2Many: this.isMany2Many,\n        });\n        this._openRecord = (params) => {\n            const activeElement = document.activeElement;\n            openRecord({\n                ...params,\n                controls: this.controls,\n                onClose: () => {\n                    if (activeElement) {\n                        activeElement.focus();\n                    }\n                },\n            });\n        };\n        this.canOpenRecord =\n            this.props.viewMode === \"list\"\n                ? !(this.archInfo.editable || this.props.editable)\n                : true;\n\n        const selectCreate = useSelectCreate({\n            resModel: this.props.record.data[this.props.name].resModel,\n            activeActions: this.activeActions,\n            onSelected: (resIds) => saveRecord(resIds),\n            onCreateEdit: ({ context }) => this._openRecord({ context }),\n            onUnselect: this.isMany2Many ? undefined : () => saveRecord(),\n        });\n\n        this.selectCreate = (params) => {\n            const p = Object.assign({}, params);\n            const currentIds = this.props.record.data[this.props.name].currentIds.filter(\n                (id) => typeof id === \"number\"\n            );\n            p.domain = [...(p.domain || []), \"!\", [\"id\", \"in\", currentIds]];\n            return selectCreate(p);\n        };\n        this.action = useService(\"action\");\n        this.notificationService = useService(\"notification\");\n    }\n\n    get activeField() {\n        return {\n            fields: this.props.relatedFields,\n            views: this.props.views,\n            viewMode: this.props.viewMode,\n            string: this.props.string,\n        };\n    }\n\n    get displayControlPanelButtons() {\n        return this.props.viewMode === \"kanban\" && this.canCreate && this.controls.length > 0;\n    }\n\n    get canCreate() {\n        return (\n            (\"link\" in this.activeActions ? this.activeActions.link : this.activeActions.create) &&\n            !this.props.readonly\n        );\n    }\n\n    get isMany2Many() {\n        return this.field.type === \"many2many\" || this.props.widget === \"many2many\";\n    }\n\n    get list() {\n        return this.props.record.data[this.props.name];\n    }\n\n    get nestedKeyOptionalFieldsData() {\n        return {\n            field: this.props.name,\n            model: this.props.record.resModel,\n            viewMode: \"form\",\n        };\n    }\n\n    get pagerProps() {\n        const list = this.list;\n        return {\n            offset: list.offset,\n            limit: list.limit,\n            total: list.count,\n            onUpdate: async ({ offset, limit }) => {\n                const initialLimit = this.list.limit;\n                const leaved = await list.leaveEditMode();\n                if (leaved) {\n                    if (initialLimit === limit && initialLimit === this.list.limit + 1) {\n                        // Unselecting the edited record might have abandonned it. If the page\n                        // size was reached before that record was created, the limit was temporarily\n                        // increased to keep that new record in the current page, and abandonning it\n                        // decreased this limit back to it's initial value, so we keep this into\n                        // account in the offset/limit update we're about to do.\n                        offset -= 1;\n                        limit -= 1;\n                    }\n                    await list.load({ limit, offset });\n                    this.render();\n                }\n            },\n            withAccessKey: false,\n        };\n    }\n\n    get rendererProps() {\n        const { archInfo } = this;\n        const props = {\n            archInfo,\n            list: this.list,\n            openRecord: this.openRecord.bind(this),\n            readonly: this.props.readonly || !this.activeActions.write,\n        };\n\n        if (this.props.viewMode === \"kanban\") {\n            const recordsDraggable = !this.props.readonly && archInfo.recordsDraggable;\n            props.archInfo = { ...archInfo, recordsDraggable };\n            props.Compiler = KanbanCompiler;\n            // TODO: apply same logic in the list case\n            props.deleteRecord = (record) => {\n                if (this.isMany2Many) {\n                    return this.list.forget(record);\n                }\n                return this.list.delete(record);\n            };\n            if (this.canCreate && this.controls.length === 0) {\n                props.addLabel = this.props.addLabel || _t(\"Add %s\", this.field.string);\n                props.onAdd = this.onAdd.bind(this);\n            }\n            return props;\n        }\n\n        const editable =\n            (this.archInfo.activeActions.edit && archInfo.editable) || this.props.editable;\n        props.activeActions = this.activeActions;\n        props.cycleOnTab = false;\n        props.editable = !this.props.readonly && editable;\n        props.nestedKeyOptionalFieldsData = this.nestedKeyOptionalFieldsData;\n        props.onAdd = (params) => {\n            params.editable =\n                !this.props.readonly && (\"editable\" in params ? params.editable : editable);\n            this.onAdd(params);\n        };\n        props.onOpenFormView = this.switchToForm.bind(this);\n        props.hasOpenFormViewButton = archInfo.editable ? archInfo.openFormView : false;\n        return props;\n    }\n\n    evalInvisible(invisible) {\n        return evaluateBooleanExpr(invisible, this.list.evalContext);\n    }\n\n    async switchToForm(record, options) {\n        let resId = null;\n        if (record.isNew) {\n            // In the case of a new record, you don't have access to the id from the start, to get it we need to:\n            // - Finds the record's index using its _virtualId.\n            // - Saves the record and compares resIds before and after to detect new records.\n            // - If the record was created, it determines its final resId by matching the index.\n            // - Opens the form view for the correct record.\n            const createCommands = this.list._commands.filter(\n                ([command]) => command === x2ManyCommands.CREATE\n            );\n            const newRecordIndex = createCommands.findIndex(\n                ([_command, virtualId]) => virtualId === record._virtualId\n            );\n            const previousResIds = this.list.resIds;\n            const saved = await this.props.record.save();\n            if (!saved) {\n                return;\n            }\n            const newResIds = symmetricalDifference(this.list.resIds, previousResIds);\n            if (newResIds.length !== createCommands.length) {\n                return this.notificationService.add(_t(\"Please save your changes first\"), {\n                    type: \"danger\",\n                });\n            }\n            newResIds.sort((x, y) => x - y);\n            resId = newResIds[newRecordIndex];\n        } else {\n            const saved = await this.props.record.save();\n            if (!saved) {\n                return;\n            }\n            resId = record.resId;\n        }\n\n        this.action.doAction(\n            {\n                type: \"ir.actions.act_window\",\n                views: [[false, \"form\"]],\n                res_id: resId,\n                res_model: this.list.resModel,\n                context: this.getFormActionContext(),\n            },\n            {\n                props: { resIds: this.list.resIds },\n                newWindow: options.newWindow,\n            }\n        );\n    }\n\n    getFormActionContext() {\n        return this.props.context;\n    }\n\n    async onAdd({ context, editable } = {}) {\n        context = makeContext([this.props.context, context]);\n        if (this.isMany2Many) {\n            const domain = getFieldDomain(this.props.record, this.props.name, this.props.domain);\n            const { string } = this.props;\n            const title = _t(\"Add: %s\", string);\n            return this.selectCreate({ domain, context, title });\n        }\n        if (editable) {\n            const editedRecord = this.list.editedRecord;\n            if (editedRecord) {\n                const proms = [];\n                this.list.model.bus.trigger(\"NEED_LOCAL_CHANGES\", { proms });\n                await Promise.all([...proms, editedRecord._updatePromise]);\n                await this.list.leaveEditMode({ canAbandon: false });\n            }\n            if (!this.list.editedRecord) {\n                return this.addInLine({ context, editable });\n            }\n            return;\n        }\n        return this._openRecord({ context });\n    }\n\n    async openRecord(record) {\n        if (this.canOpenRecord) {\n            return this._openRecord({\n                record,\n                context: this.props.context,\n                readonly: this.props.readonly,\n            });\n        }\n    }\n}\n\nexport const x2ManyField = {\n    component: X2ManyField,\n    displayName: _t(\"Relational table\"),\n    supportedTypes: [\"one2many\", \"many2many\"],\n    useSubView: true,\n    extractProps: (\n        { attrs, relatedFields, viewMode, views, widget, options, string },\n        dynamicInfo\n    ) => {\n        const props = {\n            addLabel: attrs[\"add-label\"],\n            context: dynamicInfo.context,\n            domain: dynamicInfo.domain,\n            crudOptions: options,\n            string,\n        };\n        if (viewMode) {\n            props.views = views;\n            props.viewMode = viewMode;\n            props.relatedFields = relatedFields;\n        }\n        if (widget) {\n            props.widget = widget;\n        }\n        return props;\n    },\n};\n\nregistry.category(\"fields\").add(\"one2many\", x2ManyField);\nregistry.category(\"fields\").add(\"many2many\", x2ManyField);\n", "import { useService } from \"@web/core/utils/hooks\";\nimport { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\n\nimport { Component, onWillRender } from \"@odoo/owl\";\nexport class ButtonBox extends Component {\n    static template = \"web.Form.ButtonBox\";\n    static components = { Dropdown, DropdownItem };\n    static props = {\n        slots: Object,\n        class: { type: String, optional: true },\n    };\n    static defaultProps = {\n        class: \"\",\n    };\n\n    setup() {\n        const ui = useService(\"ui\");\n        onWillRender(() => {\n            const maxVisibleButtons = [0, 0, 7, 4, 5, 8][ui.size] ?? 8;\n            const allVisibleButtons = Object.entries(this.props.slots)\n                .filter(([_, slot]) => this.isSlotVisible(slot))\n                .map(([slotName]) => slotName);\n            if (allVisibleButtons.length <= maxVisibleButtons) {\n                this.visibleButtons = allVisibleButtons;\n                this.additionalButtons = [];\n                this.isFull = allVisibleButtons.length === maxVisibleButtons;\n            } else {\n                // -1 for \"More\" dropdown\n                const splitIndex = Math.max(maxVisibleButtons - 1, 0);\n                this.visibleButtons = allVisibleButtons.slice(0, splitIndex);\n                this.additionalButtons = allVisibleButtons.slice(splitIndex);\n                this.isFull = true;\n            }\n        });\n    }\n\n    isSlotVisible(slot) {\n        return !(\"isVisible\" in slot) || slot.isVisible;\n    }\n}\n", "import { exprToBoolean } from \"@web/core/utils/strings\";\nimport { visitXML } from \"@web/core/utils/xml\";\nimport { Field } from \"@web/views/fields/field\";\nimport { getActiveActions } from \"@web/views/utils\";\nimport { Widget } from \"@web/views/widgets/widget\";\n\nexport class FormArchParser {\n    parse(xmlDoc, models, modelName) {\n        const jsClass = xmlDoc.getAttribute(\"js_class\");\n        const disableAutofocus = exprToBoolean(xmlDoc.getAttribute(\"disable_autofocus\") || \"\");\n        const activeActions = getActiveActions(xmlDoc);\n        const fieldNodes = {};\n        const widgetNodes = {};\n        let widgetNextId = 0;\n        const fieldNextIds = {};\n        const autofocusFieldIds = [];\n        visitXML(xmlDoc, (node) => {\n            if (node.tagName === \"field\") {\n                const fieldInfo = Field.parseFieldNode(node, models, modelName, \"form\", jsClass);\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                if (exprToBoolean(node.getAttribute(\"default_focus\") || \"\")) {\n                    autofocusFieldIds.push(fieldId);\n                }\n                if (fieldInfo.type === \"properties\") {\n                    activeActions.addPropertyFieldValue = true;\n                }\n                return false;\n            } else if (node.tagName === \"widget\") {\n                const widgetInfo = Widget.parseWidgetNode(node);\n                const widgetId = `widget_${++widgetNextId}`;\n                widgetNodes[widgetId] = widgetInfo;\n                node.setAttribute(\"widget_id\", widgetId);\n            }\n        });\n        return {\n            activeActions,\n            autofocusFieldIds,\n            disableAutofocus,\n            fieldNodes,\n            widgetNodes,\n            xmlDoc,\n        };\n    }\n}\n", "import { CogMenu } from \"@web/search/cog_menu/cog_menu\";\n\nexport class FormCogMenu extends CogMenu {\n    static template = \"web.FormCogMenu\";\n}\n", "import { registry } from \"@web/core/registry\";\nimport { SIZES } from \"@web/core/ui/ui_service\";\nimport {\n    append,\n    combineAttributes,\n    createElement,\n    createTextNode,\n    getTag,\n} from \"@web/core/utils/xml\";\nimport { toStringExpression } from \"@web/views/utils\";\nimport {\n    copyAttributes,\n    getModifier,\n    isComponentNode,\n    isTextNode,\n    makeSeparator,\n} from \"@web/views/view_compiler\";\nimport { ViewCompiler } from \"../view_compiler\";\nimport { exprToBoolean } from \"@web/core/utils/strings\";\n\nconst compilersRegistry = registry.category(\"form_compilers\");\n\nfunction appendAttf(el, attr, string) {\n    const attrKey = `t-attf-${attr}`;\n    const attrVal = el.getAttribute(attrKey);\n    el.setAttribute(attrKey, appendToExpr(attrVal, string));\n}\n\nfunction appendToExpr(expr, string) {\n    const re = /{{.*}}/;\n    const oldString = re.exec(expr);\n    return oldString ? `${oldString} {{${string} }}` : `{{${string} }}`;\n}\n\n/**\n * @param {Record<string, any>} obj\n * @returns {string}\n */\nexport function objectToString(obj) {\n    return `{${Object.entries(obj)\n        .map((t) => t.join(\":\"))\n        .join(\",\")}}`;\n}\n\nexport class FormCompiler extends ViewCompiler {\n    setup() {\n        this.encounteredFields = {};\n        /** @type {Record<string, Element[]>} */\n        this.labels = {};\n        this.noteBookId = 0;\n        this.compilers.push(\n            ...compilersRegistry.getAll(),\n            { selector: \"div[name='button_box']\", fn: this.compileButtonBox },\n            { selector: \"footer\", fn: this.compileFooter },\n            { selector: \"form\", fn: this.compileForm, doNotCopyAttributes: true },\n            { selector: \"group\", fn: this.compileGroup },\n            { selector: \"header\", fn: this.compileHeader },\n            { selector: \"label\", fn: this.compileLabel, doNotCopyAttributes: true },\n            { selector: \"notebook\", fn: this.compileNotebook },\n            { selector: \"setting\", fn: this.compileSetting },\n            { selector: \"separator\", fn: this.compileSeparator },\n            { selector: \"sheet\", fn: this.compileSheet }\n        );\n    }\n\n    compile(key, params = {}) {\n        const compiled = super.compile(...arguments);\n        if (!params.isSubView) {\n            compiled.children[0].setAttribute(\"t-ref\", \"compiled_view_root\");\n        }\n        return compiled;\n    }\n\n    createLabelFromField(fieldId, fieldName, fieldString, label, params) {\n        let labelText = label.textContent || fieldString;\n        if (label.hasAttribute(\"data-no-label\")) {\n            labelText = toStringExpression(\"\");\n        } else {\n            labelText = labelText\n                ? toStringExpression(labelText)\n                : `__comp__.props.record.fields['${fieldName}'].string`;\n        }\n        const formLabel = createElement(\"FormLabel\", {\n            id: `'${fieldId}'`,\n            fieldName: `'${fieldName}'`,\n            record: `__comp__.props.record`,\n            fieldInfo: `__comp__.props.archInfo.fieldNodes['${fieldId}']`,\n            className: `\"${label.className}\"`,\n            string: labelText,\n        });\n        const condition = label.getAttribute(\"t-if\");\n        if (condition) {\n            formLabel.setAttribute(\"t-if\", condition);\n        }\n        return formLabel;\n    }\n\n    /**\n     * @param {string} fieldName\n     * @returns {Element[]}\n     */\n    getLabels(fieldName) {\n        const labels = this.labels[fieldName] || [];\n        this.labels[fieldName] = null;\n        return labels;\n    }\n\n    /**\n     * @param {string} fieldName\n     * @param {Element} label\n     */\n    pushLabel(fieldName, label) {\n        this.labels[fieldName] = this.labels[fieldName] || [];\n        this.labels[fieldName].push(label);\n    }\n\n    //-----------------------------------------------------------------------------\n    // Compilers\n    //-----------------------------------------------------------------------------\n\n    /**\n     * @param {Element} el\n     * @param {Record<string, any>} params\n     * @returns {Element}\n     */\n    compileButtonBox(el, params) {\n        if (!el.children.length) {\n            return this.compileGenericNode(el, params);\n        }\n\n        el.classList.remove(\"oe_button_box\");\n        const buttonBox = createElement(\"ButtonBox\");\n        buttonBox.setAttribute(\"t-if\", \"!__comp__.env.inDialog\");\n        let slotId = 0;\n        let hasContent = false;\n        for (const child of el.children) {\n            const invisible = getModifier(child, \"invisible\");\n            if (!params.compileInvisibleNodes && (invisible === \"True\" || invisible === \"1\")) {\n                continue;\n            }\n            hasContent = true;\n            let isVisibleExpr;\n            if (!invisible || invisible === \"False\" || invisible === \"0\") {\n                isVisibleExpr = \"true\";\n            } else if (invisible === \"True\" || invisible === \"1\") {\n                isVisibleExpr = \"false\";\n            } else {\n                isVisibleExpr = `!__comp__.evaluateBooleanExpr(${JSON.stringify(\n                    invisible\n                )},__comp__.props.record.evalContextWithVirtualIds)`;\n            }\n            const mainSlot = createElement(\"t\", {\n                \"t-set-slot\": `slot_${slotId++}`,\n                isVisible: isVisibleExpr,\n            });\n            if (child.tagName === \"button\" || child.children.tagName === \"button\") {\n                child.classList.add(\n                    \"oe_stat_button\",\n                    \"btn\",\n                    \"btn-outline-secondary\",\n                    \"flex-grow-1\",\n                    \"flex-lg-grow-0\"\n                );\n            }\n            if (child.tagName === \"field\") {\n                child.classList.add(\"d-inline-block\", \"mb-0\", \"z-0\");\n            }\n            append(mainSlot, this.compileNode(child, params, false));\n            append(buttonBox, mainSlot);\n        }\n\n        return hasContent ? buttonBox : \"\";\n    }\n\n    compileButton(el, params) {\n        return super.compileButton(el, params);\n    }\n\n    /**\n     * @override\n     */\n    compileField(el, params) {\n        const field = super.compileField(el, params);\n\n        const fieldName = el.getAttribute(\"name\");\n        params.notebookPageFields?.push(fieldName);\n        const fieldString = el.getAttribute(\"string\");\n        const fieldId = el.getAttribute(\"field_id\");\n        const labelsForAttr = el.getAttribute(\"id\") || fieldName;\n        const labels = this.getLabels(labelsForAttr);\n        const dynamicLabel = (label) => {\n            const formLabel = this.createLabelFromField(fieldId, fieldName, fieldString, label, {\n                ...params,\n                currentFieldArchNode: el,\n            });\n            if (formLabel) {\n                label.replaceWith(formLabel);\n            } else {\n                label.remove();\n            }\n            return formLabel;\n        };\n        for (const label of labels) {\n            dynamicLabel(label);\n        }\n        this.encounteredFields[fieldName] = dynamicLabel;\n        return field;\n    }\n\n    /**\n     * @param {Element} el\n     * @param {Record<string, any>} params\n     * @returns {Element}\n     */\n    compileForm(el, params) {\n        let sheetNode = null;\n        for (const sheet of el.querySelectorAll(\"sheet\")) {\n            if (sheet.closest(\"form\") === el) {\n                sheetNode = sheet;\n                break;\n            }\n        }\n        const displayClasses = sheetNode\n            ? `d-flex d-print-block {{ __comp__.uiService.size < ${SIZES.XXL} ? \"flex-column\" : \"flex-nowrap h-100\" }}`\n            : \"d-block\";\n        const stateClasses =\n            \"{{ __comp__.props.record.dirty ? 'o_form_dirty' : !__comp__.props.record.isNew ? 'o_form_saved' : '' }}\";\n        const form = createElement(\"div\", {\n            class: \"o_form_renderer\",\n            \"t-att-class\": \"__comp__.props.class\",\n            \"t-attf-class\": `{{__comp__.props.record.isInEdition ? 'o_form_editable' : 'o_form_readonly'}} ${displayClasses} ${stateClasses}`,\n        });\n        if (!sheetNode) {\n            for (const child of el.childNodes) {\n                // ButtonBox are already compiled for the control panel and should not\n                // be recompiled for the renderer of the view\n                if (child.attributes?.name?.value !== \"button_box\") {\n                    append(form, this.compileNode(child, params));\n                }\n            }\n            form.classList.add(\"o_form_nosheet\");\n        } else {\n            let compiledList = [];\n            for (const child of el.childNodes) {\n                const compiled = this.compileNode(child, params);\n                if (getTag(child, true) === \"sheet\") {\n                    append(form, compiled);\n                    compiled.prepend(...compiledList);\n                    compiledList = [];\n                } else if (compiled) {\n                    compiledList.push(compiled);\n                }\n            }\n            append(form, compiledList);\n        }\n        return form;\n    }\n\n    /**\n     * @param {Element} el\n     * @param {Record<string, any>} params\n     * @returns {Element}\n     */\n    compileFooter(el, params) {\n        const footer = createElement(\"t\");\n        const replace = el.getAttribute(\"replace\");\n        if (replace && !exprToBoolean(replace)) {\n            footer.append(\n                createElement(\"t\", {\n                    \"t-call\": \"web.DefaultButtonsSlot\",\n                    \"t-call-context\": \"{ props: __comp__.props }\",\n                })\n            );\n        }\n        copyAttributes(el, footer);\n        for (const child of el.childNodes) {\n            const compiled = this.compileNode(child, params);\n            if (compiled) {\n                footer.append(compiled);\n            }\n        }\n        return footer;\n    }\n\n    /**\n     * @param {Element} el\n     * @param {Record<string, any>} params\n     * @returns {Element}\n     */\n    compileGroup(el, params) {\n        const isOuterGroup = [...el.children].some((c) => getTag(c, true) === \"group\");\n        const formGroup = createElement(isOuterGroup ? \"OuterGroup\" : \"InnerGroup\");\n\n        let slotId = 0;\n        let sequence = 0;\n\n        if (el.hasAttribute(\"col\")) {\n            formGroup.setAttribute(\"maxCols\", el.getAttribute(\"col\"));\n        }\n\n        if (el.hasAttribute(\"string\")) {\n            const titleSlot = createElement(\"t\", { \"t-set-slot\": \"title\" }, [\n                makeSeparator(el.getAttribute(\"string\")),\n            ]);\n            append(formGroup, titleSlot);\n        }\n\n        let forceNewline = false;\n        for (const child of el.children) {\n            if (getTag(child, true) === \"newline\") {\n                forceNewline = true;\n                continue;\n            }\n\n            const invisible = getModifier(child, \"invisible\");\n            if (!params.compileInvisibleNodes && (invisible === \"True\" || invisible === \"1\")) {\n                continue;\n            }\n\n            const mainSlot = createElement(\"t\", {\n                \"t-set-slot\": `item_${slotId++}`,\n                type: \"'item'\",\n                sequence: sequence++,\n                \"t-slot-scope\": \"scope\",\n            });\n            let itemSpan = parseInt(child.getAttribute(\"colspan\") || \"1\", 10);\n\n            if (forceNewline) {\n                mainSlot.setAttribute(\"newline\", true);\n                forceNewline = false;\n            }\n\n            if (getTag(child, true) === \"separator\") {\n                itemSpan = parseInt(formGroup.getAttribute(\"maxCols\") || 2, 10);\n            }\n\n            if (child.matches(\"div[class='clearfix']:empty\")) {\n                itemSpan = parseInt(formGroup.getAttribute(\"maxCols\") || 2, 10);\n            }\n\n            let slotContent;\n            if (getTag(child, true) === \"field\") {\n                const addLabel = child.hasAttribute(\"nolabel\")\n                    ? child.getAttribute(\"nolabel\") !== \"1\"\n                    : true;\n                slotContent = this.compileNode(child, { ...params, currentSlot: mainSlot }, false);\n                if (slotContent && addLabel && !isOuterGroup && !isTextNode(slotContent)) {\n                    itemSpan = itemSpan === 1 ? itemSpan + 1 : itemSpan;\n                    const fieldName = child.getAttribute(\"name\");\n                    const fieldId = slotContent.getAttribute(\"id\") || fieldName;\n                    const props = {\n                        id: `${fieldId}`,\n                        fieldName: `'${fieldName}'`,\n                        record: `__comp__.props.record`,\n                        string: child.hasAttribute(\"string\")\n                            ? toStringExpression(child.getAttribute(\"string\"))\n                            : `__comp__.props.record.fields.${fieldName}.string`,\n                        fieldInfo: `__comp__.props.archInfo.fieldNodes[${fieldId}]`,\n                    };\n                    mainSlot.setAttribute(\"props\", objectToString(props));\n                    mainSlot.setAttribute(\"Component\", \"__comp__.constructor.components.FormLabel\");\n                    mainSlot.setAttribute(\"subType\", \"'item_component'\");\n                }\n            } else {\n                // TODO: When every apps will be revamp, we could remove the condition using 'o_td_label' in favor of 'o_wrap_label'\n                if (\n                    child.classList.contains(\"o_wrap_label\") ||\n                    child.classList.contains(\"o_td_label\") ||\n                    getTag(child, true) === \"label\"\n                ) {\n                    mainSlot.setAttribute(\"subType\", \"'label'\");\n                    child.classList.remove(\"o_wrap_label\");\n                }\n                slotContent = this.compileNode(child, { ...params, currentSlot: mainSlot }, false);\n            }\n\n            if (slotContent && !isTextNode(slotContent)) {\n                let isVisibleExpr;\n                if (!invisible || invisible === \"False\" || invisible === \"0\") {\n                    isVisibleExpr = \"true\";\n                } else if (invisible === \"True\" || invisible === \"1\") {\n                    isVisibleExpr = \"false\";\n                } else {\n                    isVisibleExpr = `!__comp__.evaluateBooleanExpr(${JSON.stringify(\n                        invisible\n                    )},__comp__.props.record.evalContextWithVirtualIds)`;\n                }\n                mainSlot.setAttribute(\"isVisible\", isVisibleExpr);\n                if (itemSpan > 0) {\n                    mainSlot.setAttribute(\"itemSpan\", `${itemSpan}`);\n                }\n\n                const groupClassExpr = `scope && scope.className`;\n                if (isComponentNode(slotContent)) {\n                    if (getTag(slotContent) === \"FormLabel\") {\n                        mainSlot.prepend(\n                            createElement(\"t\", {\n                                \"t-set\": \"addClass\",\n                                \"t-value\": groupClassExpr,\n                            })\n                        );\n                        combineAttributes(\n                            slotContent,\n                            \"className\",\n                            `(addClass ? \" \" + addClass : \"\")`,\n                            `+`\n                        );\n                    } else if (getTag(child, true) !== \"button\") {\n                        if (slotContent.hasAttribute(\"class\")) {\n                            mainSlot.prepend(\n                                createElement(\"t\", {\n                                    \"t-set\": \"addClass\",\n                                    \"t-value\": groupClassExpr,\n                                })\n                            );\n                            combineAttributes(\n                                slotContent,\n                                \"class\",\n                                `(addClass ? \" \" + addClass : \"\")`,\n                                `+`\n                            );\n                        } else {\n                            slotContent.setAttribute(\"class\", groupClassExpr);\n                        }\n                    }\n                } else {\n                    appendAttf(slotContent, \"class\", `${groupClassExpr} || \"\"`);\n                }\n                append(mainSlot, slotContent);\n                append(formGroup, mainSlot);\n            }\n        }\n        return formGroup;\n    }\n\n    /**\n     * @param {Element} el\n     * @param {Record<string, any>} params\n     * @returns {Element}\n     */\n    compileHeader(el, params) {\n        const statusBar = createElement(\"div\", {\n            \"t-att-class\": \"{ 'shadow-sm': __comp__.state.isStatusbarStickyPinned }\",\n        });\n        statusBar.className = \"o_form_statusbar d-flex justify-content-between py-2\";\n        const buttons = [];\n        const others = [];\n        for (const child of el.childNodes) {\n            const compiled = this.compileNode(child, params);\n            if (!compiled || isTextNode(compiled)) {\n                continue;\n            }\n            if (getTag(child, true) === \"field\" && !child.classList.contains(\"btn\")) {\n                compiled.setAttribute(\"showTooltip\", true);\n                others.push(compiled);\n            } else {\n                if (compiled.tagName === \"ViewButton\") {\n                    compiled.setAttribute(\"defaultRank\", \"'btn-secondary'\");\n                }\n                buttons.push(compiled);\n            }\n        }\n        let slotId = 0;\n        const statusBarButtons = createElement(\"StatusBarButtons\");\n        for (const button of buttons) {\n            const slot = createElement(\"t\", {\n                \"t-set-slot\": `button_${slotId++}`,\n                isVisible: button.getAttribute(\"t-if\") || true,\n            });\n            append(slot, button);\n            append(statusBarButtons, slot);\n        }\n        append(statusBar, statusBarButtons);\n        append(statusBar, others);\n        return statusBar;\n    }\n\n    /**\n     * @param {Element} el\n     * @param {Record<string, any>} params\n     * @returns {Element}\n     */\n    compileLabel(el, params) {\n        const forAttr = el.getAttribute(\"for\");\n        // A label can contain or not the labelable Element it is referring to.\n        // If it doesn't, there is no `for=`\n        // Otherwise, the targetted element is somewhere else among its nextChildren\n        if (forAttr) {\n            let label = createElement(\"label\");\n            copyAttributes(el, label);\n            const string = el.getAttribute(\"string\");\n            if (string) {\n                append(label, createTextNode(string));\n            } else if (string === \"\") {\n                label.setAttribute(\"data-no-label\", \"true\");\n            }\n            if (this.encounteredFields[forAttr]) {\n                label = this.encounteredFields[forAttr](label);\n            } else {\n                this.pushLabel(forAttr, label);\n            }\n            return label;\n        }\n        const res = this.compileGenericNode(el, params);\n        copyAttributes(el, res);\n        return res;\n    }\n\n    /**\n     * @param {Element} el\n     * @param {Record<string, any>} params\n     * @returns {Element}\n     */\n    compileNotebook(el, params) {\n        const noteBookId = this.noteBookId++;\n        const noteBook = createElement(\"Notebook\");\n\n        if (el.hasAttribute(\"class\")) {\n            noteBook.setAttribute(\"className\", toStringExpression(el.getAttribute(\"class\")));\n            el.removeAttribute(\"class\");\n        }\n\n        noteBook.setAttribute(\n            \"defaultPage\",\n            `__comp__.props.record.isNew ? undefined : __comp__.props.activeNotebookPages[${noteBookId}]`\n        );\n        noteBook.setAttribute(\n            \"onPageUpdate\",\n            `(page) => __comp__.props.onNotebookPageChange(${noteBookId}, page)`\n        );\n\n        for (const child of el.children) {\n            if (getTag(child, true) !== \"page\") {\n                continue;\n            }\n            const invisible = getModifier(child, \"invisible\");\n            if (!params.compileInvisibleNodes && (invisible === \"True\" || invisible === \"1\")) {\n                continue;\n            }\n\n            const pageSlot = createElement(\"t\");\n            append(noteBook, pageSlot);\n\n            const pageId = `page_${this.id++}`;\n            const pageTitle = toStringExpression(\n                child.getAttribute(\"string\") || child.getAttribute(\"name\") || \"\"\n            );\n            const pageNodeName = toStringExpression(child.getAttribute(\"name\") || \"\");\n\n            pageSlot.setAttribute(\"t-set-slot\", pageId);\n            pageSlot.setAttribute(\"title\", pageTitle);\n            pageSlot.setAttribute(\"name\", pageNodeName);\n            if (child.className) {\n                pageSlot.setAttribute(\"className\", `\"${child.className}\"`);\n            }\n\n            if (child.getAttribute(\"autofocus\") === \"autofocus\") {\n                noteBook.setAttribute(\n                    \"defaultPage\",\n                    `__comp__.props.record.isNew ? \"${pageId}\" : (__comp__.props.activeNotebookPages[${noteBookId}] || \"${pageId}\")`\n                );\n            }\n\n            let isVisibleExpr;\n            if (!invisible || invisible === \"False\" || invisible === \"0\") {\n                isVisibleExpr = \"true\";\n            } else if (invisible === \"True\" || invisible === \"1\") {\n                isVisibleExpr = \"false\";\n            } else {\n                isVisibleExpr = `!__comp__.evaluateBooleanExpr(${JSON.stringify(\n                    invisible\n                )},__comp__.props.record.evalContextWithVirtualIds)`;\n            }\n            pageSlot.setAttribute(\"isVisible\", isVisibleExpr);\n\n            params.notebookPageFields = [];\n            for (const contents of child.children) {\n                append(pageSlot, this.compileNode(contents, { ...params, currentSlot: pageSlot }));\n            }\n            pageSlot.setAttribute(\"fieldNames\", `${JSON.stringify(params.notebookPageFields)}`);\n        }\n\n        return noteBook;\n    }\n\n    /**\n     * @param {Element} el\n     * @param {Record<string, any>} params\n     * @returns {Element}\n     */\n    compileSetting(el, params) {\n        const setting = createElement(params.componentName || \"Setting\", {\n            info: toStringExpression(el.getAttribute(\"info\") || \"\"),\n            title: toStringExpression(el.getAttribute(\"title\") || \"\"),\n            help: toStringExpression(el.getAttribute(\"help\") || \"\"),\n            companyDependent: el.getAttribute(\"company_dependent\") === \"1\" || \"false\",\n            documentation: toStringExpression(el.getAttribute(\"documentation\") || \"\"),\n            record: `__comp__.props.record`,\n        });\n        if (el.getAttribute(\"id\")) {\n            setting.setAttribute(\"id\", toStringExpression(el.getAttribute(\"id\")));\n        }\n        let string = toStringExpression(el.getAttribute(\"string\") || \"\");\n        let addLabel = true;\n        Array.from(el.children).forEach((child, index) => {\n            if (getTag(child, true) === \"field\" && index === 0) {\n                const fieldSlot = createElement(\"t\", { \"t-set-slot\": \"fieldSlot\" });\n                const field = this.compileNode(child, params);\n                if (field) {\n                    append(fieldSlot, field);\n                    setting.setAttribute(\"fieldInfo\", field.getAttribute(\"fieldInfo\"));\n\n                    addLabel = child.hasAttribute(\"nolabel\")\n                        ? child.getAttribute(\"nolabel\") !== \"1\"\n                        : true;\n                    const fieldName = child.getAttribute(\"name\");\n                    string = child.hasAttribute(\"string\")\n                        ? toStringExpression(child.getAttribute(\"string\"))\n                        : string;\n                    setting.setAttribute(\"fieldName\", toStringExpression(fieldName));\n                    setting.setAttribute(\n                        \"fieldId\",\n                        toStringExpression(child.getAttribute(\"field_id\"))\n                    );\n                }\n                append(setting, fieldSlot);\n            } else {\n                append(setting, this.compileNode(child, params));\n            }\n        });\n        setting.setAttribute(\"string\", string);\n        setting.setAttribute(\"addLabel\", addLabel);\n        return setting;\n    }\n\n    /**\n     * @param {Element} el\n     * @param {Record<string, any>} params\n     * @returns {Element}\n     */\n    compileSeparator(el, params = {}) {\n        const separator = makeSeparator(el.getAttribute(\"string\"));\n        copyAttributes(el, separator);\n        return this.applyInvisible(getModifier(el, \"invisible\"), separator, params);\n    }\n\n    /**\n     * @param {Element} el\n     * @param {Record<string, any>} params\n     * @returns {Element}\n     */\n    compileSheet(el, params) {\n        const sheetBG = createElement(\"div\", {\n            \"t-on-scroll\": \"__comp__.onScrollThrottled\",\n        });\n        sheetBG.className = \"o_form_sheet_bg\";\n\n        const sheetFG = createElement(\"div\");\n        sheetFG.className = \"o_form_sheet position-relative\";\n\n        append(sheetBG, sheetFG);\n        for (const child of el.childNodes) {\n            const compiled = this.compileNode(child, params);\n            if (!compiled) {\n                continue;\n            }\n            if (compiled.nodeName === \"ButtonBox\") {\n                // in form views with a sheet, the button box is moved to the\n                // control panel, and in dialogs, there's no button box\n                continue;\n            }\n            if (getTag(child, true) === \"field\") {\n                compiled.setAttribute(\"showTooltip\", true);\n            }\n            append(sheetFG, compiled);\n        }\n        return sheetBG;\n    }\n}\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { hasTouch } from \"@web/core/browser/feature_detection\";\nimport { ConfirmationDialog } from \"@web/core/confirmation_dialog/confirmation_dialog\";\nimport { makeContext } from \"@web/core/context\";\nimport { useDebugCategory } from \"@web/core/debug/debug_context\";\nimport { registry } from \"@web/core/registry\";\nimport { SIZES } from \"@web/core/ui/ui_service\";\nimport { user } from \"@web/core/user\";\nimport { useBus, useService } from \"@web/core/utils/hooks\";\nimport { omit } from \"@web/core/utils/objects\";\nimport { createElement, parseXML } from \"@web/core/utils/xml\";\nimport { evaluateBooleanExpr } from \"@web/core/py_js/py\";\nimport { useSetupAction } from \"@web/search/action_hook\";\nimport { Layout } from \"@web/search/layout\";\nimport { usePager } from \"@web/search/pager_hook\";\nimport { standardViewProps } from \"@web/views/standard_view_props\";\nimport { isX2Many } from \"@web/views/utils\";\nimport { executeButtonCallback, useViewButtons } from \"@web/views/view_button/view_button_hook\";\nimport { ViewButton } from \"@web/views/view_button/view_button\";\nimport { Field } from \"@web/views/fields/field\";\nimport { useModel } from \"@web/model/model\";\nimport { addFieldDependencies, extractFieldsFromArchInfo } from \"@web/model/relational_model/utils\";\nimport { useViewCompiler } from \"@web/views/view_compiler\";\nimport { useDeleteRecords } from \"@web/views/view_hook\";\nimport { Widget } from \"@web/views/widgets/widget\";\nimport { STATIC_ACTIONS_GROUP_NUMBER } from \"@web/search/action_menus/action_menus\";\n\nimport { ButtonBox } from \"./button_box/button_box\";\nimport { FormCompiler } from \"./form_compiler\";\nimport { FormErrorDialog } from \"./form_error_dialog/form_error_dialog\";\nimport { FormStatusIndicator } from \"./form_status_indicator/form_status_indicator\";\nimport { FormCogMenu } from \"./form_cog_menu/form_cog_menu\";\n\nimport {\n    Component,\n    onError,\n    onMounted,\n    onRendered,\n    onWillUnmount,\n    status,\n    useComponent,\n    useEffect,\n    useRef,\n    useState,\n} from \"@odoo/owl\";\nimport { FetchRecordError } from \"@web/model/relational_model/errors\";\nimport { effect } from \"@web/core/utils/reactive\";\n\nconst viewRegistry = registry.category(\"views\");\n\nexport async function loadSubViews(fieldNodes, fields, context, resModel, viewService, isSmall) {\n    for (const fieldInfo of Object.values(fieldNodes)) {\n        const fieldName = fieldInfo.name;\n        const field = fields[fieldName];\n        if (!isX2Many(field)) {\n            continue; // what follows only concerns x2many fields\n        }\n        if (fieldInfo.invisible === \"True\" || fieldInfo.invisible === \"1\") {\n            continue; // no need to fetch the sub view if the field is always invisible\n        }\n        if (!fieldInfo.field.useSubView) {\n            continue; // the FieldComponent used to render the field doesn't need a sub view\n        }\n\n        fieldInfo.views = fieldInfo.views || {};\n        let viewType = fieldInfo.viewMode || \"list,kanban\";\n        if (viewType.includes(\",\")) {\n            viewType = isSmall ? \"kanban\" : \"list\";\n        }\n        fieldInfo.viewMode = viewType;\n        if (fieldInfo.views[viewType]) {\n            continue; // the sub view is inline in the main form view\n        }\n\n        // extract *_view_ref keys from field context, to fetch the adequate view\n        const fieldContext = {};\n        const regex = /'([a-z]*_view_ref)' *: *'(.*?)'/g;\n        let matches;\n        while ((matches = regex.exec(fieldInfo.context)) !== null) {\n            fieldContext[matches[1]] = matches[2];\n        }\n        // filter out *_view_ref keys from general context\n        const refinedContext = {};\n        for (const key in context) {\n            if (key.indexOf(\"_view_ref\") === -1) {\n                refinedContext[key] = context[key];\n            }\n        }\n\n        const comodel = field.relation;\n        const {\n            fields: comodelFields,\n            relatedModels,\n            views,\n        } = await viewService.loadViews({\n            resModel: comodel,\n            views: [[false, viewType]],\n            context: makeContext([fieldContext, user.context, refinedContext]),\n        });\n        const { ArchParser } = viewRegistry.get(viewType);\n        const xmlDoc = parseXML(views[viewType].arch);\n        const archInfo = new ArchParser().parse(xmlDoc, relatedModels, comodel);\n        fieldInfo.views[viewType] = {\n            ...archInfo,\n            limit: archInfo.limit || 40,\n            fields: comodelFields,\n        };\n        fieldInfo.relatedFields = comodelFields;\n    }\n}\n\nexport function useFormViewInDialog() {\n    const component = useComponent();\n    onMounted(() => {\n        component.env.bus.trigger(\"FORM-CONTROLLER:FORM-IN-DIALOG:ADD\");\n    });\n\n    onWillUnmount(() => {\n        component.env.bus.trigger(\"FORM-CONTROLLER:FORM-IN-DIALOG:REMOVE\");\n    });\n}\n// -----------------------------------------------------------------------------\n\nexport class FormController extends Component {\n    static template = `web.FormView`;\n    static components = {\n        FormStatusIndicator,\n        Layout,\n        ButtonBox,\n        ViewButton,\n        Field,\n        CogMenu: FormCogMenu,\n        Widget,\n    };\n\n    static props = {\n        ...standardViewProps,\n        discardRecord: { type: Function, optional: true },\n        readonly: { type: Boolean, optional: true },\n        saveRecord: { type: Function, optional: true },\n        removeRecord: { type: Function, optional: true },\n        Model: Function,\n        Renderer: Function,\n        Compiler: Function,\n        archInfo: Object,\n        buttonTemplate: String,\n        preventCreate: { type: Boolean, optional: true },\n        preventEdit: { type: Boolean, optional: true },\n        onDiscard: { type: Function, optional: true },\n        onSave: { type: Function, optional: true },\n    };\n    static defaultProps = {\n        preventCreate: false,\n        preventEdit: false,\n        updateActionState: () => {},\n    };\n\n    setup() {\n        this.evaluateBooleanExpr = evaluateBooleanExpr;\n        this.actionService = useService(\"action\");\n        this.dialogService = useService(\"dialog\");\n        this.orm = useService(\"orm\");\n        this.viewService = useService(\"view\");\n        this.ui = useService(\"ui\");\n        useBus(this.ui.bus, \"resize\", this.render);\n\n        this.archInfo = this.props.archInfo;\n        const { create, edit } = this.archInfo.activeActions;\n        this.canCreate = create && !this.props.preventCreate;\n        this.canEdit = edit && !this.props.preventEdit;\n        this.duplicateId = false;\n\n        this.display = { ...this.props.display };\n        if (this.env.inDialog) {\n            this.display.controlPanel = false;\n        }\n\n        this.formInDialog = 0;\n        useBus(this.env.bus, \"FORM-CONTROLLER:FORM-IN-DIALOG:ADD\", () => this.formInDialog++);\n        useBus(this.env.bus, \"FORM-CONTROLLER:FORM-IN-DIALOG:REMOVE\", () => this.formInDialog--);\n\n        // Wait to be mounted before displaying dialog/notification for onchange warnings returned\n        // by the first onchange, for 2 reasons:\n        //  1) we don't want to show twice the warning if the component is destroyed before being\n        //     mounted and re-created\n        //  2) for form views in dialogs, this causes an infinite loop if willStart calls dialog.add\n        const mountedProm = new Promise((r) => onMounted(r));\n        this.onWillDisplayOnchangeWarning = () => mountedProm;\n\n        const beforeFirstLoad = async () => {\n            await loadSubViews(\n                this.archInfo.fieldNodes,\n                this.props.fields,\n                this.props.context,\n                this.props.resModel,\n                this.viewService,\n                this.env.isSmall\n            );\n            const { activeFields, fields } = extractFieldsFromArchInfo(\n                this.archInfo,\n                this.props.fields\n            );\n            if (this.display.controlPanel) {\n                addFieldDependencies(activeFields, fields, [\n                    { name: \"display_name\", type: \"char\", readonly: true },\n                ]);\n            }\n            this.model.config.activeFields = activeFields;\n            this.model.config.fields = fields;\n        };\n        this.model = useState(useModel(this.props.Model, this.modelParams, { beforeFirstLoad }));\n\n        onMounted(() => {\n            effect(\n                (model) => {\n                    if (status(this) === \"mounted\") {\n                        this.props.updateActionState({ resId: model.root.resId });\n                    }\n                },\n                [this.model]\n            );\n        });\n\n        onError((error) => {\n            const suggestedCompany = error.cause?.data?.context?.suggested_company;\n            if (\n                error.cause?.data?.name === \"odoo.exceptions.AccessError\" &&\n                suggestedCompany &&\n                !this.env.inDialog\n            ) {\n                this.env.pushStateBeforeReload();\n                const activeCompanyIds = user.activeCompanies.map((c) => c.id);\n                activeCompanyIds.push(suggestedCompany.id);\n                user.activateCompanies(activeCompanyIds);\n            } else {\n                throw error;\n            }\n        });\n\n        // select footers that are not in subviews and move them to another arch\n        // that will be moved to the dialog's footer (if we are in a dialog)\n        const footers = [...this.archInfo.xmlDoc.querySelectorAll(\"footer:not(field footer)\")];\n        if (footers.length) {\n            this.footerArchInfo = Object.assign({}, this.archInfo);\n            this.footerArchInfo.xmlDoc = createElement(\"t\");\n            this.footerArchInfo.xmlDoc.append(...footers);\n            this.footerArchInfo.arch = this.footerArchInfo.xmlDoc.outerHTML;\n            this.archInfo.arch = this.archInfo.xmlDoc.outerHTML;\n        }\n\n        const xmlDocButtonBox = this.archInfo.xmlDoc.querySelector(\n            \"div[name='button_box']:not(field div)\"\n        );\n        if (xmlDocButtonBox) {\n            const buttonBoxTemplates = useViewCompiler(\n                this.props.Compiler || FormCompiler,\n                { ButtonBox: xmlDocButtonBox },\n                { isSubView: true }\n            );\n            this.buttonBoxTemplate = buttonBoxTemplates.ButtonBox;\n        }\n\n        this.rootRef = useRef(\"root\");\n        useViewButtons(this.rootRef, {\n            beforeExecuteAction: this.beforeExecuteActionButton.bind(this),\n            afterExecuteAction: this.afterExecuteActionButton.bind(this),\n            reload: () => this.model.load(),\n        });\n\n        const state = this.props.state || {};\n        const activeNotebookPages = { ...state.activeNotebookPages };\n        this.onNotebookPageChange = (notebookId, page) => {\n            if (page) {\n                activeNotebookPages[notebookId] = page;\n            }\n        };\n\n        useSetupAction({\n            rootRef: this.rootRef,\n            beforeVisibilityChange: () => this.beforeVisibilityChange(),\n            beforeLeave: (options) => this.beforeLeave(options),\n            beforeUnload: (ev) => this.beforeUnload(ev),\n            getLocalState: () => ({\n                activeNotebookPages: !this.model.root.isNew ? activeNotebookPages : {},\n                modelState: this.model.exportState(),\n                resId: this.model.root.resId,\n            }),\n        });\n        useDebugCategory(\"form\", { component: this });\n\n        usePager(() => {\n            if (!this.model.root.isNew) {\n                const resIds = this.model.root.resIds;\n                return {\n                    offset: resIds.indexOf(this.model.root.resId),\n                    limit: 1,\n                    total: resIds.length,\n                    onUpdate: ({ offset }) => this.onPagerUpdate({ offset, resIds }),\n                };\n            }\n        });\n\n        onRendered(() => {\n            this.env.config.setDisplayName(this.displayName());\n        });\n\n        const { disableAutofocus } = this.archInfo;\n        if (!disableAutofocus) {\n            useEffect(\n                (isInEdition) => {\n                    if (\n                        !isInEdition &&\n                        !this.rootRef.el\n                            .querySelector(\".o_content\")\n                            .contains(document.activeElement)\n                    ) {\n                        const elementToFocus = this.rootRef.el.querySelector(\n                            \".o_content button.btn-primary\"\n                        );\n                        if (elementToFocus) {\n                            elementToFocus.focus();\n                        }\n                    }\n                },\n                () => [this.model.root.isInEdition]\n            );\n        }\n\n        if (this.env.inDialog) {\n            useFormViewInDialog();\n        }\n\n        this.deleteRecordsWithConfirmation = useDeleteRecords(this.model);\n    }\n\n    get cogMenuProps() {\n        return {\n            getActiveIds: () => (this.model.root.isNew ? [] : [this.model.root.resId]),\n            context: this.model.root.context,\n            items: this.props.info.actionMenus ? this.actionMenuItems : {},\n            isDomainSelected: this.model.root.isDomainSelected,\n            resModel: this.model.root.resModel,\n            domain: this.props.domain,\n            onActionExecuted: ({ noReload } = {}) => {\n                if (!noReload) {\n                    const { resId, resIds } = this.model.root;\n                    return this.model.load({ resId: resId, resIds: resIds });\n                }\n            },\n            shouldExecuteAction: this.shouldExecuteAction.bind(this),\n        };\n    }\n\n    get modelParams() {\n        return {\n            config: {\n                resModel: this.props.resModel,\n                resId: this.props.resId || false,\n                resIds: this.props.resIds || (this.props.resId ? [this.props.resId] : []),\n                fields: this.props.fields,\n                activeFields: {}, // will be generated after loading sub views (see willStart)\n                isMonoRecord: true,\n                mode: this.props.readonly ? \"readonly\" : \"edit\",\n                context: this.props.context,\n            },\n            state: this.props.state?.modelState,\n            hooks: {\n                onWillLoadRoot: this.onWillLoadRoot.bind(this),\n                onWillSaveRecord: this.onWillSaveRecord.bind(this),\n                onRecordSaved: this.onRecordSaved.bind(this),\n                onWillDisplayOnchangeWarning: this.onWillDisplayOnchangeWarning.bind(this),\n            },\n            useSendBeaconToSaveUrgently: true,\n        };\n    }\n\n    /**\n     * onWillLoadRoot is a callback that will be executed before (re)loading the\n     * data necessary for the root record datapoint. Note that this.model.root\n     * may not exist yet at this point, if this is the first load.\n     */\n    onWillLoadRoot() {\n        this.duplicateId = undefined;\n    }\n\n    /**\n     * onRecordSaved is a callBack that will be executed after the save\n     * if it was done. It will therefore not be executed if the record\n     * is invalid, if a server error is thrown, or if there are no\n     * changes to save.\n     * @param {Record} record\n     */\n    async onRecordSaved(record, changes) {\n        if (this.duplicateId === record.id) {\n            const translationChanges = {};\n            for (const fieldName in changes) {\n                if (record.fields[fieldName].translate) {\n                    translationChanges[fieldName] = changes[fieldName];\n                }\n            }\n            if (Object.keys(translationChanges).length) {\n                await this.orm.call(this.model.root.resModel, \"web_override_translations\", [\n                    [this.model.root.resId],\n                    translationChanges,\n                ]);\n            }\n        }\n    }\n\n    /**\n     * onWillSaveRecord is a callBack that will be executed before the\n     * record save if the record is valid if the record is valid.\n     * If it returns false, it will prevent the save.\n     * @param {Record} record\n     */\n    async onWillSaveRecord() {}\n\n    async onSaveError(error, { discard, retry }, leaving) {\n        const suggestedCompany = error.data?.context?.suggested_company;\n        const activeCompanyIds = user.activeCompanies.map((c) => c.id);\n        if (\n            error.data?.name === \"odoo.exceptions.AccessError\" &&\n            suggestedCompany &&\n            !activeCompanyIds.includes(suggestedCompany.id)\n        ) {\n            // update the context with the needed company\n            this.model.config.context.allowed_company_ids.push(suggestedCompany.id);\n            // activate the company without reloading !\n            activeCompanyIds.push(suggestedCompany.id);\n            user.activateCompanies(activeCompanyIds, { reload: false });\n            return retry();\n        }\n        if (leaving) {\n            const proceed = await new Promise((resolve) => {\n                this.model.dialog.add(FormErrorDialog, {\n                    message: error.data.message,\n                    data: error.data,\n                    onDiscard: () => {\n                        discard();\n                        resolve(true);\n                    },\n                    onRedirect: async ({ action, additionalContext }) => {\n                        try {\n                            await this.actionService.doAction(action, {\n                                additionalContext,\n                                forceLeave: true,\n                            });\n                        } finally {\n                            resolve(false);\n                        }\n                    },\n                    onStayHere: () => resolve(false),\n                });\n            });\n            return proceed;\n        }\n        throw error;\n    }\n\n    displayName() {\n        return this.model.root.data.display_name || (this.model.root.isNew && _t(\"New\")) || \"\";\n    }\n\n    async onPagerUpdate({ offset, resIds }) {\n        const dirty = await this.model.root.isDirty();\n        try {\n            if (dirty) {\n                await this.model.root.save({\n                    onError: (error, options) => this.onSaveError(error, options, true),\n                    nextId: resIds[offset],\n                });\n            } else {\n                await this.model.load({ resId: resIds[offset] });\n            }\n        } catch (e) {\n            if (e instanceof FetchRecordError) {\n                this.model.load({\n                    resIds: this.model.config.resIds.filter((id) => !e.resIds.includes(id)),\n                });\n            }\n            throw e;\n        }\n    }\n\n    beforeVisibilityChange() {\n        if (document.visibilityState === \"hidden\" && this.formInDialog === 0) {\n            return this.model.root.save();\n        }\n    }\n\n    async beforeLeave({ forceLeave } = {}) {\n        if (this.model.root.dirty && !forceLeave) {\n            return this.save({\n                reload: false,\n                onError: (error, options) => this.onSaveError(error, options, true),\n            });\n        }\n    }\n\n    async beforeUnload(ev) {\n        const succeeded = await this.model.root.urgentSave();\n        if (!succeeded) {\n            ev.preventDefault();\n            ev.returnValue = \"Unsaved changes\";\n        }\n    }\n\n    getStaticActionMenuItems() {\n        const { activeActions } = this.archInfo;\n        return {\n            addPropertyFieldValue: {\n                isAvailable: () => activeActions.addPropertyFieldValue,\n                sequence: 10,\n                icon: \"fa fa-cogs\",\n                description: _t(\"Edit Properties\"),\n                callback: () => this.model.bus.trigger(\"PROPERTY_FIELD:EDIT\"),\n            },\n            duplicate: {\n                isAvailable: () => activeActions.create && activeActions.duplicate,\n                sequence: 30,\n                icon: \"fa fa-clone\",\n                description: _t(\"Duplicate\"),\n                callback: () => this.duplicateRecord(),\n            },\n            archive: {\n                isAvailable: () => this.archiveEnabled && this.model.root.isActive,\n                sequence: 40,\n                description: _t(\"Archive\"),\n                icon: \"oi oi-archive\",\n                callback: () => {\n                    this.dialogService.add(ConfirmationDialog, this.archiveDialogProps);\n                },\n            },\n            unarchive: {\n                isAvailable: () => this.archiveEnabled && !this.model.root.isActive,\n                sequence: 45,\n                icon: \"oi oi-unarchive\",\n                description: _t(\"Unarchive\"),\n                callback: () => this.model.root.unarchive(),\n            },\n            delete: {\n                isAvailable: () => activeActions.delete && !this.model.root.isNew,\n                sequence: 50,\n                icon: \"fa fa-trash-o\",\n                description: _t(\"Delete\"),\n                class: \"text-danger\",\n                callback: () => this.deleteRecord(),\n                skipSave: true,\n            },\n        };\n    }\n\n    get archiveDialogProps() {\n        return {\n            body: _t(\"Are you sure that you want to archive this record?\"),\n            confirmLabel: _t(\"Archive\"),\n            confirm: () => this.model.root.archive(),\n            cancel: () => {},\n        };\n    }\n\n    get actionMenuItems() {\n        const { actionMenus } = this.props.info;\n        const staticActionItems = Object.entries(this.getStaticActionMenuItems())\n            .filter(([key, item]) => item.isAvailable === undefined || item.isAvailable())\n            .sort(([k1, item1], [k2, item2]) => (item1.sequence || 0) - (item2.sequence || 0))\n            .map(([key, item]) =>\n                Object.assign({ key }, omit(item, \"isAvailable\", \"sequence\"), {\n                    groupNumber: STATIC_ACTIONS_GROUP_NUMBER,\n                })\n            );\n\n        return {\n            action: [...staticActionItems, ...(actionMenus.action || [])],\n            print: actionMenus.print,\n        };\n    }\n\n    // enable the archive feature in Actions menu only if the active field is in the view\n    get archiveEnabled() {\n        return \"active\" in this.model.root.activeFields\n            ? !this.props.fields.active.readonly\n            : \"x_active\" in this.model.root.activeFields\n            ? !this.props.fields.x_active.readonly\n            : false;\n    }\n\n    async shouldExecuteAction(item) {\n        const dirty = await this.model.root.isDirty();\n        if ((dirty || this.model.root.isNew) && !item.skipSave) {\n            let hasError = false;\n            const isSaved = await this.model.root.save({\n                onError: (error, options) => {\n                    hasError = true;\n                    return this.onSaveError(error, options, true);\n                },\n            });\n            return isSaved && !hasError;\n        }\n        return true;\n    }\n\n    async duplicateRecord() {\n        await this.model.root.duplicate();\n        this.duplicateId = this.model.root.id;\n    }\n\n    get deleteConfirmationDialogProps() {\n        return {\n            confirm: async () => {\n                await this.model.root.delete();\n                if (!this.model.root.resId) {\n                    this.env.config.historyBack();\n                }\n            },\n        };\n    }\n\n    async deleteRecord() {\n        this.deleteRecordsWithConfirmation(this.deleteConfirmationDialogProps, [this.model.root]);\n    }\n\n    async beforeExecuteActionButton(clickParams) {\n        const record = this.model.root;\n        if (clickParams.special !== \"cancel\") {\n            let saved = false;\n            if (clickParams.special === \"save\" && this.props.saveRecord) {\n                saved = await this.props.saveRecord(record, clickParams);\n            } else {\n                const params = { reload: !(this.env.inDialog && clickParams.close) };\n                saved = await record.save(params);\n            }\n            if (saved !== false && this.props.onSave) {\n                this.props.onSave(record, clickParams);\n            }\n            return saved;\n        } else if (this.props.onDiscard) {\n            this.props.onDiscard(record);\n        }\n    }\n\n    async afterExecuteActionButton(clickParams) {}\n\n    async create() {\n        const dirty = await this.model.root.isDirty();\n        const onError = (error, options) => this.onSaveError(error, options, true);\n        const canProceed = !dirty || (await this.model.root.save({ onError }));\n        // FIXME: disable/enable not done in onPagerUpdate\n        if (canProceed) {\n            await executeButtonCallback(this.ui.activeElement, () =>\n                this.model.load({ resId: false })\n            );\n        }\n    }\n\n    async save(params) {\n        const record = this.model.root;\n        let saved = false;\n        if (this.props.saveRecord) {\n            saved = await this.props.saveRecord(record, params);\n        } else {\n            saved = await record.save({\n                onError: (error, options) => this.onSaveError(error, options, false),\n                ...params,\n            });\n        }\n        if (saved && this.props.onSave) {\n            this.props.onSave(record, params);\n        }\n        return saved;\n    }\n\n    saveButtonClicked(params = {}) {\n        return executeButtonCallback(this.ui.activeElement, () => this.save(params));\n    }\n\n    async discard() {\n        if (this.props.discardRecord) {\n            this.props.discardRecord(this.model.root);\n            return;\n        }\n        await this.model.root.discard();\n        if (this.props.onDiscard) {\n            this.props.onDiscard(this.model.root);\n        }\n        if (this.env.inDialog) {\n            await this.env.dialogData.close();\n        } else if (this.model.root.isNew) {\n            this.env.config.historyBack();\n        }\n    }\n\n    get className() {\n        const result = {};\n        const { size } = this.ui;\n        if (size <= SIZES.XS) {\n            result.o_xxs_form_view = true;\n        } else if (!this.env.inDialog && size === SIZES.XXL) {\n            result[\"o_xxl_form_view h-100\"] = true;\n        }\n        if (this.props.className) {\n            result[this.props.className] = true;\n        }\n        result[\"o_field_highlight\"] = size < SIZES.SM || hasTouch();\n        return result;\n    }\n}\n", "import { patch } from \"@web/core/utils/patch\";\nimport { FormController } from \"@web/views/form/form_controller\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { useEffect } from \"@odoo/owl\";\n\npatch(FormController.prototype, {\n    setup() {\n        super.setup();\n        this.aiChatLauncher = useService(\"aiChatLauncher\");\n        const openAiChat = (ev) => this.openAiChat(ev.detail.origin);\n        useEffect(\n            () => {\n                this.env.bus.addEventListener(\"AI:OPEN_AI_CHAT\", openAiChat);\n                return () => this.env.bus.removeEventListener(\"AI:OPEN_AI_CHAT\", openAiChat);\n            },\n            () => []\n        );\n    },\n\n    async openAiChat(callerComponentName) {\n        // save to allow to get messages from backend\n        if (!this.mailStore || !(await this.model.root.save())) {\n            return;\n        }\n        this.aiChatLauncher.launchAIChat({\n            callerComponentName,\n            channelTitle: this.displayName(),\n            recordModel: this.model.root.resModel,\n            recordId: this.model.root.resId,\n            originalRecordData: this.model.root.data,\n            originalRecordFields: this.model.root.fields,\n            aiChatSourceId: this.model.root.resId,\n        });\n    },\n});\n", "import { FormController } from \"@web/views/form/form_controller\";\nimport { patch } from \"@web/core/utils/patch\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { evaluateBooleanExpr } from \"@web/core/py_js/py\";\nimport { CallbackRecorder } from \"@web/search/action_hook\";\nimport {\n    useEffect,\n    useSubEnv\n} from \"@odoo/owl\";\n\n\n/**\n * Knowledge articles can interact with some records with the help of the\n * @see KnowledgeCommandsService if they have an html field.\n * The algorithm first searches for an html field name in the following list.\n * The list is ordered and the first match found in a record will take\n * precedence. If no match is found, any accessible html field in the view\n * will do, in order of declaration.\n * Once a match is found, it is stored in the\n * KnowledgeCommandsService to be accessed later by an article.\n * If the field is inside a Form notebook page, the page must have a name\n * attribute, or else it won't be considered as Knowledge macros won't be able\n * to access it through a selector.\n */\nconst KNOWLEDGE_RECORDED_FIELD_NAMES = [\n    'note',\n    'memo',\n    'description',\n    'comment',\n    'narration',\n    'additional_note',\n    'internal_notes',\n    'notes',\n];\n\n/**\n * Here are the models whose html fields won't be accessible through Knowledge.\n * A consideration to add a model in this list is how heavily modified their\n * form view is compared to the standard, because then the macro will not be\n * able to navigate them.\n */\nconst KNOWLEDGE_EXCLUDED_MODELS = new Set([\n    'knowledge.article',\n    'res.config.settings',\n]);\n\nconst FormControllerPatch = {\n    setup() {\n        super.setup(...arguments);\n        this.command = useService(\"command\");\n        if (!this.env.inDialog) {\n            this.knowledgeCommandsService = useService('knowledgeCommandsService');\n            useSubEnv({\n                __knowledgeUpdateCommandsRecordInfo__: new CallbackRecorder(),\n            });\n            useEffect(\n                () => this._evaluateRecordCandidate(),\n                () => [this.model.root.resId],\n            );\n        }\n    },\n    /**\n     * Evaluate the current record and register its relevant information in\n     * @see KnowledgeCommandsService if it can be used in a Knowledge article\n     * through a macro.\n     *\n     * The access rights information for the chatter is only available\n     * asynchronously after it is mounted, therefore populating this\n     * information for the `commandsRecordInfo` is delegated to the chatter\n     * through the __knowledgeUpdateCommandsRecordInfo__ callbackRecorder.\n     */\n    _evaluateRecordCandidate() {\n        if (\n            KNOWLEDGE_EXCLUDED_MODELS.has(this.props.resModel) ||\n            !this.env.config.breadcrumbs ||\n            !this.env.config.breadcrumbs.length\n        ) {\n            return;\n        }\n\n        const record = this.model.root;\n        const fields = this.props.fields;\n        const xmlDoc = this.props.archInfo.xmlDoc;\n        // format stored by the knowledgeCommandsService\n        const commandsRecordInfo = {\n            resId: this.model.root.resId,\n            resModel: this.props.resModel,\n            breadcrumbs: this.knowledgeCommandsService.getBreadcrumbsIdentifier(\n                this.env.config.breadcrumbs || []\n            ),\n            canPostMessages: false,\n            canAttachFiles: false,\n            withHtmlField: false,\n            fieldInfo: {},\n            xmlDoc: this.props.archInfo.xmlDoc,\n        };\n\n        // check whether the form view has a chatter\n        if (this.props.archInfo.xmlDoc.querySelector(\"chatter\")) {\n            for (const callback of this.env.__knowledgeUpdateCommandsRecordInfo__.callbacks) {\n                callback(commandsRecordInfo);\n            }\n        }\n\n        if (this.props.mode === \"readonly\" || !this.canEdit) {\n            return;\n        }\n\n        const defaultFieldNamesSet = new Set(KNOWLEDGE_RECORDED_FIELD_NAMES);\n        const fieldNames = KNOWLEDGE_RECORDED_FIELD_NAMES.filter((name) => {\n            return (\n                name in record.activeFields &&\n                fields[name].type === \"html\" &&\n                !fields[name].readonly\n            );\n        }).concat(\n            Object.getOwnPropertyNames(record.activeFields).filter((name) => {\n                return (\n                    !defaultFieldNamesSet.has(name) &&\n                    fields[name].type === \"html\" &&\n                    !fields[name].readonly\n                );\n            })\n        );\n\n        // check if there is any html field usable with knowledge\n        loopFieldNames: for (const fieldName of fieldNames) {\n            if (\n                evaluateBooleanExpr(record.activeFields[fieldName].readonly, record.evalContextWithVirtualIds) ||\n                evaluateBooleanExpr(record.activeFields[fieldName].invisible, record.evalContextWithVirtualIds)\n            ) {\n                continue loopFieldNames;\n            }\n            // Parse the xmlDoc to find all instances of the field that are\n            // not descendants of another field and whose parents are\n            // visible (relative to the current record's context). Evaluate\n            // the invisible and readonly modifiers attributes for each field\n            // instance in the xmlDoc. Don't consider html fields that are not\n            // using the `html` widget (i.e. mass_mailing_html widget).\n            const xmlFields = Array.from(xmlDoc.querySelectorAll(`field[name=\"${fieldName}\"]:is([widget=\"html\"], :not([widget])`));\n            const xmlFieldsCandidates = xmlFields.filter((xmlField) => {\n                return (\n                    !(xmlField.parentElement.closest('field')) &&\n                    !evaluateBooleanExpr(xmlField.getAttribute('readonly'), record.evalContextWithVirtualIds) &&\n                    !evaluateBooleanExpr(xmlField.getAttribute('invisible'), record.evalContextWithVirtualIds)\n                );\n            });\n            loopXmlFieldsCandidates: for (const xmlField of xmlFieldsCandidates) {\n                const xmlFieldParent = xmlField.parentElement;\n                let xmlInvisibleParent = xmlFieldParent.closest('[invisible]');\n                while (xmlInvisibleParent) {\n                    const invisibleParentModifier = xmlInvisibleParent.getAttribute('invisible');\n                    if (evaluateBooleanExpr(invisibleParentModifier, record.evalContextWithVirtualIds)) {\n                        continue loopXmlFieldsCandidates;\n                    }\n                    xmlInvisibleParent = xmlInvisibleParent.parentElement &&\n                        xmlInvisibleParent.parentElement.closest('[invisible]');\n                }\n                const page = xmlField.closest('page');\n                const pageName = page ? page.getAttribute('name') : undefined;\n                // If the field is inside an unnamed notebook page, ignore\n                // it as if it was unavailable, since the macro will not be\n                // able to open it to access the field (the name is used as\n                // a selector).\n                if (!page || pageName) {\n                    commandsRecordInfo.fieldInfo = {\n                        name: fieldName,\n                        string: fields[fieldName].string,\n                        pageName: pageName,\n                    };\n                    break loopFieldNames;\n                }\n            }\n        }\n        if (commandsRecordInfo.fieldInfo.name) {\n            commandsRecordInfo.withHtmlField = true;\n        }\n        if (this.knowledgeCommandsService.isRecordCompatibleWithMacro(commandsRecordInfo)) {\n            this.knowledgeCommandsService.setCommandsRecordInfo(commandsRecordInfo);\n        }\n    },\n    async onClickSearchKnowledgeArticle() {\n        if (await this.model.root.isDirty() || this.model.root.isNew) {\n            const saved = await this.model.root.save();\n            if (!saved) {\n                return;\n            }\n        }\n        this.command.openMainPalette({ searchValue: \"?\" });\n    },\n};\n\npatch(FormController.prototype, FormControllerPatch);\n", "import { Dialog } from \"@web/core/dialog/dialog\";\nimport { useService } from \"@web/core/utils/hooks\";\n\nimport { Component } from \"@odoo/owl\";\n\nexport class FormErrorDialog extends Component {\n    static template = \"web.FormErrorDialog\";\n    static components = { Dialog };\n    static props = {\n        message: { type: String, optional: true },\n        data: { type: Object },\n        onDiscard: Function,\n        onStayHere: Function,\n        onRedirect: { type: Function, optional: true },\n        close: Function,\n    };\n\n    setup() {\n        this.action = useService(\"action\");\n        this.message = this.props.message;\n        if (this.props?.data.name === \"odoo.exceptions.RedirectWarning\") {\n            this.message = this.props.data.arguments[0];\n            this.redirectAction = this.props.data.arguments[1];\n            this.redirectBtnLabel = this.props.data.arguments[2];\n            this.additionalContext = this.props.data.arguments[3];\n        }\n    }\n\n    async onRedirectBtnClicked() {\n        if (this.props.onRedirect) {\n            await this.props.onRedirect({\n                action: this.redirectAction,\n                additionalContext: this.additionalContext,\n            });\n            this.props.close();\n        } else {\n            await this.action.doAction(this.redirectAction, {\n                additionalContext: this.additionalContext,\n                forceLeave: true,\n            });\n            this.stay();\n        }\n    }\n\n    async discard() {\n        await this.props.onDiscard();\n        this.props.close();\n    }\n\n    async stay() {\n        await this.props.onStayHere();\n        this.props.close();\n    }\n}\n", "import { Component } from \"@odoo/owl\";\nimport { sortBy } from \"@web/core/utils/arrays\";\n\nclass Group extends Component {\n    static template = \"\";\n    static props = [\"class?\", \"slots?\", \"maxCols?\", \"style?\"];\n    static defaultProps = {\n        maxCols: 2,\n    };\n\n    _getItems() {\n        const items = Object.entries(this.props.slots || {}).filter(([k, v]) => v.type === \"item\");\n        return sortBy(items, (i) => i[1].sequence);\n    }\n\n    getItems() {\n        return this._getItems();\n    }\n\n    get allClasses() {\n        return this.props.class;\n    }\n}\n\nexport class OuterGroup extends Group {\n    static template = \"web.Form.OuterGroup\";\n    static defaultProps = {\n        ...Group.defaultProps,\n        slots: [],\n        hasOuterTemplate: true,\n    };\n\n    getItems() {\n        const nbCols = this.props.maxCols;\n        const colSize = Math.max(1, Math.round(12 / nbCols));\n\n        // Dispatch items across table rows\n        const items = super.getItems().filter(([k, v]) => !(\"isVisible\" in v) || v.isVisible);\n        return items.map((item) => {\n            const [slotName, slot] = item;\n            const itemSpan = slot.itemSpan || 1;\n            return {\n                name: slotName,\n                size: itemSpan * colSize,\n                newline: slot.newline,\n                colspan: itemSpan,\n            };\n        });\n    }\n}\n\nexport class InnerGroup extends Group {\n    static template = \"web.Form.InnerGroup\";\n    getTemplate(subType) {\n        return this.constructor.templates[subType] || this.constructor.templates.default;\n    }\n    getRows() {\n        const maxCols = this.props.maxCols;\n\n        const rows = [];\n        let currentRow = [];\n        let reservedSpace = 0;\n\n        // Dispatch items across table rows\n        const items = this.getItems();\n        while (items.length) {\n            const [slotName, slot] = items.shift();\n            if (!slot.isVisible) {\n                continue;\n            }\n\n            const { newline, itemSpan } = slot;\n            if (newline) {\n                rows.push(currentRow);\n                currentRow = [];\n                reservedSpace = 0;\n            }\n\n            const fullItemSpan = itemSpan || 1;\n\n            if (fullItemSpan + reservedSpace > maxCols) {\n                rows.push(currentRow);\n                currentRow = [];\n                reservedSpace = 0;\n            }\n\n            const isVisible = !(\"isVisible\" in slot) || slot.isVisible;\n            currentRow.push({ ...slot, name: slotName, itemSpan, isVisible });\n            reservedSpace += itemSpan || 1;\n\n            // Allows to remove the line if the content is not visible instead of leaving an empty line.\n            currentRow.isVisible = currentRow.isVisible || isVisible;\n        }\n        rows.push(currentRow);\n\n        return rows;\n    }\n}\n", "import { fieldVisualFeedback } from \"@web/views/fields/field\";\nimport { getTooltipInfo } from \"@web/views/fields/field_tooltip\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { Component } from \"@odoo/owl\";\nimport { user } from \"@web/core/user\";\n\nexport class FormLabel extends Component {\n    static template = \"web.FormLabel\";\n    static props = {\n        fieldInfo: { type: Object },\n        record: { type: Object },\n        fieldName: { type: String },\n        className: { type: String, optional: true },\n        string: { type: String },\n        id: { type: String },\n        notMuttedLabel: { type: Boolean, optional: true },\n    };\n\n    get className() {\n        const { invalid, empty, readonly } = fieldVisualFeedback(\n            this.props.fieldInfo.field,\n            this.props.record,\n            this.props.fieldName,\n            this.props.fieldInfo\n        );\n        const classes = this.props.className ? [this.props.className] : [];\n        if (invalid) {\n            classes.push(\"o_field_invalid\");\n        }\n        if (empty) {\n            classes.push(\"o_form_label_empty\");\n        }\n        if (readonly && !this.props.notMuttedLabel) {\n            classes.push(\"o_form_label_readonly\");\n        }\n        return classes.join(\" \");\n    }\n\n    get hasTooltip() {\n        return Boolean(odoo.debug) || this.tooltipHelp;\n    }\n\n    get tooltipHelp() {\n        const field = this.props.record.fields[this.props.fieldName];\n        let help = this.props.fieldInfo.help || field.help || \"\";\n        if (field.company_dependent && user.allowedCompanies.length > 1) {\n            help += (help ? \"\\n\\n\" : \"\") + _t(\"Values set here are company-specific.\");\n        }\n        return help;\n    }\n    get tooltipInfo() {\n        if (!odoo.debug) {\n            return JSON.stringify({\n                field: {\n                    help: this.tooltipHelp,\n                },\n            });\n        }\n        return getTooltipInfo({\n            viewMode: \"form\",\n            resModel: this.props.record.resModel,\n            field: this.props.record.fields[this.props.fieldName],\n            fieldInfo: this.props.fieldInfo,\n            help: this.tooltipHelp,\n        });\n    }\n}\n", "import { evaluateBooleanExpr } from \"@web/core/py_js/py\";\nimport { Notebook } from \"@web/core/notebook/notebook\";\nimport { Setting } from \"./setting/setting\";\nimport { Field } from \"@web/views/fields/field\";\nimport { browser } from \"@web/core/browser/browser\";\nimport { hasTouch } from \"@web/core/browser/feature_detection\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { useDebounced, useThrottleForAnimation } from \"@web/core/utils/timing\";\nimport { ButtonBox } from \"@web/views/form/button_box/button_box\";\nimport { InnerGroup, OuterGroup } from \"@web/views/form/form_group/form_group\";\nimport { ViewButton } from \"@web/views/view_button/view_button\";\nimport { useViewCompiler } from \"@web/views/view_compiler\";\nimport { Widget } from \"@web/views/widgets/widget\";\nimport { FormCompiler } from \"./form_compiler\";\nimport { FormLabel } from \"./form_label\";\nimport { StatusBarButtons } from \"./status_bar_buttons/status_bar_buttons\";\n\nimport {\n    Component,\n    onMounted,\n    onWillUnmount,\n    useEffect,\n    useSubEnv,\n    useRef,\n    useState,\n    xml,\n} from \"@odoo/owl\";\n\nexport class FormRenderer extends Component {\n    static template = xml`<t t-call=\"{{ templates.FormRenderer }}\" t-call-context=\"{ __comp__: Object.assign(Object.create(this), { this: this }) }\" />`;\n    static components = {\n        Field,\n        FormLabel,\n        ButtonBox,\n        ViewButton,\n        Widget,\n        Notebook,\n        Setting,\n        OuterGroup,\n        InnerGroup,\n        StatusBarButtons,\n    };\n    static props = {\n        archInfo: Object,\n        Compiler: { type: Function, optional: true },\n        record: Object,\n        // Template props : added by the FormCompiler\n        class: { type: String, optional: 1 },\n        translateAlert: { type: [Object, { value: null }], optional: true },\n        onNotebookPageChange: { type: Function, optional: true },\n        activeNotebookPages: { type: Object, optional: true },\n        readonly: { type: Boolean, optional: true },\n        saveRecord: { type: Function, optional: true },\n        setFieldAsDirty: { type: Function, optional: true },\n        slots: { type: Object, optional: true },\n    };\n    static defaultProps = {\n        activeNotebookPages: {},\n        onNotebookPageChange: () => {},\n    };\n\n    setup() {\n        this.evaluateBooleanExpr = evaluateBooleanExpr;\n        const { archInfo, Compiler, record } = this.props;\n        const templates = { FormRenderer: archInfo.xmlDoc };\n        this.state = useState({}); // Used by Form Compiler\n        this.templates = useViewCompiler(Compiler || FormCompiler, templates);\n        useSubEnv({ model: record.model });\n        this.uiService = useService(\"ui\");\n        this.onResize = useDebounced(this.render, 200);\n        this.onScrollThrottled = useThrottleForAnimation(this.onScroll);\n        onMounted(() => browser.addEventListener(\"resize\", this.onResize));\n        onWillUnmount(() => browser.removeEventListener(\"resize\", this.onResize));\n\n        const { autofocusFieldIds } = archInfo;\n        const rootRef = useRef(\"compiled_view_root\");\n        if (this.shouldAutoFocus) {\n            useEffect(\n                (isNew, rootEl) => {\n                    if (!rootEl) {\n                        return;\n                    }\n                    let elementToFocus;\n                    if (isNew) {\n                        const focusableSelectors = [\n                            'input[type=\"text\"]',\n                            \"textarea\",\n                            \"[contenteditable]\",\n                        ];\n                        for (const id of autofocusFieldIds) {\n                            elementToFocus = rootEl.querySelector(`#${id}`);\n                            if (elementToFocus) {\n                                break;\n                            }\n                        }\n                        elementToFocus =\n                            elementToFocus ||\n                            rootEl.querySelector(\n                                focusableSelectors\n                                    .map((sel) => `.o_content .o_field_widget ${sel}`)\n                                    .join(\", \")\n                            );\n                    }\n                    if (elementToFocus) {\n                        elementToFocus.focus();\n                    }\n                },\n                () => [this.props.record.isNew, rootRef.el]\n            );\n        }\n\n        if (this.env.inDialog) {\n            // try to ensure ids unicity by temporarily removing similar ids that could already\n            // exist in the DOM (e.g. in a form view displayed below this dialog which contains\n            // same field names as this form view)\n            const fieldNodeIds = Object.keys(this.props.archInfo.fieldNodes);\n            const elementsByNodeIds = {};\n            onMounted(() => {\n                if (!rootRef.el) {\n                    // t-ref is sometimes set on a <t> node, resulting in a null ref (e.g. footer case)\n                    return;\n                }\n                for (const id of fieldNodeIds) {\n                    const els = [...document.querySelectorAll(`[id=${id}]`)].filter(\n                        (el) => !rootRef.el.contains(el)\n                    );\n                    if (els.length) {\n                        els[0].removeAttribute(\"id\");\n                        elementsByNodeIds[id] = els[0];\n                    }\n                }\n            });\n            onWillUnmount(() => {\n                for (const [id, el] of Object.entries(elementsByNodeIds)) {\n                    el.setAttribute(\"id\", id);\n                }\n            });\n        }\n    }\n\n    get shouldAutoFocus() {\n        return !hasTouch() && !this.props.archInfo.disableAutofocus;\n    }\n\n    onScroll(ev) {\n        this.state.isStatusbarStickyPinned =\n            !this.env.inDialog && !this.env.isSmall && ev.target.scrollTop !== 0;\n    }\n}\n", "import { Component, useEffect, useRef, useState } from \"@odoo/owl\";\nimport { useBus } from \"@web/core/utils/hooks\";\n\nexport class FormStatusIndicator extends Component {\n    static template = \"web.FormStatusIndicator\";\n    static props = {\n        model: Object,\n        save: Function,\n        discard: Function,\n    };\n\n    setup() {\n        this.state = useState({\n            fieldIsDirty: false,\n        });\n        useBus(\n            this.props.model.bus,\n            \"FIELD_IS_DIRTY\",\n            (ev) => (this.state.fieldIsDirty = ev.detail)\n        );\n        useEffect(\n            () => {\n                if (!this.props.model.root.isNew && this.indicatorMode === \"invalid\") {\n                    this.saveButton.el.setAttribute(\"disabled\", \"1\");\n                } else {\n                    this.saveButton.el.removeAttribute(\"disabled\");\n                }\n            },\n            () => [this.props.model.root.isValid, this.state.fieldIsDirty]\n        );\n\n        this.saveButton = useRef(\"save\");\n    }\n\n    get displayButtons() {\n        return this.indicatorMode !== \"saved\";\n    }\n\n    get indicatorMode() {\n        const { isNew, isValid } = this.props.model.root;\n        const isDirty = this.props.model.root.dirty || this.state.fieldIsDirty;\n        if (isNew || isDirty) {\n            return isValid ? \"dirty\" : \"invalid\";\n        }\n        return \"saved\";\n    }\n\n    async discard() {\n        await this.props.discard();\n    }\n    async save() {\n        await this.props.save();\n    }\n}\n", "import { registry } from \"@web/core/registry\";\nimport { RelationalModel } from \"@web/model/relational_model/relational_model\";\nimport { FormRenderer } from \"./form_renderer\";\nimport { FormArchParser } from \"./form_arch_parser\";\nimport { FormController } from \"./form_controller\";\nimport { FormCompiler } from \"./form_compiler\";\n\nexport const formView = {\n    type: \"form\",\n    searchMenuTypes: [],\n    Controller: FormController,\n    Renderer: FormRenderer,\n    ArchParser: FormArchParser,\n    Model: RelationalModel,\n    Compiler: FormCompiler,\n    buttonTemplate: \"web.FormView.Buttons\",\n\n    props: (genericProps, view) => {\n        const { ArchParser } = view;\n        const { arch, relatedModels, resModel } = genericProps;\n        const archInfo = new ArchParser().parse(arch, relatedModels, resModel);\n\n        return {\n            ...genericProps,\n            readonly:\n                genericProps.readonly ||\n                (archInfo.activeActions?.edit === false && genericProps.resId !== false),\n            Model: view.Model,\n            Renderer: view.Renderer,\n            buttonTemplate: genericProps.buttonTemplate || view.buttonTemplate,\n            Compiler: view.Compiler,\n            archInfo,\n        };\n    },\n};\n\nregistry.category(\"views\").add(\"form\", formView);\n", "import { Component } from \"@odoo/owl\";\nimport { FormLabel } from \"../form_label\";\nimport { DocumentationLink } from \"@web/views/widgets/documentation_link/documentation_link\";\nimport { user } from \"@web/core/user\";\n\nexport class Setting extends Component {\n    static template = \"web.Setting\";\n    static components = {\n        FormLabel,\n        DocumentationLink,\n    };\n    static props = {\n        id: { type: String, optional: 1 },\n        info: { type: String, optional: 1 },\n        title: { type: String, optional: 1 },\n        fieldId: { type: String, optional: 1 },\n        help: { type: String, optional: 1 },\n        fieldName: { type: String, optional: 1 },\n        fieldInfo: { type: Object, optional: 1 },\n        class: { type: String, optional: 1 },\n        record: { type: Object, optional: 1 },\n        documentation: { type: String, optional: 1 },\n        string: { type: String, optional: 1 },\n        addLabel: { type: Boolean },\n        companyDependent: { type: Boolean, optional: 1 },\n        slots: { type: Object, optional: 1 },\n    };\n\n    setup() {\n        if (this.props.fieldName) {\n            this.fieldType = this.props.record.fields[this.props.fieldName].type;\n            if (this.props.fieldInfo.readonly === \"True\") {\n                this.notMuttedLabel = true;\n            }\n        }\n    }\n\n    get classNames() {\n        const { class: _class } = this.props;\n        const classNames = {\n            o_setting_box: true,\n            \"col-12\": true,\n            \"col-lg-6\": true,\n            [_class]: Boolean(_class),\n        };\n\n        return classNames;\n    }\n\n    get displayCompanyDependentIcon() {\n        return this.labelString && this.props.companyDependent && user.allowedCompanies.length > 1;\n    }\n\n    get labelString() {\n        if (this.props.string) {\n            return this.props.string;\n        }\n        const label =\n            this.props.record &&\n            this.props.record.fields[this.props.fieldName] &&\n            this.props.record.fields[this.props.fieldName].string;\n        return label || \"\";\n    }\n}\n", "import { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\n\nimport { Component } from \"@odoo/owl\";\n\nexport class StatusBarButtons extends Component {\n    static template = \"web.StatusBarButtons\";\n    static components = {\n        Dropdown,\n        DropdownItem,\n    };\n    static props = {\n        slots: { type: Object, optional: 1 },\n    };\n\n    get visibleSlotNames() {\n        if (!this.props.slots) {\n            return [];\n        }\n        return Object.entries(this.props.slots)\n            .filter((entry) => entry[1].isVisible)\n            .map((entry) => entry[0]);\n    }\n}\n", "import { exprToBoolean } from \"@web/core/utils/strings\";\nimport { extractAttributes, visitXML } from \"@web/core/utils/xml\";\nimport { stringToOrderBy } from \"@web/search/utils/order_by\";\nimport { Field } from \"@web/views/fields/field\";\nimport { getActiveActions, processButton } from \"@web/views/utils\";\nimport { Widget } from \"@web/views/widgets/widget\";\n\nexport const KANBAN_CARD_ATTRIBUTE = \"card\";\nexport const KANBAN_MENU_ATTRIBUTE = \"menu\";\n\nexport class KanbanArchParser {\n    parse(xmlDoc, models, modelName) {\n        const fields = models[modelName].fields;\n        const className = xmlDoc.getAttribute(\"class\") || null;\n        const canOpenRecords = exprToBoolean(xmlDoc.getAttribute(\"can_open\"), true);\n        let defaultOrder = stringToOrderBy(xmlDoc.getAttribute(\"default_order\") || null);\n        const limit = xmlDoc.getAttribute(\"limit\");\n        const countLimit = xmlDoc.getAttribute(\"count_limit\");\n        const recordsDraggable = exprToBoolean(xmlDoc.getAttribute(\"records_draggable\"), true);\n        const groupsDraggable = exprToBoolean(xmlDoc.getAttribute(\"groups_draggable\"), true);\n        const activeActions = getActiveActions(xmlDoc);\n        activeActions.archiveGroup = exprToBoolean(xmlDoc.getAttribute(\"archivable\"), true);\n        activeActions.createGroup = exprToBoolean(xmlDoc.getAttribute(\"group_create\"), true);\n        activeActions.deleteGroup = exprToBoolean(xmlDoc.getAttribute(\"group_delete\"), true);\n        activeActions.editGroup = exprToBoolean(xmlDoc.getAttribute(\"group_edit\"), true);\n        activeActions.quickCreate =\n            activeActions.create && exprToBoolean(xmlDoc.getAttribute(\"quick_create\"), true);\n        const defaultGroupBy = xmlDoc.hasAttribute(\"default_group_by\")\n            ? xmlDoc.getAttribute(\"default_group_by\").split(\",\")\n            : null;\n        const onCreate = xmlDoc.getAttribute(\"on_create\");\n        const quickCreateView = xmlDoc.getAttribute(\"quick_create_view\");\n        const tooltipInfo = {};\n        let handleField = null;\n        const fieldNodes = {};\n        const fieldNextIds = {};\n        const widgetNodes = {};\n        let widgetNextId = 0;\n        const jsClass = xmlDoc.getAttribute(\"js_class\");\n        const action = xmlDoc.getAttribute(\"action\");\n        const type = xmlDoc.getAttribute(\"type\");\n        const openAction = action && type ? { action, type } : null;\n        const templateDocs = {};\n        let headerButtons = [];\n        const controls = [];\n        let button_id = 0;\n        // Root level of the template\n        visitXML(xmlDoc, (node) => {\n            if (node.hasAttribute(\"t-name\")) {\n                templateDocs[node.getAttribute(\"t-name\")] = node;\n                return;\n            }\n            if (node.tagName === \"header\") {\n                headerButtons = [...node.children]\n                    .filter((node) => node.tagName === \"button\")\n                    .map((node) => ({\n                        ...this.processButton(node),\n                        type: \"button\",\n                        id: button_id++,\n                    }));\n                return false;\n            } else if (node.tagName === \"control\") {\n                for (const childNode of node.children) {\n                    if (childNode.tagName === \"button\") {\n                        controls.push({\n                            type: \"button\",\n                            ...processButton(childNode),\n                        });\n                    } else if (childNode.tagName === \"create\") {\n                        controls.push({\n                            type: \"create\",\n                            context: childNode.getAttribute(\"context\"),\n                            string: childNode.getAttribute(\"string\"),\n                            invisible: childNode.getAttribute(\"invisible\"),\n                            class: childNode.getAttribute(\"class\"),\n                        });\n                    } else if (childNode.tagName === \"delete\") {\n                        controls.push({\n                            type: \"delete\",\n                            invisible: childNode.getAttribute(\"invisible\"),\n                        });\n                    }\n                }\n                return false;\n            }\n            // Case: field node\n            if (node.tagName === \"field\") {\n                // In kanban, we display many2many fields as tags by default\n                const widget = node.getAttribute(\"widget\");\n                if (\n                    !widget &&\n                    models[modelName].fields[node.getAttribute(\"name\")].type === \"many2many\"\n                ) {\n                    node.setAttribute(\"widget\", \"many2many_tags\");\n                }\n                const fieldInfo = Field.parseFieldNode(node, models, modelName, \"kanban\", jsClass);\n                const name = fieldInfo.name;\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                if (fieldInfo.options.group_by_tooltip) {\n                    tooltipInfo[name] = fieldInfo.options.group_by_tooltip;\n                }\n                if (fieldInfo.isHandle) {\n                    handleField = name;\n                }\n            }\n            if (node.tagName === \"widget\") {\n                const widgetInfo = Widget.parseWidgetNode(node);\n                const widgetId = `widget_${++widgetNextId}`;\n                widgetNodes[widgetId] = widgetInfo;\n                node.setAttribute(\"widget_id\", widgetId);\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                    /\\bkanban_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\n        // Progressbar\n        let progressAttributes = false;\n        const progressBar = xmlDoc.querySelector(\"progressbar\");\n        if (progressBar) {\n            progressAttributes = this.parseProgressBar(progressBar, fields);\n        }\n\n        // Concrete kanban box elements in the template\n        const cardDoc = templateDocs[KANBAN_CARD_ATTRIBUTE];\n        if (!cardDoc) {\n            throw new Error(`Missing '${KANBAN_CARD_ATTRIBUTE}' template.`);\n        }\n        const cardClassName = cardDoc.getAttribute(\"class\") || \"\";\n\n        if (!defaultOrder.length && handleField) {\n            const handleFieldSort = `${handleField}, id`;\n            defaultOrder = stringToOrderBy(handleFieldSort);\n        }\n\n        return {\n            activeActions,\n            canOpenRecords,\n            cardClassName,\n            cardColorField: xmlDoc.getAttribute(\"highlight_color\"),\n            className,\n            controls,\n            defaultGroupBy,\n            fieldNodes,\n            widgetNodes,\n            handleField,\n            headerButtons,\n            defaultOrder,\n            onCreate,\n            openAction,\n            quickCreateView,\n            recordsDraggable,\n            groupsDraggable,\n            limit: limit && parseInt(limit, 10),\n            countLimit: countLimit && parseInt(countLimit, 10),\n            progressAttributes,\n            templateDocs,\n            tooltipInfo,\n            examples: xmlDoc.getAttribute(\"examples\"),\n            xmlDoc,\n        };\n    }\n\n    parseProgressBar(progressBar, fields) {\n        const attrs = extractAttributes(progressBar, [\"field\", \"colors\", \"sum_field\", \"help\"]);\n        return {\n            fieldName: attrs.field,\n            colors: JSON.parse(attrs.colors),\n            sumField: fields[attrs.sum_field] || false,\n            help: attrs.help,\n        };\n    }\n\n    processButton(node) {\n        return processButton(node);\n    }\n}\n", "import { CogMenu } from \"../../search/cog_menu/cog_menu\";\n\nexport class KanbanCogMenu extends CogMenu {\n    static template = \"web.KanbanCogMenu\";\n    static props = {\n        ...CogMenu.props,\n        hasSelectedRecords: { type: Number, optional: true },\n    };\n    _registryItems() {\n        return this.props.hasSelectedRecords ? [] : super._registryItems();\n    }\n}\n", "import { Dialog } from \"@web/core/dialog/dialog\";\nimport { Notebook } from \"@web/core/notebook/notebook\";\n\nimport { Component, useRef } from \"@odoo/owl\";\n\nconst random = (min, max) => Math.floor(Math.random() * (max - min) + min);\n\nclass KanbanExamplesNotebookTemplate extends Component {\n    static template = \"web.KanbanExamplesNotebookTemplate\";\n    static props = [\"*\"];\n    static defaultProps = {\n        columns: [],\n        foldedColumns: [],\n    };\n    setup() {\n        this.columns = [];\n        const hasBullet = this.props.bullets && this.props.bullets.length;\n        const allColumns = [...this.props.columns, ...this.props.foldedColumns];\n        for (const title of allColumns) {\n            const col = { title, records: [] };\n            this.columns.push(col);\n            for (let i = 0; i < random(1, 5); i++) {\n                const rec = { id: i };\n                if (hasBullet && Math.random() > 0.3) {\n                    const sampleId = Math.floor(Math.random() * this.props.bullets.length);\n                    rec.bullet = this.props.bullets[sampleId];\n                }\n                col.records.push(rec);\n            }\n        }\n    }\n}\n\nexport class KanbanColumnExamplesDialog extends Component {\n    static template = \"web.KanbanColumnExamplesDialog\";\n    static components = { Dialog, Notebook };\n    static props = [\"*\"];\n\n    setup() {\n        this.navList = useRef(\"navList\");\n        this.pages = [];\n        this.activePage = null;\n        this.props.examples.forEach((eg) => {\n            this.pages.push({\n                Component: KanbanExamplesNotebookTemplate,\n                title: eg.name,\n                props: eg,\n                id: eg.name,\n            });\n        });\n    }\n\n    onPageUpdate(page) {\n        this.activePage = page;\n    }\n\n    applyExamples() {\n        const index = this.props.examples.findIndex((e) => e.name === this.activePage);\n        this.props.applyExamples(index);\n        this.props.close();\n    }\n}\n", "import { useHotkey } from \"@web/core/hotkeys/hotkey_hook\";\nimport { useAutofocus, useService } from \"@web/core/utils/hooks\";\n\nimport { Component, useExternalListener, useState, useRef, onPatched } from \"@odoo/owl\";\n\nexport class KanbanColumnQuickCreate extends Component {\n    static template = \"web.KanbanColumnQuickCreate\";\n    static props = {\n        onFoldChange: Function,\n        onValidate: Function,\n        folded: Boolean,\n        groupByField: Object,\n    };\n\n    setup() {\n        this.dialog = useService(\"dialog\");\n        this.root = useRef(\"root\");\n        this.state = useState({\n            hasInputFocused: false,\n        });\n\n        useAutofocus();\n        this.inputRef = useRef(\"autofocus\");\n\n        // Close on outside click\n        useExternalListener(window, \"mousedown\", (/** @type {MouseEvent} */ ev) => {\n            // This target is kept in order to impeach close on outside click behavior if the click\n            // has been initiated from the quickcreate root element (mouse selection in an input...)\n            this.mousedownTarget = ev.target;\n        });\n        useExternalListener(\n            window,\n            \"click\",\n            (/** @type {MouseEvent} */ ev) => {\n                const target = this.mousedownTarget || ev.target;\n                const gotClickedInside = this.root.el.contains(target);\n                if (!gotClickedInside) {\n                    this.fold();\n                }\n                this.mousedownTarget = null;\n            },\n            { capture: true }\n        );\n\n        // Key Navigation\n        useHotkey(\"escape\", () => this.fold());\n        onPatched(() => {\n            if (this.state.hasInputFocused && !this.props.folded) {\n                this.root.el.scrollIntoView({ behavior: \"smooth\" });\n            }\n        });\n    }\n\n    get relatedFieldName() {\n        return this.props.groupByField.string;\n    }\n\n    fold() {\n        this.props.onFoldChange(true);\n    }\n\n    unfold() {\n        this.props.onFoldChange(false);\n    }\n\n    validate() {\n        const title = this.inputRef.el.value.trim();\n        if (title.length) {\n            this.props.onValidate(title);\n            this.inputRef.el.value = \"\";\n            this.inputRef.el.focus();\n            this.state.hasInputFocused = true;\n        }\n    }\n\n    onInputKeydown(ev) {\n        if (ev.key === \"Enter\") {\n            this.validate();\n        }\n    }\n}\n", "import {\n    append,\n    combineAttributes,\n    createElement,\n    extractAttributes,\n    getTag,\n} from \"@web/core/utils/xml\";\nimport { toStringExpression } from \"@web/views/utils\";\nimport { toInterpolatedStringExpression, ViewCompiler } from \"@web/views/view_compiler\";\n\n/**\n * @typedef {Object} DropdownDef\n * @property {Element} el\n * @property {boolean} inserted\n * @property {boolean} shouldInsert\n * @property {(\"dropdown\" | \"toggler\" | \"menu\")[]} parts\n */\n\nconst ACTION_TYPES = [\"action\", \"object\"];\nconst SPECIAL_TYPES = [\n    ...ACTION_TYPES,\n    \"open\",\n    \"delete\",\n    \"url\",\n    \"set_cover\",\n    \"archive\",\n    \"unarchive\",\n];\n\nexport class KanbanCompiler extends ViewCompiler {\n    setup() {\n        this.compilers.push(\n            { selector: \"t[t-call]\", fn: this.compileTCall },\n            { selector: \"img\", fn: this.compileImage }\n        );\n    }\n\n    //-----------------------------------------------------------------------------\n    // Compilers\n    //-----------------------------------------------------------------------------\n\n    /**\n     * @override\n     */\n    compileButton(el, params) {\n        const type = el.getAttribute(\"type\");\n        if (!SPECIAL_TYPES.includes(type)) {\n            // Not a kanban-specific action type.\n            return super.compileButton(el, params);\n        }\n\n        combineAttributes(el, \"class\", [\"oe_kanban_action\"]);\n\n        if (ACTION_TYPES.includes(type)) {\n            if (!el.hasAttribute(\"debounce\")) {\n                // action buttons are debounced in kanban records\n                el.setAttribute(\"debounce\", 300);\n            }\n            return super.compileButton(el, params);\n        }\n\n        const nodeParams = extractAttributes(el, [\"type\"]);\n        if (type === \"set_cover\") {\n            const { \"auto-open\": autoOpen, \"data-field\": fieldName } = extractAttributes(el, [\n                \"auto-open\",\n                \"data-field\",\n            ]);\n            Object.assign(nodeParams, { autoOpen, fieldName });\n        }\n        const strParams = Object.entries(nodeParams)\n            .map(([k, v]) => [k, toStringExpression(v)].join(\":\"))\n            .join(\",\");\n        el.setAttribute(\"t-on-click\", `()=>__comp__.triggerAction({${strParams}})`);\n\n        const compiled = createElement(el.nodeName);\n        for (const { name, value } of el.attributes) {\n            compiled.setAttribute(name, value);\n        }\n        if (getTag(el, true) === \"a\" && !compiled.hasAttribute(\"href\")) {\n            compiled.setAttribute(\"href\", \"#\");\n        }\n        for (const child of el.childNodes) {\n            append(compiled, this.compileNode(child, params));\n        }\n\n        return compiled;\n    }\n    /**\n     * @returns {Element}\n     */\n    compileImage(el) {\n        const element = el.cloneNode(true);\n        element.setAttribute(\"loading\", \"lazy\");\n        return element;\n    }\n\n    /**\n     * @override\n     */\n    compileField(el, params) {\n        let compiled;\n        const recordExpr = params.recordExpr || \"__comp__.props.record\";\n        const dataPointIdExpr = params.dataPointIdExpr || `${recordExpr}.id`;\n        if (!el.hasAttribute(\"widget\")) {\n            // fields without a specified widget are rendered as simple spans in kanban records\n            const fieldId = el.getAttribute(\"field_id\");\n            compiled = createElement(\"span\", {\n                \"t-out\": params.formattedValueExpr || `__comp__.getFormattedValue(\"${fieldId}\")`,\n            });\n        } else {\n            compiled = super.compileField(el, params);\n            const fieldId = el.getAttribute(\"field_id\");\n            compiled.setAttribute(\"id\", `'${fieldId}_' + ${dataPointIdExpr}`);\n            // In x2many kanban, records can be edited in a dialog. The same record as the one of\n            // the kanban is used for the form view dialog, so its mode is switched to \"edit\", but\n            // we don't want to see it in edition in the background. For that reason, we force its\n            // fields to be readonly when the record is in edition, i.e. when it is opened in a form\n            // view dialog.\n            const readonlyAttr = compiled.getAttribute(\"readonly\");\n            if (readonlyAttr) {\n                compiled.setAttribute(\"readonly\", `${recordExpr}.isInEdition || (${readonlyAttr})`);\n            } else {\n                compiled.setAttribute(\"readonly\", `${recordExpr}.isInEdition`);\n            }\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    /**\n     * @param {Element} el\n     * @param {Object} params\n     * @returns {Element}\n     */\n    compileTCall(el, params) {\n        const compiled = this.compileGenericNode(el, params);\n        const tname = el.getAttribute(\"t-call\");\n        if (tname in this.templates) {\n            compiled.setAttribute(\"t-call\", `{{__comp__.templates[${toStringExpression(tname)}]}}`);\n        }\n        return compiled;\n    }\n}\nKanbanCompiler.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];\n", "import { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { user } from \"@web/core/user\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { omit } from \"@web/core/utils/objects\";\nimport { evaluateBooleanExpr } from \"@web/core/py_js/py\";\nimport { useSetupAction } from \"@web/search/action_hook\";\nimport { ActionMenus, STATIC_ACTIONS_GROUP_NUMBER } from \"@web/search/action_menus/action_menus\";\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 { session } from \"@web/session\";\nimport { useModelWithSampleData } from \"@web/model/model\";\nimport { standardViewProps } from \"@web/views/standard_view_props\";\nimport { MultiRecordViewButton } from \"@web/views/view_button/multi_record_view_button\";\nimport { useViewButtons } from \"@web/views/view_button/view_button_hook\";\nimport { useExportRecords, useDeleteRecords } from \"@web/views/view_hook\";\nimport { addFieldDependencies, extractFieldsFromArchInfo } from \"@web/model/relational_model/utils\";\nimport { KanbanCogMenu } from \"./kanban_cog_menu\";\nimport { KanbanRenderer } from \"./kanban_renderer\";\nimport { useProgressBar } from \"./progress_bar_hook\";\nimport { SelectionBox } from \"@web/views/view_components/selection_box\";\n\nimport {\n    Component,\n    onMounted,\n    onWillStart,\n    reactive,\n    useEffect,\n    useRef,\n    useState,\n    useSubEnv,\n} from \"@odoo/owl\";\n\nconst QUICK_CREATE_FIELD_TYPES = [\"char\", \"boolean\", \"many2one\", \"selection\", \"many2many\"];\n\n// -----------------------------------------------------------------------------\n\nexport class KanbanController extends Component {\n    static template = `web.KanbanView`;\n    static components = {\n        ActionMenus,\n        DropdownItem,\n        Layout,\n        KanbanRenderer,\n        MultiRecordViewButton,\n        SearchBar,\n        CogMenu: KanbanCogMenu,\n        SelectionBox,\n    };\n    static props = {\n        ...standardViewProps,\n        editable: { type: Boolean, optional: true },\n        forceGlobalClick: { type: Boolean, optional: true },\n        onSelectionChanged: { type: Function, optional: true },\n        readonly: { type: Boolean, optional: true },\n        showButtons: { type: Boolean, optional: true },\n        Compiler: Function,\n        Model: Function,\n        Renderer: Function,\n        buttonTemplate: String,\n        archInfo: Object,\n    };\n\n    static defaultProps = {\n        createRecord: () => {},\n        forceGlobalClick: false,\n        selectRecord: () => {},\n        showButtons: true,\n    };\n\n    setup() {\n        this.actionService = useService(\"action\");\n        this.dialog = useService(\"dialog\");\n        const { Model, archInfo } = this.props;\n\n        class KanbanSampleModel extends Model {\n            /**\n             * @override\n             */\n            hasData() {\n                if (this.root.groups && !this.root.groups.length) {\n                    // While we don't have any data, we want to display the column quick create and\n                    // example background. Return true so that we don't get sample data instead\n                    return true;\n                }\n                return super.hasData();\n            }\n\n            removeSampleDataInGroups() {\n                if (this.useSampleModel) {\n                    for (const group of this.root.groups) {\n                        const list = group.list;\n                        group.count = 0;\n                        list.count = 0;\n                        if (list.records) {\n                            list.records = [];\n                        } else {\n                            list.groups = [];\n                        }\n                    }\n                }\n            }\n        }\n\n        this.model = useState(\n            useModelWithSampleData(KanbanSampleModel, this.modelParams, this.modelOptions)\n        );\n        if (archInfo.progressAttributes) {\n            const { activeBars } = this.props.state || {};\n            this.progressBarState = useProgressBar(\n                archInfo.progressAttributes,\n                this.model,\n                this.progressBarAggregateFields,\n                activeBars\n            );\n        }\n        this.headerButtons = archInfo.headerButtons;\n\n        const self = this;\n        this.quickCreateState = reactive({\n            get groupId() {\n                return this._groupId || false;\n            },\n            set groupId(groupId) {\n                if (self.model.useSampleModel) {\n                    self.model.removeSampleDataInGroups();\n                    self.model.useSampleModel = false;\n                }\n                this._groupId = groupId;\n            },\n            view: archInfo.quickCreateView,\n        });\n\n        this.rootRef = useRef(\"root\");\n        useViewButtons(this.rootRef, {\n            beforeExecuteAction: this.beforeExecuteActionButton.bind(this),\n            afterExecuteAction: this.afterExecuteActionButton.bind(this),\n            reload: () => this.model.load(),\n        });\n        const { setScrollFromState } = useSetupAction({\n            rootRef: this.rootRef,\n            beforeUnload: this.beforeUnload.bind(this),\n            beforeLeave: this.beforeLeave.bind(this),\n            getLocalState: () => {\n                const state = {\n                    activeBars: this.progressBarState?.activeBars,\n                    modelState: this.model.exportState(),\n                };\n                if (this.env.isSmall && this.model.root.isGrouped) {\n                    const columnScrollTops = [];\n                    const sel = \".o_kanban_group:not(.o_column_folded)\";\n                    const columnEls = this.rootRef.el.querySelectorAll(sel);\n                    const groups = this.model.root.groups;\n                    for (const columnEl of columnEls) {\n                        const scrollTop = columnEl.scrollTop;\n                        if (scrollTop > 0) {\n                            const group = groups.find((g) => g.id === columnEl.dataset.id);\n                            columnScrollTops.push([group.serverValue, columnEl.scrollTop]);\n                        }\n                    }\n                    state.scrollPositions = {\n                        scrollLeft: this.rootRef.el.querySelector(\".o_renderer\")?.scrollLeft || 0,\n                        columnScrollTops,\n                    };\n                }\n                return state;\n            },\n        });\n        useEffect(\n            (isReady) => {\n                if (isReady) {\n                    if (this.env.isSmall && this.model.root.isGrouped) {\n                        const { scrollPositions } = this.props.state || {};\n                        if (scrollPositions) {\n                            const { scrollLeft, columnScrollTops } = scrollPositions;\n                            this.rootRef.el.querySelector(\".o_renderer\").scrollLeft = scrollLeft;\n                            const groups = this.model.root.groups;\n                            for (const [serverValue, scrollTop] of columnScrollTops) {\n                                const group = groups.find((g) => g.serverValue === serverValue);\n                                if (group) {\n                                    const sel = `.o_kanban_group[data-id=${group.id}]`;\n                                    this.rootRef.el.querySelector(sel).scrollTop = scrollTop;\n                                }\n                            }\n                        }\n                    } else {\n                        setScrollFromState();\n                    }\n                }\n            },\n            () => [this.model.isReady]\n        );\n        usePager(() => {\n            const root = this.model.root;\n            const { count, hasLimitedCount, isGrouped, limit, offset } = root;\n            if (!isGrouped && !this.model.useSampleModel) {\n                return {\n                    offset: offset,\n                    limit: limit,\n                    total: count,\n                    onUpdate: async ({ offset, limit }, hasNavigated) => {\n                        await this.model.root.load({ offset, limit });\n                        await this.onUpdatedPager();\n                        if (hasNavigated) {\n                            this.onPageChangeScroll();\n                        }\n                    },\n                    updateTotal: hasLimitedCount ? () => root.fetchCount() : undefined,\n                };\n            }\n        });\n        this.searchBarToggler = useSearchBarToggler();\n        this.firstLoad = true;\n        onMounted(() => {\n            this.firstLoad = false;\n        });\n        useEffect(\n            () => {\n                this.onSelectionChanged();\n            },\n            () => [this.model.root.selection?.length, this.model.root.isDomainSelected]\n        );\n        onWillStart(async () => {\n            this.isExportEnable = await user.hasGroup(\"base.group_allow_export\");\n        });\n        this.archiveEnabled =\n            \"active\" in this.props.fields\n                ? !this.props.fields.active.readonly\n                : \"x_active\" in this.props.fields\n                ? !this.props.fields.x_active.readonly\n                : false;\n        useSubEnv({ model: this.model });\n        this.exportRecords = useExportRecords(this.env, this.props.context, () =>\n            this.getExportableFields()\n        );\n        this.deleteRecordsWithConfirmation = useDeleteRecords(this.model);\n    }\n\n    get display() {\n        const { controlPanel } = this.props.display;\n        if (!controlPanel) {\n            return this.props.display;\n        }\n        return {\n            ...this.props.display,\n            controlPanel: {\n                ...controlPanel,\n                layoutActions: !this.hasSelectedRecords,\n            },\n        };\n    }\n\n    get actionMenuItems() {\n        const { actionMenus } = this.props.info;\n        const staticActionItems = Object.entries(this.getStaticActionMenuItems())\n            .filter(([key, item]) => item.isAvailable === undefined || item.isAvailable())\n            .sort(([k1, item1], [k2, item2]) => (item1.sequence || 0) - (item2.sequence || 0))\n            .map(([key, item]) =>\n                Object.assign(\n                    { key, groupNumber: STATIC_ACTIONS_GROUP_NUMBER },\n                    omit(item, \"isAvailable\")\n                )\n            );\n\n        return {\n            action: [...staticActionItems, ...(actionMenus?.action || [])],\n            print: actionMenus?.print,\n        };\n    }\n\n    get actionMenuProps() {\n        return {\n            getActiveIds: () => this.model.root.selection.map((r) => r.resId),\n            context: this.model.root.context,\n            domain: this.props.domain,\n            items: this.actionMenuItems,\n            isDomainSelected: this.model.root.isDomainSelected,\n            resModel: this.model.root.resModel,\n            onActionExecuted: ({ noReload } = {}) => {\n                if (!noReload) {\n                    return this.model.load();\n                }\n            },\n        };\n    }\n\n    get hasSelectedRecords() {\n        return this.model.root.selection?.length || this.isDomainSelected;\n    }\n\n    get isDomainSelected() {\n        return this.model.root.isDomainSelected;\n    }\n\n    get modelParams() {\n        const { resModel, archInfo, limit } = this.props;\n        const { activeFields, fields } = extractFieldsFromArchInfo(archInfo, this.props.fields);\n\n        const cardColorField = archInfo.cardColorField;\n        if (cardColorField) {\n            addFieldDependencies(activeFields, fields, [{ name: cardColorField, type: \"integer\" }]);\n        }\n\n        addFieldDependencies(activeFields, fields, this.progressBarAggregateFields);\n        const modelConfig = this.props.state?.modelState?.config || {\n            resModel,\n            activeFields,\n            fields,\n            fieldsToAggregate: this.progressBarAggregateFields.map((field) => field.name),\n            openGroupsByDefault: true,\n        };\n\n        return {\n            config: modelConfig,\n            state: this.props.state?.modelState,\n            limit: archInfo.limit || limit || 40,\n            groupsLimit: Number.MAX_SAFE_INTEGER, // no limit\n            countLimit: archInfo.countLimit,\n            defaultOrderBy: archInfo.defaultOrder,\n            maxGroupByDepth: 1,\n            activeIdsLimit: session.active_ids_limit,\n            hooks: {\n                onRecordSaved: this.onRecordSaved.bind(this),\n            },\n        };\n    }\n\n    get modelOptions() {\n        return {\n            lazy:\n                !this.env.config.isReloadingController &&\n                !this.env.inDialog &&\n                !!this.props.display.controlPanel,\n        };\n    }\n\n    get progressBarAggregateFields() {\n        const res = [];\n        const { progressAttributes } = this.props.archInfo;\n        if (progressAttributes && progressAttributes.sumField) {\n            res.push(progressAttributes.sumField);\n        }\n        return res;\n    }\n\n    get className() {\n        if (this.env.isSmall && this.model.root.isGrouped) {\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 archiveDialogProps() {\n        return {};\n    }\n\n    get deleteConfirmationDialogProps() {\n        return {};\n    }\n\n    getExportableFields() {\n        return Object.keys(this.model.root.config.activeFields)\n            .map((e) => this.props.fields[e])\n            .filter((field) => field.type !== \"properties\");\n    }\n\n    async onSelectionChanged() {\n        if (this.props.onSelectionChanged) {\n            const resIds = await this.model.root.getResIds(true);\n            this.props.onSelectionChanged(resIds);\n        }\n    }\n\n    getStaticActionMenuItems() {\n        return {\n            export: {\n                isAvailable: () => this.isExportEnable,\n                sequence: 10,\n                icon: \"fa fa-upload\",\n                description: _t(\"Export\"),\n                callback: () => this.exportRecords(),\n            },\n            duplicate: {\n                isAvailable: () => this.props.archInfo.activeActions.duplicate,\n                sequence: 30,\n                icon: \"fa fa-clone\",\n                description: _t(\"Duplicate\"),\n                callback: () => this.model.root.duplicateRecords(),\n            },\n            archive: {\n                isAvailable: () => this.archiveEnabled,\n                sequence: 40,\n                icon: \"oi oi-archive\",\n                description: _t(\"Archive\"),\n                callback: () =>\n                    this.model.root.toggleArchiveWithConfirmation(true, this.archiveDialogProps),\n            },\n            unarchive: {\n                isAvailable: () => this.archiveEnabled,\n                sequence: 45,\n                icon: \"oi oi-unarchive\",\n                description: _t(\"Unarchive\"),\n                callback: () => this.model.root.toggleArchiveWithConfirmation(false),\n            },\n            delete: {\n                isAvailable: () => this.props.archInfo.activeActions.delete,\n                sequence: 50,\n                icon: \"fa fa-trash-o\",\n                description: _t(\"Delete\"),\n                class: \"text-danger\",\n                callback: () =>\n                    this.deleteRecordsWithConfirmation(this.deleteConfirmationDialogProps),\n            },\n        };\n    }\n\n    async beforeUnload() {}\n\n    async beforeLeave() {\n        // wait for potential pending write operations (e.g. records being moved)\n        return this.model.mutex.getUnlockedDef();\n    }\n\n    evalViewModifier(modifier) {\n        return evaluateBooleanExpr(modifier, { context: this.props.context });\n    }\n\n    deleteRecord(record) {\n        this.deleteRecordsWithConfirmation(this.deleteConfirmationDialogProps, [record]);\n    }\n\n    async openRecord(record, { newWindow } = {}) {\n        const activeIds = this.model.root.records.map((datapoint) => datapoint.resId);\n        this.props.selectRecord(record.resId, { activeIds, newWindow });\n    }\n\n    async createRecord() {\n        const { onCreate } = this.props.archInfo;\n        const { root } = this.model;\n        if (this.canQuickCreate && onCreate === \"quick_create\") {\n            const firstGroup = root.groups.find((group) => !group.isFolded) || root.groups[0];\n            if (firstGroup.isFolded) {\n                await firstGroup.toggle();\n            }\n            this.quickCreateState.groupId = firstGroup.id;\n        } else if (onCreate && onCreate !== \"quick_create\") {\n            const options = {\n                additionalContext: root.context,\n                onClose: async ({ noReload } = {}) => {\n                    if (!noReload) {\n                        await root.load();\n                        this.model.useSampleModel = false;\n                        this.render(true); // FIXME WOWL reactivity\n                    }\n                },\n            };\n            await this.actionService.doAction(onCreate, options);\n        } else {\n            await this.props.createRecord();\n        }\n    }\n\n    get canCreate() {\n        return this.props.archInfo.activeActions.create;\n    }\n\n    get isNewButtonDisabled() {\n        const { createGroup } = this.props.archInfo.activeActions;\n        const list = this.model.root;\n        return (\n            this.model.isReady &&\n            list.isGrouped &&\n            list.groupByField.type === \"many2one\" &&\n            list.groups.length === 0 &&\n            createGroup\n        );\n    }\n\n    get canQuickCreate() {\n        const { activeActions } = this.props.archInfo;\n        if (!activeActions.quickCreate) {\n            return false;\n        }\n        if (!this.model.isReady) {\n            return false;\n        }\n\n        const list = this.model.root;\n        if (list.groups && !list.groups.length) {\n            return false;\n        }\n\n        return this.isQuickCreateField(list.groupByField);\n    }\n\n    onRecordSaved(record) {\n        if (this.model.root.isGrouped) {\n            const group = this.model.root.groups.find((l) =>\n                l.records.find((r) => r.id === record.id)\n            );\n            this.progressBarState?.updateCounts(group);\n        }\n    }\n\n    onPageChangeScroll() {\n        if (this.rootRef && this.rootRef.el) {\n            if (this.env.isSmall) {\n                this.rootRef.el.scrollTop = 0;\n            } else {\n                this.rootRef.el.querySelector(\".o_content\").scrollTop = 0;\n            }\n        }\n    }\n\n    async beforeExecuteActionButton(clickParams) {}\n\n    async afterExecuteActionButton(clickParams) {}\n\n    async onUpdatedPager() {}\n\n    scrollTop() {\n        this.rootRef.el.querySelector(\".o_content\").scrollTo({ top: 0 });\n    }\n\n    isQuickCreateField(field) {\n        return field && QUICK_CREATE_FIELD_TYPES.includes(field.type);\n    }\n}\n", "import { Dialog } from \"@web/core/dialog/dialog\";\nimport { FileInput } from \"@web/core/file_input/file_input\";\nimport { useService } from \"@web/core/utils/hooks\";\n\nimport { Component, useState, onWillStart } from \"@odoo/owl\";\n\nlet nextDialogId = 1;\n\nexport class KanbanCoverImageDialog extends Component {\n    static template = \"web.KanbanCoverImageDialog\";\n    static components = { Dialog, FileInput };\n    static props = [\"*\"];\n    setup() {\n        this.id = `o_cover_image_upload_${nextDialogId++}`;\n        this.orm = useService(\"orm\");\n        this.http = useService(\"http\");\n        const { record, fieldName } = this.props;\n        const attachment = record.data[fieldName];\n        this.state = useState({\n            selectFile: false,\n            selectedAttachmentId: attachment?.id || false,\n        });\n        onWillStart(async () => {\n            this.attachments = await this.orm.searchRead(\n                \"ir.attachment\",\n                [\n                    [\"res_model\", \"=\", record.resModel],\n                    [\"res_id\", \"=\", record.resId],\n                    [\"mimetype\", \"ilike\", \"image\"],\n                ],\n                [\"id\"]\n            );\n            this.state.selectFile = this.props.autoOpen && this.attachments.length;\n        });\n    }\n\n    get hasCoverImage() {\n        return Boolean(this.props.record.data[this.props.fieldName]);\n    }\n\n    onUpload([attachment]) {\n        if (!attachment) {\n            return;\n        }\n        this.state.selectFile = false;\n        this.selectAttachment(attachment, true);\n    }\n\n    selectAttachment(attachment, setSelected) {\n        if (this.state.selectedAttachmentId !== attachment.id) {\n            this.state.selectedAttachmentId = attachment.id;\n        } else {\n            this.state.selectedAttachmentId = null;\n        }\n        if (setSelected) {\n            this.setCover();\n        }\n    }\n\n    removeCover() {\n        this.state.selectedAttachmentId = null;\n        this.setCover();\n    }\n\n    async setCover() {\n        const value = this.state.selectedAttachmentId ? { id: this.state.selectedAttachmentId } : false;\n        await this.props.record.update({ [this.props.fieldName]: value }, { save: true });\n        this.props.close();\n    }\n\n    uploadImage() {\n        this.state.selectFile = true;\n    }\n}\n", "import { Component } from \"@odoo/owl\";\nimport { useDropdownCloser } from \"@web/core/dropdown/dropdown_hooks\";\n\nexport class KanbanDropdownMenuWrapper extends Component {\n    static template = \"web.KanbanDropdownMenuWrapper\";\n    static props = {\n        slots: Object,\n    };\n\n    setup() {\n        this.dropdownControl = useDropdownCloser();\n    }\n\n    onClick(ev) {\n        this.dropdownControl.closeAll();\n    }\n}\n", "import { Component, useRef } 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 { usePopover } from \"@web/core/popover/popover_hook\";\nimport { registry } from \"@web/core/registry\";\nimport { utils } from \"@web/core/ui/ui_service\";\nimport { memoize } from \"@web/core/utils/functions\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { useDebounced } from \"@web/core/utils/timing\";\nimport { ColumnProgress } from \"@web/views/view_components/column_progress\";\nimport { GroupConfigMenu } from \"@web/views/view_components/group_config_menu\";\n\nclass KanbanHeaderTooltip extends Component {\n    static template = \"web.KanbanGroupTooltip\";\n    static props = {\n        tooltip: Array,\n        close: Function,\n    };\n}\n\nexport class KanbanHeader extends Component {\n    static template = \"web.KanbanHeader\";\n    static components = { ColumnProgress, Dropdown, DropdownItem, GroupConfigMenu };\n    static props = {\n        activeActions: { type: Object },\n        canQuickCreate: { type: Boolean },\n        deleteGroup: { type: Function },\n        dialogClose: { type: Array },\n        group: { type: Object },\n        list: { type: Object },\n        quickCreateState: { type: Object },\n        scrollTop: { type: Function },\n        tooltipInfo: { type: Object },\n        progressBarState: { type: true, optional: true },\n    };\n\n    setup() {\n        this.dialog = useService(\"dialog\");\n        this.orm = useService(\"orm\");\n        this.rootRef = useRef(\"root\");\n        this.popover = usePopover(KanbanHeaderTooltip);\n        this.onTitleMouseEnter = useDebounced(this.onTitleMouseEnter, 400);\n    }\n\n    async onTitleMouseEnter(ev) {\n        if (!this.hasTooltip) {\n            return;\n        }\n        const tooltip = await this.loadTooltip();\n        if (tooltip.length) {\n            this.popover.open(ev.target, { tooltip });\n        }\n    }\n\n    onTitleMouseLeave() {\n        this.onTitleMouseEnter.cancel();\n        this.popover.close();\n    }\n\n    // ------------------------------------------------------------------------\n    // Getters\n    // ------------------------------------------------------------------------\n\n    get configMenuProps() {\n        return {\n            activeActions: this.props.activeActions,\n            configItems: [\n                [\n                    \"toggle_group\",\n                    {\n                        label: _t(\"Fold\"),\n                        method: () => this.group.toggle(),\n                        isVisible: () => !utils.isSmall(),\n                        class: () => ({\n                            o_kanban_toggle_fold: true,\n                            disabled: this.props.list.model.useSampleModel,\n                        }),\n                        icon: \"fa-compress\",\n                    },\n                ],\n                ...registry.category(\"group_config_items\").getEntries(),\n            ],\n            deleteGroup: this.props.deleteGroup,\n            dialogClose: this.props.dialogClose,\n            group: this.props.group,\n            list: this.props.list,\n        };\n    }\n\n    get progressBar() {\n        return this.props.progressBarState?.getGroupInfo(this.group);\n    }\n\n    get group() {\n        return this.props.group;\n    }\n\n    get groupAggregate() {\n        const { group, progressBarState } = this.props;\n        const { sumField } = progressBarState.progressAttributes;\n        return progressBarState.getAggregateValue(group, sumField);\n    }\n\n    // ------------------------------------------------------------------------\n    // Tooltip methods\n    // ------------------------------------------------------------------------\n\n    get hasTooltip() {\n        const { name, type } = this.group.groupByField;\n        return type === \"many2one\" && this.group.value && name in this.props.tooltipInfo;\n    }\n\n    loadTooltip = memoize(async () => {\n        const { name, relation: resModel } = this.group.groupByField;\n        const tooltipInfo = this.props.tooltipInfo[name];\n        const fieldNames = Object.keys(tooltipInfo);\n        const [values] = await this.orm.silent.read(\n            resModel,\n            [this.group.value],\n            [\"display_name\", ...fieldNames]\n        );\n\n        return fieldNames\n            .filter((fieldName) => values[fieldName])\n            .map((fieldName) => ({ title: tooltipInfo[fieldName], value: values[fieldName] }));\n    });\n\n    quickCreate(group) {\n        this.props.quickCreateState.groupId = this.group.id;\n    }\n\n    toggleGroup() {\n        return this.group.toggle();\n    }\n\n    canQuickCreate() {\n        return this.props.canQuickCreate;\n    }\n\n    async onBarClicked(value) {\n        await this.props.progressBarState.selectBar(this.props.group.id, value);\n        this.props.scrollTop();\n    }\n}\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { browser } from \"@web/core/browser/browser\";\nimport { ColorList } from \"@web/core/colorlist/colorlist\";\nimport { evaluateBooleanExpr } from \"@web/core/py_js/py\";\nimport { hasTouch } from \"@web/core/browser/feature_detection\";\nimport { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\nimport { registry } from \"@web/core/registry\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { imageUrl } from \"@web/core/utils/urls\";\nimport { useRecordObserver } from \"@web/model/relational_model/utils\";\nimport { Field } from \"@web/views/fields/field\";\nimport { fileTypeMagicWordMap } from \"@web/views/fields/image/image_field\";\nimport { ViewButton } from \"@web/views/view_button/view_button\";\nimport { useViewCompiler } from \"@web/views/view_compiler\";\nimport { Widget } from \"@web/views/widgets/widget\";\nimport { getFormattedValue } from \"../utils\";\nimport { KANBAN_CARD_ATTRIBUTE, KANBAN_MENU_ATTRIBUTE } from \"./kanban_arch_parser\";\nimport { KanbanCompiler } from \"./kanban_compiler\";\nimport { KanbanCoverImageDialog } from \"./kanban_cover_image_dialog\";\nimport { KanbanDropdownMenuWrapper } from \"./kanban_dropdown_menu_wrapper\";\n\nimport { Component, onWillUpdateProps, useRef, useState } from \"@odoo/owl\";\n\nconst { COLORS } = ColorList;\n\nconst formatters = registry.category(\"formatters\");\n\n// These classes determine whether a click on a record should open it.\nexport const CANCEL_GLOBAL_CLICK = [\"a\", \".dropdown\", \".oe_kanban_action\", \"[data-bs-toggle]\"].join(\n    \",\"\n);\n\n/**\n * Returns the index of a color determined by a given record.\n */\nexport function getColorIndex(value) {\n    if (typeof value === \"number\") {\n        return Math.round(value) % COLORS.length;\n    } else if (typeof value === \"string\") {\n        const charCodeSum = [...value].reduce((acc, _, i) => acc + value.charCodeAt(i), 0);\n        return charCodeSum % COLORS.length;\n    } else {\n        return 0;\n    }\n}\n\n/**\n * Returns a \"raw\" version of the field value on a given record.\n *\n * @param {Record} record\n * @param {string} fieldName\n * @returns {any}\n */\nexport function getRawValue(record, fieldName) {\n    const field = record.fields[fieldName];\n    const value = record.data[fieldName];\n    switch (field.type) {\n        case \"one2many\":\n        case \"many2many\": {\n            return value.count ? value.currentIds : [];\n        }\n        case \"many2one\": {\n            return (value && value.id) || false;\n        }\n        case \"date\":\n        case \"datetime\": {\n            return value && value.toISO();\n        }\n        default: {\n            return value;\n        }\n    }\n}\n\n/**\n * Returns a formatted version of the field value on a given record.\n *\n * @param {Record} record\n * @param {string} fieldName\n * @returns {string}\n */\nfunction getValue(record, fieldName) {\n    const field = record.fields[fieldName];\n    const value = record.data[fieldName];\n    const formatter = formatters.get(field.type, String);\n    return formatter(value, { field, data: record.data });\n}\n\nexport function getFormattedRecord(record) {\n    const formattedRecord = {\n        id: {\n            value: record.resId,\n            raw_value: record.resId,\n        },\n    };\n\n    for (const fieldName of record.fieldNames) {\n        formattedRecord[fieldName] = {\n            value: getValue(record, fieldName),\n            raw_value: getRawValue(record, fieldName),\n        };\n    }\n    return formattedRecord;\n}\n\n/**\n * Returns the image URL of a given field on the record.\n *\n * @param {Record} record\n * @param {string} [model] model name\n * @param {string} [field] field name\n * @param {number | [number, ...any[]]} [idOrIds] id or array\n *      starting with the id of the desired record.\n * @param {string} [placeholder] fallback when the image does not\n *  exist\n * @returns {string}\n */\nexport function getImageSrcFromRecordInfo(record, model, field, idOrIds, placeholder) {\n    const id = (Array.isArray(idOrIds) ? idOrIds[0] : idOrIds) || null;\n    const isCurrentRecord =\n        record.resModel === model && (record.resId === id || (!record.resId && !id));\n    const fieldVal = record.data[field];\n    if (isCurrentRecord && fieldVal && !isBinSize(fieldVal)) {\n        // Use magic-word technique for detecting image type\n        const type = fileTypeMagicWordMap[fieldVal[0]];\n        return `data:image/${type};base64,${fieldVal}`;\n    } else if (placeholder && (!model || !field || !id || !fieldVal)) {\n        // Placeholder if either the model, field, id or value is missing or null.\n        return placeholder;\n    } else {\n        // Else: fetches the image related to the given id.\n        const unique = isCurrentRecord && record.data.write_date;\n        return imageUrl(model, id, field, { unique });\n    }\n}\n\nfunction isBinSize(value) {\n    return /^\\d+(\\.\\d*)? [^0-9]+$/.test(value);\n}\n\nexport class KanbanRecord extends Component {\n    static components = {\n        Dropdown,\n        DropdownItem,\n        KanbanDropdownMenuWrapper,\n        Field,\n        KanbanCoverImageDialog,\n        ViewButton,\n        Widget,\n    };\n    static defaultProps = {\n        colors: COLORS,\n        deleteRecord: () => {},\n        getSelection: () => [],\n        archiveRecord: () => {},\n        openRecord: () => {},\n        selectionAvailable: false,\n        toggleSelection: () => {},\n    };\n    static props = [\n        \"archInfo\",\n        \"canResequence?\",\n        \"colors?\",\n        \"Compiler?\",\n        \"forceGlobalClick?\",\n        \"getSelection?\",\n        \"group?\",\n        \"groupByField?\",\n        \"deleteRecord?\",\n        \"archiveRecord?\",\n        \"openRecord?\",\n        \"readonly?\",\n        \"record\",\n        \"selectionAvailable?\",\n        \"progressBarState?\",\n        \"toggleSelection?\",\n    ];\n    static KANBAN_CARD_ATTRIBUTE = KANBAN_CARD_ATTRIBUTE;\n    static KANBAN_MENU_ATTRIBUTE = KANBAN_MENU_ATTRIBUTE;\n    static menuTemplate = \"web.KanbanRecordMenu\";\n    static template = \"web.KanbanRecord\";\n\n    setup() {\n        this.LONG_TOUCH_THRESHOLD = this.props.canResequence ? 600 : 400;\n        this.evaluateBooleanExpr = evaluateBooleanExpr;\n        this.action = useService(\"action\");\n        this.dialog = useService(\"dialog\");\n        this.notification = useService(\"notification\");\n\n        const { Compiler, archInfo } = this.props;\n        const ViewCompiler = Compiler || KanbanCompiler;\n        const { templateDocs: templates } = archInfo;\n\n        this.templates = useViewCompiler(ViewCompiler, templates);\n\n        this.showMenu = this.constructor.KANBAN_MENU_ATTRIBUTE in templates;\n\n        this.dataState = useState({ record: {}, widget: {} });\n        this.createWidget(this.props);\n        onWillUpdateProps(this.createWidget);\n        useRecordObserver((record) =>\n            Object.assign(this.dataState.record, getFormattedRecord(record))\n        );\n        this.rootRef = useRef(\"root\");\n        this.hasTouch = hasTouch();\n\n        this.longTouchTimer = null;\n        this.touchStartMs = 0;\n    }\n\n    get record() {\n        return this.dataState.record;\n    }\n\n    getFormattedValue(fieldId) {\n        const { archInfo, record } = this.props;\n        const { name } = archInfo.fieldNodes[fieldId];\n        return getFormattedValue(record, name, archInfo.fieldNodes[fieldId]);\n    }\n\n    /**\n     * Assigns \"widget\" properties on the kanban record.\n     *\n     * @param {Object} props\n     */\n    createWidget(props) {\n        const { archInfo, groupByField } = props;\n        const { activeActions } = archInfo;\n        // Widget\n        const deletable =\n            activeActions.delete &&\n            (!groupByField || groupByField.type !== \"many2many\") &&\n            !props.readonly;\n        const editable = activeActions.edit && !props.readonly;\n        this.dataState.widget = {\n            deletable,\n            editable,\n        };\n    }\n\n    getRecordClasses() {\n        const { archInfo, canResequence, forceGlobalClick, record, progressBarState } = this.props;\n        const classes = [\"o_kanban_record d-flex\"];\n        if (canResequence) {\n            classes.push(\"o_draggable\");\n        }\n        if (forceGlobalClick || archInfo.openAction || archInfo.canOpenRecords) {\n            classes.push(\"cursor-pointer\");\n        }\n        if (progressBarState) {\n            const { fieldName, colors } = progressBarState.progressAttributes;\n            const value = record.data[fieldName];\n            const color = colors[value];\n            if (color) {\n                classes.push(`oe_kanban_card_${color}`);\n            }\n        }\n        if (archInfo.cardColorField) {\n            const value = record.data[archInfo.cardColorField];\n            classes.push(`o_kanban_color_${getColorIndex(value)}`);\n        }\n        if (!this.props.groupByField) {\n            classes.push(\"flex-grow-1 flex-md-shrink-1 flex-shrink-0\");\n        }\n        if (this.props.selectionAvailable) {\n            classes.push(\"o_record_selection_available\");\n        }\n        if (this.props.record.selected) {\n            classes.push(\"o_record_selected\");\n        }\n        classes.push(archInfo.cardClassName);\n        return classes.join(\" \");\n    }\n\n    /**\n     * @param {MouseEvent} ev\n     */\n    onGlobalClick(ev, newWindow) {\n        if (ev.target.closest(CANCEL_GLOBAL_CLICK)) {\n            return;\n        }\n        if (this.props.getSelection().length > 0 || ev.altKey) {\n            ev.stopPropagation();\n            ev.preventDefault();\n            this.rootRef.el.focus();\n            this.props.toggleSelection(this.props.record, ev.shiftKey);\n            return;\n        }\n        const { archInfo, forceGlobalClick, openRecord, record } = this.props;\n        if (!forceGlobalClick && archInfo.openAction) {\n            this.action.doActionButton(\n                {\n                    name: archInfo.openAction.action,\n                    type: archInfo.openAction.type,\n                    resModel: record.resModel,\n                    resId: record.resId,\n                    resIds: record.resIds,\n                    context: record.context,\n                    onClose: async () => {\n                        await record.model.root.load();\n                    },\n                },\n                {\n                    newWindow,\n                }\n            );\n        } else if (forceGlobalClick || this.props.archInfo.canOpenRecords) {\n            openRecord(record, { newWindow });\n        }\n    }\n\n    resetLongTouchTimer() {\n        if (this.longTouchTimer) {\n            browser.clearTimeout(this.longTouchTimer);\n            this.longTouchTimer = null;\n        }\n    }\n\n    onTouchStart() {\n        this.touchStartMs = Date.now();\n        if (this.longTouchTimer === null) {\n            this.longTouchTimer = browser.setTimeout(() => {\n                this.props.record.toggleSelection(true);\n                this.resetLongTouchTimer();\n            }, this.LONG_TOUCH_THRESHOLD);\n        }\n    }\n    onTouchEnd() {\n        const elapsedTime = Date.now() - this.touchStartMs;\n        if (elapsedTime < this.LONG_TOUCH_THRESHOLD) {\n            this.resetLongTouchTimer();\n        }\n    }\n    onTouchMoveOrCancel() {\n        this.resetLongTouchTimer();\n    }\n\n    /**\n     * @param {Object} params\n     */\n    triggerAction(params) {\n        const { archInfo, openRecord, deleteRecord, record, archiveRecord } = this.props;\n        const { type } = params;\n        switch (type) {\n            case \"open\": {\n                return openRecord(record);\n            }\n            case \"archive\": {\n                return archiveRecord(record, true);\n            }\n            case \"unarchive\": {\n                return archiveRecord(record, false);\n            }\n            case \"delete\": {\n                return deleteRecord(record);\n            }\n            case \"set_cover\": {\n                const { autoOpen, fieldName } = params;\n                const widgets = Object.values(archInfo.fieldNodes)\n                    .filter((x) => x.name === fieldName)\n                    .map((x) => x.widget);\n                const field = record.fields[fieldName];\n                if (\n                    field.type === \"many2one\" &&\n                    field.relation === \"ir.attachment\" &&\n                    widgets.includes(\"attachment_image\")\n                ) {\n                    this.dialog.add(KanbanCoverImageDialog, { autoOpen, fieldName, record });\n                } else {\n                    const warning = _t(\n                        `Could not set the cover image: incorrect field (\"%s\") is provided in the view.`,\n                        fieldName\n                    );\n                    this.notification.add({ title: warning, type: \"danger\" });\n                }\n                break;\n            }\n            default: {\n                return this.notification.add(_t(\"Kanban: no action for type: %(type)s\", { type }), {\n                    type: \"danger\",\n                });\n            }\n        }\n    }\n\n    /**\n     * Returns the card template's rendering context.\n     *\n     * Note: the keys answer to outdated standards but should not be altered for\n     * the sake of compatibility.\n     *\n     * @returns {Object}\n     */\n    get renderingContext() {\n        const renderingContext = {\n            context: this.props.record.context,\n            JSON,\n            luxon,\n            record: this.dataState.record,\n            selection_mode: this.props.forceGlobalClick,\n            widget: this.dataState.widget,\n            __comp__: Object.assign(Object.create(this), { this: this }),\n        };\n        return renderingContext;\n    }\n}\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { parseXML } from \"@web/core/utils/xml\";\nimport { useHotkey } from \"@web/core/hotkeys/hotkey_hook\";\nimport { useOwnedDialogs, useService } from \"@web/core/utils/hooks\";\n\nimport {\n    Component,\n    onMounted,\n    onWillStart,\n    useExternalListener,\n    useRef,\n    useState,\n    useSubEnv,\n} from \"@odoo/owl\";\nimport { RPCError } from \"@web/core/network/rpc\";\nimport { extractFieldsFromArchInfo } from \"@web/model/relational_model/utils\";\nimport { formView } from \"../form/form_view\";\nimport { getDefaultConfig } from \"../view\";\nimport { FormViewDialog } from \"../view_dialogs/form_view_dialog\";\n\nconst DEFAULT_QUICK_CREATE_VIEW = {\n    // note: the required modifier is written in the format returned by the server\n    arch: /* xml */ `\n        <form>\n            <field name=\"display_name\" placeholder=\"Title\" required=\"True\" />\n        </form>`,\n};\nconst DEFAULT_QUICK_CREATE_FIELDS = {\n    display_name: { string: \"Display name\", type: \"char\" },\n};\n\nconst ACTION_SELECTORS = [\n    \".o_kanban_quick_add\",\n    \".o_kanban_load_more button\",\n    \".o-kanban-button-new\",\n];\n\nexport class KanbanQuickCreateController extends Component {\n    static props = {\n        Model: Function,\n        Renderer: Function,\n        Compiler: Function,\n        resModel: String,\n        onValidate: Function,\n        onCancel: Function,\n        fields: { type: Object },\n        context: { type: Object },\n        archInfo: { type: Object },\n    };\n    static template = \"web.KanbanQuickCreateController\";\n    setup() {\n        super.setup();\n\n        this.uiService = useService(\"ui\");\n        this.rootRef = useRef(\"root\");\n        this.state = useState({ disabled: false });\n        this.addDialog = useOwnedDialogs();\n\n        const { activeFields, fields } = extractFieldsFromArchInfo(\n            this.props.archInfo,\n            this.props.fields\n        );\n\n        const modelServices = Object.fromEntries(\n            this.props.Model.services.map((servName) => [servName, useService(servName)])\n        );\n        modelServices.orm = useService(\"orm\");\n        const config = {\n            resModel: this.props.resModel,\n            resId: false,\n            resIds: [],\n            fields,\n            activeFields,\n            isMonoRecord: true,\n            mode: \"edit\",\n            context: this.props.context,\n        };\n        this.model = useState(new this.props.Model(this.env, { config }, modelServices));\n\n        onWillStart(async () => {\n            await this.model.load();\n            this.model.whenReady.resolve();\n        });\n\n        onMounted(() => {\n            this.uiActiveElement = this.uiService.activeElement;\n        });\n        // Close on outside click\n        useExternalListener(window, \"mousedown\", (/** @type {MouseEvent} */ ev) => {\n            // This target is kept in order to impeach close on outside click behavior if the click\n            // has been initiated from the quickcreate root element (mouse selection in an input...)\n            this.mousedownTarget = ev.target;\n        });\n        useExternalListener(\n            window,\n            \"click\",\n            (/** @type {MouseEvent} */ ev) => {\n                if (this.uiActiveElement !== this.uiService.activeElement) {\n                    // this component isn't in the current active element -> do nothing\n                    return;\n                }\n                const target = this.mousedownTarget || ev.target;\n                // accounts for clicking on datetime picker and legacy autocomplete\n                const gotClickedInside =\n                    target.closest(\".o_datetime_picker\") ||\n                    target.closest(\".ui-autocomplete\") ||\n                    this.rootRef.el.contains(target);\n                if (!gotClickedInside) {\n                    let force = false;\n                    for (const selector of ACTION_SELECTORS) {\n                        const closestEl = target.closest(selector);\n                        if (closestEl) {\n                            force = true;\n                            break;\n                        }\n                    }\n                    this.cancel(force);\n                }\n                this.mousedownTarget = null;\n            },\n            { capture: true }\n        );\n\n        // Key Navigation\n        useHotkey(\"enter\", () => this.validate(\"add\"), { bypassEditableProtection: true });\n        useHotkey(\"escape\", () => this.cancel(true));\n    }\n\n    async validate(mode) {\n        let resId = undefined;\n        if (this.state.disabled) {\n            return;\n        }\n        this.state.disabled = true;\n\n        const keys = Object.keys(this.model.root.activeFields);\n        if (keys.length === 1 && keys[0] === \"display_name\") {\n            const isValid = await this.model.root.checkValidity(); // needed to put the class o_field_invalid in the field\n            if (isValid) {\n                try {\n                    [resId] = await this.model.orm.call(\n                        this.props.resModel,\n                        \"name_create\",\n                        [this.model.root.data.display_name],\n                        {\n                            context: this.props.context,\n                        }\n                    );\n                } catch (e) {\n                    this.showFormDialogInError(e);\n                }\n            } else {\n                this.model.notification.add(_t(\"Invalid Display Name\"), {\n                    type: \"danger\",\n                });\n            }\n        } else {\n            await this.model.root.save({\n                reload: false,\n                onError: (e) => this.showFormDialogInError(e),\n            });\n            resId = this.model.root.resId;\n        }\n\n        if (resId) {\n            this.props.onValidate(resId, mode);\n            if (mode === \"add\") {\n                await this.model.load({ resId: false });\n                this.state.disabled = false;\n            }\n        } else {\n            this.state.disabled = false;\n        }\n    }\n\n    async cancel(force) {\n        if (this.state.disabled) {\n            return;\n        }\n        if (force || !(await this.model.root.isDirty())) {\n            this.props.onCancel();\n        }\n    }\n\n    showFormDialogInError(e) {\n        // TODO: filter RPC errors more specifically (eg, for access denied, there is no point in opening a dialog)\n        if (!(e instanceof RPCError)) {\n            throw e;\n        }\n\n        const context = this.props.context;\n        const values = this.model.root.data;\n        context.default_name = values.name || values.display_name;\n        this.addDialog(FormViewDialog, {\n            resModel: this.props.resModel,\n            context,\n            title: _t(\"Create\"),\n            onRecordSaved: async (record) => {\n                await this.props.onValidate(record.resId, \"add\");\n                await this.model.load();\n            },\n        });\n    }\n\n    get className() {\n        return \"o_kanban_quick_create o_field_highlight shadow\";\n    }\n}\n\nexport class KanbanRecordQuickCreate extends Component {\n    static components = { KanbanQuickCreateController };\n    static template = \"web.KanbanRecordQuickCreate\";\n    static props = {\n        quickCreateView: { type: [String, { value: null }], optional: 1 },\n        onValidate: Function,\n        onCancel: Function,\n        group: Object,\n    };\n\n    setup() {\n        this.state = useState({\n            isLoaded: false,\n        });\n        this.viewService = useService(\"view\");\n        onMounted(() => {\n            this.getQuickCreateProps(this.props).then(() => {\n                this.state.isLoaded = true;\n            });\n        });\n        useSubEnv({\n            config: getDefaultConfig(),\n        });\n    }\n\n    async getQuickCreateProps(props) {\n        let quickCreateFields = { fields: DEFAULT_QUICK_CREATE_FIELDS };\n        let quickCreateForm = DEFAULT_QUICK_CREATE_VIEW;\n        let quickCreateRelatedModels = {};\n\n        if (props.quickCreateView) {\n            const { fields, relatedModels, views } = await this.viewService.loadViews({\n                context: { ...props.context, form_view_ref: props.quickCreateView },\n                resModel: props.group.resModel,\n                views: [[false, \"form\"]],\n            });\n            quickCreateFields = { fields: fields };\n            quickCreateForm = views.form;\n            quickCreateRelatedModels = relatedModels;\n        }\n        const models = {\n            ...quickCreateRelatedModels,\n            [props.group.resModel]: quickCreateFields,\n        };\n        const archInfo = new formView.ArchParser().parse(\n            parseXML(quickCreateForm.arch),\n            models,\n            props.group.resModel\n        );\n        this.quickCreateProps = {\n            Model: formView.Model,\n            Renderer: formView.Renderer,\n            Compiler: formView.Compiler,\n            resModel: props.group.resModel,\n            onValidate: props.onValidate,\n            onCancel: props.onCancel,\n            fields: quickCreateFields.fields,\n            context: props.group.context,\n            archInfo,\n        };\n    }\n}\n", "import { Component, onPatched, onWillDestroy, useEffect, useRef, useState } from \"@odoo/owl\";\nimport { ConfirmationDialog } from \"@web/core/confirmation_dialog/confirmation_dialog\";\nimport { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\nimport { useHotkey } from \"@web/core/hotkeys/hotkey_hook\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { evaluateExpr } from \"@web/core/py_js/py\";\nimport { registry } from \"@web/core/registry\";\nimport { useBus, useService } from \"@web/core/utils/hooks\";\nimport { useSortable } from \"@web/core/utils/sortable_owl\";\nimport { MOVABLE_RECORD_TYPES } from \"@web/model/relational_model/dynamic_group_list\";\nimport { isNull } from \"@web/views/utils\";\nimport { ColumnProgress } from \"@web/views/view_components/column_progress\";\nimport { useBounceButton } from \"@web/views/view_hook\";\nimport { KanbanColumnExamplesDialog } from \"./kanban_column_examples_dialog\";\nimport { KanbanColumnQuickCreate } from \"./kanban_column_quick_create\";\nimport { KanbanHeader } from \"./kanban_header\";\nimport { KanbanRecord } from \"./kanban_record\";\nimport { KanbanRecordQuickCreate } from \"./kanban_record_quick_create\";\nimport { Widget } from \"@web/views/widgets/widget\";\nimport { ActionHelper } from \"@web/views/action_helper\";\n\nconst DRAGGABLE_GROUP_TYPES = [\"many2one\"];\n\nfunction validateColumnQuickCreateExamples(data) {\n    const { allowedGroupBys = [], examples = [], foldField = \"\" } = data;\n    if (!allowedGroupBys.length) {\n        throw new Error(\"The example data must contain an array of allowed groupbys\");\n    }\n    if (!examples.length) {\n        throw new Error(\"The example data must contain an array of examples\");\n    }\n    const someHasFoldedColumns = examples.some(({ foldedColumns = [] }) => foldedColumns.length);\n    if (!foldField && someHasFoldedColumns) {\n        throw new Error(\"The example data must contain a fold field if there are folded columns\");\n    }\n}\n\nexport class KanbanRenderer extends Component {\n    static template = \"web.KanbanRenderer\";\n    static components = {\n        Dropdown,\n        DropdownItem,\n        ColumnProgress,\n        KanbanColumnQuickCreate,\n        KanbanHeader,\n        KanbanRecord,\n        KanbanRecordQuickCreate,\n        Widget,\n        ActionHelper,\n    };\n    static props = [\n        \"archInfo\",\n        \"Compiler\",\n        \"list\",\n        \"deleteRecord\",\n        \"openRecord\",\n        \"readonly?\",\n        \"forceGlobalClick?\",\n        \"noContentHelp?\",\n        \"scrollTop?\",\n        \"canQuickCreate?\",\n        \"quickCreateState?\",\n        \"progressBarState?\",\n        \"addLabel?\",\n        \"onAdd?\",\n    ];\n\n    static defaultProps = {\n        scrollTop: () => {},\n        quickCreateState: { groupId: false },\n        tooltipInfo: {},\n    };\n\n    setup() {\n        this.dialogClose = [];\n        /**\n         * @type {{ processedIds: string[], columnQuickCreateIsFolded: boolean }}\n         */\n        this.state = useState({\n            selectionAvailable: false,\n            processedIds: [],\n            columnQuickCreateIsFolded:\n                !this.props.list.isGrouped || this.props.list.groups.length > 0,\n        });\n        this.dialog = useService(\"dialog\");\n        this.exampleData = registry\n            .category(\"kanban_examples\")\n            .get(this.props.archInfo.examples, null);\n        if (this.exampleData) {\n            validateColumnQuickCreateExamples(this.exampleData);\n        }\n        this.lastCheckedRecord = null;\n\n        // Sortable\n        let dataRecordId;\n        let dataGroupId;\n        this.rootRef = useRef(\"root\");\n        if (this.canUseSortable) {\n            useSortable({\n                enable: () => this.canResequenceRecords,\n                // Params\n                ref: this.rootRef,\n                elements: \".o_draggable\",\n                ignore: \".dropdown,select\",\n                groups: () => this.props.list.isGrouped && \".o_kanban_group\",\n                connectGroups: () => this.canMoveRecords,\n                cursor: \"move\",\n                placeholderClasses: [\"visible\", \"opacity-50\", \"my-2\"],\n                // Hooks\n                onDragStart: (params) => {\n                    const { element, group } = params;\n                    dataRecordId = element.dataset.id;\n                    dataGroupId = group && group.dataset.id;\n                    if (this.props.list.selection?.length) {\n                        this.props.list.selection.forEach((record) => {\n                            record.toggleSelection(false);\n                        });\n                    }\n                    return this.sortStart(params);\n                },\n                onDragEnd: (params) => this.sortStop(params),\n                onGroupEnter: (params) => this.sortRecordGroupEnter(params),\n                onGroupLeave: (params) => this.sortRecordGroupLeave(params),\n                onDrop: (params) => this.sortRecordDrop(dataRecordId, dataGroupId, params),\n            });\n            useSortable({\n                enable: () => this.canResequenceGroups,\n                // Params\n                ref: this.rootRef,\n                elements: \".o_group_draggable\",\n                handle: \".o_column_title\",\n                cursor: \"move\",\n                // Hooks\n                onDragStart: (params) => {\n                    const { element } = params;\n                    dataGroupId = element.dataset.id;\n                    return this.sortStart(params);\n                },\n                onDragEnd: (params) => this.sortStop(params),\n                onDrop: (params) => this.sortGroupDrop(dataGroupId, params),\n            });\n        }\n\n        useBounceButton(this.rootRef, (clickedEl) => {\n            if (\n                this.props.list.isGrouped\n                    ? !this.props.list.recordCount\n                    : !this.props.list.count || this.props.list.model.useSampleModel\n            ) {\n                return clickedEl.matches(\n                    [\n                        \".o_kanban_renderer\",\n                        \".o_kanban_group\",\n                        \".o_kanban_header\",\n                        \".o_column_quick_create\",\n                        \".o_view_nocontent_smiling_face\",\n                    ].join(\", \")\n                );\n            }\n            return false;\n        });\n        onWillDestroy(() => {\n            this.dialogClose.forEach((close) => close());\n        });\n\n        if (this.env.searchModel) {\n            useBus(this.env.searchModel, \"focus-view\", () => {\n                const { model } = this.props.list;\n                if (model.useSampleModel || !model.hasData()) {\n                    return;\n                }\n                const firstCard = this.rootRef.el.querySelector(\".o_kanban_record\");\n                if (firstCard) {\n                    // Focus first kanban card\n                    firstCard.focus();\n                }\n            });\n        }\n\n        useHotkey(\n            \"Enter\",\n            ({ target }) => {\n                if (target.closest(\".o_kanban_selection_active\") !== null) {\n                    return;\n                }\n\n                if (!target.classList.contains(\"o_kanban_record\")) {\n                    return;\n                }\n\n                if (this.props.archInfo.canOpenRecords) {\n                    target.click();\n                    return;\n                }\n\n                // Open first link\n                const firstLink = target.querySelector(\"a, button\");\n                if (firstLink) {\n                    firstLink.click();\n                }\n            },\n            { area: () => this.rootRef.el }\n        );\n\n        useHotkey(\"space\", ({ target }) => this.onSpaceKeyPress(target), {\n            area: () => this.rootRef.el,\n        });\n\n        useHotkey(\"shift+space\", ({ target }) => this.onSpaceKeyPress(target, true), {\n            area: () => this.rootRef.el,\n        });\n\n        const arrowsOptions = { area: () => this.rootRef.el, allowRepeat: true };\n        if (this.env.searchModel) {\n            useHotkey(\n                \"ArrowUp\",\n                ({ area }) => {\n                    if (!this.focusNextCard(area, \"up\")) {\n                        this.env.searchModel.trigger(\"focus-search\");\n                    }\n                },\n                arrowsOptions\n            );\n        }\n        useHotkey(\"ArrowDown\", ({ area }) => this.focusNextCard(area, \"down\"), arrowsOptions);\n        useHotkey(\"ArrowLeft\", ({ area }) => this.focusNextCard(area, \"left\"), arrowsOptions);\n        useHotkey(\"ArrowRight\", ({ area }) => this.focusNextCard(area, \"right\"), arrowsOptions);\n        const handleAltKeyDown = (ev) => {\n            if (ev.key === \"Alt\") {\n                this.state.selectionAvailable = true;\n            }\n        };\n        const handleAltKeyUp = () => {\n            this.state.selectionAvailable = false;\n        };\n        useEffect(\n            () => {\n                window.addEventListener(\"keydown\", handleAltKeyDown);\n                window.addEventListener(\"keyup\", handleAltKeyUp);\n                window.addEventListener(\"blur\", handleAltKeyUp);\n                return () => {\n                    window.removeEventListener(\"keydown\", handleAltKeyDown);\n                    window.removeEventListener(\"keyup\", handleAltKeyUp);\n                    window.removeEventListener(\"blur\", handleAltKeyUp);\n                };\n            },\n            () => []\n        );\n\n        // After a group is unfolded through onGroupClick, we want to scroll towards\n        // the next group if it exists and is folded, and to the unfolded group\n        // itself otherwise\n        onPatched(() => {\n            if (this.lastOpenedGroupId) {\n                const groups = this.getGroupsOrRecords();\n                const lastOpenedGroupIndex = groups.findIndex(\n                    (g) => g.group.id === this.lastOpenedGroupId\n                );\n                let groupIdToFocus = this.lastOpenedGroupId;\n                if (\n                    lastOpenedGroupIndex < groups.length - 1 &&\n                    groups[lastOpenedGroupIndex + 1].group.isFolded\n                ) {\n                    groupIdToFocus = groups[lastOpenedGroupIndex + 1].group.id;\n                }\n                const groupEl = this.rootRef.el.querySelector(\n                    `.o_kanban_group[data-id=\"${groupIdToFocus}\"]`\n                );\n                const rect = groupEl.getBoundingClientRect();\n                // Don't scroll if the group to focus is completely inside of the viewport\n                if (rect.x + rect.width > window.innerWidth) {\n                    groupEl.scrollIntoView({ behavior: \"smooth\", inline: \"end\" });\n                }\n                delete this.lastOpenedGroupId;\n            }\n        });\n    }\n\n    // ------------------------------------------------------------------------\n    // Getters\n    // ------------------------------------------------------------------------\n\n    get canUseSortable() {\n        return !this.env.isSmall;\n    }\n\n    get canMoveRecords() {\n        if (!this.canResequenceRecords) {\n            return false;\n        }\n        const groupByField = this.props.list.groupByField;\n        if (!groupByField) {\n            return true;\n        }\n        const fieldNodes = Object.values(this.props.archInfo.fieldNodes).filter(\n            (fieldNode) => fieldNode.name === groupByField.name\n        );\n        let isReadonly = this.props.list.fields[groupByField.name].readonly;\n        if (!isReadonly && fieldNodes.length) {\n            isReadonly = fieldNodes.every((fieldNode) => {\n                if (!fieldNode.readonly) {\n                    return false;\n                }\n                try {\n                    return evaluateExpr(fieldNode.readonly, this.props.list.evalContext);\n                } catch {\n                    return false;\n                }\n            });\n        }\n        return !isReadonly && this.isMovableField(groupByField);\n    }\n\n    get canResequenceGroups() {\n        if (!this.props.list.isGrouped) {\n            return false;\n        }\n        const { type } = this.props.list.groupByField;\n        const { groupsDraggable } = this.props.archInfo;\n        return groupsDraggable && DRAGGABLE_GROUP_TYPES.includes(type);\n    }\n\n    get canResequenceRecords() {\n        const { isGrouped, orderBy } = this.props.list;\n        const { handleField, recordsDraggable } = this.props.archInfo;\n        return Boolean(\n            recordsDraggable &&\n                (isGrouped || (handleField && (!orderBy[0] || orderBy[0].name === handleField)))\n        );\n    }\n\n    get canShowExamples() {\n        const { allowedGroupBys = [], examples = [] } = this.exampleData || {};\n        const hasExamples = Boolean(examples.length);\n        return hasExamples && allowedGroupBys.includes(this.props.list.groupByField.name);\n    }\n\n    get showNoContentHelper() {\n        const { model, isGrouped, groupByField, groups } = this.props.list;\n        if (model.useSampleModel) {\n            return true;\n        }\n        if (isGrouped) {\n            if (this.props.quickCreateState.groupId) {\n                return false;\n            }\n            if (this.canCreateGroup() && !this.state.columnQuickCreateIsFolded) {\n                return false;\n            }\n            if (groups.length === 0) {\n                return groupByField.type !== \"many2one\";\n            }\n        }\n        return !model.hasData();\n    }\n\n    getSelection() {\n        return this.props.list.selection || [];\n    }\n\n    /**\n     * When the kanban records are grouped, the 'false' or 'undefined' group\n     * must appear first.\n     * @returns {any[]}\n     */\n    getGroupsOrRecords() {\n        const { list } = this.props;\n        if (list.isGrouped) {\n            return [...list.groups]\n                .sort((a, b) => (a.value && !b.value ? 1 : !a.value && b.value ? -1 : 0))\n                .map((group, i) => ({\n                    group,\n                    key: isNull(group.value) ? `group_key_${i}` : String(group.value),\n                }));\n        } else {\n            return list.records.map((record) => ({ record, key: record.id }));\n        }\n    }\n\n    /**\n     * @param {RelationalGroup} group\n     * @param {boolean} isGroupProcessing\n     * @returns {string}\n     */\n    getGroupClasses(group, isGroupProcessing) {\n        const classes = [];\n        if (!isGroupProcessing && this.canResequenceGroups && group.value) {\n            classes.push(\"o_group_draggable\");\n        }\n        if (!group.count) {\n            classes.push(\"o_kanban_no_records\");\n        }\n        if (!this.env.isSmall && group.isFolded) {\n            classes.push(\"o_column_folded\", \"flex-basis-0\");\n        }\n        if (this.props.progressBarState && !group.isFolded) {\n            const progressBarInfo = this.props.progressBarState.getGroupInfo(group);\n            if (progressBarInfo.activeBar) {\n                const progressBar = progressBarInfo.bars.find(\n                    (b) => b.value === progressBarInfo.activeBar\n                );\n                classes.push(\"o_kanban_group_show\", `o_kanban_group_show_${progressBar.color}`);\n            }\n        }\n        return classes.join(\" \");\n    }\n\n    getGroupUnloadedCount(group) {\n        const records = group.list.records.filter((r) => !r.isInQuickCreation);\n        const count = this.props.progressBarState?.getGroupCount(group) || group.count;\n        return count - records.length;\n    }\n\n    /**\n     * @param {string} id\n     * @returns {boolean}\n     */\n    isProcessing(id) {\n        return this.state.processedIds.includes(id);\n    }\n\n    isMovableField(field) {\n        return MOVABLE_RECORD_TYPES.includes(field.type);\n    }\n\n    // ------------------------------------------------------------------------\n    // Permissions\n    // ------------------------------------------------------------------------\n\n    canCreateGroup() {\n        const { activeActions, defaultGroupBy } = this.props.archInfo;\n        return (\n            activeActions.createGroup &&\n            this.props.list.groupByField.type === \"many2one\" &&\n            this.props.list.groupByField.name === defaultGroupBy?.[0]\n        );\n    }\n\n    canQuickCreate() {\n        return this.props.canQuickCreate;\n    }\n\n    // ------------------------------------------------------------------------\n    // Edition methods\n    // ------------------------------------------------------------------------\n\n    async archiveRecord(record, active) {\n        if (active) {\n            this.dialog.add(ConfirmationDialog, {\n                body: _t(\"Are you sure that you want to archive this record?\"),\n                confirmLabel: _t(\"Archive\"),\n                confirm: () => record.archive(),\n                cancel: () => {},\n            });\n        } else {\n            return record.unarchive();\n        }\n    }\n\n    async validateQuickCreate(recordId, mode, group) {\n        const record = await group.addExistingRecord(recordId, true);\n        if (mode === \"edit\") {\n            await this.props.openRecord(record);\n        } else {\n            this.props.progressBarState?.updateCounts(group);\n        }\n        this.props.quickCreateState.groupId = mode === \"add\" ? group.id : false;\n    }\n\n    cancelQuickCreate() {\n        this.props.quickCreateState.groupId = false;\n    }\n\n    async deleteGroup(group) {\n        await this.props.list.deleteGroups([group]);\n        if (this.props.list.groups.length === 0) {\n            this.state.columnQuickCreateIsFolded = false;\n        }\n    }\n\n    toggleGroup(group) {\n        return group.toggle();\n    }\n\n    loadMore(group) {\n        return group.list.load({ limit: group.list.records.length + group.model.initialLimit });\n    }\n\n    /**\n     * @param {string} id\n     * @param {boolean} isProcessing\n     */\n    toggleProcessing(id, isProcessing) {\n        if (isProcessing) {\n            this.state.processedIds = [...this.state.processedIds, id];\n        } else {\n            this.state.processedIds = this.state.processedIds.filter(\n                (processedId) => processedId !== id\n            );\n        }\n    }\n\n    toggleSelection(record, isRange = false) {\n        if (isRange) {\n            this.toggleRangeSelection(record);\n        } else {\n            record.toggleSelection();\n        }\n        this.lastCheckedRecord = record;\n    }\n\n    toggleRangeSelection(record) {\n        const { records } = this.props.list;\n        const recordIndex = records.findIndex((e) => e.id === record.id);\n        const lastCheckedRecordIndex = records.findIndex((e) => e.id === this.lastCheckedRecord.id);\n        const start = Math.min(recordIndex, lastCheckedRecordIndex);\n        const end = Math.max(recordIndex, lastCheckedRecordIndex);\n        for (let i = start; i <= end; i++) {\n            records[i].toggleSelection(!record.selected);\n        }\n    }\n\n    // ------------------------------------------------------------------------\n    // Handlers\n    // ------------------------------------------------------------------------\n\n    async onGroupClick(group, ev) {\n        if (!this.env.isSmall && group.isFolded) {\n            this.lastOpenedGroupId = group.id;\n            await group.toggle();\n            this.props.scrollTop();\n        }\n    }\n\n    /**\n     * @param {string} dataGroupId\n     * @param {Object} params\n     * @param {HTMLElement} params.element\n     * @param {HTMLElement} [params.group]\n     * @param {HTMLElement} [params.next]\n     * @param {HTMLElement} [params.parent]\n     * @param {HTMLElement} [params.previous]\n     */\n    async sortGroupDrop(dataGroupId, { previous }) {\n        this.toggleProcessing(dataGroupId, true);\n        const refId = previous ? previous.dataset.id : null;\n        try {\n            await this.props.list.resequence(dataGroupId, refId);\n        } finally {\n            this.toggleProcessing(dataGroupId, false);\n        }\n    }\n\n    onSpaceKeyPress(target, isRange) {\n        if (target.classList.contains(\"o_kanban_record\")) {\n            const record = this.props.list.records.find((e) => e.id === target.dataset.id);\n            this.toggleSelection(record, isRange);\n        }\n    }\n\n    showExamples() {\n        this.dialog.add(KanbanColumnExamplesDialog, {\n            examples: this.exampleData.examples,\n            applyExamplesText: this.exampleData.applyExamplesText || _t(\"Use This For My Kanban\"),\n            applyExamples: (index) => {\n                const { examples, foldField } = this.exampleData;\n                const { columns, foldedColumns = [] } = examples[index];\n                for (const groupName of columns) {\n                    this.props.list.createGroup(groupName);\n                }\n                for (const groupName of foldedColumns) {\n                    this.props.list.createGroup(groupName, foldField);\n                }\n            },\n        });\n    }\n\n    /**\n     * @param {string} dataRecordId\n     * @param {string} dataGroupId\n     * @param {Object} params\n     * @param {HTMLElement} params.element\n     * @param {HTMLElement} [params.group]\n     * @param {HTMLElement} [params.next]\n     * @param {HTMLElement} [params.parent]\n     * @param {HTMLElement} [params.previous]\n     */\n    async sortRecordDrop(dataRecordId, dataGroupId, { element, parent, previous }) {\n        if (\n            !this.props.list.isGrouped ||\n            parent.classList.contains(\"o_kanban_hover\") ||\n            parent.dataset.id === element.parentElement.dataset.id\n        ) {\n            if (!this.props.list.records.find((r) => r.id === dataRecordId)) {\n                // Race condition: a new rendering has been scheduled/is ongoing but hasn't been\n                // applied to the DOM yet, so the user dropped a record that is no longer referenced\n                // in the model. In that case, we can't to anything else than abort.\n                return;\n            }\n            this.toggleProcessing(dataRecordId, true);\n\n            parent?.classList.remove(\"o_kanban_hover\");\n            while (previous && !previous.dataset.id) {\n                previous = previous.previousElementSibling;\n            }\n            const refId = previous ? previous.dataset.id : null;\n            const targetGroupId = parent?.dataset.id;\n            try {\n                await this.props.list.moveRecord(dataRecordId, dataGroupId, refId, targetGroupId);\n            } finally {\n                this.toggleProcessing(dataRecordId, false);\n            }\n        }\n    }\n\n    /**\n     * @param {Object} params\n     * @param {HTMLElement} params.group\n     */\n    sortRecordGroupEnter({ group }) {\n        group.classList.add(\"o_kanban_hover\");\n    }\n\n    /**\n     * @param {Object} params\n     * @param {HTMLElement} params.group\n     */\n    sortRecordGroupLeave({ group }) {\n        group.classList.remove(\"o_kanban_hover\");\n    }\n\n    /**\n     * @param {Object} params\n     * @param {HTMLElement} params.element\n     * @param {HTMLElement} [params.group]\n     */\n    sortStart({ element }) {\n        element.classList.add(\"shadow\");\n    }\n\n    /**\n     * @param {Object} params\n     * @param {HTMLElement} params.element\n     * @param {HTMLElement} [params.group]\n     */\n    sortStop({ element, group }) {\n        element.classList.remove(\"shadow\");\n        if (group) {\n            group.classList.remove(\"o_kanban_hover\");\n        }\n    }\n\n    /**\n     * Focus next card in the area within the chosen direction.\n     *\n     * @param {HTMLElement} area\n     * @param {\"down\"|\"up\"|\"right\"|\"left\"} direction\n     * @returns {true?} true if the next card has been focused\n     */\n    focusNextCard(area, direction) {\n        const { isGrouped } = this.props.list;\n        const closestCard = document.activeElement.closest(\".o_kanban_record\");\n        if (!closestCard) {\n            return;\n        }\n        const groups = isGrouped ? [...area.querySelectorAll(\".o_kanban_group\")] : [area];\n        const cards = [...groups]\n            .map((group) => [...group.querySelectorAll(\".o_kanban_record\")])\n            .filter((group) => group.length);\n\n        let iGroup;\n        let iCard;\n        for (iGroup = 0; iGroup < cards.length; iGroup++) {\n            const i = cards[iGroup].indexOf(closestCard);\n            if (i !== -1) {\n                iCard = i;\n                break;\n            }\n        }\n        // Find next card to focus\n        let nextCard;\n        switch (direction) {\n            case \"down\":\n                nextCard = iCard < cards[iGroup].length - 1 && cards[iGroup][iCard + 1];\n                break;\n            case \"up\":\n                nextCard = iCard > 0 && cards[iGroup][iCard - 1];\n                break;\n            case \"right\":\n                if (isGrouped) {\n                    nextCard = iGroup < cards.length - 1 && cards[iGroup + 1][0];\n                } else {\n                    nextCard = iCard < cards[0].length - 1 && cards[0][iCard + 1];\n                }\n                break;\n            case \"left\":\n                if (isGrouped) {\n                    nextCard = iGroup > 0 && cards[iGroup - 1][0];\n                } else {\n                    nextCard = iCard > 0 && cards[0][iCard - 1];\n                }\n                break;\n        }\n\n        if (nextCard && nextCard instanceof HTMLElement) {\n            nextCard.focus();\n            return true;\n        }\n    }\n}\n", "import { registry } from \"@web/core/registry\";\nimport { RelationalModel } from \"@web/model/relational_model/relational_model\";\nimport { KanbanArchParser } from \"./kanban_arch_parser\";\nimport { KanbanCompiler } from \"./kanban_compiler\";\nimport { KanbanController } from \"./kanban_controller\";\nimport { KanbanRenderer } from \"./kanban_renderer\";\n\nexport const kanbanView = {\n    type: \"kanban\",\n\n    ArchParser: KanbanArchParser,\n    Controller: KanbanController,\n    Model: RelationalModel,\n    Renderer: KanbanRenderer,\n    Compiler: KanbanCompiler,\n\n    buttonTemplate: \"web.KanbanView.Buttons\",\n\n    props: (genericProps, view) => {\n        const { arch, relatedModels, resModel } = genericProps;\n        const { ArchParser } = view;\n        const archInfo = new ArchParser().parse(arch, relatedModels, resModel);\n        return {\n            ...genericProps,\n            readonly: genericProps.readonly || !archInfo.activeActions?.edit,\n            Compiler: view.Compiler,\n            Model: view.Model,\n            Renderer: view.Renderer,\n            buttonTemplate: view.buttonTemplate,\n            archInfo,\n        };\n    },\n};\n\nregistry.category(\"views\").add(\"kanban\", kanbanView);\n", "import { reactive } from \"@odoo/owl\";\nimport { Domain } from \"@web/core/domain\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport {\n    extractInfoFromGroupData,\n    getAggregateSpecifications,\n} from \"@web/model/relational_model/utils\";\n\nconst FALSE = Symbol(\"False\");\n\n/**\n *\n * @param {*} groups: returned by formatted_read_group\n * @param {*} groupByField\n * @param {*} value\n * @returns\n */\n\nfunction _findGroup(groups, groupByField, value) {\n    return groups.find((g) => g[groupByField.name] === value) || {};\n}\n\nfunction _createFilterDomain(fieldName, bars, value) {\n    let filterDomain = undefined;\n    if (value === FALSE) {\n        const keys = bars.filter((x) => x.value !== FALSE).map((x) => x.value);\n        filterDomain = [\"!\", [fieldName, \"in\", keys]];\n    } else {\n        filterDomain = [[fieldName, \"=\", value]];\n    }\n    return filterDomain;\n}\n\nfunction _groupsToAggregateValues(groups, groupBy, fields, domain) {\n    const groupByFieldName = groupBy[0].split(\":\")[0];\n    return groups.map((g) => {\n        const groupInfo = extractInfoFromGroupData(g, groupBy, fields, domain);\n        return Object.assign(groupInfo.aggregates, { [groupByFieldName]: groupInfo.serverValue });\n    });\n}\n\nclass ProgressBarState {\n    constructor(progressAttributes, model, aggregateFields, activeBars = {}) {\n        this.progressAttributes = progressAttributes;\n        this.model = model;\n        this._groupsInfo = {};\n        this._aggregateFields = aggregateFields;\n        this.activeBars = activeBars;\n        this._aggregateValues = [];\n        this._pbCounts = null;\n    }\n\n    getGroupInfo(group) {\n        if (this._pbCounts === null) {\n            // progressbar isn't loaded yet\n            return {\n                activeBar: null,\n                bars: [],\n                isReady: false,\n            };\n        }\n        if (!this._groupsInfo[group.id]) {\n            const aggValues = _findGroup(\n                this._aggregateValues,\n                group.groupByField,\n                group.serverValue\n            );\n            const index = this._aggregateValues.indexOf(aggValues);\n            if (index > -1) {\n                this._aggregateValues.splice(index, 1);\n            }\n            this._aggregateValues.push({\n                ...group.aggregates,\n                [group.groupByField.name]: group.serverValue,\n            });\n            const groupValue = this._getGroupValue(group);\n            const pbCount = this._pbCounts[groupValue];\n            const { fieldName, colors } = this.progressAttributes;\n            const { selection: fieldSelection } = this.model.root.fields[fieldName];\n            const selection = fieldSelection && Object.fromEntries(fieldSelection);\n            const bars = Object.entries(colors).map(([value, color]) => {\n                let string;\n                if (selection) {\n                    string = selection[value];\n                } else {\n                    string = String(value);\n                }\n                return {\n                    count: (pbCount && pbCount[value]) || 0,\n                    value,\n                    string,\n                    color,\n                };\n            });\n            bars.push({\n                count: group.count - bars.map((r) => r.count).reduce((a, b) => a + b, 0),\n                value: FALSE,\n                string: _t(\"Other\"),\n                color: \"200\",\n            });\n\n            // Update activeBars count and aggreagates\n            if (this.activeBars[group.serverValue]) {\n                this.activeBars[group.serverValue].count = bars.find(\n                    (x) => x.value === this.activeBars[group.serverValue].value\n                ).count;\n\n                if (this.activeBars[group.serverValue].count === 0) {\n                    group.applyFilter(undefined).then(() => {\n                        delete this.activeBars[group.serverValue];\n                        group.model.notify();\n                    });\n                }\n\n                if (this._aggregateFields.length) {\n                    //recompute the aggregates is not necessary\n                    //the formatted_read_group was already done with the correct domain (containing the applied filter)\n                    this.activeBars[group.serverValue].aggregates = _findGroup(\n                        this._aggregateValues,\n                        group.groupByField,\n                        group.serverValue\n                    );\n                }\n            }\n\n            const self = this;\n            const progressBar = {\n                get activeBar() {\n                    return self.activeBars[group.serverValue]?.value || null;\n                },\n                bars,\n                isReady: true,\n            };\n\n            this._groupsInfo[group.id] = progressBar;\n        }\n        return this._groupsInfo[group.id];\n    }\n\n    getAggregateValue(group, aggregateField) {\n        const { groupByField, serverValue } = group;\n        const title = aggregateField ? aggregateField.string : _t(\"Count\");\n        let value = 0;\n        if (!this.activeBars[serverValue]) {\n            value = group.count;\n            if (value && aggregateField) {\n                value = _findGroup(this._aggregateValues, groupByField, serverValue)[\n                    aggregateField.name\n                ];\n            }\n        } else {\n            value = this.activeBars[serverValue].count;\n            if (value && aggregateField) {\n                value =\n                    this.activeBars[serverValue]?.aggregates &&\n                    this.activeBars[serverValue]?.aggregates[aggregateField.name];\n            }\n        }\n        value ||= 0;\n        if (aggregateField.type === \"monetary\" && aggregateField.currency_field) {\n            const aggValues = _findGroup(this._aggregateValues, groupByField, serverValue);\n            const currencies = aggValues?.[aggregateField.currency_field];\n            if (currencies?.length > 1) {\n                return {\n                    value,\n                    currencies,\n                };\n            }\n            if (currencies?.[0]) {\n                return {\n                    title,\n                    value,\n                    currencies: [currencies[0]],\n                };\n            }\n        }\n        return { title, value };\n    }\n\n    async selectBar(groupId, bar) {\n        const group = this.model.root.groups.find((group) => group.id === groupId);\n        const progressBar = this.getGroupInfo(group);\n        const nextActiveBar = {};\n        if (bar.value && this.activeBars[group.serverValue]?.value !== bar.value) {\n            nextActiveBar.value = bar.value;\n        } else {\n            group.applyFilter(undefined).then(() => {\n                delete this.activeBars[group.serverValue];\n                group.model.notify();\n            });\n            return;\n        }\n        const { bars } = progressBar;\n        const filterDomain = _createFilterDomain(\n            this.progressAttributes.fieldName,\n            bars,\n            nextActiveBar.value\n        );\n        const proms = [];\n        proms.push(\n            group.applyFilter(filterDomain).then((res) => {\n                const groupInfo = this.getGroupInfo(group);\n                nextActiveBar.count = groupInfo.bars.find(\n                    (x) => x.value === nextActiveBar.value\n                ).count;\n            })\n        );\n        if (this._aggregateFields.length) {\n            proms.push(this._updateAggregateGroup(group, bars, nextActiveBar));\n        }\n        await Promise.all(proms);\n        this.activeBars[group.serverValue] = nextActiveBar;\n        this.updateCounts(group);\n    }\n\n    _updateAggregateGroup(group, bars, activeBar) {\n        const filterDomain = _createFilterDomain(\n            this.progressAttributes.fieldName,\n            bars,\n            activeBar.value\n        );\n        const { context, fields, groupBy, resModel } = this.model.root;\n        const kwargs = { context };\n        const aggregateSpecs = getAggregateSpecifications(this._aggregateFields);\n        const domain = filterDomain\n            ? Domain.and([group.groupDomain, filterDomain]).toList()\n            : group.groupDomain;\n        return this.model.orm\n            .formattedReadGroup(resModel, domain, groupBy, aggregateSpecs, kwargs)\n            .then((groups) => {\n                if (groups.length) {\n                    const groupByField = group.groupByField;\n                    const aggrValues = _groupsToAggregateValues(groups, groupBy, fields, domain);\n                    activeBar.aggregates = _findGroup(aggrValues, groupByField, group.serverValue);\n                }\n            });\n    }\n\n    updateCounts(group) {\n        this._updateProgressBar();\n        if (this._aggregateFields.length) {\n            this._updateAggregates();\n            this.updateAggregateGroup(group);\n        }\n\n        // If the selected bar is empty, remove the selection\n        for (const group of this.model.root.groups) {\n            if (this.activeBars[group.serverValue] && group.list.count === 0) {\n                this.selectBar(group.id, { value: null });\n            }\n        }\n    }\n\n    updateAggregateGroup(group) {\n        if (group && this.activeBars[group.serverValue]) {\n            const { bars } = this.getGroupInfo(group);\n            this._updateAggregateGroup(group, bars, this.activeBars[group.serverValue]);\n        }\n    }\n\n    async _updateAggregates() {\n        const { context, fields, groupBy, domain, resModel } = this.model.root;\n        const kwargs = { context };\n        const groups = await this.model.orm.formattedReadGroup(\n            resModel,\n            domain,\n            groupBy,\n            getAggregateSpecifications(this._aggregateFields),\n            kwargs\n        );\n        this._aggregateValues = _groupsToAggregateValues(groups, groupBy, fields);\n    }\n\n    async _updateProgressBar() {\n        const groupBy = this.model.root.groupBy;\n        if (groupBy.length) {\n            const resModel = this.model.root.resModel;\n            const domain = this.model.root.domain;\n            const context = this.model.root.context;\n            const { colors, fieldName: field, help } = this.progressAttributes;\n            const groupsId = this.model.root.groups.map((g) => g.id).join();\n            const res = await this.model.orm.call(resModel, \"read_progress_bar\", [], {\n                domain,\n                group_by: groupBy[0],\n                progress_bar: { colors, field, help },\n                context,\n            });\n            if (groupsId !== this.model.root.groups.map((g) => g.id).join()) {\n                return;\n            }\n            this._pbCounts = res;\n            for (const group of this.model.root.groups) {\n                if (!group.isFolded) {\n                    const groupInfo = this.getGroupInfo(group);\n                    const groupValue = this._getGroupValue(group);\n                    const counts = res[groupValue];\n                    for (const bar of groupInfo.bars) {\n                        bar.count = (counts && counts[bar.value]) || 0;\n                    }\n                    groupInfo.bars.find((b) => b.value === FALSE).count = counts\n                        ? group.count - Object.values(counts).reduce((a, b) => a + b, 0)\n                        : group.count;\n\n                    if (this.activeBars[group.serverValue]) {\n                        this.activeBars[group.serverValue].count = groupInfo.bars.find(\n                            (x) => x.value === this.activeBars[group.serverValue].value\n                        ).count;\n                    }\n                }\n            }\n        }\n    }\n\n    async loadProgressBar({ context, domain, groupBy, resModel }) {\n        if (groupBy.length) {\n            const { colors, fieldName: field, help } = this.progressAttributes;\n            const res = await this.model.orm.call(resModel, \"read_progress_bar\", [], {\n                domain,\n                group_by: groupBy[0],\n                progress_bar: { colors, field, help },\n                context,\n            });\n            this._pbCounts = res;\n        }\n    }\n\n    getGroupCount(group) {\n        const progressBarInfo = this.getGroupInfo(group);\n        if (progressBarInfo.activeBar) {\n            const progressBar = progressBarInfo.bars.find(\n                (b) => b.value === progressBarInfo.activeBar\n            );\n            return progressBar.count;\n        }\n    }\n\n    /**\n     * We must be able to match groups returned by the read_progress_bar call with groups previously\n     * returned by formatted_read_group. When grouped on date(time) fields, the key of each group is the\n     * displayName of the period (e.g. \"W8 2024\"). When grouped on boolean fields, it's \"True\" and\n     * \"False\". For falsy values (e.g. unset many2one), it's \"False\". In all other cases, it's the\n     * group's value (e.g. the id for a many2one).\n     *\n     * @param {Group} group\n     * @return string\n     */\n    _getGroupValue(group) {\n        if (group.value === true) {\n            return \"True\";\n        } else if (group.value === false) {\n            return \"False\";\n        }\n        return group.serverValue;\n    }\n}\n\nexport function useProgressBar(progressAttributes, model, aggregateFields, activeBars) {\n    const progressBarState = reactive(\n        new ProgressBarState(progressAttributes, model, aggregateFields, activeBars)\n    );\n\n    const onWillLoadRoot = model.hooks.onWillLoadRoot;\n    let prom;\n    model.hooks.onWillLoadRoot = (config) => {\n        onWillLoadRoot();\n        prom = progressBarState.loadProgressBar({\n            context: config.context,\n            domain: config.domain,\n            groupBy: config.groupBy,\n            resModel: config.resModel,\n        });\n    };\n    const onRootLoaded = model.hooks.onRootLoaded;\n    model.hooks.onRootLoaded = async (root) => {\n        await onRootLoaded(root);\n        if (model.isReady) {\n            // do not wait for the progressbar on first load, to show the kanban view asap\n            return prom;\n        }\n    };\n\n    return progressBarState;\n}\n", "import { renderToElement } from \"@web/core/utils/render\";\nimport { useDebounced } from \"@web/core/utils/timing\";\nimport {\n    formatDate,\n    formatDateTime,\n    toLocaleDateString,\n    toLocaleDateTimeString,\n} from \"@web/core/l10n/dates\";\nimport { localization } from \"@web/core/l10n/localization\";\n\nimport {\n    onMounted,\n    onWillUnmount,\n    status,\n    useComponent,\n    useEffect,\n    useExternalListener,\n    xml,\n} from \"@odoo/owl\";\n\n// This file defines a hook that encapsulates the column width logic of the list view. This logic\n// aims at optimizing the available space between columns and, once computed, at freezing the table\n// to ensure that the columns don't flicker. This hook is meant to be used by the ListRenderer only,\n// it isn't a generic hook that can be used in various contexts.\n//\n// Widths computation specs\n// ------------------------\n//\n// For some field types, we harcode the column width because we know the required space to display\n// values for that type (e.g. a Date field always requires the same space). A width can also be\n// hardcoded in the arch (`width=\"60px\"`). In those cases, the column has a fixed width that we\n// enforce. Note that the column width will be the given width + the cell's left and right paddings.\n// Numeric fields don't technically have a fixed width, but rather a range: we always want enough\n// space s.t. `1 million` would fit, and we consider that we don't need more space than `1 billion`\n// would require to fit. Depending on the field type (integer, float, monetary), we determine the\n// necessary width to display those numbers.\n// The other columns have an hardcoded min width, that we always want to guarantee, but they have no\n// max width.\n//\n// There're two cases. In both of them, we need to compute a starting point for the widths:\n//   - there's no data in the table: we force all columns with hardcoded widths to those widths and\n//     uniformly distribute the remaining space among the other columns.\n//   - there're records in the table, we let the browser compute ideal widths based on the content\n//     of the table.\n// Once this is done, we ensure that each column complies with their min and max widths. It may\n// happen that some columns are too narrow (because their content is small, and there're a lot of\n// columns), so we expand them to their minimal width. It may also happen that some columns are too\n// wide (if they have a max width), so we shrink them.\n// Once this is done, we must ensure that the sum of the column widths still fills 100% of the\n// table. That means that we might have to expand/narrow columns, again. It may happen that the\n// table has too many columns s.t. they can't fit within the 100% by complying the the rules, it's\n// fine, an horizontal scrollbar will be displayed in that case.\n//\n// Freeze logic\n// ------------\n//\n// Once optimal widths have been computed, we want the table to be frozen s.t. columns don't resize\n// upon user interaction, like inline edition, adding or removing a record... The computed widths\n// are thus stored, and re-applied at each rendering. There're exceptions though. If the columns\n// change (e.g. optional column toggled), if the window is resized, if we remove a filter or open\n// a group s.t. the list contains records for the first time, we forget the computed widths and\n// start over.\n\n// Hardcoded widths\nconst DEFAULT_MIN_WIDTH = 80;\nconst SELECTOR_WIDTH = 20;\nconst OPEN_FORM_VIEW_BUTTON_WIDTH = 54;\nconst DELETE_BUTTON_WIDTH = 12;\nlet _dateWidths = null; // computed dynamically, lazily, see @computeOptimalDateWidths\nexport const FIELD_WIDTHS = Object.freeze({\n    boolean: [20, 100], // [minWidth, maxWidth]\n    char: [80], // only minWidth, no maxWidth\n    get date() {\n        if (!_dateWidths) {\n            computeOptimalDateWidths();\n        }\n        return _dateWidths.date;\n    },\n    get datetime() {\n        if (!_dateWidths) {\n            computeOptimalDateWidths();\n        }\n        return _dateWidths.datetime;\n    },\n    get numeric_date() {\n        if (!_dateWidths) {\n            computeOptimalDateWidths();\n        }\n        return _dateWidths.numericDate;\n    },\n    get numeric_datetime() {\n        if (!_dateWidths) {\n            computeOptimalDateWidths();\n        }\n        return _dateWidths.numericDatetime;\n    },\n    float: 93,\n    integer: 71,\n    many2many: [80],\n    many2one_reference: [80],\n    many2one: [80],\n    monetary: 105,\n    one2many: [80],\n    reference: [80],\n    selection: [80],\n    text: [80, 1200],\n});\n\nexport function resetDateFieldWidths() {\n    // useful for tests\n    _dateWidths = null;\n}\n\n/**\n * Compute ideal date and datetime widths. There's no static value for them as they depend on the\n * localization. Moreover, as we want to have the exact minimum width necessary, it also depends on\n * the fonts (we never want to see \"...\" in date fields). So we render date(time) values, we insert\n * them into the DOM and compute their width.\n */\nfunction computeOptimalDateWidths() {\n    const { timeFormat } = localization;\n    const values = {\n        date: [],\n        datetime: [],\n        numericDate: [],\n        numericDatetime: [],\n    };\n    // dates in the \"human readable\" format (must generate a date by month as width could vary)\n    for (let month = 1; month <= 12; month++) {\n        values.date.push(toLocaleDateString(luxon.DateTime.local(2017, month, 20)));\n        values.datetime.push(\n            toLocaleDateTimeString(luxon.DateTime.local(2017, month, 25, 10, 0, 0), {\n                showSeconds: true,\n            })\n        );\n        if (timeFormat === \"hh:mm:ss a\") {\n            // generate a date in the afternoon if time is displayed with AM/PM or equivalent\n            values.datetime.push(\n                toLocaleDateTimeString(luxon.DateTime.local(2017, month, 25, 22, 0, 0), {\n                    showSeconds: true,\n                })\n            );\n        }\n    }\n    // dates in the \"numeric\" format\n    values.numericDate.push(formatDate(luxon.DateTime.local(2017, 1, 1)));\n    values.numericDatetime.push(formatDateTime(luxon.DateTime.local(2017, 1, 1, 10, 0, 0)));\n    if (timeFormat === \"hh:mm:ss a\") {\n        // generate a date in the afternoon if time is displayed with AM/PM or equivalent\n        values.numericDatetime.push(formatDateTime(luxon.DateTime.local(2017, 1, 1, 22, 0, 0)));\n    }\n\n    const template = xml`\n        <div class=\"invisible\" style=\"font-variant-numeric: tabular-nums;\">\n            <div t-foreach=\"Object.keys(values)\" t-as=\"key\" t-key=\"key\" t-att-class=\"key\">\n                <div t-foreach=\"values[key]\" t-as=\"value\" t-key=\"value_index\">\n                    <span t-esc=\"value\"/>\n                </div>\n            </div>\n        </div>`;\n    const div = renderToElement(template, { values });\n    document.body.append(div);\n    _dateWidths = {};\n    for (const key in values) {\n        const spans = div.querySelectorAll(`.${key} span`);\n        const widths = [...spans].map((span) => span.getBoundingClientRect().width);\n        // add a 5% margin to cope with potential bold decorations\n        _dateWidths[key] = Math.ceil(Math.max(...widths) * 1.05);\n    }\n    document.body.removeChild(div);\n}\n\n/**\n * Compute ideal widths based on the rules described on top of this file.\n *\n * @params {Element} table\n * @params {Object} state\n * @params {Number} allowedWidth\n * @params {Number[]} startingWidths\n * @returns {Number[]}\n */\nfunction computeWidths(table, state, allowedWidth, startingWidths) {\n    let _columnWidths;\n    const headers = [...table.querySelectorAll(\"thead th\")];\n    const columns = state.columns;\n\n    // Starting point: compute widths\n    if (startingWidths) {\n        _columnWidths = startingWidths.slice();\n    } else if (state.isEmpty) {\n        // Table is empty => uniform distribution as starting point\n        _columnWidths = headers.map(() => allowedWidth / headers.length);\n    } else {\n        // Table contains records => let the browser compute ideal widths\n        // Set table layout auto and remove inline style\n        table.style.tableLayout = \"auto\";\n        headers.forEach((th) => {\n            th.style.width = null;\n        });\n        // Toggle a className used to remove style that could interfere with the ideal width\n        // computation algorithm (e.g. prevent text fields from being wrapped during the\n        // computation, to prevent them from being completely crushed)\n        table.classList.add(\"o_list_computing_widths\");\n        _columnWidths = headers.map((th) => th.getBoundingClientRect().width);\n        table.classList.remove(\"o_list_computing_widths\");\n    }\n\n    // Force columns to comply with their min and max widths\n    if (state.hasSelectors) {\n        _columnWidths[0] = SELECTOR_WIDTH;\n    }\n    if (state.hasOpenFormViewColumn) {\n        const index = _columnWidths.length - (state.hasActionsColumn ? 2 : 1);\n        _columnWidths[index] = OPEN_FORM_VIEW_BUTTON_WIDTH;\n    }\n    if (state.hasActionsColumn) {\n        _columnWidths[_columnWidths.length - 1] = DELETE_BUTTON_WIDTH;\n    }\n    const columnWidthSpecs = getWidthSpecs(columns);\n    const columnOffset = state.hasSelectors ? 1 : 0;\n    for (let columnIndex = 0; columnIndex < columns.length; columnIndex++) {\n        const thIndex = columnIndex + columnOffset;\n        const { minWidth, maxWidth } = columnWidthSpecs[columnIndex];\n        if (_columnWidths[thIndex] < minWidth) {\n            _columnWidths[thIndex] = minWidth;\n        } else if (maxWidth && _columnWidths[thIndex] > maxWidth) {\n            _columnWidths[thIndex] = maxWidth;\n        }\n    }\n\n    // Expand/shrink columns for the table to fill 100% of available space\n    const totalWidth = _columnWidths.reduce((tot, width) => tot + width, 0);\n    let diff = totalWidth - allowedWidth;\n    if (diff >= 1) {\n        // Case 1: table overflows its parent => shrink some columns\n        const shrinkableColumns = [];\n        let totalAvailableSpace = 0; // total space we can gain by shrinking columns\n        for (let columnIndex = 0; columnIndex < columns.length; columnIndex++) {\n            const thIndex = columnIndex + columnOffset;\n            const { minWidth, canShrink } = columnWidthSpecs[columnIndex];\n            if (_columnWidths[thIndex] > minWidth && canShrink) {\n                shrinkableColumns.push({ thIndex, minWidth });\n                totalAvailableSpace += _columnWidths[thIndex] - minWidth;\n            }\n        }\n        if (diff > totalAvailableSpace) {\n            // We can't find enough space => set all columns to their min width, and there'll be an\n            // horizontal scrollbar\n            for (const { thIndex, minWidth } of shrinkableColumns) {\n                _columnWidths[thIndex] = minWidth;\n            }\n        } else {\n            // There's enough available space among shrinkable columns => shrink them uniformly\n            let remainingColumnsToShrink = shrinkableColumns.length;\n            while (diff >= 1) {\n                const colDiff = diff / remainingColumnsToShrink;\n                for (const { thIndex, minWidth } of shrinkableColumns) {\n                    const currentWidth = _columnWidths[thIndex];\n                    if (currentWidth === minWidth) {\n                        continue;\n                    }\n                    const newWidth = Math.max(currentWidth - colDiff, minWidth);\n                    diff -= currentWidth - newWidth;\n                    _columnWidths[thIndex] = newWidth;\n                    if (newWidth === minWidth) {\n                        remainingColumnsToShrink--;\n                    }\n                }\n            }\n        }\n    } else if (diff <= -1) {\n        // Case 2: table is narrower than its parent => expand some columns\n        diff = -diff; // for better readability\n        const expandableColumns = [];\n        for (let columnIndex = 0; columnIndex < columns.length; columnIndex++) {\n            const thIndex = columnIndex + columnOffset;\n            const maxWidth = columnWidthSpecs[columnIndex].maxWidth;\n            if (!maxWidth || _columnWidths[thIndex] < maxWidth) {\n                expandableColumns.push({ thIndex, maxWidth });\n            }\n        }\n        // Expand all expandable columns uniformly (i.e. at most, expand columns with a maxWidth\n        // to their maxWidth)\n        let remainingExpandableColumns = expandableColumns.length;\n        while (diff >= 1 && remainingExpandableColumns > 0) {\n            const colDiff = diff / remainingExpandableColumns;\n            for (const { thIndex, maxWidth } of expandableColumns) {\n                const currentWidth = _columnWidths[thIndex];\n                const newWidth = Math.min(currentWidth + colDiff, maxWidth || Number.MAX_VALUE);\n                diff -= newWidth - currentWidth;\n                _columnWidths[thIndex] = newWidth;\n                if (newWidth === maxWidth) {\n                    remainingExpandableColumns--;\n                }\n            }\n        }\n        if (diff >= 1) {\n            // All columns have a maxWidth and have been expanded to their max => expand them more\n            for (let columnIndex = 0; columnIndex < columns.length; columnIndex++) {\n                const thIndex = columnIndex + columnOffset;\n                _columnWidths[thIndex] += diff / columns.length;\n            }\n        }\n    }\n    return _columnWidths;\n}\n\n/**\n * Returns for each column its minimal and (if any) maximal widths.\n *\n * @param {Object[]} columns\n * @returns {Object[]} each entry in this array has a minWidth and optionally a maxWidth key\n */\nfunction getWidthSpecs(columns) {\n    return columns.map((column) => {\n        let minWidth;\n        let maxWidth;\n        if (column.attrs && column.attrs.width) {\n            minWidth = maxWidth = parseInt(column.attrs.width.split(\"px\")[0]);\n        } else {\n            let width;\n            if (column.type === \"field\") {\n                if (column.field.listViewWidth) {\n                    width = column.field.listViewWidth;\n                    if (typeof width === \"function\") {\n                        width = width({\n                            type: column.fieldType,\n                            hasLabel: column.hasLabel,\n                            options: column.options,\n                        });\n                    }\n                } else {\n                    width = FIELD_WIDTHS[column.widget || column.fieldType];\n                }\n            } else if (column.type === \"widget\") {\n                width = column.widget.listViewWidth;\n            }\n            if (width) {\n                minWidth = Array.isArray(width) ? width[0] : width;\n                maxWidth = Array.isArray(width) ? width[1] : width;\n            } else {\n                minWidth = DEFAULT_MIN_WIDTH;\n            }\n        }\n        return { minWidth, maxWidth, canShrink: column.type === \"field\" };\n    });\n}\n\n/**\n * Given an html element, returns the sum of its left and right padding.\n *\n * @param {HTMLElement} el\n * @returns {Number}\n */\nfunction getHorizontalPadding(el) {\n    const { paddingLeft, paddingRight } = getComputedStyle(el);\n    return parseFloat(paddingLeft) + parseFloat(paddingRight);\n}\n\nexport function useMagicColumnWidths(tableRef, getState) {\n    const renderer = useComponent();\n    let columnWidths = null;\n    let allowedWidth = 0;\n    let hasAlwaysBeenEmpty = true;\n    let parentWidthFixed = false;\n    let hash;\n    let _resizing = false;\n\n    /**\n     * Apply the column widths in the DOM. If necessary, compute them first (e.g. if they haven't\n     * been computed yet, or if columns have changed).\n     *\n     * Note: the following code manipulates the DOM directly to avoid having to wait for a\n     * render + patch which would occur on the next frame and cause flickering.\n     */\n    function forceColumnWidths() {\n        const table = tableRef.el;\n        const headers = [...table.querySelectorAll(\"thead th\")];\n        const state = getState();\n\n        // Generate a hash to be able to detect when the columns change\n        const columns = state.columns;\n        // The last part of the hash is there to detect that static columns changed (typically, the\n        // selector column, which isn't displayed on small screens)\n        const nextHash = `${columns.map((column) => column.id).join(\"/\")}/${headers.length}`;\n        if (nextHash !== hash) {\n            hash = nextHash;\n            unsetWidths();\n        }\n        // If the table has always been empty until now, and it now contains records, we want to\n        // recompute the widths based on the records (typical case: we removed a filter).\n        // Exception: we were in an empty editable list, and we just added a first record.\n        if (hasAlwaysBeenEmpty && !state.isEmpty) {\n            hasAlwaysBeenEmpty = false;\n            const rows = table.querySelectorAll(\".o_data_row\");\n            if (rows.length !== 1 || !rows[0].classList.contains(\"o_selected_row\")) {\n                unsetWidths();\n            }\n        }\n\n        const parentPadding = getHorizontalPadding(table.parentNode);\n        const cellPaddings = headers.map((th) => getHorizontalPadding(th));\n        const totalCellPadding = cellPaddings.reduce((total, padding) => padding + total, 0);\n        const nextAllowedWidth = table.parentNode.clientWidth - parentPadding - totalCellPadding;\n        const allowedWidthDiff = Math.abs(allowedWidth - nextAllowedWidth);\n        allowedWidth = nextAllowedWidth;\n\n        // When a vertical scrollbar appears/disappears, it may (depending on the browser/os) change\n        // the available width. When it does, we want to keep the current widths, but tweak them a\n        // little bit s.t. the table fits in the new available space.\n        if (!columnWidths || allowedWidthDiff > 0) {\n            columnWidths = computeWidths(table, state, allowedWidth, columnWidths);\n        }\n\n        // Set the computed widths in the DOM.\n        table.style.tableLayout = \"fixed\";\n        headers.forEach((th, index) => {\n            th.style.width = `${Math.floor(columnWidths[index] + cellPaddings[index])}px`;\n        });\n    }\n\n    /**\n     * Unsets the widths. After next patch, ideal widths will be recomputed.\n     */\n    function unsetWidths() {\n        columnWidths = null;\n        // Unset widths that might have been set on the table by resizing a column\n        tableRef.el.style.width = null;\n        if (parentWidthFixed) {\n            tableRef.el.parentElement.style.width = null;\n        }\n    }\n\n    /**\n     * Handles the resize feature on the column headers\n     *\n     * @private\n     * @param {MouseEvent} ev\n     */\n    function onStartResize(ev) {\n        _resizing = true;\n        const table = tableRef.el;\n        const th = ev.target.closest(\"th\");\n        table.style.width = `${Math.floor(table.getBoundingClientRect().width)}px`;\n        const thPosition = [...th.parentNode.children].indexOf(th);\n        const resizingColumnElements = [...table.getElementsByTagName(\"tr\")]\n            .filter((tr) => tr.children.length === th.parentNode.children.length)\n            .map((tr) => tr.children[thPosition]);\n        const initialX = ev.clientX;\n        const initialWidth = th.getBoundingClientRect().width;\n        const initialTableWidth = table.getBoundingClientRect().width;\n        const resizeStoppingEvents = [\"keydown\", \"pointerdown\", \"pointerup\"];\n\n        // Fix the width so that if the resize overflows, it doesn't affect the layout of the parent\n        if (!table.parentElement.style.width) {\n            parentWidthFixed = true;\n            table.parentElement.style.width = `${Math.floor(\n                table.parentElement.getBoundingClientRect().width\n            )}px`;\n        }\n\n        // Apply classes to the selected column\n        for (const el of resizingColumnElements) {\n            el.classList.add(\"o_column_resizing\");\n        }\n        // Mousemove event : resize header\n        const resizeHeader = (ev) => {\n            ev.preventDefault();\n            ev.stopPropagation();\n            let delta = ev.clientX - initialX;\n            delta = localization.direction === \"rtl\" ? -delta : delta;\n            const newWidth = Math.max(10, initialWidth + delta);\n            const tableDelta = newWidth - initialWidth;\n            th.style.width = `${Math.floor(newWidth)}px`;\n            table.style.width = `${Math.floor(initialTableWidth + tableDelta)}px`;\n        };\n        window.addEventListener(\"pointermove\", resizeHeader);\n\n        // Mouse or keyboard events : stop resize\n        const stopResize = (ev) => {\n            _resizing = false;\n\n            // Store current column widths to freeze them\n            const headers = [...table.querySelectorAll(\"thead th\")];\n            columnWidths = headers.map(\n                (th) => th.getBoundingClientRect().width - getHorizontalPadding(th)\n            );\n\n            // Ignores the 'left mouse button down' event as it used to start resizing\n            if (ev.type === \"pointerdown\" && ev.button === 0) {\n                return;\n            }\n            ev.preventDefault();\n            ev.stopPropagation();\n\n            for (const el of resizingColumnElements) {\n                el.classList.remove(\"o_column_resizing\");\n            }\n\n            window.removeEventListener(\"pointermove\", resizeHeader);\n            for (const eventType of resizeStoppingEvents) {\n                window.removeEventListener(eventType, stopResize);\n            }\n\n            // We remove the focus to make sure that the there is no focus inside\n            // the tr.  If that is the case, there is some css to darken the whole\n            // thead, and it looks quite weird with the small css hover effect.\n            document.activeElement.blur();\n        };\n        // We have to listen to several events to properly stop the resizing function. Those are:\n        // - pointerdown (e.g. pressing right click)\n        // - pointerup : logical flow of the resizing feature (drag & drop)\n        // - keydown : (e.g. pressing 'Alt' + 'Tab' or 'Windows' key)\n        for (const eventType of resizeStoppingEvents) {\n            window.addEventListener(eventType, stopResize);\n        }\n    }\n\n    /**\n     * Forces a recomputation of column widths\n     */\n    function resetWidths() {\n        unsetWidths();\n        forceColumnWidths();\n    }\n\n    // Side effects\n    if (renderer.constructor.useMagicColumnWidths) {\n        useEffect(forceColumnWidths);\n        // Forget computed widths (and potential manual column resize) on window resize\n        useExternalListener(window, \"resize\", unsetWidths);\n        // Listen to width changes on the parent node of the table, to recompute ideal widths\n        // Note: we compute the widths once, directly, and once after parent width stabilization.\n        // The first call is only necessary to avoid an annoying flickering when opening form views\n        // with an x2many list and a chatter (when it is displayed below the form) as it may happen\n        // that the display of chatter messages introduces a vertical scrollbar, thus reducing the\n        // available width.\n        const component = useComponent();\n        let parentWidth;\n        const debouncedForceColumnWidths = useDebounced(\n            () => {\n                if (status(component) !== \"destroyed\") {\n                    forceColumnWidths();\n                }\n            },\n            200,\n            { immediate: true, trailing: true }\n        );\n        const resizeObserver = new ResizeObserver(() => {\n            const newParentWidth = tableRef.el.parentNode.clientWidth;\n            if (newParentWidth !== parentWidth) {\n                parentWidth = newParentWidth;\n                debouncedForceColumnWidths();\n            }\n        });\n        onMounted(() => {\n            parentWidth = tableRef.el.parentNode.clientWidth;\n            resizeObserver.observe(tableRef.el.parentNode);\n        });\n        onWillUnmount(() => resizeObserver.disconnect());\n    }\n\n    // API\n    return {\n        get resizing() {\n            return _resizing;\n        },\n        onStartResize,\n        resetWidths,\n    };\n}\n", "import { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\nimport { registry } from \"@web/core/registry\";\nimport { user } from \"@web/core/user\";\nimport { exprToBoolean } from \"@web/core/utils/strings\";\nimport { STATIC_ACTIONS_GROUP_NUMBER } from \"@web/search/action_menus/action_menus\";\n\nimport { Component } from \"@odoo/owl\";\n\nconst cogMenuRegistry = registry.category(\"cogMenu\");\n\n/**\n * 'Export All' menu\n *\n * This component is used to export all the records for particular model.\n * @extends Component\n */\nexport class ExportAll extends Component {\n    static template = \"web.ExportAll\";\n    static components = { DropdownItem };\n    static props = {};\n\n    //---------------------------------------------------------------------\n    // Protected\n    //---------------------------------------------------------------------\n\n    async onDirectExportData() {\n        this.env.searchModel.trigger(\"direct-export-data\");\n    }\n}\n\nexport const exportAllItem = {\n    Component: ExportAll,\n    groupNumber: STATIC_ACTIONS_GROUP_NUMBER,\n    isDisplayed: async (env) =>\n        [\"kanban\", \"list\"].includes(env.config.viewType) &&\n        !env.model.root.selection.length &&\n        (await user.hasGroup(\"base.group_allow_export\")) &&\n        exprToBoolean(env.config.viewArch.getAttribute(\"export_xlsx\"), true),\n};\n\ncogMenuRegistry.add(\"export-all-menu\", exportAllItem, { sequence: 10 });\n", "import { exprToBoolean } from \"@web/core/utils/strings\";\nimport { visitXML } from \"@web/core/utils/xml\";\nimport { combineModifiers } from \"@web/model/relational_model/utils\";\nimport { stringToOrderBy } from \"@web/search/utils/order_by\";\nimport { Field } from \"@web/views/fields/field\";\nimport { getActiveActions, getDecoration, processButton } from \"@web/views/utils\";\nimport { encodeObjectForTemplate } from \"@web/views/view_compiler\";\nimport { Widget } from \"@web/views/widgets/widget\";\n\nexport class GroupListArchParser {\n    parse(arch, models, modelName, jsClass) {\n        const fieldNodes = {};\n        const fieldNextIds = {};\n        const buttons = [];\n        let buttonId = 0;\n        visitXML(arch, (node) => {\n            if (node.tagName === \"button\") {\n                buttons.push({\n                    ...processButton(node),\n                    id: buttonId++,\n                });\n                return false;\n            } else if (node.tagName === \"field\") {\n                const fieldInfo = Field.parseFieldNode(node, models, modelName, \"list\", jsClass);\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                return false;\n            }\n        });\n        return { fieldNodes, buttons };\n    }\n}\n\nexport class ListArchParser {\n    parseFieldNode(node, models, modelName) {\n        return Field.parseFieldNode(node, models, modelName, \"list\");\n    }\n\n    parseWidgetNode(node, models, modelName) {\n        return Widget.parseWidgetNode(node);\n    }\n\n    processButton(node) {\n        return processButton(node);\n    }\n\n    parse(xmlDoc, models, modelName) {\n        const fieldNodes = {};\n        const widgetNodes = {};\n        let widgetNextId = 0;\n        const columns = [];\n        const fields = models[modelName].fields;\n        let buttonId = 0;\n        const groupBy = {\n            buttons: {},\n            fields: {},\n        };\n        let headerButtons = [];\n        const controls = [];\n        const groupListArchParser = new GroupListArchParser();\n        let buttonGroup;\n        let handleField = null;\n        const treeAttr = {};\n        let nextId = 0;\n        const fieldNextIds = {};\n        visitXML(xmlDoc, (node) => {\n            if (node.tagName !== \"button\") {\n                buttonGroup = undefined;\n            }\n            if (node.tagName === \"button\") {\n                const button = {\n                    ...this.processButton(node),\n                    defaultRank: \"btn-link\",\n                    type: \"button\",\n                    id: buttonId++,\n                };\n                const width = button.attrs.width;\n                if (buttonGroup && !width) {\n                    buttonGroup.buttons.push(button);\n                    buttonGroup.column_invisible = combineModifiers(\n                        buttonGroup.column_invisible,\n                        node.getAttribute(\"column_invisible\"),\n                        \"AND\"\n                    );\n                } else {\n                    buttonGroup = {\n                        id: `column_${nextId++}`,\n                        type: \"button_group\",\n                        buttons: [button],\n                        hasLabel: false,\n                        column_invisible: node.getAttribute(\"column_invisible\"),\n                    };\n                    columns.push(buttonGroup);\n                    if (width) {\n                        buttonGroup.attrs = { width };\n                        buttonGroup = undefined;\n                    }\n                }\n            } else if (node.tagName === \"field\") {\n                const fieldInfo = this.parseFieldNode(node, models, modelName);\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                if (fieldInfo.isHandle) {\n                    handleField = fieldInfo.name;\n                }\n                const label = fieldInfo.field.label;\n                columns.push({\n                    ...fieldInfo,\n                    id: `column_${nextId++}`,\n                    className: node.getAttribute(\"class\"), // for oe_edit_only and oe_read_only\n                    optional: node.getAttribute(\"optional\") || false,\n                    type: \"field\",\n                    fieldType: fieldInfo.type,\n                    hasLabel: !(\n                        fieldInfo.field.label === false ||\n                        exprToBoolean(fieldInfo.attrs.nolabel) === true\n                    ),\n                    label: (fieldInfo.widget && label && label.toString()) || fieldInfo.string,\n                });\n                return false;\n            } else if (node.tagName === \"widget\") {\n                const widgetInfo = this.parseWidgetNode(node);\n                const widgetId = `widget_${++widgetNextId}`;\n                widgetNodes[widgetId] = widgetInfo;\n                node.setAttribute(\"widget_id\", widgetId);\n\n                const widgetProps = {\n                    name: widgetInfo.name,\n                    // FIXME: this is dumb, we encode it into a weird object so that the widget\n                    // can decode it later...\n                    node: encodeObjectForTemplate({ attrs: widgetInfo.attrs }).slice(1, -1),\n                    className: node.getAttribute(\"class\") || \"\",\n                    widgetInfo,\n                };\n                columns.push({\n                    ...widgetInfo,\n                    props: widgetProps,\n                    id: `column_${nextId++}`,\n                    type: \"widget\",\n                });\n            } else if (node.tagName === \"groupby\" && node.getAttribute(\"name\")) {\n                const fieldName = node.getAttribute(\"name\");\n                const coModelName = fields[fieldName].relation;\n                const groupByArchInfo = groupListArchParser.parse(node, models, coModelName);\n                groupBy.buttons[fieldName] = groupByArchInfo.buttons;\n                groupBy.fields[fieldName] = {\n                    fieldNodes: groupByArchInfo.fieldNodes,\n                    fields: models[coModelName].fields,\n                };\n                return false;\n            } else if (node.tagName === \"header\") {\n                headerButtons = [...node.children].map((node) => ({\n                    ...this.processButton(node),\n                    type: \"button\",\n                    id: buttonId++,\n                }));\n                return false;\n            } else if (node.tagName === \"control\") {\n                for (const childNode of node.children) {\n                    if (childNode.tagName === \"button\") {\n                        controls.push({\n                            type: \"button\",\n                            ...processButton(childNode),\n                        });\n                    } else if (childNode.tagName === \"create\") {\n                        controls.push({\n                            type: \"create\",\n                            context: childNode.getAttribute(\"context\"),\n                            string: childNode.getAttribute(\"string\"),\n                            invisible: childNode.getAttribute(\"invisible\"),\n                        });\n                    } else if (childNode.tagName === \"delete\") {\n                        controls.push({\n                            type: \"delete\",\n                            invisible: childNode.getAttribute(\"invisible\"),\n                        });\n                    }\n                }\n                return false;\n            } else if (\"list\" === node.tagName) {\n                const activeActions = {\n                    ...getActiveActions(xmlDoc),\n                    exportXlsx: exprToBoolean(xmlDoc.getAttribute(\"export_xlsx\"), true),\n                    createGroup: exprToBoolean(xmlDoc.getAttribute(\"group_create\"), true),\n                    editGroup: exprToBoolean(xmlDoc.getAttribute(\"group_edit\"), true),\n                    deleteGroup: exprToBoolean(xmlDoc.getAttribute(\"group_delete\"), true),\n                };\n                treeAttr.activeActions = activeActions;\n\n                treeAttr.className = xmlDoc.getAttribute(\"class\") || null;\n                treeAttr.editable = xmlDoc.getAttribute(\"editable\");\n                treeAttr.multiEdit = activeActions.edit\n                    ? exprToBoolean(node.getAttribute(\"multi_edit\") || \"\")\n                    : false;\n\n                treeAttr.openFormView = treeAttr.editable\n                    ? exprToBoolean(xmlDoc.getAttribute(\"open_form_view\") || \"\")\n                    : false;\n                treeAttr.defaultGroupBy = xmlDoc.hasAttribute(\"default_group_by\")\n                    ? xmlDoc.getAttribute(\"default_group_by\").split(\",\")\n                    : null;\n\n                const limitAttr = node.getAttribute(\"limit\");\n                treeAttr.limit = limitAttr && parseInt(limitAttr, 10);\n\n                const countLimitAttr = node.getAttribute(\"count_limit\");\n                treeAttr.countLimit = countLimitAttr && parseInt(countLimitAttr, 10);\n\n                const groupsLimitAttr = node.getAttribute(\"groups_limit\");\n                treeAttr.groupsLimit = groupsLimitAttr && parseInt(groupsLimitAttr, 10);\n\n                treeAttr.noOpen = exprToBoolean(node.getAttribute(\"no_open\") || \"\");\n                treeAttr.rawExpand = xmlDoc.getAttribute(\"expand\");\n                treeAttr.decorations = getDecoration(xmlDoc);\n\n                treeAttr.defaultOrder = stringToOrderBy(\n                    xmlDoc.getAttribute(\"default_order\") || null\n                );\n\n                // custom open action when clicking on record row\n                const action = xmlDoc.getAttribute(\"action\");\n                const type = xmlDoc.getAttribute(\"type\");\n                treeAttr.openAction = action && type ? { action, type } : null;\n            }\n        });\n\n        if (!treeAttr.defaultOrder.length && handleField) {\n            const handleFieldSort = `${handleField}, id`;\n            treeAttr.defaultOrder = stringToOrderBy(handleFieldSort);\n        }\n\n        return {\n            controls,\n            headerButtons,\n            fieldNodes,\n            widgetNodes,\n            columns,\n            groupBy,\n            xmlDoc,\n            ...treeAttr,\n        };\n    }\n}\n", "import { CogMenu } from \"../../search/cog_menu/cog_menu\";\n\nexport class ListCogMenu extends CogMenu {\n    static template = \"web.ListCogMenu\";\n    static props = {\n        ...CogMenu.props,\n        hasSelectedRecords: { type: Number, optional: true },\n    };\n    _registryItems() {\n        return this.props.hasSelectedRecords ? [] : super._registryItems();\n    }\n}\n", "import { Dialog } from \"@web/core/dialog/dialog\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { useAutofocus } from \"@web/core/utils/hooks\";\nimport { TagsList } from \"@web/core/tags_list/tags_list\";\nimport { Operation } from \"@web/model/relational_model/operation\";\nimport { Field, fieldVisualFeedback } from \"@web/views/fields/field\";\n\nimport { Component } from \"@odoo/owl\";\n\nexport class ListConfirmationDialog extends Component {\n    static template = \"web.ListView.ConfirmationModal\";\n    static components = { Dialog, Field, TagsList };\n    static props = {\n        close: Function,\n        title: {\n            validate: (m) =>\n                typeof m === \"string\" ||\n                (typeof m === \"object\" && typeof m.toString === \"function\"),\n            optional: true,\n        },\n        confirm: { type: Function, optional: true },\n        cancel: { type: Function, optional: true },\n        isDomainSelected: Boolean,\n        fields: Object,\n        nbRecords: Number,\n        nbValidRecords: Number,\n        record: Object,\n        changes: Object,\n    };\n    static defaultProps = {\n        title: _t(\"Confirmation\"),\n    };\n\n    setup() {\n        useAutofocus();\n    }\n\n    get validRecordsText() {\n        return _t(\n            \"Among the %(total)s selected records, %(valid_count)s are valid for this update.\",\n            {\n                total: this.props.nbRecords,\n                valid_count: this.props.nbValidRecords,\n            }\n        );\n    }\n\n    get updateConfirmationText() {\n        return _t(\"Are you sure you want to update %(count)s records?\", {\n            count: this.props.nbValidRecords,\n        });\n    }\n\n    get showTip() {\n        return this.props.fields.some((field) =>\n            [\"monetary\", \"integer\", \"float\"].includes(field.fieldNode?.type)\n        );\n    }\n\n    _cancel() {\n        if (this.props.cancel) {\n            this.props.cancel();\n        }\n        this.props.close();\n    }\n\n    async _confirm() {\n        if (this.props.confirm) {\n            await this.props.confirm();\n        }\n        this.props.close();\n    }\n\n    getTagProps(records, field) {\n        const colorField = field.fieldNode.options?.color_field;\n        return records.map((record) => ({\n            id: record.id,\n            resId: record.resId,\n            text: record.data.display_name,\n            colorIndex: colorField ? record.data[colorField] : undefined,\n        }));\n    }\n\n    isValueEmpty(field) {\n        const fieldNode = field.fieldNode || {};\n        return fieldVisualFeedback(fieldNode.field || {}, this.props.record, field.name, {\n            ...fieldNode,\n            // force readonly as we force that state on the Field component\n            readonly: true,\n        }).empty;\n    }\n\n    isValueOperation(field) {\n        return this.props.changes[field.name] instanceof Operation;\n    }\n}\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { evaluateExpr, evaluateBooleanExpr } from \"@web/core/py_js/py\";\nimport { user } from \"@web/core/user\";\nimport { unique } from \"@web/core/utils/arrays\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { omit } from \"@web/core/utils/objects\";\nimport { useSetupAction } from \"@web/search/action_hook\";\nimport { ActionMenus, STATIC_ACTIONS_GROUP_NUMBER } from \"@web/search/action_menus/action_menus\";\nimport { Layout } from \"@web/search/layout\";\nimport { usePager } from \"@web/search/pager_hook\";\nimport { useModelWithSampleData } from \"@web/model/model\";\nimport { DynamicRecordList } from \"@web/model/relational_model/dynamic_record_list\";\nimport { extractFieldsFromArchInfo } from \"@web/model/relational_model/utils\";\nimport { standardViewProps } from \"@web/views/standard_view_props\";\nimport { MultiRecordViewButton } from \"@web/views/view_button/multi_record_view_button\";\nimport { ViewButton } from \"@web/views/view_button/view_button\";\nimport { executeButtonCallback, useViewButtons } from \"@web/views/view_button/view_button_hook\";\nimport { ListConfirmationDialog } from \"./list_confirmation_dialog\";\nimport { SearchBar } from \"@web/search/search_bar/search_bar\";\nimport { useSearchBarToggler } from \"@web/search/search_bar/search_bar_toggler\";\nimport { session } from \"@web/session\";\nimport { ListCogMenu } from \"./list_cog_menu\";\nimport { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\nimport { SelectionBox } from \"@web/views/view_components/selection_box\";\nimport { useExportRecords, useDeleteRecords } from \"@web/views/view_hook\";\n\nimport {\n    Component,\n    onWillPatch,\n    onWillRender,\n    onWillStart,\n    useEffect,\n    useRef,\n    useState,\n    useSubEnv,\n} from \"@odoo/owl\";\n\n// -----------------------------------------------------------------------------\n\nexport class ListController extends Component {\n    static template = `web.ListView`;\n    static components = {\n        ActionMenus,\n        Layout,\n        ViewButton,\n        MultiRecordViewButton,\n        SearchBar,\n        CogMenu: ListCogMenu,\n        DropdownItem,\n        SelectionBox,\n    };\n    static props = {\n        ...standardViewProps,\n        allowSelectors: { type: Boolean, optional: true },\n        onSelectionChanged: { type: Function, optional: true },\n        readonly: { type: Boolean, optional: true },\n        showButtons: { type: Boolean, optional: true },\n        allowOpenAction: { type: Boolean, optional: true },\n        Model: Function,\n        Renderer: Function,\n        buttonTemplate: String,\n        archInfo: Object,\n    };\n    static defaultProps = {\n        allowSelectors: true,\n        createRecord: () => {},\n        selectRecord: () => {},\n        showButtons: true,\n        allowOpenAction: true,\n    };\n\n    setup() {\n        this.actionService = useService(\"action\");\n        this.dialogService = useService(\"dialog\");\n        this.orm = useService(\"orm\");\n        this.rootRef = useRef(\"root\");\n\n        this.archInfo = this.props.archInfo;\n        this.activeActions = this.archInfo.activeActions;\n        this.onOpenFormView = this.openRecord.bind(this);\n        this.editable = (!this.props.readonly && this.archInfo.editable) || false;\n        this.hasOpenFormViewButton = this.editable ? this.archInfo.openFormView : false;\n        this.model = useState(\n            useModelWithSampleData(this.props.Model, this.modelParams, this.modelOptions)\n        );\n\n        // In multi edition, we save or notify invalidity directly when a field is updated, which\n        // occurs on the change event for input fields. But we don't want to do it when clicking on\n        // \"Discard\". So we set a flag on mousedown (which triggers the update) to block the multi\n        // save or invalid notification.\n        // However, if the mouseup (and click) is done outside \"Discard\", we finally want to do it.\n        // We use `nextActionAfterMouseup` for this purpose: it registers a callback to execute if\n        // the mouseup following a mousedown on \"Discard\" isn't done on \"Discard\".\n        this.hasMousedownDiscard = false;\n        this.nextActionAfterMouseup = null;\n\n        this.optionalActiveFields = {};\n\n        this.editedRecord = null;\n        onWillRender(() => {\n            this.editedRecord = this.model.root.editedRecord;\n        });\n\n        onWillStart(async () => {\n            this.isExportEnable = await user.hasGroup(\"base.group_allow_export\");\n        });\n\n        this.archiveEnabled =\n            \"active\" in this.props.fields\n                ? !this.props.fields.active.readonly\n                : \"x_active\" in this.props.fields\n                ? !this.props.fields.x_active.readonly\n                : false;\n        useSubEnv({ model: this.model }); // do this in useModelWithSampleData?\n        useViewButtons(this.rootRef, {\n            beforeExecuteAction: this.beforeExecuteActionButton.bind(this),\n            afterExecuteAction: this.afterExecuteActionButton.bind(this),\n            reload: () => this.model.load(),\n        });\n        const { setScrollFromState } = useSetupAction({\n            rootRef: this.rootRef,\n            beforeLeave: this.beforeLeave.bind(this),\n            beforeUnload: this.beforeUnload.bind(this),\n            getLocalState: () => {\n                const renderer = this.rootRef.el.querySelector(\".o_list_renderer\");\n                return {\n                    modelState: this.model.exportState(),\n                    rendererScrollPositions: {\n                        left: renderer?.scrollLeft || 0,\n                        top: renderer?.scrollTop || 0,\n                    },\n                };\n            },\n            getOrderBy: () => this.model.root.orderBy,\n        });\n\n        useEffect(\n            (isReady) => {\n                if (isReady) {\n                    if (this.env.isSmall) {\n                        setScrollFromState();\n                    } else {\n                        const { rendererScrollPositions } = this.props.state || {};\n                        if (rendererScrollPositions) {\n                            const renderer = this.rootRef.el.querySelector(\".o_list_renderer\");\n                            renderer.scrollLeft = rendererScrollPositions.left;\n                            renderer.scrollTop = rendererScrollPositions.top;\n                        }\n                    }\n                }\n            },\n            () => [this.model.isReady]\n        );\n\n        usePager(() => {\n            if (this.model.useSampleModel) {\n                return;\n            }\n            const { count, hasLimitedCount, isGrouped, limit, offset } = this.model.root;\n            return {\n                offset: offset,\n                limit: limit,\n                total: count,\n                onUpdate: async ({ offset, limit }, hasNavigated) => {\n                    if (this.editedRecord) {\n                        if (!(await this.editedRecord.save())) {\n                            return;\n                        }\n                    }\n                    await this.model.root.load({ limit, offset });\n                    if (hasNavigated) {\n                        this.onPageChangeScroll();\n                    }\n                },\n                updateTotal:\n                    !isGrouped && hasLimitedCount ? () => this.model.root.fetchCount() : undefined,\n            };\n        });\n\n        useEffect(\n            () => {\n                this.onSelectionChanged();\n            },\n            () => [this.model.root.selection.length, this.model.root.isDomainSelected]\n        );\n        this.searchBarToggler = useSearchBarToggler();\n        this.firstLoad = true;\n        onWillPatch(() => {\n            this.firstLoad = false;\n        });\n        this.exportRecords = useExportRecords(this.env, this.props.context, () =>\n            this.getExportableFields()\n        );\n        this.deleteRecordsWithConfirmation = useDeleteRecords(this.model);\n    }\n\n    get modelParams() {\n        const { rawExpand } = this.archInfo;\n        const { activeFields, fields } = extractFieldsFromArchInfo(\n            this.archInfo,\n            this.props.fields\n        );\n        const groupByInfo = {};\n        for (const fieldName in this.archInfo.groupBy.fields) {\n            const fieldNodes = this.archInfo.groupBy.fields[fieldName].fieldNodes;\n            const fields = this.archInfo.groupBy.fields[fieldName].fields;\n            groupByInfo[fieldName] = extractFieldsFromArchInfo({ fieldNodes }, fields);\n        }\n\n        const modelConfig = this.props.state?.modelState?.config || {\n            resModel: this.props.resModel,\n            fields,\n            activeFields,\n            openGroupsByDefault: rawExpand ? evaluateExpr(rawExpand, this.props.context) : false,\n        };\n\n        return {\n            config: modelConfig,\n            state: this.props.state?.modelState,\n            groupByInfo,\n            limit: this.archInfo.limit || this.props.limit,\n            countLimit: this.archInfo.countLimit,\n            defaultOrderBy: this.archInfo.defaultOrder,\n            groupsLimit: this.archInfo.groupsLimit,\n            multiEdit: !this.props.readonly && this.archInfo.multiEdit,\n            activeIdsLimit: session.active_ids_limit,\n            hooks: {\n                onRecordSaved: this.onRecordSaved.bind(this),\n                onWillSaveRecord: this.onWillSaveRecord.bind(this),\n                onWillSaveMulti: this.onWillSaveMulti.bind(this),\n                onAskMultiSaveConfirmation: this.onAskMultiSaveConfirmation.bind(this),\n                onWillSetInvalidField: this.onWillSetInvalidField.bind(this),\n            },\n        };\n    }\n\n    get modelOptions() {\n        return {\n            lazy:\n                !this.env.config.isReloadingController &&\n                !this.env.inDialog &&\n                !!this.props.display.controlPanel,\n        };\n    }\n\n    get actionMenuProps() {\n        return {\n            getActiveIds: () => this.model.root.selection.map((r) => r.resId),\n            context: this.model.root.context,\n            domain: this.props.domain,\n            items: this.actionMenuItems,\n            isDomainSelected: this.model.root.isDomainSelected,\n            resModel: this.model.root.resModel,\n            onActionExecuted: ({ noReload } = {}) => {\n                if (!noReload) {\n                    return this.model.load();\n                }\n            },\n        };\n    }\n\n    get archiveDialogProps() {\n        return {};\n    }\n\n    get deleteConfirmationDialogProps() {\n        return {};\n    }\n\n    getExportableFields() {\n        return unique(\n            this.props.archInfo.columns\n                .filter((col) => col.type === \"field\")\n                .filter((col) => !col.optional || this.optionalActiveFields[col.name])\n                .filter((col) => !evaluateBooleanExpr(col.column_invisible, this.props.context))\n                .map((col) => this.props.fields[col.name])\n                .filter((field) => field.exportable !== false)\n                .filter((field) => field.type !== \"properties\")\n        );\n    }\n\n    async beforeLeave(ev) {\n        return this.model.root.leaveEditMode();\n    }\n\n    async beforeUnload(ev) {\n        if (this.editedRecord) {\n            const isValid = await this.editedRecord.urgentSave();\n            if (!isValid) {\n                ev.preventDefault();\n                ev.returnValue = \"Unsaved changes\";\n            }\n        }\n    }\n\n    onDeleteSelectedRecords() {\n        this.deleteRecordsWithConfirmation(this.deleteConfirmationDialogProps);\n    }\n\n    /**\n     * onRecordSaved is a callBack that will be executed after the save\n     * if it was done. It will therefore not be executed if the record\n     * is invalid or if a server error is thrown.\n     * @param {Record} record\n     */\n    async onRecordSaved(record) {}\n\n    async onSelectionChanged() {\n        if (this.props.onSelectionChanged) {\n            const resIds = await this.model.root.getResIds(true);\n            this.props.onSelectionChanged(resIds);\n        }\n    }\n\n    /**\n     * onWillSaveRecord is a callBack that will be executed before the\n     * record save if the record is valid if the record is valid.\n     * If it returns false, it will prevent the save.\n     * @param {Record} record\n     */\n    async onWillSaveRecord(record) {}\n\n    async createRecord({ group } = {}) {\n        if (!this.model.isReady && !this.model.config.groupBy.length && this.editable) {\n            // If the view isn't grouped and the list is editable, a new record row will be added,\n            // in edition. In this situation, we must wait for the model to be ready.\n            await this.model.whenReady;\n        }\n        const list = (group && group.list) || this.model.root;\n        if (this.editable && !list.isGrouped) {\n            if (!(list instanceof DynamicRecordList)) {\n                throw new Error(\"List should be a DynamicRecordList\");\n            }\n            await list.leaveEditMode();\n            if (!list.editedRecord) {\n                await (group || list).addNewRecord(this.editable === \"top\");\n            }\n            this.render();\n        } else {\n            await this.props.createRecord();\n        }\n    }\n\n    async openRecord(record, { force, newWindow } = { force: false }) {\n        const dirty = await record.isDirty();\n        if (dirty) {\n            await record.save();\n        }\n        if (this.props.allowOpenAction && this.archInfo.openAction) {\n            this.actionService.doActionButton(\n                {\n                    name: this.archInfo.openAction.action,\n                    type: this.archInfo.openAction.type,\n                    resModel: record.resModel,\n                    resId: record.resId,\n                    resIds: record.resIds,\n                    context: record.context,\n                    onClose: async () => {\n                        await record.model.root.load();\n                    },\n                },\n                {\n                    newWindow,\n                }\n            );\n        } else {\n            const activeIds = this.model.root.records.map((datapoint) => datapoint.resId);\n            this.props.selectRecord(record.resId, { activeIds, force, newWindow });\n        }\n    }\n\n    async onClickCreate() {\n        return executeButtonCallback(this.rootRef.el, () => this.createRecord());\n    }\n\n    async onClickDiscard() {\n        return executeButtonCallback(this.rootRef.el, () =>\n            this.model.root.leaveEditMode({ discard: true })\n        );\n    }\n\n    async onClickSave() {\n        return executeButtonCallback(this.rootRef.el, async () => {\n            const saved = await this.editedRecord.save();\n            if (saved) {\n                await this.model.root.leaveEditMode();\n            }\n        });\n    }\n\n    onMouseDownDiscard(mouseDownEvent) {\n        this.hasMousedownDiscard = true;\n        document.addEventListener(\n            \"mouseup\",\n            (mouseUpEvent) => {\n                this.hasMousedownDiscard = false;\n                if (mouseUpEvent.target !== mouseDownEvent.target) {\n                    if (this.nextActionAfterMouseup) {\n                        this.nextActionAfterMouseup();\n                    }\n                }\n                this.nextActionAfterMouseup = null;\n            },\n            { capture: true, once: true }\n        );\n    }\n\n    onPageChangeScroll() {\n        if (this.rootRef && this.rootRef.el) {\n            if (this.env.isSmall) {\n                this.rootRef.el.scrollTop = 0;\n            } else {\n                this.rootRef.el.querySelector(\".o_content .o_list_renderer\").scrollTop = 0;\n            }\n        }\n    }\n\n    getStaticActionMenuItems() {\n        return {\n            export: {\n                isAvailable: () => this.isExportEnable,\n                sequence: 10,\n                icon: \"fa fa-upload\",\n                description: _t(\"Export\"),\n                callback: () => this.exportRecords(),\n            },\n            duplicate: {\n                isAvailable: () => this.activeActions.duplicate,\n                sequence: 30,\n                icon: \"fa fa-clone\",\n                description: _t(\"Duplicate\"),\n                callback: () => this.model.root.duplicateRecords(),\n            },\n            archive: {\n                isAvailable: () => this.archiveEnabled,\n                sequence: 40,\n                icon: \"oi oi-archive\",\n                description: _t(\"Archive\"),\n                callback: () =>\n                    this.model.root.toggleArchiveWithConfirmation(true, this.archiveDialogProps),\n            },\n            unarchive: {\n                isAvailable: () => this.archiveEnabled,\n                sequence: 45,\n                icon: \"oi oi-unarchive\",\n                description: _t(\"Unarchive\"),\n                callback: () => this.model.root.toggleArchiveWithConfirmation(false),\n            },\n            delete: {\n                isAvailable: () => this.activeActions.delete,\n                sequence: 50,\n                icon: \"fa fa-trash-o\",\n                description: _t(\"Delete\"),\n                class: \"text-danger\",\n                callback: () => this.onDeleteSelectedRecords(),\n            },\n        };\n    }\n\n    get actionMenuItems() {\n        const { actionMenus } = this.props.info;\n        const staticActionItems = Object.entries(this.getStaticActionMenuItems())\n            .filter(([key, item]) => item.isAvailable === undefined || item.isAvailable())\n            .sort(([k1, item1], [k2, item2]) => (item1.sequence || 0) - (item2.sequence || 0))\n            .map(([key, item]) =>\n                Object.assign(\n                    { key, groupNumber: STATIC_ACTIONS_GROUP_NUMBER },\n                    omit(item, \"isAvailable\")\n                )\n            );\n\n        return {\n            action: [...staticActionItems, ...(actionMenus?.action || [])],\n            print: actionMenus?.print,\n        };\n    }\n\n    get hasSelectedRecords() {\n        return this.model.root.selection.length || this.isDomainSelected;\n    }\n\n    get isDomainSelected() {\n        return this.model.root.isDomainSelected;\n    }\n\n    evalViewModifier(modifier) {\n        return evaluateBooleanExpr(modifier, this.model.root.evalContext);\n    }\n\n    get className() {\n        return this.props.className;\n    }\n\n    get display() {\n        const { controlPanel } = this.props.display;\n        if (!controlPanel) {\n            return this.props.display;\n        }\n        return {\n            ...this.props.display,\n            controlPanel: {\n                ...controlPanel,\n                layoutActions: !this.hasSelectedRecords,\n            },\n        };\n    }\n\n    discardSelection() {\n        this.model.root.records.forEach((record) => {\n            record.toggleSelection(false);\n        });\n    }\n\n    async beforeExecuteActionButton(clickParams) {\n        if (clickParams.special !== \"cancel\" && this.editedRecord) {\n            return this.editedRecord.save();\n        }\n    }\n\n    async afterExecuteActionButton(clickParams) {}\n\n    onAskMultiSaveConfirmation(changes, validSelectedRecords) {\n        if (this.model.root.selection.length > 1 && validSelectedRecords.length > 0) {\n            const record = validSelectedRecords[0];\n            const { isDomainSelected, selection } = this.model.root;\n            return new Promise((resolve) => {\n                const dialogProps = {\n                    confirm: () => resolve(true),\n                    cancel: () => resolve(false),\n                    isDomainSelected,\n                    fields: Object.keys(changes).map((fieldName) => {\n                        const fieldNode = Object.values(this.archInfo.fieldNodes).find(\n                            (fieldNode) => fieldNode.name === fieldName\n                        );\n                        const label = fieldNode && fieldNode.string;\n                        return {\n                            name: fieldName,\n                            label: label || record.fields[fieldName].string,\n                            fieldNode,\n                            widget: fieldNode && fieldNode.widget,\n                        };\n                    }),\n                    changes,\n                    nbRecords: selection.length,\n                    nbValidRecords: validSelectedRecords.length,\n                    record,\n                };\n\n                const focusedCellBeforeDialog = document.activeElement.closest(\".o_data_cell\");\n                this.dialogService.add(ListConfirmationDialog, dialogProps, {\n                    onClose: () => {\n                        if (focusedCellBeforeDialog) {\n                            focusedCellBeforeDialog.focus();\n                        }\n                        resolve(false);\n                    },\n                });\n            });\n        }\n        return true;\n    }\n\n    onWillSaveMulti(editedRecord, changes) {\n        if (this.hasMousedownDiscard) {\n            this.nextActionAfterMouseup = () => this.model.root.multiSave(editedRecord, changes);\n            return false;\n        }\n        return true;\n    }\n\n    onWillSetInvalidField(record, fieldName) {\n        if (this.hasMousedownDiscard) {\n            this.nextActionAfterMouseup = () => record.setInvalidField(fieldName);\n            return false;\n        }\n        return true;\n    }\n}\n", "import { 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 { getActiveHotkey } from \"@web/core/hotkeys/hotkey_service\";\nimport { localization } from \"@web/core/l10n/localization\";\nimport { Pager } from \"@web/core/pager/pager\";\nimport { evaluateBooleanExpr } from \"@web/core/py_js/py\";\nimport { registry } from \"@web/core/registry\";\nimport { useAutofocus, useBus, useService } from \"@web/core/utils/hooks\";\nimport { useSortable } from \"@web/core/utils/sortable_owl\";\nimport { getTabableElements } from \"@web/core/utils/ui\";\nimport { AGGREGATABLE_FIELD_TYPES, combineModifiers } from \"@web/model/relational_model/utils\";\nimport { Field, getPropertyFieldInfo } from \"@web/views/fields/field\";\nimport { getTooltipInfo } from \"@web/views/fields/field_tooltip\";\nimport {\n    computeAggregatedValue,\n    getClassNameFromDecoration,\n    getFormattedValue,\n} from \"@web/views/utils\";\nimport { ViewButton } from \"@web/views/view_button/view_button\";\nimport { useBounceButton } from \"@web/views/view_hook\";\nimport { Widget } from \"@web/views/widgets/widget\";\nimport { useMagicColumnWidths } from \"./column_width_hook\";\n\nimport {\n    Component,\n    onMounted,\n    onPatched,\n    onWillDestroy,\n    onWillPatch,\n    onWillRender,\n    onWillStart,\n    status,\n    useExternalListener,\n    useRef,\n    useState,\n} from \"@odoo/owl\";\nimport { getCurrencyRates } from \"@web/core/currency\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { usePopover } from \"@web/core/popover/popover_hook\";\nimport { user } from \"@web/core/user\";\nimport { exprToBoolean } from \"@web/core/utils/strings\";\nimport { MOVABLE_RECORD_TYPES } from \"@web/model/relational_model/dynamic_group_list\";\nimport { ActionHelper } from \"@web/views/action_helper\";\nimport { GroupConfigMenu } from \"@web/views/view_components/group_config_menu\";\nimport { MultiCurrencyPopover } from \"@web/views/view_components/multi_currency_popover\";\n\n/**\n * @typedef {import('@web/model/relational_model/dynamic_list').DynamicList} DynamicList\n * @typedef {import('@web/model/relational_model/group').Group} Group\n * @typedef {import('@web/model/relational_model/record').Record} RelationalRecord\n * @typedef {import('@web/model/relational_model/relational_model').RelationalModel} RelationalModel\n * @typedef {import('@web/model/relational_model/static_list').StaticList} StaticList\n * @typedef {import(\"../view\").ViewProps} ViewProps\n *\n * @typedef {{\n *  name: string;\n *  type: string;\n *  attrs: Record<string, string>;\n *  [key: string]: unknown;\n * }} Column\n *\n * @typedef {\"up\" | \"down\" | \"left\" | \"right\"} Direction\n *\n * @typedef {ViewProps & {\n *  list: DynamicList | StaticList;\n * }} ListRendererProps\n */\n\nconst formatters = registry.category(\"formatters\");\n\nconst DEFAULT_GROUP_PAGER_COLSPAN = 1;\n\nconst FIELD_CLASSES = {\n    char: \"o_list_char\",\n    float: \"o_list_number\",\n    integer: \"o_list_number\",\n    monetary: \"o_list_number\",\n    text: \"o_list_text\",\n    many2one: \"o_list_many2one\",\n};\n\n/**\n * @param {HTMLElement} parent\n */\nfunction containsActiveElement(parent) {\n    const { activeElement } = document;\n    return parent !== activeElement && parent.contains(activeElement);\n}\n\n/**\n * @param {HTMLTableCellElement} cell\n * @param {number} index\n */\nfunction getElementToFocus(cell, index) {\n    return getTabableElements(cell).at(index) || cell;\n}\n\n/** @extends Component<ListRendererProps, OdooEnv> */\nexport class ListRenderer extends Component {\n    static template = \"web.ListRenderer\";\n    static rowsTemplate = \"web.ListRenderer.Rows\";\n    static recordRowTemplate = \"web.ListRenderer.RecordRow\";\n    static groupRowTemplate = \"web.ListRenderer.GroupRow\";\n    static useMagicColumnWidths = true;\n    static LONG_TOUCH_THRESHOLD = 400;\n    static components = {\n        DropdownItem,\n        Field,\n        ViewButton,\n        CheckBox,\n        Dropdown,\n        Pager,\n        Widget,\n        ActionHelper,\n        GroupConfigMenu,\n    };\n    static defaultProps = { allowSelectors: false, cycleOnTab: true };\n\n    static props = [\n        \"activeActions?\",\n        \"list\",\n        \"archInfo\",\n        \"openRecord\",\n        \"onAdd?\",\n        \"cycleOnTab?\",\n        \"allowSelectors?\",\n        \"editable?\",\n        \"onOpenFormView?\",\n        \"hasOpenFormViewButton?\",\n        \"noContentHelp?\",\n        \"nestedKeyOptionalFieldsData?\",\n        \"optionalActiveFields?\",\n        \"readonly?\",\n    ];\n\n    setup() {\n        this.uiService = useService(\"ui\");\n        this.notificationService = useService(\"notification\");\n        this.orm = useService(\"orm\");\n        const key = this.createViewKey();\n        this.keyOptionalFields = `optional_fields,${key}`;\n        this.keyDebugOpenView = `debug_open_view,${key}`;\n        this.cellClassByColumn = {};\n        this.groupByButtons = this.props.archInfo.groupBy.buttons;\n        useExternalListener(document, \"click\", this.onGlobalClick.bind(this));\n        this.tableRef = useRef(\"table\");\n\n        this.longTouchTimer = null;\n        this.touchStartMs = 0;\n\n        /**\n         * When resizing columns, it's possible that the pointer is not above the resize\n         * handle (by some few pixel difference). During this scenario, click event\n         * will be triggered on the column title which will reorder the column.\n         * Column resize that triggers a reorder is not a good UX and we prevent this\n         * using the following state variables: `resizing` and `preventReorder` which\n         * are set during the column's click (onClickSortColumn), pointerup\n         * (onColumnTitleMouseUp) and onStartResize events.\n         */\n        this.preventReorder = false;\n\n        this.controls = this.props.archInfo.controls.length\n            ? this.props.archInfo.controls\n            : [{ type: \"create\", string: _t(\"Add a line\") }];\n        this.deleteControl = this.controls.find((control) => control.type === \"delete\") || {};\n\n        this.cellToFocus = null;\n        this.activeRowId = null;\n        onMounted(async () => {\n            // Due to the way elements are mounted in the DOM by Owl (bottom-to-top),\n            // we need to wait the next micro task tick to set the activeElement.\n            await Promise.resolve();\n            this.activeElement = this.uiService.activeElement;\n        });\n        onWillPatch(() => {\n            const activeRow = document.activeElement.closest(\".o_data_row.o_selected_row\");\n            this.activeRowId = activeRow ? activeRow.dataset.id : null;\n        });\n        this.optionalActiveFields = this.props.optionalActiveFields || {};\n        /** @type {Column[]} */\n        this.allColumns = [];\n        /** @type {Column[]} */\n        this.columns = [];\n        this.editedRecord = null;\n        onWillRender(() => {\n            this.editedRecord = this.props.list.editedRecord;\n            this.allColumns = this.processAllColumn(this.props.archInfo.columns, this.props.list);\n            Object.assign(this.optionalActiveFields, this.computeOptionalActiveFields());\n            this.debugOpenView = exprToBoolean(browser.localStorage.getItem(this.keyDebugOpenView));\n            this.columns = this.getActiveColumns();\n            this.withHandleColumn = this.columns.some((col) => col.widget === \"handle\");\n            this.aggregates = this.computeAggregates();\n        });\n        this.multiCurrencyPopover = usePopover(MultiCurrencyPopover, {\n            position: \"right\",\n        });\n        this.state = useState({ groupInput: false, currencyRates: null });\n        onWillStart(async () => {\n            const needsCurrencyRates = this.props.archInfo.columns.some((column) => {\n                if (column.type !== \"field\") {\n                    return false;\n                }\n                const field = this.props.list.fields[column.name];\n                if (field.type !== \"monetary\" && column.widget !== \"monetary\") {\n                    return false;\n                }\n                const currencyField = this.getCurrencyField(column);\n                if (!(currencyField in this.props.list.activeFields)) {\n                    return false;\n                }\n                return [\"sum\", \"avg\", \"max\", \"min\"].some((agg) => agg in column.attrs);\n            });\n            if (needsCurrencyRates) {\n                this.state.currencyRates = await getCurrencyRates();\n            }\n        });\n        this.groupInputRef = useRef(\"groupInput\");\n        useAutofocus({ refName: \"groupInput\" });\n        let dataRowId;\n        let dataGroupId;\n        this.rootRef = useRef(\"root\");\n        this.resequencePromise = Promise.resolve();\n        useSortable({\n            enable: () => this.canResequenceRows,\n            // Params\n            ref: this.rootRef,\n            elements: \".o_row_draggable\",\n            handle: \".o_handle_cell\",\n            cursor: \"grabbing\",\n            placeholderClasses: [\"d-table-row\"],\n            // Hooks\n            onDragStart: (params) => {\n                const { element } = params;\n                dataRowId = element.dataset.id;\n                dataGroupId = this.props.list.isGrouped && element.dataset.groupId;\n                return this.sortStart(params);\n            },\n            onDragEnd: (params) => this.sortStop(params),\n            onDrop: (params) => this.sortDrop(dataRowId, dataGroupId, params),\n        });\n\n        if (this.env.searchModel) {\n            useBus(this.env.searchModel, \"focus-view\", () => {\n                if (this.props.list.model.useSampleModel) {\n                    return;\n                }\n\n                const nextTh = this.tableRef.el.querySelector(\"thead th\");\n                const toFocus = getElementToFocus(nextTh);\n                this.focus(toFocus);\n                this.tableRef.el.querySelector(\"tbody\").classList.add(\"o_keyboard_navigation\");\n            });\n        }\n\n        useBus(this.props.list.model.bus, \"FIELD_IS_DIRTY\", (ev) => (this.lastIsDirty = ev.detail));\n\n        useBounceButton(this.rootRef, () => this.showNoContentHelper);\n\n        let isSmall = this.uiService.isSmall;\n        useBus(this.uiService.bus, \"resize\", () => {\n            if (isSmall !== this.uiService.isSmall) {\n                isSmall = this.uiService.isSmall;\n                this.render();\n            }\n        });\n\n        this.columnWidths = useMagicColumnWidths(this.tableRef, () => ({\n            columns: this.columns,\n            isEmpty: !this.props.list.records.length || this.props.list.model.useSampleModel,\n            hasSelectors: this.hasSelectors,\n            hasOpenFormViewColumn: this.hasOpenFormViewColumn,\n            hasActionsColumn: this.hasActionsColumn,\n        }));\n\n        useExternalListener(window, \"keydown\", (ev) => {\n            this.shiftKeyMode = ev.shiftKey;\n        });\n        useExternalListener(window, \"keyup\", (ev) => {\n            this.shiftKeyMode = ev.shiftKey;\n            const hotkey = getActiveHotkey(ev);\n            if (hotkey === \"shift\") {\n                this.shiftKeyedRecord = undefined;\n            }\n        });\n        useExternalListener(window, \"blur\", (ev) => {\n            this.shiftKeyMode = false;\n        });\n        onPatched(async () => {\n            // HACK: we need to wait for the next tick to be sure that the Field components are patched.\n            // OWL don't wait the patch for the children components if the children trigger a patch by himself.\n            await Promise.resolve();\n            if (status(this) === \"destroyed\") {\n                return;\n            }\n            if (this.activeElement !== this.uiService.activeElement) {\n                return;\n            }\n            if (this.editedRecord && this.activeRowId !== this.editedRecord.id) {\n                if (this.cellToFocus && this.cellToFocus.record === this.editedRecord) {\n                    const column = this.cellToFocus.column;\n                    const forward = this.cellToFocus.forward;\n                    this.focusCell(column, forward);\n                } else {\n                    const column = this.lastEditedCell?.column || this.columns[0];\n                    if (column.widget !== \"daterange\" || !this.editedRecord.data[column.name]) {\n                        this.focusCell(column);\n                    }\n                }\n            }\n            this.cellToFocus = null;\n            this.lastEditedCell = null;\n        });\n        this.isRTL = localization.direction === \"rtl\";\n        this.dialogClose = [];\n        onWillDestroy(() => {\n            this.dialogClose.forEach((close) => close());\n        });\n    }\n\n    displaySaveNotification() {\n        this.notificationService.add(_t(\"Please save your changes first\"), {\n            type: \"danger\",\n        });\n    }\n\n    getActiveColumns() {\n        return this.allColumns.filter((col) => {\n            if (col.optional && !this.optionalActiveFields[col.name]) {\n                return false;\n            }\n            if (this.evalColumnInvisible(col.column_invisible)) {\n                return false;\n            }\n            return true;\n        });\n    }\n\n    get hasSelectors() {\n        return this.props.allowSelectors && !this.env.isSmall;\n    }\n\n    get hasOpenFormViewColumn() {\n        return this.props.hasOpenFormViewButton || this.debugOpenView;\n    }\n\n    get hasOptionalOpenFormViewColumn() {\n        return this.props.editable && this.env.debug && !this.props.hasOpenFormViewButton;\n    }\n\n    get hasActionsColumn() {\n        return !!(\n            this.displayOptionalFields ||\n            this.activeActions.onDelete ||\n            this.hasOptionalOpenFormViewColumn ||\n            // spare some space to display the cog icon in group headers\n            this.props.list.isGrouped\n        );\n    }\n\n    // deprecated, remove in master\n    get hasMonetary() {\n        return this.props.archInfo.columns.some((column) => {\n            if (column.type !== \"field\") {\n                return false;\n            }\n            const field = this.props.list.fields[column.name];\n            return (\n                (field.type === \"monetary\" && field.currency_field) || column.widget === \"monetary\"\n            );\n        });\n    }\n\n    add(params) {\n        if (this.canCreate) {\n            this.props.onAdd(params);\n        }\n    }\n\n    /**\n     * @param {Group} group\n     */\n    async addInGroup(group) {\n        const left = await this.props.list.leaveEditMode({ canAbandon: false });\n        if (left) {\n            group.addNewRecord({}, this.props.editable === \"top\");\n        }\n    }\n\n    /**\n     * @param {Column[]} allColumns\n     * @param {DynamicList | StaticList} list\n     */\n    processAllColumn(allColumns, list) {\n        return allColumns.flatMap((column) => {\n            if (column.type === \"field\" && list.fields[column.name].type === \"properties\") {\n                return this.getPropertyFieldColumns(column, list);\n            } else {\n                return [column];\n            }\n        });\n    }\n\n    /**\n     * @param {Column} column\n     * @param {DynamicList | StaticList} list\n     */\n    getPropertyFieldColumns(column, list) {\n        return Object.values(list.fields)\n            .filter(\n                (field) =>\n                    list.activeFields[field.name] &&\n                    field.relatedPropertyField &&\n                    field.relatedPropertyField.name === column.name &&\n                    field.type !== \"separator\"\n            )\n            .map((propertyField) => {\n                const activeField = list.activeFields[propertyField.name];\n                return {\n                    ...getPropertyFieldInfo(propertyField),\n                    relatedPropertyField: activeField.relatedPropertyField,\n                    id: `${column.id}_${propertyField.name}`,\n                    column_invisible: combineModifiers(\n                        propertyField.column_invisible,\n                        column.column_invisible,\n                        \"OR\"\n                    ),\n                    classNames: column.classNames,\n                    optional: \"hide\",\n                    type: \"field\",\n                    hasLabel: true,\n                    label: propertyField.string,\n                    attrs: [\"integer\", \"float\"].includes(propertyField.type)\n                        ? { sum: propertyField.string }\n                        : {},\n                };\n            });\n    }\n\n    /**\n     * @param {RelationalRecord} record\n     * @param {Column} column\n     */\n    getFieldProps(record, column) {\n        return {\n            readonly:\n                this.props.readonly ||\n                this.isCellReadonly(column, record) ||\n                this.isRecordReadonly(record) ||\n                (column.widget === \"handle\" && !this.canResequenceRows),\n        };\n    }\n\n    get activeActions() {\n        return this.props.activeActions || {};\n    }\n\n    get canCreateGroup() {\n        const { archInfo, list, readonly } = this.props;\n        const { activeActions, defaultGroupBy } = archInfo;\n        return (\n            !readonly &&\n            activeActions.createGroup &&\n            list.groupByField.type === \"many2one\" &&\n            list.groupByField.name === defaultGroupBy?.[0]\n        );\n    }\n\n    get canResequenceRows() {\n        if (!this.props.list.canResequence() || this.props.readonly) {\n            return false;\n        }\n        const { groupBy, groupByField, handleField, orderBy } = this.props.list;\n        if (groupBy?.length > 1 || (groupByField && !this.isMovableField(groupByField))) {\n            return false;\n        }\n        return !orderBy.length || (orderBy.length && orderBy[0].name === handleField);\n    }\n\n    get fields() {\n        return this.props.list.fields;\n    }\n\n    get nbCols() {\n        let nbCols = this.columns.length;\n        if (this.hasSelectors) {\n            nbCols++;\n        }\n        if (this.hasActionsColumn) {\n            nbCols++;\n        }\n        if (this.hasOpenFormViewColumn) {\n            nbCols++;\n        }\n        return nbCols;\n    }\n\n    /**\n     * @param {Column} column\n     * @param {RelationalRecord} record\n     */\n    canUseFormatter(column, record) {\n        if (column.widget) {\n            return false;\n        }\n        if (record.isInEdition && (record.model.multiEdit || this.isInlineEditable(record))) {\n            // in a x2many non editable list, a record is in edition when it is opened in a dialog,\n            // but in the list we want it to still be displayed in readonly.\n            return false;\n        }\n        return true;\n    }\n\n    /**\n     * @param {RelationalRecord} record\n     */\n    isRecordReadonly(record) {\n        if (record.isNew) {\n            return false;\n        }\n        if (this.props.activeActions?.edit === false) {\n            return true;\n        }\n        if (record.isInEdition && !this.isInlineEditable(record) && !record.model.multiEdit) {\n            // in a x2many non editable list, a record is in edition when it is opened in a dialog,\n            // but in the list we want it to still be displayed in readonly.\n            return true;\n        }\n        return false;\n    }\n\n    isMovableField(field) {\n        return MOVABLE_RECORD_TYPES.includes(field.type);\n    }\n\n    focusCell(column, forward = true) {\n        const index = column\n            ? this.columns.findIndex((col) => col.id === column.id && col.name === column.name)\n            : -1;\n        let columns;\n        if (index === -1 && !forward) {\n            columns = this.columns.slice(0).reverse();\n        } else {\n            columns = [\n                ...this.columns.slice(index, this.columns.length),\n                ...this.columns.slice(0, index),\n            ];\n        }\n        for (const column of columns) {\n            if (column.type !== \"field\") {\n                continue;\n            }\n            // in findNextFocusableOnRow test is done by using classList\n            // refactor\n            if (!this.isCellReadonly(column, this.editedRecord)) {\n                const cell = this.tableRef.el.querySelector(\n                    `.o_selected_row td[name='${column.name}']`\n                );\n                if (cell) {\n                    const toFocus = getElementToFocus(cell);\n                    if (cell !== toFocus) {\n                        this.focus(toFocus);\n                        this.lastEditedCell = { column, record: this.editedRecord };\n                        break;\n                    }\n                }\n            }\n        }\n    }\n\n    /**\n     * @param {HTMLOrSVGElement} el\n     */\n    focus(el) {\n        if (!el) {\n            return;\n        }\n        el.focus();\n        if (\n            [\"text\", \"search\", \"url\", \"tel\", \"password\", \"textarea\"].includes(el.type) &&\n            el.selectionStart === el.selectionEnd\n        ) {\n            el.selectionStart = 0;\n            el.selectionEnd = el.value.length;\n        }\n    }\n\n    editGroupRecord(group) {\n        const { resId, resModel } = group.record;\n        this.env.services.action.doAction({\n            context: {\n                create: false,\n            },\n            res_model: resModel,\n            res_id: resId,\n            type: \"ir.actions.act_window\",\n            views: [[false, \"form\"]],\n        });\n    }\n\n    createViewKey() {\n        let keyParts = {\n            fields: this.props.list.fieldNames, // FIXME: use something else?\n            model: this.props.list.resModel,\n            viewMode: \"list\",\n            viewId: this.env.config.viewId,\n        };\n\n        if (this.props.nestedKeyOptionalFieldsData) {\n            keyParts = Object.assign(keyParts, {\n                model: this.props.nestedKeyOptionalFieldsData.model,\n                viewMode: this.props.nestedKeyOptionalFieldsData.viewMode,\n                relationalField: this.props.nestedKeyOptionalFieldsData.field,\n                subViewType: \"list\",\n            });\n        }\n\n        const parts = [\"model\", \"viewMode\", \"viewId\", \"relationalField\", \"subViewType\"];\n        const viewIdentifier = [];\n        parts.forEach((partName) => {\n            if (partName in keyParts) {\n                viewIdentifier.push(keyParts[partName]);\n            }\n        });\n        keyParts.fields\n            .sort((left, right) => (left < right ? -1 : 1))\n            .forEach((fieldName) => viewIdentifier.push(fieldName));\n        return viewIdentifier.join(\",\");\n    }\n\n    get optionalFieldGroups() {\n        const propertyGroups = {};\n        const optionalFields = [];\n        const optionalColumns = this.allColumns.filter(\n            (col) => col.optional && !this.evalColumnInvisible(col.column_invisible)\n        );\n        for (const col of optionalColumns) {\n            const optionalField = {\n                label: col.label,\n                name: col.name,\n                value: this.optionalActiveFields[col.name],\n            };\n            if (!col.relatedPropertyField) {\n                optionalFields.push(optionalField);\n            } else {\n                const { displayName, id } = col.relatedPropertyField;\n                if (propertyGroups[id]) {\n                    propertyGroups[id].optionalFields.push(optionalField);\n                } else {\n                    propertyGroups[id] = { id, displayName, optionalFields: [optionalField] };\n                }\n            }\n        }\n        if (optionalFields.length) {\n            return [{ optionalFields }, ...Object.values(propertyGroups)];\n        }\n        return Object.values(propertyGroups);\n    }\n\n    get hasOptionalFields() {\n        return this.allColumns.some(\n            (col) => col.optional && !this.evalColumnInvisible(col.column_invisible)\n        );\n    }\n\n    get displayOptionalFields() {\n        return this.hasOptionalFields;\n    }\n\n    nbRecordsInGroup(group) {\n        if (group.isFolded) {\n            return 0;\n        } else if (group.list.isGrouped) {\n            let count = 0;\n            for (const gr of group.list.groups) {\n                count += this.nbRecordsInGroup(gr);\n            }\n            return count;\n        } else {\n            return group.list.records.length;\n        }\n    }\n    get selectAll() {\n        const list = this.props.list;\n        const nbDisplayedRecords = list.records.length;\n        if (list.isDomainSelected) {\n            return true;\n        } else {\n            return nbDisplayedRecords > 0 && list.selection.length === nbDisplayedRecords;\n        }\n    }\n\n    computeAggregates() {\n        let values;\n        if (this.props.list.selection.length) {\n            values = this.props.list.selection.map((r) => r.data);\n        } else if (this.props.list.isGrouped) {\n            values = this.props.list.groups.map((g) => g.aggregates);\n        } else {\n            values = this.props.list.records.map((r) => r.data);\n        }\n        const aggregates = {};\n        for (const column of this.columns) {\n            if (column.type !== \"field\") {\n                continue;\n            }\n            const fieldName = column.name;\n            if (fieldName in this.optionalActiveFields && !this.optionalActiveFields[fieldName]) {\n                continue;\n            }\n            const field = this.fields[fieldName];\n            const fieldValues = values.map((v) => v[fieldName]).filter((v) => v || v === 0);\n            if (!fieldValues.length) {\n                continue;\n            }\n            const type = field.type;\n            if (!AGGREGATABLE_FIELD_TYPES.includes(type)) {\n                continue;\n            }\n            const { attrs, widget } = column;\n            const func =\n                (attrs.sum && \"sum\") ||\n                (attrs.avg && \"avg\") ||\n                (attrs.max && \"max\") ||\n                (attrs.min && \"min\");\n            let currencyId;\n            let multiCurrency = false;\n            if (type === \"monetary\" || widget === \"monetary\") {\n                const currencyField = this.getCurrencyField(column);\n                if (currencyField in this.props.list.activeFields) {\n                    if (this.props.list.isGrouped && !this.props.list.selection.length) {\n                        currencyId = values.find((v) => v[currencyField]?.length)?.[\n                            currencyField\n                        ][0];\n                    } else {\n                        currencyId = values[0][currencyField] && values[0][currencyField].id;\n                    }\n                    if (func) {\n                        const currencies = this.getFieldCurrencies(fieldName);\n                        // in case of multiple currencies, convert values into default currency using conversion rates\n                        if (currencies.size > 1) {\n                            multiCurrency = true;\n                            currencyId = user.activeCompany.currency_id;\n                            for (const i in values) {\n                                let currency = values[i][currencyField].id;\n                                if (\n                                    this.props.list.isGrouped &&\n                                    !this.props.list.selection.length\n                                ) {\n                                    currency =\n                                        values[i][currencyField].length > 1\n                                            ? currencyId\n                                            : values[i][currencyField][0];\n                                }\n                                if (currency !== currencyId) {\n                                    fieldValues[i] *= this.state.currencyRates[currency];\n                                }\n                            }\n                        }\n                    }\n                }\n            }\n            if (func) {\n                const aggregatedValue = computeAggregatedValue(fieldValues, func);\n                const formatter = formatters.get(widget, false) || formatters.get(type, false);\n                const formatOptions = {\n                    digits: attrs.digits ? JSON.parse(attrs.digits) : undefined,\n                    escape: true,\n                };\n                if (currencyId) {\n                    formatOptions.currencyId = currencyId;\n                }\n                aggregates[fieldName] = {\n                    help: multiCurrency ? \"\" : attrs[func],\n                    value: formatter ? formatter(aggregatedValue, formatOptions) : aggregatedValue,\n                    multiCurrency,\n                    rawValue: aggregatedValue,\n                };\n            }\n        }\n        return aggregates;\n    }\n\n    getFieldCurrencies(fieldName) {\n        const column = this.columns.find((c) => c.name === fieldName);\n        const currencyField = this.getCurrencyField(column);\n        let values;\n        if (this.props.list.selection.length) {\n            values = this.props.list.selection.map((r) => r.data);\n        } else if (this.props.list.isGrouped) {\n            values = this.props.list.groups.map((g) => g.aggregates);\n        } else {\n            values = this.props.list.records.map((r) => r.data);\n        }\n        if (this.props.list.isGrouped && !this.props.list.selection.length) {\n            return values.reduce((set, value) => {\n                value[currencyField].forEach((c) => {\n                    set.add(c);\n                });\n                return set;\n            }, new Set());\n        }\n        return values.reduce((set, value) => set.add(value[currencyField]?.id), new Set());\n    }\n\n    getCurrencyField(column) {\n        return (\n            column.options.currency_field ||\n            this.fields[column.name].currency_field ||\n            \"currency_id\"\n        );\n    }\n\n    getGroupConfigMenuProps(group) {\n        return {\n            activeActions: this.props.activeActions,\n            configItems: registry.category(\"group_config_items\").getEntries(),\n            deleteGroup: async () => await this.props.list.deleteGroups([group]),\n            dialogClose: this.dialogClose,\n            group,\n            list: this.props.list,\n        };\n    }\n\n    formatGroupAggregate(group, column) {\n        const { widget, attrs } = column;\n        const field = this.props.list.fields[column.name];\n        const aggregateValue = group.aggregates[column.name];\n        if (\n            !(column.name in group.aggregates) ||\n            widget === \"handle\" ||\n            !AGGREGATABLE_FIELD_TYPES.includes(field.type)\n        ) {\n            return {\n                value: \"\",\n            };\n        }\n        const formatter = formatters.get(widget, false) || formatters.get(field.type, false);\n        const formatOptions = {\n            digits: attrs.digits ? JSON.parse(attrs.digits) : field.digits,\n            escape: true,\n        };\n        if (field.type === \"monetary\") {\n            const currencies = group.aggregates[field.currency_field];\n            if (currencies.length > 1 && aggregateValue !== false) {\n                formatOptions.currencyId = user.activeCompany.currency_id;\n                return {\n                    value: formatter ? formatter(aggregateValue, formatOptions) : aggregateValue,\n                    multiCurrency: true,\n                    rawValue: aggregateValue,\n                };\n            }\n            formatOptions.currencyId = currencies[0];\n        }\n        return {\n            value: formatter ? formatter(aggregateValue, formatOptions) : aggregateValue,\n        };\n    }\n\n    getGroupLevel(group) {\n        return this.props.list.groupBy.length - group.list.groupBy.length - 1;\n    }\n\n    getColumnClass(column) {\n        const classNames = [\"align-middle\"];\n        if (this.isSortable(column)) {\n            classNames.push(\"o_column_sortable\", \"position-relative\", \"cursor-pointer\");\n        } else {\n            classNames.push(\"cursor-default\");\n        }\n        const orderBy = this.props.list.orderBy;\n        if (\n            orderBy.length &&\n            column.widget !== \"handle\" &&\n            orderBy[0].name === column.name &&\n            column.hasLabel\n        ) {\n            classNames.push(\"table-active\");\n        }\n        if (this.isNumericColumn(column)) {\n            classNames.push(\"o_list_number_th\");\n        }\n        if (column.type === \"button_group\") {\n            classNames.push(\"o_list_button\");\n        }\n        if (column.widget) {\n            classNames.push(`o_${column.widget}_cell`);\n        }\n\n        return classNames.join(\" \");\n    }\n\n    /**\n     *\n     * @param {RelationalRecord} _record\n     */\n    getColumns(_record) {\n        return this.columns;\n    }\n\n    isNumericColumn(column) {\n        const { type } = this.fields[column.name];\n        return [\"float\", \"integer\", \"monetary\"].includes(type);\n    }\n\n    isSortable(column) {\n        const { hasLabel, name, options } = column;\n        const { sortable } = this.fields[name];\n        return (sortable || options.allow_order) && hasLabel;\n    }\n\n    getSortableIconClass(column) {\n        const { orderBy } = this.props.list;\n        const classNames = this.isSortable(column) ? [\"fa\"] : [\"d-none\"];\n        if (orderBy.length && orderBy[0].name === column.name) {\n            classNames.push(orderBy[0].asc ? \"fa-sort-asc\" : \"fa-sort-desc\");\n        } else {\n            classNames.push(\"fa-sort\", \"opacity-0\", \"opacity-100-hover\");\n        }\n\n        return classNames.join(\" \");\n    }\n\n    /**\n     * Returns the classnames to apply to the row representing the given record.\n     * @param {RelationalRecord} record\n     */\n    getRowClass(record) {\n        /**\n         * Classnames coming from decorations\n         * @type {string[]}\n         */\n        const classNames = this.props.archInfo.decorations\n            .filter((decoration) =>\n                evaluateBooleanExpr(decoration.condition, record.evalContextWithVirtualIds)\n            )\n            .map((decoration) => decoration.class);\n        if (record.selected) {\n            classNames.push(\"table-info\");\n        }\n        // \"o_selected_row\" classname for the potential row in edition\n        if (record.isInEdition) {\n            classNames.push(\"o_selected_row\");\n        }\n        if (record.selected) {\n            classNames.push(\"o_data_row_selected\");\n        }\n        if (this.canResequenceRows) {\n            classNames.push(\"o_row_draggable\");\n        }\n        return classNames.join(\" \");\n    }\n\n    /**\n     * @param {Column} column\n     * @param {RelationalRecord} record\n     */\n    getCellClass(column, record) {\n        if (column.relatedPropertyField && !(column.name in record.data)) {\n            return \"\";\n        }\n\n        if (!this.cellClassByColumn[column.id]) {\n            const classNames = [\"o_data_cell\"];\n            if (column.type === \"button_group\") {\n                classNames.push(\"o_list_button\");\n            } else if (column.type === \"field\") {\n                classNames.push(\"o_field_cell\");\n                if (column.attrs && column.attrs.class && this.canUseFormatter(column, record)) {\n                    classNames.push(column.attrs.class);\n                }\n                const typeClass = FIELD_CLASSES[this.fields[column.name].type];\n                if (typeClass) {\n                    classNames.push(typeClass);\n                }\n                if (column.widget) {\n                    classNames.push(`o_${column.widget}_cell`);\n                }\n            }\n            this.cellClassByColumn[column.id] = classNames;\n        }\n        const classNames = [...this.cellClassByColumn[column.id]];\n        if (column.type === \"field\") {\n            if (evaluateBooleanExpr(column.required, record.evalContextWithVirtualIds)) {\n                classNames.push(\"o_required_modifier\");\n            }\n            if (record.isFieldInvalid(column.name)) {\n                classNames.push(\"o_invalid_cell\");\n            }\n            if (this.isCellReadonly(column, record)) {\n                classNames.push(\"o_readonly_modifier\");\n            }\n            if (this.canUseFormatter(column, record)) {\n                // generate field decorations classNames (only if field-specific decorations\n                // have been defined in an attribute, e.g. decoration-danger=\"other_field = 5\")\n                // only handle the text-decoration.\n                const { decorations } = column;\n                for (const decoName in decorations) {\n                    if (\n                        evaluateBooleanExpr(decorations[decoName], record.evalContextWithVirtualIds)\n                    ) {\n                        classNames.push(getClassNameFromDecoration(decoName));\n                    }\n                }\n            }\n            if (\n                record.isInEdition &&\n                this.editedRecord &&\n                this.isCellReadonly(column, this.editedRecord)\n            ) {\n                classNames.push(\"text-muted\");\n            } else {\n                classNames.push(\"cursor-pointer\");\n            }\n        }\n        return classNames.join(\" \");\n    }\n\n    /**\n     * @param {Column} column\n     * @param {RelationalRecord} record\n     */\n    isCellReadonly(column, record) {\n        return !!(\n            this.isRecordReadonly(record) ||\n            (column.relatedPropertyField && record.selected && record.model.multiEdit) ||\n            evaluateBooleanExpr(column.readonly, record.evalContextWithVirtualIds)\n        );\n    }\n\n    /**\n     * @param {Column} column\n     * @param {RelationalRecord} record\n     */\n    getCellTitle(column, record) {\n        // Because we freeze the column sizes, it may happen that we have to shorten field values.\n        // In order for the user to have access to the complete value in those situations, we put\n        // the value as title of the cells.\n        if ([\"many2one\", \"reference\", \"char\"].includes(this.fields[column.name].type)) {\n            return this.getFormattedValue(column, record);\n        }\n    }\n\n    getFieldClass(column) {\n        return column.attrs && column.attrs.class;\n    }\n\n    /**\n     * @param {Column} column\n     * @param {RelationalRecord} record\n     */\n    getFormattedValue(column, record) {\n        const fieldName = column.name;\n        if (column.options.enable_formatting === false) {\n            const value = record.data[fieldName];\n            return value === false ? \"\" : value;\n        }\n        return getFormattedValue(record, fieldName, column);\n    }\n\n    /**\n     * @param {string} invisible\n     * @param {RelationalRecord} record\n     */\n    evalInvisible(invisible, record) {\n        return evaluateBooleanExpr(invisible, record.evalContextWithVirtualIds);\n    }\n\n    evalColumnInvisible(columnInvisible) {\n        return evaluateBooleanExpr(columnInvisible, this.props.list.evalContext);\n    }\n\n    get canCreate() {\n        return \"link\" in this.activeActions ? this.activeActions.link : this.activeActions.create;\n    }\n\n    get isX2Many() {\n        return this.activeActions.type !== \"view\";\n    }\n\n    get getEmptyRowIds() {\n        let nbEmptyRow = Math.max(0, 4 - this.props.list.records.length);\n        if (nbEmptyRow > 0 && this.displayRowCreates) {\n            nbEmptyRow -= 1;\n        }\n        return Array.from(Array(nbEmptyRow).keys());\n    }\n\n    get displayRowCreates() {\n        return this.isX2Many && this.canCreate;\n    }\n\n    /**\n     * @param {RelationalRecord} record\n     */\n    displayDeleteIcon(record) {\n        return !evaluateBooleanExpr(this.deleteControl.invisible, record.evalContext);\n    }\n\n    // Group headers logic:\n    // if there are aggregates, the first th spans until the first\n    // aggregate column then all cells between aggregates are rendered\n    // a single cell is rendered after the last aggregated column to render the\n    // pager (with adequate colspan)\n    // ex:\n    // TH TH TH TH TH AGG AGG TH AGG AGG TH TH TH\n    // 0  1  2  3  4   5   6   7  8   9  10 11 12\n    // [    TH 5    ][TH][TH][TH][TH][TH][ TH 3 ]\n    // [ group name ][ aggregate cells  ][ pager]\n    // TODO: move this somewhere, compute this only once (same result for each groups actually) ?\n    getFirstAggregateIndex(group) {\n        const aggregates = group ? group.aggregates : this.aggregates;\n        return this.columns.findIndex(\n            (col) =>\n                col.name in aggregates &&\n                col.widget !== \"handle\" &&\n                AGGREGATABLE_FIELD_TYPES.includes(this.fields[col.name].type)\n        );\n    }\n    getLastAggregateIndex(group) {\n        const aggregates = group ? group.aggregates : this.aggregates;\n        const reversedColumns = [...this.columns].reverse(); // reverse is destructive\n        const index = reversedColumns.findIndex(\n            (col) =>\n                col.name in aggregates &&\n                col.widget !== \"handle\" &&\n                AGGREGATABLE_FIELD_TYPES.includes(this.fields[col.name].type)\n        );\n        return index > -1 ? this.columns.length - index - 1 : -1;\n    }\n    getAggregateColumns(group) {\n        const firstIndex = this.getFirstAggregateIndex(group);\n        const lastIndex = this.getLastAggregateIndex(group);\n        return this.columns.slice(firstIndex, lastIndex + 1);\n    }\n    getGroupNameCellColSpan(group) {\n        // if there are aggregates, the first th spans until the first\n        // aggregate column then all cells between aggregates are rendered\n        const firstAggregateIndex = this.getFirstAggregateIndex(group);\n        let colspan;\n        if (firstAggregateIndex > -1) {\n            colspan = firstAggregateIndex;\n        } else {\n            colspan = Math.max(1, this.columns.length - DEFAULT_GROUP_PAGER_COLSPAN);\n        }\n        if (this.hasSelectors) {\n            colspan++;\n        }\n        return colspan;\n    }\n\n    getGroupPagerCellColspan(group) {\n        const lastAggregateIndex = this.getLastAggregateIndex(group);\n        let colspan;\n        if (lastAggregateIndex > -1) {\n            colspan = this.columns.length - lastAggregateIndex - 1;\n        } else {\n            colspan = this.columns.length > 1 ? DEFAULT_GROUP_PAGER_COLSPAN : 0;\n        }\n        if (this.hasOpenFormViewColumn) {\n            colspan++;\n        }\n        return colspan;\n    }\n\n    getGroupPagerProps(group) {\n        const list = group.list;\n        return {\n            offset: list.offset,\n            limit: list.limit,\n            total: list.count,\n            onUpdate: async ({ offset, limit }) => {\n                await list.load({ limit, offset });\n                this.render(true);\n            },\n            withAccessKey: false,\n        };\n    }\n\n    computeOptionalActiveFields() {\n        const localStorageValue = browser.localStorage.getItem(this.keyOptionalFields);\n        const optionalColumn = this.allColumns.filter(\n            (col) => col.type === \"field\" && col.optional\n        );\n        const optionalActiveFields = {};\n        if (localStorageValue !== null) {\n            const localStorageOptionalActiveFields = localStorageValue.split(\",\");\n            for (const col of optionalColumn) {\n                optionalActiveFields[col.name] = localStorageOptionalActiveFields.includes(\n                    col.name\n                );\n            }\n        } else {\n            for (const col of optionalColumn) {\n                optionalActiveFields[col.name] = col.optional === \"show\";\n            }\n        }\n        return optionalActiveFields;\n    }\n\n    onClickSortColumn(column) {\n        if (this.preventReorder) {\n            this.preventReorder = false;\n            return;\n        }\n        if (this.editedRecord || this.props.list.model.useSampleModel) {\n            return;\n        }\n        const fieldName = column.name;\n        const list = this.props.list;\n        if (this.isSortable(column)) {\n            list.sortBy(fieldName);\n        }\n    }\n\n    /**\n     * @param {RelationalRecord} record\n     * @param {Column} column\n     * @param {PointerEvent} ev\n     */\n    onButtonCellClicked(record, column, ev) {\n        if (!ev.target.closest(\"button\")) {\n            this.onCellClicked(record, column, ev);\n        }\n    }\n\n    /**\n     * @param {RelationalRecord} record\n     * @param {Column} column\n     * @param {PointerEvent} ev\n     */\n    async onCellClicked(record, column, ev, newWindow) {\n        if (ev.target.special_click) {\n            return;\n        }\n\n        const multiEdit = this.props.list.model.multiEdit;\n        const hasSelection = !!this.props.list.selection.length;\n        if (hasSelection && this.canSelectRecord && (!multiEdit || !record.selected)) {\n            this.toggleRecordSelection(record);\n        } else if (\n            (multiEdit && record.selected) ||\n            (this.isInlineEditable(record) && !hasSelection)\n        ) {\n            if (record.isInEdition && this.editedRecord === record) {\n                const cell = this.tableRef.el.querySelector(\n                    `.o_selected_row td[name='${column.name}']`\n                );\n                if (cell && containsActiveElement(cell)) {\n                    this.lastEditedCell = { column, record };\n                    // Cell is already focused.\n                    return;\n                }\n                this.focusCell(column);\n                this.cellToFocus = null;\n            } else {\n                const recordIndex = this.props.list.records.indexOf(record);\n                await this.resequencePromise;\n                // row might have changed record after resequence\n                record = this.props.list.records[recordIndex] || record;\n                await this.props.list.enterEditMode(record);\n                this.cellToFocus = { column, record };\n                if (\n                    column.type === \"field\" &&\n                    record.fields[column.name].type === \"boolean\" &&\n                    (!column.widget || column.widget === \"boolean\")\n                ) {\n                    if (\n                        !this.isCellReadonly(column, record) &&\n                        !this.evalInvisible(column.invisible, record)\n                    ) {\n                        await record.update({ [column.name]: !record.data[column.name] });\n                    }\n                }\n            }\n        } else if (this.editedRecord && this.editedRecord !== record) {\n            this.props.list.leaveEditMode();\n        } else if (!this.props.archInfo.noOpen) {\n            this.props.openRecord(record, { newWindow });\n        }\n    }\n\n    /**\n     * @param {RelationalRecord} record\n     * @param {PointerEvent} ev\n     */\n    async onRemoveCellClicked(record, ev) {\n        const element = ev.target.closest(\".o_list_record_remove\");\n        if (element.dataset.clicked) {\n            return;\n        }\n        element.dataset.clicked = true;\n        try {\n            await this.onDeleteRecord(record, ev);\n        } finally {\n            delete element.dataset.clicked;\n        }\n    }\n\n    openMultiCurrencyPopover(ev, value, fieldName) {\n        if (!this.multiCurrencyPopover.isOpen) {\n            this.multiCurrencyPopover.open(ev.target, {\n                currencyIds: Array.from(this.getFieldCurrencies(fieldName)),\n                target: ev.target,\n                value,\n            });\n        }\n    }\n\n    /**\n     * @param {RelationalRecord} record\n     */\n    async onDeleteRecord(record) {\n        if (this.editedRecord && this.editedRecord !== record) {\n            const left = await this.props.list.leaveEditMode();\n            if (!left) {\n                return;\n            }\n        }\n        if (this.activeActions.onDelete) {\n            return this.activeActions.onDelete(record);\n        }\n    }\n\n    /**\n     * @param {HTMLTableCellElement} cell\n     * @param {boolean} cellIsInGroupRow\n     * @param {Direction} direction\n     */\n    findFocusFutureCell(cell, cellIsInGroupRow, direction) {\n        const row = cell.parentElement;\n        const children = [...row.children];\n        const index = children.indexOf(cell);\n        let futureCell;\n        let targetIndex;\n        switch (direction) {\n            case \"up\": {\n                let futureRow = row.previousElementSibling;\n                futureRow = futureRow || row.parentElement.previousElementSibling?.lastElementChild;\n\n                if (futureRow) {\n                    const addCell = [...futureRow.children].find((c) =>\n                        c.classList.contains(\"o_group_field_row_add\")\n                    );\n                    const nextIsGroup = futureRow.classList.contains(\"o_group_header\");\n                    const rowTypeSwitched = cellIsInGroupRow !== nextIsGroup;\n                    const isGroupToGroup = cellIsInGroupRow && nextIsGroup;\n                    if (rowTypeSwitched || isGroupToGroup) {\n                        targetIndex = this.lastKnownIndex || 0;\n                    } else {\n                        this.lastKnownIndex = index;\n                    }\n\n                    const defaultIndex = cellIsInGroupRow ? targetIndex : 0;\n\n                    futureCell =\n                        addCell ||\n                        (futureRow && futureRow.children[rowTypeSwitched ? defaultIndex : index]);\n                }\n                break;\n            }\n            case \"down\": {\n                let futureRow = row.nextElementSibling;\n                futureRow = futureRow || row.parentElement.nextElementSibling?.firstElementChild;\n                if (futureRow) {\n                    const addCell = [...futureRow.children].find((c) =>\n                        c.classList.contains(\"o_group_field_row_add\")\n                    );\n                    const nextIsGroup = futureRow.classList.contains(\"o_group_header\");\n                    const rowTypeSwitched = cellIsInGroupRow !== nextIsGroup;\n                    const isGroupToGroup = cellIsInGroupRow && nextIsGroup;\n                    const headerRow = this.tableRef.el.querySelector(\"thead tr\");\n                    if (rowTypeSwitched || isGroupToGroup) {\n                        targetIndex = this.lastKnownIndex || 0;\n                    } else {\n                        this.lastKnownIndex = index;\n                    }\n\n                    const defaultIndex = cellIsInGroupRow ? targetIndex : 0;\n                    if (headerRow == row) {\n                        this.lastKnownIndex = index;\n                    }\n\n                    futureCell =\n                        addCell ||\n                        (futureRow && futureRow.children[rowTypeSwitched ? defaultIndex : index]);\n                }\n                break;\n            }\n            case \"left\": {\n                futureCell = children[index - 1];\n                if (futureCell) {\n                    this.lastKnownIndex = index - 1;\n                }\n                break;\n            }\n            case \"right\": {\n                futureCell = children[index + 1];\n                if (futureCell) {\n                    this.lastKnownIndex = index + 1;\n                }\n                break;\n            }\n        }\n        return futureCell && getElementToFocus(futureCell);\n    }\n\n    /**\n     * @param {RelationalRecord} _record\n     */\n    isInlineEditable(_record) {\n        // /!\\ the keyboard navigation works under the hypothesis that all or\n        // none records are editable.\n        return !!this.props.editable;\n    }\n\n    /**\n     * @param {KeyboardEvent} ev\n     * @param {Group | null} group\n     * @param {RelationalRecord | null} record\n     */\n    onCellKeydown(ev, group = null, record = null) {\n        if (this.props.list.model.useSampleModel) {\n            return;\n        }\n\n        const hotkey = getActiveHotkey(ev);\n\n        if (ev.target.tagName === \"TEXTAREA\" && hotkey === \"enter\") {\n            return;\n        }\n\n        const closestCell = ev.target.closest(\"td, th\");\n\n        if (this.toggleFocusInsideCell(hotkey, closestCell)) {\n            return;\n        }\n\n        const handled = this.editedRecord\n            ? this.onCellKeydownEditMode(hotkey, closestCell, group, record)\n            : this.onCellKeydownReadOnlyMode(hotkey, closestCell, group, record); // record is supposed to be not null here\n\n        if (handled) {\n            this.lastCreatingAction = false;\n            for (const tbody of this.tableRef.el.getElementsByTagName(\"tbody\")) {\n                tbody.classList.add(\"o_keyboard_navigation\");\n            }\n            ev.preventDefault();\n            ev.stopPropagation();\n        }\n    }\n\n    /**\n     * @param {KeyboardEvent} ev\n     */\n    onGroupInputKeydown(ev) {\n        const hotkey = getActiveHotkey(ev);\n        if (hotkey === \"enter\") {\n            ev.stopPropagation();\n            this.addNewGroup();\n        }\n        if (hotkey === \"escape\") {\n            ev.stopPropagation();\n            this.state.showGroupInput = false;\n        }\n    }\n\n    addNewGroup() {\n        this.state.showGroupInput = false;\n        const value = this.groupInputRef.el.value;\n        if (value) {\n            this.props.list.createGroup(value);\n        }\n    }\n\n    /**\n     * @param {HTMLElement} row\n     * @param {HTMLTableCellElement} cell\n     */\n    findNextFocusableOnRow(row, cell) {\n        const children = [...row.children];\n        const index = children.indexOf(cell);\n        const nextCells = children.slice(index + 1);\n        for (const c of nextCells) {\n            if (!c.classList.contains(\"o_data_cell\")) {\n                continue;\n            }\n            if (\n                c.firstElementChild &&\n                c.firstElementChild.classList.contains(\"o_readonly_modifier\")\n            ) {\n                continue;\n            }\n            const toFocus = getElementToFocus(c, 0);\n            if (toFocus !== c) {\n                return toFocus;\n            }\n        }\n\n        return null;\n    }\n\n    /**\n     * @param {HTMLElement} row\n     * @param {HTMLTableCellElement} cell\n     */\n    findPreviousFocusableOnRow(row, cell) {\n        const children = [...row.children];\n        const index = children.indexOf(cell);\n        const previousCells = children.slice(0, index);\n        for (const c of previousCells.reverse()) {\n            if (!c.classList.contains(\"o_data_cell\")) {\n                continue;\n            }\n            if (\n                c.firstElementChild &&\n                c.firstElementChild.classList.contains(\"o_readonly_modifier\")\n            ) {\n                continue;\n            }\n            const toFocus = getElementToFocus(c, -1);\n            if (toFocus !== c) {\n                return toFocus;\n            }\n        }\n\n        return null;\n    }\n\n    /**\n     * @param {RelationalRecord} record\n     * @param {Direction} direction\n     */\n    expandCheckboxes(record, direction) {\n        const { records } = this.props.list;\n        if (!record && direction === \"down\") {\n            const defaultRecord = records[0];\n            this.shiftKeyedRecord = defaultRecord;\n            defaultRecord.toggleSelection(true);\n            return true;\n        }\n        const recordIndex = records.indexOf(record);\n        const shiftKeyedRecordIndex = records.indexOf(this.shiftKeyedRecord);\n        let nextRecord;\n        let isExpanding;\n        switch (direction) {\n            case \"up\":\n                if (recordIndex <= 0) {\n                    return false;\n                }\n                nextRecord = records[recordIndex - 1];\n                isExpanding = shiftKeyedRecordIndex > recordIndex - 1;\n                break;\n            case \"down\":\n                if (recordIndex === records.length - 1) {\n                    return false;\n                }\n                nextRecord = records[recordIndex + 1];\n                isExpanding = shiftKeyedRecordIndex < recordIndex + 1;\n                break;\n        }\n\n        if (isExpanding) {\n            record.toggleSelection(true);\n            nextRecord.toggleSelection(true);\n        } else {\n            record.toggleSelection(false);\n        }\n\n        return true;\n    }\n\n    /**\n     * @param {string} hotkey\n     * @param {HTMLTableCellElement} cell\n     * @param {Group} group\n     * @param {RelationalRecord} record\n     */\n    applyCellKeydownMultiEditMode(hotkey, cell, group, record) {\n        const { list } = this.props;\n        const row = cell.parentElement;\n        let toFocus, futureRecord;\n        const index = list.selection.indexOf(record);\n        if (this.lastIsDirty && [\"tab\", \"shift+tab\", \"enter\"].includes(hotkey)) {\n            list.leaveEditMode();\n            return true;\n        }\n\n        if (this.applyCellKeydownEditModeStayOnRow(hotkey, cell, group, record)) {\n            return true;\n        }\n\n        switch (hotkey) {\n            case \"tab\":\n                futureRecord = list.selection[index + 1] || list.selection[0];\n                if (record === futureRecord) {\n                    // Refocus first cell of same record\n                    toFocus = this.findNextFocusableOnRow(row, cell);\n                    this.focus(toFocus);\n                    return true;\n                }\n                break;\n\n            case \"shift+tab\":\n                futureRecord =\n                    list.selection[index - 1] || list.selection[list.selection.length - 1];\n                if (record === futureRecord) {\n                    // Refocus last cell of same record\n                    toFocus = this.findPreviousFocusableOnRow(row, cell);\n                    this.focus(toFocus);\n                    return true;\n                }\n                this.cellToFocus = { forward: false, record: futureRecord };\n                break;\n\n            case \"enter\":\n                if (list.selection.length === 1) {\n                    list.leaveEditMode();\n                    return true;\n                }\n                futureRecord = list.selection[index + 1] || list.selection[0];\n                break;\n        }\n\n        if (futureRecord) {\n            list.enterEditMode(futureRecord);\n            return true;\n        }\n        return false;\n    }\n\n    /**\n     * @param {string} hotkey\n     * @param {HTMLElement} _cell\n     * @param {Group} group\n     * @param {RelationalRecord} record\n     */\n    applyCellKeydownEditModeGroup(hotkey, _cell, group, record) {\n        const { editable } = this.props;\n        const groupIndex = group.list.records.indexOf(record);\n        const isLastOfGroup = groupIndex === group.list.records.length - 1;\n        const isDirty = record.dirty || this.lastIsDirty;\n        const isEnterBehavior = hotkey === \"enter\" && (isDirty || !record.canBeAbandoned);\n        const isTabBehavior = hotkey === \"tab\" && isDirty;\n        if (\n            isLastOfGroup &&\n            this.canCreate &&\n            editable === \"bottom\" &&\n            (isEnterBehavior || isTabBehavior)\n        ) {\n            this.add({ group });\n            return true;\n        }\n        return false;\n    }\n\n    /**\n     * @param {string} hotkey\n     * @param {HTMLTableCellElement} cell\n     * @param {Group} _group\n     * @param {RelationalRecord} _record\n     */\n    applyCellKeydownEditModeStayOnRow(hotkey, cell, _group, _record) {\n        let toFocus;\n        const row = cell.parentElement;\n\n        switch (hotkey) {\n            case \"tab\":\n                toFocus = this.findNextFocusableOnRow(row, cell);\n                break;\n            case \"shift+tab\":\n                toFocus = this.findPreviousFocusableOnRow(row, cell);\n                break;\n        }\n\n        if (toFocus) {\n            this.focus(toFocus);\n            return true;\n        }\n        return false;\n    }\n\n    editNextRecord(record, group) {\n        const list = this.props.list;\n        const topReCreate = this.props.editable === \"top\" && record.isNew;\n        const index = list.records.indexOf(record);\n        let futureRecord = list.records[index + 1];\n        if (topReCreate && index === 0) {\n            futureRecord = null;\n        }\n\n        if (!futureRecord && !this.canCreate) {\n            futureRecord = list.records[0];\n        }\n\n        if (futureRecord) {\n            list.leaveEditMode({ validate: true }).then((canProceed) => {\n                if (canProceed) {\n                    list.enterEditMode(futureRecord);\n                }\n            });\n        } else if (this.lastIsDirty || !record.canBeAbandoned || this.displayRowCreates) {\n            this.add({ group });\n        } else {\n            futureRecord = list.records.at(0);\n            list.enterEditMode(futureRecord);\n        }\n    }\n\n    /**\n     * @param {string} hotkey\n     * @param {HTMLTableCellElement} cell\n     * @param {Group | null} group\n     * @param {RelationalRecord | null} record\n     * @returns {boolean} true if some behavior has been taken\n     */\n    onCellKeydownEditMode(hotkey, cell, group, record) {\n        const { cycleOnTab, list } = this.props;\n        const row = cell.parentElement;\n        const applyMultiEditBehavior = record && record.selected && list.model.multiEdit;\n        const isDirty = record.dirty || this.lastIsDirty;\n        const topReCreate = this.props.editable === \"top\" && record.isNew;\n\n        if (\n            applyMultiEditBehavior &&\n            this.applyCellKeydownMultiEditMode(hotkey, cell, group, record)\n        ) {\n            return true;\n        }\n\n        if (this.applyCellKeydownEditModeStayOnRow(hotkey, cell, group, record)) {\n            return true;\n        }\n\n        if (group && this.applyCellKeydownEditModeGroup(hotkey, cell, group, record)) {\n            return true;\n        }\n\n        switch (hotkey) {\n            case \"tab\": {\n                const index = list.records.indexOf(record);\n                const lastIndex = topReCreate ? 0 : list.records.length - 1;\n                if (index === lastIndex) {\n                    if (this.displayRowCreates) {\n                        if (!isDirty && record.isNew) {\n                            list.leaveEditMode();\n                            return false;\n                        }\n                        // add a line\n                        const { context } = this.controls[0];\n                        this.add({ context });\n                    } else if (isDirty && this.canCreate) {\n                        this.add({ group });\n                    } else if (cycleOnTab) {\n                        if (record.canBeAbandoned) {\n                            list.leaveEditMode();\n                        }\n                        const futureRecord = list.records[0];\n                        if (record === futureRecord) {\n                            // Refocus first cell of same record\n                            const toFocus = this.findNextFocusableOnRow(row);\n                            this.focus(toFocus);\n                        } else {\n                            list.enterEditMode(futureRecord);\n                        }\n                    } else {\n                        return false;\n                    }\n                } else {\n                    const futureRecord = list.records[index + 1];\n                    list.enterEditMode(futureRecord);\n                }\n                break;\n            }\n            case \"shift+tab\": {\n                const index = list.records.indexOf(record);\n                if (index === 0) {\n                    if (cycleOnTab) {\n                        if (record.canBeAbandoned) {\n                            list.leaveEditMode();\n                        }\n                        const futureRecord = list.records[list.records.length - 1];\n                        if (record === futureRecord) {\n                            // Refocus first cell of same record\n                            const toFocus = this.findPreviousFocusableOnRow(row);\n                            this.focus(toFocus);\n                        } else {\n                            this.cellToFocus = { forward: false, record: futureRecord };\n                            list.enterEditMode(futureRecord);\n                        }\n                    } else {\n                        list.leaveEditMode();\n                        return false;\n                    }\n                } else {\n                    const futureRecord = list.records[index - 1];\n                    this.cellToFocus = { forward: false, record: futureRecord };\n                    list.enterEditMode(futureRecord);\n                }\n                break;\n            }\n            case \"enter\": {\n                this.editNextRecord(record, group);\n                break;\n            }\n            case \"escape\": {\n                // TODO this seems bad: refactor this\n                list.leaveEditMode({ discard: true });\n                const firstAddButton = this.tableRef.el.querySelector(\n                    \".o_field_x2many_list_row_add a\"\n                );\n\n                if (firstAddButton) {\n                    this.focus(firstAddButton);\n                } else if (group && record.isNew) {\n                    const children = [...row.parentElement.children];\n                    const index = children.indexOf(row);\n                    for (let i = index + 1; i < children.length; i++) {\n                        const row = children[i];\n                        if (row.classList.contains(\"o_group_header\")) {\n                            break;\n                        }\n                        const addCell = [...row.children].find((c) =>\n                            c.classList.contains(\"o_group_field_row_add\")\n                        );\n                        if (addCell) {\n                            const toFocus = addCell.querySelector(\"a\");\n                            this.focus(toFocus);\n                            return true;\n                        }\n                    }\n                    this.focus(cell);\n                } else {\n                    this.focus(cell);\n                }\n                break;\n            }\n            default:\n                return false;\n        }\n        return true;\n    }\n\n    /**\n     * @param {string} hotkey\n     * @param {HTMLTableCellElement} cell\n     * @param {Group | null} group\n     * @param {RelationalRecord | null} record\n     * @returns {boolean} true if some behavior has been taken\n     */\n    onCellKeydownReadOnlyMode(hotkey, cell, group, record) {\n        const cellIsInGroupRow = Boolean(group && !record);\n        const applyMultiEditBehavior = record && record.selected && this.props.list.model.multiEdit;\n        let toFocus;\n        switch (hotkey) {\n            case \"arrowup\":\n                toFocus = this.findFocusFutureCell(cell, cellIsInGroupRow, \"up\");\n                if (!toFocus && this.env.searchModel) {\n                    this.env.searchModel.trigger(\"focus-search\");\n                    return true;\n                }\n                break;\n            case \"arrowdown\":\n                toFocus = this.findFocusFutureCell(cell, cellIsInGroupRow, \"down\");\n                break;\n            case \"arrowleft\":\n                if (cellIsInGroupRow && !group.isFolded) {\n                    this.toggleGroup(group);\n                    return true;\n                }\n\n                if (cell.classList.contains(\"o_field_x2many_list_row_add\")) {\n                    // to refactor\n                    const a = document.activeElement;\n                    toFocus = a.previousElementSibling;\n                } else {\n                    toFocus = this.findFocusFutureCell(cell, cellIsInGroupRow, \"left\");\n                }\n                break;\n            case \"arrowright\":\n                if (cellIsInGroupRow && group.isFolded) {\n                    this.toggleGroup(group);\n                    return true;\n                }\n\n                if (cell.classList.contains(\"o_field_x2many_list_row_add\")) {\n                    // This cell contains only <a/> elements, see template.\n                    const a = document.activeElement;\n                    toFocus = a.nextElementSibling;\n                } else {\n                    toFocus = this.findFocusFutureCell(cell, cellIsInGroupRow, \"right\");\n                }\n                break;\n            case \"tab\":\n                if (cellIsInGroupRow) {\n                    const buttons = Array.from(cell.querySelectorAll(\".o_group_buttons button\"));\n                    const currentButton = document.activeElement.closest(\"button\");\n                    const index = buttons.indexOf(currentButton);\n                    toFocus = buttons[index + 1] || currentButton;\n                }\n                break;\n            case \"shift+tab\":\n                if (cellIsInGroupRow) {\n                    const buttons = Array.from(cell.querySelectorAll(\".o_group_buttons button\"));\n                    const currentButton = document.activeElement.closest(\"button\");\n                    const index = buttons.indexOf(currentButton);\n                    toFocus = buttons[index - 1] || currentButton;\n                }\n                break;\n            case \"shift+arrowdown\": {\n                if (this.expandCheckboxes(record, \"down\")) {\n                    toFocus = this.findFocusFutureCell(cell, cellIsInGroupRow, \"down\");\n                }\n                break;\n            }\n            case \"shift+arrowup\": {\n                if (this.expandCheckboxes(record, \"up\")) {\n                    toFocus = this.findFocusFutureCell(cell, cellIsInGroupRow, \"up\");\n                }\n                break;\n            }\n            case \"shift+space\":\n                this.toggleRecordSelection(record);\n                toFocus = getElementToFocus(cell);\n                break;\n            case \"shift\":\n                this.shiftKeyedRecord = record;\n                break;\n            case \"enter\":\n                if (!group && !record) {\n                    return false;\n                }\n\n                if (cell.classList.contains(\"o_list_record_remove\")) {\n                    this.onDeleteRecord(record);\n                    return true;\n                }\n\n                if (cellIsInGroupRow) {\n                    const button = document.activeElement.closest(\"button\");\n                    if (button) {\n                        button.click();\n                    } else {\n                        this.toggleGroup(group);\n                    }\n                    return true;\n                }\n\n                if (this.isInlineEditable(record) || applyMultiEditBehavior) {\n                    const column = this.columns.find((c) => c.name === cell.getAttribute(\"name\"));\n                    this.cellToFocus = { column, record };\n                    this.props.list.enterEditMode(record);\n                    return true;\n                }\n\n                if (!this.props.archInfo.noOpen) {\n                    this.props.openRecord(record);\n                    return true;\n                }\n                break;\n            default:\n                // Return with no effect (no stop or prevent default...)\n                return false;\n        }\n\n        if (toFocus) {\n            this.focus(toFocus);\n            return true;\n        }\n\n        return false;\n    }\n\n    saveOptionalActiveFields() {\n        browser.localStorage.setItem(\n            this.keyOptionalFields,\n            Object.keys(this.optionalActiveFields).filter(\n                (fieldName) => this.optionalActiveFields[fieldName]\n            )\n        );\n    }\n\n    get showNoContentHelper() {\n        const { model } = this.props.list;\n        return this.props.noContentHelp && (model.useSampleModel || !model.hasData());\n    }\n\n    /**\n     * @param {Group} group\n     */\n    showGroupPager(group) {\n        return !group.isFolded && group.list.limit < group.list.count;\n    }\n\n    /**\n     * @param {Group} group\n     */\n    showGroupConfigMenu(group) {\n        return group.value && [\"many2one\", \"many2many\"].includes(group.groupByField.type);\n    }\n\n    /**\n     * Returns true if the focus was toggled inside the same cell.\n     *\n     * @param {string} hotkey\n     * @param {HTMLTableCellElement} cell\n     */\n    toggleFocusInsideCell(hotkey, cell) {\n        if (![\"tab\", \"shift+tab\"].includes(hotkey) || !containsActiveElement(cell)) {\n            return false;\n        }\n        const focusableEls = getTabableElements(cell).filter(\n            (el) =>\n                el === document.activeElement ||\n                [\"INPUT\", \"BUTTON\", \"TEXTAREA\"].includes(el.tagName)\n        );\n        const index = focusableEls.indexOf(document.activeElement);\n        return (\n            (hotkey === \"tab\" && index < focusableEls.length - 1) ||\n            (hotkey === \"shift+tab\" && index > 0)\n        );\n    }\n\n    /**\n     * @param {PointerEvent} _ev\n     * @param {Group} group\n     */\n    async onGroupHeaderClicked(_ev, group) {\n        const left = await this.props.list.leaveEditMode();\n        if (left) {\n            this.toggleGroup(group);\n        }\n    }\n\n    /**\n     * @param {Group} group\n     */\n    toggleGroup(group) {\n        group.toggle();\n    }\n\n    get canSelectRecord() {\n        return !this.editedRecord && !this.props.list.model.useSampleModel;\n    }\n\n    toggleSelection() {\n        const list = this.props.list;\n        if (!this.canSelectRecord) {\n            return;\n        }\n        return list.toggleSelection();\n    }\n\n    /**\n     * @param {RelationalRecord} record\n     * @param {PointerEvent} _ev\n     */\n    toggleRecordSelection(record, _ev) {\n        if (!this.canSelectRecord) {\n            return;\n        }\n        const isRecordPresent = this.props.list.records.includes(this.lastCheckedRecord);\n        if (this.shiftKeyMode && isRecordPresent) {\n            this.toggleRangeSelection(record);\n        } else {\n            record.toggleSelection();\n        }\n        this.lastCheckedRecord = record;\n    }\n\n    /**\n     * @param {RelationalRecord} record\n     */\n    toggleRangeSelection(record) {\n        const { records } = this.props.list;\n        const recordIndex = records.indexOf(record);\n        const lastCheckedRecordIndex = records.indexOf(this.lastCheckedRecord);\n        const start = Math.min(recordIndex, lastCheckedRecordIndex);\n        const end = Math.max(recordIndex, lastCheckedRecordIndex);\n        for (let i = start; i <= end; i++) {\n            records[i].toggleSelection(!record.selected);\n        }\n    }\n\n    /**\n     * @param {string} fieldName\n     */\n    async toggleOptionalField(fieldName) {\n        this.optionalActiveFields[fieldName] = !this.optionalActiveFields[fieldName];\n        this.saveOptionalActiveFields(\n            this.allColumns.filter((col) => this.optionalActiveFields[col.name] && col.optional)\n        );\n        this.render();\n    }\n\n    /**\n     * @param {string} groupId\n     */\n    toggleOptionalFieldGroup(groupId) {\n        const fieldNames = this.allColumns\n            .filter(\n                (col) =>\n                    col.type === \"field\" &&\n                    col.relatedPropertyField &&\n                    col.relatedPropertyField.id === groupId\n            )\n            .map((col) => col.name);\n        const active = !fieldNames.every((fieldName) => this.optionalActiveFields[fieldName]);\n        for (const fieldName of fieldNames) {\n            this.optionalActiveFields[fieldName] = active;\n        }\n        this.saveOptionalActiveFields(\n            this.allColumns.filter((col) => this.optionalActiveFields[col.name] && col.optional)\n        );\n        this.render();\n    }\n\n    toggleDebugOpenView() {\n        this.debugOpenView = !this.debugOpenView;\n        browser.localStorage.setItem(this.keyDebugOpenView, this.debugOpenView);\n        this.render();\n    }\n\n    /**\n     * @param {PointerEvent} ev\n     */\n    onGlobalClick(ev) {\n        if (!(this.editedRecord || this.state.showGroupInput)) {\n            return; // there's no row or group in edition\n        }\n\n        this.tableRef.el.querySelector(\"tbody\").classList.remove(\"o_keyboard_navigation\");\n\n        const target = ev.target;\n        if (this.state.showGroupInput && this.groupInputRef.el !== target) {\n            this.state.showGroupInput = false;\n        }\n        if (this.tableRef.el.contains(target) && target.closest(\".o_data_row\")) {\n            // ignore clicks inside the table that are originating from a record row\n            // as they are handled directly by the renderer.\n            return;\n        }\n        if (this.activeElement !== this.uiService.activeElement) {\n            return;\n        }\n        // DateTime picker\n        if (target.closest(\".o_datetime_picker\")) {\n            return;\n        }\n        // Legacy autocomplete\n        if (ev.target.closest(\".ui-autocomplete\")) {\n            return;\n        }\n        this.props.list.leaveEditMode();\n    }\n\n    get isDebugMode() {\n        return Boolean(odoo.debug);\n    }\n\n    /**\n     * @param {Column} column\n     */\n    makeTooltip(column) {\n        return getTooltipInfo({\n            viewMode: \"list\",\n            resModel: this.props.list.resModel,\n            field: this.fields[column.name],\n            fieldInfo: column,\n        });\n    }\n\n    onColumnTitleMouseUp() {\n        if (this.columnWidths.resizing) {\n            this.preventReorder = true;\n        }\n    }\n\n    resetLongTouchTimer() {\n        if (this.longTouchTimer) {\n            browser.clearTimeout(this.longTouchTimer);\n            this.longTouchTimer = null;\n        }\n    }\n\n    /**\n     * @param {RelationalRecord} record\n     * @param {TouchEvent} ev\n     */\n    onRowTouchStart(record, ev) {\n        if (!this.props.allowSelectors) {\n            return;\n        }\n        if (this.props.list.selection.length) {\n            ev.stopPropagation(); // This is done in order to prevent the tooltip from showing up\n        }\n        this.touchStartMs = Date.now();\n        if (this.longTouchTimer === null) {\n            this.longTouchTimer = browser.setTimeout(() => {\n                this.toggleRecordSelection(record);\n                this.resetLongTouchTimer();\n            }, this.constructor.LONG_TOUCH_THRESHOLD);\n        }\n    }\n\n    /**\n     * @param {RelationalRecord} _record\n     */\n    onRowTouchEnd(_record) {\n        const elapsedTime = Date.now() - this.touchStartMs;\n        if (elapsedTime < this.constructor.LONG_TOUCH_THRESHOLD) {\n            this.resetLongTouchTimer();\n        }\n    }\n\n    /**\n     * @param {RelationalRecord} _record\n     */\n    onRowTouchMove(_record) {\n        this.resetLongTouchTimer();\n    }\n\n    /**\n     * @param {string} dataRowId\n     * @param {Object} params\n     * @param {HTMLElement} params.element\n     * @param {HTMLElement} [params.group]\n     * @param {HTMLElement} [params.next]\n     * @param {HTMLElement} [params.parent]\n     * @param {HTMLElement} [params.previous]\n     */\n    async sortDrop(dataRowId, dataGroupId, { element, previous }) {\n        element.classList.remove(\"o_row_draggable\");\n        const refId = previous ? previous.dataset.id : null;\n        try {\n            if (dataGroupId) {\n                this.resequencePromise = this.props.list.moveRecord(\n                    dataRowId,\n                    dataGroupId,\n                    refId,\n                    previous.dataset.groupId\n                );\n            } else {\n                this.resequencePromise = this.props.list.resequence(dataRowId, refId, {\n                    handleField: this.props.list.handleField,\n                });\n            }\n            await this.resequencePromise;\n        } finally {\n            element.classList.add(\"o_row_draggable\");\n            await this.props.list.leaveEditMode();\n        }\n    }\n\n    /**\n     * @param {Object} params\n     * @param {HTMLElement} params.element\n     * @param {HTMLElement} [params.group]\n     */\n    sortStart({ element }) {\n        const table = this.tableRef.el;\n        const headers = [...table.querySelectorAll(\"thead th\")];\n        const cells = [...element.querySelectorAll(\"td\")];\n        let headerIndex = 0;\n        for (const cell of cells) {\n            let width = 0;\n            for (let i = 0; i < cell.colSpan; i++) {\n                const header = headers[headerIndex + i];\n                const style = getComputedStyle(header);\n                width += parseFloat(style.width);\n            }\n            cell.style.width = `${width}px`;\n            headerIndex += cell.colSpan;\n        }\n    }\n\n    /**\n     * @param {Object} params\n     * @param {HTMLElement} params.element\n     * @param {HTMLElement} [params.group]\n     */\n    sortStop({ element }) {\n        for (const cell of element.querySelectorAll(\"td\")) {\n            cell.style.width = null;\n        }\n    }\n\n    /**\n     * @param {MouseEvent} ev\n     */\n    ignoreEventInSelectionMode(ev) {\n        const { list } = this.props;\n        if (this.env.isSmall && list.selection.length) {\n            // in selection mode, only selection is allowed.\n            ev.stopPropagation();\n            ev.preventDefault();\n        }\n    }\n\n    /**\n     * @param {RelationalRecord} record\n     * @param {PointerEvent} ev\n     */\n    onClickCapture(record, ev) {\n        const { list } = this.props;\n        if (this.env.isSmall && list.selection.length) {\n            ev.stopPropagation();\n            ev.preventDefault();\n            this.toggleRecordSelection(record);\n        }\n    }\n}\n", "import { registry } from \"@web/core/registry\";\nimport { RelationalModel } from \"@web/model/relational_model/relational_model\";\nimport { ListArchParser } from \"./list_arch_parser\";\nimport { ListController } from \"./list_controller\";\nimport { ListRenderer } from \"./list_renderer\";\n\nexport const listView = {\n    type: \"list\",\n\n    Controller: ListController,\n    Renderer: ListRenderer,\n    ArchParser: ListArchParser,\n    Model: RelationalModel,\n\n    buttonTemplate: \"web.ListView.Buttons\",\n\n    canOrderByCount: true,\n\n    props: (genericProps, view) => {\n        const { ArchParser } = view;\n        const { arch, relatedModels, resModel } = genericProps;\n        const archInfo = new ArchParser().parse(arch, relatedModels, resModel);\n        return {\n            ...genericProps,\n            readonly: genericProps.readonly || !archInfo.activeActions?.edit,\n            Model: view.Model,\n            Renderer: view.Renderer,\n            buttonTemplate: view.buttonTemplate,\n            archInfo,\n        };\n    },\n};\n\nregistry.category(\"views\").add(\"list\", listView);\n", "export const standardViewProps = {\n    info: {\n        type: Object,\n    },\n    resModel: String,\n    arch: { type: Element },\n    className: { type: String, optional: true },\n    context: { type: Object },\n    createRecord: { type: Function, optional: true },\n    display: { type: Object, optional: true },\n    domain: { type: Array },\n    fields: { type: Object },\n    globalState: { type: Object, optional: true },\n    groupBy: { type: Array, element: String },\n    limit: { type: Number, optional: true },\n    noBreadcrumbs: { type: Boolean, optional: true },\n    orderBy: { type: Array, element: Object },\n    relatedModels: { type: Object, optional: true },\n    resId: { type: [Number, Boolean], optional: true },\n    resIds: { type: Array, optional: true },\n    searchMenuTypes: { type: Array, element: String },\n    selectRecord: { type: Function, optional: true },\n    state: { type: Object, optional: true },\n    useSampleModel: { type: Boolean },\n    updateActionState: { type: Function, optional: true },\n};\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { unique } from \"@web/core/utils/arrays\";\nimport { exprToBoolean } from \"@web/core/utils/strings\";\nimport { combineModifiers } from \"@web/model/relational_model/utils\";\n\nexport const X2M_TYPES = [\"one2many\", \"many2many\"];\nconst NUMERIC_TYPES = [\"integer\", \"float\", \"monetary\"];\n\n/**\n * @typedef ViewActiveActions {\n * @property {\"view\"} type\n * @property {boolean} edit\n * @property {boolean} create\n * @property {boolean} delete\n * @property {boolean} duplicate\n */\n\nexport const BUTTON_CLICK_PARAMS = [\n    \"name\",\n    \"type\",\n    \"args\",\n    \"block-ui\", // Blocks UI with a spinner until the action is done\n    \"context\",\n    \"close\",\n    \"cancel-label\",\n    \"confirm\",\n    \"confirm-title\",\n    \"confirm-label\",\n    \"special\",\n    \"effect\",\n    \"help\",\n    // WOWL SAD: is adding the support for debounce attribute here justified or should we\n    // just override compileButton in kanban compiler to add the debounce?\n    \"debounce\",\n    // WOWL JPP: is adding the support for not oppening the dialog of confirmation in the settings view\n    // This should be refactor someday\n    \"noSaveDialog\",\n];\n\n/**\n * @param {string?} type\n * @returns {string | false}\n */\nfunction getViewClass(type) {\n    const isValidType = Boolean(type) && registry.category(\"views\").contains(type);\n    return isValidType && `o_${type}_view`;\n}\n\n/**\n * @param {string?} viewType\n * @param {Element?} rootNode\n * @param {string[]} additionalClassList\n * @returns {string}\n */\nexport function computeViewClassName(viewType, rootNode, additionalClassList = []) {\n    const subType = rootNode?.getAttribute(\"js_class\");\n    const classList = rootNode?.getAttribute(\"class\")?.split(\" \") || [];\n    const uniqueClasses = new Set([\n        getViewClass(viewType),\n        getViewClass(subType),\n        ...classList,\n        ...additionalClassList,\n    ]);\n    return Array.from(uniqueClasses)\n        .filter((c) => c) // remove falsy values\n        .join(\" \");\n}\n\n/**\n * TODO: doc\n *\n * @param {Object} fields\n * @param {Object} fieldAttrs\n * @param {string[]} activeMeasures\n * @returns {Object}\n */\nexport const computeReportMeasures = (\n    fields,\n    fieldAttrs,\n    activeMeasures,\n    { sumAggregatorOnly = false } = {}\n) => {\n    const measures = {\n        __count: { name: \"__count\", string: _t(\"Count\"), type: \"integer\" },\n    };\n    for (const [fieldName, field] of Object.entries(fields)) {\n        if (fieldName === \"id\") {\n            continue;\n        }\n        const { isInvisible } = fieldAttrs[fieldName] || {};\n        if (isInvisible) {\n            continue;\n        }\n        if (\n            [\"integer\", \"float\", \"monetary\"].includes(field.type) &&\n            ((sumAggregatorOnly && field.aggregator === \"sum\") ||\n                (!sumAggregatorOnly && field.aggregator))\n        ) {\n            measures[fieldName] = field;\n        }\n    }\n\n    // add active measures to the measure list.  This is very rarely\n    // necessary, but it can be useful if one is working with a\n    // functional field non stored, but in a model with an overridden\n    // read_group method.  In this case, the pivot view could work, and\n    // the measure should be allowed.  However, be careful if you define\n    // a measure in your pivot view: non stored functional fields will\n    // probably not work (their aggregate will always be 0).\n    for (const measure of activeMeasures) {\n        if (!measures[measure]) {\n            measures[measure] = fields[measure];\n        }\n    }\n\n    for (const fieldName in fieldAttrs) {\n        if (fieldAttrs[fieldName].string && fieldName in measures) {\n            measures[fieldName].string = fieldAttrs[fieldName].string;\n        }\n    }\n\n    const sortedMeasures = Object.entries(measures).sort(([m1, f1], [m2, f2]) => {\n        if (m1 === \"__count\" || m2 === \"__count\") {\n            return m1 === \"__count\" ? 1 : -1; // Count is always last\n        }\n        return f1.string.toLowerCase().localeCompare(f2.string.toLowerCase());\n    });\n\n    return Object.fromEntries(sortedMeasures);\n};\n\n/**\n * @param {Record} record\n * @param {String} fieldName\n * @param {Object} [fieldInfo]\n * @returns {String}\n */\nexport function getFormattedValue(record, fieldName, fieldInfo = null) {\n    const field = record.fields[fieldName];\n    const formatter = registry.category(\"formatters\").get(field.type, (val) => val);\n    const formatOptions = {};\n    if (fieldInfo && formatter.extractOptions) {\n        Object.assign(formatOptions, formatter.extractOptions(fieldInfo));\n    }\n    formatOptions.data = record.data;\n    formatOptions.field = field;\n    return record.data[fieldName] !== undefined\n        ? formatter(record.data[fieldName], formatOptions)\n        : \"\";\n}\n\n/**\n * @param {Element} rootNode\n * @returns {ViewActiveActions}\n */\nexport function getActiveActions(rootNode) {\n    const activeActions = {\n        type: \"view\",\n        edit: exprToBoolean(rootNode.getAttribute(\"edit\"), true),\n        create: exprToBoolean(rootNode.getAttribute(\"create\"), true),\n        delete: exprToBoolean(rootNode.getAttribute(\"delete\"), true),\n    };\n    activeActions.duplicate =\n        activeActions.create && exprToBoolean(rootNode.getAttribute(\"duplicate\"), true);\n    return activeActions;\n}\n\nexport function getClassNameFromDecoration(decoration) {\n    if (decoration === \"bf\") {\n        return \"fw-bold\";\n    } else if (decoration === \"it\") {\n        return \"fst-italic\";\n    }\n    return `text-${decoration}`;\n}\n\nexport function getDecoration(rootNode) {\n    const decorations = [];\n    for (const name of rootNode.getAttributeNames()) {\n        if (name.startsWith(\"decoration-\")) {\n            decorations.push({\n                class: getClassNameFromDecoration(name.replace(\"decoration-\", \"\")),\n                condition: rootNode.getAttribute(name),\n            });\n        }\n    }\n    return decorations;\n}\n\n/**\n * @param {any} field\n * @returns {boolean}\n */\nexport function isX2Many(field) {\n    return field && X2M_TYPES.includes(field.type);\n}\n\n/**\n * @param {Object} field\n * @returns {boolean} true iff the given field is a numeric field\n */\nexport function isNumeric(field) {\n    return NUMERIC_TYPES.includes(field.type);\n}\n\n/**\n * @param {any} value\n * @returns {boolean}\n */\nexport function isNull(value) {\n    return [null, undefined].includes(value);\n}\n\nexport function processButton(node) {\n    const withDefault = {\n        close: (val) => exprToBoolean(val, false),\n        context: (val) => val || \"{}\",\n    };\n    const clickParams = {};\n    const attrs = {};\n    for (const { name, value } of node.attributes) {\n        if (BUTTON_CLICK_PARAMS.includes(name)) {\n            clickParams[name] = withDefault[name] ? withDefault[name](value) : value;\n        } else {\n            attrs[name] = value;\n        }\n    }\n    return {\n        className: node.getAttribute(\"class\") || \"\",\n        disabled: !!node.getAttribute(\"disabled\") || false,\n        icon: node.getAttribute(\"icon\") || false,\n        title: node.getAttribute(\"title\") || undefined,\n        string: node.getAttribute(\"string\") || undefined,\n        options: JSON.parse(node.getAttribute(\"options\") || \"{}\"),\n        display: node.getAttribute(\"display\") || \"selection\",\n        clickParams,\n        column_invisible: node.getAttribute(\"column_invisible\"),\n        invisible: combineModifiers(\n            node.getAttribute(\"column_invisible\"),\n            node.getAttribute(\"invisible\"),\n            \"OR\"\n        ),\n        readonly: node.getAttribute(\"readonly\"),\n        required: node.getAttribute(\"required\"),\n        attrs,\n    };\n}\n\n/**\n * In the preview implementation of reporting views, the virtual field used to\n * display the number of records was named __count__, whereas __count is\n * actually the one used in xml. So basically, activating a filter specifying\n * __count as measures crashed. Unfortunately, as __count__ was used in the JS,\n * all filters saved as favorite at that time were saved with __count__, and\n * not __count. So in order the make them still work with the new\n * implementation, we handle both __count__ and __count.\n *\n * This function replaces occurences of '__count__' by '__count' in the given\n * element(s).\n *\n * @param {any | any[]} [measures]\n * @returns {any}\n */\nexport function processMeasure(measure) {\n    if (Array.isArray(measure)) {\n        return measure.map(processMeasure);\n    }\n    return measure === \"__count__\" ? \"__count\" : measure;\n}\n\n/**\n * Transforms a string into a valid expression to be injected\n * in a template as a props via setAttribute.\n * Example: myString = `Some weird language quote (\") `;\n *     should become in the template:\n *      <Component label=\"&quot;Some weird language quote (\\\\&quot;)&quot; \" />\n *     which should be interpreted by owl as a JS expression being a string:\n *      `Some weird language quote (\") `\n *\n * @param  {string} str The initial value: a pure string to be interpreted as such\n * @return {string}     the valid string to be injected into a component's node props.\n */\nexport function toStringExpression(str) {\n    return `\\`${str.replaceAll(\"`\", \"\\\\`\")}\\``;\n}\n\n/**\n * Given an array of values and an aggregator function, returns the aggregated\n * value.\n *\n * @param {number[]} values\n * @param {'sum'|'avg'|'min'|'max'|'count'|'count_distinct'} aggregator\n * @returns number\n * @throws {Error} if the aggregator function given isn't supported\n */\nexport function computeAggregatedValue(values, aggregator) {\n    if (aggregator === \"sum\") {\n        return values.reduce((acc, v) => v + acc, 0);\n    } else if (aggregator === \"avg\") {\n        return values.reduce((acc, v) => v + acc, 0) / values.length;\n    } else if (aggregator === \"min\") {\n        return Math.min(Infinity, ...values);\n    } else if (aggregator === \"max\") {\n        return Math.max(-Infinity, ...values);\n    } else if (aggregator === \"count\") {\n        return values.length;\n    } else if (aggregator === \"count_distinct\") {\n        return unique(values).length;\n    }\n    throw new Error(`Invalid aggregator '${aggregator}'`);\n}\n", "import { useDebugCategory } from \"@web/core/debug/debug_context\";\nimport { evaluateBooleanExpr } from \"@web/core/py_js/py\";\nimport { registry } from \"@web/core/registry\";\nimport { KeepLast } from \"@web/core/utils/concurrency\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { deepCopy, pick } from \"@web/core/utils/objects\";\nimport { nbsp } from \"@web/core/utils/strings\";\nimport { parseXML } from \"@web/core/utils/xml\";\nimport { extractLayoutComponents } from \"@web/search/layout\";\nimport { WithSearch } from \"@web/search/with_search/with_search\";\nimport { useActionLinks } from \"@web/views/view_hook\";\nimport { computeViewClassName } from \"./utils\";\nimport { loadBundle } from \"@web/core/assets\";\nimport { cookie } from \"@web/core/browser/cookie\";\nimport {\n    Component,\n    markRaw,\n    onWillUpdateProps,\n    onWillStart,\n    toRaw,\n    useSubEnv,\n    reactive,\n} from \"@odoo/owl\";\nimport { session } from \"@web/session\";\n\n/**\n * @typedef Config\n * @property {number | false} actionId\n * @property {string | false} actionType\n * @property {() => []} breadcrumbs\n * @property {() => string} getDisplayName\n * @property {(string) => any} setDisplayName\n * @property {() => Record<string, any>} getPagerProps\n * @property {Record<string, any>[]} viewSwitcherEntry\n * @property {typeof Component} Banner\n *\n * @typedef {import(\"@web/core/context\").Context} Context\n * @typedef {import(\"@web/env\").OdooEnv} OdooEnv\n * @typedef {import(\"@web/search/utils/order_by\").OrderTerm} OrderTerm\n *\n * @typedef ViewProps\n * @property {string} resModel\n * @property {ViewType} type\n *\n * @property {string} [arch] if given, fields must be given too /\\ no post processing is done (evaluation of \"groups\" attribute,...)\n * @property {Record<string, any>} [fields] if given, arch must be given too\n * @property {number|false} [viewId]\n * @property {Record<string, any>} [actionMenus]\n * @property {boolean} [loadActionMenus=false]\n *\n * @property {string} [searchViewArch] if given, searchViewFields must be given too\n * @property {Record<string, any>} [searchViewFields] if given, searchViewArch must be given too\n * @property {number|false} [searchViewId]\n * @property {Record<string, any>[]} [irFilters]\n * @property {boolean} [loadIrFilters=false]\n *\n * @property {Record<string, any>} [comparison]\n * @property {Context} [context={}]\n * @property {DomainRepr} [domain]\n * @property {string[]} [groupBy]\n * @property {OrderTerm[]} [orderBy]\n *\n * @property {boolean} [useSampleModel]\n * @property {string} [noContentHelp]\n *\n * @property {Record<string, any>} [display={}] to rework\n *\n * --- Manipulated by withSearch ---\n * @property {boolean} [activateFavorite]\n * @property {Record<string, any>[]} [dynamicFilters]\n * @property {boolean} [hideCustomGroupBy]\n * @property {string[]} [searchMenuTypes]\n * @property {Record<string, any>} [globalState]\n *\n * @typedef {\"activity\"\n *  | \"calendar\"\n *  | \"cohort\"\n *  | \"form\"\n *  | \"gantt\"\n *  | \"graph\"\n *  | \"grid\"\n *  | \"hierarchy\"\n *  | \"kanban\"\n *  | \"list\"\n *  | \"map\"\n *  | \"pivot\"\n *  | \"search\"\n * } ViewType\n */\n\nconst viewRegistry = registry.category(\"views\");\n\nviewRegistry.addValidation({\n    type: { validate: (t) => t in session.view_info },\n    Controller: { validate: (c) => c.prototype instanceof Component },\n    \"*\": true,\n});\n\n/**\n * Returns the default config to use if no config, or an incomplete config has\n * been provided in the env, which can happen with standalone views.\n * @returns {Config}\n */\nexport function getDefaultConfig() {\n    let displayName;\n    const config = {\n        actionId: false,\n        actionType: false,\n        cache: true,\n        actionXmlId: false,\n        embeddedActions: [],\n        currentEmbeddedActionId: false,\n        parentActionId: false,\n        breadcrumbs: reactive([\n            {\n                get name() {\n                    return displayName;\n                },\n            },\n        ]),\n        disableSearchBarAutofocus: false,\n        getDisplayName: () => displayName,\n        historyBack: () => {},\n        pagerProps: {},\n        setDisplayName: (newDisplayName) => {\n            displayName = newDisplayName;\n            // This is a hack to force the reactivity when a new displayName is set\n            config.breadcrumbs.push(undefined);\n            config.breadcrumbs.pop();\n        },\n        viewSwitcherEntries: [],\n        views: [],\n    };\n    return config;\n}\n\nexport class ViewNotFoundError extends Error {}\n\nconst CALLBACK_RECORDER_NAMES = [\n    \"__beforeLeave__\",\n    \"__getGlobalState__\",\n    \"__getLocalState__\",\n    \"__getContext__\",\n    \"__getOrderBy__\",\n];\n\nconst STANDARD_PROPS = [\n    \"resModel\",\n    \"type\",\n    \"jsClass\",\n\n    \"arch\",\n    \"fields\",\n    \"relatedModels\",\n    \"viewId\",\n    \"views\",\n    \"actionMenus\",\n    \"loadActionMenus\",\n\n    \"searchViewArch\",\n    \"searchViewFields\",\n    \"searchViewId\",\n    \"irFilters\",\n    \"loadIrFilters\",\n\n    \"context\",\n    \"domain\",\n    \"groupBy\",\n    \"orderBy\",\n\n    \"useSampleModel\",\n    \"noContentHelp\",\n    \"className\",\n\n    \"display\",\n    \"globalState\",\n\n    \"activateFavorite\",\n    \"dynamicFilters\",\n    \"hideCustomGroupBy\",\n    \"searchMenuTypes\",\n\n    ...CALLBACK_RECORDER_NAMES,\n\n    // LEGACY: remove this later (clean when mappings old state <-> new state are established)\n    \"searchPanel\",\n    \"searchModel\",\n];\n\nconst ACTIONS = [\"create\", \"delete\", \"edit\", \"group_create\", \"group_delete\", \"group_edit\"];\n\n/** @extends {Component<ViewProps, import(\"@web/env\").OdooEnv>} */\nexport class View extends Component {\n    static _download = async function () {};\n    static template = \"web.View\";\n    static components = { WithSearch };\n    static searchMenuTypes = [\"filter\", \"groupBy\", \"favorite\"];\n    static canOrderByCount = false;\n    static defaultProps = {\n        display: {},\n        context: {},\n        loadActionMenus: false,\n        loadIrFilters: false,\n        className: \"\",\n    };\n    static props = {\n        \"*\": true,\n    };\n\n    setup() {\n        const { arch, fields, resModel, searchViewArch, searchViewFields, type } = this.props;\n        if (!resModel) {\n            throw Error(`View props should have a \"resModel\" key`);\n        }\n        if (!type) {\n            throw Error(`View props should have a \"type\" key`);\n        }\n        if ((arch && !fields) || (!arch && fields)) {\n            throw new Error(`\"arch\" and \"fields\" props must be given together`);\n        }\n        if ((searchViewArch && !searchViewFields) || (!searchViewArch && searchViewFields)) {\n            throw new Error(`\"searchViewArch\" and \"searchViewFields\" props must be given together`);\n        }\n\n        this.viewService = useService(\"view\");\n        this.withSearchProps = null;\n\n        useSubEnv({\n            keepLast: new KeepLast(),\n            config: {\n                ...getDefaultConfig(),\n                ...this.env.config,\n            },\n            ...Object.fromEntries(\n                CALLBACK_RECORDER_NAMES.map((name) => [name, this.props[name] || null])\n            ),\n        });\n\n        this.handleActionLinks = useActionLinks({ resModel });\n\n        onWillStart(() => this.loadView(this.props));\n        onWillUpdateProps((nextProps) => this.onWillUpdateProps(nextProps));\n\n        useDebugCategory(\"view\", { component: this });\n    }\n\n    /**\n     * @param {ViewProps} props\n     */\n    async loadView(props) {\n        const type = props.type;\n\n        if (!session.view_info[type]) {\n            throw new Error(`Invalid view type: ${type}`);\n        }\n\n        // determine views for which descriptions should be obtained\n        let { viewId, searchViewId } = props;\n\n        const views = deepCopy(props.views || this.env.config.views);\n        const view = views.find((v) => v[1] === type) || [];\n        if (view.length) {\n            view[0] = viewId !== undefined ? viewId : view[0];\n            viewId = view[0];\n        } else {\n            view.push(viewId || false, type);\n            views.push(view); // viewId will remain undefined if not specified and loadView=false\n        }\n\n        const searchView = views.find((v) => v[1] === \"search\");\n        if (searchView) {\n            searchView[0] = searchViewId !== undefined ? searchViewId : searchView[0];\n            searchViewId = searchView[0];\n        } else if (searchViewId !== undefined) {\n            views.push([searchViewId, \"search\"]);\n        }\n        // searchViewId will remains undefined if loadSearchView=false\n\n        // prepare view description\n        const { context, resModel, loadActionMenus, loadIrFilters } = props;\n        let {\n            arch,\n            fields,\n            relatedModels,\n            searchViewArch,\n            searchViewFields,\n            irFilters,\n            actionMenus,\n        } = props;\n\n        const loadView = !arch || (!actionMenus && loadActionMenus);\n        const loadSearchView =\n            (searchViewId !== undefined && !searchViewArch) || (!irFilters && loadIrFilters);\n\n        let viewDescription = { viewId, resModel, type };\n        let searchViewDescription;\n        if (loadView || loadSearchView) {\n            // view description (or search view description if required) is incomplete\n            // a loadViews is done to complete the missing information\n            const options = {\n                actionId: this.env.config.actionId,\n                loadActionMenus,\n                loadIrFilters,\n            };\n            if (this.env.config.currentEmbeddedActionId) {\n                options.embeddedActionId = this.env.config.currentEmbeddedActionId;\n                options.embeddedParentResId = context.active_id;\n            }\n            const result = await this.viewService.loadViews({ context, resModel, views }, options);\n            // Note: if props.views is different from views, the cached descriptions\n            // will certainly not be reused! (but for the standard flow this will work as\n            // before)\n            viewDescription = result.views[type];\n            searchViewDescription = result.views.search;\n            if (loadSearchView) {\n                searchViewId = searchViewId || searchViewDescription.id;\n                if (!searchViewArch) {\n                    searchViewArch = searchViewDescription.arch;\n                    searchViewFields = result.fields;\n                }\n                if (!irFilters) {\n                    irFilters = searchViewDescription.irFilters;\n                }\n            }\n            this.env.config.views = views;\n            fields = fields || markRaw(result.fields);\n            relatedModels = relatedModels || markRaw(result.relatedModels);\n        }\n\n        if (!arch) {\n            arch = viewDescription.arch;\n        }\n        if (!actionMenus) {\n            actionMenus = viewDescription.actionMenus;\n        }\n\n        const archXmlDoc = parseXML(arch.replace(/&amp;nbsp;/g, nbsp));\n        for (const action of ACTIONS) {\n            if (action in this.props.context && !this.props.context[action]) {\n                archXmlDoc.setAttribute(action, \"0\");\n            }\n        }\n\n        const jsClass = archXmlDoc.hasAttribute(\"js_class\")\n            ? archXmlDoc.getAttribute(\"js_class\")\n            : props.jsClass || type;\n        if (!viewRegistry.contains(jsClass)) {\n            await loadBundle(\n                cookie.get(\"color_scheme\") === \"dark\"\n                    ? \"web.assets_backend_lazy_dark\"\n                    : \"web.assets_backend_lazy\"\n            );\n        }\n        const descr = viewRegistry.get(jsClass);\n\n        const sample = archXmlDoc.getAttribute(\"sample\");\n        const className = computeViewClassName(type, archXmlDoc, [\n            \"o_view_controller\",\n            ...(props.className || \"\").split(\" \"),\n        ]);\n\n        Object.assign(this.env.config, {\n            rawArch: arch,\n            viewArch: archXmlDoc,\n            viewId: viewDescription.id,\n            viewType: type,\n            viewSubType: jsClass,\n            noBreadcrumbs: props.noBreadcrumbs,\n            ...extractLayoutComponents(descr),\n        });\n        const info = {\n            actionMenus,\n            mode: props.display.mode,\n            irFilters,\n            searchViewArch,\n            searchViewFields,\n            searchViewId,\n        };\n\n        // prepare the view props\n        const viewProps = {\n            info,\n            arch: archXmlDoc,\n            fields,\n            relatedModels,\n            resModel,\n            useSampleModel: false,\n            className,\n        };\n        if (viewDescription.custom_view_id) {\n            // for dashboard\n            viewProps.info.customViewId = viewDescription.custom_view_id;\n        }\n        if (props.globalState) {\n            viewProps.globalState = props.globalState;\n        }\n\n        if (\"useSampleModel\" in props) {\n            viewProps.useSampleModel = props.useSampleModel;\n        } else if (sample) {\n            viewProps.useSampleModel = evaluateBooleanExpr(sample);\n        }\n\n        for (const key in props) {\n            if (!STANDARD_PROPS.includes(key)) {\n                viewProps[key] = props[key];\n            }\n        }\n\n        const { noContentHelp } = props;\n        if (noContentHelp) {\n            viewProps.info.noContentHelp = noContentHelp;\n        }\n\n        const searchMenuTypes =\n            props.searchMenuTypes || descr.searchMenuTypes || this.constructor.searchMenuTypes;\n        const defaultGroupBy = archXmlDoc.hasAttribute(\"default_group_by\")\n            ? archXmlDoc.getAttribute(\"default_group_by\").split(\",\")\n            : null;\n        viewProps.searchMenuTypes = searchMenuTypes;\n        const canOrderByCount = descr.canOrderByCount || this.constructor.canOrderByCount;\n\n        const finalProps = descr.props ? descr.props(viewProps, descr, this.env.config) : viewProps;\n        // prepare the WithSearch component props\n        this.Controller = descr.Controller;\n        this.componentProps = finalProps;\n        this.withSearchProps = {\n            ...toRaw(props),\n            hideCustomGroupBy: props.hideCustomGroupBy || descr.hideCustomGroupBy,\n            searchMenuTypes,\n            canOrderByCount,\n            SearchModel: descr.SearchModel,\n        };\n\n        if (searchViewId !== undefined) {\n            this.withSearchProps.searchViewId = searchViewId;\n        }\n        if (searchViewArch) {\n            this.withSearchProps.searchViewArch = searchViewArch;\n            this.withSearchProps.searchViewFields = searchViewFields;\n        }\n        if (irFilters) {\n            this.withSearchProps.irFilters = irFilters;\n        }\n\n        if (descr.display) {\n            // FIXME: there's something inelegant here: display might come from\n            // the View's defaultProps, in which case, modifying it in place\n            // would have unwanted effects.\n            const viewDisplay = deepCopy(descr.display);\n            const display = { ...this.withSearchProps.display };\n            for (const key in viewDisplay) {\n                if (typeof display[key] === \"object\") {\n                    Object.assign(display[key], viewDisplay[key]);\n                } else if (!(key in display) || display[key]) {\n                    display[key] = viewDisplay[key];\n                }\n            }\n            this.withSearchProps.display = display;\n        }\n\n        if (defaultGroupBy && defaultGroupBy.length) {\n            this.withSearchProps.defaultGroupBy = defaultGroupBy;\n        }\n\n        for (const key in this.withSearchProps) {\n            if (!(key in WithSearch.props)) {\n                delete this.withSearchProps[key];\n            }\n        }\n    }\n\n    /**\n     * @param {ViewProps} nextProps\n     */\n    onWillUpdateProps(nextProps) {\n        const oldProps = pick(this.props, \"arch\", \"type\", \"resModel\");\n        const newProps = pick(nextProps, \"arch\", \"type\", \"resModel\");\n        if (JSON.stringify(oldProps) !== JSON.stringify(newProps)) {\n            return this.loadView(nextProps);\n        }\n        // we assume that nextProps can only vary in the search keys:\n        // context, domain, groupBy, orderBy\n        const { context, domain, groupBy, orderBy } = nextProps;\n        Object.assign(this.withSearchProps, { context, domain, groupBy, orderBy });\n    }\n}\n", "import { ViewButton } from \"./view_button\";\n\nexport class MultiRecordViewButton extends ViewButton {\n    static props = [...ViewButton.props, \"list\", \"domain\"];\n\n    async onClick(ev, newWindow) {\n        const { clickParams, list } = this.props;\n        const resIds = await list.getResIds(true);\n        clickParams.buttonContext = {\n            active_domain: this.props.domain,\n            active_ids: resIds,\n            active_model: list.resModel,\n        };\n\n        this.env.onClickViewButton({\n            clickParams,\n            getResParams: () => ({\n                context: list.context,\n                evalContext: list.evalContext,\n                resModel: list.resModel,\n                resIds,\n            }),\n            newWindow,\n        });\n    }\n}\n", "import { Component } from \"@odoo/owl\";\nimport { useDropdownCloser } from \"@web/core/dropdown/dropdown_hooks\";\nimport { pick } from \"@web/core/utils/objects\";\nimport { debounce as debounceFn } from \"@web/core/utils/timing\";\n\nconst explicitRankClasses = [\n    \"btn-primary\",\n    \"btn-secondary\",\n    \"btn-link\",\n    \"btn-success\",\n    \"btn-info\",\n    \"btn-warning\",\n    \"btn-danger\",\n];\n\nconst odooToBootstrapClasses = {\n    oe_highlight: \"btn-primary\",\n    oe_link: \"btn-link\",\n};\n\nfunction iconFromString(iconString) {\n    const icon = {};\n    if (iconString.startsWith(\"fa-\")) {\n        icon.tag = \"i\";\n        icon.class = `o_button_icon fa fa-fw ${iconString}`;\n    } else if (iconString.startsWith(\"oi-\")) {\n        icon.tag = \"i\";\n        icon.class = `o_button_icon oi oi-fw ${iconString}`;\n    } else {\n        icon.tag = \"img\";\n        icon.src = iconString;\n    }\n    return icon;\n}\n\nexport class ViewButton extends Component {\n    static template = \"web.views.ViewButton\";\n    static props = [\n        \"id?\",\n        \"tag?\",\n        \"record?\",\n        \"attrs?\",\n        \"className?\",\n        \"context?\",\n        \"clickParams?\",\n        \"icon?\",\n        \"defaultRank?\",\n        \"disabled?\",\n        \"size?\",\n        \"tabindex?\",\n        \"title?\",\n        \"style?\",\n        \"string?\",\n        \"slots?\",\n        \"onClick?\",\n    ];\n    static defaultProps = {\n        tag: \"button\",\n        className: \"\",\n        clickParams: {},\n        attrs: {},\n    };\n\n    setup() {\n        if (this.props.icon) {\n            this.icon = iconFromString(this.props.icon);\n        }\n        const { debounce } = this.clickParams;\n        if (debounce) {\n            this.onClick = debounceFn(this.onClick.bind(this), debounce, true);\n        }\n        this.tooltip = JSON.stringify({\n            debug: Boolean(odoo.debug),\n            button: {\n                string: this.props.string,\n                help: this.clickParams.help,\n                context: this.clickParams.context,\n                invisible: this.props.attrs.invisible,\n                column_invisible: this.props.attrs.column_invisible,\n                readonly: this.props.attrs.readonly,\n                required: this.props.attrs.required,\n                special: this.clickParams.special,\n                type: this.clickParams.type,\n                name: this.clickParams.name,\n                title: this.props.title,\n            },\n            context: this.props.record && this.props.record.context,\n            model: this.props.record && this.props.record.resModel,\n        });\n        this.dropdownControl = useDropdownCloser();\n    }\n\n    get clickParams() {\n        return { context: this.props.context, ...this.props.clickParams };\n    }\n\n    get hasBigTooltip() {\n        return Boolean(odoo.debug) || this.clickParams.help;\n    }\n\n    get hasSmallToolTip() {\n        return !this.hasBigTooltip && this.props.title;\n    }\n\n    get disabled() {\n        const { name, type, special } = this.clickParams;\n        return (!name && !type && !special) || this.props.disabled;\n    }\n\n    /**\n     * @param {MouseEvent} ev\n     */\n    onClick(ev, newWindow) {\n        if (this.props.tag === \"a\") {\n            ev.preventDefault();\n        }\n\n        if (this.props.onClick) {\n            return this.props.onClick();\n        }\n\n        this.env.onClickViewButton({\n            clickParams: this.clickParams,\n            getResParams: () =>\n                pick(\n                    this.props.record || {},\n                    \"context\",\n                    \"evalContext\",\n                    \"resModel\",\n                    \"resId\",\n                    \"resIds\"\n                ),\n            beforeExecute: () => this.dropdownControl.close(),\n            newWindow,\n        });\n    }\n\n    getClassName() {\n        const classNames = [];\n        let hasExplicitRank = false;\n        if (this.props.className) {\n            for (let cls of this.props.className.split(\" \")) {\n                if (cls in odooToBootstrapClasses) {\n                    cls = odooToBootstrapClasses[cls];\n                }\n                classNames.push(cls);\n                if (!hasExplicitRank && explicitRankClasses.includes(cls)) {\n                    hasExplicitRank = true;\n                }\n            }\n        }\n        if (this.props.tag === \"button\") {\n            const hasOtherClasses = classNames.length;\n            classNames.unshift(\"btn\");\n            if ((!hasExplicitRank && this.props.defaultRank) || !hasOtherClasses) {\n                classNames.push(this.props.defaultRank || \"btn-secondary\");\n            }\n            if (this.props.size) {\n                classNames.push(`btn-${this.props.size}`);\n            }\n        }\n        return classNames.join(\" \");\n    }\n}\n", "import { useService } from \"@web/core/utils/hooks\";\nimport { evaluateExpr } from \"@web/core/py_js/py\";\nimport { ConfirmationDialog } from \"@web/core/confirmation_dialog/confirmation_dialog\";\n\nimport { status, useComponent, useEnv, useSubEnv } from \"@odoo/owl\";\n\nexport async function executeButtonCallback(el, fct) {\n    let btns = [];\n    function disableButtons() {\n        btns = [\n            ...btns,\n            ...el.querySelectorAll(\"button:not([disabled])\"),\n            ...document.querySelectorAll(\".o-overlay-container button:not([disabled])\"),\n        ];\n        for (const btn of btns) {\n            btn.setAttribute(\"disabled\", \"1\");\n        }\n    }\n\n    function enableButtons() {\n        for (const btn of btns) {\n            btn.removeAttribute(\"disabled\");\n        }\n    }\n\n    disableButtons();\n    let res;\n    try {\n        res = await fct();\n    } finally {\n        enableButtons();\n    }\n    return res;\n}\n\nfunction undefinedAsTrue(val) {\n    return typeof val === \"undefined\" || val;\n}\n\n/**\n * @typedef {Object} Options\n * @property {Function} [afterExecuteAction]\n * @property {Function} [beforeExecuteAction]\n * @property {Function} [reload]\n */\n\n/**\n * @param {{ readonly el: HTMLElement | null; }} ref\n * @param {Options} [options={}]\n */\nexport function useViewButtons(ref, options = {}) {\n    const action = useService(\"action\");\n    const dialog = useService(\"dialog\");\n    const comp = useComponent();\n    const env = useEnv();\n    useSubEnv({\n        async onClickViewButton({ clickParams, getResParams, beforeExecute, newWindow }) {\n            async function execute() {\n                let _continue = true;\n                if (beforeExecute) {\n                    _continue = undefinedAsTrue(await beforeExecute());\n                }\n\n                _continue =\n                    _continue && undefinedAsTrue(await options.beforeExecuteAction?.(clickParams));\n                if (!_continue) {\n                    return;\n                }\n                const closeDialog =\n                    (clickParams.close || clickParams.special) && env.dialogData?.close;\n                const params = getResParams();\n                let buttonContext = {};\n                if (clickParams.context) {\n                    if (typeof clickParams.context === \"string\") {\n                        buttonContext = evaluateExpr(clickParams.context, params.evalContext);\n                    } else {\n                        buttonContext = clickParams.context;\n                    }\n                }\n                if (clickParams.buttonContext) {\n                    Object.assign(buttonContext, clickParams.buttonContext);\n                }\n                const doActionParams = Object.assign({}, clickParams, {\n                    resModel: params.resModel,\n                    resId: params.resId,\n                    resIds: params.resIds,\n                    context: params.context || {},\n                    buttonContext,\n                    onClose: async (onCloseInfo) => {\n                        if (\n                            !closeDialog &&\n                            status(comp) !== \"destroyed\" &&\n                            !onCloseInfo?.noReload\n                        ) {\n                            await options.reload?.();\n                        }\n                    },\n                });\n                let error;\n                try {\n                    await action.doActionButton(doActionParams, { newWindow });\n                } catch (_e) {\n                    error = _e;\n                }\n                await options.afterExecuteAction?.(clickParams);\n                if (closeDialog) {\n                    closeDialog();\n                }\n                if (error) {\n                    return Promise.reject(error);\n                }\n            }\n\n            if (clickParams.confirm) {\n                executeButtonCallback(getEl(), async () => {\n                    await new Promise((resolve) => {\n                        const dialogProps = {\n                            ...(clickParams[\"confirm-title\"] && {\n                                title: clickParams[\"confirm-title\"],\n                            }),\n                            ...(clickParams[\"confirm-label\"] && {\n                                confirmLabel: clickParams[\"confirm-label\"],\n                            }),\n                            ...(clickParams[\"cancel-label\"] && {\n                                cancelLabel: clickParams[\"cancel-label\"],\n                            }),\n                            body: clickParams.confirm,\n                            confirm: () => execute(),\n                            cancel: () => {},\n                        };\n                        dialog.add(ConfirmationDialog, dialogProps, { onClose: resolve });\n                    });\n                });\n            } else {\n                return executeButtonCallback(getEl(), execute);\n            }\n        },\n    });\n\n    function getEl() {\n        if (env.inDialog) {\n            const el = ref.el;\n            return el ? el.closest(\".modal\") : null;\n        } else {\n            return ref.el;\n        }\n    }\n}\n", "import {\n    append,\n    combineAttributes,\n    createElement,\n    createTextNode,\n    getTag,\n} from \"@web/core/utils/xml\";\nimport { toStringExpression, BUTTON_CLICK_PARAMS } from \"./utils\";\n\n/**\n * @typedef Compiler\n * @property {string} selector\n * @property {(el: Element, params: Record<string, any>) => Element} fn\n * @property {string} [class]\n * @property {boolean} [doNotCopyAttributes]\n */\n\nimport { xml } from \"@odoo/owl\";\n\nconst BUTTON_STRING_PROPS = [\"string\", \"size\", \"title\", \"icon\", \"id\", \"disabled\"];\nconst INTERP_REGEXP = /(\\{\\{|#\\{)(.*?)(\\}{1,2})/g;\n\n/**\n * @param {string} str\n * @returns {string} the interpolated string to be injected into a component's node props.\n */\nexport function toInterpolatedStringExpression(str) {\n    const matches = str.matchAll(INTERP_REGEXP);\n    const parts = [];\n    let searchString = str;\n    for (const [match, head, expr] of matches) {\n        const index = searchString.indexOf(head);\n        const left = searchString.slice(0, index);\n        if (left) {\n            parts.push(toStringExpression(left));\n        }\n        parts.push(`(${expr})`);\n        searchString = searchString.slice(index + match.length);\n    }\n    parts.push(toStringExpression(searchString));\n    return parts.join(\"+\");\n}\n\n/**\n * @param {Element} el\n * @param {string} attr\n * @param {string} string\n */\nexport function appendAttr(el, attr, string) {\n    const attrKey = `t-att-${attr}`;\n    const attrVal = el.getAttribute(attrKey);\n    el.setAttribute(attrKey, appendToStringifiedObject(attrVal, string));\n}\n\n/**\n * @param {string} originalTattr\n * @param {string} string\n * @returns {string}\n */\nfunction appendToStringifiedObject(originalTattr, string) {\n    const re = /{(.*)}/;\n    const oldString = re.exec(originalTattr);\n\n    if (oldString) {\n        string = `${oldString[1]},${string}`;\n    }\n    return `{${string}}`;\n}\n\n/**\n * @param {Element} target\n * @param  {...Element} sources\n * @returns {Element}\n */\nexport function assignOwlDirectives(target, ...sources) {\n    for (const source of sources) {\n        for (const { name, value } of source.attributes) {\n            if (name.startsWith(\"t-attf-\")) {\n                const propName = name.slice(7);\n                const interpolatedExpression = toInterpolatedStringExpression(value);\n                target.setAttribute(propName, interpolatedExpression);\n            } else if (name.startsWith(\"t-att-\")) {\n                const propName = name.slice(6);\n                target.setAttribute(propName, value);\n            } else if (name.startsWith(\"t-\")) {\n                target.setAttribute(name, value);\n            }\n        }\n    }\n    return target;\n}\n\n/**\n * @param {Element} el\n * @param {Element} compiled\n */\nexport function copyAttributes(el, compiled) {\n    const isComponent = isComponentNode(compiled);\n    const classes = el.className;\n    if (classes) {\n        if (isComponent) {\n            const cls = compiled.className;\n            compiled.setAttribute(\"class\", cls ? `'${classes} ' + ${cls}` : `'${classes}'`);\n        } else {\n            compiled.classList.add(...classes.split(/\\s+/).filter(Boolean));\n        }\n    }\n\n    let att = el.getAttribute(\"style\");\n    if (att) {\n        if (isComponent) {\n            att = toStringExpression(att);\n        }\n        compiled.setAttribute(\"style\", att);\n    }\n}\n\n/**\n * Decodes a string within an attribute into an Object\n * @param  {string} str\n * @return {Object}\n */\nexport function decodeObjectForTemplate(str) {\n    return JSON.parse(decodeURI(str));\n}\n\n/**\n * Encodes an object into a string usable inside a pre-compiled template\n * @param  {Object}\n * @return {string}\n */\nexport function encodeObjectForTemplate(obj) {\n    return `\"${encodeURI(JSON.stringify(obj))}\"`;\n}\n\n/**\n * @param {Element} el\n * @param {string} modifierName\n * @returns {boolean | boolean[]}\n */\nexport function getModifier(el, modifierName) {\n    return el.getAttribute(modifierName);\n}\n\n/**\n * @param {any} node\n * @returns {string}\n */\nfunction getTitleTag(node) {\n    return getTag(node)[0].toUpperCase() + getTag(node).slice(1);\n}\n\n/**\n * @param {Node} node\n * @returns {boolean}\n */\nfunction isComment(node) {\n    return node.nodeType === 8;\n}\n\n/**\n * @param {Element} el\n * @returns {boolean}\n */\nexport function isComponentNode(el) {\n    return (\n        getTag(el) === getTitleTag(el) ||\n        (getTag(el, true) === \"t\" && \"t-component\" in el.attributes)\n    );\n}\n\n/**\n * @param {Node} node\n * @returns {boolean}\n */\nexport function isTextNode(node) {\n    return node.nodeType === 3;\n}\n\n/**\n * @param {string} title\n * @returns {Element}\n */\nexport function makeSeparator(title) {\n    const separator = createElement(\"div\");\n    separator.className = \"o_horizontal_separator mt-4 mb-3 text-uppercase fw-bolder small\";\n    separator.textContent = title;\n    return separator;\n}\n\nexport class ViewCompiler {\n    constructor(templates) {\n        /** @type {number} */\n        this.id = 1;\n        /** @type {Compiler[]} */\n        this.compilers = [\n            {\n                selector: \"a[type]:not([data-bs-toggle]),a[data-type]:not([data-bs-toggle])\",\n                fn: this.compileButton,\n            },\n            {\n                selector: \"button:not([data-bs-toggle])\",\n                fn: this.compileButton,\n                doNotCopyAttributes: true,\n            },\n            { selector: \"field\", fn: this.compileField },\n            { selector: \"widget\", fn: this.compileWidget },\n        ];\n        this.templates = templates;\n        this.ctx = { readonly: \"__comp__.props.readonly\" };\n\n        this.owlDirectiveRegexesWhitelist = this.constructor.OWL_DIRECTIVE_WHITELIST.map(\n            (d) => new RegExp(d)\n        );\n        this.setup();\n    }\n\n    setup() {}\n\n    /**\n     * @param {any} invisible\n     * @param {Element} compiled\n     * @param {Record<string, any>} params\n     * @returns {Element}\n     */\n    applyInvisible(invisible, compiled, params) {\n        if (!invisible || invisible === \"False\") {\n            return compiled;\n        }\n        if (invisible === \"True\" || invisible === \"1\") {\n            return;\n        }\n        const recordExpr = params.recordExpr || \"__comp__.props.record\";\n        let isVisileExpr = `!__comp__.evaluateBooleanExpr(${JSON.stringify(\n            invisible\n        )},${recordExpr}.evalContextWithVirtualIds)`;\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\n    /**\n     * @param {string} key\n     * @param {Record<string, any>} params\n     * @returns {string}\n     */\n    compile(key, params = {}) {\n        const root = this.templates[key].cloneNode(true);\n        const child = this.compileNode(root, params);\n        const newRoot = createElement(\"t\", child ? [child] : []);\n        newRoot.setAttribute(\"t-translation\", \"off\");\n        return newRoot;\n    }\n\n    /**\n     * @param {Node} node\n     * @param {Record<string, any>} params\n     * @returns {Element | Text | void}\n     */\n    compileNode(node, params = {}, evalInvisible = true) {\n        if (isComment(node)) {\n            return;\n        }\n        if (isTextNode(node)) {\n            return createTextNode(node.nodeValue);\n        }\n\n        if (node.hasAttribute(\"t-translation\")) {\n            node.removeAttribute(\"t-translation\");\n        }\n        this.validateNode(node);\n        let invisible;\n        if (evalInvisible) {\n            invisible = getModifier(node, \"invisible\");\n            if (!params.compileInvisibleNodes && (invisible === \"True\" || invisible === \"1\")) {\n                return;\n            }\n        }\n\n        const compiler = this.compilers.find((cp) => node.matches(cp.selector));\n        let compiledNode;\n        if (compiler) {\n            compiledNode = compiler.fn.call(this, node, params);\n            if (!compiler.doNotCopyAttributes && compiledNode) {\n                copyAttributes(node, compiledNode);\n            }\n        } else {\n            compiledNode = this.compileGenericNode(node, params);\n        }\n\n        if (evalInvisible && compiledNode) {\n            compiledNode = this.applyInvisible(invisible, compiledNode, params);\n        }\n        return compiledNode;\n    }\n\n    //-----------------------------------------------------------------------------\n    // Compilers\n    //-----------------------------------------------------------------------------\n\n    /**\n     * @param {Element} el\n     * @param {Record<string, any>} params\n     * @returns {Element}\n     */\n    compileButton(el, params) {\n        let tag = getTag(el, true);\n        const type = el.getAttribute(\"type\");\n        if (tag === \"a\" && type === \"url\") {\n            tag = \"button\";\n        }\n        const recordExpr = params.recordExpr || \"__comp__.props.record\";\n        const button = createElement(\"ViewButton\", {\n            tag: toStringExpression(tag),\n            record: recordExpr,\n        });\n\n        assignOwlDirectives(button, el);\n\n        combineAttributes(\n            button,\n            \"className\",\n            [toStringExpression(el.className), button.className],\n            \"+` `+\"\n        );\n        el.removeAttribute(\"class\");\n        button.removeAttribute(\"class\");\n\n        const clickParams = {};\n        const attrs = {};\n        for (const { name, value } of el.attributes) {\n            if (BUTTON_CLICK_PARAMS.includes(name)) {\n                clickParams[name] = value;\n            } else if (BUTTON_STRING_PROPS.includes(name)) {\n                button.setAttribute(name, toStringExpression(value));\n            } else if (!name.startsWith(\"t-\")) {\n                attrs[name] = value;\n            }\n        }\n\n        button.setAttribute(\"clickParams\", JSON.stringify(clickParams));\n        button.setAttribute(\"attrs\", JSON.stringify(attrs));\n\n        // Button's body\n        const buttonContent = [];\n        for (const child of el.childNodes) {\n            const compiled = this.compileNode(child, params);\n            if (compiled) {\n                buttonContent.push(compiled);\n            }\n        }\n        if (buttonContent.length) {\n            const contentSlot = createElement(\"t\");\n            contentSlot.setAttribute(\"t-set-slot\", \"contents\");\n            append(button, contentSlot);\n            for (const buttonChild of buttonContent) {\n                append(contentSlot, buttonChild);\n            }\n        }\n        return button;\n    }\n\n    /**\n     * @param {Element} el\n     * @returns {Element}\n     */\n    compileField(el, params) {\n        const fieldName = el.getAttribute(\"name\");\n        const fieldId = el.getAttribute(\"field_id\");\n\n        const field = createElement(\"Field\");\n        const recordExpr = params.recordExpr || \"__comp__.props.record\";\n        field.setAttribute(\"id\", `'${fieldId}'`);\n        field.setAttribute(\"name\", `'${fieldName}'`);\n        field.setAttribute(\"record\", recordExpr);\n        field.setAttribute(\"fieldInfo\", `__comp__.props.archInfo.fieldNodes['${fieldId}']`);\n        field.setAttribute(\"readonly\", `__comp__.props.readonly`);\n\n        if (el.hasAttribute(\"widget\")) {\n            field.setAttribute(\"type\", `'${el.getAttribute(\"widget\")}'`);\n        }\n\n        return field;\n    }\n\n    /**\n     * @param {Element} el\n     * @param {Record<string, any>} params\n     * @returns {Element}\n     */\n    compileGenericNode(el, params) {\n        const compiled = createElement(el.nodeName.toLowerCase());\n        const metaAttrs = [\"column_invisible\", \"invisible\", \"readonly\", \"required\"];\n        for (const attr of el.attributes) {\n            if (metaAttrs.includes(attr.name)) {\n                continue;\n            }\n            compiled.setAttribute(attr.name, attr.value);\n        }\n        for (const child of el.childNodes) {\n            append(compiled, this.compileNode(child, params));\n        }\n        if (el.hasAttribute(\"t-foreach\") && !el.hasAttribute(\"t-key\")) {\n            compiled.setAttribute(\"t-key\", `${el.getAttribute(\"t-as\")}_index`);\n            console.warn(`Missing attribute \"t-key\" in \"t-foreach\" statement.`);\n        }\n        return compiled;\n    }\n\n    /**\n     * @param {Element} el\n     * @returns {Element}\n     */\n    compileWidget(el) {\n        const widgetId = el.getAttribute(\"widget_id\");\n        const props = { record: \"__comp__.props.record\" };\n        if (el.hasAttribute(\"name\")) {\n            props.name = `'${el.getAttribute(\"name\")}'`;\n        }\n        if (el.hasAttribute(\"class\")) {\n            props.className = `'${el.getAttribute(\"class\")}'`;\n        }\n        props.widgetInfo = `__comp__.props.archInfo.widgetNodes['${widgetId}']`;\n        const widget = createElement(\"Widget\", props);\n        return assignOwlDirectives(widget, el);\n    }\n\n    validateNode(node) {\n        // detect attributes not in whitelist, starting with t-\n        const attributes = Object.values(node.attributes).map((attr) => attr.name);\n        const regexes = this.owlDirectiveRegexesWhitelist;\n        for (const attr of attributes) {\n            if (attr.startsWith(\"t-\") && !regexes.some((regex) => regex.test(attr))) {\n                console.warn(`Forbidden directive ${attr} used in arch`);\n            }\n        }\n    }\n}\nViewCompiler.OWL_DIRECTIVE_WHITELIST = [];\n\nlet templateCache = Object.create(null);\n/**\n * @param {typeof ViewCompiler} ViewCompiler\n * @param {string} key\n * @param {Record<string, Element>} templates\n * @param {Record<string, any>} [params]\n * @returns {Record<string, string>}\n */\nexport function useViewCompiler(ViewCompiler, templates, params) {\n    const compiledTemplates = {};\n    let compiler;\n    for (const tname in templates) {\n        const key = `${ViewCompiler.name}/${templates[tname].outerHTML}`;\n        if (!templateCache[key]) {\n            compiler = compiler || new ViewCompiler(templates);\n            templateCache[key] = xml`${compiler.compile(tname, params).outerHTML}`;\n        }\n        compiledTemplates[tname] = templateCache[key];\n    }\n    return compiledTemplates;\n}\n\n/*\n * clear the view compiler's cache.\n * FIXME: that function only purges the compiler's cache and NOT the cache in owl's app.\n * the owl.xml function creates an internal template each time, so the cache is here to prevent\n * creating new owl templates every time. If we clear the cache, new templates WILL be created,\n * even if the arch to compile is the same.\n * This is how a memory leak occurs. :-)\n */\nexport function resetViewCompilerCache() {\n    templateCache = Object.create(null);\n}\n", "import { browser } from \"@web/core/browser/browser\";\nimport { formatInteger, formatMonetary } from \"@web/views/fields/formatters\";\n\nimport { Component, onWillUnmount, onWillUpdateProps, useState } from \"@odoo/owl\";\nimport { usePopover } from \"@web/core/popover/popover_hook\";\nimport { user } from \"@web/core/user\";\nimport { MultiCurrencyPopover } from \"@web/views/view_components/multi_currency_popover\";\n\nexport class AnimatedNumber extends Component {\n    static template = \"web.AnimatedNumber\";\n    static props = {\n        value: Number,\n        duration: Number,\n        animationClass: { type: String, optional: true },\n        currencies: { type: Array, optional: true },\n        title: { type: String, optional: true },\n        slots: {\n            type: Object,\n            shape: {\n                prefix: { type: Object, optional: true },\n            },\n            optional: true,\n        },\n    };\n    static enableAnimations = true;\n\n    setup() {\n        this.state = useState({ value: this.props.value });\n        this.handle = null;\n        this.multiCurrencyPopover = usePopover(MultiCurrencyPopover, {\n            position: \"right\",\n        });\n        onWillUpdateProps((nextProps) => {\n            const { value: from } = this.props;\n            const { value: to, duration } = nextProps;\n            if (!this.constructor.enableAnimations || !duration || to <= from) {\n                browser.cancelAnimationFrame(this.handle);\n                this.state.value = to;\n                return;\n            }\n            const startTime = Date.now();\n            const animate = () => {\n                const progress = (Date.now() - startTime) / duration;\n                if (progress >= 1) {\n                    this.state.value = to;\n                } else {\n                    this.state.value = from + (to - from) * progress;\n                    this.handle = browser.requestAnimationFrame(animate);\n                }\n            };\n            browser.cancelAnimationFrame(this.handle);\n            animate();\n        });\n        onWillUnmount(() => browser.cancelAnimationFrame(this.handle));\n    }\n\n    format(value) {\n        if (this.currencyId) {\n            return formatMonetary(value, {\n                currencyId: this.currencyId,\n                humanReadable: true,\n                digits: [null, 0],\n                minDigits: 3,\n            });\n        }\n        return formatInteger(value, { humanReadable: true, minDigits: 3 });\n    }\n\n    openMultiCurrencyPopover(ev) {\n        if (!this.multiCurrencyPopover.isOpen) {\n            this.multiCurrencyPopover.open(ev.target, {\n                currencyIds: this.props.currencies,\n                target: ev.target,\n                value: this.props.value,\n            });\n        }\n    }\n\n    get currencyId() {\n        const { currencies } = this.props;\n        if (currencies?.length) {\n            return currencies.length > 1 ? user.activeCompany.currency_id : currencies[0];\n        }\n        return false;\n    }\n}\n", "import { Component } from \"@odoo/owl\";\nimport { AnimatedNumber } from \"./animated_number\";\n\nexport class ColumnProgress extends Component {\n    static components = {\n        AnimatedNumber,\n    };\n    static template = \"web.ColumnProgress\";\n    static props = {\n        aggregate: { type: Object },\n        group: { type: Object },\n        onBarClicked: { type: Function, optional: true },\n        progressBar: { type: Object },\n    };\n    static defaultProps = {\n        onBarClicked: () => {},\n    };\n\n    async onBarClick(bar) {\n        await this.props.onBarClicked(bar);\n    }\n}\n", "import { Component } from \"@odoo/owl\";\nimport { ConfirmationDialog } from \"@web/core/confirmation_dialog/confirmation_dialog\";\nimport { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { isRelational } from \"@web/model/relational_model/utils\";\nimport { FormViewDialog } from \"@web/views/view_dialogs/form_view_dialog\";\n\nexport class GroupConfigMenu extends Component {\n    static template = \"web.GroupConfigMenu\";\n    static components = { Dropdown, DropdownItem };\n    static props = {\n        activeActions: { type: Object },\n        configItems: { type: Object },\n        deleteGroup: { type: Function },\n        dialogClose: { type: Array },\n        group: { type: Object },\n        list: { type: Object },\n    };\n    setup() {\n        this.dialog = useService(\"dialog\");\n    }\n\n    get configItems() {\n        const args = { permissions: this.permissions };\n        return this.props.configItems.map(([key, desc]) => ({\n            key,\n            label: desc.label,\n            class: typeof desc.class === \"function\" ? desc.class(args) : desc.class,\n            icon: desc.icon,\n            isVisible: typeof desc.isVisible === \"function\" ? desc.isVisible(args) : desc.isVisible,\n            method: typeof desc.method === \"function\" ? desc.method : this[desc.method].bind(this),\n        }));\n    }\n\n    get group() {\n        return this.props.group;\n    }\n\n    get permissions() {\n        return [\"canDeleteGroup\", \"canEditGroup\"].reduce((o, key) => {\n            Object.defineProperty(o, key, { get: () => this[key]() });\n            return o;\n        }, {});\n    }\n\n    deleteGroup() {\n        this.dialog.add(ConfirmationDialog, {\n            body: _t(\"Are you sure you want to delete this column?\"),\n            confirm: () => this.props.deleteGroup(this.group),\n            confirmLabel: _t(\"Delete\"),\n            cancel: () => {},\n        });\n    }\n\n    editGroup() {\n        const { context, displayName, groupByField, value } = this.group;\n        this.props.dialogClose.push(\n            this.dialog.add(FormViewDialog, {\n                context,\n                resId: value,\n                resModel: groupByField.relation,\n                title: _t(\"Edit: %s\", displayName),\n                onRecordSaved: () => this.props.list.load(),\n            })\n        );\n    }\n\n    canDeleteGroup() {\n        const { deleteGroup } = this.props.activeActions;\n        const { groupByField, value } = this.group;\n        return deleteGroup && isRelational(groupByField) && value;\n    }\n\n    canEditGroup() {\n        const { editGroup } = this.props.activeActions;\n        const { groupByField, value } = this.group;\n        return editGroup && isRelational(groupByField) && value;\n    }\n}\n\nconst groupConfigItems = registry.category(\"group_config_items\");\ngroupConfigItems.add(\n    \"edit_group\",\n    {\n        label: _t(\"Edit\"),\n        isVisible: ({ permissions }) => permissions.canEditGroup,\n        class: \"o_group_edit\",\n        icon: \"fa-pencil\",\n        method: \"editGroup\",\n    },\n    { sequence: 20 }\n);\ngroupConfigItems.add(\n    \"delete_group\",\n    {\n        label: _t(\"Delete\"),\n        isVisible: ({ permissions }) => permissions.canDeleteGroup,\n        class: \"o_group_delete text-danger\",\n        icon: \"fa-trash\",\n        method: \"deleteGroup\",\n    },\n    { sequence: 30 }\n);\n", "import { Component } from \"@odoo/owl\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { TimePicker } from \"@web/core/time_picker/time_picker\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { Record } from \"@web/model/record\";\nimport { useCallbackRecorder } from \"@web/search/action_hook\";\nimport { FormRenderer } from \"@web/views/form/form_renderer\";\n\nexport class MultiCreatePopover extends Component {\n    static template = \"web.MultiCreatePopover\";\n    static components = {\n        FormRenderer,\n        Record,\n        TimePicker,\n    };\n    static props = {\n        close: Function,\n        multiCreateArchInfo: Object,\n        multiCreateRecordProps: Object,\n        onAdd: Function,\n        callbackRecorder: Object,\n        timeRange: { type: [Object, { value: null }] },\n    };\n\n    setup() {\n        this.notification = useService(\"notification\");\n\n        this.multiCreateData = {\n            timeRange: this.props.timeRange && { ...this.props.timeRange },\n        };\n        this.multiCreateArchInfo = this.props.multiCreateArchInfo;\n        this.multiCreateRecordProps = {\n            ...this.props.multiCreateRecordProps,\n            hooks: {\n                onRootLoaded: (record) => {\n                    this.multiCreateData.record = record;\n                },\n            },\n        };\n\n        useCallbackRecorder(this.props.callbackRecorder, () => this.multiCreateData);\n    }\n\n    setMultiCreateTimeRange(timeRange) {\n        Object.assign(this.multiCreateData.timeRange, timeRange);\n    }\n\n    async isValidMultiCreateData() {\n        const isValid = await this.multiCreateData.record.checkValidity({\n            displayNotification: true,\n        });\n        if (!isValid) {\n            return false;\n        }\n        if (this.multiCreateData.timeRange) {\n            const { start, end } = this.multiCreateData.timeRange;\n            if (!start || !end) {\n                this.notification.add(_t(\"Invalid time range\"), {\n                    title: \"User Error\",\n                    type: \"warning\",\n                });\n                return false;\n            }\n            if (\n                luxon.DateTime.fromObject(start.toObject()) >\n                luxon.DateTime.fromObject(end.toObject())\n            ) {\n                this.notification.add(_t(\"Start time should be before end time\"), {\n                    title: \"User Error\",\n                    type: \"warning\",\n                });\n                return false;\n            }\n        }\n        return true;\n    }\n\n    async onAdd() {\n        const isValid = await this.isValidMultiCreateData();\n        if (isValid) {\n            this.props.onAdd(this.multiCreateData);\n            this.props.close();\n        }\n    }\n}\n", "import { Component, onWillStart, useExternalListener, useState } from \"@odoo/owl\";\nimport { getCurrency, getCurrencyRates } from \"@web/core/currency\";\nimport { user } from \"@web/core/user\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { formatMonetary } from \"../fields/formatters\";\n\nexport class MultiCurrencyPopover extends Component {\n    static template = \"web.MultiCurrencyPopover\";\n    static props = {\n        close: Function,\n        currencyIds: Array,\n        target: HTMLElement,\n        value: Number,\n    };\n\n    setup() {\n        this.orm = useService(\"orm\");\n        this.defaultCurrency = user.activeCompany.currency_id;\n        this.state = useState({ rates: null });\n        onWillStart(async () => {\n            this.state.rates = await getCurrencyRates();\n        });\n        useExternalListener(window, \"mouseover\", (ev) => {\n            if (ev.target !== this.props.target) {\n                this.props.close();\n            }\n        });\n    }\n\n    get currencies() {\n        return this.props.currencyIds.reduce((currencies, currencyId) => {\n            if (currencyId !== this.defaultCurrency) {\n                currencies.push({\n                    ...getCurrency(currencyId),\n                    id: currencyId,\n                    rate: this.state.rates[currencyId],\n                    value: this.props.value / this.state.rates[currencyId],\n                });\n            }\n            return currencies;\n        }, []);\n    }\n\n    formatedValue(value, currencyId) {\n        return formatMonetary(value, { currencyId });\n    }\n}\n", "import { Component, onWillRender, toRaw, useEffect, useRef, useState } from \"@odoo/owl\";\nimport { browser } from \"@web/core/browser/browser\";\nimport { ConfirmationDialog } from \"@web/core/confirmation_dialog/confirmation_dialog\";\nimport { useHotkey } from \"@web/core/hotkeys/hotkey_hook\";\nimport { Time } from \"@web/core/l10n/time\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { usePopover } from \"@web/core/popover/popover_hook\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { parseXML } from \"@web/core/utils/xml\";\nimport { extractFieldsFromArchInfo } from \"@web/model/relational_model/utils\";\nimport { CallbackRecorder, useSetupAction } from \"@web/search/action_hook\";\nimport { FormArchParser } from \"@web/views/form/form_arch_parser\";\nimport { MultiCreatePopover } from \"./multi_create_popover\";\n\nexport class MultiSelectionButtons extends Component {\n    static template = \"web.MultiSelectionButtons\";\n    static props = {\n        reactive: {\n            type: Object,\n            shape: {\n                onAdd: Function,\n                onCancel: Function,\n                onDelete: Function,\n                nbSelected: Number,\n                multiCreateView: String,\n                resModel: String,\n                context: Object,\n                showMultiCreateTimeRange: Boolean,\n                visible: Boolean,\n                multiCreateValues: { type: Object, optional: true },\n            },\n        },\n    };\n    static components = {\n        Popover: MultiCreatePopover,\n    };\n\n    setup() {\n        this.viewService = useService(\"view\");\n        this.dialogService = useService(\"dialog\");\n        this.state = useState({ isReady: false });\n        onWillRender(() => {\n            if (this.props.reactive.visible && !this.state.isReady) {\n                this.loadMultiCreateView().then(() => {\n                    this.state.isReady = true;\n                });\n            }\n        });\n\n        this.multiCreateValues = this.props.reactive.multiCreateValues;\n        this.callbackRecorder = new CallbackRecorder();\n        useSetupAction({\n            getLocalState: () => {\n                const multiCreateData = this.getMultiCreateDataFromPopover();\n                if (multiCreateData) {\n                    this.storeMultiCreateData(multiCreateData);\n                }\n                return { multiCreateValues: this.multiCreateValues };\n            },\n        });\n\n        this.multiCreatePopover = usePopover(this.constructor.components.Popover, {\n            onClose: () => {\n                const multiCreateData = this.getMultiCreateDataFromPopover();\n                if (multiCreateData) {\n                    this.storeMultiCreateData(multiCreateData);\n                }\n            },\n        });\n        this.addButtonRef = useRef(\"addButton\");\n\n        const rootRef = useRef(\"root\");\n        useEffect(\n            (el) => {\n                if (!el) {\n                    return;\n                }\n                // @ts-ignore\n                const { width: parentWidth } = el.parentElement.getBoundingClientRect();\n                const { width } = el.getBoundingClientRect();\n                const left = Math.floor((parentWidth - width) / 2);\n                el.style.setProperty(\"left\", `${left}px`);\n            },\n            () => [rootRef.el]\n        );\n\n        useHotkey(\"escape\", () => {\n            if (this.props.reactive.visible) {\n                this.props.reactive.onCancel();\n            }\n        });\n    }\n\n    getMultiCreateDataFromPopover() {\n        const fn = this.callbackRecorder.callbacks[0];\n        return fn?.() || null;\n    }\n\n    storeMultiCreateData(multiCreateData) {\n        this.storeTimeRange(multiCreateData.timeRange);\n        this.multiCreateValues = this.computeValues(multiCreateData.record);\n    }\n\n    async loadMultiCreateView() {\n        // todo: accept variable context,... ?\n        const { context, resModel, multiCreateView } = this.props.reactive;\n        const { fields, relatedModels, views } = await this.viewService.loadViews({\n            context: { ...context, form_view_ref: multiCreateView },\n            resModel,\n            views: [[false, \"form\"]],\n        });\n        const parser = new FormArchParser();\n        const arch = views.form.arch;\n        this.multiCreateArchInfo = parser.parse(parseXML(arch), relatedModels, resModel);\n        const { activeFields } = extractFieldsFromArchInfo(this.multiCreateArchInfo, fields);\n        this.multiCreateRecordProps = { resModel, fields, activeFields, context };\n    }\n\n    getMultiCreatePopoverProps() {\n        return {\n            timeRange: this.props.reactive.showMultiCreateTimeRange ? this.getTimeRange() : null,\n            multiCreateArchInfo: { ...this.multiCreateArchInfo },\n            multiCreateRecordProps: {\n                ...this.multiCreateRecordProps,\n                values: this.multiCreateValues,\n            },\n            onAdd: (multiCreateData) => {\n                this.storeMultiCreateData(multiCreateData);\n                this.props.reactive.onAdd(multiCreateData);\n            },\n            callbackRecorder: this.callbackRecorder,\n        };\n    }\n\n    getTimeRange() {\n        return {\n            start: new Time(this.getItemFromStorage(\"timeRange_start\", { hour: 12, minute: 0 })),\n            end: new Time(this.getItemFromStorage(\"timeRange_end\", { hour: 13, minute: 0 })),\n        };\n    }\n\n    generateLocalStorageKey(key) {\n        const { resModel } = this.props.reactive;\n        return `multiCreate_${key}_${resModel}`;\n    }\n\n    getItemFromStorage(key, defaultValue) {\n        const item = browser.localStorage.getItem(this.generateLocalStorageKey(key));\n        try {\n            return item ? JSON.parse(item) : defaultValue;\n        } catch {\n            return defaultValue;\n        }\n    }\n\n    setItemInStorage(key, value) {\n        browser.localStorage.setItem(this.generateLocalStorageKey(key), JSON.stringify(value));\n    }\n\n    storeTimeRange(timeRange) {\n        if (timeRange?.start) {\n            this.setItemInStorage(\"timeRange_start\", timeRange.start);\n        }\n        if (timeRange?.end) {\n            this.setItemInStorage(\"timeRange_end\", timeRange.end);\n        }\n    }\n\n    computeValues(record) {\n        const multiCreateFormRecord = toRaw(record);\n        const values = Object.assign({}, multiCreateFormRecord.data);\n        for (const [fieldName, data] of Object.entries(multiCreateFormRecord.data)) {\n            if ([\"one2many\", \"many2many\"].includes(multiCreateFormRecord.fields[fieldName].type)) {\n                values[fieldName] = data.records.map((record) =>\n                    Object.assign({ id: record.resId }, record.data)\n                );\n            }\n        }\n        return values;\n    }\n\n    onAdd() {\n        if (this.multiCreatePopover.isOpen) {\n            return;\n        }\n        this.multiCreatePopover.open(this.addButtonRef.el, this.getMultiCreatePopoverProps());\n    }\n\n    onDelete() {\n        const body =\n            this.props.reactive.nbSelected === 1\n                ? _t(\"Are you sure you want to delete the selected record?\")\n                : _t(\"Are you sure you want to delete the %(nbSelected)s selected records?\", {\n                      nbSelected: this.props.reactive.nbSelected,\n                  });\n        this.dialogService.add(ConfirmationDialog, {\n            body,\n            confirm: async () => {\n                this.props.reactive.onDelete();\n            },\n            cancel: () => {},\n        });\n    }\n}\n", "import { Component } from \"@odoo/owl\";\nimport { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\n\nexport class ReportViewMeasures extends Component {\n    static template = \"web.ReportViewMeasures\";\n    static components = {\n        Dropdown,\n        DropdownItem,\n    };\n    static props = {\n        measures: true,\n        activeMeasures: { type: Array, optional: true },\n        onMeasureSelected: { type: Function, optional: true },\n    };\n}\n", "import { Component } from \"@odoo/owl\";\n\nexport class SelectionBox extends Component {\n    static components = {};\n    static template = \"web.SelectionBox\";\n    static props = {\n        root: { type: Object },\n    };\n    setup() {\n        this.root = this.props.root;\n    }\n    get nbSelected() {\n        return this.selectedRecords.length;\n    }\n    get nbTotal() {\n        return this.root.isGrouped ? this.root.recordCount : this.root.count;\n    }\n    get hasLimitedCount() {\n        return this.root.hasLimitedCount;\n    }\n    get isDomainSelected() {\n        return this.root.isDomainSelected;\n    }\n    get isPageSelected() {\n        return (\n            this.nbSelected === this.root.records.length &&\n            (!this.isRecordCountTrustable || this.nbTotal > this.selectedRecords.length)\n        );\n    }\n    get isRecordCountTrustable() {\n        return this.root.isRecordCountTrustable;\n    }\n    get selectedRecords() {\n        return this.root.selection;\n    }\n    onUnselectAll() {\n        this.selectedRecords.forEach((record) => {\n            record.toggleSelection(false);\n        });\n        this.root.selectDomain(false);\n    }\n    onSelectDomain() {\n        this.root.selectDomain(true);\n    }\n}\n", "import { Component } from \"@odoo/owl\";\nimport { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\n\nexport class ViewScaleSelector extends Component {\n    static components = {\n        Dropdown,\n        DropdownItem,\n    };\n    static template = \"web.ViewScaleSelector\";\n    static props = {\n        scales: { type: Object },\n        currentScale: { type: String },\n        isWeekendVisible: { type: Boolean, optional: true },\n        setScale: { type: Function },\n        toggleWeekendVisibility: { type: Function, optional: true },\n        dropdownClass: { type: String, optional: true },\n    };\n    get scales() {\n        return Object.entries(this.props.scales).map(([key, value]) => ({ key, ...value }));\n    }\n}\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { browser } from \"@web/core/browser/browser\";\nimport { CheckBox } from \"@web/core/checkbox/checkbox\";\nimport { Dialog } from \"@web/core/dialog/dialog\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { unique } from \"@web/core/utils/arrays\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { fuzzyLookup } from \"@web/core/utils/search\";\nimport { useSortable } from \"@web/core/utils/sortable_owl\";\nimport { useDebounced } from \"@web/core/utils/timing\";\n\nimport { Component, useRef, useState, onMounted, onWillStart, onWillUnmount } from \"@odoo/owl\";\n\nclass DeleteExportListDialog extends Component {\n    static components = { Dialog };\n    static template = \"web.DeleteExportListDialog\";\n    static props = {\n        text: String,\n        close: Function,\n        delete: Function,\n    };\n    async onDelete() {\n        await this.props.delete();\n        this.props.close();\n    }\n}\n\nclass ExportDataItem extends Component {\n    static template = \"web.ExportDataItem\";\n    static components = { ExportDataItem };\n    static props = {\n        exportList: { type: Object, optional: true },\n        field: { type: Object, optional: true },\n        filterSubfields: Function,\n        isDebug: Boolean,\n        isExpanded: Boolean,\n        isFieldExpandable: Function,\n        onAdd: Function,\n        loadFields: Function,\n    };\n\n    setup() {\n        this.state = useState({\n            subfields: [],\n        });\n        onWillStart(() => {\n            if (this.props.isExpanded) {\n                // automatically expand the item when subfields are already loaded\n                // and display subfields that match the search string\n                return this.toggleItem(this.props.field.id, false);\n            }\n        });\n    }\n\n    async toggleItem(id, isUserToggle) {\n        if (this.props.isFieldExpandable(id)) {\n            if (this.state.subfields.length) {\n                this.state.subfields = [];\n            } else {\n                const subfields = await this.props.loadFields(id, !isUserToggle);\n                if (subfields) {\n                    this.state.subfields = isUserToggle\n                        ? subfields\n                        : this.props.filterSubfields(subfields);\n                } else {\n                    this.state.subfields = [];\n                }\n            }\n        }\n    }\n\n    onDoubleClick(id) {\n        if (!this.props.isFieldExpandable(id) && !this.isFieldSelected(id)) {\n            this.props.onAdd(id);\n        }\n    }\n\n    isFieldSelected(current) {\n        return this.props.exportList.find(({ id }) => id === current);\n    }\n}\n\nexport class ExportDataDialog extends Component {\n    static template = \"web.ExportDataDialog\";\n    static components = { CheckBox, Dialog, ExportDataItem };\n    static props = {\n        close: { type: Function },\n        context: { type: Object, optional: true },\n        defaultExportList: { type: Array },\n        download: { type: Function },\n        getExportedFields: { type: Function },\n        root: { type: Object },\n    };\n\n    setup() {\n        this.dialog = useService(\"dialog\");\n        this.notification = useService(\"notification\");\n        this.orm = useService(\"orm\");\n        this.draggableRef = useRef(\"draggable\");\n        this.exportListRef = useRef(\"exportList\");\n        this.searchRef = useRef(\"search\");\n\n        this.knownFields = {};\n        this.expandedFields = {};\n        this.availableFormats = [];\n        this.templates = [];\n        this.isCompatible = false;\n\n        this.state = useState({\n            exportList: [],\n            isEditingTemplate: false,\n            search: [],\n            selectedFormat: 0,\n            templateId: null,\n            isSmall: this.env.isSmall,\n            disabled: false,\n        });\n\n        this.newTemplateText = _t(\"New template\");\n        this.removeFieldText = _t(\"Remove field\");\n\n        this.debouncedOnResize = useDebounced(this.updateSize, 300);\n\n        useSortable({\n            // Params\n            ref: this.draggableRef,\n            elements: \".o_export_field\",\n            enable: !this.state.isSmall,\n            cursor: \"grabbing\",\n            // Hooks\n            onDrop: async ({ element, previous, next }) => {\n                const indexes = [element, previous, next].map(\n                    (e) =>\n                        e &&\n                        Object.values(this.state.exportList).findIndex(\n                            ({ id }) => id === e.dataset.field_id\n                        )\n                );\n                let target;\n                if (indexes[0] < indexes[1]) {\n                    target = previous ? indexes[1] : 0;\n                } else {\n                    target = next ? indexes[2] : this.state.exportList.length - 1;\n                }\n                this.onDraggingEnd(indexes[0], target);\n            },\n        });\n\n        onWillStart(async () => {\n            this.availableFormats = await rpc(\"/web/export/formats\");\n            this.templates = await this.orm.searchRead(\n                \"ir.exports\",\n                [[\"resource\", \"=\", this.props.root.resModel]],\n                [],\n                {\n                    context: this.props.context,\n                }\n            );\n            await this.fetchFields();\n        });\n\n        onMounted(() => {\n            browser.addEventListener(\"resize\", this.debouncedOnResize);\n            this.updateSize();\n        });\n\n        onWillUnmount(() => browser.removeEventListener(\"resize\", this.debouncedOnResize));\n    }\n\n    get fieldsAvailable() {\n        if (this.searchRef.el && this.searchRef.el.value) {\n            return this.state.search.length && Object.values(this.state.search);\n        }\n        return Object.values(this.knownFields);\n    }\n\n    get isDebug() {\n        return Boolean(odoo.debug);\n    }\n\n    get rootFields() {\n        if (this.searchRef.el && this.searchRef.el.value) {\n            const rootFromSearchResults = this.fieldsAvailable.map((f) => {\n                if (f.parent) {\n                    const parentEl = this.knownFields[f.parent.id];\n                    return this.knownFields[parentEl.parent ? parentEl.parent.id : parentEl.id];\n                }\n                return this.knownFields[f.id];\n            });\n            return unique(rootFromSearchResults);\n        }\n        return this.fieldsAvailable.filter(({ parent }) => !parent);\n    }\n\n    filterSubfields(subfields) {\n        let subfieldsFromSearchResults = [];\n        let searchResults;\n        if (this.searchRef.el && this.searchRef.el.value) {\n            searchResults = this.lookup(this.searchRef.el.value);\n        }\n        const fieldsAvailable = Object.values(searchResults || this.knownFields);\n        if (this.searchRef.el && this.searchRef.el.value) {\n            subfieldsFromSearchResults = fieldsAvailable\n                .filter((f) => f.parent && this.knownFields[f.parent.id].parent)\n                .map((f) => f.parent);\n        }\n        const availableSubFields = unique([...fieldsAvailable, ...subfieldsFromSearchResults]);\n        return subfields.filter((a) => availableSubFields.some((b) => a.id === b.id));\n    }\n\n    updateSize() {\n        this.state.isSmall = this.env.isSmall;\n    }\n\n    /**\n     * Load fields to display and (re)set the list of available fields\n     */\n    async fetchFields() {\n        this.knownFields = {};\n        this.expandedFields = {};\n        await this.loadFields();\n        await this.setDefaultExportList();\n        this.state.search = [];\n        if (this.searchRef.el) {\n            this.searchRef.el.value = \"\";\n        }\n        if (this.state.templateId) {\n            this.loadExportList(this.state.templateId);\n        }\n    }\n\n    enterTemplateEdition() {\n        if (this.state.templateId && !this.state.isEditingTemplate) {\n            this.state.isEditingTemplate = true;\n        }\n    }\n\n    isFieldExpandable(id) {\n        return this.knownFields[id].children && id.split(\"/\").length < 3;\n    }\n\n    async loadExportList(value) {\n        this.state.templateId = value === \"new_template\" ? value : Number(value);\n        this.state.isEditingTemplate = value === \"new_template\";\n        if (!value || value === \"new_template\") {\n            return;\n        }\n        const fields = await rpc(\"/web/export/namelist\", {\n            model: this.props.root.resModel,\n            export_id: Number(value),\n        });\n        // Don't safe the result in this.knownFields because, the result is only partial\n        this.state.exportList = fields;\n    }\n\n    async loadFields(id, preventLoad = false) {\n        let parentField, parentParams;\n        if (id) {\n            if (this.expandedFields[id]) {\n                // we don't make a new RPC if the value is already known\n                return this.expandedFields[id].fields;\n            }\n            parentField = this.knownFields[id];\n            parentParams = {\n                ...parentField.params,\n                parent_field_type: parentField.field_type,\n                parent_field: parentField,\n                parent_name: parentField.string,\n                exclude: [parentField.relation_field],\n            };\n        }\n        if (preventLoad) {\n            return;\n        }\n        const fields = await this.props.getExportedFields(this.isCompatible, parentParams);\n        for (const field of fields) {\n            field.parent = parentField;\n            if (!this.knownFields[field.id]) {\n                this.knownFields[field.id] = field;\n            }\n        }\n        if (id) {\n            this.expandedFields[id] = { fields };\n        }\n        return fields;\n    }\n\n    onDraggingEnd(item, target) {\n        this.state.exportList.splice(target, 0, this.state.exportList.splice(item, 1)[0]);\n    }\n\n    onAddItemExportList(fieldId) {\n        this.state.exportList.push(this.knownFields[fieldId]);\n        this.enterTemplateEdition();\n    }\n\n    onRemoveItemExportList(fieldId) {\n        const item = this.state.exportList.findIndex(({ id }) => id === fieldId);\n        this.state.exportList.splice(item, 1);\n        this.enterTemplateEdition();\n    }\n\n    async onChangeExportList(ev) {\n        this.loadExportList(ev.target.value);\n    }\n\n    async onSaveExportTemplate() {\n        const name = this.exportListRef.el.value;\n        if (!name) {\n            return this.notification.add(_t(\"Please enter save field list name\"), {\n                type: \"danger\",\n            });\n        }\n        const [id] = await this.orm.create(\n            \"ir.exports\",\n            [\n                {\n                    name,\n                    export_fields: this.state.exportList.map((field) => [\n                        0,\n                        0,\n                        {\n                            name: field.id,\n                        },\n                    ]),\n                    resource: this.props.root.resModel,\n                },\n            ],\n            { context: this.props.context }\n        );\n        this.state.isEditingTemplate = false;\n        this.state.templateId = id;\n        this.templates.push({ id, name });\n    }\n\n    onCancelExportTemplate() {\n        this.state.isEditingTemplate = false;\n        if (this.state.templateId === \"new_template\") {\n            this.state.templateId = null;\n            return;\n        }\n        this.loadExportList(this.state.templateId);\n    }\n\n    async onClickExportButton() {\n        if (!this.state.exportList.length) {\n            return this.notification.add(_t(\"Please select fields to save export list...\"), {\n                type: \"danger\",\n            });\n        }\n        this.state.disabled = true;\n        await this.props.download(\n            this.state.exportList,\n            this.isCompatible,\n            this.availableFormats[this.state.selectedFormat].tag\n        );\n        this.state.disabled = false;\n    }\n\n    async onDeleteExportTemplate() {\n        this.dialog.add(DeleteExportListDialog, {\n            text: _t(\"Do you really want to delete this export template?\"),\n            delete: async () => {\n                const id = Number(this.state.templateId);\n                await this.orm.unlink(\"ir.exports\", [id], { context: this.props.context });\n                this.templates.splice(\n                    this.templates.findIndex((i) => i.id === id),\n                    1\n                );\n                this.state.templateId = null;\n                this.setDefaultExportList();\n            },\n        });\n    }\n\n    onSearch(ev) {\n        this.state.search = this.lookup(ev.target.value);\n    }\n\n    lookup(value) {\n        let lookupResult = fuzzyLookup(\n            value,\n            Object.values(this.knownFields),\n            // because fuzzyLookup gives an higher score if the string starts with the pattern,\n            // reversing the string makes the search more reliable in this context\n            (field) => field.string.split(\"/\").reverse().join(\"/\")\n        );\n        if (this.isDebug) {\n            lookupResult = unique([\n                ...lookupResult,\n                ...Object.values(this.knownFields).filter((f) => f.id.includes(value)),\n            ]);\n        }\n        return lookupResult;\n    }\n\n    onToggleCompatibleExport(value) {\n        this.isCompatible = value;\n        this.fetchFields();\n    }\n\n    async setDefaultExportList() {\n        const defaultExportList = this.props.defaultExportList\n            .map((defaultField) => this.knownFields[defaultField.name])\n            .filter((field) => field);\n\n        const defaultExportfields = Object.values(this.knownFields).filter(\n            (field) => field.default_export\n        );\n\n        this.state.exportList = unique([...defaultExportList, ...defaultExportfields]);\n    }\n\n    setFormat(ev) {\n        if (ev.target.checked) {\n            this.state.selectedFormat = this.availableFormats.findIndex(\n                ({ tag }) => tag === ev.target.value\n            );\n        }\n    }\n}\n", "import { Dialog } from \"@web/core/dialog/dialog\";\nimport { useChildRef, useService } from \"@web/core/utils/hooks\";\nimport { CallbackRecorder } from \"@web/search/action_hook\";\nimport { View } from \"@web/views/view\";\n\nimport { Component } from \"@odoo/owl\";\n\nexport class FormViewDialog extends Component {\n    static template = \"web.FormViewDialog\";\n    static components = { Dialog, View };\n    static props = {\n        close: Function,\n        resModel: String,\n\n        context: { type: Object, optional: true },\n        expandedFormRef: { type: String, optional: true },\n        nextRecordsContext: { type: Object, optional: true },\n        readonly: { type: Boolean, optional: true },\n        onRecordSaved: { type: Function, optional: true },\n        onRecordSave: { type: Function, optional: true },\n        onRecordDiscarded: { type: Function, optional: true },\n        removeRecord: { type: Function, optional: true },\n        resId: { type: [Number, Boolean], optional: true },\n        title: { type: String, optional: true },\n        viewId: { type: [Number, Boolean], optional: true },\n        preventCreate: { type: Boolean, optional: true },\n        preventEdit: { type: Boolean, optional: true },\n        canExpand: { type: Boolean, optional: true },\n        isToMany: { type: Boolean, optional: true },\n        size: Dialog.props.size,\n    };\n    static defaultProps = {\n        onRecordSaved: () => {},\n        preventCreate: false,\n        preventEdit: false,\n        canExpand: true,\n        isToMany: false,\n    };\n\n    setup() {\n        super.setup();\n\n        this.actionService = useService(\"action\");\n        this.modalRef = useChildRef();\n        this.env.dialogData.dismiss = () => this.discardRecord();\n\n        const buttonTemplate = this.props.isToMany\n            ? \"web.FormViewDialog.ToMany.buttons\"\n            : \"web.FormViewDialog.ToOne.buttons\";\n\n        this.currentResId = this.props.resId;\n\n        if (this.props.canExpand) {\n            this.onExpandCallback = this.onExpand.bind(this);\n        }\n\n        this.viewProps = {\n            type: \"form\",\n            buttonTemplate,\n\n            context: this.props.context || {},\n            display: { controlPanel: false },\n            readonly: this.props.readonly,\n            resId: this.props.resId || false,\n            resModel: this.props.resModel,\n            viewId: this.props.viewId || false,\n            preventCreate: this.props.preventCreate,\n            preventEdit: this.props.preventEdit,\n            discardRecord: this.discardRecord.bind(this),\n            saveRecord: async (record, params) => {\n                let saved;\n                if (this.props.onRecordSave) {\n                    saved = await this.props.onRecordSave(record);\n                } else {\n                    saved = await record.save({ reload: false });\n                    if (saved) {\n                        this.currentResId = record.resId;\n                        await this.props.onRecordSaved(record);\n                    }\n                }\n                if (saved) {\n                    await this.onRecordSaved(record, params);\n                }\n                return saved;\n            },\n\n            __beforeLeave__: new CallbackRecorder(),\n        };\n        if (this.props.removeRecord) {\n            this.viewProps.removeRecord = async () => {\n                await this.props.removeRecord();\n                this.props.close();\n            };\n        }\n    }\n\n    /**\n     * overridable method defining what to do on save\n     * @param {*} record, record that was saved\n     * @param {*} params, additional parameters passed to \"save\"\n     */\n    async onRecordSaved(record, params) {\n        if (params?.saveAndNew) {\n            this.currentResId = false;\n            const context = this.props.nextRecordsContext || this.props.context || {};\n            await record.model.load({ resId: false, context });\n        } else {\n            this.props.close();\n        }\n    }\n\n    async discardRecord() {\n        if (this.props.onRecordDiscarded) {\n            await this.props.onRecordDiscarded();\n        }\n        this.props.close();\n    }\n\n    async onExpand() {\n        const beforeLeaveCallbacks = this.viewProps.__beforeLeave__.callbacks;\n        const res = await Promise.all(beforeLeaveCallbacks.map((callback) => callback()));\n        if (!res.includes(false)) {\n            this.actionService.doAction({\n                type: \"ir.actions.act_window\",\n                res_model: this.props.resModel,\n                res_id: this.currentResId,\n                views: [[false, \"form\"]],\n                context: {\n                    ...this.props.context,\n                    form_view_ref: this.props.expandedFormRef,\n                },\n            });\n        }\n    }\n}\n", "import { Dialog } from \"@web/core/dialog/dialog\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { renderToMarkup } from \"@web/core/utils/render\";\nimport { View } from \"@web/views/view\";\n\nimport { FormViewDialog } from \"./form_view_dialog\";\n\nimport { Component, useState } from \"@odoo/owl\";\nimport { registry } from \"@web/core/registry\";\n\nlet _defaultNoContentHelp;\nfunction getDefaultNoContentHelp() {\n    if (!_defaultNoContentHelp) {\n        _defaultNoContentHelp = renderToMarkup(\"web.SelectCreateDialog.DefaultNoContentHelp\");\n    }\n    return _defaultNoContentHelp;\n}\n\nexport class SelectCreateDialog extends Component {\n    static components = { Dialog, View };\n    static template = \"web.SelectCreateDialog\";\n    static props = {\n        context: { type: Object, optional: true },\n        domain: { type: Array, optional: true },\n        dynamicFilters: { type: Array, optional: true },\n        resModel: String,\n        searchViewId: { type: [Number, { value: false }], optional: true },\n        multiSelect: { type: Boolean, optional: true },\n        onSelected: { type: Function, optional: true },\n        close: { type: Function, optional: true },\n        onCreateEdit: { type: Function, optional: true },\n        title: { type: String, optional: true },\n        noCreate: { type: Boolean, optional: true },\n        onUnselect: { type: Function, optional: true },\n        noContentHelp: { type: String, optional: true }, // Markup\n    };\n    static defaultProps = {\n        dynamicFilters: [],\n        multiSelect: true,\n        searchViewId: false,\n        domain: [],\n        context: {},\n    };\n\n    setup() {\n        this.viewService = useService(\"view\");\n        this.dialogService = useService(\"dialog\");\n        this.state = useState({ resIds: [] });\n        const noContentHelp = this.props.noContentHelp || getDefaultNoContentHelp();\n        this.busy = false; // flag used to ensure we only call once the onSelected/onUnselect props\n        this.baseViewProps = {\n            display: { searchPanel: false },\n            noBreadcrumbs: true,\n            noContentHelp,\n            showButtons: false,\n            selectRecord: (resId) => this.select([resId]),\n            onSelectionChanged: (resIds) => {\n                this.state.resIds = resIds;\n            },\n        };\n    }\n\n    get viewProps() {\n        const type = this.env.isSmall ? \"kanban\" : \"list\";\n        const props = {\n            loadIrFilters: true,\n            ...this.baseViewProps,\n            context: this.props.context,\n            domain: this.props.domain,\n            dynamicFilters: this.props.dynamicFilters,\n            readonly: true,\n            resModel: this.props.resModel,\n            searchViewId: this.props.searchViewId,\n            type,\n        };\n        if (type === \"list\") {\n            props.allowSelectors = this.props.multiSelect;\n            props.allowOpenAction = false;\n        } else if (type === \"kanban\") {\n            props.forceGlobalClick = true;\n        }\n        return props;\n    }\n\n    async executeOnceAndClose(callback) {\n        if (!this.busy) {\n            this.busy = true;\n            try {\n                await callback();\n            } catch (e) {\n                this.busy = false;\n                throw e;\n            }\n            this.props.close();\n        }\n    }\n\n    async select(resIds) {\n        if (this.props.onSelected) {\n            this.executeOnceAndClose(() => this.props.onSelected(resIds));\n        }\n    }\n\n    async unselect() {\n        if (this.props.onUnselect) {\n            this.executeOnceAndClose(() => this.props.onUnselect());\n        }\n    }\n\n    get canUnselect() {\n        return this.env.isSmall && !!this.props.onUnselect;\n    }\n\n    async createEditRecord() {\n        if (this.props.onCreateEdit) {\n            await this.props.onCreateEdit();\n            this.props.close();\n        } else {\n            this.dialogService.add(FormViewDialog, {\n                context: this.props.context,\n                resModel: this.props.resModel,\n                onRecordSaved: (record) => {\n                    this.props.onSelected([record.resId]);\n                    this.props.close();\n                },\n            });\n        }\n    }\n}\n\nregistry.category(\"dialogs\").add(\"select_create\", SelectCreateDialog);\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { useBus, useService } from \"@web/core/utils/hooks\";\nimport { browser } from \"@web/core/browser/browser\";\nimport { evaluateExpr } from \"@web/core/py_js/py\";\nimport { download } from \"@web/core/network/download\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { ExportDataDialog } from \"@web/views/view_dialogs/export_data_dialog\";\nimport {\n    deleteConfirmationMessage,\n    ConfirmationDialog,\n} from \"@web/core/confirmation_dialog/confirmation_dialog\";\n\nimport { useComponent, useEffect } from \"@odoo/owl\";\nimport { DynamicList } from \"@web/model/relational_model/dynamic_list\";\n\n/**\n * Allows for a component (usually a View component) to handle links with\n * attribute type=\"action\". This is used to support onboarding banners and content helpers.\n *\n * A @web/core/concurrency:KeepLast must be present in the owl environment to allow coordinating\n * between clicks. (env.keepLast)\n *\n * Note that this is similar but quite different from action buttons, since action links\n * are not dynamic according to the record.\n * @param {Object} params\n * @param  {String} params.resModel The default resModel to which actions will apply\n * @param  {Function} [params.reload] The function to execute to reload, if a button has data-reload-on-close\n */\nexport function useActionLinks({ resModel, reload }) {\n    const component = useComponent();\n    const keepLast = component.env.keepLast;\n\n    const orm = useService(\"orm\");\n    const { doAction } = useService(\"action\");\n\n    async function handler(ev) {\n        ev.preventDefault();\n        ev.stopPropagation();\n        let target = ev.target;\n        if (target.tagName !== \"A\") {\n            target = target.closest(\"a\");\n        }\n        const data = target.dataset;\n\n        if (data.method !== undefined && data.model !== undefined) {\n            const options = {};\n            if (data.reloadOnClose) {\n                options.onClose = reload || (() => component.render());\n            }\n            const action = await keepLast.add(orm.call(data.model, data.method));\n            if (action !== undefined) {\n                keepLast.add(Promise.resolve(doAction(action, options)));\n            }\n        } else if (target.getAttribute(\"name\")) {\n            const options = {};\n            if (data.context) {\n                options.additionalContext = evaluateExpr(data.context);\n            }\n            keepLast.add(doAction(target.getAttribute(\"name\"), options));\n        } else {\n            let views;\n            const resId = data.resid ? parseInt(data.resid, 10) : null;\n            if (data.views) {\n                views = evaluateExpr(data.views);\n            } else {\n                views = resId\n                    ? [[false, \"form\"]]\n                    : [\n                          [false, \"list\"],\n                          [false, \"form\"],\n                      ];\n            }\n            const action = {\n                name: target.getAttribute(\"title\") || target.textContent.trim(),\n                type: \"ir.actions.act_window\",\n                res_model: data.model || resModel,\n                target: \"current\",\n                views,\n                domain: data.domain ? evaluateExpr(data.domain) : [],\n            };\n            if (resId) {\n                action.res_id = resId;\n            }\n\n            const options = {};\n            if (data.context) {\n                options.additionalContext = evaluateExpr(data.context);\n            }\n            keepLast.add(doAction(action, options));\n        }\n    }\n\n    return (ev) => {\n        const a = ev.target.closest(`a[type=\"action\"]`);\n        if (a && ev.currentTarget.contains(a)) {\n            handler(ev);\n        }\n    };\n}\n\nexport function useBounceButton(containerRef, shouldBounce) {\n    let timeout;\n    const ui = useService(\"ui\");\n    useEffect(\n        (containerEl) => {\n            if (!containerEl) {\n                return;\n            }\n            const handler = (ev) => {\n                const button = ui.activeElement.querySelector(\"[data-bounce-button]\");\n                if (button && shouldBounce(ev.target)) {\n                    button.classList.add(\"o_catch_attention\");\n                    browser.clearTimeout(timeout);\n                    timeout = browser.setTimeout(() => {\n                        button.classList.remove(\"o_catch_attention\");\n                    }, 400);\n                }\n            };\n            containerEl.addEventListener(\"click\", handler);\n            return () => containerEl.removeEventListener(\"click\", handler);\n        },\n        () => [containerRef.el]\n    );\n}\n\nexport function useExportRecords(env, context, getDefaultExportList) {\n    const { model, searchModel } = env;\n    useBus(searchModel, \"direct-export-data\", async () => {\n        _downloadExport(getDefaultExportList(), false, \"xlsx\");\n    });\n    const _getExportedFields = async (isCompatible, parentParams) => {\n        const root = model.root;\n        let domain = parentParams ? [] : root.domain;\n        if (!root.isDomainSelected && root.selection.length > 0) {\n            const ids = root.selection.map((e) => e.resId);\n            domain = [[\"id\", \"in\", ids]];\n        }\n        return await rpc(\"/web/export/get_fields\", {\n            model: root.resModel,\n            domain,\n            import_compat: isCompatible,\n            ...parentParams,\n        });\n    };\n\n    const _downloadExport = async (fields, import_compat, format) => {\n        const root = model.root;\n        const exportedFields = fields.map((field) => ({\n            name: field.name || field.id,\n            label: field.label || field.string,\n            store: field.store,\n            type: field.field_type || field.type,\n        }));\n        if (import_compat) {\n            exportedFields.unshift({\n                name: \"id\",\n                label: _t(\"External ID\"),\n            });\n        }\n        await download({\n            data: {\n                data: JSON.stringify({\n                    import_compat,\n                    context: root.context,\n                    domain: root.domain,\n                    fields: exportedFields,\n                    groupby: root.groupBy,\n                    ids:\n                        !root.isDomainSelected && root.selection.length > 0\n                            ? root.selection.map((e) => e.resId)\n                            : false,\n                    model: root.resModel,\n                }),\n            },\n            url: `/web/export/${format}`,\n        });\n    };\n\n    return () => {\n        const root = model.root;\n        model.dialog.add(ExportDataDialog, {\n            context: root.context,\n            defaultExportList: getDefaultExportList(),\n            download: _downloadExport,\n            getExportedFields: _getExportedFields,\n            root,\n        });\n    };\n}\n\nexport function useDeleteRecords(model) {\n    function getDefaultDialogProps(records) {\n        const isDynamicList = model.root instanceof DynamicList;\n        let body = deleteConfirmationMessage;\n        if (\n            records?.length > 1 ||\n            (isDynamicList && (model.root.isDomainSelected || model.root.selection.length > 1))\n        ) {\n            body = _t(\"Are you sure you want to delete these records?\");\n        }\n        let confirm = () => records.forEach((r) => r.delete());\n        if (isDynamicList) {\n            confirm = () => model.root.deleteRecords(records);\n        }\n        return {\n            body,\n            cancel: () => {},\n            cancelLabel: _t(\"No, keep it\"),\n            confirm,\n            confirmLabel: _t(\"Delete\"),\n            title: _t(\"Bye-bye, record!\"),\n        };\n    }\n    return (dialogProps, records) => {\n        const defaultProps = getDefaultDialogProps(records);\n        model.dialog.add(ConfirmationDialog, { ...defaultProps, ...dialogProps });\n    };\n}\n", "import { rpcBus } from \"@web/core/network/rpc\";\nimport { registry } from \"@web/core/registry\";\nimport { UPDATE_METHODS } from \"@web/core/orm_service\";\n\n/**\n * @typedef {Object} IrFilter\n * @property {[number, string] | false} user_id\n * @property {string} sort\n * @property {string} context\n * @property {string} name\n * @property {string} domain\n * @property {number} id\n * @property {boolean} is_default\n * @property {string} model_id\n * @property {[number, string] | false} action_id\n * @property {number | false} embedded_action_id\n * @property {number | false} embedded_parent_res_id\n */\n\n/**\n * @typedef {Object} ViewDescription\n * @property {string} arch\n * @property {number|false} id\n * @property {number|null} [custom_view_id]\n * @property {Object} [actionMenus] // for views other than search\n * @property {IrFilter[]} [irFilters] // for search view\n */\n\n/**\n * @typedef {Object} LoadViewsParams\n * @property {string} resModel\n * @property {[number, string][]} views\n * @property {Object} context\n */\n\n/**\n * @typedef {Object} LoadViewsOptions\n * @property {number|false} actionId\n * @property {boolean} loadActionMenus\n * @property {boolean} loadIrFilters\n */\n\nexport const viewService = {\n    dependencies: [\"orm\"],\n    async: [\"loadViews\"],\n    start(env, { orm }) {\n        rpcBus.addEventListener(\"RPC:RESPONSE\", (ev) => {\n            const { model, method } = ev.detail.data.params;\n            if ([\"ir.ui.view\", \"ir.filters\"].includes(model)) {\n                if (UPDATE_METHODS.includes(method)) {\n                    rpcBus.trigger(\"CLEAR-CACHES\", \"get_views\");\n                }\n            }\n        });\n\n        /**\n         * Loads various information concerning views: fields_view for each view,\n         * fields of the corresponding model, and optionally the filters.\n         *\n         * @param {LoadViewsParams} params\n         * @param {LoadViewsOptions} [options={}]\n         * @returns {Promise<ViewDescriptions>}\n         */\n        async function loadViews(params, options = {}) {\n            const { context, resModel, views } = params;\n            const loadViewsOptions = {\n                action_id: options.actionId || false,\n                embedded_action_id: options.embeddedActionId || false,\n                embedded_parent_res_id: options.embeddedParentResId || false,\n                load_filters: options.loadIrFilters || false,\n                toolbar: (!context?.disable_toolbar && options.loadActionMenus) || false,\n            };\n            for (const key in options) {\n                if (\n                    ![\n                        \"actionId\",\n                        \"embeddedActionId\",\n                        \"embeddedParentResId\",\n                        \"loadIrFilters\",\n                        \"loadActionMenus\",\n                    ].includes(key)\n                ) {\n                    loadViewsOptions[key] = options[key];\n                }\n            }\n            if (env.isSmall) {\n                loadViewsOptions.mobile = true;\n            }\n            if (env.debug) {\n                loadViewsOptions.debug = true;\n            }\n            const filteredContext = Object.fromEntries(\n                Object.entries(context || {}).filter(\n                    ([k, v]) => k == \"lang\" || k.endsWith(\"_view_ref\")\n                )\n            );\n\n            const result = await orm.cache({ type: \"disk\" }).call(resModel, \"get_views\", [], {\n                context: filteredContext,\n                views,\n                options: loadViewsOptions,\n            });\n            const viewDescriptions = {\n                fields: result.models[resModel].fields,\n                relatedModels: result.models,\n                views: {},\n            };\n            for (const viewType in result.views) {\n                const { arch, toolbar, id, filters, custom_view_id } = result.views[viewType];\n                const viewDescription = { arch, id, custom_view_id };\n                if (toolbar) {\n                    viewDescription.actionMenus = toolbar;\n                }\n                if (filters) {\n                    viewDescription.irFilters = filters;\n                }\n                viewDescriptions.views[viewType] = viewDescription;\n            }\n            return viewDescriptions;\n        }\n        return { loadViews };\n    },\n};\n\nregistry.category(\"services\").add(\"view\", viewService);\n", "import { FileInput } from \"@web/core/file_input/file_input\";\nimport { registry } from \"@web/core/registry\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { checkFileSize } from \"@web/core/utils/files\";\nimport { standardWidgetProps } from \"@web/views/widgets/standard_widget_props\";\nimport { Component } from \"@odoo/owl\";\n\nexport class AttachDocumentWidget extends Component {\n    static template = \"web.AttachDocument\";\n    static components = {\n        FileInput,\n    };\n    static props = {\n        ...standardWidgetProps,\n        string: { type: String },\n        action: { type: String, optional: true },\n        highlight: { type: Boolean },\n    };\n\n    setup() {\n        this.http = useService(\"http\");\n        this.notification = useService(\"notification\");\n        this.fileInput = document.createElement(\"input\");\n        this.fileInput.type = \"file\";\n        this.fileInput.accept = \"*\";\n        this.fileInput.multiple = true;\n        this.fileInput.onchange = this.onInputChange.bind(this);\n    }\n\n    async onInputChange() {\n        const ufile = [...this.fileInput.files];\n        for (const file of ufile) {\n            if (!checkFileSize(file.size, this.notification)) {\n                return null;\n            }\n        }\n        const fileData = await this.http.post(\n            \"/web/binary/upload_attachment\",\n            {\n                csrf_token: odoo.csrf_token,\n                ufile: ufile,\n                model: this.props.record.resModel,\n                id: this.props.record.resId,\n            },\n            \"text\"\n        );\n        const parsedFileData = JSON.parse(fileData);\n        if (parsedFileData.error) {\n            throw new Error(parsedFileData.error);\n        }\n        await this.onFileUploaded(parsedFileData);\n    }\n\n    async triggerUpload() {\n        if (await this.beforeOpen()) {\n            this.fileInput.click();\n        }\n    }\n\n    async onFileUploaded(files) {\n        const { action, record } = this.props;\n        if (action) {\n            const { resId, resModel } = record;\n            await this.env.services.orm.call(resModel, action, [resId], {\n                attachment_ids: files.map((file) => file.id),\n            });\n            await record.load();\n        }\n    }\n\n    beforeOpen() {\n        return this.props.record.save();\n    }\n}\n\nexport const attachDocumentWidget = {\n    component: AttachDocumentWidget,\n    extractProps: ({ attrs }) => {\n        const { action, highlight, string } = attrs;\n        return {\n            action,\n            highlight: !!highlight,\n            string,\n        };\n    },\n};\n\nregistry.category(\"view_widgets\").add(\"attach_document\", attachDocumentWidget);\n", "import { session } from \"@web/session\";\nimport { standardWidgetProps } from \"@web/views/widgets/standard_widget_props\";\nimport { Component } from \"@odoo/owl\";\nimport { registry } from \"@web/core/registry\";\n\nconst LINK_REGEX = new RegExp(\"^https?://\");\n\nexport class DocumentationLink extends Component {\n    static template = \"web.DocumentationLink\";\n    static props = {\n        ...standardWidgetProps,\n        record: { type: Object, optional: 1 }, // The record is not needed in this widget\n        path: { type: String },\n        label: { type: String, optional: 1 },\n        icon: { type: String, optional: 1 },\n        alertLink: { type: Boolean, optional: 1 },\n    };\n\n    get url() {\n        if (LINK_REGEX.test(this.props.path)) {\n            return this.props.path;\n        } else {\n            const serverVersion = session.server_version_info.includes(\"final\")\n                ? `${session.server_version_info[0]}.${session.server_version_info[1]}`.replace(\n                      \"~\",\n                      \"-\"\n                  )\n                : \"master\";\n            return \"https://www.odoo.com/documentation/\" + serverVersion + this.props.path;\n        }\n    }\n\n    get classes() {\n        let classes = \"o_doc_link me-2\";\n        if (this.props.alertLink){\n            classes += \" alert-link\";\n        }\n        return classes;\n    }\n}\n\nexport const documentationLink = {\n    component: DocumentationLink,\n    extractProps: ({ attrs }) => {\n        const { path, label, icon, alert_link } = attrs;\n        return {\n            path,\n            label,\n            icon,\n            alertLink: Boolean(alert_link),\n        };\n    },\n    additionalClasses: [\"d-inline\"],\n};\n\nregistry.category(\"view_widgets\").add(\"documentation_link\", documentationLink);\n", "import { browser } from \"@web/core/browser/browser\";\nimport { registry } from \"@web/core/registry\";\nimport { standardWidgetProps } from \"@web/views/widgets/standard_widget_props\";\n\nimport { Component } from \"@odoo/owl\";\n\nexport class NotificationAlert extends Component {\n    static props = standardWidgetProps;\n    static template = \"web.NotificationAlert\";\n\n    get isNotificationBlocked() {\n        return browser.Notification && browser.Notification.permission === \"denied\";\n    }\n}\n\nexport const notificationAlert = {\n    component: NotificationAlert,\n};\n\nregistry.category(\"view_widgets\").add(\"notification_alert\", notificationAlert);\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { standardWidgetProps } from \"../standard_widget_props\";\n\nimport { Component } from \"@odoo/owl\";\n\n/**\n * This widget adds a ribbon on the top right side of the form\n *\n *      - You can specify the text with the title prop.\n *      - You can specify the title (tooltip) with the tooltip prop.\n *      - You can specify a background color for the ribbon with the bg_color prop\n *        using bootstrap classes :\n *        (bg-primary, bg-secondary, bg-success, bg-danger, bg-warning, bg-info,\n *        bg-light, bg-dark, bg-white)\n *\n *        If you don't specify the bg_color prop the bg-success class will be used\n *        by default.\n */\nexport class RibbonWidget extends Component {\n    static template = \"web.Ribbon\";\n    static props = {\n        ...standardWidgetProps,\n        record: { type: Object, optional: true },\n        text: { type: String },\n        title: { type: String, optional: true },\n        bgClass: { type: String, optional: true },\n    };\n    static defaultProps = {\n        title: \"\",\n        bgClass: \"text-bg-success\",\n    };\n\n    get classes() {\n        let classes = this.props.bgClass;\n        if (this.props.text.length > 15) {\n            classes += \" o_small\";\n        } else if (this.props.text.length > 10) {\n            classes += \" o_medium\";\n        }\n        return classes;\n    }\n}\n\nexport const ribbonWidget = {\n    component: RibbonWidget,\n    extractProps: ({ attrs }) => ({\n        text: attrs.title || attrs.text,\n        title: attrs.tooltip,\n        bgClass: attrs.bg_color,\n    }),\n    supportedAttributes: [\n        {\n            label: _t(\"Title\"),\n            name: \"title\",\n            type: \"string\",\n        },\n        {\n            label: _t(\"Background color\"),\n            name: \"bg_color\",\n            type: \"string\",\n        },\n        {\n            label: _t(\"Tooltip\"),\n            name: \"tooltip\",\n            type: \"string\",\n        },\n    ],\n};\n\nregistry.category(\"view_widgets\").add(\"web_ribbon\", ribbonWidget);\n", "import { registry } from \"@web/core/registry\";\nimport { SignatureDialog } from \"@web/core/signature/signature_dialog\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { standardWidgetProps } from \"@web/views/widgets/standard_widget_props\";\n\nimport { Component } from \"@odoo/owl\";\n\nexport class SignatureWidget extends Component {\n    static template = \"web.SignatureWidget\";\n    static props = {\n        ...standardWidgetProps,\n        fullName: { type: String, optional: true },\n        highlight: { type: Boolean, optional: true },\n        string: { type: String },\n        signatureField: { type: String, optional: true },\n    };\n\n    setup() {\n        this.dialogService = useService(\"dialog\");\n        this.orm = useService(\"orm\");\n    }\n\n    onClickSignature() {\n        const nameAndSignatureProps = {\n            mode: \"draw\",\n            displaySignatureRatio: 3,\n            signatureType: \"signature\",\n            noInputName: true,\n        };\n        const { fullName, record } = this.props;\n        let defaultName = \"\";\n        if (fullName) {\n            let signName;\n            const fullNameData = record.data[fullName];\n            if (record.fields[fullName].type === \"many2one\") {\n                signName = fullNameData && fullNameData.display_name;\n            } else {\n                signName = fullNameData;\n            }\n            defaultName = signName === \"\" ? undefined : signName;\n        }\n\n        nameAndSignatureProps.defaultFont = this.props.defaultFont;\n\n        const dialogProps = {\n            defaultName,\n            nameAndSignatureProps,\n            uploadSignature: (data) => this.uploadSignature(data),\n        };\n        this.dialogService.add(SignatureDialog, dialogProps);\n    }\n\n    async uploadSignature({ signatureImage }) {\n        const file = signatureImage.split(\",\")[1];\n        const { model, resModel, resId } = this.props.record;\n\n        await this.env.services.orm.write(resModel, [resId], {\n            [this.props.signatureField]: file,\n        });\n        await this.props.record.load();\n        model.notify();\n    }\n}\n\nexport const signatureWidget = {\n    component: SignatureWidget,\n    extractProps: ({ attrs }) => {\n        const { full_name: fullName, highlight, signature_field, string } = attrs;\n        return {\n            fullName,\n            highlight: !!highlight,\n            string,\n            signatureField: signature_field || \"signature\",\n        };\n    },\n};\n\nregistry.category(\"view_widgets\").add(\"signature\", signatureWidget);\n", "export const standardWidgetProps = {\n    readonly: { type: Boolean, optional: true },\n    record: { type: Object },\n};\n", "import { registry } from \"@web/core/registry\";\nimport { CheckBox } from \"@web/core/checkbox/checkbox\";\nimport { localization } from \"@web/core/l10n/localization\";\nimport { _t } from \"@web/core/l10n/translation\";\n\nimport { Component } from \"@odoo/owl\";\n\nconst WEEKDAYS = [\"sun\", \"mon\", \"tue\", \"wed\", \"thu\", \"fri\", \"sat\"];\n\nexport class WeekDays extends Component {\n    static template = \"web.WeekDays\";\n    static components = { CheckBox };\n    static props = {\n        record: Object,\n        readonly: Boolean,\n    };\n\n    get weekdays() {\n        return [\n            ...WEEKDAYS.slice(localization.weekStart % WEEKDAYS.length, WEEKDAYS.length),\n            ...WEEKDAYS.slice(0, localization.weekStart % WEEKDAYS.length),\n        ];\n    }\n    get data() {\n        return Object.fromEntries(this.weekdays.map((day) => [day, this.props.record.data[day]]));\n    }\n\n    onChange(day, checked) {\n        this.props.record.update({ [day]: checked });\n    }\n}\n\nexport const weekDays = {\n    component: WeekDays,\n    fieldDependencies: [\n        { name: \"sun\", type: \"boolean\", string: _t(\"Sun\"), readonly: false },\n        { name: \"mon\", type: \"boolean\", string: _t(\"Mon\"), readonly: false },\n        { name: \"tue\", type: \"boolean\", string: _t(\"Tue\"), readonly: false },\n        { name: \"wed\", type: \"boolean\", string: _t(\"Wed\"), readonly: false },\n        { name: \"thu\", type: \"boolean\", string: _t(\"Thu\"), readonly: false },\n        { name: \"fri\", type: \"boolean\", string: _t(\"Fri\"), readonly: false },\n        { name: \"sat\", type: \"boolean\", string: _t(\"Sat\"), readonly: false },\n    ],\n};\n\nregistry.category(\"view_widgets\").add(\"week_days\", weekDays);\n", "import { evaluateExpr, evaluateBooleanExpr } from \"@web/core/py_js/py\";\nimport { registry } from \"@web/core/registry\";\n\nimport { Component, xml } from \"@odoo/owl\";\nconst viewWidgetRegistry = registry.category(\"view_widgets\");\n\nconst supportedInfoValidation = {\n    type: Array,\n    element: Object,\n    shape: {\n        label: String,\n        name: String,\n        type: String,\n        availableTypes: { type: Array, element: String, optional: true },\n        default: { type: String, optional: true },\n        help: { type: String, optional: true },\n        choices: /* choices if type == selection */ {\n            type: Array,\n            element: Object,\n            shape: { label: String, value: String },\n            optional: true,\n        },\n    },\n    optional: true,\n};\n\nviewWidgetRegistry.addValidation({\n    component: { validate: (c) => c.prototype instanceof Component },\n    extractProps: { type: Function, optional: true },\n    additionalClasses: { type: Array, element: String, optional: true },\n    fieldDependencies: {\n        type: [Function, { type: Array, element: Object, shape: { name: String, type: String } }],\n        optional: true,\n    },\n    listViewWidth: {\n        type: [\n            Number,\n            {\n                type: Array,\n                element: Number,\n                validate: (array) => array.length === 1 || array.length === 2,\n            },\n            Function,\n        ],\n        optional: true,\n    },\n    supportedAttributes: supportedInfoValidation,\n    supportedOptions: supportedInfoValidation,\n});\n\n/**\n * A Component that supports rendering `<widget />` tags in a view arch\n * It should have minimum legacy support that is:\n * - getting the legacy widget class from the legacy registry\n * - instanciating a legacy widget\n * - passing to it a \"legacy node\", which is a representation of the arch's node\n * It supports instancing components from the \"view_widgets\" registry.\n */\nexport class Widget extends Component {\n    static template = xml/*xml*/ `\n        <div t-att-class=\"classNames\" t-att-style=\"props.style\">\n            <t t-component=\"widget.component\" t-props=\"widgetProps\" />\n        </div>`;\n\n    static parseWidgetNode = function (node) {\n        const name = node.getAttribute(\"name\");\n        const widget = viewWidgetRegistry.get(name);\n        const widgetInfo = {\n            name,\n            widget,\n            options: {},\n            attrs: {},\n        };\n\n        for (const { name, value } of node.attributes) {\n            if ([\"name\", \"widget\"].includes(name)) {\n                // avoid adding name and widget to attrs\n                continue;\n            }\n            if (name === \"options\") {\n                widgetInfo.options = evaluateExpr(value);\n            } else if (!name.startsWith(\"t-att\")) {\n                // all other (non dynamic) attributes\n                widgetInfo.attrs[name] = value;\n            }\n        }\n\n        return widgetInfo;\n    };\n    static props = {\n        \"*\": true,\n    };\n\n    setup() {\n        if (this.props.widgetInfo) {\n            this.widget = this.props.widgetInfo.widget;\n        } else {\n            this.widget = viewWidgetRegistry.get(this.props.name);\n        }\n    }\n\n    get classNames() {\n        const classNames = {\n            o_widget: true,\n            [`o_widget_${this.props.name}`]: true,\n            [this.props.className]: Boolean(this.props.className),\n        };\n        if (this.widget.additionalClasses) {\n            for (const cls of this.widget.additionalClasses) {\n                classNames[cls] = true;\n            }\n        }\n        return classNames;\n    }\n    get widgetProps() {\n        const record = this.props.record;\n\n        let readonlyFromModifiers = false;\n        let propsFromNode = {};\n        if (this.props.widgetInfo) {\n            const widgetInfo = this.props.widgetInfo;\n            readonlyFromModifiers = evaluateBooleanExpr(\n                widgetInfo.attrs.readonly,\n                record.evalContextWithVirtualIds\n            );\n            const dynamicInfo = {\n                readonly: readonlyFromModifiers,\n            };\n            propsFromNode = this.widget.extractProps\n                ? this.widget.extractProps(widgetInfo, dynamicInfo)\n                : {};\n        }\n        return {\n            record,\n            readonly: !record.isInEdition || readonlyFromModifiers || false,\n            ...propsFromNode,\n        };\n    }\n}\n", "import { Component, xml, onWillDestroy } from \"@odoo/owl\";\n\n// -----------------------------------------------------------------------------\n// ActionContainer (Component)\n// -----------------------------------------------------------------------------\nexport class ActionContainer extends Component {\n    static props = {};\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.info = {};\n        this.onActionManagerUpdate = ({ detail: info }) => {\n            this.info = info;\n            this.render();\n        };\n        this.env.bus.addEventListener(\"ACTION_MANAGER:UPDATE\", this.onActionManagerUpdate);\n        onWillDestroy(() => {\n            this.env.bus.removeEventListener(\"ACTION_MANAGER:UPDATE\", this.onActionManagerUpdate);\n        });\n    }\n}\n", "import { Dialog } from \"@web/core/dialog/dialog\";\nimport { DebugMenu } from \"@web/core/debug/debug_menu\";\nimport { useOwnDebugContext } from \"@web/core/debug/debug_context\";\n\nexport class ActionDialog extends Dialog {\n    static components = { ...Dialog.components, DebugMenu };\n    static template = \"web.ActionDialog\";\n    static props = {\n        ...Dialog.props,\n        close: Function,\n        slots: { optional: true },\n        ActionComponent: { optional: true },\n        actionProps: { optional: true },\n        actionType: { optional: true },\n        title: { optional: true },\n    };\n    static defaultProps = {\n        ...Dialog.defaultProps,\n        withBodyPadding: false,\n    };\n\n    setup() {\n        super.setup();\n        useOwnDebugContext();\n    }\n}\n", "import { registry } from \"@web/core/registry\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { standardActionServiceProps } from \"./action_service\";\n\nimport { Component, onWillStart } from \"@odoo/owl\";\n\n/**\n * Client action to use in a dialog to display the URL of a Kiosk, containing a\n * link to Install the corresponding PWA\n */\nexport class InstallKiosk extends Component {\n    static template = \"web.ActionInstallKioskPWA\";\n    static props = { ...standardActionServiceProps };\n\n    setup() {\n        this.resModel = this.props.action.res_model;\n        this.orm = useService(\"orm\");\n        this.dialog = useService(\"dialog\");\n        onWillStart(async () => {\n            this.url = await this.orm.call(this.resModel, \"get_kiosk_url\", [\n                this.props.action.context.active_id,\n            ]);\n        });\n    }\n\n    get appId() {\n        return this.props.action.context.app_id || this.resModel;\n    }\n\n    get installURL() {\n        return `/scoped_app?app_id=${this.appId}&path=${encodeURIComponent(\n            this.url.replace(document.location.origin + \"/\", \"\")\n        )}`;\n    }\n}\n\nregistry.category(\"actions\").add(\"install_kiosk_pwa\", InstallKiosk);\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { browser } from \"@web/core/browser/browser\";\nimport { makeContext } from \"@web/core/context\";\nimport { useDebugCategory } from \"@web/core/debug/debug_context\";\nimport { evaluateExpr } from \"@web/core/py_js/py\";\nimport { rpc, rpcBus } from \"@web/core/network/rpc\";\nimport { registry } from \"@web/core/registry\";\nimport { user } from \"@web/core/user\";\nimport { Deferred, KeepLast } from \"@web/core/utils/concurrency\";\nimport { useBus, useService } from \"@web/core/utils/hooks\";\nimport { View, ViewNotFoundError } from \"@web/views/view\";\nimport { ActionDialog } from \"./action_dialog\";\nimport { ReportAction } from \"./reports/report_action\";\nimport { UPDATE_METHODS } from \"@web/core/orm_service\";\nimport { CallbackRecorder } from \"@web/search/action_hook\";\nimport { ControlPanel } from \"@web/search/control_panel/control_panel\";\nimport { PATH_KEYS, router as _router } from \"@web/core/browser/router\";\n\nimport {\n    Component,\n    markup,\n    onMounted,\n    onWillUnmount,\n    onError,\n    useChildSubEnv,\n    xml,\n    reactive,\n    status,\n} from \"@odoo/owl\";\nimport { downloadReport, getReportUrl } from \"./reports/utils\";\nimport { zip } from \"@web/core/utils/arrays\";\nimport { isHtmlEmpty } from \"@web/core/utils/html\";\nimport { omit, pick, shallowEqual } from \"@web/core/utils/objects\";\nimport { session } from \"@web/session\";\nimport { exprToBoolean } from \"@web/core/utils/strings\";\n\nclass BlankComponent extends Component {\n    static props = [\"onMounted\", \"withControlPanel\", \"*\"];\n    static template = \"web.BlankComponent\";\n    static components = { ControlPanel };\n\n    setup() {\n        useChildSubEnv({ config: { breadcrumbs: [], noBreadcrumbs: true } });\n        onMounted(() => this.props.onMounted());\n    }\n}\n\nconst actionHandlersRegistry = registry.category(\"action_handlers\");\nconst actionRegistry = registry.category(\"actions\");\n\n/** @typedef {number|false} ActionId */\n/** @typedef {Object} ActionDescription */\n/** @typedef {\"current\" | \"fullscreen\" | \"new\" | \"main\" | \"self\"} ActionMode */\n/** @typedef {string} ActionTag */\n/** @typedef {string} ActionXMLId */\n/** @typedef {Object} Context */\n/** @typedef {Function} CallableFunction */\n/** @typedef {string} ViewType */\n\n/** @typedef {ActionId|ActionXMLId|ActionTag|ActionDescription} ActionRequest */\n\n/**\n * @typedef {Object} ActionOptions\n * @property {Context} [additionalContext]\n * @property {boolean} [clearBreadcrumbs]\n * @property {CallableFunction} [onClose]\n * @property {Object} [props]\n * @property {ViewType} [viewType]\n * @property {\"replaceCurrentAction\" | \"replacePreviousAction\"} [stackPosition]\n * @property {number} [index]\n * @property {boolean} [newWindow]\n * @property {boolean} [forceLeave]\n */\n\nexport async function clearUncommittedChanges(env, { forceLeave } = {}) {\n    const callbacks = [];\n    env.bus.trigger(\"CLEAR-UNCOMMITTED-CHANGES\", callbacks);\n    const res = await Promise.all(callbacks.map((fn) => fn({ forceLeave })));\n    return !res.includes(false);\n}\n\nexport const standardActionServiceProps = {\n    action: Object, // prop added by _getActionInfo\n    actionId: { type: Number, optional: true }, // prop added by _getActionInfo\n    className: { type: String, optional: true }, // prop added by the ActionContainer\n    globalState: { type: Object, optional: true }, // prop added by _updateUI\n    state: { type: Object, optional: true }, // prop added by _updateUI\n    resId: { type: [Number, Boolean], optional: true },\n    updateActionState: { type: Function, optional: true },\n};\n\nfunction parseActiveIds(ids) {\n    const activeIds = [];\n    if (typeof ids === \"string\") {\n        activeIds.push(...ids.split(\",\").map(Number));\n    } else if (typeof ids === \"number\") {\n        activeIds.push(ids);\n    }\n    return activeIds;\n}\n\nconst DIALOG_SIZES = {\n    \"extra-large\": \"xl\",\n    large: \"lg\",\n    medium: \"md\",\n    small: \"sm\",\n};\n\n// -----------------------------------------------------------------------------\n// Errors\n// -----------------------------------------------------------------------------\n\nexport class ControllerNotFoundError extends Error {}\n\nexport class InvalidButtonParamsError extends Error {}\n\n// -----------------------------------------------------------------------------\n// ActionManager (Service)\n// -----------------------------------------------------------------------------\n\n// regex that matches context keys not to forward from an action to another\nconst CTX_KEY_REGEX =\n    /^(?:(?:default_|search_default_|show_).+|.+_view_ref|group_by|active_id|active_ids|orderedBy)$/;\n// keys added to the context for the embedded actions feature\nconst EMBEDDED_ACTIONS_CTX_KEYS = [\n    \"current_embedded_action_id\",\n    \"parent_action_embedded_actions\",\n    \"parent_action_id\",\n    \"from_embedded_action\",\n];\n\n// only register this template once for all dynamic classes ControllerComponent\nconst ControllerComponentTemplate = xml`<t t-component=\"Component\" t-props=\"componentProps\"/>`;\n\nexport function makeActionManager(env, router = _router) {\n    const breadcrumbCache = {};\n    const keepLast = new KeepLast();\n    let id = 0;\n    let controllerStack = [];\n    let dialog = null;\n    let nextDialog = null;\n\n    router.hideKeyFromUrl(\"globalState\");\n\n    rpcBus.addEventListener(\"RPC:RESPONSE\", async (ev) => {\n        const { model, method } = ev.detail.data.params;\n        if (model === \"ir.actions.act_window\" && UPDATE_METHODS.includes(method)) {\n            rpcBus.trigger(\"CLEAR-CACHES\", \"/web/action/load\");\n            const virtualStack = await _controllersFromState(router.current);\n            const nextStack = [...virtualStack, controllerStack[controllerStack.length - 1]];\n            nextStack[nextStack.length - 1].config.breadcrumbs.splice(\n                0,\n                nextStack[nextStack.length - 1].config.breadcrumbs.length,\n                ..._getBreadcrumbs(nextStack)\n            );\n            controllerStack = nextStack;\n        }\n    });\n\n    // ---------------------------------------------------------------------------\n    // misc\n    // ---------------------------------------------------------------------------\n\n    /**\n     * Create an array of virtual controllers based on the given state.\n     *\n     * @private\n     * @param {object} state\n     * @returns {Promise<object[]>} an array of virtual controllers\n     */\n    async function _controllersFromState(state) {\n        const currentState = JSON.parse(browser.sessionStorage.getItem(\"current_state\") || \"{}\");\n        if (router.stateToUrl(currentState) === router.stateToUrl(state)) {\n            state = currentState;\n        }\n        if (!state?.actionStack?.length) {\n            return [];\n        }\n        // The last controller will be created by doAction and won't be virtual\n        const controllers = state.actionStack\n            .slice(0, -1)\n            .map((actionState, index) => {\n                const controller = _makeController({\n                    displayName: actionState.displayName,\n                    virtual: true,\n                    action: {},\n                    props: {},\n                    state: { ...actionState, actionStack: state.actionStack.slice(0, index + 1) },\n                    currentState: {},\n                });\n                if (actionState.action) {\n                    controller.action.id = actionState.action;\n\n                    const [actionRequestKey, clientAction] = actionRegistry.contains(\n                        actionState.action\n                    )\n                        ? [actionState.action, actionRegistry.get(actionState.action)]\n                        : actionRegistry\n                              .getEntries()\n                              .find((a) => a[1].path === actionState.action) ?? [];\n                    if (actionRequestKey && clientAction) {\n                        if (state.actionStack[index + 1]?.action === actionState.action) {\n                            // client actions don't have multi-record views, so we can't go further to the next controller\n                            return;\n                        }\n                        controller.action.tag = actionRequestKey;\n                        controller.action.type = \"ir.actions.client\";\n                        controller.displayName = clientAction.displayName?.toString();\n                    }\n                    if (actionState.active_id) {\n                        controller.action.context = { active_id: actionState.active_id };\n                        controller.currentState.active_id = actionState.active_id;\n                    }\n                }\n                if (actionState.model) {\n                    controller.action.type = \"ir.actions.act_window\";\n                    controller.props.resModel = actionState.model;\n                }\n                if (actionState.resId) {\n                    controller.action.type ||= \"ir.actions.act_window\";\n                    controller.props.resId = actionState.resId;\n                    controller.currentState.resId = actionState.resId;\n                    controller.props.type = \"form\";\n                }\n                return controller;\n            })\n            .filter(Boolean);\n\n        if (state.action && state.resId && controllers.at(-1)?.action?.id === state.action) {\n            // When loading the state on a form view, we will need to load the action for it,\n            // and this will give us the display name of the corresponding multi-record view in\n            // the breadcrumb.\n            // By marking the last controller as a lazyController, we can in some cases avoid\n            // _loadBreadcrumbs from doing any network request as the breadcrumbs may only contain\n            // the form view and the multi-record view.\n            const bcControllers = await _loadBreadcrumbs(controllers.slice(0, -1));\n            controllers.at(-1).lazy = true;\n            return [...bcControllers, controllers.at(-1)];\n        }\n        return _loadBreadcrumbs(controllers);\n    }\n\n    /**\n     * Load breadcrumbs for an array of controllers. This function adds display\n     * names to controllers that the current user has access to and for which\n     * the view (and record) exist. Controllers that correspond to a deleted\n     * record or a record/view that the user can't access are removed.\n     *\n     * @param {object[]} controllers an array of controllers whose breadcrumbs\n     *  should be loaded\n     * @returns {Promise<object[]>} a new array of the displayable controllers\n     *  to which a display name was added\n     */\n    async function _loadBreadcrumbs(controllers) {\n        const toFetch = [];\n        const keys = [];\n        for (const { action, state, displayName } of controllers) {\n            if (action.id === \"menu\" || (action.type === \"ir.actions.client\" && !displayName)) {\n                continue;\n            }\n            const actionInfo = pick(state, \"action\", \"model\", \"resId\");\n            const key = JSON.stringify(actionInfo);\n            keys.push(key);\n            if (displayName) {\n                breadcrumbCache[key] = { display_name: displayName };\n            }\n            if (key in breadcrumbCache) {\n                continue;\n            }\n            toFetch.push(actionInfo);\n        }\n        if (toFetch.length) {\n            const req = rpc(\"/web/action/load_breadcrumbs\", { actions: toFetch });\n            for (const [i, info] of toFetch.entries()) {\n                const key = JSON.stringify(info);\n                breadcrumbCache[key] = req.then((res) => {\n                    breadcrumbCache[key] = res[i];\n                    return res[i];\n                });\n            }\n        }\n        const results = await Promise.all(keys.map((k) => breadcrumbCache[k]));\n        const controllersToRemove = [];\n        for (const [controller, res] of zip(controllers, results)) {\n            if (\"display_name\" in res) {\n                controller.displayName = res.display_name;\n            } else {\n                controllersToRemove.push(controller);\n                if (\"error\" in res) {\n                    console.warn(\n                        \"The following element was removed from the breadcrumb and from the url.\\n\",\n                        controller.state,\n                        \"\\nThis could be because the action wasn't found or because the user doesn't have the right to access to the record, the original error is :\\n\",\n                        res.error\n                    );\n                }\n            }\n        }\n        return controllers.filter((c) => !controllersToRemove.includes(c));\n    }\n\n    /**\n     * Removes the current dialog from the action service's state.\n     * It returns the dialog's onClose callback to be able to propagate it to the next dialog.\n     *\n     * @return {Function|undefined} When there was a dialog, returns its onClose callback for propagation to next dialog.\n     */\n    async function _removeDialog(closeParams) {\n        if (dialog) {\n            const { onClose, remove } = dialog;\n            await onClose?.(closeParams);\n            dialog = null;\n            // Remove the dialog from the dialog_service.\n            // The code is well enough designed to avoid falling in a function call loop.\n            remove();\n        }\n    }\n\n    /**\n     * Returns the last controller of the current controller stack.\n     *\n     * @returns {Controller|null}\n     */\n    function _getCurrentController() {\n        const stack = controllerStack;\n        return stack.length ? stack[stack.length - 1] : null;\n    }\n\n    /**\n     * Returns the current action, which is the action of the last controller in the stack.\n     *\n     * @returns {Action|null}\n     */\n\n    async function _getCurrentAction() {\n        const currentController = _getCurrentController();\n        let action = null;\n        if (currentController) {\n            if (currentController.virtual) {\n                try {\n                    action = await _loadAction(currentController.action.id);\n                } catch (error) {\n                    if (\n                        error.exceptionName ===\n                        \"odoo.addons.web.controllers.action.MissingActionError\"\n                    ) {\n                        action = null;\n                    } else {\n                        throw error;\n                    }\n                }\n            } else {\n                action = JSON.parse(currentController.action._originalAction);\n            }\n        }\n        return action;\n    }\n\n    /**\n     * Given an id, xmlid, tag (key of the client action registry) or directly an\n     * object describing an action.\n     *\n     * @private\n     * @param {ActionRequest} actionRequest\n     * @param {Context} [context={}]\n     * @returns {Promise<Action>}\n     */\n    async function _loadAction(actionRequest, context = {}) {\n        if (typeof actionRequest === \"string\" && actionRegistry.contains(actionRequest)) {\n            // actionRequest is a key in the actionRegistry\n            return {\n                target: \"current\",\n                tag: actionRequest,\n                type: \"ir.actions.client\",\n            };\n        }\n\n        if (typeof actionRequest === \"string\" || typeof actionRequest === \"number\") {\n            // actionRequest is an id or an xmlid\n            const ctx = makeContext([user.context, context]);\n            delete ctx.params;\n            const action = await rpc(\n                \"/web/action/load\",\n                {\n                    action_id: actionRequest,\n                    context: ctx,\n                },\n                { cache: { type: \"disk\" } }\n            );\n            if (action.help) {\n                action.help = markup(action.help);\n            }\n            return Object.assign({}, action);\n        }\n\n        // actionRequest is an object describing the action\n        return actionRequest;\n    }\n\n    /**\n     * Makes a controller from the given params.\n     *\n     * @param {Object} params\n     * @returns {Controller}\n     */\n    function _makeController(params) {\n        return {\n            ...params,\n            jsId: `controller_${++id}`,\n            isMounted: false,\n        };\n    }\n\n    /**\n     * this function returns an action description\n     * with a unique jsId.\n     */\n    function _preprocessAction(action, context = {}) {\n        try {\n            delete action._originalAction;\n            action._originalAction = JSON.stringify(action);\n        } catch {\n            // do nothing, the action might simply not be serializable\n        }\n        action.context = makeContext([context, action.context], user.context);\n        const domain = action.domain || [];\n        action.domain =\n            typeof domain === \"string\"\n                ? evaluateExpr(domain, Object.assign({}, user.context, action.context))\n                : domain;\n        if (action.help) {\n            if (isHtmlEmpty(action.help)) {\n                delete action.help;\n            }\n        }\n        action = { ...action }; // manipulate a copy to keep cached action unmodified\n        action.jsId = `action_${++id}`;\n        if (action.type === \"ir.actions.act_window\" || action.type === \"ir.actions.client\") {\n            action.target = action.target || \"current\";\n        }\n        if (action.type === \"ir.actions.act_window\") {\n            action.views = [...action.views.map((v) => [v[0], v[1]])]; // manipulate a copy to keep cached action unmodified\n            action.controllers = {};\n            if (action.views.every((v) => [\"form\", \"search\"].includes(v[1]))) {\n                action.views = action.views.filter((v) => v[1] === \"form\");\n            } else {\n                const searchViewId = action.search_view_id ? action.search_view_id[0] : false;\n                action.views.push([searchViewId, \"search\"]);\n            }\n            if (\"no_breadcrumbs\" in action.context) {\n                action._noBreadcrumbs = action.context.no_breadcrumbs;\n                delete action.context.no_breadcrumbs;\n            }\n        }\n        return action;\n    }\n\n    /**\n     * @private\n     * @param {string} viewType\n     * @throws {Error} if the current controller is not a view\n     * @returns {View | null}\n     */\n    function _getView(viewType) {\n        const currentController = controllerStack[controllerStack.length - 1];\n        if (currentController.action.type !== \"ir.actions.act_window\") {\n            throw new Error(`switchView called but the current controller isn't a view`);\n        }\n        const view = currentController.views.find((view) => view.type === viewType);\n        return view || null;\n    }\n\n    /**\n     * Given a controller stack, returns the list of breadcrumb items.\n     *\n     * @private\n     * @param {ControllerStack} stack\n     * @returns {Breadcrumbs}\n     */\n    function _getBreadcrumbs(stack) {\n        return stack\n            .filter((controller) => controller.action.tag !== \"menu\")\n            .map((controller) => ({\n                jsId: controller.jsId,\n                get name() {\n                    return controller.displayName;\n                },\n                get isFormView() {\n                    return controller.props?.type === \"form\";\n                },\n                get url() {\n                    return router.stateToUrl(controller.state);\n                },\n                onSelected() {\n                    restore(controller.jsId);\n                },\n            }));\n    }\n\n    /**\n     * @private\n     * @param {object} state the state from which to get the action params\n     * @returns {{ actionRequest: object, options: object} | null}\n     */\n    function _getActionParams(state) {\n        const options = {};\n        let actionRequest = null;\n        const storedAction = browser.sessionStorage.getItem(\"current_action\");\n        const lastAction = JSON.parse(storedAction || \"{}\");\n        // If this method is called because of a company switch, the\n        // stored allowed_company_ids is incorrect.\n        delete lastAction.context?.allowed_company_ids;\n        if (lastAction.help) {\n            lastAction.help = markup(lastAction.help);\n        }\n        if (state.action) {\n            const context = {};\n            if (state.active_id) {\n                context.active_id = state.active_id;\n            }\n            if (state.active_ids) {\n                context.active_ids = parseActiveIds(state.active_ids);\n            } else if (state.active_id) {\n                context.active_ids = [state.active_id];\n            }\n            // ClientAction\n            const [actionRequestKey, clientAction] = actionRegistry.contains(state.action)\n                ? [state.action, actionRegistry.get(state.action)]\n                : actionRegistry.getEntries().find((a) => a[1].path === state.action) ?? [];\n            if (actionRequestKey && clientAction) {\n                actionRequest = {\n                    context,\n                    params: state,\n                    tag: actionRequestKey,\n                    type: \"ir.actions.client\",\n                };\n                if (clientAction.path) {\n                    actionRequest.path = clientAction.path;\n                }\n            } else {\n                // The action to load isn't the current one => executes it\n                Object.assign(options, {\n                    additionalContext: context,\n                    viewType: state.resId ? \"form\" : state.view_type,\n                });\n                if (\n                    [lastAction.id, lastAction.path, lastAction.xml_id]\n                        .filter(Boolean)\n                        .includes(state.action) &&\n                    (!lastAction.context?.active_id ||\n                        lastAction.context?.active_id === context.active_id) &&\n                    (!lastAction.context?.active_ids ||\n                        shallowEqual(lastAction.context?.active_ids, context.active_ids))\n                ) {\n                    actionRequest = lastAction;\n                } else {\n                    actionRequest = state.action;\n                }\n            }\n            if ((state.resId && state.resId !== \"new\") || state.globalState) {\n                options.props = {};\n                if (state.resId && state.resId !== \"new\") {\n                    options.props.resId = state.resId;\n                }\n                if (state.globalState) {\n                    options.props.globalState = state.globalState;\n                }\n            }\n        } else if (state.model) {\n            if (state.resId || state.view_type === \"form\") {\n                actionRequest = {\n                    res_model: state.model,\n                    res_id: state.resId === \"new\" ? undefined : state.resId,\n                    type: \"ir.actions.act_window\",\n                    views: [[state.view_id ? state.view_id : false, \"form\"]],\n                };\n            } else {\n                // This is a window action on a multi-record view => restores it from\n                // the session storage\n                if (lastAction.res_model === state.model) {\n                    actionRequest = lastAction;\n                    options.viewType = state.view_type;\n                }\n            }\n        }\n        if (!actionRequest) {\n            // If the last action isn't valid (eg a model with no resId and no view_type) which can\n            // happen if the user edits the url and removes the id from the end of the url, we don't want\n            // to send him back to the home menu: we unwind the actionStack until we find a valid action\n            const { actionStack } = state;\n            if (actionStack?.length > 1) {\n                const nextState = { actionStack: actionStack.slice(0, -1) };\n                Object.assign(nextState, nextState.actionStack.at(-1));\n                const params = _getActionParams(nextState);\n                // Place the controller at the found position in the action stack to remove all the\n                // invalid virtual controllers.\n                if (params.options && params.options.index === undefined) {\n                    params.options.index = nextState.actionStack.length - 1;\n                }\n                return params;\n            }\n            // Fall back to the home action if no valid action was found\n            actionRequest = user.homeActionId;\n        }\n        return actionRequest ? { actionRequest, options } : null;\n    }\n\n    /**\n     * @param {ClientAction} action\n     * @param {Object} props\n     * @returns {{ props: ActionProps, config: Config }}\n     */\n    function _getActionInfo(action, props) {\n        const actionProps = Object.assign({}, props, { action, actionId: action.id });\n        const currentState = {\n            resId: actionProps.resId || false,\n            active_id: action.context.active_id || false,\n        };\n        actionProps.updateActionState = (controller, patchState) => {\n            const oldState = { ...currentState };\n            Object.assign(currentState, patchState);\n            const changed = !shallowEqual(currentState, oldState);\n            if (changed && action.target !== \"new\" && controller.isMounted) {\n                pushState();\n            }\n        };\n        return {\n            props: actionProps,\n            currentState,\n            config: {\n                actionId: action.id,\n                actionType: \"ir.actions.client\",\n            },\n            displayName: action.display_name || action.name || \"\",\n        };\n    }\n\n    /**\n     * @param {Action} action\n     * @returns {ActionMode}\n     */\n    function _getActionMode(action) {\n        if (action.target === \"new\") {\n            // No possible override for target=\"new\"\n            return \"new\";\n        }\n        if (action.type === \"ir.actions.client\") {\n            const clientAction = actionRegistry.get(action.tag);\n            if (clientAction.target) {\n                // Target is forced by the definition of the client action\n                return clientAction.target;\n            }\n        }\n        if (action.target === \"fullscreen\") {\n            return \"fullscreen\";\n        }\n        // Default: current\n        return \"current\";\n    }\n\n    /**\n     * @param {BaseView} view\n     * @param {ActWindowAction} action\n     * @param {BaseView[]} views\n     * @param {Object} props\n     */\n    function _getViewInfo(view, action, views, props = {}) {\n        const target = action.target;\n        const viewSwitcherEntries = views\n            .filter((v) => v.multiRecord === view.multiRecord)\n            .map((v) => {\n                const viewSwitcherEntry = {\n                    icon: v.icon,\n                    name: v.display_name,\n                    type: v.type,\n                    multiRecord: v.multiRecord,\n                };\n                if (view.type === v.type) {\n                    viewSwitcherEntry.active = true;\n                }\n                return viewSwitcherEntry;\n            });\n        const context = action.context || {};\n        let groupBy = context.group_by || [];\n        if (typeof groupBy === \"string\") {\n            groupBy = [groupBy];\n        }\n        const openFormView = (resId, { activeIds, readonly, force, newWindow } = {}) => {\n            if (target !== \"new\") {\n                if (_getView(\"form\")) {\n                    return switchView(\n                        \"form\",\n                        { readonly, resId, resIds: activeIds },\n                        { newWindow }\n                    );\n                } else if (force || !resId) {\n                    return doAction(\n                        {\n                            type: \"ir.actions.act_window\",\n                            res_model: action.res_model,\n                            views: [[false, \"form\"]],\n                        },\n                        { newWindow, props: { readonly, resId, resIds: activeIds } }\n                    );\n                }\n            }\n        };\n        const viewProps = Object.assign({}, props, {\n            context,\n            display: { mode: target === \"new\" ? \"inDialog\" : target },\n            domain: action.domain || [],\n            groupBy,\n            loadActionMenus: target !== \"new\" && action.res_model !== \"res.config.settings\",\n            loadIrFilters: action.views.some((v) => v[1] === \"search\"),\n            resModel: action.res_model,\n            type: view.type,\n            selectRecord: openFormView,\n            createRecord: () => openFormView(false),\n        });\n        if (view.type === \"form\") {\n            if (target === \"new\") {\n                viewProps.readonly = false;\n                if (!viewProps.onSave) {\n                    viewProps.onSave = (record, params) => {\n                        if (params && params.closable) {\n                            doAction({ type: \"ir.actions.act_window_close\" });\n                        }\n                    };\n                }\n            }\n        }\n\n        const specialKeys = [\"help\", \"useSampleModel\", \"limit\", \"count\"];\n        for (const key of specialKeys) {\n            if (key in action) {\n                if (key === \"help\") {\n                    viewProps.noContentHelp = action.help;\n                } else {\n                    viewProps[key] = action[key];\n                }\n            }\n        }\n\n        if (context.search_disable_custom_filters) {\n            viewProps.activateFavorite = false;\n        }\n\n        // view specific\n        if (!viewProps.resId) {\n            viewProps.resId = action.res_id || false;\n        }\n\n        const currentState = {\n            resId: viewProps.resId,\n            active_id: action.context.active_id || false,\n        };\n        viewProps.updateActionState = (controller, patchState) => {\n            const oldState = { ...currentState };\n            Object.assign(currentState, patchState);\n            const changed = !shallowEqual(currentState, oldState);\n            if (changed && target !== \"new\" && controller.isMounted) {\n                pushState();\n            }\n        };\n\n        viewProps.noBreadcrumbs =\n            \"_noBreadcrumbs\" in action ? action._noBreadcrumbs : target === \"new\";\n\n        const embeddedActions =\n            view.type === \"form\"\n                ? []\n                : context.parent_action_embedded_actions || action.embedded_action_ids;\n        const parentActionId = (view.type !== \"form\" && context.parent_action_id) || false;\n        const currentEmbeddedActionId = context.current_embedded_action_id || false;\n        return {\n            props: viewProps,\n            currentState,\n            config: {\n                actionId: action.id,\n                actionName: action.name,\n                cache: action.cache,\n                actionType: \"ir.actions.act_window\",\n                actionXmlId: action.xml_id,\n                embeddedActions,\n                parentActionId,\n                currentEmbeddedActionId,\n                views: action.views,\n                viewSwitcherEntries,\n            },\n            displayName: action.display_name || action.name || \"\",\n        };\n    }\n\n    /**\n     * Computes the position of the controller in the nextStack according to options\n     * @param {ActionOptions} options\n     */\n    function _computeStackIndex(options) {\n        if (options.clearBreadcrumbs) {\n            return 0;\n        } else if (options.stackPosition === \"replaceCurrentAction\") {\n            const currentController = controllerStack[controllerStack.length - 1];\n            if (currentController) {\n                return controllerStack.findIndex(\n                    (ct) => ct.action.jsId === currentController.action.jsId\n                );\n            }\n        } else if (options.stackPosition === \"replacePreviousAction\") {\n            let last;\n            for (let i = controllerStack.length - 1; i >= 0; i--) {\n                const action = controllerStack[i].action.jsId;\n                if (!last) {\n                    last = action;\n                }\n                if (action !== last) {\n                    last = action;\n                    break;\n                }\n            }\n            if (last) {\n                return controllerStack.findIndex((ct) => ct.action.jsId === last);\n            }\n            // TODO: throw if there is no previous action?\n        } else if (options.index !== undefined) {\n            return options.index;\n        }\n        return controllerStack.length;\n    }\n\n    /**\n     * Open the action in a new window\n     *\n     * @param {ActionDescription} action\n     * @param {Object} state\n     */\n\n    function _openActionInNewWindow(action, state) {\n        // Session storage is duplicated in the new window\n        // https://html.spec.whatwg.org/multipage/webstorage.html#webstorage\n        // \"After creating a new auxiliary browsing context and document, the session storage is copied over.\"\n\n        // Store current action of the current window\n        const currentAction = browser.sessionStorage.getItem(\"current_action\");\n        const currentState = browser.sessionStorage.getItem(\"current_state\");\n        // Store on the session the action for the new window\n        browser.sessionStorage.setItem(\"current_action\", action._originalAction || \"{}\");\n        browser.sessionStorage.setItem(\"current_state\", JSON.stringify(state));\n        _openURL(router.stateToUrl(state));\n        // restore the current action from the current window\n        browser.sessionStorage.setItem(\"current_action\", currentAction);\n        browser.sessionStorage.setItem(\"current_state\", currentState);\n    }\n\n    /**\n     * Triggers a re-rendering with respect to the given controller.\n     *\n     * @private\n     * @param {Controller} controller\n     * @param {UpdateStackOptions} options\n     * @param {boolean} [options.clearBreadcrumbs=false]\n     * @param {number} [options.index]\n     * @returns {Promise<Number>}\n     */\n    async function _updateUI(controller, options = {}) {\n        let resolve;\n        let reject;\n        let removeDialogFn;\n        const currentActionProm = new Promise((_res, _rej) => {\n            resolve = _res;\n            reject = _rej;\n        });\n        const action = controller.action;\n        if (action.target !== \"new\" && \"newStack\" in options) {\n            controllerStack = options.newStack;\n        }\n        const index = _computeStackIndex(options);\n        const nextStack = [...controllerStack.slice(0, index), controller];\n        if (action.target !== \"new\" && options.newWindow) {\n            return _openActionInNewWindow(action, makeState(nextStack));\n        }\n        // Compute breadcrumbs\n        controller.config.breadcrumbs = reactive(\n            action.target === \"new\" ? [] : _getBreadcrumbs(nextStack)\n        );\n        controller.config.getDisplayName = () => controller.displayName;\n        controller.config.setDisplayName = (displayName) => {\n            controller.displayName = displayName;\n            if (controller === _getCurrentController()) {\n                // if not mounted yet, will be done in \"mounted\"\n                env.services.title.setParts({ action: controller.displayName });\n            }\n            if (action.target !== \"new\") {\n                // This is a hack to force the reactivity when a new displayName is set\n                controller.config.breadcrumbs.push(undefined);\n                controller.config.breadcrumbs.pop();\n            }\n        };\n        controller.config.setCurrentEmbeddedAction = (embeddedActionId) => {\n            controller.currentEmbeddedActionId = embeddedActionId;\n        };\n        controller.config.setEmbeddedActions = (embeddedActions) => {\n            controller.embeddedActions = embeddedActions;\n        };\n        controller.config.historyBack = () => {\n            const previousController = controllerStack[controllerStack.length - 2];\n            if (previousController) {\n                restore(previousController.jsId);\n            } else {\n                env.bus.trigger(\"WEBCLIENT:LOAD_DEFAULT_APP\");\n            }\n        };\n        controller.config.isReloadingController = controller === controllerStack.at(-1);\n\n        class ControllerComponent extends Component {\n            static template = ControllerComponentTemplate;\n            static Component = controller.Component;\n            static props = {\n                \"*\": true,\n            };\n            setup() {\n                this.Component = controller.Component;\n                this.titleService = useService(\"title\");\n                useDebugCategory(\"action\", { action });\n                useChildSubEnv({\n                    config: controller.config,\n                    pushStateBeforeReload: () => {\n                        if (controller.isMounted) {\n                            return;\n                        }\n                        pushState(nextStack);\n                    },\n                });\n                if (action.target !== \"new\") {\n                    this.__beforeLeave__ = new CallbackRecorder();\n                    this.__getGlobalState__ = new CallbackRecorder();\n                    this.__getLocalState__ = new CallbackRecorder();\n                    useBus(env.bus, \"CLEAR-UNCOMMITTED-CHANGES\", (ev) => {\n                        const callbacks = ev.detail;\n                        const beforeLeaveFns = this.__beforeLeave__.callbacks;\n                        callbacks.push(...beforeLeaveFns);\n                    });\n                    if (this.constructor.Component !== View) {\n                        useChildSubEnv({\n                            __beforeLeave__: this.__beforeLeave__,\n                            __getGlobalState__: this.__getGlobalState__,\n                            __getLocalState__: this.__getLocalState__,\n                        });\n                    }\n                }\n\n                onMounted(this.onMounted);\n                onWillUnmount(this.onWillUnmount);\n                onError(this.onError);\n            }\n            onError(error) {\n                if (controller.isMounted) {\n                    // the error occurred on the controller which is\n                    // already in the DOM, so simply show the error\n                    Promise.reject(error);\n                    return;\n                }\n                if (!controller.isMounted && status(this) === \"mounted\") {\n                    // The error occured during an onMounted hook of one of the components.\n                    env.bus.trigger(\"ACTION_MANAGER:UPDATE\", {\n                        id: ++id,\n                        Component: BlankComponent,\n                        componentProps: {\n                            onMounted: () => {},\n                            withControlPanel: action.type === \"ir.actions.act_window\",\n                        },\n                    });\n                    Promise.reject(error);\n                    return;\n                }\n                // forward the error to the _updateUI caller then restore the action container\n                // to an unbroken state\n                reject(error);\n                if (action.target === \"new\") {\n                    removeDialogFn?.();\n                    return;\n                }\n                const index = controllerStack.findIndex((ct) => ct.jsId === controller.jsId);\n                if (index > 0) {\n                    // The error occurred while rendering an existing controller,\n                    // so go back to the previous controller, of the current faulty one.\n                    // This occurs when clicking on a breadcrumbs.\n                    return restore(controllerStack[index - 1].jsId);\n                }\n                if (index === 0) {\n                    // No previous controller to restore, so do nothing but display the error\n                    return;\n                }\n                const lastController = controllerStack.at(-1);\n                if (lastController) {\n                    if (lastController.jsId !== controller.jsId) {\n                        // the error occurred while rendering a new controller,\n                        // so go back to the last non faulty controller\n                        // (the error will be shown anyway as the promise\n                        // has been rejected)\n                        return restore(lastController.jsId);\n                    }\n                } else {\n                    env.bus.trigger(\"ACTION_MANAGER:UPDATE\", {});\n                }\n            }\n            onMounted() {\n                if (action.target === \"new\") {\n                    dialog?.remove();\n                    dialog = nextDialog;\n                } else {\n                    controller.getGlobalState = () => {\n                        const exportFns = this.__getGlobalState__.callbacks;\n                        if (exportFns.length) {\n                            return Object.assign({}, ...exportFns.map((fn) => fn()));\n                        }\n                    };\n                    controller.getLocalState = () => {\n                        const exportFns = this.__getLocalState__.callbacks;\n                        if (exportFns.length) {\n                            return Object.assign({}, ...exportFns.map((fn) => fn()));\n                        }\n                    };\n\n                    controllerStack = nextStack; // the controller is mounted, commit the new stack\n                    pushState();\n                    this.titleService.setParts({ action: controller.displayName });\n                    browser.sessionStorage.setItem(\n                        \"current_action\",\n                        action._originalAction || \"{}\"\n                    );\n                    browser.sessionStorage.setItem(\"current_lang\", user.lang);\n                }\n                resolve();\n                env.bus.trigger(\"ACTION_MANAGER:UI-UPDATED\", _getActionMode(action));\n                controller.isMounted = true;\n            }\n            onWillUnmount() {\n                controller.isMounted = false;\n            }\n            get componentProps() {\n                const componentProps = { ...this.props };\n                const updateActionState = componentProps.updateActionState;\n                componentProps.updateActionState = (newState) =>\n                    updateActionState(controller, newState);\n                if (this.constructor.Component === View) {\n                    componentProps.__beforeLeave__ = this.__beforeLeave__;\n                    componentProps.__getGlobalState__ = this.__getGlobalState__;\n                    componentProps.__getLocalState__ = this.__getLocalState__;\n                }\n                return componentProps;\n            }\n        }\n        if (action.target === \"new\") {\n            const actionDialogProps = {\n                ActionComponent: ControllerComponent,\n                actionProps: controller.props,\n                actionType: action.type,\n            };\n            if (action.name) {\n                actionDialogProps.title = action.name;\n            }\n            const size = DIALOG_SIZES[action.context.dialog_size];\n            if (size) {\n                actionDialogProps.size = size;\n            }\n            actionDialogProps.header = action.context.header ?? actionDialogProps.header;\n            actionDialogProps.footer = action.context.footer ?? actionDialogProps.footer;\n            const onClose = dialog?.onClose;\n            delete dialog?.onClose;\n            removeDialogFn = env.services.dialog.add(ActionDialog, actionDialogProps, {\n                onClose: (closeParams) => _removeDialog(closeParams),\n            });\n            if (nextDialog) {\n                nextDialog.remove();\n            }\n            nextDialog = {\n                remove: removeDialogFn,\n                onClose: onClose || options.onClose,\n            };\n            return currentActionProm;\n        }\n\n        const currentController = _getCurrentController();\n        if (currentController && currentController.getLocalState) {\n            currentController.exportedState = currentController.getLocalState();\n        }\n        if (controller.exportedState) {\n            controller.props.state = controller.exportedState;\n        }\n\n        // TODO DAM Remarks:\n        // this thing seems useless for client actions.\n        // restore and switchView (at least) use this --> cannot be done in switchView only\n        // if prop globalState has been passed in doAction, since the action is new the prop won't be overridden in l655.\n        // if globalState is not useful for client actions --> maybe use that thing in useSetupView instead of useSetupAction?\n        // a good thing: the Object.assign seems to reflect the use of \"externalState\" in legacy Model class --> things should be fine.\n        if (currentController && currentController.getGlobalState) {\n            const globalState = Object.assign(\n                {},\n                currentController.action.globalState,\n                currentController.getGlobalState() // what if this = {}?\n            );\n\n            currentController.action.globalState = globalState;\n            // Avoid pushing the globalState, if the state on the router was changed.\n            // For instance, if a link was clicked, the state of the router will be the one of the link and not the one of the currentController.\n            // Or when using the back or forward buttons on the browser.\n            if (\n                currentController.state.action === router.current.action &&\n                currentController.state.active_id === router.current.active_id &&\n                currentController.state.resId === router.current.resId\n            ) {\n                router.pushState({ globalState }, { sync: true });\n            }\n        }\n        if (controller.action.globalState) {\n            controller.props.globalState = controller.action.globalState;\n        }\n\n        if (options.clearBreadcrumbs && !options.noEmptyTransition) {\n            const def = new Deferred();\n            env.bus.trigger(\"ACTION_MANAGER:UPDATE\", {\n                id: ++id,\n                Component: BlankComponent,\n                componentProps: {\n                    onMounted: () => def.resolve(),\n                    withControlPanel: action.type === \"ir.actions.act_window\",\n                },\n            });\n            await def;\n        }\n        if (options.onActionReady) {\n            options.onActionReady(action);\n        }\n        controller.__info__ = {\n            id: ++id,\n            Component: ControllerComponent,\n            componentProps: controller.props,\n        };\n        env.services.dialog.closeAll({ noReload: true });\n        env.bus.trigger(\"ACTION_MANAGER:UPDATE\", controller.__info__);\n        await currentActionProm;\n    }\n\n    // ---------------------------------------------------------------------------\n    // ir.actions.act_url\n    // ---------------------------------------------------------------------------\n\n    function _openURL(url) {\n        const w = browser.open(url, \"_blank\");\n        if (!w || w.closed || typeof w.closed === \"undefined\") {\n            const msg = _t(\n                \"A popup window has been blocked. You may need to change your \" +\n                    \"browser settings to allow popup windows for this page.\"\n            );\n            env.services.notification.add(msg, {\n                sticky: true,\n                type: \"warning\",\n            });\n        }\n    }\n\n    /**\n     * Executes actions of type 'ir.actions.act_url', i.e. redirects to the\n     * given url.\n     *\n     * @private\n     * @param {ActURLAction} action\n     * @param {ActionOptions} options\n     */\n    function _executeActURLAction(action, options) {\n        let url = action.url;\n        if (url && !(url.startsWith(\"http\") || url.startsWith(\"/\"))) {\n            url = \"/\" + url;\n        }\n        if (action.target === \"self\") {\n            browser.location.assign(url);\n        } else if (action.target === \"download\") {\n            _openURL(url);\n        } else {\n            _openURL(url);\n            if (action.close) {\n                return doAction(\n                    { type: \"ir.actions.act_window_close\" },\n                    { onClose: options.onClose }\n                );\n            } else if (options.onClose) {\n                options.onClose();\n            }\n        }\n    }\n\n    // ---------------------------------------------------------------------------\n    // ir.actions.act_window\n    // ---------------------------------------------------------------------------\n\n    /**\n     * Executes an action of type 'ir.actions.act_window'.\n     *\n     * @private\n     * @param {ActWindowAction} action\n     * @param {ActionOptions} options\n     */\n    async function _executeActWindowAction(action, options) {\n        const views = [];\n        const unknown = [];\n        for (const [, type] of action.views) {\n            if (type === \"search\") {\n                continue;\n            }\n            if (session.view_info[type]) {\n                const { icon, display_name, multi_record: multiRecord } = session.view_info[type];\n                views.push({ icon, display_name, multiRecord, type });\n            } else {\n                unknown.push(type);\n            }\n        }\n        if (unknown.length) {\n            throw new Error(\n                `View types not defined ${unknown.join(\", \")} found in act_window action ${\n                    action.id\n                }`\n            );\n        }\n        if (!views.length) {\n            throw new Error(`No view found for act_window action ${action.id}`);\n        }\n\n        let view = (options.viewType && views.find((v) => v.type === options.viewType)) || views[0];\n        if (env.isSmall) {\n            view = _findView(views, view.multiRecord, action.mobile_view_mode) || view;\n        }\n\n        const controller = _makeController({\n            Component: View,\n            action,\n            view,\n            views,\n            ..._getViewInfo(view, action, views, options.props),\n        });\n        action.controllers[view.type] = controller;\n\n        const newStackLastController = options.newStack?.at(-1);\n        if (newStackLastController?.lazy) {\n            const multiView = action.views.find(\n                (view) => view[1] !== \"form\" && view[1] !== \"search\"\n            );\n            if (multiView) {\n                // If the current action has a multi-record view, we add the last\n                // controller to the breadcrumb controllers.\n                delete newStackLastController.lazy;\n                newStackLastController.displayName = action.display_name || action.name || \"\";\n                newStackLastController.action = action;\n                newStackLastController.props.type = multiView[1];\n            } else {\n                // If the current action doesn't have a multi-record view,\n                // we don't need to add the last controller to the breadcrumb controllers\n                options.newStack.splice(-1);\n            }\n        }\n        return _updateUI(controller, options);\n    }\n\n    /**\n     * @private\n     * @param {Array} views an array of views\n     * @param {boolean} multiRecord true if we search for a multiRecord view\n     * @param {string} viewType type of the view to search\n     * @returns {Object|undefined} the requested view if it could be found\n     */\n    function _findView(views, multiRecord, viewType) {\n        return views.find((v) => v.type === viewType && v.multiRecord == multiRecord);\n    }\n\n    // ---------------------------------------------------------------------------\n    // ir.actions.client\n    // ---------------------------------------------------------------------------\n\n    /**\n     * Executes an action of type 'ir.actions.client'.\n     *\n     * @private\n     * @param {ClientAction} action\n     * @param {ActionOptions} options\n     */\n    async function _executeClientAction(action, options) {\n        const clientAction = actionRegistry.get(action.tag);\n        action.path ||= clientAction.path;\n        if (clientAction.prototype instanceof Component) {\n            if (action.target !== \"new\" && !options.newWindow) {\n                const canProceed = await clearUncommittedChanges(env, pick(options, \"forceLeave\"));\n                if (!canProceed) {\n                    return;\n                }\n                if (clientAction.target) {\n                    action.target = clientAction.target;\n                }\n            }\n            const props = clientAction.extractProps?.(action) || {};\n            const controller = _makeController({\n                Component: clientAction,\n                action,\n                ..._getActionInfo(action, { ...props, ...options.props }),\n            });\n            controller.displayName ||= clientAction.displayName?.toString() || \"\";\n            return _updateUI(controller, options);\n        } else {\n            const next = await clientAction(env, action, options);\n            if (next) {\n                return doAction(next, options);\n            }\n        }\n    }\n\n    // ---------------------------------------------------------------------------\n    // ir.actions.report\n    // ---------------------------------------------------------------------------\n\n    function _executeReportClientAction(action, options) {\n        const props = Object.assign({}, options.props, {\n            data: action.data,\n            display_name: action.display_name,\n            name: action.name,\n            report_file: action.report_file,\n            report_name: action.report_name,\n            report_url: getReportUrl(action, \"html\", user.context),\n            context: Object.assign({}, action.context),\n        });\n\n        const controller = _makeController({\n            Component: ReportAction,\n            action,\n            ..._getActionInfo(action, props),\n        });\n\n        return _updateUI(controller, options);\n    }\n\n    /**\n     * Executes actions of type 'ir.actions.report'.\n     *\n     * @private\n     * @param {ReportAction} action\n     * @param {ActionOptions} options\n     */\n    async function _executeReportAction(action, options) {\n        const handlers = registry.category(\"ir.actions.report handlers\").getAll();\n        for (const handler of handlers) {\n            const result = await handler(action, options, env);\n            if (result) {\n                return result;\n            }\n        }\n        if (action.report_type === \"qweb-html\") {\n            return _executeReportClientAction(action, options);\n        } else if (action.report_type === \"qweb-pdf\" || action.report_type === \"qweb-text\") {\n            const type = action.report_type.slice(5);\n            let success, message;\n            env.services.ui.block();\n            try {\n                const downloadContext = { ...user.context };\n                if (action.context) {\n                    Object.assign(downloadContext, action.context);\n                }\n                ({ success, message } = await downloadReport(rpc, action, type, downloadContext));\n            } finally {\n                env.services.ui.unblock();\n            }\n            if (message) {\n                env.services.notification.add(message, {\n                    sticky: true,\n                    title: _t(\"Report\"),\n                });\n            }\n            if (!success) {\n                return _executeReportClientAction(action, options);\n            }\n            const { onClose } = options;\n            if (action.close_on_report_download) {\n                return doAction({ type: \"ir.actions.act_window_close\" }, { onClose });\n            } else if (onClose) {\n                onClose();\n            }\n        } else {\n            console.error(\n                `The ActionManager can't handle reports of type ${action.report_type}`,\n                action\n            );\n        }\n    }\n\n    // ---------------------------------------------------------------------------\n    // ir.actions.server\n    // ---------------------------------------------------------------------------\n\n    /**\n     * Executes an action of type 'ir.actions.server'.\n     *\n     * @private\n     * @param {ServerAction} action\n     * @param {ActionOptions} options\n     * @returns {Promise<void>}\n     */\n    async function _executeServerAction(action, options) {\n        const runProm = rpc(\"/web/action/run\", {\n            action_id: action.id,\n            context: makeContext([user.context, action.context]),\n        });\n        let nextAction = await keepLast.add(runProm);\n        if (nextAction.help) {\n            nextAction.help = markup(nextAction.help);\n        }\n        nextAction = nextAction || { type: \"ir.actions.act_window_close\" };\n        if (typeof nextAction === \"object\") {\n            nextAction.path ||= action.path;\n        }\n        return doAction(nextAction, options);\n    }\n\n    function _executeCloseAction(params = {}) {\n        if (dialog) {\n            return _removeDialog(params.onCloseInfo);\n        }\n        return params.onClose?.(params.onCloseInfo);\n    }\n\n    // ---------------------------------------------------------------------------\n    // public API\n    // ---------------------------------------------------------------------------\n\n    /**\n     * Main entry point of a 'doAction' request. Loads the action and executes it.\n     *\n     * @param {ActionRequest} actionRequest\n     * @param {ActionOptions} options\n     * @returns {Promise<number | undefined | void>}\n     */\n    async function doAction(actionRequest, options = {}) {\n        const actionProm = _loadAction(actionRequest, options.additionalContext);\n        let action = await keepLast.add(actionProm);\n        action = _preprocessAction(action, options.additionalContext);\n        options.clearBreadcrumbs = action.target === \"main\" || options.clearBreadcrumbs;\n        switch (action.type) {\n            case \"ir.actions.act_url\":\n                return _executeActURLAction(action, options);\n            case \"ir.actions.act_window\":\n                if (action.target !== \"new\" && !options.newWindow) {\n                    const canProceed = await clearUncommittedChanges(\n                        env,\n                        pick(options, \"forceLeave\")\n                    );\n                    if (!canProceed) {\n                        return;\n                    }\n                }\n                return _executeActWindowAction(action, options);\n            case \"ir.actions.act_window_close\":\n                return _executeCloseAction({ onClose: options.onClose, onCloseInfo: action.infos });\n            case \"ir.actions.client\":\n                return _executeClientAction(action, options);\n            case \"ir.actions.server\":\n                return _executeServerAction(action, options);\n            case \"ir.actions.report\":\n                return _executeReportAction(action, options);\n            default: {\n                const handler = actionHandlersRegistry.get(action.type, null);\n                if (handler !== null) {\n                    return handler({ env, action, options });\n                }\n                throw new Error(\n                    `The ActionManager service can't handle actions of type ${action.type}`\n                );\n            }\n        }\n    }\n\n    /**\n     * Executes an action on top of the current one (typically, when a button in a\n     * view is clicked). The button may be of type 'object' (call a given method\n     * of a given model) or 'action' (execute a given action). Alternatively, the\n     * button may have the attribute 'special', and in this case an\n     * 'ir.actions.act_window_close' is executed.\n     *\n     * @param {DoActionButtonParams} params\n     * @params {Object} [options={}]\n     * @params {boolean} [options.isEmbeddedAction] set to true if the action request is an\n     *  embedded action. This allows to do the necessary context cleanup and avoid infinite\n     *  recursion.\n     * @params {boolean} [options.newWindow] set to true to open the action in a new tab/window.\n     * @returns {Promise<void>}\n     */\n    async function doActionButton(params, { isEmbeddedAction, newWindow } = {}) {\n        if (!params.name) {\n            return;\n        }\n        // determine the action to execute according to the params\n        let action;\n        if (!isEmbeddedAction) {\n            for (const key of EMBEDDED_ACTIONS_CTX_KEYS) {\n                delete params.context?.[key];\n            }\n        }\n        const context = makeContext([params.context, params.buttonContext]);\n        const blockUi = exprToBoolean(params[\"block-ui\"]);\n        if (blockUi) {\n            env.services.ui.block();\n        }\n        if (params.special) {\n            action = { type: \"ir.actions.act_window_close\", infos: { special: true } };\n        } else if (params.type === \"object\") {\n            // call a Python Object method, which may return an action to execute\n            let args = params.resId ? [[params.resId]] : [params.resIds];\n            if (params.args) {\n                let additionalArgs;\n                try {\n                    // warning: quotes and double quotes problem due to json and xml clash\n                    // maybe we should force escaping in xml or do a better parse of the args array\n                    additionalArgs = JSON.parse(params.args.replace(/'/g, '\"'));\n                } catch {\n                    browser.console.error(\"Could not JSON.parse arguments\", params.args);\n                }\n                args = args.concat(additionalArgs);\n            }\n            const callProm = rpc(`/web/dataset/call_button/${params.resModel}/${params.name}`, {\n                args,\n                kwargs: { context },\n                method: params.name,\n                model: params.resModel,\n            });\n            action = await keepLast.add(callProm);\n            action =\n                action && typeof action === \"object\"\n                    ? action\n                    : { type: \"ir.actions.act_window_close\" };\n            if (action.help) {\n                action.help = markup(action.help);\n            }\n        } else if (params.type === \"action\") {\n            // execute a given action, so load it first\n            context.active_id = params.resId || null;\n            context.active_ids = params.resIds;\n            context.active_model = params.resModel;\n            action = await keepLast.add(_loadAction(params.name, context));\n        } else {\n            if (blockUi) {\n                env.services.ui.unblock();\n            }\n            throw new InvalidButtonParamsError(\"Missing type for doActionButton request\");\n        }\n        if (!isEmbeddedAction && action.embedded_action_ids?.length) {\n            const embeddedActionsKey = `${action.id}+${params.resId || \"\"}`;\n            const embeddedActionsOrder =\n                user.settings.embedded_actions_config_ids?.[embeddedActionsKey]\n                    ?.embedded_actions_order;\n            const embeddedActionId = embeddedActionsOrder?.[0];\n            const embeddedAction = action.embedded_action_ids?.find(\n                (embeddedAction) => embeddedAction.id === embeddedActionId\n            );\n            if (embeddedAction) {\n                const embeddedActions = [\n                    ...action.embedded_action_ids,\n                    {\n                        id: false,\n                        name: action.name,\n                        parent_action_id: action.id,\n                        parent_res_model: action.res_model,\n                        action_id: action.id,\n                        user_id: false,\n                        context: {},\n                    },\n                ];\n                const context = {\n                    ...action.context,\n                    ...(embeddedAction.context ? makeContext([embeddedAction.context]) : {}),\n                    active_id: params.resId,\n                    active_model: params.resModel,\n                    current_embedded_action_id: embeddedActionId,\n                    parent_action_embedded_actions: embeddedActions,\n                    parent_action_id: action.id,\n                };\n                await this.doActionButton(\n                    {\n                        name:\n                            embeddedAction.python_method ||\n                            embeddedAction.action_id[0] ||\n                            embeddedAction.action_id,\n                        resId: params.resId,\n                        context,\n                        type: embeddedAction.python_method ? \"object\" : \"action\",\n                        resModel: embeddedAction.parent_res_model,\n                        viewType: embeddedAction.default_view_mode,\n                    },\n                    { isEmbeddedAction: true }\n                );\n                return;\n            }\n        }\n        // filter out context keys that are specific to the current action, because:\n        //  - wrong default_* and search_default_* values won't give the expected result\n        //  - wrong group_by values will fail and forbid rendering of the destination view\n        const currentCtx = {};\n        for (const key in params.context) {\n            if (key.match(CTX_KEY_REGEX) === null) {\n                currentCtx[key] = params.context[key];\n            }\n        }\n        const activeCtx = { active_model: params.resModel };\n        if (params.resId) {\n            activeCtx.active_id = params.resId;\n            activeCtx.active_ids = [params.resId];\n        }\n        action.context = makeContext([currentCtx, params.buttonContext, activeCtx, action.context]);\n        // in case an effect is returned from python and there is already an effect\n        // attribute on the button, the priority is given to the button attribute\n        const effect = params.effect ? evaluateExpr(params.effect) : action.effect;\n        const { onClose, stackPosition, viewType } = params;\n        await doAction(action, {\n            newWindow,\n            onClose,\n            stackPosition,\n            viewType,\n        });\n        if (params.close) {\n            await _executeCloseAction();\n        }\n        if (blockUi) {\n            env.services.ui.unblock();\n        }\n        if (effect) {\n            env.services.effect.add(effect);\n        }\n    }\n\n    /**\n     * Switches to the given view type in action of the last controller of the\n     * stack. This action must be of type 'ir.actions.act_window'.\n     *\n     * @param {ViewType} viewType\n     * @param {Object} [props={}]\n     * @params {Object} [options={}]\n     * @params {boolean} [options.newWindow] set to true to open the action in a new tab/window.\n     * @throws {ViewNotFoundError} if the viewType is not found on the current action\n     * @returns {Promise<Number>}\n     */\n    async function switchView(viewType, props = {}, { newWindow } = {}) {\n        await keepLast.add(Promise.resolve());\n        if (dialog) {\n            // we don't want to switch view when there's a dialog open, as we would\n            // not switch in the correct action (action in background != dialog action)\n            return;\n        }\n        const controller = controllerStack[controllerStack.length - 1];\n        const view = _getView(viewType);\n        if (!view) {\n            throw new ViewNotFoundError(\n                _t(\"No view of type '%s' could be found in the current action.\", viewType)\n            );\n        }\n        const newController =\n            controller.action.controllers[viewType] ||\n            _makeController({\n                Component: View,\n                action: controller.action,\n                views: controller.views,\n                view,\n            });\n\n        if (!newWindow) {\n            const canProceed = await clearUncommittedChanges(env);\n            if (!canProceed) {\n                return;\n            }\n        }\n\n        Object.assign(\n            newController,\n            _getViewInfo(view, controller.action, controller.views, props)\n        );\n        controller.action.controllers[viewType] = newController;\n        let index;\n        if (view.multiRecord) {\n            index = controllerStack.findIndex((ct) => ct.action.jsId === controller.action.jsId);\n            index = index > -1 ? index : controllerStack.length - 1;\n        } else {\n            // This case would mostly happen when loadState detects a change in the URL.\n            // Also, I guess we may need it when we have other monoRecord views\n            index = controllerStack.findIndex(\n                (ct) => ct.action.jsId === controller.action.jsId && !ct.view.multiRecord\n            );\n            index = index > -1 ? index : controllerStack.length;\n        }\n        return _updateUI(newController, { newWindow, index });\n    }\n\n    /**\n     * Restores a controller from the controller stack given its id. Typically,\n     * this function is called when clicking on the breadcrumbs. If no id is given\n     * restores the previous controller from the stack (penultimate).\n     *\n     * @param {string} jsId\n     */\n    async function restore(jsId) {\n        await keepLast.add(Promise.resolve());\n        let index;\n        if (!jsId) {\n            index = controllerStack.length - 2;\n        } else {\n            index = controllerStack.findIndex((controller) => controller.jsId === jsId);\n        }\n        if (index < 0) {\n            const msg = jsId ? \"Invalid controller to restore\" : \"No controller to restore\";\n            throw new ControllerNotFoundError(msg);\n        }\n        const canProceed = await clearUncommittedChanges(env);\n        if (!canProceed) {\n            return;\n        }\n        const controller = controllerStack[index];\n        if (controller.virtual) {\n            const actionParams = _getActionParams(controller.state);\n            if (!actionParams) {\n                throw new Error(\"Attempted to restore a virtual controller whose state is invalid\");\n            }\n            const { actionRequest, options } = actionParams;\n            controllerStack = controllerStack.slice(0, index);\n            return doAction(actionRequest, options);\n        }\n        if (controller.action.type === \"ir.actions.act_window\") {\n            if (controller.isMounted) {\n                controller.exportedState = controller.getLocalState();\n            }\n            const { action, exportedState, view, views } = controller;\n            const props = { ...controller.props };\n            if (exportedState && \"resId\" in exportedState) {\n                // When restoring, we want to use the last exported ID of the controller\n                props.resId = exportedState.resId;\n            }\n            Object.assign(controller, _getViewInfo(view, action, views, props));\n        }\n        return _updateUI(controller, { index });\n    }\n\n    /**\n     * Restores a stack of virtual controllers from the current contents of the\n     * state (usually router.current) and performs a \"doAction\" on the last one.\n     *\n     * @private\n     * @param {object} [state]\n     * @returns {Promise<boolean>} true if doAction was performed\n     */\n\n    async function loadState(state = router.current) {\n        const lang = browser.sessionStorage.getItem(\"current_lang\");\n        if (lang && lang !== user.lang) {\n            browser.sessionStorage.removeItem(\"current_action\");\n            browser.sessionStorage.removeItem(\"current_lang\");\n            browser.sessionStorage.removeItem(\"current_state\");\n        }\n        const newStack = await _controllersFromState(state);\n        const actionParams = _getActionParams(state);\n        if (actionParams) {\n            // Params valid => performs a \"doAction\"\n            const { actionRequest, options } = actionParams;\n            if (options.index) {\n                options.newStack = newStack.slice(0, options.index);\n                delete options.index;\n            } else {\n                options.newStack = newStack;\n            }\n            try {\n                await doAction(actionRequest, options);\n            } catch (error) {\n                if (\n                    error.exceptionName === \"odoo.addons.web.controllers.action.MissingActionError\"\n                ) {\n                    if (state.actionStack.length > 1) {\n                        const newState = {\n                            ...state.actionStack.slice(0, -1).at(-1),\n                            actionStack: [...state.actionStack.slice(0, -1)],\n                        };\n                        return loadState(newState);\n                    } else {\n                        env.bus.trigger(\"WEBCLIENT:LOAD_DEFAULT_APP\");\n                    }\n                } else {\n                    throw error;\n                }\n            }\n            return true;\n        }\n    }\n\n    function makeState(cStack) {\n        const actions = cStack.map((controller) => {\n            const { action, props, displayName } = controller;\n            const actionState = { displayName };\n            if (action.path || action.id) {\n                actionState.action = action.path || action.id;\n            } else if (action.type === \"ir.actions.client\") {\n                actionState.action = action.tag;\n            } else if (action.type === \"ir.actions.act_window\") {\n                actionState.model = props.resModel;\n            }\n            if (action.type === \"ir.actions.act_window\") {\n                actionState.view_type = props.type;\n                if (props.type === \"form\" && action.res_model !== \"res.config.settings\") {\n                    actionState.resId = controller.currentState.resId || \"new\";\n                }\n            }\n            if (action.type === \"ir.actions.client\" && controller.currentState?.resId) {\n                actionState.resId = controller.currentState.resId;\n            }\n\n            if (controller.currentState?.active_id) {\n                const activeId = controller.currentState.active_id;\n                if (activeId) {\n                    actionState.active_id = activeId;\n                }\n            }\n            Object.assign(actionState, omit(controller.currentState || {}, ...PATH_KEYS));\n            return actionState;\n        });\n        const newState = {\n            actionStack: actions,\n        };\n        const stateKeys = [...PATH_KEYS];\n        const { action, props, currentState } = cStack.at(-1);\n        if (props.type !== \"form\" && props.type !== action.views?.[0][1]) {\n            // add view_type only when it's not already known implicitly\n            stateKeys.push(\"view_type\");\n        }\n        if (currentState) {\n            stateKeys.push(...Object.keys(omit(currentState, ...PATH_KEYS)));\n        }\n        return Object.assign(newState, pick(newState.actionStack.at(-1), ...stateKeys));\n    }\n\n    function pushState(cStack = controllerStack) {\n        if (!cStack.length) {\n            return;\n        }\n\n        const newState = makeState(cStack);\n        browser.sessionStorage.setItem(\"current_state\", JSON.stringify(newState));\n\n        cStack.at(-1).state = newState;\n        router.pushState(newState, { replace: true });\n    }\n    return {\n        doAction,\n        doActionButton,\n        switchView,\n        restore,\n        loadState,\n        async loadAction(actionRequest, context) {\n            const action = await _loadAction(actionRequest, context);\n            return _preprocessAction(action, context);\n        },\n        get currentController() {\n            return _getCurrentController();\n        },\n        get currentAction() {\n            return _getCurrentAction();\n        },\n    };\n}\n\nexport const actionService = {\n    dependencies: [\"dialog\", \"effect\", \"localization\", \"notification\", \"title\", \"ui\"],\n    start(env) {\n        return makeActionManager(env);\n    },\n};\n\nregistry.category(\"services\").add(\"action\", actionService);\n", "import { browser } from \"@web/core/browser/browser\";\nimport { router } from \"@web/core/browser/router\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { registry } from \"@web/core/registry\";\nimport { htmlSprintf } from \"@web/core/utils/html\";\n\nimport { markup } from \"@odoo/owl\";\n\nexport function displayNotificationAction(env, action) {\n    const params = action.params || {};\n    const options = {\n        className: params.className || \"\",\n        sticky: params.sticky || false,\n        title: params.title,\n        type: params.type || \"info\",\n    };\n    const links = (params.links || []).map(\n        (link) => markup`<a href=\"${link.url}\" target=\"_blank\">${link.label}</a>`\n    );\n    const message = htmlSprintf(params.message, ...links);\n    env.services.notification.add(message, options);\n    return params.next;\n}\n\nregistry.category(\"actions\").add(\"display_notification\", displayNotificationAction);\n\n/**\n * Client action to reload the whole interface.\n * If action.params.menu_id, it opens the given menu entry.\n * If action.params.action_id, it opens the given action.\n */\nfunction reload(env, action) {\n    const { menu_id, action_id } = action.params || {};\n    let route = { ...router.current };\n\n    if (menu_id || action_id) {\n        route = {};\n        if (menu_id) {\n            route.menu_id = menu_id;\n        }\n        if (action_id) {\n            route.action = action_id;\n        }\n    }\n\n    router.pushState(route, { replace: true, reload: true });\n}\n\nregistry.category(\"actions\").add(\"reload\", reload);\n\n/**\n * Client action to go back home.\n */\nasync function home() {\n    await new Promise((resolve) => {\n        const waitForServer = (delay) => {\n            browser.setTimeout(async () => {\n                rpc(\"/web/webclient/version_info\", {})\n                    .then(resolve)\n                    .catch(() => waitForServer(250));\n            }, delay);\n        };\n        waitForServer(1000);\n    });\n    const url = \"/\" + (browser.location.search || \"\");\n    browser.location.assign(url);\n}\n\nregistry.category(\"actions\").add(\"home\", home);\n\n/**\n * Client action to refresh the session context (making sure HTTP requests will\n * have the right one). It simply reloads the page.\n */\nasync function reloadContext(env, action) {\n    reload(env, action);\n}\n\nregistry.category(\"actions\").add(\"reload_context\", reloadContext);\n\n/**\n * Client action to restore the current controller\n * Serves as a trigger to reload the interface without a full browser reload\n */\nasync function softReload(env, action) {\n    const controller = env.services.action.currentController;\n    if (controller) {\n        await env.services.action.restore(controller.jsId);\n    }\n}\n\nregistry.category(\"actions\").add(\"soft_reload\", softReload);\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { editModelDebug } from \"@web/core/debug/debug_utils\";\nimport { registry } from \"@web/core/registry\";\n\nconst debugRegistry = registry.category(\"debug\");\n\nfunction editAction({ action, env }) {\n    if (!action.id) {\n        return null;\n    }\n    const description = _t(\"Action\");\n    return {\n        type: \"item\",\n        description,\n        callback: () => {\n            editModelDebug(env, description, action.type, action.id);\n        },\n        sequence: 220,\n        section: \"ui\",\n    };\n}\n\nfunction viewFields({ action, env }) {\n    if (!action.res_model) {\n        return null;\n    }\n    const description = _t(\"Fields\");\n    return {\n        type: \"item\",\n        description,\n        callback: async () => {\n            const modelId = (\n                await env.services.orm.search(\"ir.model\", [[\"model\", \"=\", action.res_model]], {\n                    limit: 1,\n                })\n            )[0];\n            env.services.action.doAction({\n                res_model: \"ir.model.fields\",\n                name: description,\n                views: [\n                    [false, \"list\"],\n                    [false, \"form\"],\n                ],\n                domain: [[\"model_id\", \"=\", modelId]],\n                type: \"ir.actions.act_window\",\n                context: {\n                    default_model_id: modelId,\n                },\n            });\n        },\n        sequence: 250,\n        section: \"ui\",\n    };\n}\n\nfunction ViewModel({ action, env }) {\n    if (!action.res_model) {\n        return null;\n    }\n    const modelName = action.res_model;\n    return {\n        type: \"item\",\n        description: _t(\"Model: %s\", modelName),\n        callback: async () => {\n            const modelId = (\n                await env.services.orm.search(\"ir.model\", [[\"model\", \"=\", modelName]], {\n                    limit: 1,\n                })\n            )[0];\n            editModelDebug(env, modelName, \"ir.model\", modelId);\n        },\n        sequence: 210,\n        section: \"ui\",\n    };\n}\n\nfunction manageFilters({ action, env }) {\n    if (!action.res_model) {\n        return null;\n    }\n    const description = _t(\"Filters\");\n    return {\n        type: \"item\",\n        description,\n        callback: () => {\n            // manage_filters\n            env.services.action.doAction({\n                res_model: \"ir.filters\",\n                name: description,\n                views: [\n                    [false, \"list\"],\n                    [false, \"form\"],\n                ],\n                type: \"ir.actions.act_window\",\n                context: {\n                    search_default_my_filters: true,\n                    search_default_model_id: action.res_model,\n                },\n            });\n        },\n        sequence: 260,\n        section: \"ui\",\n    };\n}\n\nfunction viewAccessRights({ accessRights, action, env }) {\n    if (!action.res_model || !accessRights.canSeeModelAccess) {\n        return null;\n    }\n    const description = _t(\"Access Rights\");\n    return {\n        type: \"item\",\n        description,\n        callback: async () => {\n            const modelId = (\n                await env.services.orm.search(\"ir.model\", [[\"model\", \"=\", action.res_model]], {\n                    limit: 1,\n                })\n            )[0];\n            env.services.action.doAction({\n                res_model: \"ir.model.access\",\n                name: description,\n                views: [\n                    [false, \"list\"],\n                    [false, \"form\"],\n                ],\n                domain: [[\"model_id\", \"=\", modelId]],\n                type: \"ir.actions.act_window\",\n                context: {\n                    default_model_id: modelId,\n                },\n            });\n        },\n        sequence: 350,\n        section: \"security\",\n    };\n}\n\nfunction viewRecordRules({ accessRights, action, env }) {\n    if (!action.res_model || !accessRights.canSeeRecordRules) {\n        return null;\n    }\n    const description = _t(\"Model Record Rules\");\n    return {\n        type: \"item\",\n        description: _t(\"Record Rules\"),\n        callback: async () => {\n            const modelId = (\n                await env.services.orm.search(\"ir.model\", [[\"model\", \"=\", action.res_model]], {\n                    limit: 1,\n                })\n            )[0];\n            env.services.action.doAction({\n                res_model: \"ir.rule\",\n                name: description,\n                views: [\n                    [false, \"list\"],\n                    [false, \"form\"],\n                ],\n                domain: [[\"model_id\", \"=\", modelId]],\n                type: \"ir.actions.act_window\",\n                context: {\n                    default_model_id: modelId,\n                },\n            });\n        },\n        sequence: 360,\n        section: \"security\",\n    };\n}\n\ndebugRegistry\n    .category(\"action\")\n    .add(\"editAction\", editAction)\n    .add(\"viewFields\", viewFields)\n    .add(\"ViewModel\", ViewModel)\n    .add(\"manageFilters\", manageFilters)\n    .add(\"viewAccessRights\", viewAccessRights)\n    .add(\"viewRecordRules\", viewRecordRules);\n", "import { registry } from \"@web/core/registry\";\nimport { Transition } from \"@web/core/transition\";\nimport { user } from \"@web/core/user\";\nimport { useBus } from \"@web/core/utils/hooks\";\nimport { BurgerUserMenu } from \"./burger_user_menu/burger_user_menu\";\nimport { MobileSwitchCompanyMenu } from \"./mobile_switch_company_menu/mobile_switch_company_menu\";\n\nimport { Component, useState } from \"@odoo/owl\";\n\n/**\n * This file includes the widget Menu in mobile to render the BurgerMenu which\n * opens fullscreen and displays the user menu and the current app submenus.\n */\n\nconst SWIPE_ACTIVATION_THRESHOLD = 100;\n\nexport class BurgerMenu extends Component {\n    static template = \"web.BurgerMenu\";\n    static props = {};\n    static components = {\n        BurgerUserMenu,\n        MobileSwitchCompanyMenu,\n        Transition,\n    };\n\n    setup() {\n        this.user = user;\n        this.state = useState({\n            isBurgerOpened: false,\n        });\n        this.swipeStartX = null;\n        useBus(this.env.bus, \"HOME-MENU:TOGGLED\", () => {\n            this._closeBurger();\n        });\n        useBus(this.env.bus, \"ACTION_MANAGER:UPDATE\", ({ detail: req }) => {\n            if (req.id) {\n                this._closeBurger();\n            }\n        });\n    }\n    _closeBurger() {\n        this.state.isBurgerOpened = false;\n    }\n    _openBurger() {\n        this.state.isBurgerOpened = true;\n    }\n    _onSwipeStart(ev) {\n        this.swipeStartX = ev.changedTouches[0].clientX;\n    }\n    _onSwipeEnd(ev) {\n        if (!this.swipeStartX) {\n            return;\n        }\n        const deltaX = ev.changedTouches[0].clientX - this.swipeStartX;\n        if (deltaX < SWIPE_ACTIVATION_THRESHOLD) {\n            return;\n        }\n        this._closeBurger();\n        this.swipeStartX = null;\n    }\n}\n\nconst systrayItem = {\n    Component: BurgerMenu,\n};\n\nregistry.category(\"systray\").add(\"burger_menu\", systrayItem, { sequence: 0 });\n", "import { UserMenu } from \"@web/webclient/user_menu/user_menu\";\n\nexport class BurgerUserMenu extends UserMenu {\n    static template = \"web.BurgerUserMenu\";\n    static props = {\n        ...UserMenu.props,\n        onMenuClicked: { type: Function, optional: true },\n    };\n    _onItemClicked(callback) {\n        return (ev) => {\n            callback(ev);\n            this.props.onMenuClicked?.(ev);\n        };\n    }\n}\n", "import { SwitchCompanyMenu } from \"@web/webclient/switch_company_menu/switch_company_menu\";\n\nexport class MobileSwitchCompanyMenu extends SwitchCompanyMenu {\n    static template = \"web.MobileSwitchCompanyMenu\";\n\n    setup() {\n        super.setup();\n        this.state.isOpen = false;\n    }\n\n    get show() {\n        return !this.hasLotsOfCompanies || this.state.isOpen === true;\n    }\n\n    toggleCollapsible() {\n        if (this.hasLotsOfCompanies) {\n            this.state.isOpen = !this.state.isOpen;\n        }\n    }\n}\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { loadBundle } from \"@web/core/assets\";\nimport { registry } from \"@web/core/registry\";\nimport { browser } from \"@web/core/browser/browser\";\n\nexport async function startClickEverywhere(xmlId, light, currentState) {\n    await loadBundle(\"web.assets_clickbot\");\n    window.clickEverywhere(xmlId, light, currentState);\n}\n\nexport function runClickTestItem({ env }) {\n    return {\n        type: \"item\",\n        description: _t(\"Run Click Everywhere\"),\n        callback: () => {\n            startClickEverywhere();\n        },\n        sequence: 460,\n        section: \"testing\",\n    };\n}\n\nconst currentState = JSON.parse(browser.localStorage.getItem(\"running.clickbot\"));\nif (currentState) {\n    startClickEverywhere(currentState.xmlId, currentState.light, currentState);\n}\n\nexport default {\n    startClickEverywhere,\n    runClickTestItem,\n};\n\nregistry.category(\"debug\").category(\"default\").add(\"runClickTestItem\", runClickTestItem);\n", "import { rpcBus } from \"@web/core/network/rpc\";\nimport { registry } from \"@web/core/registry\";\nimport { currencies } from \"@web/core/currency\";\nimport { UPDATE_METHODS } from \"@web/core/orm_service\";\n\nexport const currencyService = {\n    dependencies: [\"orm\"],\n    async: [\"reload_currencies\"],\n    start(env, { orm }) {\n        /**\n         * Reload the currencies (initially given in session_info)\n         */\n        async function reloadCurrencies() {\n            const result = await orm.call(\"res.currency\", \"get_all_currencies\");\n            for (const k in currencies) {\n                delete currencies[k];\n            }\n            Object.assign(currencies, result);\n        }\n        rpcBus.addEventListener(\"RPC:RESPONSE\", (ev) => {\n            const { data, error } = ev.detail;\n            const { model, method } = data.params;\n            if (!error && model === \"res.currency\" && UPDATE_METHODS.includes(method)) {\n                reloadCurrencies();\n            }\n        });\n        return { reloadCurrencies };\n    },\n};\n\nregistry.category(\"services\").add(\"currency\", currencyService);\n", "import { browser } from \"@web/core/browser/browser\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { SelectCreateDialog } from \"@web/views/view_dialogs/select_create_dialog\";\n\nfunction runUnitTestsItem() {\n    const href = \"/web/tests?debug=assets\";\n    return {\n        type: \"item\",\n        description: _t(\"Run Unit Tests\"),\n        href,\n        callback: () => browser.open(href),\n        sequence: 450,\n        section: \"testing\",\n    };\n}\n\nexport function openViewItem({ env }) {\n    async function onSelected(records) {\n        const views = await env.services.orm.searchRead(\n            \"ir.ui.view\",\n            [[\"id\", \"=\", records[0]]],\n            [\"name\", \"model\", \"type\"],\n            { limit: 1 }\n        );\n        const view = views[0];\n        env.services.action.doAction({\n            type: \"ir.actions.act_window\",\n            name: view.name,\n            res_model: view.model,\n            views: [[view.id, view.type]],\n        });\n    }\n\n    return {\n        type: \"item\",\n        description: _t(\"Open View\"),\n        callback: () => {\n            env.services.dialog.add(SelectCreateDialog, {\n                resModel: \"ir.ui.view\",\n                title: _t(\"Select a view\"),\n                multiSelect: false,\n                domain: [\n                    [\"type\", \"!=\", \"qweb\"],\n                    [\"type\", \"!=\", \"search\"],\n                ],\n                onSelected,\n            });\n        },\n        sequence: 540,\n        section: \"tools\",\n    };\n}\n\nregistry\n    .category(\"debug\")\n    .category(\"default\")\n    .add(\"runUnitTestsItem\", runUnitTestsItem)\n    .add(\"openViewItem\", openViewItem);\n", "import { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\nimport { useBus, useService } from \"@web/core/utils/hooks\";\n\nimport { Component, EventBus } from \"@odoo/owl\";\n\nexport class ProfilingItem extends Component {\n    static components = { DropdownItem };\n    static template = \"web.DebugMenu.ProfilingItem\";\n    static props = {\n        bus: { type: EventBus },\n    };\n    setup() {\n        this.profiling = useService(\"profiling\");\n        useBus(this.props.bus, \"UPDATE\", this.render);\n    }\n\n    changeParam(param, ev) {\n        this.profiling.setParam(param, ev.target.value);\n    }\n    toggleParam(param) {\n        const value = this.profiling.state.params.execution_context_qweb;\n        this.profiling.setParam(param, !value);\n    }\n    openProfiles() {\n        if (this.env.services.action) {\n            // using doAction in the backend to preserve breadcrumbs and stuff\n            this.env.services.action.doAction(\"base.action_menu_ir_profile\");\n        } else {\n            // No action service means we are in the frontend.\n            window.location = \"/web/#action=base.action_menu_ir_profile\";\n        }\n    }\n}\n", "import { registry } from \"@web/core/registry\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { loadBundle } from \"@web/core/assets\";\nimport { renderToString } from \"@web/core/utils/render\";\nimport { useDebounced } from \"@web/core/utils/timing\";\nimport { standardFieldProps } from \"@web/views/fields/standard_field_props\";\n\nimport { Component, useState, useRef, onWillStart, onMounted, onWillUnmount } from \"@odoo/owl\";\n\nclass MenuItem extends Component {\n    static template = \"web.ProfilingQwebView.menuitem\";\n    static props = {\n        view: Object,\n    };\n}\n\nfunction processValue(value) {\n    const data = JSON.parse(value);\n    for (const line of data[0].results.data) {\n        line.xpath = line.xpath.replace(/([^\\]])\\//g, \"$1[1]/\").replace(/([^\\]])$/g, \"$1[1]\");\n    }\n    return data;\n}\n\n/**\n * This widget is intended to be used on Text fields. It will provide Ace Editor\n * for display XML and Python profiling.\n */\nexport class ProfilingQwebView extends Component {\n    static template = \"web.ProfilingQwebView\";\n    static components = { MenuItem };\n    static props = { ...standardFieldProps };\n\n    setup() {\n        super.setup();\n\n        this.orm = useService(\"orm\");\n        this.ace = useRef(\"ace\");\n        this.selector = useRef(\"selector\");\n\n        this.value = processValue(this.props.record.data[this.props.name]);\n        this.state = useState({\n            viewID: this.profile.data.length ? this.profile.data[0].view_id : 0,\n            view: null,\n        });\n\n        this.renderProfilingInformation = useDebounced(this.renderProfilingInformation, 100);\n\n        onWillStart(async () => {\n            await loadBundle(\"web.ace_lib\");\n            await this._fetchViewData();\n            this.state.view = this.viewObjects.find((view) => view.id === this.state.viewID);\n        });\n        onMounted(() => {\n            this._startAce(this.ace.el);\n            this._renderView();\n        });\n        onWillUnmount(() => {\n            if (this.aceEditor) {\n                this.aceEditor.destroy();\n            }\n            this._unmoutInfo();\n        });\n    }\n\n    /**\n     * Return JSON values to render the view\n     *\n     * @returns {archs, data: {template, xpath, directive, time, duration, query }[]}\n     */\n    get profile() {\n        return this.value ? this.value[0].results : { archs: {}, data: [] };\n    }\n\n    //--------------------------------------------------------------------------\n    // Public\n    //--------------------------------------------------------------------------\n\n    /**\n     * Return association of view key, view name, query number and total delay\n     *\n     * @private\n     * @returns {Promise<viewObjects>}\n     */\n    async _fetchViewData() {\n        const viewIDs = Array.from(new Set(this.profile.data.map((line) => line.view_id)));\n        const viewObjects = await this.orm.call(\"ir.ui.view\", \"search_read\", [], {\n            fields: [\"id\", \"display_name\", \"key\"],\n            domain: [[\"id\", \"in\", viewIDs]],\n        });\n        for (const view of viewObjects) {\n            view.delay = 0;\n            view.query = 0;\n            const lines = this.profile.data.filter((l) => l.view_id === view.id);\n            const root = lines.find((l) => l.xpath === \"\");\n            if (root) {\n                view.delay += root.delay;\n                view.query += root.query;\n            } else {\n                view.delay = lines.map((l) => l.delay).reduce((a, b) => a + b);\n                view.query = lines.map((l) => l.query).reduce((a, b) => a + b);\n            }\n            view.delay = Math.ceil(view.delay * 10) / 10;\n        }\n        this.viewObjects = viewObjects;\n    }\n\n    /**\n     * Format delay to readable.\n     *\n     * @private\n     * @param {number} delay\n     * @returns {string}\n     */\n    _formatDelay(delay) {\n        return delay ? (Math.ceil(delay * 10) / 10).toFixed(1) : \".\";\n    }\n\n    /**\n     * Starts the ace library on the given DOM element. This initializes the\n     * ace editor in readonly mode.\n     *\n     * @private\n     * @param {Node} node - the DOM element the ace library must initialize on\n     */\n    _startAce(node) {\n        this.aceEditor = window.ace.edit(node);\n        this.aceEditor.setOptions({\n            maxLines: Infinity,\n            showPrintMargin: false,\n            highlightActiveLine: false,\n            highlightGutterLine: true,\n            readOnly: true,\n        });\n        this.aceEditor.renderer.setOptions({\n            displayIndentGuides: true,\n            showGutter: true,\n        });\n        this.aceEditor.renderer.$cursorLayer.element.style.display = \"none\";\n\n        this.aceEditor.$blockScrolling = true;\n        this.aceSession = this.aceEditor.getSession();\n        this.aceSession.setOptions({\n            useWorker: false,\n            mode: \"ace/mode/qweb\",\n            tabSize: 2,\n            useSoftTabs: true,\n        });\n\n        // Ace render 3 times when change the value and 1 time per click.\n        this.aceEditor.renderer.on(\"afterRender\", this.renderProfilingInformation.bind(this));\n    }\n\n    renderProfilingInformation() {\n        this._unmoutInfo();\n\n        const flat = {};\n        const arch = [{ xpath: \"\", children: [] }];\n        const rows = this.ace.el.querySelectorAll(\".ace_gutter .ace_gutter-cell\");\n        const elems = this.ace.el.querySelectorAll(\n            \".ace_tag-open, .ace_end-tag-close, .ace_end-tag-open, .ace_qweb\"\n        );\n        elems.forEach((node) => {\n            const parent = arch[arch.length - 1];\n            let xpath = parent.xpath;\n            if (node.classList.contains(\"ace_end-tag-close\")) {\n                // Close tag.\n                let previous = node;\n                while ((previous = previous.previousElementSibling)) {\n                    if (previous && previous.classList.contains(\"ace_tag-name\")) {\n                        break;\n                    }\n                }\n                const tag = previous && previous.textContent;\n                if (parent.tag === tag) {\n                    // can be different when scroll because ace does not display the previous lines.\n                    arch.pop();\n                }\n            } else if (node.classList.contains(\"ace_end-tag-open\")) {\n                // Auto close tag.\n                const tag = node.nextElementSibling && node.nextElementSibling.textContent;\n                if (parent.tag === tag) {\n                    // can be different when scroll because ace does not display the previous lines.\n                    arch.pop();\n                }\n            } else if (node.classList.contains(\"ace_qweb\")) {\n                // QWeb attribute.\n                const directive = node.textContent;\n                parent.directive.push({\n                    el: node,\n                    directive: directive,\n                });\n\n                // Compute delay and query number.\n                let delay = 0;\n                let query = 0;\n                for (const line of this.profile.data) {\n                    if (\n                        line.view_id === this.state.viewID &&\n                        line.xpath === xpath &&\n                        line.directive.includes(directive)\n                    ) {\n                        delay += line.delay;\n                        query += line.query;\n                    }\n                }\n\n                // Render delay and query number in span visible on hover.\n                if ((delay || query) && !node.querySelector(\".o_info\")) {\n                    this._renderHover(delay, query, node);\n                }\n            } else if (node.classList.contains(\"ace_tag-open\")) {\n                // Open tag.\n                const nodeTagName = node.nextElementSibling;\n                const aceLine = nodeTagName.parentNode;\n                const index = [].indexOf.call(aceLine.parentNode.children, aceLine);\n                const row = rows[index];\n\n                // Add a children to the arch and compute the xpath.\n                xpath += \"/\" + nodeTagName.textContent;\n                let i = 1;\n                while (flat[xpath + \"[\" + i + \"]\"]) {\n                    i++;\n                }\n                xpath += \"[\" + i + \"]\";\n                flat[xpath] = {\n                    xpath: xpath,\n                    tag: nodeTagName.textContent,\n                    children: [],\n                    directive: [],\n                };\n                arch.push(flat[xpath]);\n                parent.children.push(flat[xpath]);\n\n                // Compute delay and query number.\n                const closed = !!row.querySelector(\".ace_closed\");\n                const delays = [];\n                const querys = [];\n                const groups = {};\n                let displayDetail = false;\n                for (const line of this.profile.data) {\n                    if (\n                        line.view_id === this.state.viewID &&\n                        (closed ? line.xpath.startsWith(xpath) : line.xpath === xpath)\n                    ) {\n                        delays.push(line.delay);\n                        querys.push(line.query);\n                        const directive = line.directive.split(\"=\")[0];\n                        if (!groups[directive]) {\n                            groups[directive] = {\n                                delays: [],\n                                querys: [],\n                            };\n                        } else {\n                            displayDetail = true;\n                        }\n                        groups[directive].delays.push(this._formatDelay(line.delay));\n                        groups[directive].querys.push(line.query);\n                    }\n                }\n\n                // Display delay and query number in front of the line.\n                if (delays.length && !row.querySelector(\".o_info\")) {\n                    this._renderInfo(delays, querys, displayDetail, groups, row);\n                }\n            }\n            node.setAttribute(\"data-xpath\", xpath);\n        });\n    }\n    /**\n     * Set the view ID and send atch to ACE.\n     *\n     * @private\n     */\n    _renderView() {\n        const view = this.viewObjects.find((view) => view.id === this.state.viewID);\n        if (view) {\n            const arch = this.profile.archs[view.id] || \"\";\n            if (this.aceSession.getValue() !== arch) {\n                this.aceSession.setValue(arch);\n            }\n        } else {\n            this.aceSession.setValue(\"\");\n        }\n        this.state.view = view;\n    }\n    _unmoutInfo() {\n        if (this.hover) {\n            if (this.ace.el.querySelector(\".o_ace_hover\")) {\n                this.ace.el.querySelector(\".o_ace_hover\").remove();\n            }\n        }\n        if (this.info) {\n            if (this.ace.el.querySelector(\".o_ace_info\")) {\n                this.ace.el.querySelector(\".o_ace_info\").remove();\n            }\n        }\n    }\n    _renderHover(delay, query, node) {\n        const xml = renderToString(\"web.ProfilingQwebView.hover\", {\n            delay: this._formatDelay(delay),\n            query: query,\n        });\n        const div = new DOMParser().parseFromString(xml, \"text/html\").querySelector(\"div\");\n        node.appendChild(div);\n    }\n    _renderInfo(delays, querys, displayDetail, groups, node) {\n        const xml = renderToString(\"web.ProfilingQwebView.info\", {\n            delay: this._formatDelay(delays.reduce((a, b) => a + b, 0)),\n            query: querys.reduce((a, b) => a + b, 0) || \".\",\n            displayDetail: displayDetail,\n            groups: groups,\n        });\n        const div = new DOMParser().parseFromString(xml, \"text/html\").querySelector(\"div\");\n        node.appendChild(div);\n    }\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n     * @private\n     * @param {MouseEvent} ev\n     */\n    _onSelectView(ev) {\n        this.state.viewID = +ev.currentTarget.dataset.id;\n        this._renderView();\n    }\n}\n\nexport const profilingQwebView = {\n    component: ProfilingQwebView,\n};\n\nregistry.category(\"fields\").add(\"profiling_qweb_view\", profilingQwebView);\n", "import { registry } from \"@web/core/registry\";\nimport { ProfilingItem } from \"./profiling_item\";\nimport { session } from \"@web/session\";\nimport { profilingSystrayItem } from \"./profiling_systray_item\";\n\nimport { EventBus, reactive } from \"@odoo/owl\";\n\nconst systrayRegistry = registry.category(\"systray\");\n\nexport const profilingService = {\n    dependencies: [\"orm\"],\n    start(env, { orm }) {\n        // Only set up profiling when in debug mode\n        if (!env.debug) {\n            return;\n        }\n\n        function notify() {\n            if (systrayRegistry.contains(\"web.profiling\") && state.isEnabled === false) {\n                systrayRegistry.remove(\"web.profiling\");\n            }\n            if (!systrayRegistry.contains(\"web.profiling\") && state.isEnabled === true) {\n                systrayRegistry.add(\"web.profiling\", profilingSystrayItem, { sequence: 99 });\n            }\n            bus.trigger(\"UPDATE\");\n        }\n\n        const state = reactive(\n            {\n                session: session.profile_session || false,\n                collectors: session.profile_collectors || [\"sql\", \"traces_async\"],\n                params: session.profile_params || {},\n                get isEnabled() {\n                    return Boolean(state.session);\n                },\n            },\n            notify\n        );\n\n        const bus = new EventBus();\n        notify();\n\n        async function setProfiling(params) {\n            const kwargs = Object.assign(\n                {\n                    collectors: state.collectors,\n                    params: state.params,\n                    profile: state.isEnabled,\n                },\n                params\n            );\n            const resp = await orm.call(\"ir.profile\", \"set_profiling\", [], kwargs);\n            if (resp.type) {\n                // most likely an \"ir.actions.act_window\"\n                env.services.action.doAction(resp);\n            } else {\n                state.session = resp.session;\n                state.collectors = resp.collectors;\n                state.params = resp.params;\n            }\n        }\n\n        function profilingItem() {\n            return {\n                type: \"component\",\n                Component: ProfilingItem,\n                props: { bus },\n                sequence: 570,\n                section: \"tools\",\n            };\n        }\n\n        registry.category(\"debug\").category(\"default\").add(\"profilingItem\", profilingItem);\n\n        return {\n            state,\n            async toggleProfiling() {\n                await setProfiling({ profile: !state.isEnabled });\n            },\n            async toggleCollector(collector) {\n                const nextCollectors = state.collectors.slice();\n                const index = nextCollectors.indexOf(collector);\n                if (index >= 0) {\n                    nextCollectors.splice(index, 1);\n                } else {\n                    nextCollectors.push(collector);\n                }\n                await setProfiling({ collectors: nextCollectors });\n            },\n            async setParam(key, value) {\n                const nextParams = Object.assign({}, state.params);\n                nextParams[key] = value;\n                await setProfiling({ params: nextParams });\n            },\n            isCollectorEnabled(collector) {\n                return state.collectors.includes(collector);\n            },\n        };\n    },\n};\n\nregistry.category(\"services\").add(\"profiling\", profilingService);\n", "import { Component } from \"@odoo/owl\";\n\nclass ProfilingSystrayItem extends Component {\n    static template = \"web.ProfilingSystrayItem\";\n    static props = {};\n}\n\nexport const profilingSystrayItem = {\n    Component: ProfilingSystrayItem,\n};\n", "import { ConnectionLostError } from \"@web/core/network/rpc\";\nimport { registry } from \"@web/core/registry\";\n\nconst errorHandlerRegistry = registry.category(\"error_handlers\");\n\nconst fetchErrorMessages = [\n    \"Failed to fetch\", // Chromium\n    \"Load failed\", // WebKit\n    \"NetworkError when attempting to fetch resource.\", // Firefox\n];\n\n/**\n * @param {OdooEnv} env\n * @param {UncaughError} error\n * @param {Error} originalError\n * @returns {boolean}\n */\nexport function offlineFailToFetchErrorHandler(env, error, originalError) {\n    if (originalError instanceof TypeError && fetchErrorMessages.includes(originalError.message)) {\n        Promise.resolve().then(() => {\n            throw new ConnectionLostError();\n        });\n        return true;\n    }\n}\nerrorHandlerRegistry.add(\"offlineFailToFetchErrorHandler\", offlineFailToFetchErrorHandler, {\n    sequence: 96,\n});\n", "import { browser } from \"@web/core/browser/browser\";\nimport { rpcBus } from \"@web/core/network/rpc\";\nimport { registry } from \"@web/core/registry\";\nimport { useBus } from \"@web/core/utils/hooks\";\nimport { Transition } from \"@web/core/transition\";\n\nimport { Component, useState } from \"@odoo/owl\";\n\n/**\n * Loading Indicator\n *\n * When the user performs an action, it is good to give him some feedback that\n * something is currently happening.  The purpose of the Loading Indicator is to\n * display a small rectangle on the bottom right of the screen with just the\n * text 'Loading' and the number of currently running rpcs.\n *\n * After a delay of 3s, if a rpc is still not completed, we also block the UI.\n */\nexport class LoadingIndicator extends Component {\n    static template = \"web.LoadingIndicator\";\n    static components = { Transition };\n    static props = {};\n\n    setup() {\n        this.state = useState({\n            count: 0,\n            show: false,\n        });\n        this.rpcIds = new Set();\n        this.startShowTimer = null;\n        useBus(rpcBus, \"RPC:REQUEST\", this.requestCall);\n        useBus(rpcBus, \"RPC:RESPONSE\", this.responseCall);\n    }\n\n    requestCall({ detail }) {\n        if (detail.settings.silent) {\n            return;\n        }\n        if (this.state.count === 0) {\n            browser.clearTimeout(this.startShowTimer);\n            this.startShowTimer = browser.setTimeout(() => {\n                if (this.state.count) {\n                    this.state.show = true;\n                }\n            }, 250);\n        }\n        this.rpcIds.add(detail.data.id);\n        this.state.count++;\n    }\n\n    responseCall({ detail }) {\n        if (detail.settings.silent) {\n            return;\n        }\n        this.rpcIds.delete(detail.data.id);\n        this.state.count = this.rpcIds.size;\n        if (this.state.count === 0) {\n            browser.clearTimeout(this.startShowTimer);\n            this.state.show = false;\n        }\n    }\n}\n\nregistry.category(\"main_components\").add(\"LoadingIndicator\", {\n    Component: LoadingIndicator,\n});\n", "/**\n * Traverses the given menu tree, executes the given callback for each node with\n * the node itself and the list of its ancestors as arguments.\n *\n * @param {Object} tree tree of menus as exported by the menus service\n * @param {Function} cb\n * @param {[Object]} [parents] the ancestors of the tree root, if any\n */\nfunction traverseMenuTree(tree, cb, parents = []) {\n    cb(tree, parents);\n    tree.childrenTree.forEach((c) => traverseMenuTree(c, cb, parents.concat([tree])));\n}\n\n/**\n * Computes the \"apps\" and \"menuItems\" from a given menu tree.\n *\n * @param {Object} menuTree tree of menus as exported by the menus service\n * @returns {Object} with keys \"apps\" and \"menuItems\" (HomeMenu props)\n */\nexport function computeAppsAndMenuItems(menuTree) {\n    const apps = [];\n    const menuItems = [];\n    traverseMenuTree(menuTree, (menuItem, parents) => {\n        if (!menuItem.id || !menuItem.actionID) {\n            return;\n        }\n        const isApp = menuItem.id === menuItem.appID;\n        const item = {\n            parents: parents\n                .slice(1)\n                .map((p) => p.name)\n                .join(\" / \"),\n            label: menuItem.name,\n            id: menuItem.id,\n            xmlid: menuItem.xmlid,\n            actionID: menuItem.actionID,\n            href: `/odoo/${menuItem.actionPath || \"action-\" + menuItem.actionID}`,\n            appID: menuItem.appID,\n        };\n        if (isApp) {\n            if (menuItem.webIconData) {\n                item.webIconData = menuItem.webIconData;\n            } else {\n                const [iconClass, color, backgroundColor] = (menuItem.webIcon || \"\").split(\",\");\n                if (backgroundColor !== undefined) {\n                    // Could split in three parts?\n                    item.webIcon = { iconClass, color, backgroundColor };\n                } else {\n                    item.webIconData = \"/web/static/img/default_icon_app.png\";\n                }\n            }\n        } else {\n            item.menuID = parents[1].id;\n        }\n        if (isApp) {\n            apps.push(item);\n        } else {\n            menuItems.push(item);\n        }\n    });\n    return { apps, menuItems };\n}\n\n/**\n * @param {Array} order\n * Sorts the apps in the homescreen menu according to the given order as an array of xmlid strings\n */\nexport function reorderApps(apps, order) {\n    apps.sort((a, b) => {\n        const aIndex = order.indexOf(a.xmlid);\n        const bIndex = order.indexOf(b.xmlid);\n        if (aIndex === -1 && bIndex === -1) {\n            // if both items are not present, sort by original order\n            return apps.indexOf(a) - apps.indexOf(b);\n        }\n        // not found items always before found ones\n        if (aIndex === -1) {\n            return -1;\n        }\n        if (bIndex === -1) {\n            return 1;\n        }\n        return aIndex - bIndex; // sort by order array\n    });\n}\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { fuzzyLookup } from \"@web/core/utils/search\";\nimport { computeAppsAndMenuItems } from \"@web/webclient/menus/menu_helpers\";\nimport { DefaultCommandItem } from \"@web/core/commands/command_palette\";\n\nimport { Component } from \"@odoo/owl\";\n\nclass AppIconCommand extends Component {\n    static template = \"web.AppIconCommand\";\n    static props = {\n        webIconData: { type: String, optional: true },\n        webIcon: { type: Object, optional: true },\n        ...DefaultCommandItem.props,\n    };\n}\n\nconst commandCategoryRegistry = registry.category(\"command_categories\");\ncommandCategoryRegistry.add(\"apps\", { namespace: \"/\" }, { sequence: 10 });\ncommandCategoryRegistry.add(\"menu_items\", { namespace: \"/\" }, { sequence: 20 });\n\nconst commandSetupRegistry = registry.category(\"command_setup\");\ncommandSetupRegistry.add(\"/\", {\n    emptyMessage: _t(\"No menu found\"),\n    name: _t(\"menus\"),\n    placeholder: _t(\"Search for a menu...\"),\n});\n\nconst commandProviderRegistry = registry.category(\"command_provider\");\ncommandProviderRegistry.add(\"menu\", {\n    namespace: \"/\",\n    async provide(env, options) {\n        const result = [];\n        const menuService = env.services.menu;\n        let { apps, menuItems } = computeAppsAndMenuItems(menuService.getMenuAsTree(\"root\"));\n        if (options.searchValue !== \"\") {\n            apps = fuzzyLookup(options.searchValue, apps, (menu) => menu.label);\n\n            fuzzyLookup(options.searchValue, menuItems, (menu) =>\n                (menu.parents + \" / \" + menu.label).split(\"/\").reverse().join(\"/\")\n            ).forEach((menu) => {\n                result.push({\n                    action() {\n                        menuService.selectMenu(menu);\n                    },\n                    category: \"menu_items\",\n                    name: menu.parents + \" / \" + menu.label,\n                    href: menu.href || `#menu_id=${menu.id}&amp;action_id=${menu.actionID}`,\n                });\n            });\n        }\n\n        apps.forEach((menu) => {\n            const props = {};\n            if (menu.webIconData) {\n                const prefix = menu.webIconData.startsWith(\"P\")\n                    ? \"data:image/svg+xml;base64,\"\n                    : \"data:image/png;base64,\";\n                props.webIconData = menu.webIconData.startsWith(\"data:image\")\n                    ? menu.webIconData\n                    : prefix + menu.webIconData.replace(/\\s/g, \"\");\n            } else {\n                props.webIcon = menu.webIcon;\n            }\n            result.push({\n                Component: AppIconCommand,\n                action() {\n                    menuService.selectMenu(menu);\n                },\n                category: \"apps\",\n                name: menu.label,\n                href: menu.href || `#menu_id=${menu.id}&amp;action_id=${menu.actionID}`,\n                props,\n            });\n        });\n\n        return result;\n    },\n});\n", "import { session } from \"@web/session\";\nimport { browser } from \"../../core/browser/browser\";\nimport { registry } from \"../../core/registry\";\n\nconst loadMenusUrl = `/web/webclient/load_menus`;\n\nexport const menuService = {\n    dependencies: [\"action\"],\n    async start(env) {\n        let currentAppId;\n        let menusData;\n\n        const fetchMenus = async (reload) => {\n            if (!reload && odoo.loadMenusPromise) {\n                return odoo.loadMenusPromise;\n            }\n            const res = await browser.fetch(loadMenusUrl, { cache: \"no-store\" });\n            if (!res.ok) {\n                throw new Error(\"Error while fetching menus\");\n            }\n            return res.json();\n        };\n        const storedMenus = browser.localStorage.getItem(\"webclient_menus\");\n        const storedMenusVersion = browser.localStorage.getItem(\"webclient_menus_version\");\n\n        if (storedMenus && storedMenusVersion === session.registry_hash) {\n            fetchMenus().then((res) => {\n                if (res) {\n                    const fetchedMenus = JSON.stringify(res);\n                    if (fetchedMenus !== storedMenus) {\n                        try {\n                            browser.localStorage.setItem(\"webclient_menus\", fetchedMenus);\n                        } catch (error) {\n                            console.error(\"Error while storing menus in localStorage\", error);\n                        }\n                        menusData = res;\n                        env.bus.trigger(\"MENUS:APP-CHANGED\");\n                    }\n                }\n            });\n            menusData = JSON.parse(storedMenus);\n        } else {\n            menusData = await fetchMenus();\n            if (menusData) {\n                try {\n                    browser.localStorage.setItem(\"webclient_menus_version\", session.registry_hash);\n                    browser.localStorage.setItem(\"webclient_menus\", JSON.stringify(menusData));\n                } catch (error) {\n                    console.error(\"Error while storing menus in localStorage\", error);\n                }\n            }\n        }\n\n        function _getMenu(menuId) {\n            return menusData[menuId];\n        }\n        function setCurrentMenu(menu) {\n            menu = typeof menu === \"number\" ? _getMenu(menu) : menu;\n            if (menu && menu.appID !== currentAppId) {\n                currentAppId = menu.appID;\n                browser.sessionStorage.setItem(\"menu_id\", currentAppId);\n                env.bus.trigger(\"MENUS:APP-CHANGED\");\n            }\n        }\n\n        return {\n            getAll() {\n                return Object.values(menusData);\n            },\n            getApps() {\n                return this.getMenu(\"root\").children.map((mid) => this.getMenu(mid));\n            },\n            getMenu: _getMenu,\n            getCurrentApp() {\n                if (!currentAppId) {\n                    return;\n                }\n                return this.getMenu(currentAppId);\n            },\n            getMenuAsTree(menuID) {\n                const menu = this.getMenu(menuID);\n                if (!menu.childrenTree) {\n                    menu.childrenTree = menu.children.map((mid) => this.getMenuAsTree(mid));\n                }\n                return menu;\n            },\n            async selectMenu(menu) {\n                menu = typeof menu === \"number\" ? this.getMenu(menu) : menu;\n                if (!menu.actionID) {\n                    return;\n                }\n                await env.services.action.doAction(menu.actionID, {\n                    clearBreadcrumbs: true,\n                    onActionReady: () => {\n                        setCurrentMenu(menu);\n                    },\n                });\n            },\n            setCurrentMenu,\n            async reload() {\n                if (fetchMenus) {\n                    menusData = await fetchMenus(true);\n                    env.bus.trigger(\"MENUS:APP-CHANGED\");\n                }\n            },\n        };\n    },\n};\n\nregistry.category(\"services\").add(\"menu\", menuService);\n", "import { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\nimport { DropdownGroup } from \"@web/core/dropdown/dropdown_group\";\nimport { Transition } from \"@web/core/transition\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { registry } from \"@web/core/registry\";\nimport { debounce } from \"@web/core/utils/timing\";\nimport { ErrorHandler } from \"@web/core/utils/components\";\n\nimport {\n    Component,\n    onWillDestroy,\n    useExternalListener,\n    useEffect,\n    useRef,\n    useState,\n    onWillUnmount,\n} from \"@odoo/owl\";\nconst systrayRegistry = registry.category(\"systray\");\n\nconst getBoundingClientRect = Element.prototype.getBoundingClientRect;\n\nconst SWIPE_ACTIVATION_THRESHOLD = 100;\n\nexport class MenuDropdown extends Dropdown {}\n\nexport class NavBar extends Component {\n    static template = \"web.NavBar\";\n    static components = {\n        Dropdown,\n        DropdownItem,\n        DropdownGroup,\n        MenuDropdown,\n        ErrorHandler,\n        Transition,\n    };\n    static props = {};\n\n    setup() {\n        this.currentAppSectionsExtra = [];\n        this.actionService = useService(\"action\");\n        this.menuService = useService(\"menu\");\n        this.pwa = useService(\"pwa\");\n        this.root = useRef(\"root\");\n        this.appSubMenus = useRef(\"appSubMenus\");\n        const debouncedAdapt = debounce(this.adapt.bind(this), 250);\n        onWillDestroy(() => debouncedAdapt.cancel());\n        useExternalListener(window, \"resize\", debouncedAdapt);\n\n        let adaptCounter = 0;\n        const renderAndAdapt = () => {\n            adaptCounter++;\n            this.render();\n        };\n\n        systrayRegistry.addEventListener(\"UPDATE\", renderAndAdapt);\n        this.env.bus.addEventListener(\"MENUS:APP-CHANGED\", renderAndAdapt);\n\n        onWillUnmount(() => {\n            systrayRegistry.removeEventListener(\"UPDATE\", renderAndAdapt);\n            this.env.bus.removeEventListener(\"MENUS:APP-CHANGED\", renderAndAdapt);\n        });\n\n        // We don't want to adapt every time we are patched\n        // rather, we adapt only when menus or systrays have changed.\n        useEffect(\n            () => {\n                this.adapt();\n            },\n            () => [adaptCounter]\n        );\n\n        this.state = useState({\n            isAllAppsMenuOpened: false,\n            isAppMenuSidebarOpened: false,\n        });\n    }\n\n    handleItemError(error, item) {\n        // remove the faulty component\n        item.isDisplayed = () => false;\n        Promise.resolve().then(() => {\n            throw error;\n        });\n    }\n\n    get currentApp() {\n        return this.menuService.getCurrentApp();\n    }\n\n    get currentAppSections() {\n        return (\n            (this.currentApp && this.menuService.getMenuAsTree(this.currentApp.id).childrenTree) ||\n            []\n        );\n    }\n\n    // This dummy setter is only here to prevent conflicts between the\n    // Enterprise NavBar extension and the Website NavBar patch.\n    set currentAppSections(_) {}\n\n    get isScopedApp() {\n        return this.pwa.isScopedApp;\n    }\n\n    get systrayItems() {\n        return systrayRegistry\n            .getEntries()\n            .map(([key, value]) => ({ key, ...value }))\n            .filter((item) => (\"isDisplayed\" in item ? item.isDisplayed(this.env) : true))\n            .reverse();\n    }\n\n    // This dummy setter is only here to prevent conflicts between the\n    // Enterprise NavBar extension and the Website NavBar patch.\n    set systrayItems(_) {}\n\n    /**\n     * Adapt will check the available width for the app sections to get displayed.\n     * If not enough space is available, it will replace by a \"more\" menu\n     * the least amount of app sections needed trying to fit the width.\n     *\n     * NB: To compute the widths of the actual app sections, a render needs to be done upfront.\n     *     By the end of this method another render may occur depending on the adaptation result.\n     */\n    async adapt() {\n        if (!this.root.el) {\n            /** @todo do we still need this check? */\n            // currently, the promise returned by 'render' is resolved at the end of\n            // the rendering even if the component has been destroyed meanwhile, so we\n            // may get here and have this.el unset\n            return;\n        }\n\n        // ------- Initialize -------\n        // Get the sectionsMenu\n        const sectionsMenu = this.appSubMenus.el;\n        if (!sectionsMenu) {\n            // No need to continue adaptations if there is no sections menu.\n            return;\n        }\n\n        // Save initial state to further check if new render has to be done.\n        const initialAppSectionsExtra = this.currentAppSectionsExtra;\n        const firstInitialAppSectionExtra = [...initialAppSectionsExtra].shift();\n        const initialAppId = firstInitialAppSectionExtra && firstInitialAppSectionExtra.appID;\n\n        // Restore (needed to get offset widths)\n        const sections = [\n            ...sectionsMenu.querySelectorAll(\":scope > *:not(.o_menu_sections_more)\"),\n        ];\n        for (const section of sections) {\n            section.classList.remove(\"d-none\");\n        }\n        this.currentAppSectionsExtra = [];\n\n        // ------- Check overflowing sections -------\n        // use getBoundingClientRect to get unrounded values for width in order to avoid rounding problem\n        // with offsetWidth.\n        const sectionsAvailableWidth = getBoundingClientRect.call(sectionsMenu).width;\n        const sectionsTotalWidth = sections.reduce(\n            (sum, s) => sum + getBoundingClientRect.call(s).width,\n            0\n        );\n        if (sectionsAvailableWidth < sectionsTotalWidth) {\n            // Sections are overflowing\n            // Initial width is harcoded to the width the more menu dropdown will take\n            let width = 46;\n            for (const section of sections) {\n                if (sectionsAvailableWidth < width + section.offsetWidth) {\n                    // Last sections are overflowing\n                    const overflowingSections = sections.slice(sections.indexOf(section));\n                    overflowingSections.forEach((s) => {\n                        // Hide from normal menu\n                        s.classList.add(\"d-none\");\n                        // Show inside \"more\" menu\n                        const sectionId =\n                            s.dataset.section ||\n                            s.querySelector(\"[data-section]\").getAttribute(\"data-section\");\n                        const currentAppSection = this.currentAppSections.find(\n                            (appSection) => appSection.id.toString() === sectionId\n                        );\n                        this.currentAppSectionsExtra.push(currentAppSection);\n                    });\n                    break;\n                }\n                width += section.offsetWidth;\n            }\n        }\n\n        // ------- Final rendering -------\n        const firstCurrentAppSectionExtra = [...this.currentAppSectionsExtra].shift();\n        const currentAppId = firstCurrentAppSectionExtra && firstCurrentAppSectionExtra.appID;\n        if (\n            initialAppSectionsExtra.length === this.currentAppSectionsExtra.length &&\n            initialAppId === currentAppId\n        ) {\n            // Do not render if more menu items stayed the same.\n            return;\n        }\n        return this.render();\n    }\n\n    onNavBarDropdownItemSelection(menu) {\n        if (menu) {\n            this.menuService.selectMenu(menu);\n        }\n    }\n\n    getMenuItemHref(payload) {\n        return `/odoo/${payload.actionPath || \"action-\" + payload.actionID}`;\n    }\n\n    _closeAppMenuSidebar() {\n        this.state.isAllAppsMenuOpened = false;\n        this.state.isAppMenuSidebarOpened = false;\n    }\n    _openAppMenuSidebar() {\n        this.state.isAppMenuSidebarOpened = !this.state.isAppMenuSidebarOpened;\n    }\n    onAllAppsBtnClick() {\n        this.state.isAllAppsMenuOpened = !this.state.isAllAppsMenuOpened;\n    }\n    async _onMenuClicked(menu) {\n        await this.menuService.selectMenu(menu);\n        this._closeAppMenuSidebar();\n    }\n    _onSwipeStart(ev) {\n        this.swipeStartX = ev.changedTouches[0].clientX;\n    }\n    _onSwipeEnd(ev) {\n        if (!this.swipeStartX) {\n            return;\n        }\n        const deltaX = this.swipeStartX - ev.changedTouches[0].clientX;\n        if (deltaX < SWIPE_ACTIVATION_THRESHOLD) {\n            return;\n        }\n        this._closeAppMenuSidebar();\n        this.swipeStartX = null;\n    }\n}\n", "import { browser } from \"@web/core/browser/browser\";\nimport { rpcBus } from \"@web/core/network/rpc\";\nimport { registry } from \"@web/core/registry\";\nimport { UPDATE_METHODS } from \"@web/core/orm_service\";\n\n// reload the page if changes are being done to `res.company`\n\nregistry.category(\"services\").add(\"reloadCompany\", {\n    dependencies: [\"action\"],\n    start(env, { action }) {\n        rpcBus.addEventListener(\"RPC:RESPONSE\", (ev) => {\n            const { data, error } = ev.detail;\n            const { model, method } = data.params;\n            if (!error && model === \"res.company\" && UPDATE_METHODS.includes(method)) {\n                if (!browser.localStorage.getItem(\"running_tour\")) {\n                    action.doAction(\"reload_context\");\n                }\n            }\n        });\n    },\n});\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { x2ManyCommands } from \"@web/core/orm_service\";\nimport { registry } from \"@web/core/registry\";\nimport { deepCopy } from \"@web/core/utils/objects\";\nimport { parseXML } from \"@web/core/utils/xml\";\nimport { Record } from \"@web/model/record\";\nimport { standardFieldProps } from \"@web/views/fields/standard_field_props\";\nimport { FormArchParser } from \"@web/views/form/form_arch_parser\";\nimport { FormRenderer } from \"@web/views/form/form_renderer\";\n\nimport { Component, onWillRender, toRaw, useChildSubEnv } from \"@odoo/owl\";\n\n/**\n * This widget is only used for the 'group_ids' field of the 'res.users'\n * form view or the 'implied_ids' field of the 'res.groups' form view,\n * in order to vizualize and configure access rights.\n */\nclass ResUserGroupIdsField extends Component {\n    static template = \"web.ResUserGroupIdsField\";\n    static components = { Record, FormRenderer };\n    static props = { ...standardFieldProps };\n\n    setup() {\n        const { groups, privileges, categories } = toRaw(\n            this.props.record.data.view_group_hierarchy\n        );\n\n        // Generate the \"other\" category (for privileges that do not belong to any category)\n        const privilegesWithoutCategory = Object.values(privileges)\n            .filter((privilege) => !privilege.category_id)\n            .sort((privilege) => privilege.sequence);\n        if (privilegesWithoutCategory.length) {\n            categories.push({\n                id: \"other\",\n                name: _t(\"Other\"),\n                privilege_ids: privilegesWithoutCategory.map((privilege) => privilege.id),\n            });\n        }\n\n        // Generate the extra rights category (for groups without privilege)\n        this.extraCategory = {\n            id: \"extra\",\n            name: _t(\"Extra Rights\"),\n            privileges: Object.values(groups)\n                .filter((group) => !group.privilege_id)\n                .map((group) => {\n                    const privilege = {\n                        description: group.comment,\n                        groupId: group.id,\n                        id: \"group_\" + group.id,\n                        name: group.name,\n                    };\n                    privilege.groupFieldName = this.getFieldName(privilege);\n                    return privilege;\n                })\n                .sort((p1, p2) => p1.name.localeCompare(p2.name)),\n        };\n\n        // Generate selection (for privileges) and boolean (for extra right groups) fields\n        this._fields = {};\n        const booleanFieldToGroupId = {};\n        for (const category of categories) {\n            category.privileges = [];\n            for (const privilegeId of category.privilege_ids) {\n                const privilege = privileges[privilegeId];\n                category.privileges.push(privilege);\n                const helpLines = privilege.description ? [privilege.description] : [];\n                for (const gid of privilege.group_ids) {\n                    if (groups[gid].comment) {\n                        helpLines.push(`- ${groups[gid].name}: ${groups[gid].comment}`);\n                    }\n                }\n                const selection = privilege.group_ids.map((gId) => [gId, groups[gId].name]);\n                selection.unshift([false, privilege.placeholder || \"\"]);\n                this._fields[this.getFieldName(privilege)] = {\n                    help: helpLines.join(\"\\n\"),\n                    selection,\n                    string: privilege.name,\n                    type: \"selection\",\n                };\n            }\n        }\n        for (const privilege of this.extraCategory.privileges) {\n            this._fields[privilege.groupFieldName] = {\n                help: privilege.description,\n                string: privilege.name,\n                type: \"boolean\",\n            };\n            booleanFieldToGroupId[privilege.groupFieldName] = privilege.groupId;\n        }\n        this.fields = deepCopy(this._fields); // dynamically modifed before each rendering w.r.t. to current groups\n\n        // Generate archInfo to provide to the FormRenderer\n        const models = { main: { fields: this._fields } };\n        const arch = `\n            <t>\n                <group>\n                    ${categories.map((category) => this.getCategoryArch(category)).join(\"\")}\n                </group>\n                ${odoo.debug ? this.getExtraGroupsArch() : \"\"}\n            </t>`;\n        this.archInfo = new FormArchParser().parse(parseXML(arch), models, \"main\");\n\n        // Generate information to share through the env with \"res_user_group_ids_privilege\" widgets\n        //  - `booleanFieldToGroupId` maps generated boolean field names to their group id\n        //  - `privileges` is an object mapping all privilege ids to their description\n        //  - `groups` is an object mapping all group ids to their description, which is based on\n        //     the current selected groups\n        this.info = {\n            booleanFieldToGroupId,\n            groups: {},\n            privileges,\n        };\n        useChildSubEnv({\n            resUserGroupsInfo: this.info, // computed in onWillRender\n        });\n        onWillRender(() => {\n            // Generate groups information based on current ids, i.e.\n            //  - `id`, `name`, `privilege_id`, `comment` are kept as in the static definition\n            //  - `selected` is true iff the group is explicitely selected (!= implied)\n            //  - `impliedByIds` only contain *selected* group ids that imply the given group\n            //  - `disjointIds` is only set for *selected* or *implied* groups\n            //  - `implyIds` doesn't contain itself, because it's useless and easier later\n            const selectedIds = new Set(this.props.record.data[this.props.name].currentIds);\n            for (const group of Object.values(groups)) {\n                const selected = selectedIds.has(group.id);\n                this.info.groups[group.id] = {\n                    name: group.name,\n                    id: group.id,\n                    privilege_id: group.privilege_id,\n                    comment: group.comment,\n                    impliedByIds: group.all_implied_by_ids.filter(\n                        (gid) => gid !== group.id && selectedIds.has(gid)\n                    ),\n                    implyIds: selected\n                        ? group.all_implied_ids.filter((gid) => gid !== group.id)\n                        : [],\n                    selected,\n                };\n            }\n            for (const group of Object.values(groups)) {\n                let disjointIds = [];\n                const { selected, impliedByIds } = this.info.groups[group.id];\n                if (selected || impliedByIds.length) {\n                    disjointIds = group.disjoint_ids.filter(\n                        (gid) =>\n                            this.info.groups[gid].selected ||\n                            this.info.groups[gid].impliedByIds.length\n                    );\n                }\n                this.info.groups[group.id].disjointIds = disjointIds;\n            }\n\n            // Remove lower level groups from selection fields where a higher level group is implied\n            for (const fieldName in this.fields) {\n                if (this.fields[fieldName].type === \"selection\") {\n                    const options = this._fields[fieldName].selection;\n                    this.fields[fieldName].selection = options;\n                    for (let i = options.length - 1; i > 0; i--) {\n                        // i > 0 to omit \"false\" option\n                        const group = this.info.groups[options[i][0]];\n                        const isImplied = group.impliedByIds.some(\n                            (gid) => this.info.groups[gid].privilege_id !== group.privilege_id\n                        );\n                        if (isImplied) {\n                            this.fields[fieldName].selection = options.slice(i);\n                            break;\n                        }\n                    }\n                }\n            }\n\n            // Generate values for the dynamically generated selection and boolean fields\n            this.values = {};\n            this.shadowedGroupIds = [];\n            for (const category of categories) {\n                for (const privilege of category.privileges) {\n                    let groupId =\n                        privilege.group_ids.findLast((gId) => selectedIds.has(gId)) || false;\n                    const fieldName = this.getFieldName(privilege);\n                    const options = this.fields[fieldName].selection;\n                    if (groupId && !options.some((option) => option[0] === groupId)) {\n                        // The option has been removed because a higher level group is implied\n                        // => force the value to false to show the implied group instead\n                        this.shadowedGroupIds.push(groupId);\n                        groupId = false;\n                    }\n                    this.values[fieldName] = groupId;\n                }\n            }\n            if (this.extraCategory) {\n                for (const privilege of this.extraCategory.privileges) {\n                    this.values[this.getFieldName(privilege)] = selectedIds.has(privilege.groupId);\n                }\n            }\n        });\n\n        this.hooks = {\n            onRecordChanged: this.onRecordChanged.bind(this),\n        };\n    }\n\n    getExtraGroupsArch() {\n        return `\n            <group string=\"${this.extraCategory.name}\" class=\"o_extra_rights_group\">\n                <group>\n                    ${this.extraCategory.privileges\n                        .filter((cat, index) => index % 2 === 0)\n                        .map((privilege) => this.getPrivilegeArch(privilege))\n                        .join(\"\")}\n                </group>\n                <group>\n                    ${this.extraCategory.privileges\n                        .filter((cat, index) => index % 2 === 1)\n                        .map((privilege) => this.getPrivilegeArch(privilege))\n                        .join(\"\")}\n                </group>\n            </group>`;\n    }\n\n    getFieldName(privilege) {\n        return `field_${privilege.id}`;\n    }\n\n    getPrivilegeArch(privilege) {\n        const fieldName = this.getFieldName(privilege);\n        return `<field name=\"${fieldName}\" widget=\"res_user_group_ids_privilege\"/>`;\n    }\n\n    getCategoryArch(category) {\n        return `\n            <group string=\"${category.name}\">\n                ${category.privileges.map((privilege) => this.getPrivilegeArch(privilege)).join(\"\")}\n            </group>`;\n    }\n\n    onRecordChanged(_, values) {\n        let selectedGroupIds = Object.entries(values)\n            .filter(([fieldName, gid]) => this.fields[fieldName].type === \"selection\" && gid)\n            .map(([_, gid]) => gid);\n        // Keep shadowed groups, except if an higher level group has been set, in which case they\n        // are not shadowed anymore\n        const { groups, privileges } = this.info;\n        const shadowedGroupIds = this.shadowedGroupIds.filter(\n            (gid) => !values[this.getFieldName(privileges[groups[gid].privilege_id])]\n        );\n        selectedGroupIds = selectedGroupIds.concat(shadowedGroupIds);\n        for (const privilege of this.extraCategory.privileges) {\n            if (values[privilege.groupFieldName]) {\n                selectedGroupIds.push(privilege.groupId);\n            }\n        }\n        return this.props.record.update({\n            [this.props.name]: [x2ManyCommands.set(selectedGroupIds)],\n        });\n    }\n}\n\nconst resUserGroupIdsField = {\n    component: ResUserGroupIdsField,\n    fieldDependencies: [{ name: \"view_group_hierarchy\", type: \"json\", readonly: true }],\n    additionalClasses: [\"w-100\"],\n};\n\nregistry.category(\"fields\").add(\"res_user_group_ids\", resUserGroupIdsField);\n", "import { Component, useState } from \"@odoo/owl\";\nimport { groupBy } from \"@web/core/utils/arrays\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { omit } from \"@web/core/utils/objects\";\n\nexport class ResUserGroupIdsPopover extends Component {\n    static template = \"web.ResUserGroupIdsPopover\";\n    static props = {\n        close: Function,\n        groupId: [Number, Boolean],\n        groups: Object,\n        privileges: Object,\n    };\n\n    setup() {\n        this.actionService = useService(\"action\");\n\n        this.state = useState({\n            showExtraGroups: false,\n        });\n\n        this.groups = this.props.groups;\n        this.privileges = this.props.privileges;\n        this.group = this.groups[this.props.groupId];\n        this.privilege = this.privileges[this.group.privilege_id];\n\n        // filter out impliedBy groups from same privilege\n        this.impliedGroups = this.group.impliedByIds\n            .map((gid) => this.groups[gid])\n            .filter((g) => !this.privilege || g.privilege_id !== this.privilege.id);\n\n        // split joint/joint extra/exclusive implies (at most one group by privilege, the one with\n        // higher level, and omit groups of same privilege as the current group)\n        const implyGroups = this.group.implyIds.map((gid) => this.groups[gid]);\n        const implyGroupsByPrivilege = groupBy(implyGroups, (g) => g.privilege_id);\n        const keysToOmit = this.privilege ? [\"false\", String(this.privilege.id)] : [\"false\"];\n        const groupsFromOtherPrivileges = omit(implyGroupsByPrivilege, ...keysToOmit);\n        const higherLevelGroups = Object.values(groupsFromOtherPrivileges).map((groups) => groups[groups.length-1]);\n        const groupsWithoutPrivilege = implyGroupsByPrivilege[false] || [];\n        const implyGroupsToDisplay = groupsWithoutPrivilege.concat(higherLevelGroups);\n        const { exclusive, joint, extra } = groupBy(implyGroupsToDisplay, (g) => {\n            if (g.impliedByIds.length > 1) {\n                return g.privilege_id ? \"joint\" : \"extra\";\n            }\n            return \"exclusive\";\n        });\n        this.exclusiveImplyGroups = exclusive || [];\n        this.jointImplyGroups = joint || [];\n        this.jointExtraImplyGroups = extra || [];\n    }\n\n    getGroupDisplayName(group) {\n        const prefix = group.privilege_id ? `${this.privileges[group.privilege_id].name}/` : \"\";\n        return `${prefix}${group.name}`;\n    }\n\n    onGroupClicked(group) {\n        this.actionService.doAction({\n            res_id: group.id,\n            res_model: \"res.groups\",\n            type: \"ir.actions.act_window\",\n            views: [[false, \"form\"]],\n        });\n    }\n}\n", "import { registry } from \"@web/core/registry\";\nimport { BooleanField } from \"@web/views/fields/boolean/boolean_field\";\nimport { SelectionField } from \"@web/views/fields/selection/selection_field\";\nimport { standardFieldProps } from \"@web/views/fields/standard_field_props\";\n\nimport { ResUserGroupIdsPopover } from \"./res_user_group_ids_popover\";\n\nimport { Component } from \"@odoo/owl\";\nimport { usePopover } from \"@web/core/popover/popover_hook\";\n\n/**\n * /!\\ This widget is not meant to be used anywhere else than inside form view\n * dynamically generated by the res_user_group_ids widget.\n */\n\nclass ResUserGroupIdsPrivilegeField extends Component {\n    static template = \"web.ResUserGroupIdsPrivilegeField\";\n    static components = { BooleanField, SelectionField };\n    static props = { ...standardFieldProps };\n\n    setup() {\n        this.isDebug = odoo.debug;\n        this.popover = usePopover(ResUserGroupIdsPopover);\n        this.groups = this.env.resUserGroupsInfo.groups;\n    }\n\n    get disjointGroupIds() {\n        const group = this.group || this.impliedGroup;\n        return group ? group.disjointIds : [];\n    }\n\n    get group() {\n        const groups = this.groups;\n        return groups[this.findGroupId((gid) => groups[gid].selected)] || false;\n    }\n\n    get impliedGroup() {\n        const groups = this.groups;\n        return groups[this.findGroupId((gid) => groups[gid].impliedByIds.length > 0)] || false;\n    }\n\n    get impliedGroupDisplayName() {\n        return !this.isSet && this.impliedGroup ? this.groups[this.impliedGroup.id].name : \"\";\n    }\n\n    get infoButtonClassnames() {\n        const invisible = !this.isSet && !this.impliedGroup?.id;\n        const isDisjoint = this.isDisjoint;\n        return {\n            o_group_info_button: true,\n            invisible,\n            btn: true,\n            \"btn-link\": true,\n            \"py-0\": true,\n            fa: true,\n            \"fa-info-circle\": !isDisjoint,\n            \"fa-exclamation-triangle\": isDisjoint,\n            \"link-danger\": isDisjoint,\n        };\n    }\n\n    get isDisjoint() {\n        return this.disjointGroupIds.length > 0;\n    }\n\n    get isImplied() {\n        return !this.isSet && !!this.impliedGroup;\n    }\n\n    get isSet() {\n        return !!this.props.record.data[this.props.name];\n    }\n\n    get type() {\n        return this.props.record.fields[this.props.name].type;\n    }\n\n    findGroupId(predicate) {\n        if (this.type === \"selection\") {\n            const options = this.props.record.fields[this.props.name].selection;\n            const option = options.findLast((o) => o[0] && predicate(o[0]));\n            return option ? option[0] : false;\n        } else {\n            const groupId = this.env.resUserGroupsInfo.booleanFieldToGroupId[this.props.name];\n            return predicate(groupId) ? groupId : false;\n        }\n    }\n\n    onClickInfoButton(ev) {\n        if (this.popover.isOpen) {\n            this.popover.close();\n        } else {\n            this.popover.open(ev.currentTarget, {\n                groupId: this.group ? this.group.id : this.impliedGroup.id,\n                groups: this.env.resUserGroupsInfo.groups,\n                privileges: this.env.resUserGroupsInfo.privileges,\n            });\n        }\n    }\n}\n\nconst resUserGroupIdsPrivilegeField = {\n    component: ResUserGroupIdsPrivilegeField,\n};\n\nregistry.category(\"fields\").add(\"res_user_group_ids_privilege\", resUserGroupIdsPrivilegeField);\n", "import { registry } from \"@web/core/registry\";\nimport { deepCopy } from \"@web/core/utils/objects\";\n\nexport const lazySession = {\n    dependencies: [\"orm\"],\n    start(env, { orm }) {\n        let resolveWebClientReady;\n        let lazyConfigPromise;\n        const fetchServerData = async () => {\n            await webClientReadyPromise;\n            return orm.call(\"ir.http\", \"lazy_session_info\");\n        };\n        const webClientReadyPromise = new Promise((r) => (resolveWebClientReady = r));\n        env.bus.addEventListener(\"WEB_CLIENT_READY\", resolveWebClientReady, { once: true });\n        return {\n            getValue(key, callback) {\n                if (!lazyConfigPromise) {\n                    lazyConfigPromise = fetchServerData();\n                }\n                lazyConfigPromise.then((config) => callback(deepCopy(config)[key]));\n            },\n        };\n    },\n};\n\nregistry.category(\"services\").add(\"lazy_session\", lazySession);\n", "import { registry } from \"@web/core/registry\";\nimport { BinaryField, binaryField } from \"@web/views/fields/binary/binary_field\";\n\nexport class SettingsBinaryField extends BinaryField {\n    static template = \"web.SettingsBinaryField\";\n\n    getDownloadData() {\n        const related = this.props.record.fields[this.props.name].related;\n        const [fieldName, relatedFieldName] = related.split(\".\");\n        return {\n            ...super.getDownloadData(),\n            model: this.props.record.fields[fieldName].relation,\n            field: relatedFieldName ?? fieldName,\n            id: this.props.record.data[fieldName].id,\n        }\n    }\n\n}\n\nconst settingsBinaryField = {\n    ...binaryField,\n    component: SettingsBinaryField,\n};\n\nregistry.category(\"fields\").add(\"base_settings.binary\", settingsBinaryField);\n", "import { registry } from \"@web/core/registry\";\nimport { booleanField, BooleanField } from \"@web/views/fields/boolean/boolean_field\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { UpgradeDialog } from \"./upgrade_dialog\";\n\n/**\n *  The upgrade boolean field is intended to be used in config settings.\n *  When checked, an upgrade popup is showed to the user.\n */\n\nexport class UpgradeBooleanField extends BooleanField {\n    setup() {\n        super.setup();\n        this.dialogService = useService(\"dialog\");\n        this.isEnterprise = odoo.info && odoo.info.isEnterprise;\n    }\n\n    async onChange(newValue) {\n        if (!this.isEnterprise) {\n            this.dialogService.add(\n                UpgradeDialog,\n                {},\n                {\n                    onClose: () => {\n                        this.props.record.update({ [this.props.name]: false });\n                    },\n                }\n            );\n        } else {\n            super.onChange(...arguments);\n        }\n    }\n}\n\nexport const upgradeBooleanField = {\n    ...booleanField,\n    component: UpgradeBooleanField,\n    additionalClasses: [...(booleanField.additionalClasses || []), \"o_field_boolean\"],\n};\n\nregistry.category(\"fields\").add(\"upgrade_boolean\", upgradeBooleanField);\n", "import { Dialog } from \"@web/core/dialog/dialog\";\nimport { useService } from \"@web/core/utils/hooks\";\n\nimport { Component } from \"@odoo/owl\";\n\nexport class UpgradeDialog extends Component {\n    static template = \"web.UpgradeDialog\";\n    static components = { Dialog };\n    static props = {\n        close: Function,\n    };\n    setup() {\n        this.orm = useService(\"orm\");\n    }\n    async _confirmUpgrade() {\n        const usersCount = await this.orm.call(\"res.users\", \"search_count\", [\n            [[\"share\", \"=\", false]],\n        ]);\n        window.open(\n            \"https://www.odoo.com/odoo-enterprise/upgrade?num_users=\" + usersCount,\n            \"_blank\"\n        );\n        this.props.close();\n    }\n}\n", "import { FormLabel } from \"@web/views/form/form_label\";\nimport { HighlightText } from \"./highlight_text\";\nimport { upgradeBooleanField } from \"../fields/upgrade_boolean_field\";\n\nexport class FormLabelHighlightText extends FormLabel {\n    static template = \"web.FormLabelHighlightText\";\n    static components = { HighlightText };\n    setup() {\n        super.setup();\n        const isEnterprise = odoo.info && odoo.info.isEnterprise;\n        if (this.props.fieldInfo?.field === upgradeBooleanField && !isEnterprise) {\n            this.upgradeEnterprise = true;\n        }\n    }\n}\n", "import { Component, useState, onWillRender } from \"@odoo/owl\";\nimport { highlightText } from \"@web/core/utils/html\";\n\nexport class HighlightText extends Component {\n    static template = \"web.HighlightText\";\n    static props = {\n        originalText: String,\n    };\n    setup() {\n        this.searchState = useState(this.env.searchState);\n\n        onWillRender(() => {\n            this.text = highlightText(\n                this.searchState.value,\n                this.props.originalText,\n                \"highlighter\"\n            );\n        });\n    }\n}\n", "import { registry } from \"@web/core/registry\";\nimport { radioField, RadioField } from \"@web/views/fields/radio/radio_field\";\nimport { HighlightText } from \"./highlight_text\";\n\nexport class SettingsRadioField extends RadioField {\n    static template = \"web.SettingsRadioField\";\n    static components = { ...super.components, HighlightText };\n}\n\nexport const settingsRadioField = {\n    ...radioField,\n    component: SettingsRadioField,\n};\n\nregistry.category(\"fields\").add(\"base_settings.radio\", settingsRadioField);\n", "import { onMounted, useRef, useState } from \"@odoo/owl\";\nimport { browser } from \"@web/core/browser/browser\";\nimport { normalizedMatch } from \"@web/core/l10n/utils\";\nimport { Setting } from \"@web/views/form/setting/setting\";\nimport { FormLabelHighlightText } from \"../highlight_text/form_label_highlight_text\";\nimport { HighlightText } from \"../highlight_text/highlight_text\";\n\nexport class SearchableSetting extends Setting {\n    static template = \"web.SearchableSetting\";\n    static components = {\n        ...Setting.components,\n        FormLabel: FormLabelHighlightText,\n        HighlightText,\n    };\n    setup() {\n        this.settingRef = useRef(\"setting\");\n        this.state = useState({\n            search: this.env.searchState,\n            showAllContainer: this.env.showAllContainer,\n            highlightClass: {},\n        });\n        this.labels = [];\n        this.labels.push(this.labelString, this.props.help);\n        super.setup();\n        onMounted(() => {\n            if (this.settingRef.el) {\n                const searchableTexts = this.settingRef.el.querySelectorAll(\"span[searchableText]\");\n                searchableTexts.forEach((st) => {\n                    this.labels.push(st.getAttribute(\"searchableText\"));\n                });\n            }\n            if (browser.location.hash.substring(1) === this.props.id) {\n                this.state.highlightClass = { o_setting_highlight: true };\n                setTimeout(() => (this.state.highlightClass = {}), 5000);\n            }\n        });\n    }\n\n    get classNames() {\n        const classNames = super.classNames;\n        classNames.o_searchable_setting = Boolean(this.labels.length);\n        return { ...classNames, ...this.state.highlightClass };\n    }\n\n    visible() {\n        if (!this.state.search.value) {\n            return true;\n        }\n        if (this.state.showAllContainer.showAllContainer) {\n            return true;\n        }\n        if (normalizedMatch(this.labels.join(), this.state.search.value).match) {\n            return true;\n        }\n        return false;\n    }\n}\n", "import { Setting } from \"@web/views/form/setting/setting\";\n\nexport class SettingHeader extends Setting {\n    static template = \"web.HeaderSetting\";\n\n    get classNames() {\n        const { class: _class } = this.props;\n        const classNames = {\n            app_settings_header: true,\n            \"d-flex\": true,\n            \"flex-column\": true,\n            \"flex-md-row\": true,\n            \"align-items-baseline\": true,\n            \"gap-1\": true,\n            \"gap-md-5\": true,\n            \"py-3\": true,\n            \"bg-opacity-25\": true,\n            [_class]: Boolean(_class),\n        };\n        return classNames;\n    }\n\n    get labelString() {\n        return this.props.string || this.props.record.fields[this.props.name]?.string || \"\";\n    }\n}\n", "import { Component, useState, useEffect, useRef } from \"@odoo/owl\";\n\nexport class SettingsApp extends Component {\n    static template = \"web.SettingsApp\";\n    static props = {\n        string: String,\n        imgurl: String,\n        key: String,\n        selectedTab: { type: String, optional: 1 },\n        slots: Object,\n    };\n    setup() {\n        this.state = useState({\n            search: this.env.searchState,\n        });\n        this.settingsAppRef = useRef(\"settingsApp\");\n        useEffect(\n            () => {\n                if (this.settingsAppRef.el) {\n                    const force =\n                        this.state.search.value &&\n                        !this.settingsAppRef.el.querySelector(\n                            \".o_settings_container:not(.d-none)\"\n                        ) &&\n                        !this.settingsAppRef.el.querySelector(\n                            \".o_setting_box.o_searchable_setting\"\n                        );\n                    this.settingsAppRef.el.classList.toggle(\"d-none\", force);\n                }\n            },\n            () => [this.state.search.value]\n        );\n    }\n}\n", "import { HighlightText } from \"../highlight_text/highlight_text\";\nimport { escapeRegExp } from \"@web/core/utils/strings\";\n\nimport { Component, useState, useRef, useEffect, onWillRender, useChildSubEnv } from \"@odoo/owl\";\n\nexport class SettingsBlock extends Component {\n    static template = \"web.SettingsBlock\";\n    static components = {\n        HighlightText,\n    };\n    static props = {\n        title: { type: String, optional: true },\n        tip: { type: String, optional: true },\n        slots: { type: Object, optional: true },\n        class: { type: String, optional: true },\n    };\n    setup() {\n        this.state = useState({\n            search: this.env.searchState,\n        });\n        this.showAllContainerState = useState({\n            showAllContainer: false,\n        });\n        useChildSubEnv({\n            showAllContainer: this.showAllContainerState,\n        });\n        this.settingsContainerRef = useRef(\"settingsContainer\");\n        this.settingsContainerTitleRef = useRef(\"settingsContainerTitle\");\n        this.settingsContainerTipRef = useRef(\"settingsContainerTip\");\n        useEffect(\n            () => {\n                const regexp = new RegExp(escapeRegExp(this.state.search.value), \"i\");\n                const force =\n                    this.state.search.value &&\n                    !regexp.test([this.props.title, this.props.tip].join()) &&\n                    !this.settingsContainerRef.el.querySelector(\n                        \".o_setting_box.o_searchable_setting\"\n                    );\n                this.toggleContainer(force);\n            },\n            () => [this.state.search.value]\n        );\n        onWillRender(() => {\n            const regexp = new RegExp(escapeRegExp(this.state.search.value), \"i\");\n            if (regexp.test([this.props.title, this.props.tip].join())) {\n                this.showAllContainerState.showAllContainer = true;\n            } else {\n                this.showAllContainerState.showAllContainer = false;\n            }\n        });\n    }\n    toggleContainer(force) {\n        if (this.settingsContainerTitleRef.el) {\n            this.settingsContainerTitleRef.el.classList.toggle(\"d-none\", force);\n        }\n        if (this.settingsContainerTipRef.el) {\n            this.settingsContainerTipRef.el.classList.toggle(\"d-none\", force);\n        }\n        this.settingsContainerRef.el.classList.toggle(\"d-none\", force);\n    }\n}\n", "import { ActionSwiper } from \"@web/core/action_swiper/action_swiper\";\n\nimport { Component, useState, useRef, useEffect } from \"@odoo/owl\";\nimport { browser } from \"@web/core/browser/browser\";\n\nexport class SettingsPage extends Component {\n    static template = \"web.SettingsPage\";\n    static components = { ActionSwiper };\n    static props = {\n        modules: Array,\n        anchors: Array,\n        initialTab: { type: String, optional: 1 },\n        slots: Object,\n    };\n    setup() {\n        this.state = useState({\n            selectedTab: \"\",\n            search: this.env.searchState,\n        });\n\n        if (this.props.modules) {\n            let selectedTab = this.props.initialTab || this.props.modules[0].key;\n\n            if (browser.location.hash) {\n                const hash = browser.location.hash.substring(1);\n                if (this.props.modules.map((m) => m.key).includes(hash)) {\n                    selectedTab = hash;\n                } else {\n                    const plop = this.props.anchors.find((a) => a.settingId === hash);\n                    if (plop) {\n                        selectedTab = plop.app;\n                    }\n                }\n            }\n\n            this.state.selectedTab = selectedTab;\n        }\n\n        this.settingsRef = useRef(\"settings\");\n        this.settingsTabRef = useRef(\"settings_tab\");\n        this.scrollMap = Object.create(null);\n        useEffect(\n            (settingsEl, currentTab) => {\n                if (!settingsEl) {\n                    return;\n                }\n\n                const { scrollTop } = this.scrollMap[currentTab] || 0;\n                settingsEl.scrollTop = scrollTop;\n            },\n            () => [this.settingsRef.el, this.state.selectedTab]\n        );\n    }\n\n    getCurrentIndex() {\n        return this.props.modules.findIndex((object) => {\n            return object.key === this.state.selectedTab;\n        });\n    }\n\n    hasRightSwipe() {\n        return (\n            this.env.isSmall && this.state.search.value.length === 0 && this.getCurrentIndex() !== 0\n        );\n    }\n    hasLeftSwipe() {\n        return (\n            this.env.isSmall &&\n            this.state.search.value.length === 0 &&\n            this.getCurrentIndex() !== this.props.modules.length - 1\n        );\n    }\n    async onRightSwipe(prom) {\n        this.state.selectedTab = this.props.modules[this.getCurrentIndex() - 1].key;\n        await prom;\n        this.scrollToSelectedTab();\n    }\n    async onLeftSwipe(prom) {\n        this.state.selectedTab = this.props.modules[this.getCurrentIndex() + 1].key;\n        await prom;\n        this.scrollToSelectedTab();\n    }\n\n    scrollToSelectedTab() {\n        const key = this.state.selectedTab;\n        this.settingsTabRef.el\n            .querySelector(`[data-key='${key}']`)\n            .scrollIntoView({ behavior: \"smooth\", inline: \"center\", block: \"nearest\" });\n    }\n\n    onSettingTabClick(key) {\n        if (this.settingsRef.el) {\n            const { scrollTop } = this.settingsRef.el;\n            this.scrollMap[this.state.selectedTab] = { scrollTop };\n        }\n        this.state.selectedTab = key;\n        this.env.searchState.value = \"\";\n    }\n}\n", "import { ConfirmationDialog } from \"@web/core/confirmation_dialog/confirmation_dialog\";\nimport { _t } from \"@web/core/l10n/translation\";\n\nexport class SettingsConfirmationDialog extends ConfirmationDialog {\n    static template = \"web.SettingsConfirmationDialog\";\n    static defaultProps = {\n        title: _t(\"Unsaved changes\"),\n    };\n    static props = {\n        ...ConfirmationDialog.props,\n        stayHere: { type: Function, optional: true },\n    };\n\n    _stayHere() {\n        if (this.props.stayHere) {\n            this.props.stayHere();\n        }\n        this.props.close();\n    }\n}\n", "import { append, createElement } from \"@web/core/utils/xml\";\nimport { FormCompiler } from \"@web/views/form/form_compiler\";\nimport { toStringExpression } from \"@web/views/utils\";\nimport { isTextNode } from \"@web/views/view_compiler\";\n\nexport class SettingsFormCompiler extends FormCompiler {\n    setup() {\n        super.setup();\n        this.compilers.push(\n            { selector: \"app\", fn: this.compileApp },\n            { selector: \"block\", fn: this.compileBlock }\n        );\n    }\n\n    compileForm(el, params) {\n        const settingsPage = createElement(\"SettingsPage\");\n        settingsPage.setAttribute(\n            \"slots\",\n            \"{NoContentHelper:__comp__.props.slots.NoContentHelper}\"\n        );\n        settingsPage.setAttribute(\"initialTab\", \"__comp__.props.initialApp\");\n        settingsPage.setAttribute(\"t-slot-scope\", \"settings\");\n\n        //props\n        params.modules = [];\n        params.anchors = [];\n\n        const res = super.compileForm(...arguments);\n        res.classList.remove(\"o_form_nosheet\");\n\n        settingsPage.setAttribute(\"modules\", JSON.stringify(params.modules));\n\n        // Move the compiled content of the form inside the settingsPage\n        while (res.firstChild) {\n            append(settingsPage, res.firstChild);\n        }\n\n        settingsPage.setAttribute(\"anchors\", JSON.stringify(params.anchors));\n\n        append(res, settingsPage);\n\n        return res;\n    }\n\n    compileApp(el, params) {\n        if (el.getAttribute(\"notApp\") === \"1\") {\n            //An app noted with notApp=\"1\" is not rendered.\n\n            //This hack is used when a technical module defines settings, and we don't want to render\n            //the settings until the corresponding app is not installed.\n\n            // For example, when installing the module website_sale, the module sale is also installed,\n            // but we don't want to render its settings (notApp=\"1\").\n            // On the contrary, when sale_management is installed, the module sale is also installed\n            // but in this case we want to see its settings (notApp=\"0\").\n            return;\n        }\n        const module = {\n            key: el.getAttribute(\"name\"),\n            string: el.getAttribute(\"string\"),\n            imgurl:\n                el.getAttribute(\"logo\") ||\n                \"/\" + el.getAttribute(\"name\") + \"/static/description/icon.png\",\n        };\n        params.modules.push(module);\n        const settingsApp = createElement(\"SettingsApp\", {\n            key: toStringExpression(module.key),\n            string: toStringExpression(module.string || \"\"),\n            imgurl: toStringExpression(module.imgurl),\n            selectedTab: \"settings.selectedTab\",\n        });\n\n        for (const child of el.children) {\n            append(settingsApp, this.compileNode(child, params));\n        }\n\n        params.anchors.push(\n            ...[...settingsApp.querySelectorAll(\"SearchableSetting\")]\n                .filter((s) => s.id)\n                .map((s) => ({ app: module.key, settingId: s.id.replaceAll(\"`\", \"\") }))\n        );\n        return settingsApp;\n    }\n\n    compileBlock(el, params) {\n        const settingsContainer = createElement(\"SettingsBlock\", {\n            title: toStringExpression(el.getAttribute(\"title\") || \"\"),\n            tip: toStringExpression(el.getAttribute(\"help\") || \"\"),\n        });\n        for (const child of el.children) {\n            append(settingsContainer, this.compileNode(child, params));\n        }\n        return settingsContainer;\n    }\n\n    compileSetting(el, params) {\n        params.componentName =\n            el.getAttribute(\"type\") === \"header\" ? \"SettingHeader\" : \"SearchableSetting\";\n        const res = super.compileSetting(el, params);\n        return res;\n    }\n\n    compileNode(node, params, evalInvisible) {\n        if (isTextNode(node)) {\n            if (node.textContent.trim()) {\n                return createElement(\"HighlightText\", {\n                    originalText: toStringExpression(node.textContent),\n                });\n            }\n        }\n        return super.compileNode(node, params, evalInvisible);\n    }\n\n    compileButton(el, params) {\n        const res = super.compileButton(el, params);\n        if (res.hasAttribute(\"string\") && res.children.length === 0) {\n            const contentSlot = createElement(\"t\");\n            contentSlot.setAttribute(\"t-set-slot\", \"contents\");\n            const content = createElement(\"HighlightText\", {\n                originalText: res.getAttribute(\"string\"),\n            });\n            append(contentSlot, content);\n            append(res, contentSlot);\n        }\n        return res;\n    }\n}\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { useAutofocus } from \"@web/core/utils/hooks\";\nimport { pick } from \"@web/core/utils/objects\";\nimport { formView } from \"@web/views/form/form_view\";\nimport { SettingsConfirmationDialog } from \"./settings_confirmation_dialog\";\nimport { SettingsFormRenderer } from \"./settings_form_renderer\";\n\nimport { useSubEnv, useState, useRef, useEffect } from \"@odoo/owl\";\n\nexport class SettingsFormController extends formView.Controller {\n    static template = \"web.SettingsFormView\";\n    static components = {\n        ...formView.Controller.components,\n        Renderer: SettingsFormRenderer,\n    };\n\n    setup() {\n        super.setup();\n        useAutofocus();\n        this.state = useState({ displayNoContent: false });\n        this.searchState = useState({ value: \"\" });\n        this.rootRef = useRef(\"root\");\n        this.canCreate = false;\n        useSubEnv({ searchState: this.searchState });\n        useEffect(\n            () => {\n                if (this.searchState.value) {\n                    if (\n                        this.rootRef.el.querySelector(\".o_settings_container:not(.d-none)\") ||\n                        this.rootRef.el.querySelector(\n                            \".settings .o_settings_container:not(.d-none) .o_setting_box.o_searchable_setting\"\n                        )\n                    ) {\n                        this.state.displayNoContent = false;\n                    } else {\n                        this.state.displayNoContent = true;\n                    }\n                } else {\n                    this.state.displayNoContent = false;\n                }\n            },\n            () => [this.searchState.value]\n        );\n        useEffect(() => {\n            if (this.env.__getLocalState__) {\n                this.env.__getLocalState__.remove(this);\n            }\n        });\n\n        this.initialApp = \"module\" in this.props.context ? this.props.context.module : \"\";\n    }\n\n    get modelParams() {\n        const headerFields = Object.values(this.archInfo.fieldNodes)\n            .filter((fieldNode) => fieldNode.options.isHeaderField)\n            .map((fieldNode) => fieldNode.name);\n        return {\n            ...super.modelParams,\n            headerFields,\n            onChangeHeaderFields: () => this._confirmSave(),\n        };\n    }\n\n    /**\n     * @override\n     */\n    async beforeExecuteActionButton(clickParams) {\n        if (clickParams.name === \"cancel\") {\n            return true;\n        }\n        if (\n            (await this.model.root.isDirty()) &&\n            ![\"execute\"].includes(clickParams.name) &&\n            !clickParams.noSaveDialog\n        ) {\n            return this._confirmSave();\n        } else {\n            return this.model.root.save();\n        }\n    }\n\n    displayName() {\n        return _t(\"Settings\");\n    }\n\n    async beforeLeave() {\n        const dirty = await this.model.root.isDirty();\n        if (dirty) {\n            return this._confirmSave();\n        }\n    }\n\n    //This is needed to avoid the auto save when unload\n    beforeUnload() {}\n\n    //This is needed to avoid the auto save when visibility change\n    beforeVisibilityChange() {}\n\n    async save() {\n        await this.env.onClickViewButton({\n            clickParams: {\n                name: \"execute\",\n                type: \"object\",\n            },\n            getResParams: () =>\n                pick(this.model.root, \"context\", \"evalContext\", \"resModel\", \"resId\", \"resIds\"),\n        });\n    }\n\n    discard() {\n        this.env.onClickViewButton({\n            clickParams: {\n                name: \"cancel\",\n                type: \"object\",\n                special: \"cancel\",\n            },\n            getResParams: () =>\n                pick(this.model.root, \"context\", \"evalContext\", \"resModel\", \"resId\", \"resIds\"),\n        });\n    }\n\n    async _confirmSave() {\n        let _continue = true;\n        await new Promise((resolve) => {\n            this.dialogService.add(SettingsConfirmationDialog, {\n                body: _t(\"Would you like to save your changes?\"),\n                confirm: async () => {\n                    await this.save();\n                    // It doesn't make sense to do the action of the button\n                    // as the res.config.settings `execute` method will trigger a reload.\n                    _continue = false;\n                    resolve();\n                },\n                cancel: async () => {\n                    await this.model.root.discard();\n                    await this.model.root.save();\n                    _continue = true;\n                    resolve();\n                },\n                stayHere: () => {\n                    _continue = false;\n                    resolve();\n                },\n            });\n        });\n        return _continue;\n    }\n}\n", "import { FormRenderer } from \"@web/views/form/form_renderer\";\nimport { FormLabelHighlightText } from \"./highlight_text/form_label_highlight_text\";\nimport { HighlightText } from \"./highlight_text/highlight_text\";\nimport { SearchableSetting } from \"./settings/searchable_setting\";\nimport { SettingHeader } from \"./settings/setting_header\";\nimport { SettingsBlock } from \"./settings/settings_block\";\nimport { SettingsApp } from \"./settings/settings_app\";\nimport { SettingsPage } from \"./settings/settings_page\";\n\nimport { useState } from \"@odoo/owl\";\n\nexport class SettingsFormRenderer extends FormRenderer {\n    static components = {\n        ...FormRenderer.components,\n        SearchableSetting,\n        SettingHeader,\n        SettingsBlock,\n        SettingsPage,\n        SettingsApp,\n        HighlightText,\n        FormLabel: FormLabelHighlightText,\n    };\n    static props = {\n        ...FormRenderer.props,\n        initialApp: String,\n        slots: Object,\n    };\n\n    setup() {\n        super.setup();\n        this.searchState = useState(this.env.searchState);\n    }\n\n    get shouldAutoFocus() {\n        return false;\n    }\n}\n", "import { registry } from \"@web/core/registry\";\nimport { evaluateExpr } from \"@web/core/py_js/py\";\nimport { intersection } from \"@web/core/utils/arrays\";\nimport { ControlPanel } from \"@web/search/control_panel/control_panel\";\nimport { formView } from \"@web/views/form/form_view\";\nimport { SettingsFormController } from \"./settings_form_controller\";\nimport { SettingsFormRenderer } from \"./settings_form_renderer\";\nimport { SettingsFormCompiler } from \"./settings_form_compiler\";\n\nclass SettingRecord extends formView.Model.Record {\n    _update(changes) {\n        const changedFields = Object.keys(changes);\n        let dirty = true;\n        if (intersection(changedFields, this.model._headerFields).length === changedFields.length) {\n            dirty = this.dirty;\n            if (this.dirty) {\n                this.model._onChangeHeaderFields().then(async (isDiscard) => {\n                    if (isDiscard) {\n                        await super._update(...arguments);\n                        this.dirty = false;\n                    } else {\n                        // We need to apply and then undo the changes\n                        // to force the field component to be render\n                        // and restore the previous value (like RadioField))\n                        const undoChanges = this._applyChanges(changes);\n                        undoChanges();\n                    }\n                });\n                return;\n            }\n        }\n        const prom = super._update(...arguments);\n        this.dirty = dirty;\n        return prom;\n    }\n}\n\nclass SettingModel extends formView.Model {\n    static withCache = false;\n\n    setup(params) {\n        super.setup(...arguments);\n        this._headerFields = params.headerFields;\n        this._onChangeHeaderFields = params.onChangeHeaderFields;\n    }\n    _getNextConfig() {\n        const nextConfig = super._getNextConfig(...arguments);\n        nextConfig.resId = false;\n        return nextConfig;\n    }\n}\nSettingModel.Record = SettingRecord;\n\nexport const settingsFormView = {\n    ...formView,\n    display: {},\n    Model: SettingModel,\n    ControlPanel: ControlPanel,\n    Controller: SettingsFormController,\n    Compiler: SettingsFormCompiler,\n    Renderer: SettingsFormRenderer,\n    props: (genericProps, view) => {\n        [...genericProps.arch.querySelectorAll(\"setting[type='header'] field\")].forEach((el) => {\n            const options = evaluateExpr(el.getAttribute(\"options\") || \"{}\");\n            options.isHeaderField = true;\n            el.setAttribute(\"options\", JSON.stringify(options));\n        });\n        return formView.props(genericProps, view);\n    },\n};\n\nregistry.category(\"views\").add(\"base_settings\", settingsFormView);\n", "import { rpc } from \"@web/core/network/rpc\";\nimport { registry } from \"@web/core/registry\";\n\nexport const demoDataService = {\n    async start() {\n        let isDemoDataActiveProm;\n        return {\n            isDemoDataActive() {\n                if (!isDemoDataActiveProm) {\n                    isDemoDataActiveProm = rpc(\"/base_setup/demo_active\");\n                }\n                return isDemoDataActiveProm;\n            },\n        };\n    },\n};\n\nregistry.category(\"services\").add(\"demo_data\", demoDataService);\n", "import { registry } from \"@web/core/registry\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { SettingsBlock } from \"../settings/settings_block\";\nimport { Setting } from \"../../../views/form/setting/setting\";\n\nimport { Component, onWillStart } from \"@odoo/owl\";\nimport { standardWidgetProps } from \"@web/views/widgets/standard_widget_props\";\nimport { router } from \"@web/core/browser/router\";\n\n/**\n * Widget in the settings that handles the \"Developer Tools\" section.\n * Can be used to enable/disable the debug modes.\n * Can be used to load the demo data.\n */\nexport class ResConfigDevTool extends Component {\n    static template = \"res_config_dev_tool\";\n    static components = {\n        SettingsBlock,\n        Setting,\n    };\n    static props = {\n        ...standardWidgetProps,\n    };\n\n    setup() {\n        this.isDebug = Boolean(odoo.debug);\n        this.isAssets = odoo.debug.includes(\"assets\");\n        this.isTests = odoo.debug.includes(\"tests\");\n\n        this.action = useService(\"action\");\n        this.demo = useService(\"demo_data\");\n\n        onWillStart(async () => {\n            this.isDemoDataActive = await this.demo.isDemoDataActive();\n        });\n    }\n\n    activateDebug(value) {\n        router.pushState({ debug: value }, { reload: true });\n    }\n\n    /**\n     * Forces demo data to be installed in a database without demo data installed.\n     */\n    onClickForceDemo() {\n        this.action.doAction(\"base.demo_force_install_action\");\n    }\n}\n\nexport const resConfigDevTool = {\n    component: ResConfigDevTool,\n};\n\nregistry.category(\"view_widgets\").add(\"res_config_dev_tool\", resConfigDevTool);\n", "import { registry } from \"@web/core/registry\";\nimport { session } from \"@web/session\";\nimport { Setting } from \"@web/views/form/setting/setting\";\n\nimport { Component } from \"@odoo/owl\";\nimport { standardWidgetProps } from \"@web/views/widgets/standard_widget_props\";\nconst { DateTime } = luxon;\n\n/**\n * Widget in the settings that handles a part of the \"About\" section.\n * Contains info about the odoo version, database expiration date and copyrights.\n */\nclass ResConfigEdition extends Component {\n    static template = \"res_config_edition\";\n    static components = { Setting };\n    static props = {\n        ...standardWidgetProps,\n    };\n\n    setup() {\n        this.serverVersion = session.server_version;\n        this.expirationDate = session.expiration_date\n            ? DateTime.fromSQL(session.expiration_date).toLocaleString(DateTime.DATE_FULL)\n            : DateTime.now().plus({ days: 30 }).toLocaleString(DateTime.DATE_FULL);\n    }\n}\n\nexport const resConfigEdition = {\n    component: ResConfigEdition,\n};\n\nregistry.category(\"view_widgets\").add(\"res_config_edition\", resConfigEdition);\n", "import { registry } from \"@web/core/registry\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { unique } from \"@web/core/utils/arrays\";\nimport { useService } from \"@web/core/utils/hooks\";\n\nimport { Component, useState, onWillStart } from \"@odoo/owl\";\nimport { standardWidgetProps } from \"@web/views/widgets/standard_widget_props\";\n\nclass ResConfigInviteUsers extends Component {\n    static template = \"res_config_invite_users\";\n    static props = {\n        ...standardWidgetProps,\n    };\n\n    setup() {\n        this.orm = useService(\"orm\");\n        this.invite = useService(\"user_invite\");\n        this.action = useService(\"action\");\n        this.notification = useService(\"notification\");\n\n        this.state = useState({\n            status: \"idle\", // idle, inviting\n            emails: \"\",\n            invite: null,\n        });\n\n        onWillStart(async () => {\n            this.state.invite = await this.invite.fetchData();\n        });\n    }\n\n    /**\n     * @param {string} email\n     * @returns {boolean} true if the given email address is valid\n     */\n    validateEmail(email) {\n        const re =\n            /^([a-z0-9][-a-z0-9_+.]*)@((?:[\\w-]+\\.)*\\w[\\w-]{0,66})\\.([a-z]{2,63}(?:\\.[a-z]{2})?)$/i;\n        return re.test(email);\n    }\n\n    get emails() {\n        return unique(\n            this.state.emails\n                .split(/[ ,;\\n]+/)\n                .map((email) => email.trim())\n                .filter((email) => email.length)\n        );\n    }\n\n    validate() {\n        if (!this.emails.length) {\n            throw new Error(_t(\"Empty email address\"));\n        }\n        const invalidEmails = [];\n        for (const email of this.emails) {\n            if (!this.validateEmail(email)) {\n                invalidEmails.push(email);\n            }\n        }\n        if (invalidEmails.length) {\n            const errorMessage = (() => {\n                switch (invalidEmails.length) {\n                    case 1:\n                        return _t(\"Invalid email address: %(address)s\", {\n                            address: invalidEmails[0],\n                        });\n                    case 2:\n                        return _t(\"Invalid email addresses: %(two_addresses)s\", {\n                            two_addresses: invalidEmails,\n                        });\n                    default:\n                        return _t(\"Invalid email addresses: %(addresses)s\", {\n                            addresses: invalidEmails,\n                        });\n                }\n            })();\n            throw new Error(errorMessage);\n        }\n    }\n\n    get inviteButtonText() {\n        if (this.state.status === \"inviting\") {\n            return _t(\"Inviting...\");\n        }\n        return _t(\"Invite\");\n    }\n\n    onClickMore(ev) {\n        this.action.doAction(this.state.invite.action_pending_users);\n    }\n\n    onClickUser(ev, user) {\n        const action = Object.assign({}, this.state.invite.action_pending_users, {\n            res_id: user[0],\n        });\n        this.action.doAction(action);\n    }\n\n    onKeydownUserEmails(ev) {\n        const keys = [\"Enter\", \"Tab\", \",\"];\n        if (keys.includes(ev.key)) {\n            if (ev.key === \"Tab\" && !this.emails.length) {\n                return;\n            }\n            ev.preventDefault();\n            this.sendInvite();\n        }\n    }\n\n    /**\n     * Send invitation for valid and unique email addresses\n     *\n     * @private\n     */\n    async sendInvite() {\n        try {\n            this.validate();\n        } catch (e) {\n            this.notification.add(e.message, { type: \"danger\" });\n            return;\n        }\n\n        this.state.status = \"inviting\";\n\n        const pendingUserEmails = this.state.invite.pending_users.map((user) => user[1]);\n        const emailsLeftToProcess = this.emails.filter(\n            (email) => !pendingUserEmails.includes(email)\n        );\n\n        try {\n            if (emailsLeftToProcess) {\n                await this.orm.call(\"res.users\", \"web_create_users\", [emailsLeftToProcess]);\n                this.state.invite = await this.invite.fetchData(true);\n            }\n        } finally {\n            this.state.emails = \"\";\n            this.state.status = \"idle\";\n        }\n    }\n}\n\nexport const resConfigInviteUsers = {\n    component: ResConfigInviteUsers,\n};\n\nregistry.category(\"view_widgets\").add(\"res_config_invite_users\", resConfigInviteUsers);\n", "import { rpc } from \"@web/core/network/rpc\";\nimport { registry } from \"@web/core/registry\";\n\nexport const userInviteService = {\n    async start() {\n        let dataProm;\n        return {\n            fetchData(reload = false) {\n                if (!dataProm || reload) {\n                    dataProm = rpc(\"/base_setup/data\");\n                }\n                return dataProm;\n            },\n        };\n    },\n};\n\nregistry.category(\"services\").add(\"user_invite\", userInviteService);\n", "import { registry } from \"@web/core/registry\";\nimport { browser } from \"@web/core/browser/browser\";\n\n/**\n * @return {Promise<{\n *     title:string,\n *     text:string,\n *     url:string,\n *     externalMediaFiles:File[]\n * }>}\n */\nconst getShareTargetDataFromServiceWorker = () => {\n    return new Promise((resolve) => {\n        const onmessage = (event) => {\n            if (event.data.action === \"odoo_share_target_ack\") {\n                resolve(event.data.shared_files);\n                browser.navigator.serviceWorker.removeEventListener(\"message\", onmessage);\n            }\n        };\n        browser.navigator.serviceWorker.addEventListener(\"message\", onmessage);\n        browser.navigator.serviceWorker.controller.postMessage(\"odoo_share_target\");\n    });\n};\n\nexport const shareTargetService = {\n    dependencies: [\"menu\"],\n    start(env, { menu }) {\n        let sharedFiles = null;\n        if (\n            browser.navigator.serviceWorker &&\n            new URL(browser.location).searchParams.get(\"share_target\") === \"trigger\"\n        ) {\n            const app = menu.getApps().find((app) => \"expenses\" === app.actionPath);\n            if (app) {\n                const clientReadyListener = async () => {\n                    sharedFiles = await getShareTargetDataFromServiceWorker();\n                    if (sharedFiles?.length) {\n                        await menu.selectMenu(app);\n                    }\n                    env.bus.removeEventListener(\"WEB_CLIENT_READY\", clientReadyListener);\n                };\n                env.bus.addEventListener(\"WEB_CLIENT_READY\", clientReadyListener);\n            }\n        }\n        return {\n            /**\n             * Return true if we receive share target files from service worker\n             * @return {boolean}\n             */\n            hasSharedFiles: () => !!sharedFiles?.length,\n            /**\n             * Return the shared files retrieve for upload\n             * @return {null|File[]}\n             */\n            getSharedFilesToUpload: () => {\n                const files = sharedFiles;\n                sharedFiles = null;\n                return files;\n            },\n        };\n    },\n};\n\nregistry.category(\"services\").add(\"shareTarget\", shareTargetService);\n", "import { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\nimport { Component, useState } from \"@odoo/owl\";\nimport { user } from \"@web/core/user\";\n\nexport class SwitchCompanyItem extends Component {\n    static template = \"web.SwitchCompanyItem\";\n    static components = { DropdownItem, SwitchCompanyItem };\n    static props = {\n        company: {},\n        level: { type: Number },\n    };\n\n    setup() {\n        this.companySelector = useState(this.env.companySelector);\n    }\n\n    get isCompanySelected() {\n        return this.companySelector.isCompanySelected(this.props.company.id);\n    }\n\n    get isCompanyAllowed() {\n        return user.allowedCompanies.map((c) => c.id).includes(this.props.company.id);\n    }\n\n    get isCurrent() {\n        return this.props.company.id === user.activeCompany.id;\n    }\n\n    logIntoCompany() {\n        if (this.isCompanyAllowed) {\n            this.companySelector.switchCompany(\"loginto\", this.props.company.id);\n        }\n    }\n\n    toggleCompany() {\n        if (this.isCompanyAllowed) {\n            this.companySelector.switchCompany(\"toggle\", this.props.company.id);\n        }\n    }\n}\n", "import { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { DropdownGroup } from \"@web/core/dropdown/dropdown_group\";\nimport { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\nimport { registry } from \"@web/core/registry\";\n\nimport { Component, useChildSubEnv, useRef, useState } from \"@odoo/owl\";\nimport { useCommand } from \"@web/core/commands/command_hook\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { symmetricalDifference } from \"@web/core/utils/arrays\";\nimport { useBus, useChildRef, useService } from \"@web/core/utils/hooks\";\nimport { SwitchCompanyItem } from \"@web/webclient/switch_company_menu/switch_company_item\";\nimport { useHotkey } from \"@web/core/hotkeys/hotkey_hook\";\nimport { useDropdownState } from \"@web/core/dropdown/dropdown_hooks\";\nimport { user, userBus } from \"@web/core/user\";\nimport { router } from \"@web/core/browser/router\";\n\nfunction getCompany(cid) {\n    return user.allowedCompaniesWithAncestors.find((c) => c.id === cid);\n}\n\nexport class CompanySelector {\n    constructor(actionService, dropdownState) {\n        this.actionService = actionService;\n        this.dropdownState = dropdownState;\n        this.selectedCompaniesIds = user.activeCompanies.map((c) => c.id);\n    }\n\n    get hasSelectionChanged() {\n        return (\n            symmetricalDifference(\n                this.selectedCompaniesIds,\n                user.activeCompanies.map((c) => c.id)\n            ).length > 0\n        );\n    }\n\n    isCompanySelected(companyId) {\n        return this.selectedCompaniesIds.includes(companyId);\n    }\n\n    switchCompany(mode, companyId) {\n        if (mode === \"toggle\") {\n            if (this.selectedCompaniesIds.includes(companyId)) {\n                this._deselectCompany(companyId);\n            } else {\n                this._selectCompany(companyId);\n            }\n        } else if (mode === \"loginto\") {\n            if (this._isSingleCompanyMode()) {\n                this.selectedCompaniesIds.splice(0, this.selectedCompaniesIds.length);\n            }\n            this._selectCompany(companyId, true);\n            this.apply();\n\n            this.dropdownState.close?.();\n        }\n    }\n\n    async apply() {\n        user.activateCompanies(this.selectedCompaniesIds, {\n            includeChildCompanies: false,\n            reload: false,\n        });\n\n        const controller = this.actionService.currentController;\n        const state = {};\n        const options = { reload: true };\n        if (controller?.props.resId && controller?.props.resModel) {\n            const hasReadRights = await user.checkAccessRight(\n                controller.props.resModel,\n                \"read\",\n                controller.props.resId\n            );\n\n            if (!hasReadRights) {\n                options.replace = true;\n                state.actionStack = router.current.actionStack.slice(0, -1);\n            }\n        }\n\n        router.pushState(state, options);\n    }\n\n    reset() {\n        this.selectedCompaniesIds = user.activeCompanies.map((c) => c.id);\n    }\n\n    selectAll(companyIds) {\n        let shouldSelectAll = true;\n\n        // If any company is selected, just unselect all\n        for (let i = this.selectedCompaniesIds.length - 1; i >= 0; i--) {\n            if (companyIds.includes(this.selectedCompaniesIds[i])) {\n                this.selectedCompaniesIds.splice(i, 1);\n                shouldSelectAll = false;\n            }\n        }\n\n        // If no company is selected, select all\n        if (shouldSelectAll) {\n            for (const companyId of companyIds) {\n                if (!this.selectedCompaniesIds.includes(companyId)) {\n                    this.selectedCompaniesIds.push(companyId);\n                }\n            }\n        }\n    }\n\n    _selectCompany(companyId, unshift = false) {\n        if (this._isCompanyAllowed(companyId)) {\n            if (!this.selectedCompaniesIds.includes(companyId)) {\n                if (unshift) {\n                    this.selectedCompaniesIds.unshift(companyId);\n                } else {\n                    this.selectedCompaniesIds.push(companyId);\n                }\n            } else if (unshift) {\n                const index = this.selectedCompaniesIds.findIndex((c) => c === companyId);\n                this.selectedCompaniesIds.splice(index, 1);\n                this.selectedCompaniesIds.unshift(companyId);\n            }\n        }\n\n        this._getBranches(companyId).forEach((companyId) => this._selectCompany(companyId));\n    }\n\n    _deselectCompany(companyId) {\n        if (this.selectedCompaniesIds.includes(companyId)) {\n            this.selectedCompaniesIds.splice(this.selectedCompaniesIds.indexOf(companyId), 1);\n        }\n        this._getBranches(companyId).forEach((companyId) => this._deselectCompany(companyId));\n    }\n\n    _getBranches(companyId) {\n        return getCompany(companyId).child_ids || [];\n    }\n    \n    _isCompanyAllowed(companyId) {\n        return user.allowedCompanies.some((c) => c.id == companyId);\n    }\n\n    _isSingleCompanyMode() {\n        if (this.selectedCompaniesIds.length === 1) {\n            return true;\n        }\n\n        const getActiveCompany = (companyId) => {\n            const isActive = this.selectedCompaniesIds.includes(companyId);\n            return isActive ? getCompany(companyId) : null;\n        };\n\n        let rootCompany = undefined;\n        for (const companyId of this.selectedCompaniesIds) {\n            let company = getActiveCompany(companyId);\n\n            // Find the root active parent of the company\n            while (getActiveCompany(company.parent_id)) {\n                company = getActiveCompany(company.parent_id);\n            }\n\n            if (rootCompany === undefined) {\n                rootCompany = company;\n            } else if (rootCompany !== company) {\n                return false;\n            }\n        }\n\n        // If some children or sub-children of the root company\n        // are not active, we are in multi-company mode.\n        if (rootCompany && rootCompany.child_ids) {\n            const queue = [...rootCompany.child_ids];\n            while (queue.length > 0) {\n                const company = getActiveCompany(queue.pop());\n                if (company && company.child_ids) {\n                    queue.push(...company.child_ids);\n                } else if (!company) {\n                    return false;\n                }\n            }\n        }\n\n        return true;\n    }\n}\n\nexport class SwitchCompanyMenu extends Component {\n    static template = \"web.SwitchCompanyMenu\";\n    static components = { Dropdown, DropdownItem, DropdownGroup, SwitchCompanyItem };\n    static props = {};\n    static CompanySelector = CompanySelector;\n\n    setup() {\n        this.dropdown = useDropdownState();\n        this.user = user;\n        const actionService = useService(\"action\");\n\n        this.companySelector = useState(\n            new this.constructor.CompanySelector(actionService, this.dropdown)\n        );\n        useChildSubEnv({ companySelector: this.companySelector });\n\n        this.searchInputRef = useRef(\"inputRef\");\n        this.state = useState({});\n        this.resetState();\n\n        useHotkey(\"control+enter\", () => this.confirm(), {\n            bypassEditableProtection: true,\n            isAvailable: () => this.companySelector.hasSelectionChanged,\n        });\n\n        useCommand(_t(\"Switch Company\"), () => this.dropdown.open(), { hotkey: \"alt+shift+u\" });\n        useBus(userBus, \"ACTIVE_COMPANIES_CHANGED\", () => {\n            this.companySelector.reset();\n        });\n\n        this.containerRef = useChildRef();\n        this.navigationOptions = {\n            hotkeys: {\n                space: (navigator) => {\n                    const navItem = navigator.activeItem;\n                    if (!navItem) {\n                        return;\n                    }\n                    if (navItem.el.classList.contains(\"o_switch_company_item\")) {\n                        const companyId = parseInt(navItem.el.dataset.companyId);\n                        this.companySelector.switchCompany(\"toggle\", companyId);\n                    }\n                },\n                enter: (navigator) => {\n                    const navItem = navigator.activeItem;\n                    if (!navItem) {\n                        return;\n                    }\n                    if (navItem.el.classList.contains(\"o_switch_company_item\")) {\n                        const companyId = parseInt(navItem.el.dataset.companyId);\n                        this.companySelector.switchCompany(\"loginto\", companyId);\n                        this.dropdown.close();\n                    } else {\n                        navItem.select();\n                    }\n                },\n            },\n        };\n    }\n\n    get hasLotsOfCompanies() {\n        return user.allowedCompaniesWithAncestors.length > 9;\n    }\n\n    get visibleCompanies() {\n        return this.state.visibleCompanies;\n    }\n\n    get hasSelectedCompanies() {\n        return this.visibleCompanies.some((c) =>\n            this.companySelector.isCompanySelected(c.company.id)\n        );\n    }\n\n    get selectAllClass() {\n        if (\n            this.visibleCompanies.every((c) => this.companySelector.isCompanySelected(c.company.id))\n        ) {\n            return \"btn-link text-primary\";\n        } else {\n            return \"btn-link text-secondary\";\n        }\n    }\n\n    get selectAllIcon() {\n        if (\n            this.visibleCompanies.every((c) => this.companySelector.isCompanySelected(c.company.id))\n        ) {\n            return \"fa-check-square text-primary\";\n        } else if (\n            this.visibleCompanies.some((c) => this.companySelector.isCompanySelected(c.company.id))\n        ) {\n            return \"fa-minus-square-o\";\n        } else {\n            return \"fa-square-o\";\n        }\n    }\n\n    computeVisibleCompanies() {\n        const companies = [];\n\n        const addCompany = (company, level = 0) => {\n            if (this.matchSearch(company.name)) {\n                companies.push({ company, level });\n            }\n\n            if (company.child_ids) {\n                for (const companyId of company.child_ids) {\n                    addCompany(getCompany(companyId), level + 1);\n                }\n            }\n        };\n\n        user.allowedCompaniesWithAncestors\n            .filter((c) => !c.parent_id)\n            .sort((c1, c2) => c1.sequence - c2.sequence)\n            .forEach((c) => addCompany(c));\n\n        return companies;\n    }\n\n    resetState() {\n        this.state.searchFilter = \"\";\n        this.state.showFilter = this.hasLotsOfCompanies;\n        this.state.visibleCompanies = this.computeVisibleCompanies();\n    }\n\n    onSearch(ev) {\n        this.state.searchFilter = ev.target.value;\n        this.state.showFilter = true;\n        this.state.visibleCompanies = this.computeVisibleCompanies();\n    }\n\n    matchSearch(companyName) {\n        if (!this.state.searchFilter) {\n            return true;\n        }\n\n        const name = companyName.toLocaleLowerCase().replace(/\\s/g, \"\");\n        const filter = this.state.searchFilter.toLocaleLowerCase().replace(/\\s/g, \"\");\n        return name.includes(filter);\n    }\n\n    handleDropdownChange(isOpen) {\n        if (isOpen) {\n            if (this.searchInputRef.el) {\n                this.searchInputRef.el.focus();\n            }\n\n            if (this.containerRef.el) {\n                // Fixes the container width so it doesn't change when searching.\n                const currentWidth = this.containerRef.el.getBoundingClientRect().width;\n                this.containerRef.el.style.width = currentWidth + \"px\";\n            }\n        } else {\n            this.resetState();\n        }\n    }\n\n    confirm() {\n        this.dropdown.close();\n        this.companySelector.apply();\n    }\n\n    selectAll() {\n        const companyIds = this.visibleCompanies.map((entry) => entry.company.id);\n        this.companySelector.selectAll(companyIds);\n    }\n\n    get isSingleCompany() {\n        return user.allowedCompaniesWithAncestors.length === 1;\n    }\n}\n\nexport const systrayItem = {\n    Component: SwitchCompanyMenu,\n};\n\nregistry.category(\"systray\").add(\"SwitchCompanyMenu\", systrayItem, { sequence: 1 });\n", "import { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { DropdownGroup } from \"@web/core/dropdown/dropdown_group\";\nimport { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\nimport { CheckBox } from \"@web/core/checkbox/checkbox\";\nimport { registry } from \"@web/core/registry\";\nimport { user } from \"@web/core/user\";\nimport { session } from \"@web/session\";\n\nimport { Component } from \"@odoo/owl\";\nimport { imageUrl } from \"@web/core/utils/urls\";\n\nconst userMenuRegistry = registry.category(\"user_menuitems\");\n\nexport class UserMenu extends Component {\n    static template = \"web.UserMenu\";\n    static components = { DropdownGroup, Dropdown, DropdownItem, CheckBox };\n    static props = {};\n\n    setup() {\n        this.userName = user.name;\n        this.dbName = session.db;\n        const { partnerId, writeDate } = user;\n        this.source = imageUrl(\"res.partner\", partnerId, \"avatar_128\", { unique: writeDate });\n    }\n\n    getElements() {\n        const sortedItems = userMenuRegistry\n            .getAll()\n            .map((element) => element(this.env))\n            .filter((element) => (element.show ? element.show() : true))\n            .sort((x, y) => {\n                const xSeq = x.sequence ? x.sequence : 100;\n                const ySeq = y.sequence ? y.sequence : 100;\n                return xSeq - ySeq;\n            });\n        return sortedItems;\n    }\n}\n\nexport const systrayItem = {\n    Component: UserMenu,\n};\nregistry.category(\"systray\").add(\"web.user_menu\", systrayItem, { sequence: 0 });\n", "import { Component, markup } from \"@odoo/owl\";\nimport { isMacOS } from \"@web/core/browser/feature_detection\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { user } from \"@web/core/user\";\nimport { session } from \"@web/session\";\nimport { browser } from \"../../core/browser/browser\";\nimport { registry } from \"../../core/registry\";\n\nfunction supportItem(env) {\n    const url = session.support_url;\n    return {\n        type: \"item\",\n        id: \"support\",\n        description: _t(\"Help\"),\n        href: url,\n        callback: () => {\n            browser.open(url, \"_blank\");\n        },\n        sequence: 20,\n    };\n}\n\nclass ShortcutsFooterComponent extends Component {\n    static template = \"web.UserMenu.ShortcutsFooterComponent\";\n    static props = {\n        switchNamespace: { type: Function, optional: true },\n    };\n    setup() {\n        this.runShortcutKey = isMacOS() ? \"CONTROL\" : \"ALT\";\n    }\n}\n\nfunction shortCutsItem(env) {\n    return {\n        type: \"item\",\n        id: \"shortcuts\",\n        hide: env.isSmall,\n        description: markup`\n            <div class=\"d-flex align-items-center justify-content-between p-0 w-100\">\n                <span>${_t(\"Shortcuts\")}</span>\n                <span class=\"fw-bold\">${isMacOS() ? \"CMD\" : \"CTRL\"}+K</span>\n            </div>`,\n        callback: () => {\n            env.services.command.openMainPalette({ FooterComponent: ShortcutsFooterComponent });\n        },\n        sequence: 30,\n    };\n}\n\nfunction separator() {\n    return {\n        type: \"separator\",\n        sequence: 40,\n    };\n}\n\nexport function preferencesItem(env) {\n    return {\n        type: \"item\",\n        id: \"preferences\",\n        description: _t(\"My Preferences\"),\n        callback: async function () {\n            const actionDescription = await env.services.orm.call(\"res.users\", \"action_get\");\n            actionDescription.res_id = user.userId;\n            env.services.action.doAction(actionDescription);\n        },\n        sequence: 50,\n    };\n}\n\nexport function odooAccountItem(env) {\n    return {\n        type: \"item\",\n        id: \"account\",\n        description: _t(\"My Odoo.com Account\"),\n        callback: () => {\n            rpc(\"/web/session/account\")\n                .then((url) => {\n                    browser.open(url, \"_blank\");\n                })\n                .catch(() => {\n                    browser.open(\"https://accounts.odoo.com/account\", \"_blank\");\n                });\n        },\n        sequence: 60,\n    };\n}\n\nfunction installPWAItem(env) {\n    let description = _t(\"Install App\");\n    let callback = () => env.services.pwa.show();\n    let show = () => env.services.pwa.isAvailable;\n    const currentApp = env.services.menu.getCurrentApp();\n    if (currentApp && [\"barcode\", \"field-service\", \"shop-floor\"].includes(currentApp.actionPath)) {\n        // While the feature could work with all apps, we have decided to only\n        // support the installation of the apps contained in this list\n        // The list can grow in the future, by simply adding their path\n        description = _t(\"Install %s\", currentApp.name);\n        callback = () => {\n            window.open(\n                `/scoped_app?app_id=${currentApp.webIcon.split(\",\")[0]}&path=${encodeURIComponent(\n                    \"scoped_app/\" + currentApp.actionPath\n                )}`\n            );\n        };\n        show = () => !env.services.pwa.isScopedApp;\n    }\n    return {\n        type: \"item\",\n        id: \"install_pwa\",\n        description,\n        callback,\n        show,\n        sequence: 65,\n    };\n}\n\nfunction logOutItem(env) {\n    let route = \"/web/session/logout\";\n    if (env.services.pwa.isScopedApp) {\n        route += `?redirect=${encodeURIComponent(env.services.pwa.startUrl)}`;\n    }\n    return {\n        type: \"item\",\n        id: \"logout\",\n        description: _t(\"Log out\"),\n        href: `${browser.location.origin}${route}`,\n        callback: () => {\n            browser.navigator.serviceWorker?.controller?.postMessage(\"user_logout\");\n            browser.location.href = route;\n        },\n        sequence: 70,\n    };\n}\n\nregistry\n    .category(\"user_menuitems\")\n    .add(\"support\", supportItem)\n    .add(\"shortcuts\", shortCutsItem)\n    .add(\"separator\", separator)\n    .add(\"preferences\", preferencesItem)\n    .add(\"odoo_account\", odooAccountItem)\n    .add(\"install_pwa\", installPWAItem)\n    .add(\"log_out\", logOutItem);\n", "import { useOwnDebugContext } from \"@web/core/debug/debug_context\";\nimport { Deferred } from \"@web/core/utils/concurrency\";\nimport { DebugMenu } from \"@web/core/debug/debug_menu\";\nimport { localization } from \"@web/core/l10n/localization\";\nimport { MainComponentsContainer } from \"@web/core/main_components_container\";\nimport { registry } from \"@web/core/registry\";\nimport { useBus, useService } from \"@web/core/utils/hooks\";\nimport { ActionContainer } from \"./actions/action_container\";\nimport { NavBar } from \"./navbar/navbar\";\n\nimport { Component, onMounted, onWillStart, useExternalListener, useState } from \"@odoo/owl\";\nimport { router, routerBus } from \"@web/core/browser/router\";\nimport { browser } from \"@web/core/browser/browser\";\nimport { rpcBus } from \"@web/core/network/rpc\";\n\nexport class WebClient extends Component {\n    static template = \"web.WebClient\";\n    static props = {};\n    static components = {\n        ActionContainer,\n        NavBar,\n        MainComponentsContainer,\n    };\n\n    setup() {\n        this.menuService = useService(\"menu\");\n        this.actionService = useService(\"action\");\n        this.title = useService(\"title\");\n        useOwnDebugContext({ categories: [\"default\"] });\n        if (this.env.debug) {\n            registry.category(\"systray\").add(\n                \"web.debug_mode_menu\",\n                {\n                    Component: DebugMenu,\n                },\n                { sequence: 100 }\n            );\n        }\n        this.localization = localization;\n        this.state = useState({\n            fullscreen: false,\n        });\n        useBus(routerBus, \"ROUTE_CHANGE\", async () => {\n            document.body.style.pointerEvents = \"none\";\n            try {\n                await this.loadRouterState();\n            } finally {\n                document.body.style.pointerEvents = \"auto\";\n            }\n        });\n        useBus(this.env.bus, \"ACTION_MANAGER:UI-UPDATED\", ({ detail: mode }) => {\n            if (mode !== \"new\") {\n                this.state.fullscreen = mode === \"fullscreen\";\n            }\n        });\n        useBus(this.env.bus, \"WEBCLIENT:LOAD_DEFAULT_APP\", this._loadDefaultApp);\n        onMounted(() => {\n            this.loadRouterState();\n            // the chat window and dialog services listen to 'web_client_ready' event in\n            // order to initialize themselves:\n            this.env.bus.trigger(\"WEB_CLIENT_READY\");\n        });\n        useExternalListener(window, \"click\", this.onGlobalClick, { capture: true });\n        this.serviceWorkerActivatedDeferred = new Deferred();\n        onWillStart(this.registerServiceWorker);\n    }\n\n    async loadRouterState() {\n        // ** url-retrocompatibility **\n        // the menu_id in the url is only possible if we came from an old url\n        let menuId = Number(router.current.menu_id || 0);\n        const storedMenuId = Number(browser.sessionStorage.getItem(\"menu_id\"));\n        const firstAction = router.current.actionStack?.[0]?.action;\n        if (!menuId && firstAction) {\n            // Find all menus that match this action\n            const matchingMenus = this.menuService\n                .getAll()\n                .filter((m) => m.actionID === firstAction || m.actionPath === firstAction);\n\n            if (matchingMenus.length > 0) {\n                // Use sessionStorage context to determine the correct menu\n                menuId = matchingMenus.find(m => \n                    m.appID === storedMenuId\n                )?.appID;\n                if (!menuId) {\n                    menuId = matchingMenus[0]?.appID;\n                }\n            }\n        }\n        if (menuId) {\n            this.menuService.setCurrentMenu(menuId);\n        }\n        let stateLoaded = await this.actionService.loadState();\n\n        // ** url-retrocompatibility **\n        // when there is only menu_id in url\n        if (!stateLoaded && menuId) {\n            // Determines the current actionId based on the current menu\n            const menu = this.menuService.getAll().find((m) => menuId === m.id);\n            const actionId = menu && menu.actionID;\n            if (actionId) {\n                await this.actionService.doAction(actionId, { clearBreadcrumbs: true });\n                stateLoaded = true;\n            }\n        }\n\n        // Setting the menu based on the action after it was loaded (eg when the action in url is an xmlid)\n        if (stateLoaded && !menuId) {\n            // Determines the current menu based on the current action\n            const currentController = this.actionService.currentController;\n            const actionId = currentController && currentController.action.id;\n            menuId = this.menuService.getAll().find((m) => m.actionID === actionId)?.appID;\n            if (!menuId) {\n                // Setting the menu based on the session storage if no other menu was found\n                menuId = storedMenuId;\n            }\n            if (menuId) {\n                // Sets the menu according to the current action\n                this.menuService.setCurrentMenu(menuId);\n            }\n        }\n\n        // Scroll to anchor after the state is loaded\n        if (stateLoaded) {\n            if (browser.location.hash !== \"\") {\n                try {\n                    const el = document.querySelector(browser.location.hash);\n                    if (el !== null) {\n                        el.scrollIntoView(true);\n                    }\n                } catch {\n                    // do nothing if the hash is not a correct selector.\n                }\n            }\n        }\n\n        if (!stateLoaded) {\n            // If no action => falls back to the default app\n            await this._loadDefaultApp();\n        }\n    }\n\n    _loadDefaultApp() {\n        // Selects the first root menu if any\n        const root = this.menuService.getMenu(\"root\");\n        const firstApp = root.children[0];\n        if (firstApp) {\n            return this.menuService.selectMenu(firstApp);\n        }\n    }\n\n    /**\n     * @param {MouseEvent} ev\n     */\n    onGlobalClick(ev) {\n        // When a ctrl-click occurs inside an <a href/> element\n        // we let the browser do the default behavior and\n        // we do not want any other listener to execute.\n        if (\n            (ev.ctrlKey || ev.metaKey) &&\n            !ev.target.isContentEditable &&\n            ((ev.target instanceof HTMLAnchorElement && ev.target.href) ||\n                (ev.target instanceof HTMLElement && ev.target.closest(\"a[href]:not([href=''])\")))\n        ) {\n            ev.stopImmediatePropagation();\n            return;\n        }\n    }\n\n    registerServiceWorker() {\n        if (navigator.serviceWorker) {\n            navigator.serviceWorker\n                .register(\"/web/service-worker.js\", { scope: \"/odoo\" })\n                .then((registration) => {\n                    if (registration.active && registration.active.state === \"activated\") {\n                        this.serviceWorkerActivatedDeferred.resolve();\n                    } else {\n                        const sw =\n                            registration.installing || registration.waiting || registration.active;\n                        sw.addEventListener(\"statechange\", (e) => {\n                            if (e.target.state === \"activated\") {\n                                this.serviceWorkerActivatedDeferred.resolve();\n                            }\n                        });\n                    }\n                    navigator.serviceWorker.ready.then(() => {\n                        if (!navigator.serviceWorker.controller) {\n                            // https://stackoverflow.com/questions/51597231/register-service-worker-after-hard-refresh\n                            rpcBus.trigger(\"CLEAR-CACHES\");\n                        }\n                    });\n                })\n                .catch((error) => {\n                    console.error(\"Service worker registration failed, error:\", error);\n                });\n        }\n    }\n}\n", "import { useService } from \"@web/core/utils/hooks\";\nimport { useSetupAction } from \"@web/search/action_hook\";\nimport { Layout } from \"@web/search/layout\";\nimport { getDefaultConfig } from \"@web/views/view\";\nimport { useEnrichWithActionLinks } from \"@web/webclient/actions/reports/report_hook\";\n\nimport { Component, useRef, useSubEnv } from \"@odoo/owl\";\n\n/**\n * Most of the time reports are printed as pdfs.\n * However, reports have 3 possible actions: pdf, text and HTML.\n * This file is the HTML action.\n * The HTML action is a client action (with control panel) rendering the template in an iframe.\n * If not defined as the default action, the HTML is the fallback to pdf if wkhtmltopdf is not available.\n *\n * It has a button to print the report.\n * It uses a feature to automatically create links to other odoo pages if the selector [res-id][res-model][view-type]\n * is detected.\n */\nexport class ReportAction extends Component {\n    static components = { Layout };\n    static template = \"web.ReportAction\";\n    static props = [\"*\"];\n    setup() {\n        useSubEnv({\n            config: {\n                ...getDefaultConfig(),\n                ...this.env.config,\n            },\n        });\n        useSetupAction();\n\n        this.action = useService(\"action\");\n        this.title = this.props.display_name || this.props.name;\n        this.reportUrl = this.props.report_url;\n        this.iframe = useRef(\"iframe\");\n        useEnrichWithActionLinks(this.iframe);\n    }\n\n    onIframeLoaded(ev) {\n        const iframeDocument = ev.target.contentWindow.document;\n        iframeDocument.body.classList.add(\"o_in_iframe\", \"container-fluid\");\n        iframeDocument.body.classList.remove(\"container\");\n    }\n\n    print() {\n        this.action.doAction({\n            type: \"ir.actions.report\",\n            report_type: \"qweb-pdf\",\n            report_name: this.props.report_name,\n            report_file: this.props.report_file,\n            data: this.props.data || {},\n            context: this.props.context || {},\n            display_name: this.title,\n        });\n    }\n}\n", "import { useComponent, useEffect } from \"@odoo/owl\";\n\n/**\n * Hook used to enrich html and provide automatic links to action.\n * Dom elements must have those attrs [res-id][res-model][view-type]\n * Each element with those attrs will become a link to the specified resource.\n * Works with Iframes.\n *\n * @param {owl reference} ref Owl ref to the element to enrich\n * @param {string} [selector] Selector to apply to the element resolved by the ref.\n */\nexport function useEnrichWithActionLinks(ref, selector = null) {\n    const comp = useComponent();\n    useEffect(\n        (element) => {\n            // If we get an iframe, we need to wait until everything is loaded\n            if (element.matches(\"iframe\")) {\n                element.onload = () => enrich(comp, element, selector, true);\n            } else {\n                enrich(comp, element, selector);\n            }\n        },\n        () => [ref.el]\n    );\n}\n\nfunction enrich(component, targetElement, selector, isIFrame = false) {\n    let doc = window.document;\n\n    // If we are in an iframe, we need to take the right document\n    // both for the element and the doc\n    if (isIFrame) {\n        targetElement = targetElement.contentDocument;\n        doc = targetElement;\n    }\n\n    // If there are selector, we may have multiple blocks of code to enrich\n    const targets = [];\n    if (selector) {\n        targets.push(...targetElement.querySelectorAll(selector));\n    } else {\n        targets.push(targetElement);\n    }\n\n    // Search the elements with the selector, update them and bind an action.\n    for (const currentTarget of targets) {\n        const elementsToWrap = currentTarget.querySelectorAll(\"[res-id][res-model][view-type]\");\n        for (const element of elementsToWrap.values()) {\n            const wrapper = doc.createElement(\"a\");\n            wrapper.setAttribute(\"href\", \"#\");\n            wrapper.addEventListener(\"click\", (ev) => {\n                ev.preventDefault();\n                component.env.services.action.doAction({\n                    type: \"ir.actions.act_window\",\n                    view_mode: element.getAttribute(\"view-type\"),\n                    res_id: Number(element.getAttribute(\"res-id\")),\n                    res_model: element.getAttribute(\"res-model\"),\n                    views: [[element.getAttribute(\"view-id\"), element.getAttribute(\"view-type\")]],\n                });\n            });\n            element.parentNode.insertBefore(wrapper, element);\n            wrapper.appendChild(element);\n        }\n    }\n}\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { download } from \"@web/core/network/download\";\n\n/**\n * Generates the report url given a report action.\n *\n * @param {Object} action the report action\n * @param {\"text\"|\"qweb\"|\"html\"} type the type of the report\n * @param {Object} userContext the user context\n * @returns {string}\n */\nexport function getReportUrl(action, type, userContext) {\n    let url = `/report/${type}/${action.report_name}`;\n    const actionContext = action.context || {};\n    if (action.data && JSON.stringify(action.data) !== \"{}\") {\n        // build a query string with `action.data` (it's the place where reports\n        // using a wizard to customize the output traditionally put their options)\n        const options = encodeURIComponent(JSON.stringify(action.data));\n        const context = encodeURIComponent(JSON.stringify(actionContext));\n        url += `?options=${options}&context=${context}`;\n    } else {\n        if (actionContext.active_ids) {\n            url += `/${actionContext.active_ids.join(\",\")}`;\n        }\n        if (type === \"html\") {\n            const context = encodeURIComponent(JSON.stringify(userContext));\n            url += `?context=${context}`;\n        }\n    }\n    return url;\n}\n\n// messages that might be shown to the user dependening on the state of wkhtmltopdf\nfunction getWKHTMLTOPDF_MESSAGES(status) {\n    const link = '<br><br><a href=\"http://wkhtmltopdf.org/\" target=\"_blank\">wkhtmltopdf.org</a>'; // FIXME missing markup\n    const _status = {\n        broken: _t(\n            \"Your installation of Wkhtmltopdf seems to be broken. The report will be shown in html.%(link)s\",\n            { link }\n        ),\n        install: _t(\n            \"Unable to find Wkhtmltopdf on this system. The report will be shown in html.%(link)s\",\n            { link }\n        ),\n        upgrade: _t(\n            \"You should upgrade your version of Wkhtmltopdf to at least 0.12.0 in order to get a correct display of headers and footers as well as support for table-breaking between pages.%(link)s\",\n            { link }\n        ),\n        workers: _t(\n            \"You need to start Odoo with at least two workers to print a pdf version of the reports.\"\n        ),\n    };\n    return _status[status];\n}\n\n/**\n * Launches download action of the report\n *\n * @param {Function} rpc a function to perform RPCs\n * @param {Object} action the report action\n * @param {\"pdf\"|\"text\"} type the type of the report to download\n * @param {Object} userContext the user context\n * @returns {Promise<{success: boolean, message?: string}>}\n */\nexport async function downloadReport(rpc, action, type, userContext) {\n    let message;\n    if (type === \"pdf\") {\n        // Cache the wkhtml status on the function. In prod this means is only\n        // checked once, but we can reset it between tests to test multiple statuses.\n        downloadReport.wkhtmltopdfStatusProm ||= rpc(\"/report/check_wkhtmltopdf\");\n        const status = await downloadReport.wkhtmltopdfStatusProm;\n        message = getWKHTMLTOPDF_MESSAGES(status);\n        if (![\"upgrade\", \"ok\"].includes(status)) {\n            return { success: false, message };\n        }\n    }\n    const url = getReportUrl(action, type);\n    await download({\n        url: \"/report/download\",\n        data: {\n            data: JSON.stringify([url, action.report_type]),\n            context: JSON.stringify(userContext),\n        },\n    });\n    return { success: true, message };\n}\n", "import { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\nimport { registry } from \"@web/core/registry\";\nimport { Component } from \"@odoo/owl\";\n\nconst cogMenuRegistry = registry.category(\"cogMenu\");\n\nexport class ResetModuleStateCogMenu extends Component {\n    static template = \"base_setup.ResetModuleStateCogMenu\";\n    static components = { DropdownItem };\n    static props = {};\n\n    async resetModuleState() {\n        await this.env.services.orm.call(\"ir.module.module\", \"button_reset_state\", [], {});\n        window.location.reload();\n    }\n}\n\ncogMenuRegistry.add(\"reset-module-state-cog-menu\", {\n    Component: ResetModuleStateCogMenu,\n    isDisplayed: async ({ config, searchModel, services }) =>\n        searchModel.resModel === \"ir.module.module\" &&\n        config.viewType !== \"form\" &&\n        (await services.orm.call(\"ir.module.module\", \"check_module_update\", [], {})),\n});\n", "import { registry } from \"@web/core/registry\";\n\nexport const busParametersService = {\n    start() {\n        return {\n            serverURL: window.origin,\n        };\n    },\n};\n\nregistry.category(\"services\").add(\"bus.parameters\", busParametersService);\n", "import { registry } from \"@web/core/registry\";\nimport { browser } from \"@web/core/browser/browser\";\nimport { EventBus } from \"@odoo/owl\";\n\nexport const legacyMultiTabService = {\n    start() {\n        const bus = new EventBus();\n\n        // PROPERTIES\n        const sanitizedOrigin = location.origin.replace(/:\\/{0,2}/g, \"_\");\n        const localStoragePrefix = `${this.name}.${sanitizedOrigin}.`;\n\n        function generateLocalStorageKey(baseKey) {\n            return localStoragePrefix + baseKey;\n        }\n\n        function getItemFromStorage(key, defaultValue) {\n            const item = browser.localStorage.getItem(generateLocalStorageKey(key));\n            try {\n                return item ? JSON.parse(item) : defaultValue;\n            } catch {\n                return item;\n            }\n        }\n\n        function setItemInStorage(key, value) {\n            browser.localStorage.setItem(generateLocalStorageKey(key), JSON.stringify(value));\n        }\n\n        function onStorage({ key, newValue }) {\n            if (key && key.includes(localStoragePrefix)) {\n                // Only trigger the shared_value_updated event if the key is\n                // related to this service/origin.\n                const baseKey = key.replace(localStoragePrefix, \"\");\n                bus.trigger(\"shared_value_updated\", { key: baseKey, newValue });\n            }\n        }\n\n        browser.addEventListener(\"storage\", onStorage);\n\n        return {\n            bus,\n            generateLocalStorageKey,\n            getItemFromStorage,\n            setItemInStorage,\n            /**\n             * Get value shared between all the tabs.\n             *\n             * @param {string} key\n             * @param {any} defaultValue Value to be returned if this\n             * key does not exist.\n             */\n            getSharedValue(key, defaultValue) {\n                return getItemFromStorage(key, defaultValue);\n            },\n            /**\n             * Set value shared between all the tabs.\n             *\n             * @param {string} key\n             * @param {any} value\n             */\n            setSharedValue(key, value) {\n                if (value === undefined) {\n                    return this.removeSharedValue(key);\n                }\n                setItemInStorage(key, value);\n            },\n            /**\n             * Remove value shared between all the tabs.\n             *\n             * @param {string} key\n             */\n            removeSharedValue(key) {\n                browser.localStorage.removeItem(generateLocalStorageKey(key));\n            },\n        };\n    },\n};\n\nregistry.category(\"services\").add(\"legacy_multi_tab\", legacyMultiTabService);\n", "import { browser } from \"@web/core/browser/browser\";\n\n/**\n * Returns a function, that, when invoked, will only be triggered at most once\n * during a given window of time. Normally, the throttled function will run\n * as much as it can, without ever going more than once per `wait` duration;\n * but if you'd like to disable the execution on the leading edge, pass\n * `{leading: false}`. To disable execution on the trailing edge, ditto.\n *\n * credit to `underscore.js`\n */\nfunction throttle(func, wait, options) {\n    let timeout, context, args, result;\n    let previous = 0;\n    if (!options) {\n        options = {};\n    }\n\n    const later = function () {\n        previous = options.leading === false ? 0 : luxon.DateTime.now().ts;\n        timeout = null;\n        result = func.apply(context, args);\n        if (!timeout) {\n            context = args = null;\n        }\n    };\n\n    const throttled = function () {\n        const _now = luxon.DateTime.now().ts;\n        if (!previous && options.leading === false) {\n            previous = _now;\n        }\n        const remaining = wait - (_now - previous);\n        context = this;\n        args = arguments;\n        if (remaining <= 0 || remaining > wait) {\n            if (timeout) {\n                browser.clearTimeout(timeout);\n                timeout = null;\n            }\n            previous = _now;\n            result = func.apply(context, args);\n            if (!timeout) {\n                context = args = null;\n            }\n        } else if (!timeout && options.trailing !== false) {\n            timeout = browser.setTimeout(later, remaining);\n        }\n        return result;\n    };\n\n    throttled.cancel = function () {\n        browser.clearTimeout(timeout);\n        previous = 0;\n        timeout = context = args = null;\n    };\n\n    return throttled;\n}\n\nexport const timings = {\n    throttle,\n};\n", "import { browser } from \"@web/core/browser/browser\";\nimport { EventBus } from \"@odoo/owl\";\n\nlet multiTabId = 0;\n/**\n * This service uses a Master/Slaves with Leader Election architecture in\n * order to keep track of the main tab. Tabs are synchronized thanks to the\n * localStorage.\n *\n * localStorage used keys are:\n * - multi_tab_service.lastPresenceByTab: mapping of tab ids to their last\n *   recorded presence.\n * - multi_tab_service.main: a boolean indicating whether a main tab is already\n *   present.\n * - multi_tab_service.heartbeat: last main tab heartbeat time.\n *\n * trigger:\n * - become_main_tab : when this tab became the main.\n * - no_longer_main_tab : when this tab is no longer the main.\n */\nexport const multiTabFallbackService = {\n    start(env) {\n        const bus = new EventBus();\n\n        // CONSTANTS\n        const TAB_HEARTBEAT_PERIOD = 10000; // 10 seconds\n        const MAIN_TAB_HEARTBEAT_PERIOD = 1500; // 1.5 seconds\n        const HEARTBEAT_OUT_OF_DATE_PERIOD = 5000; // 5 seconds\n        const HEARTBEAT_KILL_OLD_PERIOD = 15000; // 15 seconds\n\n        // PROPERTIES\n        let _isOnMainTab = false;\n        let lastHeartbeat = 0;\n        let heartbeatTimeout;\n        const now = new Date().getTime();\n        const tabId = `${this.name}${multiTabId++}:${now}`;\n\n        function startElection() {\n            if (_isOnMainTab) {\n                return;\n            }\n            // Check who's next.\n            const now = new Date().getTime();\n            const lastPresenceByTab =\n                JSON.parse(localStorage.getItem(\"multi_tab_service.lastPresenceByTab\")) ?? {};\n            const heartbeatKillOld = now - HEARTBEAT_KILL_OLD_PERIOD;\n            let newMain;\n            for (const [tab, lastPresence] of Object.entries(lastPresenceByTab)) {\n                // Check for dead tabs.\n                if (lastPresence < heartbeatKillOld) {\n                    continue;\n                }\n                newMain = tab;\n                break;\n            }\n            if (newMain === tabId) {\n                // We're next in queue. Electing as main.\n                lastHeartbeat = now;\n                localStorage.setItem(\"multi_tab_service.heartbeat\", lastHeartbeat);\n                localStorage.setItem(\"multi_tab_service.main\", true);\n                _isOnMainTab = true;\n                bus.trigger(\"become_main_tab\");\n                // Removing main peer from queue.\n                delete lastPresenceByTab[newMain];\n                localStorage.setItem(\n                    \"multi_tab_service.lastPresenceByTab\",\n                    JSON.stringify(lastPresenceByTab)\n                );\n            }\n        }\n\n        function heartbeat() {\n            const now = new Date().getTime();\n            let heartbeatValue = parseInt(localStorage.getItem(\"multi_tab_service.heartbeat\") ?? 0);\n            const lastPresenceByTab =\n                JSON.parse(localStorage.getItem(\"multi_tab_service.lastPresenceByTab\")) ?? {};\n            if (heartbeatValue + HEARTBEAT_OUT_OF_DATE_PERIOD < now) {\n                // Heartbeat is out of date. Electing new main.\n                startElection();\n                heartbeatValue = parseInt(localStorage.getItem(\"multi_tab_service.heartbeat\") ?? 0);\n            }\n            if (_isOnMainTab) {\n                // Walk through all tabs and kill old ones.\n                const cleanedTabs = {};\n                for (const [tabId, lastPresence] of Object.entries(lastPresenceByTab)) {\n                    if (lastPresence + HEARTBEAT_KILL_OLD_PERIOD > now) {\n                        cleanedTabs[tabId] = lastPresence;\n                    }\n                }\n                if (heartbeatValue !== lastHeartbeat) {\n                    // Someone else is also main...\n                    // It should not happen, except in some race condition situation.\n                    _isOnMainTab = false;\n                    lastHeartbeat = 0;\n                    lastPresenceByTab[tabId] = now;\n                    localStorage.setItem(\n                        \"multi_tab_service.lastPresenceByTab\",\n                        JSON.stringify(lastPresenceByTab)\n                    );\n                    bus.trigger(\"no_longer_main_tab\");\n                } else {\n                    lastHeartbeat = now;\n                    localStorage.setItem(\"multi_tab_service.heartbeat\", now);\n                    localStorage.setItem(\n                        \"multi_tab_service.lastPresenceByTab\",\n                        JSON.stringify(cleanedTabs)\n                    );\n                }\n            } else {\n                // Update own heartbeat.\n                lastPresenceByTab[tabId] = now;\n                localStorage.setItem(\n                    \"multi_tab_service.lastPresenceByTab\",\n                    JSON.stringify(lastPresenceByTab)\n                );\n            }\n            const hbPeriod = _isOnMainTab ? MAIN_TAB_HEARTBEAT_PERIOD : TAB_HEARTBEAT_PERIOD;\n            heartbeatTimeout = browser.setTimeout(heartbeat, hbPeriod);\n        }\n\n        function onStorage({ key, newValue }) {\n            if (key === \"multi_tab_service.main\" && !newValue) {\n                // Main was unloaded.\n                startElection();\n            }\n        }\n\n        /**\n         * Unregister this tab from the multi-tab service. It will no longer\n         * be able to become the main tab.\n         */\n        function unregister() {\n            clearTimeout(heartbeatTimeout);\n            const lastPresenceByTab =\n                JSON.parse(localStorage.getItem(\"multi_tab_service.lastPresenceByTab\")) ?? {};\n            delete lastPresenceByTab[tabId];\n            localStorage.setItem(\n                \"multi_tab_service.lastPresenceByTab\",\n                JSON.stringify(lastPresenceByTab)\n            );\n\n            // Unload main.\n            if (_isOnMainTab) {\n                _isOnMainTab = false;\n                bus.trigger(\"no_longer_main_tab\");\n                browser.localStorage.removeItem(\"multi_tab_service.main\");\n            }\n        }\n\n        browser.addEventListener(\"pagehide\", unregister);\n        browser.addEventListener(\"storage\", onStorage);\n\n        // REGISTER THIS TAB\n        const lastPresenceByTab =\n            JSON.parse(localStorage.getItem(\"multi_tab_service.lastPresenceByTab\")) ?? {};\n        lastPresenceByTab[tabId] = now;\n        localStorage.setItem(\n            \"multi_tab_service.lastPresenceByTab\",\n            JSON.stringify(lastPresenceByTab)\n        );\n\n        if (!localStorage.getItem(\"multi_tab_service.main\")) {\n            startElection();\n        }\n        heartbeat();\n\n        return {\n            bus,\n            /**\n             * Determine whether or not this tab is the main one.\n             * it's intentionally an async function to match the API of\n             * multiTabSharedWorkerService\n             *\n             * @returns {boolean}\n             */\n            async isOnMainTab() {\n                return _isOnMainTab;\n            },\n            /**\n             * Unregister this tab from the multi-tab service. It will no longer\n             * be able to become the main tab.\n             */\n            unregister,\n        };\n    },\n};\n", "import { browser } from \"@web/core/browser/browser\";\nimport { registry } from \"@web/core/registry\";\nimport { multiTabFallbackService } from \"@bus/multi_tab_fallback_service\";\nimport { multiTabSharedWorkerService } from \"@bus/multi_tab_shared_worker_service\";\n\nexport const multiTabService = browser.SharedWorker\n    ? multiTabSharedWorkerService\n    : multiTabFallbackService;\n\nregistry.category(\"services\").add(\"multi_tab\", multiTabService);\n", "import { browser } from \"@web/core/browser/browser\";\nimport { Deferred } from \"@web/core/utils/concurrency\";\nimport { EventBus } from \"@odoo/owl\";\n\nconst STATE = Object.freeze({\n    INIT: \"INIT\",\n    MASTER: \"MASTER\",\n    REGISTERED: \"REGISTERED\",\n    UNREGISTERED: \"UNREGISTERED\",\n});\n\nexport const multiTabSharedWorkerService = {\n    dependencies: [\"worker_service\"],\n    start(env, { worker_service: workerService }) {\n        const bus = new EventBus();\n        let responseDeferred = null;\n        let state = STATE.INIT;\n        browser.addEventListener(\"pagehide\", unregister);\n\n        function messageHandler(messageEv) {\n            const { type, data } = messageEv.data;\n            if (!type?.startsWith(\"ELECTION:\")) {\n                return;\n            }\n            switch (type) {\n                case \"ELECTION:IS_MASTER_RESPONSE\":\n                    responseDeferred?.resolve(data.answer);\n                    responseDeferred = null;\n                    break;\n                case \"ELECTION:HEARTBEAT_REQUEST\":\n                    workerService.send(\"ELECTION:HEARTBEAT\");\n                    break;\n                case \"ELECTION:ASSIGN_MASTER\":\n                    state = STATE.MASTER;\n                    bus.trigger(\"become_main_tab\");\n                    break;\n                case \"ELECTION:UNASSIGN_MASTER\":\n                    if (state !== STATE.UNREGISTERED) {\n                        state = STATE.REGISTERED;\n                    }\n                    bus.trigger(\"no_longer_main_tab\");\n                    break;\n                default:\n                    console.warn(\n                        \"multiTabSharedWorkerService received unknown message type:\",\n                        type\n                    );\n            }\n        }\n\n        async function startWorker() {\n            await workerService.ensureWorkerStarted();\n            await workerService.registerHandler(messageHandler);\n            workerService.send(\"ELECTION:REGISTER\");\n            state = STATE.REGISTERED;\n        }\n\n        function unregister() {\n            workerService.send(\"ELECTION:UNREGISTER\");\n            state = STATE.UNREGISTERED;\n        }\n\n        return {\n            bus,\n            isOnMainTab: async () => {\n                if (state === STATE.UNREGISTERED) {\n                    return false;\n                }\n                if (state === STATE.INIT) {\n                    await startWorker();\n                }\n                responseDeferred = new Deferred();\n                workerService.send(\"ELECTION:IS_MASTER?\");\n                return responseDeferred;\n            },\n            unregister,\n        };\n    },\n};\n", "import { browser } from \"@web/core/browser/browser\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { registry } from \"@web/core/registry\";\n\nexport class OutdatedPageWatcherService {\n    constructor(env, services) {\n        this.setup(env, services);\n    }\n\n    /**\n     * @param {import(\"@web/env\").OdooEnv}\n     * @param {Partial<import(\"services\").Services>} services\n     */\n    setup(env, { bus_service, multi_tab, legacy_multi_tab, notification }) {\n        this.notification = notification;\n        this.multi_tab = multi_tab;\n        this.legacy_multi_tab = legacy_multi_tab;\n        this.lastNotificationId = legacy_multi_tab.getSharedValue(\"last_notification_id\");\n        this.closeNotificationFn;\n        let wasBusAlreadyConnected;\n        bus_service.addEventListener(\n            \"BUS:WORKER_STATE_UPDATED\",\n            ({ detail: state }) => {\n                wasBusAlreadyConnected = state !== \"IDLE\";\n            },\n            { once: true }\n        );\n        bus_service.addEventListener(\n            \"BUS:DISCONNECT\",\n            () =>\n                (this.lastNotificationId = legacy_multi_tab.getSharedValue(\"last_notification_id\"))\n        );\n        bus_service.addEventListener(\"BUS:CONNECT\", async () => {\n            if (wasBusAlreadyConnected) {\n                this.checkHasMissedNotifications();\n            }\n            wasBusAlreadyConnected = true;\n        });\n        bus_service.addEventListener(\"BUS:RECONNECT\", () => this.checkHasMissedNotifications());\n        legacy_multi_tab.bus.addEventListener(\"shared_value_updated\", ({ detail: { key } }) => {\n            if (key === \"bus.has_missed_notifications\") {\n                this.showOutdatedPageNotification();\n            }\n        });\n    }\n\n    async checkHasMissedNotifications() {\n        if (!this.lastNotificationId || !(await this.multi_tab.isOnMainTab())) {\n            return;\n        }\n        const hasMissedNotifications = await rpc(\n            \"/bus/has_missed_notifications\",\n            { last_notification_id: this.lastNotificationId },\n            { silent: true }\n        );\n        if (hasMissedNotifications) {\n            this.showOutdatedPageNotification();\n            this.legacy_multi_tab.setSharedValue(\"bus.has_missed_notifications\", Date.now());\n        }\n    }\n\n    showOutdatedPageNotification() {\n        this.closeNotificationFn?.();\n        this.closeNotificationFn = this.notification.add(\n            _t(\"Save your work and refresh to get the latest updates and avoid potential issues.\"),\n            {\n                title: _t(\"The page is out of date\"),\n                type: \"warning\",\n                sticky: true,\n                buttons: [\n                    {\n                        name: _t(\"Refresh\"),\n                        primary: true,\n                        onClick: () => browser.location.reload(),\n                    },\n                ],\n            }\n        );\n    }\n}\n\nexport const outdatedPageWatcherService = {\n    dependencies: [\"bus_service\", \"multi_tab\", \"legacy_multi_tab\", \"notification\"],\n    start(env, services) {\n        return new OutdatedPageWatcherService(env, services);\n    },\n};\n\nregistry.category(\"services\").add(\"bus.outdated_page_watcher\", outdatedPageWatcherService);\n", "import { registry } from \"@web/core/registry\";\n\nexport const simpleNotificationService = {\n    dependencies: [\"bus_service\", \"notification\"],\n    start(env, { bus_service, notification: notificationService }) {\n        bus_service.subscribe(\"simple_notification\", ({ message, sticky, title, type }) => {\n            notificationService.add(message, { sticky, title, type });\n        });\n        bus_service.start();\n    },\n};\n\nregistry.category(\"services\").add(\"simple_notification\", simpleNotificationService);\n", "import { Component, useRef } from \"@odoo/owl\";\nimport { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\nimport { registry } from \"@web/core/registry\";\nimport { useService } from \"@web/core/utils/hooks\";\n\nexport class BusLogsMenuItem extends Component {\n    static components = { DropdownItem };\n    static template = \"bus.BusLogsMenuItem\";\n    static props = {};\n\n    setup() {\n        this.busLogsService = useService(\"bus.logs_service\");\n        this.downloadButton = useRef(\"downloadButton\");\n    }\n\n    onClickToggle() {\n        this.busLogsService.toggleLogging();\n    }\n\n    onClickDownload() {\n        this.env.services.bus_service.downloadLogs();\n    }\n}\n\nregistry\n    .category(\"debug\")\n    .category(\"default\")\n    .add(\"bus.download_logs\", () => ({\n        Component: BusLogsMenuItem,\n        sequence: 550,\n        section: \"tools\",\n        type: \"component\",\n    }));\n", "import { reactive } from \"@odoo/owl\";\n\nimport { registry } from \"@web/core/registry\";\n\nexport const busLogsService = {\n    dependencies: [\"bus_service\", \"legacy_multi_tab\", \"worker_service\"],\n    /**\n     * @param {import(\"@web/env\").OdooEnv}\n     * @param {Partial<import(\"services\").Services>} services\n     */\n    start(env, { bus_service, legacy_multi_tab, worker_service }) {\n        const state = reactive({\n            enabled: legacy_multi_tab.getSharedValue(\"bus_log_menu.enabled\", false),\n            toggleLogging() {\n                state.enabled = !state.enabled;\n                if (bus_service.isActive) {\n                    bus_service.setLoggingEnabled(state.enabled);\n                }\n                legacy_multi_tab.setSharedValue(\"bus_log_menu.enabled\", state.enabled);\n            },\n        });\n        legacy_multi_tab.bus.addEventListener(\"shared_value_updated\", ({ detail }) => {\n            if (detail.key === \"bus_log_menu.enabled\") {\n                state.enabled = JSON.parse(detail.newValue);\n            }\n        });\n        worker_service.connectionInitializedDeferred.then(() => {\n            bus_service.setLoggingEnabled(state.enabled);\n        });\n        odoo.busLogging = {\n            stop: () => state.enabled && state.toggleLogging(),\n            start: () => !state.enabled && state.toggleLogging(),\n            download: () => bus_service.downloadLogs(),\n        };\n        if (state.enabled) {\n            console.log(\n                \"Bus logging is enabled. To disable it, use `odoo.busLogging.stop()`. To download the logs, use `odoo.busLogging.download()`.\"\n            );\n        }\n        return state;\n    },\n};\n\nregistry.category(\"services\").add(\"bus.logs_service\", busLogsService);\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { browser } from \"@web/core/browser/browser\";\nimport { registry } from \"@web/core/registry\";\nimport { session } from \"@web/session\";\n\nexport const assetsWatchdogService = {\n    dependencies: [\"bus_service\", \"notification\"],\n\n    start(env, { bus_service, notification }) {\n        let isNotificationDisplayed = false;\n        let bundleNotifTimerID = null;\n\n        bus_service.subscribe(\"bundle_changed\", ({ server_version }) => {\n            if (server_version !== session.server_version) {\n                displayBundleChangedNotification();\n            }\n        });\n        bus_service.start();\n\n        /**\n         * Displays one notification on user's screen when assets have changed\n         */\n        function displayBundleChangedNotification() {\n            if (!isNotificationDisplayed) {\n                // Wrap the notification inside a delay.\n                // The server may be overwhelmed with recomputing assets\n                // We wait until things settle down\n                browser.clearTimeout(bundleNotifTimerID);\n                bundleNotifTimerID = browser.setTimeout(() => {\n                    notification.add(_t(\"The page appears to be out of date.\"), {\n                        title: _t(\"Refresh\"),\n                        type: \"warning\",\n                        sticky: true,\n                        buttons: [\n                            {\n                                name: _t(\"Refresh\"),\n                                primary: true,\n                                onClick: () => {\n                                    browser.location.reload();\n                                },\n                            },\n                        ],\n                        onClose: () => {\n                            isNotificationDisplayed = false;\n                        },\n                    });\n                    isNotificationDisplayed = true;\n                }, getBundleNotificationDelay());\n            }\n        }\n\n        /**\n         * Computes a random delay to avoid hammering the server\n         * when bundles change with all the users reloading\n         * at the same time\n         *\n         * @return {number} delay in milliseconds\n         */\n        function getBundleNotificationDelay() {\n            return 10000 + Math.floor(Math.random() * 50) * 1000;\n        }\n    },\n};\n\nregistry.category(\"services\").add(\"assetsWatchdog\", assetsWatchdogService);\n", "import { WORKER_STATE } from \"@bus/workers/websocket_worker\";\nimport { reactive } from \"@odoo/owl\";\nimport { browser } from \"@web/core/browser/browser\";\nimport { registry } from \"@web/core/registry\";\n\n/**\n * Detect lost connections to the bus. A connection is considered as lost if it\n * couldn't be established after a reconnect attempt.\n */\nexport class BusMonitoringService {\n    isConnectionLost = false;\n\n    constructor(env, services) {\n        const reactiveThis = reactive(this);\n        reactiveThis.setup(env, services);\n        return reactiveThis;\n    }\n\n    /**\n     * @param {import(\"@web/env\").OdooEnv} env\n     * @param {Partial<import(\"services\").Services>} services\n     */\n    setup(env, { bus_service }) {\n        bus_service.addEventListener(\"BUS:WORKER_STATE_UPDATED\", ({ detail }) =>\n            this.workerStateOnChange(detail)\n        );\n        browser.addEventListener(\"offline\", () => (this.isReconnecting = false));\n    }\n\n    /**\n     * Handle state changes for the WebSocket worker.\n     *\n     * @param {WORKER_STATE[keyof WORKER_STATE]} state\n     */\n    workerStateOnChange(state) {\n        switch (state) {\n            case WORKER_STATE.CONNECTING: {\n                this.isReconnecting = true;\n                break;\n            }\n            case WORKER_STATE.CONNECTED: {\n                this.isReconnecting = false;\n                this.isConnectionLost = false;\n                break;\n            }\n            case WORKER_STATE.DISCONNECTED: {\n                if (this.isReconnecting) {\n                    this.isConnectionLost = true;\n                    this.isReconnecting = false;\n                }\n                break;\n            }\n        }\n    }\n}\n\nexport const busMonitoringservice = {\n    dependencies: [\"bus_service\"],\n    start(env, services) {\n        return new BusMonitoringService(env, services);\n    },\n};\n\nregistry.category(\"services\").add(\"bus.monitoring_service\", busMonitoringservice);\n", "import { browser } from \"@web/core/browser/browser\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { Deferred } from \"@web/core/utils/concurrency\";\nimport { registry } from \"@web/core/registry\";\nimport { session } from \"@web/session\";\nimport { EventBus, reactive } from \"@odoo/owl\";\nimport { user } from \"@web/core/user\";\n\n// List of worker events that should not be broadcasted.\nconst INTERNAL_EVENTS = new Set([\n    \"BUS:INITIALIZED\",\n    \"BUS:OUTDATED\",\n    \"BUS:NOTIFICATION\",\n    \"BUS:PROVIDE_LOGS\",\n]);\n// Slightly delay the reconnection when coming back online as the network is not\n// ready yet and the exponential backoff would delay the reconnection by a lot.\nexport const BACK_ONLINE_RECONNECT_DELAY = 5000;\n/**\n * Communicate with a SharedWorker in order to provide a single websocket\n * connection shared across multiple tabs.\n *\n *  @emits BUS:CONNECT\n *  @emits BUS:DISCONNECT\n *  @emits BUS:RECONNECT\n *  @emits BUS:RECONNECTING\n *  @emits BUS:WORKER_STATE_UPDATED\n */\nexport const busService = {\n    dependencies: [\n        \"bus.parameters\",\n        \"localization\",\n        \"multi_tab\",\n        \"legacy_multi_tab\",\n        \"notification\",\n        \"worker_service\",\n    ],\n\n    start(\n        env,\n        {\n            multi_tab: multiTab,\n            legacy_multi_tab: legacyMultiTab,\n            notification,\n            \"bus.parameters\": params,\n            worker_service: workerService,\n        }\n    ) {\n        const bus = new EventBus();\n        const notificationBus = new EventBus();\n        const subscribeFnToWrapper = new Map();\n        let backOnlineTimeout;\n        const startedAt = luxon.DateTime.now().set({ milliseconds: 0 });\n        let connectionInitializedDeferred;\n\n        /**\n         * Handle messages received from the shared worker and fires an\n         * event according to the message type.\n         *\n         * @param {MessageEvent} messageEv\n         * @param {{type: WorkerEvent, data: any}[]}  messageEv.data\n         */\n        function handleMessage(messageEv) {\n            const { type, data } = messageEv.data;\n            switch (type) {\n                case \"BUS:PROVIDE_LOGS\": {\n                    const blob = new Blob([JSON.stringify(data, null, 2)], {\n                        type: \"application/json\",\n                    });\n                    const url = URL.createObjectURL(blob);\n                    const a = document.createElement(\"a\");\n                    a.href = url;\n                    a.download = `bus_logs_${luxon.DateTime.now().toFormat(\n                        \"yyyy-LL-dd-HH-mm-ss\"\n                    )}.json`;\n                    a.click();\n                    URL.revokeObjectURL(url);\n                    break;\n                }\n                case \"BUS:NOTIFICATION\": {\n                    const notifications = data.map(({ id, message }) => ({ id, ...message }));\n                    state.lastNotificationId = notifications.at(-1).id;\n                    legacyMultiTab.setSharedValue(\"last_notification_id\", state.lastNotificationId);\n                    for (const { id, type, payload } of notifications) {\n                        notificationBus.trigger(type, { id, payload });\n                        busService._onMessage(env, id, type, payload);\n                    }\n                    break;\n                }\n                case \"BUS:INITIALIZED\": {\n                    connectionInitializedDeferred.resolve();\n                    break;\n                }\n                case \"BUS:WORKER_STATE_UPDATED\":\n                    state.workerState = data;\n                    break;\n                case \"BUS:OUTDATED\": {\n                    multiTab.unregister();\n                    notification.add(\n                        _t(\n                            \"Save your work and refresh to get the latest updates and avoid potential issues.\"\n                        ),\n                        {\n                            title: _t(\"The page is out of date\"),\n                            type: \"warning\",\n                            sticky: true,\n                            buttons: [\n                                {\n                                    name: _t(\"Refresh\"),\n                                    primary: true,\n                                    onClick: () => {\n                                        browser.location.reload();\n                                    },\n                                },\n                            ],\n                        }\n                    );\n                    break;\n                }\n            }\n            if (!INTERNAL_EVENTS.has(type)) {\n                bus.trigger(type, data);\n            }\n        }\n\n        /**\n         * Start the \"bus_service\" workerService.\n         */\n        async function ensureWorkerStarted() {\n            if (!connectionInitializedDeferred) {\n                connectionInitializedDeferred = new Deferred();\n                let uid = Array.isArray(session.user_id) ? session.user_id[0] : user.userId;\n                if (!uid && uid !== undefined) {\n                    uid = false;\n                }\n                await workerService.ensureWorkerStarted();\n                await workerService.registerHandler(handleMessage);\n                workerService.send(\"BUS:INITIALIZE_CONNECTION\", {\n                    websocketURL: `${params.serverURL.replace(\"http\", \"ws\")}/websocket?version=${\n                        session.websocket_worker_version\n                    }`,\n                    db: session.db,\n                    debug: odoo.debug,\n                    lastNotificationId: legacyMultiTab.getSharedValue(\"last_notification_id\", 0),\n                    uid,\n                    startTs: startedAt.valueOf(),\n                });\n            }\n            await connectionInitializedDeferred;\n        }\n\n        browser.addEventListener(\"pagehide\", ({ persisted }) => {\n            if (!persisted) {\n                // Page is gonna be unloaded, disconnect this client\n                // from the worker.\n                workerService.send(\"BUS:LEAVE\");\n            }\n        });\n        browser.addEventListener(\n            \"online\",\n            () => {\n                backOnlineTimeout = browser.setTimeout(() => {\n                    if (state.isActive) {\n                        workerService.send(\"BUS:START\");\n                    }\n                }, BACK_ONLINE_RECONNECT_DELAY);\n            },\n            { capture: true }\n        );\n        browser.addEventListener(\n            \"offline\",\n            () => {\n                clearTimeout(backOnlineTimeout);\n                workerService.send(\"BUS:STOP\");\n            },\n            {\n                capture: true,\n            }\n        );\n        const state = reactive({\n            addEventListener: bus.addEventListener.bind(bus),\n            addChannel: async (channel) => {\n                await ensureWorkerStarted();\n                workerService.send(\"BUS:ADD_CHANNEL\", channel);\n                workerService.send(\"BUS:START\");\n                state.isActive = true;\n            },\n            deleteChannel: (channel) => {\n                workerService.send(\"BUS:DELETE_CHANNEL\", channel);\n            },\n            setLoggingEnabled: (isEnabled) =>\n                workerService.send(\"BUS:SET_LOGGING_ENABLED\", isEnabled),\n            downloadLogs: () => workerService.send(\"BUS:REQUEST_LOGS\"),\n            forceUpdateChannels: () => workerService.send(\"BUS:FORCE_UPDATE_CHANNELS\"),\n            trigger: bus.trigger.bind(bus),\n            removeEventListener: bus.removeEventListener.bind(bus),\n            send: (eventName, data) =>\n                workerService.send(\"BUS:SEND\", { event_name: eventName, data }),\n            start: async () => {\n                await ensureWorkerStarted();\n                workerService.send(\"BUS:START\");\n                state.isActive = true;\n            },\n            stop: () => {\n                workerService.send(\"BUS:LEAVE\");\n                state.isActive = false;\n            },\n            isActive: false,\n            /**\n             * Subscribe to a single notification type.\n             *\n             * @param {string} notificationType\n             * @param {function} callback\n             */\n            subscribe(notificationType, callback) {\n                const wrapper = ({ detail }) => {\n                    const { id, payload } = detail;\n                    callback(JSON.parse(JSON.stringify(payload)), { id });\n                };\n                subscribeFnToWrapper.set(callback, wrapper);\n                notificationBus.addEventListener(notificationType, wrapper);\n            },\n            /**\n             * Unsubscribe from a single notification type.\n             *\n             * @param {string} notificationType\n             * @param {function} callback\n             */\n            unsubscribe(notificationType, callback) {\n                notificationBus.removeEventListener(\n                    notificationType,\n                    subscribeFnToWrapper.get(callback)\n                );\n                subscribeFnToWrapper.delete(callback);\n            },\n            startedAt,\n            workerState: null,\n            /** The id of the last notification received by this tab. */\n            lastNotificationId: null,\n        });\n        return state;\n    },\n    /** Overriden to provide logs in tests. Use subscribe() in production. */\n    _onMessage(env, id, type, payload) {},\n};\nregistry.category(\"services\").add(\"bus_service\", busService);\n", "import { EventBus } from \"@odoo/owl\";\nimport { browser } from \"@web/core/browser/browser\";\nimport { registry } from \"@web/core/registry\";\n\nexport const presenceService = {\n    start(env) {\n        const LOCAL_STORAGE_PREFIX = \"presence\";\n        const bus = new EventBus();\n        let isOdooFocused = true;\n        let lastPresenceTime =\n            browser.localStorage.getItem(`${LOCAL_STORAGE_PREFIX}.lastPresence`) ||\n            luxon.DateTime.now().ts;\n\n        function onPresence() {\n            lastPresenceTime = luxon.DateTime.now().ts;\n            browser.localStorage.setItem(`${LOCAL_STORAGE_PREFIX}.lastPresence`, lastPresenceTime);\n            bus.trigger(\"presence\");\n        }\n\n        function onFocusChange(isFocused) {\n            try {\n                isFocused = parent.document.hasFocus();\n            } catch {\n                // noop\n            }\n            isOdooFocused = isFocused;\n            browser.localStorage.setItem(`${LOCAL_STORAGE_PREFIX}.focus`, isOdooFocused);\n            if (isOdooFocused) {\n                lastPresenceTime = luxon.DateTime.now().ts;\n                env.bus.trigger(\"window_focus\", isOdooFocused);\n            }\n        }\n\n        function onStorage({ key, newValue }) {\n            if (key === `${LOCAL_STORAGE_PREFIX}.focus`) {\n                isOdooFocused = JSON.parse(newValue);\n                env.bus.trigger(\"window_focus\", newValue);\n            }\n            if (key === `${LOCAL_STORAGE_PREFIX}.lastPresence`) {\n                lastPresenceTime = JSON.parse(newValue);\n                bus.trigger(\"presence\");\n            }\n        }\n        browser.addEventListener(\"storage\", onStorage);\n        browser.addEventListener(\"focus\", () => onFocusChange(true));\n        browser.addEventListener(\"blur\", () => onFocusChange(false));\n        browser.addEventListener(\"pagehide\", () => onFocusChange(false));\n        browser.addEventListener(\"click\", onPresence);\n        browser.addEventListener(\"keydown\", onPresence);\n\n        return {\n            bus,\n            getLastPresence() {\n                return lastPresenceTime;\n            },\n            isOdooFocused() {\n                return isOdooFocused;\n            },\n            getInactivityPeriod() {\n                return luxon.DateTime.now().ts - this.getLastPresence();\n            },\n        };\n    },\n};\n\nregistry.category(\"services\").add(\"presence\", presenceService);\n", "import { browser } from \"@web/core/browser/browser\";\nimport { registry } from \"@web/core/registry\";\nimport { Deferred } from \"@web/core/utils/concurrency\";\nimport { session } from \"@web/session\";\n\nexport const WORKER_STATE = Object.freeze({\n    UNINITIALIZED: \"UNINITIALIZED\",\n    INITIALIZING: \"INITIALIZING\",\n    INITIALIZED: \"INITIALIZED\",\n    FAILED: \"FAILED\",\n});\n\nexport class WorkerService {\n    constructor(env, services) {\n        this.params = services[\"bus.parameters\"];\n        this.worker = null;\n        this.isUsingSharedWorker = Boolean(browser.SharedWorker);\n        this._state = WORKER_STATE.UNINITIALIZED;\n        this.connectionInitializedDeferred = new Deferred();\n    }\n\n    startWorker() {\n        this._state = WORKER_STATE.INITIALIZING;\n        let workerURL = `${this.params.serverURL}/bus/websocket_worker_bundle?v=${session.websocket_worker_version}`;\n        if (this.params.serverURL !== window.origin) {\n            // Worker service can be loaded from a different origin than the\n            // bundle URL. The Worker expects an URL from this origin, give\n            // it a base64 URL that will then load the bundle via \"importScripts\"\n            // which allows cross origin.\n            const source = `importScripts(\"${workerURL}\");`;\n            workerURL = \"data:application/javascript;base64,\" + window.btoa(source);\n        }\n        const workerClass = this.isUsingSharedWorker ? browser.SharedWorker : browser.Worker;\n        this.worker = new workerClass(workerURL, {\n            name: this.isUsingSharedWorker ? \"odoo:bus_shared_worker\" : \"odoo:bus_worker\",\n        });\n        this.worker.onerror = (e) => this.onInitError(e);\n        this._registerHandler((ev) => {\n            if (ev.data.type === \"BASE:INITIALIZED\") {\n                this._state = WORKER_STATE.INITIALIZED;\n                this.connectionInitializedDeferred.resolve();\n            }\n        });\n        if (this.isUsingSharedWorker) {\n            this.worker.port.start();\n        }\n        this._send(\"BASE:INIT\");\n    }\n\n    async ensureWorkerStarted() {\n        if (this._state === WORKER_STATE.UNINITIALIZED) {\n            this.startWorker();\n        }\n        await this.connectionInitializedDeferred;\n    }\n\n    onInitError(e) {\n        // FIXME: SharedWorker can still fail for unknown reasons even when it is supported.\n        if (this._state === WORKER_STATE.INITIALIZING && this.isUsingSharedWorker) {\n            console.warn(\"Error while loading SharedWorker, fallback on Worker: \", e);\n            this.isUsingSharedWorker = false;\n            this.worker?.port?.close?.();\n            this.startWorker();\n        } else if (this._state === WORKER_STATE.INITIALIZING) {\n            this._state = WORKER_STATE.FAILED;\n            this.connectionInitializedDeferred.resolve();\n            console.warn(\"Worker service failed to initialize: \", e);\n        }\n    }\n\n    _registerHandler(handler) {\n        if (this.isUsingSharedWorker) {\n            this.worker.port.addEventListener(\"message\", handler);\n        } else {\n            this.worker.addEventListener(\"message\", handler);\n        }\n    }\n\n    _send(action, data) {\n        const message = { action, data };\n        if (this.isUsingSharedWorker) {\n            this.worker.port.postMessage(message);\n        } else {\n            this.worker.postMessage(message);\n        }\n    }\n\n    /**\n     * Send a message to the worker. If the worker is not yet started,\n     * ignore the message. One should call `ensureWorkerStarted` if one\n     * really needs the message to reach the worker.\n     *\n     * @param {String} action Action to be executed by the worker.\n     * @param {Object|undefined} data Data required for the action to be\n     * executed.\n     */\n    async send(action, data) {\n        if (this._state === WORKER_STATE.UNINITIALIZED) {\n            return;\n        }\n        await this.connectionInitializedDeferred;\n        if (this._state === WORKER_STATE.FAILED) {\n            console.warn(\"Worker service failed to initialize, cannot send message.\");\n        }\n        this._send(action, data);\n    }\n\n    /**\n     * Register a function to handle messages from the worker.\n     *\n     * @param {function} handler\n     */\n    async registerHandler(handler) {\n        if (this._state === WORKER_STATE.UNINITIALIZED) {\n            this.startWorker();\n        }\n        await this.connectionInitializedDeferred;\n        if (this._state === WORKER_STATE.FAILED) {\n            console.warn(\"Worker service failed to initialize, cannot register handler.\");\n        }\n        this._registerHandler(handler);\n    }\n\n    get state() {\n        return this._state;\n    }\n}\n\nexport const workerService = {\n    dependencies: [\"bus.parameters\"],\n    start(env, services) {\n        return new WorkerService(env, services);\n    },\n};\n\nregistry.category(\"services\").add(\"worker_service\", workerService);\n", "export class BaseWorker {\n    constructor(name) {\n        this.name = name;\n        this.client = null; // only for testing purposes\n    }\n\n    handleMessage(event) {\n        const { action } = event.data;\n        if (action === \"BASE:INIT\") {\n            if (this.name.includes(\"shared\")) {\n                event.target.postMessage({ type: \"BASE:INITIALIZED\" });\n            } else {\n                (this.client || globalThis).postMessage({ type: \"BASE:INITIALIZED\" });\n            }\n        }\n    }\n}\n", "/**\n * Returns a function, that, as long as it continues to be invoked, will not\n * be triggered. The function will be called after it stops being called for\n * N milliseconds. If `immediate` is passed, trigger the function on the\n * leading edge, instead of the trailing.\n *\n * Inspired by https://davidwalsh.name/javascript-debounce-function\n */\nexport function debounce(func, wait, immediate) {\n    let timeout;\n    return function () {\n        const context = this;\n        const args = arguments;\n        function later() {\n            timeout = null;\n            if (!immediate) {\n                func.apply(context, args);\n            }\n        }\n        const callNow = immediate && !timeout;\n        clearTimeout(timeout);\n        timeout = setTimeout(later, wait);\n        if (callNow) {\n            func.apply(context, args);\n        }\n    };\n}\n\n/**\n * Deferred is basically a resolvable/rejectable extension of Promise.\n */\nexport class Deferred extends Promise {\n    constructor() {\n        let resolve;\n        let reject;\n        const prom = new Promise((res, rej) => {\n            resolve = res;\n            reject = rej;\n        });\n        return Object.assign(prom, { resolve, reject });\n    }\n}\n\nexport class Logger {\n    static LOG_TTL = 24 * 60 * 60 * 1000; // 24 hours\n    static gcInterval = null;\n    static instances = [];\n    _db;\n\n    static async gcOutdatedLogs() {\n        const threshold = Date.now() - Logger.LOG_TTL;\n        for (const logger of this.instances) {\n            try {\n                await logger._ensureDatabaseAvailable();\n                await new Promise((res, rej) => {\n                    const transaction = logger._db.transaction(\"logs\", \"readwrite\");\n                    const store = transaction.objectStore(\"logs\");\n                    const req = store\n                        .index(\"timestamp\")\n                        .openCursor(IDBKeyRange.upperBound(threshold));\n                    req.onsuccess = (event) => {\n                        const cursor = event.target.result;\n                        if (cursor) {\n                            cursor.delete();\n                            cursor.continue();\n                        }\n                    };\n                    req.onerror = (e) => rej(e.target.error);\n                    transaction.oncomplete = res;\n                    transaction.onerror = (e) => rej(e.target.error);\n                });\n            } catch (error) {\n                console.error(`Failed to clear logs for logger \"${logger._name}\":`, error);\n            }\n        }\n    }\n\n    constructor(name) {\n        this._name = name;\n        Logger.instances.push(this);\n        Logger.gcOutdatedLogs();\n        clearInterval(Logger.gcInterval);\n        Logger.gcInterval = setInterval(() => Logger.gcOutdatedLogs(), Logger.LOG_TTL);\n    }\n\n    async _ensureDatabaseAvailable() {\n        if (this._db) {\n            return;\n        }\n        return new Promise((res, rej) => {\n            const request = indexedDB.open(this._name, 1);\n            request.onsuccess = (event) => {\n                this._db = event.target.result;\n                res();\n            };\n            request.onupgradeneeded = (event) => {\n                if (!event.target.result.objectStoreNames.contains(\"logs\")) {\n                    const store = event.target.result.createObjectStore(\"logs\", {\n                        autoIncrement: true,\n                    });\n                    store.createIndex(\"timestamp\", \"timestamp\", { unique: false });\n                }\n            };\n            request.onerror = rej;\n        });\n    }\n\n    async log(message) {\n        await this._ensureDatabaseAvailable();\n        const transaction = this._db.transaction(\"logs\", \"readwrite\");\n        const store = transaction.objectStore(\"logs\");\n        const addRequest = store.add({ timestamp: Date.now(), message });\n        return new Promise((res, rej) => {\n            addRequest.onsuccess = res;\n            addRequest.onerror = rej;\n        });\n    }\n\n    async getLogs() {\n        await Logger.gcOutdatedLogs();\n        await this._ensureDatabaseAvailable();\n        const transaction = this._db.transaction(\"logs\", \"readonly\");\n        const store = transaction.objectStore(\"logs\");\n        const request = store.getAll();\n        return new Promise((res, rej) => {\n            request.onsuccess = (ev) => res(ev.target.result.map(({ message }) => message));\n            request.onerror = rej;\n        });\n    }\n}\n", "import { Deferred } from \"@bus/workers/bus_worker_utils\";\n\nexport class ElectionWorker {\n    MAIN_TAB_TIMEOUT_PERIOD = 3000;\n\n    /** @type {Set<MessagePort>} */\n    candidates = new Set();\n    /** @type {Deferred|null} */\n    electionDeferred = null;\n    /** @type {number|null} */\n    heartbeatRequestInterval = null;\n    lastHeartbeat = Date.now();\n    /** @type {Deferred|null} */\n    masterReplyDeferred = null;\n    /** @type {MessagePort|null} */\n    masterTab = null;\n\n    constructor() {\n        setInterval(() => {\n            if (Date.now() - this.lastHeartbeat > this.MAIN_TAB_TIMEOUT_PERIOD) {\n                this.startElection();\n            }\n        }, this.MAIN_TAB_TIMEOUT_PERIOD);\n    }\n\n    requestHeartbeat(messagePort) {\n        if (messagePort) {\n            messagePort.postMessage({ type: \"ELECTION:HEARTBEAT_REQUEST\" });\n            return;\n        }\n        for (const candidate of this.candidates) {\n            candidate.postMessage({ type: \"ELECTION:HEARTBEAT_REQUEST\" });\n        }\n    }\n\n    async ensureMasterPresence() {\n        this.masterReplyDeferred ??= new Deferred();\n        if (this.masterTab) {\n            this.requestHeartbeat(this.masterTab);\n        } else {\n            this.startElection();\n        }\n        await this.masterReplyDeferred;\n    }\n\n    startElection() {\n        clearInterval(this.heartbeatRequestInterval);\n        this.masterTab?.postMessage({ type: \"ELECTION:UNASSIGN_MASTER\" });\n        this.masterTab = null;\n        this.electionDeferred ??= new Deferred();\n        this.requestHeartbeat();\n    }\n\n    finishElection(messagePort) {\n        this.masterTab = messagePort;\n        messagePort.postMessage({ type: \"ELECTION:ASSIGN_MASTER\" });\n        this.electionDeferred.resolve();\n        this.electionDeferred = null;\n        this.heartbeatRequestInterval = setInterval(\n            () => this.requestHeartbeat(this.masterTab),\n            this.MAIN_TAB_TIMEOUT_PERIOD / 2\n        );\n    }\n\n    async handleMessage(event) {\n        const { action } = event.data;\n        if (!action?.startsWith(\"ELECTION:\")) {\n            return;\n        }\n        switch (action) {\n            case \"ELECTION:REGISTER\":\n                this.candidates.add(event.target);\n                await this.electionDeferred;\n                if (!this.masterTab) {\n                    this.startElection();\n                }\n                break;\n            case \"ELECTION:UNREGISTER\":\n                this.candidates.delete(event.target);\n                if (this.masterTab === event.target) {\n                    this.startElection();\n                }\n                break;\n            case \"ELECTION:IS_MASTER?\":\n                await this.ensureMasterPresence();\n                event.target.postMessage({\n                    type: \"ELECTION:IS_MASTER_RESPONSE\",\n                    data: { answer: this.masterTab === event.target },\n                });\n                break;\n            case \"ELECTION:HEARTBEAT\":\n                if (this.electionDeferred) {\n                    this.finishElection(event.target);\n                }\n                if (this.masterTab === event.target) {\n                    this.lastHeartbeat = Date.now();\n                    this.masterReplyDeferred?.resolve();\n                    this.masterReplyDeferred = null;\n                }\n                break;\n            default:\n                console.warn(\"Unknown message action:\", action);\n        }\n    }\n}\n", "import { debounce, Deferred, Logger } from \"@bus/workers/bus_worker_utils\";\n\n/**\n * Type of events that can be sent from the worker to its clients.\n *\n * @typedef { 'BUS:CONNECT' | 'BUS:RECONNECT' | 'BUS:DISCONNECT' | 'BUS:RECONNECTING' | 'BUS:NOTIFICATION' | 'BUS:INITIALIZED' | 'BUS:OUTDATED'| 'BUS:WORKER_STATE_UPDATED' | 'BUS:PROVIDE_LOGS' } WorkerEvent\n */\n\n/**\n * Type of action that can be sent from the client to the worker.\n *\n * @typedef {'BUS:ADD_CHANNEL' | 'BUS:DELETE_CHANNEL' | 'BUS:FORCE_UPDATE_CHANNELS' | 'BUS:INITIALIZE_CONNECTION' | 'BUS:REQUEST_LOGS' | 'BUS:SEND' | 'BUS:SET_LOGGING_ENABLED' | 'BUS:LEAVE' | 'BUS:STOP' | 'BUS:START'} WorkerAction\n */\n\nexport const WEBSOCKET_CLOSE_CODES = Object.freeze({\n    CLEAN: 1000,\n    GOING_AWAY: 1001,\n    PROTOCOL_ERROR: 1002,\n    INCORRECT_DATA: 1003,\n    ABNORMAL_CLOSURE: 1006,\n    INCONSISTENT_DATA: 1007,\n    MESSAGE_VIOLATING_POLICY: 1008,\n    MESSAGE_TOO_BIG: 1009,\n    EXTENSION_NEGOTIATION_FAILED: 1010,\n    SERVER_ERROR: 1011,\n    RESTART: 1012,\n    TRY_LATER: 1013,\n    BAD_GATEWAY: 1014,\n    SESSION_EXPIRED: 4001,\n    KEEP_ALIVE_TIMEOUT: 4002,\n    RECONNECTING: 4003,\n    CLOSING_HANDSHAKE_ABORTED: 4004,\n});\nexport const WORKER_STATE = Object.freeze({\n    CONNECTED: \"CONNECTED\",\n    DISCONNECTED: \"DISCONNECTED\",\n    IDLE: \"IDLE\",\n    CONNECTING: \"CONNECTING\",\n});\nconst MAXIMUM_RECONNECT_DELAY = 60000;\nconst UUID = Date.now().toString(36) + Math.random().toString(36).substring(2);\nconst logger = new Logger(\"bus_websocket_worker\");\n\n/**\n * This class regroups the logic necessary in order for the\n * SharedWorker/Worker to work. Indeed, Safari and some minor browsers\n * do not support SharedWorker. In order to solve this issue, a Worker\n * is used in this case. The logic is almost the same than the one used\n * for SharedWorker and this class implements it.\n */\nexport class WebsocketWorker {\n    INITIAL_RECONNECT_DELAY = 1000;\n    RECONNECT_JITTER = 1000;\n    CONNECTION_CHECK_DELAY = 60_000;\n\n    constructor(name) {\n        this.name = name;\n        // Timestamp of start of most recent bus service sender\n        this.newestStartTs = undefined;\n        this.websocketURL = \"\";\n        this.currentUID = null;\n        this.currentDB = null;\n        this.isWaitingForNewUID = true;\n        this.channelsByClient = new Map();\n        this.connectRetryDelay = this.INITIAL_RECONNECT_DELAY;\n        this.connectTimeout = null;\n        this.debugModeByClient = new Map();\n        this.isDebug = false;\n        this.active = true;\n        this.state = WORKER_STATE.IDLE;\n        this.isReconnecting = false;\n        this.lastChannelSubscription = null;\n        this.loggingEnabled = null;\n        this.firstSubscribeDeferred = new Deferred();\n        this.lastNotificationId = 0;\n        this.messageWaitQueue = [];\n        this._forceUpdateChannels = debounce(this._forceUpdateChannels, 300);\n        this._debouncedUpdateChannels = debounce(this._updateChannels, 300);\n        this._debouncedSendToServer = debounce(this._sendToServer, 300);\n\n        this._onWebsocketClose = this._onWebsocketClose.bind(this);\n        this._onWebsocketError = this._onWebsocketError.bind(this);\n        this._onWebsocketMessage = this._onWebsocketMessage.bind(this);\n        this._onWebsocketOpen = this._onWebsocketOpen.bind(this);\n\n        globalThis.addEventListener(\"error\", ({ error }) => {\n            const params = error instanceof Error ? [error.constructor.name, error.stack] : [error];\n            this._logDebug(\"Unhandled error\", ...params);\n        });\n        globalThis.addEventListener(\"unhandledrejection\", ({ reason }) => {\n            const params =\n                reason instanceof Error ? [reason.constructor.name, reason.stack] : [reason];\n            this._logDebug(\"Unhandled rejection\", params);\n        });\n    }\n\n    //--------------------------------------------------------------------------\n    // Public\n    //--------------------------------------------------------------------------\n\n    /**\n     * Send the message to all the clients that are connected to the\n     * worker.\n     *\n     * @param {WorkerEvent} type Event to broadcast to connected\n     * clients.\n     * @param {Object} data\n     */\n    broadcast(type, data) {\n        this._logDebug(\"broadcast\", type, data);\n        for (const client of this.channelsByClient.keys()) {\n            client.postMessage({ type, data: data ? JSON.parse(JSON.stringify(data)) : undefined });\n        }\n    }\n\n    /**\n     * Register a client handled by this worker.\n     *\n     * @param {MessagePort} messagePort\n     */\n    registerClient(messagePort) {\n        messagePort.addEventListener(\"message\", (ev) => {\n            this._onClientMessage(messagePort, ev.data);\n        });\n        this.channelsByClient.set(messagePort, []);\n    }\n\n    /**\n     * Send message to the given client.\n     *\n     * @param {number} client\n     * @param {WorkerEvent} type\n     * @param {Object} data\n     */\n    sendToClient(client, type, data) {\n        if (type !== \"BUS:PROVIDE_LOGS\") {\n            this._logDebug(\"sendToClient\", type, data);\n        }\n        client.postMessage({ type, data: data ? JSON.parse(JSON.stringify(data)) : undefined });\n    }\n\n    //--------------------------------------------------------------------------\n    // PRIVATE\n    //--------------------------------------------------------------------------\n\n    /**\n     * Called when a message is posted to the worker by a client (i.e. a\n     * MessagePort connected to this worker).\n     *\n     * @param {MessagePort} client\n     * @param {Object} message\n     * @param {WorkerAction} [message.action]\n     * Action to execute.\n     * @param {Object|undefined} [message.data] Data required by the\n     * action.\n     */\n    _onClientMessage(client, { action, data }) {\n        this._logDebug(\"_onClientMessage\", action, data);\n        switch (action) {\n            case \"BUS:SEND\": {\n                if (data[\"event_name\"] === \"update_presence\") {\n                    this._debouncedSendToServer(data);\n                } else {\n                    this._sendToServer(data);\n                }\n                return;\n            }\n            case \"BUS:START\":\n                return this._start();\n            case \"BUS:STOP\":\n                return this._stop();\n            case \"BUS:LEAVE\":\n                return this._unregisterClient(client);\n            case \"BUS:ADD_CHANNEL\":\n                return this._addChannel(client, data);\n            case \"BUS:DELETE_CHANNEL\":\n                return this._deleteChannel(client, data);\n            case \"BUS:FORCE_UPDATE_CHANNELS\":\n                return this._forceUpdateChannels();\n            case \"BUS:SET_LOGGING_ENABLED\":\n                this.loggingEnabled = data;\n                break;\n            case \"BUS:REQUEST_LOGS\":\n                logger.getLogs().then((logs) => {\n                    const workerInfo = {\n                        UUID,\n                        active: this.active,\n                        channels: [\n                            ...new Set([].concat.apply([], [...this.channelsByClient.values()])),\n                        ].sort(),\n                        db: this.currentDB,\n                        is_reconnecting: this.isReconnecting,\n                        last_subscription: this.lastChannelSubscription,\n                        name: this.name,\n                        number_of_clients: this.channelsByClient.size,\n                        reconnect_delay: this.connectRetryDelay,\n                        uid: this.currentUID,\n                        websocket_url: this.websocketURL,\n                    };\n                    this.sendToClient(client, \"BUS:PROVIDE_LOGS\", { workerInfo, logs });\n                });\n                break;\n            case \"BUS:INITIALIZE_CONNECTION\":\n                return this._initializeConnection(client, data);\n        }\n    }\n\n    /**\n     * Add a channel for the given client. If this channel is not yet\n     * known, update the subscription on the server.\n     *\n     * @param {MessagePort} client\n     * @param {string} channel\n     */\n    _addChannel(client, channel) {\n        this.channelsByClient.get(client).push(channel);\n        this._debouncedUpdateChannels();\n    }\n\n    /**\n     * Remove a channel for the given client. If this channel is not\n     * used anymore, update the subscription on the server.\n     *\n     * @param {MessagePort} client\n     * @param {string} channel\n     */\n    _deleteChannel(client, channel) {\n        const clientChannels = this.channelsByClient.get(client);\n        if (!clientChannels) {\n            return;\n        }\n        const channelIndex = clientChannels.indexOf(channel);\n        if (channelIndex !== -1) {\n            clientChannels.splice(channelIndex, 1);\n            this._debouncedUpdateChannels();\n        }\n    }\n\n    /**\n     * Update the channels on the server side even if the channels on\n     * the client side are the same than the last time we subscribed.\n     */\n    _forceUpdateChannels() {\n        this._updateChannels({ force: true });\n    }\n\n    /**\n     * Remove the given client from this worker client list as well as\n     * its channels. If some of its channels are not used anymore,\n     * update the subscription on the server.\n     *\n     * @param {MessagePort} client\n     */\n    _unregisterClient(client) {\n        this.channelsByClient.delete(client);\n        this.debugModeByClient.delete(client);\n        this.isDebug = [...this.debugModeByClient.values()].some(Boolean);\n        this._debouncedUpdateChannels();\n    }\n\n    /**\n     * Initialize a client connection to this worker.\n     *\n     * @param {Object} param0\n     * @param {string} [param0.db] Database name.\n     * @param {String} [param0.debug] Current debugging mode for the\n     * given client.\n     * @param {Number} [param0.lastNotificationId] Last notification id\n     * known by the client.\n     * @param {String} [param0.websocketURL] URL of the websocket endpoint.\n     * @param {Number|false|undefined} [param0.uid] Current user id\n     *     - Number: user is logged whether on the frontend/backend.\n     *     - false: user is not logged.\n     *     - undefined: not available (e.g. livechat support page)\n     * @param {Number} param0.startTs Timestamp of start of bus service sender.\n     */\n    _initializeConnection(client, { db, debug, lastNotificationId, uid, websocketURL, startTs }) {\n        if (this.newestStartTs && this.newestStartTs > startTs) {\n            this.debugModeByClient.set(client, debug);\n            this.isDebug = [...this.debugModeByClient.values()].some(Boolean);\n            this.sendToClient(client, \"BUS:WORKER_STATE_UPDATED\", this.state);\n            this.sendToClient(client, \"BUS:INITIALIZED\");\n            return;\n        }\n        this.newestStartTs = startTs;\n        this.websocketURL = websocketURL;\n        this.lastNotificationId = lastNotificationId;\n        this.debugModeByClient.set(client, debug);\n        this.isDebug = [...this.debugModeByClient.values()].some(Boolean);\n        const isCurrentUserKnown = uid !== undefined;\n        if (this.isWaitingForNewUID && isCurrentUserKnown) {\n            this.isWaitingForNewUID = false;\n            this.currentUID = uid;\n        }\n        this.currentDB ||= db;\n        if ((this.currentUID !== uid && isCurrentUserKnown) || (db && this.currentDB !== db)) {\n            this.currentUID = uid;\n            this.currentDB = db || this.currentDB;\n            if (this.websocket) {\n                this.websocket.close(WEBSOCKET_CLOSE_CODES.CLEAN);\n            }\n            this.channelsByClient.forEach((_, key) => this.channelsByClient.set(key, []));\n        }\n        this.sendToClient(client, \"BUS:WORKER_STATE_UPDATED\", this.state);\n        this.sendToClient(client, \"BUS:INITIALIZED\");\n        if (!this.active) {\n            this.sendToClient(client, \"BUS:OUTDATED\");\n        }\n    }\n\n    /**\n     * Determine whether or not the websocket associated to this worker\n     * is connected.\n     *\n     * @returns {boolean}\n     */\n    _isWebsocketConnected() {\n        return this.websocket && this.websocket.readyState === 1;\n    }\n\n    /**\n     * Determine whether or not the websocket associated to this worker\n     * is connecting.\n     *\n     * @returns {boolean}\n     */\n    _isWebsocketConnecting() {\n        return this.websocket && this.websocket.readyState === 0;\n    }\n\n    /**\n     * Determine whether or not the websocket associated to this worker\n     * is in the closing state.\n     *\n     * @returns {boolean}\n     */\n    _isWebsocketClosing() {\n        return this.websocket && this.websocket.readyState === 2;\n    }\n\n    /**\n     * Triggered when a connection is closed. If closure was not clean ,\n     * try to reconnect after indicating to the clients that the\n     * connection was closed.\n     *\n     * @param {CloseEvent} ev\n     * @param {number} code  close code indicating why the connection\n     * was closed.\n     * @param {string} reason reason indicating why the connection was\n     * closed.\n     */\n    _onWebsocketClose({ code, reason }) {\n        clearInterval(this._connectionCheckInterval);\n        this._logDebug(\"_onWebsocketClose\", code, reason);\n        this._updateState(WORKER_STATE.DISCONNECTED);\n        this.lastChannelSubscription = null;\n        this.firstSubscribeDeferred = new Deferred();\n        if (this.isReconnecting) {\n            // Connection was not established but the close event was\n            // triggered anyway. Let the onWebsocketError method handle\n            // this case.\n            return;\n        }\n        this.broadcast(\"BUS:DISCONNECT\", { code, reason });\n        if (code === WEBSOCKET_CLOSE_CODES.CLEAN) {\n            if (reason === \"OUTDATED_VERSION\") {\n                console.warn(\"Worker deactivated due to an outdated version.\");\n                this.active = false;\n                this.broadcast(\"BUS:OUTDATED\");\n            }\n            // WebSocket was closed on purpose, do not try to reconnect.\n            return;\n        }\n        // WebSocket was not closed cleanly, let's try to reconnect.\n        this.broadcast(\"BUS:RECONNECTING\", { closeCode: code });\n        this.isReconnecting = true;\n        if (\n            [\n                WEBSOCKET_CLOSE_CODES.KEEP_ALIVE_TIMEOUT,\n                WEBSOCKET_CLOSE_CODES.CLOSING_HANDSHAKE_ABORTED,\n            ].includes(code)\n        ) {\n            // Don't wait to reconnect: keep-alive shouldn't be noticed, and the\n            // closing handshake was aborted because the client explicitly tried\n            // to connect while the socket was stuck in the closing state.\n            this.connectRetryDelay = 0;\n        }\n        if (code === WEBSOCKET_CLOSE_CODES.SESSION_EXPIRED) {\n            this.isWaitingForNewUID = true;\n        }\n        this._retryConnectionWithDelay();\n    }\n\n    /**\n     * Triggered when a connection failed or failed to established.\n     */\n    _onWebsocketError() {\n        this._logDebug(\"_onWebsocketError\");\n        this._retryConnectionWithDelay();\n    }\n\n    /**\n     * Handle data received from the bus.\n     *\n     * @param {MessageEvent} messageEv\n     */\n    _onWebsocketMessage(messageEv) {\n        this._restartConnectionCheckInterval();\n        const notifications = JSON.parse(messageEv.data);\n        this._logDebug(\"_onWebsocketMessage\", notifications);\n        this.lastNotificationId = notifications[notifications.length - 1].id;\n        this.broadcast(\"BUS:NOTIFICATION\", notifications);\n    }\n\n    async _logDebug(title, ...args) {\n        if (this.loggingEnabled) {\n            try {\n                await logger.log({\n                    dt: new Date().toISOString(),\n                    event: title,\n                    args,\n                    worker: UUID,\n                });\n            } catch (e) {\n                console.error(e);\n            }\n        }\n    }\n\n    /**\n     * Triggered on websocket open. Send message that were waiting for\n     * the connection to open.\n     */\n    _onWebsocketOpen() {\n        this._logDebug(\"_onWebsocketOpen\");\n        this._updateState(WORKER_STATE.CONNECTED);\n        this.broadcast(this.isReconnecting ? \"BUS:RECONNECT\" : \"BUS:CONNECT\");\n        this._debouncedUpdateChannels();\n        this.connectRetryDelay = this.INITIAL_RECONNECT_DELAY;\n        this.connectTimeout = null;\n        this.isReconnecting = false;\n        this.firstSubscribeDeferred.then(() => {\n            if (!this.websocket) {\n                return;\n            }\n            this.messageWaitQueue.forEach((msg) => this.websocket.send(msg));\n            this.messageWaitQueue = [];\n        });\n        this._restartConnectionCheckInterval();\n    }\n\n    /**\n     * Sends a custom application-level message to perform a connection check\n     * on the WebSocket.\n     *\n     * Browsers rely on the OS's TCP mechanism, which can take minutes or\n     * hours to detect a dead connection. Sending data triggers an immediate\n     * I/O operation, quickly revealing any network-level failure. This must be\n     * implemented at the application level because the browser WebSocket API\n     * does not expose the built-in ping/pong mechanism.\n     */\n    _restartConnectionCheckInterval() {\n        clearInterval(this._connectionCheckInterval);\n        this._connectionCheckInterval = setInterval(() => {\n            if (this._isWebsocketConnected()) {\n                this.websocket.send(new Uint8Array([0x00]));\n                this._logDebug(\"connection_checked\");\n            }\n        }, this.CONNECTION_CHECK_DELAY);\n    }\n\n    /**\n     * Try to reconnect to the server, an exponential back off is\n     * applied to the reconnect attempts.\n     */\n    _retryConnectionWithDelay() {\n        this.connectRetryDelay =\n            Math.min(this.connectRetryDelay * 1.5, MAXIMUM_RECONNECT_DELAY) +\n            this.RECONNECT_JITTER * Math.random();\n        this._logDebug(\"_retryConnectionWithDelay\", this.connectRetryDelay);\n        this.connectTimeout = setTimeout(this._start.bind(this), this.connectRetryDelay);\n    }\n\n    /**\n     * Send a message to the server through the websocket connection.\n     * If the websocket is not open, enqueue the message and send it\n     * upon the next reconnection.\n     *\n     * @param {{event_name: string, data: any }} message Message to send to the server.\n     */\n    _sendToServer(message) {\n        this._logDebug(\"_sendToServer\", message);\n        const payload = JSON.stringify(message);\n        if (!this._isWebsocketConnected()) {\n            if (message[\"event_name\"] === \"subscribe\") {\n                this.messageWaitQueue = this.messageWaitQueue.filter(\n                    (msg) => JSON.parse(msg).event_name !== \"subscribe\"\n                );\n                this.messageWaitQueue.unshift(payload);\n            } else {\n                this.messageWaitQueue.push(payload);\n            }\n        } else {\n            if (message[\"event_name\"] === \"subscribe\") {\n                this.websocket.send(payload);\n            } else {\n                this.firstSubscribeDeferred.then(() => this.websocket.send(payload));\n            }\n            this._restartConnectionCheckInterval();\n        }\n    }\n\n    _removeWebsocketListeners() {\n        this.websocket?.removeEventListener(\"open\", this._onWebsocketOpen);\n        this.websocket?.removeEventListener(\"message\", this._onWebsocketMessage);\n        this.websocket?.removeEventListener(\"error\", this._onWebsocketError);\n        this.websocket?.removeEventListener(\"close\", this._onWebsocketClose);\n    }\n\n    /**\n     * Start the worker by opening a websocket connection.\n     */\n    _start() {\n        this._logDebug(\"_start\");\n        if (!this.active || this._isWebsocketConnected() || this._isWebsocketConnecting()) {\n            return;\n        }\n        this._removeWebsocketListeners();\n        if (this._isWebsocketClosing()) {\n            // The close event didn\u2019t trigger. Trigger manually to maintain\n            // correct state and lifecycle handling.\n            this._onWebsocketClose(\n                new CloseEvent(\"close\", { code: WEBSOCKET_CLOSE_CODES.CLOSING_HANDSHAKE_ABORTED })\n            );\n            this.websocket = null;\n            return;\n        }\n        this._updateState(WORKER_STATE.CONNECTING);\n        this.websocket = new WebSocket(this.websocketURL);\n        this.websocket.addEventListener(\"open\", this._onWebsocketOpen);\n        this.websocket.addEventListener(\"error\", this._onWebsocketError);\n        this.websocket.addEventListener(\"message\", this._onWebsocketMessage);\n        this.websocket.addEventListener(\"close\", this._onWebsocketClose);\n    }\n\n    /**\n     * Stop the worker.\n     */\n    _stop() {\n        this._logDebug(\"_stop\");\n        clearTimeout(this.connectTimeout);\n        this.connectRetryDelay = this.INITIAL_RECONNECT_DELAY;\n        this.isReconnecting = false;\n        this.lastChannelSubscription = null;\n        const shouldBroadcastClose =\n            this.websocket && this.websocket.readyState !== WebSocket.CLOSED;\n        this.websocket?.close();\n        this._removeWebsocketListeners();\n        this.websocket = null;\n        if (shouldBroadcastClose) {\n            this.broadcast(\"BUS:DISCONNECT\", { code: WEBSOCKET_CLOSE_CODES.CLEAN });\n        }\n    }\n\n    /**\n     * Update the channel subscription on the server. Ignore if the channels\n     * did not change since the last subscription.\n     *\n     * @param {boolean} force Whether or not we should update the subscription\n     * event if the channels haven't change since last subscription.\n     */\n    _updateChannels({ force = false } = {}) {\n        const allTabsChannels = [\n            ...new Set([].concat.apply([], [...this.channelsByClient.values()])),\n        ].sort();\n        const allTabsChannelsString = JSON.stringify(allTabsChannels);\n        const shouldUpdateChannelSubscription =\n            allTabsChannelsString !== this.lastChannelSubscription;\n        if (force || shouldUpdateChannelSubscription) {\n            this.lastChannelSubscription = allTabsChannelsString;\n            this._sendToServer({\n                event_name: \"subscribe\",\n                data: { channels: allTabsChannels, last: this.lastNotificationId },\n            });\n            this.firstSubscribeDeferred.resolve();\n        }\n    }\n    /**\n     * Update the worker state and broadcast the new state to its clients.\n     *\n     * @param {WORKER_STATE[keyof WORKER_STATE]} newState\n     */\n    _updateState(newState) {\n        this.state = newState;\n        this.broadcast(\"BUS:WORKER_STATE_UPDATED\", newState);\n    }\n}\n", "import { Component, useEffect, useRef } from \"@odoo/owl\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { browser } from \"@web/core/browser/browser\";\nimport { usePosition } from \"@web/core/position/position_hook\";\n\n/**\n * @typedef {import(\"./tour_pointer_state\").TourPointerState} TourPointerState\n *\n * @typedef TourPointerProps\n * @property {TourPointerState} pointerState\n * @property {boolean} bounce\n */\n\n/** @extends {Component<TourPointerProps, any>} */\nexport class TourPointer extends Component {\n    static props = {\n        pointerState: {\n            type: Object,\n            shape: {\n                anchor: { type: HTMLElement, optional: true },\n                content: { type: String, optional: true },\n                isOpen: { type: Boolean, optional: true },\n                isVisible: { type: Boolean, optional: true },\n                isZone: { type: Boolean, optional: true },\n                onClick: { type: [Function, { value: null }], optional: true },\n                onMouseEnter: { type: [Function, { value: null }], optional: true },\n                onMouseLeave: { type: [Function, { value: null }], optional: true },\n                position: {\n                    type: [\n                        { value: \"left\" },\n                        { value: \"right\" },\n                        { value: \"top\" },\n                        { value: \"bottom\" },\n                    ],\n                    optional: true,\n                },\n                rev: { type: Number, optional: true },\n            },\n        },\n        bounce: { type: Boolean, optional: true },\n    };\n\n    static defaultProps = {\n        bounce: true,\n    };\n\n    static template = \"web_tour.TourPointer\";\n    static width = 28; // in pixels\n    static height = 28; // in pixels\n\n    setup() {\n        this.orm = useService(\"orm\");\n        const positionOptions = {\n            margin: 6,\n            onPositioned: (pointer, position) => {\n                const popperRect = pointer.getBoundingClientRect();\n                const { top, left, direction } = position;\n                if (direction === \"top\") {\n                    // position from the bottom instead of the top as it is needed\n                    // to ensure the expand animation is properly done\n                    pointer.style.bottom = `${window.innerHeight - top - popperRect.height}px`;\n                    pointer.style.removeProperty(\"top\");\n                } else if (direction === \"left\") {\n                    // position from the right instead of the left as it is needed\n                    // to ensure the expand animation is properly done\n                    pointer.style.right = `${window.innerWidth - left - popperRect.width}px`;\n                    pointer.style.removeProperty(\"left\");\n                }\n            },\n        };\n        Object.defineProperty(positionOptions, \"position\", {\n            get: () => this.position,\n            set: () => {}, // do not let the position hook change the position\n            enumerable: true,\n        });\n        const position = usePosition(\n            \"pointer\",\n            () => this.props.pointerState.anchor,\n            positionOptions\n        );\n        const rootRef = useRef(\"pointer\");\n        const zoneRef = useRef(\"zone\");\n        /** @type {DOMREct | null} */\n        let dimensions = null;\n        let lastMeasuredContent = null;\n        let lastOpenState = this.isOpen;\n        let lastAnchor;\n        let [anchorX, anchorY] = [0, 0];\n        useEffect(() => {\n            const { el: pointer } = rootRef;\n            const { el: zone } = zoneRef;\n            if (pointer) {\n                const hasContentChanged = lastMeasuredContent !== this.content;\n                const hasOpenStateChanged = lastOpenState !== this.isOpen;\n                lastOpenState = this.isOpen;\n\n                // Check is the pointed element is a zone\n                if (this.props.pointerState.isZone) {\n                    const { anchor } = this.props.pointerState;\n                    let offsetLeft = 0;\n                    let offsetTop = 0;\n                    if (document !== anchor.ownerDocument) {\n                        const iframe = [...document.querySelectorAll(\"iframe\")].filter(\n                            (e) => e.contentDocument === anchor.ownerDocument\n                        )[0];\n                        offsetLeft = iframe.getBoundingClientRect().left;\n                        offsetTop = iframe.getBoundingClientRect().top;\n                    }\n                    const { left, top, width, height } = anchor.getBoundingClientRect();\n                    zone.style.minWidth = width + \"px\";\n                    zone.style.minHeight = height + \"px\";\n                    zone.style.left = left + offsetLeft + \"px\";\n                    zone.style.top = top + offsetTop + \"px\";\n                }\n\n                // Content changed: we must re-measure the dimensions of the text.\n                if (hasContentChanged) {\n                    lastMeasuredContent = this.content;\n                    pointer.style.removeProperty(\"width\");\n                    pointer.style.removeProperty(\"height\");\n                    dimensions = pointer.getBoundingClientRect();\n                }\n\n                // If the content or the \"is open\" state changed: we must apply\n                // new width and height properties\n                if (hasContentChanged || hasOpenStateChanged) {\n                    const [width, height] = this.isOpen\n                        ? [dimensions.width, dimensions.height]\n                        : [this.constructor.width, this.constructor.height];\n                    if (this.isOpen) {\n                        pointer.style.removeProperty(\"transition\");\n                    } else {\n                        // No transition if switching from open to closed\n                        pointer.style.setProperty(\"transition\", \"none\");\n                    }\n                    pointer.style.setProperty(\"width\", `${width}px`);\n                    pointer.style.setProperty(\"height\", `${height}px`);\n                }\n\n                if (!this.isOpen) {\n                    const { anchor } = this.props.pointerState;\n                    if (anchor === lastAnchor) {\n                        const { x, y, width } = anchor.getBoundingClientRect();\n                        const [lastAnchorX, lastAnchorY] = [anchorX, anchorY];\n                        [anchorX, anchorY] = [x, y];\n                        // Let's just say that the anchor is static if it moved less than 1px.\n                        const delta = Math.sqrt(\n                            Math.pow(x - lastAnchorX, 2) + Math.pow(y - lastAnchorY, 2)\n                        );\n                        if (delta < 1) {\n                            position.lock();\n                            return;\n                        }\n                        const wouldOverflow = window.innerWidth - x - width / 2 < dimensions?.width;\n                        pointer.classList.toggle(\"o_expand_left\", wouldOverflow);\n                    }\n                    lastAnchor = anchor;\n                    pointer.style.bottom = \"\";\n                    pointer.style.right = \"\";\n                    position.unlock();\n                }\n            } else {\n                lastMeasuredContent = null;\n                lastOpenState = false;\n                lastAnchor = null;\n                dimensions = null;\n            }\n        });\n    }\n\n    get content() {\n        return this.props.pointerState.content || \"\";\n    }\n\n    get isOpen() {\n        return this.props.pointerState.isOpen && this.content;\n    }\n\n    get position() {\n        return this.props.pointerState.position || \"top\";\n    }\n\n    async onStopClicked() {\n        await this.orm.call(\"res.users\", \"switch_tour_enabled\", [false]);\n        browser.location.reload();\n    }\n}\n", "import { reactive } from \"@odoo/owl\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { TourPointer } from \"@web_tour/js/tour_pointer/tour_pointer\";\nimport { getScrollParent } from \"@web_tour/js/utils/tour_utils\";\n\n/**\n * @typedef {import(\"@web/core/position/position_hook\").Direction} Direction\n *\n * @typedef {\"in\" | \"out-below\" | \"out-above\" | \"unknown\"} IntersectionPosition\n *\n * @typedef {ReturnType<createPointerState>[\"methods\"]} TourPointerMethods\n *\n * @typedef TourPointerState\n * @property {HTMLElement} [anchor]\n * @property {string} [content]\n * @property {boolean} [isOpen]\n * @property {() => {}} [onClick]\n * @property {() => {}} [onMouseEnter]\n * @property {() => {}} [onMouseLeave]\n * @property {boolean} isVisible\n * @property {boolean} isZone\n * @property {Direction} position\n * @property {number} rev\n *\n * @typedef {import(\"../tour_service\").TourStep} TourStep\n */\n\nclass Intersection {\n    constructor() {\n        /** @type {Element | null} */\n        this.currentTarget = null;\n        this.rootBounds = null;\n        /** @type {IntersectionPosition} */\n        this._targetPosition = \"unknown\";\n        this._observer = new IntersectionObserver((observations) =>\n            this._handleObservations(observations)\n        );\n    }\n\n    /** @type {IntersectionObserverCallback} */\n    _handleObservations(observations) {\n        if (observations.length < 1) {\n            return;\n        }\n        const observation = observations[observations.length - 1];\n        this.rootBounds = observation.rootBounds;\n        if (this.rootBounds && this.currentTarget) {\n            if (observation.isIntersecting) {\n                this._targetPosition = \"in\";\n            } else {\n                const scrollParentElement =\n                    getScrollParent(this.currentTarget) || document.documentElement;\n                const targetBounds = this.currentTarget.getBoundingClientRect();\n                if (targetBounds.bottom > scrollParentElement.clientHeight) {\n                    this._targetPosition = \"out-below\";\n                } else if (targetBounds.top < 0) {\n                    this._targetPosition = \"out-above\";\n                } else if (targetBounds.left < 0) {\n                    this._targetPosition = \"out-left\";\n                } else if (targetBounds.right > scrollParentElement.clientWidth) {\n                    this._targetPosition = \"out-right\";\n                }\n            }\n        } else {\n            this._targetPosition = \"unknown\";\n        }\n    }\n\n    get targetPosition() {\n        if (!this.rootBounds) {\n            return this.currentTarget ? \"in\" : \"unknown\";\n        } else {\n            return this._targetPosition;\n        }\n    }\n\n    /**\n     * @param {Element} newTarget\n     */\n    setTarget(newTarget) {\n        if (this.currentTarget !== newTarget) {\n            if (this.currentTarget) {\n                this._observer.unobserve(this.currentTarget);\n            }\n            if (newTarget) {\n                this._observer.observe(newTarget);\n            }\n            this.currentTarget = newTarget;\n        }\n    }\n\n    stop() {\n        this._observer.disconnect();\n    }\n}\n\nexport function createPointerState() {\n    /**\n     * @param {Partial<TourPointerState>} newState\n     */\n    const setState = (newState) => {\n        Object.assign(state, newState);\n    };\n\n    /**\n     * @param {TourStep} step\n     * @param {HTMLElement} [anchor]\n     * @param {boolean} [isZone] will border de zone. e.g.: a dropzone\n     */\n    const pointTo = (anchor, step, isZone) => {\n        intersection.setTarget(anchor);\n        if (anchor) {\n            let { tooltipPosition, content } = step;\n            switch (intersection.targetPosition) {\n                case \"unknown\": {\n                    // Do nothing for unknown target position.\n                    break;\n                }\n                case \"in\": {\n                    if (document.body.contains(floatingAnchor)) {\n                        floatingAnchor.remove();\n                    }\n                    setState({\n                        anchor,\n                        content,\n                        isZone,\n                        onClick: null,\n                        position: tooltipPosition,\n                        isVisible: true,\n                    });\n                    break;\n                }\n                default: {\n                    const onClick = () => {\n                        anchor.scrollIntoView({ behavior: \"smooth\", block: \"nearest\" });\n                        hide();\n                    };\n\n                    const scrollParent = getScrollParent(anchor);\n                    if (!scrollParent) {\n                        setState({\n                            anchor,\n                            content,\n                            isZone,\n                            onClick: null,\n                            position: tooltipPosition,\n                            isVisible: true,\n                        });\n                        return;\n                    }\n                    let { x, y, width, height } = scrollParent.getBoundingClientRect();\n\n                    // If the scrolling element is within an iframe the offsets\n                    // must be computed taking into account the iframe.\n                    const iframeEl = scrollParent.ownerDocument.defaultView.frameElement;\n                    if (iframeEl) {\n                        const iframeOffset = iframeEl.getBoundingClientRect();\n                        x += iframeOffset.x;\n                        y += iframeOffset.y;\n                    }\n                    if (intersection.targetPosition === \"out-below\") {\n                        tooltipPosition = \"top\";\n                        content = _t(\"Scroll down to reach the next step.\");\n                        floatingAnchor.style.top = `${y + height - TourPointer.height}px`;\n                        floatingAnchor.style.left = `${x + width / 2}px`;\n                    } else if (intersection.targetPosition === \"out-above\") {\n                        tooltipPosition = \"bottom\";\n                        content = _t(\"Scroll up to reach the next step.\");\n                        floatingAnchor.style.top = `${y + TourPointer.height}px`;\n                        floatingAnchor.style.left = `${x + width / 2}px`;\n                    }\n                    if (intersection.targetPosition === \"out-left\") {\n                        tooltipPosition = \"right\";\n                        content = _t(\"Scroll left to reach the next step.\");\n                        floatingAnchor.style.top = `${y + height / 2}px`;\n                        floatingAnchor.style.left = `${x + TourPointer.width}px`;\n                    } else if (intersection.targetPosition === \"out-right\") {\n                        tooltipPosition = \"left\";\n                        content = _t(\"Scroll right to reach the next step.\");\n                        floatingAnchor.style.top = `${y + height / 2}px`;\n                        floatingAnchor.style.left = `${x + width - TourPointer.width}px`;\n                    }\n                    if (!document.contains(floatingAnchor)) {\n                        document.body.appendChild(floatingAnchor);\n                    }\n                    setState({\n                        anchor: floatingAnchor,\n                        content,\n                        onClick,\n                        position: tooltipPosition,\n                        isZone,\n                        isVisible: true,\n                    });\n                }\n            }\n        } else {\n            hide();\n        }\n    };\n\n    function hide() {\n        setState({ content: \"\", isVisible: false, isOpen: false });\n    }\n\n    function showContent(isOpen) {\n        setState({ isOpen });\n    }\n\n    function destroy() {\n        intersection.stop();\n        if (document.body.contains(floatingAnchor)) {\n            floatingAnchor.remove();\n        }\n    }\n\n    /** @type {TourPointerState} */\n    const state = reactive({});\n    const intersection = new Intersection();\n    const floatingAnchor = document.createElement(\"div\");\n    floatingAnchor.className = \"position-fixed\";\n\n    return { state, setState, showContent, pointTo, hide, destroy };\n}\n", "/**\n * Calls the given `func` then returns/resolves to `true`\n * if it will result to unloading of the page.\n * @param {(...args: any[]) => void} func\n * @param  {any[]} args\n * @returns {boolean | Promise<boolean>}\n */\nexport function callWithUnloadCheck(func, ...args) {\n    let willUnload = false;\n    const beforeunload = () => (willUnload = true);\n    window.addEventListener(\"beforeunload\", beforeunload);\n    const result = func(...args);\n    if (result instanceof Promise) {\n        return result.then(() => {\n            window.removeEventListener(\"beforeunload\", beforeunload);\n            return willUnload;\n        });\n    } else {\n        window.removeEventListener(\"beforeunload\", beforeunload);\n        return willUnload;\n    }\n}\n\nfunction formatValue(key, value, maxLength = 200) {\n    if (!value) {\n        return \"(empty)\";\n    }\n    return value.length > maxLength ? value.slice(0, maxLength) + \"...\" : value;\n}\n\nfunction serializeNode(node) {\n    if (node.nodeType === Node.TEXT_NODE) {\n        return `\"${node.nodeValue.trim()}\"`;\n    }\n    return node.outerHTML ? formatValue(\"node\", node.outerHTML, 500) : \"[Unknown Node]\";\n}\n\nexport function serializeChanges(snapshot, current) {\n    const changes = {\n        node: serializeNode(current),\n    };\n    function pushChanges(key, obj) {\n        changes[key] = changes[key] || [];\n        changes[key].push(obj);\n    }\n\n    if (snapshot.textContent !== current.textContent) {\n        pushChanges(\"modifiedText\", { before: snapshot.textContent, after: current.textContent });\n    }\n\n    const oldChildren = [...snapshot.childNodes].filter((node) => node.nodeType !== Node.TEXT_NODE);\n    const newChildren = [...current.childNodes].filter((node) => node.nodeType !== Node.TEXT_NODE);\n    oldChildren.forEach((oldNode, index) => {\n        if (!newChildren[index] || !oldNode.isEqualNode(newChildren[index])) {\n            pushChanges(\"removedNodes\", { oldNode: serializeNode(oldNode) });\n        }\n    });\n    newChildren.forEach((newNode, index) => {\n        if (!oldChildren[index] || !newNode.isEqualNode(oldChildren[index])) {\n            pushChanges(\"addedNodes\", { newNode: serializeNode(newNode) });\n        }\n    });\n\n    const oldAttrNames = new Set([...snapshot.attributes].map((attr) => attr.name));\n    const newAttrNames = new Set([...current.attributes].map((attr) => attr.name));\n    new Set([...oldAttrNames, ...newAttrNames]).forEach((attributeName) => {\n        const oldValue = snapshot.getAttribute(attributeName);\n        const newValue = current.getAttribute(attributeName);\n        const before = oldValue !== newValue || !newAttrNames.has(attributeName) ? oldValue : null;\n        const after = oldValue !== newValue || !oldAttrNames.has(attributeName) ? newValue : null;\n        if (before || after) {\n            pushChanges(\"modifiedAttributes\", { attributeName, before, after });\n        }\n    });\n    return changes;\n}\n\nexport function serializeMutation(mutation) {\n    const { type, attributeName } = mutation;\n    if (type === \"attributes\" && attributeName) {\n        return `attribute: ${attributeName}`;\n    } else {\n        return type;\n    }\n}\n\n/**\n * @param {HTMLElement} element\n * @returns {HTMLElement | null}\n */\nexport function getScrollParent(element) {\n    if (!element) {\n        return null;\n    }\n    // We cannot only rely on the fact that the element\u2019s scrollHeight is\n    // greater than its clientHeight. This might not be the case when a step\n    // starts, and the scrollbar could appear later. For example, when clicking\n    // on a \"building block\" in the \"building block previews modal\" during a\n    // tour (in website edit mode). When the modal opens, not all \"building\n    // blocks\" are loaded yet, and the scrollbar is not present initially.\n    const overflowY = window.getComputedStyle(element).overflowY;\n    const isScrollable =\n        overflowY === \"auto\" ||\n        overflowY === \"scroll\" ||\n        (overflowY === \"visible\" && element === element.ownerDocument.scrollingElement);\n    if (isScrollable) {\n        return element;\n    } else {\n        return getScrollParent(element.parentNode);\n    }\n}\n", "import { browser } from \"@web/core/browser/browser\";\n\nconst CURRENT_TOUR_LOCAL_STORAGE = \"current_tour\";\nconst CURRENT_TOUR_CONFIG_LOCAL_STORAGE = \"current_tour.config\";\nconst CURRENT_TOUR_INDEX_LOCAL_STORAGE = \"current_tour.index\";\nconst CURRENT_TOUR_ON_ERROR_LOCAL_STORAGE = \"current_tour.on_error\";\n\n/**\n * Wrapper around localStorage for persistence of the running tours.\n * Useful for resuming running tours when the page refreshed.\n */\nexport const tourState = {\n    getCurrentTour() {\n        return browser.localStorage.getItem(CURRENT_TOUR_LOCAL_STORAGE);\n    },\n    setCurrentTour(tourName) {\n        browser.localStorage.setItem(CURRENT_TOUR_LOCAL_STORAGE, tourName);\n    },\n    getCurrentIndex() {\n        const index = browser.localStorage.getItem(CURRENT_TOUR_INDEX_LOCAL_STORAGE, \"0\");\n        return parseInt(index, 10);\n    },\n    setCurrentIndex(index) {\n        browser.localStorage.setItem(CURRENT_TOUR_INDEX_LOCAL_STORAGE, index.toString());\n    },\n    getCurrentConfig() {\n        const config = browser.localStorage.getItem(CURRENT_TOUR_CONFIG_LOCAL_STORAGE, \"{}\");\n        return JSON.parse(config);\n    },\n    setCurrentConfig(config) {\n        config = JSON.stringify(config);\n        browser.localStorage.setItem(CURRENT_TOUR_CONFIG_LOCAL_STORAGE, config);\n    },\n    getCurrentTourOnError() {\n        return browser.localStorage.getItem(CURRENT_TOUR_ON_ERROR_LOCAL_STORAGE);\n    },\n    setCurrentTourOnError() {\n        browser.localStorage.setItem(CURRENT_TOUR_ON_ERROR_LOCAL_STORAGE, \"1\");\n    },\n    clear() {\n        browser.localStorage.removeItem(CURRENT_TOUR_ON_ERROR_LOCAL_STORAGE);\n        browser.localStorage.removeItem(CURRENT_TOUR_CONFIG_LOCAL_STORAGE);\n        browser.localStorage.removeItem(CURRENT_TOUR_INDEX_LOCAL_STORAGE);\n        browser.localStorage.removeItem(CURRENT_TOUR_LOCAL_STORAGE);\n    },\n};\n", "import { Component, markup, whenReady, validate } from \"@odoo/owl\";\nimport { browser } from \"@web/core/browser/browser\";\nimport { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\nimport { registry } from \"@web/core/registry\";\nimport { session } from \"@web/session\";\nimport { loadBundle } from \"@web/core/assets\";\nimport { createPointerState } from \"@web_tour/js/tour_pointer/tour_pointer_state\";\nimport { tourState } from \"@web_tour/js/tour_state\";\nimport { callWithUnloadCheck } from \"@web_tour/js/utils/tour_utils\";\nimport {\n    tourRecorderState,\n    TOUR_RECORDER_ACTIVE_LOCAL_STORAGE_KEY,\n} from \"@web_tour/js/tour_recorder/tour_recorder_state\";\nimport { redirect } from \"@web/core/utils/urls\";\n\nclass OnboardingItem extends Component {\n    static components = { DropdownItem };\n    static template = \"web_tour.OnboardingItem\";\n    static props = {\n        toursEnabled: { type: Boolean },\n        toggleItem: { type: Function },\n    };\n    setup() {}\n}\n\nconst StepSchema = {\n    id: { type: [String], optional: true },\n    content: { type: [String, Object], optional: true }, //allow object(_t && markup)\n    debugHelp: { type: String, optional: true },\n    isActive: { type: Array, element: String, optional: true },\n    run: { type: [String, Function, Boolean], optional: true },\n    timeout: {\n        optional: true,\n        validate(value) {\n            return value >= 0 && value <= 60000;\n        },\n    },\n    tooltipPosition: {\n        optional: true,\n        validate(value) {\n            return [\"top\", \"bottom\", \"left\", \"right\"].includes(value);\n        },\n    },\n    trigger: { type: String },\n    expectUnloadPage: { type: Boolean, optional: true },\n    //ONLY IN DEBUG MODE\n    pause: { type: Boolean, optional: true },\n    break: { type: Boolean, optional: true },\n};\n\nconst TourSchema = {\n    name: { type: String, optional: true },\n    steps: Function,\n    url: { type: String, optional: true },\n    wait_for: { type: [Function, Object], optional: true },\n};\n\nregistry.category(\"web_tour.tours\").addValidation(TourSchema);\nconst debugMenuRegistry = registry.category(\"debug\").category(\"default\");\n\nexport const tourService = {\n    // localization dependency to make sure translations used by tours are loaded\n    dependencies: [\"orm\", \"effect\", \"overlay\", \"localization\"],\n    start: async (env, { orm, effect, overlay }) => {\n        await whenReady();\n        let toursEnabled = session?.tour_enabled;\n        const tourRegistry = registry.category(\"web_tour.tours\");\n        const pointer = createPointerState();\n        pointer.stop = () => {};\n\n        debugMenuRegistry.add(\"onboardingItem\", () => ({\n            type: \"component\",\n            Component: OnboardingItem,\n            props: {\n                toursEnabled: toursEnabled || false,\n                toggleItem: async () => {\n                    tourState.clear();\n                    toursEnabled = await orm.call(\"res.users\", \"switch_tour_enabled\", [\n                        !toursEnabled,\n                    ]);\n                    browser.location.reload();\n                },\n            },\n            sequence: 500,\n            section: \"testing\",\n        }));\n\n        function getTourFromRegistry(tourName) {\n            if (!tourRegistry.contains(tourName)) {\n                return;\n            }\n            const tour = tourRegistry.get(tourName);\n            return {\n                ...tour,\n                steps: tour.steps(),\n                name: tourName,\n                wait_for: tour.wait_for || Promise.resolve(),\n            };\n        }\n\n        async function getTourFromDB(tourName) {\n            const tour = await orm.call(\"web_tour.tour\", \"get_tour_json_by_name\", [tourName]);\n            if (!tour) {\n                throw new Error(`Tour '${tourName}' is not found in the database.`);\n            }\n\n            if (!tour.steps.length && tourRegistry.contains(tour.name)) {\n                tour.steps = tourRegistry.get(tour.name).steps();\n            }\n\n            return tour;\n        }\n\n        function validateStep(step) {\n            try {\n                validate(step, StepSchema);\n            } catch (error) {\n                console.error(\n                    `Error in schema for TourStep ${JSON.stringify(step, null, 4)}\\n${\n                        error.message\n                    }`\n                );\n            }\n        }\n\n        async function startTour(tourName, options = {}) {\n            pointer.stop();\n            const tourFromRegistry = getTourFromRegistry(tourName);\n\n            if (!tourFromRegistry && !options.fromDB) {\n                // Sometime tours are not loaded depending on the modules.\n                // For example, point_of_sale do not load all tours assets.\n                return;\n            }\n\n            const tour = options.fromDB ? { name: tourName, url: options.url } : tourFromRegistry;\n            if (!session.is_public && !toursEnabled && options.mode === \"manual\") {\n                toursEnabled = await orm.call(\"res.users\", \"switch_tour_enabled\", [!toursEnabled]);\n            }\n\n            let tourConfig = {\n                delayToCheckUndeterminisms: 0,\n                stepDelay: 0,\n                keepWatchBrowser: false,\n                mode: \"auto\",\n                showPointerDuration: 0,\n                debug: false,\n                redirect: true,\n            };\n\n            tourConfig = Object.assign(tourConfig, options);\n            tourState.setCurrentConfig(tourConfig);\n            tourState.setCurrentTour(tour.name);\n            tourState.setCurrentIndex(0);\n\n            const willUnload = callWithUnloadCheck(() => {\n                if (tour.url && tourConfig.startUrl != tour.url && tourConfig.redirect) {\n                    redirect(tour.url);\n                }\n            });\n            if (!willUnload) {\n                await resumeTour();\n            }\n        }\n\n        async function resumeTour() {\n            const tourName = tourState.getCurrentTour();\n            const tourConfig = tourState.getCurrentConfig();\n\n            let tour = getTourFromRegistry(tourName);\n            if (tourConfig.fromDB) {\n                tour = await getTourFromDB(tourName);\n            }\n            if (!tour) {\n                return;\n            }\n\n            tour.steps.forEach((step) => validateStep(step));\n\n            if (tourConfig.mode === \"auto\") {\n                if (!odoo.loader.modules.get(\"@web_tour/js/tour_automatic/tour_automatic\")) {\n                    await loadBundle(\"web_tour.automatic\", { css: false });\n                }\n                const { TourAutomatic } = odoo.loader.modules.get(\n                    \"@web_tour/js/tour_automatic/tour_automatic\"\n                );\n                new TourAutomatic(tour).start();\n            } else {\n                await loadBundle(\"web_tour.interactive\");\n                const { TourPointer } = odoo.loader.modules.get(\n                    \"@web_tour/js/tour_pointer/tour_pointer\"\n                );\n                pointer.stop = overlay.add(\n                    TourPointer,\n                    {\n                        pointerState: pointer.state,\n                        bounce: !(tourConfig.mode === \"auto\" && tourConfig.keepWatchBrowser),\n                    },\n                    {\n                        sequence: 1100, // sequence based on bootstrap z-index values.\n                    }\n                );\n                const { TourInteractive } = odoo.loader.modules.get(\n                    \"@web_tour/js/tour_interactive/tour_interactive\"\n                );\n                new TourInteractive(tour).start(env, pointer, async () => {\n                    pointer.stop();\n                    tourState.clear();\n                    browser.console.log(\"tour succeeded\");\n                    let message = tourConfig.rainbowManMessage || tour.rainbowManMessage;\n                    if (message) {\n                        message = window.DOMPurify.sanitize(tourConfig.rainbowManMessage);\n                        effect.add({\n                            type: \"rainbow_man\",\n                            message: markup(message),\n                        });\n                    }\n\n                    const nextTour = await orm.call(\"web_tour.tour\", \"consume\", [tour.name]);\n                    if (nextTour) {\n                        startTour(nextTour.name, {\n                            mode: \"manual\",\n                            redirect: false,\n                            rainbowManMessage: nextTour.rainbowManMessage,\n                        });\n                    }\n                });\n            }\n        }\n\n        async function tourRecorder() {\n            await loadBundle(\"web_tour.recorder\");\n            const { TourRecorder } = odoo.loader.modules.get(\n                \"@web_tour/js/tour_recorder/tour_recorder\"\n            );\n            const remove = overlay.add(\n                TourRecorder,\n                {\n                    onClose: () => {\n                        remove();\n                        browser.localStorage.removeItem(TOUR_RECORDER_ACTIVE_LOCAL_STORAGE_KEY);\n                        tourRecorderState.clear();\n                    },\n                },\n                { sequence: 99999 }\n            );\n        }\n\n        async function startTourRecorder() {\n            if (!browser.localStorage.getItem(TOUR_RECORDER_ACTIVE_LOCAL_STORAGE_KEY)) {\n                await tourRecorder();\n            }\n            browser.localStorage.setItem(TOUR_RECORDER_ACTIVE_LOCAL_STORAGE_KEY, \"1\");\n        }\n\n        if (!window.frameElement) {\n            const paramsTourName = new URLSearchParams(browser.location.search).get(\"tour\");\n            if (paramsTourName) {\n                startTour(paramsTourName, { mode: \"manual\", fromDB: true });\n            }\n\n            if (tourState.getCurrentTour()) {\n                if (tourState.getCurrentConfig().mode === \"auto\" || toursEnabled) {\n                    resumeTour();\n                } else {\n                    tourState.clear();\n                }\n            } else if (session.current_tour) {\n                startTour(session.current_tour.name, {\n                    mode: \"manual\",\n                    redirect: false,\n                    rainbowManMessage: session.current_tour.rainbowManMessage,\n                });\n            }\n\n            if (\n                browser.localStorage.getItem(TOUR_RECORDER_ACTIVE_LOCAL_STORAGE_KEY) &&\n                !session.is_public\n            ) {\n                await tourRecorder();\n            }\n        }\n\n        odoo.startTour = startTour;\n        odoo.isTourReady = (tourName) => getTourFromRegistry(tourName).wait_for.then(() => true);\n\n        return {\n            startTour,\n            startTourRecorder,\n        };\n    },\n};\n\nregistry.category(\"services\").add(\"tour_service\", tourService);\n", "import { browser } from \"@web/core/browser/browser\";\n\nconst CURRENT_TOUR_RECORDER_LOCAL_STORAGE = \"current_tour_recorder\";\nconst CURRENT_TOUR_RECORDER_RECORD_LOCAL_STORAGE = \"current_tour_recorder.record\";\nexport const TOUR_RECORDER_ACTIVE_LOCAL_STORAGE_KEY = \"tour_recorder_active\";\n\n/**\n * Wrapper around localStorage for persistence of the current recording.\n * Useful for resuming recording when the page refreshed.\n */\nexport const tourRecorderState = {\n    isRecording() {\n        return browser.localStorage.getItem(CURRENT_TOUR_RECORDER_RECORD_LOCAL_STORAGE) || \"0\";\n    },\n    setIsRecording(isRecording) {\n        browser.localStorage.setItem(\n            CURRENT_TOUR_RECORDER_RECORD_LOCAL_STORAGE,\n            isRecording ? \"1\" : \"0\"\n        );\n    },\n    setCurrentTourRecorder(tour) {\n        tour = JSON.stringify(tour);\n        browser.localStorage.setItem(CURRENT_TOUR_RECORDER_LOCAL_STORAGE, tour);\n    },\n    getCurrentTourRecorder() {\n        const tour = browser.localStorage.getItem(CURRENT_TOUR_RECORDER_LOCAL_STORAGE) || \"[]\";\n        return JSON.parse(tour);\n    },\n    clear() {\n        browser.localStorage.removeItem(CURRENT_TOUR_RECORDER_LOCAL_STORAGE);\n        browser.localStorage.removeItem(CURRENT_TOUR_RECORDER_RECORD_LOCAL_STORAGE);\n    },\n};\n", "import { _t } from \"@web/core/l10n/translation\";\n\nexport const stepUtils = {\n    _getHelpMessage(functionName, ...args) {\n        return `Generated by function tour utils ${functionName}(${args.join(\", \")})`;\n    },\n\n    addDebugHelp(helpMessage, step) {\n        if (typeof step.debugHelp === \"string\") {\n            step.debugHelp = step.debugHelp + \"\\n\" + helpMessage;\n        } else {\n            step.debugHelp = helpMessage;\n        }\n        return step;\n    },\n\n    editSelectMenuInput(trigger, value) {\n        return [\n            {\n                content: \"Make sure a SelectMenu has been opened\",\n                trigger: `.o_select_menu_menu`,\n            },\n            {\n                trigger,\n                async run({ queryFirst }) {\n                    const input = queryFirst(trigger);\n                    input.focus();\n                    input.value = value;\n                    input.dispatchEvent(new Event(\"input\", { bubbles: true }));\n                    input.dispatchEvent(new Event(\"change\", { bubbles: true }));\n                },\n            },\n        ];\n    },\n\n    showAppsMenuItem() {\n        return {\n            isActive: [\"auto\", \"community\", \"desktop\"],\n            trigger: \".o_navbar_apps_menu button:enabled\",\n            tooltipPosition: \"bottom\",\n            run: \"click\",\n        };\n    },\n\n    toggleHomeMenu() {\n        return [\n            {\n                isActive: [\".o_main_navbar .o_menu_toggle\"],\n                trigger: \".o_main_navbar .o_menu_toggle\",\n                content: _t(\"Click the top left corner to navigate across apps.\"),\n                tooltipPosition: \"bottom\",\n                run: \"click\",\n            },\n            {\n                isActive: [\"mobile\"],\n                trigger: \".o_sidebar_topbar a.btn-primary\",\n                tooltipPosition: \"right\",\n                run: \"click\",\n            },\n        ];\n    },\n\n    autoExpandMoreButtons(isActiveMobile = false) {\n        const isActive = [\"auto\"];\n        if (isActiveMobile) {\n            isActive.push(\"mobile\");\n        }\n        return {\n            isActive,\n            content: `autoExpandMoreButtons`,\n            trigger: \".o-form-buttonbox\",\n            async run({ queryFirst, click }) {\n                const more = queryFirst(\".o-form-buttonbox .o_button_more\");\n                if (more) {\n                    await click(more);\n                }\n            },\n        };\n    },\n\n    goToAppSteps(dataMenuXmlid, description) {\n        return [\n            this.showAppsMenuItem(),\n            {\n                isActive: [\"community\"],\n                trigger: `.o_app[data-menu-xmlid=\"${dataMenuXmlid}\"]`,\n                content: description,\n                tooltipPosition: \"right\",\n                run: \"click\",\n            },\n            {\n                isActive: [\"enterprise\"],\n                trigger: `.o_app[data-menu-xmlid=\"${dataMenuXmlid}\"]`,\n                content: description,\n                tooltipPosition: \"bottom\",\n                run: \"click\",\n            },\n        ].map((step) =>\n            this.addDebugHelp(this._getHelpMessage(\"goToApp\", dataMenuXmlid, description), step)\n        );\n    },\n\n    statusbarButtonsSteps(innerTextButton, description, trigger) {\n        const steps = [];\n        if (trigger) {\n            steps.push({\n                isActive: [\"auto\", \"mobile\"],\n                trigger,\n            });\n        }\n        steps.push(\n            {\n                isActive: [\"auto\", \"mobile\"],\n                trigger: \".o_statusbar_buttons\",\n                async run({ queryFirst, click }) {\n                    const buttonOutSideDropdownMenu = queryFirst(\n                        `.o_statusbar_buttons button:enabled:contains('${innerTextButton}')`\n                    );\n                    const node = queryFirst(\".o_statusbar_buttons button:has(.oi-ellipsis-v)\");\n                    if (!buttonOutSideDropdownMenu && node) {\n                        await click(node);\n                    }\n                },\n            },\n            {\n                trigger: `.o_statusbar_buttons button:enabled:contains('${innerTextButton}'), .dropdown-item button:enabled:contains('${innerTextButton}')`,\n                content: description,\n                tooltipPosition: \"bottom\",\n                run: \"click\",\n            }\n        );\n        return steps.map((step) =>\n            this.addDebugHelp(\n                this._getHelpMessage(\"statusbarButtonsSteps\", innerTextButton, description),\n                step\n            )\n        );\n    },\n\n    mobileKanbanSearchMany2X(modalTitle, valueSearched) {\n        return [\n            {\n                isActive: [\"mobile\"],\n                trigger: `.modal:not(.o_inactive_modal) .o_control_panel_navigation .btn .fa-search`,\n                tooltipPosition: \"bottom\",\n                run: \"click\",\n            },\n            {\n                isActive: [\"mobile\"],\n                trigger: \".o_searchview_input\",\n                tooltipPosition: \"bottom\",\n                run: `edit ${valueSearched}`,\n            },\n            {\n                isActive: [\"mobile\"],\n                trigger: \".dropdown-menu.o_searchview_autocomplete\",\n            },\n            {\n                isActive: [\"mobile\"],\n                trigger: \".o_searchview_input\",\n                tooltipPosition: \"bottom\",\n                run: \"press Enter\",\n            },\n            {\n                isActive: [\"mobile\"],\n                trigger: `.o_kanban_record:contains('${valueSearched}')`,\n                tooltipPosition: \"bottom\",\n                run: \"click\",\n            },\n        ].map((step) =>\n            this.addDebugHelp(\n                this._getHelpMessage(\"mobileKanbanSearchMany2X\", modalTitle, valueSearched),\n                step\n            )\n        );\n    },\n    /**\n     * Utility steps to save a form and wait for the save to complete\n     */\n    saveForm() {\n        return [\n            {\n                isActive: [\"auto\"],\n                content: \"save form\",\n                trigger: \".o_form_button_save:enabled\",\n                run: \"click\",\n            },\n            {\n                content: \"wait for save completion\",\n                trigger: \".o_form_readonly, .o_form_saved\",\n            },\n        ];\n    },\n    /**\n     * Utility steps to cancel a form creation or edition.\n     *\n     * Supports creation/edition from either a form or a list view (so checks\n     * for both states).\n     */\n    discardForm() {\n        return [\n            {\n                isActive: [\"auto\"],\n                content: \"discard the form\",\n                trigger: \".o_form_button_cancel\",\n                run: \"click\",\n            },\n            {\n                content: \"wait for cancellation to complete\",\n                trigger:\n                    \".o_view_controller.o_list_view, .o_form_view > div > main > .o_form_readonly, .o_form_view > div > main > .o_form_saved\",\n            },\n        ];\n    },\n\n    waitIframeIsReady() {\n        return {\n            content: \"Wait until the iframe is ready\",\n            trigger: `:iframe body[is-ready=true]`,\n        };\n    },\n\n    goToUrl(url) {\n        return {\n            isActive: [\"auto\"],\n            content: `Navigate to ${url}`,\n            trigger: \"body\",\n            run: `goToUrl ${url}`,\n            expectUnloadPage: true,\n        };\n    },\n};\n", "import { registry } from \"@web/core/registry\";\nimport { listView } from \"@web/views/list/list_view\";\n\nregistry.category(\"views\").add(\"tour_list\", {\n    ...listView,\n    buttonTemplate: \"web_tour.TourListController.Buttons\",\n});\n", "import { charField, CharField } from \"@web/views/fields/char/char_field\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { registry } from \"@web/core/registry\";\n\nexport class TourStartWidget extends CharField {\n    static template = \"web_tour.TourStartWidget\";\n    static props = {\n        ...CharField.props,\n        link: { type: Boolean, optional: true },\n    };\n\n    setup() {\n        this.tour = useService(\"tour_service\");\n    }\n\n    get tourData() {\n        return this.props.record.data;\n    }\n\n    _onStartTour() {\n        this.tour.startTour(this.tourData.name, {\n            mode: \"manual\",\n            url: this.tourData.url,\n            fromDB: this.tourData.custom,\n            rainbowManMessage: this.tourData.rainbow_man_message,\n        });\n    }\n\n    _onTestTour() {\n        this.tour.startTour(this.tourData.name, {\n            mode: \"auto\",\n            url: this.tourData.url,\n            fromDB: this.tourData.custom,\n            showPointerDuration: 250,\n            rainbowManMessage: this.tourData.rainbow_man_message,\n        });\n    }\n}\n\nexport const tourStartWidgetField = {\n    ...charField,\n    component: TourStartWidget,\n    extractProps: ({ options }) => ({\n        link: options.link,\n    }),\n};\n\nregistry.category(\"fields\").add(\"tour_start_widget\", tourStartWidgetField);\n", "import { Component, xml } from \"@odoo/owl\";\n\nconst NO_OP = () => {};\n\nexport class Switch extends Component {\n    static props = {\n        value: { type: Boolean, optional: true },\n        extraClasses: String,\n        disabled: { type: Boolean, optional: true },\n        label: { type: String, optional: true },\n        description: { type: String, optional: true },\n        onChange: { Function, optional: true },\n    };\n    static defaultProps = {\n        onChange: NO_OP,\n    };\n    static template = xml`\n    <label t-att-class=\"'o_switch' + extraClasses\">\n        <input type=\"checkbox\"\n                name=\"switch\"\n                class=\"visually-hidden\"\n                t-att-checked=\"props.value\"\n                t-att-disabled=\"props.disabled\"\n                t-on-change=\"(ev) => props.onChange(ev.target.checked)\"\n                t-on-keyup=\"onKeyup\"/>\n        <span/>\n        <span t-if=\"props.label\" t-esc=\"props.label\" class=\"ms-2\"/>\n        <span t-if=\"props.description\" class=\"text-muted ms-2\" t-esc=\"props.description\"/>\n    </label>\n    `;\n\n    setup() {\n        this.extraClasses = this.props.extraClasses ? ` ${this.props.extraClasses}` : \"\";\n    }\n    /**\n     * @param {KeyboardEvent} ev\n     */\n    onKeyup(ev) {\n        // \"Enter\" is not a default on checkboxes, but as the switch doesn't\n        // look like a checkbox anymore, we support it.\n        if (ev.key === \"Enter\") {\n            ev.currentTarget.checked = !ev.currentTarget.checked;\n        }\n    }\n}\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { Attachment, FileSelector, IMAGE_MIMETYPES } from \"./file_selector\";\nimport { renderToElement } from \"@web/core/utils/render\";\n\nexport class DocumentAttachment extends Attachment {\n    static template = \"html_editor.DocumentAttachment\";\n}\n\nexport class DocumentSelector extends FileSelector {\n    static mediaSpecificClasses = [\"o_image\"];\n    static mediaSpecificStyles = [];\n    static mediaExtraClasses = [];\n    static tagNames = [\"A\"];\n    static attachmentsListTemplate = \"html_editor.DocumentsListTemplate\";\n    static components = {\n        ...FileSelector.components,\n        DocumentAttachment,\n    };\n\n    setup() {\n        super.setup();\n\n        this.uploadText = _t(\"Upload a document\");\n        this.urlPlaceholder = \"https://www.odoo.com/mydocument\";\n        this.addText = _t(\"Add URL\");\n        this.searchPlaceholder = _t(\"Search a document\");\n        this.allLoadedText = _t(\"All documents have been loaded\");\n    }\n\n    get attachmentsDomain() {\n        const domain = super.attachmentsDomain;\n        domain.push([\"mimetype\", \"not in\", IMAGE_MIMETYPES]);\n        // The assets should not be part of the documents.\n        // All assets begin with '/web/assets/', see _get_asset_template_url().\n        domain.unshift(\"&\", \"|\", [\"url\", \"=\", null], \"!\", [\"url\", \"=like\", \"/web/assets/%\"]);\n        return domain;\n    }\n\n    async onClickDocument(document) {\n        this.selectAttachment(document);\n        await this.props.save();\n    }\n\n    async fetchAttachments(...args) {\n        const attachments = await super.fetchAttachments(...args);\n\n        if (this.selectInitialMedia()) {\n            for (const attachment of attachments) {\n                if (\n                    `/web/content/${attachment.id}` ===\n                    this.props.media.getAttribute(\"href\").replace(/[?].*/, \"\")\n                ) {\n                    this.selectAttachment(attachment);\n                }\n            }\n        }\n        return attachments;\n    }\n\n    /**\n     * Utility method used by the MediaDialog component.\n     */\n    static async createElements(selectedMedia, { orm }) {\n        return Promise.all(\n            selectedMedia.map(async (attachment) => {\n                let url = `/web/content/${encodeURIComponent(\n                    attachment.id\n                )}?unique=${encodeURIComponent(attachment.checksum)}&download=true`;\n                if (!attachment.public) {\n                    let accessToken = attachment.access_token;\n                    if (!accessToken) {\n                        [accessToken] = await orm.call(\"ir.attachment\", \"generate_access_token\", [\n                            attachment.id,\n                        ]);\n                    }\n                    url += `&access_token=${encodeURIComponent(accessToken)}`;\n                }\n                return this.renderFileElement(attachment, url);\n            })\n        );\n    }\n\n    static renderFileElement(attachment, downloadUrl) {\n        return renderStaticFileBox(\n            attachment.name,\n            attachment.mimetype,\n            downloadUrl,\n            attachment.id\n        );\n    }\n}\n\nexport function renderStaticFileBox(filename, mimetype, downloadUrl, id) {\n    const rootSpan = document.createElement(\"span\");\n    rootSpan.classList.add(\"o_file_box\", \"o-contenteditable-false\");\n    rootSpan.contentEditable = false;\n    rootSpan.dataset.attachmentId = id;\n    const bannerElement = renderToElement(\"html_editor.StaticFileBox\", {\n        fileModel: { filename, mimetype, downloadUrl },\n    });\n    rootSpan.append(bannerElement);\n    return rootSpan;\n}\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { ConfirmationDialog } from \"@web/core/confirmation_dialog/confirmation_dialog\";\nimport { Dialog } from \"@web/core/dialog/dialog\";\nimport { KeepLast } from \"@web/core/utils/concurrency\";\nimport { useDebounced } from \"@web/core/utils/timing\";\nimport { SearchMedia } from \"./search_media\";\n\nimport { Component, xml, useState, useRef, onWillStart, useEffect } from \"@odoo/owl\";\n\nexport const IMAGE_MIMETYPES = [\n    \"image/jpg\",\n    \"image/jpeg\",\n    \"image/jpe\",\n    \"image/png\",\n    \"image/svg+xml\",\n    \"image/gif\",\n    \"image/webp\",\n];\nexport const IMAGE_EXTENSIONS = [\".jpg\", \".jpeg\", \".jpe\", \".png\", \".svg\", \".gif\", \".webp\"];\n\nclass RemoveButton extends Component {\n    static template = xml`<i class=\"fa fa-trash o_existing_attachment_remove position-absolute top-0 end-0 p-2 bg-white-25 cursor-pointer opacity-0 opacity-100-hover z-1 transition-base\" t-att-title=\"removeTitle\" role=\"img\" t-att-aria-label=\"removeTitle\" t-on-click=\"this.remove\"/>`;\n    static props = [\"model?\", \"remove\"];\n    setup() {\n        this.removeTitle = _t(\"This file is attached to the current record.\");\n        if (this.props.model === \"ir.ui.view\") {\n            this.removeTitle = _t(\"This file is a public view attachment.\");\n        }\n    }\n\n    remove(ev) {\n        ev.stopPropagation();\n        this.props.remove();\n    }\n}\n\nexport class AttachmentError extends Component {\n    static components = { Dialog };\n    static template = \"html_editor.AttachmentError\";\n    static props = [\"views\", \"close\"];\n    setup() {\n        this.title = _t(\"Alert\");\n    }\n}\n\nexport class Attachment extends Component {\n    static template = \"\";\n    static components = {\n        RemoveButton,\n    };\n    static props = [\"*\"];\n    setup() {\n        this.dialogs = useService(\"dialog\");\n    }\n\n    remove() {\n        this.dialogs.add(ConfirmationDialog, {\n            body: _t(\"Are you sure you want to delete this file?\"),\n            confirm: async () => {\n                const prevented = await rpc(\"/html_editor/attachment/remove\", {\n                    ids: [this.props.id],\n                });\n                if (!Object.keys(prevented).length) {\n                    this.props.onRemoved(this.props.id);\n                } else {\n                    this.dialogs.add(AttachmentError, {\n                        views: prevented[this.props.id],\n                    });\n                }\n            },\n        });\n    }\n}\n\nexport class FileSelectorControlPanel extends Component {\n    static template = \"html_editor.FileSelectorControlPanel\";\n    static components = {\n        SearchMedia,\n    };\n    static props = {\n        uploadUrl: Function,\n        validateUrl: Function,\n        uploadFiles: Function,\n        changeSearchService: Function,\n        changeShowOptimized: Function,\n        search: Function,\n        accept: { type: String, optional: true },\n        addText: { type: String, optional: true },\n        multiSelect: { type: true, optional: true },\n        needle: { type: String, optional: true },\n        searchPlaceholder: { type: String, optional: true },\n        searchService: { type: String, optional: true },\n        showOptimized: { type: Boolean, optional: true },\n        showOptimizedOption: { type: String, optional: true },\n        uploadText: { type: String, optional: true },\n        urlPlaceholder: { type: String, optional: true },\n        urlWarningTitle: { type: String, optional: true },\n        useMediaLibrary: { type: Boolean, optional: true },\n        useUnsplash: { type: Boolean, optional: true },\n    };\n    setup() {\n        this.state = useState({\n            showUrlInput: false,\n            urlInput: \"\",\n            isValidUrl: false,\n            isValidFileFormat: false,\n            isValidatingUrl: false,\n        });\n        this.debouncedValidateUrl = useDebounced(this.props.validateUrl, 500);\n\n        this.fileInput = useRef(\"file-input\");\n        const urlInputRef = useRef(\"urlInput\");\n\n        useEffect(\n            () => {\n                if (this.state.showUrlInput) {\n                    urlInputRef.el.focus();\n                }\n            },\n            () => [this.state.showUrlInput]\n        );\n    }\n\n    get showSearchServiceSelect() {\n        return this.props.searchService && this.props.needle;\n    }\n\n    get enableUrlUploadClick() {\n        return (\n            !this.state.showUrlInput ||\n            (this.state.urlInput && this.state.isValidUrl && this.state.isValidFileFormat)\n        );\n    }\n\n    async onUrlUploadClick() {\n        if (!this.state.showUrlInput) {\n            this.state.showUrlInput = true;\n        } else {\n            await this.props.uploadUrl(this.state.urlInput);\n            this.state.urlInput = \"\";\n        }\n    }\n\n    async onUrlInput(ev) {\n        this.state.isValidatingUrl = true;\n        const { isValidUrl, isValidFileFormat } = await this.debouncedValidateUrl(ev.target.value);\n        this.state.isValidFileFormat = isValidFileFormat;\n        this.state.isValidUrl = isValidUrl;\n        this.state.isValidatingUrl = false;\n    }\n\n    onClickUpload() {\n        this.fileInput.el.click();\n    }\n\n    async onChangeFileInput() {\n        const inputFiles = this.fileInput.el.files;\n        if (!inputFiles.length) {\n            return;\n        }\n        await this.props.uploadFiles(inputFiles);\n        const fileInputEl = this.fileInput.el;\n        if (fileInputEl) {\n            fileInputEl.value = \"\";\n        }\n    }\n}\n\nexport class FileSelector extends Component {\n    static template = \"html_editor.FileSelector\";\n    static components = {\n        FileSelectorControlPanel,\n    };\n    static props = [\"*\"];\n\n    setup() {\n        this.notificationService = useService(\"notification\");\n        this.orm = useService(\"orm\");\n        this.uploadService = useService(\"upload\");\n        this.keepLast = new KeepLast();\n\n        this.loadMoreButtonRef = useRef(\"load-more-button\");\n        this.existingAttachmentsRef = useRef(\"existing-attachments\");\n\n        this.state = useState({\n            attachments: [],\n            canScrollAttachments: false,\n            canLoadMoreAttachments: false,\n            isFetchingAttachments: false,\n            needle: \"\",\n        });\n\n        this.NUMBER_OF_ATTACHMENTS_TO_DISPLAY = 30;\n\n        onWillStart(async () => {\n            this.state.attachments = await this.fetchAttachments(\n                this.NUMBER_OF_ATTACHMENTS_TO_DISPLAY,\n                0\n            );\n        });\n\n        this.debouncedOnScroll = useDebounced(this.updateScroll, 15);\n        this.debouncedScrollUpdate = useDebounced(this.updateScroll, 500);\n\n        useEffect(\n            (modalEl) => {\n                if (modalEl) {\n                    modalEl.addEventListener(\"scroll\", this.debouncedOnScroll);\n                    return () => {\n                        modalEl.removeEventListener(\"scroll\", this.debouncedOnScroll);\n                    };\n                }\n            },\n            () => [this.props.modalRef.el?.querySelector(\"main.modal-body\")]\n        );\n\n        useEffect(\n            () => {\n                // Updating the scroll button each time the attachments change.\n                // Hiding the \"Load more\" button to prevent it from flickering.\n                this.loadMoreButtonRef.el.classList.add(\"o_hide_loading\");\n                this.state.canScrollAttachments = false;\n                this.debouncedScrollUpdate();\n            },\n            () => [this.allAttachments.length]\n        );\n    }\n\n    get canLoadMore() {\n        return this.state.canLoadMoreAttachments;\n    }\n\n    get hasContent() {\n        return this.state.attachments.length;\n    }\n\n    get isFetching() {\n        return this.state.isFetchingAttachments;\n    }\n\n    get selectedAttachmentIds() {\n        return this.props.selectedMedia[this.props.id]\n            .filter((media) => media.mediaType === \"attachment\")\n            .map(({ id }) => id);\n    }\n\n    get attachmentsDomain() {\n        const domain = [\n            \"&\",\n            [\"res_model\", \"=\", this.props.resModel],\n            [\"res_id\", \"=\", this.props.resId || 0],\n        ];\n        domain.unshift(\"|\", [\"public\", \"=\", true]);\n        domain.push([\"name\", \"ilike\", this.state.needle]);\n        return domain;\n    }\n\n    get allAttachments() {\n        return this.state.attachments;\n    }\n\n    validateUrl(url) {\n        const path = url.split(\"?\")[0];\n        const isValidUrl = /^.+\\..+$/.test(path); // TODO improve\n        const isValidFileFormat = true;\n        return { isValidUrl, isValidFileFormat, path };\n    }\n\n    async fetchAttachments(limit, offset) {\n        this.state.isFetchingAttachments = true;\n        let attachments = [];\n        try {\n            attachments = await this.orm.call(\"ir.attachment\", \"search_read\", [], {\n                domain: this.attachmentsDomain,\n                fields: [\n                    \"name\",\n                    \"mimetype\",\n                    \"description\",\n                    \"checksum\",\n                    \"url\",\n                    \"type\",\n                    \"res_id\",\n                    \"res_model\",\n                    \"public\",\n                    \"access_token\",\n                    \"image_src\",\n                    \"image_width\",\n                    \"image_height\",\n                    \"original_id\",\n                ],\n                order: \"id desc\",\n                // Try to fetch first record of next page just to know whether there is a next page.\n                limit,\n                offset,\n            });\n            attachments.forEach((attachment) => (attachment.mediaType = \"attachment\"));\n        } catch (e) {\n            // Reading attachments as a portal user is not permitted and will raise\n            // an access error so we catch the error silently and don't return any\n            // attachment so he can still use the wizard and upload an attachment\n            if (e.exceptionName !== \"odoo.exceptions.AccessError\") {\n                throw e;\n            }\n        }\n        this.state.canLoadMoreAttachments =\n            attachments.length >= this.NUMBER_OF_ATTACHMENTS_TO_DISPLAY;\n        this.state.isFetchingAttachments = false;\n        return attachments;\n    }\n\n    async handleLoadMore() {\n        await this.loadMore();\n    }\n\n    async loadMore() {\n        return this.keepLast\n            .add(\n                this.fetchAttachments(\n                    this.NUMBER_OF_ATTACHMENTS_TO_DISPLAY,\n                    this.state.attachments.length\n                )\n            )\n            .then((newAttachments) => {\n                // This is never reached if another search or loadMore occurred.\n                this.state.attachments.push(...newAttachments);\n            });\n    }\n\n    async handleSearch(needle) {\n        await this.search(needle);\n    }\n\n    async search(needle) {\n        // Prepare in case loadMore results are obtained instead.\n        this.state.attachments = [];\n        // Fetch attachments relies on the state's needle.\n        this.state.needle = needle;\n        return this.keepLast\n            .add(this.fetchAttachments(this.NUMBER_OF_ATTACHMENTS_TO_DISPLAY, 0))\n            .then((attachments) => {\n                // This is never reached if a new search occurred.\n                this.state.attachments = attachments;\n            });\n    }\n\n    async uploadFiles(files) {\n        await this.uploadService.uploadFiles(\n            files,\n            { resModel: this.props.resModel, resId: this.props.resId },\n            (attachment) => this.onUploaded(attachment)\n        );\n    }\n\n    async uploadUrl(url) {\n        await fetch(url)\n            .then(async (result) => {\n                const blob = await result.blob();\n                blob.id = new Date().getTime();\n                blob.name = new URL(url, window.location.href).pathname\n                    .split(\"/\")\n                    .findLast((s) => s);\n                await this.uploadFiles([blob]);\n            })\n            .catch(async () => {\n                await new Promise((resolve) => {\n                    // If it works from an image, use URL.\n                    const imageEl = document.createElement(\"img\");\n                    imageEl.onerror = () => {\n                        // This message is about the blob fetch failure.\n                        // It is only displayed if the fallback did not work.\n                        this.notificationService.add(\n                            _t(\"An error occurred while fetching the entered URL.\"),\n                            {\n                                type: \"danger\",\n                                sticky: true,\n                            }\n                        );\n                        resolve();\n                    };\n                    imageEl.onload = () => {\n                        this.onLoadUploadedUrl(url, resolve);\n                    };\n                    imageEl.src = url;\n                });\n            });\n    }\n\n    async onLoadUploadedUrl(url, resolve) {\n        await this.uploadService.uploadUrl(\n            url,\n            {\n                resModel: this.props.resModel,\n                resId: this.props.resId,\n            },\n            (attachment) => this.onUploaded(attachment)\n        );\n        resolve();\n    }\n\n    async onUploaded(attachment) {\n        this.state.attachments = [\n            attachment,\n            ...this.state.attachments.filter((attach) => attach.id !== attachment.id),\n        ];\n        this.selectAttachment(attachment);\n        if (!this.props.multiSelect) {\n            await this.props.save();\n        }\n        if (this.props.onAttachmentChange) {\n            this.props.onAttachmentChange(attachment);\n        }\n    }\n\n    onRemoved(attachmentId) {\n        this.state.attachments = this.state.attachments.filter(\n            (attachment) => attachment.id !== attachmentId\n        );\n    }\n\n    selectAttachment(attachment) {\n        this.props.selectMedia({ ...attachment, mediaType: \"attachment\" });\n    }\n\n    selectInitialMedia() {\n        return (\n            this.props.media &&\n            this.constructor.tagNames.includes(this.props.media.tagName) &&\n            !this.selectedAttachmentIds.length\n        );\n    }\n\n    /**\n     * Updates the scroll button, depending on whether the \"Load more\" button is\n     * fully visible or not.\n     */\n    updateScroll() {\n        const loadMoreTop = this.loadMoreButtonRef.el.getBoundingClientRect().top;\n        const modalEl = this.props.modalRef.el.querySelector(\"main.modal-body\");\n        const modalBottom = modalEl.getBoundingClientRect().bottom;\n        this.state.canScrollAttachments = loadMoreTop >= modalBottom;\n        this.loadMoreButtonRef.el.classList.remove(\"o_hide_loading\");\n    }\n\n    /**\n     * Checks if the attachment is (partially) hidden.\n     *\n     * @param {Element} attachmentEl the attachment \"container\"\n     * @returns {Boolean} true if the attachment is hidden, false otherwise.\n     */\n    isAttachmentHidden(attachmentEl) {\n        const attachmentBottom = Math.round(attachmentEl.getBoundingClientRect().bottom);\n        const modalEl = this.props.modalRef.el.querySelector(\"main.modal-body\");\n        const modalBottom = modalEl.getBoundingClientRect().bottom;\n        return attachmentBottom > modalBottom;\n    }\n\n    /**\n     * Scrolls two attachments rows at a time. If there are not enough rows,\n     * scrolls to the \"Load more\" button.\n     */\n    handleScrollAttachments() {\n        let scrollToEl = this.loadMoreButtonRef.el;\n        const attachmentEls = [\n            ...this.existingAttachmentsRef.el.querySelectorAll(\".o_existing_attachment_cell\"),\n        ];\n        const firstHiddenAttachmentEl = attachmentEls.find((el) => this.isAttachmentHidden(el));\n        if (firstHiddenAttachmentEl) {\n            const attachmentBottom = firstHiddenAttachmentEl.getBoundingClientRect().bottom;\n            const attachmentIndex = attachmentEls.indexOf(firstHiddenAttachmentEl);\n            const firstNextRowAttachmentEl = attachmentEls\n                .slice(attachmentIndex)\n                .find((el) => el.getBoundingClientRect().bottom > attachmentBottom);\n            scrollToEl = firstNextRowAttachmentEl || scrollToEl;\n        }\n        scrollToEl.scrollIntoView({ block: \"end\", inline: \"nearest\", behavior: \"smooth\" });\n    }\n}\n", "import { SearchMedia } from \"./search_media\";\nimport { fonts } from \"@html_editor/utils/fonts\";\n\nimport { Component, useState } from \"@odoo/owl\";\n\nexport class IconSelector extends Component {\n    static mediaSpecificClasses = [\"fa\"];\n    static mediaSpecificStyles = [\"color\", \"background-color\"];\n    static mediaExtraClasses = [\n        \"rounded-circle\",\n        \"rounded\",\n        \"img-thumbnail\",\n        \"shadow\",\n        /^text-\\S+$/,\n        /^bg-\\S+$/,\n        /^fa-\\S+$/,\n    ];\n    static tagNames = [\"SPAN\", \"I\"];\n    static template = \"html_editor.IconSelector\";\n    static components = {\n        SearchMedia,\n    };\n    static props = [\"*\"];\n\n    setup() {\n        this.state = useState({\n            fonts: this.props.fonts,\n            needle: \"\",\n        });\n    }\n\n    get selectedMediaIds() {\n        return this.props.selectedMedia[this.props.id].map(({ id }) => id);\n    }\n\n    search(needle) {\n        this.state.needle = needle;\n        if (!this.state.needle) {\n            this.state.fonts = this.props.fonts;\n        } else {\n            this.state.fonts = this.props.fonts.map((font) => {\n                const icons = font.icons.filter(\n                    (icon) => icon.alias.indexOf(this.state.needle.toLowerCase()) >= 0\n                );\n                return { ...font, icons };\n            });\n        }\n    }\n\n    async onClickIcon(font, icon) {\n        this.props.selectMedia({\n            ...icon,\n            fontBase: font.base,\n            // To check if the icon has changed, we only need to compare\n            // an alias of the icon with the class from the old media (some\n            // icons can have multiple classes e.g. \"fa-gears\" ~ \"fa-cogs\")\n            initialIconChanged:\n                this.props.media &&\n                !icon.names.some((name) => this.props.media.classList.contains(name)),\n        });\n        await this.props.save();\n    }\n\n    /**\n     * Utility methods, used by the MediaDialog component.\n     */\n    static createElements(selectedMedia) {\n        return selectedMedia.map((icon) => {\n            const iconEl = document.createElement(\"span\");\n            iconEl.classList.add(icon.fontBase, icon.names[0]);\n            return iconEl;\n        });\n    }\n    static initFonts() {\n        fonts.computeFonts();\n        const allFonts = fonts.fontIcons.map(({ cssData, base }) => {\n            const uniqueIcons = Array.from(\n                new Map(\n                    cssData.map((icon) => {\n                        const alias = icon.names.join(\",\");\n                        const id = `${base}_${alias}`;\n                        return [id, { ...icon, alias, id }];\n                    })\n                ).values()\n            );\n            return { base, icons: uniqueIcons };\n        });\n        return allFonts;\n    }\n}\n", "import { useRef, useState } from \"@odoo/owl\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { KeepLast } from \"@web/core/utils/concurrency\";\nimport { DEFAULT_PALETTE } from \"@html_editor/utils/color\";\nimport { getCSSVariableValue, getHtmlStyle } from \"@html_editor/utils/formatting\";\nimport { Attachment, FileSelector, IMAGE_EXTENSIONS, IMAGE_MIMETYPES } from \"./file_selector\";\nimport { isSrcCorsProtected } from \"@html_editor/utils/image\";\n\nexport class AutoResizeImage extends Attachment {\n    static template = \"html_editor.AutoResizeImage\";\n    setup() {\n        super.setup();\n\n        this.image = useRef(\"auto-resize-image\");\n        this.container = useRef(\"auto-resize-image-container\");\n\n        this.state = useState({\n            loaded: false,\n        });\n    }\n\n    async onImageLoaded() {\n        if (!this.image.el) {\n            // Do not fail if already removed.\n            return;\n        }\n        if (this.props.onLoaded) {\n            await this.props.onLoaded(this.image.el);\n            if (!this.image.el) {\n                // If replaced by colored version, aspect ratio will be\n                // computed on it instead.\n                return;\n            }\n        }\n        const aspectRatio = this.image.el.offsetWidth / this.image.el.offsetHeight;\n        const width = aspectRatio * this.props.minRowHeight;\n        this.container.el.style.flexGrow = width;\n        this.container.el.style.flexBasis = `${width}px`;\n        this.state.loaded = true;\n    }\n}\nconst newLocal = \"img-fluid\";\nexport class ImageSelector extends FileSelector {\n    static mediaSpecificClasses = [\"img\", newLocal, \"o_we_custom_image\"];\n    static mediaSpecificStyles = [];\n    static mediaExtraClasses = [\n        \"rounded-circle\",\n        \"rounded\",\n        \"img-thumbnail\",\n        \"shadow\",\n        \"w-25\",\n        \"w-50\",\n        \"w-75\",\n        \"w-100\",\n    ];\n    static tagNames = [\"IMG\"];\n    static attachmentsListTemplate = \"html_editor.ImagesListTemplate\";\n    static components = {\n        ...FileSelector.components,\n        AutoResizeImage,\n    };\n\n    setup() {\n        super.setup();\n\n        this.keepLastLibraryMedia = new KeepLast();\n\n        this.state.libraryMedia = [];\n        this.state.libraryResults = null;\n        this.state.isFetchingLibrary = false;\n        this.state.searchService = \"all\";\n        this.state.showOptimized = false;\n        this.NUMBER_OF_MEDIA_TO_DISPLAY = 10;\n\n        this.uploadText = _t(\"Upload an image\");\n        this.urlPlaceholder = \"https://www.odoo.com/logo.png\";\n        this.addText = _t(\"Add URL\");\n        this.searchPlaceholder = _t(\"Search an image\");\n        this.urlWarningTitle = _t(\n            \"Uploaded image's format is not supported. Try with: \" + IMAGE_EXTENSIONS.join(\", \")\n        );\n        this.allLoadedText = _t(\"All images have been loaded\");\n        this.showOptimizedOption = this.env.debug;\n        this.MIN_ROW_HEIGHT = 128;\n\n        this.fileMimetypes = IMAGE_MIMETYPES.join(\",\");\n        this.isImageField =\n            !!this.props.media?.closest(\"[data-oe-type=image]\") || !!this.props.addFieldImage;\n    }\n\n    get canLoadMore() {\n        // The user can load more library media only when the filter is set.\n        if (this.state.searchService === \"media-library\") {\n            return (\n                this.state.libraryResults &&\n                this.state.libraryMedia.length < this.state.libraryResults\n            );\n        }\n        return super.canLoadMore;\n    }\n\n    get hasContent() {\n        if (this.state.searchService === \"all\") {\n            return super.hasContent || !!this.state.libraryMedia.length;\n        } else if (this.state.searchService === \"media-library\") {\n            return !!this.state.libraryMedia.length;\n        }\n        return super.hasContent;\n    }\n\n    get isFetching() {\n        return super.isFetching || this.state.isFetchingLibrary;\n    }\n\n    get selectedMediaIds() {\n        return this.props.selectedMedia[this.props.id]\n            .filter((media) => media.mediaType === \"libraryMedia\")\n            .map(({ id }) => id);\n    }\n\n    get allAttachments() {\n        return [...super.allAttachments, ...this.state.libraryMedia];\n    }\n\n    get attachmentsDomain() {\n        const domain = super.attachmentsDomain;\n        domain.push([\"mimetype\", \"in\", IMAGE_MIMETYPES]);\n        if (!this.props.useMediaLibrary) {\n            domain.push(\n                \"|\",\n                [\"url\", \"=\", false],\n                \"!\",\n                \"|\",\n                [\"url\", \"=ilike\", \"/html_editor/shape/%\"],\n                [\"url\", \"=ilike\", \"/web_editor/shape/%\"]\n            );\n        }\n        domain.push(\"!\", [\"name\", \"=like\", \"%.crop\"]);\n        domain.push(\"|\", [\"type\", \"=\", \"binary\"], \"!\", [\"url\", \"=like\", \"/%/static/%\"]);\n\n        // Optimized images (meaning they are related to an `original_id`) can\n        // only be shown in debug mode as the toggler to make those images\n        // appear is hidden when not in debug mode.\n        // There is thus no point to fetch those optimized images outside debug\n        // mode. Worst, it leads to bugs: it might fetch only optimized images\n        // when clicking on \"load more\" which will look like it's bugged as no\n        // images will appear on screen (they all will be hidden).\n        if (!this.env.debug) {\n            const subDomain = [false];\n\n            // Particular exception: if the edited image is an optimized\n            // image, we need to fetch it too so it's displayed as the\n            // selected image when opening the media dialog.\n            // We might get a few more optimized image than necessary if the\n            // original image has multiple optimized images but it's not a\n            // big deal.\n            const originalId = this.props.media && this.props.media.dataset.originalId;\n            if (originalId) {\n                subDomain.push(originalId);\n            }\n\n            domain.push([\"original_id\", \"in\", subDomain]);\n        }\n\n        return domain;\n    }\n\n    async uploadFiles(files) {\n        await this.uploadService.uploadFiles(\n            files,\n            { resModel: this.props.resModel, resId: this.props.resId, isImage: true },\n            (attachment) => this.onUploaded(attachment)\n        );\n    }\n\n    async validateUrl(...args) {\n        const { isValidUrl, path } = super.validateUrl(...args);\n        const isValidFileFormat =\n            isValidUrl &&\n            (await new Promise((resolve) => {\n                const img = new Image();\n                img.src = path;\n                img.onload = () => resolve(true);\n                img.onerror = () => resolve(false);\n            }));\n        return { isValidFileFormat, isValidUrl };\n    }\n\n    async onLoadUploadedUrl(url, resolve) {\n        const urlPathname = new URL(url, window.location.href).pathname;\n        const imageExtension = IMAGE_EXTENSIONS.find((format) => urlPathname.endsWith(format));\n        if (this.isImageField && imageExtension === \".webp\") {\n            // Do not allow the user to replace an image field by a\n            // webp CORS protected image as we are not currently\n            // able to manage the report creation if such images are\n            // in there (as the equivalent jpeg can not be\n            // generated). It also causes a problem for resize\n            // operations as 'libwep' can not be used.\n            this.notificationService.add(\n                _t(\n                    \"You can not replace a field by this image. If you want to use this image, first save it on your computer and then upload it here.\"\n                ),\n                {\n                    type: \"danger\",\n                    sticky: true,\n                }\n            );\n            return resolve();\n        }\n        super.onLoadUploadedUrl(url, resolve);\n    }\n\n    isInitialMedia(attachment) {\n        if (this.props.media.dataset.originalSrc) {\n            return this.props.media.dataset.originalSrc === attachment.image_src;\n        }\n        return this.props.media.getAttribute(\"src\") === attachment.image_src;\n    }\n\n    async fetchAttachments(limit, offset) {\n        const attachments = await super.fetchAttachments(limit, offset);\n        if (this.isImageField) {\n            // The image is a field; mark the attachments if they are linked to\n            // a webp CORS protected image. Indeed, in this case, they should\n            // not be selectable on the media dialog (due to a problem of image\n            // resize and report creation).\n            for (const attachment of attachments) {\n                if (\n                    attachment.mimetype === \"image/webp\" &&\n                    (await isSrcCorsProtected(attachment.image_src))\n                ) {\n                    attachment.unselectable = true;\n                }\n            }\n        }\n        // Color-substitution for dynamic SVG attachment\n        const primaryColors = {};\n        const htmlStyle = getHtmlStyle(document);\n        for (let color = 1; color <= 5; color++) {\n            primaryColors[color] = getCSSVariableValue(\"o-color-\" + color, htmlStyle);\n        }\n        return attachments.map((attachment) => {\n            if (attachment.image_src.startsWith(\"/\")) {\n                const newURL = new URL(attachment.image_src, window.location.origin);\n                // Set the main colors of dynamic SVGs to o-color-1~5\n                if (\n                    attachment.image_src.startsWith(\"/html_editor/shape/\") ||\n                    attachment.image_src.startsWith(\"/web_editor/shape/\")\n                ) {\n                    newURL.searchParams.forEach((value, key) => {\n                        const match = key.match(/^c([1-5])$/);\n                        if (match) {\n                            newURL.searchParams.set(key, primaryColors[match[1]]);\n                        }\n                    });\n                } else {\n                    // Set height so that db images load faster\n                    newURL.searchParams.set(\"height\", 2 * this.MIN_ROW_HEIGHT);\n                }\n                attachment.thumbnail_src = newURL.pathname + newURL.search;\n            }\n            if (this.selectInitialMedia() && this.isInitialMedia(attachment)) {\n                this.selectAttachment(attachment);\n            }\n            return attachment;\n        });\n    }\n\n    async fetchLibraryMedia(offset) {\n        if (!this.state.needle) {\n            return { media: [], results: null };\n        }\n\n        this.state.isFetchingLibrary = true;\n        try {\n            const response = await rpc(\n                \"/html_editor/media_library_search\",\n                {\n                    query: this.state.needle,\n                    offset: offset,\n                },\n                {\n                    silent: true,\n                }\n            );\n            this.state.isFetchingLibrary = false;\n            const media = (response.media || []).slice(0, this.NUMBER_OF_MEDIA_TO_DISPLAY);\n            media.forEach((record) => (record.mediaType = \"libraryMedia\"));\n            return { media, results: response.results };\n        } catch {\n            // Either API endpoint doesn't exist or is misconfigured.\n            console.error(`Couldn't reach API endpoint.`);\n            this.state.isFetchingLibrary = false;\n            return { media: [], results: null };\n        }\n    }\n\n    async loadMore(...args) {\n        await super.loadMore(...args);\n        if (\n            !this.props.useMediaLibrary ||\n            // The user can load more library media only when the filter is set.\n            this.state.searchService !== \"media-library\"\n        ) {\n            return;\n        }\n        return this.keepLastLibraryMedia\n            .add(this.fetchLibraryMedia(this.state.libraryMedia.length))\n            .then(({ media }) => {\n                // This is never reached if another search or loadMore occurred.\n                this.state.libraryMedia.push(...media);\n            });\n    }\n\n    async search(...args) {\n        await super.search(...args);\n        if (!this.props.useMediaLibrary) {\n            return;\n        }\n        if (!this.state.needle) {\n            this.state.searchService = \"all\";\n        }\n        this.state.libraryMedia = [];\n        this.state.libraryResults = 0;\n        return this.keepLastLibraryMedia\n            .add(this.fetchLibraryMedia(0))\n            .then(({ media, results }) => {\n                // This is never reached if a new search occurred.\n                this.state.libraryMedia = media;\n                this.state.libraryResults = results;\n            });\n    }\n\n    async onClickAttachment(attachment) {\n        if (attachment.unselectable) {\n            this.notificationService.add(\n                _t(\n                    \"You can not replace a field by this image. If you want to use this image, first save it on your computer and then upload it here.\"\n                ),\n                {\n                    type: \"danger\",\n                    sticky: true,\n                }\n            );\n            return;\n        }\n        this.selectAttachment(attachment);\n        if (!this.props.multiSelect) {\n            await this.props.save();\n        }\n    }\n\n    async onClickMedia(media) {\n        this.props.selectMedia({ ...media, mediaType: \"libraryMedia\" });\n        if (!this.props.multiSelect) {\n            await this.props.save();\n        }\n    }\n\n    /**\n     * Utility method used by the MediaDialog component.\n     */\n    static async createElements(selectedMedia, { orm }) {\n        // Create all media-library attachments.\n        const toSave = Object.fromEntries(\n            selectedMedia\n                .filter((media) => media.mediaType === \"libraryMedia\")\n                .map((media) => [\n                    media.id,\n                    {\n                        query: media.query || \"\",\n                        is_dynamic_svg: !!media.isDynamicSVG,\n                        dynamic_colors: media.dynamicColors,\n                    },\n                ])\n        );\n        let savedMedia = [];\n        if (Object.keys(toSave).length !== 0) {\n            savedMedia = await rpc(\"/html_editor/save_library_media\", { media: toSave });\n        }\n        const selected = selectedMedia\n            .filter((media) => media.mediaType === \"attachment\")\n            .concat(savedMedia)\n            .map((attachment) => {\n                // Color-customize dynamic SVGs with the theme colors\n                if (\n                    attachment.image_src &&\n                    (attachment.image_src.startsWith(\"/html_editor/shape/\") ||\n                        attachment.image_src.startsWith(\"/web_editor/shape/\"))\n                ) {\n                    const colorCustomizedURL = new URL(\n                        attachment.image_src,\n                        window.location.origin\n                    );\n                    const htmlStyle = getHtmlStyle(document);\n                    colorCustomizedURL.searchParams.forEach((value, key) => {\n                        const match = key.match(/^c([1-5])$/);\n                        if (match) {\n                            colorCustomizedURL.searchParams.set(\n                                key,\n                                getCSSVariableValue(`o-color-${match[1]}`, htmlStyle)\n                            );\n                        }\n                    });\n                    attachment.image_src = colorCustomizedURL.pathname + colorCustomizedURL.search;\n                }\n                return attachment;\n            });\n        return Promise.all(\n            selected.map(async (attachment) => {\n                const imageEl = document.createElement(\"img\");\n                let src = attachment.image_src;\n                if (!attachment.public && !attachment.url) {\n                    let accessToken = attachment.access_token;\n                    if (!accessToken) {\n                        [accessToken] = await orm.call(\"ir.attachment\", \"generate_access_token\", [\n                            attachment.id,\n                        ]);\n                    }\n                    src += `?access_token=${encodeURIComponent(accessToken)}`;\n                }\n                imageEl.src = src;\n                imageEl.alt = attachment.description || \"\";\n                imageEl.dataset.attachmentId = attachment.id;\n                return imageEl;\n            })\n        );\n    }\n\n    async onImageLoaded(imgEl, attachment) {\n        this.debouncedScrollUpdate();\n        if (attachment.mediaType === \"libraryMedia\" && !imgEl.src.startsWith(\"blob\")) {\n            // This call applies the theme's color palette to the\n            // loaded illustration. Upon replacement of the image,\n            // `onImageLoad` is called again, but the replacement image\n            // has an URL that starts with 'blob'. The condition above\n            // uses this to avoid an infinite loop.\n            await this.onLibraryImageLoaded(imgEl, attachment);\n        }\n    }\n\n    /**\n     * This converts the colors of an svg coming from the media library to\n     * the palette's ones, and make them dynamic.\n     *\n     * @param {HTMLElement} imgEl\n     * @param {Object} media\n     * @returns\n     */\n    async onLibraryImageLoaded(imgEl, media) {\n        const mediaUrl = imgEl.src;\n        try {\n            const response = await fetch(mediaUrl);\n            if (response.headers.get(\"content-type\").startsWith(\"image/svg+xml\")) {\n                let svg = await response.text();\n                const dynamicColors = {};\n                const combinedColorsRegex = new RegExp(\n                    Object.values(DEFAULT_PALETTE).join(\"|\"),\n                    \"gi\"\n                );\n                const htmlStyle = getHtmlStyle(document);\n                svg = svg.replace(combinedColorsRegex, (match) => {\n                    const colorId = Object.keys(DEFAULT_PALETTE).find(\n                        (key) => DEFAULT_PALETTE[key] === match.toUpperCase()\n                    );\n                    const colorKey = \"c\" + colorId;\n                    dynamicColors[colorKey] = getCSSVariableValue(\"o-color-\" + colorId, htmlStyle);\n                    return dynamicColors[colorKey];\n                });\n                const fileName = mediaUrl.split(\"/\").pop();\n                const file = new File([svg], fileName, {\n                    type: \"image/svg+xml\",\n                });\n                imgEl.src = URL.createObjectURL(file);\n                if (Object.keys(dynamicColors).length) {\n                    media.isDynamicSVG = true;\n                    media.dynamicColors = dynamicColors;\n                }\n            }\n        } catch {\n            console.error(\n                \"CORS is misconfigured on the API server, image will be treated as non-dynamic.\"\n            );\n        }\n    }\n}\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { useService, useChildRef } from \"@web/core/utils/hooks\";\nimport { Dialog } from \"@web/core/dialog/dialog\";\nimport { Notebook } from \"@web/core/notebook/notebook\";\nimport { ImageSelector } from \"./image_selector\";\nimport { IconSelector } from \"./icon_selector\";\n\nimport { Component, useState, useRef, useEffect } from \"@odoo/owl\";\nimport { iconClasses } from \"@html_editor/utils/dom_info\";\n\nexport const TABS = {\n    IMAGES: {\n        id: \"IMAGES\",\n        title: _t(\"Images\"),\n        Component: ImageSelector,\n        sequence: 10,\n    },\n    ICONS: {\n        id: \"ICONS\",\n        title: _t(\"Icons\"),\n        Component: IconSelector,\n        sequence: 20,\n    },\n};\n\nconst DEFAULT_SEQUENCE = 50;\nconst sequence = (tab) => tab.sequence ?? DEFAULT_SEQUENCE;\n\nexport class MediaDialog extends Component {\n    static template = \"html_editor.MediaDialog\";\n    static defaultProps = {\n        useMediaLibrary: true,\n        extraTabs: [],\n    };\n    static components = {\n        Dialog,\n        Notebook,\n    };\n    static props = {\n        extraTabs: { type: Array, optional: true, element: Object },\n        visibleTabs: { type: Array, optional: true, element: String },\n        activeTab: { type: String, optional: true },\n        \"*\": true,\n    };\n\n    setup() {\n        this.size = \"xl\";\n        this.contentClass = \"o_select_media_dialog h-100\";\n        this.title = _t(\"Select a media\");\n        this.modalRef = useChildRef();\n\n        this.orm = useService(\"orm\");\n        this.notificationService = useService(\"notification\");\n\n        this.selectedMedia = useState({});\n\n        this.addButtonRef = useRef(\"add-button\");\n\n        this.initialIconClasses = [];\n\n        this.notebookPages = [];\n        this.addTabs();\n        this.notebookPages.sort((a, b) => sequence(a) - sequence(b));\n        this.tabs = Object.fromEntries(this.notebookPages.map((tab) => [tab.id, tab]));\n\n        this.errorMessages = {};\n\n        this.state = useState({\n            activeTab: this.initialActiveTab,\n            isSaving: false,\n        });\n\n        useEffect(\n            (nbSelectedAttachments) => {\n                // Disable/enable the add button depending on whether some media\n                // are selected or not.\n                this.addButtonRef.el.toggleAttribute(\n                    \"disabled\",\n                    !nbSelectedAttachments || this.state.isSaving\n                );\n            },\n            () => [this.selectedMedia[this.state.activeTab].length, this.state.isSaving]\n        );\n    }\n\n    get initialActiveTab() {\n        if (this.props.activeTab) {\n            return this.props.activeTab;\n        }\n        if (this.props.media) {\n            const correspondingTab = Object.keys(this.tabs).find((id) =>\n                this.tabs[id].Component.tagNames.includes(this.props.media.tagName)\n            );\n            if (correspondingTab) {\n                return correspondingTab;\n            }\n        }\n        return this.notebookPages[0].id;\n    }\n\n    addTab(tab, additionalProps = {}) {\n        if (this.props.visibleTabs && !this.props.visibleTabs.includes(tab.id)) {\n            return;\n        }\n        this.selectedMedia[tab.id] = [];\n        this.notebookPages.push({\n            ...tab,\n            props: {\n                ...tab.props,\n                ...additionalProps,\n                id: tab.id,\n                resModel: this.props.resModel,\n                resId: this.props.resId,\n                media: this.props.media,\n                // multiImages: this.props.multiImages,\n                selectedMedia: this.selectedMedia,\n                selectMedia: (...args) =>\n                    this.selectMedia(...args, tab.id, additionalProps.multiSelect),\n                save: this.save.bind(this),\n                onAttachmentChange: this.props.onAttachmentChange,\n                errorMessages: (errorMessage) => (this.errorMessages[tab.id] = errorMessage),\n                modalRef: this.modalRef,\n            },\n        });\n    }\n\n    addTabs() {\n        const onlyImages =\n            this.props.onlyImages ||\n            (this.props.media &&\n                this.props.media.parentElement &&\n                (this.props.media.parentElement.dataset.oeField === \"image\" ||\n                    this.props.media.parentElement.dataset.oeType === \"image\"));\n\n        if (!this.props.noImages) {\n            this.addTab(TABS.IMAGES, {\n                useMediaLibrary: this.props.useMediaLibrary,\n                multiSelect: this.props.multiImages,\n                addFieldImage: this.props.addFieldImage,\n            });\n        }\n        if (onlyImages) {\n            return;\n        }\n        const addIcons = !this.props.visibleTabs || this.props.visibleTabs.includes(TABS.ICONS.id);\n        if (addIcons) {\n            const fonts = TABS.ICONS.Component.initFonts();\n            this.addTab(TABS.ICONS, {\n                fonts,\n            });\n\n            if (\n                this.props.media &&\n                TABS.ICONS.Component.tagNames.includes(this.props.media.tagName)\n            ) {\n                const classes = this.props.media.className.split(/\\s+/);\n                const predefinedMediaFont = fonts.find((font) => classes.includes(font.base));\n                if (predefinedMediaFont) {\n                    const selectedIcon = predefinedMediaFont.icons.find((icon) =>\n                        icon.names.some((name) => classes.includes(name))\n                    );\n                    if (selectedIcon) {\n                        this.initialIconClasses.push(...selectedIcon.names);\n                        this.selectMedia(selectedIcon, TABS.ICONS.id);\n                    }\n                } else {\n                    const iconRegex = new RegExp(`\\\\b(?:${iconClasses.join(\"|\")})(?:-\\\\S+)?\\\\b`);\n                    const fallbackIconClasses = classes.filter((cls) => iconRegex.test(cls));\n                    this.initialIconClasses.push(...fallbackIconClasses);\n                }\n            }\n        }\n        this.props.extraTabs.forEach((tab) => this.addTab(tab));\n    }\n\n    /**\n     * Render the selected media for insertion in the editor\n     *\n     * @param {Array<Object>} selectedMedia\n     * @returns {Array<HTMLElement>}\n     */\n    async renderMedia(selectedMedia) {\n        const elements = await this.tabs[this.state.activeTab].Component.createElements(\n            selectedMedia,\n            { orm: this.orm }\n        );\n        elements.forEach((element) => {\n            if (this.props.media) {\n                element.classList.add(...this.props.media.classList);\n                const style = this.props.media.getAttribute(\"style\");\n                if (style) {\n                    element.setAttribute(\"style\", style);\n                }\n                if (this.state.activeTab === TABS.IMAGES.id) {\n                    if (this.props.media.dataset.shape) {\n                        element.dataset.shape = this.props.media.dataset.shape;\n                    }\n                    if (this.props.media.dataset.shapeColors) {\n                        element.dataset.shapeColors = this.props.media.dataset.shapeColors;\n                    }\n                    if (this.props.media.dataset.shapeFlip) {\n                        element.dataset.shapeFlip = this.props.media.dataset.shapeFlip;\n                    }\n                    if (this.props.media.dataset.shapeRotate) {\n                        element.dataset.shapeRotate = this.props.media.dataset.shapeRotate;\n                    }\n                    if (this.props.media.dataset.hoverEffect) {\n                        element.dataset.hoverEffect = this.props.media.dataset.hoverEffect;\n                    }\n                    if (this.props.media.dataset.hoverEffectColor) {\n                        element.dataset.hoverEffectColor =\n                            this.props.media.dataset.hoverEffectColor;\n                    }\n                    if (this.props.media.dataset.hoverEffectStrokeWidth) {\n                        element.dataset.hoverEffectStrokeWidth =\n                            this.props.media.dataset.hoverEffectStrokeWidth;\n                    }\n                    if (this.props.media.dataset.hoverEffectIntensity) {\n                        element.dataset.hoverEffectIntensity =\n                            this.props.media.dataset.hoverEffectIntensity;\n                    }\n                }\n            }\n            for (const otherTab of Object.keys(this.tabs).filter(\n                (key) => key !== this.state.activeTab\n            )) {\n                for (const property of this.tabs[otherTab].Component.mediaSpecificStyles) {\n                    element.style.removeProperty(property);\n                }\n                element.classList.remove(...this.tabs[otherTab].Component.mediaSpecificClasses);\n                const extraClassesToRemove = [];\n                for (const name of this.tabs[otherTab].Component.mediaExtraClasses) {\n                    if (typeof name === \"string\") {\n                        extraClassesToRemove.push(name);\n                    } else {\n                        // Regex\n                        for (const className of element.classList) {\n                            if (className.match(name)) {\n                                extraClassesToRemove.push(className);\n                            }\n                        }\n                    }\n                }\n                // Remove classes that do not also exist in the target type.\n                element.classList.remove(\n                    ...extraClassesToRemove.filter((candidateName) => {\n                        for (const name of this.tabs[this.state.activeTab].Component\n                            .mediaExtraClasses) {\n                            if (typeof name === \"string\") {\n                                if (candidateName === name) {\n                                    return false;\n                                }\n                            } else {\n                                // Regex\n                                if (candidateName.match(name)) {\n                                    return false;\n                                }\n                            }\n                        }\n                        return true;\n                    })\n                );\n            }\n            element.classList.remove(...this.initialIconClasses);\n            element.classList.remove(\"o_modified_image_to_save\");\n            element.classList.remove(\"oe_edited_link\");\n            element.classList.add(\n                ...this.tabs[this.state.activeTab].Component.mediaSpecificClasses\n            );\n        });\n        return elements;\n    }\n\n    selectMedia(media, tabId, multiSelect) {\n        if (media && !Object.keys(media).length) {\n            // Clear media selection when an empty object is passed\n            this.selectedMedia[tabId] = [];\n            return;\n        }\n        if (multiSelect) {\n            const isMediaSelected = this.selectedMedia[tabId]\n                .map(({ id }) => id)\n                .includes(media.id);\n            if (!isMediaSelected) {\n                this.selectedMedia[tabId].push(media);\n            } else {\n                this.selectedMedia[tabId] = this.selectedMedia[tabId].filter(\n                    (m) => m.id !== media.id\n                );\n            }\n        } else {\n            this.selectedMedia[tabId] = [media];\n        }\n    }\n\n    async save() {\n        if (this.errorMessages[this.state.activeTab]) {\n            this.notificationService.add(this.errorMessages[this.state.activeTab], {\n                type: \"danger\",\n            });\n            return;\n        }\n        const selectedMedia = this.selectedMedia[this.state.activeTab];\n        // TODO In master: clean the save method so it performs the specific\n        // adaptation before saving from the active media selector and find a\n        // way to simply close the dialog if the media element remains the same.\n        const saveSelectedMedia =\n            selectedMedia.length &&\n            (this.state.activeTab !== TABS.ICONS.id ||\n                selectedMedia[0].initialIconChanged ||\n                !this.props.media);\n        this.state.isSaving = true;\n        if (saveSelectedMedia) {\n            const elements = await this.renderMedia(selectedMedia);\n            if (this.props.multiImages) {\n                await this.props.save(elements, selectedMedia, this.state.activeTab);\n            } else {\n                await this.props.save(elements[0], selectedMedia, this.state.activeTab);\n            }\n        }\n        this.props.close();\n        this.state.isSaving = false;\n    }\n\n    onTabChange(tab) {\n        this.state.activeTab = tab;\n    }\n}\n", "import { useDebounced } from \"@web/core/utils/timing\";\nimport { useAutofocus } from \"@web/core/utils/hooks\";\n\nimport { Component, useEffect, useState } from \"@odoo/owl\";\n\nexport class SearchMedia extends Component {\n    static template = \"html_editor.SearchMedia\";\n    static props = [\"searchPlaceholder\", \"search\", \"needle\"];\n    setup() {\n        useAutofocus({ mobile: true });\n        this.debouncedSearch = useDebounced(this.props.search, 1000);\n\n        this.state = useState({\n            input: this.props.needle || \"\",\n        });\n\n        useEffect(\n            (input) => {\n                // Do not trigger a search on the initial render.\n                if (this.hasRendered) {\n                    this.debouncedSearch(input);\n                } else {\n                    this.hasRendered = true;\n                }\n            },\n            () => [this.state.input]\n        );\n    }\n}\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { Component, useState } from \"@odoo/owl\";\n\nexport class ProgressBar extends Component {\n    static template = \"html_editor.ProgressBar\";\n    static props = {\n        progress: { type: Number, optional: true },\n        hasError: { type: Boolean, optional: true },\n        uploaded: { type: Boolean, optional: true },\n        name: String,\n        size: { type: String, optional: true },\n        errorMessage: { type: String, optional: true },\n    };\n    static defaultProps = {\n        progress: 0,\n        hasError: false,\n        uploaded: false,\n        size: \"\",\n        errorMessage: \"\",\n    };\n\n    get errorMessage() {\n        return this.props.errorMessage || _t(\"File could not be saved\");\n    }\n\n    get progress() {\n        return Math.round(this.props.progress);\n    }\n}\n\nexport class UploadProgressToast extends Component {\n    static template = \"html_editor.UploadProgressToast\";\n    static components = {\n        ProgressBar,\n    };\n    static props = {\n        close: Function,\n    };\n\n    setup() {\n        this.uploadService = useService(\"upload\");\n        this.state = useState(this.uploadService.progressToast);\n    }\n}\n", "import { rpc } from \"@web/core/network/rpc\";\nimport { registry } from \"@web/core/registry\";\nimport { UploadProgressToast } from \"./upload_progress_toast\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { checkFileSize } from \"@web/core/utils/files\";\nimport { humanNumber } from \"@web/core/utils/numbers\";\nimport { getDataURLFromFile } from \"@web/core/utils/urls\";\nimport { reactive } from \"@odoo/owl\";\n\nexport const AUTOCLOSE_DELAY = 3000;\nexport const AUTOCLOSE_DELAY_LONG = 8000;\n\nexport const uploadService = {\n    dependencies: [\"notification\"],\n    start(env, { notification }) {\n        let fileId = 0;\n        const progressToast = reactive({\n            files: {},\n            isVisible: false,\n        });\n\n        registry.category(\"main_components\").add(\"UploadProgressToast\", {\n            Component: UploadProgressToast,\n            props: {\n                close: () => (progressToast.isVisible = false),\n            },\n        });\n\n        const addFile = (file) => {\n            progressToast.files[file.id] = file;\n            progressToast.isVisible = true;\n            return progressToast.files[file.id];\n        };\n\n        const deleteFile = (fileId) => {\n            delete progressToast.files[fileId];\n            if (!Object.keys(progressToast.files).length) {\n                progressToast.isVisible = false;\n            }\n        };\n        return {\n            get progressToast() {\n                return progressToast;\n            },\n            get fileId() {\n                return fileId;\n            },\n            addFile,\n            deleteFile,\n            incrementId() {\n                fileId++;\n            },\n            uploadUrl: async (url, { resModel, resId }, onUploaded) => {\n                const attachment = await rpc(\"/html_editor/attachment/add_url\", {\n                    url,\n                    res_model: resModel,\n                    res_id: resId,\n                });\n                await onUploaded(attachment);\n            },\n            /**\n             * This takes an array of files (from an input HTMLElement), and\n             * uploads them while managing the UploadProgressToast.\n             *\n             * @param {Array<File>} files\n             * @param {Object} options\n             * @param {Function} onUploaded\n             */\n            uploadFiles: async (files, { resModel, resId, isImage }, onUploaded) => {\n                // Upload the smallest file first to block the user the least possible.\n                const sortedFiles = Array.from(files).sort((a, b) => a.size - b.size);\n                for (const file of sortedFiles) {\n                    let fileSize = file.size;\n                    if (!checkFileSize(fileSize, notification)) {\n                        return null;\n                    }\n                    if (!fileSize) {\n                        fileSize = \"\";\n                    } else {\n                        fileSize = humanNumber(fileSize) + \"B\";\n                    }\n\n                    const id = ++fileId;\n                    file.progressToastId = id;\n                    // This reactive object, built based on the files array,\n                    // is given as a prop to the UploadProgressToast.\n                    addFile({\n                        id,\n                        name: file.name,\n                        size: fileSize,\n                    });\n                }\n\n                // Upload one file at a time: no need to parallel as upload is\n                // limited by bandwidth.\n                for (const sortedFile of sortedFiles) {\n                    const file = progressToast.files[sortedFile.progressToastId];\n                    let dataURL;\n                    try {\n                        dataURL = await getDataURLFromFile(sortedFile);\n                    } catch {\n                        deleteFile(file.id);\n                        env.services.notification.add(\n                            _t('Could not load the file \"%s\".', sortedFile.name),\n                            { type: \"danger\" }\n                        );\n                        continue;\n                    }\n                    try {\n                        const xhr = new XMLHttpRequest();\n                        xhr.upload.addEventListener(\"progress\", (ev) => {\n                            const rpcComplete = (ev.loaded / ev.total) * 100;\n                            file.progress = rpcComplete;\n                        });\n                        xhr.upload.addEventListener(\"load\", function () {\n                            // Don't show yet success as backend code only starts now\n                            file.progress = 100;\n                        });\n                        const attachment = await rpc(\n                            \"/html_editor/attachment/add_data\",\n                            {\n                                name: file.name,\n                                data: dataURL.split(\",\")[1],\n                                res_id: resId,\n                                res_model: resModel,\n                                is_image: !!isImage,\n                                width: 0,\n                                quality: 0,\n                            },\n                            { xhr }\n                        );\n                        if (attachment.error) {\n                            file.hasError = true;\n                            file.errorMessage = attachment.error;\n                        } else {\n                            if (attachment.mimetype === \"image/webp\") {\n                                // Generate alternate format for reports.\n                                const image = document.createElement(\"img\");\n                                image.src = `data:image/webp;base64,${dataURL.split(\",\")[1]}`;\n                                await new Promise((resolve) =>\n                                    image.addEventListener(\"load\", resolve)\n                                );\n                                const canvas = document.createElement(\"canvas\");\n                                canvas.width = image.width;\n                                canvas.height = image.height;\n                                const ctx = canvas.getContext(\"2d\");\n                                ctx.fillStyle = \"rgb(255, 255, 255)\";\n                                ctx.fillRect(0, 0, canvas.width, canvas.height);\n                                ctx.drawImage(image, 0, 0);\n                                const altDataURL = canvas.toDataURL(\"image/jpeg\");\n                                await rpc(\n                                    \"/html_editor/attachment/add_data\",\n                                    {\n                                        name: file.name.replace(/\\.webp$/, \".jpg\"),\n                                        data: altDataURL.split(\",\")[1],\n                                        res_id: attachment.id,\n                                        res_model: \"ir.attachment\",\n                                        is_image: true,\n                                        width: 0,\n                                        quality: 0,\n                                    },\n                                    { xhr }\n                                );\n                            }\n                            file.uploaded = true;\n                            await onUploaded(attachment);\n                        }\n                        // If there's an error, display the error message for longer\n                        const message_autoclose_delay = file.hasError\n                            ? AUTOCLOSE_DELAY_LONG\n                            : AUTOCLOSE_DELAY;\n                        setTimeout(() => deleteFile(file.id), message_autoclose_delay);\n                    } catch (error) {\n                        file.hasError = true;\n                        setTimeout(() => deleteFile(file.id), AUTOCLOSE_DELAY_LONG);\n                        throw error;\n                    }\n                }\n            },\n        };\n    },\n};\n\nregistry.category(\"services\").add(\"upload\", uploadService);\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { useAutofocus, useService } from \"@web/core/utils/hooks\";\nimport { debounce } from \"@web/core/utils/timing\";\n\nimport { Component, useState, useRef, onMounted, status } from \"@odoo/owl\";\nimport { Switch } from \"@html_editor/components/switch/switch\";\n\nclass VideoOption extends Component {\n    static template = \"html_editor.VideoOption\";\n    static components = {\n        Switch,\n    };\n    static props = {\n        description: { type: String, optional: true },\n        label: { type: String, optional: true },\n        onChangeOption: Function,\n        onChangeStartAt: Function,\n        value: { type: String, optional: true },\n        name: { type: String, optional: true },\n    };\n\n    get showStartAtInput() {\n        return this.props.name === \"start_from\";\n    }\n}\n\nclass VideoIframe extends Component {\n    static template = \"html_editor.VideoIframe\";\n    static props = {\n        src: { type: String },\n    };\n}\n\nexport class VideoSelector extends Component {\n    static mediaSpecificClasses = [\"media_iframe_video\"];\n    static mediaSpecificStyles = [];\n    static mediaExtraClasses = [];\n    static tagNames = [\"IFRAME\", \"DIV\"];\n    static template = \"html_editor.VideoSelector\";\n    static components = {\n        VideoIframe,\n        VideoOption,\n    };\n    static props = {\n        selectMedia: Function,\n        errorMessages: Function,\n        vimeoPreviewIds: { type: Array, optional: true },\n        isForBgVideo: { type: Boolean, optional: true },\n        media: { validate: (p) => p.nodeType === Node.ELEMENT_NODE, optional: true },\n        \"*\": true,\n    };\n    static defaultProps = {\n        vimeoPreviewIds: [],\n        isForBgVideo: false,\n    };\n\n    setup() {\n        this.http = useService(\"http\");\n\n        this.state = useState({\n            options: [],\n            src: \"\",\n            urlInput: \"\",\n            platform: null,\n            vimeoPreviews: [],\n            errorMessage: \"\",\n        });\n\n        this.PLATFORMS = {\n            youtube: \"youtube\",\n            dailymotion: \"dailymotion\",\n            vimeo: \"vimeo\",\n        };\n\n        this.platformParams = {\n            youtube: \"start\",\n            dailymotion: \"startTime\",\n            vimeo: \"#t=\",\n        };\n\n        this.OPTIONS = {\n            autoplay: {\n                label: _t(\"Autoplay\"),\n                description: _t(\"Videos are muted when autoplay is enabled\"),\n                platforms: [\n                    this.PLATFORMS.youtube,\n                    this.PLATFORMS.vimeo,\n                ],\n                urlParameter: () => \"autoplay=1\",\n            },\n            loop: {\n                label: _t(\"Loop\"),\n                platforms: [this.PLATFORMS.youtube, this.PLATFORMS.vimeo],\n                urlParameter: () => \"loop=1\",\n            },\n            hide_controls: {\n                label: _t(\"Hide player controls\"),\n                platforms: [\n                    this.PLATFORMS.youtube,\n                    this.PLATFORMS.vimeo,\n                ],\n                urlParameter: () => \"controls=0\",\n            },\n            hide_fullscreen: {\n                label: _t(\"Hide fullscreen button\"),\n                platforms: [this.PLATFORMS.youtube],\n                urlParameter: () => \"fs=0\",\n                isHidden: () =>\n                    this.state.options.filter((option) => option.id === \"hide_controls\")[0].value,\n            },\n            start_from: {\n                label: _t(\"Start at\"),\n                platforms: [\n                    this.PLATFORMS.youtube,\n                    this.PLATFORMS.vimeo,\n                    this.PLATFORMS.dailymotion,\n                ],\n                urlParameter: () => this.platformParams[this.state.platform],\n            },\n        };\n        this.urlInputRef = useRef(\"url-input\");\n\n        onMounted(async () => {\n            if (this.props.media) {\n                const src =\n                    this.props.media.dataset.oeExpression ||\n                    this.props.media.dataset.src ||\n                    (this.props.media.tagName === \"IFRAME\" &&\n                        this.props.media.getAttribute(\"src\")) ||\n                    \"\";\n                if (src) {\n                    this.state.urlInput = src;\n                    if (!src.startsWith(\"https:\") && !src.startsWith(\"http:\")) {\n                        this.state.urlInput = \"https:\" + this.state.urlInput;\n                    }\n                    await this.syncOptionsWithUrl();\n                    if (status(this) === \"destroyed\") {\n                        return;\n                    }\n                }\n            }\n            await this.prepareVimeoPreviews();\n        });\n\n        useAutofocus();\n\n        this.onChangeUrl = debounce(() => this.syncOptionsWithUrl(), 500);\n\n        this.onChangeStartAt = debounce(async (ev, optionId) => {\n            const start_from = this.convertTimestampToSeconds(ev.target.value);\n            this.state.options = this.state.options.map((option) => {\n                if (option.id === optionId) {\n                    // to avoid showing \"0\" when seconds are 0, we set it to \"00:00\"\n                    return { ...option, value: start_from === \"0\" ? \"00:00\" : start_from };\n                }\n                return option;\n            });\n            await this.updateVideo();\n            this.state.urlInput = \"https:\" + this.state.src;\n        }, 1000);\n    }\n\n    get shownOptions() {\n        if (this.props.isForBgVideo) {\n            return [];\n        }\n        return this.state.options.filter(\n            (option) => !this.OPTIONS[option.id].isHidden || !this.OPTIONS[option.id].isHidden()\n        );\n    }\n\n    get value() {\n        if (this.option.id === \"start_from\" && this.option.value !== \"00:00\") {\n            return this.convertSecondsToTimestamp(this.option.value);\n        }\n        return this.option.value;\n    }\n\n    async onChangeOption(optionId) {\n        this.state.options = this.state.options.map((option) => {\n            if (option.id === optionId) {\n                // used \"0\" here, to set the initial \"startAt\" value if option is toggled on,\n                // for other option it works as truthy value.\n                return { ...option, value: !option.value && \"00:00\" };\n            }\n            return option;\n        });\n        await this.updateVideo();\n        this.state.urlInput = \"https:\" + this.state.src;\n    }\n\n    async onClickSuggestion(src) {\n        this.state.urlInput = src;\n        await this.updateVideo();\n    }\n\n    async updateVideo() {\n        if (!this.state.urlInput) {\n            this.state.src = \"\";\n            this.state.urlInput = \"\";\n            this.state.options = [];\n            this.state.platform = null;\n            this.state.errorMessage = \"\";\n            /**\n             * When the url input is emptied, we need to call the `selectMedia`\n             * callback function to notify the other components that the media\n             * has changed.\n             */\n            this.props.selectMedia({});\n            return;\n        }\n\n        // Detect if we have an embed code rather than an URL\n        const embedMatch = this.state.urlInput.match(/(src|href)=[\"']?([^\"']+)?/);\n        if (embedMatch && embedMatch[2].length > 0 && embedMatch[2].indexOf(\"instagram\")) {\n            embedMatch[1] = embedMatch[2]; // Instagram embed code is different\n        }\n        const url = embedMatch ? embedMatch[1] : this.state.urlInput;\n\n        const options = {};\n        if (this.props.isForBgVideo && URL.canParse(url)) {\n            const parsedUrl = new URL(url);\n            const urlParams = parsedUrl.searchParams;\n            const startFrom =\n                urlParams.get(\"start\") || urlParams.get(\"startTime\") || urlParams.get(\"t\");\n            Object.keys(this.OPTIONS).forEach((key) => {\n                options[key] = key === \"start_from\" ? startFrom : true;\n            });\n        } else {\n            for (const option of this.shownOptions) {\n                options[option.id] = option.value;\n            }\n        }\n\n        const {\n            embed_url: src,\n            video_id: videoId,\n            params,\n            platform,\n        } = await this._getVideoURLData(url, options);\n\n        if (!src) {\n            this.state.errorMessage = _t(\"The provided url is not valid\");\n        } else if (!platform) {\n            this.state.errorMessage = _t(\"The provided url does not reference any supported video\");\n        } else {\n            this.state.errorMessage = \"\";\n        }\n        this.props.errorMessages(this.state.errorMessage);\n\n        const newOptions = [];\n        if (platform && platform !== this.state.platform) {\n            Object.keys(this.OPTIONS).forEach((key) => {\n                if (this.OPTIONS[key].platforms.includes(platform)) {\n                    const { label, description } = this.OPTIONS[key];\n                    newOptions.push({ id: key, label, description });\n                }\n            });\n        }\n\n        this.state.src = src;\n        this.props.selectMedia({\n            id: src,\n            src,\n            platform,\n            videoId,\n            params,\n        });\n        if (platform !== this.state.platform) {\n            this.state.platform = platform;\n            this.state.options = newOptions;\n        }\n    }\n\n    /**\n     * Keep rpc call in distinct method make it patchable by test.\n     */\n    async _getVideoURLData(url, options) {\n        return await rpc(\"/html_editor/video_url/data\", {\n            video_url: url,\n            ...options,\n        });\n    }\n\n    /**\n     * Utility method, called by the MediaDialog component.\n     */\n    static createElements(selectedMedia) {\n        return selectedMedia.map((video) => {\n            const div = document.createElement(\"div\");\n            div.dataset.oeExpression = video.src;\n            div.innerHTML =\n                '<div class=\"css_editable_mode_display\"></div>' +\n                '<div class=\"media_iframe_video_size\" contenteditable=\"false\"></div>' +\n                '<iframe frameborder=\"0\" contenteditable=\"false\" allowfullscreen=\"allowfullscreen\"></iframe>';\n\n            div.querySelector(\"iframe\").src = video.src;\n            return div;\n        });\n    }\n\n    /**\n     * Based on the config vimeo ids, prepare the vimeo previews.\n     */\n    async prepareVimeoPreviews() {\n        await Promise.all(\n            this.props.vimeoPreviewIds.map(async (videoId) => {\n                const { thumbnail_url: thumbnailSrc } = await this.http.get(\n                    `https://vimeo.com/api/oembed.json?url=http%3A//vimeo.com/${encodeURIComponent(\n                        videoId\n                    )}`\n                );\n                this.state.vimeoPreviews.push({\n                    id: videoId,\n                    thumbnailSrc,\n                    src: `https://player.vimeo.com/video/${encodeURIComponent(videoId)}`,\n                });\n            })\n        );\n    }\n\n    /**\n     * Utility method to make options and urlInput state consistent with state of component.\n     */\n    async syncOptionsWithUrl() {\n        await this.updateVideo();\n        if (!URL.canParse(this.state.urlInput)) {\n            return;\n        }\n        const parsedUrl = new URL(this.state.urlInput);\n        const urlParams = parsedUrl.searchParams;\n        this.state.options = this.state.options.map((option) => {\n            const urlParameter = this.OPTIONS[option.id].urlParameter();\n            let value = \"\";\n\n            switch (urlParameter) {\n                case \"#t=\":\n                    value = this.parseTimeToSeconds(this.state.urlInput.split(\"#t=\")[1]);\n                    break;\n                case \"start\":\n                    value = urlParams.get(\"start\") || urlParams.get(\"t\");\n                    break;\n                case \"startTime\":\n                    value = urlParams.get(\"startTime\") || urlParams.get(\"start\");\n                    break;\n                default:\n                    value = this.state.urlInput.includes(urlParameter);\n            }\n            if (option.id === \"start_from\" && value === \"0\") {\n                return { ...option, value: \"00:00\" };\n            }\n            return { ...option, value: value || \"\" };\n        });\n        await this.updateVideo();\n    }\n\n    /**\n     * Utility method,to convert timestamp to seconds.\n     *\n     * @param {string} timestamp - The start time in HH:MM:SS format or seconds.\n     * @returns {string} - The start time in seconds.\n     */\n    convertTimestampToSeconds(timestamp) {\n        timestamp = timestamp.trim();\n        // Regular expression for HH:MM:SS format\n        const timeRegex = /^(?:(\\d+):)?([0-5]?\\d):([0-5]?\\d)$/;\n        if (timeRegex.test(timestamp)) {\n            return (timestamp =\n                timestamp.split(\":\").reduce((acc, time) => acc * 60 + +time, 0) + \"\");\n        }\n        if (isNaN(timestamp)) {\n            return \"0\";\n        }\n        return timestamp;\n    }\n    /**\n     * Utility method,to convert seconds to timestamp.\n     *\n     * @param {string} value - The start time in seconds.\n     * @returns {string} - The start time in HH:MM:SS or MM:SS format.\n     */\n    convertSecondsToTimestamp(value) {\n        if (!value) {\n            return \"\";\n        }\n        const match = value.match(/^\\d+s?$/);\n        if (!match) {\n            return \"\";\n        }\n\n        const totalSeconds = parseInt(match[0], 10);\n        if (!Number.isFinite(totalSeconds) || totalSeconds <= 0) {\n            return value;\n        }\n        const hours = Math.floor(totalSeconds / 3600);\n        const minutes = Math.floor((totalSeconds % 3600) / 60);\n        const seconds = totalSeconds % 60;\n        const pad = (n) => String(n).padStart(2, \"0\");\n\n        if (hours > 0) {\n            return `${hours}:${pad(minutes)}:${pad(seconds)}`;\n        }\n        return `${minutes}:${pad(seconds)}`;\n    }\n    /**\n     * Utility method,to convert 'XmYs', Xm, Ys to seconds for vimeo platform.\n     *\n     * @param {string} value - The start time in 'XmYs' type format.\n     * @returns {string} - The start time in seconds.\n     */\n    parseTimeToSeconds(value) {\n        const match = value?.match(/^(?:(\\d+)m(\\d+)s|(\\d+)m|(\\d+)s|(\\d+))$/);\n        if (!match) {\n            return value;\n        }\n        let minutes = match[1] || match[3];\n        minutes = parseInt(minutes || \"0\", 10);\n        let seconds = match[2] || match[4] || match[5];\n        seconds = parseInt(seconds || \"0\", 10);\n        return String(minutes * 60 + seconds);\n    }\n}\n", "import { patch } from \"@web/core/utils/patch\";\nimport { ImageSelector as HtmlImageSelector } from \"@html_editor/main/media/media_dialog/image_selector\";\n\npatch(HtmlImageSelector.prototype, {\n    get attachmentsDomain() {\n        const domain = super.attachmentsDomain;\n        domain.push(\"|\", [\"url\", \"=\", false], \"!\", [\"url\", \"=like\", \"/web/image/website.%\"]);\n        domain.push([\"key\", \"=\", false]);\n        return domain;\n    },\n});\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { patch } from \"@web/core/utils/patch\";\nimport { KeepLast } from \"@web/core/utils/concurrency\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { ImageSelector } from \"@html_editor/main/media/media_dialog/image_selector\";\n\nimport { UnsplashError } from \"../unsplash_error/unsplash_error\";\nimport { useState } from \"@odoo/owl\";\n\npatch(ImageSelector.prototype, {\n    setup() {\n        super.setup();\n        this.unsplash = useService(\"unsplash\");\n        this.keepLastUnsplash = new KeepLast();\n        this.unsplashState = useState({\n            unsplashRecords: [],\n            isFetchingUnsplash: false,\n            isMaxed: false,\n            unsplashError: null,\n            useUnsplash: true,\n        });\n\n        this.NUMBER_OF_RECORDS_TO_DISPLAY = 30;\n\n        this.errorMessages = {\n            key_not_found: {\n                title: _t(\"Setup Unsplash to access royalty free photos.\"),\n                subtitle: \"\",\n            },\n            401: {\n                title: _t(\"Unauthorized Key\"),\n                subtitle: _t(\"Please check your Unsplash access key and application ID.\"),\n            },\n            403: {\n                title: _t(\"Search is temporarily unavailable\"),\n                subtitle: _t(\n                    \"The max number of searches is exceeded. Please retry in an hour or extend to a better account.\"\n                ),\n            },\n        };\n    },\n\n    get canLoadMore() {\n        if (this.state.searchService === \"all\") {\n            return (\n                super.canLoadMore ||\n                (this.state.needle &&\n                    !this.unsplashState.isMaxed &&\n                    !this.unsplashState.unsplashError)\n            );\n        } else if (this.state.searchService === \"unsplash\") {\n            return (\n                this.state.needle &&\n                !this.unsplashState.isMaxed &&\n                !this.unsplashState.unsplashError\n            );\n        }\n        return super.canLoadMore;\n    },\n\n    get hasContent() {\n        if (this.state.searchService === \"all\") {\n            return super.hasContent || !!this.unsplashState.unsplashRecords.length;\n        } else if (this.state.searchService === \"unsplash\") {\n            return !!this.unsplashState.unsplashRecords.length;\n        }\n        return super.hasContent;\n    },\n\n    get errorTitle() {\n        if (this.errorMessages[this.unsplashState.unsplashError]) {\n            return this.errorMessages[this.unsplashState.unsplashError].title;\n        }\n        return _t(\"Something went wrong\");\n    },\n\n    get errorSubtitle() {\n        if (this.errorMessages[this.unsplashState.unsplashError]) {\n            return this.errorMessages[this.unsplashState.unsplashError].subtitle;\n        }\n        return _t(\"Please check your internet connection or contact administrator.\");\n    },\n\n    get selectedRecordIds() {\n        return this.props.selectedMedia[this.props.id]\n            .filter((media) => media.mediaType === \"unsplashRecord\")\n            .map(({ id }) => id);\n    },\n\n    get isFetching() {\n        return super.isFetching || this.unsplashState.isFetchingUnsplash;\n    },\n\n    get combinedRecords() {\n        /**\n         * Creates an array with alternating elements from two arrays.\n         *\n         * @param {Array} a\n         * @param {Array} b\n         * @returns {Array} alternating elements from a and b, starting with\n         *     an element of a\n         */\n        function alternate(a, b) {\n            return [a.map((v, i) => (i < b.length ? [v, b[i]] : v)), b.slice(a.length)].flat(2);\n        }\n        return alternate(this.unsplashState.unsplashRecords, this.state.libraryMedia);\n    },\n\n    get allAttachments() {\n        return [...super.allAttachments, ...this.unsplashState.unsplashRecords];\n    },\n\n    async fetchUnsplashRecords(offset) {\n        if (!this.state.needle) {\n            return { records: [], isMaxed: false };\n        }\n        this.unsplashState.isFetchingUnsplash = true;\n        try {\n            const { isMaxed, images } = await this.unsplash.getImages(\n                this.state.needle,\n                offset,\n                this.NUMBER_OF_RECORDS_TO_DISPLAY,\n                this.props.orientation\n            );\n            this.unsplashState.isFetchingUnsplash = false;\n            this.unsplashState.unsplashError = false;\n            // Use a set to keep track of every image we've received so far,\n            // based on their ids. This will allow us to ignore duplicate\n            // images from Unsplash. We can assume there are no duplicates at\n            // this point as a precondition.\n            const existingIds = new Set(this.unsplashState.unsplashRecords.map(r => r.id));\n            const newImages = images.filter(record => {\n                if (existingIds.has(record.id)) {\n                    return false;\n                }\n                // Mark this image as seen so that we can ignore any duplicates\n                // from the same Unsplash batch.\n                existingIds.add(record.id);\n                return true;\n            });\n            const records = newImages.map((record) => {\n                const url = new URL(record.urls.regular);\n                // In small windows, row height could get quite a bit larger than the min, so we keep some leeway.\n                url.searchParams.set(\"h\", 2 * this.MIN_ROW_HEIGHT);\n                url.searchParams.delete(\"w\");\n                return Object.assign({}, record, {\n                    url: url.toString(),\n                    mediaType: \"unsplashRecord\",\n                });\n            });\n            return { isMaxed, records };\n        } catch (e) {\n            this.unsplashState.isFetchingUnsplash = false;\n            if (e === \"no_access\") {\n                this.unsplashState.useUnsplash = false;\n            } else {\n                this.unsplashState.unsplashError = e;\n            }\n            return { records: [], isMaxed: true };\n        }\n    },\n\n    async loadMore(...args) {\n        await super.loadMore(...args);\n        return this.keepLastUnsplash\n            .add(this.fetchUnsplashRecords(this.unsplashState.unsplashRecords.length))\n            .then(({ records, isMaxed }) => {\n                // This is never reached if another search or loadMore occurred.\n                this.unsplashState.unsplashRecords.push(...records);\n                this.unsplashState.isMaxed = isMaxed;\n            });\n    },\n\n    async search(...args) {\n        await super.search(...args);\n        await this.searchUnsplash();\n    },\n\n    async searchUnsplash() {\n        if (!this.state.needle) {\n            this.unsplashState.unsplashError = false;\n            this.unsplashState.unsplashRecords = [];\n            this.unsplashState.isMaxed = false;\n        }\n        return this.keepLastUnsplash\n            .add(this.fetchUnsplashRecords(0))\n            .then(({ records, isMaxed }) => {\n                // This is never reached if a new search occurred.\n                this.unsplashState.unsplashRecords = records;\n                this.unsplashState.isMaxed = isMaxed;\n            });\n    },\n\n    async onClickRecord(media) {\n        this.props.selectMedia({ ...media, mediaType: \"unsplashRecord\", query: this.state.needle });\n        if (!this.props.multiSelect) {\n            await this.props.save();\n        }\n    },\n\n    async submitCredentials(key, appId) {\n        this.unsplashState.unsplashError = null;\n        await rpc(\"/web_unsplash/save_unsplash\", { key, appId });\n        await this.searchUnsplash();\n    },\n});\n\nImageSelector.components = {\n    ...ImageSelector.components,\n    UnsplashError,\n};\n", "import { MediaDialog, TABS } from \"@html_editor/main/media/media_dialog/media_dialog\";\nimport { patch } from \"@web/core/utils/patch\";\nimport { useService } from \"@web/core/utils/hooks\";\n\npatch(MediaDialog.prototype, {\n    setup() {\n        super.setup();\n        this.unsplashService = useService(\"unsplash\");\n    },\n\n    async save() {\n        const selectedImages = this.selectedMedia[TABS.IMAGES.id];\n        if (selectedImages) {\n            const unsplashRecords = selectedImages.filter(\n                (media) => media.mediaType === \"unsplashRecord\"\n            );\n            if (unsplashRecords.length) {\n                await this.unsplashService.uploadUnsplashRecords(\n                    unsplashRecords,\n                    { resModel: this.props.resModel, resId: this.props.resId },\n                    (attachments) => {\n                        this.selectedMedia[TABS.IMAGES.id] = this.selectedMedia[\n                            TABS.IMAGES.id\n                        ].filter((media) => media.mediaType !== \"unsplashRecord\");\n                        this.selectedMedia[TABS.IMAGES.id] = this.selectedMedia[\n                            TABS.IMAGES.id\n                        ].concat(\n                            attachments.map((attachment) => ({\n                                ...attachment,\n                                mediaType: \"attachment\",\n                            }))\n                        );\n                    }\n                );\n            }\n        }\n        return super.save(...arguments);\n    },\n});\n", "import { Component, useState } from \"@odoo/owl\";\n\nexport class UnsplashCredentials extends Component {\n    static template = \"web_unsplash.UnsplashCredentials\";\n    static props = {\n        submitCredentials: Function,\n        hasCredentialsError: Boolean,\n    };\n    setup() {\n        this.state = useState({\n            key: \"\",\n            appId: \"\",\n            hasKeyError: this.props.hasCredentialsError,\n            hasAppIdError: this.props.hasCredentialsError,\n        });\n    }\n\n    submitCredentials() {\n        if (this.state.key === \"\") {\n            this.state.hasKeyError = true;\n        } else if (this.state.appId === \"\") {\n            this.state.hasAppIdError = true;\n        } else {\n            this.props.submitCredentials(this.state.key, this.state.appId);\n        }\n    }\n}\n", "import { Component } from \"@odoo/owl\";\nimport { UnsplashCredentials } from \"../unsplash_credentials/unsplash_credentials\";\n\nexport class UnsplashError extends Component {\n    static template = \"web_unsplash.UnsplashError\";\n    static components = {\n        UnsplashCredentials,\n    };\n    static props = {\n        title: String,\n        subtitle: String,\n        showCredentials: Boolean,\n        submitCredentials: { type: Function, optional: true },\n        hasCredentialsError: { type: Boolean, optional: true },\n    };\n}\n", "import { rpc } from \"@web/core/network/rpc\";\nimport { registry } from \"@web/core/registry\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { AUTOCLOSE_DELAY } from \"@html_editor/main/media/media_dialog/upload_progress_toast/upload_service\";\n\nexport const unsplashService = {\n    dependencies: [\"upload\"],\n    async start(env, { upload }) {\n        const _cache = {};\n        return {\n            async uploadUnsplashRecords(records, { resModel, resId }, onUploaded) {\n                upload.incrementId();\n                const file = upload.addFile({\n                    id: upload.fileId,\n                    name:\n                        records.length > 1\n                            ? _t(\"Uploading %(count)s '%(query)s' images.\", {\n                                  count: records.length,\n                                  query: records[0].query,\n                              })\n                            : _t(\"Uploading '%s' image.\", records[0].query),\n                });\n\n                try {\n                    const urls = {};\n                    for (const record of records) {\n                        const _1920Url = new URL(record.urls.regular);\n                        _1920Url.searchParams.set(\"w\", \"1920\");\n                        urls[record.id] = {\n                            url: _1920Url.href,\n                            download_url: record.links.download_location,\n                            description: record.alt_description,\n                        };\n                    }\n\n                    const xhr = new XMLHttpRequest();\n                    xhr.upload.addEventListener(\"progress\", (ev) => {\n                        const rpcComplete = (ev.loaded / ev.total) * 100;\n                        file.progress = rpcComplete;\n                    });\n                    xhr.upload.addEventListener(\"load\", function () {\n                        // Don't show yet success as backend code only starts now\n                        file.progress = 100;\n                    });\n                    const attachments = await rpc(\n                        \"/web_unsplash/attachment/add\",\n                        {\n                            res_id: resId,\n                            res_model: resModel,\n                            unsplashurls: urls,\n                            query: records[0].query,\n                        },\n                        { xhr }\n                    );\n\n                    if (attachments.error) {\n                        file.hasError = true;\n                        file.errorMessage = attachments.error;\n                    } else {\n                        file.uploaded = true;\n                        await onUploaded(attachments);\n                    }\n                    setTimeout(() => upload.deleteFile(file.id), AUTOCLOSE_DELAY);\n                } catch (error) {\n                    file.hasError = true;\n                    setTimeout(() => upload.deleteFile(file.id), AUTOCLOSE_DELAY);\n                    throw error;\n                }\n            },\n\n            async getImages(query, offset = 0, pageSize = 30, orientation) {\n                const from = offset;\n                const to = offset + pageSize;\n                // Use orientation in the cache key to not show images in cache\n                // when using the same query word but changing the orientation\n                let cachedData = orientation ? _cache[query + orientation] : _cache[query];\n\n                if (\n                    cachedData &&\n                    (cachedData.images.length >= to ||\n                        (cachedData.totalImages !== 0 && cachedData.totalImages < to))\n                ) {\n                    return {\n                        images: cachedData.images.slice(from, to),\n                        isMaxed: to > cachedData.totalImages,\n                    };\n                }\n                cachedData = await this._fetchImages(query, orientation);\n                return {\n                    images: cachedData.images.slice(from, to),\n                    isMaxed: to > cachedData.totalImages,\n                };\n            },\n            /**\n             * Fetches images from unsplash and stores it in cache\n             */\n            async _fetchImages(query, orientation) {\n                const key = orientation ? query + orientation : query;\n                if (!_cache[key]) {\n                    _cache[key] = {\n                        images: [],\n                        maxPages: 0,\n                        totalImages: 0,\n                        pageCached: 0,\n                    };\n                }\n                const cachedData = _cache[key];\n                const payload = {\n                    query: query,\n                    page: cachedData.pageCached + 1,\n                    per_page: 30, // max size from unsplash API\n                };\n                if (orientation) {\n                    payload.orientation = orientation;\n                }\n                const result = await rpc(\"/web_unsplash/fetch_images\", payload);\n                if (result.error) {\n                    return Promise.reject(result.error);\n                }\n                cachedData.pageCached++;\n                cachedData.images.push(...result.results);\n                cachedData.maxPages = result.total_pages;\n                cachedData.totalImages = result.total;\n                return cachedData;\n            },\n        };\n    },\n};\n\nregistry.category(\"services\").add(\"unsplash\", unsplashService);\n", "import {\n    Component,\n    markup,\n    onMounted,\n    onWillStart,\n    onWillUnmount,\n    onWillUpdateProps,\n    useEffect,\n    useRef,\n    useState,\n} from \"@odoo/owl\";\nimport { getBundle } from \"@web/core/assets\";\nimport { memoize } from \"@web/core/utils/functions\";\nimport { fillClipboardData } from \"@html_editor/utils/clipboard\";\nimport { fixInvalidHTML, instanceofMarkup } from \"@html_editor/utils/sanitize\";\nimport { HtmlUpgradeManager } from \"@html_editor/html_migrations/html_upgrade_manager\";\nimport { TableOfContentManager } from \"@html_editor/others/embedded_components/core/table_of_content/table_of_content_manager\";\nimport { getDeepestPosition } from \"@html_editor/utils/dom_info\";\n\nexport class HtmlViewer extends Component {\n    static template = \"html_editor.HtmlViewer\";\n    static props = {\n        config: { type: Object },\n        migrateHTML: { type: Boolean, optional: true },\n    };\n    static defaultProps = {\n        migrateHTML: true,\n    };\n\n    setup() {\n        this._cleanups = [];\n        this.htmlUpgradeManager = new HtmlUpgradeManager();\n        this.iframeRef = useRef(\"iframe\");\n\n        this.state = useState({\n            iframeVisible: false,\n            value: this.formatValue(this.props.config.value),\n        });\n        this.components = new Set();\n\n        onWillUpdateProps((newProps) => {\n            const newValue = this.formatValue(newProps.config.value);\n            if (newValue.toString() !== this.state.value.toString()) {\n                this.state.value = this.formatValue(newProps.config.value);\n                if (this.props.config.embeddedComponents) {\n                    this.destroyComponents();\n                }\n                if (this.showIframe) {\n                    this.updateIframeContent(this.state.value);\n                }\n            }\n        });\n\n        onWillUnmount(() => {\n            this.destroyComponents();\n        });\n\n        if (this.showIframe) {\n            onMounted(() => {\n                const onLoadIframe = () => this.onLoadIframe(this.state.value);\n                this.iframeRef.el.addEventListener(\"load\", onLoadIframe, { once: true });\n                // Force the iframe to call the `load` event. Without this line, the\n                // event 'load' might never trigger.\n                this.iframeRef.el.after(this.iframeRef.el);\n            });\n        } else {\n            this.readonlyElementRef = useRef(\"readonlyContent\");\n            useEffect(\n                () => {\n                    this.processReadonlyContent(this.readonlyElementRef.el);\n                },\n                () => [this.props.config.value.toString(), this.readonlyElementRef?.el]\n            );\n        }\n\n        if (this.props.config.cssAssetId) {\n            onWillStart(async () => {\n                this.cssAsset = await getBundle(this.props.config.cssAssetId);\n            });\n        }\n\n        if (this.props.config.embeddedComponents) {\n            // TODO @phoenix: should readonly iframe with embedded components be supported?\n            this.embeddedComponents = memoize((embeddedComponents = []) => {\n                const result = {};\n                for (const embedding of embeddedComponents) {\n                    result[embedding.name] = embedding;\n                }\n                return result;\n            });\n            useEffect(\n                () => {\n                    if (this.readonlyElementRef?.el) {\n                        this.mountComponents();\n                    }\n                },\n                () => [this.props.config.value.toString(), this.readonlyElementRef?.el]\n            );\n            this.tocManager = new TableOfContentManager(this.readonlyElementRef);\n        }\n    }\n\n    addDomListener(target, eventName, fn, capture = false) {\n        const handler = (ev) => {\n            fn?.call(this, ev);\n        };\n        target.addEventListener(eventName, handler, capture);\n        this._cleanups.push(() => target.removeEventListener(eventName, handler, capture));\n    }\n\n    get showIframe() {\n        return this.props.config.hasFullHtml || this.props.config.cssAssetId;\n    }\n\n    /**\n     * Allows overrides to process the value used in the Html Viewer.\n     * Typically, if the value comes from the html_field, it is already fixed\n     * (invalid and obsolete elements were replaced). If used as a standalone,\n     * the HtmlViewer has to handle invalid nodes and html upgrades.\n     *\n     * @param { string | Markup } value\n     * @returns { string | Markup }\n     */\n    formatValue(value) {\n        let newVal = fixInvalidHTML(value);\n        if (this.props.migrateHTML) {\n            newVal = this.htmlUpgradeManager.processForUpgrade(newVal, {\n                containsComplexHTML: this.props.config.hasFullHtml,\n                env: this.env,\n            });\n        }\n        if (instanceofMarkup(value)) {\n            return markup(newVal);\n        }\n        return newVal;\n    }\n\n    processReadonlyContent(container) {\n        this.retargetLinks(container);\n        this.applyAccessibilityAttributes(container);\n        this.addDomListener(container, \"copy\", this.onCopy);\n    }\n\n    /**\n     * @param {ClipboardEvent} ev\n     */\n    onCopy(ev) {\n        ev.preventDefault();\n        const selection = ev.target.ownerDocument.defaultView.getSelection();\n        const [deepAnchorNode, deepAnchorOffset] = getDeepestPosition(\n            selection.anchorNode,\n            selection.anchorOffset\n        );\n        const [deepFocusNode, deepFocusOffset] = getDeepestPosition(\n            selection.focusNode,\n            selection.focusOffset\n        );\n\n        const range = new Range();\n        range.setStart(deepAnchorNode, deepAnchorOffset);\n        range.setEnd(deepFocusNode, deepFocusOffset);\n        const clonedContents = range.cloneContents();\n        fillClipboardData(ev, clonedContents);\n    }\n\n    /**\n     * Ensure that elements with accessibility editor attributes correctly get\n     * the standard accessibility attribute (aria-label, role).\n     */\n    applyAccessibilityAttributes(container) {\n        for (const el of container.querySelectorAll(\"[data-oe-role]\")) {\n            el.setAttribute(\"role\", el.dataset.oeRole);\n        }\n        for (const el of container.querySelectorAll(\"[data-oe-aria-label]\")) {\n            el.setAttribute(\"aria-label\", el.dataset.oeAriaLabel);\n        }\n    }\n\n    /**\n     * Ensure all links are opened in a new tab.\n     */\n    retargetLinks(container) {\n        for (const link of container.querySelectorAll(\"a\")) {\n            this.retargetLink(link);\n        }\n    }\n\n    retargetLink(link) {\n        link.setAttribute(\"target\", \"_blank\");\n        link.setAttribute(\"rel\", \"noreferrer\");\n    }\n\n    updateIframeContent(content) {\n        const contentWindow = this.iframeRef.el.contentWindow;\n        const iframeTarget = this.props.config.hasFullHtml\n            ? contentWindow.document.documentElement\n            : contentWindow.document.querySelector(\"#iframe_target\");\n        iframeTarget.innerHTML = content;\n        this.processReadonlyContent(iframeTarget);\n    }\n\n    onLoadIframe(value) {\n        const contentWindow = this.iframeRef.el.contentWindow;\n        if (!this.props.config.hasFullHtml) {\n            contentWindow.document.open(\"text/html\", \"replace\").write(\n                `<!DOCTYPE html><html>\n                        <head>\n                            <meta charset=\"utf-8\"/>\n                            <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\"/>\n                            <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, user-scalable=no\"/>\n                        </head>\n                        <body class=\"o_in_iframe o_readonly\" style=\"overflow: hidden;\">\n                            <div id=\"iframe_target\"></div>\n                        </body>\n                    </html>`\n            );\n        }\n\n        if (this.cssAsset) {\n            for (const cssLib of this.cssAsset.cssLibs) {\n                const link = contentWindow.document.createElement(\"link\");\n                link.setAttribute(\"type\", \"text/css\");\n                link.setAttribute(\"rel\", \"stylesheet\");\n                link.setAttribute(\"href\", cssLib);\n                contentWindow.document.head.append(link);\n            }\n        }\n\n        this.updateIframeContent(this.state.value);\n        this.state.iframeVisible = true;\n    }\n\n    //--------------------------------------------------------------------------\n    // Embedded Components\n    //--------------------------------------------------------------------------\n\n    destroyComponent({ root, host }) {\n        const { getEditableDescendants } = this.getEmbedding(host);\n        const editableDescendants = getEditableDescendants?.(host) || {};\n        root.destroy();\n        this.components.delete(arguments[0]);\n        host.append(...Object.values(editableDescendants));\n    }\n\n    destroyComponents() {\n        for (const cleanup of this._cleanups) {\n            cleanup();\n        }\n        for (const info of [...this.components]) {\n            this.destroyComponent(info);\n        }\n    }\n\n    forEachEmbeddedComponentHost(elem, callback) {\n        const selector = `[data-embedded]`;\n        const targets = [...elem.querySelectorAll(selector)];\n        if (elem.matches(selector)) {\n            targets.unshift(elem);\n        }\n        for (const host of targets) {\n            const embedding = this.getEmbedding(host);\n            if (!embedding) {\n                continue;\n            }\n            callback(host, embedding);\n        }\n    }\n\n    getEmbedding(host) {\n        return this.embeddedComponents(this.props.config.embeddedComponents)[host.dataset.embedded];\n    }\n\n    setupNewComponent({ name, env, props }) {\n        if (name === \"tableOfContent\") {\n            Object.assign(props, {\n                manager: this.tocManager,\n            });\n        }\n    }\n\n    mountComponent(host, { Component, getEditableDescendants, getProps, name }) {\n        const props = getProps?.(host) || {};\n        // TODO ABD TODO @phoenix: check if there is too much info in the htmlViewer env.\n        // i.e.: env has X because of parent component,\n        // embedded component descendant sometimes uses X from env which is set conditionally:\n        // -> it will override the one one from the parent => OK.\n        // -> it will not => the embedded component still has X in env because of its ancestors => Issue.\n        const env = Object.create(this.env);\n        if (getEditableDescendants) {\n            env.getEditableDescendants = getEditableDescendants;\n        }\n        this.setupNewComponent({\n            name,\n            env,\n            props,\n        });\n        const root = this.__owl__.app.createRoot(Component, {\n            props,\n            env,\n        });\n        const promise = root.mount(host);\n        // Don't show mounting errors as they will happen often when the host\n        // is disconnected from the DOM because of a patch\n        promise.catch();\n        // Patch mount fiber to hook into the exact call stack where root is\n        // mounted (but before). This will remove host children synchronously\n        // just before adding the root rendered html.\n        const fiber = root.node.fiber;\n        const fiberComplete = fiber.complete;\n        fiber.complete = function () {\n            host.replaceChildren();\n            fiberComplete.call(this);\n        };\n        const info = {\n            root,\n            host,\n        };\n        this.components.add(info);\n    }\n\n    mountComponents() {\n        this.forEachEmbeddedComponentHost(this.readonlyElementRef.el, (host, embedding) => {\n            this.mountComponent(host, embedding);\n        });\n    }\n}\n", "import { Component } from \"@odoo/owl\";\nimport { MainComponentsContainer } from \"@web/core/main_components_container\";\nimport { useForwardRefToParent } from \"@web/core/utils/hooks\";\nimport { registry } from \"@web/core/registry\";\nimport { useRegistry } from \"@web/core/registry_hook\";\n\n/**\n * TODO ABD: refactor to propagate a reactive object instead of using a registry with an identifier\n */\nexport class LocalOverlayContainer extends MainComponentsContainer {\n    static template = \"html_editor.LocalOverlayContainer\";\n    static props = {\n        localOverlay: { type: Function, optional: true },\n        identifier: { type: String, optional: true },\n    };\n    static defaultProps = {\n        identifier: \"overlay_components\",\n    };\n\n    setup() {\n        const overlayComponents = registry.category(this.props.identifier);\n        // todo: remove this somehow\n        if (!overlayComponents.validationSchema) {\n            overlayComponents.addValidation({\n                Component: { validate: (c) => c.prototype instanceof Component },\n                props: { type: Object, optional: true },\n            });\n        }\n        this.Components = useRegistry(overlayComponents);\n        useForwardRefToParent(\"localOverlay\");\n    }\n}\n", "import { ancestors } from \"@html_editor/utils/dom_traversal\";\nimport { throttleForAnimation } from \"@web/core/utils/timing\";\nimport { couldBeScrollableX, couldBeScrollableY } from \"@web/core/utils/scrolling\";\nimport { useComponent, useEffect } from \"@odoo/owl\";\n\n/**\n * This hook has the same job as the PositionPlugin, but for Components.\n * It was created to be used within the Html Viewer and still have overlays.\n *\n * TODO ABD: refactor html viewer to: either use a plugin system, or generalize\n * the positioning logic so that both the plugin and the hook can use it.\n */\nexport function usePositionHook(containerRef, document, callback) {\n    const comp = useComponent();\n    const onLayoutGeometryChange = throttleForAnimation(callback.bind(comp));\n    const resizeObserver = new ResizeObserver(onLayoutGeometryChange);\n    const cleanups = [];\n    const addDomListener = (target, eventName, capture) => {\n        target.addEventListener(eventName, onLayoutGeometryChange, capture);\n        cleanups.push(() => target.removeEventListener(eventName, onLayoutGeometryChange, capture));\n    };\n    useEffect(\n        () => {\n            if (containerRef.el) {\n                resizeObserver.observe(document.body);\n                resizeObserver.observe(containerRef.el);\n                addDomListener(window, \"resize\");\n                if (document.defaultView !== window) {\n                    addDomListener(document.defaultView, \"resize\");\n                }\n                addDomListener(document, \"scroll\");\n                const scrollableElements = [containerRef.el, ...ancestors(containerRef.el)].filter(\n                    (node) => couldBeScrollableX(node) || couldBeScrollableY(node)\n                );\n                for (const scrollableElement of scrollableElements) {\n                    addDomListener(scrollableElement, \"scroll\");\n                    resizeObserver.observe(scrollableElement);\n                }\n            }\n            return () => {\n                resizeObserver.disconnect();\n                for (const cleanup of cleanups.toReversed()) {\n                    cleanup();\n                    cleanups.pop();\n                }\n            };\n        },\n        () => [containerRef.el]\n    );\n}\n", "import { registry } from \"@web/core/registry\";\n\nexport function htmlEditorVersions() {\n    return Object.keys(registry.category(\"html_editor_upgrade\").subRegistries).sort(\n        compareVersions\n    );\n}\n\nexport const VERSION_SELECTOR = \"[data-oe-version]\";\n\nexport function stripVersion(element) {\n    element.querySelectorAll(VERSION_SELECTOR).forEach((el) => {\n        delete el.dataset.oeVersion;\n    });\n}\n\n/**\n * Compare 2 versions\n *\n * @param {string} version1\n * @param {string} version2\n * @returns {number} -1 if version1 < version2\n *                   0 if version1 === version2\n *                   1 if version1 > version2\n */\nexport function compareVersions(version1, version2) {\n    version1 = version1.split(\".\").map((v) => parseInt(v));\n    version2 = version2.split(\".\").map((v) => parseInt(v));\n    if (version1[0] < version2[0] || (version1[0] === version2[0] && version1[1] < version2[1])) {\n        return -1;\n    } else if (version1[0] === version2[0] && version1[1] === version2[1]) {\n        return 0;\n    } else {\n        return 1;\n    }\n}\n", "import { markup } from \"@odoo/owl\";\nimport {\n    compareVersions,\n    VERSION_SELECTOR,\n    htmlEditorVersions,\n} from \"@html_editor/html_migrations/html_migrations_utils\";\nimport { registry } from \"@web/core/registry\";\nimport { fixInvalidHTML } from \"@html_editor/utils/sanitize\";\n\n/**\n * Handle HTML transformations dependent on the current implementation of the\n * editor and its plugins for HtmlField values that were not upgraded through\n * conventional means (python upgrade script), i.e. modify obsolete\n * classes/style, convert deprecated Knowledge Behaviors to their\n * EmbeddedComponent counterparts, ...\n *\n * How to use:\n * - Create a file to export a `migrate(element, env)` function which applies\n *   the necessary modifications inside `element` related to a specific version:\n *    - HTMLElement `element`: a container for the HtmlField value\n *    - Object `env`: the typical `owl` environment (can be used to check\n *      the current record data, use a service, ...).\n * !!!  ALWAYS assume that the `env` may not have the resource used in your\n *      migrate function and adjust accordingly.\n * - Refer to that file in the `html_editor_upgrade` registry, in the version\n *   category related to your change: `major.minor` (bump major for a change in\n *   master, and minor for a change in stable), in a sub-category related to\n *   your module.\n *   Example for the version 1.1 in `html_editor`:\n *   `registry\n *        .category(\"html_editor_upgrade\")\n *        .category(\"1.1\")\n *        .add(\"html_editor\", \"@html_editor/html_migrations/migration-1.1\")`\n */\nexport class HtmlUpgradeManager {\n    constructor() {\n        this.upgradeRegistry = registry.category(\"html_editor_upgrade\");\n        this.parser = new DOMParser();\n        this.originalValue = undefined;\n        this.upgradedValue = undefined;\n        this.element = undefined;\n        this.env = {};\n    }\n\n    get value() {\n        return this.upgradedValue;\n    }\n\n    processForUpgrade(value, { containsComplexHTML, env } = {}) {\n        this.env = env || {};\n        this.containsComplexHTML = containsComplexHTML;\n        const strValue = value.toString();\n        if (\n            strValue === this.originalValue?.toString() ||\n            strValue === this.upgradedValue?.toString()\n        ) {\n            return this.value;\n        }\n        this.originalValue = value;\n        this.upgradedValue = value;\n        this.element = this.parser.parseFromString(fixInvalidHTML(value), \"text/html\")[\n            this.containsComplexHTML ? \"documentElement\" : \"body\"\n        ];\n        const versionNode = this.element.querySelector(VERSION_SELECTOR);\n        const version = versionNode?.dataset.oeVersion || \"0.0\";\n        const VERSIONS = htmlEditorVersions();\n        const currentVersion = VERSIONS.at(-1);\n        if (!currentVersion || version === currentVersion) {\n            return this.value;\n        }\n        try {\n            const upgradeSequence = VERSIONS.filter(\n                (subVersion) =>\n                    // skip already applied versions\n                    compareVersions(subVersion, version) > 0\n            );\n            this.upgradedValue = this.upgrade(upgradeSequence);\n        } catch {\n            // If an upgrade fails, silently continue to use the raw value.\n        }\n        return this.value;\n    }\n\n    upgrade(upgradeSequence) {\n        for (const version of upgradeSequence) {\n            const modules = this.upgradeRegistry.category(version);\n            for (const [key, module] of modules.getEntries()) {\n                const migrate = odoo.loader.modules.get(module).migrate;\n                if (!migrate) {\n                    console.error(\n                        `A \"${key}\" migrate function could not be found at \"${module}\" or it did not load.`\n                    );\n                }\n                migrate(this.element, this.env);\n            }\n        }\n        return markup(this.element[this.containsComplexHTML ? \"outerHTML\" : \"innerHTML\"]);\n    }\n}\n", "import { registry } from \"@web/core/registry\";\n\n// See `HtmlUpgradeManager` docstring for usage details.\nconst html_upgrade = registry.category(\"html_editor_upgrade\");\n\n// Introduction of embedded components based on Knowledge Behaviors (Odoo 18).\nhtml_upgrade.category(\"1.0\");\n\n// Remove the Excalidraw EmbeddedComponent and replace it with a link.\nhtml_upgrade.category(\"1.1\").add(\"html_editor\", \"@html_editor/html_migrations/migration-1.1\");\n\n// Fix Banner classes to properly handle `contenteditable` attribute\nhtml_upgrade.category(\"1.2\").add(\"html_editor\", \"@html_editor/html_migrations/migration-1.2\");\n\n// Knowledge embeddedViews favorite irFilters should have a `user_ids` property.\nhtml_upgrade.category(\"2.0\");\n", "/**\n * Remove the Excalidraw EmbeddedComponent and replace it with a link\n *\n * @param {HTMLElement} container\n * @param {Object} env\n */\nexport function migrate(container) {\n    const excalidrawContainers = container.querySelectorAll(\"[data-embedded='draw']\");\n    for (const excalidrawContainer of excalidrawContainers) {\n        const source = JSON.parse(excalidrawContainer.dataset.embeddedProps).source;\n        const newParagraph = document.createElement(\"P\");\n        const anchor = document.createElement(\"A\");\n        newParagraph.append(anchor);\n        anchor.append(document.createTextNode(source));\n        anchor.href = source;\n        excalidrawContainer.after(newParagraph);\n        excalidrawContainer.remove();\n    }\n}\n", "import { _t } from \"@web/core/l10n/translation\";\n\nconst ARIA_LABELS = {\n    \".o_editor_banner.alert-danger\": _t(\"Banner Danger\"),\n    \".o_editor_banner.alert-info\": _t(\"Banner Info\"),\n    \".o_editor_banner.alert-success\": _t(\"Banner Success\"),\n    \".o_editor_banner.alert-warning\": _t(\"Banner Warning\"),\n};\n\nfunction getAriaLabel(element) {\n    for (const [selector, ariaLabel] of Object.entries(ARIA_LABELS)) {\n        if (element.matches(selector)) {\n            return ariaLabel;\n        }\n    }\n}\n\n/**\n * Replace the `o_editable` and `o_not_editable` on `banner` elements by\n * `o-contenteditable-true` and `o-content-editable-false`.\n * Add `o_editor_banner_content` to the content parent element.\n * Add accessibility editor-specific attributes (data-oe-role and\n * data-oe-aria-label).\n *\n * @param {HTMLElement} container\n */\nexport function migrate(container) {\n    const bannerContainers = container.querySelectorAll(\".o_editor_banner\");\n    for (const bannerContainer of bannerContainers) {\n        bannerContainer.classList.remove(\"o_not_editable\");\n        bannerContainer.classList.add(\"o-contenteditable-false\");\n        bannerContainer.dataset.oeRole = \"status\";\n        const icon = bannerContainer.querySelector(\".o_editor_banner_icon\");\n        if (icon) {\n            const ariaLabel = getAriaLabel(bannerContainer);\n            if (ariaLabel) {\n                icon.dataset.oeAriaLabel = ariaLabel;\n            }\n        }\n        const bannerContent = bannerContainer.querySelector(\".o_editor_banner_icon ~ div\");\n        if (bannerContent) {\n            bannerContent.classList.remove(\"o_editable\");\n            bannerContent.classList.add(\"o_editor_banner_content\");\n            bannerContent.classList.add(\"o-contenteditable-true\");\n        }\n    }\n}\n", "import {\n    onMounted,\n    onRendered,\n    onPatched,\n    onWillDestroy,\n    reactive,\n    toRaw,\n    useComponent,\n    useRef,\n    useState,\n} from \"@odoo/owl\";\n\n/**\n * @typedef {HTMLElement} HostElement host element for an embedded component\n * @typedef {Object} State state obtained from `useState` usage\n * @typedef {Record<string, HTMLElement>} EditableDescendants\n * @typedef {(state, previous, next) => void} PropertyUpdate function applying\n *          a state change which can be computed from `previous` and `next`\n *          to `state`.\n * @typedef {Record<string, PropertyUpdate>} PropertyUpdater\n *\n * @typedef {Object} StateChangeManagerConfig\n * @property {PropertyUpdater} [propertyUpdater] object mapping a key of the\n *        state to a function which will compute how values from a stateChange\n *        are applied to the current state. Defined in the embedding definition\n *        of a component.\n * @property {function(HostElement):State} [getEmbeddedState]\n *        custom function to get the first embedded state (the one used during\n *        setup), in case not all embedded props should be part of the state, or\n *        if more properties should be added to it.\n * @property {function(HostElement, State):Object} [stateToEmbeddedProps]\n *        custom function to compute the props, i.e. in case the entire state\n *        should not be converted to props.\n *\n * @typedef {Object} Embedding object provided to the instance which mounts\n *          Embedded components (EmbeddedComponentPlugin, HtmlViewer, ...)\n * @property {String} name\n * @property {Component} Component\n * @property {function(HostElement):Object} getProps props for the given\n *           Component class instance.\n * @property {function(HostElement):EditableDescendants} [getEditableDescendants]\n *           @see useEditableDescendants\n * @property {function(StateChangeManagerConfig):StateChangeManager} [getStateChangeManager]\n *           @see useEmbeddedState\n */\n\n/**\n * Get all element children with `data-embedded-editable` attribute which are\n * descendants of the host's own embedded component and not part of another\n * embedded component descendant (an embedded component can contain others).\n * If multiple elements have the same `data-embedded-editable`, only the last\n * one is considered.\n * @param {HostElement} host\n * @returns {EditableDescendants} editableDescendants\n */\nexport function getEditableDescendants(host) {\n    const editableDescendants = {};\n    for (const candidate of host.querySelectorAll(\"[data-embedded-editable]\")) {\n        if (candidate.closest(\"[data-embedded]\") === host) {\n            editableDescendants[candidate.dataset.embeddedEditable] = candidate;\n        }\n    }\n    return editableDescendants;\n}\n\n/**\n * Handle the rendering of editableDescendants:\n * It is a node owned by the editor, which will be inserted under a ref of\n * the same name as the attribute `data-embedded-editable` of that node, in the\n * component's template. This allows to use editor features inside an embedded\n * component. EditableDescendants are shared in collaboration and are saved\n * between edition sessions.\n *\n * Warning: there must be a ref in the template for every editableDescendants,\n * available at all times no matter the component state to guarantee that the\n * editor can save their values at any given time, synchronously.\n *\n * @param {HostElement} host\n * @returns {EditableDescendants} (HTMLElement) by the value of their\n *          `data-embedded-editable` attribute.\n */\nexport function useEditableDescendants(host) {\n    const component = useComponent();\n    if (!component.env.getEditableDescendants) {\n        throw new Error(\n            \"Missing `getEditableDescendants` function in the `embedding` provided to the `EmbeddedComponentPlugin`.\"\n        );\n    }\n    const editableDescendants = Object.freeze(component.env.getEditableDescendants(host));\n    const refs = {};\n    const renders = {};\n    for (const name of Object.keys(editableDescendants)) {\n        refs[name] = useRef(name);\n        renders[name] = () => refs[name].el.replaceChildren(editableDescendants[name]);\n    }\n    let _restoreSelection;\n    const restoreSelection = () => {\n        if (_restoreSelection) {\n            _restoreSelection();\n            _restoreSelection = undefined;\n        }\n    };\n    if (component.env.editorShared?.selection) {\n        onRendered(() => {\n            _restoreSelection = component.env.editorShared.selection.preserveSelection().restore;\n        });\n    }\n    onMounted(() => {\n        for (const render of Object.values(renders)) {\n            render();\n        }\n        restoreSelection();\n    });\n    onPatched(() => {\n        for (const [name, render] of Object.entries(renders)) {\n            // Handle partial patch\n            if (!host.contains(editableDescendants[name])) {\n                render();\n            }\n        }\n        restoreSelection();\n    });\n    return editableDescendants;\n}\n\n/**\n * Create a ProxyHandler to manage a serializable \"buffer\" (Proxy target) for\n * changes. The buffer must be a @see reactive which should update state\n * with its callback (commit).\n * @see useEmbeddedState\n * The Proxy target and state must be serializable through JSON.stringify.\n *\n * @param {Object} state\n * @param {Object} stateChangeManager\n * @param {Object} stateChangeManager.previousEmbeddedState null, or a deep copy\n *        of the target used as a reference point for comparison\n *        (before <-> after) so that multiple synchronous changes can be handled\n *        at once.\n * @returns {ProxyHandler}\n */\nfunction embeddedStateProxyHandler(state, stateChangeManager) {\n    return {\n        // Write operations are always done on the target (\"buffer\").\n        // During the first write operation before a commit, keep a deep copy of\n        // the target through serialization, which will be used as a reference\n        // point for a comparison (before <-> after).\n        set(target, key, value, receiver) {\n            if (\n                value !== Reflect.get(target, key, receiver) &&\n                !stateChangeManager.previousEmbeddedState\n            ) {\n                stateChangeManager.previousEmbeddedState = JSON.parse(\n                    JSON.stringify(stateChangeManager.embeddedState)\n                );\n            }\n            return Reflect.set(target, key, value, receiver);\n        },\n        deleteProperty(target, key) {\n            if (Reflect.has(target, key) && !stateChangeManager.previousEmbeddedState) {\n                stateChangeManager.previousEmbeddedState = JSON.parse(\n                    JSON.stringify(stateChangeManager.embeddedState)\n                );\n            }\n            return Reflect.deleteProperty(target, key);\n        },\n        // Read operations should also be done on state to register the\n        // rendering callback.\n        get(target, key, receiver) {\n            Reflect.get(state, key, state);\n            return Reflect.get(target, key, receiver);\n        },\n        ownKeys(target) {\n            Reflect.ownKeys(state);\n            return Reflect.ownKeys(target);\n        },\n        has(target, key) {\n            Reflect.has(state, key);\n            return Reflect.has(target, key);\n        },\n    };\n}\n\nfunction observeAllKeys(reactive) {\n    for (const key in reactive) {\n        const prop = reactive[key];\n        if (prop instanceof Object) {\n            observeAllKeys(prop);\n        }\n    }\n}\n\n/**\n * Extract props serialized in `data-embedded-props` attribute.\n *\n * @param {HostElement} host\n * @returns {Object} props\n */\nexport function getEmbeddedProps(host) {\n    return host.dataset.embeddedProps ? JSON.parse(host.dataset.embeddedProps) : {};\n}\n\nfunction sortedCopy(obj) {\n    const result = {};\n    const propNames = Object.keys(obj).sort();\n    for (const propName of propNames) {\n        result[propName] = obj[propName];\n    }\n    return result;\n}\n\n/**\n * Compute the difference between next and previous, and apply that difference\n * to container[key]. Comparison is done through JSON.stringify, so all values\n * must be serializable.\n *\n * @param {Object} container\n * @param {string} key\n * @param {Object} previous\n * @param {Object} next\n */\nexport function applyObjectPropertyDifference(container, key, previous, next) {\n    if (!container[key]) {\n        container[key] = {};\n    }\n    const obj1 = { ...(previous || {}) };\n    const obj2 = { ...(next || {}) };\n    const dest = container[key];\n    for (const key in obj2) {\n        if (JSON.stringify(obj1[key]) !== JSON.stringify(obj2[key])) {\n            dest[key] = obj2[key];\n        }\n        delete obj1[key];\n    }\n    for (const key in obj1) {\n        delete dest[key];\n    }\n    if (!Object.keys(dest).length && !next) {\n        delete container[key];\n    }\n}\n\n/**\n * Overwrite container[key] with value.\n *\n * @param {Object} container\n * @param {string} key\n * @param {Object} value\n */\nexport function replaceProperty(container, key, value) {\n    if (value === undefined) {\n        delete container[key];\n    } else {\n        container[key] = value;\n    }\n}\n\nexport class StateChangeManager {\n    /**\n     * @param {StateChangeManagerConfig} config\n     * @param {HostElement} config.host\n     * @param {Function} config.commitChanges notify the host that we can commit\n     *                                        changes\n     */\n    constructor(config) {\n        this.config = config;\n    }\n    setup() {\n        const defaultState = sortedCopy(this.getEmbeddedState());\n        const defaultStateChange = {\n            stateChangeId: null,\n            previous: defaultState,\n            next: defaultState,\n        };\n        // Used in case `data-embedded-state` is removed (i.e. when reverting\n        // the first mutation setting that attribute)\n        this.defaultStateChange = defaultStateChange;\n        // Used to keep track of the last applied stateChange, to avoid\n        // applying it multiple times (i.e. revertMutations + stageRecords\n        // during undo)\n        this.previousStateChange = defaultStateChange;\n        // Used to discard batch changes when a component is destroyed,\n        // pending state changes should not be applied\n        this.batchId = 0;\n        this.setupUnmounted();\n    }\n\n    /**\n     * Called at setup and when an embedded component is destroyed. This resets\n     * state values related to the mounted component. State changes will be\n     * handled differently when unmounted.\n     */\n    setupUnmounted() {\n        this.previousEmbeddedState = null;\n        this.state = null;\n        this.embeddedState = null;\n        this.embeddedStateProxy = null;\n        this.isLiveComponent = false;\n        this.batchId += 1;\n    }\n\n    /**\n     * Construct the proxy object to use inside an embedded component. It can\n     * be read on to register for rendering updates in the component template,\n     * and written on to trigger a re-rendering, sharing changes in\n     * collaboration and registering them for the history.\n     * @param {Object} state\n     * @returns {Proxy} embeddedStateProxy\n     */\n    constructEmbeddedState(state) {\n        this.state = state;\n        this.embeddedState = reactive(\n            this.assignDeepProxyCopy({}, state),\n            this.batchedChangeState()\n        );\n        this.embeddedStateProxy = new Proxy(\n            this.embeddedState,\n            embeddedStateProxyHandler(state, this)\n        );\n        // First subscription to changes.\n        observeAllKeys(this.embeddedStateProxy);\n        this.isLiveComponent = true;\n        return this.embeddedStateProxy;\n    }\n\n    /**\n     * Depending on whether the component is destroyed or started mounting,\n     * return its effective state.\n     * @returns {Object} state\n     */\n    getState() {\n        let state = this.state;\n        if (!this.isLiveComponent) {\n            state = this.getEmbeddedState();\n        }\n        return state;\n    }\n\n    /**\n     * Called when `data-embedded-state` attribute is being changed. This\n     * will update the state, the embedded state, the embedded props and\n     * recompute a new expression when necessary.\n     * @param {string} attrState JSON representation of a stateChange\n     * @param { Object } options\n     * @param {boolean} options.reverse whether to read the stateChange from\n     *        next to previous\n     * @param {boolean} options.forNewStep whether the attribute change is being\n     *        used to create a new step.\n     * @returns {string} new JSON representation of a stateChange, in case\n     *          it needs to be represented under another form to be shared\n     *          in collaboration (a local peer doing revertMutations implies\n     *          that collaborators will do applyMutations, so the stateChange\n     *          must be expressed with another form for them).\n     */\n    onStateChanged(attrState, { reverse = false, forNewStep = false } = {}) {\n        const stateChange = attrState ? JSON.parse(attrState) : this.defaultStateChange;\n        const state = this.getState();\n        if (reverse) {\n            this.reverseStateChange(stateChange);\n        }\n        if (!this.areStateChangesEqual(this.previousStateChange, stateChange)) {\n            const previous = JSON.stringify(sortedCopy(state));\n            this.commitStateChange(state, stateChange.previous, stateChange.next);\n            const sortedState = sortedCopy(state);\n            this.config.host.dataset.embeddedProps = JSON.stringify(\n                this.stateToEmbeddedProps(this.config.host, sortedState)\n            );\n            if (this.isLiveComponent && !this.previousEmbeddedState) {\n                // Update the embeddedState only if there is no pending change.\n                // If there is a pending change, it will be updated when the\n                // pending change is applied in `changeState`.\n                this.assignDeepProxyCopy(toRaw(this.embeddedState), sortedState);\n            }\n            if (!forNewStep) {\n                this.previousStateChange = stateChange;\n            } else {\n                // If mutations are being applied to create a new step, the\n                // state change must be expressed under another form for\n                // collaborators, since the collaborator will always\n                // \"applyMutations\" and never \"revertMutations\" when receiving\n                // external steps.\n                const next = JSON.stringify(sortedState);\n                if (previous !== next) {\n                    this.previousStateChange = {\n                        stateChangeId: this.generateId(),\n                        previous: JSON.parse(previous),\n                        next: JSON.parse(next),\n                    };\n                    return JSON.stringify(this.previousStateChange);\n                }\n            }\n        }\n    }\n\n    /**\n     * Allow to write on the embeddedState multiple times synchronously\n     * and batch all changes at once afterwards. A batch is discarded as soon\n     * as the component is destroyed.\n     * @returns {Function} batched changeState\n     */\n    batchedChangeState() {\n        let scheduled = false;\n        const batchId = this.batchId;\n        return async () => {\n            if (this.isLiveComponent && !scheduled) {\n                scheduled = true;\n                await Promise.resolve();\n                scheduled = false;\n                if (batchId === this.batchId) {\n                    this.changeState();\n                }\n            }\n        };\n    }\n\n    /**\n     * Apply a stateChange that was done on the embeddedState to the state,\n     * to trigger a re-rendering, and write the stateChange in\n     * `data-embedded-state` for the history and collaboration. Also\n     * recompute `data-embedded-props` for the next mounting operation.\n     */\n    changeState() {\n        if (!this.previousEmbeddedState) {\n            // If there is no previousEmbeddedState, it means that no\n            // effective change was performed, so there is nothing to commit.\n            return;\n        }\n        const previousEmbeddedState = this.previousEmbeddedState;\n        this.previousEmbeddedState = null;\n        const previous = JSON.stringify(sortedCopy(this.state));\n        this.commitStateChange(\n            this.state,\n            previousEmbeddedState,\n            JSON.parse(JSON.stringify(this.embeddedState))\n        );\n        const sortedState = sortedCopy(this.state);\n        const next = JSON.stringify(sortedState);\n        this.assignDeepProxyCopy(toRaw(this.embeddedState), sortedState);\n        if (previous !== next) {\n            this.previousStateChange = {\n                stateChangeId: this.generateId(),\n                previous: JSON.parse(previous),\n                next: JSON.parse(next),\n            };\n            this.config.host.dataset.embeddedState = JSON.stringify(this.previousStateChange);\n            this.config.host.dataset.embeddedProps = JSON.stringify(\n                this.stateToEmbeddedProps(this.config.host, sortedState)\n            );\n            this.config.commitStateChanges();\n        }\n        observeAllKeys(this.embeddedStateProxy);\n    }\n\n    areStateChangesEqual(sc1, sc2) {\n        return (\n            sc1.stateChangeId === sc2.stateChangeId &&\n            JSON.stringify(sc1.previous) === JSON.stringify(sc2.previous) &&\n            JSON.stringify(sc1.next) === JSON.stringify(sc2.next)\n        );\n    }\n\n    reverseStateChange(stateChange) {\n        const previous = stateChange.previous;\n        stateChange.previous = stateChange.next;\n        stateChange.next = previous;\n    }\n\n    /**\n     * Replace every key of target with deep proxy copies of source.\n     * This will make it so that any change at any level will pass by the\n     * embeddedStateProxyHandler traps.\n     * @param {Object} target\n     * @param {Object} source\n     * @returns {Object} copy with proxies as keys\n     */\n    assignDeepProxyCopy(target, source) {\n        for (const key of Object.keys(target)) {\n            delete target[key];\n        }\n        for (const key of Object.keys(source)) {\n            target[key] = this.deepProxyCopy(source[key]);\n        }\n        return target;\n    }\n\n    /**\n     * Create a deep proxy copy of value ensuring that any change at any level\n     * will pass by the embeddedStateProxyHandler traps.\n     * @param {Object} value\n     * @returns {Proxy} deep proxy copy of value\n     */\n    deepProxyCopy(value) {\n        if (value instanceof Object) {\n            const copy = value instanceof Array ? [] : {};\n            for (const prop in value) {\n                copy[prop] = this.deepProxyCopy(value[prop]);\n            }\n            return new Proxy(copy, embeddedStateProxyHandler(value, this));\n        }\n        return value;\n    }\n\n    generateId() {\n        return Math.floor(Math.random() * Math.pow(2, 52));\n    }\n\n    /**\n     * Apply a transaction to the active state. `previous` is the state\n     * before the transaction, and `next` is the state after the\n     * transaction was done. Keep in mind that the current state may have\n     * been changed after the transaction was done, but before it was\n     * applied. By default, will always accept nextState as\n     * the final state. `propertyUpdater` should be provided in the config\n     * to handle some keys differently, i.e. object composition.\n     * @see applyObjectPropertyDifference\n     * @param {Object} state current state\n     * @param {Object} previous state before the transaction\n     * @param {Object} next state after the transaction\n     */\n    commitStateChange(state, previous, next) {\n        const currentKeys = new Set([\n            ...Object.keys(state),\n            ...Object.keys(previous),\n            ...Object.keys(next),\n        ]);\n        for (const key of currentKeys) {\n            if (key in (this.config.propertyUpdater || {})) {\n                this.config.propertyUpdater[key](state, previous, next);\n            } else if (JSON.stringify(previous[key]) !== JSON.stringify(next[key])) {\n                replaceProperty(state, key, next[key]);\n            }\n        }\n    }\n\n    /**\n     * Extract values to be used as the first embedded state (used for setup)\n     * from the host.\n     * Extract all values from `data-embedded-props` by default.\n     * @returns {Object} state\n     */\n    getEmbeddedState() {\n        const host = this.config.host;\n        return this.config.getEmbeddedState?.(host) || getEmbeddedProps(host);\n    }\n\n    /**\n     * Convert a state to an object containing the props to be\n     * saved in `data-embedded-props`, which will be used for the next mount\n     * operation, and saved in the database. The returned object should be\n     * serializable using JSON.\n     * Return the entire state by default.\n     * @param {HostElement} host\n     * @param {Object} state\n     * @returns {Object} props\n     */\n    stateToEmbeddedProps(host, state) {\n        const props = this.config.stateToEmbeddedProps?.(host, state) || state;\n        // Clean undefined values to save space\n        for (const key of Object.keys(props)) {\n            if (props[key] === undefined) {\n                delete props[key];\n            }\n        }\n        return props;\n    }\n}\n\n/**\n * Manage updates to `data-embedded-props` (To change props given to an\n * embedded component when it will be mounted in the future), through history\n * and collaborative operations.\n * This is done through a special `embeddedState` which can be used externally\n * as a normal state.\n * That state can be modified through 2 channels:\n * - By the component itself, as with any normal state.\n * - By the embedded_component_plugin, during history or collaborative\n *   operations (undo/redo/resetStepsUntil/addExternalStep). The attribute\n *   `data-embedded-state` will be used to contain a serialized representation\n *   of a state change.\n *\n * While the embedded state evolves, the `data-embedded-props` attribute is\n * always maintained to its relative value.\n *\n * `data-embedded-state` and `data-embedded-props` attributes are maintained\n * even if the related component is in a destroyed state, in order to prepare\n * the next mount operation if the host is re-inserted in the DOM through an\n * history operation.\n * If the component is currently mounted/being mounted, state changes are\n * applied to the attribute and the embeddedState object.\n *\n * By default, a property change in the state is handled by replacing the\n * previous value with the new one (overwrite). To change this behavior,\n * provide a config extension in `getStateChangeManager` in the embedding\n * definition, with a @see propertyUpdater mapping each state key to a change\n * handler function.\n *\n * @param {HostElement} host\n * @returns {Proxy} embeddedState state which can be used for rendering, and\n *                  which is tied to the saved embedded props. Can only contain\n *                  JSON serializable values.\n */\nexport function useEmbeddedState(host) {\n    const component = useComponent();\n    if (!component.env.getStateChangeManager) {\n        throw new Error(\n            \"Missing `getStateChangeManager` function in the `embedding` provided to the `EmbeddedComponentPlugin`.\"\n        );\n    }\n    const stateChangeManager = component.env.getStateChangeManager(host);\n    onWillDestroy(() => stateChangeManager.setupUnmounted());\n    const state = useState(stateChangeManager.getEmbeddedState());\n    return stateChangeManager.constructEmbeddedState(state);\n}\n", "import { Component } from \"@odoo/owl\";\nimport { useForwardRefToParent } from \"@web/core/utils/hooks\";\n\nexport class EmbeddedComponentToolbar extends Component {\n    static props = {\n        buttonsGroupClass: { type: String, optional: true },\n        slots: Object,\n    };\n    static template = \"html_editor.EmbeddedComponentToolbar\";\n}\n\nexport class EmbeddedComponentToolbarButton extends Component {\n    static props = {\n        buttonRef: { type: Function, optional: true },\n        hidden: { type: Boolean, optional: true },\n        icon: { type: String, optional: true },\n        label: String,\n        name: { type: String, optional: true },\n        onClick: Function,\n        title: { type: String, optional: true },\n    };\n    static template = \"html_editor.EmbeddedComponentToolbarButton\";\n\n    setup() {\n        useForwardRefToParent(\"buttonRef\");\n    }\n}\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { downloadFile } from \"@web/core/network/download\";\nimport { useFileViewer } from \"@web/core/file_viewer/file_viewer_hook\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { AlertDialog } from \"@web/core/confirmation_dialog/confirmation_dialog\";\nimport {\n    EmbeddedComponentToolbar,\n    EmbeddedComponentToolbarButton,\n} from \"@html_editor/others/embedded_components/core/embedded_component_toolbar/embedded_component_toolbar\";\nimport { StateFileModel } from \"@html_editor/others/embedded_components/core/file/state_file_model\";\nimport { getEmbeddedProps } from \"@html_editor/others/embedded_component_utils\";\nimport { Component, useState } from \"@odoo/owl\";\n\nexport class ReadonlyEmbeddedFileComponent extends Component {\n    static components = {\n        EmbeddedComponentToolbar,\n        EmbeddedComponentToolbarButton,\n    };\n    static props = {\n        fileData: { type: Object },\n        host: { type: Object },\n    };\n    static template = \"html_editor.ReadonlyEmbeddedFile\";\n\n    setup() {\n        this.dialogService = useService(\"dialog\");\n        this.state = useState({\n            fileData: { ...this.props.fileData },\n        });\n        this.fileModel = new StateFileModel(this.state);\n        this.attachmentViewer = useFileViewer();\n    }\n\n    /**\n     * This function will simply open a link that will trigger the download of\n     * the associated file. If the url is not valid, the function will display\n     * an error message.\n     */\n    async download() {\n        try {\n            await downloadFile(this.fileModel.downloadUrl);\n        } catch {\n            this.dialogService.add(AlertDialog, {\n                body: _t(\n                    \"Oops, the file %s could not be found. Please replace this file box by a new one to re-upload the file.\",\n                    this.fileModel.name\n                ),\n                title: _t(\"Missing File\"),\n                confirm: () => {},\n                confirmLabel: _t(\"Close\"),\n            });\n        }\n    }\n\n    onClickFileImage() {\n        if (this.fileModel.isViewable) {\n            this.attachmentViewer.open(this.fileModel);\n        } else {\n            this.download();\n        }\n    }\n}\n\nexport const readonlyFileEmbedding = {\n    name: \"file\",\n    Component: ReadonlyEmbeddedFileComponent,\n    getProps: (host) => ({ host, ...getEmbeddedProps(host) }),\n};\n", "import { FileModel } from \"@web/core/file_viewer/file_model\";\n\nexport class StateFileModel extends FileModel {\n    constructor(state) {\n        super();\n        this.state = state;\n        for (const property of [\n            \"access_token\",\n            \"checksum\",\n            \"extension\",\n            \"filename\",\n            \"id\",\n            \"mimetype\",\n            \"name\",\n            \"type\",\n            \"tmpUrl\",\n            \"url\",\n            \"uploading\",\n        ]) {\n            Object.defineProperty(this, property, {\n                get() {\n                    return this.state.fileData[property];\n                },\n                set(value) {\n                    this.state.fileData[property] = value;\n                },\n                configurable: true,\n                enumerable: true,\n            });\n        }\n    }\n\n    /**\n     * For embedded files stored without an `id` (i.e. demo data or old\n     * knowledge embedded files converted from \"Knowledge Behavior\"), allow\n     * direct usage of the file `url` as an `urlRoute` for the fileViewer and\n     * download attempts.\n     *\n     * @override\n     */\n    get urlRoute() {\n        if (this.isUrl && !this.id) {\n            return this.url;\n        }\n        return super.urlRoute;\n    }\n}\n", "import { Component, onMounted, onWillStart, xml } from \"@odoo/owl\";\nimport { loadBundle } from \"@web/core/assets\";\nimport { cookie } from \"@web/core/browser/cookie\";\nimport { DEFAULT_LANGUAGE_ID, getPreValue, highlightPre } from \"./syntax_highlighting_utils\";\n\nexport class ReadonlySyntaxHighlightingComponent extends Component {\n    static props = {\n        value: { type: String },\n        languageId: { type: String },\n        host: { type: Object },\n    };\n    // The host is the `pre`. There's no need for a template but Owl requires it.\n    static template = xml`<span/>`;\n\n    setup() {\n        onWillStart(() =>\n            loadBundle(\n                `html_editor.assets_prism${cookie.get(\"color_scheme\") === \"dark\" ? \"_dark\" : \"\"}`,\n                { targetDoc: this.props.host.ownerDocument }\n            )\n        );\n        onMounted(() => {\n            const owlRoot = [...(this.props.host.children || [])].find(\n                (child) => child.nodeName === \"OWL-ROOT\"\n            );\n            highlightPre(owlRoot || this.props.host, this.props.value, this.props.languageId);\n        });\n    }\n}\n\nexport const readonlySyntaxHighlightingEmbedding = {\n    name: \"readonlySyntaxHighlighting\",\n    Component: ReadonlySyntaxHighlightingComponent,\n    getProps: (host) => ({\n        host,\n        languageId: host.dataset.languageId || DEFAULT_LANGUAGE_ID,\n        value: getPreValue(host),\n    }),\n};\n", "/* global Prism */\nimport { fillEmpty } from \"@html_editor/utils/dom\";\nimport { descendants, lastLeaf } from \"@html_editor/utils/dom_traversal\";\n\nexport const DEFAULT_LANGUAGE_ID = \"plaintext\";\n\n/**\n * Replace newlines in the given `element` with the appropriate number of line\n * breaks to preserve the visual aspect of these line breaks.\n *\n * @param {Element} element\n * @param {Document} [doc = element.ownerDocument || document]\n */\nexport const newlinesToLineBreaks = (element, doc = element.ownerDocument || document) => {\n    // 1. Replace \\n with <br>.\n    for (const node of descendants(element).filter((node) => node.nodeType === Node.TEXT_NODE)) {\n        let newline = node.textContent.indexOf(\"\\n\");\n        while (newline !== -1) {\n            node.before(doc.createTextNode(node.textContent.slice(0, newline)));\n            node.before(doc.createElement(\"BR\"));\n            node.textContent = node.textContent.slice(newline + 1);\n            newline = node.textContent.indexOf(\"\\n\");\n        }\n        if (!node.textContent) {\n            node.remove(); // Prevent empty trailing text node that would become the last leaf.\n        }\n    }\n    // 2. Handle trailing BRs. Eg, <span>ab\\n</span> -> <span>ab</span><br><br>\n    const trailingBr = lastLeaf(element);\n    if (trailingBr?.nodeName === \"BR\") {\n        element.append(trailingBr); // <span>ab<br></span> -> <span>ab</span><br>\n        trailingBr.after(doc.createElement(\"BR\")); // <br></pre> -> <br><br></pre>\n    }\n    // 3. Fill empty.\n    fillEmpty(element);\n};\n\n/**\n * Return the given `<pre>` element's inner text, cleaned of any zero-width\n * characters or trailing invisible newline characters (a trailing `<br>` in\n * the element's HTML is invisible but results in an visible `\\n` in its\n * `innerText` property, which would be visible if kept).\n *\n * @param {HTMLPreElement} pre\n * @returns {string}\n */\nexport const getPreValue = (pre) => {\n    // Trailing br gives \\n in innerText but should not be visible.\n    const trailingBrs = pre.innerHTML.match(/(<br>)+$/)?.length || 0;\n    return pre.innerText\n        .slice(0, pre.innerText.length - (trailingBrs > 1 ? trailingBrs - 1 : trailingBrs))\n        .replace(/[\\u200B\\uFEFF]/g, \"\");\n};\n\n/**\n * Use the Prism library to highlight the given HTML `value` with the given\n * `languageId` and replace the given `pre`'s inner HTML with it.\n *\n * @param {HTMLPreElement} pre\n * @param {string} value\n * @param {string} languageId\n */\nexport const highlightPre = (pre, value, languageId) => {\n    // We need a temporary element because directly changing the HTML of the\n    // PRE, or using replaceChildren both mess up the history by not\n    // recording the removal of the contents.\n    const fakeElement = pre.ownerDocument.createElement(\"pre\");\n    if (window.Prism) {\n        fakeElement.innerHTML = Prism.highlight(value, Prism.languages[languageId], languageId);\n    } else {\n        fakeElement.innerHTML = value;\n    }\n\n    // Post-process highlighted HTML.\n    newlinesToLineBreaks(fakeElement, pre.ownerDocument);\n\n    // Replace the PRE's contents with the highlighted ones.\n    [...pre.childNodes].forEach((child) => child.remove());\n    [...fakeElement.childNodes].forEach((child) => pre.append(child));\n};\n", "import { Component, onWillStart, useState } from \"@odoo/owl\";\nimport { TableOfContentManager } from \"@html_editor/others/embedded_components/core/table_of_content/table_of_content_manager\";\n\nexport class EmbeddedTableOfContentComponent extends Component {\n    static template = \"html_editor.EmbeddedTableOfContent\";\n    static props = {\n        manager: { type: TableOfContentManager },\n        readonly: { type: Boolean, optional: true },\n    };\n\n    setup() {\n        this.state = useState({ toc: this.props.manager.structure, folded: false });\n        onWillStart(async () => {\n            await this.props.manager.batchedUpdateStructure();\n        });\n    }\n\n    displayTocHint() {\n        return this.state.toc.headings.length < 2 && !this.props.readonly;\n    }\n\n    /**\n     * @param {Object} heading\n     */\n    onTocLinkClick(heading) {\n        this.props.manager.scrollIntoView(heading);\n    }\n}\n\nexport const tableOfContentEmbedding = {\n    name: \"tableOfContent\",\n    Component: EmbeddedTableOfContentComponent,\n};\n\nexport const readonlyTableOfContentEmbedding = {\n    name: \"tableOfContent\",\n    Component: EmbeddedTableOfContentComponent,\n    getProps: (host) => ({\n        readonly: true,\n    }),\n};\n", "import { batched, reactive } from \"@odoo/owl\";\n\nexport const HEADINGS = [\"H1\", \"H2\", \"H3\", \"H4\", \"H5\", \"H6\"];\n\nexport class TableOfContentManager {\n    constructor(containerRef) {\n        this.containerRef = containerRef;\n        this.structure = reactive({\n            headings: [],\n        });\n        this.batchedUpdateStructure = batched(this.updateStructure.bind(this));\n    }\n\n    getContainerEl() {\n        return this.containerRef.el;\n    }\n\n    /**\n     * Allows to fetch relevant headings in the page when building the Table of Content.\n     * Will filter out things we don't want:\n     * - Empty headers\n     * - Headers only containing the 'ZeroWidthSpace' element ('\\u200B')\n     * - Headers descendants of an element with `data-embedded`\n     *\n     * @param {Element} element\n     */\n    fetchValidHeadings(element) {\n        const inEmbeddedHeadings = new Set(\n            element.querySelectorAll(\n                HEADINGS.map((heading) => `[data-embedded] ${heading}`).join(\",\")\n            )\n        );\n        return Array.from(element.querySelectorAll(HEADINGS.join(\",\")))\n            .filter((heading) => heading.innerText.trim().replaceAll(\"\\u200B\", \"\").length > 0)\n            .filter((heading) => !inEmbeddedHeadings.has(heading));\n    }\n\n    scrollIntoView(heading) {\n        if (!heading) {\n            return;\n        }\n        const { target } = heading;\n        target.scrollIntoView({ behavior: \"smooth\" });\n        target.classList.add(\"o_embedded_toc_header_highlight\");\n        window.setTimeout(() => {\n            target.classList.remove(\"o_embedded_toc_header_highlight\");\n        }, 2000);\n    }\n\n    updateStructure() {\n        const container = this.getContainerEl();\n        if (!container) {\n            return;\n        }\n        const tagDepthStack = [];\n        this.structure.headings = this.fetchValidHeadings(container).map((heading) => {\n            while (tagDepthStack.at(-1) >= heading.tagName) {\n                tagDepthStack.pop();\n            }\n            const depth = tagDepthStack.length;\n            tagDepthStack.push(heading.tagName);\n            return {\n                depth,\n                name: heading.innerText,\n                target: heading,\n            };\n        });\n    }\n}\n", "import {\n    getEditableDescendants,\n    getEmbeddedProps,\n    useEditableDescendants,\n} from \"@html_editor/others/embedded_component_utils\";\nimport { browser } from \"@web/core/browser/browser\";\nimport { Component, useEffect, useExternalListener, useState } from \"@odoo/owl\";\n\nconst sessionStorage = browser.sessionStorage;\nexport class EmbeddedToggleBlockComponent extends Component {\n    static template = \"html_editor.EmbeddedToggleBlock\";\n    static props = {\n        host: { type: Object },\n        toggleBlockId: { type: String },\n    };\n\n    setup() {\n        useEditableDescendants(this.props.host);\n        this.state = useState({\n            showContent: sessionStorage.getItem(this.toggleStorageKey) === \"true\",\n        });\n        this.neutralRestoreSelection = () => {};\n        this.restoreSelection = this.neutralRestoreSelection;\n        useExternalListener(this.props.host, \"forceToggle\", this.onToggle);\n        useEffect(\n            () => {\n                this.restoreSelection();\n                this.restoreSelection = this.neutralRestoreSelection;\n            },\n            () => [this.restoreSelection]\n        );\n    }\n\n    get toggleStorageKey() {\n        return `html_editor.ToggleBlock${this.props.toggleBlockId}.showContent`;\n    }\n\n    onToggle(ev) {\n        let { showContent, restoreSelection } = ev.detail ?? {};\n        showContent ??= !this.state.showContent;\n        restoreSelection ??= this.neutralRestoreSelection;\n        if (this.state.showContent !== showContent) {\n            this.restoreSelection = restoreSelection;\n            this.state.showContent = showContent;\n            sessionStorage.setItem(this.toggleStorageKey, this.state.showContent);\n        } else {\n            restoreSelection();\n        }\n    }\n}\n\nexport const toggleBlockEmbedding = {\n    name: \"toggleBlock\",\n    Component: EmbeddedToggleBlockComponent,\n    getProps: (host) => ({ host, ...getEmbeddedProps(host) }),\n    getEditableDescendants: getEditableDescendants,\n};\n", "import { getEmbeddedProps } from \"@html_editor/others/embedded_component_utils\";\nimport { getVideoUrl } from \"@html_editor/utils/url\";\nimport { Component } from \"@odoo/owl\";\n\nexport class ReadonlyEmbeddedVideoComponent extends Component {\n    static template = \"html_editor.EmbeddedVideo\";\n    static props = {\n        platform: { type: String },\n        videoId: { type: String },\n        params: { type: Object, optional: true },\n    };\n\n    get url() {\n        return getVideoUrl(this.props.platform, this.props.videoId, this.props.params).toString();\n    }\n}\n\nexport const readonlyVideoEmbedding = {\n    name: \"video\",\n    Component: ReadonlyEmbeddedVideoComponent,\n    getProps: (host) => ({ ...getEmbeddedProps(host) }),\n};\n", "export const BASE_CONTAINER_CLASS = \"o-paragraph\";\n\nexport const SUPPORTED_BASE_CONTAINER_NAMES = [\"P\", \"DIV\"];\n\n/**\n * @param {string} [nodeName] @see SUPPORTED_BASE_CONTAINER_NAMES\n *                 will return the global selector if nodeName is not specified.\n * @returns {string} selector for baseContainers.\n */\nexport function getBaseContainerSelector(nodeName) {\n    if (!nodeName) {\n        return baseContainerGlobalSelector;\n    }\n    nodeName = SUPPORTED_BASE_CONTAINER_NAMES.includes(nodeName) ? nodeName : \"P\";\n    let suffix = \"\";\n    if (nodeName !== \"P\") {\n        suffix = `.${BASE_CONTAINER_CLASS}`;\n    }\n    return `${nodeName}${suffix}`;\n}\n\nexport const baseContainerGlobalSelector = `:is(${SUPPORTED_BASE_CONTAINER_NAMES.map((name) =>\n    getBaseContainerSelector(name)\n).join(\",\")})`;\n\n/**\n * Create a new baseContainer element.\n *\n * @param {string} nodeName @see SUPPORTED_BASE_CONTAINER_NAMES\n * @param {Document} [document] Used to create new baseContainer elements.\n *                   For iframes, preferably use the iframe document.\n *                   Fallbacks to the window document if possible and unspecified.\n *                   Has to be specified otherwise.\n * @returns {HTMLElement}\n */\nexport function createBaseContainer(nodeName, document) {\n    if (!document && window) {\n        document = window.document;\n    }\n    nodeName = nodeName && SUPPORTED_BASE_CONTAINER_NAMES.includes(nodeName) ? nodeName : \"P\";\n    const el = document.createElement(nodeName);\n    if (nodeName !== \"P\") {\n        el.className = BASE_CONTAINER_CLASS;\n    }\n    return el;\n}\n", "import { closestPath, findNode } from \"./dom_traversal\";\n\nconst blockTagNames = [\n    \"ADDRESS\",\n    \"ARTICLE\",\n    \"ASIDE\",\n    \"BLOCKQUOTE\",\n    \"DETAILS\",\n    \"DIALOG\",\n    \"DD\",\n    \"DIV\",\n    \"DL\",\n    \"DT\",\n    \"FIELDSET\",\n    \"FIGCAPTION\",\n    \"FIGURE\",\n    \"FOOTER\",\n    \"FORM\",\n    \"H1\",\n    \"H2\",\n    \"H3\",\n    \"H4\",\n    \"H5\",\n    \"H6\",\n    \"HEADER\",\n    \"HGROUP\",\n    \"HR\",\n    \"LI\",\n    \"MAIN\",\n    \"NAV\",\n    \"OL\",\n    \"P\",\n    \"PRE\",\n    \"SECTION\",\n    \"TABLE\",\n    \"UL\",\n    // The following elements are not in the W3C list, for some reason.\n    \"SELECT\",\n    \"OPTION\",\n    \"TR\",\n    \"TD\",\n    \"TBODY\",\n    \"THEAD\",\n    \"TH\",\n];\n\nconst computedStyleDisplayCache = new WeakMap();\n\n/**\n * Return true if the given node is a block-level element, false otherwise.\n *\n * @param node\n */\nexport function isBlock(node) {\n    if (!node || node.nodeType !== Node.ELEMENT_NODE) {\n        return false;\n    }\n    const tagName = node.nodeName.toUpperCase();\n    if (tagName === \"BR\") {\n        // A <br> is always inline but getComputedStyle(br).display mistakenly\n        // returns 'block' if its parent is display:flex (at least on Chrome and\n        // Firefox (Linux)). Browsers normally support setting a <br>'s display\n        // property to 'none' but any other change is not supported. Therefore\n        // it is safe to simply declare that a <br> is never supposed to be a\n        // block.\n        return false;\n    }\n    // The node might not be in the DOM, in which case it has no CSS values.\n    if (!node.isConnected) {\n        return blockTagNames.includes(tagName);\n    }\n    // We won't call `getComputedStyle(node).display` more than once per node.\n    let display = computedStyleDisplayCache.get(node);\n    if (display === undefined) {\n        const style = node.ownerDocument.defaultView.getComputedStyle(node);\n        display = style.display;\n        computedStyleDisplayCache.set(node, display);\n    }\n    if (display) {\n        return !display.includes(\"inline\") && display !== \"contents\";\n    }\n    return blockTagNames.includes(tagName);\n}\n\nexport function closestBlock(node) {\n    return findNode(closestPath(node), (node) => isBlock(node));\n}\n", "/**\n * Add origin to relative img src.\n * @param {Document} doc\n * @param {string} origin\n */\nfunction prependOriginToImages(doc, origin) {\n    doc.querySelectorAll(\"img\").forEach((img) => {\n        const src = img.getAttribute(\"src\");\n        if (src && !/^(http|\\/\\/|data:)/.test(src)) {\n            img.src = origin + (src.startsWith(\"/\") ? src : \"/\" + src);\n        }\n    });\n}\n\n/**\n * Fills clipboard data, also with the\n * application/vnd.odoo.odoo-editor mimetype so that it can recognized\n * on paste inside an editor.\n * @param {ClipboardEvent} ev copy event\n * @param {DocumentFragment} clonedContents\n */\nexport function fillClipboardData(ev, clonedContents) {\n    const doc = ev.target.ownerDocument;\n    const dataHtmlElement = doc.createElement(\"data\");\n    dataHtmlElement.append(clonedContents);\n    prependOriginToImages(dataHtmlElement, doc.defaultView.location.origin);\n    const htmlContent = dataHtmlElement.innerHTML;\n    ev.clipboardData.setData(\"text/html\", htmlContent);\n    ev.clipboardData.setData(\"application/vnd.odoo.odoo-editor\", htmlContent);\n}\n", "import { closestElement } from \"@html_editor/utils/dom_traversal\";\nimport { isColorGradient } from \"@web/core/utils/colors\";\nimport { isElement } from \"./dom_info\";\n\nexport const COLOR_PALETTE_COMPATIBILITY_COLOR_NAMES = [\n    \"primary\",\n    \"secondary\",\n    \"alpha\",\n    \"beta\",\n    \"gamma\",\n    \"delta\",\n    \"epsilon\",\n    \"success\",\n    \"info\",\n    \"warning\",\n    \"danger\",\n];\n\n/**\n * Colors of the default palette, used for substitution in shapes/illustrations.\n * key: number of the color in the palette (ie, o-color-<1-5>)\n * value: color hex code\n */\nexport const DEFAULT_PALETTE = {\n    1: \"#3AADAA\",\n    2: \"#7C6576\",\n    3: \"#F6F6F6\",\n    4: \"#FFFFFF\",\n    5: \"#383E45\",\n};\n\n/**\n * These constants are colors that can be edited by the user when using\n * web_editor in a website context. We keep track of them so that color\n * palettes and their preview elements can always have the right colors\n * displayed even if website has redefined the colors during an editing\n * session.\n *\n * @type {string[]}\n */\nexport const EDITOR_COLOR_CSS_VARIABLES = [...COLOR_PALETTE_COMPATIBILITY_COLOR_NAMES];\n\n// o-cc and o-colors\nfor (let i = 1; i <= 5; i++) {\n    EDITOR_COLOR_CSS_VARIABLES.push(`o-color-${i}`);\n    EDITOR_COLOR_CSS_VARIABLES.push(`o-cc${i}-bg`);\n    EDITOR_COLOR_CSS_VARIABLES.push(`o-cc${i}-bg-gradient`);\n    EDITOR_COLOR_CSS_VARIABLES.push(`o-cc${i}-headings`);\n    EDITOR_COLOR_CSS_VARIABLES.push(`o-cc${i}-text`);\n    EDITOR_COLOR_CSS_VARIABLES.push(`o-cc${i}-btn-primary`);\n    EDITOR_COLOR_CSS_VARIABLES.push(`o-cc${i}-btn-primary-text`);\n    EDITOR_COLOR_CSS_VARIABLES.push(`o-cc${i}-btn-secondary`);\n    EDITOR_COLOR_CSS_VARIABLES.push(`o-cc${i}-btn-secondary-text`);\n    EDITOR_COLOR_CSS_VARIABLES.push(`o-cc${i}-btn-primary-border`);\n    EDITOR_COLOR_CSS_VARIABLES.push(`o-cc${i}-btn-secondary-border`);\n}\n\n// Grays\nfor (let i = 100; i <= 900; i += 100) {\n    EDITOR_COLOR_CSS_VARIABLES.push(`${i}`);\n}\n\n// Black, white and their opacity variants.\n// These variables are necessary to prevent the colorpicker from being affected\n// by the backend \"Dark Mode\".\nEDITOR_COLOR_CSS_VARIABLES.push(\n    \"black\",\n    \"black-15\",\n    \"black-25\",\n    \"black-50\",\n    \"black-75\",\n    \"white\",\n    \"white-25\",\n    \"white-50\",\n    \"white-75\",\n    \"white-85\"\n);\n\n/**\n * @param {string|number} name\n * @returns {boolean}\n */\nexport function isColorCombinationName(name) {\n    const number = parseInt(name);\n    return !isNaN(number) && number % 100 !== 0;\n}\n\nexport const TEXT_CLASSES_REGEX =\n    /\\btext-(primary|secondary|success|danger|warning|info|light|dark|body|muted|white|black|reset|gradient|opacity-\\d{1,3}|o-[^\\s]+|\\d+)\\b/;\nexport const BG_CLASSES_REGEX = /\\bbg-[^\\s]*\\b/;\nexport const COLOR_COMBINATION_CLASSES_REGEX = /\\bo_cc[0-9]+\\b/g;\n\n/**\n * Returns true if the given element has a visible color applied\n * by `TEXT_CLASSES_REGEX` or `BG_CLASSES_REGEX`\n *\n * @param {Element} element\n * @param {string} mode 'color' or 'backgroundColor'\n * @returns {boolean}\n */\nexport function hasTextColorClass(element, mode) {\n    if (!element || !isElement(element)) {\n        return false;\n    }\n    const classRegex = mode === \"color\" ? TEXT_CLASSES_REGEX : BG_CLASSES_REGEX;\n    const parent = element.parentNode;\n    return (\n        classRegex.test(element.className) &&\n        (!parent || getComputedStyle(element)[mode] !== getComputedStyle(parent)[mode])\n    );\n}\n\n/**\n * Returns true if the given element has a visible color (fore- or\n * -background depending on the given mode).\n *\n * @param {Element} element\n * @param {string} mode 'color' or 'backgroundColor'\n * @returns {boolean}\n */\nexport function hasColor(element, mode) {\n    const style = element.style;\n    const parent = element.parentNode;\n    if (element.classList.contains(\"btn\")) {\n        // Ignore style applied on buttons from color detection\n        return false;\n    }\n    if (isColorGradient(style[\"background-image\"])) {\n        if (element.classList.contains(\"text-gradient\")) {\n            if (mode === \"color\") {\n                return true;\n            }\n        } else {\n            if (mode !== \"color\") {\n                return true;\n            }\n        }\n    }\n    return (\n        (style[mode] &&\n            style[mode] !== \"inherit\" &&\n            (!parent || style[mode] !== parent.style[mode])) ||\n        hasTextColorClass(element, mode)\n    );\n}\n\n/**\n * Returns true if any given nodes has a visible color (fore- or\n * -background depending on the given mode).\n *\n * @param {array} nodes\n * @param {string} mode 'color' or 'backgroundColor'\n * @returns {boolean}\n */\nexport function hasAnyNodesColor(nodes, mode) {\n    for (const node of nodes) {\n        if (hasColor(closestElement(node), mode)) {\n            return true;\n        }\n    }\n    return false;\n}\n\nexport function getTextColorOrClass(node) {\n    if (!node) {\n        return null;\n    }\n    if (node.style.color) {\n        return { type: \"style\", value: node.style.color };\n    }\n    const textColorClass = [...node.classList].find((cls) => TEXT_CLASSES_REGEX.test(cls));\n    if (textColorClass) {\n        return { type: \"class\", value: textColorClass };\n    }\n    return null;\n}\n", "export const CTYPES = {\n    // Short for CONTENT_TYPES\n    // Inline group\n    CONTENT: 1,\n    SPACE: 2,\n\n    // Block group\n    BLOCK_OUTSIDE: 4,\n    BLOCK_INSIDE: 8,\n\n    // Br group\n    BR: 16,\n};\nexport function ctypeToString(ctype) {\n    return Object.keys(CTYPES).find((key) => CTYPES[key] === ctype);\n}\nexport const CTGROUPS = {\n    // Short for CONTENT_TYPE_GROUPS\n    INLINE: CTYPES.CONTENT | CTYPES.SPACE,\n    BLOCK: CTYPES.BLOCK_OUTSIDE | CTYPES.BLOCK_INSIDE,\n    BR: CTYPES.BR,\n};\n", "import { closestBlock, isBlock } from \"./blocks\";\nimport {\n    isEmptyTextNode,\n    isParagraphRelatedElement,\n    isShrunkBlock,\n    isVisible,\n    nextLeaf,\n    previousLeaf,\n} from \"./dom_info\";\nimport { callbacksForCursorUpdate } from \"./selection\";\nimport { isEmptyBlock, isPhrasingContent } from \"../utils/dom_info\";\nimport { childNodes, descendants } from \"./dom_traversal\";\nimport { childNodeIndex, DIRECTIONS } from \"./position\";\nimport {\n    baseContainerGlobalSelector,\n    createBaseContainer,\n} from \"@html_editor/utils/base_container\";\n\n/** @typedef {import(\"@html_editor/core/selection_plugin\").Cursors} Cursors */\n\n/**\n * Take a node and unwrap all of its block contents recursively. All blocks\n * (except for firstChilds) are preceded by a <br> in order to preserve the line\n * breaks.\n *\n * @param {Node} node\n */\nexport function makeContentsInline(node) {\n    const document = node.ownerDocument;\n    let childIndex = 0;\n    for (const child of node.childNodes) {\n        if (isBlock(child)) {\n            if (childIndex && isParagraphRelatedElement(child)) {\n                child.before(document.createElement(\"br\"));\n            }\n            for (const grandChild of child.childNodes) {\n                child.before(grandChild);\n                makeContentsInline(grandChild);\n            }\n            child.remove();\n        }\n        childIndex += 1;\n    }\n}\n\n/**\n * Wrap inline children nodes in Blocks, optionally updating cursors for\n * later selection restore. A paragraph is used for phrasing node, and a div\n * is used otherwise.\n *\n * @param {HTMLElement} element - block element\n * @param {Cursors} [cursors]\n */\nexport function wrapInlinesInBlocks(\n    element,\n    { baseContainerNodeName = \"P\", cursors = { update: () => {} } } = {}\n) {\n    // Helpers to manipulate preserving selection.\n    const wrapInBlock = (node, cursors) => {\n        const block = isPhrasingContent(node)\n            ? createBaseContainer(baseContainerNodeName, node.ownerDocument)\n            : node.ownerDocument.createElement(\"DIV\");\n        cursors.update(callbacksForCursorUpdate.append(block, node));\n        cursors.update(callbacksForCursorUpdate.before(node, block));\n        if (node.nextSibling) {\n            const sibling = node.nextSibling;\n            node.remove();\n            sibling.before(block);\n        } else {\n            const parent = node.parentElement;\n            node.remove();\n            parent.append(block);\n        }\n        block.append(node);\n        return block;\n    };\n    const appendToCurrentBlock = (currentBlock, node, cursors) => {\n        if (currentBlock.matches(baseContainerGlobalSelector) && !isPhrasingContent(node)) {\n            const block = currentBlock.ownerDocument.createElement(\"DIV\");\n            cursors.update(callbacksForCursorUpdate.before(currentBlock, block));\n            currentBlock.before(block);\n            for (const child of childNodes(currentBlock)) {\n                cursors.update(callbacksForCursorUpdate.append(block, child));\n                block.append(child);\n            }\n            cursors.update(callbacksForCursorUpdate.remove(currentBlock));\n            currentBlock.remove();\n            currentBlock = block;\n        }\n        cursors.update(callbacksForCursorUpdate.append(currentBlock, node));\n        currentBlock.append(node);\n        return currentBlock;\n    };\n    const removeNode = (node, cursors) => {\n        cursors.update(callbacksForCursorUpdate.remove(node));\n        node.remove();\n    };\n\n    const children = childNodes(element);\n    const visibleNodes = new Set(children.filter(isVisible));\n\n    let currentBlock;\n    let shouldBreakLine = true;\n    for (const node of children) {\n        if (isBlock(node)) {\n            shouldBreakLine = true;\n        } else if (!visibleNodes.has(node)) {\n            removeNode(node, cursors);\n        } else if (node.nodeName === \"BR\") {\n            if (shouldBreakLine) {\n                wrapInBlock(node, cursors);\n            } else {\n                // BR preceded by inline content: discard it and make sure\n                // next inline goes in a new Block\n                removeNode(node, cursors);\n                shouldBreakLine = true;\n            }\n        } else if (shouldBreakLine) {\n            currentBlock = wrapInBlock(node, cursors);\n            shouldBreakLine = false;\n        } else {\n            currentBlock = appendToCurrentBlock(currentBlock, node, cursors);\n        }\n    }\n}\n\nexport function unwrapContents(node) {\n    const contents = childNodes(node);\n    for (const child of contents) {\n        node.parentNode.insertBefore(child, node);\n    }\n    node.parentNode.removeChild(node);\n    return contents;\n}\n\n/**\n * Removes the specified class names from the given element.  If the element has\n * no more class names after removal, the \"class\" attribute is removed.\n *\n * @param {Element} element - The element from which to remove the class names.\n * @param {...string} classNames - The class names to be removed.\n */\nexport function removeClass(element, ...classNames) {\n    const classNamesSet = new Set(classNames);\n    if ([...element.classList].every((className) => classNamesSet.has(className))) {\n        element.removeAttribute(\"class\");\n    } else {\n        element.classList.remove(...classNames);\n    }\n}\n\nexport function removeStyle(element, ...styleProperties) {\n    const propsToRemoveSet = new Set(styleProperties);\n    if ([...element.style].every((prop) => propsToRemoveSet.has(prop))) {\n        element.removeAttribute(\"style\");\n    } else {\n        styleProperties.forEach((prop) => element.style.removeProperty(prop));\n    }\n}\n\n/**\n * Add a BR in the given node if its closest ancestor block has nothing to make\n * it visible, and/or add a zero-width space in the given node if it's an empty\n * inline so the cursor can stay in it.\n *\n * @param {HTMLElement} el\n * @returns {Object} { br: the inserted <br> if any,\n *                     zws: the inserted zero-width space if any }\n */\nexport function fillEmpty(el) {\n    const document = el.ownerDocument;\n    if (!isBlock(el) && !isVisible(el) && !el.hasAttribute(\"data-oe-zws-empty-inline\")) {\n        const zws = document.createTextNode(\"\\u200B\");\n        el.appendChild(zws);\n        el.setAttribute(\"data-oe-zws-empty-inline\", \"\");\n        const previousSibling = el.previousSibling;\n        if (previousSibling && previousSibling.nodeName === \"BR\") {\n            previousSibling.remove();\n        }\n        return { zws };\n    } else {\n        // If a ZWS was inserted, there is no need for a <br>.\n        return fillShrunkPhrasingParent(el);\n    }\n}\n\n/**\n * Add a BR in a shrunk phrasing parent to make it visible.\n * A shrunk block is assumed to be a phrasing parent, and the inserted\n * <br> must be wrapped in a paragraph by the caller if necessary.\n *\n * @param {HTMLElement} el\n * @returns {Object} { br: the inserted <br> if any }\n */\nexport function fillShrunkPhrasingParent(el) {\n    const document = el.ownerDocument;\n    const fillers = {};\n    const blockEl = closestBlock(el);\n    if (isShrunkBlock(blockEl)) {\n        const br = document.createElement(\"br\");\n        blockEl.appendChild(br);\n        fillers.br = br;\n    }\n    return fillers;\n}\n\n/**\n * Removes a trailing BR if it is unnecessary:\n * in a non-empty block, if the last childNode is a BR and its previous sibling\n * is not a BR, remove the BR.\n *\n * @param {HTMLElement} el\n * @param {Array} predicates exceptions where a trailing BR should not be removed\n * @returns {HTMLElement|undefined} the removed br, if any\n */\nexport function cleanTrailingBR(el, predicates = []) {\n    const candidate = el?.lastChild;\n    if (\n        candidate?.nodeName === \"BR\" &&\n        candidate.previousSibling?.nodeName !== \"BR\" &&\n        !isEmptyBlock(el) &&\n        !predicates.some((predicate) => predicate(candidate))\n    ) {\n        candidate.remove();\n        return candidate;\n    }\n}\n\n/**\n * Wrapper for classList.toggle that removes the class attribute if the\n * element has no class name after the toggle.\n *\n * @param {Element} element\n * @param {string} className\n * @param {boolean} [force]\n */\nexport function toggleClass(element, className, force) {\n    element.classList.toggle(className, force);\n    if (!element.className) {\n        element.removeAttribute(\"class\");\n    }\n}\n\n/**\n * Remove all occurrences of a character from a text node and optionally update\n * cursors for later selection restore.\n *\n * In web_editor the text nodes used to be replaced by new ones with the updated\n * text rather than just changing the text content of the node because it\n * creates different mutations and it used to break the tour system. In\n * html_editor the text content is changed instead because other plugins rely on\n * the reference to the text node.\n *\n * @param {Node} node text node\n * @param {String} char character to remove (string of length 1)\n * @param {Cursors} [cursors]\n */\nexport function cleanTextNode(node, char, cursors) {\n    const removedIndexes = [];\n    node.textContent = node.textContent.replaceAll(char, (_, offset) => {\n        removedIndexes.push(offset);\n        return \"\";\n    });\n    if (isEmptyTextNode(node)) {\n        cursors?.update(callbacksForCursorUpdate.remove(node));\n        node.remove();\n    } else {\n        cursors?.update((cursor) => {\n            if (cursor.node === node) {\n                cursor.offset -= removedIndexes.filter((index) => cursor.offset > index).length;\n            }\n        });\n    }\n}\n\n/**\n * Splits a text node in two parts.\n * If the split occurs at the beginning or the end, the text node stays\n * untouched and unsplit. If a split actually occurs, the original text node\n * still exists and become the right part of the split.\n *\n * Note: if split after or before whitespace, that whitespace may become\n * invisible, it is up to the caller to replace it by nbsp if needed.\n *\n * @param {Text} textNode\n * @param {number} offset\n * @param {boolean} originalNodeSide Whether the original node ends up on left\n * or right after the split\n * @returns {number} The parentOffset if the cursor was between the two text\n *          node parts after the split.\n */\nexport function splitTextNode(textNode, offset, originalNodeSide = DIRECTIONS.RIGHT) {\n    const document = textNode.ownerDocument;\n    let parentOffset = childNodeIndex(textNode);\n\n    if (offset > 0) {\n        parentOffset++;\n\n        if (offset < textNode.length) {\n            const left = textNode.nodeValue.substring(0, offset);\n            const right = textNode.nodeValue.substring(offset);\n            if (originalNodeSide === DIRECTIONS.LEFT) {\n                const newTextNode = document.createTextNode(right);\n                textNode.after(newTextNode);\n                textNode.nodeValue = left;\n            } else {\n                const newTextNode = document.createTextNode(left);\n                textNode.before(newTextNode);\n                textNode.nodeValue = right;\n            }\n        }\n    }\n    return parentOffset;\n}\n\n/**\n * Remove invisible whitespace from an element and adapt the given cursors\n * accordingly if any.\n *\n * Note (TODO): in the future, this function should use the mechanism used by\n * `enforceWhitespace` but doing so would require a little overhaul of it and of\n * `getState`/`restoreState` to isolate the part that identifies invisible\n * whitespace.\n *\n * @param {Element} el\n * @param {import(\"@html_editor/core/selection_plugin\").Cursors} [cursors]\n */\nexport function removeInvisibleWhitespace(el, cursors) {\n    const [countLeadingWhitespace, countTrailingWhitespace] = [/^\\s+/, /\\s+$/].map(\n        (regex) => (node) => node?.textContent.match(regex)?.[0]?.length || 0\n    );\n    const isInlineElement = (node) => node?.nodeType === Node.ELEMENT_NODE && !isBlock(node);\n    const textChildren = descendants(el).filter((child) => child.nodeType === Node.TEXT_NODE);\n    let removedTrailingSpaceBefore = false;\n    let index = 0;\n    for (const child of textChildren) {\n        let leadingWhitespace = countLeadingWhitespace(child);\n        let trailingWhitespace = countTrailingWhitespace(child);\n        const previous = previousLeaf(child, el);\n        if (\n            leadingWhitespace &&\n            previous &&\n            (isInlineElement(child.previousSibling) || removedTrailingSpaceBefore)\n        ) {\n            // `<span>a</span>\\n   b` shows as `<span>a</span> b`\n            leadingWhitespace -= 1; // Keep one space.\n        } else if (\n            trailingWhitespace &&\n            index !== textChildren.length - 1 &&\n            isInlineElement(child.nextSibling) &&\n            !countTrailingWhitespace(nextLeaf(child, el))\n        ) {\n            // `a\\n   <span>\\n   b\\n</span>` shows as `a <span>b</span>`\n            trailingWhitespace -= 1; // Keep one space.\n        }\n        removedTrailingSpaceBefore = !!trailingWhitespace;\n        cursors?.shiftOffset(child, -leadingWhitespace);\n        child.textContent = child.textContent\n            .substring(\n                leadingWhitespace,\n                child.textContent.length - trailingWhitespace || leadingWhitespace\n            )\n            .replace(/^\\s+/, \" \")\n            .replace(/\\s+$/, \" \");\n        if (!child.textContent) {\n            child.remove();\n        }\n        index += 1;\n    }\n}\n", "import { baseContainerGlobalSelector } from \"./base_container\";\nimport { closestBlock, isBlock } from \"./blocks\";\nimport { childNodes, closestElement, firstLeaf, lastLeaf } from \"./dom_traversal\";\nimport { DIRECTIONS, nodeSize } from \"./position\";\n\nexport function isEmpty(el) {\n    if (isProtecting(el) || isProtected(el)) {\n        return false;\n    }\n    const content = el.innerHTML.trim();\n    if (content === \"\" || content === \"<br>\") {\n        return true;\n    }\n    return false;\n}\n\nexport function isEmptyTextNode(node) {\n    if (node.nodeType !== Node.TEXT_NODE) {\n        return false;\n    }\n    if (!node.textContent) {\n        return true;\n    }\n    const trimmedContent = node.textContent.trim();\n    if (!trimmedContent) {\n        // Only `\\n` is considered as empty\n        if (node.textContent.includes(\"\\n\")) {\n            return true;\n        }\n        // Only spaces is not considered as empty\n        // we technically can apply styles on spaces\n        if (node.textContent) {\n            return false;\n        }\n    }\n    return !trimmedContent;\n}\n\n/**\n * Return true if the given node appears bold. The node is considered to appear\n * bold if its font weight is bigger than 500 (eg.: Heading 1), or if its font\n * weight is bigger than that of its closest block.\n *\n * @param {Node} node\n * @returns {boolean}\n */\nexport function isBold(node) {\n    const fontWeight = +getComputedStyle(closestElement(node)).fontWeight;\n    const referenceElement = closestElement(\n        node,\n        (el) => isBlock(el) || +getComputedStyle(el).fontWeight !== fontWeight\n    );\n    return fontWeight > 500 || fontWeight > +getComputedStyle(referenceElement).fontWeight;\n}\n\n/**\n * Return true if the given node appears italic.\n *\n * @param {Node} node\n * @returns {boolean}\n */\nexport function isItalic(node) {\n    return getComputedStyle(closestElement(node)).fontStyle === \"italic\";\n}\n\n/**\n * Return true if the given node appears underlined.\n *\n * @param {Node} node\n * @returns {boolean}\n */\nexport function isUnderline(node) {\n    let parent = closestElement(node);\n    while (parent) {\n        if (getComputedStyle(parent).textDecorationLine.includes(\"underline\")) {\n            return true;\n        }\n        parent = parent.parentElement;\n    }\n    return false;\n}\n\n/**\n * Return true if the given node appears struck through.\n *\n * @param {Node} node\n * @returns {boolean}\n */\nexport function isStrikeThrough(node) {\n    let parent = closestElement(node);\n    while (parent) {\n        if (\n            !parent.classList.contains(\"o_checked\") &&\n            getComputedStyle(parent).textDecorationLine.includes(\"line-through\")\n        ) {\n            return true;\n        }\n        parent = parent.parentElement;\n    }\n    return false;\n}\n\n/**\n * Return true if the given node font-size is equal to `props.size`.\n *\n * @param {Object} props\n * @param {Node} props.node A node to compare the font-size against.\n * @param {String} props.size The font-size value of the node that will be\n *     checked against.\n * @returns {boolean}\n */\nexport function isFontSize(node, props) {\n    const element = closestElement(node);\n    return getComputedStyle(element)[\"font-size\"] === props.size;\n}\n\n/**\n * Return true if the given node classlist contains `props.className`.\n *\n * @param {Object} props\n * @param {Node} node A node to compare the font-size against.\n * @param {String} props.className The name of the class.\n * @returns {boolean}\n */\nexport function hasClass(node, props) {\n    const element = closestElement(node);\n    return element.classList.contains(props.className);\n}\n\n/**\n * Return true if the given node appears in a different direction than that of\n * the editable ('ltr' or 'rtl').\n *\n * Note: The direction of the editable is set on its \"dir\" attribute, to the\n * value of the \"direction\" option on instantiation of the editor.\n *\n * @param {Node} node\n * @param {Element} editable\n * @returns {boolean}\n */\nexport function isDirectionSwitched(node, editable) {\n    const defaultDirection = editable.getAttribute(\"dir\") || \"ltr\";\n    return getComputedStyle(closestElement(node)).direction !== defaultDirection;\n}\n\n// /**\n//  * Return true if the given node is a row element.\n//  */\nexport function isRow(node) {\n    return [\"TH\", \"TD\"].includes(node.tagName);\n}\n\nexport function isZWS(node) {\n    return node && node.textContent === \"\\u200B\";\n}\n\n/**\n * Returns true if the given node is in a PRE context for whitespace handling.\n *\n * @param {Node} node\n * @returns {boolean}\n */\nexport function isInPre(node) {\n    const element = node.nodeType === Node.TEXT_NODE ? node.parentElement : node;\n    return (\n        !!element &&\n        (!!element.closest(\"pre\") ||\n            getComputedStyle(element).getPropertyValue(\"white-space\") === \"pre\")\n    );\n}\n\nexport const ZERO_WIDTH_CHARS = [\"\\u200b\", \"\\ufeff\"];\n\nexport const whitespace = `[^\\\\S\\\\u00A0\\\\u0009\\\\ufeff]`; // for formatting (no \"real\" content) (TODO: 0009 shouldn't be included)\nconst whitespaceRegex = new RegExp(`^${whitespace}*$`);\nexport function isWhitespace(value) {\n    const str = typeof value === \"string\" ? value : value.nodeValue;\n    return whitespaceRegex.test(str);\n}\n\n// eslint-disable-next-line no-control-regex\nconst visibleCharRegex = /[^\\s\\u200b]|[\\u00A0\\u0009]$/; // contains at least a char that is always visible (TODO: 0009 shouldn't be included)\nexport function isVisibleTextNode(testedNode) {\n    if (!testedNode || !testedNode.length || testedNode.nodeType !== Node.TEXT_NODE) {\n        return false;\n    }\n    if (isProtected(testedNode)) {\n        return true;\n    }\n    if (\n        visibleCharRegex.test(testedNode.textContent) ||\n        (isInPre(testedNode) && isWhitespace(testedNode))\n    ) {\n        return true;\n    }\n    if (ZERO_WIDTH_CHARS.includes(testedNode.textContent)) {\n        return false; // a ZW(NB)SP is always invisible, regardless of context.\n    }\n    // The following assumes node is made entirely of whitespace and is not\n    // preceded of followed by a block.\n    // Find out contiguous preceding and following text nodes\n    let preceding;\n    let following;\n    // Control variable to know whether the current node has been found\n    let foundTestedNode;\n    const currentNodeParentBlock = closestBlock(testedNode);\n    if (!currentNodeParentBlock) {\n        return false;\n    }\n    const nodeIterator = document.createNodeIterator(currentNodeParentBlock);\n    for (let node = nodeIterator.nextNode(); node; node = nodeIterator.nextNode()) {\n        if (node.nodeType === Node.TEXT_NODE) {\n            // If we already found the tested node, the current node is the\n            // contiguous following, and we can stop looping\n            // If the current node is the tested node, mark it as found and\n            // continue.\n            // If we haven't reached the tested node, overwrite the preceding\n            // node.\n            if (foundTestedNode) {\n                following = node;\n                break;\n            } else if (testedNode === node) {\n                foundTestedNode = true;\n            } else {\n                preceding = node;\n            }\n        } else if (isBlock(node)) {\n            // If we found the tested node, then the following node is irrelevant\n            // If we didn't, then the current preceding node is irrelevant\n            if (foundTestedNode) {\n                break;\n            } else {\n                preceding = null;\n            }\n        } else if (foundTestedNode && !isWhitespace(node)) {\n            // <block>space<inline>text</inline></block> -> space is visible\n            following = node;\n            break;\n        }\n    }\n    while (following && !visibleCharRegex.test(following.textContent)) {\n        following = following.nextSibling;\n    }\n    // Missing preceding or following: invisible.\n    // Preceding or following not in the same block as tested node: invisible.\n    if (\n        !(preceding && following) ||\n        currentNodeParentBlock !== closestBlock(preceding) ||\n        currentNodeParentBlock !== closestBlock(following)\n    ) {\n        return false;\n    }\n    // Preceding is whitespace or following is whitespace: invisible\n    return visibleCharRegex.test(preceding.textContent);\n}\n\n/**\n * Returns whether the given node is a element that could be considered to be\n * removed by itself = self closing tags.\n *\n * @param {Node} node\n * @returns {boolean}\n */\nconst selfClosingElementTags = [\"BR\", \"IMG\", \"INPUT\", \"T\", \"HR\"];\nexport function isSelfClosingElement(node) {\n    return node && selfClosingElementTags.includes(node.nodeName);\n}\n\n/**\n * Returns whether removing the given node from the DOM will have a visible\n * effect or not.\n *\n * Note: TODO this is not handling all cases right now, just the ones the\n * caller needs at the moment. For example a space text node between two inlines\n * will always return 'true' while it is sometimes invisible.\n *\n * @param {Node} node\n * @returns {boolean}\n */\nexport function isVisible(node) {\n    return (\n        !!node &&\n        ((node.nodeType === Node.TEXT_NODE && isVisibleTextNode(node)) ||\n            isSelfClosingElement(node) ||\n            // @todo: handle it in resources?\n            isMediaElement(node) ||\n            hasVisibleContent(node) ||\n            isProtecting(node) ||\n            isEmbeddedComponent(node))\n    );\n}\nexport function hasVisibleContent(node) {\n    return (node ? childNodes(node) : []).some((n) => isVisible(n));\n}\n\nexport function isButton(node) {\n    if (!node || node.nodeType !== Node.ELEMENT_NODE) {\n        return false;\n    }\n    return node.nodeName === \"BUTTON\" || node.classList.contains(\"btn\");\n}\n\nexport function isZwnbsp(node) {\n    return node?.nodeType === Node.TEXT_NODE && node.textContent === \"\\ufeff\";\n}\n\nexport function isTangible(node) {\n    return isVisible(node) || isZwnbsp(node) || hasTangibleContent(node);\n}\n\nexport function hasTangibleContent(node) {\n    return (node ? childNodes(node) : []).some((n) => isTangible(n));\n}\n\nexport const isNotEditableNode = (node) =>\n    node.getAttribute &&\n    node.getAttribute(\"contenteditable\") &&\n    node.getAttribute(\"contenteditable\").toLowerCase() === \"false\";\n\nconst iconTags = [\"I\", \"SPAN\"];\n// @todo @phoenix: move the specific part in a proper plugin.\nexport const iconClasses = [\"fa\", \"fab\", \"fad\", \"far\", \"oi\"];\n\nexport const ICON_SELECTOR = iconTags\n    .map((tag) => iconClasses.map((cls) => `${tag}.${cls}`).join(\", \"))\n    .join(\", \");\n\nexport const MEDIA_SELECTOR = `${ICON_SELECTOR} , .o_image, .media_iframe_video`;\n\nexport const EDITABLE_MEDIA_CLASS = \"o_editable_media\";\n\n/**\n * Indicates if the given node is an icon element.\n *\n * @see ICON_SELECTOR\n * @param {?Node} [node]\n * @returns {boolean}\n */\nexport function isIconElement(node) {\n    return !!(\n        node &&\n        iconTags.includes(node.nodeName) &&\n        iconClasses.some((cls) => node.classList.contains(cls))\n    );\n}\n// @todo @phoenix: move the specific part in a proper plugin.\nexport function isMediaElement(node) {\n    return (\n        isIconElement(node) ||\n        (node.classList &&\n            (node.classList.contains(\"o_image\") ||\n                node.classList.contains(\"media_iframe_video\"))) ||\n        node.nodeName === \"CANVAS\"\n    );\n}\n\n// See https://developer.mozilla.org/en-US/docs/Web/HTML/Content_categories#phrasing_content\nconst phrasingTagNames = new Set([\n    \"ABBR\",\n    \"AUDIO\",\n    \"B\",\n    \"BDI\",\n    \"BDO\",\n    \"BR\",\n    \"BUTTON\",\n    \"CANVAS\",\n    \"CITE\",\n    \"CODE\",\n    \"DATA\",\n    \"DATALIST\",\n    \"DFN\",\n    \"EM\",\n    \"EMBED\",\n    \"I\",\n    \"IFRAME\",\n    \"IMG\",\n    \"INPUT\",\n    \"KBD\",\n    \"LABEL\",\n    \"MARK\",\n    \"MATH\",\n    \"METER\",\n    \"NOSCRIPT\",\n    \"OBJECT\",\n    \"OUTPUT\",\n    \"PICTURE\",\n    \"PROGRESS\",\n    \"Q\",\n    \"RUBY\",\n    \"S\",\n    \"SAMP\",\n    \"SCRIPT\",\n    \"SELECT\",\n    \"SLOT\",\n    \"SMALL\",\n    \"SPAN\",\n    \"STRONG\",\n    \"SUB\",\n    \"SUP\",\n    \"SVG\",\n    \"TEMPLATE\",\n    \"TEXTAREA\",\n    \"TIME\",\n    \"U\",\n    \"VAR\",\n    \"VIDEO\",\n    \"WBR\",\n    \"FONT\", // TODO @phoenix: font is deprecated, replace usage\n    // The following elements are phrasing content under specific conditions,\n    // evaluate if those conditions are applicable when using this set.\n    \"A\",\n    \"AREA\",\n    \"DEL\",\n    \"INS\",\n    \"LINK\",\n    \"MAP\",\n    \"META\",\n]);\n\nexport function isPhrasingContent(node) {\n    if (\n        node &&\n        (node.nodeType === Node.TEXT_NODE ||\n            (node.nodeType === Node.ELEMENT_NODE && phrasingTagNames.has(node.tagName)))\n    ) {\n        return true;\n    }\n    return false;\n}\n\nexport function containsAnyInline(element) {\n    if (!element) {\n        return false;\n    }\n    let child = element.firstChild;\n    while (child) {\n        if (\n            (!isBlock(child) && child.nodeType === Node.ELEMENT_NODE) ||\n            (child.nodeType === Node.TEXT_NODE && child.textContent.trim() !== \"\")\n        ) {\n            return true;\n        }\n        child = child.nextSibling;\n    }\n    return false;\n}\n\nexport function containsAnyNonPhrasingContent(element) {\n    if (!element) {\n        return false;\n    }\n    let child = element.firstChild;\n    while (child) {\n        if (!isPhrasingContent(child)) {\n            return true;\n        }\n        child = child.nextSibling;\n    }\n    return false;\n}\n\nexport function isEmbeddedComponent(node) {\n    return node.nodeType === Node.ELEMENT_NODE && node.matches(\"[data-embedded]\");\n}\n\n/**\n * A \"protected\" node will have its mutations filtered and not be registered\n * in an history step. Some editor features like selection handling, command\n * hint, toolbar, tooltip, etc. are also disabled. Protected roots have their\n * data-oe-protected attribute set to either \"\" or \"true\". If the closest parent\n * with a data-oe-protected attribute has the value \"false\", it is not\n * protected. Unknown values are ignored.\n *\n * @param {Node} node\n * @returns {boolean}\n */\nexport function isProtected(node) {\n    if (!node) {\n        return false;\n    }\n    const candidate = node.parentElement\n        ? closestElement(node.parentElement, \"[data-oe-protected]\")\n        : null;\n    if (!candidate || candidate.dataset.oeProtected === \"false\") {\n        return false;\n    }\n    return true;\n}\n\n/**\n * A \"protecting\" element contains childNodes that are protected.\n *\n * @param {Node} node\n * @returns {boolean}\n */\nexport function isProtecting(node) {\n    if (!node) {\n        return false;\n    }\n    return (\n        node.nodeType === Node.ELEMENT_NODE &&\n        node.dataset.oeProtected !== \"false\" &&\n        node.dataset.oeProtected !== undefined\n    );\n}\n\nexport function isUnprotecting(node) {\n    if (!node) {\n        return false;\n    }\n    return node.nodeType === Node.ELEMENT_NODE && node.dataset.oeProtected === \"false\";\n}\n\n// This is a list of \"paragraph-related elements\", defined as elements that\n// behave like paragraphs. It is non-exhaustive and should not be used as a\n// standalone. @see isParagraphRelatedElement\nexport const paragraphRelatedElements = [\"P\", \"H1\", \"H2\", \"H3\", \"H4\", \"H5\", \"H6\", \"PRE\"];\n\n/**\n * Return true if the given node allows \"paragraph-related elements\".\n *\n * @see paragraphRelatedElements\n * @param {Node} node\n * @returns {boolean}\n */\nexport function allowsParagraphRelatedElements(node) {\n    return isBlock(node) && !isParagraphRelatedElement(node);\n}\n\nexport const phrasingContent = new Set([\"#text\", ...phrasingTagNames]);\nconst flowContent = new Set([...phrasingContent, ...paragraphRelatedElements, \"DIV\", \"HR\"]);\nexport const listItem = new Set([\"LI\"]);\nconst listContainers = new Set([\"UL\", \"OL\"]);\n\nconst allowedContent = {\n    BLOCKQUOTE: flowContent,\n    DIV: flowContent,\n    H1: phrasingContent,\n    H2: phrasingContent,\n    H3: phrasingContent,\n    H4: phrasingContent,\n    H5: phrasingContent,\n    H6: phrasingContent,\n    HR: new Set(),\n    LI: flowContent,\n    OL: listItem,\n    UL: listItem,\n    P: phrasingContent,\n    PRE: phrasingContent,\n    TD: flowContent,\n    TR: new Set([\"TD\"]),\n};\n\nexport function isParagraphRelatedElement(node) {\n    if (!node) {\n        return false;\n    }\n    return (\n        paragraphRelatedElements.includes(node.nodeName) ||\n        (node.nodeType === Node.ELEMENT_NODE && node.matches(baseContainerGlobalSelector))\n    );\n}\n\nexport const paragraphRelatedElementsSelector = [\n    ...paragraphRelatedElements,\n    baseContainerGlobalSelector,\n].join(\",\");\n\nexport function isListItemElement(node) {\n    return [...listItem].includes(node.nodeName);\n}\n\nexport const listItemElementSelector = [...listItem].join(\",\");\n\nexport function isListElement(node) {\n    return [...listContainers].includes(node.nodeName);\n}\n\nexport const listElementSelector = [...listContainers].join(\",\");\n\nexport function isTableCell(node) {\n    return [\"TH\", \"TD\"].includes(node.nodeName);\n}\n\n/**\n * @param {Element} parentBlock\n * @param {Node[]} nodes\n * @returns {boolean}\n */\nexport function isAllowedContent(parentBlock, nodes) {\n    let allowedContentSet = allowedContent[parentBlock.nodeName];\n    if (!allowedContentSet) {\n        // Spec: a block not listed in allowedContent allows anything.\n        // See \"custom-block\" in tests.\n        return true;\n    }\n    if (parentBlock.matches(baseContainerGlobalSelector)) {\n        // A baseContainer DIV can only have phrasingContent, as a P would.\n        allowedContentSet = phrasingContent;\n    }\n    return nodes.every((node) => allowedContentSet.has(node.nodeName));\n}\n\n/**\n * Checks whether or not the given block has any visible content, except for\n * a placeholder BR.\n *\n * @param {HTMLElement} blockEl\n * @returns {boolean}\n */\nexport function isEmptyBlock(blockEl) {\n    if (!blockEl || blockEl.nodeType !== Node.ELEMENT_NODE) {\n        return false;\n    }\n    if (visibleCharRegex.test(blockEl.textContent)) {\n        return false;\n    }\n    if (blockEl.querySelectorAll(\"br\").length >= 2) {\n        return false;\n    }\n    if (isProtecting(blockEl) || isProtected(blockEl)) {\n        // Protecting nodes should never be considered empty for editor\n        // operations, as their content is a \"black box\". Their content should\n        // be managed by a specialized plugin.\n        return false;\n    }\n    const nodes = blockEl.querySelectorAll(\"*\");\n    for (const node of nodes) {\n        // There is no text and no double BR, the only thing that could make\n        // this visible is a \"visible empty\" node like an image.\n        if (\n            node.nodeName != \"BR\" &&\n            (isSelfClosingElement(node) ||\n                isMediaElement(node) ||\n                isProtecting(node) ||\n                isButton(node))\n        ) {\n            return false;\n        }\n    }\n    return isBlock(blockEl);\n}\n/**\n * Checks whether or not the given block element has something to make it have\n * a visible height (except for padding / border).\n *\n * @param {HTMLElement} blockEl\n * @returns {boolean}\n */\nexport function isShrunkBlock(blockEl) {\n    return isEmptyBlock(blockEl) && !blockEl.querySelector(\"br\") && !isSelfClosingElement(blockEl);\n}\n\nexport function isEditorTab(node) {\n    return node && node.nodeName === \"SPAN\" && node.classList.contains(\"oe-tabs\");\n}\n\nexport function getDeepestPosition(node, offset) {\n    let direction = DIRECTIONS.RIGHT;\n    let next = node;\n    while (next) {\n        if (isTangible(next) || (isZWS(next) && isContentEditable(next))) {\n            // Valid node: update position then try to go deeper.\n            if (next !== node) {\n                [node, offset] = [next, direction ? 0 : nodeSize(next)];\n            }\n            // First switch direction to left if offset is at the end.\n            const childrenNodes = childNodes(node);\n            direction = offset < childrenNodes.length;\n            next = childrenNodes[direction ? offset : offset - 1];\n        } else if (direction && next.nextSibling && closestBlock(node).contains(next.nextSibling)) {\n            // Invalid node: skip to next sibling (without crossing blocks).\n            next = next.nextSibling;\n        } else {\n            // Invalid node: skip to previous sibling (without crossing blocks).\n            direction = DIRECTIONS.LEFT;\n            next = closestBlock(node).contains(next.previousSibling) && next.previousSibling;\n        }\n        // Avoid too-deep ranges inside self-closing elements like [BR, 0].\n        next = !isSelfClosingElement(next) && next;\n    }\n    return [node, offset];\n}\n\nexport function previousLeaf(node, editable, skipInvisible = false) {\n    let ancestor = node;\n    while (ancestor && !ancestor.previousSibling && ancestor !== editable) {\n        ancestor = ancestor.parentElement;\n    }\n    if (ancestor && ancestor !== editable) {\n        if (skipInvisible && !isVisible(ancestor.previousSibling)) {\n            return previousLeaf(ancestor.previousSibling, editable, skipInvisible);\n        } else {\n            const last = lastLeaf(ancestor.previousSibling);\n            if (skipInvisible && !isVisible(last)) {\n                return previousLeaf(last, editable, skipInvisible);\n            } else {\n                return last;\n            }\n        }\n    }\n}\nexport function nextLeaf(node, editable, skipInvisible = false) {\n    let ancestor = node;\n    while (ancestor && !ancestor.nextSibling && ancestor !== editable) {\n        ancestor = ancestor.parentElement;\n    }\n    if (ancestor && ancestor !== editable) {\n        if (skipInvisible && ancestor.nextSibling && !isVisible(ancestor.nextSibling)) {\n            return nextLeaf(ancestor.nextSibling, editable, skipInvisible);\n        } else {\n            const first = firstLeaf(ancestor.nextSibling);\n            if (skipInvisible && !isVisible(first)) {\n                return nextLeaf(first, editable, skipInvisible);\n            } else {\n                return first;\n            }\n        }\n    }\n}\n\nfunction hasPseudoElementContent(node, pseudoSelector) {\n    const content = getComputedStyle(node, pseudoSelector).getPropertyValue(\"content\");\n    return content && content !== \"none\";\n}\n\nconst NOT_A_NUMBER = /[^\\d]/g;\n\nexport function areSimilarElements(node, node2) {\n    if (![node, node2].every((n) => n?.nodeType === Node.ELEMENT_NODE)) {\n        return false; // The nodes don't both exist or aren't both elements.\n    }\n    if (node.nodeName !== node2.nodeName) {\n        return false; // The nodes aren't the same type of element.\n    }\n    for (const name of new Set([...node.getAttributeNames(), ...node2.getAttributeNames()])) {\n        if (name === \"style\") {\n            if (!hasSameStyleAttributes(node, node2)) {\n                return false;\n            }\n        } else if (name === \"class\") {\n            if (!hasSameClasses(node, node2)) {\n                return false; // The nodes don't have the same classes.\n            }\n        } else if (node.getAttribute(name) !== node2.getAttribute(name)) {\n            return false; // The nodes don't have the same attributes.\n        }\n    }\n    if (\n        [node, node2].some(\n            (n) => hasPseudoElementContent(n, \":before\") || hasPseudoElementContent(n, \":after\")\n        )\n    ) {\n        return false; // The nodes have pseudo elements with content.\n    }\n    if (isBlock(node)) {\n        return false;\n    }\n    const nodeStyle = getComputedStyle(node);\n    const node2Style = getComputedStyle(node2);\n    return (\n        !+nodeStyle.padding.replace(NOT_A_NUMBER, \"\") &&\n        !+node2Style.padding.replace(NOT_A_NUMBER, \"\") &&\n        !+nodeStyle.margin.replace(NOT_A_NUMBER, \"\") &&\n        !+node2Style.margin.replace(NOT_A_NUMBER, \"\")\n    );\n}\n\nexport function hasSameStyleAttributes(node, node2) {\n    const getNodeStyles = (node) =>\n        (node.getAttribute(\"style\") || \"\")\n            .split(\";\")\n            .map((style) => style.trim())\n            .filter(Boolean);\n    const [nodeStyles, node2Styles] = [node, node2].map(getNodeStyles);\n    return (\n        nodeStyles.length === node2Styles.length &&\n        nodeStyles.every((style) => node2Styles.includes(style))\n    );\n}\n\nexport function hasSameClasses(node, node2) {\n    const getNodeClasses = (node) =>\n        (node.getAttribute(\"class\") || \"\")\n            .split(/\\s+/)\n            .map((c) => c.trim())\n            .filter(Boolean);\n    const [nodeClasses, node2Classes] = [node, node2].map(getNodeClasses);\n    return (\n        nodeClasses.length === node2Classes.length &&\n        nodeClasses.every((cls) => node2Classes.includes(cls))\n    );\n}\n\nexport function isTextNode(node) {\n    return node.nodeType === Node.TEXT_NODE;\n}\n\nexport function isElement(node) {\n    return node.nodeType === Node.ELEMENT_NODE;\n}\n\nexport function isContentEditable(node) {\n    const element = isTextNode(node) ? node.parentElement : node;\n    return element && element.isContentEditable;\n}\n\nexport function isContentEditableAncestor(node) {\n    if (node.nodeType !== Node.ELEMENT_NODE) {\n        return false;\n    }\n    return node.isContentEditable && node.matches(\"[contenteditable]\");\n}\n\n/**\n * Checks if all classes in node are present in node2 (subset check)\n */\nfunction hasClassesSubset(node, node2) {\n    const getNodeClasses = (n) => (n || \"\").trim().split(/\\s+/).filter(Boolean);\n    const [nodeClasses, node2Classes] = [node, node2].map(getNodeClasses);\n    return nodeClasses.every((cls) => node2Classes.includes(cls));\n}\n\n/**\n * Checks if all styles in node are present in node2 (subset check)\n */\nfunction hasStylesSubset(node, node2) {\n    const getNodeStyles = (n) =>\n        (n || \"\")\n            .split(\";\")\n            .map((s) => s.trim())\n            .filter(Boolean);\n    const [nodeStyles, node2Styles] = [node, node2].map(getNodeStyles);\n    return nodeStyles.every((style) => node2Styles.includes(style));\n}\n\n/**\n * Checks if a node is redundant based on its closest element with same tag.\n *\n * A node is considered redundant if:\n * - It is an Element node with a parent.\n * - There is a closest element with the same tag name.\n * - All of the node's attributes are present in that closest element:\n *   - All classes exist in the closest element's class list (subset check).\n *   - All inline styles are present in the closest element's style attribute (subset check).\n *   - All other attributes must have identical values.\n *\n * @param {Node} node - The DOM node to evaluate.\n * @returns {boolean} True if the node is redundant, false otherwise.\n */\nexport function isRedundantElement(node) {\n    // Check for valid element node and existence of a parent.\n    if (!node || node.nodeType !== Node.ELEMENT_NODE || !node.parentElement) {\n        return false;\n    }\n\n    // Find the closest element with the same tag name.\n    const closestEl = closestElement(node.parentElement, node.tagName);\n    if (!closestEl) {\n        return false;\n    }\n\n    // Check each attribute from node.\n    for (const { name: attrName, value: nodeAttrVal } of node.attributes) {\n        const closestElAttrVal = closestEl.getAttribute(attrName);\n\n        if (!closestElAttrVal) {\n            return false; // Attribute missing in closest element.\n        }\n\n        if (attrName === \"class\") {\n            // All classes on the node must exist in closest element.\n            if (!hasClassesSubset(nodeAttrVal, closestElAttrVal)) {\n                return false;\n            }\n        } else if (attrName === \"style\") {\n            // All inline styles on the node must exist in closest element.\n            if (!hasStylesSubset(nodeAttrVal, closestElAttrVal)) {\n                return false;\n            }\n        } else {\n            // For other attributes, values must match exactly.\n            if (nodeAttrVal !== closestElAttrVal) {\n                return false;\n            }\n        }\n    }\n\n    return true;\n}\n", "import { isBlock } from \"./blocks\";\nimport { CTGROUPS, CTYPES, ctypeToString } from \"./content_types\";\nimport { isInPre, isVisible, isWhitespace, whitespace } from \"./dom_info\";\nimport {\n    PATH_END_REASONS,\n    ancestors,\n    closestElement,\n    closestPath,\n    createDOMPathGenerator,\n} from \"./dom_traversal\";\nimport { DIRECTIONS, leftPos, rightPos } from \"./position\";\n\nconst prepareUpdateLockedEditables = new Set();\n/**\n * Any editor command is applied to a selection (collapsed or not). After the\n * command, the content type on the selection boundaries, in both direction,\n * should be preserved (some whitespace should disappear as went from collapsed\n * to non collapsed, or converted to &nbsp; as went from non collapsed to\n * collapsed, there also <br> to remove/duplicate, etc).\n *\n * This function returns a callback which allows to do that after the command\n * has been done.\n *\n * Note: the method has been made generic enough to work with non-collapsed\n * selection but can be used for an unique cursor position.\n *\n * @param {HTMLElement} el\n * @param {number} offset\n * @param {...(HTMLElement|number)} args - argument 1 and 2 can be repeated for\n *     multiple preparations with only one restore callback returned. Note: in\n *     that case, the positions should be given in the document node order.\n * @param {Object} [options]\n * @param {boolean} [options.allowReenter = true] - if false, all calls to\n *     prepareUpdate before this one gets restored will be ignored.\n * @param {string} [options.label = <random 6 character string>]\n * @param {boolean} [options.debug = false] - if true, adds nicely formatted\n *     console logs to help with debugging.\n * @returns {function}\n */\nexport function prepareUpdate(...args) {\n    const closestRoot =\n        args.length &&\n        ancestors(args[0]).find((ancestor) => ancestor.classList.contains(\"odoo-editor-editable\"));\n    const isPrepareUpdateLocked = closestRoot && prepareUpdateLockedEditables.has(closestRoot);\n    const hash = (Math.random() + 1).toString(36).substring(7);\n    const options = {\n        allowReenter: true,\n        label: hash,\n        debug: false,\n        ...(args.length && args[args.length - 1] instanceof Object ? args.pop() : {}),\n    };\n    if (options.debug) {\n        console.log(\n            \"%cPreparing%c update: \" +\n                options.label +\n                (options.label === hash ? \"\" : ` (${hash})`) +\n                \"%c\" +\n                (isPrepareUpdateLocked ? \" LOCKED\" : \"\"),\n            \"color: cyan;\",\n            \"color: white;\",\n            \"color: red; font-weight: bold;\"\n        );\n    }\n    if (isPrepareUpdateLocked) {\n        return () => {\n            if (options.debug) {\n                console.log(\n                    \"%cRestoring%c update: \" +\n                        options.label +\n                        (options.label === hash ? \"\" : ` (${hash})`) +\n                        \"%c LOCKED\",\n                    \"color: lightgreen;\",\n                    \"color: white;\",\n                    \"color: red; font-weight: bold;\"\n                );\n            }\n        };\n    }\n    if (!options.allowReenter && closestRoot) {\n        prepareUpdateLockedEditables.add(closestRoot);\n    }\n    const positions = [...args];\n\n    // Check the state in each direction starting from each position.\n    const restoreData = [];\n    let el, offset;\n    while (positions.length) {\n        // Note: important to get the positions in reverse order to restore\n        // right side before left side.\n        offset = positions.pop();\n        el = positions.pop();\n        const left = getState(el, offset, DIRECTIONS.LEFT);\n        const right = getState(el, offset, DIRECTIONS.RIGHT, left.cType);\n        if (options.debug) {\n            const editable = el && closestElement(el, \".odoo-editor-editable\");\n            const oldEditableHTML =\n                (editable && editable.innerHTML.replaceAll(\" \", \"_\").replaceAll(\"\\u200B\", \"ZWS\")) ||\n                \"\";\n            left.oldEditableHTML = oldEditableHTML;\n            right.oldEditableHTML = oldEditableHTML;\n        }\n        restoreData.push(left, right);\n    }\n\n    // Create the callback that will be able to restore the state in each\n    // direction wherever the node in the opposite direction has landed.\n    return function restoreStates() {\n        if (options.debug) {\n            console.log(\n                \"%cRestoring%c update: \" +\n                    options.label +\n                    (options.label === hash ? \"\" : ` (${hash})`),\n                \"color: lightgreen;\",\n                \"color: white;\"\n            );\n        }\n        for (const data of restoreData) {\n            restoreState(data, options.debug);\n        }\n        if (!options.allowReenter && closestRoot) {\n            prepareUpdateLockedEditables.delete(closestRoot);\n        }\n    };\n}\n\nexport const leftLeafOnlyNotBlockPath = createDOMPathGenerator(DIRECTIONS.LEFT, {\n    leafOnly: true,\n    stopTraverseFunction: isBlock,\n    stopFunction: isBlock,\n});\n\nconst rightLeafOnlyNotBlockPath = createDOMPathGenerator(DIRECTIONS.RIGHT, {\n    leafOnly: true,\n    stopTraverseFunction: isBlock,\n    stopFunction: isBlock,\n});\n\n/**\n * Retrieves the \"state\" from a given position looking at the given direction.\n * The \"state\" is the type of content. The functions also returns the first\n * meaninful node looking in the opposite direction = the first node we trust\n * will not disappear if a command is played in the given direction.\n *\n * Note: only work for in-between nodes positions. If the position is inside a\n * text node, first split it @see splitTextNode.\n *\n * @param {HTMLElement} el\n * @param {number} offset\n * @param {boolean} direction @see DIRECTIONS.LEFT @see DIRECTIONS.RIGHT\n * @param {CTYPES} [leftCType]\n * @returns {Object}\n */\nexport function getState(el, offset, direction, leftCType) {\n    const leftDOMPath = leftLeafOnlyNotBlockPath;\n    const rightDOMPath = rightLeafOnlyNotBlockPath;\n\n    let domPath;\n    let inverseDOMPath;\n    const whitespaceAtStartRegex = new RegExp(\"^\" + whitespace + \"+\");\n    const whitespaceAtEndRegex = new RegExp(whitespace + \"+$\");\n    const reasons = [];\n    if (direction === DIRECTIONS.LEFT) {\n        domPath = leftDOMPath(el, offset, reasons);\n        inverseDOMPath = rightDOMPath(el, offset);\n    } else {\n        domPath = rightDOMPath(el, offset, reasons);\n        inverseDOMPath = leftDOMPath(el, offset);\n    }\n\n    // TODO I think sometimes, the node we have to consider as the\n    // anchor point to restore the state is not the first one of the inverse\n    // path (like for example, empty text nodes that may disappear\n    // after the command so we would not want to get those ones).\n    const boundaryNode = inverseDOMPath.next().value;\n\n    // We only traverse through deep inline nodes. If we cannot find a\n    // meanfingful state between them, that means we hit a block.\n    let cType = undefined;\n\n    // Traverse the DOM in the given direction to check what type of content\n    // there is.\n    let lastSpace = null;\n    for (const node of domPath) {\n        if (node.nodeType === Node.TEXT_NODE) {\n            const value = node.nodeValue;\n            // If we hit a text node, the state depends on the path direction:\n            // any space encountered backwards is a visible space if we hit\n            // visible content afterwards. If going forward, spaces are only\n            // visible if we have content backwards.\n            if (direction === DIRECTIONS.LEFT) {\n                if (!isWhitespace(value)) {\n                    if (lastSpace) {\n                        cType = CTYPES.SPACE;\n                    } else {\n                        const rightLeaf = rightLeafOnlyNotBlockPath(node).next().value;\n                        const hasContentRight =\n                            rightLeaf && !whitespaceAtStartRegex.test(rightLeaf.textContent);\n                        cType =\n                            !hasContentRight && whitespaceAtEndRegex.test(node.textContent)\n                                ? CTYPES.SPACE\n                                : CTYPES.CONTENT;\n                    }\n                    break;\n                }\n                if (value.length) {\n                    lastSpace = node;\n                }\n            } else {\n                leftCType = leftCType || getState(el, offset, DIRECTIONS.LEFT).cType;\n                if (whitespaceAtStartRegex.test(value)) {\n                    const leftLeaf = leftLeafOnlyNotBlockPath(node).next().value;\n                    const hasContentLeft =\n                        leftLeaf && !whitespaceAtEndRegex.test(leftLeaf.textContent);\n                    const rct = !isWhitespace(value)\n                        ? CTYPES.CONTENT\n                        : getState(...rightPos(node), DIRECTIONS.RIGHT).cType;\n                    cType =\n                        leftCType & CTYPES.CONTENT &&\n                        rct & (CTYPES.CONTENT | CTYPES.BR) &&\n                        !hasContentLeft\n                            ? CTYPES.SPACE\n                            : rct;\n                    break;\n                }\n                if (!isWhitespace(value)) {\n                    cType = CTYPES.CONTENT;\n                    break;\n                }\n            }\n        } else if (node.nodeName === \"BR\") {\n            cType = CTYPES.BR;\n            break;\n        } else if (isVisible(node)) {\n            // E.g. an image\n            cType = CTYPES.CONTENT;\n            break;\n        }\n    }\n\n    if (cType === undefined) {\n        cType = reasons.includes(PATH_END_REASONS.BLOCK_HIT)\n            ? CTYPES.BLOCK_OUTSIDE\n            : CTYPES.BLOCK_INSIDE;\n    }\n\n    return {\n        node: boundaryNode,\n        direction: direction,\n        cType: cType, // Short for contentType\n    };\n}\nconst priorityRestoreStateRules = [\n    // Each entry is a list of two objects, with each key being optional (the\n    // more key-value pairs, the bigger the priority).\n    // {direction: ..., cType1: ..., cType2: ...}\n    // ->\n    // {spaceVisibility: (false|true), brVisibility: (false|true)}\n    [\n        // Replace a space by &nbsp; when it was not collapsed before and now is\n        // collapsed (one-letter word removal for example).\n        { cType1: CTYPES.CONTENT, cType2: CTYPES.SPACE | CTGROUPS.BLOCK },\n        { spaceVisibility: true },\n    ],\n    [\n        // Replace a space by &nbsp; when it was content before and now it is\n        // a BR.\n        { direction: DIRECTIONS.LEFT, cType1: CTGROUPS.INLINE, cType2: CTGROUPS.BR },\n        { spaceVisibility: true },\n    ],\n    [\n        // Replace a space by &nbsp; when it was content before and now it is\n        // a BR (removal of last character before a BR for example).\n        { direction: DIRECTIONS.RIGHT, cType1: CTGROUPS.CONTENT, cType2: CTGROUPS.BR },\n        { spaceVisibility: true },\n    ],\n    [\n        // Replace a space by &nbsp; when it was visible thanks to a BR which\n        // is now gone.\n        { direction: DIRECTIONS.RIGHT, cType1: CTGROUPS.BR, cType2: CTYPES.SPACE },\n        { spaceVisibility: true },\n    ],\n    [\n        // Replace a space by &nbsp; when it was visible thanks to a BR which\n        // is now gone and duplicate a BR which was visible thanks to a second\n        // BR which is now gone.\n        { direction: DIRECTIONS.RIGHT, cType1: CTGROUPS.BR, cType2: CTGROUPS.BLOCK },\n        { spaceVisibility: true, brVisibility: true },\n    ],\n    [\n        // Remove all collapsed spaces when a space is removed.\n        { cType1: CTYPES.SPACE },\n        { spaceVisibility: false },\n    ],\n    [\n        // Remove spaces once the preceeding BR is removed\n        { direction: DIRECTIONS.LEFT, cType1: CTGROUPS.BR },\n        { spaceVisibility: false },\n    ],\n    [\n        // Remove space before block once content is put after it (otherwise it\n        // would become visible).\n        { cType1: CTGROUPS.BLOCK, cType2: CTGROUPS.INLINE | CTGROUPS.BR },\n        { spaceVisibility: false },\n    ],\n    [\n        // Duplicate a BR once the content afterwards disappears\n        { direction: DIRECTIONS.RIGHT, cType1: CTGROUPS.INLINE, cType2: CTGROUPS.BLOCK },\n        { brVisibility: true },\n    ],\n    [\n        // Remove a BR at the end of a block once inline content is put after\n        // it (otherwise it would act as a line break).\n        {\n            direction: DIRECTIONS.RIGHT,\n            cType1: CTGROUPS.BLOCK,\n            cType2: CTGROUPS.INLINE | CTGROUPS.BR,\n        },\n        { brVisibility: false },\n    ],\n    [\n        // Remove a BR once the BR that preceeds it is now replaced by\n        // content (or if it was a BR at the start of a block which now is\n        // a trailing BR).\n        {\n            direction: DIRECTIONS.LEFT,\n            cType1: CTGROUPS.BR | CTGROUPS.BLOCK,\n            cType2: CTGROUPS.INLINE,\n        },\n        { brVisibility: false, extraBRRemovalCondition: (brNode) => isFakeLineBreak(brNode) },\n    ],\n];\nfunction restoreStateRuleHashCode(direction, cType1, cType2) {\n    return `${direction}-${cType1}-${cType2}`;\n}\nconst allRestoreStateRules = (function () {\n    const map = new Map();\n\n    const keys = [\"direction\", \"cType1\", \"cType2\"];\n    for (const direction of Object.values(DIRECTIONS)) {\n        for (const cType1 of Object.values(CTYPES)) {\n            for (const cType2 of Object.values(CTYPES)) {\n                const rule = { direction: direction, cType1: cType1, cType2: cType2 };\n\n                // Search for the rules which match whatever their priority\n                const matchedRules = [];\n                for (const entry of priorityRestoreStateRules) {\n                    let priority = 0;\n                    for (const key of keys) {\n                        const entryKeyValue = entry[0][key];\n                        if (entryKeyValue !== undefined) {\n                            if (\n                                typeof entryKeyValue === \"boolean\"\n                                    ? rule[key] === entryKeyValue\n                                    : rule[key] & entryKeyValue\n                            ) {\n                                priority++;\n                            } else {\n                                priority = -1;\n                                break;\n                            }\n                        }\n                    }\n                    if (priority >= 0) {\n                        matchedRules.push([priority, entry[1]]);\n                    }\n                }\n\n                // Create the final rule by merging found rules by order of\n                // priority\n                const finalRule = {};\n                for (let p = 0; p <= keys.length; p++) {\n                    for (const entry of matchedRules) {\n                        if (entry[0] === p) {\n                            Object.assign(finalRule, entry[1]);\n                        }\n                    }\n                }\n\n                // Create an unique identifier for the set of values\n                // direction - state 1 - state2 to add the rule in the map\n                const hashCode = restoreStateRuleHashCode(direction, cType1, cType2);\n                map.set(hashCode, finalRule);\n            }\n        }\n    }\n\n    return map;\n})();\n/**\n * Restores the given state starting before the given while looking in the given\n * direction.\n *\n * @param {Object} prevStateData @see getState\n * @param {boolean} debug=false - if true, adds nicely formatted\n *     console logs to help with debugging.\n * @returns {Object|undefined} the rule that was applied to restore the state,\n *     if any, for testing purposes.\n */\nexport function restoreState(prevStateData, debug = false) {\n    const { node, direction, cType: cType1, oldEditableHTML } = prevStateData;\n    if (!node || !node.parentNode) {\n        // FIXME sometimes we want to restore the state starting from a node\n        // which has been removed by another restoreState call... Not sure if\n        // it is a problem or not, to investigate.\n        return;\n    }\n    const [el, offset] = direction === DIRECTIONS.LEFT ? leftPos(node) : rightPos(node);\n    const { cType: cType2 } = getState(el, offset, direction);\n\n    /**\n     * Knowing the old state data and the new state data, we know if we have to\n     * do something or not, and what to do.\n     */\n    const ruleHashCode = restoreStateRuleHashCode(direction, cType1, cType2);\n    const rule = allRestoreStateRules.get(ruleHashCode);\n    if (debug) {\n        const editable = closestElement(node, \".odoo-editor-editable\");\n        console.log(\n            \"%c\" +\n                node.textContent.replaceAll(\" \", \"_\").replaceAll(\"\\u200B\", \"ZWS\") +\n                \"\\n\" +\n                \"%c\" +\n                (direction === DIRECTIONS.LEFT ? \"left\" : \"right\") +\n                \"\\n\" +\n                \"%c\" +\n                ctypeToString(cType1) +\n                \"\\n\" +\n                \"%c\" +\n                ctypeToString(cType2) +\n                \"\\n\" +\n                \"%c\" +\n                \"BEFORE: \" +\n                (oldEditableHTML || \"(unavailable)\") +\n                \"\\n\" +\n                \"%c\" +\n                \"AFTER:  \" +\n                (editable\n                    ? editable.innerHTML.replaceAll(\" \", \"_\").replaceAll(\"\\u200B\", \"ZWS\")\n                    : \"(unavailable)\") +\n                \"\\n\",\n            \"color: white; display: block; width: 100%;\",\n            \"color: \" +\n                (direction === DIRECTIONS.LEFT ? \"magenta\" : \"lightgreen\") +\n                \"; display: block; width: 100%;\",\n            \"color: pink; display: block; width: 100%;\",\n            \"color: lightblue; display: block; width: 100%;\",\n            \"color: white; display: block; width: 100%;\",\n            \"color: white; display: block; width: 100%;\",\n            rule\n        );\n    }\n    if (Object.values(rule).filter((x) => x !== undefined).length) {\n        const inverseDirection = direction === DIRECTIONS.LEFT ? DIRECTIONS.RIGHT : DIRECTIONS.LEFT;\n        enforceWhitespace(el, offset, inverseDirection, rule);\n    }\n    return rule;\n}\n\n/**\n * Returns whether or not the given node is a BR element which does not really\n * act as a line break, but as a placeholder for the cursor or to make some left\n * element (like a space) visible.\n * @todo @phoenix this depends on state, so hard to move it to dom_info\n *\n * @param {HTMLBRElement} brEl\n * @returns {boolean}\n */\nexport function isFakeLineBreak(brEl) {\n    return !(getState(...rightPos(brEl), DIRECTIONS.RIGHT).cType & (CTYPES.CONTENT | CTGROUPS.BR));\n}\n\n/**\n * Enforces the whitespace and BR visibility in the given direction starting\n * from the given position.\n *\n * @param {HTMLElement} el\n * @param {number} offset\n * @param {number} direction @see DIRECTIONS.LEFT @see DIRECTIONS.RIGHT\n * @param {Object} rule\n * @param {boolean} [rule.spaceVisibility]\n * @param {boolean} [rule.brVisibility]\n */\nexport function enforceWhitespace(el, offset, direction, rule) {\n    const document = el.ownerDocument;\n    let domPath, whitespaceAtEdgeRegex;\n    if (direction === DIRECTIONS.LEFT) {\n        domPath = leftLeafOnlyNotBlockPath(el, offset);\n        whitespaceAtEdgeRegex = new RegExp(whitespace + \"+$\");\n    } else {\n        domPath = rightLeafOnlyNotBlockPath(el, offset);\n        whitespaceAtEdgeRegex = new RegExp(\"^\" + whitespace + \"+\");\n    }\n\n    const invisibleSpaceTextNodes = [];\n    let foundVisibleSpaceTextNode = null;\n    for (const node of domPath) {\n        if (node.nodeName === \"BR\") {\n            if (rule.brVisibility === undefined) {\n                break;\n            }\n            if (rule.brVisibility) {\n                node.before(document.createElement(\"br\"));\n            } else {\n                if (!rule.extraBRRemovalCondition || rule.extraBRRemovalCondition(node)) {\n                    node.remove();\n                }\n            }\n            break;\n        } else if (node.nodeType === Node.TEXT_NODE && !isInPre(node)) {\n            if (whitespaceAtEdgeRegex.test(node.nodeValue)) {\n                // If we hit spaces going in the direction, either they are in a\n                // visible text node and we have to change the visibility of\n                // those spaces, or it is in an invisible text node. In that\n                // last case, we either remove the spaces if there are spaces in\n                // a visible text node going further in the direction or we\n                // change the visiblity or those spaces.\n                if (!isWhitespace(node)) {\n                    foundVisibleSpaceTextNode = node;\n                    break;\n                } else {\n                    invisibleSpaceTextNodes.push(node);\n                }\n            } else if (!isWhitespace(node)) {\n                break;\n            }\n        } else {\n            break;\n        }\n    }\n\n    if (rule.spaceVisibility === undefined) {\n        return;\n    }\n    if (!rule.spaceVisibility) {\n        for (const node of invisibleSpaceTextNodes) {\n            // Empty and not remove to not mess with offset-based positions in\n            // commands implementation, also remove non-block empty parents.\n            node.nodeValue = \"\";\n            const ancestorPath = closestPath(node.parentNode);\n            let toRemove = null;\n            for (const pNode of ancestorPath) {\n                if (toRemove) {\n                    toRemove.remove();\n                }\n                if (pNode.childNodes.length === 1 && !isBlock(pNode)) {\n                    pNode.after(node);\n                    toRemove = pNode;\n                } else {\n                    break;\n                }\n            }\n        }\n    }\n    const spaceNode = foundVisibleSpaceTextNode || invisibleSpaceTextNodes[0];\n    if (spaceNode) {\n        let spaceVisibility = rule.spaceVisibility;\n        // In case we are asked to replace the space by a &nbsp;, disobey and\n        // do the opposite if that space is currently not visible\n        // TODO I'd like this to not be needed, it feels wrong...\n        if (\n            spaceVisibility &&\n            !foundVisibleSpaceTextNode &&\n            getState(...rightPos(spaceNode), DIRECTIONS.RIGHT).cType & CTGROUPS.BLOCK &&\n            getState(...leftPos(spaceNode), DIRECTIONS.LEFT).cType !== CTYPES.CONTENT\n        ) {\n            spaceVisibility = false;\n        }\n        spaceNode.nodeValue = spaceNode.nodeValue.replace(\n            whitespaceAtEdgeRegex,\n            spaceVisibility ? \"\\u00A0\" : \"\"\n        );\n    }\n}\n\n/**\n * Call this function to start watching for mutations.\n * Call the returned function to stop watching and get the mutation records.\n *\n * @returns {() => MutationRecord[]}\n */\nexport function observeMutations(target, observerOptions) {\n    const records = [];\n    const observerCallback = (mutations) => records.push(...mutations);\n    const observer = new MutationObserver(observerCallback);\n    observer.observe(target, observerOptions);\n    return () => {\n        observerCallback(observer.takeRecords());\n        observer.disconnect();\n        return records;\n    };\n}\n", "import { DIRECTIONS } from \"./position\";\n\nexport const closestPath = function* (node) {\n    while (node) {\n        yield node;\n        node = node.parentNode;\n    }\n};\n\n/**\n * Find a node.\n * @param {findCallback} findCallback - This callback check if this function\n *      should return `node`.\n * @param {findCallback} stopCallback - This callback check if this function\n *      should stop when it receive `node`.\n */\nexport function findNode(domPath, findCallback = () => true, stopCallback = () => false) {\n    for (const node of domPath) {\n        if (findCallback(node)) {\n            return node;\n        }\n        if (stopCallback(node)) {\n            break;\n        }\n    }\n    return null;\n}\n\n/**\n * @param {Node} node\n * @param {HTMLElement} limitAncestor - non inclusive limit ancestor to search for\n * @param {Function} predicate\n * @returns {Node|null}\n */\nexport function findUpTo(node, limitAncestor, predicate) {\n    while (node !== limitAncestor) {\n        if (predicate(node)) {\n            return node;\n        }\n        node = node.parentElement;\n    }\n    return null;\n}\n\n/**\n * @param {Node} node\n * @param {HTMLElement} limitAncestor - non inclusive limit ancestor to search for\n * @param {Function} predicate\n * @returns {Node|undefined}\n */\nexport function findFurthest(node, limitAncestor, predicate) {\n    const nodes = [];\n    while (node !== limitAncestor) {\n        nodes.push(node);\n        node = node.parentNode;\n    }\n    return nodes.findLast(predicate);\n}\n\n/**\n * Returns the closest HTMLElement of the provided Node. If the predicate is a\n * string, returns the closest HTMLElement that match the predicate selector. If\n * the predicate is a function, returns the closest element that matches the\n * predicate. Any returned element will be contained within the editable, or is\n * disconnected from any Document.\n *\n * Rationale: this helper is used to manipulate editor nodes, and should never\n * match any node outside of that scope. Disconnected nodes are assumed to be\n * from the editor, since they are likely removed nodes evaluated in the context\n * of the MutationObserver handler @see ProtectedNodePlugin\n *\n * @param {Node} node\n * @param {string | Function} [predicate='*']\n * @returns {HTMLElement|null}\n */\nexport function closestElement(node, predicate = \"*\") {\n    let element = node.nodeType === Node.ELEMENT_NODE ? node : node.parentElement;\n    const editable = element?.closest(\".odoo-editor-editable\");\n    if (typeof predicate === \"function\") {\n        while (element && !predicate(element)) {\n            element = element.parentElement;\n        }\n    } else {\n        element = element?.closest(predicate);\n    }\n    if ((editable && editable.contains(element)) || !node.isConnected) {\n        return element;\n    }\n    return null;\n}\n\n/**\n * Returns a list of all the ancestors nodes of the provided node.\n *\n * @param {Node} node\n * @param {Node} [editable] include to prevent bubbling up further than the editable.\n * @returns {HTMLElement[]}\n */\nexport function ancestors(node, editable) {\n    const result = [];\n    while (node && node.parentElement && node !== editable) {\n        result.push(node.parentElement);\n        node = node.parentElement;\n    }\n    return result;\n}\n\n/**\n * Get a static array of children, to avoid manipulating the live HTMLCollection\n * for better performances.\n *\n * @param {Element}} elem\n * @returns {Array<Element>} children\n */\nexport function children(elem) {\n    const children = [];\n    let child = elem.firstElementChild;\n    while (child) {\n        children.push(child);\n        child = child.nextElementSibling;\n    }\n    return children;\n}\n\n/**\n * Get a static array of childNodes, to avoid manipulating the live NodeList for\n * better performances.\n *\n * @param {Node}} node\n * @returns {Array<Node>} childNodes\n */\nexport function childNodes(node) {\n    const childNodes = [];\n    let child = node.firstChild;\n    while (child) {\n        childNodes.push(child);\n        child = child.nextSibling;\n    }\n    return childNodes;\n}\n\n/**\n * Take a node, return all of its descendants, in depth-first order.\n *\n * @param {Node} node\n * @returns {Node[]}\n */\nexport function descendants(node, posterity = []) {\n    let child = node.firstChild;\n    while (child) {\n        posterity.push(child);\n        descendants(child, posterity);\n        child = child.nextSibling;\n    }\n    return posterity;\n}\n\n/**\n * Values which can be returned while browsing the DOM which gives information\n * to why the path ended.\n */\nexport const PATH_END_REASONS = {\n    NO_NODE: 0,\n    BLOCK_OUT: 1,\n    BLOCK_HIT: 2,\n    OUT_OF_SCOPE: 3,\n};\n\n/**\n * Creates a generator function according to the given parameters. Pre-made\n * generators to traverse the DOM are made using this function:\n *\n * @see leftLeafFirstPath\n * @see leftLeafOnlyNotBlockPath\n * @see leftLeafOnlyInScopeNotBlockEditablePath\n * @see rightLeafOnlyNotBlockPath\n * @see rightLeafOnlyNotBlockNotEditablePath\n *\n * @param {boolean} direction\n * @param {Object} options\n * @param {boolean} [options.leafOnly] if true, do not yield any non-leaf node\n * @param {boolean} [options.inScope] if true, stop the generator as soon as a node is not\n *                      a descendant of `node` provided when traversing the\n *                      generated function.\n * @param {Function} [options.stopTraverseFunction] a function that takes a node\n *                      and should return true when a node descendant should not\n *                      be traversed.\n * @param {Function} [options.stopFunction] function that makes the generator stop when a\n *                      node is encountered.\n */\nexport function createDOMPathGenerator(\n    direction,\n    { leafOnly = false, inScope = false, stopTraverseFunction, stopFunction } = {}\n) {\n    const nextDeepest =\n        direction === DIRECTIONS.LEFT\n            ? (node) => lastLeaf(node.previousSibling, stopTraverseFunction)\n            : (node) => firstLeaf(node.nextSibling, stopTraverseFunction);\n\n    const firstNode =\n        direction === DIRECTIONS.LEFT\n            ? (node, offset) => lastLeaf(node.childNodes[offset - 1], stopTraverseFunction)\n            : (node, offset) => firstLeaf(node.childNodes[offset], stopTraverseFunction);\n\n    // Note \"reasons\" is a way for the caller to be able to know why the\n    // generator ended yielding values.\n    return function* (node, offset, reasons = []) {\n        let movedUp = false;\n\n        let currentNode = firstNode(node, offset);\n        if (!currentNode) {\n            movedUp = true;\n            currentNode = node;\n        }\n\n        while (currentNode) {\n            if (stopFunction && stopFunction(currentNode)) {\n                reasons.push(movedUp ? PATH_END_REASONS.BLOCK_OUT : PATH_END_REASONS.BLOCK_HIT);\n                break;\n            }\n            if (inScope && currentNode === node) {\n                reasons.push(PATH_END_REASONS.OUT_OF_SCOPE);\n                break;\n            }\n            if (!(leafOnly && movedUp)) {\n                yield currentNode;\n            }\n\n            movedUp = false;\n            let nextNode = nextDeepest(currentNode);\n            if (!nextNode) {\n                movedUp = true;\n                nextNode = currentNode.parentNode;\n            }\n            currentNode = nextNode;\n        }\n\n        reasons.push(PATH_END_REASONS.NO_NODE);\n    };\n}\n\n/**\n * Returns the deepest child in last position.\n *\n * @param {Node} node\n * @param {Function} [stopTraverseFunction]\n * @returns {Node}\n */\nexport function lastLeaf(node, stopTraverseFunction) {\n    while (node && node.lastChild && !(stopTraverseFunction && stopTraverseFunction(node))) {\n        node = node.lastChild;\n    }\n    return node;\n}\n/**\n * Returns the deepest child in first position.\n *\n * @param {Node} node\n * @param {Function} [stopTraverseFunction]\n * @returns {Node}\n */\nexport function firstLeaf(node, stopTraverseFunction) {\n    while (node && node.firstChild && !(stopTraverseFunction && stopTraverseFunction(node))) {\n        node = node.firstChild;\n    }\n    return node;\n}\n\n/**\n * Returns all the previous siblings of the given node until the first\n * sibling that does not satisfy the predicate, in lookup order.\n *\n * @param {Node} node\n * @param {Function} [predicate] (node: Node) => boolean\n */\nexport function getAdjacentPreviousSiblings(node, predicate = (n) => !!n) {\n    let previous = node.previousSibling;\n    const list = [];\n    while (previous && predicate(previous)) {\n        list.push(previous);\n        previous = previous.previousSibling;\n    }\n    return list;\n}\n/**\n * Returns all the next siblings of the given node until the first\n * sibling that does not satisfy the predicate, in lookup order.\n *\n * @param {Node} node\n * @param {Function} [predicate] (node: Node) => boolean\n */\nexport function getAdjacentNextSiblings(node, predicate = (n) => !!n) {\n    let next = node.nextSibling;\n    const list = [];\n    while (next && predicate(next)) {\n        list.push(next);\n        next = next.nextSibling;\n    }\n    return list;\n}\n/**\n * Returns all the adjacent siblings of the given node until the first sibling\n * (in both directions) that does not satisfy the predicate, in index order. If\n * the given node does not satisfy the predicate, an empty array is returned.\n *\n * @param {Node} node\n * @param {Function} [predicate] (node: Node) => boolean\n */\nexport function getAdjacents(node, predicate = (n) => !!n) {\n    const previous = getAdjacentPreviousSiblings(node, predicate);\n    const next = getAdjacentNextSiblings(node, predicate);\n    return predicate(node) ? [...previous.reverse(), node, ...next] : [];\n}\n\n/**\n * Returns the deepest common ancestor element of the given nodes within the\n * specified root element. If no root element is provided, the entire document\n * is considered as the root.\n *\n * @param {Node[]} nodes - The nodes for which to find the common ancestor.\n * @param {Element} [root] - The root element within which to search for the common ancestor.\n * @returns {Element|null} - The common ancestor element, or null if no common ancestor is found.\n */\nexport function getCommonAncestor(nodes, root = undefined) {\n    const pathsToRoot = nodes.map((node) => [node, ...ancestors(node, root)]);\n\n    let candidate = pathsToRoot[0]?.at(-1);\n    if (root && candidate !== root) {\n        return null;\n    }\n    let commonAncestor = null;\n    while (candidate && pathsToRoot.every((path) => path.at(-1) === candidate)) {\n        commonAncestor = candidate;\n        pathsToRoot.forEach((path) => path.pop());\n        candidate = pathsToRoot[0].at(-1);\n    }\n    return commonAncestor;\n}\n\n/**\n * Basically a wrapper around `root.querySelectorAll` that includes the\n * root.\n *\n * @param {Element} root\n * @param {string} selector\n * @returns {Generator<Element>}\n */\nexport const selectElements = function* (root, selector) {\n    if (root.matches(selector)) {\n        yield root;\n    }\n    for (const elem of root.querySelectorAll(selector)) {\n        yield elem;\n    }\n};\n", "import { makeDraggableHook } from \"@web/core/utils/draggable_hook_builder\";\nimport { pick } from \"@web/core/utils/objects\";\nimport { reactive } from \"@odoo/owl\";\nimport { throttleForAnimation } from \"@web/core/utils/timing\";\nimport { closest, touching } from \"@web/core/utils/ui\";\n\n/** @typedef {import(\"@web/core/utils/draggable_hook_builder\").DraggableHandlerParams} DraggableHandlerParams */\n/** @typedef {import(\"@web/core/utils/draggable_hook_builder\").DraggableBuilderParams} DraggableBuilderParams */\n/** @typedef {import(\"@web/core/utils/draggable\").DraggableParams} DraggableParams */\n\n/** @typedef {DraggableHandlerParams & { dropzone: HTMLElement | null, helper: HTMLElement }} DragAndDropHandlerParams */\n/** @typedef {DraggableHandlerParams & { helper: HTMLElement }} DragAndDropStartParams */\n/** @typedef {DraggableHandlerParams & { dropzone: HTMLElement }} DropzoneHandlerParams */\n/**\n * @typedef DragAndDropParams\n * @extends {DraggableParams}\n *\n * MANDATORY\n * @property {(() => Array)} dropzones a function that returns the available dropzones\n * @property {(() => HTMLElement)} helper a function that returns a helper element\n * that will follow the cursor when dragging\n * @property {(() => HTMLElement)} scrollingElement a function that returns the\n * element on which a scroll should be triggered\n *\n * HANDLERS (Optional)\n * @property {(params: DragAndDropStartParams) => any} [onDragStart]\n * called when a dragging sequence is initiated\n * @property {(params: DropzoneHandlerParams) => any} [dropzoneOver]\n * called when an element is over a dropzone\n * @property {(params: DropzoneHandlerParams) => any} [dropzoneOut]\n * called when an element is leaving a dropzone\n * @property {(params: DragAndDropHandlerParams) => any} [onDrag]\n * called when an element is being dragged\n * @property {(params: DragAndDropHandlerParams) => any} [onDragEnd]\n * called when the dragging sequence is over\n */\n/**\n * @typedef NativeDraggableState\n * @property {(params: DraggableParams) => any} update\n * method to update the params of the draggable\n * @property {import(\"@web/core/utils/draggable\").DraggableState} state\n * state of the draggable component\n * @property {() => any} destroy\n * method to destroy and unbind the draggable component\n */\n/**\n * Utility function to create a native draggable component\n *\n * @param {DraggableBuilderParams} hookParams\n * @param {DraggableParams} initialParams\n * @returns {NativeDraggableState}\n */\nexport function useNativeDraggable(hookParams, initialParams) {\n    const setupFunctions = new Map();\n    const cleanupFunctions = [];\n    const currentParams = { ...initialParams };\n    const setupHooks = {\n        wrapState: reactive,\n        throttle: throttleForAnimation,\n        addListener: (el, type, callback, options) => {\n            el.addEventListener(type, callback, options);\n            cleanupFunctions.push(() => el.removeEventListener(type, callback));\n        },\n        setup: (setupFn, depsFn) => setupFunctions.set(setupFn, depsFn),\n        teardown: (cleanupFn) => {\n            cleanupFunctions.push(cleanupFn);\n        },\n    };\n    // Compatibility for tests\n    const el = initialParams.ref.el;\n    // TODO this is probably to be removed in master: the received params\n    // contain the selector that should be checked and it will be transferred\n    // to the makeDraggableHook function. There should not be any need to add\n    // the default selector class here.\n    el.classList.add(\"o_draggable\");\n    cleanupFunctions.push(() => el.classList.remove(\"o_draggable\"));\n\n    const draggableState = makeDraggableHook({ setupHooks, ...hookParams })(currentParams);\n    draggableState.enable = true;\n    const draggableComponent = {\n        state: draggableState,\n        update: (newParams) => {\n            Object.assign(currentParams, newParams);\n            setupFunctions.forEach((depsFn, setupFn) => setupFn(...depsFn()));\n        },\n        destroy: () => {\n            cleanupFunctions.forEach((cleanupFn) => cleanupFn());\n        },\n    };\n    draggableComponent.update({});\n    return draggableComponent;\n}\n\nfunction updateElementPosition(el, { x, y }, styleFn, offset = { x: 0, y: 0 }) {\n    return styleFn(el, { top: `${y - offset.y}px`, left: `${x - offset.x}px` });\n}\n/** @type DraggableBuilderParams */\nconst dragAndDropHookParams = {\n    name: \"useDragAndDrop\",\n    acceptedParams: {\n        dropzones: [Function],\n        scrollingElement: [Function],\n        helper: [Function],\n        extraWindow: [Object, Function],\n    },\n    edgeScrolling: { enabled: true },\n    onComputeParams({ ctx, params }) {\n        // The helper is mandatory and will follow the cursor instead\n        ctx.followCursor = false;\n        ctx.getScrollingElement = params.scrollingElement;\n        ctx.getHelper = params.helper;\n        ctx.getDropZones = params.dropzones;\n    },\n    onWillStartDrag: ({ ctx }) => {\n        ctx.current.container = ctx.getScrollingElement();\n        ctx.current.helperOffset = { x: 0, y: 0 };\n    },\n    onDragStart: ({ ctx, addStyle, addCleanup }) => {\n        // Use the helper as the tracking element to properly update scroll values.\n        ctx.current.helper = ctx.getHelper({ ...ctx.current, ...ctx.pointer });\n        ctx.current.helper.style.position = \"fixed\";\n        // We want the pointer events on the helper so that the cursor\n        // is properly displayed.\n        ctx.current.element.classList.remove(\"o_dragged\");\n        ctx.current.helper.style.cursor = ctx.cursor;\n        ctx.current.helper.style.pointerEvents = \"auto\";\n\n        // If the helper is inside the iframe, we want pointer events on the\n        // frame element so that they reach the window and properly apply\n        // the cursor.\n        const frameElement = ctx.current.helper.ownerDocument.defaultView.frameElement;\n        if (frameElement) {\n            addStyle(frameElement, { pointerEvents: \"auto\" });\n        }\n\n        addCleanup(() => ctx.current.helper.remove());\n\n        updateElementPosition(ctx.current.helper, ctx.pointer, addStyle, ctx.current.helperOffset);\n\n        return pick(ctx.current, \"element\", \"helper\");\n    },\n    onDrag: ({ ctx, addStyle, callHandler }) => {\n        ctx.current.helper.classList.add(\"o_draggable_dragging\");\n\n        updateElementPosition(ctx.current.helper, ctx.pointer, addStyle, ctx.current.helperOffset);\n        // Unfortunately, DOMRect is not an Object, so spreading operator from\n        // `touching` does not work, so convert DOMRect to plain object.\n        let helperRect = ctx.current.helper.getBoundingClientRect();\n        helperRect = {\n            x: helperRect.x,\n            y: helperRect.y,\n            width: helperRect.width,\n            height: helperRect.height,\n        };\n        const dropzoneEl = closest(touching(ctx.getDropZones(), helperRect), helperRect);\n        // Update the drop zone if it's in grid mode\n        if (\n            ctx.current.dropzone?.el &&\n            ctx.current.dropzone.el.classList.contains(\"oe_grid_zone\")\n        ) {\n            ctx.current.dropzone.rect = ctx.current.dropzone.el.getBoundingClientRect();\n        }\n        if (\n            ctx.current.dropzone &&\n            (ctx.current.dropzone.el === dropzoneEl ||\n                (!dropzoneEl &&\n                    touching([ctx.current.helper], ctx.current.dropzone.rect).length > 0))\n        ) {\n            // If no new dropzone but old one is still valid, return early.\n            return pick(ctx.current, \"element\", \"dropzone\", \"helper\");\n        }\n\n        if (ctx.current.dropzone && dropzoneEl !== ctx.current.dropzone.el) {\n            callHandler(\"dropzoneOut\", {\n                dropzone: ctx.current.dropzone,\n                helper: ctx.current.helper,\n            });\n            delete ctx.current.dropzone;\n        }\n\n        if (dropzoneEl) {\n            // Save rect information prior to calling the over function\n            // to keep a consistent dropzone even if content was added.\n            const rect = DOMRect.fromRect(dropzoneEl.getBoundingClientRect());\n            ctx.current.dropzone = {\n                el: dropzoneEl,\n                rect: {\n                    x: rect.x,\n                    y: rect.y,\n                    width: rect.width,\n                    height: rect.height,\n                },\n            };\n            callHandler(\"dropzoneOver\", {\n                dropzone: ctx.current.dropzone,\n                helper: ctx.current.helper,\n            });\n        }\n        return pick(ctx.current, \"element\", \"dropzone\", \"helper\");\n    },\n    onDragEnd({ ctx }) {\n        return pick(ctx.current, \"element\", \"dropzone\", \"helper\");\n    },\n};\n/**\n * Function to start a drag and drop handler\n *\n * @param {DragAndDropParams} initialParams params given to the drag and drop\n * component\n * @returns {NativeDraggableState}\n */\nexport function useDragAndDrop(initialParams) {\n    return useNativeDraggable(dragAndDropHookParams, initialParams);\n}\n", "export const fonts = {\n    /**\n     * Retrieves all the CSS rules which match the given parser (Regex).\n     *\n     * @param {Regex} filter\n     * @returns {Object[]} Array of CSS rules descriptions (objects). A rule is\n     *          defined by 3 values: 'selector', 'css' and 'names'. 'selector'\n     *          is a string which contains the whole selector, 'css' is a string\n     *          which contains the css properties and 'names' is an array of the\n     *          first captured groups for each selector part. E.g.: if the\n     *          filter is set to match .fa-* rules and capture the icon names,\n     *          the rule:\n     *              '.fa-alias1::before, .fa-alias2::before { hello: world; }'\n     *          will be retrieved as\n     *              {\n     *                  selector: '.fa-alias1::before, .fa-alias2::before',\n     *                  css: 'hello: world;',\n     *                  names: ['.fa-alias1', '.fa-alias2'],\n     *              }\n     */\n    cacheCssSelectors: {},\n    getCssSelectors: function (filter) {\n        if (this.cacheCssSelectors[filter]) {\n            return this.cacheCssSelectors[filter];\n        }\n        this.cacheCssSelectors[filter] = [];\n        var sheets = document.styleSheets;\n        for (var i = 0; i < sheets.length; i++) {\n            var rules;\n            try {\n                // try...catch because Firefox not able to enumerate\n                // document.styleSheets[].cssRules[] for cross-domain\n                // stylesheets.\n                rules = sheets[i].rules || sheets[i].cssRules;\n            } catch {\n                continue;\n            }\n            if (!rules) {\n                continue;\n            }\n\n            for (var r = 0; r < rules.length; r++) {\n                var selectorText = rules[r].selectorText;\n                if (!selectorText) {\n                    continue;\n                }\n                var selectors = selectorText.split(/\\s*,\\s*/);\n                var data = null;\n                for (var s = 0; s < selectors.length; s++) {\n                    var match = selectors[s].trim().match(filter);\n                    if (!match) {\n                        continue;\n                    }\n                    if (!data) {\n                        data = {\n                            selector: match[0],\n                            css: rules[r].cssText.replace(/(^.*\\{\\s*)|(\\s*\\}\\s*$)/g, \"\"),\n                            names: [match[1]],\n                        };\n                    } else {\n                        data.selector += \", \" + match[0];\n                        data.names.push(match[1]);\n                    }\n                }\n                if (data) {\n                    this.cacheCssSelectors[filter].push(data);\n                }\n            }\n        }\n        return this.cacheCssSelectors[filter];\n    },\n    /**\n     * List of font icons to load by editor. The icons are displayed in the media\n     * editor and identified like font and image (can be colored, spinned, resized\n     * with fa classes).\n     * To add font, push a new object {base, parser}\n     *\n     * - base: class who appear on all fonts\n     * - parser: regular expression used to select all font in css stylesheets\n     *\n     * @type Array\n     */\n    fontIcons: [{ base: \"fa\", parser: /\\.(fa-(?:\\w|-)+)::?before/i }],\n    computedFonts: false,\n    /**\n     * Searches the fonts described by the @see fontIcons variable.\n     */\n    computeFonts: function () {\n        if (!this.computedFonts) {\n            var self = this;\n            this.fontIcons.forEach((data) => {\n                data.cssData = self.getCssSelectors(data.parser);\n                data.alias = data.cssData.map((x) => x.names).flat();\n            });\n            this.computedFonts = true;\n        }\n    },\n};\n", "import { normalizeCSSColor } from \"@web/core/utils/colors\";\nimport { removeClass } from \"./dom\";\nimport { isBold, isDirectionSwitched, isItalic, isStrikeThrough, isUnderline } from \"./dom_info\";\nimport { closestElement, closestPath, findNode } from \"./dom_traversal\";\nimport { closestBlock, isBlock } from \"./blocks\";\n\n/**\n * Array of all the classes used by the editor to change the font size.\n */\nexport const FONT_SIZE_CLASSES = [\n    \"display-1-fs\",\n    \"display-2-fs\",\n    \"display-3-fs\",\n    \"display-4-fs\",\n    \"h1-fs\",\n    \"h2-fs\",\n    \"h3-fs\",\n    \"h4-fs\",\n    \"h5-fs\",\n    \"h6-fs\",\n    \"base-fs\",\n    \"small\",\n    \"o_small-fs\",\n];\n\nexport const TEXT_STYLE_CLASSES = [\"display-1\", \"display-2\", \"display-3\", \"display-4\", \"lead\"];\n\nexport const DEFAULT_FONT_SIZE_CLASSES = [\n    \"h1\",\n    \"h2\",\n    \"h3\",\n    \"h4\",\n    \"h5\",\n    \"h6\",\n    \"o_default_font_size\",\n];\n\nexport const FORMATTABLE_TAGS = [\"SPAN\", \"FONT\", \"B\", \"STRONG\", \"I\", \"EM\", \"U\", \"S\"];\n\nexport const formatsSpecs = {\n    italic: {\n        tagName: \"em\",\n        isFormatted: isItalic,\n        isTag: (node) => [\"EM\", \"I\"].includes(node.tagName),\n        hasStyle: (node) => Boolean(node.style && node.style[\"font-style\"]),\n        addStyle: (node) => (node.style[\"font-style\"] = \"italic\"),\n        addNeutralStyle: (node) => (node.style[\"font-style\"] = \"normal\"),\n        removeStyle: (node) => removeStyle(node, \"font-style\"),\n    },\n    bold: {\n        tagName: \"strong\",\n        isFormatted: isBold,\n        isTag: (node) => [\"STRONG\", \"B\"].includes(node.tagName),\n        hasStyle: (node) => Boolean(node.style && node.style[\"font-weight\"]),\n        addStyle: (node) => (node.style[\"font-weight\"] = \"bolder\"),\n        addNeutralStyle: (node) => {\n            node.style[\"font-weight\"] = \"normal\";\n        },\n        removeStyle: (node) => removeStyle(node, \"font-weight\"),\n    },\n    underline: {\n        tagName: \"u\",\n        isFormatted: isUnderline,\n        isTag: (node) => node.tagName === \"U\",\n        hasStyle: (node) =>\n            node.style &&\n            (node.style[\"text-decoration\"].includes(\"underline\") ||\n                node.style[\"text-decoration-line\"].includes(\"underline\")),\n        addStyle: (node) => (node.style[\"text-decoration-line\"] += \" underline\"),\n        removeStyle: (node) =>\n            removeStyle(\n                node,\n                node.style[\"text-decoration\"].includes(\"underline\")\n                    ? \"text-decoration\"\n                    : \"text-decoration-line\",\n                \"underline\"\n            ),\n    },\n    strikeThrough: {\n        tagName: \"s\",\n        isFormatted: isStrikeThrough,\n        isTag: (node) => node.tagName === \"S\",\n        hasStyle: (node) =>\n            node.style &&\n            (node.style[\"text-decoration\"].includes(\"line-through\") ||\n                node.style[\"text-decoration-line\"].includes(\"line-through\")),\n        addStyle: (node) => (node.style[\"text-decoration-line\"] += \" line-through\"),\n        removeStyle: (node) =>\n            removeStyle(\n                node,\n                node.style[\"text-decoration\"].includes(\"line-through\")\n                    ? \"text-decoration\"\n                    : \"text-decoration-line\",\n                \"line-through\"\n            ),\n    },\n    fontFamily: {\n        isFormatted: (node) => !!closestElement(node, (el) => el.style[\"font-family\"]),\n        hasStyle: (node) => node.style && node.style[\"font-family\"],\n        addStyle: (node, props) => {\n            removeStyle(node, \"font-family\");\n            if (props.fontFamily) {\n                node.style[\"font-family\"] = props.fontFamily;\n            }\n        },\n        removeStyle: (node) => removeStyle(node, \"font-family\"),\n    },\n    fontSize: {\n        isFormatted: (node, props) => {\n            const fontSize = (\n                findNode(closestPath(node), (el) => el.style?.[\"font-size\"], isBlock) ||\n                closestElement(node, \"li\")\n            )?.style[\"font-size\"];\n            return props?.size ? fontSize === props.size : fontSize;\n        },\n        hasStyle: (node) => node.style && node.style[\"font-size\"],\n        addStyle: (node, props) => {\n            node.style[\"font-size\"] = props.size;\n            removeClass(node, ...FONT_SIZE_CLASSES);\n        },\n        removeStyle: (node) => removeStyle(node, \"font-size\"),\n    },\n    setFontSizeClassName: {\n        isFormatted: (node, props) =>\n            props?.className\n                ? FONT_SIZE_CLASSES.includes(props.className) &&\n                  !!(\n                      findNode(\n                          closestPath(node),\n                          (el) => el.classList?.contains(props.className),\n                          (el) => el === closestBlock(node).parentElement\n                      ) || closestElement(node, \"li\")?.classList?.contains(props.className)\n                  )\n                : !!findNode(\n                      closestPath(node),\n                      (el) => FONT_SIZE_CLASSES.find((cls) => el.classList?.contains(cls)),\n                      (el) => el === closestBlock(node).parentElement\n                  ) ||\n                  FONT_SIZE_CLASSES.find((cls) =>\n                      closestElement(node, \"li\")?.classList.contains(cls)\n                  ),\n        hasStyle: (node, props) =>\n            [...FONT_SIZE_CLASSES, ...TEXT_STYLE_CLASSES, ...DEFAULT_FONT_SIZE_CLASSES].find(\n                (cls) => node.classList.contains(cls)\n            ),\n        addStyle: (node, props) => {\n            node.style.removeProperty(\"font-size\");\n            node.classList.add(props.className);\n        },\n        removeStyle: (node) => {\n            removeStyle(node, \"font-size\");\n            removeClass(node, ...FONT_SIZE_CLASSES);\n            // Typography classes should be preserved on block elements since\n            // they act as semantic equivalents of <h1>, <h2>, etc., not just\n            // removable styles.\n            if (!isBlock(node)) {\n                removeClass(node, ...TEXT_STYLE_CLASSES, ...DEFAULT_FONT_SIZE_CLASSES);\n            }\n        },\n        addNeutralStyle: function (node) {\n            const block = closestBlock(node);\n            if ([\"H1\", \"H2\", \"H3\", \"H4\", \"H5\", \"H6\"].includes(block.nodeName)) {\n                node.classList.add(block.nodeName.toLowerCase());\n            } else {\n                node.classList.add(\"o_default_font_size\");\n            }\n        },\n    },\n    switchDirection: {\n        isFormatted: (node, props) => isDirectionSwitched(node, props.editable),\n    },\n};\n\nfunction removeStyle(node, styleName, item) {\n    if (item) {\n        const newStyle = node.style[styleName]\n            .split(\" \")\n            .filter((x) => x !== item)\n            .join(\" \");\n        node.style[styleName] = newStyle || null;\n    } else {\n        node.style[styleName] = null;\n    }\n    if (node.getAttribute(\"style\") === \"\") {\n        node.removeAttribute(\"style\");\n    }\n}\n\n/**\n * @param {string} key\n * @param {object} htmlStyle\n * @returns {string}\n */\nexport function getCSSVariableValue(key, htmlStyle) {\n    // Get trimmed value from the HTML element\n    let value = htmlStyle.getPropertyValue(`--${key}`).trim();\n    // If it is a color value, it needs to be normalized\n    value = normalizeCSSColor(value);\n    // Normally scss-string values are \"printed\" single-quoted. That way no\n    // magic conversation is needed when customizing a variable: either save it\n    // quoted for strings or non quoted for colors, numbers, etc. However,\n    // Chrome has the annoying behavior of changing the single-quotes to\n    // double-quotes when reading them through getPropertyValue...\n    return value.replace(/\"/g, \"'\");\n}\n\n/**\n * Key-value mapping to list converters from an unit A to an unit B.\n * - The key is a string in the format '$1-$2' where $1 is the CSS symbol of\n *   unit A and $2 is the CSS symbol of unit B.\n * - The value is a function that converts the received value (expressed in\n *   unit A) to another value expressed in unit B. Two other parameters is\n *   received: the css property on which the unit applies and the jQuery element\n *   on which that css property may change.\n */\nconst CSS_UNITS_CONVERSION = {\n    \"s-ms\": () => 1000,\n    \"ms-s\": () => 0.001,\n    \"rem-px\": (htmlStyle) => parseFloat(htmlStyle[\"font-size\"]),\n    \"px-rem\": (htmlStyle) => 1 / parseFloat(htmlStyle[\"font-size\"]),\n    \"%-px\": () => -1, // Not implemented but should simply be ignored for now\n    \"px-%\": () => -1, // Not implemented but should simply be ignored for now\n};\n\n/**\n * Converts the given numeric value expressed in the given css unit into\n * the corresponding numeric value expressed in the other given css unit.\n *\n * e.g. fct(400, 'ms', 's') -> 0.4\n *\n * @param {number} value\n * @param {string} unitFrom\n * @param {string} unitTo\n * @param {object} htmlStyle\n * @returns {number}\n */\nexport function convertNumericToUnit(value, unitFrom, unitTo, htmlStyle) {\n    if (Math.abs(value) < Number.EPSILON || unitFrom === unitTo) {\n        return value;\n    }\n    const converter = CSS_UNITS_CONVERSION[`${unitFrom}-${unitTo}`];\n    if (converter === undefined) {\n        throw new Error(`Cannot convert '${unitFrom}' units into '${unitTo}' units !`);\n    }\n    return value * converter(htmlStyle);\n}\n\nexport function getHtmlStyle(document) {\n    return document.defaultView.getComputedStyle(document.documentElement);\n}\n\n/**\n * Finds the font size to display for the current selection. We cannot rely\n * on the computed font-size only as font-sizes are responsive and we always\n * want to display the desktop (integer when possible) one.\n *\n * @param {Selection} sel The current selection.\n * @param {Document} document The document of the current selection.\n * @returns {Float} The font size to display.\n */\nexport function getFontSizeDisplayValue(sel, document) {\n    const tagNameRelatedToFontSize = [\"h1\", \"h2\", \"h3\", \"h4\", \"h5\", \"h6\"];\n    const styleClassesRelatedToFontSize = [\n        \"display-1\",\n        \"display-2\",\n        \"display-3\",\n        \"display-4\",\n        \"lead\",\n    ];\n    const closestStartContainerEl = closestElement(sel.startContainer);\n    const closestFontSizedEl = closestStartContainerEl.closest(`\n        [style*='font-size'],\n        ${FONT_SIZE_CLASSES.map((className) => `.${className}`)},\n        ${styleClassesRelatedToFontSize.map((className) => `.${className}`)},\n        ${tagNameRelatedToFontSize}\n    `);\n    let remValue;\n    const htmlStyle = getHtmlStyle(document);\n    if (closestFontSizedEl) {\n        const useFontSizeInput = closestFontSizedEl.style.fontSize;\n        if (useFontSizeInput) {\n            // Use the computed value to always convert to px. However, this\n            // currently does not check that the inline font-size is the one\n            // actually having an effect (there could be an !important CSS rule\n            // forcing something else).\n            // TODO align with the behavior of the rest of the editor snippet\n            // options.\n            return parseFloat(getComputedStyle(closestStartContainerEl).fontSize);\n        }\n        // It's a class font size or a hN tag. We don't return the computed\n        // font size because it can be different from the one displayed in\n        // the toolbar because it's responsive.\n        const fontSizeClass = FONT_SIZE_CLASSES.find((className) =>\n            closestFontSizedEl.classList.contains(className)\n        );\n        let fsName;\n        if (fontSizeClass) {\n            fsName = fontSizeClass.substring(0, fontSizeClass.length - 3); // Without -fs\n        } else {\n            fsName =\n                styleClassesRelatedToFontSize.find((className) =>\n                    closestFontSizedEl.classList.contains(className)\n                ) || closestFontSizedEl.tagName.toLowerCase();\n        }\n        remValue = parseFloat(getCSSVariableValue(`${fsName}-font-size`, htmlStyle));\n    }\n    const pxValue = remValue && convertNumericToUnit(remValue, \"rem\", \"px\", htmlStyle);\n    return pxValue || parseFloat(getComputedStyle(closestStartContainerEl).fontSize);\n}\n\nexport function getFontSizeOrClass(node) {\n    if (!node) {\n        return null;\n    }\n\n    if (node.style.fontSize) {\n        return { type: \"font-size\", value: node.style.fontSize };\n    }\n\n    const fontSizeClass = FONT_SIZE_CLASSES.find((cls) => node.classList.contains(cls));\n    if (fontSizeClass) {\n        return { type: \"class\", value: fontSizeClass };\n    }\n    return null;\n}\n", "/**\n * Creates a version of the function that's memoized on the value of its first\n * argument, which must be an Object.\n * This is a version of @web's memoize, with the difference that it uses a\n * WeakMap instead of a Map, making it more suitable for functions that take\n * objects as arguments, as it avoids memory leaks by allowing the garbage\n * collector to clean up unused objects.\n *\n * @template T, U\n * @param {(arg: T) => U} func the function to memoize\n * @returns {(arg: T) => U} a memoized version of the original function\n */\nexport function weakMemoize(func) {\n    const cache = new WeakMap();\n    const funcName = func.name ? func.name + \" (memoized)\" : \"memoized\";\n    return {\n        [funcName](firstArg, ...args) {\n            if (!cache.has(firstArg)) {\n                cache.set(firstArg, func(firstArg, ...args));\n            }\n            return cache.get(firstArg);\n        },\n    }[funcName];\n}\n", "/**\n * @param { Document } document\n * @param { string } html\n * @returns { DocumentFragment }\n */\nexport function parseHTML(document, html) {\n    const fragment = document.createDocumentFragment();\n    const parser = new document.defaultView.DOMParser();\n    const parsedDocument = parser.parseFromString(html, \"text/html\");\n    fragment.replaceChildren(...parsedDocument.body.childNodes);\n    return fragment;\n}\n\n/**\n * Server-side, HTML is stored as a string which can have a different format\n * than what the current browser returns through outerHTML or innerHTML, notably\n * because of HTML entities.\n * This function can be used to convert strings with potential HTML entities to\n * the format used by the current browser. This allows comparisons between\n * values returned by the server and values extracted from the DOM using i.e.\n * innerHTML.\n *\n * @param { string } content\n * @param { function } cleanup receives the body element containing the parsed\n *        html, to perform some cleanup for the comparison.\n * @returns { string }\n */\nexport function normalizeHTML(content, cleanup = () => {}) {\n    const parser = new document.defaultView.DOMParser();\n    const body = parser.parseFromString(content, \"text/html\").body;\n    cleanup(body);\n    return body.innerHTML;\n}\n", "import { isColorGradient } from \"@web/core/utils/colors\";\n\n/**\n * Extracts url and gradient parts from the background-image CSS property.\n *\n * @param {string} CSS 'background-image' property value\n * @returns {Object} contains the separated 'url' and 'gradient' parts\n */\nexport function backgroundImageCssToParts(css = \"\") {\n    const parts = {};\n    if (css.startsWith(\"url(\")) {\n        const urlEnd = css.indexOf(\")\") + 1;\n        parts.url = css.substring(0, urlEnd).trim();\n        const commaPos = css.indexOf(\",\", urlEnd);\n        css = commaPos > 0 ? css.substring(commaPos + 1) : \"\";\n    }\n    if (isColorGradient(css)) {\n        parts.gradient = css.trim();\n    }\n    return parts;\n}\n\n/**\n * Combines url and gradient parts into a background-image CSS property value\n *\n * @param {Object} contains the separated 'url' and 'gradient' parts\n * @returns {string} CSS 'background-image' property value\n */\nexport function backgroundImagePartsToCss(parts) {\n    return [parts.url, parts.gradient].filter(Boolean).join(\", \") || \"\";\n}\n\n/**\n * @param {HTMLImageElement} image\n * @returns {string|null} The mimetype of the image.\n */\nexport function getMimetype(image, data = image.dataset) {\n    const src = getImageSrc(image);\n\n    return (\n        data.mimetype ||\n        data.mimetypeBeforeConversion ||\n        (src &&\n            ((src.endsWith(\".png\") && \"image/png\") ||\n                (src.endsWith(\".webp\") && \"image/webp\") ||\n                (src.endsWith(\".jpg\") && \"image/jpeg\") ||\n                (src.endsWith(\".jpeg\") && \"image/jpeg\"))) ||\n        null\n    );\n}\n\n/**\n * @param {HTMLImageElement} img\n * @returns {Promise<Boolean>}\n */\nexport async function isImageCorsProtected(img) {\n    const src = img.getAttribute(\"src\");\n    if (!src) {\n        return false;\n    }\n    let isCorsProtected = false;\n    if (!src.startsWith(\"/\") || /\\/web\\/image\\/\\d+-redirect\\//.test(src)) {\n        // The `fetch()` used later in the code might fail if the image is\n        // CORS protected. We check upfront if it's the case.\n        // Two possible cases:\n        // 1. the `src` is an absolute URL from another domain.\n        //    For instance, abc.odoo.com vs abc.com which are actually the\n        //    same database behind.\n        // 2. A \"attachment-url\" which is just a redirect to the real image\n        //    which could be hosted on another website.\n        isCorsProtected = await fetch(src, { method: \"HEAD\" })\n            .then(() => false)\n            .catch(() => true);\n    }\n    return isCorsProtected;\n}\n\n/**\n * @param {string} src\n * @returns {Promise<Boolean>}\n */\nexport async function isSrcCorsProtected(src) {\n    const dummyImg = document.createElement(\"img\");\n    dummyImg.src = src;\n    return isImageCorsProtected(dummyImg);\n}\n\n/**\n * Returns the src of the image, or the src of the background-image if the\n * element is not an image.\n *\n * @param {HTMLElement} el The element to get the src or background-image from.\n * @returns {string|null} The src of the image.\n */\nexport function getImageSrc(el) {\n    if (el.tagName === \"IMG\") {\n        return el.getAttribute(\"src\");\n    }\n    // TODO: Parallax handling is incorrectly coupled with background image source.\n    // The plugin transfer the `src` on a `span`, but parallax can be achieved via other means.\n    // example: CSS variables without this DOM manipulation.\n    // Decouple.\n    if (el.querySelector(\".s_parallax_bg\")) {\n        el = el.querySelector(\".s_parallax_bg\");\n    }\n    const url = backgroundImageCssToParts(el.style.backgroundImage).url;\n    return url && getBgImageURLFromURL(url);\n}\n\n/**\n * Parse an element's background-image's url.\n *\n * @param {string} string a css value in the form 'url(\"...\")'\n * @returns {string|false} the src of the image or false if not parsable\n */\nexport function getBgImageURLFromURL(url) {\n    const match = url.match(/^url\\((['\"])(.*?)\\1\\)$/);\n    if (!match) {\n        return \"\";\n    }\n    const matchedURL = match[2];\n    // Make URL relative if possible\n    const fullURL = new URL(matchedURL, window.location.origin);\n    if (fullURL.origin === window.location.origin) {\n        return fullURL.href.slice(fullURL.origin.length);\n    }\n    return matchedURL;\n}\n", "import { rpc } from \"@web/core/network/rpc\";\nimport { pick } from \"@web/core/utils/objects\";\nimport { loadBundle } from \"@web/core/assets\";\nimport { getImageSrc } from \"./image\";\n\n// Fields returned by cropperjs 'getData' method, also need to be passed when\n// initializing the cropper to reuse the previous crop.\nexport const cropperDataFields = [\"x\", \"y\", \"width\", \"height\", \"rotate\", \"scaleX\", \"scaleY\"];\nexport const cropperDataFieldsWithAspectRatio = [...cropperDataFields, \"aspectRatio\"];\nexport const isGif = (mimetype) => mimetype === \"image/gif\";\n\nlet _isWebGLEnabled;\n/**\n * Cacheable check telling whether the current browser can allocate a WebGL context.\n */\nexport function isWebGLEnabled() {\n    if (_isWebGLEnabled !== undefined) {\n        return _isWebGLEnabled;\n    }\n    try {\n        const canvas = document.createElement(\"canvas\");\n        _isWebGLEnabled = !!(\n            window.WebGLRenderingContext &&\n            (canvas.getContext(\"webgl\") || canvas.getContext(\"experimental-webgl\"))\n        );\n    } catch {\n        _isWebGLEnabled = false;\n    }\n    return _isWebGLEnabled;\n}\n\nconst modifierFields = [\n    \"filter\",\n    \"quality\",\n    \"mimetype\",\n    \"glFilter\",\n    \"originalId\",\n    \"originalSrc\",\n    \"resizeWidth\",\n    \"aspectRatio\",\n    \"mimetypeBeforeConversion\",\n];\n\nexport const removeOnImageChangeAttrs = [...cropperDataFields, ...modifierFields];\n\nconst cache = {};\n\nconst placeholderHref = \"/web/image/__odoo__unknown__src__/\";\n\nfunction _getValidSrc(src) {\n    if (src in cache) {\n        return cache[src];\n    }\n    const prom = new Promise((resolve) => {\n        fetch(src)\n            .then((response) => {\n                resolve(response.ok ? src : placeholderHref);\n            })\n            .catch(() => {\n                resolve(placeholderHref);\n            });\n    });\n    cache[src] = prom;\n    return prom;\n}\n\n/**\n * Loads an src into an HTMLImageElement.\n *\n * @param {String} src URL of the image to load\n * @param {HTMLImageElement} [img] img element in which to load the image\n * @returns {Promise<HTMLImageElement>} Promise that resolves to the loaded img\n *     or a placeholder image if the src is not found.\n */\nexport async function loadImage(src, img = new Image()) {\n    const source = await _getValidSrc(src);\n    return new Promise((resolve, reject) => {\n        img.addEventListener(\"load\", () => resolve(img), { once: true });\n        img.addEventListener(\"error\", reject, { once: true });\n        img.src = source;\n    });\n}\n\n// Because cropperjs acquires images through XHRs on the image src and we don't\n// want to load big images over the network many times when adjusting quality\n// and filter, we create a local cache of the images using object URLs.\nconst imageCache = new Map();\n\n/**\n * Loads image object URL into cache if not already set and returns it.\n *\n * @param {String} src\n * @returns {Promise}\n */\nfunction _loadImageObjectURL(src) {\n    return _updateImageData(src);\n}\n\n/**\n * Gets image dataURL from cache in the same way as object URL.\n *\n * @param {String} src\n * @returns {Promise}\n */\nexport function loadImageDataURL(src) {\n    return _updateImageData(src, \"dataURL\");\n}\n\n/**\n * @param {String} src used as a key on the image cache map.\n * @param {String} [key='objectURL'] specifies the image data to update/return.\n * @returns {Promise<String>} resolves with either dataURL/objectURL value.\n */\nasync function _updateImageData(src, key = \"objectURL\") {\n    const currentImageData = imageCache.get(src);\n    if (currentImageData && currentImageData[key]) {\n        return currentImageData[key];\n    }\n    let value = \"\";\n    const blob = await fetch(src).then((res) => res.blob());\n    if (key === \"dataURL\") {\n        value = await createDataURL(blob);\n    } else {\n        value = URL.createObjectURL(blob);\n    }\n    imageCache.set(src, Object.assign(currentImageData || {}, { [key]: value, size: blob.size }));\n    return value;\n}\n\n/**\n * Returns the size of a cached image.\n * Warning: this supposes that the image is already in the cache, i.e. that\n * _updateImageData was called before.\n *\n * @param {String} src used as a key on the image cache map.\n * @returns {Number} size of the image in bytes.\n */\nexport function getImageSizeFromCache(src) {\n    return imageCache.get(src).size;\n}\n\n/**\n * Activates the cropper on a given image.\n *\n * @param {jQuery} $image the image on which to activate the cropper\n * @param {Number} aspectRatio the aspectRatio of the crop box\n * @param {DOMStringMap} dataset dataset containing the cropperDataFields\n */\nexport async function activateCropper(image, aspectRatio, dataset) {\n    await loadBundle(\"html_editor.assets_image_cropper\");\n    const oldSrc = image.src;\n    const newSrc = await _loadImageObjectURL(image.getAttribute(\"src\"));\n    image.src = newSrc;\n    let readyResolve;\n    const readyPromise = new Promise((resolve) => (readyResolve = resolve));\n    // eslint-disable-next-line no-undef\n    const cropper = new Cropper(image, {\n        viewMode: 2,\n        dragMode: \"move\",\n        autoCropArea: 1.0,\n        aspectRatio: aspectRatio,\n        data: Object.fromEntries(\n            Object.entries(pick(dataset, ...cropperDataFields)).map(([key, value]) => [\n                key,\n                parseFloat(value),\n            ])\n        ),\n        // Can't use 0 because it's falsy and cropperjs will then use its defaults (200x100)\n        minContainerWidth: 1,\n        minContainerHeight: 1,\n        ready: readyResolve,\n    });\n    if (oldSrc === newSrc && image.complete) {\n        return;\n    }\n    await readyPromise;\n    return cropper;\n}\n\n/**\n * Marks an <img> with its attachment data (originalId, originalSrc, mimetype)\n *\n * @param {HTMLElement} el\n * @param {string} [attachmentSrc=''] specifies the URL of the corresponding\n * attachment if it can't be found in the 'src' attribute.\n */\nexport async function loadImageInfo(el, attachmentSrc = \"\") {\n    const newDataset = {};\n    const elSrc = getImageSrc(el);\n\n    const src = attachmentSrc || elSrc;\n    // If there is a marked originalSrc, the data is already loaded.\n    // If the image does not have the \"mimetypeBeforeConversion\" attribute, it\n    // has to be added.\n    if ((el.dataset.originalSrc && el.dataset.mimetypeBeforeConversion) || !src) {\n        return newDataset;\n    }\n    // In order to be robust to absolute, relative and protocol relative URLs,\n    // the src of the img is first converted to an URL object. To do so, the URL\n    // of the document in which the img is located is used as a base to build\n    // the URL object if the src of the img is a relative or protocol relative\n    // URL. The original attachment linked to the img is then retrieved thanks\n    // to the path of the built URL object.\n    let docHref = el.ownerDocument.defaultView.location.href;\n    if (docHref.startsWith(\"about:\")) {\n        docHref = window.location.href;\n    }\n\n    const srcUrl = new URL(src, docHref);\n    let relativeSrc = decodeURI(srcUrl.pathname);\n\n    let match = relativeSrc.match(/\\/(?:web_editor|html_editor)\\/image_shape\\/(\\w+\\.\\w+)/);\n    if (el.dataset.shape && match) {\n        match = match[1];\n        if (match.endsWith(\"_perspective\")) {\n            // As an image might already have been modified with a\n            // perspective for some customized snippets in themes. We need\n            // to find the original image to set the 'data-original-src'\n            // attribute.\n            match = match.slice(0, -12);\n        }\n        relativeSrc = `/web/image/${encodeURIComponent(match)}`;\n    }\n\n    const { original } = await rpc(\n        \"/html_editor/get_image_info\",\n        { src: relativeSrc },\n        { cache: true }\n    );\n    // If src was an absolute \"external\" URL, we consider unlikely that its\n    // relative part matches something from the DB and even if it does, nothing\n    // bad happens, besides using this random image as the original when using\n    // the options, instead of having no option. Note that we do not want to\n    // check if the image is local or not here as a previous bug converted some\n    // local (relative src) images to absolute URL... and that before users had\n    // setup their website domain. That means they can have an absolute URL that\n    // looks like \"https://mycompany.odoo.com/web/image/123\" that leads to a\n    // \"local\" image even if the domain name is now \"mycompany.be\".\n    //\n    // The \"redirect\" check is for when it is a redirect image attachment due to\n    // an external URL upload.\n    if (\n        original &&\n        original.image_src &&\n        !/\\/web\\/image\\/\\d+-redirect\\//.test(original.image_src)\n    ) {\n        newDataset.originalId = original.id;\n        newDataset.originalSrc = original.image_src;\n        newDataset.mimetypeBeforeConversion = original.mimetype;\n    }\n    return newDataset;\n}\n\n/**\n * @param {Blob} blob\n * @returns {Promise}\n */\nexport function createDataURL(blob) {\n    return new Promise((resolve, reject) => {\n        const reader = new FileReader();\n        reader.addEventListener(\"load\", () => resolve(reader.result));\n        reader.addEventListener(\"abort\", reject);\n        reader.addEventListener(\"error\", reject);\n        reader.readAsDataURL(blob);\n    });\n}\n\n/**\n * @param {String} dataURL\n * @returns {Number} number of bytes represented with base64\n */\nexport function getDataURLBinarySize(dataURL) {\n    // Every 4 bytes of base64 represent 3 bytes.\n    return (dataURL.split(\",\")[1].length / 4) * 3;\n}\n\n/**\n * Returns the aspect ratio from a string or number.\n * If the input is a string, it can be a ratio (e.g. \"16:9\") or a single number.\n * If the input is a number, it is returned as is.\n *\n * @param {string|number} ratio\n * @returns {number}\n */\nexport function getAspectRatio(ratio) {\n    if (typeof ratio === \"number\") {\n        return ratio;\n    }\n    const [a, b] = ratio.split(/[:/]/).map((n) => parseFloat(n));\n    // If the ratio is invalid, return only a.\n    if (!b) {\n        return a;\n    }\n    return a / b;\n}\n", "/**\n * Transform a 2D point using a projective transformation matrix. Note that\n * this method is only well behaved for points that don't map to infinity!\n *\n * @param {number[][]} matrix - A projective transformation matrix\n * @param {number[]} point - A 2D point\n * @returns The transformed 2D point\n */\nexport function transform([[a, b, c], [d, e, f], [g, h, i]], [x, y]) {\n    const z = g * x + h * y + i;\n    return [(a * x + b * y + c) / z, (d * x + e * y + f) / z];\n}\n\n/**\n * Calculate the inverse of a 3x3 matrix assuming it is invertible.\n *\n * @param {number[][]} matrix - A 3x3 matrix\n * @returns The resulting 3x3 matrix\n */\nfunction invert([[a, b, c], [d, e, f], [g, h, i]]) {\n    const determinant = a * e * i - a * f * h - b * d * i + b * f * g + c * d * h - c * e * g;\n    return [\n        [\n            (e * i - h * f) / determinant,\n            (h * c - b * i) / determinant,\n            (b * f - e * c) / determinant,\n        ],\n        [\n            (g * f - d * i) / determinant,\n            (a * i - g * c) / determinant,\n            (d * c - a * f) / determinant,\n        ],\n        [\n            (d * h - g * e) / determinant,\n            (g * b - a * h) / determinant,\n            (a * e - d * b) / determinant,\n        ],\n    ];\n}\n\n/**\n * Multiply two 3x3 matrices.\n *\n * @param {number[][]} a - A 3x3 matrix\n * @param {number[][]} b - A 3x3 matrix\n * @returns The resulting 3x3 matrix\n */\nfunction multiply(a, b) {\n    const [[a0, a1, a2], [a3, a4, a5], [a6, a7, a8]] = a;\n    const [[b0, b1, b2], [b3, b4, b5], [b6, b7, b8]] = b;\n    return [\n        [a0 * b0 + a1 * b3 + a2 * b6, a0 * b1 + a1 * b4 + a2 * b7, a0 * b2 + a1 * b5 + a2 * b8],\n        [a3 * b0 + a4 * b3 + a5 * b6, a3 * b1 + a4 * b4 + a5 * b7, a3 * b2 + a4 * b5 + a5 * b8],\n        [a6 * b0 + a7 * b3 + a8 * b6, a6 * b1 + a7 * b4 + a8 * b7, a6 * b2 + a7 * b5 + a8 * b8],\n    ];\n}\n\n/**\n * Find a projective transformation mapping a rectangular area at origin (0,0)\n * with a given width and height to a certain quadrilateral.\n *\n * @param {number} width - The width of the rectangular area\n * @param {number} height - The height of the rectangular area\n * @param {number[][]} quadrilateral - The vertices of the quadrilateral\n * @returns A projective transformation matrix\n */\nexport function getProjective(width, height, [[x0, y0], [x1, y1], [x2, y2], [x3, y3]]) {\n    // Calculate a set of homogeneous coordinates a, b, c of the first\n    // point using the other three points as basis vectors in the\n    // underlying vector space.\n    const denominator = x3 * (y1 - y2) + x1 * (y2 - y3) + x2 * (y3 - y1);\n    const a = (x0 * (y2 - y3) + x2 * (y3 - y0) + x3 * (y0 - y2)) / denominator;\n    const b = (x0 * (y3 - y1) + x3 * (y1 - y0) + x1 * (y0 - y3)) / denominator;\n    const c = (x0 * (y1 - y2) + x1 * (y2 - y0) + x2 * (y0 - y1)) / denominator;\n\n    // The reverse transformation maps the homogeneous coordinates of\n    // the last three corners of the original image onto the basis vectors\n    // while mapping the first corner onto (1, 1, 1). The forward\n    // transformation maps those basis vectors in addition to (1, 1, 1)\n    // onto homogeneous coordinates of the corresponding corners of the\n    // projective image. Combining these together yields the projective\n    // transformation we are looking for.\n    const reverse = invert([\n        [width, -width, 0],\n        [0, -height, height],\n        [1, -1, 1],\n    ]);\n    const forward = [\n        [a * x1, b * x2, c * x3],\n        [a * y1, b * y2, c * y3],\n        [a, b, c],\n    ];\n\n    return multiply(forward, reverse);\n}\n\n/**\n * Find an affine transformation matrix that exactly maps the vertices of a\n * triangle to their corresponding images of a projective transformation. The\n * resulting transformation will be an approximation of the projective\n * transformation for the area inside the triangle.\n *\n * @param {number[][]} projective - A projective transformation matrix\n * @param {number[][]} triangle - The vertices of a triangle\n * @returns - An affine transformation matrix\n */\nexport function getAffineApproximation(projective, [[x0, y0], [x1, y1], [x2, y2]]) {\n    const a = transform(projective, [x0, y0]);\n    const b = transform(projective, [x1, y1]);\n    const c = transform(projective, [x2, y2]);\n\n    return multiply(\n        [\n            [a[0], b[0], c[0]],\n            [a[1], b[1], c[1]],\n            [1, 1, 1],\n        ],\n        invert([\n            [x0, x1, x2],\n            [y0, y1, y2],\n            [1, 1, 1],\n        ])\n    );\n}\n", "// Position and sizes\n//------------------------------------------------------------------------------\n\nexport const DIRECTIONS = {\n    LEFT: false,\n    RIGHT: true,\n};\n\n/**\n * @param {Node} node\n * @returns {[HTMLElement, number]}\n */\nexport function leftPos(node) {\n    return [node.parentElement, childNodeIndex(node)];\n}\n/**\n * @param {Node} node\n * @returns {[HTMLElement, number]}\n */\nexport function rightPos(node) {\n    return [node.parentElement, childNodeIndex(node) + 1];\n}\n/**\n * @param {Node} node\n * @returns {[HTMLElement, number, HTMLElement, number]}\n */\nexport function boundariesOut(node) {\n    const index = childNodeIndex(node);\n    return [node.parentElement, index, node.parentElement, index + 1];\n}\n/**\n * @param {Node} node\n * @returns {[HTMLElement, number, HTMLElement, number]}\n */\nexport function boundariesIn(node) {\n    return [node, 0, node, nodeSize(node)];\n}\n/**\n * @param {Node} node\n * @returns {[Node, number]}\n */\nexport function startPos(node) {\n    return [node, 0];\n}\n/**\n * @param {Node} node\n * @returns {[Node, number]}\n */\nexport function endPos(node) {\n    return [node, nodeSize(node)];\n}\n/**\n * Returns the given node's position relative to its parent (= its index in the\n * child nodes of its parent).\n *\n * @param {Node} node\n * @returns {number}\n */\nexport function childNodeIndex(node) {\n    let i = 0;\n    while (node.previousSibling) {\n        i++;\n        node = node.previousSibling;\n    }\n    return i;\n}\n/**\n * Returns the size of the node = the number of characters for text nodes and\n * the number of child nodes for element nodes.\n *\n * @param {Node} node\n * @returns {number}\n */\nexport function nodeSize(node) {\n    const isTextNode = node.nodeType === Node.TEXT_NODE;\n    if (isTextNode) {\n        return node.length;\n    } else {\n        const child = node.lastChild;\n        return child ? childNodeIndex(child) + 1 : 0;\n    }\n}\n", "/* eslint-disable */\n\nconst tldWhitelist = [\n    'com', 'net', 'org', 'ac', 'ad', 'ae', 'af', 'ag', 'ai', 'al', 'am', 'an',\n    'ao', 'aq', 'ar', 'as', 'at', 'au', 'aw', 'ax', 'az', 'ba', 'bb', 'bd',\n    'be', 'bf', 'bg', 'bh', 'bi', 'bj', 'bl', 'bm', 'bn', 'bo', 'br', 'bq',\n    'bs', 'bt', 'bv', 'bw', 'by', 'bz', 'ca', 'cc', 'cd', 'cf', 'cg', 'ch',\n    'ci', 'ck', 'cl', 'cm', 'cn', 'co', 'cr', 'cs', 'cu', 'cv', 'cw', 'cx',\n    'cy', 'cz', 'dd', 'de', 'dj', 'dk', 'dm', 'do', 'dz', 'ec', 'ee', 'eg',\n    'eh', 'er', 'es', 'et', 'eu', 'fi', 'fj', 'fk', 'fm', 'fo', 'fr', 'ga',\n    'gb', 'gd', 'ge', 'gf', 'gg', 'gh', 'gi', 'gl', 'gm', 'gn', 'gp', 'gq',\n    'gr', 'gs', 'gt', 'gu', 'gw', 'gy', 'hk', 'hm', 'hn', 'hr', 'ht', 'hu',\n    'id', 'ie', 'il', 'im', 'in', 'io', 'iq', 'ir', 'is', 'it', 'je', 'jm',\n    'jo', 'jp', 'ke', 'kg', 'kh', 'ki', 'km', 'kn', 'kp', 'kr', 'kw', 'ky',\n    'kz', 'la', 'lb', 'lc', 'li', 'lk', 'lr', 'ls', 'lt', 'lu', 'lv', 'ly',\n    'ma', 'mc', 'md', 'me', 'mf', 'mg', 'mh', 'mk', 'ml', 'mm', 'mn', 'mo',\n    'mp', 'mq', 'mr', 'ms', 'mt', 'mu', 'mv', 'mw', 'mx', 'my', 'mz', 'na',\n    'nc', 'ne', 'nf', 'ng', 'ni', 'nl', 'no', 'np', 'nr', 'nu', 'nz', 'om',\n    'pa', 'pe', 'pf', 'pg', 'ph', 'pk', 'pl', 'pm', 'pn', 'pr', 'ps', 'pt',\n    'pw', 'py', 'qa', 're', 'ro', 'rs', 'ru', 'rw', 'sa', 'sb', 'sc', 'sd',\n    'se', 'sg', 'sh', 'si', 'sj', 'sk', 'sl', 'sm', 'sn', 'so', 'sr', 'ss',\n    'st', 'su', 'sv', 'sx', 'sy', 'sz', 'tc', 'td', 'tf', 'tg', 'th', 'tj',\n    'tk', 'tl', 'tm', 'tn', 'to', 'tp', 'tr', 'tt', 'tv', 'tw', 'tz', 'ua',\n    'ug', 'uk', 'um', 'us', 'uy', 'uz', 'va', 'vc', 've', 'vg', 'vi', 'vn',\n    'vu', 'wf', 'ws', 'ye', 'yt', 'yu', 'za', 'zm', 'zr', 'zw', 'co\\\\.uk'];\n\nconst urlRegexBase = `|(?:www.))[-a-zA-Z0-9@:%._\\\\+~#=]{2,256}\\\\.[a-zA-Z][a-zA-Z0-9]{1,62}|(?:[-a-zA-Z0-9@:%._\\\\+~#=]{2,256}\\\\.(?:${tldWhitelist.join('|')})\\\\b))(?:(?:[/?#])[^\\\\s]*[^!.,})\\\\]'\"\\\\s]|(?:[^!(){}.,[\\\\]'\"\\\\s]+))?`;\nconst httpCapturedRegex= `(https?:\\\\/\\\\/)`;\n\nexport const URL_REGEX = new RegExp(`((?:(?:${httpCapturedRegex}${urlRegexBase})`, 'i');\n", "export const resourceSequenceSymbol = Symbol(\"resourceSequence\");\n\n/**\n * @template T\n * @typedef {Object} ResourceWithSequence\n * @property {T} object\n */\n\n/**\n * @template T\n * @param {number} sequenceNumber\n * @param {T} object\n * @returns {ResourceWithSequence<T>}\n */\nexport function withSequence(sequenceNumber, object) {\n    if (typeof sequenceNumber !== \"number\") {\n        throw new Error(\n            `sequenceNumber must be a number. Got ${sequenceNumber} (${typeof sequenceNumber}).`\n        );\n    }\n    return {\n        [resourceSequenceSymbol]: sequenceNumber,\n        object,\n    };\n}\n", "import { containsAnyInline } from \"./dom_info\";\nimport { wrapInlinesInBlocks } from \"./dom\";\nimport { markup } from \"@odoo/owl\";\nimport { htmlReplace } from \"@web/core/utils/html\";\n\nexport function initElementForEdition(element, options = {}) {\n    if (\n        element?.nodeType === Node.ELEMENT_NODE &&\n        containsAnyInline(element) &&\n        !options.allowInlineAtRoot\n    ) {\n        // No matter the inline content, it will be wrapped in a DIV to try\n        // and match the current style of the content as much as possible.\n        // (P has a margin-bottom, DIV does not).\n        wrapInlinesInBlocks(element, {\n            baseContainerNodeName: \"DIV\",\n        });\n    }\n\n    // During `convert_inline`, image elements may receive `width` and `height` attributes,\n    // along with inline styles. These attributes force specific dimensions, which breaks\n    // the fallback to default sizing. We remove them here to allow proper resizing behavior.\n    // The attributes will be re-applied on save.\n    for (const img of element.querySelectorAll(\"img[width], img[height]\")) {\n        const width = img.getAttribute(\"width\");\n        const height = img.getAttribute(\"height\");\n        img.removeAttribute(\"height\");\n        img.removeAttribute(\"width\");\n        img.style.setProperty(\"width\", isNaN(width) ? width : `${width}px`);\n        img.style.setProperty(\"height\", isNaN(height) ? height : `${height}px`);\n    }\n}\n\n/**\n * Converts XML-style self-closing tags (e.g., <div/> <span/> <t/>) into proper\n * HTML start/end tag pairs, except for true HTML void elements.\n *\n * @param {string | ReturnType<markup>} content\n * @returns {ReturnType<markup>}\n */\nexport function fixInvalidHTML(content) {\n    if (!content) {\n        return content;\n    }\n    // Match self-closing tags EXCEPT HTML void elements.\n    // We do not use selfClosingElementTags because it includes XML-only tags\n    // such as <t>, which must not be treated as void in HTML.\n    const regex =\n        /<\\s*(?!area\\b|base\\b|br\\b|col\\b|embed\\b|hr\\b|img\\b|input\\b|link\\b|meta\\b|param\\b|source\\b|track\\b|wbr\\b)([a-zA-Z0-9:-]+)\\s*((?:(?:\\s+[\\w:-]+(?:\\s*=\\s*(?:\"[^\"]*\"|'[^']*'|[^\\s\"'=<>`]+))?)*))\\s*\\/>/g;\n    return htmlReplace(content, regex, (match, tag, attributes) => {\n        // markup: content is either already markup or escaped in htmlReplace\n        attributes = markup(attributes);\n        return markup`<${tag}${attributes}></${tag}>`;\n    });\n}\n\nlet Markup = null;\n\nexport function instanceofMarkup(value) {\n    if (!Markup) {\n        Markup = markup(\"\").constructor;\n    }\n    return value instanceof Markup;\n}\n", "import { closestBlock, isBlock } from \"./blocks\";\nimport {\n    getDeepestPosition,\n    isContentEditable,\n    isNotEditableNode,\n    isSelfClosingElement,\n    nextLeaf,\n    previousLeaf,\n} from \"./dom_info\";\nimport { isFakeLineBreak } from \"./dom_state\";\nimport { closestElement, createDOMPathGenerator } from \"./dom_traversal\";\nimport {\n    DIRECTIONS,\n    childNodeIndex,\n    endPos,\n    leftPos,\n    nodeSize,\n    rightPos,\n    startPos,\n} from \"./position\";\n\n/**\n * @typedef { import(\"./selection_plugin\").EditorSelection } EditorSelection\n */\n\n/**\n * From selection position, checks if it is left-to-right or right-to-left.\n *\n * @param {Node} anchorNode\n * @param {number} anchorOffset\n * @param {Node} focusNode\n * @param {number} focusOffset\n * @returns {boolean} the direction of the current range if the selection not is collapsed | false\n */\nexport function getCursorDirection(anchorNode, anchorOffset, focusNode, focusOffset) {\n    if (anchorNode === focusNode) {\n        if (anchorOffset === focusOffset) {\n            return false;\n        }\n        return anchorOffset < focusOffset ? DIRECTIONS.RIGHT : DIRECTIONS.LEFT;\n    }\n    return anchorNode.compareDocumentPosition(focusNode) & Node.DOCUMENT_POSITION_FOLLOWING\n        ? DIRECTIONS.RIGHT\n        : DIRECTIONS.LEFT;\n}\n\n/**\n * @param {EditorSelection} selection\n * @param {string} selector\n */\nexport function findInSelection(selection, selector) {\n    const selectorInStartAncestors = closestElement(selection.startContainer, selector);\n    if (selectorInStartAncestors) {\n        return selectorInStartAncestors;\n    } else {\n        const commonElementAncestor = closestElement(selection.commonAncestorContainer);\n        return (\n            commonElementAncestor &&\n            [...commonElementAncestor.querySelectorAll(selector)].find((node) =>\n                selection.intersectsNode(node)\n            )\n        );\n    }\n}\n\nconst leftLeafOnlyInScopeNotBlockEditablePath = createDOMPathGenerator(DIRECTIONS.LEFT, {\n    leafOnly: true,\n    inScope: true,\n    stopTraverseFunction: (node) => isNotEditableNode(node) || isBlock(node),\n    stopFunction: (node) => isNotEditableNode(node) || isBlock(node),\n});\n\nconst rightLeafOnlyInScopeNotBlockEditablePath = createDOMPathGenerator(DIRECTIONS.RIGHT, {\n    leafOnly: true,\n    inScope: true,\n    stopTraverseFunction: (node) => isNotEditableNode(node) || isBlock(node),\n    stopFunction: (node) => isNotEditableNode(node) || isBlock(node),\n});\n\nexport function normalizeSelfClosingElement(node, offset) {\n    if (isSelfClosingElement(node)) {\n        // Cannot put cursor inside those elements, put it after instead.\n        [node, offset] = rightPos(node);\n    }\n    return [node, offset];\n}\n\nexport function normalizeNotEditableNode(node, offset, position = \"right\") {\n    const editable = closestElement(node, \".odoo-editor-editable\");\n    let closest = closestElement(node);\n    while (closest && closest !== editable && !closest.isContentEditable) {\n        [node, offset] = position === \"right\" ? rightPos(node) : leftPos(node);\n        closest = node;\n    }\n    return [node, offset];\n}\n\nexport function normalizeCursorPosition(node, offset, position = \"right\") {\n    [node, offset] = normalizeSelfClosingElement(node, offset);\n    [node, offset] = normalizeNotEditableNode(node, offset, position);\n    // todo @phoenix: we should maybe remove it\n    // // Be permissive about the received offset.\n    // offset = Math.min(Math.max(offset, 0), nodeSize(node));\n    return [node, offset];\n}\n\nexport function normalizeFakeBR(node, offset) {\n    const prevNode = node.nodeType === Node.ELEMENT_NODE && node.childNodes[offset - 1];\n    if (prevNode && prevNode.nodeName === \"BR\" && isFakeLineBreak(prevNode)) {\n        // If trying to put the cursor on the right of a fake line break, put\n        // it before instead.\n        offset--;\n    }\n    return [node, offset];\n}\n\n/**\n * From a given position, returns the normalized version.\n *\n * E.g. <b>abc</b>[]def -> <b>abc[]</b>def\n *\n * @param {Node} node\n * @param {number} offset\n * @returns { [Node, number] }\n */\nexport function normalizeDeepCursorPosition(node, offset) {\n    // Put the cursor in deepest inline node around the given position if\n    // possible.\n    let el;\n    let elOffset;\n    if (node.nodeType === Node.ELEMENT_NODE) {\n        el = node;\n        elOffset = offset;\n    } else if (node.nodeType === Node.TEXT_NODE) {\n        if (offset === 0) {\n            el = node.parentNode;\n            elOffset = childNodeIndex(node);\n        } else if (offset === node.length) {\n            el = node.parentNode;\n            elOffset = childNodeIndex(node) + 1;\n        }\n    }\n    if (el) {\n        const leftInlineNode = leftLeafOnlyInScopeNotBlockEditablePath(el, elOffset).next().value;\n        let leftVisibleEmpty = false;\n        if (leftInlineNode) {\n            leftVisibleEmpty =\n                isSelfClosingElement(leftInlineNode) || !isContentEditable(leftInlineNode);\n            [node, offset] = leftVisibleEmpty ? rightPos(leftInlineNode) : endPos(leftInlineNode);\n        }\n        if (!leftInlineNode || leftVisibleEmpty) {\n            const rightInlineNode = rightLeafOnlyInScopeNotBlockEditablePath(el, elOffset).next()\n                .value;\n            if (rightInlineNode) {\n                const closest = closestElement(rightInlineNode);\n                const rightVisibleEmpty =\n                    isSelfClosingElement(rightInlineNode) || !closest || !closest.isContentEditable;\n                if (!(leftVisibleEmpty && rightVisibleEmpty)) {\n                    [node, offset] = rightVisibleEmpty\n                        ? leftPos(rightInlineNode)\n                        : startPos(rightInlineNode);\n                }\n            }\n        }\n    }\n    return [node, offset];\n}\n\nfunction updateCursorBeforeMove(destParent, destIndex, node, cursor) {\n    if (cursor.node === destParent && cursor.offset >= destIndex) {\n        // Update cursor at destination\n        cursor.offset += 1;\n    } else if (cursor.node === node.parentNode) {\n        const childIndex = childNodeIndex(node);\n        // Update cursor at origin\n        if (cursor.offset === childIndex && cursor.offset === 0) {\n            // Keep cursor before the moved node if it's the first child before the move\n            [cursor.node, cursor.offset] = [destParent, destIndex];\n        } else if (cursor.offset === childIndex + 1 && cursor.offset === nodeSize(cursor.node)) {\n            // Keep cursor after the moved node if it's the last child before the move\n            [cursor.node, cursor.offset] = [destParent, destIndex + 1];\n        } else if (cursor.offset > childIndex) {\n            cursor.offset -= 1;\n        }\n    }\n}\n\nfunction updateCursorBeforeRemove(node, cursor) {\n    if (node.contains(cursor.node)) {\n        [cursor.node, cursor.offset] = [node.parentNode, childNodeIndex(node)];\n    } else if (cursor.node === node.parentNode && cursor.offset > childNodeIndex(node)) {\n        cursor.offset -= 1;\n    }\n}\n\nfunction updateCursorBeforeUnwrap(node, cursor) {\n    if (cursor.node === node) {\n        [cursor.node, cursor.offset] = [node.parentNode, cursor.offset + childNodeIndex(node)];\n    } else if (cursor.node === node.parentNode && cursor.offset > childNodeIndex(node)) {\n        cursor.offset += nodeSize(node) - 1;\n    }\n}\n\nfunction updateCursorBeforeMergeIntoPreviousSibling(node, cursor) {\n    if (cursor.node === node) {\n        cursor.node = node.previousSibling;\n        cursor.offset += node.previousSibling.childNodes.length;\n    } else if (cursor.node === node.parentNode) {\n        const childIndex = childNodeIndex(node);\n        if (cursor.offset === childIndex) {\n            cursor.node = node.previousSibling;\n            cursor.offset = node.previousSibling.childNodes.length;\n        } else if (cursor.offset > childIndex) {\n            cursor.offset--;\n        }\n    }\n}\n\n/** @typedef {import(\"@html_editor/core/selection_plugin\").Cursor} Cursor */\n\nexport const callbacksForCursorUpdate = {\n    /** @type {(node: Node) => (cursor: Cursor) => void} */\n    remove: (node) => (cursor) => updateCursorBeforeRemove(node, cursor),\n    /** @type {(ref: HTMLElement, node: Node) => (cursor: Cursor) => void} */\n    before: (ref, node) => (cursor) =>\n        updateCursorBeforeMove(ref.parentNode, childNodeIndex(ref), node, cursor),\n    /** @type {(ref: HTMLElement, node: Node) => (cursor: Cursor) => void} */\n    after: (ref, node) => (cursor) =>\n        updateCursorBeforeMove(ref.parentNode, childNodeIndex(ref) + 1, node, cursor),\n    /** @type {(ref: HTMLElement, node: Node) => (cursor: Cursor) => void} */\n    append: (to, node) => (cursor) =>\n        updateCursorBeforeMove(to, to.childNodes.length, node, cursor),\n    /** @type {(ref: HTMLElement, node: Node) => (cursor: Cursor) => void} */\n    prepend: (to, node) => (cursor) => updateCursorBeforeMove(to, 0, node, cursor),\n    /** @type {(node: HTMLElement) => (cursor: Cursor) => void} */\n    unwrap: (node) => (cursor) => updateCursorBeforeUnwrap(node, cursor),\n    /** @type {(node: HTMLElement) => (cursor: Cursor) => void} */\n    merge: (node) => (cursor) => updateCursorBeforeMergeIntoPreviousSibling(node, cursor),\n};\n\n/**\n * @param {Selection} selection\n * @param {\"previous\"|\"next\"} side\n * @param {HTMLElement} editable\n * @returns {string | undefined}\n */\nexport function getAdjacentCharacter(selection, side, editable) {\n    let { focusNode, focusOffset } = selection;\n    [focusNode, focusOffset] = getDeepestPosition(focusNode, focusOffset);\n    const originalBlock = closestBlock(focusNode);\n    let adjacentCharacter;\n    while (!adjacentCharacter && focusNode) {\n        if (side === \"previous\") {\n            adjacentCharacter = focusOffset > 0 && focusNode.textContent[focusOffset - 1];\n        } else {\n            adjacentCharacter = focusNode.textContent[focusOffset];\n        }\n        if (!adjacentCharacter) {\n            if (side === \"previous\") {\n                focusNode = previousLeaf(focusNode, editable);\n                focusOffset = focusNode && nodeSize(focusNode);\n            } else {\n                focusNode = nextLeaf(focusNode, editable);\n                focusOffset = 0;\n            }\n            const characterIndex = side === \"previous\" ? focusOffset - 1 : focusOffset;\n            adjacentCharacter = focusNode && focusNode.textContent[characterIndex];\n        }\n    }\n    if (!focusNode || !isContentEditable(focusNode) || closestBlock(focusNode) !== originalBlock) {\n        return undefined;\n    }\n    return adjacentCharacter;\n}\n", "import { closestElement } from \"./dom_traversal\";\n\n/**\n * Get the index of the given table row/cell.\n *\n * @private\n * @param {HTMLTableRowElement|HTMLTableCellElement} trOrTd\n * @returns {number}\n */\nexport function getRowIndex(trOrTd) {\n    const tr = closestElement(trOrTd, \"tr\");\n    return tr.rowIndex;\n}\n\n/**\n * Get the index of the given table cell.\n *\n * @private\n * @param {HTMLTableCellElement} td\n * @returns {number}\n */\nexport function getColumnIndex(td) {\n    return td.cellIndex;\n}\n\n/**\n * Get all the cells of given table\n * (excluding nested table cells).\n *\n * @param {HTMLTableElement} table\n * @returns {Array<HTMLTableCellElement>}\n */\nexport function getTableCells(table) {\n    return [...table.querySelectorAll(\"td, th\")].filter(\n        (cell) => closestElement(cell, \"table\") === table\n    );\n}\n", "/**\n * Creates a function to track whether a key has been seen before.\n * The returned function returns true for the first occurrence of each key,\n * false for subsequent ones.\n */\nexport function trackOccurrences() {\n    const visited = new Set();\n    return function isFirstOccurrence(key) {\n        if (visited.has(key)) {\n            return false;\n        }\n        visited.add(key);\n        return true;\n    };\n}\n\n/**\n * Creates a function to track whether a pair of keys has been seen before.\n * The returned function returns true for the first occurrence of each pair of\n * keys, false for subsequent ones.\n * Order matters, i.e. (a, b) is not the same as (b, a).\n */\nexport function trackOccurrencesPair() {\n    const visited = new Map();\n    /** @type {(a, b) => boolean} */\n    return function isFirstOccurrence(a, b) {\n        if (!visited.has(a)) {\n            visited.set(a, trackOccurrences());\n        }\n        return visited.get(a)(b);\n    };\n}\n", "import { session } from \"@web/session\";\n\nconst ODOO_DOMAIN_REGEX = new RegExp(`^https?://${session.db}\\\\.odoo\\\\.com(/.*)?$`);\n\n/**\n * Checks if the given URL contains the specified hostname and returns a reconstructed URL if it does.\n *\n * @param {string} url - The URL to be checked\n * @param {Array} hostname - The hostname to be included in the modified URL\n * @return {string|boolean} The modified URL with the specified hostname included, or false if the URL does not meet the conditions\n */\nexport function checkURL(url, hostnameList) {\n    if (url) {\n        let potentialURL;\n        try {\n            potentialURL = new URL(url);\n        } catch {\n            return false;\n        }\n        if (hostnameList.includes(potentialURL.hostname)) {\n            return `https://${potentialURL.hostname}${potentialURL.pathname}`;\n        }\n    }\n    return false;\n}\n\n/**\n * @param {string} url\n */\nexport function isImageUrl(url) {\n    const urlFileExtention = url.split(\".\").pop();\n    return [\"jpg\", \"jpeg\", \"png\", \"gif\", \"svg\", \"webp\"].includes(urlFileExtention.toLowerCase());\n}\n\n/**\n * @param {string} platform\n * @param {string} videoId\n * @param {Object} params\n * @throws {Error} if the given video config is not recognized\n * @returns {URL}\n */\nexport function getVideoUrl(platform, videoId, params) {\n    let url;\n    switch (platform) {\n        case \"youtube\":\n            url = new URL(`https://www.youtube.com/embed/${videoId}`);\n            break;\n        case \"vimeo\":\n            url = new URL(`https://player.vimeo.com/video/${videoId}`);\n            break;\n        case \"dailymotion\":\n            url = new URL(`https://www.dailymotion.com/embed/video/${videoId}`);\n            break;\n        case \"instagram\":\n            url = new URL(`https://www.instagram.com/p/${videoId}/embed`);\n            break;\n        default:\n            throw new Error(`Unsupported platform: ${platform}`);\n    }\n    url.search = new URLSearchParams(params);\n    return url;\n}\n\n/**\n * Checks if the given URL is using the domain where the content being\n * edited is reachable, i.e. if this URL should be stripped of its domain\n * part and converted to a relative URL if put as a link in the content.\n *\n * @param {string} url\n * @returns {boolean}\n */\nexport function isAbsoluteURLInCurrentDomain(url, env = null) {\n    // First check if it is a relative URL: if it is, we don't want to check\n    // further as we will always leave those untouched.\n    let hasProtocol;\n    try {\n        hasProtocol = !!new URL(url).protocol;\n    } catch {\n        hasProtocol = false;\n    }\n    if (!hasProtocol) {\n        return false;\n    }\n\n    const urlObj = new URL(url, window.location.origin);\n    return (\n        urlObj.origin === window.location.origin ||\n        // Chosen heuristic to detect someone trying to enter a link using\n        // its Odoo instance domain. We just suppose it should be a relative\n        // URL (if unexpected behavior, the user can just not enter its Odoo\n        // instance domain but its real domain, or opt-out from the domain\n        // stripping). Mentioning an .odoo.com domain, especially its own\n        // one, is always a bad practice anyway.\n        ODOO_DOMAIN_REGEX.test(urlObj.origin)\n    );\n}\n", "import { useEffect, useState } from \"@odoo/owl\";\n\nexport function useDropdownAutoVisibility(overlayState, popoverRef) {\n    if (!overlayState) {\n        return;\n    }\n    const state = useState(overlayState);\n    useEffect(\n        () => {\n            if (popoverRef.el) {\n                if (!state.isOverlayVisible) {\n                    popoverRef.el.style.visibility = \"hidden\";\n                } else {\n                    popoverRef.el.style.visibility = \"visible\";\n                }\n            }\n        },\n        () => [state.isOverlayVisible]\n    );\n}\n", "import { MAIN_PLUGINS } from \"./plugin_sets\";\nimport { createBaseContainer, SUPPORTED_BASE_CONTAINER_NAMES } from \"./utils/base_container\";\nimport { fillShrunkPhrasingParent, removeClass } from \"./utils/dom\";\nimport { isEmpty } from \"./utils/dom_info\";\nimport { resourceSequenceSymbol, withSequence } from \"./utils/resource\";\nimport { fixInvalidHTML, initElementForEdition } from \"./utils/sanitize\";\nimport { setElementContent } from \"@web/core/utils/html\";\n\n/** @typedef {import(\"plugins\").EditorResources} EditorResources */\n/** @typedef {import(\"plugins\").GlobalResources} GlobalResources */\n/** @typedef {keyof GlobalResources} GlobalResourcesId */\n/**\n * @typedef {import(\"plugins\").SharedMethods} SharedMethods\n * @typedef {import(\"plugins\").PluginConstructor} PluginConstructor\n **/\n\n/**\n * @typedef { Object } CollaborationConfig\n * @property { string } collaboration.peerId\n * @property { Object } collaboration.busService\n * @property { Object } collaboration.collaborationChannel\n * @property { String } collaboration.collaborationChannel.collaborationModelName\n * @property { String } collaboration.collaborationChannel.collaborationFieldName\n * @property { Number } collaboration.collaborationChannel.collaborationResId\n * @property { 'start' | 'focus' } [collaboration.collaborativeTrigger]\n\n * @typedef { Object } EditorConfig\n * @property { string } [content]\n * @property { boolean } [allowInlineAtRoot]\n * @property { string[] } [baseContainers]\n * @property { PluginConstructor[] } [Plugins]\n * @property { string[] } [classList]\n * @property { Object } [localOverlayContainers]\n * @property { Object } [embeddedComponentInfo]\n * @property { Object } [resources]\n * @property { string } [direction=\"ltr\"]\n * @property { Function } [onChange]\n * @property { Function } [onEditorReady]\n * @property { boolean } [dropImageAsAttachment]\n * @property { CollaborationConfig } [collaboration]\n * @property { Function } getRecordInfo\n *\n * @typedef { Object } EditorContext\n * @property { Document } document\n * @property { HTMLElement } editable\n * @property { SharedMethods } dependencies\n * @property { import(\"./editor\").EditorConfig } config\n * @property { import(\"services\").ServiceFactories } services\n * @property { Editor['getResource'] } getResource\n * @property { Editor['dispatchTo'] } dispatchTo\n * @property { Editor['delegateTo'] } delegateTo\n */\n\n/**\n * @typedef {((arg: {root: EditorContext[\"editable\"]}) => void)[]} clean_for_save_handlers\n * @typedef {(() => void)[]} start_edition_handlers\n */\n\n/**\n * Clean up DOM before taking into account for next history step remaining in\n * edit mode\n * @typedef {((root: EditorContext[\"editable\"] | HTMLElement) => void)[]} normalize_handlers\n */\n\n/**\n * @param {PluginConstructor[]} plugins\n * @returns {PluginConstructor[]}\n */\nfunction sortPlugins(plugins) {\n    const initialPlugins = new Set(plugins);\n    const inResult = new Set();\n    // need to sort them\n    const result = [];\n    let P;\n\n    function findPlugin() {\n        for (const P of initialPlugins) {\n            if (P.dependencies.every((dep) => inResult.has(dep))) {\n                initialPlugins.delete(P);\n                return P;\n            }\n        }\n    }\n    while ((P = findPlugin())) {\n        inResult.add(P.id);\n        result.push(P);\n    }\n    if (initialPlugins.size) {\n        const messages = [];\n        for (const P of initialPlugins) {\n            messages.push(\n                `\"${P.id}\" is missing (${P.dependencies\n                    .filter((d) => !inResult.has(d))\n                    .join(\", \")})`\n            );\n        }\n        throw new Error(`Missing dependencies:  ${messages.join(\", \")}`);\n    }\n    return result;\n}\n\nexport class Editor {\n    /**\n     * @param { EditorConfig } config\n     */\n    constructor(config, services) {\n        this.isReady = false;\n        this.isDestroyed = false;\n        this.config = config;\n        this.services = services;\n        /** @type { EditorResources } */\n        this.resources = null;\n        this.plugins = [];\n        /** @type { HTMLElement } **/\n        this.editable = null;\n        /** @type { Document } **/\n        this.document = null;\n        /** @ts-ignore  @type { SharedMethods } **/\n        this.shared = {};\n    }\n\n    attachTo(editable) {\n        if (this.isDestroyed || this.editable) {\n            throw new Error(\"Cannot re-attach an editor\");\n        }\n        this.editable = editable;\n        this.document = editable.ownerDocument;\n        this.preparePlugins();\n        if (\"content\" in this.config) {\n            setElementContent(editable, fixInvalidHTML(this.config.content));\n            if (isEmpty(editable)) {\n                const baseContainer = createBaseContainer(\n                    this.config.baseContainers[0],\n                    this.document\n                );\n                fillShrunkPhrasingParent(baseContainer);\n                editable.replaceChildren(baseContainer);\n            }\n        }\n        editable.setAttribute(\"contenteditable\", true);\n        initElementForEdition(editable, { allowInlineAtRoot: !!this.config.allowInlineAtRoot });\n        editable.classList.add(\"odoo-editor-editable\");\n        if (this.config.classList) {\n            editable.classList.add(...this.config.classList);\n        }\n        if (this.config.height) {\n            editable.style.height = this.config.height;\n        }\n        if (\n            !this.config.baseContainers.every((name) =>\n                SUPPORTED_BASE_CONTAINER_NAMES.includes(name)\n            )\n        ) {\n            throw new Error(\n                `Invalid baseContainers: ${this.config.baseContainers.join(\n                    \", \"\n                )}. Supported: ${SUPPORTED_BASE_CONTAINER_NAMES.join(\", \")}`\n            );\n        }\n        this.startPlugins();\n        this.isReady = true;\n        this.config.onEditorReady?.();\n    }\n\n    preparePlugins() {\n        const Plugins = sortPlugins(this.config.Plugins || MAIN_PLUGINS);\n        this.config = Object.assign({}, ...Plugins.map((P) => P.defaultConfig), this.config);\n        this.pluginsMap = new Map();\n        for (const P of Plugins) {\n            if (P.id === \"\") {\n                throw new Error(`Missing plugin id (class ${P.name})`);\n            }\n            if (this.pluginsMap.has(P.id)) {\n                throw new Error(`Duplicate plugin id: ${P.id}`);\n            }\n            this.pluginsMap.set(P.id, P);\n            const plugin = new P(this.getEditorContext(P.dependencies));\n            plugin.__editor = this;\n            this.plugins.push(plugin);\n            const exports = {};\n            for (const h of P.shared) {\n                if (!(h in plugin)) {\n                    throw new Error(`Missing helper implementation: ${h} in plugin ${P.id}`);\n                }\n                exports[h] = plugin[h].bind(plugin);\n            }\n            this.shared[P.id] = exports;\n        }\n        const resources = this.createResources();\n        for (const plugin of this.plugins) {\n            plugin._resources = resources;\n        }\n        this.resources = resources;\n    }\n\n    startPlugins() {\n        for (const plugin of this.plugins) {\n            plugin.setup();\n        }\n        this.resources[\"normalize_handlers\"].forEach((cb) => cb(this.editable));\n        this.resources[\"start_edition_handlers\"].forEach((cb) => cb());\n    }\n\n    getDependencies(dependencies) {\n        const deps = {};\n        for (const depName of dependencies) {\n            if (!(depName in this.shared)) {\n                throw new Error(`Missing dependency: ${depName}`);\n            }\n            deps[depName] = this.shared[depName];\n        }\n        return deps;\n    }\n\n    createResources() {\n        const resources = {};\n\n        function registerResources(obj) {\n            for (const key in obj) {\n                if (!(key in resources)) {\n                    resources[key] = [];\n                }\n                resources[key].push(obj[key]);\n            }\n        }\n        if (this.config.resources) {\n            registerResources(this.config.resources);\n        }\n        for (const plugin of this.plugins) {\n            if (plugin.resources) {\n                registerResources(plugin.resources);\n            }\n        }\n\n        for (const key in resources) {\n            const resource = resources[key]\n                .flat()\n                .map((r) => {\n                    const isObjectWithSequence =\n                        typeof r === \"object\" && r !== null && resourceSequenceSymbol in r;\n                    return isObjectWithSequence ? r : withSequence(10, r);\n                })\n                .sort((a, b) => a[resourceSequenceSymbol] - b[resourceSequenceSymbol])\n                .map((r) => r.object);\n\n            resources[key] = resource;\n            Object.freeze(resources[key]);\n        }\n\n        return Object.freeze(resources);\n    }\n\n    /**\n     * @return { EditorContext }\n     */\n    getEditorContext(dependencies = []) {\n        return {\n            document: this.document,\n            editable: this.editable,\n            dependencies: this.getDependencies(dependencies),\n            config: this.config,\n            services: this.services,\n            getResource: this.getResource.bind(this),\n            dispatchTo: this.dispatchTo.bind(this),\n            delegateTo: this.delegateTo.bind(this),\n        };\n    }\n\n    /**\n     * @template {GlobalResourcesId} R\n     * @param {R} resourceId\n     * @returns {GlobalResources[R]}\n     */\n    getResource(resourceId) {\n        return this.resources[resourceId] || [];\n    }\n\n    /**\n     * Execute the functions registered under resourceId with the given\n     * arguments.\n     *\n     * This function is meant to enhance code readability by clearly expressing\n     * its intent.\n     *\n     * This function can be thought as an event dispatcher, calling the handlers\n     * with `args` as the payload.\n     *\n     * Example:\n     * ```js\n     * this.dispatchTo(\"my_event_handlers\", arg1, arg2);\n     * ```\n     *\n     * @template {GlobalResourcesId} R\n     * @param {R} resourceId\n     * @param {Parameters<GlobalResources[R][0]>} args The arguments to pass to the handlers.\n     */\n    dispatchTo(resourceId, ...args) {\n        this.getResource(resourceId).forEach((handler) => handler(...args));\n    }\n    /**\n     * Execute a series of functions until one of them returns a truthy value.\n     *\n     * This function is meant to enhance code readability by clearly expressing\n     * its intent.\n     *\n     * A command \"delegates\" its execution to one of the overriding functions,\n     * which return a truthy value to signal it has been handled.\n     *\n     * It is the the caller's responsability to stop the execution when this\n     * function returns true.\n     *\n     * Example:\n     * ```js\n     * if (this.delegateTo(\"my_command_overrides\", arg1, arg2)) {\n     *   return;\n     * }\n     * ```\n     *\n     * @template {GlobalResourcesId} R\n     * @param {R} resourceId\n     * @param {Parameters<GlobalResources[R][0]>} args The arguments to pass to the overrides.\n     * @returns {boolean} Whether one of the overrides returned a truthy value.\n     */\n    delegateTo(resourceId, ...args) {\n        return this.getResource(resourceId).some((fn) => fn(...args));\n    }\n\n    getContent() {\n        return this.getElContent().innerHTML;\n    }\n\n    getElContent() {\n        const el = this.editable.cloneNode(true);\n        this.resources[\"clean_for_save_handlers\"].forEach((cb) => cb({ root: el }));\n        return el;\n    }\n\n    destroy(willBeRemoved) {\n        if (this.editable) {\n            let plugin;\n            while ((plugin = this.plugins.pop())) {\n                plugin.destroy();\n            }\n            this.shared = {};\n            if (!willBeRemoved) {\n                // we only remove class/attributes when necessary. If we know that the editable\n                // element will be removed, no need to make changes that may require the browser\n                // to recompute the layout\n                this.editable.removeAttribute(\"contenteditable\");\n                removeClass(this.editable, \"odoo-editor-editable\");\n            }\n            this.editable = null;\n        }\n        this.isDestroyed = true;\n    }\n}\n", "import { isProtected, isProtecting, isUnprotecting } from \"./utils/dom_info\";\n\nexport const isValidTargetForDomListener = (target) =>\n    !isProtecting(target) && (!isProtected(target) || isUnprotecting(target));\n\n/**\n * @typedef { import(\"./editor\").Editor } Editor\n * @typedef { import(\"./editor\").EditorContext } EditorContext\n */\n\nexport class Plugin {\n    static id = \"\";\n    static dependencies = [];\n    static shared = [];\n    static defaultConfig = {};\n\n    /** @type {Partial<import(\"plugins\").Resources>} */\n    resources;\n\n    /**\n     * @param { EditorContext } context\n     */\n    constructor(context) {\n        /** @type { EditorContext['document'] } **/\n        this.document = context.document;\n        this.window = context.document.defaultView;\n        /** @type { EditorContext['editable'] } **/\n        this.editable = context.editable;\n        /** @type { EditorContext['config'] } **/\n        this.config = context.config;\n        /** @type { EditorContext['services'] } **/\n        this.services = context.services;\n        /** @type { EditorContext['dependencies'] } **/\n        this.dependencies = context.dependencies;\n        /** @type { EditorContext['getResource'] } **/\n        this.getResource = context.getResource;\n        /** @type { EditorContext['dispatchTo'] } **/\n        this.dispatchTo = context.dispatchTo;\n        /** @type { EditorContext['delegateTo'] } **/\n        this.delegateTo = context.delegateTo;\n\n        this._cleanups = [];\n        this.isDestroyed = false;\n    }\n\n    setup() {}\n\n    isValidTargetForDomListener(ev) {\n        return isValidTargetForDomListener(ev.target);\n    }\n\n    /**\n     * Add an event listener on a given target, that will only be executed if\n     * the target is valid (unless `isGlobal` is true), and ensure it is removed\n     * when we destroy the editor.\n     *\n     * @param {Element} target\n     * @param {string} eventName\n     * @param {function(Event):void} fn\n     * @param {boolean} [capture=false] `useCapture` flag of `addEventListener`\n     * @param {boolean} [isGlobal=false] if true, don't check target validity\n     */\n    addDomListener(target, eventName, fn, capture = false, isGlobal = false) {\n        const handler = (ev) => {\n            if (isGlobal || this.isValidTargetForDomListener(ev)) {\n                fn?.call(this, ev);\n            }\n        };\n        target.addEventListener(eventName, handler, capture);\n        this._cleanups.push(() => target.removeEventListener(eventName, handler, capture));\n    }\n\n    /**\n     * Add an event listener on the editor's document, and ensure it is removed\n     * when we destroy the editor.\n     *\n     * @todo Use this function to avoid iframe problems.\n     *\n     * @param {string} eventName\n     * @param {function(Event):void} fn\n     * @param {boolean} [capture=false] `useCapture` flag of `addEventListener`\n     */\n    addGlobalDomListener(eventName, fn, capture = false) {\n        this.addDomListener(this.document, eventName, fn, capture, true);\n    }\n\n    destroy() {\n        for (const cleanup of this._cleanups) {\n            cleanup();\n        }\n        this.isDestroyed = true;\n    }\n}\n", "import { BaseContainerPlugin } from \"./core/base_container_plugin\";\nimport { ClipboardPlugin } from \"./core/clipboard_plugin\";\nimport { CommentPlugin } from \"./core/comment_plugin\";\nimport { DeletePlugin } from \"./core/delete_plugin\";\nimport { DialogPlugin } from \"./core/dialog_plugin\";\nimport { DomPlugin } from \"./core/dom_plugin\";\nimport { SeparatorPlugin } from \"./main/separator_plugin\";\nimport { FormatPlugin } from \"./core/format_plugin\";\nimport { HistoryPlugin } from \"./core/history_plugin\";\nimport { InputPlugin } from \"./core/input_plugin\";\nimport { LineBreakPlugin } from \"./core/line_break_plugin\";\nimport { NoInlineRootPlugin } from \"./core/no_inline_root_plugin\";\nimport { OverlayPlugin } from \"./core/overlay_plugin\";\nimport { ProtectedNodePlugin } from \"./core/protected_node_plugin\";\nimport { SanitizePlugin } from \"./core/sanitize_plugin\";\nimport { SelectionPlugin } from \"./core/selection_plugin\";\nimport { ShortCutPlugin } from \"./core/shortcut_plugin\";\nimport { SplitPlugin } from \"./core/split_plugin\";\nimport { UserCommandPlugin } from \"./core/user_command_plugin\";\nimport { AlignPlugin } from \"./main/align/align_plugin\";\nimport { BannerPlugin } from \"./main/banner_plugin\";\nimport { ChatGPTTranslatePlugin } from \"./main/chatgpt/chatgpt_translate_plugin\";\nimport { ColumnPlugin } from \"./main/column_plugin\";\nimport { EmojiPlugin } from \"./main/emoji_plugin\";\nimport { ColorPlugin } from \"./main/font/color_plugin\";\nimport { ColorUIPlugin } from \"./main/font/color_ui_plugin\";\nimport { FeffPlugin } from \"./main/feff_plugin\";\nimport { FontPlugin } from \"./main/font/font_plugin\";\nimport { FontFamilyPlugin } from \"./main/font/font_family_plugin\";\nimport { HintPlugin } from \"./main/hint_plugin\";\nimport { InlineCodePlugin } from \"./main/inline_code\";\nimport { LinkPastePlugin } from \"./main/link/link_paste_plugin\";\nimport { LinkPlugin } from \"./main/link/link_plugin\";\nimport { OdooLinkSelectionPlugin } from \"./main/link/link_selection_odoo_plugin\";\nimport { LinkSelectionPlugin } from \"./main/link/link_selection_plugin\";\nimport { ListPlugin } from \"./main/list/list_plugin\";\nimport { LocalOverlayPlugin } from \"./main/local_overlay_plugin\";\nimport { FilePlugin } from \"./main/media/file_plugin\";\nimport { IconPlugin } from \"./main/media/icon_plugin\";\nimport { IconColorPlugin } from \"./main/media/icon_color_plugin\";\nimport { ImageCropPlugin } from \"./main/media/image_crop_plugin\";\nimport { ImagePlugin } from \"./main/media/image_plugin\";\nimport { ImageSavePlugin } from \"./main/media/image_save_plugin\";\nimport { MediaPlugin } from \"./main/media/media_plugin\";\nimport { MoveNodePlugin } from \"./main/movenode_plugin\";\nimport { PowerButtonsPlugin } from \"./main/power_buttons_plugin\";\nimport { PositionPlugin } from \"./main/position_plugin\";\nimport { PowerboxPlugin } from \"./main/powerbox/powerbox_plugin\";\nimport { MediaUrlPastePlugin } from \"./main/link/powerbox_url_paste_plugin\";\nimport { SearchPowerboxPlugin } from \"./main/powerbox/search_powerbox_plugin\";\nimport { StarPlugin } from \"./main/star_plugin\";\nimport { TableAlignPlugin } from \"./main/table/table_align_plugin\";\nimport { TablePlugin } from \"./main/table/table_plugin\";\nimport { TableResizePlugin } from \"./main/table/table_resize_plugin\";\nimport { TableUIPlugin } from \"./main/table/table_ui_plugin\";\nimport { TabulationPlugin } from \"./main/tabulation_plugin\";\nimport { TextDirectionPlugin } from \"./main/text_direction_plugin\";\nimport { ToolbarPlugin } from \"./main/toolbar/toolbar_plugin\";\nimport { VideoPlugin } from \"./main/media/video_plugin\";\nimport { YoutubePlugin } from \"./main/youtube_plugin\";\nimport { PlaceholderPlugin } from \"./main/placeholder_plugin\";\nimport { CollaborationOdooPlugin } from \"./others/collaboration/collaboration_odoo_plugin\";\nimport { CollaborationPlugin } from \"./others/collaboration/collaboration_plugin\";\nimport { CollaborationSelectionAvatarPlugin } from \"./others/collaboration/collaboration_selection_avatar_plugin\";\nimport { CollaborationSelectionPlugin } from \"./others/collaboration/collaboration_selection_plugin\";\nimport { EmbeddedComponentPlugin } from \"./others/embedded_component_plugin\";\nimport { TableOfContentPlugin } from \"@html_editor/others/embedded_components/plugins/table_of_content_plugin/table_of_content_plugin\";\nimport { ToggleBlockPlugin } from \"@html_editor/others/embedded_components/plugins/toggle_block_plugin/toggle_block_plugin\";\nimport { EmbeddedVideoPlugin } from \"@html_editor/others/embedded_components/plugins/video_plugin/embedded_video_plugin\";\nimport { EmbeddedYoutubePlugin } from \"./others/embedded_components/plugins/video_plugin/embedded_youtube_plugin\";\nimport { CaptionPlugin } from \"@html_editor/others/embedded_components/plugins/caption_plugin/caption_plugin\";\nimport { EmbeddedFilePlugin } from \"@html_editor/others/embedded_components/plugins/embedded_file_plugin/embedded_file_plugin\";\nimport { SyntaxHighlightingPlugin } from \"@html_editor/others/embedded_components/plugins/syntax_highlighting_plugin/syntax_highlighting_plugin\";\nimport { QWebPlugin } from \"./others/qweb_plugin\";\nimport { EditorVersionPlugin } from \"./core/editor_version_plugin\";\nimport { ImagePostProcessPlugin } from \"./main/media/image_post_process_plugin\";\nimport { DoubleClickImagePreviewPlugin } from \"./main/media/dblclick_image_preview_plugin\";\nimport { StylePlugin } from \"./core/style_plugin\";\nimport { ContentEditablePlugin } from \"./core/content_editable_plugin\";\nimport { SelectionPlaceholderPlugin } from \"./main/selection_placeholder_plugin\";\n\nexport const CORE_PLUGINS = [\n    BaseContainerPlugin,\n    ClipboardPlugin,\n    CommentPlugin,\n    DeletePlugin,\n    DialogPlugin,\n    DomPlugin,\n    FormatPlugin,\n    HistoryPlugin,\n    InputPlugin,\n    LineBreakPlugin,\n    NoInlineRootPlugin,\n    OverlayPlugin,\n    ProtectedNodePlugin,\n    SanitizePlugin,\n    SelectionPlugin,\n    SplitPlugin,\n    UserCommandPlugin,\n    StylePlugin,\n    ContentEditablePlugin,\n];\n\nexport const MAIN_PLUGINS = [\n    ...CORE_PLUGINS,\n    BannerPlugin,\n    ChatGPTTranslatePlugin,\n    ColorPlugin,\n    ColorUIPlugin,\n    SeparatorPlugin,\n    ColumnPlugin,\n    EmojiPlugin,\n    HintPlugin,\n    AlignPlugin,\n    ListPlugin,\n    MediaPlugin,\n    ImageSavePlugin,\n    ShortCutPlugin,\n    PowerboxPlugin,\n    SearchPowerboxPlugin,\n    MediaUrlPastePlugin,\n    StarPlugin,\n    TablePlugin,\n    TableAlignPlugin,\n    TableUIPlugin,\n    TabulationPlugin,\n    ToolbarPlugin,\n    FontPlugin, // note: if before ListPlugin, there are a few split tests that fails\n    FontFamilyPlugin,\n    IconPlugin,\n    IconColorPlugin,\n    ImagePlugin,\n    ImagePostProcessPlugin,\n    ImageCropPlugin,\n    DoubleClickImagePreviewPlugin,\n    LinkPlugin,\n    LinkPastePlugin,\n    FeffPlugin,\n    LinkSelectionPlugin,\n    OdooLinkSelectionPlugin,\n    PowerButtonsPlugin,\n    MoveNodePlugin,\n    LocalOverlayPlugin,\n    PositionPlugin,\n    TextDirectionPlugin,\n    InlineCodePlugin,\n    TableResizePlugin,\n    PlaceholderPlugin,\n    SelectionPlaceholderPlugin,\n];\n\nexport const COLLABORATION_PLUGINS = [\n    CollaborationPlugin,\n    CollaborationOdooPlugin,\n    CollaborationSelectionPlugin,\n    CollaborationSelectionAvatarPlugin,\n];\n\nexport const EMBEDDED_COMPONENT_PLUGINS = [\n    EmbeddedComponentPlugin,\n    TableOfContentPlugin,\n    ToggleBlockPlugin,\n    EmbeddedVideoPlugin,\n    EmbeddedYoutubePlugin,\n    CaptionPlugin,\n    EmbeddedFilePlugin,\n    SyntaxHighlightingPlugin,\n];\n\nexport const NO_EMBEDDED_COMPONENTS_FALLBACK_PLUGINS = [FilePlugin, VideoPlugin, YoutubePlugin];\n\nexport const EXTRA_PLUGINS = [\n    ...COLLABORATION_PLUGINS,\n    ...MAIN_PLUGINS,\n    ...EMBEDDED_COMPONENT_PLUGINS,\n    EditorVersionPlugin,\n    QWebPlugin,\n];\n", "import { Component, onMounted, onWillDestroy, useRef, useSubEnv } from \"@odoo/owl\";\nimport { Editor } from \"./editor\";\nimport { Toolbar } from \"./main/toolbar/toolbar\";\nimport { useChildRef, useSpellCheck } from \"@web/core/utils/hooks\";\nimport { LocalOverlayContainer } from \"./local_overlay_container\";\nimport { uniqueId } from \"@web/core/utils/functions\";\n\n/**\n * @typedef { import(\"./editor\").EditorConfig } EditorConfig\n **/\n\nfunction copyCssRules(sourceDoc, targetDoc) {\n    for (const sheet of sourceDoc.styleSheets) {\n        const rules = [];\n        for (const r of sheet.cssRules) {\n            rules.push(r.cssText);\n        }\n        const cssRules = rules.join(\" \");\n        const styleTag = targetDoc.createElement(\"style\");\n        styleTag.appendChild(targetDoc.createTextNode(cssRules));\n        targetDoc.head.appendChild(styleTag);\n    }\n}\n\nexport class Wysiwyg extends Component {\n    static template = \"html_editor.Wysiwyg\";\n    static components = { Toolbar, LocalOverlayContainer };\n    static props = {\n        config: { type: Object, optional: true },\n        class: { type: String, optional: true },\n        contentClass: { type: String, optional: true }, // on editable element\n        style: { type: String, optional: true },\n        iframe: { type: Boolean, optional: true },\n        copyCss: { type: Boolean, optional: true },\n        onLoad: { type: Function, optional: true },\n        onBlur: { type: Function, optional: true },\n        dynamicPlaceholder: { type: Boolean, optional: true },\n    };\n\n    static defaultProps = {\n        onLoad: () => {},\n        onBlur: () => {},\n    };\n\n    setup() {\n        this.overlayRef = useChildRef();\n        useSubEnv({\n            localOverlayContainerKey: uniqueId(\"wysiwyg\"),\n        });\n        const contentRef = useRef(\"content\");\n        this.editor = this.props.editor;\n        const config = this.getEditorConfig();\n        this.editor = new Editor(config, this.env.services);\n        this.props.onLoad(this.editor);\n        useSpellCheck({\n            refName: \"content\",\n        });\n\n        onMounted(() => {\n            /** @type { any } **/\n            const el = contentRef.el;\n\n            if (el.tagName === \"IFRAME\") {\n                // grab the inner body instead\n                const attachEditor = () => {\n                    if (!this.editor.isDestroyed) {\n                        if (this.props.copyCss) {\n                            copyCssRules(document, el.contentDocument);\n                        }\n                        const additionalClasses = el.dataset.class?.trim().split(\" \");\n                        if (additionalClasses) {\n                            for (const c of additionalClasses) {\n                                el.contentDocument.body.classList.add(c);\n                            }\n                        }\n                        this.editor.attachTo(el.contentDocument.body);\n                    }\n                };\n                if (el.contentDocument.readyState === \"complete\") {\n                    attachEditor();\n                } else {\n                    // in firefox, iframe is not immediately available. we need to wait\n                    // for it to be ready before mounting editor\n                    el.addEventListener(\n                        \"load\",\n                        () => {\n                            attachEditor();\n                            this.render();\n                        },\n                        { once: true }\n                    );\n                }\n            } else {\n                this.editor.attachTo(el);\n            }\n        });\n        onWillDestroy(() => this.editor.destroy(true));\n    }\n\n    getEditorConfig() {\n        return {\n            ...this.props.config,\n            localOverlayContainers: {\n                key: this.env.localOverlayContainerKey,\n                ref: this.overlayRef,\n            },\n        };\n    }\n}\n", "import { Dialog } from \"@web/core/dialog/dialog\";\nimport { Notebook } from \"@web/core/notebook/notebook\";\nimport { formatDateTime } from \"@web/core/l10n/dates\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { memoize } from \"@web/core/utils/functions\";\nimport { Component, onMounted, useState, markup, onWillStart, onWillDestroy } from \"@odoo/owl\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { user } from \"@web/core/user\";\nimport { HtmlViewer } from \"@html_editor/components/html_viewer/html_viewer\";\nimport { READONLY_MAIN_EMBEDDINGS } from \"@html_editor/others/embedded_components/embedding_sets\";\nimport { browser } from \"@web/core/browser/browser\";\nimport { cookie } from \"@web/core/browser/cookie\";\nimport { loadBundle } from \"@web/core/assets\";\nimport { htmlReplaceAll } from \"@web/core/utils/html\";\n\nconst { DateTime } = luxon;\n\nexport class HistoryDialog extends Component {\n    static template = \"html_editor.HistoryDialog\";\n    static components = { Dialog, HtmlViewer, Notebook };\n    static props = {\n        recordId: Number,\n        recordModel: String,\n        close: Function,\n        restoreRequested: Function,\n        historyMetadata: Array,\n        versionedFieldName: String,\n        title: { String, optional: true },\n        noContentHelper: { String, optional: true }, //Markup\n        embeddedComponents: { Array, optional: true },\n    };\n\n    DEFAULT_AVATAR = \"/mail/static/src/img/smiley/avatar.jpg\";\n\n    static defaultProps = {\n        title: _t(\"History\"),\n        noContentHelper: markup(\"\"),\n        embeddedComponents: [...READONLY_MAIN_EMBEDDINGS],\n    };\n\n    state = useState({\n        revisionsData: [],\n        currentView: \"content\", // \"content\" or \"comparison\"\n        isComparisonSplit: false, // true for side-by-side, false for unified diff\n        revisionContent: null,\n        revisionComparison: null,\n        revisionId: null,\n        revisionLoading: true,\n        cssMaxHeight: 400,\n    });\n\n    setup() {\n        this.size = \"fullscreen\";\n        this.title = this.props.title;\n        this.orm = useService(\"orm\");\n        this.resizeObserver = null;\n\n        onWillStart(async () => {\n            // We include the current document version as the first revision,\n            // and we shift the rest of the metadata to be more logical for the user.\n            let revisionId = -1;\n            const revisionData = [];\n            for (const metadata of this.props.historyMetadata) {\n                revisionData.push({ ...metadata, revision_id: revisionId });\n                revisionId = metadata[\"revision_id\"];\n            }\n            // add the initial revision data based on the record creation date and user\n            const record = await this.orm.read(\n                this.props.recordModel,\n                [this.props.recordId],\n                [\"create_date\", \"create_uid\"]\n            );\n            revisionData.push({\n                revision_id: revisionId,\n                create_date: DateTime.fromFormat(\n                    record[0][\"create_date\"],\n                    \"yyyy-MM-dd HH:mm:ss\"\n                ).toISO(),\n                create_uid: record[0][\"create_uid\"][0],\n                create_user_name: record[0][\"create_uid\"][1],\n            });\n\n            this.state.revisionsData = revisionData;\n            this.resizeObserver = new ResizeObserver(this.resize.bind(this));\n            this.resizeObserver.observe(document.body);\n        });\n        onMounted(() => this.init());\n        onWillDestroy(() => {\n            this.resizeObserver?.disconnect();\n        });\n    }\n\n    resize() {\n        const dialogContainer = document.querySelector(\".html-history-dialog-container\");\n        const computedStyle = getComputedStyle(dialogContainer);\n        this.state.cssMaxHeight = parseInt(computedStyle.height.replace(\"px\", \"\")) - 160;\n    }\n\n    getConfig(value) {\n        return {\n            value: this.state[value],\n            embeddedComponents: this.props.embeddedComponents,\n        };\n    }\n\n    async init() {\n        // Load diff2html only in debug mode, as the side-by-side comparison is only available in debug mode.\n        if (this.env.debug) {\n            await loadBundle(\"html_editor.assets_history_diff\");\n        }\n        await this.updateCurrentRevision(this.state.revisionsData[0][\"revision_id\"]);\n        this.resize();\n    }\n\n    async updateCurrentRevision(revisionId) {\n        if (this.state.revisionId === revisionId) {\n            return;\n        }\n        this.state.revisionLoading = true;\n        this.state.revisionId = revisionId;\n        this.state.revisionContent = await this.getRevisionContent(revisionId);\n        this.state.revisionComparison = await this.getRevisionComparison(revisionId);\n        this.state.revisionComparisonSplit = await this.getRevisionComparisonSplit(revisionId);\n        this.state.revisionLoading = false;\n    }\n\n    getRevisionComparison = memoize(\n        async function getRevisionComparison(revisionId) {\n            if (revisionId === -1) {\n                return \"\";\n            }\n            const comparison = await this.orm.call(\n                this.props.recordModel,\n                \"html_field_history_get_comparison_at_revision\",\n                [this.props.recordId, this.props.versionedFieldName, revisionId]\n            );\n            return this._removeExternalBlockHtml(markup(comparison));\n        }.bind(this)\n    );\n\n    getRevisionComparisonSplit = memoize(\n        async function getRevisionComparisonSplit(revisionId) {\n            if (!this.env.debug || revisionId === -1) {\n                return \"\";\n            }\n            let unifiedDiffString = await this.orm.call(\n                this.props.recordModel,\n                \"html_field_history_get_unified_diff_at_revision\",\n                [this.props.recordId, this.props.versionedFieldName, revisionId]\n            );\n            // Remove unnecessary linebreaks\n            unifiedDiffString = unifiedDiffString.replace(/^\\s*[\\r\\n]/gm, \"\");\n            const colorScheme = cookie.get(\"color_scheme\") === \"dark\" ? \"dark\" : \"light\";\n            // eslint-disable-next-line no-undef\n            const diffHtml = Diff2Html.html(unifiedDiffString, {\n                drawFileList: false,\n                matching: \"lines\",\n                outputFormat: \"side-by-side\",\n                colorScheme: colorScheme,\n            });\n            return markup(diffHtml);\n        }.bind(this)\n    );\n\n    getRevisionContent = memoize(\n        async function getRevisionContent(revisionId) {\n            if (revisionId === -1) {\n                const curentContent = await this.orm.read(\n                    this.props.recordModel,\n                    [this.props.recordId],\n                    [this.props.versionedFieldName]\n                );\n                if (!curentContent || !curentContent.length) {\n                    return this.props.noContentHelper;\n                }\n                return this._removeExternalBlockHtml(\n                    markup(curentContent[0][this.props.versionedFieldName])\n                );\n            }\n            const content = await this.orm.call(\n                this.props.recordModel,\n                \"html_field_history_get_content_at_revision\",\n                [this.props.recordId, this.props.versionedFieldName, revisionId]\n            );\n            return this._removeExternalBlockHtml(markup(content));\n        }.bind(this)\n    );\n\n    async _onRestoreRevisionClick() {\n        this.env.services.ui.block();\n        const restoredContent = await this.getRevisionContent(this.state.revisionId);\n        this.props.restoreRequested(restoredContent, this.props.close);\n        this.env.services.ui.unblock();\n    }\n\n    _removeExternalBlockHtml(baseHtml) {\n        const filteringRegex = /<[a-z ]+data-embedded=\"(?:(?!<).)+<\\/[a-z]+>/gim;\n        const placeholderHtml = markup`<div class=\"embedded-history-dialog-placeholder\">${_t(\n            \"Dynamic element\"\n        )}</div>`;\n        return htmlReplaceAll(baseHtml, filteringRegex, () => placeholderHtml);\n    }\n\n    /**\n     * Getters\n     **/\n    getRevisionDate(revision) {\n        if (!revision || !revision[\"create_date\"]) {\n            return \"--\";\n        }\n        const userTZ = user.tz || \"local\";\n        return formatDateTime(\n            DateTime.fromISO(revision[\"create_date\"], { zone: \"utc\" }).setZone(userTZ),\n            { showSeconds: false }\n        );\n    }\n    getRevisionClasses(revision) {\n        let classesStr = \"btn\";\n\n        if (\n            this.state.revisionId !== -1 &&\n            (this.state.revisionId < revision.revision_id || revision.revision_id === -1)\n        ) {\n            classesStr += \" targeted\";\n        } else if (this.state.revisionId === revision.revision_id) {\n            classesStr += \" selected\";\n        }\n\n        return classesStr;\n    }\n    getRevisionAuthorAvatar(revision) {\n        if (!revision || !revision[\"create_uid\"]) {\n            return this.DEFAULT_AVATAR;\n        }\n        return `${browser.location.origin}/web/image?model=res.users&field=avatar_128&id=${revision[\"create_uid\"]}`;\n    }\n\n    get currentRevision() {\n        const id = this.state?.revisionId || this.state.revisionsData[0][\"revision_id\"];\n        return this.state.revisionsData.find((revision) => revision[\"revision_id\"] === id);\n    }\n}\n", "import {\n    containsAnyNonPhrasingContent,\n    getDeepestPosition,\n    isContentEditable,\n    isElement,\n    isEmpty,\n    isMediaElement,\n    isProtected,\n    isProtecting,\n} from \"@html_editor/utils/dom_info\";\nimport { Plugin } from \"../plugin\";\nimport { fillEmpty } from \"@html_editor/utils/dom\";\nimport {\n    BASE_CONTAINER_CLASS,\n    baseContainerGlobalSelector,\n    createBaseContainer,\n} from \"../utils/base_container\";\nimport { withSequence } from \"@html_editor/utils/resource\";\nimport { selectElements } from \"@html_editor/utils/dom_traversal\";\nimport { childNodeIndex } from \"@html_editor/utils/position\";\n\n/**\n * @typedef { Object } BaseContainerShared\n * @property { BaseContainerPlugin['createBaseContainer'] } createBaseContainer\n * @property { BaseContainerPlugin['getDefaultNodeName'] } getDefaultNodeName\n * @property { BaseContainerPlugin['isCandidateForBaseContainer'] } isCandidateForBaseContainer\n */\n\n/**\n * @typedef {((node: Node) => boolean)[]} invalid_for_base_container_predicates\n */\n\nexport class BaseContainerPlugin extends Plugin {\n    static id = \"baseContainer\";\n    static shared = [\"createBaseContainer\", \"getDefaultNodeName\", \"isCandidateForBaseContainer\"];\n    static defaultConfig = {\n        baseContainers: [\"P\", \"DIV\"],\n    };\n    static dependencies = [\"selection\"];\n    /**\n     * Register one of the predicates for `invalid_for_base_container_predicates`\n     * as a property for optimization, see variants of `isCandidateForBaseContainer`.\n     */\n    hasNonPhrasingContentPredicate = (element) =>\n        element?.nodeType === Node.ELEMENT_NODE && containsAnyNonPhrasingContent(element);\n    /**\n     * The `unsplittable` predicate for `invalid_for_base_container_predicates`\n     * is defined in this file and not in split_plugin because it has to be removed\n     * in a specific case: see `isCandidateForBaseContainerAllowUnsplittable`.\n     */\n    isUnsplittablePredicate = (element) =>\n        this.getResource(\"unsplittable_node_predicates\").some((fn) => fn(element));\n    /** @type {import(\"plugins\").EditorResources} */\n    resources = {\n        clean_for_save_handlers: this.cleanForSave.bind(this),\n        // `baseContainer` normalization should occur after every other normalization\n        // because a `div` may only have the baseContainer identity if it does not\n        // already have another incompatible identity given by another plugin.\n        normalize_handlers: withSequence(Infinity, this.normalizeDivBaseContainers.bind(this)),\n        delete_handlers: () => {\n            if (this.config.cleanEmptyStructuralContainers === false) {\n                return;\n            }\n            this.cleanEmptyStructuralContainers();\n        },\n        unsplittable_node_predicates: (node) => {\n            if (node.nodeName !== \"DIV\") {\n                return false;\n            }\n            return !this.isCandidateForBaseContainerAllowUnsplittable(node);\n        },\n        invalid_for_base_container_predicates: [\n            (node) =>\n                !node ||\n                node.nodeType !== Node.ELEMENT_NODE ||\n                !this.config.baseContainers.includes(node.tagName) ||\n                isProtected(node) ||\n                isProtecting(node) ||\n                isMediaElement(node),\n            this.isUnsplittablePredicate,\n            this.hasNonPhrasingContentPredicate,\n        ],\n        system_classes: [BASE_CONTAINER_CLASS],\n    };\n\n    createBaseContainer(nodeName = this.getDefaultNodeName()) {\n        return createBaseContainer(nodeName, this.document);\n    }\n\n    getDefaultNodeName() {\n        return this.config.baseContainers[0];\n    }\n\n    cleanEmptyStructuralContainers() {\n        const node = this.document.getSelection().anchorNode;\n\n        if (!isElement(node) || !isEmpty(node)) {\n            return;\n        }\n\n        const closestEditable = (n) =>\n            isContentEditable(n.parentElement) ? closestEditable(n.parentElement) : n;\n\n        const isUnsplittable = this.isUnsplittablePredicate(node);\n        const isCandidateForBase = this.isCandidateForBaseContainerAllowUnsplittable(node);\n\n        if (isUnsplittable || !isCandidateForBase) {\n            return;\n        }\n\n        let anchorNode = node.parentElement;\n        if (\n            anchorNode === closestEditable(node) ||\n            !this.config.baseContainers.includes(anchorNode.nodeName) ||\n            this.getResource(\"unremovable_node_predicates\").some((p) => p(anchorNode))\n        ) {\n            return;\n        }\n\n        if (isEmpty(anchorNode)) {\n            fillEmpty(anchorNode);\n        }\n\n        let anchorOffset = childNodeIndex(node);\n        node.remove();\n\n        [anchorNode, anchorOffset] = getDeepestPosition(anchorNode, anchorOffset);\n        this.dependencies.selection.setSelection({\n            anchorNode,\n            anchorOffset,\n        });\n    }\n\n    /**\n     * Evaluate if an element is eligible to become a baseContainer (i.e. an\n     * unmarked div which could receive baseContainer attributes to inherit\n     * paragraph-like features).\n     *\n     * This function considers unsplittable and childNodes.\n     */\n    isCandidateForBaseContainer(element) {\n        return !this.getResource(\"invalid_for_base_container_predicates\").some((fn) => fn(element));\n    }\n\n    /**\n     * Evaluate if an element would be eligible to become a baseContainer\n     * without considering unsplittable.\n     *\n     * This function is only meant to be used during `unsplittable_node_predicates` to\n     * avoid an infinite loop:\n     * Considering a `DIV`,\n     * - During `unsplittable_node_predicates`, one predicate should return true\n     *   if the `DIV` is NOT a baseContainer candidate (Odoo specification),\n     *   therefore `invalid_for_base_container_predicates` should be evaluated.\n     * - During `invalid_for_base_container_predicates`, one predicate should\n     *   return true if the `DIV` is unsplittable, because a node has to be\n     *   splittable to use the featureSet associated with paragraphs.\n     * Each resource has to call the other. To avoid the issue, during\n     * `unsplittable_node_predicates`, the baseContainer predicate will execute\n     * all predicates for `invalid_for_base_container_predicates` except\n     * the one using `unsplittable_node_predicates`, since it is already being\n     * evaluated.\n     *\n     * In simpler terms:\n     * A `DIV` is unsplittable by default;\n     * UNLESS it is eligible to be a baseContainer => it becomes one;\n     * UNLESS it has to be unsplittable for an explicit reason (i.e. has class\n     * oe_unbreakable) => it stays unsplittable.\n     */\n    isCandidateForBaseContainerAllowUnsplittable(element) {\n        for (const predicate of this.getResource(\"invalid_for_base_container_predicates\")) {\n            if (predicate === this.isUnsplittablePredicate) {\n                continue;\n            }\n            if (predicate(element)) {\n                return false;\n            }\n        }\n        return true;\n    }\n\n    /**\n     * Evaluate if an element would be eligible to become a baseContainer\n     * without considering its childNodes.\n     *\n     * This function is only meant to be used internally, to avoid having to\n     * compute childNodes multiple times in more complex operations.\n     */\n    shallowIsCandidateForBaseContainer(element) {\n        const predicates = this.getResource(\"invalid_for_base_container_predicates\");\n        for (const predicate of predicates) {\n            if (predicate === this.hasNonPhrasingContentPredicate) {\n                continue;\n            }\n            if (predicate(element)) {\n                return false;\n            }\n        }\n        return true;\n    }\n\n    cleanForSave({ root }) {\n        for (const baseContainer of selectElements(root, `.${BASE_CONTAINER_CLASS}`)) {\n            baseContainer.classList.remove(BASE_CONTAINER_CLASS);\n            if (baseContainer.classList.length === 0) {\n                baseContainer.removeAttribute(\"class\");\n            }\n        }\n    }\n\n    normalizeDivBaseContainers(element = this.editable) {\n        if (this.config.baseContainers && !this.config.baseContainers.includes(\"DIV\")) {\n            return;\n        }\n        const newBaseContainers = [];\n        const divSelector = `div:not(.${BASE_CONTAINER_CLASS})`;\n        const targets = [...element.querySelectorAll(divSelector)];\n        if (element.matches(divSelector)) {\n            targets.unshift(element);\n        }\n        for (const div of targets) {\n            if (\n                // Ensure that newly created `div` baseContainers are never themselves\n                // children of a baseContainer. BaseContainers should always only\n                // contain phrasing content (even `div`), because they could be\n                // converted to an element which can actually only contain phrasing\n                // content. In practice a div should never be a child of a\n                // baseContainer, since a baseContainer should only contain\n                // phrasingContent.\n                !div.parentElement?.matches(baseContainerGlobalSelector) &&\n                this.shallowIsCandidateForBaseContainer(div) &&\n                !containsAnyNonPhrasingContent(div)\n            ) {\n                div.classList.add(BASE_CONTAINER_CLASS);\n                newBaseContainers.push(div);\n                fillEmpty(div);\n            }\n        }\n    }\n}\n", "import { isTextNode, isParagraphRelatedElement, isEmptyBlock } from \"../utils/dom_info\";\nimport { Plugin } from \"../plugin\";\nimport { closestBlock } from \"../utils/blocks\";\nimport { unwrapContents, wrapInlinesInBlocks, splitTextNode, fillEmpty } from \"../utils/dom\";\nimport { fillClipboardData } from \"../utils/clipboard\";\nimport { childNodes, closestElement } from \"../utils/dom_traversal\";\nimport { parseHTML } from \"../utils/html\";\nimport {\n    baseContainerGlobalSelector,\n    getBaseContainerSelector,\n} from \"@html_editor/utils/base_container\";\nimport { DIRECTIONS } from \"../utils/position\";\nimport { isHtmlContentSupported } from \"./selection_plugin\";\n\n/**\n * @typedef { import(\"./selection_plugin\").EditorSelection } EditorSelection\n *\n * @typedef {(() => boolean)[]} bypass_paste_image_files\n */\n\nconst CLIPBOARD_BLACKLISTS = {\n    unwrap: [\n        // These elements' children will be unwrapped.\n        \".Apple-interchange-newline\",\n        \"DIV\", // DIV is unwrapped unless eligible to be a baseContainer, see cleanForPaste\n    ],\n    remove: [\"META\", \"STYLE\", \"SCRIPT\"], // These elements will be removed along with their children.\n};\nexport const CLIPBOARD_WHITELISTS = {\n    nodes: [\n        // Style\n        \"P\",\n        \"H1\",\n        \"H2\",\n        \"H3\",\n        \"H4\",\n        \"H5\",\n        \"H6\",\n        \"BLOCKQUOTE\",\n        \"PRE\",\n        // List\n        \"UL\",\n        \"OL\",\n        \"LI\",\n        // Inline style\n        \"I\",\n        \"B\",\n        \"U\",\n        \"S\",\n        \"EM\",\n        \"FONT\",\n        \"STRONG\",\n        // Table\n        \"TABLE\",\n        \"THEAD\",\n        \"TH\",\n        \"TBODY\",\n        \"TR\",\n        \"TD\",\n        // Miscellaneous\n        \"IMG\",\n        \"BR\",\n        \"A\",\n        \".fa\",\n    ],\n    classes: [\n        // Media\n        /^float-/,\n        \"d-block\",\n        \"mx-auto\",\n        \"img-fluid\",\n        \"img-thumbnail\",\n        \"rounded\",\n        \"rounded-circle\",\n        // Odoo tables\n        \"o_table\",\n        \"table\",\n        \"table-bordered\",\n        /^padding-/,\n        /^shadow/,\n        // Odoo colors\n        /^text-o-/,\n        /^bg-o-/,\n        // Odoo lists\n        \"o_checked\",\n        \"o_checklist\",\n        \"oe-nested\",\n        // Miscellaneous\n        /^btn/,\n        /^fa/,\n    ],\n    attributes: [\"class\", \"href\", \"src\", \"target\"],\n    styledTags: [\"SPAN\", \"B\", \"STRONG\", \"I\", \"S\", \"U\", \"FONT\", \"TD\"],\n};\n\nconst ONLY_LINK_REGEX = /^(https?:\\/\\/)?([\\w-]+\\.)+[\\w-]+(\\/[\\w-./?%&=]*)?$/i;\n\n/**\n * @typedef {Object} ClipboardShared\n * @property {ClipboardPlugin['pasteText']} pasteText\n */\n\n/**\n * @typedef {((img: HTMLImageElement) => void)[]} added_image_handlers\n * @typedef {(() => void)[]} after_paste_handlers\n * @typedef {(() => void)[]} before_paste_handlers\n *\n * @typedef {((selection: EditorSelection, text: string) => boolean)[]} paste_text_overrides\n *\n * @typedef {((\n *     clonedContents: DocumentFragment,\n *     selection: EditorSelection\n *   ) => void | clonedContents)[]} clipboard_content_processors\n * @typedef {((textContent: string) => string)[]} clipboard_text_processors\n */\n\nexport class ClipboardPlugin extends Plugin {\n    static id = \"clipboard\";\n    static dependencies = [\n        \"baseContainer\",\n        \"dom\",\n        \"selection\",\n        \"sanitize\",\n        \"history\",\n        \"split\",\n        \"delete\",\n        \"lineBreak\",\n    ];\n    static shared = [\"pasteText\"];\n\n    setup() {\n        this.addDomListener(this.editable, \"copy\", this.onCopy);\n        this.addDomListener(this.editable, \"cut\", this.onCut);\n        this.addDomListener(this.editable, \"paste\", this.onPaste);\n        this.addDomListener(this.editable, \"dragstart\", this.onDragStart);\n        this.addDomListener(this.editable, \"drop\", this.onDrop);\n    }\n\n    onCut(ev) {\n        this.onCopy(ev);\n        this.dependencies.history.stageSelection();\n        this.dependencies.delete.deleteSelection();\n        this.dependencies.history.addStep();\n    }\n\n    /**\n     * @param {ClipboardEvent} ev\n     */\n    onCopy(ev) {\n        ev.preventDefault();\n        const selection = this.dependencies.selection.getEditableSelection();\n        let clonedContents = selection.cloneContents();\n        if (!clonedContents.hasChildNodes()) {\n            return;\n        }\n\n        // Prepare text content for clipboard.\n        let textContent = selection.textContent();\n        for (const processor of this.getResource(\"clipboard_text_processors\")) {\n            textContent = processor(textContent);\n        }\n        ev.clipboardData.setData(\"text/plain\", textContent);\n\n        // Prepare html content for clipboard.\n        for (const processor of this.getResource(\"clipboard_content_processors\")) {\n            clonedContents = processor(clonedContents, selection) || clonedContents;\n        }\n        this.dependencies.dom.removeSystemProperties(clonedContents);\n        fillClipboardData(ev, clonedContents);\n    }\n\n    /**\n     * Handle safe pasting of html or plain text into the editor.\n     */\n    onPaste(ev) {\n        let selection = this.dependencies.selection.getEditableSelection();\n        if (\n            !selection.anchorNode.isConnected ||\n            !closestElement(selection.anchorNode).isContentEditable\n        ) {\n            return;\n        }\n        ev.preventDefault();\n\n        this.dependencies.history.stageSelection();\n\n        this.dispatchTo(\"before_paste_handlers\", selection, ev);\n        // refresh selection after potential changes from `before_paste` handlers\n        selection = this.dependencies.selection.getEditableSelection();\n\n        this.handlePasteUnsupportedHtml(selection, ev.clipboardData) ||\n            this.handlePasteOdooEditorHtml(ev.clipboardData) ||\n            this.handlePasteHtml(selection, ev.clipboardData) ||\n            this.handlePasteText(selection, ev.clipboardData);\n\n        this.dispatchTo(\"after_paste_handlers\", selection);\n        this.dependencies.history.addStep();\n    }\n    /**\n     * @param {EditorSelection} selection\n     * @param {DataTransfer} clipboardData\n     */\n    handlePasteUnsupportedHtml(selection, clipboardData) {\n        if (!isHtmlContentSupported(selection)) {\n            const text = clipboardData.getData(\"text/plain\");\n            this.dependencies.dom.insert(text);\n            return true;\n        }\n    }\n    /**\n     * @param {DataTransfer} clipboardData\n     */\n    handlePasteOdooEditorHtml(clipboardData) {\n        const odooEditorHtml = clipboardData.getData(\"application/vnd.odoo.odoo-editor\");\n        const textContent = clipboardData.getData(\"text/plain\");\n        if (ONLY_LINK_REGEX.test(textContent)) {\n            return false;\n        }\n        if (odooEditorHtml) {\n            const fragment = parseHTML(this.document, odooEditorHtml);\n            this.dependencies.sanitize.sanitize(fragment);\n            if (fragment.hasChildNodes()) {\n                this.dependencies.dom.insert(fragment);\n            }\n            return true;\n        }\n    }\n    /**\n     * @param {EditorSelection} selection\n     * @param {DataTransfer} clipboardData\n     */\n    handlePasteHtml(selection, clipboardData) {\n        const files = this.delegateTo(\"bypass_paste_image_files\")\n            ? []\n            : getImageFiles(clipboardData);\n        const clipboardHtml = clipboardData.getData(\"text/html\");\n        const textContent = clipboardData.getData(\"text/plain\");\n        if (ONLY_LINK_REGEX.test(textContent)) {\n            return false;\n        }\n        if (files.length || clipboardHtml) {\n            const clipboardElem = this.prepareClipboardData(clipboardHtml);\n            // @phoenix @todo: should it be handled in table plugin?\n            // When copy pasting a table from the outside, a picture of the\n            // table can be included in the clipboard as an image file. In that\n            // particular case the html table is given a higher priority than\n            // the clipboard picture.\n            if (files.length && !clipboardElem.querySelector(\"table\")) {\n                // @phoenix @todo: should it be handled in image plugin?\n                return this.addImagesFiles(files).then((html) => {\n                    this.dependencies.dom.insert(html);\n                    this.dependencies.history.addStep();\n                });\n            } else if (clipboardElem.hasChildNodes()) {\n                if (closestElement(selection.anchorNode, \"a\")) {\n                    this.dependencies.dom.insert(clipboardElem.textContent);\n                } else {\n                    this.dependencies.dom.insert(clipboardElem);\n                }\n            }\n            return true;\n        }\n    }\n    /**\n     * @param {EditorSelection} selection\n     * @param {DataTransfer} clipboardData\n     */\n    handlePasteText(selection, clipboardData) {\n        const text = clipboardData.getData(\"text/plain\");\n        if (this.delegateTo(\"paste_text_overrides\", selection, text)) {\n            return;\n        } else {\n            this.pasteText(text);\n        }\n    }\n    /**\n     * @param {string} text\n     */\n    pasteText(text) {\n        const textFragments = text.split(/\\r?\\n/);\n        let selection = this.dependencies.selection.getEditableSelection();\n        const preEl = closestElement(selection.anchorNode, \"PRE\");\n        let textIndex = 1;\n        for (const textFragment of textFragments) {\n            let modifiedTextFragment = textFragment;\n\n            // <pre> preserves whitespace by default, so no need for &nbsp.\n            if (!preEl) {\n                // Replace consecutive spaces by alternating nbsp.\n                modifiedTextFragment = textFragment.replace(/( {2,})/g, (match) => {\n                    let alternateValue = false;\n                    return match.replace(/ /g, () => {\n                        alternateValue = !alternateValue;\n                        const replaceContent = alternateValue ? \"\\u00A0\" : \" \";\n                        return replaceContent;\n                    });\n                });\n            }\n            this.dependencies.dom.insert(modifiedTextFragment);\n            if (textIndex < textFragments.length) {\n                selection = this.dependencies.selection.getEditableSelection();\n                // Break line by inserting new paragraph and\n                // remove current paragraph's bottom margin.\n                const block = closestBlock(selection.anchorNode);\n                if (\n                    this.dependencies.split.isUnsplittable(block) ||\n                    closestElement(selection.anchorNode).tagName === \"PRE\"\n                ) {\n                    this.dependencies.lineBreak.insertLineBreak();\n                } else {\n                    const [blockBefore] = this.dependencies.split.splitBlock();\n                    if (\n                        block &&\n                        block.matches(baseContainerGlobalSelector) &&\n                        blockBefore &&\n                        !blockBefore.matches(getBaseContainerSelector(\"DIV\"))\n                    ) {\n                        // Do something only if blockBefore is not a DIV (which is the no-margin option)\n                        // replace blockBefore by a DIV.\n                        const div = this.dependencies.baseContainer.createBaseContainer(\"DIV\");\n                        const cursors = this.dependencies.selection.preserveSelection();\n                        blockBefore.before(div);\n                        div.replaceChildren(...childNodes(blockBefore));\n                        blockBefore.remove();\n                        cursors.remapNode(blockBefore, div).restore();\n                    }\n                }\n            }\n            textIndex++;\n        }\n    }\n\n    /**\n     * Prepare clipboard data (text/html) for safe pasting into the editor.\n     *\n     * @private\n     * @param {string} clipboardData\n     * @returns {DocumentFragment}\n     */\n    prepareClipboardData(clipboardData) {\n        const fragment = parseHTML(this.document, clipboardData);\n        this.dependencies.sanitize.sanitize(fragment);\n        const container = this.document.createElement(\"fake-container\");\n        container.append(fragment);\n\n        for (const tableElement of container.querySelectorAll(\"table\")) {\n            tableElement.classList.add(\"table\", \"table-bordered\", \"o_table\");\n        }\n        if (this.delegateTo(\"bypass_paste_image_files\")) {\n            for (const imgElement of container.querySelectorAll(\"img\")) {\n                imgElement.remove();\n            }\n        }\n\n        // todo: should it be in its own plugin ?\n        const progId = container.querySelector('meta[name=\"ProgId\"]');\n        if (progId && progId.content === \"Excel.Sheet\") {\n            // Microsoft Excel keeps table style in a <style> tag with custom\n            // classes. The following lines parse that style and apply it to the\n            // style attribute of <td> tags with matching classes.\n            const xlStylesheet = container.querySelector(\"style\");\n            const xlNodes = container.querySelectorAll(\"[class*=xl],[class*=font]\");\n            for (const xlNode of xlNodes) {\n                for (const xlClass of xlNode.classList) {\n                    // Regex captures a CSS rule definition for that xlClass.\n                    const xlStyle = xlStylesheet.textContent\n                        .match(`.${xlClass}[^{]*{(?<xlStyle>[^}]*)}`)\n                        .groups.xlStyle.replace(\"background:\", \"background-color:\");\n                    xlNode.setAttribute(\"style\", xlNode.style.cssText + \";\" + xlStyle);\n                }\n            }\n        }\n        const childContent = childNodes(container);\n        for (const child of childContent) {\n            this.cleanForPaste(child);\n        }\n        // Identify the closest baseContainer from the selection. This will\n        // determine which baseContainer will be used by default for the\n        // clipboard content if it has to be modified.\n        const selection = this.dependencies.selection.getEditableSelection();\n        const closestBaseContainer =\n            selection.anchorNode &&\n            closestElement(selection.anchorNode, baseContainerGlobalSelector);\n        // Force inline nodes at the root of the container into separate `baseContainers`\n        // elements. This is a tradeoff to ensure some features that rely on\n        // nodes having a parent (e.g. convert to list, title, etc.) can work\n        // properly on such nodes without having to actually handle that\n        // particular case in all of those functions. In fact, this case cannot\n        // happen on a new document created using this editor, but will happen\n        // instantly when editing a document that was created from Etherpad.\n        wrapInlinesInBlocks(container, {\n            baseContainerNodeName:\n                closestBaseContainer?.nodeName ||\n                this.dependencies.baseContainer.getDefaultNodeName(),\n        });\n        const result = this.document.createDocumentFragment();\n        result.replaceChildren(...childNodes(container));\n\n        // Split elements containing <br> into separate elements for each line.\n        const brs = result.querySelectorAll(\"br\");\n        for (const br of brs) {\n            const block = closestBlock(br);\n            if (\n                (isParagraphRelatedElement(block) ||\n                    this.dependencies.baseContainer.isCandidateForBaseContainer(block)) &&\n                block.nodeName !== \"PRE\"\n            ) {\n                // A linebreak at the beginning of a block is an empty line.\n                const isEmptyLine = block.firstChild.nodeName === \"BR\";\n                // Split blocks around it until only the BR remains in the\n                // block.\n                const remainingBrContainer = this.dependencies.split.splitAroundUntil(br, block);\n                // Remove the container unless it represented an empty line.\n                if (!isEmptyLine) {\n                    remainingBrContainer.remove();\n                }\n            }\n        }\n        return result;\n    }\n    /**\n     * Clean a node for safely pasting. Cleaning an element involves unwrapping\n     * its contents if it's an illegal (blacklisted or not whitelisted) element,\n     * or removing its illegal attributes and classes.\n     *\n     * @param {Node} node\n     */\n    cleanForPaste(node) {\n        if (\n            !this.isWhitelisted(node) ||\n            this.isBlacklisted(node) ||\n            // Google Docs have their html inside a B tag with custom id.\n            (node.id && node.id.startsWith(\"docs-internal-guid\"))\n        ) {\n            if (!node.matches || node.matches(CLIPBOARD_BLACKLISTS.remove.join(\",\"))) {\n                node.remove();\n            } else {\n                let childrenNodes;\n                if (node.nodeName === \"DIV\") {\n                    if (!node.hasChildNodes()) {\n                        node.remove();\n                        return;\n                    } else if (this.dependencies.baseContainer.isCandidateForBaseContainer(node)) {\n                        const whiteSpace = node.style?.whiteSpace;\n                        if (whiteSpace && ![\"normal\", \"nowrap\"].includes(whiteSpace)) {\n                            node.innerHTML = node.innerHTML.replace(/\\n/g, \"<br>\");\n                        }\n                        const baseContainer = this.dependencies.baseContainer.createBaseContainer();\n                        const dir = node.getAttribute(\"dir\");\n                        if (dir) {\n                            baseContainer.setAttribute(\"dir\", dir);\n                        }\n                        baseContainer.append(...node.childNodes);\n\n                        node.replaceWith(baseContainer);\n                        childrenNodes = childNodes(baseContainer);\n                    } else {\n                        childrenNodes = unwrapContents(node);\n                    }\n                } else {\n                    // Unwrap the illegal node's contents.\n                    childrenNodes = unwrapContents(node);\n                }\n                for (const child of childrenNodes) {\n                    this.cleanForPaste(child);\n                }\n            }\n        } else if (node.nodeType !== Node.TEXT_NODE) {\n            if (node.nodeName === \"THEAD\") {\n                const tbody = node.nextElementSibling;\n                if (tbody) {\n                    // If a <tbody> already exists, move all rows from\n                    // <thead> into the start of <tbody>.\n                    tbody.prepend(...node.children);\n                    node.remove();\n                    node = tbody;\n                } else {\n                    // Otherwise, replace the <thead> with <tbody>\n                    node = this.dependencies.dom.setTagName(node, \"TBODY\");\n                }\n            } else if ([\"TD\", \"TH\"].includes(node.nodeName)) {\n                // Insert base container into empty TD.\n                if (isEmptyBlock(node)) {\n                    const baseContainer = this.dependencies.baseContainer.createBaseContainer();\n                    fillEmpty(baseContainer);\n                    node.replaceChildren(baseContainer);\n                }\n\n                if (node.hasAttribute(\"bgcolor\") && !node.style[\"background-color\"]) {\n                    node.style[\"background-color\"] = node.getAttribute(\"bgcolor\");\n                }\n            } else if (node.nodeName === \"FONT\") {\n                // FONT tags have some style information in custom attributes,\n                // this maps them to the style attribute.\n                if (node.hasAttribute(\"color\") && !node.style[\"color\"]) {\n                    node.style[\"color\"] = node.getAttribute(\"color\");\n                }\n                if (node.hasAttribute(\"size\") && !node.style[\"font-size\"]) {\n                    // FONT size uses non-standard numeric values.\n                    node.style[\"font-size\"] = +node.getAttribute(\"size\") + 10 + \"pt\";\n                }\n            } else if (\n                [\"S\", \"U\"].includes(node.nodeName) &&\n                childNodes(node).length === 1 &&\n                node.firstChild.nodeName === \"FONT\"\n            ) {\n                // S and U tags sometimes contain FONT tags. We prefer the\n                // strike to adopt the style of the text, so we invert them.\n                const fontNode = node.firstChild;\n                node.before(fontNode);\n                node.replaceChildren(...childNodes(fontNode));\n                fontNode.appendChild(node);\n            } else if (\n                node.nodeName === \"IMG\" &&\n                node.getAttribute(\"aria-roledescription\") === \"checkbox\"\n            ) {\n                const checklist = node.closest(\"ul\");\n                const closestLi = node.closest(\"li\");\n                if (checklist) {\n                    checklist.classList.add(\"o_checklist\");\n                    if (node.getAttribute(\"alt\") === \"checked\") {\n                        closestLi.classList.add(\"o_checked\");\n                    }\n                    node.remove();\n                    node = checklist;\n                }\n            }\n            // Remove all illegal attributes and classes from the node, then\n            // clean its children.\n            for (const attribute of [...node.attributes]) {\n                // Keep allowed styles on nodes with allowed tags.\n                // todo: should the whitelist be a resource?\n                if (\n                    CLIPBOARD_WHITELISTS.styledTags.includes(node.nodeName) &&\n                    attribute.name === \"style\"\n                ) {\n                    node.removeAttribute(attribute.name);\n                    if ([\"SPAN\", \"FONT\"].includes(node.tagName)) {\n                        for (const unwrappedNode of unwrapContents(node)) {\n                            this.cleanForPaste(unwrappedNode);\n                        }\n                    }\n                } else if (!this.isWhitelisted(attribute)) {\n                    node.removeAttribute(attribute.name);\n                }\n            }\n            for (const klass of [...node.classList]) {\n                if (!this.isWhitelisted(klass)) {\n                    node.classList.remove(klass);\n                }\n            }\n            for (const child of childNodes(node)) {\n                this.cleanForPaste(child);\n            }\n        }\n    }\n    /**\n     * Return true if the given attribute, class or node is whitelisted for\n     * pasting, false otherwise.\n     *\n     * @private\n     * @param {Attr | string | Node} item\n     * @returns {boolean}\n     */\n    isWhitelisted(item) {\n        if (item.nodeType === Node.ATTRIBUTE_NODE) {\n            return CLIPBOARD_WHITELISTS.attributes.includes(item.name);\n        } else if (typeof item === \"string\") {\n            return CLIPBOARD_WHITELISTS.classes.some((okClass) =>\n                okClass instanceof RegExp ? okClass.test(item) : okClass === item\n            );\n        } else {\n            return isTextNode(item) || item.matches?.(CLIPBOARD_WHITELISTS.nodes.join(\",\"));\n        }\n    }\n    /**\n     * Return true if the given node is blacklisted for pasting, false\n     * otherwise.\n     *\n     * @private\n     * @param {Node} node\n     * @returns {boolean}\n     */\n    isBlacklisted(node) {\n        return (\n            !isTextNode(node) &&\n            node.matches([].concat(...Object.values(CLIPBOARD_BLACKLISTS)).join(\",\"))\n        );\n    }\n\n    /**\n     * @param {DragEvent} ev\n     */\n    onDragStart(ev) {\n        if (ev.target.nodeName === \"IMG\") {\n            this.dragImage = ev.target instanceof HTMLElement && ev.target;\n            ev.dataTransfer.setData(\n                \"application/vnd.odoo.odoo-editor-node\",\n                this.dragImage.outerHTML\n            );\n        }\n    }\n    /**\n     * Handle safe dropping of html into the editor.\n     *\n     * @param {DragEvent} ev\n     */\n    async onDrop(ev) {\n        ev.preventDefault();\n        const selection = this.dependencies.selection.getEditableSelection();\n        if (!isHtmlContentSupported(selection)) {\n            return;\n        }\n        const nodeToSplit =\n            selection.direction === DIRECTIONS.RIGHT ? selection.focusNode : selection.anchorNode;\n        const offsetToSplit =\n            selection.direction === DIRECTIONS.RIGHT\n                ? selection.focusOffset\n                : selection.anchorOffset;\n        if (nodeToSplit.nodeType === Node.TEXT_NODE && !selection.isCollapsed) {\n            const selectionToRestore = this.dependencies.selection.preserveSelection();\n            // Split the text node beforehand to ensure the insertion offset\n            // remains correct after deleting the selection.\n            splitTextNode(nodeToSplit, offsetToSplit, DIRECTIONS.LEFT);\n            selectionToRestore.restore();\n        }\n\n        const dataTransfer = (ev.originalEvent || ev).dataTransfer;\n        const imageNodeHTML = ev.dataTransfer.getData(\"application/vnd.odoo.odoo-editor-node\");\n        const image =\n            imageNodeHTML &&\n            this.dragImage &&\n            imageNodeHTML === this.dragImage.outerHTML &&\n            this.dragImage;\n\n        const fileTransferItems = getImageFiles(dataTransfer);\n        const htmlTransferItem = [...dataTransfer.items].find((item) => item.type === \"text/html\");\n        if (image || fileTransferItems.length || htmlTransferItem) {\n            if (this.document.caretPositionFromPoint) {\n                const range = this.document.caretPositionFromPoint(ev.clientX, ev.clientY);\n                this.dependencies.delete.deleteSelection();\n                this.dependencies.selection.setSelection({\n                    anchorNode: range.offsetNode,\n                    anchorOffset: range.offset,\n                });\n            } else if (this.document.caretRangeFromPoint) {\n                const range = this.document.caretRangeFromPoint(ev.clientX, ev.clientY);\n                this.dependencies.delete.deleteSelection();\n                this.dependencies.selection.setSelection({\n                    anchorNode: range.startContainer,\n                    anchorOffset: range.startOffset,\n                });\n            }\n        }\n        if (image) {\n            const fragment = this.document.createDocumentFragment();\n            fragment.append(image);\n            this.dependencies.dom.insert(fragment);\n            this.dependencies.history.addStep();\n        } else if (fileTransferItems.length) {\n            const html = await this.addImagesFiles(fileTransferItems);\n            this.dependencies.dom.insert(html);\n            this.dependencies.history.addStep();\n        } else if (htmlTransferItem) {\n            htmlTransferItem.getAsString((pastedText) => {\n                this.dependencies.dom.insert(this.prepareClipboardData(pastedText));\n                this.dependencies.history.addStep();\n            });\n        }\n    }\n    // @phoenix @todo: move to image or image paste plugin?\n    /**\n     * Add images inside the editable at the current selection.\n     *\n     * @param {File[]} imageFiles\n     */\n    async addImagesFiles(imageFiles) {\n        const promises = [];\n        for (const imageFile of imageFiles) {\n            const imageNode = this.document.createElement(\"img\");\n            imageNode.classList.add(\"img-fluid\");\n            this.dispatchTo(\"added_image_handlers\", imageNode);\n            imageNode.dataset.fileName = imageFile.name;\n            promises.push(\n                getImageUrl(imageFile).then((url) => {\n                    imageNode.src = url;\n                    return imageNode;\n                })\n            );\n        }\n        const nodes = await Promise.all(promises);\n        const fragment = this.document.createDocumentFragment();\n        fragment.append(...nodes);\n        return fragment;\n    }\n}\n\n/**\n * @param {DataTransfer} dataTransfer\n */\nfunction getImageFiles(dataTransfer) {\n    return [...dataTransfer.items]\n        .filter((item) => item.kind === \"file\" && item.type.includes(\"image/\"))\n        .map((item) => item.getAsFile());\n}\n/**\n * @param {File} file\n */\nfunction getImageUrl(file) {\n    return new Promise((resolve, reject) => {\n        const reader = new FileReader();\n\n        reader.readAsDataURL(file);\n        reader.onloadend = (e) => {\n            if (reader.error) {\n                return reject(reader.error);\n            }\n            resolve(e.target.result);\n        };\n    });\n}\n", "import { isProtected } from \"@html_editor/utils/dom_info\";\nimport { Plugin } from \"../plugin\";\nimport { descendants } from \"../utils/dom_traversal\";\n\nexport class CommentPlugin extends Plugin {\n    static id = \"comment\";\n    /** @type {import(\"plugins\").EditorResources} */\n    resources = {\n        normalize_handlers: this.removeComment.bind(this),\n    };\n\n    removeComment(node) {\n        for (const el of [node, ...descendants(node)]) {\n            if (el.nodeType === Node.COMMENT_NODE && !isProtected(el)) {\n                el.remove();\n            }\n        }\n    }\n}\n", "import { isArtificialVoidElement } from \"@html_editor/core/selection_plugin\";\nimport { Plugin } from \"@html_editor/plugin\";\nimport { selectElements } from \"@html_editor/utils/dom_traversal\";\nimport { withSequence } from \"@html_editor/utils/resource\";\n\n/** @typedef {import(\"@html_editor/editor\").EditorContext} EditorContext */\n/** @typedef {import(\"plugins\").CSSSelector} CSSSelector */\n\n/**\n * @typedef {((el: HTMLElement) => boolean)[]} valid_contenteditable_predicates\n *\n * @typedef {((root: EditorContext[\"editable\"]) => HTMLElement[])[]} content_editable_providers\n * @typedef {((root: EditorContext[\"editable\"]) => HTMLElement[])[]} content_not_editable_providers\n *\n * @typedef {CSSSelector[]} contenteditable_to_remove_selector\n */\n\n/**\n * This plugin is responsible for setting the contenteditable attribute on some\n * elements.\n *\n * The content_editable_providers and content_not_editable_providers resources\n * allow other plugins to easily add editable or non editable elements.\n */\n\nexport class ContentEditablePlugin extends Plugin {\n    static id = \"contentEditablePlugin\";\n    /** @type {import(\"plugins\").EditorResources} */\n    resources = {\n        normalize_handlers: withSequence(5, this.normalize.bind(this)),\n        clean_for_save_handlers: withSequence(Infinity, this.cleanForSave.bind(this)),\n    };\n\n    normalize(root) {\n        const contentNotEditableEls = [];\n        for (const fn of this.getResource(\"content_not_editable_providers\")) {\n            contentNotEditableEls.push(...fn(root));\n        }\n        for (const contentNotEditableEl of contentNotEditableEls) {\n            contentNotEditableEl.setAttribute(\"contenteditable\", \"false\");\n        }\n        const contentEditableEls = [];\n        for (const fn of this.getResource(\"content_editable_providers\")) {\n            contentEditableEls.push(...fn(root));\n        }\n        const filteredContentEditableEls = contentEditableEls.filter((contentEditableEl) =>\n            this.getResource(\"valid_contenteditable_predicates\").every((p) => p(contentEditableEl))\n        );\n        for (const contentEditableEl of filteredContentEditableEls) {\n            if (!contentEditableEl.isContentEditable) {\n                if (\n                    isArtificialVoidElement(contentEditableEl) ||\n                    contentEditableEl.nodeName === \"IMG\"\n                ) {\n                    contentEditableEl.classList.add(\"o_editable_media\");\n                    continue;\n                }\n                if (!contentNotEditableEls.includes(contentEditableEl)) {\n                    contentEditableEl.setAttribute(\"contenteditable\", true);\n                }\n            }\n        }\n    }\n\n    cleanForSave({ root }) {\n        const toRemoveSelector = this.getResource(\"contenteditable_to_remove_selector\").join(\",\");\n        const contenteditableEls = toRemoveSelector\n            ? [...selectElements(root, toRemoveSelector)]\n            : [];\n        for (const contenteditableEl of contenteditableEls) {\n            contenteditableEl.removeAttribute(\"contenteditable\");\n        }\n    }\n}\n", "import { Plugin } from \"../plugin\";\nimport { closestBlock, isBlock } from \"../utils/blocks\";\nimport {\n    isAllowedContent,\n    isButton,\n    isContentEditable,\n    isEmpty,\n    isInPre,\n    isProtected,\n    isShrunkBlock,\n    isTangible,\n    isTextNode,\n    isVisibleTextNode,\n    isWhitespace,\n    isZwnbsp,\n    isZWS,\n    nextLeaf,\n    previousLeaf,\n    isEmptyBlock,\n} from \"../utils/dom_info\";\nimport { getState, isFakeLineBreak, observeMutations, prepareUpdate } from \"../utils/dom_state\";\nimport {\n    childNodes,\n    closestElement,\n    findUpTo,\n    descendants,\n    firstLeaf,\n    getCommonAncestor,\n    lastLeaf,\n    findFurthest,\n} from \"../utils/dom_traversal\";\nimport {\n    DIRECTIONS,\n    childNodeIndex,\n    endPos,\n    leftPos,\n    nodeSize,\n    rightPos,\n    startPos,\n} from \"../utils/position\";\nimport { CTYPES } from \"../utils/content_types\";\nimport { withSequence } from \"@html_editor/utils/resource\";\nimport { compareListTypes } from \"@html_editor/main/list/utils\";\nimport { hasTouch, isBrowserChrome, isMacOS } from \"@web/core/browser/feature_detection\";\nimport { normalizeDeepCursorPosition, normalizeFakeBR } from \"@html_editor/utils/selection\";\n\n/**\n * @typedef {Object} RangeLike\n * @property {Node} startContainer\n * @property {number} startOffset\n * @property {Node} endContainer\n * @property {number} endOffset\n */\n\n/** @typedef {import(\"@html_editor/core/selection_plugin\").EditorSelection} EditorSelection */\n\n/**\n * @typedef {Object} DeleteShared\n * @property { DeletePlugin['delete'] } delete\n * @property { DeletePlugin['deleteRange'] } deleteRange\n * @property { DeletePlugin['deleteSelection'] } deleteSelection\n * @property { DeletePlugin['deleteBackward'] } deleteBackward\n * @property { DeletePlugin['deleteForward'] } deleteForward\n */\n\n/**\n * @typedef {(() => void)[]} before_delete_handlers\n * @typedef {(() => void)[]} delete_handlers\n *\n * @typedef {((range: RangeLike) => void | true)[]} delete_backward_overrides\n * @typedef {((range: RangeLike) => void | true)[]} delete_backward_word_overrides\n * @typedef {((range: RangeLike) => void | true)[]} delete_backward_line_overrides\n * @typedef {((range: RangeLike) => void | true)[]} delete_forward_overrides\n * @typedef {((range: RangeLike) => void | true)[]} delete_forward_word_overrides\n * @typedef {((range: RangeLike) => void | true)[]} delete_forward_line_overrides\n * @typedef {((range: RangeLike) => void | true)[]} delete_range_overrides\n *\n * @typedef {((node: Node) => boolean)[]} functional_empty_node_predicates\n * @typedef {((node: Node) => boolean)[]} is_empty_predicates\n *\n * @typedef {((node: Node) => Node[])[]} removable_descendants_providers\n *\n * @typedef {CSSSelector[]} system_node_selectors\n */\n/**\n * The `root` argument is used by some predicates in which a node is\n * conditionally unremovable (e.g. a table cell is only removable if its\n * ancestor table is also being removed).\n * @typedef {((node: Node, root: HTMLElement) => boolean)[]} unremovable_node_predicates\n */\n\n// @todo @phoenix: move these predicates to different plugins\nexport const unremovableNodePredicates = [\n    (node) => node.classList?.contains(\"oe_unremovable\"),\n    // Monetary field\n    (node) => node.matches?.(\"[data-oe-type='monetary'] > span\"),\n];\n\nexport class DeletePlugin extends Plugin {\n    static dependencies = [\"baseContainer\", \"selection\", \"history\", \"input\", \"userCommand\"];\n    static id = \"delete\";\n    static shared = [\"deleteBackward\", \"deleteForward\", \"deleteRange\", \"deleteSelection\", \"delete\"];\n    /** @type {import(\"plugins\").EditorResources} */\n    resources = {\n        user_commands: [\n            { id: \"deleteBackward\", run: () => this.delete(\"backward\", \"character\") },\n            { id: \"deleteForward\", run: () => this.delete(\"forward\", \"character\") },\n            { id: \"deleteBackwardWord\", run: () => this.delete(\"backward\", \"word\") },\n            { id: \"deleteForwardWord\", run: () => this.delete(\"forward\", \"word\") },\n            { id: \"deleteBackwardLine\", run: () => this.delete(\"backward\", \"line\") },\n            { id: \"deleteForwardLine\", run: () => this.delete(\"forward\", \"line\") },\n        ],\n        shortcuts: [\n            { hotkey: \"backspace\", commandId: \"deleteBackward\" },\n            { hotkey: \"delete\", commandId: \"deleteForward\" },\n            { hotkey: \"control+backspace\", commandId: \"deleteBackwardWord\" },\n            { hotkey: \"control+delete\", commandId: \"deleteForwardWord\" },\n            { hotkey: \"control+shift+backspace\", commandId: \"deleteBackwardLine\" },\n            { hotkey: \"control+shift+delete\", commandId: \"deleteForwardLine\" },\n        ],\n        /** Handlers */\n        beforeinput_handlers: [\n            withSequence(5, this.onBeforeInputInsertText.bind(this)),\n            this.onBeforeInputDelete.bind(this),\n        ],\n        input_handlers: (ev) => this.onAndroidChromeInput?.(ev),\n        selectionchange_handlers: withSequence(5, () => this.onAndroidChromeSelectionChange?.()),\n        /** Overrides */\n        delete_backward_overrides: withSequence(30, this.deleteBackwardUnmergeable.bind(this)),\n        delete_backward_word_overrides: withSequence(20, this.deleteBackwardUnmergeable.bind(this)),\n        delete_backward_line_overrides: this.deleteBackwardUnmergeable.bind(this),\n        delete_forward_overrides: withSequence(20, this.deleteForwardUnmergeable.bind(this)),\n        delete_forward_word_overrides: this.deleteForwardUnmergeable.bind(this),\n        delete_forward_line_overrides: this.deleteForwardUnmergeable.bind(this),\n\n        unremovable_node_predicates: unremovableNodePredicates,\n        invalid_for_base_container_predicates: (node) => this.isUnremovable(node, this.editable),\n    };\n\n    setup() {\n        this.findPreviousPosition = this.makeFindPositionFn(\"backward\");\n        this.findNextPosition = this.makeFindPositionFn(\"forward\");\n        if (isMacOS()) {\n            // Bypass the hotkey service for Alt+Backspace and Cmd+Backspace\n            // on macOS which would otherwise conflict with other shortcuts.\n            this.addDomListener(this.editable, \"keydown\", (event) => {\n                const runCommand = (commandId) => {\n                    this.dependencies.userCommand.getCommand(commandId).run();\n                    event.stopImmediatePropagation();\n                    event.preventDefault();\n                };\n                // Delete word backward: Option + Backspace\n                if (event.altKey && event.key === \"Backspace\") {\n                    return runCommand(\"deleteBackwardWord\");\n                }\n\n                // Delete word forward: Option + Delete\n                if (event.altKey && event.key === \"Delete\") {\n                    return runCommand(\"deleteForwardWord\");\n                }\n\n                // Delete line backward: Command + Backspace\n                if (event.metaKey && event.key === \"Backspace\") {\n                    return runCommand(\"deleteBackwardLine\");\n                }\n\n                // Delete line forward: Command + Delete\n                if (event.metaKey && event.key === \"Delete\") {\n                    return runCommand(\"deleteForwardLine\");\n                }\n            });\n        }\n    }\n\n    /**\n     * @param {EditorSelection} selection\n     * @returns {Range}\n     */\n    getNormalizedRange(selection) {\n        let { startContainer, startOffset, endContainer, endOffset, isCollapsed } = selection;\n        for (const normalizer of [normalizeDeepCursorPosition, normalizeFakeBR]) {\n            [startContainer, startOffset] = normalizer(startContainer, startOffset);\n            [endContainer, endOffset] = isCollapsed\n                ? [startContainer, startOffset]\n                : normalizer(endContainer, endOffset);\n        }\n        const range = this.document.createRange();\n        range.setStart(startContainer, startOffset);\n        range.setEnd(endContainer, endOffset);\n        return range;\n    }\n\n    // --------------------------------------------------------------------------\n    // commands\n    // --------------------------------------------------------------------------\n\n    /**\n     * @param {EditorSelection} [selection]\n     */\n    deleteSelection(selection = this.dependencies.selection.getEditableSelection()) {\n        // @todo @phoenix: handle non-collapsed selection around a ZWS\n        // see collapseIfZWS\n\n        let range = this.getNormalizedRange(selection);\n        if (range.collapsed) {\n            return;\n        }\n        // Delete only if the targeted nodes are all editable or if every\n        // non-editable node's editable ancestor is fully selected. We use the\n        // targeted nodes here to be sure to include a partial text node\n        // selection.\n        const selectedNodes = this.dependencies.selection.getTargetedNodes();\n        const canBeDeleted = (node) =>\n            this.dependencies.selection.isNodeEditable(node) ||\n            selectedNodes.includes(\n                closestElement(node, (node) => this.dependencies.selection.isNodeEditable(node))\n            );\n        if (selectedNodes.some((node) => !canBeDeleted(node))) {\n            return;\n        }\n        range = this.adjustRange(range, [\n            this.expandRangeToIncludeNonEditables,\n            this.includeEndOrStartBlock,\n            this.fullyIncludeLinks,\n        ]);\n\n        if (this.delegateTo(\"delete_range_overrides\", range)) {\n            return;\n        }\n\n        range = this.deleteRange(range);\n        this.setCursorFromRange(range);\n    }\n\n    /**\n     * @param {\"backward\"|\"forward\"} direction\n     * @param {\"character\"|\"word\"|\"line\"} granularity\n     */\n    delete(direction, granularity) {\n        const selection = this.dependencies.selection.getEditableSelection();\n        this.dispatchTo(\"before_delete_handlers\");\n\n        if (!selection.isCollapsed) {\n            this.deleteSelection(selection);\n        } else if (direction === \"backward\") {\n            this.deleteBackward(selection, granularity);\n        } else if (direction === \"forward\") {\n            this.deleteForward(selection, granularity);\n        } else {\n            throw new Error(\"Invalid direction\");\n        }\n        this.dispatchTo(\"delete_handlers\");\n        this.dependencies.history.addStep();\n    }\n\n    // --------------------------------------------------------------------------\n    // Delete backward/forward\n    // --------------------------------------------------------------------------\n\n    /**\n     * @param {EditorSelection} selection\n     * @param {\"character\"|\"word\"|\"line\"} granularity\n     */\n    deleteBackward(selection, granularity) {\n        const { endContainer, endOffset } = this.getNormalizedRange(selection);\n        if (!closestElement(endContainer).isContentEditable) {\n            return;\n        }\n\n        let range = this.getRangeForDelete(endContainer, endOffset, \"backward\", granularity);\n\n        const resourceIds = {\n            character: \"delete_backward_overrides\",\n            word: \"delete_backward_word_overrides\",\n            line: \"delete_backward_line_overrides\",\n        };\n        if (this.delegateTo(resourceIds[granularity], range)) {\n            return;\n        }\n\n        range = this.adjustRange(range, [\n            this.includeEmptyInlineEnd,\n            this.includePreviousZWS,\n            this.includeEndOrStartBlock,\n        ]);\n        range = this.deleteRange(range);\n        this.document.getSelection()?.removeAllRanges();\n        this.setCursorFromRange(range, { collapseToEnd: true });\n    }\n\n    /**\n     * @param {EditorSelection} selection\n     * @param {\"character\"|\"word\"|\"line\"} granularity\n     */\n    deleteForward(selection, granularity) {\n        const { startContainer, startOffset } = this.getNormalizedRange(selection);\n        if (!closestElement(startContainer).isContentEditable) {\n            return;\n        }\n\n        let range = this.getRangeForDelete(startContainer, startOffset, \"forward\", granularity);\n\n        const resourceIds = {\n            character: \"delete_forward_overrides\",\n            word: \"delete_forward_word_overrides\",\n            line: \"delete_forward_line_overrides\",\n        };\n        if (this.delegateTo(resourceIds[granularity], range)) {\n            return;\n        }\n\n        range = this.adjustRange(range, [\n            this.includeEmptyInlineStart,\n            this.includeNextZWS,\n            this.includeEndOrStartBlock,\n        ]);\n        range = this.deleteRange(range);\n        this.setCursorFromRange(range);\n    }\n\n    getRangeForDelete(node, offset, direction, granularity) {\n        let destContainer, destOffset;\n        if (granularity === \"word\") {\n            // In some browsers such as Firefox or Safari, if the cursor\n            // is at the start of a block (when direction is \"backward\") or\n            // at the end of a block (when direction is \"forward\"),\n            // Selection.modify(\"extend\", direction, \"word\") ends up\n            // selecting the previous or next adjacent text node, respectively.\n            // To handle such cases, the granularity should be \"character\".\n            const blockEl = closestBlock(node);\n            if (\n                (direction === \"backward\" &&\n                    this.isCursorAtStartOfElement(blockEl, node, offset)) ||\n                (direction === \"forward\" && this.isCursorAtEndOfElement(blockEl, node, offset))\n            ) {\n                granularity = \"character\";\n            }\n        }\n        switch (granularity) {\n            case \"character\":\n                [destContainer, destOffset] = this.findAdjacentPosition(node, offset, direction);\n                break;\n            case \"word\":\n                ({ focusNode: destContainer, focusOffset: destOffset } =\n                    this.dependencies.selection.modifySelection(\"extend\", direction, \"word\"));\n                break;\n            case \"line\":\n                [destContainer, destOffset] = this.findLineBoundary(node, offset, direction);\n                break;\n            default:\n                throw new Error(\"Invalid granularity\");\n        }\n\n        if (!destContainer) {\n            [destContainer, destOffset] = [node, offset];\n        }\n        const [startContainer, startOffset, endContainer, endOffset] =\n            direction === \"forward\"\n                ? [node, offset, destContainer, destOffset]\n                : [destContainer, destOffset, node, offset];\n\n        return { startContainer, startOffset, endContainer, endOffset };\n    }\n\n    // --------------------------------------------------------------------------\n    // Delete range\n    // --------------------------------------------------------------------------\n\n    /*\n    Inline:\n        Empty inlines get filled, no joining.\n        <b>[abc]</b> -> <b>[]ZWS</b>\n        <b>[abc</b> <b>d]ef</b> -> <b>[]ZWS</b> <b>ef</b>\n        <b>[abc</b> <b>def]</b> -> <b>[]ZWS</b> <b>ZWS</b>\n\n    Block:\n        Shrunk blocks get filled.\n        <p>[abc]</p> -> <p>[]<br></p>\n\n        End block's content is appended to start block on join.\n        <h1>a[bc</h1> <p>de]f</p> -> <h1>a[]f</h1>\n        <h1>[abc</h1> <p>def]</p> -> <h1>[]<br></h1>\n\n        To make left block disappear instead, use this range:\n        [<h1>abc</h1> <p>de]f</p> -> []<p>f</p> (which can be normalized later, see setCursorFromRange)\n\n    Block + Inline:\n        Inline content after block is appended to block on join.\n        <p>a[bc</p> d]ef -> <p>a[]ef</p>\n\n    Inline + Block:\n        Block content is unwrapped on join.\n        ab[c <p>de]f</p> -> ab[]f\n        ab[c <p>de]f</p> ghi -> ab[]f<br>ghi\n\n    */\n\n    /**\n     * Removes (removable) nodes and merges block with block/inline when\n     * applicable (and mergeable).\n     * Returns the updated range, which is collapsed to start if the original\n     * range could be completely deleted and merged.\n     *\n     * @param {RangeLike} range\n     * @returns {RangeLike}\n     */\n    deleteRange(range) {\n        // Do nothing if the range is collapsed.\n        if (range.startContainer === range.endContainer && range.startOffset === range.endOffset) {\n            return range;\n        }\n        // Split text nodes in order to have elements as start/end containers.\n        range = this.splitTextNodes(range);\n\n        const { startContainer, startOffset, endContainer, endOffset } = range;\n        const restoreSpaces = prepareUpdate(startContainer, startOffset, endContainer, endOffset);\n\n        let restoreFakeBRs;\n        ({ restoreFakeBRs, range } = this.removeFakeBRs(range));\n\n        // Remove nodes.\n        let allNodesRemoved;\n        ({ allNodesRemoved, range } = this.removeNodes(range));\n\n        this.fillEmptyInlines(range);\n\n        // Join fragments.\n        const originalCommonAncestor = range.commonAncestorContainer;\n        if (allNodesRemoved) {\n            range = this.joinFragments(range);\n        }\n\n        restoreFakeBRs();\n        this.fillShrunkBlocks(originalCommonAncestor);\n        restoreSpaces();\n\n        return range;\n    }\n\n    splitTextNodes({ startContainer, startOffset, endContainer, endOffset }) {\n        // Splits text nodes only if necessary.\n        const split = (textNode, offset) => {\n            let didSplit = false;\n            if (offset === 0) {\n                offset = childNodeIndex(textNode);\n            } else if (offset === nodeSize(textNode)) {\n                offset = childNodeIndex(textNode) + 1;\n            } else {\n                textNode.splitText(offset);\n                didSplit = true;\n                offset = childNodeIndex(textNode) + 1;\n            }\n            return [textNode.parentElement, offset, didSplit];\n        };\n\n        if (endContainer.nodeType === Node.TEXT_NODE) {\n            [endContainer, endOffset] = split(endContainer, endOffset);\n        }\n        if (startContainer.nodeType === Node.TEXT_NODE) {\n            let didSplit;\n            [startContainer, startOffset, didSplit] = split(startContainer, startOffset);\n            if (startContainer === endContainer && didSplit) {\n                endOffset += 1;\n            }\n        }\n\n        return {\n            startContainer,\n            startOffset,\n            endContainer,\n            endOffset,\n            commonAncestorContainer: getCommonAncestor(\n                [startContainer, endContainer],\n                this.editable\n            ),\n        };\n    }\n\n    // Removes fake line breaks, so that each BR left is an actual line break.\n    // Returns the updated range and a function to later restore the fake BRs.\n    removeFakeBRs(range) {\n        let { startContainer, startOffset, endContainer, endOffset, commonAncestorContainer } =\n            range;\n        const visitedNodes = new Set();\n        const removeBRs = (container, offset) => {\n            let node = container;\n            while (node !== commonAncestorContainer) {\n                const lastBR = childNodes(node).findLast((child) => child.nodeName === \"BR\");\n                if (lastBR && isFakeLineBreak(lastBR)) {\n                    if (lastBR === container) {\n                        [container, offset] = leftPos(lastBR);\n                    } else if (node === container && offset > childNodeIndex(lastBR)) {\n                        offset -= 1;\n                    }\n                    lastBR.remove();\n                }\n                visitedNodes.add(node);\n                node = node.parentNode;\n            }\n            return [container, offset];\n        };\n        [startContainer, startOffset] = removeBRs(startContainer, startOffset);\n        [endContainer, endOffset] = removeBRs(endContainer, endOffset);\n        range = { startContainer, startOffset, endContainer, endOffset, commonAncestorContainer };\n\n        const restoreFakeBRs = () => {\n            for (const node of visitedNodes) {\n                if (!node.isConnected) {\n                    continue;\n                }\n                const lastBR = childNodes(node).findLast((child) => child.nodeName === \"BR\");\n                if (lastBR && isFakeLineBreak(lastBR)) {\n                    lastBR.after(this.document.createElement(\"br\"));\n                }\n                // Shrunk blocks are restored by `fillShrunkBlocks`.\n            }\n        };\n\n        return { restoreFakeBRs, range };\n    }\n\n    fillEmptyInlines(range) {\n        const nodes = [range.startContainer];\n        if (range.endContainer !== range.startContainer) {\n            nodes.push(range.endContainer);\n        }\n        for (const node of nodes) {\n            // @todo: mind Icons?\n            // Probably need to get deepest position's element\n            // @todo: update fillEmpty\n            if (!isBlock(node) && !isTangible(node) && !isZWS(node) && !isZwnbsp(node)) {\n                node.appendChild(this.document.createTextNode(\"\\u200B\"));\n                node.setAttribute(\"data-oe-zws-empty-inline\", \"\");\n            }\n        }\n    }\n\n    fillShrunkBlocks(commonAncestor) {\n        const fillBlock = (block) => {\n            if (\n                block.matches(\"div[contenteditable='true']\") &&\n                !block.parentElement.isContentEditable\n            ) {\n                // @todo: not sure we want this when allowInlineAtRoot is true\n                const baseContainer = this.dependencies.baseContainer.createBaseContainer();\n                baseContainer.appendChild(this.document.createElement(\"br\"));\n                block.appendChild(baseContainer);\n            } else {\n                block.appendChild(this.document.createElement(\"br\"));\n            }\n        };\n        // @todo: this ends up filling shrunk blocks outside the affected range.\n        // Ideally, it should only affect the block within the boundaries of the\n        // original range.\n        for (const node of descendants(commonAncestor).reverse()) {\n            if (isBlock(node) && isShrunkBlock(node)) {\n                fillBlock(node);\n            }\n        }\n        const containingBlock = closestBlock(commonAncestor);\n        if (isShrunkBlock(containingBlock)) {\n            fillBlock(containingBlock);\n        }\n    }\n\n    // --------------------------------------------------------------------------\n    // Remove nodes\n    // --------------------------------------------------------------------------\n\n    removeNodes(range) {\n        const { startContainer, startOffset, endContainer, commonAncestorContainer } = range;\n        let { endOffset } = range;\n        const nodesToRemove = [];\n\n        // Pick child nodes to the right for later removal, propagate until\n        // commonAncestorContainer (non-inclusive)\n        let node = startContainer;\n        let startRemoveIndex = startOffset;\n        while (node !== commonAncestorContainer) {\n            for (let i = startRemoveIndex; i < node.childNodes.length; i++) {\n                nodesToRemove.push(node.childNodes[i]);\n            }\n            startRemoveIndex = childNodeIndex(node) + 1;\n            node = node.parentElement;\n        }\n\n        // Pick child nodes to the left for later removal, propagate until\n        // commonAncestorContainer (non-inclusive)\n        node = endContainer;\n        let endRemoveIndex = endOffset;\n        while (node !== commonAncestorContainer) {\n            for (let i = 0; i < endRemoveIndex; i++) {\n                nodesToRemove.push(node.childNodes[i]);\n            }\n            endRemoveIndex = childNodeIndex(node);\n            node = node.parentElement;\n        }\n\n        // Pick commonAncestorContainer's direct children for removal\n        for (let i = startRemoveIndex; i < endRemoveIndex; i++) {\n            nodesToRemove.push(commonAncestorContainer.childNodes[i]);\n        }\n\n        // Remove nodes\n        let allNodesRemoved = true;\n        for (const node of nodesToRemove) {\n            const parent = node.parentNode;\n            const didRemove = this.removeNode(node);\n            allNodesRemoved &&= didRemove;\n            if (didRemove && endContainer === parent) {\n                endOffset -= 1;\n            }\n        }\n\n        const endContainerList = closestElement(endContainer, \"UL, OL\");\n        if (\n            [\"OL\", \"UL\"].includes(startContainer.nodeName) &&\n            endContainerList &&\n            !compareListTypes(startContainer, endContainerList)\n        ) {\n            const newRange = this.document.createRange();\n            newRange.setStart(range.endContainer, endOffset);\n            return { allNodesRemoved, range: newRange };\n        }\n        return { allNodesRemoved, range: { ...range, endOffset } };\n    }\n\n    // The root argument is used by some predicates in which a node is\n    // conditionally unremovable (e.g. a table cell is only removable if its\n    // ancestor table is also being removed).\n    isUnremovable(node, root = undefined) {\n        return this.getResource(\"unremovable_node_predicates\").some((p) => p(node, root));\n    }\n\n    // Returns true if the entire subtree rooted at node was removed.\n    // Unremovable nodes take the place of removable ancestors.\n    removeNode(node) {\n        const root = node;\n        const remove = (node) => {\n            let customHandling = false;\n            let customIsUnremovable;\n            for (const cb of this.getResource(\"removable_descendants_providers\")) {\n                const descendantsToRemove = cb(node);\n                if (descendantsToRemove) {\n                    for (const descendant of descendantsToRemove) {\n                        remove(descendant);\n                    }\n                    customHandling = true;\n                    customIsUnremovable = this.isUnremovable(node, root);\n                    if (!customIsUnremovable) {\n                        // TODO ABD: test protected + unremovable\n                        node.remove();\n                    }\n                }\n            }\n            if (customHandling) {\n                return !customIsUnremovable;\n            }\n            for (const child of [...node.childNodes]) {\n                remove(child);\n            }\n            if (this.isUnremovable(node, root)) {\n                return false;\n            }\n            if (node.hasChildNodes()) {\n                node.before(...node.childNodes);\n                node.remove();\n                return false;\n            }\n            node.remove();\n            return true;\n        };\n        return remove(node);\n    }\n\n    // --------------------------------------------------------------------------\n    // Join\n    // --------------------------------------------------------------------------\n\n    // Joins both ends of the range if possible: block + block/inline.\n    // If joined, the range is collapsed to start.\n    // Returns the updated range.\n    joinFragments(range) {\n        const joinableLeft = this.getJoinableFragment(range, \"start\");\n        const joinableRight = this.getJoinableFragment(range, \"end\");\n        const join = this.getJoinOperation(joinableLeft.type, joinableRight.type);\n\n        const didJoin = join(joinableLeft.node, joinableRight.node, range.commonAncestorContainer);\n\n        return didJoin ? this.collapseRange(range) : range;\n    }\n\n    /**\n     * Retrieves the joinable fragment based on the given range and side.\n     *\n     * @param {Object} range - range-like object.\n     * @param {\"start\"|\"end\"} side\n     * @returns {Object} - { node: Node|null, type: \"block\"|\"inline\"|\"null\" }\n     */\n    getJoinableFragment(range, side) {\n        const commonAncestor = range.commonAncestorContainer;\n        const container = side === \"start\" ? range.startContainer : range.endContainer;\n        const offset = side === \"start\" ? range.startOffset : range.endOffset;\n\n        if (container === range.commonAncestorContainer) {\n            // This means a direct child of the commonAncestor was removed.\n            // The joinable in this case is its sibling (previous for the start\n            // side, next for the end side), but only if inline.\n            const sibling = childNodes(commonAncestor)[side === \"start\" ? offset - 1 : offset];\n            if (\n                sibling &&\n                !isBlock(sibling) &&\n                !(sibling.nodeType === Node.TEXT_NODE && !isVisibleTextNode(sibling))\n            ) {\n                return { node: sibling, type: \"inline\" };\n            }\n            // No fragment to join.\n            return { node: null, type: \"null\" };\n        }\n        // Starting from `container`, find the closest block up to\n        // (not-inclusive) the common ancestor. If not found, keep the common\n        // ancestor's child inline element.\n        let last;\n        let element = container;\n        while (element !== commonAncestor) {\n            if (isBlock(element)) {\n                return { node: element, type: \"block\" };\n            }\n            last = element;\n            element = element.parentElement;\n        }\n        return { node: last, type: \"inline\" };\n    }\n\n    getJoinOperation(leftType, rightType) {\n        return (\n            {\n                \"block + block\": this.joinBlocks,\n                \"block + inline\": this.joinInlineIntoBlock,\n                \"inline + block\": this.joinBlockIntoInline,\n            }[leftType + \" + \" + rightType] || (() => true)\n        ).bind(this);\n        // \"inline + inline\": Nothing to do, consider it joined.\n        // Same any combination involving type \"null\" (no joinable element).\n    }\n\n    /**\n     * An unsplittable element is also unmergeable and vice-versa (as split and\n     * merge are reverse operations from one another).\n     */\n    isUnmergeable(node) {\n        return this.getResource(\"unsplittable_node_predicates\").some((p) => p(node));\n    }\n\n    joinBlocks(left, right, commonAncestor) {\n        // Check if both blocks are mergeable.\n        const canMerge = (n) => !findUpTo(n, commonAncestor, this.isUnmergeable.bind(this));\n        if (!canMerge(left) || !canMerge(right)) {\n            return false;\n        }\n\n        // Check if left block allows right block's content.\n        const rightChildNodes = childNodes(right);\n        if (!isAllowedContent(left, rightChildNodes)) {\n            return false;\n        }\n\n        left.append(...rightChildNodes);\n        let toRemove = right;\n        let parent = right.parentElement;\n        // Propagate until commonAncestor, removing empty blocks\n        while (parent !== commonAncestor && parent.childNodes.length === 1) {\n            toRemove = parent;\n            parent = parent.parentElement;\n        }\n        toRemove.remove();\n        return true;\n    }\n\n    joinInlineIntoBlock(leftBlock, rightInline, commonAncestor) {\n        if (findUpTo(leftBlock, commonAncestor, (node) => this.isUnmergeable(node))) {\n            // Left block is unmergeable.\n            return false;\n        }\n\n        // @todo: avoid appending a BR as last child of the block\n        while (rightInline && !isBlock(rightInline)) {\n            const toAppend = rightInline;\n            rightInline = rightInline.nextSibling;\n            leftBlock.append(toAppend);\n        }\n        return true;\n    }\n\n    joinBlockIntoInline(leftInline, rightBlock, commonAncestor) {\n        if (findUpTo(rightBlock, commonAncestor, (node) => this.isUnmergeable(node))) {\n            // Right block is unmergeable.\n            return false;\n        }\n\n        leftInline.after(...childNodes(rightBlock));\n        let toRemove = rightBlock;\n        let parent = rightBlock.parentElement;\n        // Propagate until commonAncestor, removing empty blocks\n        while (parent !== commonAncestor && parent.childNodes.length === 1) {\n            toRemove = parent;\n            parent = parent.parentElement;\n        }\n        // Restore line break between removed block and inline content after it.\n        if (parent === commonAncestor) {\n            const rightSibling = toRemove.nextSibling;\n            if (rightSibling && !isBlock(rightSibling)) {\n                rightSibling.before(this.document.createElement(\"br\"));\n            }\n        }\n        toRemove.remove();\n        return true;\n    }\n\n    // --------------------------------------------------------------------------\n    // Adjust range\n    // --------------------------------------------------------------------------\n\n    /**\n     * @param {RangeLike}\n     * @param {((range: Range) => Range)[]} callbacks\n     * @returns {RangeLike}\n     */\n    adjustRange({ startContainer, startOffset, endContainer, endOffset }, callbacks) {\n        let range = this.document.createRange();\n        range.setStart(startContainer, startOffset);\n        range.setEnd(endContainer, endOffset);\n\n        for (const callback of callbacks) {\n            range = callback.call(this, range);\n        }\n\n        ({ startContainer, startOffset, endOffset, endContainer } = range);\n        return { startContainer, startOffset, endOffset, endContainer };\n    }\n\n    /**\n     * <h1>[abc</h1><p>d]ef</p> -> [<h1>abc</h1><p>d]ef</p>\n     *\n     * @param {HTMLElement} block\n     * @param {Range} range\n     * @returns {Range}\n     */\n    includeBlockStart(block, range) {\n        const { startContainer, startOffset, commonAncestorContainer } = range;\n        if (\n            block === commonAncestorContainer ||\n            !this.isCursorAtStartOfElement(block, startContainer, startOffset)\n        ) {\n            return range;\n        }\n        range.setStartBefore(block);\n        return this.includeBlockStart(block.parentNode, range);\n    }\n\n    /**\n     * <p>ab[c</p><div>def]</div> ->  <p>ab[c</p><div>def</div>]\n     *\n     * @param {HTMLElement} block\n     * @param {Range} range\n     * @returns {Range}\n     */\n    includeBlockEnd(block, range) {\n        const { startContainer, endContainer, endOffset, commonAncestorContainer } = range;\n        const startList = closestElement(startContainer, \"UL, OL\");\n        const endList = closestElement(endContainer, \"UL, OL\");\n        if (\n            block === commonAncestorContainer ||\n            !this.isCursorAtEndOfElement(block, endContainer, endOffset) ||\n            (startList &&\n                endList &&\n                !compareListTypes(startList, endList) &&\n                !startList.contains(endList))\n        ) {\n            return range;\n        }\n        range.setEndAfter(block);\n        return this.includeBlockEnd(block.parentNode, range);\n    }\n\n    /**\n     * If range spans two blocks, try to fully include the right (end) one OR\n     * the left (start) one (but not both).\n     *\n     * E.g.:\n     * Fully includes the right block:\n     * <p>ab[c</p><div>def]</div> ->  <p>ab[c</p><div>def</div>]\n     * <p>[abc</p><div>def]</div> ->  <p>[abc</p><div>def</div>]\n     *\n     * Fully includes the left block:\n     * <h1>[abc</h1><p>d]ef</p> -> [<h1>abc</h1><p>d]ef</p>\n     *\n     * @param {Range} range\n     * @returns {Range}\n     */\n    includeEndOrStartBlock(range) {\n        const { startContainer, endContainer, commonAncestorContainer } = range;\n        const startBlock = findUpTo(startContainer, commonAncestorContainer, isBlock);\n        const endBlock = findUpTo(endContainer, commonAncestorContainer, isBlock);\n        if (!startBlock || !endBlock) {\n            return range;\n        }\n        range = this.includeBlockEnd(endBlock, range);\n        // Only include start block if end block could not be included.\n        if (range.endContainer === endContainer) {\n            range = this.includeBlockStart(startBlock, range);\n        }\n        return range;\n    }\n\n    /**\n     * Fully select link if:\n     * - range spans content inside and outside the link AND\n     * - all of its content is selected.\n     *\n     * <a>[abc</a>d]ef -> [<a>abc</a>d]ef\n     * ab[c<a>def]</a> ->  ab[c<a>def</a>]\n     * But:\n     * <a>[abc]</a> -> <a>[abc]</a> (remains unchanged)\n     *\n     * @param {Range} range\n     * @returns {Range}\n     */\n    fullyIncludeLinks(range) {\n        const { startContainer, startOffset, endContainer, endOffset, commonAncestorContainer } =\n            range;\n        const [startLink, endLink] = [startContainer, endContainer].map((container) =>\n            findUpTo(container, commonAncestorContainer, (node) => node.nodeName === \"A\")\n        );\n        if (startLink && this.isCursorAtStartOfElement(startLink, startContainer, startOffset)) {\n            range.setStartBefore(startLink);\n        }\n        if (endLink && this.isCursorAtEndOfElement(endLink, endContainer, endOffset)) {\n            range.setEndAfter(endLink);\n        }\n        return range;\n    }\n\n    /**\n     * @param {Range} range\n     * @returns {Range}\n     */\n    includeEmptyInlineStart(range) {\n        const element = closestElement(range.startContainer);\n        if (this.isEmptyInline(element)) {\n            range.setStartBefore(element);\n        }\n        return range;\n    }\n\n    /**\n     * @param {Range} range\n     * @returns {Range}\n     */\n    includeEmptyInlineEnd(range) {\n        const element = closestElement(range.endContainer);\n        if (this.isEmptyInline(element)) {\n            range.setEndAfter(element);\n        }\n        return range;\n    }\n\n    // @todo @phoenix This is here because of the second test case in\n    // delete/forward/selection collapsed/basic/should ignore ZWS, and its\n    // importance is questionable.\n    /**\n     * @param {Range} range\n     * @returns {Range}\n     */\n    includeNextZWS(range) {\n        const { endContainer, endOffset } = range;\n        if (isTextNode(endContainer) && endContainer.textContent[endOffset] === \"\\u200B\") {\n            range.setEnd(endContainer, endOffset + 1);\n        }\n        return range;\n    }\n\n    /**\n     * @param {Range} range\n     * @returns {Range}\n     */\n    includePreviousZWS(range) {\n        const { startContainer, startOffset } = range;\n        if (\n            isTextNode(startContainer) &&\n            startContainer.textContent[startOffset - 1] === \"\\u200B\"\n        ) {\n            range.setStart(startContainer, startOffset - 1);\n        }\n        return range;\n    }\n\n    /**\n     * Expand the range to fully include all contentEditable=False elements.\n     * This scenario happens when the range has one end inside a non-editable\n     * element and the other end outside of it.\n     *\n     * @param {Range} range\n     * @returns {Range}\n     */\n    expandRangeToIncludeNonEditables(range) {\n        const {\n            startContainer,\n            startOffset,\n            endContainer,\n            endOffset,\n            commonAncestorContainer: commonAncestor,\n        } = range;\n        const isNonEditable = (node) => !isContentEditable(node);\n        const startUneditable =\n            startOffset === 0 &&\n            !previousLeaf(startContainer, closestBlock(startContainer)) &&\n            findFurthest(startContainer, commonAncestor, isNonEditable);\n        if (startUneditable) {\n            range.setStartBefore(startUneditable);\n        }\n        const endUneditable =\n            endOffset === nodeSize(endContainer) &&\n            !nextLeaf(endContainer, closestBlock(endContainer)) &&\n            findFurthest(endContainer, commonAncestor, isNonEditable);\n        if (endUneditable) {\n            range.setEndAfter(endUneditable);\n        }\n        return range;\n    }\n\n    // --------------------------------------------------------------------------\n    // Find previous/next position\n    // --------------------------------------------------------------------------\n\n    /**\n     * Returns the next/previous position for deletion.\n     *\n     * @param {Node} node\n     * @param {number} offset\n     * @param {\"forward\"|\"backward\"} direction\n     * @returns {[Node|null, Number|null]}\n     */\n    findAdjacentPosition(node, offset, direction) {\n        return direction === \"forward\"\n            ? this.findNextPosition(node, offset)\n            : this.findPreviousPosition(node, offset);\n    }\n\n    /**\n     *  Returns a function to find the adjacent position in the given direction.\n     *\n     * @param {\"forward\"|\"backward\"} direction\n     */\n    makeFindPositionFn(direction) {\n        const isDirectionForward = direction === \"forward\";\n\n        // Define helper functions based on the direction.\n        // Text node helpers.\n        const findVisibleChar = (\n            isDirectionForward ? this.findNextVisibleChar : this.findPreviousVisibleChar\n        ).bind(this);\n        const charLeftPos = (index, char) => index;\n        const charRightPos = (index, char) => index + char.length;\n        const indexBeforeChar = isDirectionForward ? charLeftPos : charRightPos;\n        const indexAfterChar = isDirectionForward ? charRightPos : charLeftPos;\n        const textEdgePos = isDirectionForward ? startPos : endPos;\n        // Leaf helpers.\n        const adjacentLeaf = (isDirectionForward ? this.nextLeaf : this.previousLeaf).bind(this);\n        const adjacentLeafFromPos = (\n            isDirectionForward ? this.nextLeafFromPos : this.previousLeafFromPos\n        ).bind(this);\n        const beforePos = isDirectionForward ? leftPos : rightPos;\n        const afterPos = isDirectionForward ? rightPos : leftPos;\n\n        /**\n         * Returns the next/previous position for deletion.\n         *\n         * \"Before\" and \"after\" have different meanings depending on the\n         * direction: before and after mean, respectively, previous and next in\n         * DOM order when direction is \"forward\", and the other way around when\n         * direction is \"backward\".\n         *\n         * @param {Node} node\n         * @param {number} offset\n         * @returns {[Node|null, Number|null]}\n         */\n        return function findPosition(node, offset) {\n            if (node.nodeType === Node.TEXT_NODE) {\n                const [char, index] = findVisibleChar(node, offset);\n                if (char) {\n                    return [node, indexAfterChar(index, char)];\n                }\n            }\n\n            // Define context: search is restricted to the closest editable root.\n            const isEditableRoot = (n) => n.isContentEditable && !n.parentNode.isContentEditable;\n            const editableRoot = findUpTo(node, this.editable.parentNode, isEditableRoot);\n\n            let blockSwitch;\n            const nodeClosestBlock = closestBlock(node);\n            let leaf = adjacentLeafFromPos(node, offset, editableRoot);\n            while (leaf) {\n                const leafClosestBlock = closestBlock(leaf);\n                blockSwitch ||= leafClosestBlock !== nodeClosestBlock;\n\n                if (this.shouldSkip(leaf, blockSwitch)) {\n                    leaf = adjacentLeaf(leaf, editableRoot);\n                    continue;\n                }\n\n                if (\n                    leaf.nodeType === Node.TEXT_NODE &&\n                    !(blockSwitch && isEmptyBlock(leafClosestBlock))\n                ) {\n                    const [char, index] = findVisibleChar(...textEdgePos(leaf));\n                    if (char) {\n                        const idx = (blockSwitch ? indexBeforeChar : indexAfterChar)(index, char);\n                        return [leaf, idx];\n                    }\n                } else if (!leaf.isContentEditable && isBlock(leaf)) {\n                    // E.g. Desired range for deleteForward:\n                    // <p>abc[</p><div contenteditable=\"false\">def</div>]<p>ghi</p>\n                    return afterPos(leaf);\n                } else {\n                    return blockSwitch ? beforePos(leaf) : afterPos(leaf);\n                }\n                leaf = adjacentLeaf(leaf, editableRoot);\n            }\n            return [null, null];\n        };\n    }\n\n    findLineBoundary(container, offset, direction) {\n        const adjacentLeaf = direction === \"forward\" ? nextLeaf : previousLeaf;\n        const edgeIndex = (node) => (direction === \"forward\" ? nodeSize(node) : 0);\n        const block = closestBlock(container);\n        let last = container;\n        let node = adjacentLeaf(container, this.editable);\n        // look for a BR or a block start\n        while (node && node.nodeName !== \"BR\" && closestBlock(node) === block) {\n            last = node;\n            node = adjacentLeaf(node, this.editable);\n        }\n        if (last === container && offset === edgeIndex(container)) {\n            // Cursor is already next to the line break, go to following position.\n            return this.findAdjacentPosition(container, offset, direction);\n        }\n        return direction === \"forward\" ? rightPos(last) : leftPos(last);\n    }\n\n    // @todo @phoenix: there are not enough tests for visibility of characters\n    // (invisible whitespace, separate nodes, etc.)\n    isVisibleChar(char, textNode, offset) {\n        // Protected nodes are always \"visible\" for the editor\n        if (isProtected(textNode)) {\n            // TODO ABD: add test\n            return true;\n        }\n        const isZwnbspLinkPad = (node) =>\n            isButton(node.previousSibling) || isButton(node.nextSibling);\n        if (isZwnbsp(textNode) && isZwnbspLinkPad(textNode)) {\n            return true;\n        }\n        // ZWS and ZWNBSP are invisible.\n        if ([\"\\u200B\", \"\\uFEFF\"].includes(char)) {\n            return false;\n        }\n        if (!isWhitespace(char) || isInPre(textNode)) {\n            return true;\n        }\n\n        // Assess visibility of whitespace.\n        // Whitespace is visible if it's immediately preceded by content, and\n        // followed by content before a BR or block start/end.\n\n        // If not preceded by content, it is invisible.\n        if (offset) {\n            return !isWhitespace(textNode.textContent[offset - char.length]);\n        } else if (!(getState(...leftPos(textNode), DIRECTIONS.LEFT).cType & CTYPES.CONTENT)) {\n            return false;\n        }\n\n        // Space is only visible if it's followed by content (with an optional\n        // sequence of invisible spaces in between), before a BR or block\n        // end/start.\n        const charsToTheRight = textNode.textContent.slice(offset + char.length);\n        for (char of charsToTheRight) {\n            if (!isWhitespace(char)) {\n                return true;\n            }\n        }\n        // No content found in text node, look to the right of it\n        if (getState(...rightPos(textNode), DIRECTIONS.RIGHT).cType & CTYPES.CONTENT) {\n            return true;\n        }\n\n        return false;\n    }\n\n    shouldSkip(leaf, blockSwitch) {\n        // A system node is a node that should be ignored by the editor. In\n        // other words, if the editor had a VDOM, it would be absent from it.\n        const systemNodeSelectors = this.getResource(\"system_node_selectors\").join(\",\");\n        if (systemNodeSelectors && closestElement(leaf, systemNodeSelectors)) {\n            return true;\n        }\n        if (leaf.nodeType === Node.TEXT_NODE) {\n            return false;\n        }\n        // @todo Maybe skip anything that is not an element (e.g. comment nodes)\n        if (blockSwitch) {\n            return false;\n        }\n        if (leaf.nodeName === \"BR\" && isFakeLineBreak(leaf)) {\n            return true;\n        }\n        if (\n            this.getResource(\"functional_empty_node_predicates\").some((predicate) =>\n                predicate(leaf)\n            )\n        ) {\n            return false;\n        }\n        if (isEmpty(leaf) || isZWS(leaf)) {\n            return true;\n        }\n        return false;\n    }\n\n    findPreviousVisibleChar(textNode, index) {\n        // @todo @phoenix: write tests for chars with size > 1 (emoji, etc.)\n        // Use the string iterator to handle surrogate pairs.\n        const chars = [...textNode.textContent.slice(0, index)];\n        let char = chars.pop();\n        while (char) {\n            index -= char.length;\n            if (this.isVisibleChar(char, textNode, index)) {\n                return [char, index];\n            }\n            char = chars.pop();\n        }\n        return [null, null];\n    }\n\n    findNextVisibleChar(textNode, index) {\n        // Use the string iterator to handle surrogate pairs.\n        for (const char of textNode.textContent.slice(index)) {\n            if (this.isVisibleChar(char, textNode, index)) {\n                return [char, index];\n            }\n            index += char.length;\n        }\n        return [null, null];\n    }\n\n    // If leaf is part of a contenteditable=false tree, consider its root as the\n    // leaf instead.\n    adjustedLeaf(leaf, refEditableRoot) {\n        const isNonEditable = (node) => !isContentEditable(node);\n        const nonEditableRoot = leaf && findFurthest(leaf, refEditableRoot, isNonEditable);\n        return nonEditableRoot || leaf;\n    }\n\n    previousLeaf(node, editableRoot) {\n        return this.adjustedLeaf(previousLeaf(node, editableRoot), editableRoot);\n    }\n\n    nextLeaf(node, editableRoot) {\n        return this.adjustedLeaf(nextLeaf(node, editableRoot), editableRoot);\n    }\n\n    previousLeafFromPos(node, offset, editableRoot) {\n        const leaf =\n            node.hasChildNodes() && offset > 0\n                ? lastLeaf(node.childNodes[offset - 1])\n                : previousLeaf(node, editableRoot);\n        return this.adjustedLeaf(leaf, editableRoot);\n    }\n\n    nextLeafFromPos(node, offset, editableRoot) {\n        const leaf =\n            node.hasChildNodes() && offset < nodeSize(node)\n                ? firstLeaf(node.childNodes[offset])\n                : nextLeaf(node, editableRoot);\n        return this.adjustedLeaf(leaf, editableRoot);\n    }\n\n    // --------------------------------------------------------------------------\n    // Event handlers\n    // --------------------------------------------------------------------------\n\n    onBeforeInputDelete(ev) {\n        const handledInputTypes = {\n            deleteContentBackward: [\"backward\", \"character\"],\n            deleteContentForward: [\"forward\", \"character\"],\n            deleteWordBackward: [\"backward\", \"word\"],\n            deleteWordForward: [\"forward\", \"word\"],\n            deleteHardLineBackward: [\"backward\", \"line\"],\n            deleteHardLineForward: [\"forward\", \"line\"],\n        };\n        const argsForDelete = handledInputTypes[ev.inputType];\n        if (argsForDelete) {\n            this.delete(...argsForDelete);\n            ev.preventDefault();\n            if (isBrowserChrome() && hasTouch()) {\n                this.preventDefaultDeleteAndroidChrome(ev);\n            }\n        }\n    }\n\n    onBeforeInputInsertText(ev) {\n        if (ev.inputType === \"insertText\") {\n            const selection = this.dependencies.selection.getSelectionData().deepEditableSelection;\n            if (!selection.isCollapsed) {\n                this.dispatchTo(\"before_delete_handlers\");\n                this.deleteSelection(selection);\n                this.dispatchTo(\"delete_handlers\");\n            }\n            // Default behavior: insert text and trigger input event\n        }\n    }\n\n    /**\n     * Beforeinput event of type deleteContentBackward cannot be default\n     * prevented in Android Chrome. So we need to revert:\n     * - eventual mutations between beforeinput and input events\n     * - eventual selection change after input event\n     *\n     * @param {InputEvent} beforeInputEvent\n     */\n    preventDefaultDeleteAndroidChrome(beforeInputEvent) {\n        const restoreDOM = this.dependencies.history.makeSavePoint();\n        this.onAndroidChromeInput = (ev) => {\n            if (ev.inputType !== beforeInputEvent.inputType) {\n                return;\n            }\n            // Revert DOM changes that occurred between beforeinput and input.\n            restoreDOM();\n\n            // Revert selection changes after input event, within the same tick.\n            // If further mutations occurred, consider selection change legit\n            // (e.g. dictionary input) and do not revert it.\n            const { restore: restoreSelection } = this.dependencies.selection.preserveSelection();\n            const observerOptions = { childList: true, subtree: true, characterData: true };\n            const getMutationRecords = observeMutations(this.editable, observerOptions);\n            this.onAndroidChromeSelectionChange = () => {\n                const shouldRevertSelectionChanges = !getMutationRecords().length;\n                if (shouldRevertSelectionChanges) {\n                    restoreSelection();\n                }\n            };\n            setTimeout(() => delete this.onAndroidChromeSelectionChange);\n        };\n    }\n\n    // ======== AD-HOC STUFF ========\n\n    deleteBackwardUnmergeable(range) {\n        const { startContainer, startOffset, endContainer, endOffset } = range;\n        return this.deleteCharUnmergeable(endContainer, endOffset, startContainer, startOffset);\n    }\n\n    // @todo @phoenix: write tests for this\n    deleteForwardUnmergeable(range) {\n        const { startContainer, startOffset, endContainer, endOffset } = range;\n        return this.deleteCharUnmergeable(startContainer, startOffset, endContainer, endOffset);\n    }\n\n    // Trap cursor inside unmergeable element. Remove it if empty.\n    deleteCharUnmergeable(sourceContainer, sourceOffset, destContainer, destOffset) {\n        if (!destContainer) {\n            return;\n        }\n        const commonAncestor = getCommonAncestor([sourceContainer, destContainer], this.editable);\n        const closestUnmergeable = findUpTo(sourceContainer, commonAncestor, (node) =>\n            this.isUnmergeable(node)\n        );\n        if (!closestUnmergeable) {\n            return;\n        }\n\n        if (\n            (isEmpty(closestUnmergeable) ||\n                this.getResource(\"is_empty_predicates\").some((p) => p(closestUnmergeable))) &&\n            !this.isUnremovable(closestUnmergeable)\n        ) {\n            closestUnmergeable.remove();\n            this.dependencies.selection.setSelection({\n                anchorNode: destContainer,\n                anchorOffset: destOffset,\n            });\n        } else {\n            this.dependencies.selection.setSelection({\n                anchorNode: sourceContainer,\n                anchorOffset: sourceOffset,\n            });\n        }\n        return true;\n    }\n\n    // --------------------------------------------------------------------------\n    // utils\n    // --------------------------------------------------------------------------\n\n    isEmptyInline(element) {\n        if (isBlock(element)) {\n            return false;\n        }\n        if (isZWS(element)) {\n            return true;\n        }\n        return element.innerHTML.trim() === \"\";\n    }\n\n    isCursorAtStartOfElement(element, cursorNode, cursorOffset) {\n        const [node] = this.findPreviousPosition(cursorNode, cursorOffset);\n        return !element.contains(node);\n    }\n\n    isCursorAtEndOfElement(element, cursorNode, cursorOffset) {\n        const [node] = this.findNextPosition(cursorNode, cursorOffset);\n        return !element.contains(node);\n    }\n\n    /**\n     * @param {RangeLike} range\n     */\n    setCursorFromRange(range, { collapseToEnd = false } = {}) {\n        range = this.collapseRange(range, { toEnd: collapseToEnd });\n        const [anchorNode, anchorOffset] = this.normalizeEnterBlock(\n            range.startContainer,\n            range.startOffset\n        );\n        this.dependencies.selection.setSelection({ anchorNode, anchorOffset });\n    }\n\n    // @todo: no need for this once selection in the editable root is corrected?\n    normalizeEnterBlock(node, offset) {\n        while (isBlock(node.childNodes[offset])) {\n            [node, offset] = [node.childNodes[offset], 0];\n        }\n        return [node, offset];\n    }\n\n    /**\n     * @param {RangeLike} range\n     */\n    collapseRange(range, { toEnd = false } = {}) {\n        let { startContainer, startOffset, endContainer, endOffset } = range;\n        if (toEnd) {\n            [startContainer, startOffset] = [endContainer, endOffset];\n        } else {\n            [endContainer, endOffset] = [startContainer, startOffset];\n        }\n        const commonAncestorContainer = startContainer;\n        return { startContainer, startOffset, endContainer, endOffset, commonAncestorContainer };\n    }\n}\n", "import { Plugin } from \"../plugin\";\n\n/**\n * @typedef {typeof import(\"@odoo/owl\").Component} Component\n * @typedef {import(\"@web/core/dialog/dialog_service\").DialogServiceInterfaceAddOptions} DialogServiceInterfaceAddOptions\n */\n\n/**\n * @typedef {Object} DialogShared\n * @property {DialogPlugin['addDialog']} addDialog\n */\n\nexport class DialogPlugin extends Plugin {\n    static id = \"dialog\";\n    static dependencies = [\"selection\"];\n    static shared = [\"addDialog\"];\n\n    /**\n     * @param {Component} DialogClass\n     * @param {Object} props\n     * @param {DialogServiceInterfaceAddOptions} options\n     * @returns {Promise<void>}\n     */\n    addDialog(DialogClass, props, options = {}) {\n        return new Promise((resolve) => {\n            this.services.dialog.add(DialogClass, props, {\n                onClose: () => {\n                    this.dependencies.selection.focusEditable();\n                    resolve();\n                },\n                ...options,\n            });\n        });\n    }\n}\n", "import { Plugin } from \"../plugin\";\nimport { closestBlock, isBlock } from \"../utils/blocks\";\nimport {\n    cleanTrailingBR,\n    fillEmpty,\n    fillShrunkPhrasingParent,\n    makeContentsInline,\n    removeClass,\n    removeStyle,\n    splitTextNode,\n    unwrapContents,\n    wrapInlinesInBlocks,\n} from \"../utils/dom\";\nimport {\n    allowsParagraphRelatedElements,\n    getDeepestPosition,\n    isContentEditable,\n    isContentEditableAncestor,\n    isEmptyBlock,\n    isListElement,\n    isListItemElement,\n    isParagraphRelatedElement,\n    isProtecting,\n    isProtected,\n    isSelfClosingElement,\n    isShrunkBlock,\n    isTangible,\n    isUnprotecting,\n    listElementSelector,\n    isEditorTab,\n    isPhrasingContent,\n} from \"../utils/dom_info\";\nimport {\n    childNodes,\n    children,\n    closestElement,\n    descendants,\n    firstLeaf,\n    lastLeaf,\n} from \"../utils/dom_traversal\";\nimport { FONT_SIZE_CLASSES, TEXT_STYLE_CLASSES } from \"../utils/formatting\";\nimport { DIRECTIONS, childNodeIndex, nodeSize, rightPos } from \"../utils/position\";\nimport { normalizeCursorPosition } from \"@html_editor/utils/selection\";\nimport { baseContainerGlobalSelector } from \"@html_editor/utils/base_container\";\nimport { isHtmlContentSupported } from \"@html_editor/core/selection_plugin\";\n\n/**\n * Get distinct connected parents of nodes\n *\n * @param {Iterable} nodes\n * @returns {Set}\n */\nfunction getConnectedParents(nodes) {\n    const parents = new Set();\n    for (const node of nodes) {\n        if (node.isConnected && node.parentElement) {\n            parents.add(node.parentElement);\n        }\n    }\n    return parents;\n}\n\n/**\n * @typedef {Object} DomShared\n * @property { DomPlugin['insert'] } insert\n * @property { DomPlugin['copyAttributes'] } copyAttributes\n * @property { DomPlugin['canSetBlock'] } canSetBlock\n * @property { DomPlugin['setBlock'] } setBlock\n * @property { DomPlugin['setTagName'] } setTagName\n * @property { DomPlugin['removeSystemProperties'] } removeSystemProperties\n */\n\n/**\n * @typedef {((insertedNodes: Node[]) => void)[]} after_insert_handlers\n * @typedef {((el: HTMLElement) => void)[]} before_set_tag_handlers\n *\n * @typedef {((insertedNode: Node) => insertedNode)[]} before_insert_processors\n * @typedef {((arg: { nodeToInsert: Node, container: HTMLElement }) => nodeToInsert)[]} node_to_insert_processors\n *\n * @typedef {string[]} system_attributes\n * @typedef {string[]} system_classes\n * @typedef {string[]} system_style_properties\n */\n\nexport class DomPlugin extends Plugin {\n    static id = \"dom\";\n    static dependencies = [\"baseContainer\", \"selection\", \"history\", \"split\", \"delete\", \"lineBreak\"];\n    static shared = [\n        \"insert\",\n        \"copyAttributes\",\n        \"canSetBlock\",\n        \"setBlock\",\n        \"setTagName\",\n        \"removeSystemProperties\",\n    ];\n    /** @type {import(\"plugins\").EditorResources} */\n    resources = {\n        user_commands: [\n            {\n                id: \"insertFontAwesome\",\n                run: this.insertFontAwesome.bind(this),\n                isAvailable: isHtmlContentSupported,\n            },\n            {\n                id: \"setTag\",\n                run: this.setBlock.bind(this),\n                isAvailable: isHtmlContentSupported,\n            },\n        ],\n        /** Handlers */\n        clean_for_save_handlers: ({ root }) => {\n            this.removeEmptyClassAndStyleAttributes(root);\n        },\n        clipboard_content_processors: this.removeEmptyClassAndStyleAttributes.bind(this),\n        functional_empty_node_predicates: [isSelfClosingElement, isEditorTab],\n    };\n\n    setup() {\n        this.systemClasses = this.getResource(\"system_classes\");\n        this.systemAttributes = this.getResource(\"system_attributes\");\n        this.systemStyleProperties = this.getResource(\"system_style_properties\");\n        this.systemPropertiesSelector = [\n            ...this.systemClasses.map((className) => `.${className}`),\n            ...this.systemAttributes.map((attr) => `[${attr}]`),\n            ...this.systemStyleProperties.map((prop) => `[style*=\"${prop}\"]`),\n        ].join(\",\");\n    }\n\n    // Shared\n\n    /**\n     * @param {string | DocumentFragment | Element | null} content\n     */\n    insert(content) {\n        if (!content) {\n            return;\n        }\n        let selection = this.dependencies.selection.getEditableSelection();\n        if (!selection.isCollapsed) {\n            this.dependencies.delete.deleteSelection();\n            selection = this.dependencies.selection.getEditableSelection();\n        }\n\n        let container = this.document.createElement(\"fake-element\");\n        const containerFirstChild = this.document.createElement(\"fake-element-fc\");\n        const containerLastChild = this.document.createElement(\"fake-element-lc\");\n        if (typeof content === \"string\") {\n            container.textContent = content;\n        } else {\n            if (content.nodeType === Node.ELEMENT_NODE) {\n                this.dispatchTo(\"normalize_handlers\", content);\n            } else {\n                for (const child of children(content)) {\n                    this.dispatchTo(\"normalize_handlers\", child);\n                }\n            }\n            container.replaceChildren(content);\n        }\n\n        const block = closestBlock(selection.anchorNode);\n        for (const cb of this.getResource(\"before_insert_processors\")) {\n            container = cb(container, block);\n        }\n        selection = this.dependencies.selection.getEditableSelection();\n\n        let startNode;\n        let insertBefore = false;\n        if (selection.startContainer.nodeType === Node.TEXT_NODE) {\n            insertBefore = !selection.startOffset;\n            splitTextNode(selection.startContainer, selection.startOffset, DIRECTIONS.LEFT);\n            startNode = selection.startContainer;\n        }\n\n        const allInsertedNodes = [];\n        // In case the html inserted starts with a list and will be inserted within\n        // a list, unwrap the list elements from the list.\n        const hasSingleChild = nodeSize(container) === 1;\n        if (\n            closestElement(selection.anchorNode, listElementSelector) &&\n            isListElement(container.firstChild)\n        ) {\n            unwrapContents(container.firstChild);\n        }\n        // Similarly if the html inserted ends with a list.\n        if (\n            closestElement(selection.focusNode, listElementSelector) &&\n            isListElement(container.lastChild) &&\n            !hasSingleChild\n        ) {\n            unwrapContents(container.lastChild);\n        }\n\n        startNode = startNode || this.dependencies.selection.getEditableSelection().anchorNode;\n\n        const shouldUnwrap = (node) =>\n            (isParagraphRelatedElement(node) || isListItemElement(node)) &&\n            !isEmptyBlock(block) &&\n            !isEmptyBlock(node) &&\n            (isContentEditable(node) ||\n                (!node.isConnected && !closestElement(node, \"[contenteditable]\"))) &&\n            !this.dependencies.split.isUnsplittable(node) &&\n            (node.nodeName === block.nodeName ||\n                (this.dependencies.baseContainer.isCandidateForBaseContainer(node) &&\n                    this.dependencies.baseContainer.isCandidateForBaseContainer(block)) ||\n                block.nodeName === \"PRE\" ||\n                (block.nodeName === \"DIV\" && this.dependencies.split.isUnsplittable(block))) &&\n            // If the selection anchorNode is the editable itself, the content\n            // should not be unwrapped.\n            !this.isEditionBoundary(selection.anchorNode);\n\n        // Empty block must contain a br element to allow cursor placement.\n        if (\n            container.lastElementChild &&\n            isBlock(container.lastElementChild) &&\n            !container.lastElementChild.hasChildNodes()\n        ) {\n            fillEmpty(container.lastElementChild);\n        }\n\n        // In case the html inserted is all contained in a single root <p> or <li>\n        // tag, we take the all content of the <p> or <li> and avoid inserting the\n        // <p> or <li>.\n        if (\n            container.childElementCount === 1 &&\n            (this.dependencies.baseContainer.isCandidateForBaseContainer(container.firstChild) ||\n                shouldUnwrap(container.firstChild))\n        ) {\n            const nodeToUnwrap = container.firstElementChild;\n            container.replaceChildren(...childNodes(nodeToUnwrap));\n        } else if (container.childElementCount > 1) {\n            const isSelectionAtStart =\n                firstLeaf(block) === selection.anchorNode && selection.anchorOffset === 0;\n            const isSelectionAtEnd =\n                lastLeaf(block) === selection.focusNode &&\n                selection.focusOffset === nodeSize(selection.focusNode);\n            // Grab the content of the first child block and isolate it.\n            if (shouldUnwrap(container.firstChild) && !isSelectionAtStart) {\n                // Unwrap the deepest nested first <li> element in the\n                // container to extract and paste the text content of the list.\n                if (isListItemElement(container.firstChild)) {\n                    const deepestBlock = closestBlock(firstLeaf(container.firstChild));\n                    this.dependencies.split.splitAroundUntil(deepestBlock, container.firstChild);\n                    container.firstElementChild.replaceChildren(...childNodes(deepestBlock));\n                }\n                containerFirstChild.replaceChildren(...childNodes(container.firstElementChild));\n                container.firstElementChild.remove();\n            }\n            // Grab the content of the last child block and isolate it.\n            if (shouldUnwrap(container.lastChild) && !isSelectionAtEnd) {\n                // Unwrap the deepest nested last <li> element in the container\n                // to extract and paste the text content of the list.\n                if (isListItemElement(container.lastChild)) {\n                    const deepestBlock = closestBlock(lastLeaf(container.lastChild));\n                    this.dependencies.split.splitAroundUntil(deepestBlock, container.lastChild);\n                    container.lastElementChild.replaceChildren(...childNodes(deepestBlock));\n                }\n                containerLastChild.replaceChildren(...childNodes(container.lastElementChild));\n                container.lastElementChild.remove();\n            }\n        }\n\n        const textNode = this.document.createTextNode(\"\");\n        if (startNode.nodeType === Node.ELEMENT_NODE) {\n            if (selection.anchorOffset === 0) {\n                if (isSelfClosingElement(startNode)) {\n                    startNode.parentNode.insertBefore(textNode, startNode);\n                } else {\n                    startNode.prepend(textNode);\n                }\n                startNode = textNode;\n                allInsertedNodes.push(textNode);\n            } else {\n                startNode = childNodes(startNode).at(selection.anchorOffset - 1);\n            }\n        }\n\n        // If we have isolated block content, first we split the current focus\n        // element if it's a block then we insert the content in the right places.\n        let currentNode = startNode;\n        const _insertAt = (reference, nodes, insertBefore) => {\n            for (const child of insertBefore ? nodes.reverse() : nodes) {\n                reference[insertBefore ? \"before\" : \"after\"](child);\n                reference = child;\n            }\n        };\n        const lastInsertedNodes = childNodes(containerLastChild);\n        if (containerLastChild.hasChildNodes()) {\n            const toInsert = childNodes(containerLastChild); // Prevent mutation\n            _insertAt(currentNode, [...toInsert], insertBefore);\n            currentNode = insertBefore ? toInsert[0] : currentNode;\n            toInsert[toInsert.length - 1];\n        }\n        const firstInsertedNodes = childNodes(containerFirstChild);\n        if (containerFirstChild.hasChildNodes()) {\n            const toInsert = childNodes(containerFirstChild); // Prevent mutation\n            _insertAt(currentNode, [...toInsert], insertBefore);\n            currentNode = toInsert[toInsert.length - 1];\n            insertBefore = false;\n        }\n        allInsertedNodes.push(...firstInsertedNodes);\n\n        // If all the Html have been isolated, We force a split of the parent element\n        // to have the need new line in the final result\n        if (!container.hasChildNodes()) {\n            if (this.dependencies.split.isUnsplittable(closestBlock(currentNode.nextSibling))) {\n                this.dependencies.lineBreak.insertLineBreakNode({\n                    targetNode: currentNode.nextSibling,\n                    targetOffset: 0,\n                });\n            } else {\n                // If we arrive here, the o_enter index should always be 0.\n                const parent = currentNode.nextSibling.parentElement;\n                const index = childNodes(parent).indexOf(currentNode.nextSibling);\n                this.dependencies.split.splitBlockNode({\n                    targetNode: parent,\n                    targetOffset: index,\n                });\n            }\n        }\n\n        let nodeToInsert;\n        let doesCurrentNodeAllowsP = allowsParagraphRelatedElements(currentNode);\n        const candidatesForRemoval = [];\n        const insertedNodes = childNodes(container);\n        while ((nodeToInsert = container.firstChild)) {\n            if (isBlock(nodeToInsert) && !doesCurrentNodeAllowsP) {\n                // Split blocks at the edges if inserting new blocks (preventing\n                // <p><p>text</p></p> or <li><li>text</li></li> scenarios).\n                while (\n                    !this.isEditionBoundary(currentNode.parentElement) &&\n                    (!allowsParagraphRelatedElements(currentNode.parentElement) ||\n                        (isListItemElement(currentNode.parentElement) &&\n                            !this.dependencies.split.isUnsplittable(nodeToInsert)))\n                ) {\n                    if (this.dependencies.split.isUnsplittable(currentNode.parentElement)) {\n                        // If we have to insert an unsplittable element, we cannot afford to\n                        // unwrap it we need to search for a more suitable spot to put it\n                        if (this.dependencies.split.isUnsplittable(nodeToInsert)) {\n                            currentNode = currentNode.parentElement;\n                            doesCurrentNodeAllowsP = allowsParagraphRelatedElements(currentNode);\n                            continue;\n                        } else {\n                            makeContentsInline(container);\n                            nodeToInsert = container.firstChild;\n                            break;\n                        }\n                    }\n                    let offset = childNodeIndex(currentNode);\n                    if (!insertBefore) {\n                        offset += 1;\n                    }\n                    if (offset) {\n                        const [left, right] = this.dependencies.split.splitElement(\n                            currentNode.parentElement,\n                            offset\n                        );\n                        currentNode = insertBefore ? right : left;\n                        const otherNode = insertBefore ? left : right;\n                        if (isBlock(otherNode)) {\n                            fillShrunkPhrasingParent(otherNode);\n                        }\n                        // After the content insertion, the right-part of a\n                        // split is evaluated for removal.\n                        candidatesForRemoval.push(right);\n                    } else {\n                        if (isBlock(currentNode)) {\n                            fillShrunkPhrasingParent(currentNode);\n                        }\n                        currentNode = currentNode.parentElement;\n                    }\n                    doesCurrentNodeAllowsP = allowsParagraphRelatedElements(currentNode);\n                }\n                if (\n                    isListItemElement(currentNode.parentElement) &&\n                    isBlock(nodeToInsert) &&\n                    this.dependencies.split.isUnsplittable(nodeToInsert)\n                ) {\n                    const br = document.createElement(\"br\");\n                    currentNode[\n                        isEmptyBlock(currentNode) || !isTangible(currentNode) ? \"before\" : \"after\"\n                    ](br);\n                }\n            }\n            // Ensure that all adjacent paragraph elements are converted to\n            // <li> when inserting in a list.\n            const block = closestBlock(currentNode);\n            for (const processor of this.getResource(\"node_to_insert_processors\")) {\n                nodeToInsert = processor({ nodeToInsert, container: block });\n            }\n            if (insertBefore) {\n                currentNode.before(nodeToInsert);\n                insertBefore = false;\n            } else {\n                currentNode.after(nodeToInsert);\n            }\n            allInsertedNodes.push(nodeToInsert);\n            if (currentNode.tagName !== \"BR\" && isShrunkBlock(currentNode)) {\n                currentNode.remove();\n            }\n            currentNode = nodeToInsert;\n        }\n        // Remove the empty text node created earlier\n        textNode.remove();\n        allInsertedNodes.push(...lastInsertedNodes);\n        this.getResource(\"after_insert_handlers\").forEach((handler) => handler(allInsertedNodes));\n        let insertedNodesParents = getConnectedParents(allInsertedNodes);\n        for (const parent of insertedNodesParents) {\n            if (\n                !this.config.allowInlineAtRoot &&\n                this.isEditionBoundary(parent) &&\n                allowsParagraphRelatedElements(parent)\n            ) {\n                // Ensure that edition boundaries do not have inline content.\n                wrapInlinesInBlocks(parent, {\n                    baseContainerNodeName: this.dependencies.baseContainer.getDefaultNodeName(),\n                });\n            }\n        }\n        insertedNodesParents = getConnectedParents(allInsertedNodes);\n        for (const parent of insertedNodesParents) {\n            if (\n                !isProtecting(parent) &&\n                !(isProtected(parent) && !isUnprotecting(parent)) &&\n                parent.isContentEditable\n            ) {\n                cleanTrailingBR(parent);\n            }\n        }\n        for (const candidateForRemoval of candidatesForRemoval) {\n            if (\n                candidateForRemoval.isConnected &&\n                (isParagraphRelatedElement(candidateForRemoval) ||\n                    isListItemElement(candidateForRemoval)) &&\n                candidateForRemoval.parentElement.isContentEditable &&\n                isEmptyBlock(candidateForRemoval)\n            ) {\n                candidateForRemoval.remove();\n            }\n        }\n        for (const insertedNode of allInsertedNodes.reverse()) {\n            if (insertedNode.isConnected) {\n                currentNode = insertedNode;\n                break;\n            }\n        }\n        let lastPosition =\n            isParagraphRelatedElement(currentNode) ||\n            isListItemElement(currentNode) ||\n            isListElement(currentNode)\n                ? rightPos(lastLeaf(currentNode))\n                : rightPos(currentNode);\n        lastPosition = normalizeCursorPosition(lastPosition[0], lastPosition[1], \"right\");\n\n        if (!this.config.allowInlineAtRoot && this.isEditionBoundary(lastPosition[0])) {\n            // Correct the position if it happens to be in the editable root.\n            lastPosition = getDeepestPosition(...lastPosition);\n        }\n        this.dependencies.selection.setSelection(\n            { anchorNode: lastPosition[0], anchorOffset: lastPosition[1] },\n            { normalize: false }\n        );\n        return firstInsertedNodes.concat(insertedNodes).concat(lastInsertedNodes);\n    }\n\n    isEditionBoundary(node) {\n        if (!node) {\n            return false;\n        }\n        if (node === this.editable) {\n            return true;\n        }\n        return isContentEditableAncestor(node);\n    }\n\n    /**\n     * @param {HTMLElement} source\n     * @param {HTMLElement} target\n     */\n    copyAttributes(source, target) {\n        if (source?.nodeType !== Node.ELEMENT_NODE || target?.nodeType !== Node.ELEMENT_NODE) {\n            return;\n        }\n        const ignoredAttrs = new Set(this.getResource(\"system_attributes\"));\n        const ignoredClasses = new Set(this.getResource(\"system_classes\"));\n        for (const attr of source.attributes) {\n            if (ignoredAttrs.has(attr.name)) {\n                continue;\n            }\n            if (attr.name !== \"class\" || ignoredClasses.size === 0) {\n                target.setAttribute(attr.name, attr.value);\n            } else {\n                const classes = [...source.classList];\n                for (const className of classes) {\n                    if (!ignoredClasses.has(className)) {\n                        target.classList.add(className);\n                    }\n                }\n            }\n        }\n    }\n\n    /**\n     * Basic method to change an element tagName.\n     * It is a technical function which only modifies a tag and its attributes.\n     * It does not modify descendants nor handle the cursor.\n     * @see setBlock for the more thorough command.\n     *\n     * @param {HTMLElement} el\n     * @param {string} newTagName\n     */\n    setTagName(el, newTagName) {\n        const document = el.ownerDocument;\n        if (el.tagName === newTagName) {\n            return el;\n        }\n        const newEl = document.createElement(newTagName);\n        const content = childNodes(el);\n        if (isListItemElement(el)) {\n            el.append(newEl);\n            newEl.replaceChildren(...content);\n        } else {\n            if (el.parentElement) {\n                el.before(newEl);\n            }\n            this.copyAttributes(el, newEl);\n            newEl.replaceChildren(...content);\n            el.remove();\n        }\n        return newEl;\n    }\n\n    /**\n     * Remove system-specific classes, attributes, and style properties from a\n     * fragment or an element.\n     *\n     * @param {DocumentFragment|HTMLElement} root\n     */\n    removeSystemProperties(root) {\n        const clean = (element) => {\n            removeClass(element, ...this.systemClasses);\n            this.systemAttributes.forEach((attr) => element.removeAttribute(attr));\n            removeStyle(element, ...this.systemStyleProperties);\n        };\n        if (root.matches?.(this.systemPropertiesSelector)) {\n            clean(root);\n        }\n        for (const element of root.querySelectorAll(this.systemPropertiesSelector)) {\n            clean(element);\n        }\n    }\n\n    // --------------------------------------------------------------------------\n    // commands\n    // --------------------------------------------------------------------------\n\n    insertFontAwesome({ faClass = \"fa fa-star\" } = {}) {\n        const fontAwesomeNode = document.createElement(\"i\");\n        fontAwesomeNode.className = faClass;\n        this.insert(fontAwesomeNode);\n        this.dependencies.history.addStep();\n        const [anchorNode, anchorOffset] = rightPos(fontAwesomeNode);\n        this.dependencies.selection.setSelection({ anchorNode, anchorOffset });\n    }\n\n    /**\n     * Determines if a block element can be safely retagged.\n     *\n     * Certain blocks (like 'o_editable') should not be retagged because doing so\n     * will recreate the block, potentially causing issues. This function checks\n     * if retagging a block is safe.\n     *\n     * @param {HTMLElement} block\n     * @returns {boolean}\n     */\n    isRetaggingSafe(block) {\n        return !(\n            (isParagraphRelatedElement(block) ||\n                isListItemElement(block) ||\n                isPhrasingContent(block)) &&\n            this.getResource(\"unremovable_node_predicates\").some((predicate) => predicate(block))\n        );\n    }\n\n    getBlocksToSet() {\n        const targetedBlocks = [...this.dependencies.selection.getTargetedBlocks()];\n        return targetedBlocks.filter(\n            (block) =>\n                this.isRetaggingSafe(block) &&\n                !descendants(block).some((descendant) => targetedBlocks.includes(descendant)) &&\n                block.isContentEditable\n        );\n    }\n\n    canSetBlock() {\n        return this.getBlocksToSet().length > 0;\n    }\n\n    /**\n     * @param {Object} param0\n     * @param {string} param0.tagName\n     * @param {string} [param0.extraClass]\n     */\n    setBlock({ tagName, extraClass = \"\" }) {\n        let newCandidate = this.document.createElement(tagName.toUpperCase());\n        if (extraClass) {\n            newCandidate.classList.add(extraClass);\n        }\n        if (this.dependencies.baseContainer.isCandidateForBaseContainer(newCandidate)) {\n            const baseContainer = this.dependencies.baseContainer.createBaseContainer(\n                newCandidate.nodeName\n            );\n            this.copyAttributes(newCandidate, baseContainer);\n            newCandidate = baseContainer;\n        }\n        const cursors = this.dependencies.selection.preserveSelection();\n        const newEls = [];\n        for (const block of this.getBlocksToSet()) {\n            if (\n                isParagraphRelatedElement(block) ||\n                isListItemElement(block) ||\n                isPhrasingContent(block) ||\n                block.nodeName === \"BLOCKQUOTE\"\n            ) {\n                if (newCandidate.matches(baseContainerGlobalSelector) && isListItemElement(block)) {\n                    continue;\n                }\n                this.dispatchTo(\"before_set_tag_handlers\", block, tagName, cursors);\n                const newEl = this.setTagName(block, tagName);\n                cursors.remapNode(block, newEl);\n                // We want to be able to edit the case `<h2 class=\"h3\">`\n                // but in that case, we want to display \"Header 2\" and\n                // not \"Header 3\" as it is more important to display\n                // the semantic tag being used (especially for h1 ones).\n                // This is why those are not in `TEXT_STYLE_CLASSES`.\n                const headingClasses = [\"h1\", \"h2\", \"h3\", \"h4\", \"h5\", \"h6\"];\n                removeClass(newEl, ...FONT_SIZE_CLASSES, ...TEXT_STYLE_CLASSES, ...headingClasses);\n                delete newEl.style.fontSize;\n                if (extraClass) {\n                    newEl.classList.add(extraClass);\n                }\n                newEls.push(newEl);\n            } else {\n                // eg do not change a <div> into a h1: insert the h1\n                // into it instead.\n                newCandidate.append(...childNodes(block));\n                block.append(newCandidate);\n                cursors.remapNode(block, newCandidate);\n            }\n        }\n        cursors.restore();\n        this.dependencies.history.addStep();\n    }\n\n    removeEmptyClassAndStyleAttributes(root) {\n        for (const node of [root, ...descendants(root)]) {\n            if (node.classList && !node.classList.length) {\n                node.removeAttribute(\"class\");\n            }\n            if (node.style && !node.style.length) {\n                node.removeAttribute(\"style\");\n            }\n        }\n    }\n}\n", "import {\n    htmlEditorVersions,\n    stripVersion,\n    VERSION_SELECTOR,\n} from \"@html_editor/html_migrations/html_migrations_utils\";\nimport { Plugin } from \"@html_editor/plugin\";\n\nexport class EditorVersionPlugin extends Plugin {\n    static id = \"editorVersion\";\n    /** @type {import(\"plugins\").EditorResources} */\n    resources = {\n        clean_for_save_handlers: this.cleanForSave.bind(this),\n        normalize_handlers: this.normalize.bind(this),\n    };\n\n    normalize(element) {\n        if (element.matches(VERSION_SELECTOR) && element !== this.editable) {\n            delete element.dataset.oeVersion;\n        }\n        stripVersion(element);\n    }\n\n    cleanForSave({ root }) {\n        const VERSIONS = htmlEditorVersions();\n        const firstChild = root.firstElementChild;\n        const version = VERSIONS.at(-1);\n        if (firstChild && version) {\n            firstChild.dataset.oeVersion = version;\n        }\n    }\n}\n", "import { prepareUpdate } from \"@html_editor/utils/dom_state\";\nimport { withSequence } from \"@html_editor/utils/resource\";\nimport { callbacksForCursorUpdate } from \"@html_editor/utils/selection\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { Plugin } from \"../plugin\";\nimport { closestBlock, isBlock } from \"../utils/blocks\";\nimport { cleanTextNode, fillEmpty, removeClass, splitTextNode, unwrapContents } from \"../utils/dom\";\nimport {\n    areSimilarElements,\n    isContentEditable,\n    isElement,\n    isEmptyBlock,\n    isEmptyTextNode,\n    isSelfClosingElement,\n    isTextNode,\n    isVisibleTextNode,\n    isZwnbsp,\n    isZWS,\n    previousLeaf,\n} from \"../utils/dom_info\";\nimport { isFakeLineBreak } from \"../utils/dom_state\";\nimport {\n    childNodes,\n    closestElement,\n    descendants,\n    findFurthest,\n    selectElements,\n} from \"../utils/dom_traversal\";\nimport { formatsSpecs, FORMATTABLE_TAGS } from \"../utils/formatting\";\nimport { boundariesIn, boundariesOut, DIRECTIONS, leftPos, rightPos } from \"../utils/position\";\nimport { isHtmlContentSupported } from \"@html_editor/core/selection_plugin\";\n\nconst allWhitespaceRegex = /^[\\s\\u200b]*$/;\n\nfunction isFormatted(formatPlugin, format) {\n    return (sel, nodes) => formatPlugin.isSelectionFormat(format, nodes);\n}\n\n/**\n * @typedef {Object} FormatShared\n * @property { FormatPlugin['isSelectionFormat'] } isSelectionFormat\n * @property { FormatPlugin['insertAndSelectZws'] } insertAndSelectZws\n * @property { FormatPlugin['mergeAdjacentInlines'] } mergeAdjacentInlines\n * @property { FormatPlugin['formatSelection'] } formatSelection\n */\n\n/**\n * @typedef {((formatName: string, options: {\n *      formatProps: object,\n *      applyStyle: boolean,\n * }) => void | boolean)[]} format_selection_handlers\n * @typedef {(() => void)[]} remove_all_formats_handlers\n *\n * @typedef {((className: string) => boolean)[]} format_class_predicates\n * @typedef {((node: Node) => boolean)[]} has_format_predicates\n */\n\nexport class FormatPlugin extends Plugin {\n    static id = \"format\";\n    static dependencies = [\"selection\", \"history\", \"input\", \"split\"];\n    // TODO ABD: refactor to handle Knowledge comments inside this plugin without sharing mergeAdjacentInlines.\n    static shared = [\n        \"isSelectionFormat\",\n        \"insertAndSelectZws\",\n        \"mergeAdjacentInlines\",\n        \"formatSelection\",\n    ];\n    /** @type {import(\"plugins\").EditorResources} */\n    resources = {\n        user_commands: [\n            {\n                id: \"formatBold\",\n                description: _t(\"Toggle bold\"),\n                icon: \"fa-bold\",\n                run: this.formatSelection.bind(this, \"bold\"),\n                isAvailable: isHtmlContentSupported,\n            },\n            {\n                id: \"formatItalic\",\n                description: _t(\"Toggle italic\"),\n                icon: \"fa-italic\",\n                run: this.formatSelection.bind(this, \"italic\"),\n                isAvailable: isHtmlContentSupported,\n            },\n            {\n                id: \"formatUnderline\",\n                description: _t(\"Toggle underline\"),\n                icon: \"fa-underline\",\n                run: this.formatSelection.bind(this, \"underline\"),\n                isAvailable: isHtmlContentSupported,\n            },\n            {\n                id: \"formatStrikethrough\",\n                description: _t(\"Toggle strikethrough\"),\n                icon: \"fa-strikethrough\",\n                run: this.formatSelection.bind(this, \"strikeThrough\"),\n                isAvailable: isHtmlContentSupported,\n            },\n            {\n                id: \"formatFontSize\",\n                run: ({ size }) =>\n                    this.formatSelection(\"fontSize\", {\n                        applyStyle: true,\n                        formatProps: { size },\n                    }),\n                isAvailable: isHtmlContentSupported,\n            },\n            {\n                id: \"formatFontSizeClassName\",\n                run: ({ className }) =>\n                    this.formatSelection(\"setFontSizeClassName\", {\n                        applyStyle: true,\n                        formatProps: { className },\n                    }),\n                isAvailable: isHtmlContentSupported,\n            },\n            {\n                id: \"removeFormat\",\n                description: (sel, nodes) =>\n                    nodes && this.hasAnyFormat(nodes)\n                        ? _t(\"Remove Format\")\n                        : _t(\"Selection has no format\"),\n                icon: \"fa-eraser\",\n                run: this.removeAllFormats.bind(this),\n                isAvailable: isHtmlContentSupported,\n            },\n        ],\n        shortcuts: [\n            { hotkey: \"control+b\", commandId: \"formatBold\" },\n            { hotkey: \"control+i\", commandId: \"formatItalic\" },\n            { hotkey: \"control+u\", commandId: \"formatUnderline\" },\n            { hotkey: \"control+5\", commandId: \"formatStrikethrough\" },\n            { hotkey: \"control+space\", commandId: \"removeFormat\" },\n        ],\n        toolbar_groups: withSequence(20, { id: \"decoration\" }),\n        toolbar_items: [\n            {\n                id: \"bold\",\n                groupId: \"decoration\",\n                namespaces: [\"compact\", \"expanded\"],\n                commandId: \"formatBold\",\n                isActive: isFormatted(this, \"bold\"),\n            },\n            {\n                id: \"italic\",\n                groupId: \"decoration\",\n                namespaces: [\"compact\", \"expanded\"],\n                commandId: \"formatItalic\",\n                isActive: isFormatted(this, \"italic\"),\n            },\n            {\n                id: \"underline\",\n                groupId: \"decoration\",\n                namespaces: [\"compact\", \"expanded\"],\n                commandId: \"formatUnderline\",\n                isActive: isFormatted(this, \"underline\"),\n            },\n            {\n                id: \"strikethrough\",\n                groupId: \"decoration\",\n                commandId: \"formatStrikethrough\",\n                isActive: isFormatted(this, \"strikeThrough\"),\n            },\n            withSequence(20, {\n                id: \"remove_format\",\n                groupId: \"decoration\",\n                commandId: \"removeFormat\",\n                isDisabled: (sel, nodes) => !this.hasAnyFormat(nodes),\n            }),\n        ],\n        /** Handlers */\n        beforeinput_handlers: withSequence(20, this.onBeforeInput.bind(this)),\n        clean_for_save_handlers: this.cleanForSave.bind(this),\n        normalize_handlers: this.normalize.bind(this),\n        selectionchange_handlers: this.removeEmptyInlineElement.bind(this),\n        before_set_tag_handlers: this.removeFontSizeFormat.bind(this),\n        before_insert_processors: this.unwrapEmptyFormat.bind(this),\n\n        intangible_char_for_keyboard_navigation_predicates: (_, char) => char === \"\\u200b\",\n    };\n\n    /**\n     * @param {string[]} formats\n     * @param {Node[]} targetedNodes\n     */\n    removeFormats(formats, targetedNodes) {\n        for (const format of formats) {\n            if (\n                !formatsSpecs[format].removeStyle ||\n                !this.hasSelectionFormat(format, targetedNodes)\n            ) {\n                continue;\n            }\n            this.formatSelection(format, { applyStyle: false, removeFormat: true });\n        }\n    }\n\n    unwrapEmptyFormat(insertedNode) {\n        const anchorNode = this.dependencies.selection.getEditableSelection().anchorNode;\n        if (!allWhitespaceRegex.test(insertedNode.textContent)) {\n            return insertedNode;\n        }\n        const emptyZWS = closestElement(anchorNode, \"[data-oe-zws-empty-inline]\");\n        if (\n            !emptyZWS ||\n            !emptyZWS.parentElement.isContentEditable ||\n            this.getResource(\"unremovable_node_predicates\").some((p) => p(emptyZWS))\n        ) {\n            return insertedNode;\n        }\n        const cursors = this.dependencies.selection.preserveSelection();\n        cursors.update(callbacksForCursorUpdate.remove(emptyZWS));\n        emptyZWS.remove();\n        cursors.restore();\n        return insertedNode;\n    }\n\n    removeAllFormats() {\n        const targetedNodes = this.dependencies.selection.getTargetedNodes();\n        this.removeFormats(Object.keys(formatsSpecs), targetedNodes);\n        this.dispatchTo(\"remove_all_formats_handlers\");\n        this.dependencies.history.addStep();\n    }\n\n    removeFontSizeFormat(el) {\n        this.removeFormats([\"fontSize\", \"setFontSizeClassName\"], [el, ...descendants(el)]);\n    }\n\n    /**\n     * Return true if the current selection on the editable contains a formated\n     * node\n     *\n     * @param {String} format 'bold'|'italic'|'underline'|'strikeThrough'|'switchDirection'\n     * @param {Node[]} [targetedNodes]\n     * @returns {boolean}\n     */\n    hasSelectionFormat(format, targetedNodes = this.dependencies.selection.getTargetedNodes()) {\n        const targetedTextNodes = targetedNodes.filter(isTextNode);\n        const isFormatted = formatsSpecs[format].isFormatted;\n        return targetedTextNodes.some((n) => isFormatted(n, { editable: this.editable }));\n    }\n    /**\n     * Return true if the current selection on the editable appears as the given\n     * format. The selection is considered to appear as that format if every\n     * text node in it appears as that format.\n     *\n     * @param {String} format 'bold'|'italic'|'underline'|'strikeThrough'|'switchDirection'\n     * @param {Node[]} [targetedNodes]\n     * @returns {boolean}\n     */\n    isSelectionFormat(format, targetedNodes = this.dependencies.selection.getTargetedNodes()) {\n        const targetedTextNodes = targetedNodes.filter(isTextNode);\n        const isFormatted = formatsSpecs[format].isFormatted;\n        return (\n            targetedTextNodes.length &&\n            targetedTextNodes.every(\n                (node) =>\n                    isZwnbsp(node) ||\n                    isEmptyTextNode(node) ||\n                    isFormatted(node, { editable: this.editable })\n            )\n        );\n    }\n\n    hasAnyFormat(targetedNodes) {\n        for (const format of Object.keys(formatsSpecs)) {\n            if (\n                formatsSpecs[format].removeStyle &&\n                this.hasSelectionFormat(format, targetedNodes)\n            ) {\n                return true;\n            }\n        }\n        return targetedNodes.some((node) =>\n            this.getResource(\"has_format_predicates\").some((predicate) => predicate(node))\n        );\n    }\n\n    formatSelection(formatName, options) {\n        this.dispatchTo(\"format_selection_handlers\", formatName, options);\n        if (this._formatSelection(formatName, options) && !options?.removeFormat) {\n            this.dependencies.history.addStep();\n        }\n    }\n\n    // @todo phoenix: refactor this method.\n    _formatSelection(formatName, { applyStyle, formatProps } = {}) {\n        this.dependencies.selection.selectAroundNonEditable();\n        // note: does it work if selection is in opposite direction?\n        const selection = this.dependencies.split.splitSelection();\n        if (typeof applyStyle === \"undefined\") {\n            applyStyle = !this.isSelectionFormat(formatName);\n        }\n\n        let zws;\n        if (selection.isCollapsed) {\n            if (isTextNode(selection.anchorNode) && selection.anchorNode.textContent === \"\\u200b\") {\n                zws = selection.anchorNode;\n                this.dependencies.selection.setSelection({\n                    anchorNode: zws,\n                    anchorOffset: 0,\n                    focusNode: zws,\n                    focusOffset: 1,\n                });\n            } else {\n                zws = this.insertAndSelectZws();\n            }\n        }\n\n        const selectedTextNodes = /** @type { Text[] } **/ (\n            this.dependencies.selection\n                .getTargetedNodes()\n                .filter(\n                    (n) =>\n                        this.dependencies.selection.areNodeContentsFullySelected(n) &&\n                        ((isTextNode(n) && (isVisibleTextNode(n) || isZWS(n))) ||\n                            (n.nodeName === \"BR\" &&\n                                (isFakeLineBreak(n) ||\n                                    previousLeaf(n, closestBlock(n))?.nodeName === \"BR\"))) &&\n                        isContentEditable(n)\n                )\n        );\n        const unformattedTextNodes = selectedTextNodes.filter((n) => {\n            const listItem = closestElement(n, \"li\");\n            if (listItem && this.dependencies.selection.areNodeContentsFullySelected(listItem)) {\n                const hasFontSizeStyle =\n                    formatName === \"setFontSizeClassName\"\n                        ? listItem.classList.contains(formatProps?.className)\n                        : listItem.style.fontSize;\n                return !hasFontSizeStyle;\n            }\n            return true;\n        });\n\n        const tagetedFieldNodes = new Set(\n            this.dependencies.selection\n                .getTargetedNodes()\n                .map((n) => closestElement(n, \"*[t-field],*[t-out],*[t-esc]\"))\n                .filter(Boolean)\n        );\n        const formatSpec = formatsSpecs[formatName];\n        for (const node of unformattedTextNodes) {\n            const inlineAncestors = [];\n            /** @type { Node } */\n            let currentNode = node;\n            let parentNode = node.parentElement;\n\n            // Remove the format on all inline ancestors until a block or an element\n            // with a class that is not indicated as splittable.\n            const isClassListSplittable = (classList) =>\n                [...classList].every((className) =>\n                    this.getResource(\"format_class_predicates\").some((cb) => cb(className))\n                );\n\n            while (\n                parentNode &&\n                !isBlock(parentNode) &&\n                !this.dependencies.split.isUnsplittable(parentNode) &&\n                (parentNode.classList.length === 0 || isClassListSplittable(parentNode.classList))\n            ) {\n                const isUselessZws =\n                    parentNode.tagName === \"SPAN\" &&\n                    parentNode.hasAttribute(\"data-oe-zws-empty-inline\") &&\n                    parentNode.getAttributeNames().length === 1;\n\n                if (isUselessZws) {\n                    unwrapContents(parentNode);\n                } else {\n                    const newLastAncestorInlineFormat = this.dependencies.split.splitAroundUntil(\n                        currentNode,\n                        parentNode\n                    );\n                    removeFormat(newLastAncestorInlineFormat, formatSpec);\n                    if ([\"setFontSizeClassName\", \"fontSize\"].includes(formatName) && applyStyle) {\n                        removeClass(newLastAncestorInlineFormat, \"o_default_font_size\");\n                    }\n                    if (newLastAncestorInlineFormat.isConnected) {\n                        inlineAncestors.push(newLastAncestorInlineFormat);\n                        currentNode = newLastAncestorInlineFormat;\n                    }\n                }\n\n                parentNode = currentNode.parentElement;\n            }\n\n            const firstBlockOrClassHasFormat = formatSpec.isFormatted(parentNode, formatProps);\n            if (firstBlockOrClassHasFormat && !applyStyle) {\n                const isParentNodeBlockAndCompletelySelected =\n                    isBlock(parentNode) &&\n                    this.dependencies.selection.areNodeContentsFullySelected(parentNode);\n                if (\n                    isParentNodeBlockAndCompletelySelected &&\n                    formatName === \"setFontSizeClassName\"\n                ) {\n                    for (const node of [parentNode, ...descendants(parentNode).filter(isElement)]) {\n                        removeFormat(node, formatSpec);\n                    }\n                } else {\n                    formatSpec.addNeutralStyle &&\n                        formatSpec.addNeutralStyle(getOrCreateSpan(node, inlineAncestors));\n                }\n            } else if (\n                (!firstBlockOrClassHasFormat || parentNode.nodeName === \"LI\") &&\n                applyStyle\n            ) {\n                const tag = formatSpec.tagName && this.document.createElement(formatSpec.tagName);\n                if (tag) {\n                    node.after(tag);\n                    tag.append(node);\n\n                    if (!formatSpec.isFormatted(tag, formatProps)) {\n                        tag.after(node);\n                        tag.remove();\n                        formatSpec.addStyle(getOrCreateSpan(node, inlineAncestors), formatProps);\n                    }\n                } else if (formatName !== \"fontSize\" || formatProps.size !== undefined) {\n                    formatSpec.addStyle(getOrCreateSpan(node, inlineAncestors), formatProps);\n                }\n            }\n        }\n\n        for (const targetedFieldNode of tagetedFieldNodes) {\n            if (applyStyle) {\n                formatSpec.addStyle(targetedFieldNode, formatProps);\n            } else {\n                formatSpec.removeStyle(targetedFieldNode);\n            }\n        }\n\n        if (zws) {\n            const siblings = [...zws.parentElement.childNodes];\n            if (\n                !isBlock(zws.parentElement) &&\n                unformattedTextNodes.includes(siblings[0]) &&\n                unformattedTextNodes.includes(siblings[siblings.length - 1])\n            ) {\n                zws.parentElement.setAttribute(\"data-oe-zws-empty-inline\", \"\");\n            } else {\n                const span = this.document.createElement(\"span\");\n                span.setAttribute(\"data-oe-zws-empty-inline\", \"\");\n                zws.before(span);\n                span.append(zws);\n            }\n        }\n\n        if (\n            unformattedTextNodes.length === 1 &&\n            unformattedTextNodes[0] &&\n            unformattedTextNodes[0].textContent === \"\\u200B\"\n        ) {\n            this.dependencies.selection.setCursorStart(unformattedTextNodes[0]);\n        } else if (selectedTextNodes.length) {\n            const firstNode = selectedTextNodes[0];\n            const lastNode = selectedTextNodes[selectedTextNodes.length - 1];\n            let newSelection;\n            if (selection.direction === DIRECTIONS.RIGHT) {\n                newSelection = {\n                    anchorNode: firstNode,\n                    anchorOffset: 0,\n                    focusNode: lastNode,\n                    focusOffset: lastNode.length,\n                };\n            } else {\n                newSelection = {\n                    anchorNode: lastNode,\n                    anchorOffset: lastNode.length,\n                    focusNode: firstNode,\n                    focusOffset: 0,\n                };\n            }\n            this.dependencies.selection.setSelection(newSelection, { normalize: false });\n            return true;\n        }\n        if (tagetedFieldNodes.size > 0) {\n            return true;\n        }\n    }\n\n    normalize(root) {\n        for (const el of selectElements(root, \"[data-oe-zws-empty-inline]\")) {\n            if (!allWhitespaceRegex.test(el.textContent)) {\n                // The element has some meaningful text. Remove the ZWS in it.\n                delete el.dataset.oeZwsEmptyInline;\n                this.cleanZWS(el);\n                if (\n                    el.tagName === \"SPAN\" &&\n                    el.getAttributeNames().length === 0 &&\n                    el.classList.length === 0\n                ) {\n                    // Useless span, unwrap it.\n                    unwrapContents(el);\n                }\n            }\n        }\n        this.mergeAdjacentInlines(root);\n    }\n\n    cleanForSave({ root, preserveSelection = false } = {}) {\n        for (const element of root.querySelectorAll(\"[data-oe-zws-empty-inline]\")) {\n            let currentElement = element.parentElement;\n            this.cleanElement(element, { preserveSelection });\n            while (\n                currentElement &&\n                !isBlock(currentElement) &&\n                !currentElement.childNodes.length\n            ) {\n                const parentElement = currentElement.parentElement;\n                currentElement.remove();\n                currentElement = parentElement;\n            }\n            if (currentElement && isBlock(currentElement)) {\n                fillEmpty(currentElement);\n            }\n        }\n        this.mergeAdjacentInlines(root, { preserveSelection });\n    }\n\n    removeEmptyInlineElement(selectionData) {\n        const { anchorNode } = selectionData.editableSelection;\n        const blockEl = closestBlock(anchorNode);\n        const inlineElement = findFurthest(\n            closestElement(anchorNode),\n            blockEl,\n            (e) => !isBlock(e) && e.textContent === \"\\u200b\"\n        );\n        if (\n            this.lastEmptyInlineElement?.isConnected &&\n            this.lastEmptyInlineElement !== inlineElement\n        ) {\n            // Remove last empty inline element.\n            this.cleanElement(this.lastEmptyInlineElement, { preserveSelection: true });\n        }\n        // Skip if current block is empty.\n        if (inlineElement && !isEmptyBlock(blockEl)) {\n            this.lastEmptyInlineElement = inlineElement;\n        } else {\n            this.lastEmptyInlineElement = null;\n        }\n    }\n\n    cleanElement(element, { preserveSelection }) {\n        delete element.dataset.oeZwsEmptyInline;\n        if (!allWhitespaceRegex.test(element.textContent)) {\n            // The element has some meaningful text. Remove the ZWS in it.\n            this.cleanZWS(element, { preserveSelection });\n            return;\n        }\n        if (this.getResource(\"unremovable_node_predicates\").some((p) => p(element))) {\n            return;\n        }\n        if (\n            ![...element.classList].every((c) =>\n                this.getResource(\"format_class_predicates\").some((p) => p(c))\n            )\n        ) {\n            // Original comment from web_editor:\n            // We only remove the empty element if it has no class, to ensure we\n            // don't break visual styles (in that case, its ZWS was kept to\n            // ensure the cursor can be placed in it).\n            return;\n        }\n        const restore = prepareUpdate(...leftPos(element), ...rightPos(element));\n        element.remove();\n        restore();\n    }\n\n    cleanZWS(element, { preserveSelection = true } = {}) {\n        const textNodes = descendants(element).filter(isTextNode);\n        const cursors = preserveSelection ? this.dependencies.selection.preserveSelection() : null;\n        for (const node of textNodes) {\n            cleanTextNode(node, \"\\u200B\", cursors);\n        }\n        cursors?.restore();\n    }\n\n    insertText(selection, content) {\n        if (selection.anchorNode.nodeType === Node.TEXT_NODE) {\n            selection = this.dependencies.selection.setSelection(\n                {\n                    anchorNode: selection.anchorNode.parentElement,\n                    anchorOffset: splitTextNode(selection.anchorNode, selection.anchorOffset),\n                },\n                { normalize: false }\n            );\n        }\n\n        const txt = this.document.createTextNode(content || \"#\");\n        const restore = prepareUpdate(selection.anchorNode, selection.anchorOffset);\n        selection.anchorNode.insertBefore(\n            txt,\n            selection.anchorNode.childNodes[selection.anchorOffset]\n        );\n        restore();\n        const [anchorNode, anchorOffset, focusNode, focusOffset] = boundariesOut(txt);\n        this.dependencies.selection.setSelection(\n            { anchorNode, anchorOffset, focusNode, focusOffset },\n            { normalize: false }\n        );\n        return txt;\n    }\n\n    /**\n     * Use the actual selection (assumed to be collapsed) and insert a\n     * zero-width space at its anchor point. Then, select that zero-width\n     * space.\n     *\n     * @returns {Node} the inserted zero-width space\n     */\n    insertAndSelectZws() {\n        const selection = this.dependencies.selection.getEditableSelection();\n        const zws = this.insertText(selection, \"\\u200B\");\n        splitTextNode(zws, selection.anchorOffset);\n        return zws;\n    }\n\n    onBeforeInput(ev) {\n        if (\n            ev.inputType.startsWith(\"format\") &&\n            !isHtmlContentSupported(this.dependencies.selection.getEditableSelection())\n        ) {\n            ev.preventDefault();\n        }\n        if (ev.inputType === \"insertText\") {\n            const selection = this.dependencies.selection.getEditableSelection();\n            if (!selection.isCollapsed) {\n                return;\n            }\n            const element = closestElement(selection.anchorNode);\n            if (element.hasAttribute(\"data-oe-zws-empty-inline\")) {\n                // Select its ZWS content to make sure the text will be\n                // inserted inside the element, and not before (outside) it.\n                // This addresses an undesired behavior of the\n                // contenteditable.\n                const [anchorNode, anchorOffset, focusNode, focusOffset] = boundariesIn(element);\n                this.dependencies.selection.setSelection({\n                    anchorNode,\n                    anchorOffset,\n                    focusNode,\n                    focusOffset,\n                });\n            }\n        }\n    }\n\n    /**\n     * @param {Node} root\n     * @param {Object} [options]\n     * @param {boolean} [options.preserveSelection=true]\n     */\n    mergeAdjacentInlines(root, { preserveSelection = true } = {}) {\n        let selectionToRestore = null;\n        for (const node of [root, ...descendants(root)].filter(isElement)) {\n            if (this.shouldBeMergedWithPreviousSibling(node)) {\n                if (preserveSelection) {\n                    selectionToRestore ??= this.dependencies.selection.preserveSelection();\n                    selectionToRestore.update(callbacksForCursorUpdate.merge(node));\n                }\n                node.previousSibling.append(...childNodes(node));\n                node.remove();\n            }\n        }\n        selectionToRestore?.restore();\n    }\n\n    shouldBeMergedWithPreviousSibling(node) {\n        const isMergeable = (node) =>\n            FORMATTABLE_TAGS.includes(node.nodeName) &&\n            !this.getResource(\"unsplittable_node_predicates\").some((predicate) => predicate(node));\n        return (\n            !isSelfClosingElement(node) &&\n            areSimilarElements(node, node.previousSibling) &&\n            isMergeable(node)\n        );\n    }\n}\n\nfunction getOrCreateSpan(node, ancestors) {\n    const document = node.ownerDocument;\n    const span = ancestors.find((element) => element.tagName === \"SPAN\" && element.isConnected);\n    const lastInlineAncestor = ancestors.findLast(\n        (element) => !isBlock(element) && element.isConnected\n    );\n    if (span) {\n        return span;\n    } else {\n        const span = document.createElement(\"span\");\n        // Apply font span above current inline top ancestor so that\n        // the font style applies to the other style tags as well.\n        if (lastInlineAncestor) {\n            lastInlineAncestor.after(span);\n            span.append(lastInlineAncestor);\n        } else {\n            node.after(span);\n            span.append(node);\n        }\n        return span;\n    }\n}\nfunction removeFormat(node, formatSpec) {\n    const document = node.ownerDocument;\n    node = closestElement(node);\n    if (formatSpec.hasStyle(node)) {\n        formatSpec.removeStyle(node);\n        if ([\"SPAN\", \"FONT\"].includes(node.tagName) && !node.getAttributeNames().length) {\n            return unwrapContents(node);\n        }\n    }\n\n    if (formatSpec.isTag && formatSpec.isTag(node)) {\n        const attributesNames = node\n            .getAttributeNames()\n            .filter((name) => name !== \"data-oe-zws-empty-inline\");\n        if (attributesNames.length) {\n            // Change tag name\n            const newNode = document.createElement(\"span\");\n            while (node.firstChild) {\n                newNode.appendChild(node.firstChild);\n            }\n            for (let index = node.attributes.length - 1; index >= 0; --index) {\n                newNode.attributes.setNamedItem(node.attributes[index].cloneNode());\n            }\n            node.parentNode.replaceChild(newNode, node);\n        } else {\n            unwrapContents(node);\n        }\n    }\n}\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { Plugin } from \"../plugin\";\nimport { childNodes, descendants, getCommonAncestor } from \"../utils/dom_traversal\";\nimport { hasTouch } from \"@web/core/browser/feature_detection\";\nimport { withSequence } from \"@html_editor/utils/resource\";\nimport { Deferred } from \"@web/core/utils/concurrency\";\nimport { toggleClass } from \"@html_editor/utils/dom\";\nimport { omit, pick } from \"@web/core/utils/objects\";\nimport { trackOccurrences, trackOccurrencesPair } from \"../utils/tracking\";\n\n/**\n * @typedef { import(\"./selection_plugin\").EditorSelection } EditorSelection\n *\n * @typedef { Object } SerializedSelection\n * @property { string } anchorNodeId\n * @property { number } anchorOffset\n * @property { string } focusNodeId\n * @property { number } focusOffset\n *\n * @typedef { Object } SerializedNode\n * @property { number } nodeType\n * @property { string } nodeId\n * @property { string } textValue\n * @property { string } tagName\n * @property { SerializedNode[] } children\n * @property { Record<string, string> } attributes\n *\n * @typedef { Object } HistoryStep\n * @property { string } id\n * @property {\"original\"|\"undo\"|\"redo\"|\"restore\"} type\n * @property { SerializedSelection } selection\n * @property { HistoryMutation[] } mutations\n * @property { string } previousStepId\n * @property { Object } extraStepInfos\n *\n * @typedef { Object } HistoryMutationCharacterData\n * @property { \"characterData\" } type\n * @property { string } nodeId\n * @property { string } value\n * @property { string } oldValue\n *\n * @typedef { Object } HistoryMutationAttributes\n * @property { \"attributes\" } type\n * @property { string } nodeId\n * @property { string } attributeName\n * @property { string } value\n * @property { string } oldValue\n *\n * @typedef { Object } HistoryMutationClassList\n * @property { \"classList\" } type\n * @property { string } nodeId\n * @property { string } className\n * @property { boolean } value\n * @property { boolean } oldValue\n *\n * @typedef { Object } HistoryMutationAdd\n * @property { \"add\" } type\n * @property { string } nodeId\n * @property { string } parentNodeId\n * @property { SerializedNode } serializedNode\n * @property { string } nextNodeId\n * @property { string } previousNodeId\n *\n * @typedef { Object } HistoryMutationRemove\n * @property { \"remove\" } type\n * @property { string } nodeId\n * @property { string } parentNodeId\n * @property { SerializedNode } serializedNode\n * @property { string } nextNodeId\n * @property { string } previousNodeId\n *\n * @typedef { HistoryMutationCharacterData | HistoryMutationAttributes | HistoryMutationClassList | HistoryMutationAdd | HistoryMutationRemove } HistoryMutation\n *\n * @typedef {Object} MutationRecordClassList\n * @property { \"classList\" } type\n * @property { Node } target\n * @property { string } className\n * @property { boolean } oldValue\n * @property { boolean } value\n *\n * @typedef {Object} MutationRecordAttributes\n * @property { \"attributes\" } type\n * @property { Node } target\n * @property { string } attributeName\n * @property { string } oldValue\n * @property { string } value\n *\n * @typedef {Object} MutationRecordCharacterData\n * @property { \"characterData\" } type\n * @property { Node } target\n * @property { string } oldValue\n * @property { string } value\n *\n * @typedef {Object} Tree\n * @property {Node} node\n * @property {Tree[]} children\n *\n * @typedef {Object} MutationRecordChildList\n * @property { \"childList\" } type\n * @property { Node } target\n * @property { Node } previousSibling\n * @property { Node } nextSibling\n * @property { Tree[] } addedTrees\n * @property { Tree[] } removedTrees\n *\n * @typedef { MutationRecordClassList | MutationRecordAttributes | MutationRecordCharacterData | MutationRecordChildList } HistoryMutationRecord\n *\n * @typedef { Object } PreviewableOperation\n * @property { Function } commit\n * @property { Function } preview\n * @property { Function } revert\n */\n\n/**\n * @typedef { Object } HistoryShared\n * @property { HistoryPlugin['addCustomMutation'] } addCustomMutation\n * @property { HistoryPlugin['applyCustomMutation'] } applyCustomMutation\n * @property { HistoryPlugin['addExternalStep'] } addExternalStep\n * @property { HistoryPlugin['addStep'] } addStep\n * @property { HistoryPlugin['canRedo'] } canRedo\n * @property { HistoryPlugin['canUndo'] } canUndo\n * @property { HistoryPlugin['ignoreDOMMutations'] } ignoreDOMMutations\n * @property { HistoryPlugin['getHistorySteps'] } getHistorySteps\n * @property { HistoryPlugin['getNodeById'] } getNodeById\n * @property { HistoryPlugin['makePreviewableOperation'] } makePreviewableOperation\n * @property { HistoryPlugin['makePreviewableAsyncOperation'] } makePreviewableAsyncOperation\n * @property { HistoryPlugin['makeSavePoint'] } makeSavePoint\n * @property { HistoryPlugin['makeSnapshotStep'] } makeSnapshotStep\n * @property { HistoryPlugin['redo'] } redo\n * @property { HistoryPlugin['reset'] } reset\n * @property { HistoryPlugin['resetFromSteps'] } resetFromSteps\n * @property { HistoryPlugin['serializeSelection'] } serializeSelection\n * @property { HistoryPlugin['stageSelection'] } stageSelection\n * @property { HistoryPlugin['stageFocus'] } stageFocus\n * @property { HistoryPlugin['undo'] } undo\n * @property { HistoryPlugin['getIsPreviewing'] } getIsPreviewing\n * @property { HistoryPlugin['setStepExtra'] } setStepExtra\n * @property { HistoryPlugin['getIsCurrentStepModified'] } getIsCurrentStepModified\n */\n\n/**\n * @typedef {((record: HistoryMutationRecord) => void)[]} attribute_change_handlers\n * @typedef {(() => void)[]} before_add_step_handlers\n * @typedef {((records: HistoryMutationRecord[]) => void)[]} before_filter_mutation_record_handlers\n * @typedef {((root: HTMLElement) => void)[]} content_updated_handlers\n * @typedef {(() => void)[]} external_step_added_handlers\n * @typedef {((records: HistoryMutationRecord[], currentOperation: \"original\"|\"undo\"|\"redo\"|\"restore\") => void)[]} handleNewRecords\n * @typedef {(() => void)[]} history_cleaned_handlers\n * @typedef {(() => void)[]} history_reset_handlers\n * @typedef {(() => void)[]} history_reset_from_steps_handlers\n * @typedef {((revertedStep: HistoryStep) => void)[]} post_redo_handlers\n * @typedef {((revertedStep: HistoryStep) => void)[]} post_undo_handlers\n * @typedef {(() => void)[]} restore_savepoint_handlers\n * @typedef {((arg: { step: HistoryStep, stepCommonAncestor: HTMLElement, isPreviewing: boolean }) => void)[]} step_added_handlers\n *\n * @typedef {((record: HistoryMutationRecord) => boolean)[]} savable_mutation_record_predicates\n * @typedef {((step: HistoryStep) => boolean)[]} unreversible_step_predicates\n *\n * @typedef {((\n *    arg: {\n *      target: Node,\n *      attributeName: string,\n *      oldValue: string,\n *      value: string,\n *      reverse: boolean,\n *    },\n *    options: { forNewStep: boolean }\n *  ) => void)[]} attribute_change_processors\n * @typedef {((step: HistoryStep) => HistoryStep)[]} history_step_processors\n * @typedef {((node: Node, childTreesToSerialize: Tree[]) => Tree[])[]} serializable_descendants_processors\n * @typedef {((node: Node, attributeName: string, attributeValue: string) => boolean)[]} set_attribute_overrides\n */\n\nexport class HistoryPlugin extends Plugin {\n    static id = \"history\";\n    static dependencies = [\"selection\", \"sanitize\"];\n    static shared = [\n        \"addCustomMutation\",\n        \"applyCustomMutation\",\n        \"addExternalStep\",\n        \"addStep\",\n        \"canRedo\",\n        \"canUndo\",\n        \"ignoreDOMMutations\",\n        \"getHistorySteps\",\n        \"getNodeById\",\n        \"makePreviewableOperation\",\n        \"makePreviewableAsyncOperation\",\n        \"makeSavePoint\",\n        \"makeSnapshotStep\",\n        \"redo\",\n        \"reset\",\n        \"resetFromSteps\",\n        \"serializeSelection\",\n        \"stageSelection\",\n        \"stageFocus\",\n        \"undo\",\n        \"getIsPreviewing\",\n        \"setStepExtra\",\n        \"getIsCurrentStepModified\",\n    ];\n    /** @type {import(\"plugins\").EditorResources} */\n    resources = {\n        user_commands: [\n            {\n                id: \"historyUndo\",\n                description: _t(\"Undo\"),\n                icon: \"fa-undo\",\n                run: this.undo.bind(this),\n            },\n            {\n                id: \"historyRedo\",\n                description: _t(\"Redo\"),\n                icon: \"fa-repeat\",\n                run: this.redo.bind(this),\n            },\n        ],\n        ...(hasTouch() && {\n            toolbar_groups: withSequence(5, { id: \"historyMobile\" }),\n            toolbar_items: [\n                {\n                    id: \"undo\",\n                    groupId: \"historyMobile\",\n                    commandId: \"historyUndo\",\n                    isDisabled: () => !this.canUndo(),\n                    namespaces: [\"compact\", \"expanded\"],\n                },\n                {\n                    id: \"redo\",\n                    groupId: \"historyMobile\",\n                    commandId: \"historyRedo\",\n                    isDisabled: () => !this.canRedo(),\n                    namespaces: [\"compact\", \"expanded\"],\n                },\n            ],\n        }),\n        shortcuts: [\n            { hotkey: \"control+z\", commandId: \"historyUndo\", global: true },\n            { hotkey: \"control+y\", commandId: \"historyRedo\", global: true },\n            { hotkey: \"control+shift+z\", commandId: \"historyRedo\", global: true },\n        ],\n        start_edition_handlers: () => {\n            this.enableObserver();\n            this.reset(this.config.content);\n        },\n        on_prepare_drag_handlers: this.disableIsCurrentStepModifiedWarning.bind(this),\n    };\n\n    setup() {\n        this.mutationFilteredClasses = new Set(this.getResource(\"system_classes\"));\n        this.mutationFilteredAttributes = new Set(this.getResource(\"system_attributes\"));\n        this._onKeyupResetContenteditableNodes = [];\n        this.addDomListener(this.document, \"beforeinput\", this._onDocumentBeforeInput.bind(this));\n        this.addDomListener(this.document, \"input\", this._onDocumentInput.bind(this));\n        this.addGlobalDomListener(\"pointerup\", (ev) => {\n            if (this.editable.contains(ev.target)) {\n                this.stageSelection();\n            }\n        });\n        this.observer = new MutationObserver((records) => this.handleNewRecords(records));\n        this.enableObserverCallbacks = new Set();\n        this._cleanups.push(() => this.observer.disconnect());\n        this.clean();\n    }\n\n    getIsPreviewing() {\n        return this.isPreviewing;\n    }\n\n    clean() {\n        this.handleObserverRecords();\n        /** @type { HistoryStep[] } */\n        this.steps = [];\n        /** @type { HistoryStep } */\n        this.currentStep = this.processHistoryStep({\n            selection: {},\n            mutations: [],\n            id: this.generateId(),\n            previousStepId: undefined,\n            extraStepInfos: {},\n        });\n        /** @type {Set<string>} Steps reverted by undo/redo operations */\n        this.revertedSteps = new Set();\n        /** @type {Set<string>} Steps reverted by restoring to a save point */\n        this.discardedSteps = new Set();\n        this.nodeMap = new NodeMap();\n        /** @type { WeakMap<Node, { attributes: Map<string, string>, classList: Map<string, boolean>, characterData: Map<string, string> }> } */\n        this.lastObservedState = new WeakMap();\n        this.setNodeId(this.editable);\n        this.dispatchTo(\"history_cleaned_handlers\");\n    }\n    /**\n     * @param {string} id\n     * @returns {Node}\n     */\n    getNodeById(id) {\n        return this.nodeMap.getNode(id);\n    }\n    /**\n     * Reset the history.\n     *\n     * @param { string } content\n     */\n    reset(content) {\n        this.clean();\n        this.stageSelection();\n        this.steps.push(this.makeSnapshotStep());\n        this.dispatchTo(\"history_reset_handlers\", content);\n    }\n    /**\n     * @param { HistoryStep[] } steps\n     */\n    resetFromSteps(steps) {\n        this.withObserverOff(() => {\n            this.editable.replaceChildren();\n            this.clean();\n            this.stageSelection();\n            for (const step of steps) {\n                this.applyMutations(step.mutations);\n            }\n            this.steps = steps;\n            // todo: to test\n            this.dispatchTo(\"history_reset_from_steps_handlers\");\n        });\n        this.dispatchTo(\"history_reset_from_steps_handlers\");\n    }\n    makeSnapshotStep() {\n        return {\n            selection: {\n                anchorNode: undefined,\n                anchorOffset: undefined,\n                focusNode: undefined,\n                focusOffset: undefined,\n            },\n            mutations: childNodes(this.editable)\n                .filter((node) => this.nodeMap.hasNode(node))\n                .map((node) => ({\n                    type: \"add\",\n                    parentNodeId: \"root\",\n                    nodeId: this.nodeMap.getId(node),\n                    serializedNode: this.serializeNode(node),\n                    nextNodeId: null,\n                })),\n            id: this.steps[this.steps.length - 1]?.id || this.generateId(),\n            previousStepId: undefined,\n        };\n    }\n\n    getHistorySteps() {\n        return this.steps;\n    }\n    /**\n     * @param { HistoryStep } step\n     */\n    processHistoryStep(step) {\n        for (const fn of this.getResource(\"history_step_processors\")) {\n            step = fn(step);\n        }\n        return step;\n    }\n\n    enableObserver() {\n        this.observer.observe(this.editable, {\n            childList: true,\n            subtree: true,\n            attributes: true,\n            attributeOldValue: true,\n            characterData: true,\n            characterDataOldValue: true,\n        });\n    }\n\n    /**\n     * Disable the mutation observer.\n     *\n     * /!\\ This method should be used with extreme caution. Not observing some\n     * mutations could lead to mutations that are impossible to undo/redo.\n     */\n    disableObserver() {\n        const enableObserver = () => {\n            this.enableObserverCallbacks.delete(enableObserver);\n            if (this.enableObserverCallbacks.size > 0) {\n                return;\n            }\n            this.handleObserverRecords();\n            this.isObserverDisabled = false;\n        };\n        this.enableObserverCallbacks.add(enableObserver);\n        this.handleObserverRecords();\n        this.isObserverDisabled = true;\n        return enableObserver;\n    }\n\n    /**\n     * Execute {@link callback} while the MutationObserver is disabled.\n     *\n     * /!\\ This method should be used with extreme caution. Not observing some\n     * mutations could lead to mutations that are impossible to undo/redo.\n     *\n     * /!\\ Do not re-introduce nodes that had been already added to the DOM in\n     * a history step. @see isObservedNode\n     *\n     * @param {Function} callback\n     */\n    ignoreDOMMutations(callback) {\n        const enableObserver = this.disableObserver();\n        try {\n            return callback();\n        } finally {\n            enableObserver();\n        }\n    }\n\n    /**\n     * This is not shared as it is only used internally by the history plugin.\n     * Other plugins should use {@link ignoreDOMMutations} instead.\n     */\n    withObserverOff(callback) {\n        this.handleObserverRecords();\n        this.observer.disconnect();\n        callback();\n        this.enableObserver();\n    }\n\n    handleObserverRecords(dispatch = true) {\n        this.handleNewRecords(this.observer.takeRecords(), dispatch);\n    }\n\n    /**\n     * @param { MutationRecord[] } mutationRecords\n     * @returns { HistoryMutationRecord[] }\n     */\n    processNewRecords(mutationRecords) {\n        if (this.observer.takeRecords().length) {\n            throw new Error(\"MutationObserver has pending records\");\n        }\n        mutationRecords = this.filterMutationRecords(mutationRecords);\n        /** @type {HistoryMutationRecord[]} */\n        let records = this.transformToHistoryMutationRecords(mutationRecords);\n        records = records.filter((record) => !this.isSystemMutationRecord(record));\n        records = this.filterAndAdjustHistoryMutationRecords(records);\n        this.stageRecords(records);\n        records\n            .filter(({ type }) => type === \"attributes\")\n            .forEach((record) => this.dispatchTo(\"attribute_change_handlers\", record));\n        return records;\n    }\n\n    /**\n     * @param {HistoryMutationRecord} record\n     */\n    isValidRecord(record) {\n        switch (record.type) {\n            case \"attributes\":\n            case \"classList\":\n            case \"characterData\":\n                // Filter out no-op\n                return record.value !== record.oldValue;\n            case \"childList\":\n                return (\n                    // Filter out no-op\n                    (record.addedTrees.length || record.removedTrees.length) &&\n                    // Filter out mutation without a valid position for node insertion\n                    (record.previousSibling !== undefined || record.nextSibling !== undefined)\n                );\n        }\n    }\n\n    dispatchContentUpdated() {\n        if (!this.currentStep?.mutations?.length) {\n            return;\n        }\n        // @todo @phoenix remove this?\n        // @todo @phoenix this includes previous mutations that were already\n        // stored in the current step. Ideally, it should only include the new ones.\n        const root = this.getMutationsRoot(this.currentStep.mutations);\n        if (!root) {\n            return;\n        }\n        this.dispatchTo(\"content_updated_handlers\", root);\n    }\n\n    /**\n     * @param { MutationRecord[] } records\n     * @param { boolean } [dispatch]\n     */\n    handleNewRecords(records, dispatch = true) {\n        const processedRecords = this.processNewRecords(records);\n        if (processedRecords.length) {\n            // TODO modify `handleMutations` of web_studio to handle\n            // `undoOperation`\n            if (dispatch) {\n                const stepType = this.currentStep.type;\n                this.dispatchTo(\"handleNewRecords\", processedRecords, stepType);\n            }\n            // Process potential new records adds by handleNewRecords.\n            this.processNewRecords(this.observer.takeRecords());\n            this.dispatchContentUpdated();\n        }\n    }\n\n    /**\n     * @param {HistoryMutationRecord} record\n     */\n    setIdOnAddedNodes(record) {\n        if (record.type !== \"childList\") {\n            return;\n        }\n        record.addedTrees\n            .flatMap(treeToNodes)\n            .filter((node) => !this.nodeMap.hasNode(node))\n            .forEach((node) => this.nodeMap.set(this.generateId(), node));\n    }\n\n    /**\n     * @param { MutationRecord[] } records\n     * @returns { MutationRecord[] }\n     */\n    filterMutationRecords(records) {\n        records = this.filterAttributeMutationRecords(records);\n        records = this.filterSameTextContentMutationRecords(records);\n        records = this.filterOutIntermediateStateMutationRecords(records);\n        return records;\n    }\n\n    /**\n     * @param { MutationRecord[] } records\n     */\n    filterAttributeMutationRecords(records) {\n        return records.filter((record) => {\n            if (record.type !== \"attributes\") {\n                return true;\n            }\n            // Skip the attributes change on the dom.\n            if (record.target === this.editable) {\n                return false;\n            }\n            if (record.attributeName === \"contenteditable\") {\n                return false;\n            }\n            return true;\n        });\n    }\n\n    /**\n     * @param { MutationRecord[] } records\n     * @returns { MutationRecord[] }\n     */\n    filterSameTextContentMutationRecords(records) {\n        const filteredRecords = [];\n        for (const record of records) {\n            if (record.type === \"childList\" && this.isSameTextContentMutation(record)) {\n                const { addedNodes, removedNodes } = record;\n                const oldId = this.nodeMap.getId(removedNodes[0]);\n                if (oldId) {\n                    this.nodeMap.set(oldId, addedNodes[0]);\n                    continue;\n                }\n            }\n            filteredRecords.push(record);\n        }\n        return filteredRecords;\n    }\n\n    /**\n     * Mutation records of type \"attribute\" and \"characterData\" provide the old\n     * value, but not the new value. When multiple mutations occur in the same\n     * batch for an element's attribute or characterData, we only know the final\n     * value of the accumulated changes, which is the DOM's current state.\n     *\n     *  The oldValue provided by mutations after the first one are intermediate\n     *  states that we do not care about. Discarding them allows us to store a\n     *  single record representing the accumulated changes, instead of\n     *  reconstructing the new value introduced by each mutation.\n     *\n     * @param { MutationRecord[] } records\n     */\n    filterOutIntermediateStateMutationRecords(records) {\n        // Keep track of visited attributes per each node\n        const isFirstAttributeOccurrence = trackOccurrencesPair();\n        // Keep track of visited nodes for characterData mutations\n        const isFirstCharDataOccurence = trackOccurrences();\n        const filteredRecords = [];\n        for (const record of records) {\n            if (record.type === \"attributes\") {\n                // Keep only the first mutation record for each (node, attribute) pair.\n                if (isFirstAttributeOccurrence(record.target, record.attributeName)) {\n                    filteredRecords.push(record);\n                }\n            } else if (record.type === \"characterData\") {\n                // Keep only the first charData mutation record for each node.\n                if (isFirstCharDataOccurence(record.target)) {\n                    filteredRecords.push(record);\n                }\n            } else {\n                filteredRecords.push(record);\n            }\n        }\n        return filteredRecords;\n    }\n\n    /**\n     * Transforms MutationRecords into HistoryMutationRecords.\n     *\n     * ChildList record have added/removed trees added to them.\n     * Class attribute records are expanded into multiple classList records.\n     * Attribute records have their oldValue normalized and new value added to it.\n     * CharacterData records have the new value added to it.\n     *\n     * @param {MutationRecord[]} records\n     * @returns {HistoryMutationRecord[]}\n     */\n    transformToHistoryMutationRecords(records) {\n        records = this.transformChildListRecords(records);\n        return records.flatMap((record) => {\n            if (record.type === \"attributes\") {\n                if (record.attributeName === \"class\") {\n                    return this.splitClassMutationRecord(record);\n                }\n                const oldValue = record.oldValue === undefined ? null : record.oldValue;\n                const value = record.target.getAttribute(record.attributeName);\n                return { ...pick(record, \"type\", \"target\", \"attributeName\"), oldValue, value };\n            }\n            if (record.type === \"characterData\") {\n                const value = record.target.textContent;\n                return { ...pick(record, \"type\", \"target\", \"oldValue\"), value };\n            }\n            return record;\n        });\n    }\n\n    /**\n     * ChildList mutation records do not contain information about the\n     * descendants of the added/removed nodes at the time of the mutation. This\n     * method transforms childList mutation records to include information about\n     * the added/removed trees.\n     *\n     * @param {MutationRecord[]} records\n     * @returns {(HistoryMutationRecord|MutationRecord)[]}\n     */\n    transformChildListRecords(records) {\n        /** @type {WeakMap<Node, Node[]>} */\n        const childListSnapshot = new WeakMap();\n        /** @type {(node: Node) => Node[]} */\n        const getChildListSnapshot = (node) => childListSnapshot.get(node) || childNodes(node);\n        /** @type {(node: Node) => Tree} */\n        const makeSnapshotTree = (node) => ({\n            node,\n            children: getChildListSnapshot(node).map(makeSnapshotTree),\n        });\n\n        // Reconstructs the child list before a mutation based on the state\n        // after it and the child list modifications\n        /** @type {(childListAfter: Node[], record: MutationRecord) => Node[]} */\n        const reconstructChildList = (childListAfter, record) => {\n            const { removedNodes, previousSibling, nextSibling } = record;\n            const previousSiblingNodes = previousSibling\n                ? childListAfter.slice(0, childListAfter.indexOf(previousSibling) + 1)\n                : [];\n            const nextSiblingNodes = nextSibling\n                ? childListAfter.slice(childListAfter.indexOf(nextSibling))\n                : [];\n            return [...previousSiblingNodes, ...removedNodes, ...nextSiblingNodes];\n        };\n\n        return records\n            .toReversed()\n            .map((/** @type {MutationRecord} */ record) => {\n                if (record.type !== \"childList\") {\n                    return record;\n                }\n                const transformedRecord = {\n                    ...pick(record, \"type\", \"previousSibling\", \"nextSibling\", \"target\"),\n                    addedTrees: [...record.addedNodes].map(makeSnapshotTree),\n                    removedTrees: [...record.removedNodes].map(makeSnapshotTree),\n                };\n                // Update snapshot for previous mutations\n                const childListAfterMutation = getChildListSnapshot(record.target);\n                const childListBefore = reconstructChildList(childListAfterMutation, record);\n                childListSnapshot.set(record.target, childListBefore);\n                return transformedRecord;\n            })\n            .toReversed();\n    }\n\n    /**\n     * Breaks down a class attribute mutation into individual class\n     * addition/removal records for more precise history tracking.\n     *\n     * @param { MutationRecord } record of type \"attributes\" with attributeName === \"class\"\n     * @returns { MutationRecordClassList[]}\n     */\n    splitClassMutationRecord(record) {\n        // oldValue can be nullish, or have extra spaces\n        const oldValue = record.oldValue?.split(\" \").filter(Boolean);\n        const classesBefore = new Set(oldValue);\n        const classesAfter = new Set(record.target.classList);\n        // @todo: use Set.prototype.difference when it becomes widely available\n        const setDifference = (setA, setB) => {\n            const diff = new Set(setA);\n            setB.forEach((item) => diff.delete(item));\n            return diff;\n        };\n        const addedClasses = setDifference(classesAfter, classesBefore);\n        const removedClasses = setDifference(classesBefore, classesAfter);\n\n        /** @type {(className: string, isAdded: boolean) => MutationRecordClassList } */\n        const createClassRecord = (className, isAdded) => ({\n            type: \"classList\",\n            target: record.target,\n            className,\n            value: isAdded,\n            oldValue: !isAdded,\n        });\n        // Generate records for each class change\n        return [\n            ...[...addedClasses].map((cls) => createClassRecord(cls, true)),\n            ...[...removedClasses].map((cls) => createClassRecord(cls, false)),\n        ];\n    }\n\n    /**\n     * @param { HistoryMutationRecord } record\n     */\n    isSystemMutationRecord(record) {\n        if (record.type === \"attributes\") {\n            return this.mutationFilteredAttributes.has(record.attributeName);\n        }\n        if (record.type === \"classList\") {\n            return this.mutationFilteredClasses.has(record.className);\n        }\n        return false;\n    }\n\n    /**\n     * If the observer is disabled, store the last observed state of the\n     * target's affected property (attribute/class/textContent) and drop the\n     * record.\n     *\n     * Otherwise (observer enabled), update the record as follows:\n     * - mutations targeting an unobserved node are dropped\n     * - mutations of type \"attributes\", \"classList\", and \"characterData\" have\n     * their `oldValue` adjusted to the last observed state of that target's\n     * property\n     * - mutations of type \"childList\" are updated to not include references to\n     * unobserved nodes.\n     *\n     * @param {HistoryMutationRecord[]} records\n     * @returns {HistoryMutationRecord[]}\n     */\n    filterAndAdjustHistoryMutationRecords(records) {\n        this.dispatchTo(\"before_filter_mutation_record_handlers\", records);\n        const savableRecordPredicates = this.getResource(\"savable_mutation_record_predicates\");\n        const isRecordSavable = (record) => savableRecordPredicates.every((p) => p(record));\n        const result = [];\n        for (const record of records) {\n            if (!this.isObservedNode(record.target)) {\n                continue;\n            }\n            if (this.isObserverDisabled || !isRecordSavable(record)) {\n                if (record.type !== \"childList\") {\n                    this.storeOldValue(record);\n                }\n                continue;\n            }\n            const updatedRecord =\n                record.type === \"childList\"\n                    ? this.updateChildListRecord(record)\n                    : this.updateOldValue(record);\n            if (this.isValidRecord(updatedRecord)) {\n                this.setIdOnAddedNodes(record);\n                result.push(updatedRecord);\n            }\n        }\n        return result;\n    }\n\n    /**\n     * Any node that was added to the DOM without a mutation record in a history\n     * step (tipically due to {@link ignoreDOMMutations}) is considered an\n     * unobserved node.\n     *\n     * A known limitation to this approach is when a node that had been present\n     * in the editable before (and thus has an entry in the nodeMap) is re-added\n     * with {@link ignoreDOMMutations}. Such node will not be flagged as\n     * unobserved and history might become inconsistent.\n     *\n     * @param {Node} node\n     * @returns {boolean}\n     */\n    isObservedNode(node) {\n        return this.nodeMap.hasNode(node);\n    }\n\n    /**\n     * This function, alongside @see updateOldValue, ensures mutation records\n     * have the correct historical \"oldValue\" by checking against the last\n     * observed state.\n     *\n     * When the observer is disabled, we store the record's `oldValue` for a\n     * node's attribute/class/textContent as the last observed value.\n     *\n     * As multiple mutations to the same node-attribute/class/textContent can\n     * happen with the observer disabled, we store only the first value\n     * encountered for each node-attribute/class/text. This way, we capture the\n     * state as it was before any modifications in the disabled observer\n     * sequence began.\n     *\n     * @see updateOldValue\n     *\n     * @param {MutationRecordAttributes|MutationRecordClassList|MutationRecordCharacterData} record\n     */\n    storeOldValue(record) {\n        const { stateMap, key } = this.getObservedStateStorage(record);\n        // Only store it if not already stored.\n        if (!stateMap.has(key)) {\n            stateMap.set(key, record.oldValue);\n        }\n    }\n\n    /**\n     * This function, alongside @see storeOldValue, ensures mutation records\n     * have the correct historical \"oldValue\" by checking against the last\n     * observed state.\n     *\n     * When the observer is enabled, it updates a record's `oldValue` with the last\n     * observed state, and removes the entry to prevent reuse. Without removing\n     * the entry, the same historical value might be incorrectly applied to\n     * future mutation records targeting the same attribute/class of the same\n     * element, which would create incorrect history mutations.\n     *\n     * @param {MutationRecordAttributes|MutationRecordClassList|MutationRecordCharacterData} record\n     * @returns {MutationRecordAttributes|MutationRecordClassList|MutationRecordCharacterData}\n     */\n    updateOldValue(record) {\n        const { stateMap, key } = this.getObservedStateStorage(record);\n        if (!stateMap.has(key)) {\n            return record;\n        }\n        const lastObservedValue = stateMap.get(key);\n        // Remove entry, so it won't be used again.\n        stateMap.delete(key);\n        return { ...record, oldValue: lastObservedValue };\n    }\n\n    /**\n     * @param {HistoryMutationRecord} record\n     * @returns { { stateMap: Map, key: string } }\n     */\n    getObservedStateStorage(record) {\n        // Add entry for current target if not already present.\n        if (!this.lastObservedState.has(record.target)) {\n            this.lastObservedState.set(record.target, {\n                attributes: new Map(),\n                classList: new Map(),\n                characterData: new Map(),\n            });\n        }\n        const stateMap = this.lastObservedState.get(record.target)[record.type];\n        switch (record.type) {\n            case \"attributes\":\n                return { stateMap, key: record.attributeName };\n            case \"classList\":\n                return { stateMap, key: record.className };\n            case \"characterData\":\n                return { stateMap, key: \"textContent\" };\n            default:\n                throw new Error(`Unsupported mutation type: ${record.type}`);\n        }\n    }\n\n    /**\n     * @param {MutationRecordChildList} record\n     * @returns {MutationRecordChildList}\n     */\n    updateChildListRecord(record) {\n        // Invalidate sibling references to unobserved nodes\n        const isValidReference = (node) => node === null || this.isObservedNode(node);\n        const updateSibling = (sibling) => (isValidReference(sibling) ? sibling : undefined);\n        const previousSibling = updateSibling(record.previousSibling);\n        const nextSibling = updateSibling(record.nextSibling);\n\n        // Filter out unobserved nodes in removedTrees\n        const removeUnobservedNodes = (tree) => {\n            if (!this.isObservedNode(tree.node)) {\n                return null;\n            }\n            return {\n                node: tree.node,\n                children: tree.children.map(removeUnobservedNodes).filter(Boolean),\n            };\n        };\n        const removedTrees = record.removedTrees.map(removeUnobservedNodes).filter(Boolean);\n\n        return {\n            ...record,\n            previousSibling,\n            nextSibling,\n            removedTrees,\n        };\n    }\n\n    /**\n     * Check if a mutation consists of removing and adding a single text node\n     * with the same text content, which occurs in Firefox but is optimized\n     * away in Chrome.\n     *\n     * @param { MutationRecord } record\n     */\n    isSameTextContentMutation(record) {\n        const { addedNodes, removedNodes } = record;\n        return (\n            record.type === \"childList\" &&\n            addedNodes.length === 1 &&\n            removedNodes.length === 1 &&\n            addedNodes[0].nodeType === Node.TEXT_NODE &&\n            removedNodes[0].nodeType === Node.TEXT_NODE &&\n            addedNodes[0].textContent === removedNodes[0].textContent\n        );\n    }\n\n    /**\n     * Set the serialized selection of the currentStep.\n     *\n     * This method is used to save a serialized selection in the currentStep.\n     * It will be necessary if the step is reverted at some point because we need\n     * to set the selection to where it was before any mutation was made.\n     *\n     * It means that we should not call this method in the middle of mutations\n     * because if a selection is set onto a node that is edited/added/removed\n     * within the same step, it might become impossible to set the selection\n     * when reverting the step.\n     */\n    stageSelection() {\n        this.stageFocus();\n        const selection = this.dependencies.selection.getEditableSelection();\n        if (this.getIsCurrentStepModified()) {\n            console.warn(\n                `should not have any \"characterData\", \"remove\" or \"add\" mutations in current step when you update the selection`\n            );\n            return;\n        }\n        this.currentStep.selection = this.serializeSelection(selection);\n    }\n    /**\n     * Set the serialized focus of the currentStep.\n     */\n    stageFocus() {\n        let activeElement = this.document.activeElement;\n        if (activeElement.contains(this.editable)) {\n            activeElement = this.editable;\n        }\n        if (this.editable.contains(activeElement)) {\n            this.currentStep.activeElementId = this.setNodeId(activeElement);\n        }\n    }\n    /**\n     * @param { HistoryMutationRecord[] } records\n     */\n    stageRecords(records) {\n        for (const record of records) {\n            switch (record.type) {\n                case \"characterData\":\n                case \"classList\":\n                case \"attributes\": {\n                    const nodeId = this.nodeMap.getId(record.target);\n                    this.currentStep.mutations.push({ ...omit(record, \"target\"), nodeId });\n                    break;\n                }\n                case \"childList\": {\n                    this.currentStep.mutations.push(...this.splitChildListRecord(record));\n                    break;\n                }\n            }\n        }\n    }\n\n    /**\n     * @param {MutationRecordChildList} record\n     * @returns { (HistoryMutationRemove|HistoryMutationAdd)[] }\n     */\n    splitChildListRecord(record) {\n        const parentNodeId = this.nodeMap.getId(record.target);\n        if (!parentNodeId) {\n            throw new Error(\"Unknown parent node\");\n        }\n\n        const makeSingleNodeRecords = (trees, type) =>\n            trees.map((tree, index, treeList) => {\n                const node = tree.node;\n                const nodeList = treeList.map((t) => t.node);\n                const [previousSibling, nextSibling] =\n                    type === \"add\"\n                        ? [nodeList[index - 1] || record.previousSibling, record.nextSibling]\n                        : [record.previousSibling, nodeList[index + 1] || record.nextSibling];\n                const [nextNodeId, previousNodeId] = [nextSibling, previousSibling].map((sibling) =>\n                    // Preserve undefined and null values\n                    sibling ? this.nodeMap.getId(sibling) : sibling\n                );\n                const nodeId = this.nodeMap.getId(node);\n                const serializedNode = this.serializeTree(tree);\n                return { type, nodeId, parentNodeId, serializedNode, nextNodeId, previousNodeId };\n            });\n\n        return [\n            ...makeSingleNodeRecords(record.removedTrees, \"remove\"),\n            ...makeSingleNodeRecords(record.addedTrees, \"add\"),\n        ];\n    }\n\n    applyCustomMutation({ apply, revert }) {\n        apply();\n        this.addCustomMutation({ apply, revert });\n    }\n\n    addCustomMutation({ apply, revert }) {\n        const customMutation = {\n            type: \"custom\",\n            apply: () => {\n                apply();\n                this.addCustomMutation({ apply, revert });\n            },\n            revert: () => {\n                revert();\n                this.addCustomMutation({ apply: revert, revert: apply });\n            },\n        };\n        this.currentStep.mutations.push(customMutation);\n    }\n\n    /**\n     * @param { Node } node\n     */\n    setNodeId(node) {\n        let id = this.nodeMap.getId(node);\n        if (!id) {\n            id = node === this.editable ? \"root\" : this.generateId();\n            this.nodeMap.set(id, node);\n            node = node.firstChild;\n            while (node) {\n                this.setNodeId(node);\n                node = node.nextSibling;\n            }\n        }\n        return id;\n    }\n    generateId() {\n        // No need for secure random number.\n        return Math.floor(Math.random() * Math.pow(2, 52)).toString();\n    }\n\n    /**\n     * @param { Object } [params]\n     * @param { \"original\"|\"undo\"|\"redo\"|\"restore\" } [params.type]\n     * @param {Object} [params.extraStepInfos]\n     */\n    addStep({ type = \"original\", extraStepInfos } = {}) {\n        // @todo @phoenix should we allow to pause the making of a step?\n        // if (!this.stepsActive) {\n        //     return;\n        // }\n        // @todo @phoenix link zws plugin\n        // this._resetLinkZws();\n        // @todo @phoenix sanitize plugin\n        // this.sanitize();\n\n        // Set the state of the step here.\n        // That way, the state of undo and redo is truly accessible when\n        // executing the onChange callback.\n        // It is useful for external components if they execute shared.can[Undo|Redo]\n        const currentStep = this.currentStep;\n        currentStep.type = type;\n        this.handleObserverRecords();\n        const currentMutationsCount = currentStep.mutations.length;\n        if (currentMutationsCount === 0) {\n            return false;\n        }\n        const stepCommonAncestor = this.getMutationsRoot(currentStep.mutations) || this.editable;\n        this.dispatchTo(\"normalize_handlers\", stepCommonAncestor, type);\n        this.handleObserverRecords(false);\n        if (currentMutationsCount === currentStep.mutations.length) {\n            // If there was no registered mutation during the normalization step,\n            // force the dispatch of a content_updated to allow i.e. the hint\n            // plugin to react to non-observed changes (i.e. a div becoming\n            // a baseContainer).\n            this.dispatchContentUpdated();\n        }\n\n        currentStep.previousStepId = this.steps.at(-1)?.id;\n\n        currentStep.selectionAfter = this.serializeSelection(\n            this.dependencies.selection.getEditableSelection()\n        );\n        this.steps.push(currentStep);\n        // @todo @phoenix add this in the linkzws plugin.\n        // this._setLinkZws();\n        this.dispatchTo(\"before_add_step_handlers\");\n        if (extraStepInfos) {\n            currentStep.extraStepInfos = extraStepInfos;\n        }\n        this.currentStep = this.processHistoryStep({\n            id: this.generateId(),\n            type: undefined,\n            selection: {},\n            mutations: [],\n            previousStepId: undefined,\n            extraStepInfos: {},\n        });\n        this.stageSelection();\n        this.dispatchTo(\"step_added_handlers\", {\n            step: currentStep,\n            stepCommonAncestor,\n            isPreviewing: this.isPreviewing,\n        });\n        this.config.onChange?.({ isPreviewing: this.isPreviewing });\n        return currentStep;\n    }\n    canUndo() {\n        return this.getNextUndoIndex() > 0;\n    }\n    canRedo() {\n        return this.getNextRedoIndex() > 0;\n    }\n    undo() {\n        if (this.steps.length === 1) {\n            return;\n        }\n        this.handleObserverRecords();\n        // The last step is considered an uncommited draft so always revert it.\n        const lastStep = this.currentStep;\n        this.revertMutations(lastStep.mutations);\n        // Discard mutations generated by the revert.\n        this.observer.takeRecords();\n        // Clean the last step otherwise if no other step is created after, the\n        // mutations of the revert itself will be added to the same step and\n        // grow exponentially at each undo.\n        lastStep.mutations = [];\n\n        const pos = this.getNextUndoIndex();\n        let revertedStep;\n        if (pos > 0) {\n            revertedStep = this.steps[pos];\n            this.revertedSteps.add(revertedStep.id);\n            this.revertMutations(revertedStep.mutations, { forNewStep: true });\n            this.setSerializedFocus(revertedStep.activeElementId);\n            this.stageFocus();\n            this.setSerializedSelection(revertedStep.selection);\n            this.currentStep.selection = revertedStep.selectionAfter;\n            this.addStep({ type: \"undo\", extraStepInfos: revertedStep.extraStepInfos });\n            // Consider the last position of the history as an undo.\n        }\n        this.dispatchTo(\"post_undo_handlers\", revertedStep);\n    }\n    redo() {\n        this.handleObserverRecords();\n        // Current step is considered an uncommitted draft, so revert it,\n        // otherwise a redo would not be possible.\n        this.revertMutations(this.currentStep.mutations);\n        // Discard mutations generated by the revert.\n        this.observer.takeRecords();\n        // At this point, _currentStep.mutations contains the current step's\n        // mutations plus the ones that revert it, with net effect zero.\n        this.currentStep.mutations = [];\n\n        const pos = this.getNextRedoIndex();\n        let revertedStep;\n        if (pos > 0) {\n            revertedStep = this.steps[pos];\n            this.revertedSteps.add(revertedStep.id);\n            this.revertMutations(revertedStep.mutations, { forNewStep: true });\n            this.setSerializedFocus(revertedStep.activeElementId);\n            this.stageFocus();\n            this.setSerializedSelection(revertedStep.selection);\n            this.currentStep.selection = revertedStep.selectionAfter;\n            this.addStep({ type: \"redo\", extraStepInfos: revertedStep.extraStepInfos });\n        }\n        this.dispatchTo(\"post_redo_handlers\", revertedStep);\n    }\n    /**\n     * @param { SerializedSelection } selection\n     */\n    setSerializedSelection(selection) {\n        if (!selection.anchorNodeId) {\n            return;\n        }\n        const anchorNode = this.nodeMap.getNode(selection.anchorNodeId);\n        if (!anchorNode) {\n            return;\n        }\n        const newSelection = {\n            anchorNode,\n            anchorOffset: selection.anchorOffset,\n        };\n        const focusNode = this.nodeMap.getNode(selection.focusNodeId);\n        if (focusNode) {\n            newSelection.focusNode = focusNode;\n            newSelection.focusOffset = selection.focusOffset;\n        }\n        this.dependencies.selection.setSelection(newSelection, { normalize: false });\n        // @todo @phoenix add this in the selection or table plugin.\n        // // If a table must be selected, ensure it's in the same tick.\n        // this._handleSelectionInTable();\n    }\n    /**\n     * @param { string } activeElementId\n     */\n    setSerializedFocus(activeElementId) {\n        const elementToFocus =\n            activeElementId === \"root\"\n                ? this.editable\n                : activeElementId && this.nodeMap.getNode(activeElementId);\n        if (elementToFocus?.isConnected && elementToFocus !== this.document.activeElement) {\n            elementToFocus.focus();\n        }\n    }\n    /**\n     * Get the step index in the history to undo.\n     * Return -1 if no undo index can be found.\n     */\n    getNextUndoIndex() {\n        // Go back to first step that can be undone (\"original\" or \"redo\").\n        for (let index = this.steps.length - 1; index >= 0; index--) {\n            const step = this.steps[index];\n            if (!this.isReversibleStep(index) || this.discardedSteps.has(step.id)) {\n                continue;\n            }\n            if ([\"original\", \"redo\"].includes(step.type) && !this.revertedSteps.has(step.id)) {\n                return index;\n            }\n        }\n        // There is no steps left to be undone, return an index that does not\n        // point to any step\n        return -1;\n    }\n    /**\n     * Meant to be overriden.\n     *\n     * @param { number } index\n     */\n    isReversibleStep(index) {\n        const step = this.steps[index];\n        if (!step) {\n            return false;\n        }\n        return !this.getResource(\"unreversible_step_predicates\").some((predicate) =>\n            predicate(step)\n        );\n    }\n    /**\n     * Get the step index in the history to redo.\n     * Return -1 if no redo index can be found.\n     */\n    getNextRedoIndex() {\n        // Look for an \"undo\" step that has not yet been redone. Stop search if\n        // a \"original\" step is found.\n        for (let index = this.steps.length - 1; index >= 0; index--) {\n            const step = this.steps[index];\n            if (!this.isReversibleStep(index) || this.discardedSteps.has(step.id)) {\n                continue;\n            }\n            if (step.type === \"original\") {\n                return -1;\n            }\n            if (step.type === \"undo\" && !this.revertedSteps.has(step.id)) {\n                return index;\n            }\n        }\n        return -1;\n    }\n    /**\n     * Insert a step in the history.\n     *\n     * @param { HistoryStep } newStep\n     * @param { number } index\n     */\n    addExternalStep(newStep, index) {\n        this.withObserverOff(() => {\n            // The last step is an uncommited draft, revert it first\n            this.revertMutations(this.currentStep.mutations);\n\n            const stepsAfterNewStep = this.steps.slice(index);\n\n            for (const stepToRevert of stepsAfterNewStep.slice().reverse()) {\n                this.revertMutations(stepToRevert.mutations);\n            }\n            this.applyMutations(newStep.mutations);\n            this.dispatchTo(\n                \"normalize_handlers\",\n                this.getMutationsRoot(newStep.mutations) || this.editable\n            );\n            this.steps.splice(index, 0, newStep);\n            for (const stepToApply of stepsAfterNewStep) {\n                this.applyMutations(stepToApply.mutations);\n            }\n            // Reapply the uncommited draft, since this is not an operation which should cancel it\n            this.applyMutations(this.currentStep.mutations);\n            this.dispatchTo(\"external_step_added_handlers\");\n        });\n    }\n    /**\n     * @param { HistoryMutation[] } mutations\n     * @param { Object } options\n     * @param { boolean } options.forNewStep whether the mutations will be used\n     *        to create a new step\n     * @param { boolean } options.reverse whether the mutations are the reverse\n     *        of other mutations\n     */\n    applyMutations(mutations, { forNewStep = false, reverse } = {}) {\n        if (forNewStep) {\n            this.fixClassListMutationsForNewStep(mutations);\n        }\n        for (const mutation of mutations) {\n            switch (mutation.type) {\n                case \"custom\": {\n                    mutation.apply();\n                    break;\n                }\n                case \"characterData\": {\n                    const node = this.nodeMap.getNode(mutation.nodeId);\n                    if (node) {\n                        node.textContent = mutation.value;\n                    }\n                    break;\n                }\n                case \"classList\": {\n                    const node = this.nodeMap.getNode(mutation.nodeId);\n                    if (node) {\n                        toggleClass(node, mutation.className, mutation.value);\n                    }\n                    break;\n                }\n                case \"attributes\": {\n                    const node = this.nodeMap.getNode(mutation.nodeId);\n                    if (node) {\n                        let value = mutation.value;\n                        for (const cb of this.getResource(\"attribute_change_processors\")) {\n                            value = cb(\n                                {\n                                    target: node,\n                                    attributeName: mutation.attributeName,\n                                    oldValue: mutation.oldValue,\n                                    value,\n                                    reverse,\n                                },\n                                { forNewStep }\n                            );\n                        }\n                        this.setAttribute(node, mutation.attributeName, value);\n                    }\n                    break;\n                }\n                case \"remove\": {\n                    this.applyRemoveMutation(mutation);\n                    break;\n                }\n                case \"add\": {\n                    this.applyAddMutation(mutation);\n                    break;\n                }\n            }\n        }\n    }\n\n    /**\n     * When applying mutations for a new step, we expect them to produce\n     * observable mutations, which will then be stored in a new step. However,\n     * there are situations where applying a classList mutation would not\n     * produce an observable mutation:\n     * - adding a class that is already present\n     * - removing a class that is already absent\n     * These scenarios might happen due to the class having been already added\n     * or removed by a previous unobserved mutation. We want, nevertheless to\n     * produce the observable mutation of adding/removing this class, as this\n     * does correspond to a state change in observable history and should be\n     * included in the new step. In order to produce such observable mutations,\n     * we set the dom state to the one that would produce the desired result.\n     * This is equivalent to restoring the dom to the observed state in recorded\n     * history before applying a mutation, that is, oldValue (as oldValue is\n     * always !value for staged classList records).\n     *\n     * @param { HistoryMutation[] } mutations\n     */\n    fixClassListMutationsForNewStep(mutations) {\n        const isFirstOcurrence = trackOccurrencesPair();\n        // Mutations that when applied would not produce observable classList mutations\n        const nonObservableClassMutations = mutations\n            .filter((mutation) => mutation.type === \"classList\")\n            .filter(({ nodeId, className }) => isFirstOcurrence(nodeId, className))\n            .map((mutation) => ({ ...mutation, node: this.nodeMap.getNode(mutation.nodeId) }))\n            .filter(({ node, className, value }) => value === node?.classList.contains(className));\n        if (nonObservableClassMutations.length) {\n            const setToOldValue = ({ node, className, oldValue }) =>\n                toggleClass(node, className, oldValue);\n            this.withObserverOff(() => nonObservableClassMutations.forEach(setToOldValue));\n        }\n    }\n\n    /**\n     * @param {HistoryMutationRemove} mutation\n     */\n    applyRemoveMutation(mutation) {\n        const parent = this.nodeMap.getNode(mutation.parentNodeId);\n        const toRemove = this.nodeMap.getNode(mutation.nodeId);\n        if (!toRemove) {\n            console.warn(\"Mutation could not be applied, node to remove is unknown.\", mutation);\n            return;\n        }\n        if (toRemove.parentElement !== parent) {\n            console.warn(\"Mutation could not be applied, parent node does not match.\", mutation);\n            return;\n        }\n        toRemove.remove();\n    }\n\n    /**\n     * @param {HistoryMutationAdd} mutation\n     */\n    applyAddMutation(mutation) {\n        const { nodeId, serializedNode, parentNodeId, nextNodeId, previousNodeId } = mutation;\n\n        const toAdd = this.nodeMap.getNode(nodeId) || this.unserializeNode(serializedNode);\n        if (!toAdd) {\n            return;\n        }\n\n        const parent = this.nodeMap.getNode(parentNodeId);\n        if (!parent) {\n            console.warn(\"Mutation could not be applied, parent node is missing.\", mutation);\n            return;\n        }\n        if (previousNodeId === null) {\n            parent.prepend(toAdd);\n            return;\n        }\n        if (nextNodeId === null) {\n            parent.append(toAdd);\n            return;\n        }\n        const isValid = (node) => node?.parentNode === parent;\n        const previousNode = this.nodeMap.getNode(previousNodeId);\n        if (isValid(previousNode)) {\n            previousNode.after(toAdd);\n            return;\n        }\n        const nextNode = this.nodeMap.getNode(nextNodeId);\n        if (isValid(nextNode)) {\n            nextNode.before(toAdd);\n            return;\n        }\n        console.warn(\"Mutation could not be applied, reference nodes are invalid.\", mutation);\n    }\n\n    revertMutations(mutations, { forNewStep = false } = {}) {\n        const revertedMutations = mutations.map((mutation) => {\n            switch (mutation.type) {\n                case \"characterData\":\n                case \"classList\":\n                case \"attributes\":\n                    return { ...mutation, value: mutation.oldValue, oldValue: mutation.value };\n                case \"remove\":\n                    return { ...mutation, type: \"add\" };\n                case \"add\":\n                    return { ...mutation, type: \"remove\" };\n                case \"custom\":\n                    return { ...mutation, apply: mutation.revert, revert: mutation.apply };\n                default:\n                    throw new Error(`Unknown mutation type: ${mutation.type}`);\n            }\n        });\n        this.applyMutations(revertedMutations.toReversed(), { forNewStep, reverse: true });\n    }\n\n    /**\n     * Serialize an editor selection.\n     * @param { EditorSelection } selection\n     * @returns { SerializedSelection }\n     */\n    serializeSelection(selection) {\n        return {\n            anchorNodeId: this.nodeMap.getId(selection.anchorNode),\n            anchorOffset: selection.anchorOffset,\n            focusNodeId: this.nodeMap.getId(selection.focusNode),\n            focusOffset: selection.focusOffset,\n        };\n    }\n    /**\n     * Returns the deepest common ancestor element of the given mutations.\n     * @param {HistoryMutation[]} mutations - The array of mutations.\n     * @returns {HTMLElement|null} - The common ancestor element.\n     */\n    getMutationsRoot(mutations) {\n        const nodes = mutations\n            .map((m) => this.nodeMap.getNode(m.parentNodeId || m.nodeId))\n            .filter((node) => this.editable.contains(node));\n        let commonAncestor = getCommonAncestor(nodes, this.editable);\n        if (commonAncestor?.nodeType === Node.TEXT_NODE) {\n            commonAncestor = commonAncestor.parentElement;\n        }\n        return commonAncestor;\n    }\n    /**\n     * Returns a function that can be later called to revert history to the\n     * current state.\n     * @returns {Function}\n     */\n    makeSavePoint() {\n        this.handleObserverRecords();\n        const draftMutations = this.currentStep.mutations.slice();\n        const step = this.steps.at(-1);\n        let applied = false;\n        // TODO ABD TODO @phoenix: selection may become obsolete, it should evolve with mutations.\n        const selectionToRestore = this.dependencies.selection.preserveSelection();\n        const extraToRestore = { ...this.currentStep.extraStepInfos };\n        return () => {\n            if (applied) {\n                return;\n            }\n            applied = true;\n            const stepIndex = this.steps.findLastIndex((item) => item === step);\n            this.restoreToStep(stepIndex);\n            // Apply draft mutations to recover the same currentStep state\n            // as before.\n            this.applyMutations(draftMutations, { forNewStep: true });\n            this.handleObserverRecords();\n            // TODO ABD TODO @phoenix: evaluate if the selection is not restorable at the desired position\n            selectionToRestore.restore();\n            this.currentStep.extraStepInfos = extraToRestore;\n            this.dispatchTo(\"restore_savepoint_handlers\");\n        };\n    }\n    /**\n     * Creates a set of functions to preview, apply, and revert an operation.\n     * @param {Function} operation\n     * @returns {PreviewableOperation}\n     */\n    makePreviewableOperation(operation) {\n        let revertOperation = () => {};\n\n        return {\n            preview: (...args) => {\n                revertOperation();\n                revertOperation = this.makeSavePoint();\n                this.isPreviewing = true;\n                this.stageSelection();\n                operation(...args);\n                // todo: We should not add a step on preview as it would send\n                // unnecessary steps in collaboration and let the other peer see\n                // what we preview.\n                //\n                // The operation should be similar than in the 'commit'\n                // (normalize etc...) hence the 'addStep' (but we need to remove\n                // it for the collaboration).\n                this.addStep();\n            },\n            commit: (...args) => {\n                revertOperation();\n                this.isPreviewing = false;\n                operation(...args);\n                this.addStep();\n            },\n            revert: () => {\n                revertOperation();\n                revertOperation = () => {};\n                this.isPreviewing = false;\n            },\n        };\n    }\n\n    /**\n     * Creates a set of functions to preview, apply, and revert an async operation.\n     * @param {Function} operation\n     * @returns {PreviewableOperation}\n     */\n    makePreviewableAsyncOperation(operation) {\n        let revertOperation = () => {};\n\n        return {\n            preview: async (...args) => {\n                await revertOperation();\n                const def = new Deferred();\n                const revertSavePoint = this.makeSavePoint();\n                revertOperation = async () => {\n                    await def;\n                    revertSavePoint();\n                };\n                this.isPreviewing = true;\n                try {\n                    await operation(...args);\n                } catch (error) {\n                    revertSavePoint();\n                    throw error;\n                } finally {\n                    def.resolve();\n                }\n                if (this.isDestroyed) {\n                    return;\n                }\n                // todo: We should not add a step on preview as it would send\n                // unnecessary steps in collaboration and let the other peer see\n                // what we preview.\n                //\n                // The operation should be similar than in the 'commit'\n                // (normalize etc...) hence the 'addStep' (but we need to remove\n                // it for the collaboration).\n                this.addStep();\n            },\n            commit: async (...args) => {\n                await revertOperation();\n                this.isPreviewing = false;\n                const revertSavePoint = this.makeSavePoint();\n                try {\n                    await operation(...args);\n                } catch (error) {\n                    revertSavePoint();\n                    throw error;\n                }\n                if (this.isDestroyed) {\n                    return;\n                }\n                this.addStep();\n            },\n            revert: async () => {\n                await revertOperation();\n                revertOperation = () => {};\n                this.isPreviewing = false;\n            },\n        };\n    }\n\n    /**\n     * Restores the editable to the state of a previous step.\n     * It does so by discarding the current draft and reverting reversible steps\n     * until the specified step index, while ensuring that irreversible steps\n     * are maintained. This will add a new \"restore\" step and set the reverted\n     * steps's state to \"discarded\".\n     *\n     * @param {Number} stepIndex\n     */\n    restoreToStep(stepIndex) {\n        // Discard current draft.\n        this.handleObserverRecords();\n        this.revertMutations(this.currentStep.mutations);\n        this.observer.takeRecords();\n        this.currentStep.mutations = [];\n        let lastRevertedStep = this.currentStep;\n\n        if (stepIndex === this.steps.length - 1) {\n            return;\n        }\n        // Revert all mutations until stepIndex, and mark all reversible\n        // steps as \"discarded\" in the process (typically current peer steps).\n        for (let i = this.steps.length - 1; i > stepIndex; i--) {\n            const currentStep = this.steps[i];\n            this.revertMutations(currentStep.mutations, { forNewStep: true });\n            // Process (filter, handle and stage) mutations so that the\n            // attribute comparison for the state change is done with the\n            // intermediate attribute value and not with the final value in the\n            // DOM after all steps were reverted then applied again.\n            this.processNewRecords(this.observer.takeRecords());\n            if (this.isReversibleStep(i)) {\n                this.discardedSteps.add(currentStep.id);\n                lastRevertedStep = currentStep;\n            }\n        }\n        // Re-apply every non reversible steps (typically collaborators steps).\n        for (let i = stepIndex + 1; i < this.steps.length; i++) {\n            const currentStep = this.steps[i];\n            if (!this.isReversibleStep(i)) {\n                this.applyMutations(currentStep.mutations, { forNewStep: true });\n                this.processNewRecords(this.observer.takeRecords());\n            }\n        }\n        // TODO ABD TODO @phoenix: review selections, this selection could be obsolete\n        // depending on the non-reversible steps that were applied.\n        this.setSerializedSelection(lastRevertedStep.selection);\n        // Register resulting mutations as a new \"restore\" step (prevent undo).\n        this.dispatchContentUpdated();\n        this.addStep({ type: \"restore\" });\n    }\n\n    setStepExtra(key, value) {\n        this.currentStep.extraStepInfos[key] = value;\n    }\n\n    disableIsCurrentStepModifiedWarning() {\n        this.ignoreIsCurrentStepModified = true;\n        return () => {\n            this.ignoreIsCurrentStepModified = false;\n        };\n    }\n\n    getIsCurrentStepModified() {\n        if (this.ignoreIsCurrentStepModified) {\n            return false;\n        }\n        return this.currentStep.mutations.find((m) =>\n            [\"characterData\", \"remove\", \"add\"].includes(m.type)\n        );\n    }\n\n    /**\n     * @param { Node } node\n     * @param { string } attributeName\n     * @param { string } attributeValue\n     */\n    setAttribute(node, attributeName, attributeValue) {\n        if (this.delegateTo(\"set_attribute_overrides\", node, attributeName, attributeValue)) {\n            return;\n        }\n\n        // if attributeValue is falsy but not null, we still need to apply it\n        if (attributeValue !== null) {\n            node.setAttribute(attributeName, attributeValue);\n        } else {\n            node.removeAttribute(attributeName);\n        }\n    }\n    /**\n     * Serialize a node and its children.\n     * @param { Node } node\n     */\n    serializeNode(node) {\n        return this.serializeTree(nodeToTree(node));\n    }\n    /**\n     * Unserialize a node and its children.\n     *\n     * @param { SerializedNode } node\n     * @returns { Node }\n     */\n    unserializeNode(node) {\n        let [unserializedNode, newNodesMap] = this._unserializeNode(node, this.nodeMap);\n        if (!unserializedNode) {\n            return null;\n        }\n        const fakeNode = this.document.createElement(\"fake-el\");\n        fakeNode.appendChild(unserializedNode);\n        this.dependencies.sanitize.sanitize(fakeNode, { IN_PLACE: true });\n        unserializedNode = fakeNode.firstChild;\n        if (!unserializedNode) {\n            return null;\n        }\n        // Only assing id to the remaining nodes, otherwise the removed nodes\n        // will still be accessible through the nodeMap and could lead to\n        // security issues.\n        for (const node of [unserializedNode, ...descendants(unserializedNode)]) {\n            if (this.nodeMap.hasNode(node)) {\n                continue;\n            }\n            const id = newNodesMap.get(node);\n            if (id) {\n                this.nodeMap.set(id, node);\n            }\n        }\n        return unserializedNode;\n    }\n\n    /**\n     * @param {Tree} tree\n     * @returns {SerializedNode|null}\n     */\n    serializeTree(tree) {\n        const node = tree.node;\n        const nodeId = this.nodeMap.getId(node);\n        if (!nodeId) {\n            return null;\n        }\n        const result = {\n            nodeType: node.nodeType,\n            nodeId: nodeId,\n        };\n        if (node.nodeType === Node.TEXT_NODE) {\n            result.textValue = node.nodeValue;\n        } else if (node.nodeType === Node.ELEMENT_NODE) {\n            let childTreesToSerialize = tree.children;\n            for (const cb of this.getResource(\"serializable_descendants_processors\")) {\n                childTreesToSerialize = cb(node, childTreesToSerialize);\n            }\n            result.tagName = node.tagName;\n            result.attributes = Object.fromEntries(\n                [...node.attributes].map((attr) => [attr.name, attr.value])\n            );\n            result.children = childTreesToSerialize\n                .map((tree) => this.serializeTree(tree))\n                .filter(Boolean);\n        }\n        return result;\n    }\n    /**\n     * Unserialize a node and its children.\n     * @param { SerializedNode } serializedNode\n     * @param { NodeMap} nodeMap\n     * @param { Map<Node, string> } _map\n     * @returns { [Node, Map<Node, string>] }\n     */\n    _unserializeNode(serializedNode, nodeMap = new NodeMap(), _map = new Map()) {\n        let node = nodeMap.getNode(serializedNode.nodeId);\n        if (node) {\n            return [node, _map];\n        }\n        if (serializedNode.nodeType === Node.TEXT_NODE) {\n            node = this.document.createTextNode(serializedNode.textValue);\n        } else if (serializedNode.nodeType === Node.ELEMENT_NODE) {\n            node = this.document.createElement(serializedNode.tagName);\n            for (const key in serializedNode.attributes) {\n                node.setAttribute(key, serializedNode.attributes[key]);\n            }\n            node.append(\n                ...serializedNode.children\n                    .map((child) => this._unserializeNode(child, nodeMap, _map)[0])\n                    .filter(Boolean)\n            );\n        } else {\n            console.warn(\"unknown node type\");\n            return [null, _map];\n        }\n        _map.set(node, serializedNode.nodeId);\n        return [node, _map];\n    }\n\n    _onDocumentBeforeInput(ev) {\n        if (this.editable.contains(ev.target)) {\n            return;\n        }\n        if ([\"historyUndo\", \"historyRedo\"].includes(ev.inputType)) {\n            this._onKeyupResetContenteditableNodes.push(\n                ...this.editable.querySelectorAll(\"[contenteditable=true]\")\n            );\n            if (this.editable.getAttribute(\"contenteditable\") === \"true\") {\n                this._onKeyupResetContenteditableNodes.push(this.editable);\n            }\n\n            for (const node of this._onKeyupResetContenteditableNodes) {\n                node.setAttribute(\"contenteditable\", false);\n            }\n        }\n    }\n\n    _onDocumentInput(ev) {\n        if (\n            [\"historyUndo\", \"historyRedo\"].includes(ev.inputType) &&\n            this._onKeyupResetContenteditableNodes.length\n        ) {\n            for (const node of this._onKeyupResetContenteditableNodes) {\n                node.setAttribute(\"contenteditable\", true);\n            }\n            this._onKeyupResetContenteditableNodes = [];\n        }\n    }\n}\n\n/**\n * @param {Node} node\n * @returns {Tree}\n */\nexport function nodeToTree(node) {\n    return {\n        node,\n        children: childNodes(node).map(nodeToTree),\n    };\n}\n\n/**\n * @param {Tree} tree\n * @returns {Node[]}\n */\nfunction treeToNodes(tree) {\n    return [tree.node, ...tree.children.flatMap(treeToNodes)];\n}\n\n/**\n * Bidirectional map between IDs (string) and Node objects.\n */\nclass NodeMap {\n    constructor() {\n        // Private properties enclosed in the constructor\n        /** @type {Map<string, Node>} */\n        const idToNodeMap = new Map();\n        /** @type {Map<Node, string>} */\n        const nodeToIdMap = new Map();\n\n        // Public methods\n        /** @type {(id: string, node: Node) => void} */\n        this.set = (id, node) => {\n            if (!id || !node) {\n                throw new Error(\"Id and Node cannot be nullish\");\n            }\n            // Remove old mappings\n            const oldNode = idToNodeMap.get(id);\n            nodeToIdMap.delete(oldNode);\n            const oldId = nodeToIdMap.get(node);\n            idToNodeMap.delete(oldId);\n            // Set new mappings\n            idToNodeMap.set(id, node);\n            nodeToIdMap.set(node, id);\n        };\n\n        /** @type {(id: string) => Node | undefined} */\n        this.getNode = (id) => idToNodeMap.get(id);\n\n        /** @type {(node: Node) => string | undefined} */\n        this.getId = (node) => nodeToIdMap.get(node);\n\n        /** @type {(node: Node) => boolean} */\n        this.hasNode = (node) => nodeToIdMap.has(node);\n    }\n}\n", "import { Plugin } from \"../plugin\";\n\n/**\n * @typedef {((ev: InputEvent) => void)[]} beforeinput_handlers\n * @typedef {((ev: InputEvent) => void)[]} input_handlers\n */\n\nexport class InputPlugin extends Plugin {\n    static id = \"input\";\n    static dependencies = [\"history\"];\n    setup() {\n        this.addDomListener(this.editable, \"beforeinput\", this.onBeforeInput);\n        this.addDomListener(this.editable, \"input\", this.onInput);\n    }\n\n    onBeforeInput(ev) {\n        this.dependencies.history.stageSelection();\n        this.dispatchTo(\"beforeinput_handlers\", ev);\n    }\n\n    onInput(ev) {\n        this.dependencies.history.addStep();\n        this.dispatchTo(\"input_handlers\", ev);\n    }\n}\n", "import { splitTextNode } from \"@html_editor/utils/dom\";\nimport { Plugin } from \"../plugin\";\nimport { CTGROUPS, CTYPES } from \"../utils/content_types\";\nimport { getState, isFakeLineBreak, prepareUpdate } from \"../utils/dom_state\";\nimport { DIRECTIONS, leftPos, rightPos } from \"../utils/position\";\nimport { closestElement } from \"@html_editor/utils/dom_traversal\";\nimport { closestBlock, isBlock } from \"../utils/blocks\";\nimport { nextLeaf } from \"../utils/dom_info\";\n\n/**\n * @typedef { Object } LineBreakShared\n * @property { LineBreakPlugin['insertLineBreak'] } insertLineBreak\n * @property { LineBreakPlugin['insertLineBreakElement'] } insertLineBreakElement\n * @property { LineBreakPlugin['insertLineBreakNode'] } insertLineBreakNode\n */\n\n/**\n * @typedef {(() => void)[]} before_line_break_handlers\n * @typedef {((params: { targetNode: Element, targetOffset: number }) => void | true)[]} insert_line_break_element_overrides\n */\n\nexport class LineBreakPlugin extends Plugin {\n    static dependencies = [\"selection\", \"history\", \"input\", \"delete\"];\n    static id = \"lineBreak\";\n    static shared = [\"insertLineBreak\", \"insertLineBreakNode\", \"insertLineBreakElement\"];\n    /** @type {import(\"plugins\").EditorResources} */\n    resources = {\n        beforeinput_handlers: this.onBeforeInput.bind(this),\n        legit_feff_predicates: [\n            (node) =>\n                !node.nextSibling &&\n                !isBlock(closestElement(node)) &&\n                nextLeaf(node, closestBlock(node)),\n        ],\n    };\n\n    insertLineBreak() {\n        this.dispatchTo(\"before_line_break_handlers\");\n        let selection = this.dependencies.selection.getSelectionData().deepEditableSelection;\n        if (!selection.isCollapsed) {\n            // @todo @phoenix collapseIfZWS is not tested\n            // this.shared.collapseIfZWS();\n            this.dependencies.delete.deleteSelection();\n            selection = this.dependencies.selection.getEditableSelection();\n        }\n\n        const targetNode = selection.anchorNode;\n        const targetOffset = selection.anchorOffset;\n\n        this.insertLineBreakNode({ targetNode, targetOffset });\n        this.dependencies.history.addStep();\n    }\n\n    /**\n     * @param {Object} params\n     * @param {Node} params.targetNode\n     * @param {number} params.targetOffset\n     */\n    insertLineBreakNode({ targetNode, targetOffset }) {\n        const closestEl = closestElement(targetNode);\n        if (closestEl && !closestEl.isContentEditable) {\n            return;\n        }\n        if (targetNode.nodeType === Node.TEXT_NODE) {\n            targetOffset = splitTextNode(targetNode, targetOffset);\n            targetNode = targetNode.parentElement;\n        }\n\n        if (this.delegateTo(\"insert_line_break_element_overrides\", { targetNode, targetOffset })) {\n            return;\n        }\n\n        this.insertLineBreakElement({ targetNode, targetOffset });\n    }\n\n    /**\n     * @param {Object} params\n     * @param {HTMLElement} params.targetNode\n     * @param {number} params.targetOffset\n     */\n    insertLineBreakElement({ targetNode, targetOffset }) {\n        const closestEl = closestElement(targetNode);\n        if (closestEl && !closestEl.isContentEditable) {\n            return;\n        }\n        const restore = prepareUpdate(targetNode, targetOffset);\n\n        const brEl = this.document.createElement(\"br\");\n        const brEls = [brEl];\n        if (targetOffset >= targetNode.childNodes.length) {\n            targetNode.appendChild(brEl);\n            if (\n                !isBlock(closestElement(targetNode)) &&\n                nextLeaf(targetNode, closestBlock(targetNode))\n            ) {\n                targetNode.appendChild(this.document.createTextNode(\"\\uFEFF\"));\n            }\n        } else {\n            targetNode.insertBefore(brEl, targetNode.childNodes[targetOffset]);\n        }\n        if (\n            isFakeLineBreak(brEl) &&\n            !(getState(...leftPos(brEl), DIRECTIONS.LEFT).cType & (CTGROUPS.BLOCK | CTYPES.BR))\n        ) {\n            const brEl2 = this.document.createElement(\"br\");\n            brEl.before(brEl2);\n            brEls.unshift(brEl2);\n        }\n\n        restore();\n\n        // @todo ask AGE about why this code was only needed for unbreakable.\n        // See `this._applyCommand('oEnter') === UNBREAKABLE_ROLLBACK_CODE` in\n        // web_editor. Because now we should have a strong handling of the link\n        // selection with the link isolation, if we want to insert a BR outside,\n        // we can move the cursor outside the link.\n        // So if there is no reason to keep this code, we should remove it.\n        //\n        // const anchor = brEls[0].parentElement;\n        // // @todo @phoenix should this case be handled by a LinkPlugin?\n        // // @todo @phoenix Don't we want this for all spans ?\n        // if (anchor.nodeName === \"A\" && brEls.includes(anchor.firstChild)) {\n        //     brEls.forEach((br) => anchor.before(br));\n        //     const pos = rightPos(brEls[brEls.length - 1]);\n        //     this.dependencies.selection.setSelection({ anchorNode: pos[0], anchorOffset: pos[1] });\n        // } else if (anchor.nodeName === \"A\" && brEls.includes(anchor.lastChild)) {\n        //     brEls.forEach((br) => anchor.after(br));\n        //     const pos = rightPos(brEls[0]);\n        //     this.dependencies.selection.setSelection({ anchorNode: pos[0], anchorOffset: pos[1] });\n        // }\n        for (const el of brEls) {\n            // @todo @phoenix we don t want to setSelection multiple times\n            if (el.parentNode) {\n                const pos = rightPos(el);\n                this.dependencies.selection.setSelection({\n                    anchorNode: pos[0],\n                    anchorOffset: pos[1],\n                });\n                break;\n            }\n        }\n    }\n\n    onBeforeInput(e) {\n        if (e.inputType === \"insertLineBreak\") {\n            e.preventDefault();\n            this.insertLineBreak();\n        }\n    }\n}\n", "import { getDeepestPosition, isParagraphRelatedElement } from \"@html_editor/utils/dom_info\";\nimport { Plugin } from \"../plugin\";\nimport { isNotAllowedContent } from \"./selection_plugin\";\nimport { endPos, startPos } from \"@html_editor/utils/position\";\nimport { childNodes } from \"@html_editor/utils/dom_traversal\";\n\nexport class NoInlineRootPlugin extends Plugin {\n    static id = \"noInlineRoot\";\n    static dependencies = [\"baseContainer\", \"selection\", \"history\"];\n\n    /** @type {import(\"plugins\").EditorResources} */\n    resources = {\n        fix_selection_on_editable_root_overrides: this.fixSelectionOnEditableRoot.bind(this),\n    };\n\n    setup() {\n        this.addDomListener(this.editable, \"keydown\", (ev) => {\n            this.currentKeyDown = ev.key;\n        });\n        this.addDomListener(this.editable, \"pointerdown\", () => {\n            this.isPointerDown = true;\n        });\n        this.addDomListener(this.editable, \"pointerup\", () => {\n            this.isPointerDown = false;\n        });\n    }\n\n    /**\n     * Places the cursor in a safe place (not the editable root).\n     * Inserts an empty paragraph if selection results from mouse click and\n     * there's no other way to insert text before/after a block.\n     *\n     * @param {import(\"./selection_plugin\").EditorSelection} selection\n     * @returns {boolean} Whether the selection was fixed\n     */\n    fixSelectionOnEditableRoot(selection) {\n        if (!selection.isCollapsed || selection.anchorNode !== this.editable) {\n            return false;\n        }\n\n        const children = childNodes(this.editable);\n        const nodeAfterCursor = children[selection.anchorOffset];\n        const nodeBeforeCursor = children[selection.anchorOffset - 1];\n        const key = this.currentKeyDown;\n        delete this.currentKeyDown;\n\n        if (key?.startsWith(\"Arrow\")) {\n            return this.fixSelectionOnEditableRootArrowKeys(nodeAfterCursor, nodeBeforeCursor, key);\n        }\n        return this.fixSelectionOnEditableRootGeneric(nodeAfterCursor, nodeBeforeCursor);\n    }\n    /**\n     * @param {Node} nodeAfterCursor\n     * @param {Node} nodeBeforeCursor\n     * @param {string} key\n     * @returns {boolean} Whether the selection was fixed\n     */\n    fixSelectionOnEditableRootArrowKeys(nodeAfterCursor, nodeBeforeCursor, key) {\n        if (![\"ArrowRight\", \"ArrowLeft\", \"ArrowUp\", \"ArrowDown\"].includes(key)) {\n            return false;\n        }\n        const directionForward = [\"ArrowRight\", \"ArrowDown\"].includes(key);\n        let node = directionForward ? nodeAfterCursor : nodeBeforeCursor;\n        while (node && isNotAllowedContent(node)) {\n            node = directionForward ? node.nextElementSibling : node.previousElementSibling;\n        }\n        if (!node) {\n            return false;\n        }\n        let [anchorNode, anchorOffset] = directionForward ? startPos(node) : endPos(node);\n        [anchorNode, anchorOffset] = getDeepestPosition(anchorNode, anchorOffset);\n        this.dependencies.selection.setSelection({ anchorNode, anchorOffset });\n        return true;\n    }\n    /**\n     * @param {Node} nodeAfterCursor\n     * @param {Node} nodeBeforeCursor\n     * @returns {boolean} Whether the selection was fixed\n     */\n    fixSelectionOnEditableRootGeneric(nodeAfterCursor, nodeBeforeCursor) {\n        if (isParagraphRelatedElement(nodeAfterCursor)) {\n            // Cursor is right before a 'P'.\n            this.dependencies.selection.setCursorStart(nodeAfterCursor);\n            return true;\n        }\n        if (isParagraphRelatedElement(nodeBeforeCursor)) {\n            // Cursor is right after a 'P'.\n            this.dependencies.selection.setCursorEnd(nodeBeforeCursor);\n            return true;\n        }\n        return false;\n    }\n}\n", "import {\n    Component,\n    onWillDestroy,\n    useEffect,\n    useExternalListener,\n    useRef,\n    useState,\n    useSubEnv,\n    xml,\n} from \"@odoo/owl\";\nimport { OVERLAY_SYMBOL } from \"@web/core/overlay/overlay_container\";\nimport { usePosition } from \"@web/core/position/position_hook\";\nimport { useActiveElement } from \"@web/core/ui/ui_service\";\n\nexport class EditorOverlay extends Component {\n    static template = xml`\n        <div t-ref=\"root\" class=\"overlay\" t-att-class=\"props.className\" t-on-pointerdown.stop=\"() => {}\">\n            <t t-component=\"props.Component\" t-props=\"props.props\"/>\n        </div>`;\n\n    static props = {\n        target: { validate: (el) => el.nodeType === Node.ELEMENT_NODE, optional: true },\n        initialSelection: { type: Object, optional: true },\n        Component: Function,\n        props: { type: Object, optional: true },\n        editable: { validate: (el) => el.nodeType === Node.ELEMENT_NODE },\n        bus: Object,\n        history: Object,\n        close: Function,\n        isOverlayOpen: Function,\n\n        // Props from createOverlay\n        positionOptions: { type: Object, optional: true },\n        className: { type: String, optional: true },\n        closeOnPointerdown: { type: Boolean, optional: true },\n        hasAutofocus: { type: Boolean, optional: true },\n    };\n\n    static defaultProps = {\n        className: \"\",\n        closeOnPointerdown: true,\n        hasAutofocus: false,\n    };\n\n    setup() {\n        this.lastSelection = this.props.initialSelection;\n        /** @type {HTMLElement} */\n        const editable = this.props.editable;\n        let getTarget, position;\n        if (this.props.target) {\n            getTarget = () => this.props.target;\n        } else {\n            this.rangeElement = editable.ownerDocument.createElement(\"range-el\");\n            editable.after(this.rangeElement);\n            onWillDestroy(() => {\n                this.rangeElement.remove();\n            });\n            getTarget = this.getSelectionTarget.bind(this);\n        }\n\n        useExternalListener(this.props.bus, \"updatePosition\", () => {\n            position.unlock();\n        });\n\n        const rootRef = useRef(\"root\");\n\n        if (this.props.positionOptions?.updatePositionOnResize ?? true) {\n            const resizeObserver = new ResizeObserver(() => {\n                position.unlock();\n            });\n            useEffect(\n                (root) => {\n                    resizeObserver.observe(root);\n                    return () => {\n                        resizeObserver.unobserve(root);\n                    };\n                },\n                () => [rootRef.el]\n            );\n        }\n\n        if (this.props.closeOnPointerdown) {\n            const clickAway = (ev) => {\n                if (!this.env[OVERLAY_SYMBOL]?.contains(ev.composedPath()[0])) {\n                    this.props.close();\n                }\n            };\n            const editableDocument = this.props.editable.ownerDocument;\n            useExternalListener(editableDocument, \"pointerdown\", clickAway);\n            // Listen to pointerdown outside the iframe\n            if (editableDocument !== document) {\n                useExternalListener(document, \"pointerdown\", clickAway);\n            }\n        }\n\n        if (this.props.hasAutofocus) {\n            useActiveElement(\"root\");\n        }\n        const topDocument = editable.ownerDocument.defaultView.top.document;\n        const scrollContainer = getScrollContainer(editable);\n        const container = scrollContainer || topDocument.documentElement;\n        const resizeObserver = new ResizeObserver(() => position.unlock());\n        resizeObserver.observe(container);\n        onWillDestroy(() => resizeObserver.disconnect());\n        const positionOptions = {\n            position: \"bottom-start\",\n            container: container,\n            ...this.props.positionOptions,\n            onPositioned: (el, solution) => {\n                this.props.positionOptions?.onPositioned?.(el, solution);\n                this.updateVisibility(el, solution, scrollContainer);\n            },\n        };\n        position = usePosition(\"root\", getTarget, positionOptions);\n\n        this.overlayState = useState({ isOverlayVisible: true });\n        useSubEnv({ overlayState: this.overlayState });\n    }\n\n    getSelectionTarget() {\n        const doc = this.props.editable.ownerDocument;\n        const selection = doc.getSelection();\n        if (!selection || !selection.rangeCount || !this.props.isOverlayOpen()) {\n            return null;\n        }\n        const inEditable = this.props.editable.contains(selection.anchorNode);\n        let range;\n        if (inEditable) {\n            range = selection.getRangeAt(0);\n            this.lastSelection = { range };\n        } else {\n            if (!this.lastSelection) {\n                return null;\n            }\n            range = this.lastSelection.range;\n        }\n        let rect = range.getBoundingClientRect();\n        if (rect.x === 0 && rect.width === 0 && rect.height === 0) {\n            // Attention, ignoring DOM mutations is always dangerous (when we add or remove nodes)\n            // because if another mutation uses the target that is not observed, that mutation can never be applied\n            // again (when undo/redo and in collaboration).\n            this.props.history.ignoreDOMMutations(() => {\n                const clonedRange = range.cloneRange();\n                const shadowCaret = doc.createTextNode(\"|\");\n                clonedRange.insertNode(shadowCaret);\n                clonedRange.selectNode(shadowCaret);\n                rect = clonedRange.getBoundingClientRect();\n                shadowCaret.remove();\n                clonedRange.detach();\n            });\n        }\n        // Html element with a patched getBoundingClientRect method. It\n        // represents the range as a (HTMLElement) target for the usePosition\n        // hook.\n        this.rangeElement.getBoundingClientRect = () => rect;\n        return this.rangeElement;\n    }\n\n    updateVisibility(overlayElement, solution, scrollContainer) {\n        // @todo: mobile tests rely on a visible (yet overflowing) toolbar\n        // Remove this once the mobile toolbar is fixed?\n        if (this.env.isSmall) {\n            return;\n        }\n        const shouldBeVisible = this.shouldOverlayBeVisible(\n            overlayElement,\n            solution,\n            scrollContainer\n        );\n        overlayElement.style.visibility = shouldBeVisible ? \"visible\" : \"hidden\";\n        this.overlayState.isOverlayVisible = shouldBeVisible;\n    }\n\n    /**\n     * @param {HTMLElement} overlayElement\n     * @param {Object} solution\n     * @param {HTMLElement} scrollContainer\n     */\n    shouldOverlayBeVisible(overlayElement, solution, scrollContainer) {\n        if (!scrollContainer) {\n            return true;\n        }\n        const scrollContainerRect = scrollContainer.getBoundingClientRect();\n        const top = Math.max(scrollContainerRect.top, 0);\n        const bottom = top + scrollContainerRect.height;\n        const overflowsTop = solution.top < top;\n        const overflowsBottom = solution.top + overlayElement.offsetHeight > bottom;\n        const canFlip = this.props.positionOptions?.flip ?? true;\n        if (overflowsTop) {\n            if (overflowsBottom) {\n                // Overlay is bigger than the cointainer. Hiding it would make\n                // it always invisible.\n                return true;\n            }\n            if (solution.direction === \"top\" && canFlip) {\n                // Scrolling down will make overlay eventually flip and no longer overflow\n                return true;\n            }\n            return false;\n        }\n        if (overflowsBottom) {\n            if (solution.direction === \"bottom\" && canFlip) {\n                // Scrolling up will make overlay eventually flip and no longer overflow\n                return true;\n            }\n            return false;\n        }\n        return true;\n    }\n}\n\n/**\n * The scroll container is an ancestor of {@link el} that is:\n * - scrollable and\n * - not also ancestor of a fixed element encosing `el` in the same\n * document (as this makes `el` fixed and not affected by scrolls of\n * that ancestor)\n *\n * @param {HTMLElement} el\n * @returns {HTMLElement|null}\n */\nexport function getScrollContainer(el) {\n    const isScrollable = (/** @type {HTMLElement} */ el) => {\n        if (el.tagName === \"HTML\") {\n            return el.scrollHeight > el.ownerDocument.defaultView.visualViewport.height;\n        }\n        return (\n            el.scrollHeight > el.clientHeight &&\n            /\\bauto\\b|\\bscroll\\b/.test(getComputedStyle(el)[\"overflow-y\"])\n        );\n    };\n    const isFixed = (el) => getComputedStyle(el).position === \"fixed\";\n    while (el) {\n        if (isScrollable(el)) {\n            return el;\n        }\n        if (isFixed(el)) {\n            // Any scrollable ancestor in the same document does not affect it.\n            // Search in the enclosing document, if any.\n            el = el.ownerDocument.defaultView.frameElement;\n            continue;\n        }\n        el = el.parentElement || el.ownerDocument.defaultView.frameElement;\n    }\n    return null;\n}\n", "import { markRaw, EventBus } from \"@odoo/owl\";\nimport { Plugin } from \"../plugin\";\nimport { EditorOverlay } from \"./overlay\";\n\n/**\n * @typedef { Object } OverlayShared\n * @property { OverlayPlugin['createOverlay'] } createOverlay\n */\n\n/**\n * Provides the following feature:\n * - adding a component in overlay above the editor, with proper positioning\n */\nexport class OverlayPlugin extends Plugin {\n    static id = \"overlay\";\n    static dependencies = [\"history\"];\n    static shared = [\"createOverlay\"];\n\n    overlays = [];\n\n    destroy() {\n        super.destroy();\n        for (const overlay of this.overlays) {\n            overlay.close();\n        }\n    }\n\n    /**\n     * Creates an overlay component and adds it to the list of overlays.\n     *\n     * @param {Function} Component\n     * @param {Object} [props={}]\n     * @param {Object} [options]\n     * @returns {Overlay}\n     */\n    createOverlay(Component, props = {}, options) {\n        const overlay = new Overlay(this, Component, props, options);\n        this.overlays.push(overlay);\n        return overlay;\n    }\n}\n\nexport class Overlay {\n    constructor(plugin, C, props, options) {\n        this.plugin = plugin;\n        this.C = C;\n        this.editorOverlayProps = props;\n        this.options = options;\n        this.isOpen = false;\n        this._remove = null;\n        this.component = null;\n        this.bus = new EventBus();\n    }\n\n    /**\n     * @param {Object} options\n     * @param {HTMLElement | null} [options.target] for the overlay.\n     *  If null or undefined, the current selection will be used instead\n     * @param {any} [options.props] overlay component props\n     */\n    open({ target, props }) {\n        if (this.isOpen) {\n            this.updatePosition();\n        } else {\n            this.isOpen = true;\n            const selection = this.plugin.editable.ownerDocument.getSelection();\n            let initialSelection;\n            if (selection && selection.type !== \"None\") {\n                initialSelection = {\n                    range: selection.getRangeAt(0),\n                };\n            }\n            this._remove = this.plugin.services.overlay.add(\n                EditorOverlay,\n                markRaw({\n                    ...this.editorOverlayProps,\n                    Component: this.C,\n                    editable: this.plugin.editable,\n                    props,\n                    target,\n                    initialSelection,\n                    bus: this.bus,\n                    close: this.close.bind(this),\n                    isOverlayOpen: this.isOverlayOpen.bind(this),\n                    history: {\n                        ignoreDOMMutations: this.plugin.dependencies.history.ignoreDOMMutations,\n                    },\n                }),\n                {\n                    ...this.options,\n                }\n            );\n        }\n    }\n\n    close() {\n        this.isOpen = false;\n        if (this._remove) {\n            this._remove();\n        }\n    }\n\n    isOverlayOpen() {\n        return this.isOpen;\n    }\n\n    updatePosition() {\n        this.bus.trigger(\"updatePosition\");\n    }\n}\n", "import { Plugin } from \"../plugin\";\nimport { isProtecting, isUnprotecting } from \"../utils/dom_info\";\nimport { childNodes } from \"../utils/dom_traversal\";\nimport { withSequence } from \"@html_editor/utils/resource\";\n\nconst PROTECTED_SELECTOR = `[data-oe-protected=\"true\"],[data-oe-protected=\"\"]`;\nconst UNPROTECTED_SELECTOR = `[data-oe-protected=\"false\"]`;\n\n/**\n * @typedef { Object } ProtectedNodeShared\n * @property { ProtectedNodePlugin['setProtectingNode'] } setProtectingNode\n *\n * @typedef { import(\"./history_plugin\").HistoryMutationRecord } HistoryMutationRecord\n */\n\nexport class ProtectedNodePlugin extends Plugin {\n    static id = \"protectedNode\";\n    static shared = [\"setProtectingNode\"];\n    /** @type {import(\"plugins\").EditorResources} */\n    resources = {\n        /** Handlers */\n        clean_for_save_handlers: ({ root }) => this.cleanForSave(root),\n        normalize_handlers: withSequence(0, this.normalize.bind(this)),\n        before_filter_mutation_record_handlers: this.beforeFilteringMutationRecords.bind(this),\n\n        unsplittable_node_predicates: [\n            isProtecting, // avoid merge\n            isUnprotecting,\n        ],\n        savable_mutation_record_predicates: this.isMutationRecordSavable.bind(this),\n        removable_descendants_providers: this.filterDescendantsToRemove.bind(this),\n    };\n\n    setup() {\n        this.protectedNodes = new WeakSet();\n    }\n\n    filterDescendantsToRemove(elem) {\n        // TODO @phoenix: history plugin can register protected nodes in its\n        // id maps, should it be prevented? => if yes, take care that data-oe-protected=\"false\"\n        // elements should also be registered even though they are protected.\n        if (isProtecting(elem)) {\n            const descendantsToRemove = [];\n            for (const candidate of elem.querySelectorAll(UNPROTECTED_SELECTOR)) {\n                if (candidate.closest(PROTECTED_SELECTOR) === elem) {\n                    descendantsToRemove.push(...childNodes(candidate));\n                }\n            }\n            return descendantsToRemove;\n        }\n    }\n\n    protectNode(node) {\n        if (node.nodeType === Node.ELEMENT_NODE) {\n            if (node.matches(UNPROTECTED_SELECTOR)) {\n                this.unProtectDescendants(node);\n            } else if (!this.protectedNodes.has(node)) {\n                this.protectDescendants(node);\n            }\n            // assume that descendants are already handled if the node\n            // is already protected.\n        }\n        this.protectedNodes.add(node);\n    }\n\n    unProtectNode(node) {\n        if (node.nodeType === Node.ELEMENT_NODE) {\n            if (node.matches(PROTECTED_SELECTOR)) {\n                this.protectDescendants(node);\n            } else if (this.protectedNodes.has(node)) {\n                this.unProtectDescendants(node);\n            }\n            // assume that descendants are already handled if the node\n            // is already not protected.\n        }\n        this.protectedNodes.delete(node);\n    }\n\n    protectDescendants(node) {\n        let child = node.firstChild;\n        while (child) {\n            this.protectNode(child);\n            child = child.nextSibling;\n        }\n    }\n\n    unProtectDescendants(node) {\n        let child = node.firstChild;\n        while (child) {\n            this.unProtectNode(child);\n            child = child.nextSibling;\n        }\n    }\n\n    /**\n     * @param {HistoryMutationRecord[]} records\n     */\n    beforeFilteringMutationRecords(records) {\n        for (const record of records) {\n            if (record.type === \"childList\") {\n                if (record.target.nodeType !== Node.ELEMENT_NODE) {\n                    return;\n                }\n                const addedNodes = record.addedTrees.map((tree) => tree.node);\n                if (\n                    (this.protectedNodes.has(record.target) &&\n                        !record.target.matches(UNPROTECTED_SELECTOR)) ||\n                    record.target.matches(PROTECTED_SELECTOR)\n                ) {\n                    for (const addedNode of addedNodes) {\n                        this.protectNode(addedNode);\n                    }\n                } else if (\n                    !this.protectedNodes.has(record.target) ||\n                    record.target.matches(UNPROTECTED_SELECTOR)\n                ) {\n                    for (const addedNode of addedNodes) {\n                        this.unProtectNode(addedNode);\n                    }\n                }\n            }\n        }\n    }\n\n    /**\n     * @param {HistoryMutationRecord} record\n     * @return {boolean}\n     */\n    isMutationRecordSavable(record) {\n        if (record.type === \"childList\") {\n            return !(\n                (this.protectedNodes.has(record.target) &&\n                    !record.target.matches(UNPROTECTED_SELECTOR)) ||\n                record.target.matches(PROTECTED_SELECTOR)\n            );\n        }\n        return !this.protectedNodes.has(record.target);\n    }\n\n    forEachProtectingElem(elem, callback) {\n        const selector = `[data-oe-protected]`;\n        const protectingNodes = [...elem.querySelectorAll(selector)].reverse();\n        if (elem.matches(selector)) {\n            protectingNodes.push(elem);\n        }\n        for (const protectingNode of protectingNodes) {\n            if (protectingNode.dataset.oeProtected === \"false\") {\n                callback(protectingNode, false);\n            } else {\n                callback(protectingNode, true);\n            }\n        }\n    }\n\n    normalize(elem) {\n        this.forEachProtectingElem(elem, this.setProtectingNode.bind(this));\n    }\n\n    setProtectingNode(elem, protecting) {\n        elem.dataset.oeProtected = protecting;\n        // contenteditable attribute is set on (un)protecting nodes for\n        // implementation convenience. This could be removed but the editor\n        // should be adapted to handle some use cases that are handled for\n        // contenteditable elements. Currently unsupported configurations:\n        // 1) unprotected non-editable content: would typically be added/removed\n        // programmatically and shared in collaboration => some logic should\n        // be added to handle undo/redo properly for consistency.\n        // -> A adds content, A replaces his content with a new one, B replaces\n        //   content of A with his own, A undo => there is now the content of B\n        //   and the old content of A in the node, is it still coherent?\n        // 2) protected editable content: need a specification of which\n        // functions of the editor are allowed to work (and how) in that\n        // editable part (none?) => should be enforced.\n        if (protecting) {\n            elem.setAttribute(\"contenteditable\", \"false\");\n            this.protectDescendants(elem);\n        } else {\n            elem.setAttribute(\"contenteditable\", \"true\");\n            this.unProtectDescendants(elem);\n        }\n    }\n\n    cleanForSave(clone) {\n        this.forEachProtectingElem(clone, (protectingNode) => {\n            protectingNode.removeAttribute(\"contenteditable\");\n        });\n    }\n}\n", "import { selectElements } from \"@html_editor/utils/dom_traversal\";\nimport { Plugin } from \"../plugin\";\n\n/**\n * @typedef { Object } SanitizeShared\n * @property { SanitizePlugin['sanitize'] } sanitize\n */\n\nexport class SanitizePlugin extends Plugin {\n    static id = \"sanitize\";\n    static shared = [\"sanitize\"];\n    /** @type {import(\"plugins\").EditorResources} */\n    resources = {\n        clean_for_save_handlers: this.cleanForSave.bind(this),\n        normalize_handlers: this.normalize.bind(this),\n    };\n\n    setup() {\n        if (!window.DOMPurify) {\n            throw new Error(\"DOMPurify is not available\");\n        }\n        this.DOMPurify = DOMPurify(this.window);\n    }\n    /**\n     * Sanitizes in place an html element. Current implementation uses the\n     * DOMPurify library.\n     *\n     * @param {HTMLElement} elem\n     * @returns {HTMLElement} the element itself\n     */\n    sanitize(elem) {\n        return this.DOMPurify.sanitize(elem, {\n            IN_PLACE: true,\n            ADD_TAGS: [\"#document-fragment\", \"fake-el\"],\n            ADD_ATTR: [\"contenteditable\", \"t-field\", \"t-out\", \"t-esc\"],\n        });\n    }\n\n    normalize(element) {\n        for (const el of selectElements(\n            element,\n            \".o-contenteditable-false, .o-contenteditable-true\"\n        )) {\n            el.contentEditable = el.matches(\".o-contenteditable-true\");\n        }\n        for (const el of selectElements(element, \"[data-oe-role]\")) {\n            el.setAttribute(\"role\", el.dataset.oeRole);\n        }\n        for (const el of selectElements(element, \"[data-oe-aria-label]\")) {\n            el.setAttribute(\"aria-label\", el.dataset.oeAriaLabel);\n        }\n    }\n\n    /**\n     * Ensure that attributes sanitized by the server are properly removed before\n     * the save, to avoid mismatches and a reset of the editable content.\n     * Only attributes under the responsibility (associated with an editor\n     * attribute or class) of the sanitize plugin are removed.\n     *\n     * /!\\ CAUTION: using server-sanitized attributes without editor-specific\n     * classes/attributes in a custom plugin should be managed by that same\n     * custom plugin.\n     */\n    cleanForSave({ root }) {\n        for (const el of selectElements(\n            root,\n            \".o-contenteditable-false, .o-contenteditable-true\"\n        )) {\n            el.removeAttribute(\"contenteditable\");\n        }\n        for (const el of selectElements(root, \"[data-oe-role]\")) {\n            el.removeAttribute(\"role\");\n        }\n        for (const el of selectElements(root, \"[data-oe-aria-label]\")) {\n            el.removeAttribute(\"aria-label\");\n        }\n    }\n}\n", "import { closestBlock } from \"@html_editor/utils/blocks\";\nimport {\n    getDeepestPosition,\n    isMediaElement,\n    isProtected,\n    isProtecting,\n    isUnprotecting,\n} from \"@html_editor/utils/dom_info\";\nimport {\n    childNodes,\n    closestElement,\n    descendants,\n    firstLeaf,\n    lastLeaf,\n} from \"@html_editor/utils/dom_traversal\";\nimport { getActiveHotkey } from \"@web/core/hotkeys/hotkey_service\";\nimport { Plugin } from \"../plugin\";\nimport { DIRECTIONS, leftPos, nodeSize, rightPos } from \"../utils/position\";\nimport {\n    getAdjacentCharacter,\n    normalizeDeepCursorPosition,\n    normalizeFakeBR,\n    normalizeNotEditableNode,\n    normalizeSelfClosingElement,\n} from \"../utils/selection\";\nimport { closestScrollableY } from \"@web/core/utils/scrolling\";\nimport { weakMemoize } from \"@html_editor/utils/functions\";\n\n/**\n * @typedef { Object } EditorSelection\n * @property { Node } anchorNode\n * @property { number } anchorOffset\n * @property { Node } focusNode\n * @property { number } focusOffset\n * @property { Node } startContainer\n * @property { number } startOffset\n * @property { Node } endContainer\n * @property { number } endOffset\n * @property { Node } commonAncestorContainer\n * @property { boolean } isCollapsed\n * @property { boolean } direction\n * @property { () => string } textContent\n * @property { (node: Node) => boolean } intersectsNode\n */\n\n/**\n * @typedef {Object} SelectionData\n * @property {EditorSelection} documentSelection\n * @property {EditorSelection} editableSelection\n * @property {EditorSelection} deepEditableSelection\n * @property { boolean } documentSelectionIsInEditable\n * @property { boolean } documentSelectionIsProtected\n * @property { boolean } documentSelectionIsProtecting\n * @property { boolean } currentSelectionIsInEditable\n */\n\n/**\n * @typedef {Object} Cursors\n * @property {() => void} restore\n * @property {(callback: (cursor: Cursor) => void) => Cursors} update\n * @property {(node: Node, newNode: Node) => Cursors} remapNode\n * @property {(node: Node, newOffset: number) => Cursors} setOffset\n * @property {(node: Node, shiftOffset: number) => Cursors} shiftOffset\n */\n\n/**\n * @typedef {Object} Cursor\n * @property {Node} node\n * @property {number} offset\n */\n\n// https://developer.mozilla.org/en-US/docs/Glossary/Void_element\nconst VOID_ELEMENT_NAMES = [\n    \"AREA\",\n    \"BASE\",\n    \"BR\",\n    \"COL\",\n    \"EMBED\",\n    \"HR\",\n    \"IMG\",\n    \"INPUT\",\n    \"KEYGEN\",\n    \"LINK\",\n    \"META\",\n    \"PARAM\",\n    \"SOURCE\",\n    \"TRACK\",\n    \"WBR\",\n];\n\nexport function isArtificialVoidElement(node) {\n    return isMediaElement(node) || node.nodeName === \"HR\";\n}\n\nexport function isNotAllowedContent(node) {\n    return isArtificialVoidElement(node) || VOID_ELEMENT_NAMES.includes(node.nodeName);\n}\n\nexport const isHtmlContentSupported = weakMemoize(\n    (/** @type {EditorSelection} */ selection) =>\n        !closestElement(\n            selection.focusNode,\n            '[data-oe-model]:not([data-oe-type=\"html\"]):not([data-oe-field=\"arch\"]):not([data-oe-translation-source-sha])'\n        )\n);\n\n/**\n * @returns edge text nodes if they do not have content selected\n */\nfunction getUnselectedEdgeTextNodes(selection) {\n    const startEdgeNodes = (node, offset) =>\n        node === selection.commonAncestorContainer || offset < nodeSize(node)\n            ? []\n            : [node, ...startEdgeNodes(...rightPos(node))];\n    const endEdgeNodes = (node, offset) =>\n        node === selection.commonAncestorContainer || offset > 0\n            ? []\n            : [node, ...endEdgeNodes(...leftPos(node))];\n    return new Set(\n        [\n            ...startEdgeNodes(selection.startContainer, selection.startOffset),\n            ...endEdgeNodes(selection.endContainer, selection.endOffset),\n        ].filter((node) => node.nodeType === Node.TEXT_NODE)\n    );\n}\n\n/**\n * Scrolls the view to a specific node's position in the document\n * @param {Selection} selection - The current document selection\n * @returns {void}\n */\nfunction scrollToSelection(selection) {\n    const range = selection.getRangeAt(0);\n    const container = closestScrollableY(range.startContainer.parentElement);\n    if (!container) {\n        // If the container is not scrollable we don't scroll\n        return;\n    }\n    let rect = range.getBoundingClientRect();\n    // If the range is invisible (0 width & height),\n    // We call `getBoundingClientRect` on closest element.\n    if (rect.width === 0 && rect.height === 0 && selection.isCollapsed) {\n        rect = closestElement(selection.anchorNode).getBoundingClientRect();\n    }\n\n    const containerRect = container.getBoundingClientRect();\n    const offsetTop = rect.top - containerRect.top + container.scrollTop;\n    const offsetBottom = rect.bottom - containerRect.top + container.scrollTop;\n\n    if (rect.bottom > containerRect.top && rect.top < containerRect.bottom) {\n        // If selection is partially visible, no need to scroll.\n        return;\n    }\n    // Simulate the \"nearest\" behavior by scrolling to the closest top/bottom edge\n    if (rect.top < containerRect.top) {\n        container.scrollTo({ top: offsetTop, behavior: \"instant\" });\n    } else if (rect.bottom > containerRect.bottom) {\n        container.scrollTo({ top: offsetBottom - container.clientHeight, behavior: \"instant\" });\n    }\n}\n\n/**\n * @typedef { Object } SelectionShared\n * @property { SelectionPlugin['extractContent'] } extractContent\n * @property { SelectionPlugin['focusEditable'] } focusEditable\n * @property { SelectionPlugin['getEditableSelection'] } getEditableSelection\n * @property { SelectionPlugin['getSelectionData'] } getSelectionData\n * @property { SelectionPlugin['getTargetedBlocks'] } getTargetedBlocks\n * @property { SelectionPlugin['getTargetedNodes'] } getTargetedNodes\n * @property { SelectionPlugin['modifySelection'] } modifySelection\n * @property { SelectionPlugin['preserveSelection'] } preserveSelection\n * @property { SelectionPlugin['rectifySelection'] } rectifySelection\n * @property { SelectionPlugin['areNodeContentsFullySelected'] } areNodeContentsFullySelected\n * @property { SelectionPlugin['resetSelection'] } resetSelection\n * @property { SelectionPlugin['setCursorEnd'] } setCursorEnd\n * @property { SelectionPlugin['setCursorStart'] } setCursorStart\n * @property { SelectionPlugin['setSelection'] } setSelection\n * @property { SelectionPlugin['isSelectionInEditable'] } isSelectionInEditable\n * @property { SelectionPlugin['isNodeEditable'] } isNodeEditable\n * @property { SelectionPlugin['selectAroundNonEditable'] } selectAroundNonEditable\n */\n\n/**\n * @typedef {((selectionData: SelectionData) => void)[]} selectionchange_handlers\n * @typedef {(() => void)[]} selection_leave_handlers\n *\n * @typedef {((ev: PointerEvent) => void | true)[]} double_click_overrides\n * @typedef {((ev: PointerEvent) => void | true)[]} triple_click_overrides\n * @typedef {((selection: EditorSelection) => boolean)[]} fix_selection_on_editable_root_overrides\n *\n * @typedef {((node: Node, selection: EditorSelection, range: Range) => boolean)[]} fully_selected_node_predicates\n * @typedef {((ev: Event, char: string, lastSkipped: string) => boolean)[]} intangible_char_for_keyboard_navigation_predicates\n * @typedef {((node: Node) => boolean)[]} is_node_editable_predicates\n *\n * @typedef {((targetedNodes: Node[]) => Node[])[]} targeted_nodes_processors\n */\n\nexport class SelectionPlugin extends Plugin {\n    static id = \"selection\";\n    static shared = [\n        \"getSelectionData\",\n        \"getEditableSelection\",\n        \"setSelection\",\n        \"setCursorStart\",\n        \"setCursorEnd\",\n        \"extractContent\",\n        \"preserveSelection\",\n        \"resetSelection\",\n        \"getTargetedNodes\",\n        \"getTargetedBlocks\",\n        \"modifySelection\",\n        \"rectifySelection\",\n        \"areNodeContentsFullySelected\",\n        \"focusEditable\",\n        // \"collapseIfZWS\",\n        \"isSelectionInEditable\",\n        \"isNodeEditable\",\n        \"selectAroundNonEditable\",\n    ];\n    /** @type {import(\"plugins\").EditorResources} */\n    resources = {\n        user_commands: { id: \"selectAll\", run: this.selectAll.bind(this) },\n        shortcuts: [{ hotkey: \"control+a\", commandId: \"selectAll\" }],\n    };\n\n    setup() {\n        this.resetSelection();\n        this.addGlobalDomListener(\"selectionchange\", () => {\n            this.updateActiveSelection();\n            const selection = this.document.getSelection();\n            if (this.isSelectionInEditable(selection)) {\n                scrollToSelection(selection);\n            }\n        });\n        this.addDomListener(this.editable, \"mousedown\", (ev) => {\n            if (ev.detail && ev.detail % 3 === 2) {\n                this.onDoubleClick(ev);\n            }\n            if (ev.detail && ev.detail % 3 === 0) {\n                this.onTripleClick(ev);\n            }\n        });\n        this.addDomListener(this.editable, \"keydown\", (ev) => {\n            const handled = [\n                \"arrowright\",\n                \"shift+arrowright\",\n                \"arrowleft\",\n                \"shift+arrowleft\",\n                \"shift+arrowup\",\n                \"shift+arrowdown\",\n            ];\n            if (handled.includes(getActiveHotkey(ev))) {\n                this.onKeyDownArrows(ev);\n            }\n        });\n\n        this.focusEditableDocument = true;\n        if (this.document !== document) {\n            const focusEditable = () => {\n                this.focusEditableDocument = true;\n            };\n            const unFocusEditable = (ev) => {\n                if (this.focusEditableDocument) {\n                    // autofocus trigger when you close a popover (like color picker)\n                    if (ev.target.tagName === \"IFRAME\") {\n                        return;\n                    }\n                    const preventClosing = ev.target?.closest?.(\"[data-prevent-closing-overlay]\");\n                    if (preventClosing?.dataset?.preventClosingOverlay === \"true\") {\n                        return;\n                    }\n                    this.focusEditableDocument = false;\n                    this.dispatchTo(\"selection_leave_handlers\");\n                }\n            };\n            this.addDomListener(this.document, \"focusin\", focusEditable, { capture: true });\n            this.addDomListener(document, \"focusin\", unFocusEditable, { capture: true });\n            this.addDomListener(this.document, \"pointerdown\", focusEditable, { capture: true });\n            this.addDomListener(document, \"pointerdown\", unFocusEditable, { capture: true });\n        }\n    }\n\n    selectAll() {\n        const selection = this.getEditableSelection();\n        const containerSelector = \"#wrap > *, .oe_structure > *, [contenteditable]\";\n        const container = selection && closestElement(selection.anchorNode, containerSelector);\n        const [anchorNode, anchorOffset] = getDeepestPosition(container, 0);\n        const [focusNode, focusOffset] = getDeepestPosition(container, nodeSize(container));\n        if (\n            this.delegateTo(\"select_all_overrides\", {\n                anchorNode,\n                anchorOffset,\n                focusNode,\n                focusOffset,\n            })\n        ) {\n            return;\n        }\n        this.setSelection({ anchorNode, anchorOffset, focusNode, focusOffset });\n    }\n\n    resetSelection() {\n        this.activeSelection = this.makeActiveSelection();\n    }\n\n    onDoubleClick(ev) {\n        const selectionData = this.getSelectionData();\n        if (selectionData.documentSelectionIsInEditable) {\n            if (this.delegateTo(\"double_click_overrides\", ev)) {\n                // If the override is handled, we don't do anything.\n                return;\n            }\n        }\n    }\n\n    onTripleClick(ev) {\n        const selectionData = this.getSelectionData();\n        if (selectionData.documentSelectionIsInEditable) {\n            if (this.delegateTo(\"triple_click_overrides\", ev)) {\n                // If the override is handled, we don't do anything.\n                return;\n            }\n            const { documentSelection } = selectionData;\n            const block = closestBlock(documentSelection.anchorNode);\n            const [anchorNode, anchorOffset] = getDeepestPosition(block, 0);\n            const [focusNode, focusOffset] = getDeepestPosition(block, nodeSize(block));\n            this.setSelection({ anchorNode, anchorOffset, focusNode, focusOffset });\n            ev.preventDefault();\n            return;\n        }\n    }\n\n    /**\n     * Update the active selection to the current selection in the editor.\n     */\n    updateActiveSelection() {\n        this.previousActiveSelection = this.activeSelection;\n        // getSelectionData sets this.activeSelection to the current selection\n        const selectionData = this.getSelectionData();\n        if (this.fixSelectionOnEditableRoot(selectionData)) {\n            return;\n        }\n        this.dispatchTo(\"selectionchange_handlers\", selectionData);\n    }\n\n    /**\n     * @param { Selection } [selection] The DOM selection\n     * @return { EditorSelection }\n     */\n    makeActiveSelection(selection) {\n        let range;\n        let activeSelection;\n        if (!selection || !selection.rangeCount) {\n            const [targetNode, targetOffset] = this.config.allowInlineAtRoot\n                ? [this.editable, 0]\n                : getDeepestPosition(this.editable, 0);\n            activeSelection = {\n                anchorNode: targetNode,\n                anchorOffset: targetOffset,\n                focusNode: targetNode,\n                focusOffset: targetOffset,\n                startContainer: targetNode,\n                startOffset: targetOffset,\n                endContainer: targetNode,\n                endOffset: targetOffset,\n                commonAncestorContainer: targetNode,\n                isCollapsed: true,\n                direction: DIRECTIONS.RIGHT,\n                textContent: () => \"\",\n                intersectsNode: () => false,\n            };\n        } else {\n            range = selection.getRangeAt(0);\n            let { anchorNode, anchorOffset, focusNode, focusOffset } = selection;\n            let direction =\n                anchorNode === range.startContainer ? DIRECTIONS.RIGHT : DIRECTIONS.LEFT;\n            if (anchorNode === focusNode && focusOffset < anchorOffset) {\n                direction = !direction;\n            }\n            if (\n                this.activeSelection &&\n                (isProtecting(anchorNode) ||\n                    (isProtected(anchorNode) && !isUnprotecting(anchorNode)))\n            ) {\n                // Keep the previous activeSelection in case of user interactions\n                // inside a protected zone.\n                return this.activeSelection;\n            }\n            [anchorNode, anchorOffset] = normalizeSelfClosingElement(anchorNode, anchorOffset);\n            [focusNode, focusOffset] = normalizeSelfClosingElement(focusNode, focusOffset);\n            const [startContainer, startOffset, endContainer, endOffset] =\n                direction === DIRECTIONS.RIGHT\n                    ? [anchorNode, anchorOffset, focusNode, focusOffset]\n                    : [focusNode, focusOffset, anchorNode, anchorOffset];\n            range = this.document.createRange();\n            range.setStart(startContainer, startOffset);\n            range.setEnd(endContainer, endOffset);\n\n            activeSelection = {\n                anchorNode,\n                anchorOffset,\n                focusNode,\n                focusOffset,\n                startContainer,\n                startOffset,\n                endContainer,\n                endOffset,\n                commonAncestorContainer: range.commonAncestorContainer,\n                isCollapsed: range.collapsed,\n                direction,\n                textContent: () => (range.collapsed ? \"\" : selection.toString()),\n                intersectsNode: (node) => range.intersectsNode(node),\n            };\n        }\n\n        Object.freeze(activeSelection);\n        return activeSelection;\n    }\n\n    /**\n     * @param { EditorSelection } selection\n     */\n    extractContent(selection) {\n        const range = new Range();\n        range.setStart(selection.startContainer, selection.startOffset);\n        range.setEnd(selection.endContainer, selection.endOffset);\n        this.setSelection({\n            anchorNode: selection.startContainer,\n            anchorOffset: selection.startOffset,\n        });\n        return range.extractContents();\n    }\n\n    /**\n     * @param { Node } anchorNode\n     * @param { number } anchorOffset\n     * @param { Node } focusNode\n     * @param { number } focusOffset\n     * @param { boolean } direction\n     *\n     * @return { EditorSelection }\n     */\n    createEditorSelection(anchorNode, anchorOffset, focusNode, focusOffset, direction) {\n        let startContainer, startOffset, endContainer, endOffset;\n        const range = new Range();\n        if (direction) {\n            [startContainer, startOffset] = [anchorNode, anchorOffset];\n            [endContainer, endOffset] = [focusNode, focusOffset];\n        } else {\n            [startContainer, startOffset] = [focusNode, focusOffset];\n            [endContainer, endOffset] = [anchorNode, anchorOffset];\n        }\n\n        range.setStart(startContainer, startOffset);\n        range.setEnd(endContainer, endOffset);\n        return Object.freeze({\n            ...this.activeSelection,\n            anchorNode,\n            anchorOffset,\n            focusNode,\n            focusOffset,\n            startContainer,\n            startOffset,\n            endContainer,\n            endOffset,\n            commonAncestorContainer: range.commonAncestorContainer,\n            cloneContents: () => range.cloneContents(),\n        });\n    }\n    /**\n     @return { EditorSelection }\n     */\n    getEditableSelection() {\n        return this.getSelectionData().editableSelection;\n    }\n\n    /**\n     * @return { SelectionData }\n     */\n    getSelectionData() {\n        const selection = this.document.getSelection();\n        const documentSelectionIsInEditable = selection && this.isSelectionInEditable(selection);\n        const documentSelection =\n            selection?.anchorNode && selection?.focusNode\n                ? Object.freeze({\n                      get isCollapsed() {\n                          if (this.collapsed === undefined) {\n                              this.collapsed = selection.isCollapsed;\n                          }\n                          return this.collapsed;\n                      },\n                      anchorNode: selection.anchorNode,\n                      anchorOffset: selection.anchorOffset,\n                      focusNode: selection.focusNode,\n                      focusOffset: selection.focusOffset,\n                      commonAncestorContainer: selection.rangeCount\n                          ? selection.getRangeAt(0).commonAncestorContainer\n                          : null,\n                  })\n                : null;\n        if (documentSelectionIsInEditable) {\n            this.activeSelection = this.makeActiveSelection(selection);\n        } else if (!this.activeSelection.anchorNode.isConnected) {\n            this.activeSelection = this.makeActiveSelection();\n        }\n        let { anchorNode, anchorOffset, focusNode, focusOffset, isCollapsed, direction } =\n            this.activeSelection;\n\n        const editableSelection = this.createEditorSelection(\n            anchorNode,\n            anchorOffset,\n            focusNode,\n            focusOffset,\n            direction\n        );\n\n        const selectionData = {\n            documentSelection: documentSelection,\n            editableSelection: editableSelection,\n            documentSelectionIsInEditable: documentSelectionIsInEditable,\n            currentSelectionIsInEditable:\n                documentSelectionIsInEditable && this.focusEditableDocument,\n        };\n\n        Object.defineProperty(selectionData, \"deepEditableSelection\", {\n            get: function () {\n                // Transform the selection to return the depest possible node.\n                [anchorNode, anchorOffset] = getDeepestPosition(anchorNode, anchorOffset);\n                [focusNode, focusOffset] = isCollapsed\n                    ? [anchorNode, anchorOffset]\n                    : getDeepestPosition(focusNode, focusOffset);\n                return this.createEditorSelection(\n                    anchorNode,\n                    anchorOffset,\n                    focusNode,\n                    focusOffset,\n                    direction\n                );\n            }.bind(this),\n        });\n\n        Object.defineProperty(selectionData, \"documentSelectionIsProtecting\", {\n            get: function () {\n                return documentSelection?.anchorNode\n                    ? isProtecting(documentSelection.anchorNode)\n                    : false;\n            }.bind(this),\n        });\n        Object.defineProperty(selectionData, \"documentSelectionIsProtected\", {\n            get: function () {\n                return documentSelection?.anchorNode\n                    ? isProtected(documentSelection.anchorNode)\n                    : false;\n            }.bind(this),\n        });\n\n        return Object.freeze(selectionData);\n    }\n\n    /**\n     * Returns true if selection is valid and in the editable.\n     * Otherwise, returns false and logs a warning.\n     */\n    validateSelection({ anchorNode, anchorOffset, focusNode, focusOffset }) {\n        const validateNode = (node) => {\n            if (!this.editable.contains(node)) {\n                console.warn(\"Invalid selection. Node is not part of the editable:\", node);\n                return false;\n            }\n            return true;\n        };\n        const validateOffset = (node, offset) => {\n            if (offset < 0 || offset > nodeSize(node)) {\n                console.warn(\"Invalid selection. Offset is out of bounds:\", offset, node);\n                return false;\n            }\n            return true;\n        };\n        const isCollapsed = anchorNode === focusNode && anchorOffset === focusOffset;\n        return (\n            validateNode(anchorNode) &&\n            (focusNode === anchorNode || validateNode(focusNode)) &&\n            validateOffset(anchorNode, anchorOffset) &&\n            (isCollapsed || validateOffset(focusNode, focusOffset))\n        );\n    }\n\n    /**\n     * Set the selection in the editor.\n     *\n     * @param { Object } selection\n     * @param { Node } selection.anchorNode\n     * @param { number } selection.anchorOffset\n     * @param { Node } [selection.focusNode=selection.anchorNode]\n     * @param { number } [selection.focusOffset=selection.anchorOffset]\n     * @param { Object } [options]\n     * @param { boolean } [options.normalize=true] Normalize deep the selection\n     * @return { EditorSelection | null }\n     */\n    setSelection(\n        { anchorNode, anchorOffset, focusNode = anchorNode, focusOffset = anchorOffset },\n        { normalize = true } = {}\n    ) {\n        if (!this.validateSelection({ anchorNode, anchorOffset, focusNode, focusOffset })) {\n            return null;\n        }\n        const restore = this.preserveTextareaSelections();\n        const isCollapsed = anchorNode === focusNode && anchorOffset === focusOffset;\n        [focusNode, focusOffset] = normalizeSelfClosingElement(focusNode, focusOffset, \"right\");\n        [anchorNode, anchorOffset] = isCollapsed\n            ? [focusNode, focusOffset]\n            : normalizeSelfClosingElement(anchorNode, anchorOffset, \"left\");\n        if (normalize) {\n            // normalize selection\n            [anchorNode, anchorOffset] = normalizeDeepCursorPosition(anchorNode, anchorOffset);\n            [focusNode, focusOffset] = isCollapsed\n                ? [anchorNode, anchorOffset]\n                : normalizeDeepCursorPosition(focusNode, focusOffset);\n        }\n\n        [anchorNode, anchorOffset] = normalizeFakeBR(anchorNode, anchorOffset);\n        [focusNode, focusOffset] = normalizeFakeBR(focusNode, focusOffset);\n        const selection = this.document.getSelection();\n        const documentSelectionIsInEditable = selection && this.isSelectionInEditable(selection);\n        if (selection) {\n            if (documentSelectionIsInEditable || selection.anchorNode === null) {\n                selection.setBaseAndExtent(anchorNode, anchorOffset, focusNode, focusOffset);\n                this.activeSelection = this.makeActiveSelection(selection, true);\n            } else {\n                let range = new Range();\n                range.setStart(anchorNode, anchorOffset);\n                range.setEnd(focusNode, focusOffset);\n                if (anchorNode !== focusNode || anchorOffset !== focusOffset) {\n                    // Check if the direction is correct\n                    if (range.collapsed) {\n                        range = new Range();\n                        range.setEnd(anchorNode, anchorOffset);\n                        range.setStart(focusNode, focusOffset);\n                    }\n                }\n\n                this.activeSelection = this.makeActiveSelection({\n                    anchorNode,\n                    anchorOffset,\n                    focusNode,\n                    focusOffset,\n                    getRangeAt: () => range,\n                    rangeCount: 1,\n                });\n            }\n        }\n        restore();\n\n        return this.activeSelection;\n    }\n\n    /**\n     * Take the selections in all `<textarea>` elements in the editable and\n     * return a function that restores them.\n     *\n     * @returns {() => void}\n     */\n    preserveTextareaSelections() {\n        const focusedTextarea =\n            this.document.activeElement?.nodeName === \"TEXTAREA\" && this.document.activeElement;\n        const selections = [...this.editable.querySelectorAll(\"textarea\")].map((textarea) => ({\n            textarea,\n            start: textarea.selectionStart,\n            end: textarea.selectionEnd,\n            direction: textarea.selectionDirection,\n        }));\n        return () => {\n            if (focusedTextarea) {\n                // If a textarea is targeted, focus it so its selection is active.\n                focusedTextarea.focus();\n            }\n            for (const { textarea, start, end, direction } of selections) {\n                textarea.setSelectionRange(start, end, direction);\n            }\n        };\n    }\n\n    /**\n     * Set the cursor at the start of the given node.\n     * @param { Node } node\n     */\n    setCursorStart(node) {\n        return this.setSelection({ anchorNode: node, anchorOffset: 0 });\n    }\n\n    /**\n     * Set the cursor at the end of the given node.\n     * @param { Node } node\n     */\n    setCursorEnd(node) {\n        return this.setSelection({ anchorNode: node, anchorOffset: nodeSize(node) });\n    }\n\n    /**\n     * Stores the current selection and returns an object with methods to:\n     * - update the cursors (anchor and focus) node and offset after DOM\n     * manipulations that migh affect them. Such methods are chainable.\n     * - restore the updated selection.\n     * @returns {Cursors}\n     */\n    preserveSelection() {\n        const hadSelection =\n            this.document.getSelection() && this.document.getSelection().anchorNode !== null;\n        const selectionData = this.getSelectionData();\n        const selection = selectionData.editableSelection;\n        const anchor = { node: selection.anchorNode, offset: selection.anchorOffset };\n        const focus = { node: selection.focusNode, offset: selection.focusOffset };\n\n        return {\n            restore: () => {\n                if (!hadSelection) {\n                    return;\n                }\n                this.setSelection(\n                    {\n                        anchorNode: anchor.node,\n                        anchorOffset: anchor.offset,\n                        focusNode: focus.node,\n                        focusOffset: focus.offset,\n                    },\n                    { normalize: false }\n                );\n            },\n            update(callback) {\n                callback(anchor);\n                callback(focus);\n                return this;\n            },\n            remapNode(node, newNode) {\n                return this.update((cursor) => {\n                    if (cursor.node === node) {\n                        cursor.node = newNode;\n                    }\n                });\n            },\n            setOffset(node, newOffset) {\n                return this.update((cursor) => {\n                    if (cursor.node === node) {\n                        cursor.offset = newOffset;\n                    }\n                });\n            },\n            shiftOffset(node, shiftOffset) {\n                return this.update((cursor) => {\n                    if (cursor.node === node) {\n                        cursor.offset += shiftOffset;\n                    }\n                });\n            },\n        };\n    }\n\n    areNodeContentsFullySelected(node) {\n        const selection = this.getEditableSelection();\n        const range = new Range();\n        range.setStart(selection.startContainer, selection.startOffset);\n        range.setEnd(selection.endContainer, selection.endOffset);\n\n        const firstLeafNode = firstLeaf(node);\n        const lastLeafNode = lastLeaf(node);\n        return (\n            // Custom rules\n            this.getResource(\"fully_selected_node_predicates\").some((cb) =>\n                cb(node, selection, range)\n            ) ||\n            // Default rule\n            (range.isPointInRange(firstLeafNode, 0) &&\n                range.isPointInRange(lastLeafNode, nodeSize(lastLeafNode)))\n        );\n    }\n\n    /**\n     * Returns the nodes targeted by the current selection, from top to bottom\n     * and left to right.\n     * This includes nodes intersected by the selection, as well as the deepest\n     * anchor and offset nodes that are at least partly contained in the\n     * selection.\n     * An element is considered intersected by the selection when reading the\n     * normalized selection's HTML contents would involve reading the opening or\n     * closing tags of the element.\n     * A collapsed selection returns the node in which it is collapsed.\n     *\n     * @example\n     * <p>a[]b</p> -> [\"ab\"]\n     * @example\n     * <p>a[b</p><h1>c]d</h1> -> [P, \"ab\", H1, \"cd\"]\n     * @example\n     * <p>a[b</p><h1>]cd</h1> -> [P, \"ab\", H1]\n     * @example\n     * <div><p>a[b</p><h1>cd</h1></div><h2>e]f</h2> -> [DIV, P, \"ab\", H1, \"cd\", H2, \"ef\"]\n     *\n     * @returns {Node[]}\n     */\n    getTargetedNodes() {\n        const selectionData = this.getSelectionData();\n        const selection = selectionData.deepEditableSelection;\n        const { commonAncestorContainer: root } = selectionData.editableSelection;\n\n        let targetedNodes = [];\n        if (selection.isCollapsed && selection.anchorNode.nodeType !== Node.TEXT_NODE) {\n            targetedNodes = [root];\n        }\n        targetedNodes.push(...descendants(root));\n        if (!targetedNodes.length) {\n            targetedNodes = [root];\n        }\n\n        targetedNodes = targetedNodes.filter(\n            (node) =>\n                selectionData.editableSelection.intersectsNode(node) ||\n                (node.nodeType === Node.TEXT_NODE &&\n                    (node === selection.anchorNode || node === selection.focusNode))\n        );\n\n        const modifiers = [\n            // Remove the editable from the list\n            (nodes) => (nodes[0] === this.editable ? nodes.slice(1) : nodes),\n            // Filter out text nodes that have no content selected\n            (nodes) => {\n                if (selection.isCollapsed) {\n                    return nodes;\n                } else {\n                    const edgeTextNodes = getUnselectedEdgeTextNodes(selection);\n                    return nodes.filter((node) => !edgeTextNodes.has(node));\n                }\n            },\n            // Custom modifiers\n            ...this.getResource(\"targeted_nodes_processors\"),\n        ];\n        for (const modifier of modifiers) {\n            targetedNodes = modifier(targetedNodes);\n        }\n        return targetedNodes;\n    }\n\n    /**\n     * Returns a Set of targeted blocks within the given range.\n     *\n     * @returns {Set<HTMLElement>}\n     */\n    getTargetedBlocks() {\n        return new Set(this.getTargetedNodes().map(closestBlock).filter(Boolean));\n    }\n\n    // @todo @phoenix we should find a real use case and test it\n    // /**\n    //  * Set a deep selection that split the text and collapse it if only one ZWS is\n    //  * selected.\n    //  *\n    //  * @returns {boolean} true if the selection has only one ZWS.\n    //  */\n    // collapseIfZWS() {\n    //     const selection = this.getSelectionData().deepEditableSelection;\n    //     if (\n    //         selection.startContainer === selection.endContainer &&\n    //         selection.startContainer.nodeType === Node.TEXT_NODE &&\n    //         selection.startContainer.textContent === \"\\u200B\"\n    //     ) {\n    //         // We Collapse the selection and bypass deleteRange\n    //         // if the range content is only one ZWS.\n    //         this.setCursorStart(selection.startContainer);\n    //         return true;\n    //     }\n    //     return false;\n    // }\n\n    /**\n     * @param {SelectionData} selectionData\n     * @returns {boolean} Whether the selection was fixed\n     */\n    fixSelectionOnEditableRoot(selectionData) {\n        const { editableSelection, documentSelectionIsInEditable } = selectionData;\n        if (this.config.allowInlineAtRoot || !documentSelectionIsInEditable) {\n            return false;\n        }\n        const isSelectionOnEditableRoot = (s) => s.isCollapsed && s.anchorNode === this.editable;\n        if (!isSelectionOnEditableRoot(editableSelection)) {\n            return false;\n        }\n        if (this.delegateTo(\"fix_selection_on_editable_root_overrides\", editableSelection)) {\n            return true;\n        }\n        // Revert the selection to the previous one\n        if (isSelectionOnEditableRoot(this.previousActiveSelection)) {\n            // Last stored selection is also at the editable root\n            return false;\n        }\n        const selection = this.document.getSelection();\n        if (!selection) {\n            return false;\n        }\n        const { anchorNode, anchorOffset, focusNode, focusOffset } = this.previousActiveSelection;\n        selection.setBaseAndExtent(anchorNode, anchorOffset, focusNode, focusOffset);\n        return true;\n    }\n\n    /**\n     * This function adjusts a given selection to the current nodeSize of its\n     * anchorNode and focusNode, only if they are both present in the given\n     * editable. Apply and return: a valid given selection, a modified\n     * selection if some offset needed to be adjusted. Do nothing if the given\n     * selection anchor or focus nodes are not in this.editable.\n     *\n     * @param { Object } selection\n     * @param { Node } selection.anchorNode\n     * @param { number } selection.anchorOffset\n     * @param { Node } selection.focusNode\n     * @param { number } selection.focusOffset\n     * @returns { EditorSelection|null } selection, rectified selection or null\n     */\n    rectifySelection(selection) {\n        if (!this.isSelectionInEditable(selection)) {\n            return null;\n        }\n        const anchorNode = selection.anchorNode;\n        let anchorOffset = selection.anchorOffset;\n        const focusNode = selection.focusNode;\n        let focusOffset = selection.focusOffset;\n        const anchorSize = nodeSize(anchorNode);\n        const focusSize = nodeSize(focusNode);\n        if (anchorSize < anchorOffset) {\n            anchorOffset = anchorSize;\n        }\n        if (focusSize < focusOffset) {\n            focusOffset = focusSize;\n        }\n        const anchorTarget = childNodes(anchorNode).at(anchorOffset);\n        const focusTarget = childNodes(focusNode).at(focusOffset);\n        const protectionCheck = (node) =>\n            isProtecting(node) || (isProtected(node) && !isUnprotecting(node));\n        if (\n            focusTarget !== anchorTarget &&\n            focusTarget.previousSibling === anchorTarget &&\n            protectionCheck(anchorTarget)\n        ) {\n            return;\n        }\n        if (protectionCheck(anchorNode) || protectionCheck(focusNode)) {\n            // TODO @phoenix, TODO ABD: better handle setSelection on protected\n            // elements\n            return;\n        }\n        return this.setSelection({\n            anchorNode,\n            anchorOffset,\n            focusNode,\n            focusOffset,\n        });\n    }\n\n    /**\n     * @param {\"move\"|\"extend\"} alter\n     * @param {\"backward\"|\"forward\"} direction\n     * @param {\"character\"|\"word\"|\"line\"} granularity\n     * @returns {EditorSelection}\n     */\n    modifySelection(alter, direction, granularity) {\n        const selectionData = this.getSelectionData();\n        if (!selectionData.documentSelectionIsInEditable) {\n            return selectionData.editableSelection;\n        }\n        const selection = this.document.getSelection();\n        if (!selection) {\n            return selectionData.editableSelection;\n        }\n        selection.modify(alter, direction, granularity);\n        if (!this.isSelectionInEditable(selection)) {\n            // If selection was moved to outside the editable, restore it.\n            return this.setSelection(selectionData.editableSelection);\n        }\n        this.activeSelection = this.makeActiveSelection(selection);\n        return this.activeSelection;\n    }\n\n    /**\n     * Changes the selection before the browser's default behavior moves the\n     * cursor, in order to skip undesired characters (typically invisible\n     * characters).\n     */\n    onKeyDownArrows(ev) {\n        const selection = this.document.getSelection();\n        if (!selection || !this.isSelectionInEditable(selection)) {\n            return;\n        }\n\n        // Whether moving a collapsed cursor or extending a selection.\n        const mode = ev.shiftKey ? \"extend\" : \"move\";\n\n        if ([\"ArrowLeft\", \"ArrowRight\"].includes(ev.key)) {\n            // Direction of the movement (take rtl writing into account)\n            const screenDirection = ev.key === \"ArrowLeft\" ? \"left\" : \"right\";\n            const isRtl = closestElement(selection.focusNode, \"[dir]\")?.dir === \"rtl\";\n            const domDirection = (screenDirection === \"left\") ^ isRtl ? \"previous\" : \"next\";\n\n            // Whether the character next to the cursor should be skipped.\n            const shouldSkipCallbacks = this.getResource(\n                \"intangible_char_for_keyboard_navigation_predicates\"\n            );\n            let adjacentCharacter = getAdjacentCharacter(selection, domDirection, this.editable);\n            let shouldSkip = shouldSkipCallbacks.some((cb) => cb(ev, adjacentCharacter));\n\n            while (shouldSkip) {\n                const { focusNode: nodeBefore, focusOffset: offsetBefore } = selection;\n\n                selection.modify(mode, screenDirection, \"character\");\n\n                const hasSelectionChanged =\n                    nodeBefore !== selection.focusNode || offsetBefore !== selection.focusOffset;\n                const lastSkippedChar = adjacentCharacter;\n                adjacentCharacter = getAdjacentCharacter(selection, domDirection, this.editable);\n\n                shouldSkip =\n                    hasSelectionChanged &&\n                    shouldSkipCallbacks.some((cb) => cb(ev, adjacentCharacter, lastSkippedChar));\n            }\n        }\n\n        const { focusNode, focusOffset } = selection;\n        if (mode === \"extend\") {\n            // Since selection can't traverse contenteditable=\"false\" elements,\n            // we adjust the selection to the sibling of non editable element.\n            const selectingBackward = [\"ArrowLeft\", \"ArrowUp\"].includes(ev.key);\n            const currentBlock = closestBlock(focusNode);\n            const isAtBoundary = selectingBackward\n                ? firstLeaf(currentBlock) === focusNode && focusOffset === 0\n                : lastLeaf(currentBlock) === focusNode && focusOffset === nodeSize(focusNode);\n            const adjacentBlock = selectingBackward\n                ? currentBlock.previousElementSibling\n                : currentBlock.nextElementSibling;\n            const targetBlock = selectingBackward\n                ? adjacentBlock?.previousElementSibling\n                : adjacentBlock?.nextElementSibling;\n            if (!adjacentBlock?.isContentEditable && targetBlock && isAtBoundary) {\n                const leafNode = selectingBackward ? lastLeaf(targetBlock) : firstLeaf(targetBlock);\n                const offset = selectingBackward ? nodeSize(leafNode) : 0;\n                selection.extend(leafNode, offset);\n                ev.preventDefault();\n            }\n        }\n    }\n\n    isSelectionInEditable({ anchorNode, focusNode } = {}) {\n        return (\n            !!anchorNode &&\n            !!focusNode &&\n            this.editable.contains(anchorNode) &&\n            (focusNode === anchorNode || this.editable.contains(focusNode))\n        );\n    }\n\n    isNodeEditable(node) {\n        const results = this.getResource(\"is_node_editable_predicates\")\n            .map((p) => p(node))\n            .filter((r) => r !== undefined);\n        if (!results.length) {\n            return node.parentElement?.isContentEditable;\n        }\n        return results.every((r) => r);\n    }\n\n    focusEditable() {\n        const { editableSelection, currentSelectionIsInEditable } = this.getSelectionData();\n        if (this.editable.contains(this.document.activeElement) && currentSelectionIsInEditable) {\n            // Editor has focus \u2014 nothing to do.\n            return;\n        }\n\n        const closestNonEditable = (node) => closestElement(node, (el) => !el.isContentEditable);\n        // If selection includes a non-editable element, focusing editor will move cursor to different position.\n        if (\n            !closestNonEditable(editableSelection.anchorNode) &&\n            !closestNonEditable(editableSelection.focusNode)\n        ) {\n            // Manualy focusing the editable is necessary to avoid some non-deterministic error in the HOOT unit tests.\n            this.editable.focus({ preventScroll: true });\n        }\n\n        if (!currentSelectionIsInEditable) {\n            // Selection is outside the editor \u2014 restore it.\n            const { anchorNode, anchorOffset, focusNode, focusOffset } = editableSelection;\n            const selection = this.document.getSelection();\n            if (selection) {\n                selection.setBaseAndExtent(anchorNode, anchorOffset, focusNode, focusOffset);\n            }\n        }\n    }\n\n    /**\n     * @returns {EditorSelection}\n     */\n    selectAroundNonEditable() {\n        // Get up-to-date selection\n        const { editableSelection } = this.getSelectionData();\n        // Avoid setting the selection if it's not inside an uneditable element\n        const isInUneditable = (node) => !!closestElement(node, (elem) => !elem.isContentEditable);\n        let { startContainer: start, endContainer: end } = editableSelection;\n        if (!(isInUneditable(start) || (end !== start && isInUneditable(end)))) {\n            return editableSelection;\n        }\n        // Normalize both sides\n        let { startOffset, endOffset, direction } = editableSelection;\n        [start, startOffset] = normalizeNotEditableNode(start, startOffset, \"left\");\n        [end, endOffset] = normalizeNotEditableNode(end, endOffset, \"right\");\n        // Set the new selection\n        const [anchorNode, anchorOffset, focusNode, focusOffset] = direction\n            ? [start, startOffset, end, endOffset]\n            : [end, endOffset, start, startOffset];\n        return this.setSelection({ anchorNode, anchorOffset, focusNode, focusOffset });\n    }\n}\n", "import { Plugin, isValidTargetForDomListener } from \"../plugin\";\nimport { closestBlock } from \"@html_editor/utils/blocks\";\nimport { fillEmpty } from \"@html_editor/utils/dom\";\nimport { leftLeafOnlyNotBlockPath } from \"@html_editor/utils/dom_state\";\n\n/**\n * @typedef {Object} Shortcut\n * @property {string} hotkey\n * @property {string} commandId\n * @property {Object} [commandParams]\n * @property {boolean} [global]\n *\n * @typedef {Shortcut[]} shortcuts\n *\n * Example:\n *\n *     resources = {\n *         // See UserCommand\n *         user_commands: [\n *             { id: \"myCommands\", run: myCommandFunction },\n *         ],\n *         // See Shortcut\n *         shortcuts: [\n *             { hotkey: \"control+shift+q\", commandId: \"myCommands\" },\n *         ],\n *     }\n */\n\n/**\n * @typedef {{\n *     pattern: RegExp;\n *     commandId: string;\n *     commandParams?: object;\n * }[]} shorthands\n */\n\nexport class ShortCutPlugin extends Plugin {\n    static id = \"shortcut\";\n    static dependencies = [\"userCommand\", \"selection\"];\n\n    /** @type {import(\"plugins\").EditorResources} */\n    resources = {\n        input_handlers: this.onInput.bind(this),\n    };\n\n    setup() {\n        const hotkeyService = this.services.hotkey;\n        if (!hotkeyService) {\n            throw new Error(\"ShorcutPlugin needs hotkey service to properly work\");\n        }\n        if (document !== this.document) {\n            hotkeyService.registerIframe({ contentWindow: this.window });\n        }\n        for (const shortcut of this.getResource(\"shortcuts\")) {\n            const command = this.dependencies.userCommand.getCommand(shortcut.commandId);\n            this.addShortcut(\n                shortcut.hotkey,\n                () => {\n                    command.run(shortcut.commandParams);\n                },\n                {\n                    isAvailable: command.isAvailable,\n                    global: !!shortcut.global,\n                }\n            );\n        }\n    }\n\n    addShortcut(hotkey, action, { isAvailable, global }) {\n        this._cleanups.push(\n            this.services.hotkey.add(hotkey, action, {\n                area: () => this.editable,\n                bypassEditableProtection: true,\n                allowRepeat: true,\n                isAvailable: (target) =>\n                    (!isAvailable ||\n                        isAvailable(this.dependencies.selection.getEditableSelection())) &&\n                    (global || isValidTargetForDomListener(target)),\n            })\n        );\n    }\n\n    onInput(ev) {\n        if (ev.data !== \" \") {\n            return;\n        }\n        const selection = this.dependencies.selection.getEditableSelection();\n        const blockEl = closestBlock(selection.anchorNode);\n        const leftDOMPath = leftLeafOnlyNotBlockPath(selection.anchorNode);\n        let spaceOffset = selection.anchorOffset;\n        let leftLeaf = leftDOMPath.next().value;\n        while (leftLeaf) {\n            // Calculate spaceOffset by adding lengths of previous text nodes\n            // to correctly find offset position for selection within inline\n            // elements. e.g. <p>ab<strong>cd []e</strong></p>\n            spaceOffset += leftLeaf.length;\n            leftLeaf = leftDOMPath.next().value;\n        }\n        const precedingText = blockEl.textContent.substring(0, spaceOffset - 1);\n        const matchedShortcut = this.getResource(\"shorthands\").find(({ pattern }) =>\n            pattern.test(precedingText)\n        );\n        if (matchedShortcut) {\n            const command = this.dependencies.userCommand.getCommand(matchedShortcut.commandId);\n            if (command) {\n                this.dependencies.selection.setSelection({\n                    anchorNode: blockEl.firstChild,\n                    anchorOffset: 0,\n                    focusNode: selection.focusNode,\n                    focusOffset: selection.focusOffset,\n                });\n                this.dependencies.selection.extractContent(\n                    this.dependencies.selection.getEditableSelection()\n                );\n                fillEmpty(blockEl);\n                command.run(matchedShortcut.commandParams);\n            }\n        }\n    }\n}\n", "import { callbacksForCursorUpdate } from \"@html_editor/utils/selection\";\nimport { Plugin } from \"../plugin\";\nimport { isBlock } from \"../utils/blocks\";\nimport { fillEmpty, splitTextNode } from \"../utils/dom\";\nimport {\n    isContentEditable,\n    isContentEditableAncestor,\n    isElement,\n    isTextNode,\n    isVisible,\n} from \"../utils/dom_info\";\nimport { prepareUpdate } from \"../utils/dom_state\";\nimport {\n    childNodes,\n    closestElement,\n    descendants,\n    firstLeaf,\n    lastLeaf,\n} from \"../utils/dom_traversal\";\nimport { DIRECTIONS, childNodeIndex, nodeSize } from \"../utils/position\";\nimport { isProtected, isProtecting } from \"@html_editor/utils/dom_info\";\n\n/**\n * @typedef { Object } SplitShared\n * @property { SplitPlugin['isUnsplittable'] } isUnsplittable\n * @property { SplitPlugin['splitAroundUntil'] } splitAroundUntil\n * @property { SplitPlugin['splitBlock'] } splitBlock\n * @property { SplitPlugin['splitBlockNode'] } splitBlockNode\n * @property { SplitPlugin['splitElement'] } splitElement\n * @property { SplitPlugin['splitElementBlock'] } splitElementBlock\n * @property { SplitPlugin['splitSelection'] } splitSelection\n */\n\n/**\n * @typedef {(({element: HTMLElement, secondPart: HTMLElement}) => void)[]} after_split_element_handlers\n * @typedef {(() => void)[]} before_split_block_handlers\n *\n * @typedef {((params: { targetNode: Node, targetOffset: number, blockToSplit: HTMLElement | null }) => void | true)[]} split_element_block_overrides\n *\n * @typedef {((node: Node) => boolean)[]} unsplittable_node_predicates\n */\n\nexport class SplitPlugin extends Plugin {\n    static dependencies = [\"baseContainer\", \"selection\", \"history\", \"input\", \"delete\", \"lineBreak\"];\n    static id = \"split\";\n    static shared = [\n        \"splitBlock\",\n        \"splitBlockNode\",\n        \"splitElementBlock\",\n        \"splitElement\",\n        \"splitAroundUntil\",\n        \"splitSelection\",\n        \"isUnsplittable\",\n    ];\n    /** @type {import(\"plugins\").EditorResources} */\n    resources = {\n        beforeinput_handlers: this.onBeforeInput.bind(this),\n\n        unsplittable_node_predicates: [\n            // An unremovable element is also unmergeable (as merging two\n            // elements results in removing one of them).\n            // An unmergeable element is unsplittable and vice-versa (as\n            // split and merge are reverse operations from one another).\n            // Therefore, unremovable nodes are also unsplittable.\n            (node) =>\n                this.getResource(\"unremovable_node_predicates\").some((predicate) =>\n                    predicate(node)\n                ),\n            // \"Unbreakable\" is a legacy term that means unsplittable and\n            // unmergeable.\n            (node) => node.classList?.contains(\"oe_unbreakable\"),\n            (node) => {\n                const isExplicitlyNotContentEditable = (node) =>\n                    // In the `contenteditable` attribute consideration,\n                    // disconnected nodes can be unsplittable only if they are\n                    // explicitly set under a contenteditable=\"false\" element.\n                    !isContentEditable(node) &&\n                    (node.isConnected || closestElement(node, \"[contenteditable]\"));\n                return (\n                    isExplicitlyNotContentEditable(node) ||\n                    // If node sets contenteditable='true' and is inside a non-editable\n                    // context, it has to be unsplittable since splitting it would modify\n                    // the non-editable parent content.\n                    (node.parentElement &&\n                        isContentEditableAncestor(node) &&\n                        isExplicitlyNotContentEditable(node.parentElement))\n                );\n            },\n            (node) => node.nodeName === \"SECTION\",\n        ],\n        selection_blocker_predicates: (blocker) => {\n            if (this.isUnsplittable(blocker)) {\n                return true;\n            }\n        },\n    };\n\n    // --------------------------------------------------------------------------\n    // commands\n    // --------------------------------------------------------------------------\n    splitBlock() {\n        this.dispatchTo(\"before_split_block_handlers\");\n        let selection = this.dependencies.selection.getSelectionData().deepEditableSelection;\n        if (!selection.isCollapsed) {\n            // @todo @phoenix collapseIfZWS is not tested\n            // this.shared.collapseIfZWS();\n            this.dependencies.delete.deleteSelection();\n            selection = this.dependencies.selection.getEditableSelection();\n        }\n\n        return this.splitBlockNode({\n            targetNode: selection.anchorNode,\n            targetOffset: selection.anchorOffset,\n        });\n    }\n\n    /**\n     * @param {Object} param0\n     * @param {Node} param0.targetNode\n     * @param {number} param0.targetOffset\n     * @returns {[HTMLElement|undefined, HTMLElement|undefined]}\n     */\n    splitBlockNode({ targetNode, targetOffset }) {\n        if (targetNode.nodeType === Node.TEXT_NODE) {\n            targetOffset = splitTextNode(targetNode, targetOffset);\n            targetNode = targetNode.parentElement;\n        }\n        const blockToSplit = closestElement(targetNode, isBlock);\n        const params = { targetNode, targetOffset, blockToSplit };\n\n        if (this.delegateTo(\"split_element_block_overrides\", params)) {\n            return [undefined, undefined];\n        }\n\n        return this.splitElementBlock(params);\n    }\n    /**\n     * @param {Object} param0\n     * @param {HTMLElement} param0.targetNode\n     * @param {number} param0.targetOffset\n     * @param {HTMLElement} param0.blockToSplit\n     * @returns {[HTMLElement|undefined, HTMLElement|undefined]}\n     */\n    splitElementBlock({ targetNode, targetOffset, blockToSplit }) {\n        // If the block is unsplittable, insert a line break instead.\n        if (this.isUnsplittable(blockToSplit)) {\n            // @todo: t-if, t-else etc are not blocks, but they are\n            // unsplittable.  The check must be done from the targetNode up to\n            // the block for unsplittables. There are apparently no tests for\n            // this.\n            this.dependencies.lineBreak.insertLineBreakElement({ targetNode, targetOffset });\n            return [undefined, undefined];\n        }\n        const restore = prepareUpdate(targetNode, targetOffset);\n\n        const [beforeElement, afterElement] = this.splitElementUntil(\n            targetNode,\n            targetOffset,\n            blockToSplit.parentElement\n        );\n        restore();\n        const fillEmptyElement = (node) => {\n            if (isProtecting(node) || isProtected(node)) {\n                // TODO ABD: add test\n                return;\n            } else if (node.nodeType === Node.TEXT_NODE && !isVisible(node)) {\n                const parent = node.parentElement;\n                node.remove();\n                fillEmptyElement(parent);\n            } else if (node.nodeType === Node.ELEMENT_NODE) {\n                if (node.hasAttribute(\"data-oe-zws-empty-inline\")) {\n                    delete node.dataset.oeZwsEmptyInline;\n                }\n                fillEmpty(node);\n            }\n        };\n        fillEmptyElement(lastLeaf(beforeElement));\n        fillEmptyElement(firstLeaf(afterElement));\n\n        this.dependencies.selection.setCursorStart(afterElement);\n\n        return [beforeElement, afterElement];\n    }\n\n    /**\n     * @param {Node} node\n     * @returns {boolean}\n     */\n    isUnsplittable(node) {\n        return this.getResource(\"unsplittable_node_predicates\").some((p) => p(node));\n    }\n\n    /**\n     * Split the given element at the given offset. The element will be removed in\n     * the process so caution is advised in dealing with its reference. Returns a\n     * tuple containing the new elements on both sides of the split.\n     *\n     * @param {HTMLElement} element\n     * @param {number} offset\n     * @returns {[HTMLElement, HTMLElement]}\n     */\n    splitElement(element, offset) {\n        /** @type {HTMLElement} **/\n        const firstPart = element.cloneNode();\n        /** @type {HTMLElement} **/\n        const secondPart = element.cloneNode();\n        element.before(firstPart);\n        element.after(secondPart);\n        const children = childNodes(element);\n        firstPart.append(...children.slice(0, offset));\n        secondPart.append(...children.slice(offset));\n        element.remove();\n        this.dispatchTo(\"after_split_element_handlers\", { firstPart, secondPart });\n        return [firstPart, secondPart];\n    }\n\n    /**\n     * Split the given element at the given offset, until the given limit ancestor.\n     * The element will be removed in the process so caution is advised in dealing\n     * with its reference. Returns a tuple containing the new elements on both sides\n     * of the split.\n     *\n     * @param {HTMLElement} element\n     * @param {number} offset\n     * @param {HTMLElement} limitAncestor\n     * @returns {[HTMLElement, HTMLElement]}\n     */\n    splitElementUntil(element, offset, limitAncestor) {\n        if (element === limitAncestor) {\n            return [element, element];\n        }\n        let [before, after] = this.splitElement(element, offset);\n        if (after.parentElement !== limitAncestor) {\n            const afterIndex = childNodeIndex(after);\n            [before, after] = this.splitElementUntil(\n                after.parentElement,\n                afterIndex,\n                limitAncestor\n            );\n        }\n        return [before, after];\n    }\n\n    /**\n     * Split around the given elements, until a given ancestor (included). Elements\n     * will be removed in the process so caution is advised in dealing with their\n     * references. Returns the new split root element that is a clone of\n     * limitAncestor or the original limitAncestor if no split occured.\n     *\n     * @param {Node[] | Node} elements\n     * @param {HTMLElement} limitAncestor\n     * @returns { Node }\n     */\n    splitAroundUntil(elements, limitAncestor, cursors = null) {\n        elements = Array.isArray(elements) ? elements : [elements];\n        const firstNode = elements[0];\n        const lastNode = elements[elements.length - 1];\n        if ([firstNode, lastNode].includes(limitAncestor)) {\n            return limitAncestor;\n        }\n        let before = firstNode.previousSibling;\n        let after = lastNode.nextSibling;\n        let beforeSplit, afterSplit;\n        if (\n            !before &&\n            !after &&\n            firstNode.parentElement !== limitAncestor &&\n            lastNode.parentElement !== limitAncestor\n        ) {\n            return this.splitAroundUntil(\n                [firstNode.parentElement, lastNode.parentElement],\n                limitAncestor,\n                cursors\n            );\n        } else if (!after && lastNode.parentElement !== limitAncestor) {\n            return this.splitAroundUntil(\n                [firstNode, lastNode.parentElement],\n                limitAncestor,\n                cursors\n            );\n        } else if (!before && firstNode.parentElement !== limitAncestor) {\n            return this.splitAroundUntil(\n                [firstNode.parentElement, lastNode],\n                limitAncestor,\n                cursors\n            );\n        }\n        // Split up ancestors up to font\n        while (after && after.parentElement !== limitAncestor) {\n            afterSplit = this.splitElement(after.parentElement, childNodeIndex(after))[0];\n            after = afterSplit.nextSibling;\n        }\n        if (after) {\n            afterSplit = this.splitElement(limitAncestor, childNodeIndex(after))[0];\n            limitAncestor = afterSplit;\n        }\n        while (before && before.parentElement !== limitAncestor) {\n            beforeSplit = this.splitElement(before.parentElement, childNodeIndex(before) + 1)[1];\n            before = beforeSplit.previousSibling;\n        }\n        if (before) {\n            beforeSplit = this.splitElement(limitAncestor, childNodeIndex(before) + 1)[1];\n        }\n        const result = beforeSplit || afterSplit || limitAncestor;\n        this.fixSplitAroundUntilEmptyNodes(result.parentElement, cursors);\n        return result;\n    }\n\n    /**\n     * Fix for stable to remove empty nodes created by `splitAroundUntil`\n     * and properly manage the cursor.\n     * @param {Node} node\n     * @param {HTMLElement} limitAncestor\n     * @returns { Node }\n     */\n    fixSplitAroundUntilEmptyNodes(node, cursors) {\n        node &&\n            descendants(node)\n                .filter(\n                    (node) =>\n                        isElement(node) &&\n                        node.childNodes.length &&\n                        [...node.childNodes].every((n) => isTextNode(n)) &&\n                        !node.textContent.replaceAll(\"\\ufeff\", \"\")\n                )\n                .forEach((node) => {\n                    cursors?.update(callbacksForCursorUpdate.remove(node));\n                    node.remove();\n                });\n    }\n\n    splitSelection() {\n        let { startContainer, startOffset, endContainer, endOffset, direction } =\n            this.dependencies.selection.getEditableSelection();\n        const isInSingleContainer = startContainer === endContainer;\n        if (isTextNode(endContainer) && endOffset > 0 && endOffset < nodeSize(endContainer)) {\n            const endParent = endContainer.parentNode;\n            const splitOffset = splitTextNode(endContainer, endOffset);\n            endContainer = endParent.childNodes[splitOffset - 1] || endParent.firstChild;\n            if (isInSingleContainer) {\n                startContainer = endContainer;\n            }\n            endOffset = endContainer.textContent.length;\n        }\n        if (\n            isTextNode(startContainer) &&\n            startOffset > 0 &&\n            startOffset < nodeSize(startContainer)\n        ) {\n            splitTextNode(startContainer, startOffset);\n            startOffset = 0;\n            if (isInSingleContainer) {\n                endOffset = startContainer.textContent.length;\n            }\n        }\n\n        const selection =\n            direction === DIRECTIONS.RIGHT\n                ? {\n                      anchorNode: startContainer,\n                      anchorOffset: startOffset,\n                      focusNode: endContainer,\n                      focusOffset: endOffset,\n                  }\n                : {\n                      anchorNode: endContainer,\n                      anchorOffset: endOffset,\n                      focusNode: startContainer,\n                      focusOffset: startOffset,\n                  };\n        return this.dependencies.selection.setSelection(selection, { normalize: false });\n    }\n\n    onBeforeInput(e) {\n        if (e.inputType === \"insertParagraph\") {\n            e.preventDefault();\n            this.splitBlock();\n            this.dependencies.history.addStep();\n        }\n    }\n}\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { backgroundImageCssToParts, backgroundImagePartsToCss } from \"@html_editor/utils/image\";\n\n/**\n * @typedef { Object } StyleShared\n * @property { StylePlugin['setBackgroundImageUrl'] } setBackgroundImageUrl\n */\n\nexport class StylePlugin extends Plugin {\n    static id = \"style\";\n    static shared = [\"setBackgroundImageUrl\"];\n\n    setBackgroundImageUrl(el, value) {\n        const parts = backgroundImageCssToParts(el.style[\"background-image\"]);\n        if (value) {\n            parts.url = `url('${value}')`;\n        } else {\n            delete parts.url;\n        }\n        el.style[\"background-image\"] = backgroundImagePartsToCss(parts);\n    }\n}\n", "import { Plugin } from \"../plugin\";\n\n/**\n * @typedef { import(\"./selection_plugin\").EditorSelection } EditorSelection\n */\n\n/**\n * @typedef { Object } UserCommand\n * @property { string } id\n * @property { Function } run\n * @property { String } [title]\n * @property { String } [description]\n * @property { string } [icon]\n * @property { (selection: EditorSelection) => boolean  } [isAvailable]\n */\n\n/**\n * @typedef { Object } UserCommandShared\n * @property { UserCommandPlugin['getCommand'] } getCommand\n */\n\n/**\n * @typedef {UserCommand[]} user_commands\n */\n\nexport class UserCommandPlugin extends Plugin {\n    static id = \"userCommand\";\n    static shared = [\"getCommand\"];\n\n    setup() {\n        this.commands = {};\n        for (const command of this.getResource(\"user_commands\")) {\n            if (command.id in this.commands) {\n                throw new Error(`Duplicate user command id: ${command.id}`);\n            }\n            this.commands[command.id] = command;\n        }\n        Object.freeze(this.commands);\n    }\n\n    /**\n     * @param {string} commandId\n     * @returns {UserCommand}\n     * @throws {Error} if the command ID is unknown.\n     */\n    getCommand(commandId) {\n        const command = this.commands[commandId];\n        if (!command) {\n            throw new Error(`Unknown user command id: ${commandId}`);\n        }\n        return command;\n    }\n}\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { closestBlock } from \"@html_editor/utils/blocks\";\nimport { isVisibleTextNode } from \"@html_editor/utils/dom_info\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { AlignSelector } from \"./align_selector\";\nimport { reactive } from \"@odoo/owl\";\nimport { isHtmlContentSupported } from \"@html_editor/core/selection_plugin\";\nimport { weakMemoize } from \"@html_editor/utils/functions\";\n\nconst alignmentItems = [\n    { mode: \"left\" },\n    { mode: \"center\" },\n    { mode: \"right\" },\n    { mode: \"justify\" },\n];\n\nexport class AlignPlugin extends Plugin {\n    static id = \"align\";\n    static dependencies = [\"history\", \"selection\"];\n    /** @type {import(\"plugins\").EditorResources} */\n    resources = {\n        user_commands: [\n            {\n                id: \"alignLeft\",\n                run: () => this.setAlignment(\"left\"),\n                isAvailable: this.canSetAlignment.bind(this),\n            },\n            {\n                id: \"alignCenter\",\n                run: () => this.setAlignment(\"center\"),\n                isAvailable: this.canSetAlignment.bind(this),\n            },\n            {\n                id: \"alignRight\",\n                run: () => this.setAlignment(\"right\"),\n                isAvailable: this.canSetAlignment.bind(this),\n            },\n            {\n                id: \"justify\",\n                run: () => this.setAlignment(\"justify\"),\n                isAvailable: this.canSetAlignment.bind(this),\n            },\n        ],\n        toolbar_items: [\n            {\n                id: \"alignment\",\n                groupId: \"layout\",\n                description: _t(\"Align text\"),\n                Component: AlignSelector,\n                props: {\n                    getItems: () => alignmentItems,\n                    getDisplay: () => this.alignment,\n                    onSelected: (item) => {\n                        this.setAlignment(item.mode);\n                    },\n                },\n                isAvailable: this.canSetAlignment.bind(this),\n            },\n        ],\n\n        /** Handlers */\n        selectionchange_handlers: this.updateAlignmentParams.bind(this),\n        post_undo_handlers: this.updateAlignmentParams.bind(this),\n        post_redo_handlers: this.updateAlignmentParams.bind(this),\n        remove_all_formats_handlers: this.setAlignment.bind(this),\n\n        /** Predicates */\n        has_format_predicates: (node) => closestBlock(node)?.style.textAlign,\n    };\n\n    setup() {\n        this.alignment = reactive({ displayName: \"\" });\n        this.canSetAlignmentMemoized = weakMemoize(\n            (selection) => isHtmlContentSupported(selection) && this.getBlocksToAlign().length > 0\n        );\n    }\n\n    get alignmentMode() {\n        const sel = this.dependencies.selection.getSelectionData().deepEditableSelection;\n        const block = closestBlock(sel?.anchorNode);\n        const textAlign = this.getTextAlignment(block);\n        return [\"center\", \"right\", \"justify\"].includes(textAlign) ? textAlign : \"left\";\n    }\n\n    getTextAlignment(block) {\n        const { direction, textAlign } = getComputedStyle(block);\n        if (textAlign === \"start\") {\n            return direction === \"rtl\" ? \"right\" : \"left\";\n        } else if (textAlign === \"end\") {\n            return direction === \"rtl\" ? \"left\" : \"right\";\n        }\n        return textAlign;\n    }\n\n    getBlocksToAlign() {\n        return this.dependencies.selection\n            .getTargetedNodes()\n            .filter((node) => isVisibleTextNode(node) || node.nodeName === \"BR\")\n            .map((node) => closestBlock(node))\n            .filter((block) => block.isContentEditable);\n    }\n\n    setAlignment(mode = \"\") {\n        const visitedBlocks = new Set();\n        let isAlignmentUpdated = false;\n\n        for (const block of this.getBlocksToAlign()) {\n            if (!visitedBlocks.has(block)) {\n                const currentTextAlign = this.getTextAlignment(block);\n                if (currentTextAlign !== mode) {\n                    block.style.textAlign = mode;\n                    isAlignmentUpdated = true;\n                }\n                visitedBlocks.add(block);\n            }\n        }\n        if (mode && isAlignmentUpdated) {\n            this.dependencies.history.addStep();\n        }\n        this.updateAlignmentParams();\n    }\n\n    canSetAlignment(selection) {\n        return this.canSetAlignmentMemoized(selection);\n    }\n\n    updateAlignmentParams() {\n        this.alignment.displayName = this.alignmentMode;\n    }\n}\n", "import { Component, useState } from \"@odoo/owl\";\nimport { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\nimport { toolbarButtonProps } from \"@html_editor/main/toolbar/toolbar\";\nimport { useDropdownAutoVisibility } from \"@html_editor/dropdown_autovisibility_hook\";\nimport { useChildRef } from \"@web/core/utils/hooks\";\n\nexport class AlignSelector extends Component {\n    static template = \"html_editor.AlignSelector\";\n    static props = {\n        getItems: Function,\n        getDisplay: Function,\n        onSelected: Function,\n        ...toolbarButtonProps,\n    };\n    static components = { Dropdown, DropdownItem };\n\n    setup() {\n        this.items = this.props.getItems();\n        this.state = useState(this.props.getDisplay());\n        this.menuRef = useChildRef();\n        useDropdownAutoVisibility(this.env.overlayState, this.menuRef);\n    }\n\n    onSelected(item) {\n        this.props.onSelected(item);\n    }\n}\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { fillShrunkPhrasingParent } from \"@html_editor/utils/dom\";\nimport { closestElement } from \"@html_editor/utils/dom_traversal\";\nimport { parseHTML } from \"@html_editor/utils/html\";\nimport { withSequence } from \"@html_editor/utils/resource\";\nimport { htmlEscape } from \"@odoo/owl\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { closestBlock } from \"@html_editor/utils/blocks\";\nimport { isParagraphRelatedElement } from \"../utils/dom_info\";\nimport { isHtmlContentSupported } from \"@html_editor/core/selection_plugin\";\n\nfunction isAvailable(selection) {\n    return (\n        isHtmlContentSupported(selection) &&\n        !closestElement(selection.anchorNode, \".o_editor_banner\")\n    );\n}\n\n/**\n * @typedef { Object } BannerShared\n * @property { BannerPlugin['insertBanner'] } insertBanner\n */\n\nexport class BannerPlugin extends Plugin {\n    static id = \"banner\";\n    // sanitize plugin is required to handle `contenteditable` attribute.\n    static dependencies = [\"baseContainer\", \"history\", \"dom\", \"emoji\", \"selection\", \"sanitize\"];\n    static shared = [\"insertBanner\"];\n    /** @type {import(\"plugins\").EditorResources} */\n    resources = {\n        user_commands: [\n            {\n                id: \"banner_info\",\n                title: _t(\"Banner Info\"),\n                description: _t(\"Insert an info banner\"),\n                icon: \"fa-info-circle\",\n                isAvailable,\n                run: () => {\n                    this.insertBanner(_t(\"Banner Info\"), \"\ud83d\udca1\", \"info\");\n                },\n            },\n            {\n                id: \"banner_success\",\n                title: _t(\"Banner Success\"),\n                description: _t(\"Insert a success banner\"),\n                icon: \"fa-check-circle\",\n                isAvailable,\n                run: () => {\n                    this.insertBanner(_t(\"Banner Success\"), \"\u2705\", \"success\");\n                },\n            },\n            {\n                id: \"banner_warning\",\n                title: _t(\"Banner Warning\"),\n                description: _t(\"Insert a warning banner\"),\n                icon: \"fa-exclamation-triangle\",\n                isAvailable,\n                run: () => {\n                    this.insertBanner(_t(\"Banner Warning\"), \"\u26a0\ufe0f\", \"warning\");\n                },\n            },\n            {\n                id: \"banner_danger\",\n                title: _t(\"Banner Danger\"),\n                description: _t(\"Insert a danger banner\"),\n                icon: \"fa-exclamation-circle\",\n                isAvailable,\n                run: () => {\n                    this.insertBanner(_t(\"Banner Danger\"), \"\u274c\", \"danger\");\n                },\n            },\n        ],\n        powerbox_categories: withSequence(20, { id: \"banner\", name: _t(\"Banner\") }),\n        powerbox_items: [\n            {\n                commandId: \"banner_info\",\n                categoryId: \"banner\",\n            },\n            {\n                commandId: \"banner_success\",\n                categoryId: \"banner\",\n            },\n            {\n                commandId: \"banner_warning\",\n                categoryId: \"banner\",\n            },\n            {\n                commandId: \"banner_danger\",\n                categoryId: \"banner\",\n            },\n        ],\n        power_buttons_visibility_predicates: ({ anchorNode }) =>\n            !closestElement(anchorNode, \".o_editor_banner\"),\n        move_node_blacklist_selectors: \".o_editor_banner *\",\n        move_node_whitelist_selectors: \".o_editor_banner\",\n    };\n\n    setup() {\n        this.addDomListener(this.editable, \"click\", (e) => {\n            if (e.target.classList.contains(\"o_editor_banner_icon\")) {\n                this.onBannerEmojiChange(e.target);\n            }\n        });\n    }\n\n    insertBanner(title, emoji, alertClass, containerClass = \"\", contentClass = \"\") {\n        containerClass = containerClass ? `${containerClass} ` : \"\";\n        contentClass = contentClass ? `${contentClass} ` : \"\";\n\n        const selection = this.dependencies.selection.getEditableSelection();\n        const blockEl = closestBlock(selection.anchorNode);\n        let baseContainer;\n        if (isParagraphRelatedElement(blockEl)) {\n            baseContainer = this.document.createElement(blockEl.nodeName);\n            baseContainer.append(...blockEl.childNodes);\n        } else if (blockEl.nodeName === \"LI\") {\n            baseContainer = this.dependencies.baseContainer.createBaseContainer();\n            baseContainer.append(...blockEl.childNodes);\n            fillShrunkPhrasingParent(blockEl);\n        } else {\n            baseContainer = this.dependencies.baseContainer.createBaseContainer();\n            fillShrunkPhrasingParent(baseContainer);\n        }\n        const baseContainerHtml = baseContainer.outerHTML;\n        const bannerElement = parseHTML(\n            this.document,\n            `<div class=\"${containerClass}o_editor_banner user-select-none o-contenteditable-false lh-1 d-flex align-items-center alert alert-${alertClass} pb-0 pt-3\" data-oe-role=\"status\">\n                <i class=\"o_editor_banner_icon mb-3 fst-normal\" data-oe-aria-label=\"${htmlEscape(\n                    title\n                )}\">${emoji}</i>\n                <div class=\"${contentClass}o_editor_banner_content o-contenteditable-true w-100 px-3\">\n                    ${baseContainerHtml}\n                </div>\n            </div>`\n        ).childNodes[0];\n        this.dependencies.dom.insert(bannerElement);\n        this.dependencies.selection.setCursorEnd(\n            bannerElement.querySelector(`.o_editor_banner_content > ${baseContainer.tagName}`)\n        );\n        // Remove the old element.\n        if (bannerElement.nextSibling?.nodeName === blockEl.nodeName) {\n            bannerElement.nextSibling.remove();\n        }\n        this.dependencies.history.addStep();\n    }\n\n    onBannerEmojiChange(iconElement) {\n        this.dependencies.emoji.showEmojiPicker({\n            target: iconElement,\n            onSelect: (emoji) => {\n                iconElement.textContent = emoji;\n                this.dependencies.history.addStep();\n            },\n        });\n    }\n}\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { Dialog } from \"@web/core/dialog/dialog\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { Component, useState, onWillDestroy, status, markup } from \"@odoo/owl\";\n\nconst POSTPROCESS_GENERATED_CONTENT = (content, baseContainer) => {\n    let lines = content.split(\"\\n\");\n    if (baseContainer.toUpperCase() === \"P\") {\n        // P has a margin bottom which is used as an interline, no need to\n        // keep empty lines in that case.\n        lines = lines.filter((line) => line.trim().length);\n    }\n    const fragment = document.createDocumentFragment();\n    let parentUl, parentOl;\n    let lineIndex = 0;\n    for (const line of lines) {\n        if (line.trim().startsWith(\"- \")) {\n            // Create or continue an unordered list.\n            parentUl = parentUl || document.createElement(\"ul\");\n            const li = document.createElement(\"li\");\n            li.innerText = line.trim().slice(2);\n            parentUl.appendChild(li);\n        } else if (\n            (parentOl && line.startsWith(`${parentOl.children.length + 1}. `)) ||\n            (!parentOl && line.startsWith(\"1. \") && lines[lineIndex + 1]?.startsWith(\"2. \"))\n        ) {\n            // Create or continue an ordered list (only if the line starts\n            // with the next number in the current ordered list (or 1 if no\n            // ordered list was in progress and it's followed by a 2).\n            parentOl = parentOl || document.createElement(\"ol\");\n            const li = document.createElement(\"li\");\n            li.innerText = line.slice(line.indexOf(\".\") + 2);\n            parentOl.appendChild(li);\n        } else if (line.trim().length === 0) {\n            const emptyLine = document.createElement(\"DIV\");\n            emptyLine.append(document.createElement(\"BR\"));\n            fragment.appendChild(emptyLine);\n        } else {\n            // Insert any list in progress, and a new block for the current\n            // line.\n            [parentUl, parentOl].forEach((list) => list && fragment.appendChild(list));\n            parentUl = parentOl = undefined;\n            const block = document.createElement(line.startsWith(\"Title: \") ? \"h2\" : baseContainer);\n            block.innerText = line;\n            fragment.appendChild(block);\n        }\n        lineIndex += 1;\n    }\n    [parentUl, parentOl].forEach((list) => list && fragment.appendChild(list));\n    return fragment;\n};\n\nexport class ChatGPTDialog extends Component {\n    static template = \"\";\n    static components = { Dialog };\n    static props = {\n        insert: { type: Function },\n        close: { type: Function },\n        sanitize: { type: Function },\n        baseContainer: { type: String, optional: true },\n    };\n    static defaultProps = {\n        baseContainer: \"DIV\",\n    };\n\n    setup() {\n        this.notificationService = useService(\"notification\");\n        this.state = useState({ selectedMessageId: null });\n        onWillDestroy(() => this.pendingRpcPromise?.abort());\n    }\n\n    selectMessage(ev) {\n        this.state.selectedMessageId = +ev.currentTarget.getAttribute(\"data-message-id\");\n    }\n\n    insertMessage(ev) {\n        this.selectMessage(ev);\n        this._confirm();\n    }\n\n    formatContent(content) {\n        const fragment = POSTPROCESS_GENERATED_CONTENT(content, this.props.baseContainer);\n        let result = \"\";\n        for (const child of fragment.children) {\n            this.props.sanitize(child, { IN_PLACE: true });\n            result += child.outerHTML;\n        }\n        return markup(result);\n    }\n\n    generate(prompt, callback) {\n        const protectedCallback = (...args) => {\n            if (status(this) !== \"destroyed\") {\n                delete this.pendingRpcPromise;\n                return callback(...args);\n            }\n        };\n        this.pendingRpcPromise = rpc(\n            \"/html_editor/generate_text\",\n            {\n                prompt,\n                conversation_history: this.state.conversationHistory,\n            },\n            { silent: true }\n        );\n        return this.pendingRpcPromise\n            .then((content) => protectedCallback(content))\n            .catch((error) => protectedCallback(_t(error.data?.message || error.message), true));\n    }\n\n    _cancel() {\n        this.props.close();\n    }\n\n    _confirm() {\n        try {\n            this.props.close();\n            const text = this.state.messages.find(\n                (message) => message.id === this.state.selectedMessageId\n            )?.text;\n            this.notificationService.add(_t(\"Your content was successfully generated.\"), {\n                title: _t(\"Content generated\"),\n                type: \"success\",\n            });\n            const fragment = POSTPROCESS_GENERATED_CONTENT(text || \"\", this.props.baseContainer);\n            this.props.sanitize(fragment, { IN_PLACE: true });\n            this.props.insert(fragment);\n        } catch (e) {\n            this.props.close();\n            throw e;\n        }\n    }\n}\n", "import { useState } from \"@odoo/owl\";\nimport { ChatGPTDialog } from \"./chatgpt_dialog\";\n\nexport class ChatGPTTranslateDialog extends ChatGPTDialog {\n    static template = \"html_editor.ChatGPTTranslateDialog\";\n    static props = {\n        ...super.props,\n        originalText: String,\n        language: String,\n    };\n\n    setup() {\n        super.setup();\n        this.state = useState({\n            ...this.state,\n            conversationHistory: [\n                {\n                    role: \"system\",\n                    content:\n                        \"You are a translation assistant. You goal is to translate text while maintaining the original format and\" +\n                        \"respecting specific instructions. \\n\" +\n                        \"Instructions: \\n\" +\n                        \"- You must respect the format (wrapping the translated text between <generated_text> and </generated_text>)\\n\" +\n                        \"- Do not write HTML.\",\n                },\n            ],\n            messages: [],\n            translationInProgress: true,\n        });\n        this.translate();\n    }\n\n    async translate() {\n        const query = `Translate <generated_text>${this.props.originalText}</generated_text> to ${this.props.language}`;\n        const messageId = new Date().getTime();\n        await this.generate(query, (content, isError) => {\n            let translatedText = content\n                .replace(/^[\\s\\S]*<generated_text>/, \"\")\n                .replace(/<\\/generated_text>[\\s\\S]*$/, \"\");\n            if (!this.formatContent(translatedText).length) {\n                isError = true;\n                translatedText = \"You didn't select any text.\";\n            }\n            this.state.translationInProgress = false;\n            if (!isError) {\n                // There was no error, add the response to the history.\n                this.state.conversationHistory.push(\n                    {\n                        role: \"user\",\n                        content: query,\n                    },\n                    {\n                        role: \"assistant\",\n                        content,\n                    }\n                );\n            }\n            this.state.messages.push({\n                author: \"assistant\",\n                text: translatedText,\n                id: messageId,\n                isError,\n            });\n            this.state.selectedMessageId = messageId;\n        });\n    }\n}\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { Plugin } from \"@html_editor/plugin\";\nimport { closestElement } from \"@html_editor/utils/dom_traversal\";\nimport { ChatGPTTranslateDialog } from \"@html_editor/main/chatgpt/chatgpt_translate_dialog\";\nimport { LanguageSelector } from \"@html_editor/main/chatgpt/language_selector\";\nimport { withSequence } from \"@html_editor/utils/resource\";\nimport { user } from \"@web/core/user\";\nimport { isContentEditable } from \"@html_editor/utils/dom_info\";\n\nexport class ChatGPTTranslatePlugin extends Plugin {\n    static id = \"chatgpt_translate\";\n    static dependencies = [\n        \"baseContainer\",\n        \"selection\",\n        \"history\",\n        \"dom\",\n        \"sanitize\",\n        \"dialog\",\n        \"split\",\n    ];\n    /** @type {import(\"plugins\").EditorResources} */\n    resources = {\n        toolbar_groups: withSequence(50, {\n            id: \"ai\",\n        }),\n        toolbar_items: [\n            {\n                id: \"translate\",\n                groupId: \"ai\",\n                description: _t(\"Translate with AI\"),\n                isAvailable: (selection) => !selection.isCollapsed && user.userId,\n                isDisabled: this.isNotReplaceableByAI.bind(this),\n                Component: LanguageSelector,\n                props: {\n                    onSelected: (language) => this.openDialog({ language }),\n                },\n            },\n        ],\n    };\n\n    isNotReplaceableByAI(selection = this.dependencies.selection.getEditableSelection()) {\n        const isEmpty = !selection.textContent().replace(/\\s+/g, \"\");\n        const cannotReplace = this.dependencies.selection\n            .getTargetedNodes()\n            .find((el) => this.dependencies.split.isUnsplittable(el) || !isContentEditable(el));\n        return cannotReplace || isEmpty;\n    }\n\n    openDialog(params = {}) {\n        const selection = this.dependencies.selection.getEditableSelection();\n        const dialogParams = {\n            insert: (content) => {\n                const insertedNodes = this.dependencies.dom.insert(content);\n                this.dependencies.history.addStep();\n                // Add a frame around the inserted content to highlight it for 2\n                // seconds.\n                const start = insertedNodes?.length && closestElement(insertedNodes[0]);\n                const end =\n                    insertedNodes?.length &&\n                    closestElement(insertedNodes[insertedNodes.length - 1]);\n                if (start && end) {\n                    const divContainer = this.editable.parentElement;\n                    let [parent, left, top] = [\n                        start.offsetParent,\n                        start.offsetLeft,\n                        start.offsetTop - start.scrollTop,\n                    ];\n                    while (parent && !parent.contains(divContainer)) {\n                        left += parent.offsetLeft;\n                        top += parent.offsetTop - parent.scrollTop;\n                        parent = parent.offsetParent;\n                    }\n                    let [endParent, endTop] = [end.offsetParent, end.offsetTop - end.scrollTop];\n                    while (endParent && !endParent.contains(divContainer)) {\n                        endTop += endParent.offsetTop - endParent.scrollTop;\n                        endParent = endParent.offsetParent;\n                    }\n                    const div = document.createElement(\"div\");\n                    div.classList.add(\"o-chatgpt-content\");\n                    const FRAME_PADDING = 3;\n                    div.style.left = `${left - FRAME_PADDING}px`;\n                    div.style.top = `${top - FRAME_PADDING}px`;\n                    div.style.width = `${\n                        Math.max(start.offsetWidth, end.offsetWidth) + FRAME_PADDING * 2\n                    }px`;\n                    div.style.height = `${endTop + end.offsetHeight - top + FRAME_PADDING * 2}px`;\n                    divContainer.prepend(div);\n                    setTimeout(() => div.remove(), 2000);\n                }\n            },\n            ...params,\n        };\n        dialogParams.baseContainer = this.dependencies.baseContainer.getDefaultNodeName();\n        // collapse to end\n        const sanitize = this.dependencies.sanitize.sanitize;\n        const originalText = selection.textContent() || \"\";\n        this.dependencies.dialog.addDialog(ChatGPTTranslateDialog, {\n            ...dialogParams,\n            originalText,\n            sanitize,\n        });\n        if (this.services.ui.isSmall) {\n            // TODO: Find a better way and avoid modifying range\n            // HACK: In the case of opening through dropdown:\n            // - when dropdown open, it keep the element focused before the open\n            // - when opening the dialog through the dropdown, the dropdown closes\n            // - upon close, the generic code of the dropdown sets focus on the kept element (in our case, the editable)\n            // - we need to remove the range after the generic code of the dropdown is triggered so we hack it by removing the range in the next tick\n            Promise.resolve().then(() => {\n                // If the dialog is opened on a small screen, remove all selection\n                // because the selection can be seen through the dialog on some devices.\n                this.document.getSelection()?.removeAllRanges();\n            });\n        }\n    }\n}\n", "import { Component, onWillStart, useState } from \"@odoo/owl\";\nimport { useChildRef, useService } from \"@web/core/utils/hooks\";\nimport { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\nimport { loadLanguages } from \"@web/core/l10n/translation\";\nimport { jsToPyLocale } from \"@web/core/l10n/utils\";\nimport { toolbarButtonProps } from \"@html_editor/main/toolbar/toolbar\";\nimport { user } from \"@web/core/user\";\nimport { useDropdownAutoVisibility } from \"@html_editor/dropdown_autovisibility_hook\";\n\nexport class LanguageSelector extends Component {\n    static template = \"html_editor.LanguageSelector\";\n    static props = {\n        ...toolbarButtonProps,\n        onSelected: { type: Function },\n    };\n    static components = { Dropdown, DropdownItem };\n\n    setup() {\n        this.orm = useService(\"orm\");\n        this.state = useState({\n            languages: [],\n        });\n        this.menuRef = useChildRef();\n        useDropdownAutoVisibility(this.env.overlayState, this.menuRef);\n        onWillStart(() => {\n            if (user.userId) {\n                const userLang = jsToPyLocale(user.lang);\n                loadLanguages(this.orm).then((res) => {\n                    const userLangIndex = res.findIndex((lang) => lang[0] === userLang);\n                    if (userLangIndex !== -1) {\n                        const [userLangItem] = res.splice(userLangIndex, 1);\n                        res.unshift(userLangItem);\n                    }\n                    this.state.languages = res;\n                });\n            }\n        });\n    }\n    onSelected(language) {\n        this.props.onSelected(language);\n    }\n}\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { Plugin } from \"@html_editor/plugin\";\nimport { closestBlock } from \"@html_editor/utils/blocks\";\nimport { unwrapContents } from \"@html_editor/utils/dom\";\nimport { closestElement, firstLeaf } from \"@html_editor/utils/dom_traversal\";\nimport { baseContainerGlobalSelector } from \"@html_editor/utils/base_container\";\nimport { isHtmlContentSupported } from \"@html_editor/core/selection_plugin\";\n\nconst REGEX_BOOTSTRAP_COLUMN = /(?:^| )col(-[a-zA-Z]+)?(-\\d+)?(?= |$)/;\n\nfunction isUnremovableColumn(node, root) {\n    const isColumnInnerStructure =\n        node.nodeName === \"DIV\" && [...node.classList].some((cls) => /^row$|^col$|^col-/.test(cls));\n\n    if (!isColumnInnerStructure) {\n        return false;\n    }\n    if (!root) {\n        return true;\n    }\n    const closestColumnContainer = closestElement(node, \"div.o_text_columns\");\n    return !root.contains(closestColumnContainer);\n}\n\nfunction columnIsAvailable(numberOfColumns) {\n    return (selection) => {\n        const row = closestElement(selection.anchorNode, \".o_text_columns .row\");\n        return !(row && row.childElementCount === numberOfColumns);\n    };\n}\n\nexport class ColumnPlugin extends Plugin {\n    static id = \"column\";\n    static dependencies = [\"baseContainer\", \"selection\", \"history\", \"dom\"];\n    /** @type {import(\"plugins\").EditorResources} */\n    resources = {\n        user_commands: [\n            {\n                id: \"columnize\",\n                title: _t(\"Columnize\"),\n                description: _t(\"Convert into columns\"),\n                icon: \"fa-columns\",\n                run: this.columnize.bind(this),\n                isAvailable: isHtmlContentSupported,\n            },\n        ],\n        powerbox_items: [\n            {\n                title: _t(\"2 columns\"),\n                description: _t(\"Convert into 2 columns\"),\n                categoryId: \"structure\",\n                isAvailable: columnIsAvailable(2),\n                commandId: \"columnize\",\n                commandParams: 2,\n            },\n            {\n                title: _t(\"3 columns\"),\n                description: _t(\"Convert into 3 columns\"),\n                categoryId: \"structure\",\n                isAvailable: columnIsAvailable(3),\n                commandId: \"columnize\",\n                commandParams: 3,\n            },\n            {\n                title: _t(\"4 columns\"),\n                description: _t(\"Convert into 4 columns\"),\n                categoryId: \"structure\",\n                isAvailable: columnIsAvailable(4),\n                commandId: \"columnize\",\n                commandParams: 4,\n            },\n            {\n                title: _t(\"Remove columns\"),\n                description: _t(\"Back to one column\"),\n                categoryId: \"structure\",\n                isAvailable: (selection) =>\n                    !!closestElement(selection.anchorNode, \".o_text_columns .row\"),\n                commandId: \"columnize\",\n                commandParams: 0,\n            },\n        ],\n        hints: [\n            {\n                selector: `.odoo-editor-editable .o_text_columns div[class^='col-'],\n                            .odoo-editor-editable .o_text_columns div[class^='col-']>${baseContainerGlobalSelector}:first-child`,\n                text: _t(\"Empty column\"),\n            },\n        ],\n        unremovable_node_predicates: isUnremovableColumn,\n        power_buttons_visibility_predicates: ({ anchorNode }) =>\n            !closestElement(anchorNode, \".o_text_columns\"),\n        move_node_whitelist_selectors: \".o_text_columns\",\n        move_node_blacklist_selectors: \".o_text_columns *\",\n        hint_targets_providers: (selectionData) => {\n            if (!selectionData.documentSelection) {\n                return [];\n            }\n            const anchorNode = selectionData.documentSelection.anchorNode;\n            const columnContainer = closestElement(anchorNode, \"div.o_text_columns\");\n            if (!columnContainer) {\n                return [];\n            }\n            const closestColumn = closestElement(anchorNode, \"div[class^='col-']\");\n            const closestBlockEl = closestBlock(anchorNode);\n            return [...columnContainer.querySelectorAll(\"div[class^='col-']\")]\n                .map((column) => {\n                    const block = closestBlock(firstLeaf(column));\n                    return column === closestColumn && block !== closestBlockEl ? null : block;\n                })\n                .filter(Boolean);\n        },\n    };\n\n    columnize(numberOfColumns) {\n        const selectionToRestore = this.dependencies.selection.getEditableSelection();\n        const anchor = selectionToRestore.anchorNode;\n        const hasColumns = !!closestElement(anchor, \".o_text_columns\");\n        if (hasColumns) {\n            if (numberOfColumns) {\n                this.changeColumnsNumber(anchor, numberOfColumns);\n            } else {\n                this.removeColumns(anchor);\n            }\n        } else if (numberOfColumns) {\n            this.createColumns(anchor, numberOfColumns);\n        }\n        this.dependencies.selection.setSelection(selectionToRestore);\n        this.dependencies.history.addStep();\n    }\n\n    removeColumns(anchor) {\n        const container = closestElement(anchor, \".o_text_columns\");\n        const rows = unwrapContents(container);\n        for (const row of rows) {\n            const columns = unwrapContents(row);\n            for (const column of columns) {\n                unwrapContents(column);\n                // const columnContents = unwrapContents(column);\n                // for (const node of columnContents) {\n                //     resetOuids(node);\n                // }\n            }\n        }\n    }\n\n    createColumns(anchor, numberOfColumns) {\n        const container = this.document.createElement(\"div\");\n        if (!closestElement(anchor, \".container\")) {\n            container.classList.add(\"container\");\n        }\n        container.classList.add(\"o_text_columns\", \"o-contenteditable-false\");\n        const row = this.document.createElement(\"div\");\n        row.classList.add(\"row\");\n        container.append(row);\n        const block = closestBlock(anchor);\n        // resetOuids(block);\n        const columnSize = Math.floor(12 / numberOfColumns);\n        const columns = [];\n        for (let i = 0; i < numberOfColumns; i++) {\n            const column = this.document.createElement(\"div\");\n            column.classList.add(`col-${columnSize}`, \"o-contenteditable-true\");\n            row.append(column);\n            columns.push(column);\n        }\n        columns.shift().append(block);\n        for (const column of columns) {\n            const baseContainer = this.dependencies.baseContainer.createBaseContainer();\n            baseContainer.append(this.document.createElement(\"br\"));\n            column.append(baseContainer);\n        }\n        this.dependencies.dom.insert(container);\n    }\n\n    changeColumnsNumber(anchor, numberOfColumns) {\n        const row = closestElement(anchor, \".row\");\n        const columns = [...row.children];\n        const columnSize = Math.floor(12 / numberOfColumns);\n        const diff = numberOfColumns - columns.length;\n        if (!diff) {\n            return;\n        }\n        for (const column of columns) {\n            column.className = column.className.replace(\n                REGEX_BOOTSTRAP_COLUMN,\n                `col$1-${columnSize}`\n            );\n        }\n        if (diff > 0) {\n            // Add extra columns.\n            let lastColumn = columns[columns.length - 1];\n            for (let i = 0; i < diff; i++) {\n                const column = this.document.createElement(\"div\");\n                column.classList.add(`col-${columnSize}`, \"o-contenteditable-true\");\n                const baseContainer = this.dependencies.baseContainer.createBaseContainer();\n                baseContainer.append(this.document.createElement(\"br\"));\n                column.append(baseContainer);\n                lastColumn.after(column);\n                lastColumn = column;\n            }\n        } else if (diff < 0) {\n            // Remove superfluous columns.\n            const contents = [];\n            for (let i = diff; i < 0; i++) {\n                const column = columns.pop();\n                const columnContents = unwrapContents(column);\n                // for (const node of columnContents) {\n                //     resetOuids(node);\n                // }\n                contents.unshift(...columnContents);\n            }\n            columns[columns.length - 1].append(...contents);\n        }\n    }\n}\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { EmojiPicker } from \"@web/core/emoji_picker/emoji_picker\";\nimport { _t } from \"@web/core/l10n/translation\";\n\n/**\n * @typedef { Object } EmojiShared\n * @property { EmojiPlugin['showEmojiPicker'] } showEmojiPicker\n */\n\nexport class EmojiPlugin extends Plugin {\n    static id = \"emoji\";\n    static dependencies = [\"history\", \"overlay\", \"dom\", \"selection\"];\n    static shared = [\"showEmojiPicker\"];\n    /** @type {import(\"plugins\").EditorResources} */\n    resources = {\n        user_commands: [\n            {\n                id: \"addEmoji\",\n                title: _t(\"Emoji\"),\n                description: _t(\"Add an emoji\"),\n                icon: \"fa-smile-o\",\n                run: this.showEmojiPicker.bind(this),\n            },\n        ],\n        powerbox_items: [\n            {\n                categoryId: \"widget\",\n                commandId: \"addEmoji\",\n            },\n        ],\n    };\n\n    setup() {\n        this.overlay = this.dependencies.overlay.createOverlay(EmojiPicker, {\n            hasAutofocus: true,\n            className: \"popover\",\n        });\n    }\n\n    /**\n     * @param {Object} options\n     * @param {HTMLElement} options.target - The target element to position the overlay.\n     * @param {Function} [options.onSelect] - The callback function to handle the selection of an emoji.\n     * If not provided, the emoji will be inserted into the editor and a step will be trigerred.\n     */\n    showEmojiPicker({ target, onSelect } = {}) {\n        this.overlay.open({\n            props: {\n                close: () => {\n                    this.overlay.close();\n                    this.dependencies.selection.focusEditable();\n                },\n                onSelect: (str) => {\n                    if (onSelect) {\n                        onSelect(str);\n                        return;\n                    }\n                    this.dependencies.dom.insert(str);\n                    this.dependencies.history.addStep();\n                },\n            },\n            target,\n        });\n    }\n}\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { cleanTextNode } from \"@html_editor/utils/dom\";\nimport { isTextNode, isZwnbsp } from \"@html_editor/utils/dom_info\";\nimport { prepareUpdate } from \"@html_editor/utils/dom_state\";\nimport { descendants, selectElements } from \"@html_editor/utils/dom_traversal\";\nimport { leftPos, rightPos } from \"@html_editor/utils/position\";\nimport { callbacksForCursorUpdate } from \"@html_editor/utils/selection\";\n\n/** @typedef {import(\"../core/selection_plugin\").Cursors} Cursors */\n\n/**\n * @typedef { Object } FeffShared\n * @property { FeffPlugin['addFeff'] } addFeff\n * @property { FeffPlugin['removeFeffs'] } removeFeffs\n */\n\n/**\n * @typedef {((node: Node) => boolean)[]} legit_feff_predicates\n * @typedef {((root: EditorContext[\"editable\"], cursors: Cursors) => Node[])[]} feff_providers\n * @typedef {(() => string)[]} selectors_for_feff_providers\n */\n\n/**\n * This plugin manages the insertion and removal of the zero-width no-break\n * space character (U+FEFF). These characters enable the user to place the\n * cursor in positions that would otherwise not be easy or possible, such as\n * between two contenteditable=false elements, or at the end (but inside) of a\n * link.\n */\nexport class FeffPlugin extends Plugin {\n    static id = \"feff\";\n    static dependencies = [\"selection\"];\n    static shared = [\"addFeff\", \"removeFeffs\", \"surroundWithFeffs\"];\n\n    /** @type {import(\"plugins\").EditorResources} */\n    resources = {\n        normalize_handlers: this.updateFeffs.bind(this),\n        clean_for_save_handlers: this.cleanForSave.bind(this),\n        intangible_char_for_keyboard_navigation_predicates: (ev, char, lastSkipped) =>\n            // Skip first FEFF, but not the second one (unless shift is pressed).\n            char === \"\\uFEFF\" && (ev.shiftKey || lastSkipped !== \"\\uFEFF\"),\n        clipboard_content_processors: this.processContentForClipboard.bind(this),\n        clipboard_text_processors: (text) => text.replace(/\\ufeff/g, \"\"),\n    };\n\n    cleanForSave({ root, preserveSelection = false }) {\n        if (preserveSelection) {\n            const cursors = this.getCursors();\n            this.removeFeffs(root, cursors);\n            cursors.restore();\n        } else {\n            this.removeFeffs(root, null);\n        }\n    }\n\n    /**\n     * @param {Element} root\n     * @param {Cursors} [cursors]\n     * @param {Object} [options]\n     */\n    removeFeffs(root, cursors, { exclude = () => false } = {}) {\n        const hasFeff = (node) => isTextNode(node) && node.textContent.includes(\"\\ufeff\");\n        const isEditable = (node) => node.parentElement.isContentEditable;\n        const composedFilter = (node) => hasFeff(node) && isEditable(node) && !exclude(node);\n\n        for (const node of descendants(root).filter(composedFilter)) {\n            // Remove all FEFF within a `prepareUpdate` to make sure to make <br>\n            // nodes visible if needed.\n            const restoreSpaces = prepareUpdate(...leftPos(node), ...rightPos(node));\n            cleanTextNode(node, \"\\ufeff\", cursors);\n            restoreSpaces();\n        }\n    }\n\n    /**\n     * @param {Element} element\n     * @param {'before'|'after'|'prepend'|'append'} position\n     * @param {Cursors} [cursors]\n     * @returns {Node}\n     */\n    addFeff(element, position, cursors) {\n        const feff = this.document.createTextNode(\"\\ufeff\");\n        cursors?.update(callbacksForCursorUpdate[position](element, feff));\n        element[position](feff);\n        return feff;\n    }\n\n    surroundWithFeffs(node, cursors) {\n        const addFeff = (position) => {\n            // skip cursor update for append, we want to keep it before\n            // the added FEFF\n            const c = position === \"append\" ? null : cursors;\n            return this.addFeff(node, position, c);\n        };\n\n        const zwnbspNodes = [];\n        for (const [position, relation] of [\n            [\"before\", \"previousSibling\"],\n            [\"after\", \"nextSibling\"],\n            [\"prepend\", \"firstChild\"],\n            [\"append\", \"lastChild\"],\n        ]) {\n            const candidate = node[relation];\n            const feff =\n                isZwnbsp(candidate) && !zwnbspNodes.includes(candidate)\n                    ? candidate\n                    : addFeff(position);\n            zwnbspNodes.push(feff);\n        }\n        return zwnbspNodes;\n    }\n\n    /**\n     * Adds a FEFF before and after each element that matches the selectors\n     * provided by the registered providers.\n     *\n     * @param {Element} root\n     * @param {Cursors} cursors\n     * @returns {Node[]}\n     */\n    padWithFeffs(root, cursors) {\n        const combinedSelector = this.getResource(\"selectors_for_feff_providers\")\n            .map((provider) => provider())\n            .join(\", \");\n        if (!combinedSelector) {\n            return [];\n        }\n        const elements = [...selectElements(root, combinedSelector)];\n        const isEditable = (node) => node.parentElement?.isContentEditable;\n        const feffNodes = elements\n            .filter(isEditable)\n            .flatMap((el) => {\n                const addFeff = (position) => this.addFeff(el, position, cursors);\n                return [\n                    isZwnbsp(el.previousSibling) ? el.previousSibling : addFeff(\"before\"),\n                    isZwnbsp(el.nextSibling) ? el.nextSibling : addFeff(\"after\"),\n                ];\n            })\n            // Avoid sequential FEFFs\n            .filter((feff, i, array) => !(i > 0 && areCloseSiblings(array[i - 1], feff)));\n        return feffNodes;\n    }\n\n    updateFeffs(root) {\n        const cursors = this.getCursors();\n        // Pad based on selectors\n        const feffNodesBasedOnSelectors = this.padWithFeffs(root, cursors);\n        // Custom feff adding\n        // Each provider is responsible for adding (or keeping) FEFF nodes and\n        // returning a list of them.\n        const customFeffNodes = this.getResource(\"feff_providers\").flatMap((p) => p(root, cursors));\n        const feffNodesToKeep = new Set([...feffNodesBasedOnSelectors, ...customFeffNodes]);\n        this.removeFeffs(root, cursors, {\n            exclude: (node) =>\n                feffNodesToKeep.has(node) ||\n                this.getResource(\"legit_feff_predicates\").some((predicate) => predicate(node)),\n        });\n        cursors.restore();\n    }\n\n    /**\n     * Retuns a patched version of cursors in which `restore` does nothing\n     * unless `update` has been called at least once.\n     */\n    getCursors() {\n        const cursors = this.dependencies.selection.preserveSelection();\n        const originalUpdate = cursors.update.bind(cursors);\n        const originalRestore = cursors.restore.bind(cursors);\n        let shouldRestore = false;\n        cursors.update = (...args) => {\n            shouldRestore = true;\n            return originalUpdate(...args);\n        };\n        cursors.restore = () => {\n            if (shouldRestore) {\n                originalRestore();\n            }\n        };\n        return cursors;\n    }\n\n    processContentForClipboard(clonedContent) {\n        descendants(clonedContent)\n            .filter(isTextNode)\n            .filter((node) => node.textContent.includes(\"\\ufeff\"))\n            .forEach((node) => (node.textContent = node.textContent.replace(/\\ufeff/g, \"\")));\n        return clonedContent;\n    }\n}\n\n/**\n * Whether two nodes are consecutive siblings, ignoring empty text nodes between\n * them.\n *\n * @param {Node} a\n * @param {Node} b\n */\nfunction areCloseSiblings(a, b) {\n    let next = a.nextSibling;\n    // skip empty text nodes\n    while (next && isTextNode(next) && !next.textContent) {\n        next = next.nextSibling;\n    }\n    return next === b;\n}\n", "import { Component, useState } from \"@odoo/owl\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { applyOpacityToGradient, isColorGradient } from \"@web/core/utils/colors\";\nimport { GradientPicker } from \"./gradient_picker/gradient_picker\";\n\nconst DEFAULT_GRADIENT_COLORS = [\n    \"linear-gradient(135deg, rgb(255, 204, 51) 0%, rgb(226, 51, 255) 100%)\",\n    \"linear-gradient(135deg, rgb(102, 153, 255) 0%, rgb(255, 51, 102) 100%)\",\n    \"linear-gradient(135deg, rgb(47, 128, 237) 0%, rgb(178, 255, 218) 100%)\",\n    \"linear-gradient(135deg, rgb(203, 94, 238) 0%, rgb(75, 225, 236) 100%)\",\n    \"linear-gradient(135deg, rgb(214, 255, 127) 0%, rgb(0, 179, 204) 100%)\",\n    \"linear-gradient(135deg, rgb(255, 222, 69) 0%, rgb(69, 33, 0) 100%)\",\n    \"linear-gradient(135deg, rgb(222, 222, 222) 0%, rgb(69, 69, 69) 100%)\",\n    \"linear-gradient(135deg, rgb(255, 222, 202) 0%, rgb(202, 115, 69) 100%)\",\n];\n\nexport class ColorPickerGradientTab extends Component {\n    static template = \"html_editor.ColorPickerGradientTab\";\n    static components = { GradientPicker };\n    static props = {\n        applyColor: Function,\n        onColorClick: Function,\n        onColorPreview: Function,\n        onColorPointerOver: Function,\n        onColorPointerOut: Function,\n        onFocusin: Function,\n        onFocusout: Function,\n        setOnCloseCallback: { type: Function, optional: true },\n        setOperationCallbacks: { type: Function, optional: true },\n        defaultOpacity: { type: Number, optional: true },\n        noTransparency: { type: Boolean, optional: true },\n        selectedColor: { type: String, optional: true },\n        \"*\": { optional: true },\n    };\n    setup() {\n        this.state = useState({\n            showGradientPicker: false,\n        });\n        this.applyOpacityToGradient = applyOpacityToGradient;\n        this.DEFAULT_GRADIENT_COLORS = DEFAULT_GRADIENT_COLORS;\n    }\n\n    getCurrentGradientColor() {\n        if (isColorGradient(this.props.selectedColor)) {\n            return this.props.selectedColor;\n        }\n    }\n\n    toggleGradientPicker() {\n        this.state.showGradientPicker = !this.state.showGradientPicker;\n    }\n}\n\nregistry.category(\"color_picker_tabs\").add(\n    \"html_editor.gradient\",\n    {\n        id: \"gradient\",\n        name: _t(\"Gradient\"),\n        component: ColorPickerGradientTab,\n    },\n    { sequence: 60 }\n);\n", "import { Plugin } from \"@html_editor/plugin\";\nimport {\n    BG_CLASSES_REGEX,\n    COLOR_COMBINATION_CLASSES_REGEX,\n    hasAnyNodesColor,\n    hasColor,\n    TEXT_CLASSES_REGEX,\n    hasTextColorClass,\n} from \"@html_editor/utils/color\";\nimport { fillEmpty, unwrapContents } from \"@html_editor/utils/dom\";\nimport {\n    isEmptyBlock,\n    isRedundantElement,\n    isTextNode,\n    isWhitespace,\n    isZwnbsp,\n} from \"@html_editor/utils/dom_info\";\nimport { closestElement, descendants, selectElements } from \"@html_editor/utils/dom_traversal\";\nimport { isColorGradient, rgbaToHex } from \"@web/core/utils/colors\";\nimport { backgroundImageCssToParts, backgroundImagePartsToCss } from \"@html_editor/utils/image\";\nimport { isHtmlContentSupported } from \"@html_editor/core/selection_plugin\";\nimport { isBlock } from \"@html_editor/utils/blocks\";\n\nconst COLOR_COMBINATION_CLASSES = [1, 2, 3, 4, 5].map((i) => `o_cc${i}`);\nconst COLOR_COMBINATION_SELECTOR = COLOR_COMBINATION_CLASSES.map((c) => `.${c}`).join(\", \");\n\n/**\n * @typedef { Object } ColorShared\n * @property { ColorPlugin['colorElement'] } colorElement\n * @property { ColorPlugin['removeAllColor'] } removeAllColor\n * @property { ColorPlugin['getElementColors'] } getElementColors\n * @property { ColorPlugin['applyColor'] } applyColor\n */\n\n/**\n * @typedef {((element: HTMLElement, cssProp: string, color: string) => boolean)[]} apply_color_style_overrides\n * @typedef {((color: string, mode: \"color\" | \"backgroundColor\") => void)[]} color_apply_overrides\n * @typedef {((color: string, mode: \"color\" | \"backgroundColor\") => string)[]} apply_background_color_processors\n * @typedef {((color: string) => string)[]} get_background_color_processors\n *\n * @typedef {((el: HTMLElement, actionParam: string) => string)[]} color_combination_getters\n */\n\nexport class ColorPlugin extends Plugin {\n    static id = \"color\";\n    static dependencies = [\"selection\", \"split\", \"history\", \"format\"];\n    static shared = [\n        \"colorElement\",\n        \"removeAllColor\",\n        \"getElementColors\",\n        \"getColorCombination\",\n        \"applyColor\",\n    ];\n    /** @type {import(\"plugins\").EditorResources} */\n    resources = {\n        user_commands: [\n            {\n                id: \"applyColor\",\n                run: ({ color, mode }) => {\n                    this.applyColor(color, mode);\n                    this.dependencies.history.addStep();\n                },\n                isAvailable: isHtmlContentSupported,\n            },\n        ],\n        /** Handlers */\n        remove_all_formats_handlers: this.removeAllColor.bind(this),\n        color_combination_getters: getColorCombinationFromClass,\n\n        /** Predicates */\n        has_format_predicates: [\n            (node) => hasColor(closestElement(node), \"color\"),\n            (node) => hasColor(closestElement(node), \"backgroundColor\"),\n        ],\n        format_class_predicates: (className) =>\n            TEXT_CLASSES_REGEX.test(className) || BG_CLASSES_REGEX.test(className),\n        normalize_handlers: this.normalize.bind(this),\n    };\n\n    normalize(root) {\n        for (const el of selectElements(root, \"font\")) {\n            if (isRedundantElement(el)) {\n                unwrapContents(el);\n            }\n        }\n    }\n\n    getElementColors(el) {\n        const elStyle = getComputedStyle(el);\n        const backgroundImage = elStyle.backgroundImage;\n        const gradient = backgroundImageCssToParts(backgroundImage).gradient;\n        const hasGradient = isColorGradient(gradient);\n        const hasTextGradientClass = el.classList.contains(\"text-gradient\");\n\n        let backgroundColor = elStyle.backgroundColor;\n        for (const processor of this.getResource(\"get_background_color_processors\")) {\n            backgroundColor = processor(backgroundColor);\n        }\n\n        return {\n            color: hasGradient && hasTextGradientClass ? gradient : rgbaToHex(elStyle.color),\n            backgroundColor:\n                hasGradient && !hasTextGradientClass ? gradient : rgbaToHex(backgroundColor),\n        };\n    }\n\n    removeAllColor() {\n        const colorModes = [\"color\", \"backgroundColor\"];\n        let someColorWasRemoved = true;\n        while (someColorWasRemoved) {\n            someColorWasRemoved = false;\n            for (const mode of colorModes) {\n                let max = 40;\n                const hasAnySelectedNodeColor = (mode) => {\n                    const nodes = this.dependencies.selection\n                        .getTargetedNodes()\n                        .filter(\n                            (n) =>\n                                isTextNode(n) ||\n                                (mode === \"backgroundColor\" &&\n                                    n.classList.contains(\"o_selected_td\"))\n                        );\n                    return hasAnyNodesColor(nodes, mode);\n                };\n                while (hasAnySelectedNodeColor(mode) && max > 0) {\n                    this.applyColor(\"\", mode);\n                    someColorWasRemoved = true;\n                    max--;\n                }\n                if (max === 0) {\n                    someColorWasRemoved = false;\n                    throw new Error(\"Infinite Loop in removeAllColor().\");\n                }\n            }\n        }\n    }\n\n    /**\n     * Apply a css or class color on the current selection (wrapped in <font>).\n     *\n     * @param {string} color hexadecimal or bg-name/text-name class\n     * @param {string} mode 'color' or 'backgroundColor'\n     * @param {boolean} [previewMode=false] true - apply color in preview mode\n     */\n    applyColor(color, mode, previewMode = false) {\n        this.dependencies.selection.selectAroundNonEditable();\n        if (mode === \"backgroundColor\") {\n            for (const processor of this.getResource(\"apply_background_color_processors\")) {\n                color = processor(color, mode);\n            }\n        }\n        if (this.delegateTo(\"color_apply_overrides\", color, mode, previewMode)) {\n            return;\n        }\n        let selection = this.dependencies.selection.getEditableSelection();\n        let targetedNodes;\n        // Get the <font> nodes to color\n        if (selection.isCollapsed) {\n            let zws;\n            if (\n                selection.anchorNode.nodeType !== Node.TEXT_NODE &&\n                selection.anchorNode.textContent !== \"\\u200b\"\n            ) {\n                zws = selection.anchorNode;\n            } else {\n                zws = this.dependencies.format.insertAndSelectZws();\n            }\n            selection = this.dependencies.selection.setSelection(\n                {\n                    anchorNode: zws,\n                    anchorOffset: 0,\n                },\n                { normalize: false }\n            );\n            targetedNodes = [zws];\n        } else {\n            selection = this.dependencies.split.splitSelection();\n            targetedNodes = this.dependencies.selection\n                .getTargetedNodes()\n                .filter(\n                    (node) =>\n                        this.dependencies.selection.isNodeEditable(node) && node.nodeName !== \"T\"\n                );\n            if (isEmptyBlock(selection.endContainer)) {\n                targetedNodes.push(selection.endContainer, ...descendants(selection.endContainer));\n            }\n        }\n\n        const findTopMostDecoration = (current) => {\n            const decoration = closestElement(current.parentNode, \"s, u\");\n            return decoration?.textContent === current.textContent\n                ? findTopMostDecoration(decoration)\n                : current;\n        };\n\n        const hexColor = rgbaToHex(color).toLowerCase();\n        const selectedNodes = targetedNodes\n            .filter((node) => {\n                if (mode === \"backgroundColor\" && color) {\n                    return !closestElement(node, \"table.o_selected_table\");\n                }\n                if (closestElement(node).classList.contains(\"o_default_color\")) {\n                    return false;\n                }\n                const li = closestElement(node, \"li\");\n                if (li && color && this.dependencies.selection.areNodeContentsFullySelected(li)) {\n                    return rgbaToHex(li.style.color).toLowerCase() !== hexColor;\n                }\n                return true;\n            })\n            .map((node) => findTopMostDecoration(node));\n\n        const targetedFieldNodes = new Set(\n            this.dependencies.selection\n                .getTargetedNodes()\n                .map((n) => closestElement(n, \"*[t-field],*[t-out],*[t-esc]\"))\n                .filter(Boolean)\n        );\n\n        const getFonts = (selectedNodes) =>\n            selectedNodes.flatMap((node) => {\n                // Invisible nodes like `feff`s can be removed during `splitAroundUntil`\n                // so we filter them out.\n                if (!node.isConnected) {\n                    return [];\n                }\n                let font =\n                    closestElement(node, \"font\") ||\n                    closestElement(\n                        node,\n                        '[style*=\"color\"]:not(li), [style*=\"background-color\"]:not(li), [style*=\"background-image\"]:not(li)'\n                    ) ||\n                    closestElement(node, \"span\") ||\n                    closestElement(node, (node) => hasTextColorClass(node, mode));\n\n                const faNodes = font?.querySelectorAll(\".fa\");\n                if (faNodes && Array.from(faNodes).some((faNode) => faNode.contains(node))) {\n                    return font;\n                }\n                const children = font && descendants(font);\n                const hasInlineGradient = font && isColorGradient(font.style[\"background-image\"]);\n                const isFullySelected =\n                    children && children.every((child) => selectedNodes.includes(child));\n                const isTextGradient =\n                    hasInlineGradient && font.classList.contains(\"text-gradient\");\n                const shouldReplaceExistingGradient =\n                    isFullySelected &&\n                    ((mode === \"color\" && isTextGradient) ||\n                        (mode === \"backgroundColor\" && !isTextGradient));\n                if (\n                    font &&\n                    font.nodeName !== \"T\" &&\n                    (font.nodeName !== \"SPAN\" ||\n                        font.style[mode] ||\n                        font.style.backgroundImage ||\n                        hasTextColorClass(font, mode)) &&\n                    (isColorGradient(color) ||\n                        color === \"\" ||\n                        !hasInlineGradient ||\n                        shouldReplaceExistingGradient) &&\n                    !this.dependencies.split.isUnsplittable(font)\n                ) {\n                    // Partially selected <font>: split it.\n                    const selectedChildren = children.filter((child) =>\n                        selectedNodes.includes(child)\n                    );\n                    if (selectedChildren.length) {\n                        if (isBlock(font)) {\n                            const colorStyles = [\"color\", \"background-color\", \"background-image\"];\n                            const newFont = this.document.createElement(\"font\");\n                            for (const style of colorStyles) {\n                                const styleValue = font.style[style];\n                                if (styleValue) {\n                                    this.colorElement(newFont, styleValue, style);\n                                    font.style.removeProperty(style);\n                                }\n                            }\n                            font.classList.forEach((className) => {\n                                if (TEXT_CLASSES_REGEX.test(className)) {\n                                    font.classList.remove(className);\n                                    newFont.classList.add(className);\n                                }\n                            });\n                            newFont.append(...font.childNodes);\n                            font.append(newFont);\n                            font = newFont;\n                        }\n                        const closestGradientEl = closestElement(\n                            node,\n                            'font[style*=\"background-image\"], span[style*=\"background-image\"]'\n                        );\n                        const isGradientBeingUpdated = closestGradientEl && isColorGradient(color);\n                        const splitnode = isGradientBeingUpdated ? closestGradientEl : font;\n                        const cursors = this.dependencies.selection.preserveSelection();\n                        font = this.dependencies.split.splitAroundUntil(\n                            selectedChildren,\n                            splitnode,\n                            cursors\n                        );\n                        cursors.restore();\n                        if (isGradientBeingUpdated) {\n                            const classRegex =\n                                mode === \"color\" ? TEXT_CLASSES_REGEX : BG_CLASSES_REGEX;\n                            // When updating a gradient, remove color applied to\n                            // its descendants.This ensures the gradient remains\n                            // visible without being overwritten by a descendant's color.\n                            for (const node of descendants(font)) {\n                                if (\n                                    node.nodeType === Node.ELEMENT_NODE &&\n                                    (node.style[mode] || classRegex.test(node.className))\n                                ) {\n                                    this.colorElement(node, \"\", mode);\n                                    node.style.webkitTextFillColor = \"\";\n                                    if (!node.getAttribute(\"style\")) {\n                                        unwrapContents(node);\n                                    }\n                                }\n                            }\n                        } else if (\n                            mode === \"color\" &&\n                            (font.style.webkitTextFillColor ||\n                                (closestGradientEl &&\n                                    closestGradientEl.classList.contains(\"text-gradient\") &&\n                                    !shouldReplaceExistingGradient))\n                        ) {\n                            font.style.webkitTextFillColor = color;\n                        }\n                    } else {\n                        font = [];\n                    }\n                } else if (\n                    (node.nodeType === Node.TEXT_NODE && !isZwnbsp(node)) ||\n                    (node.nodeName === \"BR\" && isEmptyBlock(node.parentNode)) ||\n                    (node.nodeType === Node.ELEMENT_NODE &&\n                        [\"inline\", \"inline-block\"].includes(getComputedStyle(node).display) &&\n                        !isWhitespace(node.textContent) &&\n                        !node.classList.contains(\"btn\") &&\n                        !node.querySelector(\"font\") &&\n                        node.nodeName !== \"A\" &&\n                        !(node.nodeName === \"SPAN\" && node.style[\"fontSize\"]))\n                ) {\n                    // Node is a visible text or inline node without font nor a button:\n                    // wrap it in a <font>.\n                    const previous = node.previousSibling;\n                    const classRegex = mode === \"color\" ? BG_CLASSES_REGEX : TEXT_CLASSES_REGEX;\n                    if (\n                        previous &&\n                        previous.nodeName === \"FONT\" &&\n                        !previous.style[mode === \"color\" ? \"backgroundColor\" : \"color\"] &&\n                        !classRegex.test(previous.className) &&\n                        selectedNodes.includes(previous.firstChild) &&\n                        selectedNodes.includes(previous.lastChild)\n                    ) {\n                        // Directly follows a fully selected <font> that isn't\n                        // colored in the other mode: append to that.\n                        font = previous;\n                    } else {\n                        // No <font> found: insert a new one.\n                        font = this.document.createElement(\"font\");\n                        node.after(font);\n                        if (isTextGradient && mode === \"color\") {\n                            font.style.webkitTextFillColor = color;\n                        }\n                    }\n                    if (node.textContent) {\n                        font.appendChild(node);\n                    } else {\n                        fillEmpty(font);\n                    }\n                } else {\n                    font = []; // Ignore non-text or invisible text nodes.\n                }\n                return font;\n            });\n\n        for (const fieldNode of targetedFieldNodes) {\n            this.colorElement(fieldNode, color, mode);\n        }\n\n        let fonts = getFonts(selectedNodes);\n        // Dirty fix as the previous call could have unconnected elements\n        // because of the `splitAroundUntil`. Another call should provide he\n        // correct list of fonts.\n        if (!fonts.every((font) => font.isConnected)) {\n            fonts = getFonts(selectedNodes);\n        }\n\n        // Color the selected <font>s and remove uncolored fonts.\n        const fontsSet = new Set(fonts);\n        for (const font of fontsSet) {\n            this.colorElement(font, color, mode);\n            if (\n                !hasColor(font, \"color\") &&\n                !hasColor(font, \"backgroundColor\") &&\n                [\"FONT\", \"SPAN\"].includes(font.nodeName) &&\n                (!font.hasAttribute(\"style\") || !color)\n            ) {\n                for (const child of [...font.childNodes]) {\n                    font.parentNode.insertBefore(child, font);\n                }\n                font.parentNode.removeChild(font);\n                fontsSet.delete(font);\n            }\n        }\n        this.dependencies.selection.setSelection(selection, { normalize: false });\n    }\n\n    /**\n     * Applies a css or class color (fore- or background-) to an element.\n     * Replace the color that was already there if any.\n     *\n     * @param {Element} element\n     * @param {string} color hexadecimal or bg-name/text-name class\n     * @param {'color'|'backgroundColor'} mode 'color' or 'backgroundColor'\n     */\n    colorElement(element, color, mode) {\n        let parts = backgroundImageCssToParts(element.style[\"background-image\"]);\n        const oldClassName = element.getAttribute(\"class\") || \"\";\n\n        if (element.matches(COLOR_COMBINATION_SELECTOR)) {\n            removePresetGradient(element);\n        }\n\n        const hasGradientStyle = element.style.backgroundImage.includes(\"-gradient\");\n        if (mode === \"backgroundColor\") {\n            if (!color) {\n                element.classList.remove(\"o_cc\", ...COLOR_COMBINATION_CLASSES);\n            }\n            const hasGradient = getComputedStyle(element).backgroundImage.includes(\"-gradient\");\n            delete parts.gradient;\n            let newBackgroundImage = backgroundImagePartsToCss(parts);\n            // we override the bg image if the new bg image is empty, but the previous one is a gradient.\n            if (hasGradient && !newBackgroundImage) {\n                newBackgroundImage = \"none\";\n            }\n            element.style.backgroundImage = newBackgroundImage;\n            element.style[\"background-color\"] = \"\";\n        }\n\n        const newClassName = oldClassName\n            .replace(mode === \"color\" ? TEXT_CLASSES_REGEX : BG_CLASSES_REGEX, \"\")\n            .replace(/\\btext-gradient\\b/g, \"\") // cannot be combined with setting a background\n            .replace(/\\s+/, \" \");\n        if (oldClassName !== newClassName) {\n            element.setAttribute(\"class\", newClassName);\n        }\n        if (color.startsWith(\"text\") || color.startsWith(\"bg-\")) {\n            element.style[mode] = \"\";\n            element.classList.add(color);\n        } else if (isColorGradient(color)) {\n            element.style[mode] = \"\";\n            parts.gradient = color;\n            if (mode === \"color\") {\n                element.style[\"background-color\"] = \"\";\n                element.classList.add(\"text-gradient\");\n            }\n            this.applyColorStyle(element, \"background-image\", backgroundImagePartsToCss(parts));\n        } else {\n            delete parts.gradient;\n            if (hasGradientStyle && !backgroundImagePartsToCss(parts)) {\n                element.style[\"background-image\"] = \"\";\n            }\n            // Change camelCase to kebab-case.\n            mode = mode.replace(\"backgroundColor\", \"background-color\");\n            this.applyColorStyle(element, mode, color);\n        }\n\n        // It was decided that applying a color combination removes any \"color\"\n        // value (custom color, color classes, gradients, ...). Changing any\n        // \"color\", including color combinations, should still not remove the\n        // other background layers though (image, video, shape, ...).\n        if (color.startsWith(\"o_cc\")) {\n            parts = backgroundImageCssToParts(element.style[\"background-image\"]);\n            element.classList.remove(...COLOR_COMBINATION_CLASSES);\n            element.classList.add(\"o_cc\", color);\n\n            const hasBackgroundColor = !!getComputedStyle(element).backgroundColor;\n            const hasGradient = getComputedStyle(element).backgroundImage.includes(\"-gradient\");\n            const backgroundImage = element.style[\"background-image\"];\n            // Override gradient background image if coming from css rather than inline style.\n            if (hasBackgroundColor && hasGradient && !backgroundImage) {\n                element.style.backgroundImage = \"none\";\n            }\n        }\n\n        this.fixColorCombination(element, color);\n    }\n    /**\n     * There is a limitation with css. The defining a background image and a\n     * background gradient is done only by setting one style (background-image).\n     * If there is a class (in this case o_cc[1-5]) that defines a gradient, it\n     * will be overridden by the background-image property.\n     *\n     * This function will set the gradient of the o_cc in the background-image\n     * so that setting an image in the background-image property will not\n     * override the gradient.\n     */\n    fixColorCombination(element, color) {\n        const parts = backgroundImageCssToParts(element.style[\"background-image\"]);\n        const hasBackgroundColor =\n            element.style[\"background-color\"] ||\n            !!element.className.match(/\\bbg-/) ||\n            parts.gradient;\n\n        if (!hasBackgroundColor && (isColorGradient(color) || color.startsWith(\"o_cc\"))) {\n            element.style[\"background-image\"] = \"\";\n            parts.gradient = backgroundImageCssToParts(\n                // Compute the style from o_cc class.\n                getComputedStyle(element).backgroundImage\n            ).gradient;\n            element.style[\"background-image\"] = backgroundImagePartsToCss(parts);\n        }\n    }\n\n    getColorCombination(el, actionParam) {\n        for (const handler of this.getResource(\"color_combination_getters\")) {\n            const value = handler(el, actionParam);\n            if (value) {\n                return value;\n            }\n        }\n    }\n\n    /**\n     * @param {Element} element\n     * @param {string} cssProp\n     * @param {string} cssValue\n     */\n    applyColorStyle(element, mode, color) {\n        if (this.delegateTo(\"apply_color_style_overrides\", element, mode, color)) {\n            return;\n        }\n        element.style[mode] = color;\n    }\n}\n\nfunction getColorCombinationFromClass(el) {\n    return el.className.match?.(COLOR_COMBINATION_CLASSES_REGEX)?.[0];\n}\n\n/**\n * Remove the gradient of the element only if it is the inheritance from the o_cc selector.\n */\nfunction removePresetGradient(element) {\n    const oldBackgroundImage = element.style[\"background-image\"];\n    const parts = backgroundImageCssToParts(oldBackgroundImage);\n    const currentGradient = parts.gradient;\n    element.style.removeProperty(\"background-image\");\n    const styleWithoutGradient = getComputedStyle(element);\n    const presetGradient = backgroundImageCssToParts(styleWithoutGradient.backgroundImage).gradient;\n    if (presetGradient !== currentGradient) {\n        const withGradient = backgroundImagePartsToCss(parts);\n        element.style[\"background-image\"] = withGradient === \"none\" ? \"\" : withGradient;\n    } else {\n        delete parts.gradient;\n        const withoutGradient = backgroundImagePartsToCss(parts);\n        element.style[\"background-image\"] = withoutGradient === \"none\" ? \"\" : withoutGradient;\n    }\n}\n", "import { isColorGradient } from \"@web/core/utils/colors\";\nimport { Component, useState } from \"@odoo/owl\";\nimport {\n    useColorPicker,\n    DEFAULT_COLORS,\n    DEFAULT_THEME_COLOR_VARS,\n} from \"@web/core/color_picker/color_picker\";\nimport { effect } from \"@web/core/utils/reactive\";\nimport { toolbarButtonProps } from \"../toolbar/toolbar\";\nimport { getCSSVariableValue, getHtmlStyle } from \"@html_editor/utils/formatting\";\nimport { useChildRef } from \"@web/core/utils/hooks\";\nimport { useDropdownAutoVisibility } from \"@html_editor/dropdown_autovisibility_hook\";\n\nexport class ColorSelector extends Component {\n    static template = \"html_editor.ColorSelector\";\n    static props = {\n        ...toolbarButtonProps,\n        mode: { type: String },\n        type: { type: String },\n        getSelectedColors: Function,\n        applyColor: Function,\n        applyColorPreview: Function,\n        applyColorResetPreview: Function,\n        getUsedCustomColors: Function,\n        getTargetedElements: Function,\n        colorPrefix: { type: String },\n        enabledTabs: { type: Array, optional: true },\n        cssVarColorPrefix: { type: String, optional: true },\n        onClose: Function,\n    };\n    static defaultProps = {\n        cssVarColorPrefix: \"\",\n        enabledTabs: [\"solid\", \"gradient\", \"custom\"],\n    };\n\n    setup() {\n        this.state = useState({});\n        const htmlStyle = getHtmlStyle(document);\n        const defaultThemeColors = DEFAULT_THEME_COLOR_VARS.map((color) =>\n            getCSSVariableValue(color, htmlStyle)\n        );\n        this.solidColors = [\n            ...DEFAULT_COLORS.flat(),\n            ...defaultThemeColors,\n            getCSSVariableValue(\"body-color\", htmlStyle), // Default applied color\n            \"#00000000\", //Default Background color\n        ];\n        effect(\n            (selectedColors) => {\n                this.state.selectedColor = selectedColors[this.props.mode];\n                this.state.defaultTab = \"solid\";\n                this.state.selectedTab = this.getCorrespondingColorTab(\n                    selectedColors[this.props.mode]\n                );\n                this.state.getTargetedElements = this.props.getTargetedElements;\n                this.state.mode = this.props.mode;\n            },\n            [this.props.getSelectedColors()]\n        );\n\n        const colorPickerRef = useChildRef();\n        this.colorPicker = useColorPicker(\n            \"root\",\n            {\n                state: this.state,\n                applyColor: this.props.applyColor,\n                applyColorPreview: this.props.applyColorPreview,\n                applyColorResetPreview: this.props.applyColorResetPreview,\n                getUsedCustomColors: this.props.getUsedCustomColors,\n                colorPrefix: this.props.colorPrefix,\n                enabledTabs: this.props.enabledTabs,\n                cssVarColorPrefix: this.props.cssVarColorPrefix,\n            },\n            {\n                env: this.__owl__.childEnv,\n                onClose: () => {\n                    this.props.applyColorResetPreview();\n                    this.props.onClose();\n                },\n                ref: colorPickerRef,\n            }\n        );\n        useDropdownAutoVisibility(this.env.overlayState, colorPickerRef);\n    }\n\n    getCorrespondingColorTab(color) {\n        if (!color || this.solidColors.includes(color.toUpperCase())) {\n            return \"solid\";\n        } else if (isColorGradient(color)) {\n            return \"gradient\";\n        } else {\n            return \"custom\";\n        }\n    }\n\n    getSelectedColorStyle() {\n        if (isColorGradient(this.state.selectedColor)) {\n            return `border-bottom: 2px solid transparent; border-image: ${this.state.selectedColor}; border-image-slice: 1`;\n        }\n        return `border-bottom: 2px solid ${this.state.selectedColor}`;\n    }\n}\n", "import { isHtmlContentSupported } from \"@html_editor/core/selection_plugin\";\nimport { Plugin } from \"@html_editor/plugin\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { ColorSelector } from \"./color_selector\";\nimport { reactive } from \"@odoo/owl\";\nimport { isTextNode } from \"@html_editor/utils/dom_info\";\nimport { closestElement } from \"@html_editor/utils/dom_traversal\";\nimport { isCSSColor, RGBA_REGEX, rgbaToHex } from \"@web/core/utils/colors\";\n\nconst RGBA_OPACITY = 0.6;\nconst HEX_OPACITY = \"99\";\n\n/**\n * @typedef { Object } ColorUIShared\n * @property { ColorUIPlugin['getPropsForColorSelector'] } getPropsForColorSelector\n */\n\nexport class ColorUIPlugin extends Plugin {\n    static id = \"colorUi\";\n    static dependencies = [\"color\", \"history\", \"selection\"];\n    static shared = [\"getPropsForColorSelector\"];\n    /** @type {import(\"plugins\").EditorResources} */\n    resources = {\n        toolbar_items: [\n            {\n                id: \"forecolor\",\n                groupId: \"decoration\",\n                namespaces: [\"compact\", \"expanded\"],\n                description: _t(\"Apply Font Color\"),\n                Component: ColorSelector,\n                props: this.getPropsForColorSelector(\"foreground\"),\n                isAvailable: isHtmlContentSupported,\n            },\n            {\n                id: \"backcolor\",\n                groupId: \"decoration\",\n                description: _t(\"Apply Background Color\"),\n                Component: ColorSelector,\n                props: this.getPropsForColorSelector(\"background\"),\n                isAvailable: isHtmlContentSupported,\n            },\n        ],\n        selectionchange_handlers: this.updateSelectedColor.bind(this),\n        get_background_color_processors: this.getBackgroundColorProcessor.bind(this),\n        apply_background_color_processors: this.applyBackgroundColorProcessor.bind(this),\n    };\n\n    setup() {\n        this.selectedColors = reactive({ color: \"\", backgroundColor: \"\" });\n        this.previewableApplyColor = this.dependencies.history.makePreviewableOperation(\n            (color, mode, previewMode) =>\n                this.dependencies.color.applyColor(color, mode, previewMode)\n        );\n    }\n\n    /**\n     * @param {'foreground'|'background'} type\n     */\n    getPropsForColorSelector(type) {\n        const mode = type === \"foreground\" ? \"color\" : \"backgroundColor\";\n        return {\n            type,\n            mode,\n\n            getUsedCustomColors: () => this.getUsedCustomColors(mode),\n            getSelectedColors: () => this.selectedColors,\n            applyColor: (color) => this.applyColorCommit({ color, mode }),\n            applyColorPreview: (color) => this.applyColorPreview({ color, mode }),\n            applyColorResetPreview: this.applyColorResetPreview.bind(this),\n            colorPrefix: mode === \"color\" ? \"text-\" : \"bg-\",\n            onClose: () => this.dependencies.selection.focusEditable(),\n            getTargetedElements: () => {\n                const nodes = this.dependencies.selection.getTargetedNodes().filter(isTextNode);\n                return nodes.map((node) => closestElement(node));\n            },\n        };\n    }\n\n    /**\n     * Apply a css or class color on the current selection (wrapped in <font>).\n     *\n     * @param {Object} param\n     * @param {string} param.color hexadecimal or bg-name/text-name class\n     * @param {string} param.mode 'color' or 'backgroundColor'\n     */\n    applyColorCommit({ color, mode }) {\n        this.previewableApplyColor.commit(color, mode);\n        this.updateSelectedColor();\n    }\n    /**\n     * Apply a css or class color on the current selection (wrapped in <font>)\n     * in preview mode so that it can be reset.\n     *\n     * @param {Object} param\n     * @param {string} param.color hexadecimal or bg-name/text-name class\n     * @param {string} param.mode 'color' or 'backgroundColor'\n     */\n    applyColorPreview({ color, mode }) {\n        // Preview the color before applying it.\n        this.previewableApplyColor.preview(color, mode, true);\n        this.updateSelectedColor();\n    }\n    /**\n     * Reset the color applied in preview mode.\n     */\n    applyColorResetPreview() {\n        this.previewableApplyColor.revert();\n        this.updateSelectedColor();\n    }\n\n    getUsedCustomColors(mode) {\n        const allFont = this.editable.querySelectorAll(\"font\");\n        const usedCustomColors = new Set();\n        for (const font of allFont) {\n            if (isCSSColor(font.style[mode])) {\n                usedCustomColors.add(rgbaToHex(font.style[mode]));\n            }\n        }\n        return usedCustomColors;\n    }\n\n    updateSelectedColor() {\n        const nodes = this.dependencies.selection.getTargetedNodes().filter(isTextNode);\n        if (nodes.length === 0) {\n            return;\n        }\n        const el = closestElement(nodes[0]);\n        if (!el) {\n            return;\n        }\n\n        Object.assign(this.selectedColors, this.dependencies.color.getElementColors(el));\n    }\n\n    getBackgroundColorProcessor(backgroundColor) {\n        const activeTab = document\n            .querySelector(\".o_font_color_selector button.active\")\n            ?.innerHTML.trim();\n        if (backgroundColor.startsWith(\"rgba\") && (!activeTab || activeTab === \"Solid\")) {\n            // Buttons in the solid tab of color selector have no\n            // opacity, hence to match selected color correctly,\n            // we need to remove applied 0.6 opacity.\n            const values = backgroundColor.match(RGBA_REGEX) || [];\n            const alpha = parseFloat(values.pop()); // Extract alpha value\n            if (alpha === RGBA_OPACITY) {\n                backgroundColor = `rgb(${values.slice(0, 3).join(\", \")})`; // Remove alpha\n            }\n        }\n        return backgroundColor;\n    }\n\n    applyBackgroundColorProcessor(brackgroundColor) {\n        const activeTab = document\n            .querySelector(\".o_font_color_selector button.active\")\n            ?.innerHTML.trim();\n        if (activeTab === \"Solid\" && brackgroundColor.startsWith(\"#\")) {\n            // Apply default transparency to selected solid tab colors in background\n            // mode to make text highlighting more usable between light and dark modes.\n            brackgroundColor += HEX_OPACITY;\n        }\n        return brackgroundColor;\n    }\n}\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { FontFamilySelector } from \"@html_editor/main/font/font_family_selector\";\nimport { reactive } from \"@odoo/owl\";\nimport { closestElement } from \"../../utils/dom_traversal\";\nimport { withSequence } from \"@html_editor/utils/resource\";\nimport { isHtmlContentSupported } from \"@html_editor/core/selection_plugin\";\n\nexport const defaultFontFamily = {\n    name: \"Default system font\",\n    nameShort: \"Default font\",\n    fontFamily: false,\n};\nexport const fontFamilyItems = [\n    defaultFontFamily,\n    { name: \"Arial (sans-serif)\", nameShort: \"Arial\", fontFamily: \"Arial, sans-serif\" },\n    { name: \"Verdana (sans-serif)\", nameShort: \"Verdana\", fontFamily: \"Verdana, sans-serif\" },\n    { name: \"Tahoma (sans-serif)\", nameShort: \"Tahoma\", fontFamily: \"Tahoma, sans-serif\" },\n    {\n        name: \"Trebuchet MS (sans-serif)\",\n        nameShort: \"Trebuchet MS\",\n        fontFamily: '\"Trebuchet MS\", sans-serif',\n    },\n    {\n        name: \"Courier New (monospace)\",\n        nameShort: \"Courier New\",\n        fontFamily: '\"Courier New\", monospace',\n    },\n];\n\nexport class FontFamilyPlugin extends Plugin {\n    static id = \"fontFamily\";\n    static dependencies = [\"split\", \"selection\", \"dom\", \"format\", \"font\"];\n    fontFamily = reactive({ displayName: defaultFontFamily.nameShort });\n    /** @type {import(\"plugins\").EditorResources} */\n    resources = {\n        toolbar_items: [\n            withSequence(15, {\n                id: \"font-family\",\n                groupId: \"font\",\n                description: _t(\"Select font family\"),\n                Component: FontFamilySelector,\n                props: {\n                    fontFamilyItems: fontFamilyItems,\n                    currentFontFamily: this.fontFamily,\n                    onSelected: (item) => {\n                        this.dependencies.format.formatSelection(\"fontFamily\", {\n                            applyStyle: item.fontFamily !== false,\n                            formatProps: item,\n                        });\n                        this.fontFamily.displayName = item.nameShort;\n                    },\n                },\n                isAvailable: isHtmlContentSupported,\n            }),\n        ],\n        /** Handlers */\n        selectionchange_handlers: this.updateCurrentFontFamily.bind(this),\n        post_undo_handlers: this.updateCurrentFontFamily.bind(this),\n        post_redo_handlers: this.updateCurrentFontFamily.bind(this),\n    };\n\n    updateCurrentFontFamily(ev) {\n        const selelectionData = this.dependencies.selection.getSelectionData();\n        if (!selelectionData.documentSelectionIsInEditable) {\n            return;\n        }\n        const anchorElement = closestElement(selelectionData.editableSelection.anchorNode);\n        const anchorElementFontFamily = getComputedStyle(anchorElement).fontFamily;\n        const currentFontItem =\n            anchorElementFontFamily &&\n            fontFamilyItems.find((item) => item.fontFamily === anchorElementFontFamily);\n\n        this.fontFamily.displayName = (currentFontItem || defaultFontFamily).nameShort;\n    }\n}\n", "import { Component } from \"@odoo/owl\";\nimport { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\nimport { toolbarButtonProps } from \"@html_editor/main/toolbar/toolbar\";\nimport { useDropdownAutoVisibility } from \"@html_editor/dropdown_autovisibility_hook\";\nimport { useChildRef } from \"@web/core/utils/hooks\";\n\nexport class FontFamilySelector extends Component {\n    static template = \"html_editor.FontFamilySelector\";\n    static props = {\n        document: { optional: true },\n        fontFamilyItems: Object,\n        currentFontFamily: Object,\n        onSelected: Function,\n        ...toolbarButtonProps,\n    };\n    static components = { Dropdown, DropdownItem };\n\n    setup() {\n        this.menuRef = useChildRef();\n        useDropdownAutoVisibility(this.env.overlayState, this.menuRef);\n    }\n}\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { isBlock, closestBlock } from \"@html_editor/utils/blocks\";\nimport { unwrapContents } from \"@html_editor/utils/dom\";\nimport {\n    isParagraphRelatedElement,\n    isRedundantElement,\n    isEmptyBlock,\n    isVisibleTextNode,\n    isZWS,\n} from \"@html_editor/utils/dom_info\";\nimport {\n    ancestors,\n    childNodes,\n    closestElement,\n    createDOMPathGenerator,\n    descendants,\n    selectElements,\n} from \"@html_editor/utils/dom_traversal\";\nimport {\n    convertNumericToUnit,\n    getCSSVariableValue,\n    getHtmlStyle,\n    getFontSizeDisplayValue,\n    FONT_SIZE_CLASSES,\n} from \"@html_editor/utils/formatting\";\nimport { DIRECTIONS } from \"@html_editor/utils/position\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { FontSelector } from \"./font_selector\";\nimport {\n    getBaseContainerSelector,\n    SUPPORTED_BASE_CONTAINER_NAMES,\n} from \"@html_editor/utils/base_container\";\nimport { withSequence } from \"@html_editor/utils/resource\";\nimport { reactive } from \"@odoo/owl\";\nimport { FontSizeSelector } from \"./font_size_selector\";\nimport { isHtmlContentSupported } from \"@html_editor/core/selection_plugin\";\nimport { weakMemoize } from \"@html_editor/utils/functions\";\n\n/** @typedef {import(\"plugins\").TranslatedString} TranslatedString */\n\n/**\n * @typedef {((insertedNode: Node) => insertedNode)[]} before_insert_within_pre_processors\n * @typedef {{ name: TranslatedString; tagName: string; extraClass?: string; }[]} font_items\n */\n\nexport const fontSizeItems = [\n    { variableName: \"display-1-font-size\", className: \"display-1-fs\" },\n    { variableName: \"display-2-font-size\", className: \"display-2-fs\" },\n    { variableName: \"display-3-font-size\", className: \"display-3-fs\" },\n    { variableName: \"display-4-font-size\", className: \"display-4-fs\" },\n    { variableName: \"h1-font-size\", className: \"h1-fs\" },\n    { variableName: \"h2-font-size\", className: \"h2-fs\" },\n    { variableName: \"h3-font-size\", className: \"h3-fs\" },\n    { variableName: \"h4-font-size\", className: \"h4-fs\" },\n    { variableName: \"h5-font-size\", className: \"h5-fs\" },\n    { variableName: \"h6-font-size\", className: \"h6-fs\" },\n    { variableName: \"font-size-base\", className: \"base-fs\" },\n    { variableName: \"small-font-size\", className: \"o_small-fs\" },\n];\n\nconst rightLeafOnlyNotBlockPath = createDOMPathGenerator(DIRECTIONS.RIGHT, {\n    leafOnly: true,\n    stopTraverseFunction: isBlock,\n    stopFunction: isBlock,\n});\n\nconst headingTags = [\"H1\", \"H2\", \"H3\", \"H4\", \"H5\", \"H6\"];\nconst handledElemSelector = [...headingTags, \"PRE\", \"BLOCKQUOTE\"].join(\", \");\n\nexport class FontPlugin extends Plugin {\n    static id = \"font\";\n    static dependencies = [\n        \"baseContainer\",\n        \"input\",\n        \"split\",\n        \"selection\",\n        \"dom\",\n        \"format\",\n        \"lineBreak\",\n    ];\n    /** @type {import(\"plugins\").EditorResources} */\n    resources = {\n        font_items: [\n            withSequence(10, {\n                name: _t(\"Header 1 Display 1\"),\n                tagName: \"h1\",\n                extraClass: \"display-1\",\n            }),\n            ...[\n                { name: _t(\"Header 1\"), tagName: \"h1\" },\n                { name: _t(\"Header 2\"), tagName: \"h2\" },\n                { name: _t(\"Header 3\"), tagName: \"h3\" },\n                { name: _t(\"Header 4\"), tagName: \"h4\" },\n                { name: _t(\"Header 5\"), tagName: \"h5\" },\n                { name: _t(\"Header 6\"), tagName: \"h6\" },\n            ].map((item) => withSequence(20, item)),\n            withSequence(30, {\n                name: _t(\"Normal\"),\n                tagName: \"div\",\n                // for the FontSelector component\n                selector: getBaseContainerSelector(\"DIV\"),\n            }),\n            withSequence(40, { name: _t(\"Paragraph\"), tagName: \"p\" }),\n            withSequence(50, { name: _t(\"Code\"), tagName: \"pre\" }),\n            withSequence(60, { name: _t(\"Quote\"), tagName: \"blockquote\" }),\n        ],\n        user_commands: [\n            {\n                id: \"setTagHeading\",\n                run: ({ level } = {}) =>\n                    this.dependencies.dom.setBlock({ tagName: `H${level ?? 1}` }),\n                isAvailable: this.blockFormatIsAvailable.bind(this),\n            },\n            {\n                id: \"setTagHeading1\",\n                title: _t(\"Heading 1\"),\n                description: _t(\"Big section heading\"),\n                icon: \"fa-header\",\n                run: () => this.dependencies.dom.setBlock({ tagName: \"H1\" }),\n                isAvailable: this.blockFormatIsAvailable.bind(this),\n            },\n            {\n                id: \"setTagHeading2\",\n                title: _t(\"Heading 2\"),\n                description: _t(\"Medium section heading\"),\n                icon: \"fa-header\",\n                run: () => this.dependencies.dom.setBlock({ tagName: \"H2\" }),\n                isAvailable: this.blockFormatIsAvailable.bind(this),\n            },\n            {\n                id: \"setTagHeading3\",\n                title: _t(\"Heading 3\"),\n                description: _t(\"Small section heading\"),\n                icon: \"fa-header\",\n                run: () => this.dependencies.dom.setBlock({ tagName: \"H3\" }),\n                isAvailable: this.blockFormatIsAvailable.bind(this),\n            },\n            {\n                id: \"setTagParagraph\",\n                title: _t(\"Text\"),\n                description: _t(\"Paragraph block\"),\n                icon: \"fa-paragraph\",\n                run: () => {\n                    this.dependencies.dom.setBlock({\n                        tagName: this.dependencies.baseContainer.getDefaultNodeName(),\n                    });\n                },\n                isAvailable: this.blockFormatIsAvailable.bind(this),\n            },\n            {\n                id: \"setTagQuote\",\n                title: _t(\"Quote\"),\n                description: _t(\"Add a blockquote section\"),\n                icon: \"fa-quote-right\",\n                run: () => this.dependencies.dom.setBlock({ tagName: \"blockquote\" }),\n                isAvailable: this.blockFormatIsAvailable.bind(this),\n            },\n            {\n                id: \"setTagPre\",\n                title: _t(\"Code\"),\n                description: _t(\"Add a code section\"),\n                icon: \"fa-code\",\n                run: () => this.dependencies.dom.setBlock({ tagName: \"pre\" }),\n                isAvailable: this.blockFormatIsAvailable.bind(this),\n            },\n        ],\n        toolbar_groups: [\n            withSequence(10, {\n                id: \"font\",\n            }),\n        ],\n        toolbar_items: [\n            withSequence(10, {\n                id: \"font\",\n                groupId: \"font\",\n                namespaces: [\"compact\", \"expanded\"],\n                description: _t(\"Select font style\"),\n                Component: FontSelector,\n                props: {\n                    getItems: () => this.availableFontItems,\n                    getDisplay: () => this.font,\n                    onSelected: (item) => {\n                        this.dependencies.dom.setBlock({\n                            tagName: item.tagName,\n                            extraClass: item.extraClass,\n                        });\n                        this.updateFontSelectorParams();\n                    },\n                },\n                isAvailable: this.blockFormatIsAvailable.bind(this),\n            }),\n            withSequence(20, {\n                id: \"font-size\",\n                groupId: \"font\",\n                namespaces: [\"compact\", \"expanded\"],\n                description: _t(\"Select font size\"),\n                Component: FontSizeSelector,\n                props: {\n                    getItems: () => this.fontSizeItems,\n                    getDisplay: () => this.fontSize,\n                    onFontSizeInput: (size) => {\n                        this.dependencies.format.formatSelection(\"fontSize\", {\n                            formatProps: { size },\n                            applyStyle: true,\n                        });\n                        this.updateFontSizeSelectorParams();\n                    },\n                    onSelected: (item) => {\n                        this.dependencies.format.formatSelection(\"setFontSizeClassName\", {\n                            formatProps: { className: item.className },\n                            applyStyle: true,\n                        });\n                        this.updateFontSizeSelectorParams();\n                    },\n                    onBlur: () => this.dependencies.selection.focusEditable(),\n                    document: this.document,\n                },\n                isAvailable: isHtmlContentSupported,\n            }),\n        ],\n        powerbox_categories: withSequence(5, { id: \"format\", name: _t(\"Format\") }),\n        powerbox_items: [\n            {\n                categoryId: \"format\",\n                commandId: \"setTagHeading1\",\n                keywords: [_t(\"title\")],\n            },\n            {\n                categoryId: \"format\",\n                commandId: \"setTagHeading2\",\n                keywords: [_t(\"title\")],\n            },\n            {\n                categoryId: \"format\",\n                commandId: \"setTagHeading3\",\n                keywords: [_t(\"title\")],\n            },\n            {\n                categoryId: \"format\",\n                commandId: \"setTagParagraph\",\n            },\n            {\n                categoryId: \"format\",\n                commandId: \"setTagQuote\",\n            },\n            {\n                categoryId: \"format\",\n                commandId: \"setTagPre\",\n            },\n        ],\n        shorthands: [\n            {\n                pattern: /^#$/,\n                commandId: \"setTagHeading\",\n                commandParams: { level: 1 },\n            },\n            {\n                pattern: /^##$/,\n                commandId: \"setTagHeading\",\n                commandParams: { level: 2 },\n            },\n            {\n                pattern: /^###$/,\n                commandId: \"setTagHeading\",\n                commandParams: { level: 3 },\n            },\n            {\n                pattern: /^####$/,\n                commandId: \"setTagHeading\",\n                commandParams: { level: 4 },\n            },\n            {\n                pattern: /^#####$/,\n                commandId: \"setTagHeading\",\n                commandParams: { level: 5 },\n            },\n            {\n                pattern: /^######$/,\n                commandId: \"setTagHeading\",\n                commandParams: { level: 6 },\n            },\n            {\n                pattern: /^>$/,\n                commandId: \"setTagQuote\",\n            },\n        ],\n        hints: [\n            { selector: \"H1\", text: _t(\"Heading 1\") },\n            { selector: \"H2\", text: _t(\"Heading 2\") },\n            { selector: \"H3\", text: _t(\"Heading 3\") },\n            { selector: \"H4\", text: _t(\"Heading 4\") },\n            { selector: \"H5\", text: _t(\"Heading 5\") },\n            { selector: \"H6\", text: _t(\"Heading 6\") },\n            { selector: \"PRE\", text: _t(\"Code\") },\n            { selector: \"BLOCKQUOTE\", text: _t(\"Quote\") },\n        ],\n\n        /** Handlers */\n        selectionchange_handlers: [\n            this.updateFontSelectorParams.bind(this),\n            this.updateFontSizeSelectorParams.bind(this),\n        ],\n        post_undo_handlers: [\n            this.updateFontSelectorParams.bind(this),\n            this.updateFontSizeSelectorParams.bind(this),\n        ],\n        post_redo_handlers: [\n            this.updateFontSelectorParams.bind(this),\n            this.updateFontSizeSelectorParams.bind(this),\n        ],\n        normalize_handlers: this.normalize.bind(this),\n\n        /** Overrides */\n        split_element_block_overrides: [\n            this.handleSplitBlockHeading.bind(this),\n            this.handleSplitBlockPRE.bind(this),\n            this.handleSplitBlockquote.bind(this),\n        ],\n        delete_backward_overrides: withSequence(20, this.handleDeleteBackward.bind(this)),\n        delete_backward_word_overrides: this.handleDeleteBackward.bind(this),\n\n        /** Processors */\n        clipboard_content_processors: this.processContentForClipboard.bind(this),\n        before_insert_processors: this.handleInsertWithinPre.bind(this),\n\n        format_class_predicates: (className) =>\n            [...FONT_SIZE_CLASSES, \"o_default_font_size\"].includes(className),\n    };\n\n    setup() {\n        this.fontSize = reactive({ displayName: \"\" });\n        this.font = reactive({ displayName: \"\" });\n        this.blockFormatIsAvailableMemoized = weakMemoize(\n            (selection) => isHtmlContentSupported(selection) && this.dependencies.dom.canSetBlock()\n        );\n        this.availableFontItems = this.getResource(\"font_items\").filter(\n            ({ tagName }) =>\n                !SUPPORTED_BASE_CONTAINER_NAMES.includes(tagName.toUpperCase()) ||\n                this.config.baseContainers.includes(tagName.toUpperCase())\n        );\n    }\n\n    normalize(root) {\n        for (const el of selectElements(\n            root,\n            \"strong, b, span[style*='font-weight: bolder'], small\"\n        )) {\n            if (isRedundantElement(el)) {\n                unwrapContents(el);\n            }\n        }\n    }\n\n    get fontName() {\n        const sel = this.dependencies.selection.getSelectionData().deepEditableSelection;\n        // if (!sel) {\n        //     return \"Normal\";\n        // }\n        const anchorNode = sel.anchorNode;\n        const block = closestBlock(anchorNode);\n        const tagName = block.tagName.toLowerCase();\n\n        const matchingItems = this.availableFontItems.filter((item) =>\n            item.selector ? block.matches(item.selector) : item.tagName === tagName\n        );\n\n        const matchingItemsWitoutExtraClass = matchingItems.filter((item) => !item.extraClass);\n\n        if (!matchingItems.length) {\n            return _t(\"Normal\");\n        }\n\n        return (\n            matchingItems.find((item) => block.classList.contains(item.extraClass)) ||\n            (matchingItemsWitoutExtraClass.length && matchingItemsWitoutExtraClass[0])\n        ).name;\n    }\n\n    get fontSizeName() {\n        const sel = this.dependencies.selection.getSelectionData().deepEditableSelection;\n        if (!sel) {\n            return fontSizeItems[0].name;\n        }\n        return Math.round(getFontSizeDisplayValue(sel, this.document));\n    }\n\n    get fontSizeItems() {\n        const style = getHtmlStyle(this.document);\n        const nameAlreadyUsed = new Set();\n        return fontSizeItems\n            .flatMap((item) => {\n                const strValue = getCSSVariableValue(item.variableName, style);\n                if (!strValue) {\n                    return [];\n                }\n                const remValue = parseFloat(strValue);\n                const pxValue = convertNumericToUnit(remValue, \"rem\", \"px\", style);\n                const roundedValue = Math.round(pxValue);\n                if (nameAlreadyUsed.has(roundedValue)) {\n                    return [];\n                }\n                nameAlreadyUsed.add(roundedValue);\n\n                return [{ ...item, tagName: \"span\", name: roundedValue }];\n            })\n            .sort((a, b) => a.name - b.name);\n    }\n\n    blockFormatIsAvailable(selection) {\n        return this.blockFormatIsAvailableMemoized(selection);\n    }\n\n    // @todo @phoenix: Move this to a specific Pre/CodeBlock plugin?\n    /**\n     * Specific behavior for pre: insert newline (\\n) in text or insert p at\n     * end.\n     */\n    handleSplitBlockPRE({ targetNode, targetOffset }) {\n        const closestPre = closestElement(targetNode, \"pre\");\n        const closestBlockNode = closestBlock(targetNode);\n        if (\n            !closestPre ||\n            (closestBlockNode.nodeName !== \"PRE\" &&\n                ((closestBlockNode.textContent && !isZWS(closestBlockNode)) ||\n                    closestBlockNode.nextSibling))\n        ) {\n            return;\n        }\n\n        // Nodes to the right of the split position.\n        const nodesAfterTarget = [...rightLeafOnlyNotBlockPath(targetNode, targetOffset)];\n        if (\n            !nodesAfterTarget.length ||\n            (nodesAfterTarget.length === 1 && nodesAfterTarget[0].nodeName === \"BR\") ||\n            isEmptyBlock(closestBlockNode)\n        ) {\n            // Remove the last empty block node within pre tag\n            const [beforeElement, afterElement] = this.dependencies.split.splitElementBlock({\n                targetNode,\n                targetOffset,\n                blockToSplit: closestBlockNode,\n            });\n            const isPreBlock = beforeElement.nodeName === \"PRE\";\n            const baseContainer = isPreBlock\n                ? this.dependencies.baseContainer.createBaseContainer()\n                : afterElement;\n            if (isPreBlock) {\n                baseContainer.replaceChildren(...afterElement.childNodes);\n                afterElement.replaceWith(baseContainer);\n            } else {\n                beforeElement.remove();\n                closestPre.after(afterElement);\n            }\n            const dir = closestBlockNode.getAttribute(\"dir\") || closestPre.getAttribute(\"dir\");\n            if (dir) {\n                baseContainer.setAttribute(\"dir\", dir);\n            }\n            this.dependencies.selection.setCursorStart(baseContainer);\n        } else {\n            const lineBreak = this.document.createElement(\"br\");\n            targetNode.insertBefore(lineBreak, targetNode.childNodes[targetOffset]);\n            this.dependencies.selection.setCursorEnd(lineBreak);\n        }\n        return true;\n    }\n\n    /**\n     * Specific behavior for blockquote: insert p at end and remove the last\n     * empty node.\n     */\n    handleSplitBlockquote({ targetNode, targetOffset, blockToSplit }) {\n        const closestQuote = closestElement(targetNode, \"blockquote\");\n        const closestBlockNode = closestBlock(targetNode);\n        const blockQuotedir = closestQuote && closestQuote.getAttribute(\"dir\");\n\n        if (!closestQuote || closestBlockNode.nodeName !== \"BLOCKQUOTE\") {\n            // If the closestBlockNode is the last element child of its parent\n            // and the parent is a blockquote\n            // we should move the current block ouside of the blockquote.\n            if (\n                closestBlockNode.parentElement === closestQuote &&\n                closestBlockNode.parentElement.lastElementChild === closestBlockNode &&\n                !closestBlockNode.textContent\n            ) {\n                closestQuote.after(closestBlockNode);\n\n                if (blockQuotedir && !closestBlockNode.getAttribute(\"dir\")) {\n                    closestBlockNode.setAttribute(\"dir\", blockQuotedir);\n                }\n                this.dependencies.selection.setSelection({\n                    anchorNode: closestBlockNode,\n                    anchorOffset: 0,\n                });\n                return true;\n            }\n            return;\n        }\n\n        const selection = this.dependencies.selection.getEditableSelection();\n        const previousElementSibling = selection.anchorNode?.childNodes[selection.anchorOffset - 1];\n        const nextElementSibling = selection.anchorNode?.childNodes[selection.anchorOffset];\n        // Double enter at the end of blockquote => we should break out of the blockquote element.\n        if (previousElementSibling?.tagName === \"BR\" && nextElementSibling?.tagName === \"BR\") {\n            nextElementSibling.remove();\n            previousElementSibling.remove();\n            this.dependencies.split.splitElementBlock({\n                targetNode,\n                targetOffset,\n                blockToSplit,\n            });\n            this.dependencies.dom.setBlock({\n                tagName: this.dependencies.baseContainer.getDefaultNodeName(),\n            });\n            return true;\n        }\n\n        this.dependencies.lineBreak.insertLineBreakElement({ targetNode, targetOffset });\n        return true;\n    }\n\n    // @todo @phoenix: Move this to a specific Heading plugin?\n    /**\n     * Specific behavior for headings: do not split in two if cursor at the end but\n     * instead create a paragraph.\n     * Cursor end of line: <h1>title[]</h1> + ENTER <=> <h1>title</h1><p>[]<br/></p>\n     * Cursor in the line: <h1>tit[]le</h1> + ENTER <=> <h1>tit</h1><h1>[]le</h1>\n     */\n    handleSplitBlockHeading(params) {\n        const closestHeading = closestElement(params.targetNode, (element) =>\n            headingTags.includes(element.tagName)\n        );\n        if (closestHeading) {\n            const [, newElement] = this.dependencies.split.splitElementBlock(params);\n            // @todo @phoenix: if this condition can be anticipated before the split,\n            // handle the splitBlock only in such case.\n            if (\n                newElement &&\n                headingTags.includes(newElement.tagName) &&\n                !descendants(newElement).some(isVisibleTextNode)\n            ) {\n                const baseContainer = this.dependencies.baseContainer.createBaseContainer();\n                const dir = newElement.getAttribute(\"dir\");\n                if (dir) {\n                    baseContainer.setAttribute(\"dir\", dir);\n                }\n                baseContainer.replaceChildren(...newElement.childNodes);\n                newElement.replaceWith(baseContainer);\n                this.dependencies.selection.setCursorStart(baseContainer);\n            }\n            return true;\n        }\n    }\n\n    /**\n     * Transform an empty heading or pre at the beginning of the\n     * editable into a base container. An empty blockquote is transformed\n     * into a base container, regardless of its position in the editable.\n     */\n    handleDeleteBackward({ startContainer, startOffset, endContainer, endOffset }) {\n        // Detect if cursor is at the start of the editable (collapsed range).\n        const rangeIsCollapsed = startContainer === endContainer && startOffset === endOffset;\n        const closestHandledElement = closestElement(endContainer, handledElemSelector);\n        if (!rangeIsCollapsed && closestHandledElement?.tagName !== \"BLOCKQUOTE\") {\n            return;\n        }\n        // Check if cursor is inside an empty heading, blockquote or pre.\n        if (!closestHandledElement || closestHandledElement.textContent.length) {\n            return;\n        }\n        // Check if unremovable.\n        if (this.getResource(\"unremovable_node_predicates\").some((p) => p(closestHandledElement))) {\n            return;\n        }\n        const baseContainer = this.dependencies.baseContainer.createBaseContainer();\n        baseContainer.append(...closestHandledElement.childNodes);\n        closestHandledElement.after(baseContainer);\n        closestHandledElement.remove();\n        this.dependencies.selection.setCursorStart(baseContainer);\n        return true;\n    }\n\n    updateFontSelectorParams() {\n        this.font.displayName = this.fontName;\n    }\n\n    updateFontSizeSelectorParams() {\n        this.fontSize.displayName = this.fontSizeName;\n    }\n\n    processContentForClipboard(clonedContents, selection) {\n        const commonAncestorElement = closestElement(selection.commonAncestorContainer);\n        if (commonAncestorElement && !isBlock(clonedContents.firstChild)) {\n            // Get the list of ancestor elements starting from the provided\n            // commonAncestorElement up to the block-level element.\n            const blockEl = closestBlock(commonAncestorElement);\n            const ancestorsList = [\n                commonAncestorElement,\n                ...ancestors(commonAncestorElement, blockEl),\n            ];\n            // Wrap rangeContent with clones of their ancestors to keep the styles.\n            for (const ancestor of ancestorsList) {\n                // Keep the formatting by keeping inline ancestors and paragraph\n                // related ones like headings etc.\n                if (!isBlock(ancestor) || isParagraphRelatedElement(ancestor)) {\n                    const clone = ancestor.cloneNode();\n                    clone.append(...childNodes(clonedContents));\n                    clonedContents.appendChild(clone);\n                }\n            }\n        }\n        return clonedContents;\n    }\n\n    handleInsertWithinPre(insertContainer, block) {\n        if (block.nodeName !== \"PRE\") {\n            return insertContainer;\n        }\n        for (const cb of this.getResource(\"before_insert_within_pre_processors\")) {\n            insertContainer = cb(insertContainer);\n        }\n        const isDeepestBlock = (node) =>\n            isBlock(node) && ![...node.querySelectorAll(\"*\")].some(isBlock);\n        let linebreak;\n        const processNode = (node) => {\n            const children = childNodes(node);\n            if (isDeepestBlock(node) && node.nextSibling) {\n                linebreak = this.document.createTextNode(\"\\n\");\n                node.append(linebreak);\n            }\n            if (node.nodeType === Node.ELEMENT_NODE) {\n                unwrapContents(node);\n            }\n            for (const child of children) {\n                processNode(child);\n            }\n        };\n        for (const node of childNodes(insertContainer)) {\n            processNode(node);\n        }\n        return insertContainer;\n    }\n}\n", "import { Component, useState } from \"@odoo/owl\";\nimport { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\nimport { toolbarButtonProps } from \"@html_editor/main/toolbar/toolbar\";\nimport { useDropdownAutoVisibility } from \"@html_editor/dropdown_autovisibility_hook\";\nimport { useChildRef } from \"@web/core/utils/hooks\";\n\nexport class FontSelector extends Component {\n    static template = \"html_editor.FontSelector\";\n    static props = {\n        ...toolbarButtonProps,\n        getItems: Function,\n        getDisplay: Function,\n        onSelected: Function,\n    };\n    static components = { Dropdown, DropdownItem };\n\n    setup() {\n        this.items = this.props.getItems();\n        this.state = useState(this.props.getDisplay());\n        this.menuRef = useChildRef();\n        useDropdownAutoVisibility(this.env.overlayState, this.menuRef);\n    }\n\n    onSelected(item) {\n        this.props.onSelected(item);\n    }\n}\n", "import { Component, onMounted, useEffect, useRef, useState } from \"@odoo/owl\";\nimport { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\nimport { toolbarButtonProps } from \"@html_editor/main/toolbar/toolbar\";\nimport { useDropdownState } from \"@web/core/dropdown/dropdown_hooks\";\nimport { useDebounced } from \"@web/core/utils/timing\";\nimport { cookie } from \"@web/core/browser/cookie\";\nimport { getCSSVariableValue, getHtmlStyle } from \"@html_editor/utils/formatting\";\nimport { useDropdownAutoVisibility } from \"@html_editor/dropdown_autovisibility_hook\";\nimport { useChildRef } from \"@web/core/utils/hooks\";\n\nconst MAX_FONT_SIZE = 144;\n\nexport class FontSizeSelector extends Component {\n    static template = \"html_editor.FontSizeSelector\";\n    static props = {\n        getItems: Function,\n        getDisplay: Function,\n        onFontSizeInput: Function,\n        onSelected: Function,\n        onBlur: { type: Function, optional: true },\n        document: { validate: (p) => p.nodeType === Node.DOCUMENT_NODE },\n        ...toolbarButtonProps,\n    };\n    static components = { Dropdown, DropdownItem };\n\n    setup() {\n        this.items = this.props.getItems();\n        this.state = useState(this.props.getDisplay());\n        this.dropdown = useDropdownState();\n        this.menuRef = useChildRef();\n        useDropdownAutoVisibility(this.env.overlayState, this.menuRef);\n        this.iframeContentRef = useRef(\"iframeContent\");\n        this.debouncedCustomFontSizeInput = useDebounced(this.onCustomFontSizeInput, 200);\n\n        onMounted(() => {\n            const iframeEl = this.iframeContentRef.el;\n\n            const initFontSizeInput = () => {\n                const iframeDoc = iframeEl.contentWindow.document;\n\n                // Skip if already initialized.\n                if (this.fontSizeInput || !iframeDoc.body) {\n                    return;\n                }\n\n                this.fontSizeInput = iframeDoc.createElement(\"input\");\n                const isDarkMode = cookie.get(\"color_scheme\") === \"dark\";\n                const htmlStyle = getHtmlStyle(document);\n                const backgroundColor = getCSSVariableValue(\n                    isDarkMode ? \"gray-200\" : \"white\",\n                    htmlStyle\n                );\n                const color = getCSSVariableValue(\"black\", htmlStyle);\n                const fontFamily = getCSSVariableValue(\"o-system-fonts\", htmlStyle);\n                Object.assign(iframeDoc.body.style, {\n                    padding: \"0\",\n                    margin: \"0\",\n                });\n                Object.assign(this.fontSizeInput.style, {\n                    width: \"100%\",\n                    height: \"100%\",\n                    border: \"none\",\n                    outline: \"none\",\n                    textAlign: \"center\",\n                    backgroundColor: backgroundColor,\n                    color: color,\n                    fontFamily: fontFamily,\n                });\n                this.fontSizeInput.type = \"text\";\n                this.fontSizeInput.name = \"font-size-input\";\n                this.fontSizeInput.autocomplete = \"off\";\n                this.fontSizeInput.value = this.state.displayName;\n                iframeDoc.body.appendChild(this.fontSizeInput);\n                this.fontSizeInput.addEventListener(\"click\", () => {\n                    if (!this.dropdown.isOpen) {\n                        this.dropdown.open();\n                    }\n                });\n                this.fontSizeInput.addEventListener(\"input\", this.debouncedCustomFontSizeInput);\n                this.fontSizeInput.addEventListener(\n                    \"keydown\",\n                    this.onKeyDownFontSizeInput.bind(this)\n                );\n            };\n            if (iframeEl.contentDocument.readyState === \"complete\") {\n                initFontSizeInput();\n            } else {\n                // in firefox, iframe is not immediately available. we need to wait\n                // for it to be ready before mounting.\n                iframeEl.addEventListener(\n                    \"load\",\n                    () => {\n                        initFontSizeInput();\n                    },\n                    { once: true }\n                );\n            }\n        });\n        useEffect(\n            () => {\n                if (this.fontSizeInput) {\n                    // Update `fontSizeInputValue` whenever the font size changes.\n                    this.fontSizeInput.value = this.state.displayName;\n                }\n            },\n            () => [this.state.displayName]\n        );\n        useEffect(\n            () => {\n                if (this.fontSizeInput) {\n                    // Focus input on dropdown open, blur on close.\n                    if (this.dropdown.isOpen) {\n                        this.fontSizeInput.select();\n                    } else if (\n                        this.iframeContentRef.el?.contains(this.props.document.activeElement)\n                    ) {\n                        this.fontSizeInput.blur();\n                        this.props.onBlur?.();\n                    }\n                }\n            },\n            () => [this.dropdown.isOpen]\n        );\n    }\n\n    onCustomFontSizeInput(ev) {\n        let fontSize = parseInt(ev.target.value, 10);\n        if (fontSize > 0) {\n            fontSize = Math.min(fontSize, MAX_FONT_SIZE);\n            if (this.state.displayName !== fontSize) {\n                this.props.onFontSizeInput(`${fontSize}px`);\n            } else {\n                // Reset input if state.displayName does not change.\n                this.fontSizeInput.value = this.state.displayName;\n            }\n        }\n        this.fontSizeInput.focus();\n    }\n\n    onKeyDownFontSizeInput(ev) {\n        if ([\"Enter\", \"Tab\"].includes(ev.key) && this.dropdown.isOpen) {\n            this.dropdown.close();\n        } else if ([\"ArrowUp\", \"ArrowDown\"].includes(ev.key)) {\n            const fontSizeSelectorMenu = document.querySelector(\".o_font_size_selector_menu div\");\n            if (!fontSizeSelectorMenu) {\n                return;\n            }\n            ev.target.blur();\n            const fontSizeMenuItemToFocus =\n                ev.key === \"ArrowUp\"\n                    ? fontSizeSelectorMenu.lastElementChild\n                    : fontSizeSelectorMenu.firstElementChild;\n            if (fontSizeMenuItemToFocus) {\n                fontSizeMenuItemToFocus.focus();\n            }\n        }\n    }\n\n    onSelected(item) {\n        this.props.onSelected(item);\n    }\n}\n", "import { Component, onWillUpdateProps, useState, useRef } from \"@odoo/owl\";\nimport { CustomColorPicker as ColorPicker } from \"@web/core/color_picker/custom_color_picker/custom_color_picker\";\nimport {\n    isColorGradient,\n    standardizeGradient,\n    rgbaToHex,\n    convertCSSColorToRgba,\n} from \"@web/core/utils/colors\";\n\nexport class GradientPicker extends Component {\n    static components = { ColorPicker };\n    static template = \"html_editor.GradientPicker\";\n    static props = {\n        onGradientChange: { type: Function, optional: true },\n        onGradientPreview: { type: Function, optional: true },\n        setOnCloseCallback: { type: Function, optional: true },\n        setOperationCallbacks: { type: Function, optional: true },\n        selectedGradient: { type: String, optional: true },\n        noTransparency: { type: Boolean, optional: true },\n    };\n\n    setup() {\n        this.state = useState({\n            type: \"linear\",\n            angle: 135,\n            currentColorIndex: 0,\n            size: \"closest-side\",\n        });\n        this.positions = useState({ x: 25, y: 25 });\n        this.colors = useState([\n            { hex: \"#DF7CC4\", percentage: 0 },\n            { hex: \"#6C3582\", percentage: 100 },\n        ]);\n        this.cssGradients = useState({ preview: \"\", linear: \"\", radial: \"\", sliderThumbStyle: \"\" });\n        this.knobRef = useRef(\"gradientAngleKnob\");\n\n        if (this.props.selectedGradient && isColorGradient(this.props.selectedGradient)) {\n            // initialization of the gradient with the selected value\n            this.setGradientFromString(this.props.selectedGradient);\n        } else {\n            // initialization of the gradient with default value\n            this.onColorGradientChange();\n        }\n\n        onWillUpdateProps((newProps) => {\n            if (newProps.selectedGradient) {\n                this.setGradientFromString(newProps.selectedGradient);\n            }\n        });\n    }\n\n    setGradientFromString(gradient) {\n        if (!gradient || !isColorGradient(gradient)) {\n            return;\n        }\n        gradient = standardizeGradient(gradient);\n        const colors = [\n            ...gradient.matchAll(\n                /(#[0-9a-f]{6}|rgba?\\(\\s*[0-9]+\\s*,\\s*[0-9]+\\s*,\\s*[0-9]+\\s*[,\\s*[0-9.]*]?\\s*\\)|[a-z]+)\\s*([[0-9]+%]?)/g\n            ),\n        ].filter((color) => rgbaToHex(color[1]) !== \"#\");\n\n        this.colors.splice(0, this.colors.length);\n        for (const color of colors) {\n            this.colors.push({ hex: rgbaToHex(color[1]), percentage: color[2].replace(\"%\", \"\") });\n        }\n\n        const isLinear = gradient.startsWith(\"linear-gradient(\");\n        if (isLinear) {\n            const angle = gradient.match(/(-?[0-9]+)deg/);\n            if (angle) {\n                this.state.angle = parseInt(angle[1]);\n            }\n        } else {\n            this.state.type = \"radial\";\n            const sizeMatch = gradient.match(/(closest|farthest)-(side|corner)/);\n            const size = sizeMatch ? sizeMatch[0] : \"farthest-corner\";\n            this.state.size = size;\n\n            const position = gradient.match(/ at ([0-9]+)% ([0-9]+)%/) || [\"\", \"50\", \"50\"];\n            this.positions.x = position[1];\n            this.positions.y = position[2];\n        }\n\n        this.updateCssGradients();\n    }\n\n    selectType(type) {\n        this.state.type = type;\n        this.onColorGradientChange();\n    }\n\n    onAngleChange(ev) {\n        const angle = parseInt(ev.target.value);\n        if (!isNaN(angle)) {\n            const clampedAngle = Math.min(Math.max(angle, 0), 360);\n            ev.target.value = clampedAngle;\n            this.state.angle = clampedAngle;\n            this.onColorGradientChange();\n        }\n    }\n\n    onPositionChange(position, ev) {\n        const inputValue = parseFloat(ev.target.value);\n        if (!isNaN(inputValue)) {\n            const clampedValue = Math.min(Math.max(inputValue, 0), 100);\n            ev.target.value = clampedValue;\n            this.positions[position] = clampedValue;\n            this.onColorGradientChange();\n        }\n    }\n\n    onColorChange(color) {\n        const hex = rgbaToHex(color.cssColor);\n        this.colors[this.state.currentColorIndex].hex = hex;\n        this.onColorGradientChange();\n    }\n\n    onColorPreview(color) {\n        const hex = rgbaToHex(color.cssColor);\n        this.colors[this.state.currentColorIndex].hex = hex;\n        this.onColorGradientPreview();\n    }\n\n    onSizeChange(size) {\n        this.state.size = size;\n        this.onColorGradientChange();\n    }\n\n    onColorPercentageChange(colorIndex, ev) {\n        this.state.currentColorIndex = colorIndex;\n        this.colors[colorIndex].percentage = ev.target.value;\n        this.sortColors();\n        this.onColorGradientChange();\n    }\n\n    onGradientPreviewClick(ev) {\n        const width = parseInt(window.getComputedStyle(ev.target).width, 10);\n        const percentage = Math.round((100 * ev.offsetX) / width);\n        this.addColorStop(percentage);\n    }\n\n    addColorStop(percentage) {\n        let color;\n\n        let previousColor = this.colors.findLast((color) => color.percentage <= percentage);\n        let nextColor = this.colors.find((color) => color.percentage > percentage);\n        if (!previousColor && nextColor) {\n            // Click position is before the first color\n            color = nextColor.hex;\n        } else if (!nextColor && previousColor) {\n            //  Click position is after the last color\n            color = previousColor.hex;\n        } else if (nextColor && previousColor) {\n            const previousRatio =\n                (nextColor.percentage - percentage) /\n                (nextColor.percentage - previousColor.percentage);\n            const nextRatio = 1 - previousRatio;\n\n            previousColor = convertCSSColorToRgba(previousColor.hex);\n            nextColor = convertCSSColorToRgba(nextColor.hex);\n\n            const red = Math.round(previousRatio * previousColor.red + nextRatio * nextColor.red);\n            const green = Math.round(\n                previousRatio * previousColor.green + nextRatio * nextColor.green\n            );\n            const blue = Math.round(\n                previousRatio * previousColor.blue + nextRatio * nextColor.blue\n            );\n            const opacity = Math.round(\n                previousRatio * previousColor.opacity + nextRatio * nextColor.opacity\n            );\n            color = `rgba(${red}, ${green}, ${blue}, ${opacity / 100})`;\n        }\n\n        this.colors.push({ hex: color, percentage });\n        this.sortColors();\n        this.state.currentColorIndex = this.colors.findIndex(\n            (color) => color.percentage === percentage\n        );\n        this.onColorGradientChange();\n    }\n\n    removeColor(colorIndex) {\n        if (this.colors.length <= 2) {\n            return;\n        }\n        this.colors.splice(colorIndex, 1);\n        this.state.currentColorIndex = 0;\n        this.onColorGradientChange();\n    }\n\n    sortColors() {\n        this.colors = this.colors.sort((a, b) => a.percentage - b.percentage);\n    }\n\n    updateCssGradients() {\n        const gradientColors = this.colors\n            .map((color) => `${color.hex} ${color.percentage}%`)\n            .join(\", \");\n        let sliderThumbStyle = \"\";\n        // color the slider thumb with the color of the gradient\n        for (let i = 0; i < this.colors.length; i++) {\n            const selector = `.gradient-colors div:nth-child(${i + 1}) input[type=\"range\"]`;\n            const style = `background-color: ${this.colors[i].hex};`;\n            sliderThumbStyle += `${selector}::-webkit-slider-thumb { ${style} }\\n`;\n            sliderThumbStyle += `${selector}::-moz-range-thumb { ${style} }\\n`;\n        }\n\n        this.cssGradients.preview = `linear-gradient(90deg, ${gradientColors})`;\n        this.cssGradients.linear = `linear-gradient(${this.state.angle}deg, ${gradientColors})`;\n        this.cssGradients.radial = `radial-gradient(circle ${this.state.size} at ${this.positions.x}% ${this.positions.y}%, ${gradientColors})`;\n        this.cssGradients.sliderThumbStyle = sliderThumbStyle;\n    }\n\n    onColorGradientChange() {\n        this.updateCssGradients();\n        this.props?.onGradientChange(this.cssGradients[this.state.type]);\n    }\n\n    onColorGradientPreview() {\n        this.updateCssGradients();\n        this.props.onGradientPreview?.({ gradient: this.cssGradients[this.state.type] });\n    }\n\n    get currentColorHex() {\n        return this.colors?.[this.state.currentColorIndex]?.hex || \"#000000\";\n    }\n\n    onKnobMouseDown(ev) {\n        const knobEl = this.knobRef.el;\n        if (!knobEl) {\n            return;\n        }\n        const knobRadius = knobEl.offsetWidth / 2;\n        const knobRect = knobEl.getBoundingClientRect();\n        const centerX = knobRect.left + knobRadius;\n        const centerY = knobRect.top + knobRadius;\n\n        const updateAngle = (ev) => {\n            // calculate the differences between the mouse position and the\n            // center of the knob\n            const distanceX = ev.clientX - centerX;\n            const distanceY = ev.clientY - centerY;\n\n            // calculate the angle between the center and the mouse position\n            const angle = Math.atan2(distanceY, distanceX) * (180 / Math.PI);\n            this.state.angle = Math.round((angle + 360) % 360);\n        };\n\n        updateAngle(ev);\n        this.onColorGradientChange();\n\n        const onKnobMouseMove = (ev) => {\n            updateAngle(ev);\n            this.onColorGradientChange();\n        };\n        const onKnobMouseUp = () => document.removeEventListener(\"mousemove\", onKnobMouseMove);\n\n        document.addEventListener(\"mousemove\", onKnobMouseMove);\n        document.addEventListener(\"mouseup\", onKnobMouseUp, { once: true });\n    }\n}\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { isEditorTab, isEmptyBlock, isProtected } from \"@html_editor/utils/dom_info\";\nimport { removeClass } from \"@html_editor/utils/dom\";\nimport { descendants, selectElements } from \"@html_editor/utils/dom_traversal\";\nimport { closestBlock } from \"../utils/blocks\";\n\n/**\n * @typedef {import(\"@html_editor/editor\").EditorContext} EditorContext\n * @typedef {import(\"@html_editor/core/selection_plugin\").SelectionData} SelectionData\n * @typedef {import(\"plugins\").CSSSelector} CSSSelector\n * @typedef {import(\"plugins\").TranslatedString} TranslatedString\n */\n\n/**\n * @typedef {((\n *   selectionData: SelectionData,\n *   editable: EditorContext[\"editable\"]\n * ) => HTMLElement[] | NodeList)[]} hint_targets_providers\n * @typedef {{ selector: CSSSelector; text: TranslatedString; }[]} hints\n */\n\nexport class HintPlugin extends Plugin {\n    static id = \"hint\";\n    static dependencies = [\"history\", \"selection\"];\n    /** @type {import(\"plugins\").EditorResources} */\n    resources = {\n        /** Handlers */\n        selectionchange_handlers: this.updateHints.bind(this),\n        external_history_step_handlers: () => {\n            this.clearHints();\n            this.updateHints();\n        },\n        normalize_handlers: this.normalize.bind(this),\n        clean_for_save_handlers: ({ root }) => this.clearHints(root),\n        content_updated_handlers: this.updateHints.bind(this),\n\n        hint_targets_providers: (selectionData, editable) => {\n            if (!selectionData.currentSelectionIsInEditable || !selectionData.documentSelection) {\n                return [];\n            }\n            const blockEl = closestBlock(selectionData.documentSelection.anchorNode);\n            if (this.dependencies.selection.isNodeEditable(blockEl)) {\n                return [blockEl];\n            } else {\n                return [];\n            }\n        },\n        system_classes: [\"o-we-hint\"],\n        system_attributes: [\"o-we-hint-text\"],\n    };\n\n    setup() {\n        this.updateHints(this.editable);\n    }\n\n    destroy() {\n        super.destroy();\n        this.clearHints();\n    }\n\n    normalize() {\n        this.clearHints();\n        this.updateHints();\n    }\n\n    /**\n     * @param {HTMLElement} [root]\n     */\n    updateHints() {\n        const selectionData = this.dependencies.selection.getSelectionData();\n        const editableSelection = selectionData.editableSelection;\n        this.clearHints();\n        if (editableSelection.isCollapsed) {\n            const hints = this.getResource(\"hints\");\n            for (const provideTargets of this.getResource(\"hint_targets_providers\")) {\n                for (const target of provideTargets(selectionData, this.editable)) {\n                    const nodeHint = hints.find((h) => target.matches(h.selector))?.text;\n                    if (\n                        target &&\n                        nodeHint &&\n                        isEmptyBlock(target) &&\n                        !isProtected(target) &&\n                        !descendants(target).some(isEditorTab)\n                    ) {\n                        this.makeHint(target, nodeHint);\n                    }\n                }\n            }\n        }\n    }\n\n    makeHint(el, text) {\n        this.dispatchTo(\"make_hint_handlers\", el);\n        el.setAttribute(\"o-we-hint-text\", text);\n        el.classList.add(\"o-we-hint\");\n    }\n\n    removeHint(el) {\n        el.removeAttribute(\"o-we-hint-text\");\n        removeClass(el, \"o-we-hint\");\n        this.getResource(\"system_style_properties\").forEach((n) => el.style.removeProperty(n));\n    }\n\n    clearHints(root = this.editable) {\n        for (const elem of selectElements(root, \".o-we-hint\")) {\n            this.removeHint(elem);\n        }\n    }\n}\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { isBlock, closestBlock } from \"@html_editor/utils/blocks\";\nimport { splitTextNode, unwrapContents } from \"@html_editor/utils/dom\";\nimport { isElement, isTextNode, isZwnbsp } from \"@html_editor/utils/dom_info\";\nimport { closestElement, selectElements, findFurthest } from \"@html_editor/utils/dom_traversal\";\nimport { DIRECTIONS, nodeSize } from \"@html_editor/utils/position\";\n\n/** @typedef {((codeElement: HTMLElement) => void)[]} to_inline_code_processors */\n\nexport class InlineCodePlugin extends Plugin {\n    static id = \"inlineCode\";\n    static dependencies = [\"selection\", \"history\", \"input\", \"split\", \"feff\"];\n    /** @type {import(\"plugins\").EditorResources} */\n    resources = {\n        input_handlers: this.onInput.bind(this),\n        selectionchange_handlers: this.handleSelectionChange.bind(this),\n        feff_providers: (root, cursors) =>\n            [...selectElements(root, \".o_inline_code\")].flatMap((code) =>\n                this.dependencies.feff.surroundWithFeffs(code, cursors)\n            ),\n    };\n\n    setup() {\n        this.addDomListener(this.document, \"keydown\", this.onKeyDown.bind(this));\n    }\n\n    handleSelectionChange() {\n        if (this.historySavePointRestore) {\n            delete this.historySavePointRestore;\n        }\n    }\n\n    onKeyDown() {\n        const selection = this.dependencies.selection.getEditableSelection();\n        if (\n            selection.isCollapsed ||\n            closestElement(selection.anchorNode, \"code\") ||\n            closestElement(selection.focusNode, \"code\")\n        ) {\n            return;\n        }\n        const targetBlocks = this.dependencies.selection.getTargetedBlocks();\n        const hasTextNode = this.dependencies.selection.getTargetedNodes().some(isTextNode);\n        if (targetBlocks.size === 1 && hasTextNode) {\n            this.historySavePointRestore = this.dependencies.history.makeSavePoint();\n        }\n    }\n\n    onInput(ev) {\n        const selection = this.dependencies.selection.getEditableSelection();\n        if (ev.data !== \"`\" || closestElement(selection.anchorNode, \"code\")) {\n            return;\n        }\n        if (this.historySavePointRestore) {\n            this.historySavePointRestore();\n            let { anchorNode, anchorOffset, focusNode, focusOffset, direction } =\n                this.dependencies.split.splitSelection();\n            const blockEl = closestBlock(anchorNode);\n            // Adjust if anchor/focus directly equals block element\n            const deepChild = (node, offset) => (node === blockEl ? node.childNodes[offset] : node);\n            anchorNode = deepChild(anchorNode, anchorOffset);\n            focusNode = deepChild(focusNode, focusOffset);\n            if (direction === DIRECTIONS.LEFT) {\n                // Swap anchorNode and focusNode\n                [anchorNode, anchorOffset, focusNode, focusOffset] = [\n                    focusNode,\n                    focusOffset,\n                    anchorNode,\n                    anchorOffset,\n                ];\n            }\n            const furthestAnchorElement = findFurthest(anchorNode, blockEl, (n) => !isBlock(n));\n            let start = this.dependencies.split.splitAroundUntil(anchorNode, furthestAnchorElement);\n            const furthestFocusElement = findFurthest(focusNode, blockEl, (n) => !isBlock(n));\n            const end = this.dependencies.split.splitAroundUntil(focusNode, furthestFocusElement);\n\n            let codeElement = this.document.createElement(\"code\");\n            codeElement.classList.add(\"o_inline_code\");\n            start.before(codeElement);\n            while (start) {\n                if (isElement(start)) {\n                    for (const code of selectElements(start, \"code\")) {\n                        start = unwrapContents(code)[0];\n                    }\n                }\n                const next = start.nextSibling;\n                if (start.nodeName === \"IMG\") {\n                    // Only create <code> if we still have nodes to process\n                    // after this one.\n                    if (start !== end && next) {\n                        codeElement = this.document.createElement(\"code\");\n                        codeElement.classList.add(\"o_inline_code\");\n                    }\n                } else {\n                    if (!codeElement.isConnected) {\n                        start.before(codeElement);\n                    }\n                    codeElement.appendChild(start);\n                }\n                if (start === end) {\n                    break;\n                }\n                start = next;\n            }\n            this.dispatchTo(\"to_inline_code_processors\", codeElement);\n            this.dependencies.selection.setSelection({\n                anchorNode: codeElement,\n                anchorOffset: nodeSize(codeElement),\n            });\n            this.dependencies.history.addStep();\n            delete this.historySavePointRestore;\n            return;\n        }\n\n        // We just inserted a backtick, check if there was another\n        // one in the text.\n        let textNode = selection.startContainer;\n        const wholeText = textNode.wholeText;\n        const textHasTwoTicks = /`.*`/.test(wholeText);\n        // We don't apply the code tag if there is no content between the two `\n        if (textHasTwoTicks && wholeText.replace(/`/g, \"\").length) {\n            let offset = selection.startOffset;\n            let sibling = textNode.previousSibling;\n            while (sibling && sibling.nodeType === Node.TEXT_NODE) {\n                if (!isZwnbsp(sibling)) {\n                    offset += sibling.textContent.length;\n                }\n                sibling.textContent += textNode.textContent;\n                textNode.remove();\n                textNode = sibling;\n                sibling = sibling.previousSibling;\n            }\n            sibling = textNode.nextSibling;\n            while (sibling && sibling.nodeType === Node.TEXT_NODE) {\n                if (!isZwnbsp(sibling)) {\n                    textNode.textContent += sibling.textContent;\n                }\n                sibling.remove();\n                sibling = sibling.nextSibling;\n            }\n            this.dependencies.selection.setSelection({\n                anchorNode: textNode,\n                anchorOffset: offset,\n            });\n            this.dependencies.history.addStep();\n            const insertedBacktickIndex = offset - 1;\n            const textBeforeInsertedBacktick = textNode.textContent.substring(\n                0,\n                insertedBacktickIndex\n            );\n            let startOffset, endOffset;\n            const isClosingForward = textBeforeInsertedBacktick.includes(\"`\");\n            if (isClosingForward) {\n                // There is a backtick before the new backtick.\n                startOffset = textBeforeInsertedBacktick.lastIndexOf(\"`\");\n                endOffset = insertedBacktickIndex;\n            } else {\n                // There is a backtick after the new backtick.\n                const textAfterInsertedBacktick = textNode.textContent.substring(offset);\n                startOffset = insertedBacktickIndex;\n                endOffset = offset + textAfterInsertedBacktick.indexOf(\"`\");\n            }\n            // Split around the backticks if needed so text starts\n            // and ends with a backtick.\n            if (endOffset && endOffset < textNode.textContent.length) {\n                splitTextNode(textNode, endOffset + 1, DIRECTIONS.LEFT);\n            }\n            if (startOffset) {\n                splitTextNode(textNode, startOffset);\n            }\n            // Remove ticks.\n            textNode.textContent = textNode.textContent.substring(\n                1,\n                textNode.textContent.length - 1\n            );\n            // Insert code element.\n            const codeElement = this.document.createElement(\"code\");\n            codeElement.classList.add(\"o_inline_code\");\n            textNode.before(codeElement);\n            codeElement.append(textNode);\n            if (!codeElement.textContent.length) {\n                this.dependencies.history.addStep();\n                this.dependencies.selection.setSelection({\n                    anchorNode: codeElement.firstChild,\n                    anchorOffset: 1,\n                });\n            } else if (isClosingForward) {\n                // Move selection out of code element.\n                this.dependencies.history.addStep();\n                this.dependencies.selection.setSelection({\n                    anchorNode: codeElement.nextSibling,\n                    anchorOffset: 1,\n                });\n            } else {\n                this.dependencies.history.addStep();\n                this.dependencies.selection.setSelection({\n                    anchorNode: codeElement.firstChild,\n                    anchorOffset: 0,\n                });\n            }\n        }\n    }\n}\n", "import { registry } from \"@web/core/registry\";\n\nconst commandCategoryRegistry = registry.category(\"command_categories\");\n// A shortcut conflict occurs when actions are bound to the same\n// shortcut as the command palette. To avoid this, those actions can be\n// added to the command palette itself within this high priority category\n// so that they appear first in the results.\ncommandCategoryRegistry.add(\"shortcut_conflict\", {}, { sequence: 5 });\n", "import { closestElement } from \"@html_editor/utils/dom_traversal\";\nimport { URL_REGEX, cleanZWChars } from \"./utils\";\nimport { isImageUrl } from \"@html_editor/utils/url\";\nimport { Plugin } from \"@html_editor/plugin\";\nimport { childNodeIndex } from \"@html_editor/utils/position\";\n\n/**\n * @typedef {((text: string, url: string) => void | true)[]} paste_url_overrides\n */\n\nexport class LinkPastePlugin extends Plugin {\n    static id = \"linkPaste\";\n    static dependencies = [\"link\", \"clipboard\", \"selection\", \"dom\", \"history\"];\n    /** @type {import(\"plugins\").EditorResources} */\n    resources = {\n        before_paste_handlers: this.selectFullySelectedLink.bind(this),\n        paste_text_overrides: this.handlePasteText.bind(this),\n    };\n\n    /**\n     * @param {EditorSelection} selection\n     * @param {string} text\n     */\n    handlePasteText(selection, text) {\n        let splitAroundUrl;\n        // todo: add placeholder plugin that prevent any other plugin\n        // Avoid transforming dynamic placeholder pattern to url.\n        if (!text.match(/\\${.*}/gi)) {\n            splitAroundUrl = text.split(URL_REGEX);\n            // Remove 'http(s)://' capturing group from the result (indexes\n            // 2, 5, 8, ...).\n            splitAroundUrl = splitAroundUrl.filter((_, index) => (index + 1) % 3);\n        }\n        if (\n            !splitAroundUrl ||\n            splitAroundUrl.length < 3 ||\n            closestElement(selection.anchorNode, \"pre\")\n        ) {\n            // Let the default paste handle the text.\n            return false;\n        }\n        if (splitAroundUrl.length === 3 && !splitAroundUrl[0] && !splitAroundUrl[2]) {\n            // Pasted content is a single URL.\n            this.handlePasteTextUrl(selection, text);\n        } else {\n            this.handlePasteTextMultiUrl(selection, splitAroundUrl);\n        }\n        return true;\n    }\n    /**\n     * @param {EditorSelection} selection\n     * @param {string} text\n     */\n    handlePasteTextUrl(selection, text) {\n        const selectionIsInsideALink = !!closestElement(selection.anchorNode, \"a\");\n        const url = /^https?:\\/\\//i.test(text) ? text : \"http://\" + text;\n        if (selectionIsInsideALink) {\n            this.handlePasteTextUrlInsideLink(text, url);\n            return;\n        }\n        if (this.delegateTo(\"paste_url_overrides\", text, url)) {\n            return;\n        }\n        this.dependencies.link.insertLink(url, text);\n    }\n    /**\n     * @param {string} text\n     * @param {string} url\n     */\n    handlePasteTextUrlInsideLink(text, url) {\n        // A url cannot be transformed inside an existing link.\n        // An image can be embedded inside an existing link, a video cannot.\n        if (isImageUrl(url)) {\n            const img = this.document.createElement(\"IMG\");\n            img.setAttribute(\"src\", url);\n            this.dependencies.dom.insert(img);\n        } else {\n            this.dependencies.dom.insert(text);\n        }\n    }\n    /**\n     * @param {EditorSelection} selection\n     * @param {string[]} splitAroundUrl\n     */\n    handlePasteTextMultiUrl(selection, splitAroundUrl) {\n        const selectionIsInsideALink = !!closestElement(selection.anchorNode, \"a\");\n        for (let i = 0; i < splitAroundUrl.length; i++) {\n            const url = /^https?:\\/\\//gi.test(splitAroundUrl[i])\n                ? splitAroundUrl[i]\n                : \"http://\" + splitAroundUrl[i];\n            // Even indexes will always be plain text, and odd indexes will always be URL.\n            // A url cannot be transformed inside an existing link.\n            if (i % 2 && !selectionIsInsideALink) {\n                this.dependencies.dom.insert(\n                    this.dependencies.link.createLink(url, splitAroundUrl[i])\n                );\n            } else if (splitAroundUrl[i] !== \"\") {\n                this.dependencies.clipboard.pasteText(splitAroundUrl[i]);\n            }\n        }\n    }\n\n    /**\n     * @param {EditorSelection} selection\n     */\n    selectFullySelectedLink(selection) {\n        const link = closestElement(selection.anchorNode, \"a\");\n        if (\n            link?.parentElement?.isContentEditable &&\n            cleanZWChars(selection.textContent()) === cleanZWChars(link.innerText) &&\n            !this.getResource(\"unremovable_node_predicates\").some((p) => p(link))\n        ) {\n            this.dependencies.selection.setSelection({\n                anchorNode: link.parentElement,\n                anchorOffset: childNodeIndex(link) + (selection.direction ? 0 : 1),\n                focusNode: link.parentElement,\n                focusOffset: childNodeIndex(link) + (selection.direction ? 1 : 0),\n            });\n        }\n    }\n}\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { unwrapContents } from \"@html_editor/utils/dom\";\nimport { closestElement, descendants, selectElements } from \"@html_editor/utils/dom_traversal\";\nimport { findInSelection, callbacksForCursorUpdate } from \"@html_editor/utils/selection\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { LinkPopover } from \"./link_popover\";\nimport { DIRECTIONS, leftPos, nodeSize, rightPos } from \"@html_editor/utils/position\";\nimport { EMAIL_REGEX, URL_REGEX, cleanZWChars, deduceURLfromText } from \"./utils\";\nimport { isElement, isVisible, isZwnbsp } from \"@html_editor/utils/dom_info\";\nimport { KeepLast } from \"@web/core/utils/concurrency\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { memoize } from \"@web/core/utils/functions\";\nimport { withSequence } from \"@html_editor/utils/resource\";\nimport { isBlock, closestBlock } from \"@html_editor/utils/blocks\";\nimport { isHtmlContentSupported } from \"@html_editor/core/selection_plugin\";\nimport { isBrowserFirefox } from \"@web/core/browser/feature_detection\";\n\n/** @typedef {import(\"@odoo/owl\").Component} Component */\n/** @typedef {import(\"plugins\").CSSSelector} CSSSelector */\n/**\n * @typedef {import(\"@html_editor/core/selection_plugin\").EditorSelection} EditorSelection\n */\n\n/**\n * @param {EditorSelection} selection\n */\nfunction isLinkActive(selection) {\n    const linkElementAnchor = closestElement(selection.anchorNode, \"A\");\n    const linkElementFocus = closestElement(selection.focusNode, \"A\");\n    if (linkElementFocus && linkElementAnchor) {\n        return linkElementAnchor === linkElementFocus;\n    }\n    if (linkElementAnchor || linkElementFocus) {\n        return true;\n    }\n\n    return false;\n}\n\n/**\n * @param { HTMLAnchorElement } link\n * @param {number} offset\n * @returns {\"start\"|\"end\"|false}\n */\nfunction isPositionAtEdgeofLink(link, offset) {\n    const childNodes = [...link.childNodes];\n    if (!childNodes.length) {\n        return \"end\";\n    }\n    let firstVisibleIndex = childNodes.findIndex(isVisible);\n    firstVisibleIndex = firstVisibleIndex === -1 ? 0 : firstVisibleIndex;\n    if (offset <= firstVisibleIndex) {\n        return \"start\";\n    }\n    let lastVisibleIndex = childNodes.reverse().findIndex(isVisible);\n    lastVisibleIndex = lastVisibleIndex === -1 ? 0 : childNodes.length - lastVisibleIndex;\n    if (offset >= lastVisibleIndex) {\n        return \"end\";\n    }\n    return false;\n}\n\nasync function fetchExternalMetaData(url) {\n    // Get the external metadata\n    try {\n        return await rpc(\"/html_editor/link_preview_external\", {\n            preview_url: url,\n        });\n    } catch {\n        // when it's not possible to fetch the metadata we don't want to block the ui\n        return;\n    }\n}\n\nasync function fetchInternalMetaData(url) {\n    // Get the internal metadata\n    const keepLastPromise = new KeepLast();\n    const urlParsed = new URL(url);\n    // Enforce the current page's protocol to prevent mixed content issues.\n    if (urlParsed.protocol !== window.location.protocol) {\n        urlParsed.protocol = window.location.protocol;\n    }\n\n    const result = await keepLastPromise\n        .add(fetch(urlParsed))\n        .then((response) => response.text())\n        .then(async (content) => {\n            const html_parser = new window.DOMParser();\n            const doc = html_parser.parseFromString(content, \"text/html\");\n            const internalUrlMetaData = await rpc(\"/html_editor/link_preview_internal\", {\n                preview_url: urlParsed.href,\n            });\n\n            internalUrlMetaData[\"favicon\"] = doc.querySelector(\"link[rel~='icon']\");\n            internalUrlMetaData[\"ogTitle\"] = doc.querySelector(\"[property='og:title']\");\n            internalUrlMetaData[\"title\"] = doc.querySelector(\"title\");\n\n            return internalUrlMetaData;\n        })\n        .catch((error) => {\n            // HTTP error codes should not prevent to edit the links, so we\n            // only check for proper instances of Error.\n            if (error instanceof Error) {\n                return Promise.reject(error);\n            }\n        });\n    return result;\n}\n\nasync function fetchAttachmentMetaData(url, ormService) {\n    try {\n        const urlParsed = new URL(url, window.location.origin);\n        const attachementId = parseInt(urlParsed.pathname.split(\"/\").pop());\n        return (\n            await ormService.read(\"ir.attachment\", [attachementId], [\"name\", \"mimetype\", \"type\"])\n        )[0];\n    } catch {\n        return { name: url };\n    }\n}\n\n/**\n * @typedef { Object } LinkShared\n * @property { LinkPlugin['createLink'] } createLink\n * @property { LinkPlugin['getPathAsUrlCommand'] } getPathAsUrlCommand\n * @property { LinkPlugin['insertLink'] } insertLink\n */\n\n/**\n * @typedef {((link: HTMLLinkElement) => boolean)[]} is_link_editable_predicates\n * @typedef {((link: HTMLLinkElement) => boolean)[]} legit_empty_link_predicates\n * @typedef {(() => boolean)[]} link_compatible_selection_predicates\n * @typedef {CSSSelector[]} immutable_link_selectors\n * @typedef {{\n *      PopoverClass: Component;\n *      isAvailable: (linkEl: HTMLLinkElement) => boolean;\n *      getProps: (props) => props;\n *  }[]} link_popovers\n * @typedef {((linkEl: HTMLAnchorElement) => void)[]} create_link_handlers\n */\n\nexport class LinkPlugin extends Plugin {\n    static id = \"link\";\n    static dependencies = [\n        \"dom\",\n        \"history\",\n        \"input\",\n        \"selection\",\n        \"split\",\n        \"lineBreak\",\n        \"overlay\",\n        \"color\",\n        \"baseContainer\",\n        \"feff\",\n    ];\n    static defaultConfig = {\n        allowStripDomain: true,\n    };\n    // @phoenix @todo: do we want to have createLink and insertLink methods in link plugin?\n    static shared = [\"createLink\", \"insertLink\", \"getPathAsUrlCommand\"];\n    /** @type {import(\"plugins\").EditorResources} */\n    resources = {\n        user_commands: [\n            {\n                id: \"openLinkTools\",\n                title: _t(\"Link\"),\n                description: _t(\"Add a link\"),\n                icon: \"fa-link\",\n                run: ({ link, type } = {}) => this.openLinkTools(link, type),\n                isAvailable: (selection) => {\n                    const linkEl = findInSelection(selection, \"a\");\n                    return linkEl\n                        ? this.getResource(\"link_popovers\").some((p) => p.isAvailable(linkEl))\n                        : isHtmlContentSupported(selection);\n                },\n            },\n            {\n                id: \"removeLinkFromSelection\",\n                title: _t(\"Remove Link\"),\n                description: _t(\"Remove Link\"),\n                icon: \"fa-unlink\",\n                isAvailable: (selection) => {\n                    if (!isHtmlContentSupported(selection)) {\n                        return false;\n                    }\n                    for (const node of this.dependencies.selection.getTargetedNodes()) {\n                        const linkEl = closestElement(node, \"a\");\n                        if (\n                            linkEl &&\n                            !this.isLinkImmutable(linkEl) &&\n                            linkEl.parentElement.isContentEditable\n                        ) {\n                            return true;\n                        }\n                    }\n                },\n                run: this.removeLinkFromSelection.bind(this),\n            },\n        ],\n\n        toolbar_groups: [\n            withSequence(40, { id: \"link\", namespaces: [\"compact\", \"expanded\"] }),\n            withSequence(30, { id: \"image_link\", namespaces: [\"image\"] }),\n        ],\n        toolbar_items: [\n            {\n                id: \"link\",\n                groupId: \"link\",\n                commandId: \"openLinkTools\",\n                isActive: isLinkActive,\n                isDisabled: () => !this.isLinkAllowedOnSelection(),\n            },\n            {\n                id: \"unlink\",\n                groupId: \"link\",\n                commandId: \"removeLinkFromSelection\",\n                isDisabled: () => this.removeLinkFromSelectionIsDisabled(),\n            },\n            {\n                id: \"link\",\n                groupId: \"image_link\",\n                commandId: \"openLinkTools\",\n                isActive: isLinkActive,\n                isDisabled: () => !this.isLinkAllowedOnSelection(),\n            },\n            {\n                id: \"unlink\",\n                groupId: \"image_link\",\n                commandId: \"removeLinkFromSelection\",\n                isDisabled: () => this.removeLinkFromSelectionIsDisabled(),\n            },\n        ],\n\n        powerbox_categories: withSequence(50, { id: \"navigation\", name: _t(\"Navigation\") }),\n        powerbox_items: [\n            {\n                categoryId: \"navigation\",\n                commandId: \"openLinkTools\",\n            },\n            {\n                title: _t(\"Button\"),\n                description: _t(\"Add a button\"),\n                categoryId: \"navigation\",\n                commandId: \"openLinkTools\",\n                commandParams: { type: \"primary\" },\n            },\n        ],\n\n        power_buttons: withSequence(10, {\n            commandId: \"openLinkTools\",\n            commandParams: { type: \"primary\" },\n            description: _t(\"Add a button\"),\n            icon: \"fa-square\",\n        }),\n\n        link_popovers: [\n            withSequence(50, {\n                //Default option\n                PopoverClass: LinkPopover,\n                isAvailable: (linkEl) => !linkEl || !this.isLinkImmutable(linkEl),\n                getProps: (props) => props,\n            }),\n        ],\n\n        immutable_link_selectors: [\n            '[data-bs-toggle=\"tab\"]',\n            '[data-bs-toggle=\"collapse\"]',\n            '[data-bs-toggle=\"dropdown\"]',\n            \".dropdown-item\",\n            \"[data-oe-model]\",\n            \":has(>[data-oe-model])\",\n            \".o_prevent_link_editor a\",\n        ],\n        legit_empty_link_predicates: (linkEl) => linkEl.hasAttribute(\"data-mimetype\"),\n\n        /** Handlers */\n        beforeinput_handlers: withSequence(5, this.onBeforeInput.bind(this)),\n        input_handlers: this.onInputDeleteNormalizeLink.bind(this),\n        before_delete_handlers: this.updateCurrentLinkSyncState.bind(this),\n        delete_handlers: this.onInputDeleteNormalizeLink.bind(this),\n        before_paste_handlers: this.updateCurrentLinkSyncState.bind(this),\n        after_paste_handlers: this.onPasteNormalizeLink.bind(this),\n        selectionchange_handlers: this.handleSelectionChange.bind(this),\n        clean_for_save_handlers: ({ root }) => this.removeEmptyLinks(root),\n        normalize_handlers: this.normalizeLink.bind(this),\n        after_insert_handlers: this.handleAfterInsert.bind(this),\n\n        /** Overrides */\n        split_element_block_overrides: this.handleSplitBlock.bind(this),\n        insert_line_break_element_overrides: this.handleInsertLineBreak.bind(this),\n        delete_image_overrides: this.deleteImageLink.bind(this),\n        double_click_overrides: this.doubleClickLinkOverrides.bind(this),\n        triple_click_overrides: this.tripleClickButtonOverrides.bind(this),\n\n        /** Processors */\n        to_inline_code_processors: (node) => {\n            this.removeEmptyLinks(node);\n            for (const btn of selectElements(node, \"a.btn\")) {\n                // Remove all attributes from the button link except \"href\"\n                [...btn.attributes].forEach(\n                    (attr) => attr.name !== \"href\" && btn.removeAttribute(attr.name)\n                );\n            }\n        },\n    };\n\n    setup() {\n        this.initializePopovers();\n        this.currentOverlay = this.getActivePopover().overlay;\n        this.addDomListener(this.editable, \"click\", (ev) => {\n            const linkEl = ev.target.closest(\"a\");\n            if (linkEl) {\n                if (ev.ctrlKey || ev.metaKey) {\n                    window.open(linkEl.href, \"_blank\");\n                }\n                ev.preventDefault();\n            }\n        });\n        this.addDomListener(this.editable, \"mousedown\", () => {\n            this._isNavigatingByMouse = true;\n        });\n        this.addDomListener(this.editable, \"keydown\", () => {\n            delete this._isNavigatingByMouse;\n        });\n        this.addDomListener(this.editable, \"auxclick\", (ev) => {\n            if (ev.button === 1) {\n                const link = closestElement(ev.target, \"a\");\n                if (link?.href) {\n                    window.open(link.href, \"_blank\");\n                    ev.preventDefault();\n                }\n            }\n        });\n        // link creation is added to the command service because of a shortcut conflict,\n        // as ctrl+k is used for invoking the command palette\n        this.unregisterLinkCommandCallback = this.services.command?.add(\n            \"Create link\",\n            () => {\n                this.dependencies.selection.focusEditable();\n                // To avoid a race condition between the events spawn by :\n                // 1. the `focus editable` and\n                // 2. the odoo `Shortcut bar` closure\n                // Which can affect the link overlay opening sequence if we keep it in sync.\n                // Therefore we need to wait for the next tick before triggering openLinkTools.\n                setTimeout(() => this.openLinkTools());\n            },\n            {\n                hotkey: \"control+k\",\n                category: \"shortcut_conflict\",\n                isAvailable: () => {\n                    const selectionData = this.dependencies.selection.getSelectionData();\n                    return (\n                        selectionData.documentSelectionIsInEditable &&\n                        isHtmlContentSupported(selectionData.editableSelection)\n                    );\n                },\n            }\n        );\n\n        this.getExternalMetaData = memoize(fetchExternalMetaData);\n        this.getInternalMetaData = memoize(fetchInternalMetaData);\n        this.getAttachmentMetadata = memoize((url) =>\n            fetchAttachmentMetaData(url, this.services.orm)\n        );\n        this.LinkPopoverState = { editing: false };\n        this.newlyInsertedLinks = new Set();\n    }\n\n    destroy() {\n        this.unregisterLinkCommandCallback?.();\n    }\n\n    // -------------------------------------------------------------------------\n    // Commands\n    // -------------------------------------------------------------------------\n\n    /**\n     * @param {string} url\n     * @param {string} label\n     *\n     * @return {HTMLElement} link\n     */\n    createLink(url, label = \"\") {\n        const link = this.document.createElement(\"a\");\n        if (url !== undefined) {\n            link.setAttribute(\"href\", url);\n        }\n        for (const [param, value] of Object.entries(this.config.defaultLinkAttributes || {})) {\n            link.setAttribute(param, `${value}`);\n        }\n        link.innerText = label;\n        this.dispatchTo(\"create_link_handlers\", link);\n        return link;\n    }\n\n    /**\n     * @param {string} url\n     * @param {string} label\n     */\n    insertLink(url, label) {\n        const selection = this.dependencies.selection.getEditableSelection();\n        let link = closestElement(selection.anchorNode, \"a\");\n        if (link) {\n            link.setAttribute(\"href\", url);\n            link.innerText = label;\n        } else {\n            link = this.createLink(url, label);\n            this.dependencies.dom.insert(link);\n        }\n        this.dependencies.history.addStep();\n        const linkParent = link.parentElement;\n        const linkOffset = Array.from(linkParent.childNodes).indexOf(link);\n        this.dependencies.selection.setSelection(\n            { anchorNode: linkParent, anchorOffset: linkOffset + 1 },\n            { normalize: false }\n        );\n    }\n\n    /**\n     * @param {string} text\n     * @param {string} url\n     */\n    getPathAsUrlCommand(text, url) {\n        const pasteAsURLCommand = {\n            title: _t(\"Paste as URL\"),\n            description: _t(\"Create an URL.\"),\n            icon: \"fa-link\",\n            run: () => {\n                this.dependencies.dom.insert(this.createLink(url, text));\n                this.dependencies.history.addStep();\n            },\n        };\n        return pasteAsURLCommand;\n    }\n\n    isLinkAllowedOnSelection() {\n        if (this.getResource(\"link_compatible_selection_predicates\").some((p) => p())) {\n            return true;\n        }\n        const targetedNodes = this.dependencies.selection.getTargetedNodes();\n        const targetedBlocks = targetedNodes.filter(isBlock);\n        const linksInSelection = targetedNodes.filter((n) => n.tagName === \"A\");\n        return (\n            linksInSelection.length < 2 &&\n            // Prevent a link across sibling blocks:\n            targetedBlocks.every((node) =>\n                targetedNodes.every((other) => node.contains(other) || other.contains(node))\n            )\n        );\n    }\n\n    /**\n     * open the Link popover to edit links\n     *\n     * @param {HTMLElement} [linkElement]\n     */\n    openLinkTools(linkElement, type) {\n        this.currentOverlay.close();\n        this.LinkPopoverState.editing = false;\n        if (!this.isLinkAllowedOnSelection()) {\n            return this.services.notification.add(\n                _t(\"Unable to create a link on the current selection.\"),\n                { type: \"danger\" }\n            );\n        }\n        let selection = this.dependencies.selection.getEditableSelection();\n        let cursorsToRestore = this.dependencies.selection.preserveSelection();\n        const commonAncestor = closestElement(selection.commonAncestorContainer);\n        linkElement = linkElement || findInSelection(selection, \"a\");\n        this.type = type;\n        if (\n            linkElement &&\n            (!linkElement.contains(selection.anchorNode) ||\n                !linkElement.contains(selection.focusNode))\n        ) {\n            this.extendLinkToSelection(linkElement, selection);\n            linkElement = findInSelection(selection, \"a\");\n            this.dependencies.history.addStep();\n            cursorsToRestore = this.dependencies.selection.preserveSelection();\n        }\n        this.linkInDocument = linkElement;\n        if (!linkElement) {\n            // create a new link element\n            linkElement = this.createLink(undefined, selection.textContent());\n        }\n\n        const selectionTextContent = selection?.textContent();\n        const isImage = !!findInSelection(selection, \"img\");\n\n        const applyCallback = (\n            url,\n            label,\n            classes,\n            customStyle,\n            linkTarget,\n            attachmentId,\n            relValue\n        ) => {\n            if (this.linkInDocument) {\n                if (url) {\n                    this.linkInDocument.href = url;\n                } else {\n                    this.linkInDocument.removeAttribute(\"href\");\n                }\n                if (relValue) {\n                    this.linkInDocument.setAttribute(\"rel\", relValue);\n                } else {\n                    this.linkInDocument.removeAttribute(\"rel\");\n                }\n                if (linkTarget) {\n                    this.linkInDocument.setAttribute(\"target\", linkTarget);\n                } else {\n                    this.linkInDocument.removeAttribute(\"target\");\n                }\n                if (!isImage) {\n                    if (classes) {\n                        this.linkInDocument.className = classes;\n                    } else {\n                        this.linkInDocument.removeAttribute(\"class\");\n                    }\n                    if (customStyle) {\n                        this.linkInDocument.setAttribute(\"style\", customStyle);\n                    } else {\n                        this.linkInDocument.removeAttribute(\"style\");\n                    }\n                    if (\n                        this.linkInDocument.childElementCount == 0 &&\n                        cleanZWChars(this.linkInDocument.innerText) !== label\n                    ) {\n                        this.linkInDocument.innerText = label;\n                        cursorsToRestore = null;\n                    }\n                }\n            } else if (url) {\n                // prevent the link creation if the url field was empty\n\n                // create a new link with current selection as a content\n                if ((selectionTextContent && selectionTextContent === label) || isImage) {\n                    const link = this.createLink(url);\n                    if (relValue) {\n                        link.setAttribute(\"rel\", relValue);\n                    }\n                    const image = isImage && findInSelection(selection, \"img\");\n                    const figure =\n                        image?.parentElement?.matches(\"figure[contenteditable=false]\") &&\n                        image.parentElement;\n                    if (figure) {\n                        figure.before(link);\n                        link.append(figure);\n                        if (link.parentElement === this.editable) {\n                            const baseContainer =\n                                this.dependencies.baseContainer.createBaseContainer();\n                            link.before(baseContainer);\n                            baseContainer.append(link);\n                        }\n                    } else {\n                        const content = this.dependencies.selection.extractContent(selection);\n                        link.append(content);\n                        link.normalize();\n                        cursorsToRestore = null;\n                        selection = this.dependencies.selection.getEditableSelection();\n                        const anchorClosestElement = closestElement(selection.anchorNode);\n                        if (commonAncestor !== anchorClosestElement) {\n                            // We force the cursor after the anchorClosestElement\n                            // To be sure the link is inserted in the correct place in the dom.\n                            const [anchorNode, anchorOffset] = rightPos(anchorClosestElement);\n                            this.dependencies.selection.setSelection(\n                                { anchorNode, anchorOffset },\n                                { normalize: false }\n                            );\n                        }\n                        this.dependencies.dom.insert(link);\n                    }\n                    this.linkInDocument = link;\n                } else if (label) {\n                    const link = this.createLink(url, label);\n                    if (classes) {\n                        link.className = classes;\n                    }\n                    if (customStyle) {\n                        link.setAttribute(\"style\", customStyle);\n                    }\n                    if (linkTarget) {\n                        link.setAttribute(\"target\", linkTarget);\n                    }\n                    this.linkInDocument = link;\n                    cursorsToRestore = null;\n                    this.dependencies.dom.insert(link);\n                }\n            }\n            if (attachmentId) {\n                this.linkInDocument.dataset.attachmentId = attachmentId;\n            }\n        };\n\n        this.restoreSavePoint = this.dependencies.history.makeSavePoint();\n        const props = {\n            document: this.document,\n            linkElement,\n            isImage: isImage,\n            onApply: (...args) => {\n                delete this._isNavigatingByMouse;\n                applyCallback(...args);\n                this.closeLinkTools(cursorsToRestore);\n                this.dependencies.selection.focusEditable();\n                this.dependencies.history.addStep();\n            },\n            onChange: applyCallback,\n            onDiscard: () => {\n                this.restoreSavePoint();\n                if (linkElement.isConnected) {\n                    this.openLinkTools(linkElement);\n                } else {\n                    this.linkInDocument = null;\n                    this.currentOverlay.close();\n                }\n                this.dependencies.selection.focusEditable();\n            },\n            onRemove: () => {\n                this.removeLinkInDocument();\n                this.linkInDocument = null;\n                this.currentOverlay.close();\n            },\n            onCopy: () => {\n                this.linkInDocument = null;\n                this.currentOverlay.close();\n            },\n            onEdit: () => {\n                this.restoreSavePoint = this.dependencies.history.makeSavePoint();\n            },\n            getInternalMetaData: this.getInternalMetaData,\n            getExternalMetaData: this.getExternalMetaData,\n            getAttachmentMetadata: this.getAttachmentMetadata,\n            recordInfo: this.config.getRecordInfo?.() || {},\n            canEdit:\n                !this.linkInDocument || !this.linkInDocument.classList.contains(\"o_link_readonly\"),\n            canRemove:\n                this.linkInDocument &&\n                this.linkInDocument.parentElement.isContentEditable &&\n                !this.isUnremovable(this.linkInDocument),\n            canUpload: this.config.allowFile,\n            onUpload: this.config.onAttachmentChange,\n            type: this.type || \"\",\n            LinkPopoverState: this.LinkPopoverState,\n            showReplaceTitleBanner: this.newlyInsertedLinks.has(linkElement),\n            allowCustomStyle: this.config.allowCustomStyle,\n            allowTargetBlank: this.config.allowTargetBlank,\n            allowStripDomain: this.config.allowStripDomain,\n        };\n\n        const popover = this.getActivePopover(linkElement);\n        if (popover) {\n            this.currentOverlay = popover.overlay;\n            if (!linkElement.href) {\n                this.LinkPopoverState.editing = true;\n            }\n            this.currentOverlay.open({ props: popover.getProps(props) });\n            if (this.linkInDocument) {\n                if (this.newlyInsertedLinks.has(this.linkInDocument)) {\n                    this.newlyInsertedLinks.delete(this.linkInDocument);\n                }\n            }\n        }\n    }\n\n    /**\n     * close the link tool\n     *\n     */\n    closeLinkTools(cursors = null) {\n        const link = this.linkInDocument;\n        this.linkInDocument = null;\n        // Some unit tests fail when this.overlay.isOpen but the DOM don't contain the linkPopover yet.\n        // Because of some kind of race condition between the hoot mock event and the owl renderer.\n        // This is why we check for the popover in the DOM.\n        if (this.currentOverlay.isOpen && document.querySelector(\".o-we-linkpopover\")) {\n            this.currentOverlay.close();\n            if (link && link.isConnected) {\n                this.dependencies.selection.setSelection({\n                    anchorNode: link,\n                    anchorOffset: 0,\n                    focusNode: link,\n                    focusOffset: nodeSize(link),\n                });\n                const saveCustomStyle = link.getAttribute(\"style\");\n                link.removeAttribute(\"style\");\n                this.dependencies.color.removeAllColor();\n                if (\n                    saveCustomStyle &&\n                    this.config.allowCustomStyle &&\n                    link.className.includes(\"custom\")\n                ) {\n                    link.setAttribute(\"style\", saveCustomStyle);\n                }\n                // Remove the current link (linkInDocument) if it has no content\n                if (cleanZWChars(link.textContent) === \"\" && !link.querySelector(\"img\")) {\n                    const [anchorNode, anchorOffset] = rightPos(link);\n                    // We force the cursor after the link before removing the link\n                    // to ensure we don't lose the selection position.\n                    this.dependencies.selection.setSelection(\n                        { anchorNode, anchorOffset },\n                        { normalize: false }\n                    );\n                    link.remove();\n                } else if (cursors) {\n                    cursors.restore();\n                } else {\n                    this.dependencies.selection.setCursorEnd(link);\n                }\n            }\n        }\n    }\n\n    normalizeLink(root) {\n        for (const anchorEl of selectElements(root, \"a\")) {\n            if (/btn(-[a-z0-9_-]*)custom/.test(anchorEl.className)) {\n                // if the link is a customized button, we don't want to change the color\n                continue;\n            }\n            const { color } = anchorEl.style;\n            const childNodes = [...anchorEl.childNodes];\n            // For each anchor element, if it has an inline color style,\n            // (converted from an external style), remove it from the anchor,\n            // create a font tag inside it, and move the color to the font tag.\n            // This ensures the color is applied to the font element instead of\n            // the anchor element itself.\n            if (color && childNodes.every((n) => !isBlock(n))) {\n                anchorEl.style.removeProperty(\"color\");\n                const font = selectElements(anchorEl, \"font\").next().value;\n                if (font && cleanZWChars(anchorEl.textContent) === font.textContent) {\n                    continue;\n                }\n                const newFont = this.document.createElement(\"font\");\n                newFont.append(...childNodes);\n                anchorEl.appendChild(newFont);\n                this.dependencies.color.colorElement(newFont, color, \"color\");\n            }\n\n            // When a link contains unsupported element (like an iframe or a link),\n            // we remove the link. Cases can happen when a image link is replaced\n            // by a document or a video\n            const hasUnsupportedMedia = anchorEl.querySelector(\"a, iframe\");\n            if (hasUnsupportedMedia) {\n                this.removeLinkInDocument(anchorEl);\n            }\n        }\n    }\n\n    handleSelectionChange(selectionData) {\n        const selection = selectionData.editableSelection;\n        if (\n            this._isNavigatingByMouse &&\n            selection.isCollapsed &&\n            selectionData.documentSelectionIsInEditable\n        ) {\n            delete this._isNavigatingByMouse;\n            const { startContainer, startOffset, endContainer, endOffset } = selection;\n            const linkElement = closestElement(startContainer, \"a\");\n            if (\n                linkElement &&\n                linkElement.textContent.startsWith(\"\\uFEFF\") &&\n                linkElement.textContent.endsWith(\"\\uFEFF\")\n            ) {\n                const linkDescendants = descendants(linkElement);\n\n                // Check if the cursor is positioned at the begining of link.\n                const isCursorAtStartOfLink = isZwnbsp(startContainer)\n                    ? linkDescendants.indexOf(startContainer) === 0\n                    : startContainer.nodeType === Node.TEXT_NODE &&\n                      linkDescendants.indexOf(startContainer) === 1 &&\n                      startOffset === 0;\n\n                // Check if the cursor is positioned at the end of link.\n                const isCursorAtEndOfLink = isZwnbsp(endContainer)\n                    ? linkDescendants.indexOf(endContainer) === linkDescendants.length - 1\n                    : endContainer.nodeType === Node.TEXT_NODE &&\n                      linkDescendants.indexOf(endContainer) === linkDescendants.length - 2 &&\n                      endOffset === nodeSize(endContainer);\n\n                // Handle selection movement.\n                if (isCursorAtStartOfLink || isCursorAtEndOfLink) {\n                    const [targetNode, targetOffset] = isCursorAtStartOfLink\n                        ? leftPos(linkElement)\n                        : rightPos(linkElement);\n                    this.dependencies.selection.setSelection({\n                        anchorNode: targetNode,\n                        anchorOffset: isCursorAtStartOfLink ? targetOffset - 1 : targetOffset + 1,\n                    });\n                    return;\n                }\n            }\n        }\n        if (!selectionData.currentSelectionIsInEditable) {\n            const popoverEl = document.querySelector(\".o-we-linkpopover\");\n            const anchorNode = document.getSelection()?.anchorNode;\n            if (\n                (popoverEl && !selectionData.documentSelection) ||\n                (anchorNode && isElement(anchorNode) && anchorNode.closest(\".o-we-linkpopover\"))\n            ) {\n                return;\n            }\n            this.linkInDocument = null;\n            this.closeLinkTools();\n        } else if (!selection.isCollapsed) {\n            // Open the link tool only if we have an image selected and the selection\n            // is fully contained in the image parent link.\n            const imageNode = findInSelection(selection, \"img\");\n            const parentElement = imageNode?.parentElement;\n            const linkContainingImage = imageNode && closestElement(imageNode, \"a\");\n            if (\n                linkContainingImage &&\n                this.isLinkAllowedOnSelection() &&\n                parentElement.contains(selection.anchorNode) &&\n                parentElement.contains(selection.focusNode)\n            ) {\n                this.openLinkTools(linkContainingImage);\n            } else {\n                this.linkInDocument = null;\n                this.closeLinkTools();\n            }\n        } else {\n            const closestLinkElement = closestElement(selection.anchorNode, \"A\");\n            const isLinkEditable = this.getResource(\"is_link_editable_predicates\").some((p) =>\n                p(closestLinkElement)\n            );\n            if (closestLinkElement && closestLinkElement.isContentEditable) {\n                if (closestLinkElement !== this.linkInDocument || !this.currentOverlay.isOpen) {\n                    this.openLinkTools(closestLinkElement);\n                }\n            } else if (isLinkEditable) {\n                this.openLinkTools(closestLinkElement);\n            } else {\n                this.linkInDocument = null;\n                this.closeLinkTools();\n            }\n        }\n    }\n\n    /**\n     * extend the given link element to include all content from the given selection\n     *\n     * @param {HTMLLinkElement} linkElement\n     * @return {boolean}\n     */\n    extendLinkToSelection(linkElement) {\n        this.dependencies.split.splitSelection();\n        const selectedNodes = this.dependencies.selection.getTargetedNodes();\n        let before = linkElement.previousSibling;\n        while (before !== null && selectedNodes.includes(before)) {\n            linkElement.insertBefore(before, linkElement.firstChild);\n            before = linkElement.previousSibling;\n        }\n        let after = linkElement.nextSibling;\n        while (after && selectedNodes.includes(after)) {\n            linkElement.appendChild(after);\n            after = linkElement.nextSibling;\n        }\n        this.dependencies.selection.setCursorEnd(linkElement);\n    }\n\n    isUnremovable(linkEl) {\n        return this.getResource(\"unremovable_node_predicates\").some((p) => p(linkEl));\n    }\n\n    /**\n     * Remove the link from the collapsed selection\n     */\n    removeLinkInDocument(link = this.linkInDocument) {\n        if (!link.parentElement.isContentEditable || this.isUnremovable(link)) {\n            return;\n        }\n        const cursors = this.dependencies.selection.preserveSelection();\n        if (link && link.isContentEditable && link.parentElement.isContentEditable) {\n            cursors.update(callbacksForCursorUpdate.unwrap(link));\n            unwrapContents(link);\n        }\n        cursors.restore();\n        this.linkInDocument = null;\n        this.dependencies.selection.focusEditable();\n        this.dependencies.history.addStep();\n    }\n\n    removeLinkFromSelectionIsDisabled(selection) {\n        for (const node of this.dependencies.selection.getTargetedNodes()) {\n            const linkEl = closestElement(node, \"a\");\n            if (linkEl && !this.isLinkImmutable(linkEl) && !this.isUnremovable(linkEl)) {\n                return false;\n            }\n        }\n        return true;\n    }\n    removeLinkFromSelection() {\n        const selection = this.dependencies.split.splitSelection();\n\n        // If not, unlink only the part(s) of the link(s) that are selected:\n        // `<a>a[b</a>c<a>d</a>e<a>f]g</a>` => `<a>a</a>[bcdef]<a>g</a>`.\n        let { anchorNode, focusNode, anchorOffset, focusOffset } = selection;\n        const direction = selection.direction;\n        // Split the links around the selection.\n        let [startLink, endLink] = [\n            closestElement(anchorNode, \"a\"),\n            closestElement(focusNode, \"a\"),\n        ];\n        let cursors;\n        if (startLink) {\n            // If a FEFF character is present as anchorNode or focusNode,\n            // restoring the selection later may throw an error. Therefore,\n            // FEFF characters should be cleaned before splitting the link.\n            cursors = this.dependencies.selection.preserveSelection();\n            this.dependencies.feff.removeFeffs(startLink, cursors);\n            cursors.restore();\n        }\n        if (endLink && startLink !== endLink) {\n            cursors = this.dependencies.selection.preserveSelection();\n            this.dependencies.feff.removeFeffs(endLink, cursors);\n            cursors.restore();\n        }\n        ({ anchorNode, focusNode, anchorOffset, focusOffset } =\n            this.dependencies.selection.getEditableSelection());\n        cursors = this.dependencies.selection.preserveSelection();\n        // to remove link from selected images\n        let targetedNodes = this.dependencies.selection.getTargetedNodes();\n        const selectedImageNodes = targetedNodes.filter((node) => node.tagName === \"IMG\");\n        if (selectedImageNodes.length && startLink && endLink && startLink === endLink) {\n            for (const imageNode of selectedImageNodes) {\n                let imageLink;\n                const figure = closestElement(imageNode, \"figure\");\n                if (direction === DIRECTIONS.RIGHT) {\n                    imageLink = this.dependencies.split.splitAroundUntil(\n                        figure || imageNode,\n                        endLink\n                    );\n                } else {\n                    imageLink = this.dependencies.split.splitAroundUntil(\n                        figure || imageNode,\n                        startLink\n                    );\n                }\n                cursors.update(callbacksForCursorUpdate.unwrap(imageLink));\n                unwrapContents(imageLink);\n                if (figure && figure.parentElement !== this.editable) {\n                    // Remove the base container parent if there is one. Figure\n                    // is a block so it's not needed.\n                    unwrapContents(figure.parentElement);\n                }\n                // update the links at the selection\n                [startLink, endLink] = [\n                    closestElement(anchorNode, \"a\"),\n                    closestElement(focusNode, \"a\"),\n                ];\n            }\n            cursors.restore();\n            // when only unlink an inline image, add step after the unwrapping\n            if (\n                selectedImageNodes.length === 1 &&\n                selectedImageNodes.length === targetedNodes.length\n            ) {\n                this.dependencies.history.addStep();\n                return;\n            }\n        }\n        const startBlock = closestBlock(startLink);\n        const endBlock = closestBlock(endLink);\n        if (\n            startLink &&\n            startLink.isConnected &&\n            startLink.parentElement.isContentEditable &&\n            !this.isUnremovable(startLink)\n        ) {\n            anchorNode = this.dependencies.split.splitAroundUntil(anchorNode, startLink);\n            anchorOffset = direction === DIRECTIONS.RIGHT ? 0 : nodeSize(anchorNode);\n            this.dependencies.selection.setSelection(\n                { anchorNode, anchorOffset, focusNode, focusOffset },\n                { normalize: true }\n            );\n        }\n        // Only split the end link if it was not already done above.\n        if (\n            endLink &&\n            endLink.isConnected &&\n            endLink.parentElement.isContentEditable &&\n            !this.isUnremovable(endLink)\n        ) {\n            focusNode = this.dependencies.split.splitAroundUntil(focusNode, endLink);\n            focusOffset = direction === DIRECTIONS.RIGHT ? nodeSize(focusNode) : 0;\n            this.dependencies.selection.setSelection(\n                { anchorNode, anchorOffset, focusNode, focusOffset },\n                { normalize: true }\n            );\n        }\n        targetedNodes = this.dependencies.selection.getTargetedNodes();\n        const links = new Set(\n            targetedNodes\n                .map((node) => closestElement(node, \"a\"))\n                .filter(\n                    (a) =>\n                        a &&\n                        a.isContentEditable &&\n                        a.parentElement.isContentEditable &&\n                        !this.isUnremovable(a)\n                )\n        );\n        if (links.size) {\n            for (const link of links) {\n                cursors.update(callbacksForCursorUpdate.unwrap(link));\n                unwrapContents(link);\n            }\n            cursors.restore();\n        }\n        if (startBlock) {\n            // Remove empty links splitted by `splitAroundUntil` due to\n            // adjacent invisible text nodes.\n            this.removeEmptyLinks(startBlock);\n        }\n        if (endBlock && endBlock !== startBlock) {\n            this.removeEmptyLinks(endBlock);\n        }\n        this.dependencies.history.addStep();\n    }\n\n    removeEmptyLinks(root) {\n        const remove = (node) => {\n            for (const child of node.childNodes) {\n                remove(child);\n            }\n            if (!this.isUnremovable(node)) {\n                node.before(...node.childNodes);\n                node.remove();\n            }\n        };\n        // @todo: preserve spaces\n        for (const link of root.querySelectorAll(\"a\")) {\n            if (\n                [...link.childNodes].some(isVisible) ||\n                !link.parentElement.isContentEditable ||\n                this.isUnremovable(link) ||\n                this.getResource(\"legit_empty_link_predicates\").some((p) => p(link))\n            ) {\n                continue;\n            }\n            remove(link);\n        }\n    }\n\n    updateCurrentLinkSyncState() {\n        const { anchorNode } = this.dependencies.selection.getEditableSelection();\n        const linkEl = closestElement(anchorNode, \"a\");\n        if (linkEl && linkEl.isContentEditable) {\n            const label = linkEl.innerText;\n            const url = deduceURLfromText(label, linkEl);\n            const href = linkEl.getAttribute(\"href\");\n            if (\n                url &&\n                (url === href || url + \"/\" === href || url === deduceURLfromText(href, linkEl))\n            ) {\n                this.isCurrentLinkInSync = true;\n            }\n        }\n    }\n\n    onBeforeInput(ev) {\n        if (ev.inputType === \"insertParagraph\" || ev.inputType === \"insertLineBreak\") {\n            const nodeForSelectionRestore = this.handleAutomaticLinkInsertion();\n            if (nodeForSelectionRestore) {\n                this.dependencies.selection.setCursorStart(nodeForSelectionRestore);\n                this.dependencies.history.addStep();\n            }\n        }\n        if (ev.inputType === \"insertText\" && ev.data === \" \") {\n            const nodeForSelectionRestore = this.handleAutomaticLinkInsertion();\n            if (nodeForSelectionRestore) {\n                // Since we manually insert a space here, we will be adding a history step\n                // after link creation with selection at the end of the link and another\n                // after inserting the space. So first undo will remove the space, and the\n                // second will undo the link creation.\n                this.dependencies.selection.setSelection({\n                    anchorNode: nodeForSelectionRestore,\n                    anchorOffset: 0,\n                });\n                this.dependencies.history.addStep();\n                nodeForSelectionRestore.textContent =\n                    \"\\u00A0\" + nodeForSelectionRestore.textContent;\n                this.dependencies.selection.setSelection({\n                    anchorNode: nodeForSelectionRestore,\n                    anchorOffset: 1,\n                });\n                this.dependencies.history.addStep();\n                ev.preventDefault();\n            }\n        }\n        this.updateCurrentLinkSyncState();\n    }\n\n    onInputDeleteNormalizeLink() {\n        const { anchorNode } = this.dependencies.selection.getEditableSelection();\n        const linkEl = closestElement(anchorNode, \"a\");\n        if (linkEl && linkEl.isContentEditable) {\n            const label = linkEl.innerText;\n            const url = deduceURLfromText(label, linkEl);\n            if (url && this.isCurrentLinkInSync) {\n                linkEl.setAttribute(\"href\", url);\n                this.isCurrentLinkInSync = false;\n                if (this.currentOverlay.isOpen) {\n                    this.currentOverlay.close();\n                }\n            }\n        }\n    }\n    onPasteNormalizeLink() {\n        this.updateCurrentLinkSyncState();\n        this.onInputDeleteNormalizeLink();\n    }\n\n    deleteImageLink(imageToDelete) {\n        if (\n            imageToDelete.parentElement.tagName === \"A\" &&\n            !this.isUnremovable(imageToDelete.parentElement) &&\n            imageToDelete.parentElement.parentElement.isContentEditable\n        ) {\n            // If the link is empty after removing the image, remove it.\n            const cursors = this.dependencies.selection.preserveSelection();\n            cursors.update(callbacksForCursorUpdate.remove(imageToDelete));\n            imageToDelete.remove();\n            this.closeLinkTools(cursors);\n            this.dependencies.history.addStep();\n            return true;\n        }\n        return false;\n    }\n\n    /**\n     * Inserts a link in the editor. Called after pressing space or (shif +) enter.\n     * Performs a regex check to determine if the url has correct syntax.\n     */\n    handleAutomaticLinkInsertion() {\n        let selection = this.dependencies.selection.getEditableSelection();\n        if (\n            isHtmlContentSupported(selection) &&\n            !closestElement(selection.anchorNode, \"a\") &&\n            selection.anchorNode.nodeType === Node.TEXT_NODE\n        ) {\n            // Merge adjacent text nodes.\n            selection.anchorNode.parentNode.normalize();\n            selection = this.dependencies.selection.getEditableSelection();\n            const textSliced = selection.anchorNode.textContent.slice(0, selection.anchorOffset);\n            const textNodeSplitted = textSliced.split(/\\s/);\n            const potentialUrl = textNodeSplitted.pop();\n            // In case of multiple matches, only the last one will be converted.\n            const match = [...potentialUrl.matchAll(new RegExp(URL_REGEX, \"g\"))].pop();\n\n            if (match && !EMAIL_REGEX.test(match[0])) {\n                const nodeForSelectionRestore = selection.anchorNode.splitText(\n                    selection.anchorOffset\n                );\n                const url = match[2] ? match[0] : \"https://\" + match[0];\n                const startOffset = selection.anchorOffset - potentialUrl.length + match.index;\n                const text = selection.anchorNode.textContent.slice(\n                    startOffset,\n                    startOffset + match[0].length\n                );\n                const link = this.createLink(url, text);\n                // split the text node and replace the url text with the link\n                const textNodeToReplace = selection.anchorNode.splitText(startOffset);\n                textNodeToReplace.splitText(match[0].length);\n                selection.anchorNode.parentElement.replaceChild(link, textNodeToReplace);\n                if (link.getAttribute(\"href\") === link.textContent) {\n                    this.newlyInsertedLinks.add(link);\n                }\n                return nodeForSelectionRestore;\n            }\n        }\n    }\n\n    /**\n     * Special behavior for links: do not break the link at its edges, but\n     * rather before/after it.\n     *\n     * @param {Object} params\n     * @param {Element} params.targetNode\n     * @param {number} params.targetOffset\n     * @param {Element} params.blockToSplit\n     */\n    handleSplitBlock(params) {\n        return this.handleEnterAtEdgeOfLink(params, this.dependencies.split.splitElementBlock);\n    }\n\n    /**\n     * Special behavior for links: do not add a line break at its edges, but\n     * rather outside it.\n     *\n     * @param {Object} params\n     * @param {Element} params.targetNode\n     * @param {number} params.targetOffset\n     */\n    handleInsertLineBreak(params) {\n        return this.handleEnterAtEdgeOfLink(\n            params,\n            this.dependencies.lineBreak.insertLineBreakElement\n        );\n    }\n\n    /**\n     * @param {Object} params\n     * @param {Element} params.targetNode\n     * @param {number} params.targetOffset\n     * @param {Element} [params.blockToSplit]\n     * @param {Function} splitOrLineBreakCallback\n     */\n    handleEnterAtEdgeOfLink(params, splitOrLineBreakCallback) {\n        // @todo: handle target Node being a descendent of a link (iterate over\n        // leaves inside the link, rather than childNodes)\n        let { targetNode, targetOffset, blockToSplit } = params;\n        if (targetNode.tagName !== \"A\") {\n            return;\n        }\n        const edge = isPositionAtEdgeofLink(targetNode, targetOffset);\n        if (!edge) {\n            return;\n        }\n        [targetNode, targetOffset] = edge === \"start\" ? leftPos(targetNode) : rightPos(targetNode);\n        blockToSplit = targetNode;\n        splitOrLineBreakCallback({ ...params, targetNode, targetOffset, blockToSplit });\n        return true;\n    }\n\n    handleAfterInsert(insertedNodes) {\n        for (const node of insertedNodes) {\n            if (node.nodeType === Node.ELEMENT_NODE) {\n                for (const link of selectElements(node, \"A\")) {\n                    if (link.getAttribute(\"href\") === link.textContent && !this.isImage) {\n                        this.newlyInsertedLinks.add(link);\n                    }\n                }\n            }\n        }\n    }\n\n    initializePopovers() {\n        this.overlays = [];\n        this.getResource(\"link_popovers\").map((link_popover) => {\n            this.overlays.push({\n                overlay: this.dependencies.overlay.createOverlay(\n                    link_popover.PopoverClass,\n                    {\n                        closeOnPointerdown: true,\n                    },\n                    {\n                        sequence: 50,\n                    }\n                ),\n                isAvailable: link_popover.isAvailable,\n                getProps: link_popover.getProps,\n            });\n        });\n    }\n\n    getActivePopover(linkElement) {\n        return this.overlays.find((overlay) => overlay.isAvailable(linkElement));\n    }\n\n    isLinkImmutable(linkEl) {\n        return this.getResource(\"immutable_link_selectors\").some((s) => linkEl.matches(s));\n    }\n\n    doubleClickLinkOverrides(ev) {\n        const clickedLink = closestElement(ev.target, \"a\");\n        // If we double click on a link, limit the selection inside the link\n        if (clickedLink) {\n            // mimic the double click behavior of browsers\n            this.dependencies.selection.modifySelection(\"extend\", \"backward\", \"word\");\n            this.document.getSelection().collapseToStart();\n            this.dependencies.selection.modifySelection(\"extend\", \"forward\", \"word\");\n\n            const { anchorNode, focusNode, anchorOffset, focusOffset } =\n                this.dependencies.selection.getEditableSelection();\n\n            // We reset the word selection of double click to be inside the current clicked link\n            // when it spreads over different links. Because it's a word selection, we need to keep\n            // the correct offsets when resetting.\n            if (clickedLink.contains(anchorNode) && !clickedLink.contains(focusNode)) {\n                this.dependencies.selection.setSelection({\n                    anchorNode,\n                    anchorOffset,\n                    focusNode: clickedLink,\n                    focusOffset: nodeSize(clickedLink) - 1, // -1 to avoid the FEFF char\n                });\n            } else if (!clickedLink.contains(anchorNode) && clickedLink.contains(focusNode)) {\n                this.dependencies.selection.setSelection({\n                    anchorNode: clickedLink,\n                    anchorOffset: 1, // 1 to avoid the FEFF char\n                    focusNode,\n                    focusOffset,\n                });\n            } else if (!clickedLink.contains(anchorNode) && !clickedLink.contains(focusNode)) {\n                this.dependencies.selection.setSelection({\n                    anchorNode: clickedLink,\n                    anchorOffset: 1, // 1 to avoid the FEFF char\n                    focusNode: clickedLink,\n                    focusOffset: nodeSize(clickedLink) - 1, // -1 to avoid the FEFF char\n                });\n            } else {\n                this.dependencies.selection.setSelection({\n                    anchorNode,\n                    anchorOffset,\n                    focusNode,\n                    focusOffset,\n                });\n            }\n\n            return true;\n        }\n    }\n\n    tripleClickButtonOverrides(ev) {\n        const selection = this.dependencies.selection.getEditableSelection();\n        const buttonElement = isBrowserFirefox()\n            ? findInSelection(selection, \"a.btn\")\n            : closestElement(selection.anchorNode, \"a.btn\");\n        if (buttonElement) {\n            this.dependencies.selection.setSelection({\n                anchorNode: buttonElement,\n                anchorOffset: 0,\n                focusNode: buttonElement,\n                focusOffset: nodeSize(buttonElement),\n            });\n            ev.preventDefault();\n            return true;\n        }\n    }\n}\n", "import { session } from \"@web/session\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { Component, useState, useRef, useEffect, useExternalListener } from \"@odoo/owl\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { browser } from \"@web/core/browser/browser\";\nimport { cleanZWChars, deduceURLfromText } from \"./utils\";\nimport { useColorPicker } from \"@web/core/color_picker/color_picker\";\nimport { CheckBox } from \"@web/core/checkbox/checkbox\";\n\nconst DEFAULT_CUSTOM_TEXT_COLOR = \"#714B67\";\nconst DEFAULT_CUSTOM_FILL_COLOR = \"#ffffff\";\n\nconst isCSSVariable = (color) => color.match(/^o-color-\\d$|^\\d{3}$/);\nconst formatColor = (color) => {\n    if (color.match(/^o-color-\\d$/gm)) {\n        return `var(--hb-cp-${color})`;\n    }\n    if (color.match(/^\\d{3}$/gm)) {\n        return `var(--${color})`;\n    }\n    return color;\n};\n\nexport class LinkPopover extends Component {\n    static template = \"html_editor.linkPopover\";\n    static props = {\n        document: { validate: (p) => p.nodeType === Node.DOCUMENT_NODE },\n        linkElement: { validate: (el) => el.nodeType === Node.ELEMENT_NODE },\n        onApply: Function,\n        onChange: Function,\n        onDiscard: Function,\n        onRemove: Function,\n        onCopy: Function,\n        onEdit: Function,\n        getInternalMetaData: Function,\n        getExternalMetaData: Function,\n        getAttachmentMetadata: Function,\n        isImage: Boolean,\n        showReplaceTitleBanner: Boolean,\n        type: String,\n        LinkPopoverState: Object,\n        recordInfo: Object,\n        canEdit: { type: Boolean, optional: true },\n        canRemove: { type: Boolean, optional: true },\n        canUpload: { type: Boolean, optional: true },\n        onUpload: { type: Function, optional: true },\n        allowCustomStyle: { type: Boolean, optional: true },\n        allowTargetBlank: { type: Boolean, optional: true },\n        allowStripDomain: { type: Boolean, optional: true },\n        formatColor: { type: Function, optional: true },\n    };\n    static defaultProps = {\n        canEdit: true,\n        canRemove: true,\n        formatColor: formatColor,\n    };\n    static components = { CheckBox };\n    colorsData = [\n        { type: \"\", label: _t(\"Link\"), btnPreview: \"link\" },\n        { type: \"primary\", label: _t(\"Button Primary\"), btnPreview: \"primary\" },\n        { type: \"secondary\", label: _t(\"Button Secondary\"), btnPreview: \"secondary\" },\n        { type: \"custom\", label: _t(\"Custom\"), btnPreview: \"custom\" },\n        // Note: by compatibility the dialog should be able to remove old\n        // colors that were suggested like the BS status colors or the\n        // alpha -> epsilon classes. This is currently done by removing\n        // all btn-* classes anyway.\n    ];\n    buttonSizesData = [\n        { size: \"sm\", label: _t(\"Small\") },\n        { size: \"\", label: _t(\"Medium\") },\n        { size: \"lg\", label: _t(\"Large\") },\n    ];\n    borderData = [\n        { style: \"solid\", label: \"\u2501\u2501\u2501\" },\n        { style: \"dashed\", label: \"\u254c\u254c\u254c\" },\n        { style: \"dotted\", label: \"\u2504\u2504\u2504\" },\n        { style: \"double\", label: \"\u2550\u2550\u2550\" },\n    ];\n    buttonShapeData = [\n        { shape: \"\", label: \"Default\" },\n        { shape: \"rounded-circle\", label: \"Default + Rounded\" },\n        { shape: \"outline\", label: \"Outline\" },\n        { shape: \"outline rounded-circle\", label: \"Outline + Rounded\" },\n        { shape: \"fill\", label: \"Fill\" },\n        { shape: \"fill rounded-circle\", label: \"Fill + Rounded\" },\n        { shape: \"flat\", label: \"Flat\" },\n    ];\n    setup() {\n        this.ui = useService(\"ui\");\n        this.notificationService = useService(\"notification\");\n        this.uploadService = useService(\"uploadLocalFiles\");\n\n        const linkElement = this.props.linkElement;\n        const textContent = cleanZWChars(linkElement.textContent);\n        const labelEqualsUrl =\n            textContent === linkElement.getAttribute(\"href\") ||\n            textContent + \"/\" === linkElement.getAttribute(\"href\");\n\n        const computedStyle = this.props.document.defaultView.getComputedStyle(linkElement);\n        const currentRelValues = linkElement.rel.split(\" \");\n        this.state = useState({\n            editing: this.props.LinkPopoverState.editing,\n            // `.getAttribute(\"href\")` instead of `.href` to keep relative url\n            url: linkElement.getAttribute(\"href\") || this.deduceUrl(textContent),\n            label: labelEqualsUrl ? \"\" : textContent,\n            previewIcon: {\n                /** @type {'fa'|'imgSrc'|'mimetype'} */\n                type: \"fa\",\n                value: \"fa-globe\",\n            },\n            urlTitle: \"\",\n            urlDescription: \"\",\n            linkPreviewName: \"\",\n            imgSrc: \"\",\n            type:\n                this.props.type ||\n                linkElement.className.match(/btn(-[a-z0-9_-]*)(primary|secondary|custom)/)?.pop() ||\n                \"\",\n            linkTarget: linkElement.target === \"_blank\" ? \"_blank\" : \"\",\n            directDownload: true,\n            isDocument: false,\n            buttonSize: linkElement.className.match(/btn-(sm|lg)/)?.[1] || \"\",\n            buttonShape: this.getButtonShape(),\n            customBorderSize: computedStyle.borderWidth.replace(\"px\", \"\") || \"0\",\n            customBorderStyle: computedStyle.borderStyle || \"solid\",\n            isImage: this.props.isImage,\n            showReplaceTitleBanner: this.props.showReplaceTitleBanner,\n            showLabel: !linkElement.childElementCount,\n            stripDomain: true,\n            showAdvancedOptions: false,\n            relAttributeOptions: {\n                nofollow: {\n                    label: \"nofollow\",\n                    description: _t(\"Tells search engines not to follow this link\"),\n                    isChecked: currentRelValues.includes(\"nofollow\"),\n                },\n                noreferrer: {\n                    label: \"noreferrer\",\n                    description: _t(\"Removes referrer information sent to the target site\"),\n                    isChecked: currentRelValues.includes(\"noreferrer\"),\n                },\n                sponsored: {\n                    label: \"sponsored\",\n                    description: _t(\"Indicates the link is sponsored or paid content\"),\n                    isChecked: currentRelValues.includes(\"sponsored\"),\n                },\n                noopener: {\n                    label: \"noopener\",\n                    description: _t(\n                        \"Prevents the new page from accessing the original window (security)\"\n                    ),\n                    isChecked: currentRelValues.includes(\"noopener\"),\n                },\n            },\n        });\n\n        const getTargetedElements = () => [this.props.linkElement];\n        this.customTextColorState = useState({\n            selectedColor: computedStyle.color || DEFAULT_CUSTOM_TEXT_COLOR,\n            defaultTab: \"solid\",\n            getTargetedElements,\n            mode: \"color\",\n        });\n        this.customTextResetPreviewColor = this.customTextColorState.selectedColor;\n        this.customFillColorState = useState({\n            selectedColor:\n                (computedStyle.backgroundImage === \"none\"\n                    ? undefined\n                    : computedStyle.backgroundImage) ||\n                computedStyle.backgroundColor ||\n                DEFAULT_CUSTOM_FILL_COLOR,\n            defaultTab: \"solid\",\n            getTargetedElements,\n            mode: \"background-color\",\n        });\n        this.customFillResetPreviewColor = this.customFillColorState.selectedColor;\n        this.customBorderColorState = useState({\n            selectedColor: computedStyle.borderColor || DEFAULT_CUSTOM_TEXT_COLOR,\n            defaultTab: \"solid\",\n            getTargetedElements,\n            mode: \"border-color\",\n        });\n        this.customBorderResetPreviewColor = this.customBorderColorState.selectedColor;\n\n        if (this.props.allowCustomStyle) {\n            const createCustomColorPicker = (refName, colorStateRef, resetValueRef) =>\n                useColorPicker(\n                    refName,\n                    {\n                        state: this[colorStateRef],\n                        enabledTabs:\n                            colorStateRef === \"customFillColorState\"\n                                ? [\"solid\", \"custom\", \"gradient\"]\n                                : [\"solid\", \"custom\"],\n                        getUsedCustomColors: () => [],\n                        colorPrefix: \"\",\n                        cssVarColorPrefix: \"hb-cp-\",\n                        applyColor: (colorValue) => {\n                            this[colorStateRef].selectedColor = colorValue;\n                            this[resetValueRef] = colorValue;\n                        },\n                        applyColorPreview: (colorValue) => {\n                            this[colorStateRef].selectedColor = colorValue;\n                            this.onChange();\n                        },\n                        applyColorResetPreview: () => {\n                            this[colorStateRef].selectedColor = this[resetValueRef];\n                            this.onChange();\n                        },\n                    },\n                    {\n                        env: this.__owl__.childEnv,\n                    }\n                );\n            this.customTextColorPicker = createCustomColorPicker(\n                \"customTextColorButton\",\n                \"customTextColorState\",\n                \"customTextResetPreviewColor\"\n            );\n            this.customFillColorPicker = createCustomColorPicker(\n                \"customFillColorButton\",\n                \"customFillColorState\",\n                \"customFillResetPreviewColor\"\n            );\n            this.customBorderColorPicker = createCustomColorPicker(\n                \"customBorderColorButton\",\n                \"customBorderColorState\",\n                \"customBorderResetPreviewColor\"\n            );\n        }\n        this.updateDocumentState();\n        this.editingWrapper = useRef(\"editing-wrapper\");\n        this.inputRef = useRef(\n            this.state.isImage || (this.state.label && !this.state.url) ? \"url\" : \"label\"\n        );\n        useEffect(\n            (el) => {\n                if (el) {\n                    el.focus();\n                }\n            },\n            () => [this.inputRef.el]\n        );\n        if (!this.state.editing) {\n            this.loadAsyncLinkPreview();\n        }\n        const onPointerDown = (ev) => {\n            if (this.state.isImage) {\n                return;\n            }\n            this.state.url ||= \"#\";\n            if (this.editingWrapper?.el && !this.editingWrapper.el.contains(ev.target)) {\n                this.onClickApply();\n            }\n        };\n        useExternalListener(this.props.document, \"pointerdown\", onPointerDown);\n        if (this.props.document !== document) {\n            // Listen to pointerdown outside the iframe\n            useExternalListener(document, \"pointerdown\", onPointerDown);\n        }\n    }\n\n    toggleAdvancedOptions() {\n        this.state.showAdvancedOptions = !this.state.showAdvancedOptions;\n    }\n\n    toggleRelAttr(attr) {\n        const option = this.state.relAttributeOptions[attr];\n        option.isChecked = !option.isChecked;\n    }\n\n    onChange() {\n        // Apply changes to update the link preview.\n        this.props.onChange(\n            this.state.url,\n            this.state.label,\n            this.classes,\n            this.customStyles,\n            this.state.linkTarget,\n            this.state.attachmentId\n        );\n        this.updateDocumentState();\n    }\n    onClickApply() {\n        const relOptions = this.state.relAttributeOptions;\n        const relValue = Object.keys(relOptions)\n            .filter((key) => relOptions[key].isChecked)\n            .join(\" \");\n        this.state.editing = false;\n        this.applyDeducedUrl();\n        this.props.onApply(\n            this.state.url,\n            this.state.label,\n            this.classes,\n            this.customStyles,\n            this.state.linkTarget,\n            this.state.attachmentId,\n            relValue\n        );\n    }\n    applyDeducedUrl() {\n        if (this.state.label === \"\") {\n            this.state.label = this.state.url;\n        }\n        const deducedUrl = this.deduceUrl(this.state.url);\n        this.state.url = deducedUrl\n            ? this.correctLink(deducedUrl)\n            : this.correctLink(this.state.url);\n        if (\n            this.props.allowStripDomain &&\n            this.state.stripDomain &&\n            this.isAbsoluteURLInCurrentDomain()\n        ) {\n            const urlObj = new URL(this.state.url, window.location.origin);\n            // Not necessarily equal to window.location.origin\n            // (see isAbsoluteURLInCurrentDomain)\n            this.state.url = this.state.url.replace(urlObj.origin, \"\");\n        }\n    }\n    onClickEdit() {\n        this.state.editing = true;\n        this.props.onEdit();\n        this.updateUrlAndLabel();\n    }\n    updateUrlAndLabel() {\n        this.state.url = this.props.linkElement.getAttribute(\"href\");\n\n        const textContent = cleanZWChars(this.props.linkElement.textContent);\n        const labelEqualsUrl =\n            textContent === this.props.linkElement.getAttribute(\"href\") ||\n            textContent + \"/\" === this.props.linkElement.getAttribute(\"href\");\n        this.state.label = labelEqualsUrl ? \"\" : textContent;\n    }\n    // TODO: remove in master\n    async onClickCopy(ev) {\n        ev.preventDefault();\n        await browser.navigator.clipboard.writeText(this.props.linkElement.href || \"\");\n        this.notificationService.add(_t(\"Link copied to clipboard.\"), {\n            type: \"success\",\n        });\n        this.props.onCopy();\n    }\n    onClickRemove() {\n        this.props.onRemove();\n    }\n\n    onKeydownEnter(ev) {\n        const isAutoCompleteDropdownOpen = document.querySelector(\".o-autocomplete--dropdown-menu\");\n        if (ev.key === \"Enter\" && !isAutoCompleteDropdownOpen && this.state.url) {\n            ev.preventDefault();\n            this.onClickApply();\n        }\n    }\n\n    onKeydown(ev) {\n        if (ev.key === \"Escape\") {\n            ev.preventDefault();\n            ev.stopImmediatePropagation();\n            this.onClickApply();\n        } else if (ev.key == \"Tab\") {\n            ev.preventDefault();\n            const focusableElements = [\n                ...this.editingWrapper.el.querySelectorAll(\"input, select, button:not([disabled])\"),\n            ];\n            const currentIndex = focusableElements.indexOf(document.activeElement);\n            const nextIndex =\n                (currentIndex + (ev.shiftKey ? -1 : 1) + focusableElements.length) %\n                focusableElements.length;\n            focusableElements[nextIndex].focus();\n        }\n    }\n\n    onInput() {\n        this.onChange();\n    }\n\n    onClickReplaceTitle() {\n        this.state.label = this.state.urlTitle;\n        this.onClickApply();\n    }\n\n    onClickDirectDownload(checked) {\n        this.state.directDownload = checked;\n        this.state.url = this.state.url.replace(\"&download=true\", \"\");\n        if (this.state.directDownload) {\n            this.state.url += \"&download=true\";\n        }\n    }\n\n    onClickNewWindow(checked) {\n        this.state.linkTarget = checked ? \"_blank\" : \"\";\n        if (!checked) {\n            this.state.relAttributeOptions.noopener.isChecked = false;\n        }\n    }\n\n    onClickStripDomain(checked) {\n        this.state.stripDomain = checked;\n    }\n\n    /**\n     * @private\n     */\n    async updateDocumentState() {\n        const url = this.state.url;\n        const urlObject = URL.parse(url, this.props.document.URL);\n        if (\n            url &&\n            (url.startsWith(\"/web/content/\") ||\n                (urlObject &&\n                    urlObject.pathname.startsWith(\"/web/content\") &&\n                    urlObject.host === document.location.host))\n        ) {\n            const { type } = await this.props.getAttachmentMetadata(url);\n            this.state.isDocument = type !== \"url\";\n            this.state.directDownload = url.includes(\"&download=true\");\n        } else {\n            this.state.isDocument = false;\n            this.state.directDownload = true;\n        }\n    }\n    correctLink(url) {\n        if (\n            url &&\n            !url.startsWith(\"tel:\") &&\n            !url.startsWith(\"mailto:\") &&\n            !url.includes(\"://\") &&\n            !url.startsWith(\"/\") &&\n            !url.startsWith(\"#\") &&\n            !url.startsWith(\"${\")\n        ) {\n            url = \"https://\" + url;\n        }\n        if (url && (url.startsWith(\"http:\") || url.startsWith(\"https:\"))) {\n            url = URL.parse(url) ? url : \"\";\n        }\n        return url;\n    }\n    deduceUrl(text) {\n        text = text.trim();\n        if (/^(https?:|mailto:|tel:)/.test(text)) {\n            // Text begins with a known protocol, accept it as valid URL.\n            return text;\n        } else {\n            return deduceURLfromText(text, this.props.linkElement) || \"\";\n        }\n    }\n    getButtonShape() {\n        const shapeToRegex = (shape) => {\n            const parts = shape.trim().split(/\\s+/);\n            const regexParts = parts.map((cls) => {\n                if ([\"outline\", \"fill\"].includes(cls)) {\n                    cls = `btn-${cls}`;\n                }\n                return `(?=.*\\\\b${cls}\\\\b)`;\n            });\n            return { regex: new RegExp(regexParts.join(\"\")), nbParts: parts.length };\n        };\n        // If multiple shapes match, prefer the one with more specificity.\n        let shapeMatched = \"\";\n        let matchScore = 0;\n        for (const { shape } of this.buttonShapeData) {\n            if (!shape) {\n                continue;\n            }\n            const { regex, nbParts } = shapeToRegex(shape);\n            if (regex.test(this.props.linkElement.className)) {\n                if (matchScore < nbParts) {\n                    matchScore = nbParts;\n                    shapeMatched = shape;\n                }\n            }\n        }\n        return shapeMatched;\n    }\n    /**\n     * link preview in the popover\n     */\n    resetPreview() {\n        this.state.previewIcon = { type: \"fa\", value: \"fa-globe\" };\n        this.state.urlTitle = this.state.url || _t(\"No URL specified\");\n        this.state.urlDescription = \"\";\n        this.state.linkPreviewName = \"\";\n    }\n    async loadAsyncLinkPreview() {\n        let url;\n        if (this.state.url === \"\") {\n            this.resetPreview();\n            this.state.previewIcon.value = \"fa-question-circle-o\";\n            return;\n        }\n        if (this.isLogoutUrl()) {\n            // The session ends if we fetch this url, so the preview is hardcoded\n            this.resetPreview();\n            this.state.urlTitle = _t(\"Logout\");\n            this.state.previewIcon.value = \"fa-sign-out\";\n            return;\n        }\n        if (this.isAttachmentUrl()) {\n            const { name, mimetype } = await this.props.getAttachmentMetadata(this.state.url);\n            this.resetPreview();\n            this.state.urlTitle = name;\n            this.state.previewIcon = { type: \"mimetype\", value: mimetype };\n            return;\n        }\n        try {\n            url = new URL(this.state.url, this.props.document.URL); // relative to absolute\n        } catch {\n            // Invalid URL, might happen with editor unsuported protocol. eg type\n            // `geo:37.786971,-122.399677`, become `http://geo:37.786971,-122.399677`\n            this.notificationService.add(_t(\"This URL is invalid. Preview couldn't be updated.\"), {\n                type: \"danger\",\n            });\n            return;\n        }\n        this.resetPreview();\n        const protocol = url.protocol;\n        if (!protocol.startsWith(\"http\")) {\n            const faMap = { \"mailto:\": \"fa-envelope-o\", \"tel:\": \"fa-phone\" };\n            const icon = faMap[protocol];\n            if (icon) {\n                this.state.previewIcon.value = icon;\n            }\n        } else if (\n            window.location.hostname !== url.hostname &&\n            !new RegExp(`^https?://${session.db}\\\\.odoo\\\\.com(/.*)?$`).test(url.origin)\n        ) {\n            // Preview pages from current website only. External website will\n            // most of the time raise a CORS error. To avoid that error, we\n            // would need to fetch the page through the server (s2s), involving\n            // enduser fetching problematic pages such as illicit content.\n            this.state.previewIcon = {\n                type: \"imgSrc\",\n                value: `https://www.google.com/s2/favicons?sz=16&domain=${encodeURIComponent(url)}`,\n            };\n\n            const externalMetadata = await this.props.getExternalMetaData(this.state.url);\n\n            this.state.urlTitle = externalMetadata?.og_title || this.state.url;\n            this.state.urlDescription = externalMetadata?.og_description || \"\";\n            this.state.imgSrc = externalMetadata?.og_image || \"\";\n            if (\n                externalMetadata?.og_image &&\n                this.state.label &&\n                this.state.urlTitle === this.state.url\n            ) {\n                this.state.urlTitle = this.state.label;\n            }\n        } else {\n            // Set state based on cached link meta data\n            // for record missing errors, we push a warning that the url is likely invalid\n            // for other errors, we log them to not block the ui\n            const internalMetadata = await this.props\n                .getInternalMetaData(url.href)\n                .catch((error) => {\n                    console.warn(`Error fetching internal metadata for ${url.href}:`, error);\n                    return {};\n                });\n            if (internalMetadata.favicon) {\n                this.state.previewIcon = {\n                    type: \"imgSrc\",\n                    value: internalMetadata.favicon.href,\n                };\n            }\n            if (internalMetadata.error_msg) {\n                this.notificationService.add(internalMetadata.error_msg, {\n                    type: \"warning\",\n                });\n            } else if (internalMetadata.other_error_msg) {\n                console.error(\n                    \"Internal meta data retrieve error for link preview: \" +\n                        internalMetadata.other_error_msg\n                );\n            } else {\n                this.state.linkPreviewName =\n                    internalMetadata.link_preview_name ||\n                    internalMetadata.display_name ||\n                    internalMetadata.name;\n                this.state.urlDescription = internalMetadata?.description || \"\";\n                this.state.urlTitle = this.state.linkPreviewName\n                    ? this.state.linkPreviewName\n                    : this.state.url;\n            }\n\n            if (\n                (internalMetadata.ogTitle || internalMetadata.title) &&\n                !this.state.linkPreviewName\n            ) {\n                this.state.urlTitle = internalMetadata.ogTitle\n                    ? internalMetadata.ogTitle.getAttribute(\"content\")\n                    : internalMetadata.title.text.trim();\n            }\n        }\n    }\n\n    get classes() {\n        const classes = [...this.props.linkElement.classList].filter(\n            (value) => !value.match(/^(btn.*|rounded-circle|flat|(text|bg)-(o-color-\\d$|\\d{3}$))$/)\n        );\n\n        let stylePrefix = \"\";\n        if (this.state.type) {\n            if (this.state.buttonSize) {\n                classes.push(`btn-${this.state.buttonSize}`);\n            }\n\n            if (this.state.buttonShape) {\n                const buttonShape = this.state.buttonShape.split(\" \");\n                if ([\"outline\", \"fill\"].includes(buttonShape[0])) {\n                    stylePrefix = `${buttonShape[0]}-`;\n                }\n                classes.push(buttonShape.slice(stylePrefix ? 1 : 0).join(\" \"));\n            }\n\n            classes.push(`btn`, `btn-${stylePrefix}${this.state.type}`);\n        }\n\n        const textColor = this.customTextColorState.selectedColor;\n        if (isCSSVariable(textColor)) {\n            classes.push(`text-${textColor}`);\n        }\n\n        const fillColor = this.customFillColorState.selectedColor;\n        if (isCSSVariable(fillColor)) {\n            classes.push(`bg-${fillColor}`);\n        }\n\n        // Ensure single space between classes\n        return classes.filter(Boolean).join(\" \");\n    }\n\n    get customStyles() {\n        if (!this.props.allowCustomStyle || this.state.type !== \"custom\") {\n            return false;\n        }\n        let customStyles = \"\";\n\n        const textColor = this.customTextColorState.selectedColor;\n        if (!isCSSVariable(textColor)) {\n            customStyles += `color: ${textColor}; `;\n        }\n\n        const fillColor = this.customFillColorState.selectedColor;\n        if (!isCSSVariable(fillColor)) {\n            const backgroundProperty = fillColor.includes(\"gradient\")\n                ? \"background-image\"\n                : \"background-color\";\n            customStyles += `${backgroundProperty}: ${fillColor}; `;\n        }\n\n        const borderColor = this.customBorderColorState.selectedColor;\n        customStyles += `border-width: ${this.state.customBorderSize}px; `;\n        customStyles += `border-color: ${formatColor(borderColor)}; `;\n        customStyles += `border-style: ${this.state.customBorderStyle}; `;\n\n        return customStyles;\n    }\n\n    async uploadFile() {\n        const { upload, getURL } = this.uploadService;\n        const { resModel, resId } = this.props.recordInfo;\n        const [attachment] = await upload({ resModel, resId, accessToken: true });\n        if (!attachment) {\n            // No file selected or upload failed\n            return;\n        }\n        this.props.onUpload?.(attachment);\n        this.state.url = getURL(attachment, { download: true, unique: true, accessToken: true });\n        this.state.label ||= attachment.name;\n        this.state.attachmentId = attachment.id;\n        this.onChange();\n    }\n\n    isLogoutUrl() {\n        return !!this.state.url.match(/\\/web\\/session\\/logout\\b/);\n    }\n    isAttachmentUrl() {\n        return !!this.state.url.match(/\\/web\\/content\\/\\d+/);\n    }\n    /**\n     * Checks if the given URL is using the domain where the content being\n     * edited is reachable, i.e. if this URL should be stripped of its domain\n     * part and converted to a relative URL if put as a link in the content.\n     *\n     * @private\n     * @returns {boolean}\n     */\n    isAbsoluteURLInCurrentDomain() {\n        // First check if it is a relative URL: if it is, we don't want to check\n        // further as we will always leave those untouched.\n        let hasProtocol;\n        try {\n            hasProtocol = !!new URL(this.state.url).protocol;\n        } catch {\n            hasProtocol = false;\n        }\n        if (!hasProtocol) {\n            return false;\n        }\n\n        const urlObj = new URL(this.state.url, window.location.origin);\n        // Chosen heuristic to detect someone trying to enter a link using\n        // its Odoo instance domain. We just suppose it should be a relative\n        // URL (if unexpected behavior, the user can just not enter its Odoo\n        // instance domain but its real domain, or opt-out from the domain\n        // stripping). Mentioning an .odoo.com domain, especially its own\n        // one, is always a bad practice anyway.\n        return (\n            urlObj.origin === window.location.origin ||\n            new RegExp(`^https?://${session.db}\\\\.odoo\\\\.com(/.*)?$`).test(urlObj.origin)\n        );\n    }\n}\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { isBlock } from \"@html_editor/utils/blocks\";\n\nexport class OdooLinkSelectionPlugin extends Plugin {\n    static id = \"odooLinkSelection\";\n    /** @type {import(\"plugins\").EditorResources} */\n    resources = {\n        ineligible_link_for_zwnbsp_predicates: [\n            (link) =>\n                [link, ...link.querySelectorAll(\"*\")].some(\n                    (el) => el.nodeName === \"IMG\" || isBlock(el)\n                ),\n            (link) => link.matches(\"a.nav-link\"),\n        ],\n        ineligible_link_for_selection_indication_predicates: (link) => link.matches(\".btn\"),\n    };\n}\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { closestElement, selectElements } from \"@html_editor/utils/dom_traversal\";\nimport { removeClass } from \"@html_editor/utils/dom\";\nimport { isProtected, isProtecting } from \"@html_editor/utils/dom_info\";\n\n/*\n    This plugin solves selection issues around links (allowing the cursor at the\n    inner and outer edges of links).\n\n    Every link receives 4 zero-width non-breaking spaces (unicode FEFF\n    characters, hereafter referred to as ZWNBSP):\n    - one before the link\n    - one as the link's first child\n    - one as the link's last child\n    - one after the link\n    like so: `//ZWNBSP//<a>//ZWNBSP//label//ZWNBSP//</a>//ZWNBSP`.\n\n    A visual indication ( `o_link_in_selection` class) is added to a link when\n    the selection is contained within it.\n\n    This is not applied in the following cases:\n\n    - in a navbar (since its links are managed via the snippets system, not\n    via pure edition) and, similarly, in .nav-link links\n    - in links that have content more complex than simple text\n    - on non-editable links or links that are not within the editable area\n */\n\n/**\n * @typedef { Object } LinkSelectionShared\n * @property { LinkSelectionPlugin['padLinkWithZwnbsp'] } padLinkWithZwnbsp\n */\n\n/**\n * @typedef {((link: HTMLLinkElement) => boolean)[]} ineligible_link_for_selection_indication_predicates\n * @typedef {((link: HTMLLinkElement) => boolean)[]} ineligible_link_for_zwnbsp_predicates\n */\n\nexport class LinkSelectionPlugin extends Plugin {\n    static id = \"linkSelection\";\n    static dependencies = [\"selection\", \"feff\"];\n    // TODO ABD: refactor to handle Knowledge comments inside this plugin without sharing padLinkWithZwnbsp.\n    static shared = [\"padLinkWithZwnbsp\"];\n    /** @type {import(\"plugins\").EditorResources} */\n    resources = {\n        /** Handlers */\n        selectionchange_handlers: this.resetLinkInSelection.bind(this),\n        clean_for_save_handlers: ({ root }) => this.clearLinkInSelectionClass(root),\n        normalize_handlers: () => this.resetLinkInSelection(),\n        feff_providers: this.addFeffsToLinks.bind(this),\n        system_classes: [\"o_link_in_selection\"],\n        selection_placeholder_container_predicates: (container) => {\n            if (container.nodeName === \"BUTTON\" || container.nodeName === \"A\") {\n                // We sometimes have buttons or links that are blocks with\n                // contenteditable=true but we never want to insert a paragraph\n                // in them.\n                // Note: this can be removed if `allowsParagraphRelatedElements`\n                // is adapted to return false in these cases.\n                return false;\n            }\n        },\n    };\n\n    addFeffsToLinks(root, cursors) {\n        return [...selectElements(root, \"a\")]\n            .filter(this.isLinkEligibleForZwnbsp.bind(this))\n            .flatMap((link) => this.dependencies.feff.surroundWithFeffs(link, cursors));\n    }\n\n    /**\n     * Take a link and pad it with non-break zero-width spaces to ensure that it\n     * is always possible to place the cursor at its inner and outer edges.\n     *\n     * @param {HTMLAnchorElement} link\n     */\n    padLinkWithZwnbsp(link) {\n        const cursors = this.dependencies.selection.preserveSelection();\n        this.dependencies.feff.surroundWithFeffs(link, cursors);\n        cursors.restore();\n    }\n\n    isLinkEligibleForZwnbsp(link) {\n        return (\n            link.isContentEditable &&\n            link.parentElement.isContentEditable &&\n            this.editable.contains(link) &&\n            !isProtected(link) &&\n            !isProtecting(link) &&\n            !this.getResource(\"ineligible_link_for_zwnbsp_predicates\").some((p) => p(link))\n        );\n    }\n\n    isLinkEligibleForVisualIndication(link) {\n        return (\n            this.isLinkEligibleForZwnbsp(link) &&\n            !this.getResource(\"ineligible_link_for_selection_indication_predicates\").some(\n                (predicate) => predicate(link)\n            )\n        );\n    }\n\n    /**\n     * Apply the o_link_in_selection class if the selection is in a single link,\n     * remove it otherwise.\n     *\n     * @param {SelectionData} [selectionData]\n     */\n    resetLinkInSelection(selectionData = this.dependencies.selection.getSelectionData()) {\n        this.clearLinkInSelectionClass(this.editable);\n\n        const { anchorNode, focusNode } = selectionData.editableSelection;\n        const [anchorLink, focusLink] = [anchorNode, focusNode].map((node) =>\n            closestElement(node, \"a\")\n        );\n        const singleLinkInSelection = anchorLink === focusLink && anchorLink;\n\n        if (\n            singleLinkInSelection &&\n            this.isLinkEligibleForVisualIndication(singleLinkInSelection)\n        ) {\n            singleLinkInSelection.classList.add(\"o_link_in_selection\");\n        }\n    }\n\n    clearLinkInSelectionClass(root) {\n        for (const link of selectElements(root, \".o_link_in_selection\")) {\n            removeClass(link, \"o_link_in_selection\");\n        }\n    }\n}\n", "import { Plugin } from \"@html_editor/plugin\";\n\n/**\n * @typedef {import(\"@html_editor/core/user_command_plugin\").UserCommand} UserCommand\n *\n * @typedef {((url: string) => UserCommand)[]} paste_media_url_command_providers\n */\n\nexport class MediaUrlPastePlugin extends Plugin {\n    static id = \"mediaUrlPaste\";\n    static dependencies = [\"link\", \"dom\", \"history\", \"powerbox\"];\n    /** @type {import(\"plugins\").EditorResources} */\n    resources = {\n        paste_url_overrides: this.openPowerboxOnUrlPaste.bind(this),\n    };\n\n    /**\n     * @param {string} text\n     * @param {string} url\n     */\n    openPowerboxOnUrlPaste(text, url) {\n        const commands = this.getResource(\"paste_media_url_command_providers\")\n            .map((provider) => provider(url))\n            .filter(Boolean);\n        if (commands.length) {\n            commands.push(this.dependencies.link.getPathAsUrlCommand(text, url));\n            const restoreSavepoint = this.dependencies.history.makeSavePoint();\n            // Open powerbox with commands to embed media or paste as link.\n            // Insert URL as text, revert it later if a command is triggered.\n            this.dependencies.dom.insert(text);\n            this.dependencies.history.addStep();\n            this.dependencies.powerbox.openPowerbox({ commands, onApplyCommand: restoreSavepoint });\n            return true;\n        }\n    }\n}\n", "/* eslint-disable */\n\nconst tldWhitelist = [\n    \"com\", \"net\", \"org\", \"ac\", \"ad\", \"ae\", \"af\", \"ag\", \"ai\", \"al\", \"am\", \"an\",\n    \"ao\", \"aq\", \"ar\", \"as\", \"at\", \"au\", \"aw\", \"ax\", \"az\", \"ba\", \"bb\", \"bd\",\n    \"be\", \"bf\", \"bg\", \"bh\", \"bi\", \"bj\", \"bl\", \"bm\", \"bn\", \"bo\", \"br\", \"bq\",\n    \"bs\", \"bt\", \"bv\", \"bw\", \"by\", \"bz\", \"ca\", \"cc\", \"cd\", \"cf\", \"cg\", \"ch\",\n    \"ci\", \"ck\", \"cl\", \"cm\", \"cn\", \"co\", \"cr\", \"cs\", \"cu\", \"cv\", \"cw\", \"cx\",\n    \"cy\", \"cz\", \"dd\", \"de\", \"dj\", \"dk\", \"dm\", \"do\", \"dz\", \"ec\", \"ee\", \"eg\",\n    \"eh\", \"er\", \"es\", \"et\", \"eu\", \"fi\", \"fj\", \"fk\", \"fm\", \"fo\", \"fr\", \"ga\",\n    \"gb\", \"gd\", \"ge\", \"gf\", \"gg\", \"gh\", \"gi\", \"gl\", \"gm\", \"gn\", \"gp\", \"gq\",\n    \"gr\", \"gs\", \"gt\", \"gu\", \"gw\", \"gy\", \"hk\", \"hm\", \"hn\", \"hr\", \"ht\", \"hu\",\n    \"id\", \"ie\", \"il\", \"im\", \"in\", \"io\", \"iq\", \"ir\", \"is\", \"it\", \"je\", \"jm\",\n    \"jo\", \"jp\", \"ke\", \"kg\", \"kh\", \"ki\", \"km\", \"kn\", \"kp\", \"kr\", \"kw\", \"ky\",\n    \"kz\", \"la\", \"lb\", \"lc\", \"li\", \"lk\", \"lr\", \"ls\", \"lt\", \"lu\", \"lv\", \"ly\",\n    \"ma\", \"mc\", \"md\", \"me\", \"mf\", \"mg\", \"mh\", \"mk\", \"ml\", \"mm\", \"mn\", \"mo\",\n    \"mp\", \"mq\", \"mr\", \"ms\", \"mt\", \"mu\", \"mv\", \"mw\", \"mx\", \"my\", \"mz\", \"na\",\n    \"nc\", \"ne\", \"nf\", \"ng\", \"ni\", \"nl\", \"no\", \"np\", \"nr\", \"nu\", \"nz\", \"om\",\n    \"pa\", \"pe\", \"pf\", \"pg\", \"ph\", \"pk\", \"pl\", \"pm\", \"pn\", \"pr\", \"ps\", \"pt\",\n    \"pw\", \"py\", \"qa\", \"re\", \"ro\", \"rs\", \"ru\", \"rw\", \"sa\", \"sb\", \"sc\", \"sd\",\n    \"se\", \"sg\", \"sh\", \"si\", \"sj\", \"sk\", \"sl\", \"sm\", \"sn\", \"so\", \"sr\", \"ss\",\n    \"st\", \"su\", \"sv\", \"sx\", \"sy\", \"sz\", \"tc\", \"td\", \"tf\", \"tg\", \"th\", \"tj\",\n    \"tk\", \"tl\", \"tm\", \"tn\", \"to\", \"tp\", \"tr\", \"tt\", \"tv\", \"tw\", \"tz\", \"ua\",\n    \"ug\", \"uk\", \"um\", \"us\", \"uy\", \"uz\", \"va\", \"vc\", \"ve\", \"vg\", \"vi\", \"vn\",\n    \"vu\", \"wf\", \"ws\", \"ye\", \"yt\", \"yu\", \"za\", \"zm\", \"zr\", \"zw\", \"co\\\\.uk\"];\n\nconst urlRegexBase = `|(?:www.))[-a-zA-Z0-9@:%._\\\\+~#=]{2,256}\\\\.[a-zA-Z][a-zA-Z0-9]{1,62}|(?:[-a-zA-Z0-9@:%._\\\\+~#=]{2,256}\\\\.(?:${tldWhitelist.join(\n    \"|\"\n)})\\\\b))(?:(?:[/?#])[^\\\\s]*[^!.,})\\\\]'\"\\\\s]|(?:[^!(){}.,[\\\\]'\"\\\\s]+))?`;\nconst httpCapturedRegex = `(https?:\\\\/\\\\/)`;\n\nexport const URL_REGEX = new RegExp(`((?:(?:${httpCapturedRegex}${urlRegexBase})`, \"i\");\nexport const EMAIL_REGEX = /^(mailto:)?[\\w-.]+@(?:[\\w-]+\\.)+[\\w-]{2,4}$/i;\nexport const PHONE_REGEX = /^(tel:(?:\\/\\/)?)?\\+?[\\d\\s.\\-()/]{3,25}$/;\n\nexport function cleanZWChars(text) {\n    return text.replace(/\\u200B|\\uFEFF/g, \"\");\n}\n\n/**\n * Returns a complete URL if text is a valid email address, http URL or telephone\n * number, null otherwise.\n * The optional link parameter is used to prevent protocol switching between\n * 'http' and 'https'.\n *\n * @param {String} text\n * @param {HTMLAnchorElement} [link]\n * @returns {String|null}\n */\nexport function deduceURLfromText(text, link) {\n    const label = cleanZWChars(text).trim();\n    // Check first for e-mail.\n    let match = label.match(EMAIL_REGEX);\n    if (match) {\n        return match[1] ? match[0] : \"mailto:\" + match[0];\n    }\n    // Check for http link.\n    match = label.match(URL_REGEX);\n    if (match && match[0] === label) {\n        const currentHttpProtocol = (link?.href.match(/^http(s)?:\\/\\//gi) || [])[0];\n        if (match[2]) {\n            return match[0];\n        } else if (currentHttpProtocol) {\n            // Avoid converting a http link to https.\n            return currentHttpProtocol + match[0];\n        } else {\n            return \"https://\" + match[0];\n        }\n    }\n    // Check for telephone url.\n    match = label.match(PHONE_REGEX);\n    if (match) {\n        return (match[1] ? match[0] : \"tel:\" + match[0]).replace(/\\s+/g, \"\");\n    }\n    return null;\n}\n\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { closestBlock, isBlock } from \"@html_editor/utils/blocks\";\nimport {\n    removeClass,\n    removeStyle,\n    toggleClass,\n    unwrapContents,\n    wrapInlinesInBlocks,\n} from \"@html_editor/utils/dom\";\nimport {\n    getDeepestPosition,\n    isElement,\n    isEmptyBlock,\n    isListElement,\n    isListItemElement,\n    isParagraphRelatedElement,\n    isProtected,\n    isProtecting,\n    isShrunkBlock,\n    isVisibleTextNode,\n    listElementSelector,\n} from \"@html_editor/utils/dom_info\";\nimport {\n    closestElement,\n    descendants,\n    getAdjacents,\n    selectElements,\n    ancestors,\n    childNodes,\n    firstLeaf,\n    lastLeaf,\n} from \"@html_editor/utils/dom_traversal\";\nimport { childNodeIndex, nodeSize } from \"@html_editor/utils/position\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { compareListTypes, createList, insertListAfter, isListItem } from \"./utils\";\nimport { callbacksForCursorUpdate } from \"@html_editor/utils/selection\";\nimport { withSequence } from \"@html_editor/utils/resource\";\nimport { FONT_SIZE_CLASSES, getFontSizeOrClass, getHtmlStyle } from \"@html_editor/utils/formatting\";\nimport { getTextColorOrClass, TEXT_CLASSES_REGEX } from \"@html_editor/utils/color\";\nimport { baseContainerGlobalSelector } from \"@html_editor/utils/base_container\";\nimport { ListSelector } from \"./list_selector\";\nimport { reactive } from \"@odoo/owl\";\nimport { composeToolbarButton } from \"../toolbar/toolbar\";\nimport { isHtmlContentSupported } from \"@html_editor/core/selection_plugin\";\nimport { pick } from \"@web/core/utils/objects\";\nimport { weakMemoize } from \"@html_editor/utils/functions\";\nimport { isColorGradient } from \"@web/core/utils/colors\";\n\nconst listSelectorItems = [\n    {\n        id: \"bulleted_list\",\n        commandId: \"toggleListUL\",\n        mode: \"UL\",\n    },\n    {\n        id: \"numbered_list\",\n        commandId: \"toggleListOL\",\n        mode: \"OL\",\n    },\n    {\n        id: \"checklist\",\n        commandId: \"toggleListCL\",\n        mode: \"CL\",\n    },\n];\n\nexport class ListPlugin extends Plugin {\n    static id = \"list\";\n    static dependencies = [\n        \"baseContainer\",\n        \"tabulation\",\n        \"history\",\n        \"input\",\n        \"split\",\n        \"selection\",\n        \"delete\",\n        \"dom\",\n        \"color\",\n    ];\n    static defaultConfig = {\n        allowChecklist: true,\n    };\n    toolbarListSelectorKey = reactive({ value: 0 });\n    /** @type {import(\"plugins\").EditorResources} */\n    resources = {\n        user_commands: [\n            {\n                id: \"toggleListUL\",\n                title: _t(\"Bulleted list\"),\n                description: _t(\"Create a simple bulleted list\"),\n                icon: \"fa-list-ul\",\n                run: () => this.toggleListCommand({ mode: \"UL\" }),\n                isAvailable: this.canToggleList.bind(this),\n            },\n            {\n                id: \"toggleListOL\",\n                title: _t(\"Numbered list\"),\n                description: _t(\"Create a list with numbering\"),\n                icon: \"fa-list-ol\",\n                run: ({ listStyle } = {}) => this.toggleListCommand({ mode: \"OL\", listStyle }),\n                isAvailable: this.canToggleList.bind(this),\n            },\n            {\n                id: \"toggleListCL\",\n                title: _t(\"Checklist\"),\n                description: _t(\"Track tasks with a checklist\"),\n                icon: \"fa-check-square-o\",\n                run: () => this.toggleListCommand({ mode: \"CL\" }),\n                isAvailable: (selection) =>\n                    this.config.allowChecklist && this.canToggleList(selection),\n            },\n        ],\n        shortcuts: [\n            { hotkey: \"control+shift+7\", commandId: \"toggleListOL\" },\n            { hotkey: \"control+shift+8\", commandId: \"toggleListUL\" },\n            { hotkey: \"control+shift+9\", commandId: \"toggleListCL\" },\n        ],\n        shorthands: [\n            {\n                pattern: /^1[.)]$/,\n                commandId: \"toggleListOL\",\n            },\n            {\n                pattern: /^a[.)]$/,\n                commandId: \"toggleListOL\",\n                commandParams: { listStyle: \"lower-alpha\" },\n            },\n            {\n                pattern: /^A[.)]$/,\n                commandId: \"toggleListOL\",\n                commandParams: { listStyle: \"upper-alpha\" },\n            },\n            {\n                pattern: /^[-*]$/,\n                commandId: \"toggleListUL\",\n            },\n            {\n                pattern: /^\\[\\]$/,\n                commandId: \"toggleListCL\",\n            },\n        ],\n        toolbar_items: [\n            withSequence(5, {\n                id: \"list\",\n                groupId: \"layout\",\n                description: _t(\"Toggle List\"),\n                Component: ListSelector,\n                props: {\n                    getButtons: () => this.listSelectorButtons,\n                    getListMode: this.getListMode.bind(this),\n                    key: this.toolbarListSelectorKey,\n                },\n                isAvailable: this.canToggleList.bind(this),\n            }),\n        ],\n        powerbox_items: [\n            {\n                categoryId: \"structure\",\n                commandId: \"toggleListUL\",\n            },\n            {\n                categoryId: \"structure\",\n                commandId: \"toggleListOL\",\n            },\n            {\n                categoryId: \"structure\",\n                commandId: \"toggleListCL\",\n            },\n        ].map((item) => withSequence(5, item)),\n        power_buttons: [\n            { commandId: \"toggleListUL\" },\n            { commandId: \"toggleListOL\" },\n            { commandId: \"toggleListCL\" },\n        ].map((item) => withSequence(15, item)),\n\n        hints: [{ selector: `LI, LI > ${baseContainerGlobalSelector}`, text: _t(\"List\") }],\n\n        /** Handlers */\n        normalize_handlers: this.normalize.bind(this),\n        step_added_handlers: this.updateToolbarButtons.bind(this),\n        delete_handlers: this.adjustListPaddingOnDelete.bind(this),\n\n        /** Overrides */\n        delete_backward_overrides: this.handleDeleteBackward.bind(this),\n        delete_range_overrides: this.handleDeleteRange.bind(this),\n        tab_overrides: this.handleTab.bind(this),\n        shift_tab_overrides: this.handleShiftTab.bind(this),\n        split_element_block_overrides: this.handleSplitBlock.bind(this),\n        color_apply_overrides: this.applyColorToListItem.bind(this),\n        format_selection_handlers: this.applyFormatToListItem.bind(this),\n        node_to_insert_processors: this.processNodeToInsert.bind(this),\n        clipboard_content_processors: this.processContentForClipboard.bind(this),\n        before_insert_within_pre_processors: this.insertListWithinPre.bind(this),\n\n        fully_selected_node_predicates: (node, selection, range) => {\n            if (node.nodeName === \"LI\") {\n                const nonListChildren = childNodes(node).filter(\n                    (n) => ![\"UL\", \"OL\"].includes(n.nodeName)\n                );\n                if (!nonListChildren.length) {\n                    return;\n                }\n                const startLeaf = firstLeaf(nonListChildren[0]);\n                const endLeaf = lastLeaf(nonListChildren[nonListChildren.length - 1]);\n                return (\n                    range.isPointInRange(startLeaf, 0) &&\n                    range.isPointInRange(endLeaf, nodeSize(endLeaf))\n                );\n            }\n        },\n    };\n\n    setup() {\n        this.addDomListener(this.editable, \"touchstart\", this.onPointerdown);\n        this.addDomListener(this.editable, \"mousedown\", this.onPointerdown);\n        this.listSelectorButtons = this.getListSelectorButtons();\n        this.canToggleListMemoized = weakMemoize(\n            (selection) =>\n                isHtmlContentSupported(selection) && this.getBlocksToToggleList().length > 0\n        );\n    }\n\n    toggleListCommand({ mode, listStyle } = {}) {\n        this.toggleList(mode, listStyle);\n        this.dependencies.history.addStep();\n    }\n\n    getBlocksToToggleList() {\n        const targetedBlocks = [...this.dependencies.selection.getTargetedBlocks()];\n        return targetedBlocks.filter(\n            (block) =>\n                !descendants(block).some((descendant) => targetedBlocks.includes(descendant)) &&\n                block.isContentEditable &&\n                ![\"OL\", \"UL\"].includes(block.tagName)\n        );\n    }\n\n    canToggleList(selection) {\n        return this.canToggleListMemoized(selection);\n    }\n\n    // --------------------------------------------------------------------------\n    // Commands\n    // --------------------------------------------------------------------------\n\n    /**\n     * Classifies the selected blocks into three categories:\n     * - LI that are part of a list of the same mode as the target one.\n     * - Lists (UL or OL) that need to have its mode switched to the target mode.\n     * - Blocks that need to be converted to lists.\n     *\n     *  If (and only if) all blocks fall into the first category, the list items\n     *  are converted into paragraphs (result is toggle list OFF).\n     *  Otherwise, the LIs in this category remain unchanged and the other two\n     *  categories are processed.\n     *\n     * @param {string} mode - The list mode to toggle (UL, OL, CL).\n     * @param {string} [listStyle] - The list style ( see listStyle css property)\n     * @throws {Error} If an invalid list type is provided.\n     */\n    toggleList(mode, listStyle) {\n        if (![\"UL\", \"OL\", \"CL\"].includes(mode)) {\n            throw new Error(`Invalid list type: ${mode}`);\n        }\n        if (mode === \"CL\" && !!listStyle) {\n            throw new Error(`listStyle is not compatible with \"CL\" list type`);\n        }\n\n        // @todo @phoenix: original implementation removed whitespace-only text nodes from targetedNodes.\n        // Check if this is necessary.\n\n        // Classify targeted blocks.\n        const sameModeListItems = new Set();\n        const nonListBlocks = new Set();\n        const listsToSwitch = new Set();\n        for (const block of this.getBlocksToToggleList()) {\n            const li = closestElement(block, isListItem);\n            if (li) {\n                if (this.getListMode(li.parentElement) === mode) {\n                    sameModeListItems.add(li);\n                } else {\n                    listsToSwitch.add(li.parentElement);\n                }\n            } else {\n                nonListBlocks.add(block);\n            }\n        }\n\n        // Apply changes.\n        if (listsToSwitch.size || nonListBlocks.size) {\n            for (const list of listsToSwitch) {\n                const cursors = this.dependencies.selection.preserveSelection();\n                const newList = this.switchListMode(list, mode);\n                cursors.remapNode(list, newList).restore();\n            }\n            for (const block of nonListBlocks) {\n                const list = this.blockToList(block, mode, listStyle);\n                if (listStyle) {\n                    list.style.listStyle = listStyle;\n                }\n            }\n        } else {\n            for (const li of sameModeListItems) {\n                this.liToBlocks(li);\n            }\n        }\n    }\n\n    normalize(root = this.editable) {\n        const closestNestedLI = closestElement(root, \"li:has(ul, ol)\");\n        if (closestNestedLI && closestNestedLI.closest(\"ul, ol\")) {\n            root = closestNestedLI.parentElement;\n        }\n        for (let element of selectElements(root, \"ul, ol, li\")) {\n            if (isProtected(element) || isProtecting(element)) {\n                continue;\n            }\n            for (const fn of [\n                this.liWithoutParentToP,\n                this.mergeSimilarLists,\n                this.normalizeLI,\n                this.normalizeNestedList,\n            ]) {\n                const updatedElement = fn.call(this, element);\n                if (updatedElement) {\n                    element = updatedElement;\n                }\n            }\n        }\n    }\n\n    // --------------------------------------------------------------------------\n    // Helpers for toggleList\n    // --------------------------------------------------------------------------\n\n    /**\n     * @param {HTMLElement} element\n     * @param {\"UL\"|\"OL\"|\"CL\"} mode\n     */\n    blockToList(element, mode) {\n        if (element.matches(baseContainerGlobalSelector)) {\n            return this.baseContainerToList(element, mode);\n        }\n        // @todo @phoenix: check for callbacks registered as resources instead?\n        if (element.matches(\"td, th, li.nav-item\")) {\n            return this.blockContentsToList(element, mode);\n        }\n        let list;\n        const cursors = this.dependencies.selection.preserveSelection();\n        if (element === this.editable) {\n            // @todo @phoenix: check if this is needed\n            // Refactor insertListAfter in order to make proper preserveCursor\n            // possible.\n            const callingNode = element.firstChild;\n            const group = getAdjacents(callingNode, (n) => !isBlock(n));\n            list = insertListAfter(this.document, callingNode, mode, group);\n        } else {\n            const parent = element.parentNode;\n            const childIndex = childNodeIndex(element);\n            list = insertListAfter(this.document, element, mode, [element]);\n            cursors.update((cursor) => {\n                if (cursor.node === parent) {\n                    if (cursor.offset === childIndex) {\n                        [cursor.node, cursor.offset] = [list.firstChild, 0];\n                    } else if (cursor.offset === childIndex + 1) {\n                        [cursor.node, cursor.offset] = [list.firstChild, 1];\n                    }\n                }\n            });\n            if (element.hasAttribute(\"dir\")) {\n                list.setAttribute(\"dir\", element.getAttribute(\"dir\"));\n            }\n        }\n        cursors.restore();\n        return list;\n    }\n\n    /**\n     * @param {HTMLElement} baseContainer baseContainer Element (can be a div with the\n     *        necessary classes/attributes).\n     * @param {\"UL\"|\"OL\"|\"CL\"} mode\n     */\n    baseContainerToList(baseContainer, mode) {\n        const cursors = this.dependencies.selection.preserveSelection();\n        const list = insertListAfter(this.document, baseContainer, mode, childNodes(baseContainer));\n        const textAlign = baseContainer.style.getPropertyValue(\"text-align\");\n        if (textAlign) {\n            // Copy text-align style from base container to li.\n            list.firstElementChild.style.setProperty(\"text-align\", textAlign);\n            baseContainer.style.removeProperty(\"text-align\");\n        }\n        this.dependencies.dom.copyAttributes(baseContainer, list);\n        this.adjustListPadding(list);\n        baseContainer.remove();\n        cursors.remapNode(baseContainer, list.firstChild).restore();\n        return list;\n    }\n\n    blockContentsToList(block, mode) {\n        const cursors = this.dependencies.selection.preserveSelection();\n        const list = insertListAfter(this.document, block.lastChild, mode, [...block.childNodes]);\n        cursors.remapNode(block, list.firstChild).restore();\n        return list;\n    }\n\n    /**\n     * Converts a list element and its nested elements to the given list mode.\n     *\n     * @see switchListMode\n     * @param {HTMLUListElement|HTMLOListElement|HTMLLIElement} node - HTML element\n     * representing a list or list item.\n     * @param {string} newMode - Target list mode\n     * @param {Object} options\n     * @returns {HTMLUListElement|HTMLOListElement|HTMLLIElement} node - Modified\n     * list element after conversion.\n     */\n    convertList(node, newMode) {\n        if (![\"UL\", \"OL\", \"LI\"].includes(node.tagName)) {\n            return;\n        }\n        const listMode = this.getListMode(node);\n        if (listMode && newMode !== listMode) {\n            node = this.switchListMode(node, newMode);\n        }\n        for (const child of node.children) {\n            this.convertList(child, newMode);\n        }\n        return node;\n    }\n\n    /**\n     * @param {HTMLElement} element\n     * @returns {\"UL\"|\"OL\"|\"CL\"|undefined}\n     */\n    getListMode(listContainerEl) {\n        if (![\"UL\", \"OL\"].includes(listContainerEl.tagName)) {\n            return;\n        }\n        if (listContainerEl.tagName === \"OL\") {\n            return \"OL\";\n        }\n        return listContainerEl.classList.contains(\"o_checklist\") ? \"CL\" : \"UL\";\n    }\n\n    /**\n     * Switches the list mode of the given list element.\n     *\n     * @param {HTMLOListElement|HTMLUListElement} list - The list element to switch the mode of.\n     * @param {\"UL\"|\"OL\"|\"CL\"} newMode - The new mode to switch to.\n     * @param {Object} options\n     * @returns {HTMLOListElement|HTMLUListElement} The modified list element.\n     */\n    switchListMode(list, newMode) {\n        if (this.getListMode(list) === newMode) {\n            return;\n        }\n        const newTag = newMode === \"CL\" ? \"UL\" : newMode;\n        const newList = this.dependencies.dom.setTagName(list, newTag);\n        // Remove any previously set list-style so that when changing the list\n        // type, the new list can show its correct default marker style.\n        newList.style.removeProperty(\"list-style\");\n        for (const li of newList.children) {\n            li.style.removeProperty(\"list-style\");\n        }\n        removeClass(newList, \"o_checklist\");\n        if (newMode === \"CL\") {\n            newList.classList.add(\"o_checklist\");\n        }\n        this.adjustListPadding(newList);\n        return newList;\n    }\n\n    /**\n     * Unwraps LI's content into blocks. Equivalent to fully outdenting the LI.\n     *\n     * @param {HTMLLIElement} li\n     */\n    liToBlocks(li) {\n        while (li) {\n            li = this.outdentLI(li);\n        }\n    }\n\n    // --------------------------------------------------------------------------\n    // Helpers for normalize\n    // --------------------------------------------------------------------------\n\n    liWithoutParentToP(element) {\n        const isOrphan = element.nodeName === \"LI\" && !element.closest(\"ul, ol\");\n        if (!isOrphan) {\n            return;\n        }\n        if (element.children.length && [...element.children].every(isBlock)) {\n            // Unwrap <li> if each of its children is a block element.\n            unwrapContents(element);\n        } else {\n            // Otherwise, wrap its content in a new <p> element.\n            const paragraph = this.dependencies.baseContainer.createBaseContainer();\n            element.replaceWith(paragraph);\n            paragraph.replaceChildren(...element.childNodes);\n        }\n    }\n\n    mergeSimilarLists(element) {\n        if (\n            !element.matches(\"ul, ol, li.oe-nested\") ||\n            (element.matches(\"li.oe-nested\") && !element.querySelector(\"ul, ol\"))\n        ) {\n            return;\n        }\n        const previousSibling = element.previousElementSibling;\n        if (\n            previousSibling &&\n            element.isContentEditable &&\n            previousSibling.isContentEditable &&\n            (compareListTypes(previousSibling, element) ||\n                (element.tagName === \"LI\" &&\n                    isListItem(previousSibling) &&\n                    isListElement(element.firstChild)))\n        ) {\n            const cursors = this.dependencies.selection.preserveSelection();\n            cursors.update(callbacksForCursorUpdate.merge(element));\n            previousSibling.append(...element.childNodes);\n            // @todo @phoenix: what if unremovable/unmergeable?\n            element.remove();\n            this.adjustListPadding(previousSibling);\n            cursors.restore();\n            return previousSibling;\n        }\n    }\n\n    /**\n     * Wraps inlines in P to avoid inlines with block siblings.\n     */\n    normalizeLI(element) {\n        if (!isListItem(element)) {\n            return;\n        }\n\n        if (\n            element.firstChild?.nodeType === Node.ELEMENT_NODE &&\n            isListElement(element.firstChild)\n        ) {\n            element.classList.add(\"oe-nested\");\n        }\n\n        if (\n            [...element.children].some(\n                (child) => isBlock(child) && !this.dependencies.split.isUnsplittable(child)\n            )\n        ) {\n            const cursors = this.dependencies.selection.preserveSelection();\n            wrapInlinesInBlocks(element, {\n                baseContainerNodeName: this.dependencies.baseContainer.getDefaultNodeName(),\n                cursors,\n            });\n            cursors.restore();\n        }\n    }\n\n    normalizeNestedList(element) {\n        if (element.tagName === \"LI\") {\n            return;\n        }\n        if ([\"UL\", \"OL\"].includes(element.parentElement?.tagName)) {\n            const cursors = this.dependencies.selection.preserveSelection();\n            let li;\n            if (element.previousElementSibling?.nodeName === \"LI\") {\n                li = element.previousElementSibling;\n            } else {\n                li = this.document.createElement(\"li\");\n                li.classList.add(\"oe-nested\");\n            }\n            element.parentElement.insertBefore(li, element);\n            li.appendChild(element);\n            cursors.restore();\n        }\n    }\n\n    // --------------------------------------------------------------------------\n    // Indentation\n    // --------------------------------------------------------------------------\n\n    // @temp comment: former oTab\n    /**\n     * @param {HTMLLIElement} li\n     */\n    indentLI(li) {\n        const lip = li.previousElementSibling || this.document.createElement(\"li\");\n        if (!lip.hasChildNodes()) {\n            lip.classList.add(\"oe-nested\");\n        }\n        const parentLi = li.parentElement;\n        const nextSiblingLi = li.nextSibling;\n        const destul =\n            li.previousElementSibling?.querySelector(\"ol, ul\") ||\n            li.querySelector(\"ol, ul\") ||\n            li.closest(\"ol, ul\");\n        const cursors = this.dependencies.selection.preserveSelection();\n        // Remove the LI first to force a removal mutation in collaboration.\n        parentLi.removeChild(li);\n        const ul = createList(this.document, this.getListMode(destul));\n        lip.append(ul);\n\n        // lip replaces li\n        li.before(lip);\n        ul.append(li);\n        const nestedLists = childNodes(li).filter((n) => isListElement(n));\n        ul.after(...nestedLists);\n        parentLi.insertBefore(lip, nextSiblingLi);\n        cursors.update((cursor) => {\n            if (cursor.node === lip.parentNode) {\n                const childIndex = childNodeIndex(lip);\n                if (cursor.offset === childIndex) {\n                    [cursor.node, cursor.offset] = [ul, 0];\n                } else if (cursor.offset === childIndex + 1) {\n                    [cursor.node, cursor.offset] = [ul, 1];\n                }\n            }\n        });\n        cursors.restore();\n    }\n\n    /**\n     * @param {HTMLLIElement} li\n     * @returns {HTMLLIElement|null} li or null if it no longer exists.\n     */\n    outdentLI(li) {\n        const listToSplit = li.querySelector(\"ol, ul\") || li.nextElementSibling;\n        if (listToSplit) {\n            this.splitList(listToSplit);\n        }\n\n        if (isListItem(li.parentNode.parentNode)) {\n            this.outdentNestedLI(li);\n            return li;\n        }\n        this.outdentTopLevelLI(li);\n        return null;\n    }\n\n    /**\n     * Splits a list at the given LI element (li is moved to the new list).\n     *\n     * @param {HTMLUListElement|HTMLOListElement|HTMLLIElement} node - HTML element\n     */\n    splitList(node) {\n        const cursors = this.dependencies.selection.preserveSelection();\n        // Create new list\n        const currentList = closestElement(node, \"ul, ol\");\n        const newList = currentList.cloneNode(false);\n        const isList = isListElement(node);\n        const wrapperLi = isList ? this.document.createElement(\"li\") : node;\n\n        if (isList) {\n            wrapperLi.classList.add(\"oe-nested\");\n            newList.append(wrapperLi);\n            cursors.update(callbacksForCursorUpdate.after(node.parentNode.parentNode, newList));\n            node.parentNode.parentNode.after(newList);\n        } else if (isListItem(node.parentNode.parentNode)) {\n            // li is nested list item\n            const lip = this.document.createElement(\"li\");\n            lip.classList.add(\"oe-nested\");\n            lip.append(newList);\n            cursors.update(callbacksForCursorUpdate.after(node.parentNode.parentNode, lip));\n            node.parentNode.parentNode.after(lip);\n        } else {\n            cursors.update(callbacksForCursorUpdate.after(node.parentNode, newList));\n            node.parentNode.after(newList);\n        }\n\n        const moveFrom = isList ? node.parentElement : node;\n        while (moveFrom.nextSibling) {\n            cursors.update(callbacksForCursorUpdate.append(newList, moveFrom.nextSibling));\n            newList.append(moveFrom.nextSibling);\n        }\n\n        const moveTo = isList ? wrapperLi : newList;\n        cursors.update(callbacksForCursorUpdate.prepend(moveTo, node));\n        moveTo.prepend(node);\n        cursors.restore();\n        this.adjustListPadding(currentList);\n        this.adjustListPadding(newList);\n        return newList;\n    }\n\n    outdentNestedLI(li) {\n        const cursors = this.dependencies.selection.preserveSelection();\n        const ul = li.parentNode;\n        const lip = ul.parentNode;\n        // Move LI\n        cursors.update(callbacksForCursorUpdate.after(lip, li));\n        lip.after(li);\n        while (ul.nextSibling) {\n            cursors.update(callbacksForCursorUpdate.append(li, ul.nextSibling));\n            li.append(ul.nextSibling);\n        }\n        // Remove UL and LI.oe-nested if left empty.\n        if (!ul.children.length) {\n            cursors.update(callbacksForCursorUpdate.remove(ul));\n            ul.remove();\n        }\n        // @todo @phoenix: not sure in which scenario lip would not have\n        // oe-nested class\n        if (!lip.children.length && lip.classList.contains(\"oe-nested\")) {\n            cursors.update(callbacksForCursorUpdate.remove(lip));\n            lip.remove();\n        }\n        this.adjustListPadding(li.parentElement);\n        cursors.restore();\n    }\n\n    /**\n     * @param {HTMLLIElement} li\n     */\n    outdentTopLevelLI(li) {\n        const cursors = this.dependencies.selection.preserveSelection();\n        const ul = li.parentNode;\n        const children = childNodes(li);\n        if (!children.every(isBlock)) {\n            const baseContainer = this.dependencies.baseContainer.createBaseContainer();\n            for (const child of children) {\n                cursors.update(callbacksForCursorUpdate.append(baseContainer, child));\n                baseContainer.append(child);\n            }\n            if (isShrunkBlock(baseContainer)) {\n                baseContainer.append(this.document.createElement(\"br\"));\n            }\n            li.append(baseContainer);\n            cursors.remapNode(li, baseContainer);\n        }\n        // Move LI's children to after UL\n        const blocksToMove = childNodes(li);\n        for (const block of blocksToMove.toReversed()) {\n            cursors.update(callbacksForCursorUpdate.after(ul, block));\n            ul.after(block);\n        }\n        // Preserve style properties\n        const dir = li.getAttribute(\"dir\") || ul.getAttribute(\"dir\");\n        const textAlign = li.style.getPropertyValue(\"text-align\");\n        const liColorStyle = getTextColorOrClass(li);\n        const liFontSizeStyle = getFontSizeOrClass(li);\n        const wrapChildren = (parent, tag) => {\n            const wrapper = this.document.createElement(tag);\n            wrapper.append(...parent.childNodes);\n            parent.replaceChildren(wrapper);\n            cursors.remapNode(parent, wrapper);\n            return wrapper;\n        };\n        for (const block of blocksToMove) {\n            // text direction\n            if (dir && !block.getAttribute(\"dir\")) {\n                block.setAttribute(\"dir\", dir);\n            }\n            // text alignment\n            if (textAlign && !block.style.getPropertyValue(\"text-align\")) {\n                block.style.setProperty(\"text-align\", textAlign);\n            }\n            // text color\n            if (liColorStyle) {\n                const font = wrapChildren(block, \"font\");\n                this.dependencies.color.colorElement(font, liColorStyle.value, \"color\");\n            }\n            // font-size\n            if (liFontSizeStyle && !isEmptyBlock(block)) {\n                const span = wrapChildren(block, \"span\");\n                if (liFontSizeStyle.type === \"font-size\") {\n                    span.style.fontSize = liFontSizeStyle.value;\n                } else if (liFontSizeStyle.type === \"class\") {\n                    span.classList.add(liFontSizeStyle.value);\n                }\n            }\n        }\n        // Remove LI\n        cursors.update(callbacksForCursorUpdate.remove(li));\n        li.remove();\n        // Remove UL if left empty\n        if (!ul.firstElementChild) {\n            cursors.update(callbacksForCursorUpdate.remove(ul));\n            ul.remove();\n        } else {\n            this.adjustListPadding(ul);\n        }\n        cursors.restore();\n    }\n\n    indentListNodes(listNodes) {\n        for (const li of listNodes) {\n            this.indentLI(li);\n        }\n    }\n\n    outdentListNodes(listNodes) {\n        for (const li of listNodes) {\n            this.outdentLI(li);\n        }\n    }\n\n    separateListItems() {\n        const listItems = new Set();\n        const navListItems = new Set();\n        const nonListItems = [];\n        const blocks = [...this.dependencies.selection.getTargetedBlocks()].filter(\n            (n) => !n.querySelector(\"li\")\n        );\n        for (const block of blocks) {\n            const closestLI = block.closest(\"li\");\n            if (closestLI) {\n                if (closestLI.classList.contains(\"nav-item\")) {\n                    navListItems.add(closestLI);\n                } else if (closestLI.isContentEditable) {\n                    listItems.add(closestLI);\n                }\n            } else if (![\"UL\", \"OL\"].includes(block.tagName)) {\n                nonListItems.push(block);\n            }\n        }\n        return { listItems: [...listItems], navListItems: [...navListItems], nonListItems };\n    }\n\n    // --------------------------------------------------------------------------\n    // Handlers of other plugins commands\n    // --------------------------------------------------------------------------\n\n    processNodeToInsert({ nodeToInsert, container }) {\n        if (isListItemElement(container) && isParagraphRelatedElement(nodeToInsert)) {\n            nodeToInsert = this.dependencies.dom.setTagName(nodeToInsert, \"LI\");\n        }\n        const listEl = container && closestElement(container, listElementSelector);\n        if (!listEl) {\n            return nodeToInsert;\n        }\n        const mode = container && this.getListMode(listEl);\n        if (isListItemElement(nodeToInsert) && nodeToInsert.querySelector(\"ol, ul\")) {\n            return this.convertList(nodeToInsert, mode);\n        }\n        if (isListElement(nodeToInsert)) {\n            return this.convertList(nodeToInsert, this.getListMode(nodeToInsert));\n        }\n        return nodeToInsert;\n    }\n\n    handleTab() {\n        const selection = this.dependencies.selection.getEditableSelection();\n        const closestLI = closestElement(selection.anchorNode, \"LI\");\n        if (closestLI) {\n            const block = closestBlock(selection.anchorNode);\n            const isLiContainsUnSpittable =\n                isParagraphRelatedElement(block) &&\n                ancestors(block, closestLI).find((node) =>\n                    this.dependencies.split.isUnsplittable(node)\n                );\n            if (isLiContainsUnSpittable) {\n                return;\n            }\n        }\n        const { listItems, navListItems, nonListItems } = this.separateListItems();\n        if (listItems.length || navListItems.length) {\n            this.indentListNodes(listItems);\n            this.dependencies.tabulation.indentBlocks(nonListItems);\n            const listsToAdjustPadding = new Set(\n                listItems.map((li) => closestElement(li, \"ul, ol\")).filter(Boolean)\n            );\n            for (const list of listsToAdjustPadding) {\n                this.adjustListPadding(list);\n            }\n            // Do nothing to nav-items.\n            this.dependencies.history.addStep();\n            return true;\n        }\n    }\n\n    handleShiftTab() {\n        const selection = this.dependencies.selection.getEditableSelection();\n        const closestLI = closestElement(selection.anchorNode, \"LI\");\n        if (closestLI) {\n            const block = closestBlock(selection.anchorNode);\n            const isLiContainsUnSpittable =\n                isParagraphRelatedElement(block) &&\n                ancestors(block, closestLI).find((node) =>\n                    this.dependencies.split.isUnsplittable(node)\n                );\n            if (isLiContainsUnSpittable) {\n                return;\n            }\n        }\n        const { listItems, navListItems, nonListItems } = this.separateListItems();\n        if (listItems.length || navListItems.length) {\n            this.outdentListNodes(listItems);\n            this.dependencies.tabulation.outdentBlocks(nonListItems);\n            // Do nothing to nav-items.\n            this.dependencies.history.addStep();\n            return true;\n        }\n    }\n\n    handleSplitBlock(params) {\n        const closestLI = closestElement(params.targetNode, \"LI\");\n        const isBlockUnsplittable =\n            closestLI &&\n            Array.from(closestLI.childNodes).some(\n                (node) => isBlock(node) && this.dependencies.split.isUnsplittable(node)\n            );\n        if (!closestLI || isBlockUnsplittable) {\n            return;\n        }\n        if (isEmptyBlock(closestLI)) {\n            this.outdentLI(closestLI);\n            return true;\n        }\n        const [, newLI] = this.dependencies.split.splitElementBlock({\n            ...params,\n            blockToSplit: closestLI,\n        });\n        if (newLI) {\n            if (closestLI.classList.contains(\"o_checked\")) {\n                removeClass(newLI, \"o_checked\");\n            }\n            const [anchorNode, anchorOffset] = getDeepestPosition(newLI, 0);\n            this.dependencies.selection.setSelection({ anchorNode, anchorOffset });\n            this.adjustListPadding(newLI.parentElement);\n        }\n        return true;\n    }\n\n    /**\n     * Fully outdent list item if cursor is at its beginning.\n     */\n    handleDeleteBackward(range) {\n        const { startContainer, startOffset, endContainer, endOffset } = range;\n        const closestLIendContainer = closestElement(endContainer, \"LI\");\n        if (!closestLIendContainer) {\n            return;\n        }\n        // Detect if cursor is at beginning of LI (or the editable === collapsed range).\n        const isCursorAtStartofLI =\n            (startContainer === endContainer && startOffset === endOffset) ||\n            closestElement(startContainer, \"LI\") !== closestLIendContainer;\n        if (!isCursorAtStartofLI) {\n            return;\n        }\n        // Check if li or parent list(s) are unsplittable.\n        let element = closestLIendContainer;\n        while ([\"LI\", \"UL\", \"OL\"].includes(element.tagName)) {\n            if (this.dependencies.split.isUnsplittable(element)) {\n                return;\n            }\n            element = element.parentElement;\n        }\n        if (!closestLIendContainer.classList.contains(\"oe-nested\")) {\n            // Remove LI marker on first backspace.\n            closestLIendContainer.classList.add(\"oe-nested\");\n            closestLIendContainer.classList.remove(\"o_checked\");\n        } else {\n            // Fully outdent the LI but keep its direction.\n            const list = closestElement(closestLIendContainer, \"ul[dir], ol[dir]\");\n            const dir = list?.getAttribute(\"dir\");\n            if (dir) {\n                closestLIendContainer.setAttribute(\"dir\", dir);\n            }\n            this.liToBlocks(closestLIendContainer);\n        }\n        return true;\n    }\n\n    // Uncheck checklist item left empty after deleting a multi-LI selection.\n    handleDeleteRange(range) {\n        const { startContainer, endContainer } = range;\n        const startCheckedLi = closestElement(startContainer, \"li.o_checked\");\n        if (!startCheckedLi) {\n            return;\n        }\n        const endLi = closestElement(endContainer, \"li\");\n        if (startCheckedLi === endLi) {\n            return;\n        }\n\n        range = this.dependencies.delete.deleteRange(range);\n        this.dependencies.selection.setSelection({\n            anchorNode: range.startContainer,\n            anchorOffset: range.startOffset,\n        });\n\n        if (isEmptyBlock(startCheckedLi)) {\n            removeClass(startCheckedLi, \"o_checked\");\n        }\n\n        return true;\n    }\n\n    /**\n     * @param {DocumentFragment} clonedContents\n     * @param {import(\"@html_editor/core/selection_plugin\").EditorSelection} selection\n     */\n    processContentForClipboard(clonedContents, selection) {\n        if (clonedContents.firstChild.nodeName === \"LI\") {\n            const list = selection.commonAncestorContainer.cloneNode();\n            list.replaceChildren(...childNodes(clonedContents));\n            clonedContents = list;\n        }\n        return clonedContents;\n    }\n\n    insertListWithinPre(node) {\n        const listItems = node.querySelectorAll(\"li:not(.oe-nested)\");\n        for (const li of listItems) {\n            const nestingLvl = ancestors(li).filter(isListElement).length - 1;\n            const list = closestElement(li, \"ul, ol\");\n            const listMode = this.getListMode(list);\n            let char;\n            if (listMode === \"CL\") {\n                char = \"[] \";\n            } else if (listMode === \"OL\") {\n                const children = childNodes(li.parentElement).filter(\n                    (n) => !n.classList.contains(\"oe-nested\")\n                );\n                char = `${children.indexOf(li) + 1}. `;\n            } else {\n                char = \"* \";\n            }\n            const prefix = \" \".repeat(nestingLvl * 4) + char;\n            li.prepend(this.document.createTextNode(prefix));\n        }\n        return node;\n    }\n\n    // --------------------------------------------------------------------------\n    // Event handlers\n    // --------------------------------------------------------------------------\n\n    /**\n     * @param {MouseEvent | TouchEvent} ev\n     */\n    onPointerdown(ev) {\n        const node = ev.target;\n        const isChecklistItem =\n            node.tagName == \"LI\" && this.getListMode(node.parentElement) == \"CL\";\n        if (!isChecklistItem) {\n            return;\n        }\n        let offsetX = ev.offsetX;\n        let offsetY = ev.offsetY;\n        if (ev.type === \"touchstart\") {\n            const rect = node.getBoundingClientRect();\n            offsetX = ev.touches[0].clientX - rect.x;\n            offsetY = ev.touches[0].clientY - rect.y;\n        }\n\n        if (isChecklistItem && this.isPointerInsideCheckbox(node, offsetX, offsetY)) {\n            toggleClass(node, \"o_checked\");\n            const { documentSelectionIsInEditable } =\n                this.dependencies.selection.getSelectionData();\n            // When the editable is not focused, clicking on checkbox\n            // wont make it focused So changes will be lost\n            // as no blur event will occur when clicking outside.\n            if (!documentSelectionIsInEditable) {\n                this.editable.focus();\n                this.dependencies.selection.setSelection({ anchorNode: node, anchorOffset: 0 });\n            }\n            ev.preventDefault();\n            this.dependencies.history.addStep();\n        }\n    }\n\n    /**\n     * @param {MouseEvent} ev\n     * @param {HTMLLIElement} li - LI element inside a checklist.\n     */\n    isPointerInsideCheckbox(li, pointerOffsetX, pointerOffsetY) {\n        const beforeStyle = this.window.getComputedStyle(li, \":before\");\n        const checkboxPosition = {\n            left: parseInt(beforeStyle.left),\n            top: parseInt(beforeStyle.top),\n        };\n        checkboxPosition.right = checkboxPosition.left + parseInt(beforeStyle.width);\n        checkboxPosition.bottom = checkboxPosition.top + parseInt(beforeStyle.height);\n\n        return (\n            pointerOffsetX >= checkboxPosition.left &&\n            pointerOffsetX <= checkboxPosition.right &&\n            pointerOffsetY >= checkboxPosition.top &&\n            pointerOffsetY <= checkboxPosition.bottom\n        );\n    }\n\n    applyColorToListItem(color, mode) {\n        this.dependencies.split.splitSelection();\n        const targetedNodes = this.dependencies.selection.getTargetedNodes();\n        const listItems = new Set(\n            targetedNodes.map((n) => closestElement(n, \"li\")).filter(Boolean)\n        );\n        if (!listItems.size || mode !== \"color\" || isColorGradient(color)) {\n            return;\n        }\n        const cursors = this.dependencies.selection.preserveSelection();\n        for (const listItem of listItems) {\n            if (this.dependencies.selection.areNodeContentsFullySelected(listItem)) {\n                for (const node of [\n                    listItem,\n                    ...descendants(listItem).filter(\n                        (n) => isElement(n) && closestElement(n, \"LI\") === listItem\n                    ),\n                ]) {\n                    // Remove any color-related classes.\n                    const classesToRemove = [...node.classList].filter(\n                        (cls) => cls === \"o_default_color\" || TEXT_CLASSES_REGEX.test(cls)\n                    );\n                    removeClass(node, ...classesToRemove);\n\n                    if (node.style.color) {\n                        removeStyle(node, \"color\");\n                    }\n                }\n\n                if (color) {\n                    this.dependencies.color.colorElement(listItem, color, mode);\n                    const sublists = childNodes(listItem).filter(isListElement);\n                    for (const list of sublists) {\n                        list.classList.add(\"o_default_color\");\n                    }\n                }\n            } else if (\n                color === \"\" &&\n                (listItem.style.color ||\n                    [...listItem.classList].some((cls) => TEXT_CLASSES_REGEX.test(cls)))\n            ) {\n                const textNodes = targetedNodes.filter(\n                    (n) => isVisibleTextNode(n) && closestElement(n, \"li\") === listItem\n                );\n                // Remove inline color from partial selection by\n                // wrapping in font with default color.\n                for (const node of textNodes) {\n                    const font = this.document.createElement(\"font\");\n                    font.classList.add(\"o_default_color\");\n                    node.before(font);\n                    cursors.update(callbacksForCursorUpdate.before(node, font));\n                    font.append(node);\n                    cursors.update(callbacksForCursorUpdate.append(font, node));\n                }\n            }\n        }\n        cursors.restore();\n    }\n\n    applyFormatToListItem(formatName, { formatProps, applyStyle } = {}) {\n        if (![\"setFontSizeClassName\", \"fontSize\"].includes(formatName)) {\n            return;\n        }\n        this.dependencies.split.splitSelection();\n        const targetedNodes = this.dependencies.selection.getTargetedNodes();\n        const listItems = new Set(\n            targetedNodes.map((n) => closestElement(n, \"li\")).filter(Boolean)\n        );\n        if (!listItems.size) {\n            return false;\n        }\n        const listsSet = new Set();\n        const cursors = this.dependencies.selection.preserveSelection();\n        for (const listItem of listItems) {\n            // Skip list items with block descendants other than base\n            // container or a list related elements or no font size formatting\n            // to remove.\n            const hasOnlyBaseBlocks = [...descendants(listItem)]\n                .filter(isBlock)\n                .every((n) => n.matches(`${baseContainerGlobalSelector}, ol, ul, li`));\n            const hasExistingFontSize =\n                FONT_SIZE_CLASSES.some((c) => listItem.classList.contains(c)) ||\n                listItem.style.fontSize;\n            if (!hasOnlyBaseBlocks || (!applyStyle && !hasExistingFontSize)) {\n                continue;\n            }\n\n            if (this.dependencies.selection.areNodeContentsFullySelected(listItem)) {\n                for (const node of [\n                    listItem,\n                    ...descendants(listItem).filter(\n                        (n) => isElement(n) && closestElement(n, \"LI\") === listItem\n                    ),\n                ]) {\n                    removeClass(node, ...FONT_SIZE_CLASSES, \"o_default_font_size\");\n                    if (node.style.fontSize) {\n                        node.style.fontSize = \"\";\n                    }\n                }\n\n                if (applyStyle) {\n                    if (formatName === \"setFontSizeClassName\") {\n                        listItem.classList.add(formatProps.className);\n                    } else if (formatName === \"fontSize\") {\n                        listItem.style.fontSize = formatProps.size;\n                    }\n                    const sublists = childNodes(listItem).filter(isListElement);\n                    for (const list of sublists) {\n                        list.classList.add(\"o_default_font_size\");\n                    }\n                }\n            } else if (!applyStyle && hasExistingFontSize) {\n                const textNodes = targetedNodes.filter(\n                    (n) => isVisibleTextNode(n) && closestElement(n, \"li\") === listItem\n                );\n                // Remove inline font size from partial selection by\n                // wrapping in span with default font size.\n                for (const node of textNodes) {\n                    const span = this.document.createElement(\"span\");\n                    span.classList.add(\"o_default_font_size\");\n                    node.before(span);\n                    cursors.update(callbacksForCursorUpdate.before(node, span));\n                    span.append(node);\n                    cursors.update(callbacksForCursorUpdate.append(span, node));\n                }\n            }\n            listsSet.add(listItem.parentElement);\n        }\n        cursors.restore();\n        for (const list of listsSet) {\n            this.adjustListPadding(list);\n        }\n        return true;\n    }\n\n    /**\n     * Adjusts the left padding of a list (`ul` or `ol`) to ensure that\n     * its `::marker` is always visible and doesn't overflow, especially\n     * when the marker width exceeds the default padding.\n     *\n     * @param {HTMLElement} list - The `<ul>` element used to determine the parent list and marker width.\n     */\n    adjustListPadding(list) {\n        if (!isListElement(list)) {\n            return;\n        }\n        list.style.removeProperty(\"padding-inline-start\");\n        if (list.classList.contains(\"o_checklist\")) {\n            return;\n        }\n\n        const largestMarker = list.children[Symbol.iterator]()\n            .map((li) => {\n                const markerWidth = parseFloat(this.window.getComputedStyle(li, \"::marker\").width);\n                return isNaN(markerWidth) ? 0 : markerWidth;\n            })\n            .reduce((accumulator, currentValue) => Math.max(accumulator, currentValue));\n        // For `UL` with large font size the marker width is so big that more padding is needed.\n        const largestMarkerPadding = Math.round(largestMarker) * (list.nodeName === \"UL\" ? 2 : 1);\n\n        // bootstrap sets ul { padding-left: 2rem; }\n        const defaultPadding = parseFloat(getHtmlStyle(this.document).fontSize) * 2;\n        // Align the whole list based on the item that requires the largest padding.\n        // For smaller font sizes, doubling the width of the dot marker is still lower than the\n        // default. The default is kept in that case.\n        if (largestMarkerPadding > defaultPadding) {\n            list.style.paddingInlineStart = `${largestMarkerPadding}px`;\n        }\n    }\n\n    adjustListPaddingOnDelete() {\n        const selection = this.document.getSelection();\n        if (!selection.isCollapsed || !selection.anchorNode) {\n            return;\n        }\n        const listItem = closestElement(selection.anchorNode);\n        if (isListItem(listItem)) {\n            this.adjustListPadding(listItem.parentElement);\n        }\n    }\n\n    // --------------------------------------------------------------------------\n    // Toolbar buttons\n    // --------------------------------------------------------------------------\n\n    updateToolbarButtons() {\n        this.toolbarListSelectorKey.value++;\n    }\n\n    getListSelectorButtons() {\n        return listSelectorItems\n            .filter((item) => item.commandId != \"toggleListCL\" || this.config.allowChecklist)\n            .map((item) => {\n                const command = this.resources.user_commands.find(\n                    (cmd) => cmd.id === item.commandId\n                );\n                const button = composeToolbarButton(command, item);\n                return {\n                    ...pick(button, \"id\", \"icon\", \"run\", \"mode\"),\n                    // We want short descriptions for these buttons.\n                    description: command.title,\n                };\n            });\n    }\n}\n", "import { Component } from \"@odoo/owl\";\nimport { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { toolbarButtonProps } from \"../toolbar/toolbar\";\nimport { closestElement } from \"@html_editor/utils/dom_traversal\";\nimport { useDropdownAutoVisibility } from \"@html_editor/dropdown_autovisibility_hook\";\nimport { useChildRef } from \"@web/core/utils/hooks\";\n\nexport class ListSelector extends Component {\n    static template = \"html_editor.ListSelector\";\n    static props = {\n        ...toolbarButtonProps,\n        getButtons: Function,\n        getListMode: Function,\n        key: Object,\n    };\n    static components = { Dropdown };\n\n    setup() {\n        this.menuRef = useChildRef();\n        useDropdownAutoVisibility(this.env.overlayState, this.menuRef);\n    }\n    getActiveMode() {\n        const { editableSelection: selection } = this.props.getSelection();\n        const closestLI = closestElement(selection.anchorNode, \"LI\");\n        return closestLI && this.props.getListMode(closestLI.parentNode);\n    }\n}\n", "import { unwrapContents } from \"@html_editor/utils/dom\";\nimport { closestElement, firstLeaf, lastLeaf } from \"@html_editor/utils/dom_traversal\";\nimport { getFontSizeOrClass } from \"@html_editor/utils/formatting\";\n\nexport function createList(document, mode) {\n    const node = document.createElement(mode === \"OL\" ? \"OL\" : \"UL\");\n    if (mode === \"CL\") {\n        node.classList.add(\"o_checklist\");\n    }\n    return node;\n}\n\nexport function insertListAfter(document, afterNode, mode, content = []) {\n    const list = createList(document, mode);\n    afterNode.after(list);\n    const li = document.createElement(\"LI\");\n    li.append(...content);\n    if (content.length === 1 && content[0].nodeType === Node.ELEMENT_NODE) {\n        const firstLeafNode = firstLeaf(content[0]);\n        const lastLeafNode = lastLeaf(content[0]);\n        const firstClosestFont = closestElement(firstLeafNode, \"font\");\n        const lastClosestFont = closestElement(lastLeafNode, \"font\");\n        if (firstClosestFont && lastClosestFont && firstClosestFont === lastClosestFont) {\n            li.style.color = firstClosestFont.style.color;\n            unwrapContents(firstClosestFont);\n        }\n        const firstClosestSpan = closestElement(firstLeafNode, \"span\");\n        const lastClosestSpan = closestElement(lastLeafNode, \"span\");\n        let fontSizeStyle;\n        if (\n            firstClosestSpan &&\n            lastClosestSpan &&\n            firstClosestSpan === lastClosestSpan &&\n            (fontSizeStyle = getFontSizeOrClass(firstClosestSpan))\n        ) {\n            if (fontSizeStyle.type === \"font-size\") {\n                li.style.fontSize = fontSizeStyle.value;\n            } else if (fontSizeStyle.type === \"class\") {\n                li.classList.add(fontSizeStyle.value);\n            }\n            unwrapContents(firstClosestSpan);\n        }\n    }\n    list.append(li);\n    return list;\n}\n\n/* Returns true if the two lists are of the same type among:\n * - OL\n * - regular UL\n * - checklist (ul.o_checklist)\n * - container for nested lists (li.oe-nested)\n */\nexport function compareListTypes(a, b) {\n    if (!a || !b || a.tagName !== b.tagName) {\n        return false;\n    }\n    if (a.classList.contains(\"o_checklist\") !== b.classList.contains(\"o_checklist\")) {\n        return false;\n    }\n    if (a.tagName === \"LI\") {\n        if (a.classList.contains(\"oe-nested\") !== b.classList.contains(\"oe-nested\")) {\n            return false;\n        }\n        return compareListTypes(a.firstElementChild, b.firstElementChild);\n    }\n    return true;\n}\n\nexport function isListItem(node) {\n    return node.nodeName === \"LI\" && !node.classList.contains(\"nav-item\");\n}\n", "import { Plugin } from \"../plugin\";\n\n/**\n * @typedef { Object } LocalOverlayShared\n * @property { LocalOverlayPlugin['makeLocalOverlay'] } makeLocalOverlay\n */\n\n/**\n * This plugins provides a way to create a \"local\" overlays so that their\n * visibility is relative to the overflow of their ancestors.\n */\nexport class LocalOverlayPlugin extends Plugin {\n    static id = \"localOverlay\";\n    static shared = [\"makeLocalOverlay\"];\n\n    setup() {\n        this.localOverlayContainer = this.config.localOverlayContainers?.ref.el;\n    }\n\n    /**\n     * Make a local container to organise floating elements inside it's own\n     * box and z-index isolation.\n     *\n     * @param {string} containerId An id to add to the container in order to make\n     *              the container more visible in the devtool and potentially\n     *              add css rules for the container and it's children.\n     */\n    makeLocalOverlay(containerId) {\n        const container = this.document.createElement(\"div\");\n        container.className = `oe-local-overlay`;\n        container.setAttribute(\"data-oe-local-overlay-id\", containerId);\n        if (this.localOverlayContainer) {\n            this.localOverlayContainer.append(container);\n        }\n        return container;\n    }\n}\n", "import { Plugin } from \"@html_editor/plugin\";\n\nexport class DoubleClickImagePreviewPlugin extends Plugin {\n    static id = \"dblclickImagePreview\";\n    static dependencies = [\"image\"];\n\n    setup() {\n        this.addDomListener(this.editable, \"dblclick\", (e) => {\n            if (e.target.tagName === \"IMG\") {\n                this.dependencies.image.previewImage();\n            }\n        });\n    }\n}\n", "import {\n    DocumentSelector,\n    renderStaticFileBox,\n} from \"@html_editor/main/media/media_dialog/document_selector\";\nimport { Plugin } from \"@html_editor/plugin\";\nimport { withSequence } from \"@html_editor/utils/resource\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { isHtmlContentSupported } from \"@html_editor/core/selection_plugin\";\n\nexport class FilePlugin extends Plugin {\n    static id = \"file\";\n    static dependencies = [\"dom\", \"history\"];\n    static defaultConfig = {\n        allowFile: true,\n    };\n    /** @type {import(\"plugins\").EditorResources} */\n    resources = {\n        user_commands: {\n            id: \"uploadFile\",\n            title: _t(\"Upload a file\"),\n            description: _t(\"Add a download box\"),\n            icon: \"fa-upload\",\n            run: this.uploadAndInsertFiles.bind(this),\n            isAvailable: (selection) =>\n                this.isUploadCommandAvailable(selection) && isHtmlContentSupported(selection),\n        },\n        powerbox_items: {\n            categoryId: \"media\",\n            commandId: \"uploadFile\",\n            keywords: [_t(\"file\"), _t(\"document\")],\n        },\n        power_buttons: withSequence(5, {\n            commandId: \"uploadFile\",\n            description: _t(\"Upload a file\"),\n        }),\n        unsplittable_node_predicates: (node) => node.classList?.contains(\"o_file_box\"),\n        ...(this.config.allowFile &&\n            this.config.allowMediaDocuments && {\n                media_dialog_extra_tabs: {\n                    id: \"DOCUMENTS\",\n                    title: _t(\"Documents\"),\n                    Component: this.componentForMediaDialog,\n                    sequence: 15,\n                },\n            }),\n        selectors_for_feff_providers: () => \".o_file_box\",\n        functional_empty_node_predicates: (node) =>\n            node?.nodeName === \"SPAN\" && node.classList.contains(\"o_file_box\"),\n        is_node_editable_predicates: (node) => {\n            if (node?.nodeName === \"SPAN\" && node.classList.contains(\"o_file_box\")) {\n                return false;\n            }\n        },\n    };\n\n    get recordInfo() {\n        return this.config.getRecordInfo?.() || {};\n    }\n\n    isUploadCommandAvailable() {\n        return this.config.allowFile;\n    }\n\n    get componentForMediaDialog() {\n        return DocumentSelector;\n    }\n\n    async uploadAndInsertFiles() {\n        // Upload\n        const attachments = await this.services.uploadLocalFiles.upload(this.recordInfo, {\n            multiple: true,\n            accessToken: true,\n        });\n        if (!attachments.length) {\n            // No files selected or error during upload\n            this.editable.focus();\n            return;\n        }\n        if (this.config.onAttachmentChange) {\n            attachments.forEach(this.config.onAttachmentChange);\n        }\n        // Render\n        const fileCards = attachments.map(this.renderDownloadBox.bind(this));\n        // Insert\n        fileCards.forEach(this.dependencies.dom.insert);\n        this.dependencies.history.addStep();\n    }\n\n    renderDownloadBox(attachment) {\n        const url = this.services.uploadLocalFiles.getURL(attachment, {\n            download: true,\n            unique: true,\n            accessToken: true,\n        });\n        const { name: filename, mimetype, id } = attachment;\n        return renderStaticFileBox(filename, mimetype, url, id);\n    }\n}\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { withSequence } from \"@html_editor/utils/resource\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { ColorSelector } from \"../font/color_selector\";\nimport { isHtmlContentSupported } from \"@html_editor/core/selection_plugin\";\n\nexport class IconColorPlugin extends Plugin {\n    static id = \"iconColor\";\n    static dependencies = [\"icon\", \"colorUi\"];\n    /** @type {import(\"plugins\").EditorResources} */\n    resources = {\n        toolbar_groups: withSequence(1, { id: \"icon_color\", namespaces: [\"icon\"] }),\n        toolbar_items: [\n            {\n                id: \"icon_forecolor\",\n                groupId: \"icon_color\",\n                description: _t(\"Select Font Color\"),\n                Component: ColorSelector,\n                props: this.dependencies.colorUi.getPropsForColorSelector(\"foreground\"),\n                isAvailable: isHtmlContentSupported,\n            },\n            {\n                id: \"icon_backcolor\",\n                groupId: \"icon_color\",\n                description: _t(\"Select Background Color\"),\n                Component: ColorSelector,\n                props: this.dependencies.colorUi.getPropsForColorSelector(\"background\"),\n                isAvailable: isHtmlContentSupported,\n            },\n        ],\n    };\n}\n", "import { withSequence } from \"@html_editor/utils/resource\";\nimport { Plugin } from \"../../plugin\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { MediaDialog } from \"./media_dialog/media_dialog\";\nimport { isHtmlContentSupported } from \"@html_editor/core/selection_plugin\";\nimport { ICON_SELECTOR, isElement } from \"@html_editor/utils/dom_info\";\n\nexport class IconPlugin extends Plugin {\n    static id = \"icon\";\n    static dependencies = [\"history\", \"selection\", \"dialog\"];\n    toolbarNamespace = \"icon\";\n    /** @type {import(\"plugins\").EditorResources} */\n    resources = {\n        user_commands: [\n            {\n                id: \"resizeIcon1\",\n                description: _t(\"Resize icon 1x\"),\n                run: () => this.resizeIcon({ size: \"1\" }),\n                isAvailable: isHtmlContentSupported,\n            },\n            {\n                id: \"resizeIcon2\",\n                description: _t(\"Resize icon 2x\"),\n                run: () => this.resizeIcon({ size: \"2\" }),\n                isAvailable: isHtmlContentSupported,\n            },\n            {\n                id: \"resizeIcon3\",\n                description: _t(\"Resize icon 3x\"),\n                run: () => this.resizeIcon({ size: \"3\" }),\n                isAvailable: isHtmlContentSupported,\n            },\n            {\n                id: \"resizeIcon4\",\n                description: _t(\"Resize icon 4x\"),\n                run: () => this.resizeIcon({ size: \"4\" }),\n                isAvailable: isHtmlContentSupported,\n            },\n            {\n                id: \"resizeIcon5\",\n                description: _t(\"Resize icon 5x\"),\n                run: () => this.resizeIcon({ size: \"5\" }),\n                isAvailable: isHtmlContentSupported,\n            },\n            {\n                id: \"toggleSpinIcon\",\n                description: _t(\"Toggle icon spin\"),\n                icon: \"fa-play\",\n                run: this.toggleSpinIcon.bind(this),\n                isAvailable: isHtmlContentSupported,\n            },\n            {\n                id: \"replaceIcon\",\n                description: _t(\"Replace icon\"),\n                run: this.openIconDialog.bind(this),\n                isAvailable: isHtmlContentSupported,\n            },\n        ],\n        toolbar_namespace_providers: [\n            (targetedNodes) => {\n                if (\n                    targetedNodes.length &&\n                    targetedNodes.every(\n                        // All nodes should be icons, its ZWS child or its ancestors\n                        (node) =>\n                            node.classList?.contains(\"fa\") ||\n                            node.parentElement.classList.contains(\"fa\") ||\n                            (node.querySelector?.(\".fa\") && node.isContentEditable !== false)\n                    )\n                ) {\n                    return this.toolbarNamespace;\n                }\n            },\n        ],\n        toolbar_groups: [\n            withSequence(2, { id: \"icon_size\", namespaces: [\"icon\"] }),\n            withSequence(3, { id: \"icon_spin\", namespaces: [\"icon\"] }),\n            withSequence(3, { id: \"icon_replace\", namespaces: [\"icon\"] }),\n        ],\n        toolbar_items: [\n            {\n                id: \"icon_size_1\",\n                groupId: \"icon_size\",\n                commandId: \"resizeIcon1\",\n                text: \"1x\",\n                isActive: () => this.hasIconSize(\"1\"),\n            },\n            {\n                id: \"icon_size_2\",\n                groupId: \"icon_size\",\n                commandId: \"resizeIcon2\",\n                text: \"2x\",\n                isActive: () => this.hasIconSize(\"2\"),\n            },\n            {\n                id: \"icon_size_3\",\n                groupId: \"icon_size\",\n                commandId: \"resizeIcon3\",\n                text: \"3x\",\n                isActive: () => this.hasIconSize(\"3\"),\n            },\n            {\n                id: \"icon_size_4\",\n                groupId: \"icon_size\",\n                commandId: \"resizeIcon4\",\n                text: \"4x\",\n                isActive: () => this.hasIconSize(\"4\"),\n            },\n            {\n                id: \"icon_size_5\",\n                groupId: \"icon_size\",\n                commandId: \"resizeIcon5\",\n                text: \"5x\",\n                isActive: () => this.hasIconSize(\"5\"),\n            },\n            {\n                id: \"icon_spin\",\n                groupId: \"icon_spin\",\n                commandId: \"toggleSpinIcon\",\n                isActive: () => this.hasSpinIcon(),\n            },\n            {\n                id: \"icon_replace\",\n                groupId: \"icon_replace\",\n                commandId: \"replaceIcon\",\n                text: _t(\"Replace\"),\n            },\n        ],\n    };\n\n    getTargetedIcon() {\n        const targetedNodes = this.dependencies.selection.getTargetedNodes();\n        return targetedNodes.find((node) => isElement(node) && node.matches(ICON_SELECTOR));\n    }\n\n    resizeIcon({ size }) {\n        const targetedIcon = this.getTargetedIcon();\n        if (!targetedIcon) {\n            return;\n        }\n        for (const classString of targetedIcon.classList) {\n            if (classString.match(/^fa-[2-5]x$/)) {\n                targetedIcon.classList.remove(classString);\n            }\n        }\n        if (size !== \"1\") {\n            targetedIcon.classList.add(`fa-${size}x`);\n        }\n        this.dependencies.history.addStep();\n    }\n\n    toggleSpinIcon() {\n        const selectedIcon = this.getTargetedIcon();\n        if (!selectedIcon) {\n            return;\n        }\n        selectedIcon.classList.toggle(\"fa-spin\");\n        this.dependencies.history.addStep();\n    }\n\n    hasIconSize(size) {\n        const selectedIcon = this.getTargetedIcon();\n        if (!selectedIcon) {\n            return;\n        }\n        if (size === \"1\") {\n            return ![...selectedIcon.classList].some((classString) =>\n                classString.match(/^fa-[2-5]x$/)\n            );\n        }\n        return selectedIcon.classList.contains(`fa-${size}x`);\n    }\n\n    hasSpinIcon() {\n        const selectedIcon = this.getTargetedIcon();\n        if (!selectedIcon) {\n            return;\n        }\n        return selectedIcon.classList.contains(\"fa-spin\");\n    }\n\n    openIconDialog() {\n        const selectedIcon = this.getTargetedIcon();\n        if (!selectedIcon) {\n            return;\n        }\n        this.dependencies.dialog.addDialog(MediaDialog, {\n            visibleTabs: [\"ICONS\"],\n            media: selectedIcon,\n            save: (el) => this.onSaveIcon(el, selectedIcon),\n        });\n    }\n\n    onSaveIcon(icon, prevIcon) {\n        for (const attribute of icon.attributes) {\n            prevIcon.setAttribute(attribute.nodeName, attribute.nodeValue);\n        }\n        this.dependencies.history.addStep();\n    }\n}\n", "import {\n    activateCropper,\n    loadImage,\n    loadImageInfo,\n    cropperDataFieldsWithAspectRatio,\n} from \"@html_editor/utils/image_processing\";\nimport { IMAGE_SHAPES } from \"./image_plugin\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport {\n    Component,\n    useRef,\n    onMounted,\n    onWillDestroy,\n    markup,\n    useExternalListener,\n    status,\n} from \"@odoo/owl\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { scrollTo, closestScrollableY } from \"@web/core/utils/scrolling\";\n\nexport const cropperAspectRatios = {\n    \"0/0\": { label: _t(\"Flexible\"), value: 0 },\n    \"16/9\": { label: \"16:9\", value: 16 / 9 },\n    \"4/3\": { label: \"4:3\", value: 4 / 3 },\n    \"1/1\": { label: \"1:1\", value: 1 },\n    \"2/3\": { label: \"2:3\", value: 2 / 3 },\n};\n\nexport class ImageCrop extends Component {\n    static template = \"html_editor.ImageCrop\";\n    static props = {\n        document: { validate: (p) => p.nodeType === Node.DOCUMENT_NODE },\n        media: { optional: true },\n        onClose: { type: Function, optional: true },\n        onSave: { type: Function, optional: true },\n    };\n\n    setup() {\n        this.aspectRatios = cropperAspectRatios;\n        this.notification = useService(\"notification\");\n        this.media = this.props.media;\n        this.document = this.props.document;\n\n        this.elRef = useRef(\"el\");\n        this.cropperWrapper = useRef(\"cropperWrapper\");\n        this.imageRef = useRef(\"imageRef\");\n        this.isCropperActive = false;\n\n        // We use capture so that the handler is called before other editor handlers\n        // like save, such that we can restore the src before a save.\n        // We need to add event listeners to the owner document of the widget.\n        useExternalListener(this.document, \"mousedown\", this.onDocumentMousedown, {\n            capture: true,\n        });\n        useExternalListener(this.document, \"keydown\", this.onDocumentKeydown, {\n            capture: true,\n        });\n        useExternalListener(\n            this.document,\n            \"selectionchange\",\n            () => {\n                if (!this.props.media.isConnected) {\n                    this.closeCropper();\n                }\n            },\n            { capture: true }\n        );\n\n        onMounted(() => {\n            this.hasModifiedImageClass = this.media.classList.contains(\"o_modified_image_to_save\");\n            if (this.hasModifiedImageClass) {\n                this.media.classList.remove(\"o_modified_image_to_save\");\n            }\n            this.show();\n        });\n        onWillDestroy(this.closeCropper);\n    }\n\n    closeCropper() {\n        if (!this.isCropperActive && !this.forceClose) {\n            return;\n        }\n        this.cropper?.destroy?.();\n        this.media.setAttribute(\"src\", this.initialSrc);\n        if (\n            this.hasModifiedImageClass &&\n            !this.media.classList.contains(\"o_modified_image_to_save\")\n        ) {\n            this.media.classList.add(\"o_modified_image_to_save\");\n        }\n        this.props?.onClose?.();\n        this.isCropperActive = false;\n    }\n\n    /**\n     * Resets the crop\n     */\n    async reset() {\n        if (this.cropper) {\n            this.cropper.reset();\n            if (this.aspectRatio !== \"0/0\") {\n                this.aspectRatio = \"0/0\";\n                this.cropper.setAspectRatio(cropperAspectRatios[this.aspectRatio].value);\n            }\n            await this.save();\n        }\n    }\n\n    async show() {\n        if (this.isCropperActive) {\n            return;\n        }\n        // key: ratio identifier, label: displayed to user, value: used by cropper lib\n        const src = this.media.getAttribute(\"src\");\n        const data = { ...this.media.dataset };\n        this.initialSrc = src;\n        this.aspectRatio = data.aspectRatio || \"0/0\";\n\n        // todo: check that the mutations of loadImage are not problematic (they most probably are).\n        Object.assign(this.media.dataset, await loadImageInfo(this.media));\n        const isIllustration = /^\\/(?:html|web)_editor\\/shape\\/illustration\\//.test(\n            this.media.dataset.originalSrc\n        );\n        this.uncroppable = false;\n        if (this.media.dataset.originalSrc && !isIllustration) {\n            this.originalSrc = this.media.dataset.originalSrc;\n            this.originalId = this.media.dataset.originalId;\n        } else {\n            // Couldn't find an attachment: not croppable.\n            this.uncroppable = true;\n        }\n\n        if (this.uncroppable) {\n            this.notification.add(\n                markup(\n                    _t(\n                        \"This type of image is not supported for cropping.<br/>If you want to crop it, please first download it from the original source and upload it in Odoo.\"\n                    )\n                ),\n                {\n                    title: _t(\"This image is an external image\"),\n                    type: \"warning\",\n                }\n            );\n            this.forceClose = true;\n            return this.closeCropper();\n        }\n\n        await this.scrollToInvisibleImage();\n        // Replacing the src with the original's so that the layout is correct.\n        await loadImage(this.originalSrc, this.media);\n        if (status(this) !== \"mounted\") {\n            // Abort if the component has been destroyed in the meantime\n            // since `this.imageRef.el` is `null` when it is not mounted.\n            return;\n        }\n        const cropperImage = this.imageRef.el;\n        [cropperImage.style.width, cropperImage.style.height] = [\n            this.media.width + \"px\",\n            this.media.height + \"px\",\n        ];\n\n        const sel = this.document.getSelection();\n        sel && sel.removeAllRanges();\n\n        // Overlaying the cropper image over the real image\n        let offset = undefined;\n        if (!this.media.getClientRects().length) {\n            offset = { top: 0, left: 0 };\n        } else {\n            const rect = this.media.getBoundingClientRect();\n            offset = {\n                top: rect.top,\n                left: rect.left,\n            };\n        }\n\n        offset.left += parseInt(this.media.style.paddingLeft || 0);\n        offset.top += parseInt(this.media.style.paddingRight || 0);\n        const frameElement = this.media.ownerDocument.defaultView.frameElement;\n        if (frameElement) {\n            const frameRect = frameElement.getBoundingClientRect();\n            offset.left += frameRect.left;\n            offset.top += frameRect.top;\n        }\n\n        this.cropperWrapper.el.style.left = `${offset.left}px`;\n        this.cropperWrapper.el.style.top = `${offset.top}px`;\n\n        await loadImage(this.originalSrc, cropperImage);\n        if (status(this) !== \"mounted\") {\n            return;\n        }\n\n        this.cropper = await activateCropper(\n            cropperImage,\n            cropperAspectRatios[this.aspectRatio]?.value || 0,\n            this.media.dataset\n        );\n\n        this.cropper.element.addEventListener(\"ready\", () => {\n            const cropperMove = this.cropperWrapper.el.querySelector(\".cropper-face.cropper-move\");\n            for (const shape of IMAGE_SHAPES) {\n                if (this.media.classList.contains(shape)) {\n                    cropperMove.classList.add(shape);\n                } else {\n                    cropperMove.classList.remove(shape);\n                }\n            }\n        });\n        this.isCropperActive = true;\n    }\n    /**\n     * Updates the DOM image with cropped data and associates required\n     * information for a potential future save (where required cropped data\n     * attachments will be created).\n     *\n     * @private\n     * @param {boolean} [cropped=true]\n     */\n    async save() {\n        const cropperData = this.getCropperData(this.cropper);\n        this.props.onSave?.({\n            aspectRatio: this.aspectRatio,\n            ...cropperData,\n        });\n        this.closeCropper();\n    }\n    /**\n     * Resets the crop box to prevent it going outside the image.\n     *\n     * @private\n     */\n    resetCropBox() {\n        this.cropper.clear();\n        this.cropper.crop();\n    }\n    /**\n     * Make sure the targeted image is in the visible viewport before crop.\n     *\n     * @private\n     */\n    async scrollToInvisibleImage() {\n        const rect = this.media.getBoundingClientRect();\n        const viewportTop = this.document.documentElement.scrollTop || 0;\n        const viewportBottom = viewportTop + window.innerHeight;\n        // Give priority to the closest scrollable element (e.g. for images in\n        // HTML fields, the element to scroll is different from the document's\n        // scrolling element).\n        const scrollable = closestScrollableY(this.media);\n\n        // The image must be in a position that allows access to it and its crop\n        // options buttons. Otherwise, the crop widget container can be scrolled\n        // to allow editing.\n        if (rect.top < viewportTop || viewportBottom - rect.bottom < 100) {\n            await scrollTo(this.media, {\n                behavior: \"smooth\",\n                ...(scrollable && { scrollable }),\n            });\n        }\n    }\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    onZoom(scale) {\n        this.cropper.zoom(scale);\n    }\n\n    onReset() {\n        this.cropper.reset();\n    }\n\n    onRotate(degree) {\n        this.cropper.rotate(degree);\n    }\n\n    onFlip(scaleDirection) {\n        const amount = this.cropper.getData()[scaleDirection] * -1;\n        this.cropper[scaleDirection](amount);\n    }\n\n    setAspectRatio(ratio) {\n        this.cropper.reset();\n        this.aspectRatio = ratio;\n        this.cropper.setAspectRatio(cropperAspectRatios[this.aspectRatio].value);\n    }\n\n    /**\n     * Discards crop if the user clicks outside of the widget.\n     *\n     * @private\n     * @param {MouseEvent} ev\n     */\n    onDocumentMousedown(ev) {\n        if (\n            this.props.document.body.contains(ev.target) &&\n            (this.elRef.el === ev.target || !this.elRef.el.contains(ev.target))\n        ) {\n            return this.closeCropper();\n        }\n    }\n    /**\n     * Save crop if user hits enter,\n     * discard crop on escape.\n     *\n     * @private\n     * @param {KeyboardEvent} ev\n     */\n    onDocumentKeydown(ev) {\n        if (ev.key === \"Enter\") {\n            return this.save();\n        } else if (ev.key === \"Escape\") {\n            ev.stopImmediatePropagation();\n            return this.closeCropper();\n        }\n    }\n    /**\n     * @param {Cropper} cropper\n     */\n    getCropperData(cropper) {\n        return Object.fromEntries(\n            cropperDataFieldsWithAspectRatio\n                .map((field) => [field, cropper.getData()[field]])\n                .filter(([, value]) => value)\n        );\n    }\n    /**\n     * Resets the cropbox on zoom to prevent crop box overflowing.\n     *\n     * @private\n     */\n    async onCropZoom() {\n        // Wait for the zoom event to be fully processed before reseting.\n        await new Promise((res) => setTimeout(res, 0));\n        this.resetCropBox();\n    }\n}\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { Plugin } from \"../../plugin\";\nimport { ImageCrop } from \"./image_crop\";\nimport { isHtmlContentSupported } from \"@html_editor/core/selection_plugin\";\n\n/**\n * @typedef { Object } ImageCropShared\n * @property { ImageCropPlugin['openCropImage'] } openCropImage\n */\n\nexport class ImageCropPlugin extends Plugin {\n    static id = \"imageCrop\";\n    static dependencies = [\"selection\", \"history\", \"imagePostProcess\"];\n    static shared = [\"openCropImage\"];\n    /** @type {import(\"plugins\").EditorResources} */\n    resources = {\n        user_commands: [\n            {\n                id: \"cropImage\",\n                run: this.openCropImage.bind(this),\n                description: _t(\"Crop image\"),\n                icon: \"fa-crop\",\n                isAvailable: isHtmlContentSupported,\n            },\n        ],\n        toolbar_items: [\n            {\n                id: \"image_crop\",\n                commandId: \"cropImage\",\n                groupId: \"image_modifiers\",\n            },\n        ],\n    };\n\n    getTargetedImage() {\n        const targetedNodes = this.dependencies.selection.getTargetedNodes();\n        return targetedNodes.find((node) => node.tagName === \"IMG\");\n    }\n\n    async openCropImage(targetedImg, imageCropProps = {}) {\n        targetedImg = targetedImg || this.getTargetedImage();\n        if (!targetedImg) {\n            return;\n        }\n        return registry.category(\"main_components\").add(\"ImageCropping\", {\n            Component: ImageCrop,\n            props: {\n                media: targetedImg,\n                onSave: async (newDataset) => {\n                    // todo: should use the mutex if there is one?\n                    const updateImageAttributes =\n                        await this.dependencies.imagePostProcess.processImage({\n                            img: targetedImg,\n                            newDataset,\n                        });\n                    updateImageAttributes();\n                    this.dependencies.history.addStep();\n                },\n                document: this.document,\n                ...imageCropProps,\n                onClose: () => {\n                    registry.category(\"main_components\").remove(\"ImageCropping\");\n                    imageCropProps.onClose?.();\n                },\n            },\n        });\n    }\n}\n", "import { Component, useEffect, useRef } from \"@odoo/owl\";\nimport { Dialog } from \"@web/core/dialog/dialog\";\nimport { toolbarButtonProps } from \"@html_editor/main/toolbar/toolbar\";\nimport { useHotkey } from \"@web/core/hotkeys/hotkey_hook\";\n\nexport class ImageDescription extends Component {\n    static components = { Dialog };\n    static props = {\n        ...toolbarButtonProps,\n        openImageDescriptionPopover: Function,\n    };\n    static template = \"html_editor.ImageDescription\";\n}\n\nexport class ImageDescriptionPopover extends Component {\n    static props = {\n        close: Function,\n        description: {\n            type: String,\n            optional: true,\n        },\n        onConfirm: Function,\n        tooltip: {\n            type: String,\n            optional: true,\n        },\n    };\n    static template = \"html_editor.ImageDescriptionPopover\";\n\n    setup() {\n        this.state = {\n            description: this.props.description,\n            tooltip: this.props.tooltip,\n        };\n        this.inputRef = useRef(\"description\");\n        useEffect(\n            (el) => el?.focus(),\n            () => [this.inputRef.el]\n        );\n        useHotkey(\"escape\", () => this.props.close());\n    }\n\n    onSave() {\n        this.props.onConfirm(this.state.description || \"\", this.state.tooltip || \"\");\n        this.props.close();\n    }\n}\n", "import { Plugin } from \"../../plugin\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { isImageUrl } from \"@html_editor/utils/url\";\nimport { ImageDescription, ImageDescriptionPopover } from \"./image_description\";\nimport { ImageToolbarDropdown } from \"./image_toolbar_dropdown\";\nimport { createFileViewer } from \"@web/core/file_viewer/file_viewer_hook\";\nimport { isHtmlContentSupported } from \"@html_editor/core/selection_plugin\";\nimport { boundariesOut } from \"@html_editor/utils/position\";\nimport { withSequence } from \"@html_editor/utils/resource\";\nimport { ImageTransformButton } from \"./image_transform_button\";\nimport { callbacksForCursorUpdate } from \"@html_editor/utils/selection\";\nimport { closestBlock } from \"@html_editor/utils/blocks\";\nimport { fillEmpty } from \"@html_editor/utils/dom\";\nimport { reactive } from \"@odoo/owl\";\n\nfunction hasShape(imagePlugin, shapeName) {\n    return () => imagePlugin.isSelectionShaped(shapeName);\n}\n\nexport const IMAGE_SHAPES = [\"rounded\", \"rounded-circle\", \"shadow\", \"img-thumbnail\"];\n\nconst IMAGE_PADDING = [\n    { name: \"None\", value: 0 },\n    { name: \"Small\", value: 1 },\n    { name: \"Medium\", value: 2 },\n    { name: \"Large\", value: 3 },\n    { name: \"XL\", value: 5 },\n];\n\nconst IMAGE_SIZE = [\n    { name: \"Default\", value: \"\" },\n    { name: \"100%\", value: \"100%\" },\n    { name: \"50%\", value: \"50%\" },\n    { name: \"25%\", value: \"25%\" },\n];\n\n/**\n * @typedef { Object } ImageShared\n * @property { ImagePlugin['getTargetedImage'] } getTargetedImage\n * @property { ImagePlugin['previewImage'] } previewImage\n * @property { ImagePlugin['resetImageTransformation'] } resetImageTransformation\n */\n\n/**\n * @typedef {((img: HTMLImageElement) => void | true)[]} delete_image_overrides\n * @typedef {((img: HTMLImageElement) => boolean)[]} image_name_predicates\n */\n\nexport class ImagePlugin extends Plugin {\n    static id = \"image\";\n    static dependencies = [\"history\", \"dom\", \"selection\", \"overlay\"];\n    static shared = [\"getTargetedImage\", \"previewImage\", \"resetImageTransformation\"];\n    static defaultConfig = { allowImageTransform: true };\n    toolbarNamespace = \"image\";\n    /** @type {import(\"plugins\").EditorResources} */\n    resources = {\n        user_commands: [\n            {\n                id: \"deleteImage\",\n                description: _t(\"Remove (DELETE) image\"),\n                icon: \"fa-trash text-danger\",\n                run: this.deleteImage.bind(this),\n                isAvailable: isHtmlContentSupported,\n            },\n            {\n                id: \"previewImage\",\n                description: _t(\"Preview image\"),\n                icon: \"fa-search-plus\",\n                run: this.previewImage.bind(this),\n                isAvailable: isHtmlContentSupported,\n            },\n            {\n                id: \"setImageShapeRounded\",\n                description: _t(\"Set shape: Rounded\"),\n                icon: \"fa-square\",\n                run: () => this.setImageShape(\"rounded\", { excludeClasses: [\"rounded-circle\"] }),\n                isAvailable: isHtmlContentSupported,\n            },\n            {\n                id: \"setImageShapeCircle\",\n                description: _t(\"Set shape: Circle\"),\n                icon: \"fa-circle-o\",\n                run: () => this.setImageShape(\"rounded-circle\", { excludeClasses: [\"rounded\"] }),\n                isAvailable: isHtmlContentSupported,\n            },\n            {\n                id: \"setImageShapeShadow\",\n                description: _t(\"Set shape: Shadow\"),\n                icon: \"fa-sun-o\",\n                run: () => this.setImageShape(\"shadow\"),\n                isAvailable: isHtmlContentSupported,\n            },\n            {\n                id: \"setImageShapeThumbnail\",\n                description: _t(\"Set shape: Thumbnail\"),\n                icon: \"fa-picture-o\",\n                run: () => this.setImageShape(\"img-thumbnail\"),\n                isAvailable: isHtmlContentSupported,\n            },\n            {\n                id: \"resizeImage\",\n                run: this.resizeImage.bind(this),\n                isAvailable: isHtmlContentSupported,\n            },\n        ],\n        toolbar_namespace_providers: [\n            (targetedNodes) => {\n                if (\n                    targetedNodes.length &&\n                    targetedNodes.every((node) => node.nodeName === \"IMG\")\n                ) {\n                    return this.toolbarNamespace;\n                }\n            },\n        ],\n        toolbar_groups: [\n            withSequence(23, { id: \"image_preview\", namespaces: [\"image\"] }),\n            withSequence(24, { id: \"image_description\", namespaces: [\"image\"] }),\n            withSequence(25, { id: \"image_shape\", namespaces: [\"image\"] }),\n            withSequence(26, { id: \"image_padding\", namespaces: [\"image\"] }),\n            withSequence(26, { id: \"image_size\", namespaces: [\"image\"] }),\n            withSequence(26, { id: \"image_modifiers\", namespaces: [\"image\"] }),\n            withSequence(32, { id: \"image_delete\", namespaces: [\"image\"] }),\n        ],\n        toolbar_items: [\n            {\n                id: \"image_preview\",\n                groupId: \"image_preview\",\n                commandId: \"previewImage\",\n            },\n            {\n                id: \"image_description\",\n                description: _t(\"Edit media description\"),\n                groupId: \"image_description\",\n                Component: ImageDescription,\n                props: {\n                    openImageDescriptionPopover: this.openImageDescriptionPopover.bind(this),\n                },\n                isAvailable: isHtmlContentSupported,\n            },\n            {\n                id: \"shape_rounded\",\n                groupId: \"image_shape\",\n                commandId: \"setImageShapeRounded\",\n                isActive: hasShape(this, \"rounded\"),\n            },\n            {\n                id: \"shape_circle\",\n                groupId: \"image_shape\",\n                commandId: \"setImageShapeCircle\",\n                isActive: hasShape(this, \"rounded-circle\"),\n            },\n            {\n                id: \"shape_shadow\",\n                groupId: \"image_shape\",\n                commandId: \"setImageShapeShadow\",\n                isActive: hasShape(this, \"shadow\"),\n            },\n            {\n                id: \"shape_thumbnail\",\n                groupId: \"image_shape\",\n                commandId: \"setImageShapeThumbnail\",\n                isActive: hasShape(this, \"img-thumbnail\"),\n            },\n            {\n                id: \"image_padding\",\n                groupId: \"image_padding\",\n                description: _t(\"Set image padding\"),\n                Component: ImageToolbarDropdown,\n                props: {\n                    name: \"image_padding\",\n                    icon: \"html_editor.ImagePaddingIcon\",\n                    items: IMAGE_PADDING,\n                    onSelected: (item) => {\n                        this.setImagePadding({ size: item.value });\n                    },\n                },\n                isAvailable: isHtmlContentSupported,\n            },\n            {\n                id: \"image_size\",\n                groupId: \"image_size\",\n                description: _t(\"Resize image\"),\n                Component: ImageToolbarDropdown,\n                props: {\n                    name: \"image_size\",\n                    getDisplay: () => this.imageSize,\n                    items: IMAGE_SIZE,\n                    onSelected: (item) => {\n                        this.resizeImage({ size: item.value });\n                        this.updateImageParams();\n                    },\n                },\n                isAvailable: (selection) =>\n                    isHtmlContentSupported(selection) && (this.config.allowImageResize ?? true),\n            },\n            {\n                id: \"image_transform\",\n                groupId: \"image_modifiers\",\n                description: _t(\"Transform the picture (click twice to reset transformation)\"),\n                Component: ImageTransformButton,\n                props: this.getImageTransformProps(),\n                isAvailable: (selection) =>\n                    this.config.allowImageTransform && isHtmlContentSupported(selection),\n            },\n            {\n                id: \"image_delete\",\n                groupId: \"image_delete\",\n                commandId: \"deleteImage\",\n            },\n        ],\n\n        /** Handlers */\n        selectionchange_handlers: this.updateImageParams.bind(this),\n        post_undo_handlers: this.updateImageParams.bind(this),\n        post_redo_handlers: this.updateImageParams.bind(this),\n\n        /** Providers */\n        paste_media_url_command_providers: this.getCommandForImageUrlPaste.bind(this),\n    };\n\n    setup() {\n        this.imageSize = reactive({ displayName: \"Default\" });\n        this.addDomListener(this.editable, \"pointerup\", (e) => {\n            if (e.target.tagName === \"IMG\") {\n                const [anchorNode, anchorOffset, focusNode, focusOffset] = boundariesOut(e.target);\n                this.dependencies.selection.setSelection({\n                    anchorNode,\n                    anchorOffset,\n                    focusNode,\n                    focusOffset,\n                });\n                this.dependencies.selection.focusEditable();\n            }\n        });\n        this.fileViewer = createFileViewer();\n        this.overlay = this.dependencies.overlay.createOverlay(ImageDescriptionPopover, {\n            className: \"popover\",\n        });\n    }\n\n    destroy() {\n        super.destroy();\n    }\n\n    get imageSizeName() {\n        const targetedImg = this.getTargetedImage();\n        if (!targetedImg) {\n            return \"Default\";\n        }\n        return targetedImg.style.width || \"Default\";\n    }\n\n    setImagePadding({ size } = {}) {\n        const targetedImg = this.getTargetedImage();\n        if (!targetedImg) {\n            return;\n        }\n        for (const classString of targetedImg.classList) {\n            if (classString.match(/^p-[0-9]$/)) {\n                targetedImg.classList.remove(classString);\n            }\n        }\n        targetedImg.classList.add(`p-${size}`);\n        this.dependencies.history.addStep();\n    }\n    resizeImage({ size } = {}) {\n        const targetedImg = this.getTargetedImage();\n        if (!targetedImg) {\n            return;\n        }\n        targetedImg.style.width = size || \"\";\n        this.dependencies.history.addStep();\n    }\n\n    setImageShape(className, { excludeClasses = [] } = {}) {\n        const targetedImg = this.getTargetedImage();\n        if (!targetedImg) {\n            return;\n        }\n        for (const classString of excludeClasses) {\n            if (targetedImg.classList.contains(classString)) {\n                targetedImg.classList.remove(classString);\n            }\n        }\n        targetedImg.classList.toggle(className);\n        this.dependencies.history.addStep();\n    }\n\n    previewImage() {\n        const targetedImg = this.getTargetedImage();\n        if (!targetedImg) {\n            return;\n        }\n        let imageName;\n        // Keep the result from the first predicate that returns something.\n        this.getResource(\"image_name_predicates\").find((p) => {\n            imageName = p(targetedImg);\n            return imageName;\n        });\n        const fileModel = {\n            isImage: true,\n            isViewable: true,\n            name: imageName || targetedImg.src,\n            defaultSource: targetedImg.src,\n            downloadUrl: targetedImg.src,\n        };\n        this.document.getSelection().collapseToEnd();\n        this.fileViewer.open(fileModel);\n    }\n\n    deleteImage() {\n        const targetedImg = this.getTargetedImage();\n        if (targetedImg) {\n            if (this.delegateTo(\"delete_image_overrides\", targetedImg)) {\n                return;\n            }\n            const cursors = this.dependencies.selection.preserveSelection();\n            cursors.update(callbacksForCursorUpdate.remove(targetedImg));\n            const parentEl = closestBlock(targetedImg);\n            targetedImg.remove();\n            cursors.restore();\n            fillEmpty(parentEl);\n            this.dependencies.history.addStep();\n        }\n    }\n\n    getTargetedImage() {\n        const targetedNodes = this.dependencies.selection.getTargetedNodes();\n        return targetedNodes.find((node) => node.tagName === \"IMG\");\n    }\n\n    hasImageSize(size) {\n        const targetedImg = this.getTargetedImage();\n        return targetedImg?.style?.width === size;\n    }\n\n    isSelectionShaped(shape) {\n        const targetedNodes = this.dependencies.selection\n            .getTargetedNodes()\n            .filter((n) => n.tagName === \"IMG\" && n.classList.contains(shape));\n        return targetedNodes.length > 0;\n    }\n\n    getImageAttribute(attributeName) {\n        const targetedNodes = this.dependencies.selection.getTargetedNodes();\n        const targetedImg = targetedNodes.find((node) => node.tagName === \"IMG\");\n        return targetedImg.getAttribute(attributeName) || undefined;\n    }\n\n    /**\n     * @param {string} url\n     */\n    getCommandForImageUrlPaste(url) {\n        if (isImageUrl(url)) {\n            return {\n                title: _t(\"Embed Image\"),\n                description: _t(\"Embed the image in the document.\"),\n                icon: \"fa-image\",\n                run: () => {\n                    const img = this.document.createElement(\"IMG\");\n                    img.setAttribute(\"src\", url);\n                    this.dependencies.dom.insert(img);\n                    this.dependencies.history.addStep();\n                },\n            };\n        }\n    }\n\n    updateImageDescription({ description, tooltip } = {}) {\n        const targetedImg = this.getTargetedImage();\n        if (!targetedImg) {\n            return;\n        }\n        targetedImg.setAttribute(\"alt\", description);\n        targetedImg.setAttribute(\"title\", tooltip);\n        this.dependencies.history.addStep();\n    }\n\n    resetImageTransformation(image) {\n        image.setAttribute(\n            \"style\",\n            (image.getAttribute(\"style\") || \"\").replace(/[^;]*transform[\\w:]*;?/g, \"\")\n        );\n        image.style.removeProperty(\"width\");\n        image.style.removeProperty(\"height\");\n        this.dependencies.history.addStep();\n    }\n\n    getImageTransformProps() {\n        return {\n            id: \"image_transform\",\n            icon: \"fa-object-ungroup\",\n            title: _t(\"Transform the picture (click twice to reset transformation)\"),\n            getTargetedImage: this.getTargetedImage.bind(this),\n            resetImageTransformation: this.resetImageTransformation.bind(this),\n            addStep: this.dependencies.history.addStep.bind(this),\n            document: this.document,\n            editable: this.editable,\n            activeTitle: _t(\"Click again to reset transformation\"),\n        };\n    }\n\n    updateImageParams() {\n        this.imageSize.displayName = this.imageSizeName;\n    }\n\n    openImageDescriptionPopover() {\n        const image = this.getTargetedImage();\n        if (image) {\n            this.overlay.open({\n                target: image,\n                props: {\n                    close: () => this.overlay.close(),\n                    description: this.getImageAttribute(\"alt\"),\n                    tooltip: this.getImageAttribute(\"title\"),\n                    onConfirm: (description, tooltip) => {\n                        this.updateImageDescription({ description, tooltip });\n                    },\n                },\n            });\n        }\n    }\n}\n", "import {\n    activateCropper,\n    getAspectRatio,\n    getDataURLBinarySize,\n    getImageSizeFromCache,\n    isGif,\n    isWebGLEnabled,\n    loadImage,\n    loadImageDataURL,\n    loadImageInfo,\n} from \"@html_editor/utils/image_processing\";\nimport { Plugin } from \"../../plugin\";\nimport { getAffineApproximation, getProjective } from \"@html_editor/utils/perspective_utils\";\n\nexport const DEFAULT_IMAGE_QUALITY = \"92\";\n\n/**\n * @typedef { Object } ImagePostProcessShared\n * @property { ImagePostProcessPlugin['processImage'] } processImage\n * @property { ImagePostProcessPlugin['getProcessedImageSize'] } getProcessedImageSize\n */\n\n/**\n * @typedef {(\n *   (img: HTMLImageElement, newDataset: object) => Promise<{\n *     getHeight: (canvas: HTMLCanvasElement) => number,\n *     perspective: string | null,\n *     newDataset: object,\n *     postProcessCroppedCanvas: (canvas: HTMLCanvasElement) => Promise<HTMLCanvasElement>,\n *     svg: SVGElement,\n *     svgAspectRatio: number,\n *     svgWidth: number,\n *   }>\n * )[]} process_image_warmup_handlers\n * @typedef {(\n *   (\n *     url: string,\n *     newDataset: object,\n *     processContext: { svg: SVGElement, svgAspectRatio: number, svgWidth: number }\n *   ) => Promise<[newUrl: string, handlerDataset: object]>\n * )[]} process_image_post_handlers\n * @typedef {((args: {imageEl: HTMLElement}) => void)[]} on_image_updated_handlers\n */\n\nexport class ImagePostProcessPlugin extends Plugin {\n    static id = \"imagePostProcess\";\n    static dependencies = [\"style\"];\n    static shared = [\"processImage\", \"getProcessedImageSize\"];\n\n    /**\n     * Applies data-attributes modifications to an img tag and returns a dataURL\n     * containing the result. This function does not modify the original image.\n     *\n     * @param {HTMLImageElement} img the image to which modifications are applied\n     * @param {Object} newDataset an object containing the modifications to apply\n     * @param {Function} [onImageInfoLoaded] can be used to fill\n     * newDataset after having access to image info, return true to cancel call\n     * @returns {{ url: string, newDataset: object }} Object containing the image\n     * URL and the updated dataset.\n     */\n    async _processImage({ img, newDataset = {}, onImageInfoLoaded }) {\n        const processContext = {};\n        if (!newDataset.originalSrc || !newDataset.mimetypeBeforeConversion) {\n            Object.assign(newDataset, await loadImageInfo(img));\n        }\n        if (onImageInfoLoaded) {\n            if (await onImageInfoLoaded(newDataset)) {\n                return;\n            }\n        }\n        for (const cb of this.getResource(\"process_image_warmup_handlers\")) {\n            const addedContext = await cb(img, newDataset);\n            if (addedContext) {\n                if (addedContext.newDataset) {\n                    Object.assign(newDataset, addedContext.newDataset);\n                }\n                Object.assign(processContext, addedContext);\n            }\n        }\n\n        const data = getImageTransformationData({ ...img.dataset, ...newDataset });\n        const {\n            mimetypeBeforeConversion,\n            formatMimetype,\n            width,\n            height,\n            resizeWidth,\n            filter,\n            glFilter,\n            filterOptions,\n            aspectRatio,\n            quality,\n        } = data;\n\n        const { postProcessCroppedCanvas, perspective, getHeight } = processContext;\n\n        // loadImage may have ended up loading a different src (see: LOAD_IMAGE_404)\n        const originalImg = await loadImage(data.originalSrc);\n        const originalSrc = originalImg.getAttribute(\"src\");\n\n        if (shouldPreventGifTransformation(data)) {\n            const [postUrl, postDataset] = await this.postProcessImage(\n                await loadImageDataURL(originalSrc),\n                newDataset,\n                processContext\n            );\n            return { url: postUrl, newDataset: postDataset };\n        }\n        // Crop\n        const container = document.createElement(\"div\");\n        container.appendChild(originalImg);\n        const cropper = await activateCropper(originalImg, aspectRatio, data);\n        const croppedCanvas = cropper.getCroppedCanvas(width, height);\n        cropper.destroy();\n        const processedCanvas = (await postProcessCroppedCanvas?.(croppedCanvas)) || croppedCanvas;\n\n        // Width\n        const canvas = document.createElement(\"canvas\");\n        canvas.width = resizeWidth || processedCanvas.width;\n        canvas.height = getHeight\n            ? getHeight(canvas)\n            : (processedCanvas.height * canvas.width) / processedCanvas.width;\n        const ctx = canvas.getContext(\"2d\");\n        ctx.imageSmoothingQuality = \"high\";\n        ctx.mozImageSmoothingEnabled = true;\n        ctx.webkitImageSmoothingEnabled = true;\n        ctx.msImageSmoothingEnabled = true;\n        ctx.imageSmoothingEnabled = true;\n\n        // Perspective 3D\n        if (perspective) {\n            // x, y coordinates of the corners of the image as a percentage\n            // (relative to the width or height of the image) needed to apply\n            // the 3D effect.\n            const points = JSON.parse(perspective);\n            const divisions = 10;\n            const w = processedCanvas.width,\n                h = processedCanvas.height;\n\n            const project = getProjective(w, h, [\n                [(canvas.width / 100) * points[0][0], (canvas.height / 100) * points[0][1]], // Top-left [x, y]\n                [(canvas.width / 100) * points[1][0], (canvas.height / 100) * points[1][1]], // Top-right [x, y]\n                [(canvas.width / 100) * points[2][0], (canvas.height / 100) * points[2][1]], // bottom-right [x, y]\n                [(canvas.width / 100) * points[3][0], (canvas.height / 100) * points[3][1]], // bottom-left [x, y]\n            ]);\n\n            for (let i = 0; i < divisions; i++) {\n                for (let j = 0; j < divisions; j++) {\n                    const [dx, dy] = [w / divisions, h / divisions];\n\n                    const upper = {\n                        origin: [i * dx, j * dy],\n                        sides: [dx, dy],\n                        flange: 0.1,\n                        overlap: 0,\n                    };\n                    const lower = {\n                        origin: [i * dx + dx, j * dy + dy],\n                        sides: [-dx, -dy],\n                        flange: 0,\n                        overlap: 0.1,\n                    };\n\n                    for (const { origin, sides, flange, overlap } of [upper, lower]) {\n                        const [[a, c, e], [b, d, f]] = getAffineApproximation(project, [\n                            origin,\n                            [origin[0] + sides[0], origin[1]],\n                            [origin[0], origin[1] + sides[1]],\n                        ]);\n\n                        const ox = (i !== divisions ? overlap * sides[0] : 0) + flange * sides[0];\n                        const oy = (j !== divisions ? overlap * sides[1] : 0) + flange * sides[1];\n\n                        origin[0] += flange * sides[0];\n                        origin[1] += flange * sides[1];\n\n                        sides[0] -= flange * sides[0];\n                        sides[1] -= flange * sides[1];\n\n                        ctx.save();\n                        ctx.setTransform(a, b, c, d, e, f);\n\n                        ctx.beginPath();\n                        ctx.moveTo(origin[0] - ox, origin[1] - oy);\n                        ctx.lineTo(origin[0] + sides[0], origin[1] - oy);\n                        ctx.lineTo(origin[0] + sides[0], origin[1]);\n                        ctx.lineTo(origin[0], origin[1] + sides[1]);\n                        ctx.lineTo(origin[0] - ox, origin[1] + sides[1]);\n                        ctx.closePath();\n                        ctx.clip();\n                        ctx.drawImage(processedCanvas, 0, 0);\n\n                        ctx.restore();\n                    }\n                }\n            }\n        } else {\n            ctx.drawImage(\n                processedCanvas,\n                0,\n                0,\n                processedCanvas.width,\n                processedCanvas.height,\n                0,\n                0,\n                canvas.width,\n                canvas.height\n            );\n        }\n\n        // GL filter\n        const canUseWebGL = glFilter && isWebGLEnabled() && window.WebGLImageFilter;\n        if (canUseWebGL) {\n            const glf = new window.WebGLImageFilter();\n            const cv = document.createElement(\"canvas\");\n            cv.width = canvas.width;\n            cv.height = canvas.height;\n            applyAll = _applyAll.bind(null, canvas);\n            glFilters[glFilter](glf, cv, filterOptions);\n            const filtered = glf.apply(canvas);\n            ctx.drawImage(\n                filtered,\n                0,\n                0,\n                filtered.width,\n                filtered.height,\n                0,\n                0,\n                canvas.width,\n                canvas.height\n            );\n        }\n\n        // Color filter\n        ctx.fillStyle = filter || \"#0000\";\n        ctx.fillRect(0, 0, canvas.width, canvas.height);\n\n        // Quality\n        newDataset.mimetype = formatMimetype || mimetypeBeforeConversion;\n        const dataURL = canvas.toDataURL(newDataset.mimetype, quality / 100);\n        const newSize = getDataURLBinarySize(dataURL);\n        const originalSize = getImageSizeFromCache(originalSrc);\n        const isChanged =\n            !!perspective ||\n            !!glFilter ||\n            originalImg.width !== canvas.width ||\n            originalImg.height !== canvas.height ||\n            originalImg.width !== processedCanvas.width ||\n            originalImg.height !== processedCanvas.height;\n\n        let url =\n            isChanged || originalSize >= newSize ? dataURL : await loadImageDataURL(originalSrc);\n        [url, newDataset] = await this.postProcessImage(url, newDataset, processContext);\n        return { url, newDataset };\n    }\n    async processImage(params) {\n        const processed = await this._processImage(params);\n        if (!processed) {\n            return () => {};\n        }\n        return () => this.updateImageAttributes(params.img, processed.url, processed.newDataset);\n    }\n    async getProcessedImageSize(img) {\n        const processed = await this._processImage({ img });\n        return getDataURLBinarySize(processed.url);\n    }\n    async postProcessImage(url, newDataset, processContext) {\n        for (const cb of this.getResource(\"process_image_post_handlers\")) {\n            const [newUrl, handlerDataset] = (await cb(url, newDataset, processContext)) || [];\n            url = newUrl || url;\n            newDataset = handlerDataset || newDataset;\n        }\n        return [url, newDataset];\n    }\n    updateImageAttributes(el, url, newDataset) {\n        el.classList.add(\"o_modified_image_to_save\");\n        if (el.tagName === \"IMG\") {\n            el.setAttribute(\"src\", url);\n        } else {\n            this.dependencies.style.setBackgroundImageUrl(el, url);\n        }\n        for (const key in newDataset) {\n            const value = newDataset[key];\n            if (value) {\n                el.dataset[key] = value;\n            } else {\n                delete el.dataset[key];\n            }\n        }\n        this.dispatchTo(\"on_image_updated_handlers\", { imageEl: el });\n    }\n}\n\nexport function getImageTransformationData(dataset) {\n    const data = Object.assign(\n        {\n            glFilter: \"\",\n            filter: \"#0000\",\n            forceModification: false,\n        },\n        dataset\n    );\n    for (const key of [\"width\", \"height\", \"resizeWidth\"]) {\n        data[key] = parseFloat(data[key]);\n    }\n    if (!(\"quality\" in data)) {\n        data.quality = DEFAULT_IMAGE_QUALITY;\n    }\n    // todo: this information could be inferred from x/y/width/height dataset\n    // properties.\n    data.aspectRatio = data.aspectRatio ? getAspectRatio(data.aspectRatio) : 0;\n    return data;\n}\n\nfunction shouldTransformImage(data) {\n    return (\n        data.perspective ||\n        data.glFilter ||\n        data.width ||\n        data.height ||\n        data.resizeWidth ||\n        data.aspectRatio\n    );\n}\n\nexport function shouldPreventGifTransformation(data) {\n    return isGif(data.mimetypeBeforeConversion) && !shouldTransformImage(data);\n}\n\nexport const defaultImageFilterOptions = {\n    blend: \"normal\",\n    filterColor: \"\",\n    blur: \"0\",\n    desaturateLuminance: \"0\",\n    saturation: \"0\",\n    contrast: \"0\",\n    brightness: \"0\",\n    sepia: \"0\",\n};\n\n// webgl color filters\nconst _applyAll = (result, filter, filters) => {\n    filters.forEach((f) => {\n        if (f[0] === \"blend\") {\n            const cv = f[1];\n            const ctx = result.getContext(\"2d\");\n            ctx.globalCompositeOperation = f[2];\n            ctx.globalAlpha = f[3];\n            ctx.drawImage(cv, 0, 0);\n            ctx.globalCompositeOperation = \"source-over\";\n            ctx.globalAlpha = 1.0;\n        } else {\n            filter.addFilter(...f);\n        }\n    });\n};\nlet applyAll;\n\nconst glFilters = {\n    blur: (filter) => filter.addFilter(\"blur\", 10),\n\n    1977: (filter, cv) => {\n        const ctx = cv.getContext(\"2d\");\n        ctx.fillStyle = \"rgb(243, 106, 188)\";\n        ctx.fillRect(0, 0, cv.width, cv.height);\n        applyAll(filter, [\n            [\"blend\", cv, \"screen\", 0.3],\n            [\"brightness\", 0.1],\n            [\"contrast\", 0.1],\n            [\"saturation\", 0.3],\n        ]);\n    },\n\n    aden: (filter, cv) => {\n        const ctx = cv.getContext(\"2d\");\n        ctx.fillStyle = \"rgb(66, 10, 14)\";\n        ctx.fillRect(0, 0, cv.width, cv.height);\n        applyAll(filter, [\n            [\"blend\", cv, \"darken\", 0.2],\n            [\"brightness\", 0.2],\n            [\"contrast\", -0.1],\n            [\"saturation\", -0.15],\n            [\"hue\", 20],\n        ]);\n    },\n\n    brannan: (filter, cv) => {\n        const ctx = cv.getContext(\"2d\");\n        ctx.fillStyle = \"rgb(161, 44, 191)\";\n        ctx.fillRect(0, 0, cv.width, cv.height);\n        applyAll(filter, [\n            [\"blend\", cv, \"lighten\", 0.31],\n            [\"sepia\", 0.5],\n            [\"contrast\", 0.4],\n        ]);\n    },\n\n    earlybird: (filter, cv) => {\n        const ctx = cv.getContext(\"2d\");\n        const gradient = ctx.createRadialGradient(\n            cv.width / 2,\n            cv.height / 2,\n            0,\n            cv.width / 2,\n            cv.height / 2,\n            Math.hypot(cv.width, cv.height) / 2\n        );\n        gradient.addColorStop(0.2, \"#D0BA8E\");\n        gradient.addColorStop(1, \"#1D0210\");\n        ctx.fillStyle = gradient;\n        ctx.fillRect(0, 0, cv.width, cv.height);\n        applyAll(filter, [\n            [\"blend\", cv, \"overlay\", 0.2],\n            [\"sepia\", 0.2],\n            [\"contrast\", -0.1],\n        ]);\n    },\n\n    inkwell: (filter, cv) => {\n        applyAll(filter, [\n            [\"sepia\", 0.3],\n            [\"brightness\", 0.1],\n            [\"contrast\", -0.1],\n            [\"desaturateLuminance\"],\n        ]);\n    },\n\n    // Needs hue blending mode for perfect reproduction. Close enough?\n    maven: (filter, cv) => {\n        applyAll(filter, [\n            [\"sepia\", 0.25],\n            [\"brightness\", -0.05],\n            [\"contrast\", -0.05],\n            [\"saturation\", 0.5],\n        ]);\n    },\n\n    toaster: (filter, cv) => {\n        const ctx = cv.getContext(\"2d\");\n        const gradient = ctx.createRadialGradient(\n            cv.width / 2,\n            cv.height / 2,\n            0,\n            cv.width / 2,\n            cv.height / 2,\n            Math.hypot(cv.width, cv.height) / 2\n        );\n        gradient.addColorStop(0, \"#0F4E80\");\n        gradient.addColorStop(1, \"#3B003B\");\n        ctx.fillStyle = gradient;\n        ctx.fillRect(0, 0, cv.width, cv.height);\n        applyAll(filter, [\n            [\"blend\", cv, \"screen\", 0.5],\n            [\"brightness\", -0.1],\n            [\"contrast\", 0.5],\n        ]);\n    },\n\n    walden: (filter, cv) => {\n        const ctx = cv.getContext(\"2d\");\n        ctx.fillStyle = \"#CC4400\";\n        ctx.fillRect(0, 0, cv.width, cv.height);\n        applyAll(filter, [\n            [\"blend\", cv, \"screen\", 0.3],\n            [\"sepia\", 0.3],\n            [\"brightness\", 0.1],\n            [\"saturation\", 0.6],\n            [\"hue\", 350],\n        ]);\n    },\n\n    valencia: (filter, cv) => {\n        const ctx = cv.getContext(\"2d\");\n        ctx.fillStyle = \"#3A0339\";\n        ctx.fillRect(0, 0, cv.width, cv.height);\n        applyAll(filter, [\n            [\"blend\", cv, \"exclusion\", 0.5],\n            [\"sepia\", 0.08],\n            [\"brightness\", 0.08],\n            [\"contrast\", 0.08],\n        ]);\n    },\n\n    xpro: (filter, cv) => {\n        const ctx = cv.getContext(\"2d\");\n        const gradient = ctx.createRadialGradient(\n            cv.width / 2,\n            cv.height / 2,\n            0,\n            cv.width / 2,\n            cv.height / 2,\n            Math.hypot(cv.width, cv.height) / 2\n        );\n        gradient.addColorStop(0.4, \"#E0E7E6\");\n        gradient.addColorStop(1, \"#2B2AA1\");\n        ctx.fillStyle = gradient;\n        ctx.fillRect(0, 0, cv.width, cv.height);\n        applyAll(filter, [\n            [\"blend\", cv, \"color-burn\", 0.7],\n            [\"sepia\", 0.3],\n        ]);\n    },\n\n    custom: (filter, cv, filterOptions) => {\n        const options = Object.assign(defaultImageFilterOptions, JSON.parse(filterOptions || \"{}\"));\n        const filters = [];\n        if (options.filterColor) {\n            const ctx = cv.getContext(\"2d\");\n            ctx.fillStyle = options.filterColor;\n            ctx.fillRect(0, 0, cv.width, cv.height);\n            filters.push([\"blend\", cv, options.blend, 1]);\n        }\n        delete options.blend;\n        delete options.filterColor;\n        filters.push(\n            ...Object.entries(options).map(([filter, amount]) => [filter, parseInt(amount) / 100])\n        );\n        applyAll(filter, filters);\n    },\n};\n", "import { Plugin } from \"@html_editor/plugin\";\nimport {\n    backgroundImageCssToParts,\n    backgroundImagePartsToCss,\n    getImageSrc,\n} from \"@html_editor/utils/image\";\nimport { rpc } from \"@web/core/network/rpc\";\n\n/**\n * @typedef { Object } ImageSaveShared\n * @property { ImageSavePlugin['savePendingImages'] } savePendingImages\n */\n\n/**\n * @typedef {((el: HTMLElement) => HTMLElement)[]} closest_savable_providers\n */\n\nexport class ImageSavePlugin extends Plugin {\n    static id = \"imageSave\";\n    static shared = [\"savePendingImages\"];\n\n    /** @type {import(\"plugins\").EditorResources} */\n    resources = {\n        before_save_handlers: this.savePendingImages.bind(this),\n\n        ...(this.config.dropImageAsAttachment && {\n            added_image_handlers: (img) => img.classList.add(\"o_b64_image_to_save\"),\n        }),\n    };\n\n    async savePendingImages(editableEl = this.editable) {\n        // When saving a webp, o_b64_image_to_save is turned into\n        // o_modified_image_to_save by saveB64Image to request the saving\n        // of the pre-converted webp resizes and all the equivalent jpgs.\n        const getClosestSavable = (el) => {\n            for (const provider of this.getResource(\"closest_savable_providers\")) {\n                const value = provider(el);\n                if (value) {\n                    return value;\n                }\n            }\n        };\n        const oldSrcToNewSrcMap = new Map();\n        const b64Proms = [...editableEl.querySelectorAll(\".o_b64_image_to_save\")].map(\n            async (el) => {\n                const { resModel, resId } = this.getRecordInfo(getClosestSavable(el));\n                const oldSrc = el.getAttribute(\"src\");\n                await this.saveB64Image(el, resModel, resId);\n                oldSrcToNewSrcMap.set(oldSrc, el.getAttribute(\"src\"));\n            }\n        );\n        const modifiedProms = [...editableEl.querySelectorAll(\".o_modified_image_to_save\")].map(\n            async (el) => {\n                const { resModel, resId } = this.getRecordInfo(getClosestSavable(el));\n                const oldSrc = el.getAttribute(\"src\");\n                await this.saveModifiedImage(el, resModel, resId);\n                oldSrcToNewSrcMap.set(oldSrc, el.getAttribute(\"src\"));\n            }\n        );\n        const proms = [...b64Proms, ...modifiedProms];\n        const hasChange = !!proms.length;\n        if (hasChange) {\n            await Promise.all(proms);\n        }\n        return hasChange ? oldSrcToNewSrcMap : undefined;\n    }\n\n    createAttachment({ el, imageData, resModel, resId }) {\n        return rpc(\"/html_editor/attachment/add_data\", {\n            name: el.dataset.fileName || \"\",\n            data: imageData,\n            is_image: true,\n            res_model: resModel,\n            res_id: resId,\n        });\n    }\n\n    /**\n     * Saves a base64 encoded image as an attachment.\n     * Relies on saveModifiedImage being called after it for webp.\n     *\n     * @private\n     * @param {Element} el\n     * @param {string} resModel\n     * @param {number} resId\n     */\n    async saveB64Image(el, resModel, resId) {\n        const imageData = el.getAttribute(\"src\").split(\"base64,\")[1];\n        if (!imageData) {\n            // Checks if the image is in base64 format for RPC call. Relying\n            // only on the presence of the class \"o_b64_image_to_save\" is not\n            // robust enough.\n            el.classList.remove(\"o_b64_image_to_save\");\n            return;\n        }\n        const attachment = await this.createAttachment({\n            el,\n            imageData,\n            resId,\n            resModel,\n        });\n        if (!attachment) {\n            return;\n        }\n        if (attachment.mimetype === \"image/webp\") {\n            el.classList.add(\"o_modified_image_to_save\");\n            el.dataset.originalId = attachment.id;\n            el.dataset.mimetype = attachment.mimetype;\n            el.dataset.fileName = attachment.name;\n            return this.saveModifiedImage(el, resModel, resId);\n        } else {\n            let src = attachment.image_src;\n            if (!attachment.public) {\n                let accessToken = attachment.access_token;\n                if (!accessToken) {\n                    [accessToken] = await this.services.orm.call(\n                        \"ir.attachment\",\n                        \"generate_access_token\",\n                        [attachment.id]\n                    );\n                }\n                src += `?access_token=${encodeURIComponent(accessToken)}`;\n            }\n            el.setAttribute(\"src\", src);\n        }\n        el.classList.remove(\"o_b64_image_to_save\");\n    }\n\n    /**\n     * Saves a modified image as an attachment.\n     *\n     * @private\n     * @param {Element} el\n     * @param {string} resModel\n     * @param {number} resId\n     */\n    async saveModifiedImage(el, resModel, resId) {\n        const isBackground = !el.matches(\"img\");\n        // Modifying an image always creates a copy of the original, even if\n        // it was modified previously, as the other modified image may be used\n        // elsewhere if the snippet was duplicated or was saved as a custom one.\n        let altData = undefined;\n        const isImageField = !!el.closest(\"[data-oe-type=image]\");\n        if (el.dataset.mimetype === \"image/webp\" && isImageField) {\n            // Generate alternate sizes and format for reports.\n            altData = {};\n            const image = document.createElement(\"img\");\n            image.src = getImageSrc(el);\n            await new Promise((resolve) => image.addEventListener(\"load\", resolve));\n            const originalSize = Math.max(image.width, image.height);\n            const smallerSizes = [1024, 512, 256, 128].filter((size) => size < originalSize);\n            for (const size of [originalSize, ...smallerSizes]) {\n                const ratio = size / originalSize;\n                const canvas = document.createElement(\"canvas\");\n                canvas.width = image.width * ratio;\n                canvas.height = image.height * ratio;\n                const ctx = canvas.getContext(\"2d\");\n                ctx.fillStyle = \"rgb(255, 255, 255)\";\n                ctx.fillRect(0, 0, canvas.width, canvas.height);\n                ctx.drawImage(\n                    image,\n                    0,\n                    0,\n                    image.width,\n                    image.height,\n                    0,\n                    0,\n                    canvas.width,\n                    canvas.height\n                );\n                altData[size] = {\n                    \"image/jpeg\": canvas.toDataURL(\"image/jpeg\").split(\",\")[1],\n                };\n                if (size !== originalSize) {\n                    altData[size][\"image/webp\"] = canvas\n                        .toDataURL(\"image/webp\")\n                        .split(\",\")[1];\n                }\n            }\n        }\n        const newAttachmentSrc = await rpc(\n            `/html_editor/modify_image/${encodeURIComponent(el.dataset.originalId)}`,\n            {\n                res_model: resModel,\n                res_id: parseInt(resId),\n                data: getImageSrc(el).split(\",\")[1],\n                alt_data: altData,\n                mimetype: isBackground\n                    ? el.dataset.mimetype\n                    : el.getAttribute(\"src\").split(\":\")[1].split(\";\")[0],\n                name: el.dataset.fileName ? el.dataset.fileName : null,\n            }\n        );\n        el.classList.remove(\"o_modified_image_to_save\");\n        if (isBackground) {\n            const parts = backgroundImageCssToParts(el.style[\"background-image\"]);\n            parts.url = `url('${newAttachmentSrc}')`;\n            const combined = backgroundImagePartsToCss(parts);\n            el.style[\"background-image\"] = combined;\n        } else {\n            el.setAttribute(\"src\", newAttachmentSrc);\n        }\n        this.dispatchTo(\"on_image_saved_handlers\", { imageEl: el });\n    }\n\n    getRecordInfo(editableEl = null) {\n        return this.config.getRecordInfo ? this.config.getRecordInfo(editableEl) : {};\n    }\n}\n", "import { Component, useState } from \"@odoo/owl\";\nimport { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\nimport { toolbarButtonProps } from \"@html_editor/main/toolbar/toolbar\";\nimport { useChildRef } from \"@web/core/utils/hooks\";\nimport { useDropdownAutoVisibility } from \"@html_editor/dropdown_autovisibility_hook\";\n\nexport class ImageToolbarDropdown extends Component {\n    static components = { Dropdown, DropdownItem };\n    static props = {\n        ...toolbarButtonProps,\n        name: String,\n        icon: { type: String, optional: true },\n        onSelected: Function,\n        items: Array,\n        getDisplay: { type: Function, optional: true },\n    };\n    static template = \"html_editor.ImageToolbarDropdown\";\n\n    setup() {\n        this.items = this.props.items;\n        if (this.props.getDisplay) {\n            this.state = useState(this.props.getDisplay());\n        }\n        this.menuRef = useChildRef();\n        useDropdownAutoVisibility(this.env.overlayState, this.menuRef);\n    }\n\n    onSelected(item) {\n        this.props.onSelected(item);\n    }\n}\n", "import { Component, useExternalListener, useState } from \"@odoo/owl\";\nimport { toolbarButtonProps } from \"@html_editor/main/toolbar/toolbar\";\nimport { registry } from \"@web/core/registry\";\nimport { ImageTransformation } from \"./image_transformation\";\n\nexport function useImageTransform({ document, closeImageTransformation, buttonSelector }) {\n    let pointerDownInsideTransform = false;\n\n    // We close the image transform when we click outside any element not\n    // related to it. When the pointerdown of the click is inside the image\n    // transform and pointerup is outside while resizing or rotating the\n    // image it will consider the click as being done outside image transform.\n    // So we need to keep track if the pointerdown is inside or outside to know\n    // if we want to close the image transform component or not.\n    useExternalListener(document, \"pointerdown\", (ev) => {\n        if (isNodeInsideTransform(ev.target)) {\n            pointerDownInsideTransform = true;\n        } else {\n            closeImageTransformation();\n            pointerDownInsideTransform = false;\n        }\n    });\n    useExternalListener(\n        document,\n        \"click\",\n        (ev) => {\n            if (!isNodeInsideTransform(ev.target) && !pointerDownInsideTransform) {\n                closeImageTransformation();\n            }\n            pointerDownInsideTransform = false;\n        },\n        { capture: true }\n    );\n    // When we click on any character the image is deleted and we need to close\n    // the image transform. We handle this by selectionchange.\n    useExternalListener(document, \"selectionchange\", (ev) => {\n        closeImageTransformation();\n    });\n\n    function isNodeInsideTransform(node) {\n        if (!node) {\n            return false;\n        }\n        if (node.nodeType === Node.TEXT_NODE) {\n            node = node.parentElement;\n        }\n        if (node.matches(buttonSelector)) {\n            return true;\n        }\n        if (isImageTransformationOpen() && node.matches(\".transfo-controls, .transfo-controls *\")) {\n            return true;\n        }\n        return false;\n    }\n\n    function isImageTransformationOpen() {\n        return registry.category(\"main_components\").contains(\"ImageTransformation\");\n    }\n\n    return { isImageTransformationOpen };\n}\n\nexport class ImageTransformButton extends Component {\n    static template = \"html_editor.ImageTransformButton\";\n    static props = {\n        id: String,\n        icon: String,\n        title: String,\n        getTargetedImage: Function,\n        resetImageTransformation: Function,\n        addStep: Function,\n        document: { validate: (p) => p.nodeType === Node.DOCUMENT_NODE },\n        editable: { validate: (p) => p.nodeType === Node.ELEMENT_NODE },\n        ...toolbarButtonProps,\n        activeTitle: String,\n    };\n\n    setup() {\n        this.state = useState({ active: false });\n        this.transform = useImageTransform({\n            document: this.props.document,\n            closeImageTransformation: this.closeImageTransformation.bind(this),\n            buttonSelector: '[name=\"image_transform\"], [name=\"image_transform\"] *',\n        });\n    }\n\n    onButtonClick() {\n        this.handleImageTransformation(this.props.getTargetedImage());\n    }\n\n    handleImageTransformation(image) {\n        if (this.transform.isImageTransformationOpen()) {\n            this.props.resetImageTransformation(image);\n            this.closeImageTransformation();\n        } else {\n            this.openImageTransformation(image);\n        }\n    }\n\n    openImageTransformation(image) {\n        this.state.active = true;\n        registry.category(\"main_components\").add(\"ImageTransformation\", {\n            Component: ImageTransformation,\n            props: {\n                image,\n                document: this.props.document,\n                editable: this.props.editable,\n                destroy: () => this.closeImageTransformation(),\n                onChange: () => this.props.addStep(),\n            },\n        });\n    }\n\n    closeImageTransformation() {\n        this.state.active = false;\n        if (this.transform.isImageTransformationOpen()) {\n            registry.category(\"main_components\").remove(\"ImageTransformation\");\n        }\n    }\n}\n", "/*\nCopyright (c) 2014 Christophe Matthieu,\n\nPermission is hereby granted, free of charge, to any person\nobtaining a copy of this software and associated documentation\nfiles (the \"Software\"), to deal in the Software without\nrestriction, including without limitation the rights to use,\ncopy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the\nSoftware is furnished to do so, subject to the following\nconditions:\n\nThe above copyright notice and this permission notice shall be\nincluded in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES\nOF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\nNONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT\nHOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\nWHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\nFROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR\nOTHER DEALINGS IN THE SOFTWARE.\n*/\n\nimport { Component, onMounted, useExternalListener, useRef } from \"@odoo/owl\";\nimport { useHotkey } from \"@web/core/hotkeys/hotkey_hook\";\nimport { usePositionHook } from \"@html_editor/position_hook\";\n\nconst rad = Math.PI / 180;\nconst MIN_IMAGE_SIZE = 20;\n\nexport class ImageTransformation extends Component {\n    static template = \"html_editor.ImageTransformation\";\n    static props = {\n        document: { validate: (p) => p.nodeType === Node.DOCUMENT_NODE },\n        editable: { validate: (p) => p.nodeType === Node.ELEMENT_NODE },\n        image: { validate: (p) => p.tagName === \"IMG\" },\n        destroy: { type: Function },\n        onChange: { type: Function },\n        onApply: { type: Function, optional: true },\n        onComponentMounted: { type: Function, optional: true },\n    };\n    static defaultProps = {\n        onComponentMounted: () => {},\n    };\n\n    setup() {\n        this.isCurrentlyTransforming = false;\n        this.document = this.props.document;\n        this.image = this.props.image;\n        this.transfoContainer = useRef(\"transfoContainer\");\n        this.transfoControls = useRef(\"transfoControls\");\n        this.transfoCenter = useRef(\"transfoCenter\");\n        this.computeImageTransformations();\n        onMounted(() => {\n            this.positionTransfoContainer();\n            this.props.onComponentMounted();\n        });\n        useExternalListener(window, \"mousemove\", this.mouseMove);\n        useExternalListener(window, \"mouseup\", this.mouseUp);\n        if (this.document.defaultView.frameElement) {\n            const iframeWindow = this.document.defaultView;\n            useExternalListener(iframeWindow, \"mousemove\", this.mouseMove);\n            useExternalListener(iframeWindow, \"mouseup\", this.mouseUp);\n        }\n        // When a character key is pressed and the image gets deleted,\n        // close the image transform via selectionchange.\n        useExternalListener(this.document, \"selectionchange\", () => this.destroy());\n        // Backspace/Delete don\u2019t trigger selectionchange on image\n        // delete in Chrome, so we use keydown event.\n        useExternalListener(this.document, \"keydown\", (ev) => {\n            if ([\"Backspace\", \"Delete\"].includes(ev.key)) {\n                this.destroy();\n            }\n        });\n        useHotkey(\"escape\", () => this.destroy());\n        usePositionHook({ el: this.props.editable }, this.document, () => {\n            if (!this.isCurrentlyTransforming) {\n                this.resetHandlers();\n            }\n        });\n    }\n\n    destroy() {\n        this.props.onApply?.();\n        this.props.destroy();\n    }\n\n    mouseMove(ev) {\n        if (!this.transfo.active) {\n            return;\n        }\n        ev.preventDefault();\n        const settings = this.transfo.settings;\n        const center = this.transfo.active.center;\n        const cdx = center.left - ev.pageX;\n        const cdy = center.top - ev.pageY;\n        if (this.transfo.active.type == \"rotator\") {\n            let ang;\n            const dang = Math.atan(settings.width / settings.height) / rad;\n\n            if (cdy) {\n                ang = Math.atan(-cdx / cdy) / rad;\n            } else {\n                ang = 0;\n            }\n            if (ev.pageY >= center.top && ev.pageX >= center.left) {\n                ang += 180;\n            } else if (ev.pageY >= center.top && ev.pageX < center.left) {\n                ang += 180;\n            } else if (ev.pageY < center.top && ev.pageX < center.left) {\n                ang += 360;\n            }\n\n            ang -= dang;\n\n            if (!ev.ctrlKey) {\n                settings.angle =\n                    Math.round(ang / this.transfo.settings.rotationStep) *\n                    this.transfo.settings.rotationStep;\n            } else {\n                settings.angle = ang;\n            }\n\n            // reset position : don't move center\n            this.positionTransfoContainer();\n            const new_center = this.getOffset(this.transfoCenter.el);\n            const x = center.left - new_center.left;\n            const y = center.top - new_center.top;\n            const angle = ang * rad;\n            settings.translatex += x * Math.cos(angle) - y * Math.sin(-angle);\n            settings.translatey += -x * Math.sin(angle) + y * Math.cos(-angle);\n        } else if (this.transfo.active.type == \"position\") {\n            const angle = settings.angle * rad;\n            const x = ev.pageX - this.transfo.active.pageX;\n            const y = ev.pageY - this.transfo.active.pageY;\n            this.transfo.active.pageX = ev.pageX;\n            this.transfo.active.pageY = ev.pageY;\n            const dx = x * Math.cos(angle) - y * Math.sin(-angle);\n            const dy = -x * Math.sin(angle) + y * Math.cos(-angle);\n\n            settings.translatex += dx;\n            settings.translatey += dy;\n        } else if (this.transfo.active.type.length === 2) {\n            const width = this.transfo.active.width;\n            const height = this.transfo.active.height;\n            const deltaX = ev.pageX - this.transfo.active.pageX;\n            const deltaY = ev.pageY - this.transfo.active.pageY;\n\n            let newWidth = width;\n            let newHeight = height;\n\n            if (this.transfo.active.type.indexOf(\"t\") != -1) {\n                newHeight = height - deltaY;\n            }\n            if (this.transfo.active.type.indexOf(\"b\") != -1) {\n                newHeight = height + deltaY;\n            }\n            if (this.transfo.active.type.indexOf(\"l\") != -1) {\n                newWidth = width - deltaX;\n            }\n            if (this.transfo.active.type.indexOf(\"r\") != -1) {\n                newWidth = width + deltaX;\n            }\n\n            // Ensure minimum dimensions\n            if (newWidth < MIN_IMAGE_SIZE) {\n                newWidth = MIN_IMAGE_SIZE;\n            }\n            if (newHeight < MIN_IMAGE_SIZE) {\n                newHeight = MIN_IMAGE_SIZE;\n            }\n\n            if (\n                ev.shiftKey &&\n                (this.transfo.active.type === \"tl\" ||\n                    this.transfo.active.type === \"bl\" ||\n                    this.transfo.active.type === \"tr\" ||\n                    this.transfo.active.type === \"br\")\n            ) {\n                const aspectRatio = width / height;\n                if (Math.abs(deltaX) > Math.abs(deltaY)) {\n                    newHeight = newWidth / aspectRatio;\n                } else {\n                    newWidth = newHeight * aspectRatio;\n                }\n            }\n            this.image.style.width = newWidth + \"px\";\n            this.image.style.height = newHeight + \"px\";\n            settings.width = newWidth;\n            settings.height = newHeight;\n        }\n\n        settings.angle = Math.round(settings.angle);\n        settings.translatex = Math.round(settings.translatex);\n        settings.translatey = Math.round(settings.translatey);\n\n        // When rotating, the offset used for the rotation center must be stable.\n        // getOffset normally includes CSS transforms, which would move the\n        // transfoCenter on each call and cause flickering.\n        // Temporarily remove the transform to compute the correct static position.\n        const prevImageTransform = this.image.style.transform;\n        this.image.style.transform = \"\";\n        this.transfo.settings.pos = this.getOffset(this.image);\n        this.image.style.transform = prevImageTransform;\n\n        this.positionTransfoContainer();\n    }\n\n    mouseUp() {\n        this.isCurrentlyTransforming = false;\n        this.transfo.active = null;\n        this.props.onApply?.();\n        this.props.onChange();\n    }\n\n    mouseDown(ev) {\n        if (this.transfo.active) {\n            return;\n        }\n        this.isCurrentlyTransforming = true;\n        let type = \"position\";\n        const target = ev.target.closest(\"div\");\n\n        if (target.classList.contains(\"transfo-rotator\")) {\n            type = \"rotator\";\n        } else if (target.classList.contains(\"transfo-scaler-tl\")) {\n            type = \"tl\";\n        } else if (target.classList.contains(\"transfo-scaler-tr\")) {\n            type = \"tr\";\n        } else if (target.classList.contains(\"transfo-scaler-br\")) {\n            type = \"br\";\n        } else if (target.classList.contains(\"transfo-scaler-bl\")) {\n            type = \"bl\";\n        } else if (target.classList.contains(\"transfo-scaler-tc\")) {\n            type = \"tc\";\n        } else if (target.classList.contains(\"transfo-scaler-bc\")) {\n            type = \"bc\";\n        } else if (target.classList.contains(\"transfo-scaler-ml\")) {\n            type = \"ml\";\n        } else if (target.classList.contains(\"transfo-scaler-mr\")) {\n            type = \"mr\";\n        }\n\n        this.transfo.active = {\n            type: type,\n            pageX: ev.pageX,\n            pageY: ev.pageY,\n            width: parseFloat(getComputedStyle(this.image).width),\n            height: parseFloat(getComputedStyle(this.image).height),\n            center: this.getOffset(this.transfoCenter.el),\n        };\n    }\n\n    computeImageTransformations() {\n        this.transfo = {};\n        const transform = this.image.style.transform || \"\";\n\n        this.transfo.settings = {};\n\n        this.transfo.settings.angle =\n            transform.indexOf(\"rotate\") != -1\n                ? parseFloat(transform.match(/rotate\\(([^)]+)deg\\)/)[1])\n                : 0;\n\n        this.image.style.transform = \"\";\n\n        this.transfo.settings.pos = this.getOffset(this.image);\n        this.transfo.settings.width = parseFloat(getComputedStyle(this.image).width);\n        this.transfo.settings.height = parseFloat(getComputedStyle(this.image).height);\n\n        const translatex = transform.match(/translateX\\(([0-9.-]+)(%|px)\\)/);\n        const translatey = transform.match(/translateY\\(([0-9.-]+)(%|px)\\)/);\n        this.transfo.settings.translate = \"%\";\n\n        if (translatex && translatex[2] === \"%\") {\n            this.transfo.settings.translatexp = parseFloat(translatex[1]);\n            this.transfo.settings.translatex =\n                (this.transfo.settings.translatexp / 100) * this.transfo.settings.width;\n        } else {\n            this.transfo.settings.translatex = translatex ? parseFloat(translatex[1]) : 0;\n        }\n        if (translatey && translatey[2] === \"%\") {\n            this.transfo.settings.translateyp = parseFloat(translatey[1]);\n            this.transfo.settings.translatey =\n                (this.transfo.settings.translateyp / 100) * this.transfo.settings.height;\n        } else {\n            this.transfo.settings.translatey = translatey ? parseFloat(translatey[1]) : 0;\n        }\n\n        this.transfo.settings.css = window.getComputedStyle(this.image, null);\n        this.transfo.settings.rotationStep = 5;\n    }\n\n    positionTransfoContainer() {\n        const settings = this.transfo.settings;\n        const width = parseFloat(getComputedStyle(this.image).width);\n        const height = parseFloat(getComputedStyle(this.image).height);\n        settings.translatexp = Math.round((settings.translatex / width) * 1000) / 10;\n        settings.translateyp = Math.round((settings.translatey / height) * 1000) / 10;\n\n        this.setImageTransformation(this.image);\n\n        this.transfoContainer.el.style.position = \"absolute\";\n        this.transfoContainer.el.style.width = width + \"px\";\n        this.transfoContainer.el.style.height = height + \"px\";\n        this.transfoContainer.el.style.top = settings.pos.top + \"px\";\n        this.transfoContainer.el.style.left = settings.pos.left + \"px\";\n\n        const controls = this.transfoControls.el;\n\n        this.setImageTransformation(controls);\n        controls.style.width = width + \"px\";\n        controls.style.height = height + \"px\";\n        controls.style.cursor = \"move\";\n    }\n\n    setImageTransformation(element) {\n        let transform = \"\";\n        if (this.transfo.settings.angle !== 0) {\n            transform += \" rotate(\" + this.transfo.settings.angle + \"deg) \";\n        }\n        if (this.transfo.settings.translatex) {\n            transform +=\n                \" translateX(\" +\n                (this.transfo.settings.translate === \"%\"\n                    ? this.transfo.settings.translatexp + \"%\"\n                    : this.transfo.settings.translatex + \"px\") +\n                \") \";\n        }\n        if (this.transfo.settings.translatey) {\n            transform +=\n                \" translateY(\" +\n                (this.transfo.settings.translate === \"%\"\n                    ? this.transfo.settings.translateyp + \"%\"\n                    : this.transfo.settings.translatey + \"px\") +\n                \") \";\n        }\n        element.style.transform = transform;\n    }\n\n    getOffset(target) {\n        if (!target.getClientRects().length) {\n            return { top: 0, left: 0 };\n        } else {\n            const rect = target.getBoundingClientRect();\n            const frameElement = target.ownerDocument.defaultView.frameElement;\n            const offset = { top: 0, left: 0 };\n            if (frameElement) {\n                const frameRect = frameElement.getBoundingClientRect();\n                offset.left += frameRect.left;\n                offset.top += frameRect.top;\n            }\n            return {\n                top: rect.top + window.pageYOffset + offset.top,\n                left: rect.left + window.pageXOffset + offset.left,\n            };\n        }\n    }\n\n    resetHandlers() {\n        this.computeImageTransformations();\n        this.positionTransfoContainer();\n    }\n}\n", "import { Plugin } from \"@html_editor/plugin\";\nimport {\n    ICON_SELECTOR,\n    MEDIA_SELECTOR,\n    EDITABLE_MEDIA_CLASS,\n    isIconElement,\n    isMediaElement,\n    isProtected,\n    isProtecting,\n    paragraphRelatedElementsSelector,\n    isContentEditable,\n} from \"@html_editor/utils/dom_info\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { MediaDialog, TABS } from \"./media_dialog/media_dialog\";\nimport { isHtmlContentSupported } from \"@html_editor/core/selection_plugin\";\nimport { boundariesOut, rightPos } from \"@html_editor/utils/position\";\nimport { withSequence } from \"@html_editor/utils/resource\";\nimport { closestElement } from \"@html_editor/utils/dom_traversal\";\nimport { fuzzyLookup } from \"@web/core/utils/search\";\nimport { FORMATTABLE_TAGS } from \"@html_editor/utils/formatting\";\n\n/**\n * @typedef { Object } MediaShared\n * @property { MediaPlugin['openMediaDialog'] } openMediaDialog\n */\n\n/**\n * @typedef {((mediaEl: HTMLElement) => void)[]} after_save_media_dialog_handlers\n * @typedef {((arg: { newMediaEl: HTMLElement }) => void)[]} on_added_media_handlers\n * @typedef {((elements: HTMLElement[], params: { node: Node }) => Promise<void>)[]} on_media_dialog_saved_handlers\n * @typedef {((arg: { newMediaEl: HTMLElement }) => void)[]} on_replaced_media_handlers\n * @typedef {((args: {imageEl: HTMLElement}) => void)[]} on_image_saved_handlers\n *\n * @typedef {{\n *      id: \"DOCUMENTS\" | \"ICONS\" | \"IMAGES\" | \"VIDEOS\";\n *      title: import(\"plugins\").TranslatedString;\n *      Component: import(\"@odoo/owl\").Component;\n *      sequence: number;\n *  }[]} media_dialog_extra_tabs\n */\n\nexport class MediaPlugin extends Plugin {\n    static id = \"media\";\n    static dependencies = [\"selection\", \"history\", \"dom\", \"dialog\"];\n    static shared = [\"openMediaDialog\"];\n    static defaultConfig = {\n        allowImage: true,\n        allowMediaDocuments: true,\n    };\n    /** @type {import(\"plugins\").EditorResources} */\n    resources = {\n        user_commands: [\n            {\n                id: \"replaceImage\",\n                description: _t(\"Replace media\"),\n                icon: \"fa-exchange\",\n                run: this.replaceImage.bind(this),\n                isAvailable: isHtmlContentSupported,\n            },\n            {\n                id: \"insertMedia\",\n                title: _t(\"Media\"),\n                description: this.config.allowVideo\n                    ? _t(\"Insert image, icon or video\")\n                    : _t(\"Insert image or icon\"),\n                icon: \"fa-file-image-o\",\n                run: (params, context = {}) =>\n                    this.openMediaDialog({\n                        activeTab: this.getActiveDialogTab(context.searchTerm),\n                    }),\n                isAvailable: isHtmlContentSupported,\n            },\n        ],\n        toolbar_groups: withSequence(31, { id: \"replace_image\", namespaces: [\"image\"] }),\n        toolbar_items: [\n            {\n                id: \"replace_image\",\n                groupId: \"replace_image\",\n                commandId: \"replaceImage\",\n            },\n        ],\n        powerbox_categories: withSequence(40, { id: \"media\", name: _t(\"Media\") }),\n        ...(this.config.allowImage && {\n            powerbox_items: this.getInsertMediaPowerboxItem(),\n        }),\n        power_buttons: withSequence(1, { commandId: \"insertMedia\" }),\n        closest_savable_providers: withSequence(20, (el) => this.editable),\n\n        /** Handlers */\n        clean_for_save_handlers: ({ root }) => this.cleanForSave(root),\n        normalize_handlers: this.normalizeMedia.bind(this),\n        selectionchange_handlers: this.selectAroundIcon.bind(this),\n\n        unsplittable_node_predicates: isIconElement, // avoid merge\n        is_node_editable_predicates: this.isEditableMediaElement.bind(this),\n        clipboard_content_processors: this.clean.bind(this),\n        clipboard_text_processors: (text) => text.replace(/\\u200B/g, \"\"),\n        functional_empty_node_predicates: isMediaElement,\n\n        selectors_for_feff_providers: () =>\n            `:is(${paragraphRelatedElementsSelector}, ${FORMATTABLE_TAGS.join(\n                \", \"\n            )}, A) > :is(${ICON_SELECTOR})`,\n    };\n\n    setup() {\n        this.availableTabs = [\n            ...Object.values(TABS),\n            ...this.getResource(\"media_dialog_extra_tabs\"),\n        ];\n    }\n\n    getInsertMediaPowerboxItem() {\n        const self = this;\n        return {\n            categoryId: \"media\",\n            commandId: \"insertMedia\",\n            // Evaluation is deferred because this.availableTabs is only ready after setup.\n            get keywords() {\n                return self.availableTabs.map((tab) => tab.title);\n            },\n        };\n    }\n\n    getRecordInfo(editableEl = null) {\n        return this.config.getRecordInfo ? this.config.getRecordInfo(editableEl) : {};\n    }\n\n    isEditableMediaElement(node) {\n        if (\n            (isMediaElement(node) || node.nodeName === \"IMG\") &&\n            (node.classList.contains(EDITABLE_MEDIA_CLASS) || isContentEditable(node))\n        ) {\n            return true;\n        }\n    }\n\n    replaceImage() {\n        const targetedNodes = this.dependencies.selection.getTargetedNodes();\n        const node = targetedNodes.find((node) => node.tagName === \"IMG\");\n        if (node) {\n            this.openMediaDialog({ node });\n            this.dependencies.history.addStep();\n        }\n    }\n\n    normalizeMedia(node) {\n        const mediaElements = [...node.querySelectorAll(MEDIA_SELECTOR)];\n        if (node.matches(MEDIA_SELECTOR)) {\n            mediaElements.push(node);\n        }\n        for (const el of mediaElements) {\n            if (isProtected(el) || isProtecting(el)) {\n                continue;\n            }\n            el.setAttribute(\n                \"contenteditable\",\n                el.hasAttribute(\"contenteditable\") ? el.getAttribute(\"contenteditable\") : \"false\"\n            );\n            // Do not update the text if it's already OK to avoid recording a\n            // mutation on Firefox. (Chrome filters them out.)\n            if (isIconElement(el) && el.textContent !== \"\\u200B\") {\n                el.textContent = \"\\u200B\";\n            }\n        }\n    }\n\n    clean(root) {\n        for (const el of root.querySelectorAll(MEDIA_SELECTOR)) {\n            if (isIconElement(el)) {\n                el.textContent = \"\";\n            }\n        }\n    }\n\n    cleanForSave(root) {\n        for (const el of root.querySelectorAll(MEDIA_SELECTOR)) {\n            if (isIconElement(el)) {\n                el.textContent = \"\";\n            }\n            el.removeAttribute(\"contenteditable\");\n        }\n    }\n\n    async onSaveMediaDialog(element, { node }) {\n        if (!element) {\n            // @todo @phoenix to remove\n            throw new Error(\"Element is required: onSaveMediaDialog\");\n            // return;\n        }\n        if (node) {\n            const changedIcon = isIconElement(node) && isIconElement(element);\n            if (changedIcon) {\n                // Preserve tag name when changing an icon and not recreate the\n                // editors unnecessarily.\n                for (const attribute of element.attributes) {\n                    node.setAttribute(attribute.nodeName, attribute.nodeValue);\n                }\n                element = node;\n            } else {\n                node.replaceWith(element);\n            }\n            this.dispatchTo(\"on_replaced_media_handlers\", { newMediaEl: element });\n        } else {\n            this.dependencies.dom.insert(element);\n            this.dispatchTo(\"on_added_media_handlers\", { newMediaEl: element });\n        }\n        // Collapse selection after the inserted/replaced element.\n        const [anchorNode, anchorOffset] = rightPos(element);\n        this.dependencies.selection.setSelection({ anchorNode, anchorOffset });\n        this.dispatchTo(\"after_save_media_dialog_handlers\", element);\n        this.dependencies.history.addStep();\n    }\n\n    openMediaDialog(params = {}, editableEl = null) {\n        const oldSave =\n            params.save || ((element) => this.onSaveMediaDialog(element, { node: params.node }));\n        params.save = async (...args) => {\n            const selection = args[0];\n            const elements = selection\n                ? selection[Symbol.iterator]\n                    ? selection\n                    : [selection]\n                : [];\n            for (const onMediaDialogSaved of this.getResource(\"on_media_dialog_saved_handlers\")) {\n                await onMediaDialogSaved(elements, { node: params.node });\n            }\n            return oldSave(...args);\n        };\n        const { resModel, resId, field, type } = this.getRecordInfo(editableEl);\n        const mediaDialogClosedPromise = this.dependencies.dialog.addDialog(MediaDialog, {\n            resModel,\n            resId,\n            useMediaLibrary: !!(\n                field &&\n                ((resModel === \"ir.ui.view\" && field === \"arch\") || type === \"html\")\n            ), // @todo @phoenix: should be removed and moved to config.mediaModalParams\n            media: params.node,\n            onAttachmentChange: this.config.onAttachmentChange || (() => {}),\n            noImages: !this.config.allowImage,\n            extraTabs: this.getResource(\"media_dialog_extra_tabs\"),\n            ...this.config.mediaModalParams,\n            ...params,\n        });\n        return mediaDialogClosedPromise;\n    }\n\n    /**\n     * @param {import(\"@html_editor/core/selection_plugin\").SelectionData} param0\n     */\n    selectAroundIcon({ editableSelection }) {\n        if (!editableSelection.isCollapsed) {\n            return;\n        }\n        const iconEl = closestElement(editableSelection.anchorNode, isIconElement);\n        if (!iconEl) {\n            return;\n        }\n        const [anchorNode, anchorOffset, focusNode, focusOffset] = boundariesOut(iconEl);\n        const iconOuterBoundaries = { anchorNode, anchorOffset, focusNode, focusOffset };\n        this.dependencies.selection.setSelection(iconOuterBoundaries);\n    }\n\n    /**\n     * @param {string} searchTerm\n     * @returns {string|undefined}\n     */\n    getActiveDialogTab(searchTerm) {\n        if (!searchTerm) {\n            return undefined;\n        }\n        const matchedTabs = fuzzyLookup(searchTerm, this.availableTabs, (tab) => tab.title);\n        if (!matchedTabs.length) {\n            return undefined;\n        }\n        return matchedTabs[0].id;\n    }\n}\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { VideoSelector } from \"./media_dialog/video_selector\";\nimport { _t } from \"@web/core/l10n/translation\";\n\nexport class VideoPlugin extends Plugin {\n    static id = \"video\";\n    static defaultConfig = {\n        allowVideo: true,\n    };\n    /** @type {import(\"plugins\").EditorResources} */\n    resources = {\n        ...(this.config.allowVideo && {\n            media_dialog_extra_tabs: {\n                id: \"VIDEOS\",\n                title: _t(\"Videos\"),\n                Component: this.componentForMediaDialog,\n                sequence: 30,\n            },\n        }),\n    };\n\n    get componentForMediaDialog() {\n        return VideoSelector;\n    }\n}\n", "import { useNativeDraggable } from \"@html_editor/utils/drag_and_drop\";\nimport { childNodeIndex, endPos, leftPos, nodeSize, rightPos } from \"@html_editor/utils/position\";\nimport { xml } from \"@odoo/owl\";\nimport { Plugin } from \"../plugin\";\nimport { closestElement } from \"../utils/dom_traversal\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { baseContainerGlobalSelector } from \"@html_editor/utils/base_container\";\nimport { getDeepestPosition, isContentEditable } from \"@html_editor/utils/dom_info\";\n\n/** @typedef {import(\"plugins\").CSSSelector} CSSSelector */\n\n/**\n * @typedef {CSSSelector[]} move_node_blacklist_selectors\n * @typedef {CSSSelector[]} move_node_whitelist_selectors\n * @typedef {((movableElement: HTMLElement) => void)[]} set_movable_element_handlers\n * @typedef {(() => void)[]} unset_movable_element_handlers\n */\n\nconst WIDGET_CONTAINER_WIDTH = 25;\nconst WIDGET_MOVE_SIZE = 20;\n\nconst ALLOWED_ELEMENTS = \"h1, h2, h3, p, hr, pre, blockquote, li\";\n\nexport class MoveNodePlugin extends Plugin {\n    static id = \"movenode\";\n    static dependencies = [\"baseContainer\", \"selection\", \"history\", \"position\", \"localOverlay\"];\n    /** @type {import(\"plugins\").EditorResources} */\n    resources = {\n        layout_geometry_change_handlers: () => {\n            if (this.currentMovableElement) {\n                this.setMovableElement(this.currentMovableElement);\n            }\n            this.updateHooks();\n        },\n    };\n\n    setup() {\n        this.intersectionObserver = new IntersectionObserver(\n            this.intersectionObserverCallback.bind(this),\n            {\n                root: document,\n            }\n        );\n        this.visibleMovableElements = new Set();\n\n        this.elementHookMap = new Map();\n\n        this.addDomListener(this.editable, \"mousemove\", this.onMousemove, true);\n        this.addDomListener(this.editable, \"touchmove\", this.onMousemove, true);\n        this.addDomListener(this.document, \"keydown\", this.onDocumentKeydown, true);\n        this.addDomListener(this.document, \"mousemove\", this.onDocumentMousemove, true);\n        this.addDomListener(this.document, \"touchmove\", this.onDocumentMousemove, true);\n\n        // This container help to add zone into which the mouse can activate the move widget.\n        this.widgetHookContainer = this.dependencies.localOverlay.makeLocalOverlay(\n            \"oe-widget-hooks-container\"\n        );\n        // This container contains the differents widgets.\n        this.widgetContainer =\n            this.dependencies.localOverlay.makeLocalOverlay(\"oe-widgets-container\");\n        // This container contains the jquery helper element.\n        this.dragHelperContainer = this.dependencies.localOverlay.makeLocalOverlay(\n            \"oe-movenode-helper-container\"\n        );\n        // This container contains drop zones. They are the zones that handle where the drop should happen.\n        this.dropzonesContainer =\n            this.dependencies.localOverlay.makeLocalOverlay(\"oe-dropzones-container\");\n        // This container contains drop hint. The final rectangle showed to the user.\n        this.dropzoneHintContainer = this.dependencies.localOverlay.makeLocalOverlay(\n            \"oe-dropzone-hint-container\"\n        );\n\n        // Uncomment line for debugging tranparent zones\n        // this.widgetHookContainer.classList.add(\"debug\");\n        // this.dropzonesContainer.classList.add(\"debug\");\n\n        this.scrollableElement = closestElement(this.editable.parentElement);\n        while (\n            this.scrollableElement &&\n            getComputedStyle(this.scrollableElement).overflowY !== \"auto\"\n        ) {\n            this.scrollableElement = this.scrollableElement.parentElement;\n        }\n        this.scrollableElement = this.scrollableElement || this.editable;\n\n        this.resetHooksNextMousemove = true;\n        this.mutationObserver = new MutationObserver(() => {\n            this.resetHooksNextMousemove = true;\n            this.removeMoveWidget();\n        });\n        this.mutationObserver.observe(this.editable, {\n            childList: true,\n            subtree: true,\n            characterData: true,\n            characterDataOldValue: true,\n        });\n    }\n    destroy() {\n        super.destroy();\n        this.intersectionObserver.disconnect();\n        this.mutationObserver.disconnect();\n        this.smoothScrollOnDrag && this.smoothScrollOnDrag.destroy();\n    }\n    intersectionObserverCallback(entries) {\n        for (const entry of entries) {\n            const element = entry.target;\n            if (entry.isIntersecting && element.isConnected) {\n                this.visibleMovableElements.add(element);\n                this.resetHooksNextMousemove = true;\n            } else {\n                this.visibleMovableElements.delete(element);\n                const hookElement = this.elementHookMap.get(element);\n                if (hookElement) {\n                    // If hookElement is undefined, it means that this callback\n                    // was called after a new element was inserted in the\n                    // editable, but before the next updateHooks. The hook will\n                    // be created when that happens.\n                    hookElement.style.display = `none`;\n                }\n            }\n        }\n    }\n    updateHooks() {\n        const editableStyles = getComputedStyle(this.editable);\n        this.editableRect = this.editable.getBoundingClientRect();\n        const paddingLeft = parseInt(editableStyles.paddingLeft, 10) || 0;\n        this.editableRect.x = this.editableRect.x + paddingLeft - (WIDGET_CONTAINER_WIDTH + 5);\n        this.editableRect.width =\n            this.editableRect.width - paddingLeft + (WIDGET_CONTAINER_WIDTH + 5);\n        const containerRect = this.widgetHookContainer.getBoundingClientRect();\n        const elements = this.getMovableElements();\n\n        const elementsToGarbageCollect = new Set(this.elementHookMap.keys());\n        for (const index in elements) {\n            const element = elements[index];\n            elementsToGarbageCollect.delete(element);\n            let hookElement = this.elementHookMap.get(element);\n            if (!hookElement) {\n                hookElement = document.createElement(\"div\");\n                this.elementHookMap.set(element, hookElement);\n                hookElement.classList.add(\"oe-dropzone-hook\");\n                hookElement.addEventListener(\"mouseenter\", () => {\n                    if (element !== this.currentMovableElement) {\n                        this.setMovableElement(element);\n                    }\n                });\n                this.widgetHookContainer.append(hookElement);\n                hookElement.style.display = `none`;\n\n                this.intersectionObserver.observe(element);\n            }\n            hookElement.style.zIndex = index;\n        }\n        // For all the elements that are not in the dom, remove their\n        // corresponding hook.\n        for (const element of elementsToGarbageCollect) {\n            this.visibleMovableElements.delete(element);\n            this.elementHookMap.get(element).remove();\n            this.intersectionObserver.unobserve(element);\n            this.elementHookMap.delete(element);\n        }\n\n        const visibleElements = [...this.visibleMovableElements];\n        // Prevent layout thrashing by computing all the rects in advance.\n        const elementRects = visibleElements.map((element) => element.getBoundingClientRect());\n        for (const index in visibleElements) {\n            const element = visibleElements[index];\n            const elementRect = elementRects[index];\n            const hookElement = this.elementHookMap.get(element);\n\n            const style = getComputedStyle(element);\n            const marginTop = parseInt(style.marginTop, 10) || 0;\n            const marginBottom = parseInt(style.marginBottom, 10) || 0;\n            let hookBox;\n            if (element.tagName === \"HR\") {\n                hookBox = new DOMRect(\n                    elementRect.x - containerRect.left - WIDGET_CONTAINER_WIDTH,\n                    elementRect.y - containerRect.top - marginTop,\n                    elementRect.width + WIDGET_CONTAINER_WIDTH,\n                    elementRect.height + marginTop + marginBottom\n                );\n            } else if (element.tagName === \"LI\") {\n                // For <li>, move hookBox to the left to avoid blocking\n                // checkboxes \u2014 needed for proper list item interaction.\n                hookBox = new DOMRect(\n                    elementRect.x - containerRect.left - WIDGET_CONTAINER_WIDTH - WIDGET_MOVE_SIZE,\n                    elementRect.y - containerRect.top - marginTop,\n                    WIDGET_CONTAINER_WIDTH,\n                    elementRect.height + marginTop + marginBottom\n                );\n            } else {\n                hookBox = new DOMRect(\n                    elementRect.x - containerRect.left - WIDGET_CONTAINER_WIDTH,\n                    elementRect.y - containerRect.top - marginTop,\n                    WIDGET_CONTAINER_WIDTH,\n                    elementRect.height + marginTop + marginBottom\n                );\n            }\n\n            hookElement.style.left = `${hookBox.x}px`;\n            hookElement.style.top = `${hookBox.y}px`;\n            hookElement.style.width = `${hookBox.width}px`;\n            hookElement.style.height = `${hookBox.height}px`;\n            hookElement.style.display = `block`;\n        }\n    }\n    _updateAnchorWidgets(newAnchorWidget) {\n        const movableElement =\n            newAnchorWidget &&\n            closestElement(\n                newAnchorWidget,\n                (node) =>\n                    this.isNodeMovable(node) &&\n                    node.matches(\n                        [\n                            ALLOWED_ELEMENTS,\n                            baseContainerGlobalSelector,\n                            ...this.getResource(\"move_node_whitelist_selectors\"),\n                        ].join(\", \")\n                    )\n            );\n\n        if (movableElement && movableElement !== this.currentMovableElement) {\n            this.setMovableElement(movableElement);\n        }\n    }\n    getMovableElements() {\n        const elems = [];\n        for (const el of this.editable.querySelectorAll(\n            [\n                ALLOWED_ELEMENTS,\n                baseContainerGlobalSelector,\n                ...this.getResource(\"move_node_whitelist_selectors\"),\n            ].join(\", \")\n        )) {\n            if (this.isNodeMovable(el)) {\n                elems.push(el);\n            }\n        }\n        return elems;\n    }\n    getDroppableElements(draggableNode) {\n        return this.getMovableElements().filter(\n            (node) => !closestElement(node.parentElement, (n) => n === draggableNode)\n        );\n    }\n    setMovableElement(movableElement) {\n        this.removeMoveWidget();\n        this.currentMovableElement = movableElement;\n        this.dispatchTo(\"set_movable_element_handlers\", movableElement);\n\n        const containerRect = this.widgetContainer.getBoundingClientRect();\n        const anchorBlockRect = this.currentMovableElement.getBoundingClientRect();\n        const anchorX =\n            this.currentMovableElement.tagName === \"LI\"\n                ? anchorBlockRect.x - WIDGET_MOVE_SIZE // Prevent overlap bullets.\n                : anchorBlockRect.x;\n        let anchorY = anchorBlockRect.y;\n        if (this.currentMovableElement.tagName.match(/H[1-6]/)) {\n            anchorY += (anchorBlockRect.height - WIDGET_MOVE_SIZE) / 2;\n        }\n\n        this.moveWidget = this.document.createElement(\"div\");\n        this.moveWidget.className = \"oe-sidewidget-move oi oi-draggable\";\n        this.widgetContainer.append(this.moveWidget);\n\n        let moveWidgetOffsetTop = 0;\n        if (movableElement.tagName === \"HR\") {\n            const style = getComputedStyle(movableElement);\n            moveWidgetOffsetTop = parseInt(style.marginTop, 10) || 0;\n        }\n\n        this.moveWidget.style.width = `${WIDGET_MOVE_SIZE}px`;\n        this.moveWidget.style.height = `${WIDGET_MOVE_SIZE}px`;\n        this.moveWidget.style.top = `${anchorY - containerRect.y - moveWidgetOffsetTop}px`;\n        this.moveWidget.style.left = `${anchorX - containerRect.x - WIDGET_CONTAINER_WIDTH}px`;\n\n        this.services.tooltip.add(this.moveWidget, {\n            template: xml`\n                <div class=\"o-tooltip tooltip-inner text-start px-3\">\n                    ${_t(\"Drag to move\")}<br/>\n                    ${_t(\"Click to select\")}\n                </div>`,\n            arrow: true,\n        });\n\n        this.addDomListener(this.moveWidget, \"click\", () => {\n            const isNodeContentEditable = isContentEditable(movableElement);\n            const [anchorNode, anchorOffset] = isNodeContentEditable\n                ? getDeepestPosition(movableElement, 0)\n                : leftPos(movableElement);\n            const [focusNode, focusOffset] = isNodeContentEditable\n                ? getDeepestPosition(movableElement, nodeSize(movableElement))\n                : rightPos(movableElement);\n            this.dependencies.selection.setSelection({\n                anchorNode,\n                anchorOffset,\n                focusNode,\n                focusOffset,\n            });\n            this.dependencies.selection.focusEditable();\n        });\n\n        if (this.scrollableElement) {\n            this.smoothScrollOnDrag && this.smoothScrollOnDrag.destroy();\n            // TODO: This should be made more generic, one hook for the entire\n            // editable with each element handled.\n            this.smoothScrollOnDrag = useNativeDraggable(simpleDraggableHook, {\n                ref: { el: this.widgetContainer },\n                elements: \".oe-sidewidget-move\",\n                onDragStart: () => this.startDropzones(movableElement, containerRect),\n                onDragEnd: () => this._stopDropzones(movableElement),\n                helper: () => {\n                    const container =\n                        movableElement.tagName === \"LI\"\n                            ? movableElement.parentElement.cloneNode(false)\n                            : document.createElement(\"div\");\n                    if (container.tagName === \"OL\") {\n                        const originalIndex = childNodeIndex(movableElement) + 1;\n                        container.setAttribute(\"start\", originalIndex);\n                    }\n                    container.append(movableElement.cloneNode(true));\n                    const style = getComputedStyle(movableElement);\n                    container.style.height = style.height;\n                    container.style.width = style.width;\n                    container.style.paddingLeft = \"25px\";\n                    container.style.opacity = \"0.4\";\n                    this.dragHelperContainer.append(container);\n                    return container;\n                },\n            });\n        }\n    }\n    removeMoveWidget() {\n        this.dispatchTo(\"unset_movable_element_handlers\");\n        this.moveWidget?.remove();\n        this.moveWidget = undefined;\n        this.currentMovableElement = undefined;\n    }\n    startDropzones(movableElement, containerRect, directions = [\"north\", \"south\"]) {\n        this.removeMoveWidget();\n        const elements = this.getDroppableElements(movableElement);\n\n        this.dropzonesContainer.replaceChildren();\n        this.editable.classList.add(\"oe-editor-dragging\");\n\n        for (const element of elements) {\n            const originalRect = element.getBoundingClientRect();\n            const style = getComputedStyle(element);\n            const marginTop = parseInt(style.marginTop, 10);\n            const marginBottom = parseInt(style.marginBottom, 10);\n            const marginLeft = parseInt(style.marginLeft, 10);\n            const marginRight = parseInt(style.marginRight, 10);\n\n            const dropzoneRect = new DOMRect(\n                originalRect.left - marginLeft - WIDGET_CONTAINER_WIDTH,\n                originalRect.top - marginTop,\n                originalRect.width + marginLeft + marginRight + WIDGET_CONTAINER_WIDTH,\n                originalRect.height + marginTop + marginBottom\n            );\n            const dropzoneHintRect = new DOMRect(\n                originalRect.left - marginLeft,\n                originalRect.top - marginTop,\n                originalRect.width + marginLeft + marginRight,\n                originalRect.height + marginTop + marginBottom\n            );\n\n            const dropzoneBox = document.createElement(\"div\");\n            dropzoneBox.className = `oe-dropzone-box`;\n            dropzoneBox.style.top = `${dropzoneRect.top - containerRect.top}px`;\n            dropzoneBox.style.left =\n                element.tagName == \"LI\"\n                    ? `${dropzoneRect.left - containerRect.left - WIDGET_MOVE_SIZE}px`\n                    : `${dropzoneRect.left - containerRect.left}px`;\n            dropzoneBox.style.width = `${dropzoneRect.width}px`;\n            dropzoneBox.style.height = `${dropzoneRect.height}px`;\n\n            const dropzoneHintBox = document.createElement(\"div\");\n            dropzoneHintBox.className = `oe-dropzone-box`;\n            dropzoneHintBox.style.top = `${dropzoneHintRect.top - containerRect.top}px`;\n            dropzoneHintBox.style.left = `${dropzoneHintRect.left - containerRect.left}px`;\n            dropzoneHintBox.style.width = `${dropzoneHintRect.width}px`;\n            dropzoneHintBox.style.height = `${dropzoneHintRect.height}px`;\n\n            const sideElements = {};\n            for (const direction of directions) {\n                const sideElement = document.createElement(\"div\");\n                sideElement.className = `oe-dropzone-box-side oe-dropzone-box-side-${direction}`;\n                sideElements[direction] = sideElement;\n                dropzoneBox.append(sideElement);\n                const onEnter = () => {\n                    this._currentZone = [direction];\n\n                    removeDropHint();\n                    this._currentDropHint = document.createElement(\"div\");\n                    this._currentDropHint.className = `oe-current-drop-hint`;\n                    const currentDropHintSize = 4;\n                    const currentDropHintSizeHalf = currentDropHintSize / 2;\n\n                    if (direction === \"north\") {\n                        this._currentDropHint.style[\"top\"] = `-${currentDropHintSizeHalf}px`;\n                        this._currentDropHint.style[\"width\"] = `100%`;\n                        this._currentDropHint.style[\"height\"] = `${currentDropHintSize}px`;\n                        dropzoneHintBox.append(this._currentDropHint);\n                        this._currentDropHintElementPosition = [\"top\", element];\n                    } else if (direction === \"south\") {\n                        this._currentDropHint.style[\"bottom\"] = `-${currentDropHintSizeHalf}px`;\n                        this._currentDropHint.style[\"width\"] = `100%`;\n                        this._currentDropHint.style[\"height\"] = `${currentDropHintSize}px`;\n                        dropzoneHintBox.append(this._currentDropHint);\n                        this._currentDropHintElementPosition = [\"bottom\", element];\n                    } else if (direction === \"west\") {\n                        this._currentDropHint.style[\"left\"] = `-${currentDropHintSizeHalf}px`;\n                        this._currentDropHint.style[\"height\"] = `100%`;\n                        this._currentDropHint.style[\"width\"] = `${currentDropHintSize}px`;\n                        dropzoneHintBox.append(this._currentDropHint);\n                        this._currentDropHintElementPosition = [\"left\", element];\n                    } else if (direction === \"east\") {\n                        this._currentDropHint.style[\"right\"] = `-${currentDropHintSizeHalf}px`;\n                        this._currentDropHint.style[\"height\"] = `100%`;\n                        this._currentDropHint.style[\"width\"] = `${currentDropHintSize}px`;\n                        dropzoneHintBox.append(this._currentDropHint);\n                        this._currentDropHintElementPosition = [\"right\", element];\n                    }\n                };\n                sideElement.addEventListener(\"mouseenter\", onEnter);\n                sideElement.addEventListener(\"pointerenter\", onEnter);\n                const removeDropHint = () => {\n                    if (this._currentDropHint) {\n                        this._currentDropHint.remove();\n                        this._currentDropHint = null;\n                    }\n                    this._currentDropHintElementPosition = null;\n                };\n                dropzoneBox.addEventListener(\"mouseleave\", removeDropHint);\n                dropzoneBox.addEventListener(\"pointerleave\", removeDropHint);\n            }\n\n            this.dropzonesContainer.append(dropzoneBox);\n            this.dropzoneHintContainer.append(dropzoneHintBox);\n        }\n    }\n    _stopDropzones(movableElement) {\n        this.editable.classList.remove(\"oe-editor-dragging\");\n        this.dropzonesContainer.replaceChildren();\n        this.dropzoneHintContainer.replaceChildren();\n\n        if (this._currentDropHintElementPosition) {\n            const [position, focusElelement] = this._currentDropHintElementPosition;\n            this._currentDropHintElementPosition = undefined;\n            const previousParent = movableElement.parentElement;\n\n            const isFocusInsideList = [\"UL\", \"OL\"].includes(focusElelement?.parentElement?.tagName);\n            if (movableElement.tagName === \"LI\" && !isFocusInsideList) {\n                // If LI is moved outside a list, wrap it in UL/OL (previous parent)\n                const wrapperList = previousParent.cloneNode(false);\n                wrapperList.appendChild(movableElement);\n                movableElement = wrapperList;\n            } else if (movableElement.tagName !== \"LI\" && isFocusInsideList) {\n                // If non-LI element is moved into a list, wrap it in a LI\n                const wrapperLI = this.document.createElement(\"LI\");\n                wrapperLI.appendChild(movableElement);\n                movableElement = wrapperLI;\n            }\n            if (position === \"top\") {\n                focusElelement.before(movableElement);\n            } else if (position === \"bottom\") {\n                focusElelement.after(movableElement);\n            }\n            if (previousParent.innerHTML.trim() === \"\") {\n                if ([\"UL\", \"OL\"].includes(previousParent.tagName)) {\n                    previousParent.remove();\n                } else {\n                    const baseContainer = this.dependencies.baseContainer.createBaseContainer();\n                    const br = document.createElement(\"br\");\n                    baseContainer.append(br);\n                    previousParent.append(baseContainer);\n                }\n            }\n            const selectionPosition = endPos(movableElement);\n            this.dependencies.selection.setSelection({\n                anchorNode: selectionPosition[0],\n                anchorOffset: selectionPosition[1],\n            });\n            this.dependencies.history.addStep();\n        }\n    }\n    onMousemove(e) {\n        this._updateAnchorWidgets(e.target);\n    }\n    onDocumentKeydown() {\n        // Hide the move widget upon keystroke for visual clarity and provide\n        // visibility to a collaborative avatar.\n        this.removeMoveWidget();\n    }\n    onDocumentMousemove(e) {\n        if (this.resetHooksNextMousemove) {\n            this.resetHooksNextMousemove = false;\n            this.removeMoveWidget();\n            this.updateHooks();\n        }\n        const clientX = e.clientX ?? e.touches?.[0]?.clientX;\n        const clientY = e.clientY ?? e.touches?.[0]?.clientY;\n        if (this.editableRect && !isPointInside(this.editableRect, clientX, clientY)) {\n            this.removeMoveWidget();\n        }\n    }\n    isNodeMovable(node) {\n        const blacklistSelectors = this.getResource(\"move_node_blacklist_selectors\").join(\", \");\n        if (blacklistSelectors && node.matches(blacklistSelectors)) {\n            return false;\n        }\n        return (\n            node.parentElement?.getAttribute(\"contentEditable\") === \"true\" ||\n            (node.tagName === \"LI\" && node.parentElement.isContentEditable)\n        );\n    }\n}\n\nfunction isPointInside(rect, x, y) {\n    return rect.left <= x && rect.right >= x && rect.top <= y && rect.bottom >= y;\n}\n\nconst simpleDraggableHook = {\n    acceptedParams: {\n        helper: [Function],\n    },\n    edgeScrolling: { enable: true },\n    onComputeParams({ ctx, params }) {\n        ctx.helper = params.helper;\n        ctx.followCursor = false;\n        ctx.tolerance = 0;\n    },\n    onDragStart({ ctx }) {\n        ctx.current.element = ctx.helper();\n        ctx.current.element.style.left = `${ctx.pointer.x + 10}px`;\n        ctx.current.element.style.top = `${ctx.pointer.y + 10}px`;\n        ctx.current.element.style.position = \"fixed\";\n        // makeDraggableHook disables pointer events, we want them in this case\n        document.body.classList.remove(\"pe-none\");\n        document.body.style.cursor = \"grabbing\";\n        return ctx.current;\n    },\n    onDrag({ ctx }) {\n        ctx.current.element.style.left = `${ctx.pointer.x}px`;\n        ctx.current.element.style.top = `${ctx.pointer.y}px`;\n    },\n    onDragEnd({ ctx }) {\n        ctx.current.element.remove();\n        if (document.body.style.cursor === \"grabbing\") {\n            document.body.style.cursor = \"\";\n        }\n        return ctx.current;\n    },\n};\n", "import { baseContainerGlobalSelector } from \"@html_editor/utils/base_container\";\nimport { Plugin } from \"../plugin\";\nimport { childNodes } from \"@html_editor/utils/dom_traversal\";\nimport { isEmptyBlock } from \"@html_editor/utils/dom_info\";\nimport { withSequence } from \"@html_editor/utils/resource\";\n\nexport class PlaceholderPlugin extends Plugin {\n    static id = \"placeholder\";\n    /** @type {import(\"plugins\").EditorResources} */\n    resources = {\n        ...(this.config.placeholder && {\n            hints: [\n                withSequence(1, {\n                    selector: `.odoo-editor-editable:not(:focus) > ${baseContainerGlobalSelector}:only-child`,\n                    text: this.config.placeholder,\n                }),\n            ],\n            hint_targets_providers: (selectionData, editable) => {\n                const el = editable.firstChild;\n                if (\n                    !selectionData.documentSelectionIsInEditable &&\n                    childNodes(editable).length === 1 &&\n                    isEmptyBlock(el) &&\n                    el.matches(baseContainerGlobalSelector)\n                ) {\n                    return [el];\n                } else {\n                    return [];\n                }\n            },\n        }),\n    };\n}\n", "import { ancestors } from \"@html_editor/utils/dom_traversal\";\nimport { Plugin } from \"../plugin\";\nimport { throttleForAnimation } from \"@web/core/utils/timing\";\nimport { couldBeScrollableX, couldBeScrollableY } from \"@web/core/utils/scrolling\";\n\n/**\n * @typedef {(() => void)[]} layout_geometry_change_handlers\n */\n/**\n * This plugin broadcasts layout/geometry changes to other plugins when\n * scrolling, resizing, or history changes occur.\n */\nexport class PositionPlugin extends Plugin {\n    static id = \"position\";\n    /** @type {import(\"plugins\").EditorResources} */\n    resources = {\n        // todo: it is strange that the position plugin is aware of external_history_step_handlers and history_reset_from_steps_handlers.\n        external_history_step_handlers: this.layoutGeometryChange.bind(this),\n        history_reset_from_steps_handlers: this.layoutGeometryChange.bind(this),\n        step_added_handlers: this.layoutGeometryChange.bind(this),\n    };\n\n    setup() {\n        this.layoutGeometryChange = throttleForAnimation(this.layoutGeometryChange.bind(this));\n        this.resizeObserver = new ResizeObserver(this.layoutGeometryChange);\n        this.resizeObserver.observe(this.document.body);\n        this.resizeObserver.observe(this.editable);\n        this.addDomListener(window, \"resize\", this.layoutGeometryChange);\n        if (this.window !== window) {\n            this.addDomListener(this.window, \"resize\", this.layoutGeometryChange);\n        }\n        const scrollableElements = [this.editable, ...ancestors(this.editable)].filter(\n            (node) => couldBeScrollableX(node) || couldBeScrollableY(node)\n        );\n        for (const scrollableElement of scrollableElements) {\n            this.addDomListener(scrollableElement, \"scroll\", () => {\n                this.layoutGeometryChange();\n            });\n            this.resizeObserver.observe(scrollableElement);\n        }\n    }\n\n    destroy() {\n        this.resizeObserver.disconnect();\n        super.destroy();\n    }\n    layoutGeometryChange() {\n        this.dispatchTo(\"layout_geometry_change_handlers\");\n    }\n}\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { baseContainerGlobalSelector } from \"@html_editor/utils/base_container\";\nimport { closestBlock } from \"@html_editor/utils/blocks\";\nimport { isEditorTab, isEmptyBlock } from \"@html_editor/utils/dom_info\";\nimport { closestElement, descendants } from \"@html_editor/utils/dom_traversal\";\nimport { omit, pick } from \"@web/core/utils/objects\";\n\n/** @typedef {import(\"./powerbox/powerbox_plugin\").PowerboxCommand} PowerboxCommand */\n/** @typedef {import(\"@html_editor/core/selection_plugin\").EditorSelection} EditorSelection */\n\n/**\n * @typedef {Object} PowerButton\n * @property {string} commandId\n * @property {Object} [commandParams]\n * @property {string} [description] Can be inferred from the user command\n * @property {string} [icon] Can be inferred from the user command\n * @property {string} [text] Mandatory if `icon` is not provided\n * @property {string} [isAvailable] Can be inferred from the user command\n */\n\n/**\n * @typedef {((selection: EditorSelection) => boolean)[]} power_buttons_visibility_predicates\n */\n\n/**\n * @typedef {{ commandId: string }[]} power_buttons\n *\n * A power button is added by referencing an existing user command.\n *\n * Example:\n *\n *     resources = {\n *          user_commands: [\n *              {\n *                  id: myCommand,\n *                  run: myCommandFunction,\n *                  description: _t(\"Apply my command\"),\n *                  icon: \"fa-bug\",\n *              },\n *          ],\n *          power_buttons: [\n *              {\n *                  commandId: \"myCommand\",\n *                  commandParams: { myParam: \"myValue\" },\n *                  description: _t(\"Do powerfull stuff\"), // overrides the user command's `description`\n *                  // `icon` is derived from the user command\n *              }\n *          ],\n *     };\n */\n\nexport class PowerButtonsPlugin extends Plugin {\n    static id = \"powerButtons\";\n    static dependencies = [\n        \"baseContainer\",\n        \"selection\",\n        \"position\",\n        \"localOverlay\",\n        \"powerbox\",\n        \"userCommand\",\n        \"history\",\n    ];\n    /** @type {import(\"plugins\").EditorResources} */\n    resources = {\n        layout_geometry_change_handlers: this.updatePowerButtons.bind(this),\n        selectionchange_handlers: this.updatePowerButtons.bind(this),\n        post_mount_component_handlers: this.updatePowerButtons.bind(this),\n    };\n\n    setup() {\n        this.powerButtonsOverlay = this.dependencies.localOverlay.makeLocalOverlay(\n            \"oe-power-buttons-overlay\"\n        );\n        this.createPowerButtons();\n    }\n\n    createPowerButtons() {\n        const composePowerButton = (/**@type {PowerButton} */ item) => {\n            const command = this.dependencies.userCommand.getCommand(item.commandId);\n            return {\n                ...pick(command, \"description\", \"icon\"),\n                ...omit(item, \"commandId\", \"commandParams\"),\n                run: () => command.run(item.commandParams),\n                isAvailable: (selection) =>\n                    [command.isAvailable, item.isAvailable]\n                        .filter(Boolean)\n                        .every((predicate) => predicate(selection)),\n            };\n        };\n        const renderButton = ({ description, icon, text, run }) => {\n            const btn = this.document.createElement(\"button\");\n            let className = \"power_button btn px-2 py-1 cursor-pointer\";\n            if (icon) {\n                const iconLibrary = icon.includes(\"fa-\") ? \"fa\" : \"oi\";\n                className += ` ${iconLibrary} ${icon}`;\n            } else {\n                const span = this.document.createElement(\"span\");\n                span.textContent = text;\n                span.className = \"d-flex align-items-center text-nowrap\";\n                span.style.height = \"1em\";\n                btn.append(span);\n            }\n            btn.className = className;\n            btn.title = description;\n            this.addDomListener(btn, \"click\", () => this.applyCommand(run));\n            return btn;\n        };\n\n        /** @type {PowerButton[]} */\n        const powerButtonsDefinitions = this.getResource(\"power_buttons\");\n        // Merge properties from power_button and user_command.\n        const powerButtons = powerButtonsDefinitions.map(composePowerButton);\n        // Render HTML buttons.\n        this.descriptionToElementMap = new Map(powerButtons.map((pb) => [pb, renderButton(pb)]));\n\n        this.powerButtonsContainer = this.document.createElement(\"div\");\n        this.powerButtonsContainer.className = `o_we_power_buttons d-flex justify-content-center d-none`;\n        this.powerButtonsContainer.append(...this.descriptionToElementMap.values());\n        this.powerButtonsOverlay.append(this.powerButtonsContainer);\n    }\n\n    updatePowerButtons() {\n        this.powerButtonsContainer.classList.add(\"d-none\");\n        const { editableSelection, currentSelectionIsInEditable } =\n            this.dependencies.selection.getSelectionData();\n        if (!currentSelectionIsInEditable) {\n            return;\n        }\n        const block = closestBlock(editableSelection.anchorNode);\n        const element = closestElement(editableSelection.anchorNode);\n        const blockRect = block.getBoundingClientRect();\n        const editableRect = this.editable.getBoundingClientRect();\n        if (\n            editableSelection.isCollapsed &&\n            block?.matches(baseContainerGlobalSelector) &&\n            editableRect.bottom > blockRect.top &&\n            isEmptyBlock(block) &&\n            !descendants(block).some(isEditorTab) &&\n            !this.services.ui.isSmall &&\n            !closestElement(editableSelection.anchorNode, \"td, th, li\") &&\n            !block.style.textAlign &&\n            this.getResource(\"power_buttons_visibility_predicates\").every((predicate) =>\n                predicate(editableSelection)\n            )\n        ) {\n            this.powerButtonsContainer.classList.remove(\"d-none\");\n            const direction = closestElement(element, \"[dir]\")?.getAttribute(\"dir\");\n            this.powerButtonsContainer.setAttribute(\"dir\", direction);\n            // Hide/show buttons based on their availability.\n            for (const [{ isAvailable }, buttonElement] of this.descriptionToElementMap.entries()) {\n                const shouldHide = Boolean(!isAvailable(editableSelection));\n                buttonElement.classList.toggle(\"d-none\", shouldHide); // 2nd arg must be a boolean\n            }\n            this.setPowerButtonsPosition(block, blockRect, direction);\n        }\n    }\n\n    getPlaceholderWidth(block) {\n        let width;\n        this.dependencies.history.ignoreDOMMutations(() => {\n            const clone = block.cloneNode(true);\n            clone.innerText = clone.getAttribute(\"o-we-hint-text\");\n            clone.style.width = \"fit-content\";\n            clone.style.visibility = \"hidden\";\n            this.editable.appendChild(clone);\n            width = clone.getBoundingClientRect().width;\n            this.editable.removeChild(clone);\n        });\n        return width;\n    }\n\n    /**\n     *\n     * @param {HTMLElement} block\n     * @param {string} direction\n     */\n    setPowerButtonsPosition(block, blockRect, direction) {\n        const overlayStyles = this.powerButtonsOverlay.style;\n        // Resetting the position of the power buttons.\n        overlayStyles.top = \"0px\";\n        overlayStyles.left = \"0px\";\n        const buttonsRect = this.powerButtonsContainer.getBoundingClientRect();\n        const placeholderWidth = this.getPlaceholderWidth(block) + 20;\n        if (direction === \"rtl\") {\n            overlayStyles.left =\n                blockRect.right - buttonsRect.width - buttonsRect.x - placeholderWidth + \"px\";\n        } else {\n            overlayStyles.left = blockRect.left - buttonsRect.x + placeholderWidth + \"px\";\n        }\n        overlayStyles.top = blockRect.top - buttonsRect.top + \"px\";\n        overlayStyles.height = blockRect.height + \"px\";\n    }\n\n    /**\n     * @param {Function} commandFn\n     */\n    async applyCommand(commandFn) {\n        const btns = [...this.powerButtonsContainer.querySelectorAll(\".btn\")];\n        btns.forEach((btn) => btn.classList.add(\"disabled\"));\n        await commandFn();\n        btns.forEach((btn) => btn.classList.remove(\"disabled\"));\n    }\n}\n", "import { Component, onPatched, useEffect, useExternalListener, useRef } from \"@odoo/owl\";\n\n/**\n * @todo @phoenix i think that most of the \"control\" code in this component\n * should move to the powerbox plugin instead. This would probably be more robust\n */\nexport class Powerbox extends Component {\n    static template = \"html_editor.Powerbox\";\n    static props = {\n        document: { validate: (doc) => doc.constructor.name === \"HTMLDocument\" },\n        close: Function,\n        state: Object,\n        activateCommand: Function,\n        applyCommand: Function,\n    };\n\n    setup() {\n        const ref = useRef(\"root\");\n\n        onPatched(() => {\n            const activeCommand = ref.el.querySelector(\".o-we-command.active\");\n            if (activeCommand) {\n                activeCommand.scrollIntoView({ block: \"nearest\", inline: \"nearest\" });\n            }\n        });\n\n        this.mouseSelectionActive = false;\n        const onMouseMove = () => (this.mouseSelectionActive = true);\n        useExternalListener(this.props.document, \"mousemove\", onMouseMove);\n\n        // If necessary attach the same listener on the document on which\n        // the powerbox is mounted, serving the same purpose:\n        // do not trigger re-renderings when we are scrolling the powerbox\n        useEffect(\n            (ownDoc, propsDoc) => {\n                if (ownDoc && propsDoc && ownDoc !== propsDoc) {\n                    ownDoc.addEventListener(\"mousemove\", onMouseMove);\n                    return () => ownDoc.removeEventListener(\"mousemove\", onMouseMove);\n                }\n            },\n            () => [ref.el?.ownerDocument, this.props.document]\n        );\n    }\n\n    get commands() {\n        return this.props.state.commands;\n    }\n\n    get currentIndex() {\n        return this.props.state.currentIndex;\n    }\n\n    get showCategories() {\n        return this.props.state.showCategories;\n    }\n\n    onScroll() {\n        this.mouseSelectionActive = false;\n    }\n\n    onMouseEnter(index) {\n        if (this.mouseSelectionActive) {\n            this.props.activateCommand(index);\n        }\n    }\n}\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { reactive } from \"@odoo/owl\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { rotate } from \"@web/core/utils/arrays\";\nimport { Powerbox } from \"./powerbox\";\nimport { withSequence } from \"@html_editor/utils/resource\";\nimport { omit, pick } from \"@web/core/utils/objects\";\nimport { baseContainerGlobalSelector } from \"@html_editor/utils/base_container\";\nimport { closestElement } from \"@html_editor/utils/dom_traversal\";\n\n/** @typedef { import(\"@html_editor/core/selection_plugin\").EditorSelection } EditorSelection */\n/** @typedef { import(\"@html_editor/core/user_command_plugin\").UserCommand } UserCommand */\n/** @typedef { ReturnType<_t> } TranslatedString */\n\n/**\n * @typedef {Object} PowerboxCategory\n * @property {string} id\n * @property {TranslatedString} name\n *\n * @typedef {Object} PowerboxItem\n * @property {string} categoryId Id of a powerbox category\n * @property {string} commandId Id of a user command to extend\n * @property {Object} [commandParams] Passed to the command's `run` function - optional\n * @property {TranslatedString} [title] Inheritable\n * @property {TranslatedString} [description] Inheritable\n * @property {string} [icon] fa-class - Inheritable\n * @property {TranslatedString[]} [keywords]\n * @property {(selection: EditorSelection) => boolean} [isAvailable] Optional and inheritable\n */\n\n/**\n * The resulting powerbox command after deriving properties from a user command\n * (type for internal use).\n * @typedef {Object} PowerboxCommand\n * @property {string} categoryId\n * @property {string} categoryName\n * @property {string} title\n * @property {string} description\n * @property {string} icon\n * @property {Function} run\n * @property {TranslatedString[]} [keywords]\n * @property { (selection: EditorSelection) => boolean } isAvailable\n */\n\n/**\n * @typedef { Object } PowerboxShared\n * @property { PowerboxPlugin['closePowerbox'] } closePowerbox\n * @property { PowerboxPlugin['getAvailablePowerboxCommands'] } getAvailablePowerboxCommands\n * @property { PowerboxPlugin['openPowerbox'] } openPowerbox\n * @property { PowerboxPlugin['updatePowerbox'] } updatePowerbox\n */\n\n/** @typedef {PowerboxCategory[]} powerbox_categories */\n/**\n * @typedef {import(\"plugins\").CSSSelector[]} powerbox_blacklist_selectors\n *\n * @see UserCommand\n * @typedef {PowerboxItem[]} powerbox_items\n *\n * A powerbox item must derive from a user command (see UserCommand) specified\n * by commandId. Properties defined in a powerbox item override those from a\n * user command. Other properties are inferred from the UserCommand.\n *\n * Example:\n *\n *     resources = {\n *          user_commands: [\n *              // see {UserCommand}\n *              {\n *                  id: myCommand,\n *                  run: myCommandFunction,\n *                  title: _t(\"My Command\"),\n *                  description: _t(\"My command's description\"),\n *                  icon: \"fa-bug\",\n *              },\n *          ],\n *          powerbox_categories: [\n *              // see {PowerboxCategory}\n *              { id: \"myCategory\", name: _t(\"My Category\") }\n *          ],\n *          powerbox_items: [\n *              // see {PowerboxItem}\n *              {\n *                  categoryId: \"myCategory\",\n *                  commandId: \"myCommand\",\n *                  title: _t(\"My Powerbox Command\"), // overrides the user command's `title`\n *                  // `description` and `icon` are inferred from the user command\n *              }\n *          ],\n *     };\n */\n\nexport class PowerboxPlugin extends Plugin {\n    static id = \"powerbox\";\n    static dependencies = [\"overlay\", \"selection\", \"history\", \"userCommand\"];\n    static shared = [\n        \"closePowerbox\",\n        \"getAvailablePowerboxCommands\",\n        \"openPowerbox\",\n        \"updatePowerbox\",\n    ];\n    /** @type {import(\"plugins\").EditorResources} */\n    resources = {\n        user_commands: {\n            id: \"openPowerbox\",\n            run: () =>\n                this.openPowerbox({\n                    commands: this.getAvailablePowerboxCommands(),\n                    categories: this.getResource(\"powerbox_categories\"),\n                }),\n        },\n        powerbox_categories: [\n            withSequence(10, { id: \"structure\", name: _t(\"Structure\") }),\n            withSequence(60, { id: \"widget\", name: _t(\"Widget\") }),\n            withSequence(100, { id: \"modules\", name: _t(\"Modules\") }),\n        ],\n        power_buttons: withSequence(100, {\n            commandId: \"openPowerbox\",\n            description: _t(\"More options\"),\n            icon: \"oi-ellipsis-v\",\n        }),\n        hints: withSequence(30, {\n            selector: baseContainerGlobalSelector,\n            text: _t('Type \"/\" for commands'),\n        }),\n    };\n\n    setup() {\n        /** @type {import(\"@html_editor/core/overlay_plugin\").Overlay} */\n        this.overlay = this.dependencies.overlay.createOverlay(Powerbox);\n\n        this.state = reactive({});\n        this.overlayProps = {\n            document: this.document,\n            close: () => this.overlay.close(),\n            state: this.state,\n            activateCommand: (currentIndex) => {\n                this.state.currentIndex = currentIndex;\n            },\n            applyCommand: this.applyCommand.bind(this),\n        };\n        this.powerboxCommands = this.makePowerboxCommands();\n        this.addDomListener(this.editable.ownerDocument, \"keydown\", this.onKeyDown);\n    }\n\n    /**\n     * @returns {PowerboxCommand[]}\n     */\n    getAvailablePowerboxCommands() {\n        const selection = this.dependencies.selection.getEditableSelection();\n        const blacklistSelector = this.getResource(\"powerbox_blacklist_selectors\").join(\", \");\n        if (blacklistSelector && closestElement(selection.anchorNode).matches(blacklistSelector)) {\n            return [];\n        }\n        return this.powerboxCommands.filter((cmd) => cmd.isAvailable(selection));\n    }\n\n    /**\n     * @returns {PowerboxCommand[]}\n     */\n    makePowerboxCommands() {\n        /** @type {PowerboxItem[]} */\n        const powerboxItems = this.getResource(\"powerbox_items\");\n        /** @type {PowerboxCategory[]} */\n        const categories = this.getResource(\"powerbox_categories\");\n        const categoryDict = Object.fromEntries(\n            categories.map((category) => [category.id, category])\n        );\n        return powerboxItems.map((/** @type {PowerboxItem} */ item) => {\n            const command = this.dependencies.userCommand.getCommand(item.commandId);\n            return {\n                ...pick(command, \"title\", \"description\", \"icon\"),\n                ...omit(item, \"commandId\", \"commandParams\"),\n                categoryName: categoryDict[item.categoryId].name,\n                run: (context) => command.run(item.commandParams, context),\n                isAvailable: (selection) =>\n                    [command.isAvailable, item.isAvailable]\n                        .filter(Boolean)\n                        .every((predicate) => predicate(selection)),\n            };\n        });\n    }\n\n    /**\n     * @param {Object} params\n     * @param {PowerboxCommand[]} params.commands\n     * @param {PowerboxCategory[]} [params.categories]\n     * @param {Function} [params.onApplyCommand=() => {}]\n     * @param {Function} [params.onClose=() => {}]\n     */\n    openPowerbox({ commands, categories, onApplyCommand = () => {}, onClose = () => {} } = {}) {\n        this.closePowerbox();\n        if (!commands.length) {\n            return;\n        }\n        this.onApplyCommand = onApplyCommand;\n        this.onClose = onClose;\n        this.updatePowerbox(commands, categories);\n    }\n\n    /**\n     * @param {PowerboxCommand[]} commands\n     * @param {PowerboxCategory[]} [categories]\n     */\n    updatePowerbox(commands, categories) {\n        if (categories) {\n            const orderCommands = [];\n            for (const category of categories) {\n                orderCommands.push(\n                    ...commands.filter((command) => command.categoryId === category.id)\n                );\n            }\n            commands = orderCommands;\n        }\n        Object.assign(this.state, {\n            showCategories: !!categories,\n            commands,\n            currentIndex: 0,\n        });\n        this.overlay.open({ props: this.overlayProps });\n    }\n\n    closePowerbox() {\n        if (!this.overlay.isOpen) {\n            return;\n        }\n        this.onClose();\n        this.overlay.close();\n    }\n\n    onKeyDown(ev) {\n        if (!this.overlay.isOpen) {\n            return;\n        }\n        const key = ev.key;\n        switch (key) {\n            case \"Escape\":\n                ev.stopImmediatePropagation();\n                this.closePowerbox();\n                break;\n            case \"Enter\":\n            case \"Tab\":\n                ev.preventDefault();\n                ev.stopImmediatePropagation();\n                this.applyCommand(this.state.commands[this.state.currentIndex]);\n                break;\n            case \"ArrowUp\": {\n                ev.preventDefault();\n                this.state.currentIndex = rotate(this.state.currentIndex, this.state.commands, -1);\n                break;\n            }\n            case \"ArrowDown\": {\n                ev.preventDefault();\n                this.state.currentIndex = rotate(this.state.currentIndex, this.state.commands, 1);\n                break;\n            }\n            case \"ArrowLeft\":\n            case \"ArrowRight\": {\n                this.closePowerbox();\n                break;\n            }\n        }\n    }\n\n    applyCommand(command) {\n        const context = {};\n        this.onApplyCommand(command, context);\n        command.run(context);\n        this.closePowerbox();\n    }\n}\n", "import { fuzzyLookup } from \"@web/core/utils/search\";\nimport { Plugin } from \"../../plugin\";\n\n/**\n * @typedef {import(\"./powerbox_plugin\").PowerboxCategory} CommandGroup\n * @typedef {import(\"../core/selection_plugin\").EditorSelection} EditorSelection\n */\n\nexport class SearchPowerboxPlugin extends Plugin {\n    static id = \"searchPowerbox\";\n    static dependencies = [\"powerbox\", \"selection\", \"history\", \"input\"];\n    /** @type {import(\"plugins\").EditorResources} */\n    resources = {\n        beforeinput_handlers: this.onBeforeInput.bind(this),\n        input_handlers: this.onInput.bind(this),\n        delete_handlers: this.update.bind(this),\n        post_undo_handlers: this.update.bind(this),\n        post_redo_handlers: this.update.bind(this),\n    };\n    setup() {\n        const categoryIds = new Set();\n        for (const category of this.getResource(\"powerbox_categories\")) {\n            if (categoryIds.has(category.id)) {\n                throw new Error(`Duplicate category id: ${category.id}`);\n            }\n            categoryIds.add(category.id);\n        }\n        this.categories = this.getResource(\"powerbox_categories\");\n        this.shouldUpdate = false;\n    }\n    onBeforeInput(ev) {\n        if (ev.data === \"/\") {\n            this.historySavePointRestore = this.dependencies.history.makeSavePoint();\n        }\n    }\n    onInput(ev) {\n        this.searchTerm = undefined;\n        if (ev.data === \"/\") {\n            this.openPowerbox();\n        } else {\n            this.update();\n        }\n    }\n    update() {\n        if (!this.shouldUpdate) {\n            return;\n        }\n        const selection = this.dependencies.selection.getEditableSelection();\n        this.searchNode = selection.startContainer;\n        if (!this.isSearching(selection)) {\n            this.dependencies.powerbox.closePowerbox();\n            return;\n        }\n        const searchTerm = this.searchNode.nodeValue.slice(this.offset + 1, selection.endOffset);\n        if (!searchTerm) {\n            this.dependencies.powerbox.updatePowerbox(this.enabledCommands, this.categories);\n            return;\n        }\n        if (searchTerm.includes(\" \")) {\n            this.dependencies.powerbox.closePowerbox();\n            return;\n        }\n        const commands = this.filterCommands(searchTerm);\n        if (!commands.length) {\n            this.dependencies.powerbox.closePowerbox();\n            this.shouldUpdate = true;\n            return;\n        }\n        this.searchTerm = searchTerm;\n        this.dependencies.powerbox.updatePowerbox(commands);\n    }\n    /**\n     * @param {string} searchTerm\n     */\n    filterCommands(searchTerm) {\n        return fuzzyLookup(searchTerm, this.enabledCommands, (cmd) => [\n            cmd.title,\n            cmd.categoryName,\n            cmd.description,\n            ...(cmd.keywords || []),\n        ]);\n    }\n    /**\n     * @param {EditorSelection} selection\n     */\n    isSearching(selection) {\n        return (\n            selection.endContainer === this.searchNode &&\n            this.searchNode.nodeValue &&\n            this.searchNode.nodeValue[this.offset] === \"/\" &&\n            selection.endOffset >= this.offset\n        );\n    }\n    openPowerbox() {\n        const selection = this.dependencies.selection.getEditableSelection();\n        this.offset = selection.startOffset - 1;\n        this.enabledCommands = this.dependencies.powerbox.getAvailablePowerboxCommands();\n        this.dependencies.powerbox.openPowerbox({\n            commands: this.enabledCommands,\n            categories: this.categories,\n            onApplyCommand: (command, context) => {\n                context.searchTerm = this.searchTerm;\n                this.historySavePointRestore?.();\n            },\n            onClose: () => {\n                this.shouldUpdate = false;\n            },\n        });\n        this.shouldUpdate = true;\n    }\n}\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { closestBlock, isBlock } from \"@html_editor/utils/blocks\";\nimport { fillEmpty } from \"@html_editor/utils/dom\";\nimport {\n    allowsParagraphRelatedElements,\n    isEmpty,\n    isNotEditableNode,\n} from \"@html_editor/utils/dom_info\";\nimport {\n    closestElement,\n    getAdjacentNextSiblings,\n    getAdjacentPreviousSiblings,\n} from \"@html_editor/utils/dom_traversal\";\nimport { withSequence } from \"@html_editor/utils/resource\";\n\nconst BLINKER_CLASS = \"o-horizontal-caret\";\nconst PLACEHOLDER_ATTRIBUTE = \"data-selection-placeholder\";\nconst PLACEHOLDER_SELECTOR = `[${PLACEHOLDER_ATTRIBUTE}]`;\n\nexport class SelectionPlaceholderPlugin extends Plugin {\n    static id = \"selectionPlaceholder\";\n    static dependencies = [\"baseContainer\", \"history\", \"selection\"];\n    resources = {\n        external_history_step_handlers: this.updatePlaceholders.bind(this),\n        normalize_handlers: this.updatePlaceholders.bind(this),\n        step_added_handlers: this.updatePlaceholders.bind(this),\n        selectionchange_handlers: (selectionData) => this.onSelectionChange(selectionData),\n        clean_for_save_handlers: withSequence(0, ({ root }) => {\n            for (const placeholder of root.querySelectorAll(PLACEHOLDER_SELECTOR)) {\n                placeholder.remove();\n            }\n        }),\n        split_element_block_overrides: ({ blockToSplit }) => {\n            if (blockToSplit.hasAttribute(PLACEHOLDER_ATTRIBUTE)) {\n                this.persistPlaceholder(blockToSplit);\n                return true;\n            }\n        },\n        selection_blocker_predicates: (blocker) => {\n            if ((blocker.nodeType === Node.ELEMENT_NODE && blocker.hasAttribute(PLACEHOLDER_ATTRIBUTE)) || !isBlock(blocker)) {\n                return false;\n            } else if (isNotEditableNode(blocker)) {\n                return true;\n            }\n        },\n        selection_placeholder_container_predicates: (container) => {\n            if (!container.isContentEditable || !allowsParagraphRelatedElements(container)) {\n                return false;\n            } else if (container.getAttribute(\"contenteditable\") === \"true\") {\n                return true;\n            }\n        },\n        power_buttons_visibility_predicates: ({ anchorNode }) =>\n            !closestElement(anchorNode, PLACEHOLDER_SELECTOR),\n        move_node_blacklist_selectors: PLACEHOLDER_SELECTOR,\n        system_node_selectors: PLACEHOLDER_SELECTOR,\n        system_classes: BLINKER_CLASS,\n    };\n\n    setup() {\n        this.addDomListener(\n            this.editable,\n            \"focusout\",\n            () => this.editable.querySelectorAll(`.${BLINKER_CLASS}`).forEach(this.cleanBlinker),\n            { isGlobal: true }\n        );\n        this.addDomListener(this.editable, \"focusin\", () => this.resetBlinkerClasses(), {\n            isGlobal: true,\n        });\n    }\n\n    /**\n     * Update all placeholders and blinker classes so they are present\n     * everywhere we need them, and absent wherever they are not useful.\n     */\n    updatePlaceholders() {\n        const checkPredicate = (resourceId, node) => {\n            const results = this.getResource(resourceId)\n                .map((p) => p(node))\n                .filter((result) => result !== undefined);\n            return !!results.length && results.every(Boolean);\n        };\n        const isSelectionBlocker = (node) => checkPredicate(\"selection_blocker_predicates\", node);\n        const placeholderParents = [this.editable, ...this.editable.querySelectorAll(\"*\")].filter(\n            (container) => checkPredicate(\"selection_placeholder_container_predicates\", container)\n        );\n\n        // 1. Update current placeholders.\n        for (const placeholder of this.editable.querySelectorAll(PLACEHOLDER_SELECTOR)) {\n            const siblings = [\"before\", \"after\"].map((side) =>\n                getNonWhitespaceSibling(side, placeholder)\n            );\n            if (!isEmpty(placeholder) || !siblings.filter(Boolean).length) {\n                // Persist non-empty placeholders and any suddenly lonely placeholder.\n                this.persistPlaceholder(placeholder);\n            } else if (\n                !placeholderParents.includes(placeholder.parentElement) ||\n                !siblings.every((sibling) => !sibling || isSelectionBlocker(sibling))\n            ) {\n                // Remove illegitimate placeholders.\n                placeholder.remove();\n            } else {\n                // Update the margins.\n                this.applyMargin(placeholder, ...siblings);\n            }\n        }\n\n        // Get the blocks to check.\n        const blockers = [\n            ...new Set(placeholderParents.flatMap((element) => [...element.children])),\n        ].filter((element) => isSelectionBlocker(element));\n\n        // 2. Add placeholders before and after every blocker where necessary.\n        for (const blocker of blockers) {\n            for (const side of [\"before\", \"after\"]) {\n                // Get the first non-whitespace sibling.\n                const sibling = getNonWhitespaceSibling(side, blocker);\n                // Insert a placeholder if there is no such sibling or if it's a\n                // selection blocker.\n                if (!sibling || isSelectionBlocker(sibling)) {\n                    // Create the placeholder.\n                    const placeholder = this.dependencies.baseContainer.createBaseContainer();\n                    fillEmpty(placeholder);\n                    placeholder.setAttribute(PLACEHOLDER_ATTRIBUTE, \"\");\n                    // Position the placeholder.\n                    const siblings = side === \"before\" ? [sibling, blocker] : [blocker, sibling];\n                    this.applyMargin(placeholder, ...siblings);\n                    // Insert the placeholder.\n                    blocker[side](placeholder);\n                }\n            }\n        }\n        // 3. Reset blinker classes.\n        this.resetBlinkerClasses();\n    }\n\n    /**\n     * Position a placeholder between its siblings.\n     *\n     * @param {Element} placeholder\n     * @param {Element} previous\n     * @param {Element} next\n     */\n    applyMargin(placeholder, previous, next) {\n        const marginBefore = previous ? getMargin(previous, \"bottom\") : 0;\n        const marginAfter = next ? getMargin(next, \"top\") : 0;\n        const middleMargin = Math.abs(marginBefore - marginAfter) / 2;\n        if (middleMargin) {\n            const positiveMargin = Math.abs(\n                middleMargin - (marginAfter >= marginBefore ? marginAfter : marginBefore)\n            );\n            const negativeMargin = -1 - middleMargin;\n            const marginTop = marginAfter >= marginBefore ? positiveMargin : negativeMargin;\n            const marginBottom = marginAfter >= marginBefore ? negativeMargin : positiveMargin;\n            placeholder.style.margin = `${marginTop}px 0 ${marginBottom}px`;\n        }\n    }\n\n    /**\n     * Turn a selection placeholder into a real block.\n     *\n     * @param {Element} placeholder\n     */\n    persistPlaceholder(placeholder) {\n        placeholder.removeAttribute(PLACEHOLDER_ATTRIBUTE);\n        this.cleanBlinker(placeholder);\n        placeholder.removeAttribute(\"style\");\n    }\n\n    /**\n     * Remove the horizontal caret class from a placeholder element.\n     *\n     * @param {Element} blinker\n     */\n    cleanBlinker(blinker) {\n        if (blinker.className === BLINKER_CLASS) {\n            blinker.removeAttribute(\"class\");\n        } else {\n            blinker.classList.remove(BLINKER_CLASS);\n        }\n    }\n\n    /**\n     * Remove any irrelevant blinker class (horizontal caret) and make sure\n     * there is one on the placeholder in collapsed selection, if any.\n     *\n     * @param {import(\"@html_editor/core/selection_plugin\").EditorSelection} selection\n     */\n    resetBlinkerClasses(selection = this.dependencies.selection.getEditableSelection()) {\n        const anchorPlaceholder =\n            selection.isCollapsed && closestElement(selection.anchorNode, PLACEHOLDER_SELECTOR);\n        if (anchorPlaceholder && this.document.activeElement.contains(anchorPlaceholder)) {\n            anchorPlaceholder.classList.add(BLINKER_CLASS);\n        }\n        for (const blinker of this.editable.querySelectorAll(`.${BLINKER_CLASS}`)) {\n            if (blinker !== anchorPlaceholder || !this.document.activeElement.contains(blinker)) {\n                this.cleanBlinker(blinker);\n            }\n        }\n    }\n\n    /**\n     * Update the placeholders' states in function of the selection, by\n     * potentially persisting one, and by reseting the blinker classes.\n     *\n     * @param {import(\"@html_editor/core/selection_plugin\").SelectionData} selectionData\n     */\n    onSelectionChange(selectionData) {\n        const selection = selectionData.editableSelection;\n        this.resetBlinkerClasses(selection);\n        if (selection.isCollapsed) {\n            const anchor = closestElement(selection.anchorNode);\n            if (\n                closestBlock(anchor.parentElement) === this.editable &&\n                anchor?.hasAttribute(PLACEHOLDER_ATTRIBUTE) &&\n                !getNonWhitespaceSibling(\"next\", anchor)\n            ) {\n                // If it's at the bottom of the document, just persist immediately.\n                this.persistPlaceholder(anchor);\n                this.dependencies.history.addStep();\n            }\n        }\n    }\n}\n\n/**\n * @param {\"before\"|\"after\"} side\n * @param {Node} node\n * @returns {Node|undefined}\n */\nconst getNonWhitespaceSibling = (side, node) => {\n    const siblings =\n        side === \"before\" ? getAdjacentPreviousSiblings(node) : getAdjacentNextSiblings(node);\n    return siblings.find(\n        (sibling) => !(sibling.nodeType === Node.TEXT_NODE && !sibling.textContent.trim())\n    );\n};\n/**\n * Get an element's top or bottom margin as a number.\n *\n * @param {Element} element\n * @param {\"top\"|\"bottom\"} side\n * @returns {Number}\n */\nconst getMargin = (element, side) =>\n    +element.ownerDocument.defaultView\n        .getComputedStyle(element)\n        [side === \"top\" ? \"marginTop\" : \"marginBottom\"].replace(\"px\", \"\");\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { Plugin } from \"../plugin\";\nimport { closestBlock } from \"../utils/blocks\";\nimport { closestElement, firstLeaf, selectElements } from \"../utils/dom_traversal\";\nimport {\n    isEmptyBlock,\n    isListItemElement,\n    paragraphRelatedElementsSelector,\n} from \"../utils/dom_info\";\nimport { isHtmlContentSupported } from \"@html_editor/core/selection_plugin\";\nimport { removeClass } from \"@html_editor/utils/dom\";\nimport { withSequence } from \"@html_editor/utils/resource\";\nimport { fillEmpty } from \"../utils/dom\";\n\nexport class SeparatorPlugin extends Plugin {\n    static id = \"separator\";\n    static dependencies = [\"selection\", \"history\", \"split\", \"delete\", \"lineBreak\", \"baseContainer\"];\n    /** @type {import(\"plugins\").EditorResources} */\n    resources = {\n        user_commands: [\n            {\n                id: \"insertSeparator\",\n                title: _t(\"Separator\"),\n                description: _t(\"Insert a horizontal rule separator\"),\n                icon: \"fa-minus\",\n                run: this.insertSeparator.bind(this),\n                isAvailable: isHtmlContentSupported,\n            },\n        ],\n        powerbox_items: withSequence(1, {\n            categoryId: \"structure\",\n            commandId: \"insertSeparator\",\n        }),\n        content_not_editable_providers: (rootEl) => [...selectElements(rootEl, \"hr\")],\n        contenteditable_to_remove_selector: \"hr[contenteditable]\",\n        shorthands: [\n            {\n                pattern: /^---$/,\n                commandId: \"insertSeparator\",\n            },\n        ],\n\n        /** Handlers */\n        selectionchange_handlers: this.handleSelectionInHr.bind(this),\n        deselect_custom_selected_nodes_handlers: this.deselectHR.bind(this),\n        clean_handlers: this.deselectHR.bind(this),\n        clean_for_save_handlers: ({ root }) => {\n            this.deselectHR(root);\n        },\n    };\n\n    insertSeparator() {\n        const selection = this.dependencies.selection.getSelectionData().deepEditableSelection;\n        const block = closestBlock(selection.startContainer);\n        const element =\n            closestElement(selection.startContainer, paragraphRelatedElementsSelector) ||\n            (block && !isListItemElement(block) ? block : null);\n\n        if (element && element !== this.editable) {\n            const sep = this.document.createElement(\"hr\");\n            const firstLeafNode = firstLeaf(block);\n            /**\n             * Insert the separator before the element when it\u2019s empty\n             * or when the caret is at the very start of the block.\n             */\n            if (\n                isEmptyBlock(element) ||\n                (selection.anchorNode === firstLeafNode && selection.anchorOffset === 0)\n            ) {\n                element.before(sep);\n            } else {\n                element.after(sep);\n                const baseContainer = this.dependencies.baseContainer.createBaseContainer();\n                fillEmpty(baseContainer);\n                sep.after(baseContainer);\n                this.dependencies.selection.setCursorStart(baseContainer);\n            }\n        }\n        this.dependencies.history.addStep();\n    }\n\n    deselectHR(root = this.editable) {\n        for (const hr of root.querySelectorAll(\".o_selected_hr\")) {\n            removeClass(hr, \"o_selected_hr\");\n        }\n    }\n\n    handleSelectionInHr() {\n        this.deselectHR();\n        const targetedNodes = this.dependencies.selection.getTargetedNodes();\n        for (const node of targetedNodes) {\n            if (node.nodeName === \"HR\") {\n                node.classList.toggle(\"o_selected_hr\", true);\n            }\n        }\n    }\n}\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { parseHTML } from \"@html_editor/utils/html\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { isHtmlContentSupported } from \"@html_editor/core/selection_plugin\";\n\nexport class StarPlugin extends Plugin {\n    static id = \"star\";\n    static dependencies = [\"dom\", \"history\"];\n    /** @type {import(\"plugins\").EditorResources} */\n    resources = {\n        user_commands: [\n            {\n                id: \"addStars\",\n                title: _t(\"Stars\"),\n                description: _t(\"Insert a rating\"),\n                icon: \"fa-star\",\n                run: this.addStars.bind(this),\n                isAvailable: isHtmlContentSupported,\n            },\n        ],\n        powerbox_items: [\n            {\n                title: _t(\"3 Stars\"),\n                description: _t(\"Insert a rating over 3 stars\"),\n                categoryId: \"widget\",\n                icon: \"fa-star-o\",\n                commandId: \"addStars\",\n                commandParams: { length: 3 },\n            },\n            {\n                title: _t(\"5 Stars\"),\n                description: _t(\"Insert a rating over 5 stars\"),\n                categoryId: \"widget\",\n                commandId: \"addStars\",\n                commandParams: { length: 5 },\n            },\n        ],\n    };\n\n    setup() {\n        this.addDomListener(this.editable, \"pointerdown\", this.onMouseDown);\n    }\n\n    onMouseDown(ev) {\n        const node = ev.target;\n        const isStar = (node) =>\n            node.nodeType === Node.ELEMENT_NODE &&\n            (node.classList.contains(\"fa-star\") || node.classList.contains(\"fa-star-o\"));\n        if (\n            isStar(node) &&\n            node.parentElement &&\n            node.parentElement.className.includes(\"o_stars\")\n        ) {\n            const allStars = Array.from(node.parentElement.childNodes).filter(isStar);\n            const currentStarIndex = allStars.indexOf(node);\n            const previousStars = allStars.slice(0, currentStarIndex);\n            const nextStars = allStars.slice(currentStarIndex + 1);\n            if (nextStars.length || previousStars.length) {\n                const shouldToggleOff =\n                    node.classList.contains(\"fa-star\") &&\n                    (!nextStars[0] || !nextStars[0].classList.contains(\"fa-star\"));\n                for (const star of [...previousStars, node]) {\n                    star.classList.toggle(\"fa-star-o\", shouldToggleOff);\n                    star.classList.toggle(\"fa-star\", !shouldToggleOff);\n                }\n                for (const star of nextStars) {\n                    star.classList.toggle(\"fa-star-o\", true);\n                    star.classList.toggle(\"fa-star\", false);\n                }\n                this.dependencies.history.addStep();\n            }\n            ev.stopPropagation();\n            ev.preventDefault();\n        }\n    }\n\n    addStars({ length }) {\n        const stars = Array.from({ length }, () => '<i class=\"fa fa-star-o\"></i>').join(\"\");\n        const html = `\\u200B<span contenteditable=\"false\" class=\"o_stars\">${stars}</span>\\u200B`;\n        this.dependencies.dom.insert(parseHTML(this.document, html));\n        this.dependencies.history.addStep();\n    }\n}\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { closestElement } from \"@html_editor/utils/dom_traversal\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { reactive } from \"@odoo/owl\";\nimport { TableAlignSelector } from \"./table_align_selector\";\n\nconst verticalAlignmentItems = [\n    {\n        mode: \"top\",\n        template: \"html_editor.VerticalAlignTop\",\n    },\n    {\n        mode: \"middle\",\n        template: \"html_editor.VerticalAlignMiddle\",\n    },\n    {\n        mode: \"bottom\",\n        template: \"html_editor.VerticalAlignBottom\",\n    },\n];\n\nexport class TableAlignPlugin extends Plugin {\n    static id = \"tableAlign\";\n    static dependencies = [\"history\", \"selection\"];\n\n    /** @type {import(\"plugins\").EditorResources} */\n    resources = {\n        user_commands: [\n            {\n                id: \"alignTop\",\n                run: () => this.setVerticalAlignment(\"top\"),\n            },\n            {\n                id: \"alignMiddle\",\n                run: () => this.setVerticalAlignment(\"middle\"),\n            },\n            {\n                id: \"alignBottom\",\n                run: () => this.setVerticalAlignment(\"bottom\"),\n            },\n        ],\n        toolbar_items: [\n            {\n                id: \"table_alignment\",\n                groupId: \"layout\",\n                description: _t(\"Vertical align table cells content\"),\n                isAvailable: () =>\n                    this.dependencies.selection\n                        .getTargetedNodes()\n                        .some((node) => closestElement(node, \"td, th\")),\n                Component: TableAlignSelector,\n                props: {\n                    getItems: () => verticalAlignmentItems,\n                    getDisplay: () => this.verticalAlignMode,\n                    onSelected: (item) => {\n                        this.setVerticalAlignment(item.mode);\n                    },\n                },\n            },\n        ],\n\n        /** Handlers */\n        selectionchange_handlers: this.updateVerticalAlignParams.bind(this),\n        post_undo_handlers: this.updateVerticalAlignParams.bind(this),\n        post_redo_handlers: this.updateVerticalAlignParams.bind(this),\n        remove_all_formats_handlers: this.setVerticalAlignment.bind(this),\n\n        /** Predicates */\n        has_format_predicates: (node) => closestElement(node, \"td, th\")?.style.verticalAlign,\n    };\n\n    setup() {\n        this.verticalAlignMode = reactive({ displayName: \"\" });\n    }\n\n    get currentVerticalAlign() {\n        const targetedCells = this.dependencies.selection\n            .getTargetedNodes()\n            .map((node) => closestElement(node, \"td, th\"))\n            .filter(Boolean);\n\n        if (!targetedCells.length) {\n            return \"\";\n        }\n\n        const verticalAlign = targetedCells[0].style.verticalAlign;\n        return verticalAlign &&\n            targetedCells.every((cell) => cell.style.verticalAlign === verticalAlign)\n            ? verticalAlign\n            : \"\";\n    }\n\n    setVerticalAlignment(mode = \"\") {\n        const targetedCells = new Set(\n            this.dependencies.selection\n                .getTargetedNodes()\n                .map((node) => closestElement(node, \"td, th\"))\n                .filter(Boolean)\n        );\n        let isAlignmentUpdated = false;\n\n        for (const cell of targetedCells) {\n            if (cell.isContentEditable && cell.style.verticalAlign !== mode) {\n                cell.style.verticalAlign = mode;\n                isAlignmentUpdated = true;\n            }\n        }\n\n        if (isAlignmentUpdated) {\n            this.dependencies.history.addStep();\n        }\n        this.updateVerticalAlignParams();\n    }\n\n    updateVerticalAlignParams() {\n        this.verticalAlignMode.displayName = this.currentVerticalAlign;\n    }\n}\n", "import { Component, useState } from \"@odoo/owl\";\nimport { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { toolbarButtonProps } from \"@html_editor/main/toolbar/toolbar\";\nimport { useDropdownAutoVisibility } from \"@html_editor/dropdown_autovisibility_hook\";\nimport { useChildRef } from \"@web/core/utils/hooks\";\n\nexport class TableAlignSelector extends Component {\n    static template = \"html_editor.TableAlignSelector\";\n    static props = {\n        getItems: Function,\n        getDisplay: Function,\n        onSelected: Function,\n        ...toolbarButtonProps,\n    };\n    static components = { Dropdown };\n\n    setup() {\n        this.items = this.props.getItems();\n        this.state = useState(this.props.getDisplay());\n        this.menuRef = useChildRef();\n        useDropdownAutoVisibility(this.env.overlayState, this.menuRef);\n    }\n\n    onSelected(item) {\n        this.props.onSelected(item);\n    }\n}\n", "import { closestElement } from \"@html_editor/utils/dom_traversal\";\nimport { Component } 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\";\n\nexport class TableMenu extends Component {\n    static template = \"html_editor.TableMenu\";\n    static props = {\n        type: String, // column or row\n        moveColumn: Function,\n        addColumn: Function,\n        removeColumn: Function,\n        moveRow: Function,\n        addRow: Function,\n        removeRow: Function,\n        turnIntoHeader: Function,\n        turnIntoRow: Function,\n        resetRowHeight: Function,\n        resetColumnWidth: Function,\n        resetTableSize: Function,\n        clearColumnContent: Function,\n        clearRowContent: Function,\n        overlay: Object,\n        dropdownState: Object,\n        target: { validate: (el) => el.nodeType === Node.ELEMENT_NODE },\n        direction: { type: String, optional: true },\n    };\n    static defaultProps = { direction: \"ltr\" };\n    static components = { Dropdown, DropdownItem };\n\n    setup() {\n        if (this.props.type === \"column\") {\n            this.isFirst = this.props.target.cellIndex === 0;\n            this.isLast = !this.props.target.nextElementSibling;\n        } else {\n            const tr = this.props.target.parentElement;\n            this.isFirst = !tr.previousElementSibling;\n            this.isLast = !tr.nextElementSibling;\n            this.isTableHeader = [...tr.children][0].nodeName === \"TH\";\n        }\n        this.items = this.props.type === \"column\" ? this.colItems() : this.rowItems();\n    }\n\n    get hasCustomTableSize() {\n        const table = closestElement(this.props.target, \"table\");\n        if (!table) {\n            return false;\n        }\n        const rows = [...table.rows];\n        const firstRowCells = [...rows[0].cells];\n        const rowHasHeight = rows.some((row) => row.style.height);\n        const cellHasWidth = firstRowCells.some((cell) => cell.style.width);\n        return rowHasHeight || cellHasWidth;\n    }\n\n    get hasCustomRowHeight() {\n        return !!this.props.target.closest(\"tr\").style.height;\n    }\n\n    get hasCustomColumnWidth() {\n        return (\n            !!this.props.target.closest(\"td\")?.style?.width ||\n            !!this.props.target.closest(\"th\")?.style?.width\n        );\n    }\n\n    onSelected(item) {\n        item.action(this.props.target);\n        this.props.overlay.close();\n    }\n\n    colItems() {\n        const ltr = this.props.direction === \"ltr\";\n        return [\n            !this.isFirst && {\n                name: \"move_left\",\n                icon: \"fa-chevron-left disabled\",\n                text: ltr ? _t(\"Move left\") : _t(\"Move right\"),\n                action: this.props.moveColumn.bind(this, \"left\"),\n            },\n            !this.isLast && {\n                name: \"move_right\",\n                icon: \"fa-chevron-right\",\n                text: ltr ? _t(\"Move right\") : _t(\"Move left\"),\n                action: this.props.moveColumn.bind(this, \"right\"),\n            },\n            {\n                name: \"insert_left\",\n                icon: \"fa-plus\",\n                text: ltr ? _t(\"Insert left\") : _t(\"Insert right\"),\n                action: this.props.addColumn.bind(this, \"before\"),\n            },\n            {\n                name: \"insert_right\",\n                icon: \"fa-plus\",\n                text: ltr ? _t(\"Insert right\") : _t(\"Insert left\"),\n                action: this.props.addColumn.bind(this, \"after\"),\n            },\n            {\n                name: \"delete\",\n                icon: \"fa-trash\",\n                text: _t(\"Delete\"),\n                action: this.props.removeColumn.bind(this),\n            },\n            this.hasCustomColumnWidth && {\n                name: \"reset_column_size\",\n                icon: \"fa-table\",\n                text: _t(\"Reset column size\"),\n                action: (target) => this.props.resetColumnWidth(target.closest(\"td, th\")),\n            },\n            this.hasCustomTableSize && {\n                name: \"reset_table_size\",\n                icon: \"fa-table\",\n                text: _t(\"Reset table size\"),\n                action: (target) => this.props.resetTableSize(target.closest(\"table\")),\n            },\n            {\n                name: \"clear_content\",\n                icon: \"fa-times-circle\",\n                text: _t(\"Clear content\"),\n                action: this.props.clearColumnContent.bind(this),\n            },\n        ].filter(Boolean);\n    }\n\n    rowItems() {\n        return [\n            this.isFirst &&\n                !this.isTableHeader && {\n                    name: \"make_header\",\n                    icon: \"fa-th-large\",\n                    text: _t(\"Turn into header\"),\n                    action: (target) => this.props.turnIntoHeader(target.parentElement),\n                },\n            this.isFirst &&\n                this.isTableHeader && {\n                    name: \"remove_header\",\n                    icon: \"fa-table\",\n                    text: _t(\"Turn into row\"),\n                    action: (target) => this.props.turnIntoRow(target.parentElement),\n                },\n            !this.isFirst && {\n                name: \"move_up\",\n                icon: \"fa-chevron-up\",\n                text: _t(\"Move up\"),\n                action: (target) => this.props.moveRow(\"up\", target.parentElement),\n            },\n            !this.isLast && {\n                name: \"move_down\",\n                icon: \"fa-chevron-down\",\n                text: _t(\"Move down\"),\n                action: (target) => this.props.moveRow(\"down\", target.parentElement),\n            },\n            !this.isTableHeader && {\n                name: \"insert_above\",\n                icon: \"fa-plus\",\n                text: _t(\"Insert above\"),\n                action: (target) => this.props.addRow(\"before\", target.parentElement),\n            },\n            {\n                name: \"insert_below\",\n                icon: \"fa-plus\",\n                text: _t(\"Insert below\"),\n                action: (target) => this.props.addRow(\"after\", target.parentElement),\n            },\n            {\n                name: \"delete\",\n                icon: \"fa-trash\",\n                text: _t(\"Delete\"),\n                action: (target) => this.props.removeRow(target.parentElement),\n            },\n            this.hasCustomRowHeight && {\n                name: \"reset_row_size\",\n                icon: \"fa-table\",\n                text: _t(\"Reset row size\"),\n                action: (target) => this.props.resetRowHeight(target.closest(\"tr\")),\n            },\n            this.hasCustomTableSize && {\n                name: \"reset_table_size\",\n                icon: \"fa-table\",\n                text: _t(\"Reset table size\"),\n                action: (target) => this.props.resetTableSize(target.closest(\"table\")),\n            },\n            {\n                name: \"clear_content\",\n                icon: \"fa-times-circle\",\n                text: _t(\"Clear content\"),\n                action: (target) => this.props.clearRowContent(target.parentElement),\n            },\n        ].filter(Boolean);\n    }\n}\n", "import { Component, useExternalListener, useState } from \"@odoo/owl\";\n\nexport class TablePicker extends Component {\n    static template = \"html_editor.TablePicker\";\n    static props = {\n        insertTable: Function,\n        editable: {\n            validate: (el) => el.nodeType === Node.ELEMENT_NODE,\n        },\n        overlay: Object,\n        direction: String,\n    };\n\n    setup() {\n        this.state = useState({\n            cols: 3,\n            rows: 3,\n        });\n        useExternalListener(\n            this.props.editable.ownerDocument,\n            \"keydown\",\n            (ev) => {\n                ev.stopPropagation();\n                const key = ev.key;\n                const isRTL = this.props.direction === \"rtl\";\n                switch (key) {\n                    case \"Enter\":\n                        ev.preventDefault();\n                        this.insertTable();\n                        break;\n                    case \"ArrowUp\":\n                        ev.preventDefault();\n                        if (this.state.rows > 1) {\n                            this.state.rows -= 1;\n                        }\n                        break;\n                    case \"ArrowDown\":\n                        this.state.rows += 1;\n                        ev.preventDefault();\n                        break;\n                    case \"ArrowLeft\":\n                        ev.preventDefault();\n                        if (isRTL) {\n                            this.state.cols += 1;\n                        } else {\n                            if (this.state.cols > 1) {\n                                this.state.cols -= 1;\n                            }\n                        }\n                        break;\n                    case \"ArrowRight\":\n                        ev.preventDefault();\n                        if (isRTL) {\n                            if (this.state.cols > 1) {\n                                this.state.cols -= 1;\n                            }\n                        } else {\n                            this.state.cols += 1;\n                        }\n                        break;\n                    default:\n                        ev.stopImmediatePropagation();\n                        this.props.overlay.close();\n                        break;\n                }\n            },\n            { capture: true }\n        );\n    }\n\n    updateSize(cols, rows) {\n        this.state.cols = cols;\n        this.state.rows = rows;\n    }\n\n    insertTable() {\n        this.props.insertTable({ cols: this.state.cols, rows: this.state.rows });\n        this.props.overlay.close();\n    }\n}\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { baseContainerGlobalSelector } from \"@html_editor/utils/base_container\";\nimport { isBlock } from \"@html_editor/utils/blocks\";\nimport {\n    fillEmpty,\n    fillShrunkPhrasingParent,\n    removeClass,\n    splitTextNode,\n} from \"@html_editor/utils/dom\";\nimport {\n    getDeepestPosition,\n    isProtected,\n    isProtecting,\n    isEmptyBlock,\n    isTextNode,\n    nextLeaf,\n    previousLeaf,\n    isTableCell,\n} from \"@html_editor/utils/dom_info\";\nimport {\n    ancestors,\n    closestElement,\n    createDOMPathGenerator,\n    descendants,\n    firstLeaf,\n    lastLeaf,\n} from \"@html_editor/utils/dom_traversal\";\nimport { parseHTML } from \"@html_editor/utils/html\";\nimport { DIRECTIONS, leftPos, rightPos, nodeSize } from \"@html_editor/utils/position\";\nimport { withSequence } from \"@html_editor/utils/resource\";\nimport { findInSelection } from \"@html_editor/utils/selection\";\nimport { getColumnIndex, getRowIndex, getTableCells } from \"@html_editor/utils/table\";\nimport { isBrowserFirefox } from \"@web/core/browser/feature_detection\";\nimport { getActiveHotkey } from \"@web/core/hotkeys/hotkey_service\";\nimport { isHtmlContentSupported } from \"@html_editor/core/selection_plugin\";\n\nexport const BORDER_SENSITIVITY = 5;\n\nconst tableInnerComponents = new Set([\"THEAD\", \"TBODY\", \"TFOOT\", \"TR\", \"TH\", \"TD\"]);\nfunction isUnremovableTableComponent(node, root) {\n    if (!tableInnerComponents.has(node.nodeName)) {\n        return false;\n    }\n    if (!root) {\n        return true;\n    }\n    const closestTable = closestElement(node, \"table\");\n    return !root.contains(closestTable);\n}\n\n/**\n * @typedef { Object } TableShared\n * @property { TablePlugin['addColumn'] } addColumn\n * @property { TablePlugin['addRow'] } addRow\n * @property { TablePlugin['turnIntoHeader'] } turnIntoHeader\n * @property { TablePlugin['moveColumn'] } moveColumn\n * @property { TablePlugin['moveRow'] } moveRow\n * @property { TablePlugin['removeColumn'] } removeColumn\n * @property { TablePlugin['removeRow'] } removeRow\n * @property { TablePlugin['turnIntoRow'] } turnIntoRow\n * @property { TablePlugin['resetRowHeight'] } resetRowHeight\n * @property { TablePlugin['resetColumnWidth'] } resetColumnWidth\n * @property { TablePlugin['resetTableSize'] } resetTableSize\n * @property { TablePlugin['clearColumnContent'] } clearColumnContent\n * @property { TablePlugin['clearRowContent'] } clearRowContent\n */\n\n/**\n * @typedef {((el: HTMLElement) => void)[]} deselect_custom_selected_nodes_handlers\n */\n\n/**\n * This plugin only contains the table manipulation and selection features. All UI overlay\n * code is located in the table_ui plugin\n */\nexport class TablePlugin extends Plugin {\n    static id = \"table\";\n    static dependencies = [\n        \"baseContainer\",\n        \"dom\",\n        \"history\",\n        \"selection\",\n        \"delete\",\n        \"split\",\n        \"color\",\n    ];\n    static shared = [\n        \"insertTable\",\n        \"addColumn\",\n        \"addRow\",\n        \"removeColumn\",\n        \"removeRow\",\n        \"moveColumn\",\n        \"turnIntoHeader\",\n        \"turnIntoRow\",\n        \"moveRow\",\n        \"resetRowHeight\",\n        \"resetColumnWidth\",\n        \"resetTableSize\",\n        \"clearColumnContent\",\n        \"clearRowContent\",\n    ];\n    /** @type {import(\"plugins\").EditorResources} */\n    resources = {\n        user_commands: [\n            {\n                id: \"insertTable\",\n                run: (params) => {\n                    this.insertTable(params);\n                },\n                isAvailable: isHtmlContentSupported,\n            },\n        ],\n        toolbar_namespace_providers: [\n            withSequence(\n                90,\n                (targetedNodes, editableSelection) =>\n                    closestElement(editableSelection.anchorNode, \".o_selected_td\") && \"compact\"\n            ),\n        ],\n\n        /** Handlers */\n        selectionchange_handlers: this.updateSelectionTable.bind(this),\n        clipboard_content_processors: this.processContentForClipboard.bind(this),\n        clean_for_save_handlers: ({ root }) => this.deselectTable(root),\n        before_line_break_handlers: this.resetTableSelection.bind(this),\n        before_split_block_handlers: this.resetTableSelection.bind(this),\n\n        /** Overrides */\n        tab_overrides: withSequence(20, this.handleTab.bind(this)),\n        shift_tab_overrides: withSequence(20, this.handleShiftTab.bind(this)),\n        delete_range_overrides: this.handleDeleteRange.bind(this),\n        color_apply_overrides: this.applyTableColor.bind(this),\n\n        unremovable_node_predicates: isUnremovableTableComponent,\n        unsplittable_node_predicates: (node) =>\n            node.nodeName === \"TABLE\" || tableInnerComponents.has(node.nodeName),\n        fully_selected_node_predicates: (node) => !!closestElement(node, \".o_selected_td\"),\n        targeted_nodes_processors: this.adjustTargetedNodes.bind(this),\n        move_node_whitelist_selectors: \"table\",\n        selection_blocker_predicates: (node) => {\n            if (node.nodeName === \"TABLE\") {\n                return true;\n            }\n        },\n        selection_placeholder_container_predicates: (container) => {\n            if (container.nodeName === \"TABLE\") {\n                return false;\n            } else if ([\"TD\", \"TH\"].includes(container.nodeName)) {\n                return true;\n            }\n        },\n        normalize_handlers: this.distributeTableColorsToAllCells.bind(this),\n    };\n\n    setup() {\n        this.addDomListener(this.editable, \"mousedown\", this.onMousedown);\n        this.addDomListener(this.editable, \"mouseup\", this.onMouseup);\n        this.addDomListener(this.editable, \"keydown\", (ev) => {\n            this._isKeyDown = true;\n            const arrowHandled = [\"arrowup\", \"control+arrowup\", \"arrowdown\", \"control+arrowdown\"];\n            if (arrowHandled.includes(getActiveHotkey(ev))) {\n                this.navigateCell(ev);\n            }\n            const shiftArrowHandled = [\n                \"shift+arrowup\",\n                \"shift+arrowright\",\n                \"shift+arrowdown\",\n                \"shift+arrowleft\",\n                \"control+shift+arrowup\",\n                \"control+shift+arrowright\",\n                \"control+shift+arrowdown\",\n                \"control+shift+arrowleft\",\n            ];\n            if (shiftArrowHandled.includes(getActiveHotkey(ev))) {\n                this.isShiftArrowKeyboardSelection = true;\n                this.updateTableKeyboardSelection(ev);\n            }\n        });\n        this.onMousemove = this.onMousemove.bind(this);\n    }\n\n    handleTab() {\n        const selection = this.dependencies.selection.getEditableSelection();\n        const inTable = closestElement(selection.anchorNode, \"table\");\n        if (inTable) {\n            // Move cursor to next cell.\n            const shouldAddNewRow = !this.shiftCursorToTableCell(1);\n            if (shouldAddNewRow) {\n                this.addRow(\"after\", findInSelection(selection, \"tr\"));\n                this.shiftCursorToTableCell(1);\n                this.dependencies.history.addStep();\n            }\n            return true;\n        }\n    }\n\n    handleShiftTab() {\n        const selection = this.dependencies.selection.getEditableSelection();\n        const inTable = closestElement(selection.anchorNode, \"table\");\n        if (inTable) {\n            // Move cursor to previous cell.\n            this.shiftCursorToTableCell(-1);\n            return true;\n        }\n    }\n\n    /**\n     * Inherits table-level colors to all child tds to make it\n     * easier to add/remove style on tables.\n     *\n     * @param {Element} root\n     */\n    distributeTableColorsToAllCells(root) {\n        [...root.querySelectorAll(\"table\")]\n            .filter((table) => table.style[\"color\"] || table.style[\"backgroundColor\"])\n            .forEach((table) => {\n                const tds = table.querySelectorAll(\"td\");\n                for (const td of tds) {\n                    td.style[\"color\"] = td.style[\"color\"] || table.style[\"color\"];\n                    td.style[\"backgroundColor\"] =\n                        td.style[\"backgroundColor\"] || table.style[\"backgroundColor\"];\n                }\n                table.style[\"color\"] = \"\";\n                table.style[\"backgroundColor\"] = \"\";\n            });\n    }\n\n    createTable({ rows = 2, cols = 2 } = {}) {\n        const baseContainer = this.dependencies.baseContainer.createBaseContainer();\n        fillShrunkPhrasingParent(baseContainer);\n        const baseContainerHtml = baseContainer.outerHTML;\n        const tdsHtml = new Array(cols).fill(`<td>${baseContainerHtml}</td>`).join(\"\");\n        const trsHtml = new Array(rows).fill(`<tr>${tdsHtml}</tr>`).join(\"\");\n        const tableHtml = `<table class=\"table table-bordered o_table\"><tbody>${trsHtml}</tbody></table>`;\n        return parseHTML(this.document, tableHtml);\n    }\n\n    _insertTable({ rows = 2, cols = 2 } = {}) {\n        const newTable = this.createTable({ rows, cols });\n        let sel = this.dependencies.selection.getEditableSelection();\n        if (!sel.isCollapsed) {\n            this.dependencies.delete.deleteSelection();\n        }\n        while (!isBlock(sel.anchorNode)) {\n            const anchorNode = sel.anchorNode;\n            const isTextNode = anchorNode.nodeType === Node.TEXT_NODE;\n            const newAnchorNode = isTextNode\n                ? splitTextNode(anchorNode, sel.anchorOffset, DIRECTIONS.LEFT) + 1 && anchorNode\n                : this.dependencies.split.splitElement(anchorNode, sel.anchorOffset).shift();\n            const newPosition = rightPos(newAnchorNode);\n            sel = this.dependencies.selection.setSelection(\n                { anchorNode: newPosition[0], anchorOffset: newPosition[1] },\n                { normalize: false }\n            );\n        }\n        const [table] = this.dependencies.dom.insert(newTable);\n        return table;\n    }\n    insertTable({ rows = 2, cols = 2 } = {}) {\n        const table = this._insertTable({ rows, cols });\n        this.dependencies.selection.setCursorStart(\n            table.querySelector(baseContainerGlobalSelector)\n        );\n        this.dependencies.history.addStep();\n    }\n    /**\n     * @param {'before'|'after'} position\n     * @param {HTMLTableCellElement} reference\n     */\n    addColumn(position, reference) {\n        const columnIndex = getColumnIndex(reference);\n        const table = closestElement(reference, \"table\");\n        const tableWidth = table.style.width && parseFloat(table.style.width);\n        const referenceColumn = table.querySelectorAll(\n            `tr :is(td, th):nth-of-type(${columnIndex + 1})`\n        );\n        const referenceCellWidth = reference.style.width\n            ? parseFloat(reference.style.width)\n            : reference.clientWidth;\n        // Temporarily set widths so proportions are respected.\n        const firstRow = table.querySelector(\"tr\");\n        const firstRowCells = [...firstRow.children].filter(\n            (child) => child.nodeName === \"TD\" || child.nodeName === \"TH\"\n        );\n        let totalWidth = 0;\n        if (tableWidth) {\n            for (const cell of firstRowCells) {\n                const width = parseFloat(cell.style.width);\n                cell.style.width = width + \"px\";\n                // Spread the widths to preserve proportions.\n                // -1 for the width of the border of the new column.\n                const newWidth = Math.max(\n                    Math.round((width * tableWidth) / (tableWidth + referenceCellWidth - 1)),\n                    13\n                );\n                cell.style.width = newWidth + \"px\";\n                totalWidth += newWidth;\n            }\n        }\n        referenceColumn.forEach((cell, rowIndex) => {\n            const newCell = this.document.createElement(cell.tagName);\n            const baseContainer = this.dependencies.baseContainer.createBaseContainer();\n            baseContainer.append(this.document.createElement(\"br\"));\n            newCell.append(baseContainer);\n            cell[position](newCell);\n            // If the first row is a header, ensure the new column's\n            // first cell is also marked as a header (<th>).\n            if (rowIndex === 0 && cell.classList.contains(\"o_table_header\")) {\n                newCell.classList.add(\"o_table_header\");\n            }\n            if (rowIndex === 0 && tableWidth) {\n                newCell.style.width = cell.style.width;\n                totalWidth += parseFloat(cell.style.width);\n            }\n        });\n        if (tableWidth) {\n            if (totalWidth !== tableWidth - 1) {\n                // -1 for the width of the border of the new column.\n                firstRowCells[firstRowCells.length - 1].style.width =\n                    parseFloat(firstRowCells[firstRowCells.length - 1].style.width) +\n                    (tableWidth - totalWidth - 1) +\n                    \"px\";\n            }\n            // Fix the table and row's width so it doesn't change.\n            table.style.width = tableWidth + \"px\";\n        }\n    }\n    /**\n     * @param {'before'|'after'} position\n     * @param {HTMLTableRowElement} reference\n     */\n    addRow(position, reference) {\n        const referenceRowHeight = reference.style.height && parseFloat(reference.style.height);\n        const newRow = this.document.createElement(\"tr\");\n        if (referenceRowHeight) {\n            newRow.style.height = referenceRowHeight + \"px\";\n        }\n        const cells = reference.querySelectorAll(\"td, th\");\n        const referenceRowWidths = [...cells].map((cell) => cell.style.width);\n        newRow.append(\n            ...Array.from(cells).map(() => {\n                const td = this.document.createElement(\"td\");\n                const baseContainer = this.dependencies.baseContainer.createBaseContainer();\n                baseContainer.append(this.document.createElement(\"br\"));\n                td.append(baseContainer);\n                return td;\n            })\n        );\n        reference[position](newRow);\n        if (referenceRowHeight) {\n            newRow.style.height = referenceRowHeight + \"px\";\n        }\n        // Preserve the width of the columns (applied only on the first row).\n        if (getRowIndex(newRow) === 0) {\n            let columnIndex = 0;\n            for (const column of newRow.children) {\n                column.style.width = referenceRowWidths[columnIndex];\n                cells[columnIndex].style.width = \"\";\n                columnIndex++;\n            }\n        }\n    }\n    /**\n     * @param {HTMLTableRowElement} reference\n     */\n    turnIntoHeader(reference) {\n        const preserveSelection = this.dependencies.selection.preserveSelection();\n        [...reference.children].forEach((td) => {\n            if (td.nodeName == \"TD\") {\n                const th = this.document.createElement(\"th\");\n                if (td.style?.cssText.length) {\n                    th.style.cssText = td.style?.cssText;\n                }\n                th.classList.add(\"o_table_header\");\n                th.append(...td.childNodes);\n                td.replaceWith(th);\n            }\n        });\n        preserveSelection.restore();\n    }\n    /**\n     * @param {HTMLTableRowElement} reference\n     */\n    turnIntoRow(reference) {\n        const preserveSelection = this.dependencies.selection.preserveSelection();\n        [...reference.children].forEach((th) => {\n            if (th.nodeName == \"TH\") {\n                const td = this.document.createElement(\"td\");\n                if (th.style?.cssText.length) {\n                    td.style.cssText = th.style?.cssText;\n                }\n                td.append(...th.childNodes);\n                th.replaceWith(td);\n            }\n        });\n        preserveSelection.restore();\n    }\n    /**\n     * @param {HTMLTableCellElement} cell\n     */\n    removeColumn(cell) {\n        const table = closestElement(cell, \"table\");\n        const cells = [...closestElement(cell, \"tr\").querySelectorAll(\"th, td\")];\n        const index = cells.findIndex((td) => td === cell);\n        const siblingCell = cells[index - 1] || cells[index + 1];\n        table\n            .querySelectorAll(`tr :is(td, th):nth-of-type(${index + 1})`)\n            .forEach((td) => td.remove());\n        // not sure we should move the cursor?\n        siblingCell\n            ? this.dependencies.selection.setCursorStart(siblingCell)\n            : this.deleteTable(table);\n    }\n    /**\n     * @param {HTMLTableRowElement} row\n     */\n    removeRow(row) {\n        const table = closestElement(row, \"table\");\n        const siblingRow = row.previousElementSibling || row.nextElementSibling;\n        row.remove();\n        // not sure we should move the cursor?\n        siblingRow\n            ? this.dependencies.selection.setCursorStart(siblingRow.querySelector(\"td, th\"))\n            : this.deleteTable(table);\n    }\n    /**\n     * @param {'left'|'right'} position\n     * @param {HTMLTableCellElement} cell\n     */\n    moveColumn(position, cell) {\n        const columnIndex = getColumnIndex(cell);\n        const nColumns = cell.parentElement.children.length;\n        if (\n            columnIndex < 0 ||\n            (position === \"left\" && columnIndex === 0) ||\n            (position !== \"left\" && columnIndex === nColumns - 1)\n        ) {\n            return;\n        }\n\n        const trs = cell.parentElement.parentElement.children;\n        const tdsToMove = [...trs].map((tr) => tr.children[columnIndex]);\n        const selectionToRestore = this.dependencies.selection.getEditableSelection();\n        if (position === \"left\") {\n            tdsToMove.forEach((td) => td.previousElementSibling.before(td));\n        } else {\n            tdsToMove.forEach((td) => td.nextElementSibling.after(td));\n        }\n        this.dependencies.selection.setSelection(selectionToRestore);\n    }\n    /**\n     * @param {'up'|'down'} position\n     * @param {HTMLTableRowElement} row\n     */\n    moveRow(position, row) {\n        const selectionToRestore = this.dependencies.selection.getEditableSelection();\n        let adjustedRow;\n        if (position === \"up\") {\n            const isPreviousRowHeader =\n                [...row.previousElementSibling.children][0].nodeName === \"TH\";\n            row.previousElementSibling?.before(row);\n            adjustedRow = row;\n            if (isPreviousRowHeader) {\n                this.turnIntoHeader(row);\n                this.turnIntoRow(row.nextElementSibling);\n            }\n        } else {\n            const isRowHeader = [...row.children][0].nodeName === \"TH\";\n            row.nextElementSibling?.after(row);\n            adjustedRow = row.previousElementSibling;\n            if (isRowHeader) {\n                this.turnIntoHeader(adjustedRow);\n                this.turnIntoRow(row);\n            }\n        }\n\n        // If the moved row becomes the first row, copy the widths of its td\n        // elements from the previous first row, as td widths are only applied\n        // to the first row.\n        if (!adjustedRow.previousElementSibling) {\n            adjustedRow.childNodes.forEach((cell, index) => {\n                cell.style.width = adjustedRow.nextElementSibling.childNodes[index].style.width;\n            });\n        }\n        this.dependencies.selection.setSelection(selectionToRestore);\n    }\n\n    /**\n     * @param {HTMLTableElement} table\n     */\n    normalizeRowHeight(table) {\n        const rows = [...table.rows];\n        const referenceRow = rows.find((row) => !row.style.height);\n        const referenceRowHeight = parseFloat(getComputedStyle(referenceRow).height);\n        rows.forEach((row) => {\n            if (\n                row.style.height &&\n                Math.abs(parseFloat(row.style.height) - referenceRowHeight) <= 1\n            ) {\n                row.style.height = \"\";\n            }\n        });\n    }\n\n    /**\n     * @param {HTMLTableRowElement} row\n     */\n    resetRowHeight(row) {\n        const table = closestElement(row, \"table\");\n        row.style.height = \"\";\n        this.normalizeRowHeight(table);\n    }\n\n    /**\n     * @param {HTMLTableElement} table\n     */\n    normalizeColumnWidth(table) {\n        const rows = [...table.rows];\n        const firstRowCells = [...rows[0].cells];\n        const tableWidth = parseFloat(table.style.width);\n        if (tableWidth) {\n            const expectedCellWidth = tableWidth / firstRowCells.length;\n            firstRowCells.forEach((cell, i) => {\n                const cellWidth = parseFloat(cell.style.width);\n                if (cellWidth && Math.abs(cellWidth - expectedCellWidth) <= 1) {\n                    rows.forEach((row) => (row.cells[i].style.width = \"\"));\n                }\n            });\n        }\n    }\n\n    /**\n     * @param {HTMLTableCellElement} cell\n     */\n    resetColumnWidth(cell) {\n        const currentCellWidth = parseFloat(cell.style.width);\n        if (!currentCellWidth) {\n            return;\n        }\n\n        const table = closestElement(cell, \"table\");\n        const tableWidth = parseFloat(table.style.width);\n        const currentRow = cell.parentElement;\n        const currentRowCells = [...currentRow.cells];\n        const rowCellCount = currentRowCells.length;\n        const expectedCellWidth = tableWidth / rowCellCount;\n        const widthDifference = currentCellWidth - expectedCellWidth;\n        const currentColumnIndex = getColumnIndex(cell);\n\n        let totalWidthLeftOfCell = 0,\n            totalWidthRightOfCell = 0;\n        currentRowCells.forEach((rowCell, i) => {\n            const cellWidth = parseFloat(rowCell.style.width) || rowCell.clientWidth;\n            if (i < currentColumnIndex) {\n                totalWidthLeftOfCell += cellWidth;\n            } else if (i > currentColumnIndex) {\n                totalWidthRightOfCell += cellWidth;\n            }\n        });\n\n        let expectedWidthLeftOfCell = currentColumnIndex * expectedCellWidth;\n        let expectedWidthRightOfCell = (rowCellCount - 1 - currentColumnIndex) * expectedCellWidth;\n        let cellsToAdjust = [];\n        for (\n            let i = currentColumnIndex - 1;\n            i >= 0 && Math.abs(expectedWidthLeftOfCell - totalWidthLeftOfCell) > 1;\n            i--\n        ) {\n            cellsToAdjust.push(currentRowCells[i]);\n            totalWidthLeftOfCell -=\n                parseFloat(currentRowCells[i].style.width) || currentRowCells[i].clientWidth;\n            expectedWidthLeftOfCell -= expectedCellWidth;\n        }\n        for (\n            let j = currentColumnIndex + 1;\n            j < rowCellCount && Math.abs(expectedWidthRightOfCell - totalWidthRightOfCell) > 1;\n            j++\n        ) {\n            cellsToAdjust.push(currentRowCells[j]);\n            totalWidthRightOfCell -=\n                parseFloat(currentRowCells[j].style.width) || currentRowCells[j].clientWidth;\n            expectedWidthRightOfCell -= expectedCellWidth;\n        }\n\n        cellsToAdjust = cellsToAdjust.filter((adjCell) => {\n            const cellWidth = parseFloat(adjCell.style.width) || adjCell.clientWidth;\n            return widthDifference > 0\n                ? cellWidth < expectedCellWidth\n                : cellWidth > expectedCellWidth;\n        });\n\n        const totalWidthForAdjustment = cellsToAdjust.reduce((width, adjCell) => {\n            const cellWidth = parseFloat(adjCell.style.width) || adjCell.clientWidth;\n            return width + Math.abs(expectedCellWidth - cellWidth);\n        }, 0);\n\n        cell.style.width = `${expectedCellWidth}px`;\n        cellsToAdjust.forEach((adjCell) => {\n            const adjCellWidth = parseFloat(adjCell.style.width) || adjCell.clientWidth;\n            const adjustmentWidth =\n                (Math.abs(expectedCellWidth - adjCellWidth) / totalWidthForAdjustment) *\n                Math.abs(widthDifference);\n            adjCell.style.width = `${\n                adjCellWidth + (widthDifference > 0 ? adjustmentWidth : -adjustmentWidth)\n            }px`;\n        });\n        this.normalizeColumnWidth(table);\n    }\n\n    /**\n     * @param {HTMLTableElement} table\n     */\n    resetTableSize(table) {\n        table.removeAttribute(\"style\");\n        const cells = [...table.querySelectorAll(\"tr, td, th\")];\n        cells.forEach((cell) => {\n            const cStyle = cell.style;\n            if (cell.tagName === \"TR\") {\n                cStyle.height = \"\";\n            } else {\n                cStyle.width = \"\";\n            }\n        });\n    }\n    /**\n     * @param {HTMLTableCellElement} cell\n     */\n    clearColumnContent(cell) {\n        const table = closestElement(cell, \"table\");\n        const cells = [...closestElement(cell, \"tr\").querySelectorAll(\"th, td\")];\n        const index = cells.findIndex((td) => td === cell);\n        table.querySelectorAll(`tr :is(td, th):nth-of-type(${index + 1})`).forEach((td) => {\n            const baseContainer = this.dependencies.baseContainer.createBaseContainer();\n            fillEmpty(baseContainer);\n            td.replaceChildren(baseContainer);\n        });\n    }\n    /**\n     * @param {HTMLTableRowElement} row\n     */\n    clearRowContent(row) {\n        row.querySelectorAll(\"td, th\").forEach((td) => {\n            const baseContainer = this.dependencies.baseContainer.createBaseContainer();\n            fillEmpty(baseContainer);\n            td.replaceChildren(baseContainer);\n        });\n    }\n    deleteTable(table) {\n        table =\n            table || findInSelection(this.dependencies.selection.getEditableSelection(), \"table\");\n        if (!table) {\n            return;\n        }\n        const baseContainer = this.dependencies.baseContainer.createBaseContainer();\n        baseContainer.appendChild(this.document.createElement(\"br\"));\n        table.before(baseContainer);\n        table.remove();\n        this.dependencies.selection.setCursorStart(baseContainer);\n    }\n\n    // @todo @phoenix: handle deleteBackward on table cells\n    // deleteBackwardBefore({ targetNode, targetOffset }) {\n    //     // If the cursor is at the beginning of a row, prevent deletion.\n    //     if (targetNode.nodeType === Node.ELEMENT_NODE && isRow(targetNode) && !targetOffset) {\n    //         return true;\n    //     }\n    // }\n\n    /**\n     * Removes fully selected rows or columns, clears the content of selected\n     * cells otherwise.\n     *\n     * @param {NodeListOf<HTMLTableCellElement>} selectedTds - Non-empty\n     * NodeList of selected table cells.\n     */\n    deleteTableCells(selectedTds) {\n        const rows = [...closestElement(selectedTds[0], \"tr\").parentElement.children].filter(\n            (child) => child.nodeName === \"TR\"\n        );\n        const firstRowCells = [...rows[0].children].filter(\n            (child) => child.nodeName === \"TD\" || child.nodeName === \"TH\"\n        );\n        const firstCellRowIndex = getRowIndex(selectedTds[0]);\n        const firstCellColumnIndex = getColumnIndex(selectedTds[0]);\n        const lastCellRowIndex = getRowIndex(selectedTds[selectedTds.length - 1]);\n        const lastCellColumnIndex = getColumnIndex(selectedTds[selectedTds.length - 1]);\n\n        const areFullColumnsSelected =\n            firstCellRowIndex === 0 && lastCellRowIndex === rows.length - 1;\n        const areFullRowsSelected =\n            firstCellColumnIndex === 0 && lastCellColumnIndex === firstRowCells.length - 1;\n\n        if (areFullColumnsSelected) {\n            for (let index = firstCellColumnIndex; index <= lastCellColumnIndex; index++) {\n                this.removeColumn(firstRowCells[index]);\n            }\n            return;\n        }\n\n        if (areFullRowsSelected) {\n            for (let index = firstCellRowIndex; index <= lastCellRowIndex; index++) {\n                this.removeRow(rows[index]);\n            }\n            return;\n        }\n\n        for (const td of selectedTds) {\n            const baseContainer = this.dependencies.baseContainer.createBaseContainer();\n            baseContainer.appendChild(this.document.createElement(\"br\"));\n            td.replaceChildren(baseContainer);\n        }\n        this.dependencies.selection.setCursorStart(selectedTds[0].firstChild);\n    }\n\n    /**\n     * @param {Object} range - Range-like object.\n     * @param {Array} fullySelectedTables - Non-empty array of table elements.\n     */\n    deleteRangeWithFullySelectedTables(range, fullySelectedTables) {\n        let { startContainer, startOffset, endContainer, endOffset } = range;\n\n        // Expand range to fully include tables.\n        const firstTable = fullySelectedTables[0];\n        if (firstTable.contains(startContainer)) {\n            [startContainer, startOffset] = leftPos(firstTable);\n        }\n        const lastTable = fullySelectedTables.at(-1);\n        if (lastTable.contains(endContainer)) {\n            [endContainer, endOffset] = rightPos(lastTable);\n        }\n        range = { startContainer, startOffset, endContainer, endOffset };\n\n        range = this.dependencies.delete.deleteRange(range);\n\n        // Normalize deep.\n        // @todo @phoenix: Use something from the selection plugin (normalize deep?)\n        const [anchorNode, anchorOffset] = getDeepestPosition(\n            range.startContainer,\n            range.startOffset\n        );\n\n        this.dependencies.selection.setSelection({ anchorNode, anchorOffset });\n    }\n\n    handleDeleteRange(range) {\n        // @todo @phoenix: this does not depend on the range. This should be\n        // optimized by keeping in memory the state of selected cells/tables.\n        const fullySelectedTables = [...this.editable.querySelectorAll(\".o_selected_table\")].filter(\n            (table) =>\n                [...table.querySelectorAll(\"td, th\")].every(\n                    (td) =>\n                        closestElement(td, \"table\") !== table ||\n                        td.classList.contains(\"o_selected_td\")\n                )\n        );\n        if (fullySelectedTables.length) {\n            this.deleteRangeWithFullySelectedTables(range, fullySelectedTables);\n            return true;\n        }\n\n        const selectedTds = this.editable.querySelectorAll(\".o_selected_td\");\n        if (selectedTds.length) {\n            this.deleteTableCells(selectedTds);\n            // this._toggleTableUi();\n            return true;\n        }\n\n        return false;\n    }\n\n    /**\n     * Moves the cursor by shiftIndex table cells.\n     *\n     * @param {Number} shiftIndex - The index to shift the cursor by.\n     * @returns {boolean} - True if the cursor was successfully moved, false otherwise.\n     */\n    shiftCursorToTableCell(shiftIndex) {\n        const sel = this.dependencies.selection.getEditableSelection();\n        const currentTd = closestElement(sel.anchorNode, isTableCell);\n        const closestTable = closestElement(currentTd, \"table\");\n        if (!currentTd || !closestTable) {\n            return false;\n        }\n        const tds = [...closestTable.querySelectorAll(\"td, th\")];\n        const cursorDestination = tds[tds.findIndex((td) => currentTd === td) + shiftIndex];\n        if (!cursorDestination) {\n            return false;\n        }\n        this.dependencies.selection.setCursorEnd(lastLeaf(cursorDestination));\n        return true;\n    }\n\n    hanldeFirefoxSelection(ev = null) {\n        const selection = this.document.getSelection();\n        if (isBrowserFirefox()) {\n            if (!this.dependencies.selection.isSelectionInEditable(selection)) {\n                return false;\n            }\n            if (selection.rangeCount > 1 || selection.anchorNode?.tagName === \"TR\") {\n                // In Firefox, selecting multiple cells within a table using the mouse can create multiple ranges.\n                // This behavior can cause the original selection (where the selection started) to be lost.\n                // To solve the issue we merge the ranges of the selection together the first time we find\n                // selection.rangeCount > 1. Morover, when hitting a double click on a cell, it spans a row\n                // inside selection which needs to be simplified here.\n                let [anchorNode, anchorOffset] = getDeepestPosition(\n                    selection.getRangeAt(0).startContainer,\n                    selection.getRangeAt(0).startOffset\n                );\n                let [focusNode, focusOffset] = getDeepestPosition(\n                    selection.getRangeAt(selection.rangeCount - 1).startContainer,\n                    selection.getRangeAt(selection.rangeCount - 1).startOffset\n                );\n                if (this.selectionDirection === \"backward\") {\n                    [anchorNode, focusNode] = [focusNode, anchorNode];\n                    [anchorOffset, focusOffset] = [focusOffset, anchorOffset];\n                }\n                this.dependencies.selection.setSelection({\n                    anchorNode,\n                    anchorOffset,\n                    focusNode,\n                    focusOffset,\n                });\n                return true;\n            } else if (\n                ev &&\n                closestElement(ev.target, \"table\") ===\n                    closestElement(selection.anchorNode, \"table\") &&\n                closestElement(ev.target, isTableCell) !==\n                    closestElement(selection.focusNode, isTableCell)\n            ) {\n                // After the manual update firefox will not be able the table selection automatically\n                // so we need to update the selection manually too.\n                // When we hover on a new table cell we mark it as the new focusNode.\n                this.dependencies.selection.setSelection({\n                    anchorNode: selection.anchorNode,\n                    anchorOffset: selection.anchorOffset,\n                    focusNode: ev.target,\n                    focusOffset: 0,\n                });\n                this.selectionDirection = selection.direction;\n                return true;\n            }\n        }\n        return false;\n    }\n\n    /**\n     * Sets selection in table to make cell selection\n     * rectangularly when pressing shift + arrow key.\n     *\n     * @private\n     * @param {KeyboardEvent} ev\n     */\n    updateTableKeyboardSelection(ev) {\n        const selection = this.dependencies.selection.getSelectionData().deepEditableSelection;\n        const startTable = closestElement(selection.anchorNode, \"table\");\n        const endTable = closestElement(selection.focusNode, \"table\");\n        if (!(startTable || endTable)) {\n            return;\n        }\n        const [startTd, endTd] = [\n            closestElement(selection.anchorNode, isTableCell),\n            closestElement(selection.focusNode, isTableCell),\n        ];\n        if (startTable !== endTable) {\n            // Deselect the table if it was fully selected.\n            if (endTable) {\n                const deselectingBackward =\n                    [\"ArrowLeft\", \"ArrowUp\"].includes(ev.key) &&\n                    selection.direction === DIRECTIONS.RIGHT;\n                const deselectingForward =\n                    [\"ArrowRight\", \"ArrowDown\"].includes(ev.key) &&\n                    selection.direction === DIRECTIONS.LEFT;\n                let targetNode;\n                if (deselectingBackward) {\n                    targetNode = endTable.previousElementSibling;\n                } else if (deselectingForward) {\n                    targetNode = endTable.nextElementSibling;\n                }\n                if (targetNode) {\n                    ev.preventDefault();\n                    this.dependencies.selection.setSelection({\n                        anchorNode: selection.anchorNode,\n                        anchorOffset: selection.anchorOffset,\n                        focusNode: targetNode,\n                        focusOffset: deselectingBackward ? nodeSize(targetNode) : 0,\n                    });\n                }\n            }\n            return;\n        }\n        // Handle selection for the single cell.\n        if (startTd === endTd && !startTd.classList.contains(\"o_selected_td\")) {\n            const { focusNode, focusOffset } = selection;\n            // Do not prevent default when there is a text in cell.\n            if (focusNode.nodeType === Node.TEXT_NODE) {\n                const textNodes = descendants(startTd).filter(isTextNode);\n                const lastTextChild = textNodes[textNodes.length - 1];\n                const firstTextChild = textNodes[0];\n                const isAtTextBoundary = {\n                    ArrowRight: nodeSize(focusNode) === focusOffset && focusNode === lastTextChild,\n                    ArrowLeft: focusOffset === 0 && focusNode === firstTextChild,\n                    ArrowUp: focusNode === firstTextChild,\n                    ArrowDown: focusNode === lastTextChild,\n                };\n                if (isAtTextBoundary[ev.key]) {\n                    ev.preventDefault();\n                    this.selectTableCells(this.dependencies.selection.getEditableSelection());\n                }\n            } else {\n                ev.preventDefault();\n                this.selectTableCells(this.dependencies.selection.getEditableSelection());\n            }\n            return;\n        }\n        // Select cells symmetrically.\n        const endCellPosition = { x: getRowIndex(endTd), y: getColumnIndex(endTd) };\n        const tds = [...startTable.rows].map((row) => [...row.cells]);\n        let targetTd, targetNode;\n        switch (ev.key) {\n            case \"ArrowUp\": {\n                if (endCellPosition.x > 0) {\n                    targetTd = tds[endCellPosition.x - 1][endCellPosition.y];\n                } else {\n                    targetNode = previousLeaf(startTable, this.editable);\n                }\n                break;\n            }\n            case \"ArrowDown\": {\n                if (endCellPosition.x < tds.length - 1) {\n                    targetTd = tds[endCellPosition.x + 1][endCellPosition.y];\n                } else {\n                    targetNode = nextLeaf(startTable, this.editable);\n                }\n                break;\n            }\n            case \"ArrowRight\": {\n                if (endCellPosition.y < tds[0].length - 1) {\n                    targetTd = tds[endCellPosition.x][endCellPosition.y + 1];\n                }\n                break;\n            }\n            case \"ArrowLeft\": {\n                if (endCellPosition.y > 0) {\n                    targetTd = tds[endCellPosition.x][endCellPosition.y - 1];\n                }\n                break;\n            }\n        }\n        if (targetTd || targetNode) {\n            this.dependencies.selection.setSelection({\n                anchorNode: selection.anchorNode,\n                anchorOffset: selection.anchorOffset,\n                focusNode: targetTd || targetNode,\n                focusOffset: 0,\n            });\n        }\n        ev.preventDefault();\n    }\n\n    updateSelectionTable(selectionData) {\n        if (\n            this.hanldeFirefoxSelection() ||\n            this._isFirefoxDoubleMousedown ||\n            this._isTripleClickInTable\n        ) {\n            // It will be retriggered with selectionchange\n            delete this._isFirefoxDoubleMousedown;\n            delete this._isTripleClickInTable;\n            return;\n        }\n        if (!selectionData.documentSelectionIsInEditable) {\n            return;\n        }\n        const selection = selectionData.editableSelection;\n        const startTd = closestElement(selection.startContainer, isTableCell);\n        const endTd = closestElement(selection.endContainer, isTableCell);\n        const selectSingleCell =\n            startTd &&\n            startTd === endTd &&\n            startTd.classList.contains(\"o_selected_td\") &&\n            this.isShiftArrowKeyboardSelection;\n        if (!(startTd && startTd === endTd) || this._isKeyDown) {\n            delete this._isKeyDown;\n            // Prevent deselecting single cell unless selection changes\n            // through keyboard.\n            this.deselectTable();\n        }\n        delete this.isShiftArrowKeyboardSelection;\n        const startTable = ancestors(selection.startContainer, this.editable)\n            .filter((node) => node.nodeName === \"TABLE\")\n            .pop();\n        const endTable = ancestors(selection.endContainer, this.editable)\n            .filter((node) => node.nodeName === \"TABLE\")\n            .pop();\n\n        const targetedNodes = this.dependencies.selection.getTargetedNodes();\n        if ((startTd !== endTd || selectSingleCell) && startTable === endTable) {\n            if (!isProtected(startTable) && !isProtecting(startTable)) {\n                // The selection goes through at least two different cells ->\n                // select cells.\n                // Select single cell if selection goes from two cells to\n                // one using shift + arrow key.\n                this.selectTableCells(selection);\n            }\n        } else if (!targetedNodes.every((node) => closestElement(node.parentElement, \"table\"))) {\n            const endSelectionTable = closestElement(selection.focusNode, \"table\");\n            const endSelectionTableTds = endSelectionTable && getTableCells(endSelectionTable);\n            const targetedTds = new Set(\n                targetedNodes.map((node) => closestElement(node, isTableCell))\n            );\n            const isTableFullySelected = endSelectionTableTds?.every((td) => targetedTds.has(td));\n            if (endSelectionTable && !isTableFullySelected) {\n                // Make sure all the cells are targeted in actual selection\n                // when selecting full table. If not, they will be selected\n                // forcefully and updateSelectionTable will be called again.\n                const targetTd =\n                    selection.direction === DIRECTIONS.RIGHT\n                        ? endSelectionTableTds.pop()\n                        : endSelectionTableTds.shift();\n                this.dependencies.selection.setSelection({\n                    anchorNode: selection.anchorNode,\n                    anchorOffset: selection.anchorOffset,\n                    focusNode: targetTd,\n                    focusOffset: selection.direction === DIRECTIONS.RIGHT ? nodeSize(targetTd) : 0,\n                });\n            }\n            const targetedTables = new Set(\n                targetedNodes\n                    .map((node) => closestElement(node, \"table\"))\n                    .filter((node) => node && !isProtected(node) && !isProtecting(node))\n            );\n            for (const table of targetedTables) {\n                // Don't apply several nested levels of selection.\n                if (!ancestors(table, this.editable).some((node) => targetedTables.has(node))) {\n                    table.classList.toggle(\"o_selected_table\", true);\n                    for (const td of getTableCells(table)) {\n                        td.classList.toggle(\"o_selected_td\", true);\n                        this.dispatchTo(\"deselect_custom_selected_nodes_handlers\", td);\n                    }\n                }\n            }\n        }\n    }\n\n    onMousedown(ev) {\n        this._currentMouseState = ev.type;\n        this._lastMousedownPosition = [ev.x, ev.y];\n        const isPointerInsideCell = this.isPointerInsideCell(ev);\n        const td = closestElement(ev.target, isTableCell);\n        if (isPointerInsideCell) {\n            if (\n                !isProtected(td) &&\n                !isProtecting(td) &&\n                ((isEmptyBlock(td) && ev.detail === 2) || ev.detail === 3)\n            ) {\n                this.hanldeFirefoxSelection();\n                this.selectTableCells(this.dependencies.selection.getEditableSelection());\n                if (isBrowserFirefox()) {\n                    // In firefox, selection changes when hitting mouseclick\n                    // second time in an empty cell. It calls updateSelectionTable\n                    // which deselects the single cell. Hence, we need a label\n                    // to keep it selected.\n                    this._isFirefoxDoubleMousedown = true;\n                }\n                if (ev.detail === 3) {\n                    // Doing a tripleclick on a text will change the selection.\n                    // In such case updateSelectionTable should not do anything.\n                    this._isTripleClickInTable = true;\n                }\n            } else {\n                this.editable.addEventListener(\"mousemove\", this.onMousemove);\n                const currentSelection = this.dependencies.selection.getEditableSelection();\n                // disable dragging on table\n                if (closestElement(ev.target, \"td.o_selected_td\")) {\n                    this.dependencies.selection.setCursorStart(currentSelection.anchorNode);\n                }\n                this.deselectTable();\n            }\n        }\n    }\n\n    onMouseup(ev) {\n        delete this._mouseMovePositionWhenAllContentsSelected;\n        this._currentMouseState = ev.type;\n        this.editable.removeEventListener(\"mousemove\", this.onMousemove);\n    }\n\n    /**\n     * Checks if mouse is effectively inside the cell and not overlapping\n     * the cell borders to prevent cell selection while resizing table.\n     *\n     * @param {MouseEvent} ev\n     * @returns {Boolean}\n     */\n    isPointerInsideCell(ev) {\n        const td = closestElement(ev.target, isTableCell);\n        if (td) {\n            const targetRect = td.getBoundingClientRect();\n            if (\n                ev.clientX > targetRect.x + BORDER_SENSITIVITY &&\n                ev.clientX < targetRect.x + td.clientWidth - BORDER_SENSITIVITY &&\n                ev.clientY > targetRect.y + BORDER_SENSITIVITY &&\n                ev.clientY < targetRect.y + td.clientHeight - BORDER_SENSITIVITY\n            ) {\n                return true;\n            }\n        }\n        return false;\n    }\n\n    onMousemove(ev) {\n        if (this._currentMouseState !== \"mousedown\") {\n            return;\n        }\n        if (this.hanldeFirefoxSelection(ev)) {\n            return;\n        }\n        const selection = this.dependencies.selection.getEditableSelection();\n        const startTd = closestElement(selection.startContainer, isTableCell);\n        const endTd = closestElement(selection.endContainer, isTableCell);\n        if (startTd && startTd === endTd && !isProtected(startTd) && !isProtecting(startTd)) {\n            const selectedNodes = this.dependencies.selection\n                .getTargetedNodes()\n                .filter(this.dependencies.selection.areNodeContentsFullySelected);\n            const cellContents = descendants(startTd);\n            const areCellContentsFullySelected = cellContents\n                .filter((d) => !isBlock(d))\n                .every((child) => selectedNodes.includes(child));\n            if (areCellContentsFullySelected) {\n                const SENSITIVITY = 5;\n                if (!this._mouseMovePositionWhenAllContentsSelected) {\n                    this._mouseMovePositionWhenAllContentsSelected = [ev.clientX, ev.clientY];\n                }\n                const isMovingAwayFromSelection =\n                    Math.abs(ev.clientX - this._mouseMovePositionWhenAllContentsSelected[0]) >=\n                    SENSITIVITY;\n                if (isMovingAwayFromSelection) {\n                    // A cell is fully selected and the mouse is moving away\n                    // from the selection, within said cell -> select the cell.\n                    this.selectTableCells(selection);\n                }\n            } else if (\n                cellContents.filter(isBlock).every(isEmptyBlock) &&\n                Math.abs(\n                    ev.clientX -\n                        (this._lastMousedownPosition ? this._lastMousedownPosition[0] : ev.clientX)\n                ) >= 20\n            ) {\n                // Handle selecting an empty cell.\n                this.selectTableCells(selection);\n            }\n        }\n    }\n\n    navigateCell(ev) {\n        const selection = this.dependencies.selection.getSelectionData().deepEditableSelection;\n        const anchorNode = selection.anchorNode;\n        const currentCell = closestElement(anchorNode, isTableCell);\n        const currentTable = closestElement(anchorNode, \"table\");\n        if (!selection.isCollapsed || !currentCell) {\n            return;\n        }\n        const isArrowUp = ev.key === \"ArrowUp\";\n        const cellPosition = {\n            row: getRowIndex(currentCell),\n            col: getColumnIndex(currentCell),\n        };\n        const tableRows = [...currentTable.rows].map((row) => [...row.cells]);\n        const shouldNavigateCell = (currentNode) => {\n            const siblingDirection = isArrowUp ? \"previousElementSibling\" : \"nextElementSibling\";\n            const direction = isArrowUp ? DIRECTIONS.LEFT : DIRECTIONS.RIGHT;\n            const domPath = createDOMPathGenerator(direction, {\n                stopTraverseFunction: (node) => node === currentCell,\n                stopFunction: (node) => node === currentCell,\n            });\n            const domPathNode = domPath(currentNode);\n            let node = domPathNode.next().value;\n            while (node) {\n                if ((isBlock(node) && node[siblingDirection]) || node.nodeName === \"BR\") {\n                    return false;\n                }\n                node = domPathNode.next().value;\n            }\n            return true;\n        };\n        const rowOffset = isArrowUp ? -1 : 1;\n        let targetNode = tableRows[cellPosition.row + rowOffset]?.[cellPosition.col];\n        const siblingElement = isArrowUp\n            ? currentTable.previousElementSibling\n            : currentTable.nextElementSibling;\n        if (!targetNode && siblingElement) {\n            // If no target cell is available, navigate to sibling element\n            targetNode = siblingElement;\n        }\n        if (shouldNavigateCell(anchorNode)) {\n            ev.preventDefault();\n            if (targetNode) {\n                targetNode = isArrowUp ? lastLeaf(targetNode) : firstLeaf(targetNode);\n                const targetOffset = isArrowUp ? nodeSize(targetNode) : 0;\n                this.dependencies.selection.setSelection({\n                    anchorNode: targetNode,\n                    anchorOffset: targetOffset,\n                });\n            }\n        }\n    }\n\n    selectTableCells(selection) {\n        const table = closestElement(selection.commonAncestorContainer, \"table\");\n        if (!table) {\n            return;\n        }\n        table.classList.toggle(\"o_selected_table\", true);\n        const columns = getTableCells(table);\n        const startCol =\n            [selection.startContainer, ...ancestors(selection.startContainer, this.editable)].find(\n                (node) => isTableCell(node) && closestElement(node, \"table\") === table\n            ) || columns[0];\n        const endCol =\n            [selection.endContainer, ...ancestors(selection.endContainer, this.editable)].find(\n                (node) => isTableCell(node) && closestElement(node, \"table\") === table\n            ) || columns[columns.length - 1];\n        const [startRow, endRow] = [closestElement(startCol, \"tr\"), closestElement(endCol, \"tr\")];\n        const [startColIndex, endColIndex] = [getColumnIndex(startCol), getColumnIndex(endCol)];\n        const [startRowIndex, endRowIndex] = [getRowIndex(startRow), getRowIndex(endRow)];\n        const [minRowIndex, maxRowIndex] = [\n            Math.min(startRowIndex, endRowIndex),\n            Math.max(startRowIndex, endRowIndex),\n        ];\n        const [minColIndex, maxColIndex] = [\n            Math.min(startColIndex, endColIndex),\n            Math.max(startColIndex, endColIndex),\n        ];\n        // Create an array of arrays of tds (each of which is a row).\n        const grid = [...table.querySelectorAll(\"tr\")]\n            .filter((tr) => closestElement(tr, \"table\") === table)\n            .map((tr) => [...tr.children].filter(isTableCell));\n        for (const tds of grid.filter((_, index) => index >= minRowIndex && index <= maxRowIndex)) {\n            for (const td of tds.filter(\n                (_, index) => index >= minColIndex && index <= maxColIndex\n            )) {\n                td.classList.toggle(\"o_selected_td\", true);\n                this.dispatchTo(\"deselect_custom_selected_nodes_handlers\", td);\n            }\n        }\n    }\n\n    /**\n     * Remove any custom table selection from the editor.\n     *\n     * @returns {boolean} true if a table was deselected\n     */\n    deselectTable(root = this.editable) {\n        let didDeselectTable = false;\n        for (const table of root.querySelectorAll(\".o_selected_table\")) {\n            removeClass(table, \"o_selected_table\");\n            for (const td of table.querySelectorAll(\".o_selected_td\")) {\n                removeClass(td, \"o_selected_td\");\n            }\n            didDeselectTable = true;\n        }\n        return didDeselectTable;\n    }\n\n    applyTableColor(color, mode, previewMode) {\n        const selectedTds = [...this.editable.querySelectorAll(\".o_selected_td\")].filter(\n            (node) => node.isContentEditable\n        );\n        if (selectedTds.length && (mode === \"backgroundColor\" || (mode === \"color\" && !color))) {\n            // Disable the `box-shadow` while previewing the background color.\n            selectedTds.forEach((td) =>\n                td.classList.toggle(\"o_selected_td_bg_color_preview\", previewMode)\n            );\n            for (const td of selectedTds) {\n                this.dependencies.color.colorElement(td, color, mode);\n                if (color) {\n                    td.style[\"color\"] = getComputedStyle(td).color;\n                } else {\n                    td.style[\"color\"] = \"\";\n                }\n            }\n        }\n    }\n\n    adjustTargetedNodes(targetedNodes) {\n        const modifiedTargetedNodes = [];\n        const visitedTables = new Set();\n        for (const node of targetedNodes) {\n            const selectedTable = closestElement(node, \".o_selected_table\");\n            if (selectedTable) {\n                if (visitedTables.has(selectedTable)) {\n                    continue;\n                }\n                visitedTables.add(selectedTable);\n                for (const selectedTd of selectedTable.querySelectorAll(\".o_selected_td\")) {\n                    modifiedTargetedNodes.push(selectedTd, ...descendants(selectedTd));\n                }\n            } else {\n                modifiedTargetedNodes.push(node);\n            }\n        }\n        return modifiedTargetedNodes;\n    }\n\n    resetTableSelection() {\n        const selection = this.dependencies.selection.getEditableSelection({ deep: true });\n        const anchorTD = closestElement(selection.anchorNode, \".o_selected_td\");\n        if (!anchorTD) {\n            return;\n        }\n        this.deselectTable();\n        this.dependencies.selection.setSelection({\n            anchorNode: anchorTD.firstChild,\n            anchorOffset: 0,\n            focusNode: anchorTD.lastChild,\n            focusOffset: nodeSize(anchorTD.lastChild),\n        });\n    }\n\n    /**\n     * @param {DocumentFragment} clonedContents\n     * @param {import(\"@html_editor/core/selection_plugin\").EditorSelection} selection\n     */\n    processContentForClipboard(clonedContents, selection) {\n        if (clonedContents.firstChild.nodeName === \"TR\" || isTableCell(clonedContents.firstChild)) {\n            // We enter this case only if selection is within single table.\n            const table = closestElement(selection.commonAncestorContainer, \"table\");\n            const tableClone = table.cloneNode(true);\n            // A table is considered fully selected if it is nested inside a\n            // cell that is itself selected, or if all its own cells are\n            // selected.\n            const isTableFullySelected =\n                (table.parentElement && !!closestElement(table.parentElement, \".o_selected_td\")) ||\n                getTableCells(table).every((td) => td.classList.contains(\"o_selected_td\"));\n            if (!isTableFullySelected) {\n                for (const td of tableClone.querySelectorAll(\":is(td, th):not(.o_selected_td)\")) {\n                    if (closestElement(td, \"table\") === tableClone) {\n                        // ignore nested\n                        td.remove();\n                    }\n                }\n                const trsWithoutTd = Array.from(tableClone.querySelectorAll(\"tr\")).filter(\n                    (row) => !row.querySelector(\"td, th\")\n                );\n                for (const tr of trsWithoutTd) {\n                    if (closestElement(tr, \"table\") === tableClone) {\n                        // ignore nested\n                        tr.remove();\n                    }\n                }\n            }\n            // If it is fully selected, clone the whole table rather than\n            // just its rows.\n            clonedContents = tableClone;\n        }\n        const startTable = closestElement(selection.startContainer, \"table\");\n        if (clonedContents.firstChild.nodeName === \"TABLE\" && startTable) {\n            // Make sure the full leading table is copied.\n            clonedContents.firstChild.after(startTable.cloneNode(true));\n            clonedContents.firstChild.remove();\n        }\n        const endTable = closestElement(selection.endContainer, \"table\");\n        if (clonedContents.lastChild.nodeName === \"TABLE\" && endTable) {\n            // Make sure the full trailing table is copied.\n            clonedContents.lastChild.before(endTable.cloneNode(true));\n            clonedContents.lastChild.remove();\n        }\n        this.deselectTable(clonedContents);\n        return clonedContents;\n    }\n}\n", "import { Plugin } from \"@html_editor/plugin\";\nimport {\n    closestElement,\n    getAdjacentNextSiblings,\n    getAdjacentPreviousSiblings,\n} from \"@html_editor/utils/dom_traversal\";\nimport { getColumnIndex } from \"@html_editor/utils/table\";\nimport { BORDER_SENSITIVITY } from \"@html_editor/main/table/table_plugin\";\nimport { isTableCell } from \"@html_editor/utils/dom_info\";\n\nexport class TableResizePlugin extends Plugin {\n    static id = \"tableResize\";\n    static dependencies = [\"table\", \"history\"];\n\n    setup() {\n        this.addDomListener(this.editable, \"dblclick\", this.fitToContent);\n        this.addDomListener(this.editable, \"mousedown\", this.onMousedown);\n        this.addDomListener(this.editable, \"mousemove\", this.onMousemove);\n    }\n\n    /**\n     * If the mouse is hovering over one of the borders of a table cell element,\n     * return the side of that border ('left'|'top'|'right'|'bottom').\n     * Otherwise, return false.\n     *\n     * @private\n     * @param {MouseEvent} ev\n     * @returns {string|boolean}\n     */\n    isHoveringTdBorder(ev) {\n        const target = /** @type {HTMLElement} */ (ev.target);\n        if (ev.target && isTableCell(target) && target.isContentEditable) {\n            const targetRect = target.getBoundingClientRect();\n            if (ev.clientX <= targetRect.x + BORDER_SENSITIVITY) {\n                return \"left\";\n            } else if (ev.clientY <= targetRect.y + BORDER_SENSITIVITY) {\n                return \"top\";\n            } else if (ev.clientX >= targetRect.x + target.clientWidth - BORDER_SENSITIVITY) {\n                return \"right\";\n            } else if (ev.clientY >= targetRect.y + target.clientHeight - BORDER_SENSITIVITY) {\n                return \"bottom\";\n            }\n        }\n        return false;\n    }\n    /**\n     * Change the cursor to a resizing cursor, in the direction specified. If no\n     * direction is specified, return the cursor to its default.\n     *\n     * @private\n     * @param {'col'|'row'|false} direction 'col'/'row' to hint column/row,\n     *                                      false to remove the hints\n     */\n    setTableResizeCursor(direction) {\n        const classList = this.editable.classList;\n        if (classList.contains(\"o_col_resize\")) {\n            classList.remove(\"o_col_resize\");\n        }\n        if (classList.contains(\"o_row_resize\")) {\n            classList.remove(\"o_row_resize\");\n        }\n        if (direction === \"col\") {\n            this.editable.classList.add(\"o_col_resize\");\n        } else if (direction === \"row\") {\n            this.editable.classList.add(\"o_row_resize\");\n        }\n    }\n\n    /**\n     * Resizes a table in the given direction, by \"pulling\" the border between\n     * the given targets (ordered left to right or top to bottom).\n     *\n     * @param {MouseEvent} ev\n     * @param {'col'|'row'} direction\n     * @param {HTMLElement} target1\n     * @param {HTMLElement} target2\n     */\n    resizeTable(ev, direction, target1, target2) {\n        ev.preventDefault();\n        const position = target1 ? (target2 ? \"middle\" : \"last\") : \"first\";\n        let [item, neighbor] = [target1 || target2, target2];\n        const table = closestElement(item, \"table\");\n        const [sizeProp, positionProp, clientPositionProp] =\n            direction === \"col\" ? [\"width\", \"x\", \"clientX\"] : [\"height\", \"y\", \"clientY\"];\n\n        const isRTL = this.config.direction === \"rtl\";\n        // Preserve current width.\n        if (sizeProp === \"width\") {\n            const tableRect = table.getBoundingClientRect();\n            table.style[sizeProp] = tableRect[sizeProp] + \"px\";\n        }\n        const unsizedItemsSelector = `${\n            direction === \"col\" ? \"td\" : \"tr\"\n        }:not([style*=${sizeProp}])`;\n        for (const unsizedItem of table.querySelectorAll(unsizedItemsSelector)) {\n            unsizedItem.style[sizeProp] = unsizedItem.getBoundingClientRect()[sizeProp] + \"px\";\n        }\n\n        // TD widths should only be applied in the first row. Change targets and\n        // clean the rest.\n        if (direction === \"col\") {\n            let hostCell = closestElement(table, isTableCell);\n            const hostCells = [];\n            while (hostCell) {\n                hostCells.push(hostCell);\n                hostCell = closestElement(hostCell.parentElement, isTableCell);\n            }\n            const nthColumn = getColumnIndex(item);\n            const firstRow = [...table.querySelector(\"tr\").children];\n            [item, neighbor] = [firstRow[nthColumn], firstRow[nthColumn + 1]];\n            for (const td of hostCells) {\n                if (\n                    td !== item &&\n                    td !== neighbor &&\n                    closestElement(td, \"table\") === table &&\n                    getColumnIndex(td) !== 0\n                ) {\n                    td.style.removeProperty(sizeProp);\n                }\n            }\n            if (isRTL && position == \"middle\") {\n                [item, neighbor] = [neighbor, item];\n            }\n        }\n\n        const MIN_SIZE = 33; // TODO: ideally, find this value programmatically.\n        switch (position) {\n            case \"first\": {\n                const marginProp =\n                    direction === \"col\" ? (isRTL ? \"marginRight\" : \"marginLeft\") : \"marginTop\";\n                const itemRect = item.getBoundingClientRect();\n                const tableStyle = getComputedStyle(table);\n                const currentMargin = parseFloat(tableStyle[marginProp]);\n                let sizeDelta = itemRect[positionProp] - ev[clientPositionProp];\n                if (direction === \"col\" && isRTL) {\n                    sizeDelta =\n                        ev[clientPositionProp] - itemRect[positionProp] - itemRect[sizeProp];\n                }\n                const newMargin = currentMargin - sizeDelta;\n                const currentSize = itemRect[sizeProp];\n                const newSize = currentSize + sizeDelta;\n                if (newMargin >= 0 && newSize > MIN_SIZE) {\n                    const tableRect = table.getBoundingClientRect();\n                    // Check if a nested table would overflow its parent cell.\n                    const hostCell = closestElement(table.parentElement, isTableCell);\n                    const childTable = item.querySelector(\"table\");\n                    const endProp = isRTL ? \"left\" : \"right\";\n                    if (\n                        direction === \"col\" &&\n                        ((hostCell &&\n                            tableRect[endProp] + sizeDelta >\n                                hostCell.getBoundingClientRect()[endProp] - 5) ||\n                            (childTable &&\n                                childTable.getBoundingClientRect()[endProp] >\n                                    itemRect[endProp] + sizeDelta - 5))\n                    ) {\n                        break;\n                    }\n                    table.style[marginProp] = newMargin + \"px\";\n                    item.style[sizeProp] = newSize + \"px\";\n                    if (sizeProp === \"width\") {\n                        table.style[sizeProp] = tableRect[sizeProp] + sizeDelta + \"px\";\n                    }\n                }\n                break;\n            }\n            case \"middle\": {\n                const [itemRect, neighborRect] = [\n                    item.getBoundingClientRect(),\n                    neighbor.getBoundingClientRect(),\n                ];\n                const [currentSize, newSize] = [\n                    itemRect[sizeProp],\n                    ev[clientPositionProp] - itemRect[positionProp],\n                ];\n                const editableStyle = getComputedStyle(this.editable);\n                const sizeDelta = newSize - currentSize;\n                const currentNeighborSize = neighborRect[sizeProp];\n                const newNeighborSize = currentNeighborSize - sizeDelta;\n                const enclosingCell = closestElement(table, \"td, th\");\n                const containerWidth =\n                    enclosingCell?.getBoundingClientRect().width || this.editable.clientWidth;\n                const maxWidth =\n                    containerWidth -\n                    parseFloat(editableStyle.paddingLeft) -\n                    parseFloat(editableStyle.paddingRight);\n                const tableRect = table.getBoundingClientRect();\n                if (\n                    newSize > MIN_SIZE &&\n                    // prevent resizing horizontally beyond the bounds of\n                    // the editable:\n                    (direction === \"row\" ||\n                        newNeighborSize > MIN_SIZE ||\n                        tableRect[sizeProp] + sizeDelta < maxWidth)\n                ) {\n                    // Check if a nested table would overflow its parent cell.\n                    const childTable = item.querySelector(\"table\");\n                    if (\n                        direction === \"col\" &&\n                        childTable &&\n                        childTable.getBoundingClientRect().right > itemRect.right + sizeDelta - 5\n                    ) {\n                        break;\n                    }\n                    item.style[sizeProp] = newSize + \"px\";\n                    if (direction === \"col\") {\n                        neighbor.style[sizeProp] =\n                            (newNeighborSize > MIN_SIZE ? newNeighborSize : currentNeighborSize) +\n                            \"px\";\n                    } else if (sizeProp === \"width\") {\n                        table.style[sizeProp] = tableRect[sizeProp] + sizeDelta + \"px\";\n                    }\n                }\n                break;\n            }\n            case \"last\": {\n                const itemRect = item.getBoundingClientRect();\n                let sizeDelta =\n                    ev[clientPositionProp] - (itemRect[positionProp] + itemRect[sizeProp]); // todo: rephrase\n                if (direction === \"col\" && isRTL) {\n                    sizeDelta = itemRect[positionProp] - ev[clientPositionProp];\n                }\n                const currentSize = itemRect[sizeProp];\n                const newSize = currentSize + sizeDelta;\n                if ((newSize >= 0 || direction === \"row\") && newSize > MIN_SIZE) {\n                    const tableRect = table.getBoundingClientRect();\n                    // Check if a nested table would overflow its parent cell.\n                    const hostCell = closestElement(table.parentElement, isTableCell);\n                    const childTable = item.querySelector(\"table\");\n                    const endProp = isRTL ? \"left\" : \"right\";\n                    if (\n                        direction === \"col\" &&\n                        ((hostCell &&\n                            tableRect[endProp] + sizeDelta >\n                                hostCell.getBoundingClientRect()[endProp] - 5) ||\n                            (childTable &&\n                                childTable.getBoundingClientRect()[endProp] >\n                                    itemRect[endProp] + sizeDelta - 5))\n                    ) {\n                        break;\n                    }\n                    if (sizeProp === \"width\") {\n                        table.style[sizeProp] = tableRect[sizeProp] + sizeDelta + \"px\";\n                    }\n                    item.style[sizeProp] = newSize + \"px\";\n                }\n                break;\n            }\n        }\n    }\n\n    /**\n     * Resizes rows and columns based on the mouse's double-click on the borders.\n     * Adjusts width of columns or height of rows depending on the cursor position.\n     * Adjacent rows/columns are resized as well.\n     *\n     * @param {MouseEvent} ev - The double-click mouse event.\n     */\n    fitToContent(ev) {\n        const isHoveringTdBorder = this.isHoveringTdBorder(ev);\n        if (!isHoveringTdBorder) {\n            return;\n        }\n        const cell = ev.target;\n        if ([\"left\", \"right\"].includes(isHoveringTdBorder)) {\n            const table = closestElement(cell, \"table\");\n            const currentColumnIndex = getColumnIndex(cell);\n            const currentColumnCells = table.querySelectorAll(\n                `tr :is(td, th):nth-of-type(${currentColumnIndex + 1})`\n            );\n            this.dependencies.table.resetColumnWidth(currentColumnCells[0]);\n            const isLeftSideClick = isHoveringTdBorder === \"left\";\n            if (\n                (isLeftSideClick && currentColumnIndex > 0) ||\n                (!isLeftSideClick && currentColumnIndex < table.rows[0].cells.length - 1)\n            ) {\n                const siblingColumnIndex = isLeftSideClick\n                    ? currentColumnIndex - 1\n                    : currentColumnIndex + 1;\n                const siblingColumnCells = table.querySelectorAll(\n                    `tr :is(td, th):nth-of-type(${siblingColumnIndex + 1})`\n                );\n                this.dependencies.table.resetColumnWidth(siblingColumnCells[0]);\n            }\n        } else if ([\"top\", \"bottom\"].includes(isHoveringTdBorder)) {\n            const currentRow = cell.parentElement;\n            this.dependencies.table.resetRowHeight(currentRow);\n            const siblingRow =\n                isHoveringTdBorder === \"top\"\n                    ? currentRow.previousElementSibling\n                    : currentRow.nextElementSibling;\n            if (siblingRow) {\n                this.dependencies.table.resetRowHeight(siblingRow);\n            }\n        }\n    }\n\n    onMousedown(ev) {\n        const isHoveringTdBorder = this.isHoveringTdBorder(ev);\n        const isRTL = this.config.direction === \"rtl\";\n        if (isHoveringTdBorder) {\n            ev.preventDefault();\n            const direction =\n                { top: \"row\", right: \"col\", bottom: \"row\", left: \"col\" }[isHoveringTdBorder] ||\n                false;\n            let target1, target2;\n            const column = closestElement(ev.target, \"tr\");\n            if (isHoveringTdBorder === \"top\" && column) {\n                target1 = getAdjacentPreviousSiblings(column).find(\n                    (node) => node.nodeName === \"TR\"\n                );\n                target2 = closestElement(ev.target, \"tr\");\n            } else if (isHoveringTdBorder === \"right\") {\n                if (isRTL) {\n                    target1 = getAdjacentPreviousSiblings(ev.target).find(isTableCell);\n                    target2 = ev.target;\n                } else {\n                    target1 = ev.target;\n                    target2 = getAdjacentNextSiblings(ev.target).find(isTableCell);\n                }\n            } else if (isHoveringTdBorder === \"bottom\" && column) {\n                target1 = closestElement(ev.target, \"tr\");\n                target2 = getAdjacentNextSiblings(column).find((node) => node.nodeName === \"TR\");\n            } else if (isHoveringTdBorder === \"left\") {\n                if (isRTL) {\n                    target1 = ev.target;\n                    target2 = getAdjacentNextSiblings(ev.target).find(isTableCell);\n                } else {\n                    target1 = getAdjacentPreviousSiblings(ev.target).find(isTableCell);\n                    target2 = ev.target;\n                }\n            }\n            this.isResizingTable = true;\n            this.setTableResizeCursor(direction);\n            const resizeTable = (ev) => this.resizeTable(ev, direction, target1, target2);\n            const stopResizing = (ev) => {\n                ev.preventDefault();\n                this.isResizingTable = false;\n                this.setTableResizeCursor(false);\n                this.dependencies.history.addStep();\n                this.document.removeEventListener(\"mousemove\", resizeTable);\n                this.document.removeEventListener(\"mouseup\", stopResizing);\n                this.document.removeEventListener(\"mouseleave\", stopResizing);\n            };\n            this.document.addEventListener(\"mousemove\", resizeTable);\n            this.document.addEventListener(\"mouseup\", stopResizing);\n            this.document.addEventListener(\"mouseleave\", stopResizing);\n        }\n    }\n    onMousemove(ev) {\n        const direction =\n            { top: \"row\", right: \"col\", bottom: \"row\", left: \"col\" }[this.isHoveringTdBorder(ev)] ||\n            false;\n        if (direction || !this.isResizingTable) {\n            this.setTableResizeCursor(direction);\n        }\n    }\n}\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { closestElement } from \"@html_editor/utils/dom_traversal\";\nimport { reactive } from \"@odoo/owl\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { TableMenu } from \"./table_menu\";\nimport { TablePicker } from \"./table_picker\";\nimport { isHtmlContentSupported } from \"@html_editor/core/selection_plugin\";\n\n/**\n * This plugin only contains the table ui feature (table picker, menus, ...).\n * All actual table manipulation code is located in the table plugin.\n */\nexport class TableUIPlugin extends Plugin {\n    static id = \"tableUi\";\n    static dependencies = [\"history\", \"overlay\", \"table\"];\n    /** @type {import(\"plugins\").EditorResources} */\n    resources = {\n        user_commands: [\n            {\n                id: \"openTablePicker\",\n                title: _t(\"Table\"),\n                description: _t(\"Insert a table\"),\n                icon: \"fa-table\",\n                run: this.openPickerOrInsertTable.bind(this),\n                isAvailable: isHtmlContentSupported,\n            },\n        ],\n        powerbox_items: [\n            {\n                categoryId: \"structure\",\n                commandId: \"openTablePicker\",\n            },\n        ],\n    };\n\n    setup() {\n        /** @type {import(\"@html_editor/core/overlay_plugin\").Overlay} */\n        this.picker = this.dependencies.overlay.createOverlay(TablePicker, {\n            positionOptions: {\n                updatePositionOnResize: false,\n                onPositioned: (picker, position) => {\n                    const popperRect = picker.getBoundingClientRect();\n                    const { left } = position;\n                    if (this.config.direction === \"rtl\") {\n                        // position from the right instead of the left as it is needed\n                        // to ensure the expand animation is properly done\n                        picker.style.right = `${window.innerWidth - left - popperRect.width}px`;\n                        picker.style.removeProperty(\"left\");\n                    }\n                },\n            },\n        });\n\n        this.activeTd = null;\n\n        /** @type {import(\"@html_editor/core/overlay_plugin\").Overlay} */\n        this.colMenu = this.dependencies.overlay.createOverlay(TableMenu, {\n            positionOptions: {\n                position: \"top-fit\",\n                flip: false,\n            },\n        });\n        /** @type {import(\"@html_editor/core/overlay_plugin\").Overlay} */\n        this.rowMenu = this.dependencies.overlay.createOverlay(TableMenu, {\n            positionOptions: {\n                position: \"left-fit\",\n            },\n        });\n        this.addDomListener(this.document, \"pointermove\", this.onMouseMove);\n        const closeMenus = () => {\n            if (this.isMenuOpened) {\n                this.isMenuOpened = false;\n                this.colMenu.close();\n                this.rowMenu.close();\n            }\n        };\n        this.addDomListener(this.document, \"scroll\", closeMenus, true);\n    }\n\n    openPicker() {\n        this.picker.open({\n            props: {\n                editable: this.editable,\n                overlay: this.picker,\n                direction: this.config.direction || \"ltr\",\n                insertTable: (params) => this.dependencies.table.insertTable(params),\n            },\n        });\n    }\n\n    openPickerOrInsertTable() {\n        if (this.services.ui.isSmall) {\n            this.dependencies.table.insertTable({ cols: 3, rows: 3 });\n        } else {\n            this.openPicker();\n        }\n    }\n\n    onMouseMove(ev) {\n        const target = ev.target;\n        if (this.isMenuOpened) {\n            return;\n        }\n        if (\n            [\"TD\", \"TH\"].includes(target.tagName) &&\n            target !== this.activeTd &&\n            this.editable.contains(target)\n        ) {\n            if (ev.target.isContentEditable && closestElement(target, \"table\").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, \"td, th\");\n            if (!parentTd) {\n                this.setActiveTd(null);\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    setActiveTd(td) {\n        this.activeTd = td;\n        this.colMenu.close();\n        this.rowMenu.close();\n        if (!td) {\n            return;\n        }\n        const withAddStep =\n            (fn) =>\n            (...args) => {\n                fn(...args);\n                this.dependencies.history.addStep();\n            };\n        const tableMethods = {\n            moveColumn: withAddStep(this.dependencies.table.moveColumn),\n            addColumn: withAddStep(this.dependencies.table.addColumn),\n            removeColumn: withAddStep(this.dependencies.table.removeColumn),\n            moveRow: withAddStep(this.dependencies.table.moveRow),\n            addRow: withAddStep(this.dependencies.table.addRow),\n            removeRow: withAddStep(this.dependencies.table.removeRow),\n            turnIntoHeader: withAddStep(this.dependencies.table.turnIntoHeader),\n            turnIntoRow: withAddStep(this.dependencies.table.turnIntoRow),\n            resetRowHeight: withAddStep(this.dependencies.table.resetRowHeight),\n            resetColumnWidth: withAddStep(this.dependencies.table.resetColumnWidth),\n            resetTableSize: withAddStep(this.dependencies.table.resetTableSize),\n            clearColumnContent: withAddStep(this.dependencies.table.clearColumnContent),\n            clearRowContent: withAddStep(this.dependencies.table.clearRowContent),\n        };\n        if (td.cellIndex === 0) {\n            this.rowMenu.open({\n                target: td,\n                props: {\n                    type: \"row\",\n                    overlay: this.rowMenu,\n                    target: td,\n                    dropdownState: this.createDropdownState(this.colMenu),\n                    ...tableMethods,\n                },\n            });\n        }\n        if (td.parentElement.rowIndex === 0) {\n            this.colMenu.open({\n                target: td,\n                props: {\n                    type: \"column\",\n                    overlay: this.colMenu,\n                    target: td,\n                    dropdownState: this.createDropdownState(this.rowMenu),\n                    direction: this.config.direction || \"ltr\",\n                    ...tableMethods,\n                },\n            });\n        }\n    }\n}\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { closestBlock, isBlock } from \"@html_editor/utils/blocks\";\nimport { splitTextNode } from \"@html_editor/utils/dom\";\nimport { isEditorTab, isTextNode, isZWS } from \"@html_editor/utils/dom_info\";\nimport {\n    descendants,\n    getAdjacentPreviousSiblings,\n    closestElement,\n    firstLeaf,\n    selectElements,\n} from \"@html_editor/utils/dom_traversal\";\nimport { parseHTML } from \"@html_editor/utils/html\";\nimport { DIRECTIONS, childNodeIndex } from \"@html_editor/utils/position\";\nimport { isHtmlContentSupported } from \"@html_editor/core/selection_plugin\";\n\nconst tabHtml = '<span class=\"oe-tabs\" contenteditable=\"false\">\\u0009</span>\\u200B';\nconst GRID_COLUMN_WIDTH = 40; //@todo Configurable?\n\n/**\n * Checks if the given tab element represents an indentation.\n * An indentation tab is one that is not preceded by visible text.\n *\n * @param {HTMLElement} tab - The tab element to check.\n * @returns {boolean} - True if the tab represents an indentation, false otherwise.\n */\nfunction isIndentationTab(tab) {\n    return !getAdjacentPreviousSiblings(tab).some(\n        (sibling) => isTextNode(sibling) && !/^[\\u200B\\s]*$/.test(sibling.textContent)\n    );\n}\n\n/**\n * @typedef { Object } TabulationShared\n * @property { TabulationPlugin['indentBlocks'] } indentBlocks\n * @property { TabulationPlugin['outdentBlocks'] } outdentBlocks\n */\n\n/**\n * @typedef {(() => void | true)[]} shift_tab_overrides\n * @typedef {(() => void | true)[]} tab_overrides\n */\n\nexport class TabulationPlugin extends Plugin {\n    static id = \"tabulation\";\n    static dependencies = [\"dom\", \"selection\", \"history\", \"delete\"];\n    static shared = [\"indentBlocks\", \"outdentBlocks\"];\n    /** @type {import(\"plugins\").EditorResources} */\n    resources = {\n        user_commands: [\n            {\n                id: \"tab\",\n                run: this.handleTab.bind(this),\n                isAvailable: isHtmlContentSupported,\n            },\n            {\n                id: \"shiftTab\",\n                run: this.handleShiftTab.bind(this),\n                isAvailable: isHtmlContentSupported,\n            },\n        ],\n        shortcuts: [\n            { hotkey: \"tab\", commandId: \"tab\" },\n            { hotkey: \"shift+tab\", commandId: \"shiftTab\" },\n        ],\n        content_not_editable_providers: (rootEl) => [...selectElements(rootEl, \".oe-tabs\")],\n        contenteditable_to_remove_selector: \"span.oe-tabs\",\n\n        /** Handlers */\n        normalize_handlers: this.normalize.bind(this),\n\n        /** Overrides */\n        delete_forward_overrides: this.handleDeleteForward.bind(this),\n\n        unsplittable_node_predicates: isEditorTab, // avoid merge\n    };\n\n    handleTab() {\n        if (this.delegateTo(\"tab_overrides\")) {\n            return;\n        }\n\n        const selection = this.dependencies.selection.getEditableSelection();\n        if (selection.isCollapsed) {\n            this.insertTab();\n        } else {\n            const targetedBlocks = this.dependencies.selection.getTargetedBlocks();\n            this.indentBlocks(targetedBlocks);\n        }\n        this.dependencies.history.addStep();\n    }\n\n    handleShiftTab() {\n        if (this.delegateTo(\"shift_tab_overrides\")) {\n            return;\n        }\n        const targetedBlocks = this.dependencies.selection.getTargetedBlocks();\n        this.outdentBlocks(targetedBlocks);\n        this.dependencies.history.addStep();\n    }\n\n    insertTab() {\n        const selection = this.dependencies.selection.getEditableSelection();\n        const element = closestElement(selection.anchorNode);\n        const isSelectionAtStart =\n            firstLeaf(element) === selection.anchorNode &&\n            (selection.anchorOffset === 0 || element.textContent === \"\\u200B\");\n        const tab = parseHTML(this.document, tabHtml);\n        if (isSelectionAtStart && !isBlock(element)) {\n            element.before(tab);\n        } else {\n            this.dependencies.dom.insert(tab);\n        }\n    }\n\n    /**\n     * @param {HTMLElement} blocks\n     */\n    indentBlocks(blocks) {\n        const selectionToRestore = this.dependencies.selection.getEditableSelection();\n        const tab = parseHTML(this.document, tabHtml);\n        for (const block of blocks) {\n            block.prepend(tab.cloneNode(true));\n        }\n        this.dependencies.selection.setSelection(selectionToRestore, { normalize: false });\n    }\n\n    /**\n     * @param {HTMLElement} blocks\n     */\n    outdentBlocks(blocks) {\n        for (const block of blocks) {\n            const firstTab = descendants(block).find(isEditorTab);\n            if (firstTab && isIndentationTab(firstTab)) {\n                this.removeTrailingZWS(firstTab);\n                firstTab.remove();\n            }\n        }\n    }\n\n    removeTrailingZWS(tab) {\n        const selection = this.dependencies.selection.getEditableSelection();\n        const { anchorNode, anchorOffset, focusNode, focusOffset } = selection;\n        const updateAnchor = anchorNode === tab.nextSibling;\n        const updateFocus = focusNode === tab.nextSibling;\n        let zwsRemoved = 0;\n        while (\n            tab.nextSibling &&\n            tab.nextSibling.nodeType === Node.TEXT_NODE &&\n            tab.nextSibling.textContent.startsWith(\"\\u200B\")\n        ) {\n            splitTextNode(tab.nextSibling, 1, DIRECTIONS.LEFT);\n            tab.nextSibling.remove();\n            zwsRemoved++;\n        }\n        if (updateAnchor || updateFocus) {\n            this.dependencies.selection.setSelection({\n                anchorNode: updateAnchor ? tab.nextSibling : anchorNode,\n                anchorOffset: updateAnchor ? Math.max(0, anchorOffset - zwsRemoved) : anchorOffset,\n                focusNode: updateFocus ? tab.nextSibling : focusNode,\n                focusOffset: updateFocus ? Math.max(0, focusOffset - zwsRemoved) : focusOffset,\n            });\n        }\n    }\n\n    /**\n     * @param {HTMLSpanElement} tabSpan - span.oe-tabs element\n     */\n    adjustTabWidth(tabSpan) {\n        let tabPreviousSibling = tabSpan.previousSibling;\n        while (isZWS(tabPreviousSibling)) {\n            tabPreviousSibling = tabPreviousSibling.previousSibling;\n        }\n        if (isEditorTab(tabPreviousSibling)) {\n            tabSpan.style.width = `${GRID_COLUMN_WIDTH}px`;\n            return;\n        }\n        const spanRect = tabSpan.getBoundingClientRect();\n        const referenceRect = this.editable.firstElementChild?.getBoundingClientRect();\n        // @ todo @phoenix Re-evaluate if this check is necessary.\n        // Values from getBoundingClientRect() are all zeros during\n        // Editor startup or saving. We cannot recalculate the tabs\n        // width in thoses cases.\n        if (!referenceRect?.width || !spanRect.width) {\n            return;\n        }\n        const relativePosition = spanRect.left - referenceRect.left;\n        const distToNextGridLine = GRID_COLUMN_WIDTH - (relativePosition % GRID_COLUMN_WIDTH);\n        // Round to the first decimal point.\n        const width = distToNextGridLine.toFixed(1);\n        tabSpan.style.width = `${width}px`;\n    }\n\n    /**\n     * Aligns the tabs under the specified tree to a grid.\n     *\n     * @param {HTMLElement} [root] - The tree root.\n     */\n    alignTabs(root = this.editable) {\n        const block = closestBlock(root);\n        if (!block) {\n            return;\n        }\n        for (const tab of block.querySelectorAll(\"span.oe-tabs\")) {\n            this.adjustTabWidth(tab);\n        }\n    }\n\n    // When deleting an editor tab, we need to ensure it's related\n    // ZWS will deleted as well.\n    // @todo @phoenix: for some reason, there might be more than one ZWS.\n    // Investigate why.\n    expandRangeToIncludeZWS(tabElement) {\n        let previous = tabElement;\n        let node = tabElement.nextSibling;\n        while (node?.nodeType === Node.TEXT_NODE) {\n            for (let i = 0; i < node.textContent.length; i++) {\n                if (node.textContent[i] !== \"\\u200B\") {\n                    return [node, i];\n                }\n            }\n            previous = node;\n            node = node.nextSibling;\n        }\n        return [previous.parentElement, childNodeIndex(previous) + 1];\n    }\n\n    // @todo consider registering this as adjustRange callback instead.\n    handleDeleteForward(range) {\n        let { endContainer, endOffset } = range;\n        if (!(endContainer?.nodeType === Node.ELEMENT_NODE) || !endOffset) {\n            return;\n        }\n        const nodeToDelete = endContainer.childNodes[endOffset - 1];\n        if (isEditorTab(nodeToDelete)) {\n            [endContainer, endOffset] = this.expandRangeToIncludeZWS(nodeToDelete);\n            range = this.dependencies.delete.deleteRange({ ...range, endContainer, endOffset });\n            this.dependencies.selection.setSelection({\n                anchorNode: range.startContainer,\n                anchorOffset: range.startOffset,\n            });\n            return true;\n        }\n    }\n    normalize(el) {\n        this.alignTabs(el);\n    }\n}\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { Plugin } from \"../plugin\";\nimport { closestBlock } from \"../utils/blocks\";\nimport { closestElement } from \"../utils/dom_traversal\";\nimport { isContentEditable, isTextNode } from \"@html_editor/utils/dom_info\";\nimport { isHtmlContentSupported } from \"@html_editor/core/selection_plugin\";\n\nexport class TextDirectionPlugin extends Plugin {\n    static id = \"textDirection\";\n    static dependencies = [\"selection\", \"history\", \"split\", \"format\"];\n    /** @type {import(\"plugins\").EditorResources} */\n    resources = {\n        user_commands: [\n            {\n                id: \"switchDirection\",\n                title: _t(\"Switch direction\"),\n                description: _t(\"Switch the text's direction\"),\n                icon: \"fa-exchange\",\n                run: this.switchDirection.bind(this),\n                isAvailable: isHtmlContentSupported,\n            },\n        ],\n        powerbox_items: [\n            {\n                categoryId: \"format\",\n                commandId: \"switchDirection\",\n            },\n        ],\n    };\n\n    setup() {\n        if (this.config.direction) {\n            this.editable.setAttribute(\"dir\", this.config.direction);\n        }\n        this.direction = this.config.direction || \"ltr\";\n    }\n\n    switchDirection() {\n        const selection = this.dependencies.split.splitSelection();\n        const targetedTextNodes = [\n            selection.anchorNode,\n            ...this.dependencies.selection.getTargetedNodes(),\n        ].filter((n) => isTextNode(n) && isContentEditable(n) && n.nodeValue.trim().length);\n        const blocks = new Set(\n            targetedTextNodes.map(\n                (textNode) =>\n                    closestElement(textNode, \"ul,ol\") ||\n                    closestElement(textNode, \"[data-embedded='toggleBlock']\") ||\n                    closestBlock(textNode)\n            )\n        );\n\n        const shouldApplyStyle = !this.dependencies.format.isSelectionFormat(\"switchDirection\");\n\n        for (const block of blocks) {\n            for (const node of block.querySelectorAll(\"ul,ol\")) {\n                blocks.add(node);\n            }\n        }\n        for (const block of blocks) {\n            if (!shouldApplyStyle) {\n                block.removeAttribute(\"dir\");\n            } else {\n                block.setAttribute(\"dir\", this.direction === \"ltr\" ? \"rtl\" : \"ltr\");\n            }\n        }\n\n        for (const element of blocks) {\n            const style = getComputedStyle(element);\n            if (style.direction === \"ltr\" && style.textAlign === \"right\") {\n                element.style.setProperty(\"text-align\", \"left\");\n            } else if (style.direction === \"rtl\" && style.textAlign === \"left\") {\n                element.style.setProperty(\"text-align\", \"right\");\n            }\n        }\n        this.dependencies.history.addStep();\n    }\n}\n", "import { Component, onMounted, useExternalListener, useRef } from \"@odoo/owl\";\nimport { Toolbar } from \"./toolbar\";\n\nexport class ToolbarMobile extends Component {\n    static template = \"html_editor.MobileToolbar\";\n    static props = [\"*\"];\n    static components = {\n        Toolbar,\n    };\n\n    setup() {\n        this.toolbar = useRef(\"toolbarWrapper\");\n        useExternalListener(window.visualViewport, \"resize\", this.fixToolbarPosition);\n        useExternalListener(window.visualViewport, \"scroll\", this.fixToolbarPosition);\n        onMounted(() => {\n            this.fixToolbarPosition();\n        });\n    }\n\n    /**\n     * Fixes the position of the toolbar for the keyboard height.\n     */\n    fixToolbarPosition() {\n        const keyboardHeight =\n            window.innerHeight - (window.visualViewport.height + window.visualViewport.offsetTop);\n        if (keyboardHeight > 0) {\n            this.toolbar.el.style.bottom = `${keyboardHeight}px`;\n        } else {\n            this.toolbar.el.style.bottom = `0px`;\n        }\n    }\n}\n", "import { Component, useState, validate } from \"@odoo/owl\";\nimport { omit, pick } from \"@web/core/utils/objects\";\n\nexport class Toolbar extends Component {\n    static template = \"html_editor.Toolbar\";\n    static props = {\n        class: { type: String, optional: true },\n        getSelection: Function,\n        focusEditable: Function,\n        state: {\n            type: Object,\n            shape: {\n                namespace: { type: String, optional: true },\n                buttonGroups: {\n                    type: Array,\n                    element: {\n                        type: Object,\n                        shape: {\n                            id: String,\n                            buttons: {\n                                type: Array,\n                                element: {\n                                    type: Object,\n                                    validate: (button) => {\n                                        const base = {\n                                            id: String,\n                                            description: String,\n                                            isDisabled: Boolean,\n                                        };\n                                        if (button.Component) {\n                                            validate(button, {\n                                                ...base,\n                                                Component: Function,\n                                                props: { type: Object, optional: true },\n                                            });\n                                        } else {\n                                            validate(button, {\n                                                ...base,\n                                                run: Function,\n                                                icon: { type: String, optional: true },\n                                                text: { type: String, optional: true },\n                                                isActive: Boolean,\n                                            });\n                                        }\n                                        return true;\n                                    },\n                                },\n                            },\n                        },\n                    },\n                },\n            },\n        },\n    };\n\n    setup() {\n        this.state = useState(this.props.state);\n    }\n\n    onButtonClick(button) {\n        button.run();\n        this.props.focusEditable();\n    }\n}\n\nexport const toolbarButtonProps = {\n    title: [String, Function],\n    getSelection: Function,\n    isDisabled: Boolean,\n};\n\n/** @typedef {import(\"@html_editor/core/user_command_plugin\").UserCommand} UserCommand */\n/** @typedef {import(\"./toolbar_plugin\").ToolbarCommandItem} ToolbarCommandItem */\n/** @typedef {import(\"./toolbar_plugin\").ToolbarCommandButton} ToolbarCommandButton */\n\n/**\n * @param {UserCommand} userCommand\n * @param {ToolbarCommandItem} toolbarItem\n * @returns {ToolbarCommandButton}\n */\nexport function composeToolbarButton(userCommand, toolbarItem) {\n    const description = toolbarItem.description || userCommand.description;\n    return {\n        ...pick(userCommand, \"icon\"),\n        ...omit(toolbarItem, \"commandId\", \"commandParams\"),\n        run: () => userCommand.run(toolbarItem.commandParams),\n        isAvailable: (selection) =>\n            [userCommand.isAvailable, toolbarItem.isAvailable]\n                .filter(Boolean)\n                .every((predicate) => predicate(selection)),\n        description: description instanceof Function ? description : () => description,\n    };\n}\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { isEmptyTextNode, isZWS } from \"@html_editor/utils/dom_info\";\nimport { reactive } from \"@odoo/owl\";\nimport { composeToolbarButton, Toolbar } from \"./toolbar\";\nimport { hasTouch } from \"@web/core/browser/feature_detection\";\nimport { registry } from \"@web/core/registry\";\nimport { ToolbarMobile } from \"./mobile_toolbar\";\nimport { debounce } from \"@web/core/utils/timing\";\nimport { omit, pick } from \"@web/core/utils/objects\";\nimport { withSequence } from \"@html_editor/utils/resource\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { memoize } from \"@web/core/utils/functions\";\nimport { closestElement } from \"@html_editor/utils/dom_traversal\";\n\n/** @typedef { import(\"@html_editor/core/selection_plugin\").EditorSelection } EditorSelection */\n/** @typedef {import(\"@html_editor/core/selection_plugin\").SelectionData} SelectionData */\n/** @typedef { import(\"@html_editor/core/user_command_plugin\").UserCommand } UserCommand */\n/** @typedef { import(\"@web/core/l10n/translation.js\")._t} _t */\n/** @typedef { ReturnType<_t> } TranslatedString */\n/** @typedef { (selection: EditorSelection, nodes: Node[]) => TranslatedString } TranslatedStringGetter */\n\n/**\n * @typedef {Object} ToolbarNamespace\n * @property {string} id\n * @property {(targetedNodes: Node[]) => boolean} isApplied\n *\n *\n * @typedef {Object} ToolbarGroup\n * @property {string} id\n * @property {string[]} [namespaces]\n *\n *\n * @typedef {ToolbarCommandItem | ToolbarComponentItem} ToolbarItem\n *\n * @typedef {Object} ToolbarCommandItem\n * Regular button: derives from a user command specified by commandId.\n * The properties maked with * can be omitted if they are present in the user command.\n * The ones marked with ?* are both optional and derivable from the user command.\n * @property {string} id\n * @property {string} groupId Id of a toolbar group\n * @property {string} commandId\n * @property {string[]} [namespaces]\n * @property {Object} [commandParams] Passed to the command's `run` function\n * @property {TranslatedString | TranslatedStringGetter} [description] * - becomes the button's title (and tooltip content)\n * @property {string} [icon] *\n * @property {string} [text] Can be used with (or instead of) `icon`\n * @property {(selection: EditorSelection) => boolean} [isAvailable] ? *\n * @property {(selection: EditorSelection, nodes: Node[]) => boolean} [isActive]\n * @property {(selection: EditorSelection, nodes: Node[]) => boolean} [isDisabled]\n *\n * @typedef {Object} ToolbarComponentItem\n * Adds a custom component to the toolbar.\n * @property {string} id\n * @property {string} groupId\n * @property {string[]} [namespaces]\n * @property {TranslatedString | TranslatedStringGetter} [description]\n * @property {Function} Component\n * @property {Object} props\n * @property {(selection: EditorSelection) => boolean} [isAvailable]\n *\n * ToolbarItem.id maps to the button's `name` attribute\n * ToolbarItem.description maps to the button's `title` attribute (tooltip content)\n */\n\n/**\n * Types after conversion to renderable toolbar buttons:\n *\n * @typedef {Object} ToolbarCommandButton\n * @property {string} id\n * @property {string} groupId\n * @property {TranslatedStringGetter} description\n * @property {Function} run\n * @property {string} [icon]\n * @property {string} [text]\n * @property {(selection: EditorSelection) => boolean} isAvailable\n * @property {(selection: EditorSelection, nodes: Node[]) => boolean} [isActive]\n * @property {(selection: EditorSelection, nodes: Node[]) => boolean} [isDisabled]\n *\n * @typedef {Object} ToolbarComponentButton\n * Adds a custom component to the toolbar (processed version with required fields).\n * @property {string} id\n * @property {string} groupId\n * @property {string[]} [namespaces]\n * @property {TranslatedStringGetter} description\n * @property {Function} Component\n * @property {Object} props\n * @property {(selection: EditorSelection) => boolean} isAvailable\n *\n * @typedef {ToolbarCommandButton | ToolbarComponentButton} ToolbarButton\n */\n\n/** Delay in ms for toolbar open after keyup, double click or triple click. */\nconst DELAY_TOOLBAR_OPEN = 300;\n/** Number of buttons below which toolbar will open directly in its expanded form */\nconst MIN_SIZE_FOR_COMPACT = 7;\n/** Special namespace that prevents the toolbar from opening */\nexport const DISABLED_NAMESPACE = \"disabled\";\n\n/**\n * @typedef { Object } ToolbarShared\n * @property { ToolbarPlugin['getToolbarInfo'] } getToolbarInfo\n */\n\n/**\n * @typedef {((namespace: string) => boolean)[]} can_display_toolbar\n * @typedef {((selectionData: SelectionData) => boolean)[]} collapsed_selection_toolbar_predicate\n *\n * @typedef {ToolbarGroup[]} toolbar_groups\n * @typedef {ToolbarNamespace[]} toolbar_namespaces\n */\n\n/**\n * @see UserCommand\n * @typedef {(ToolbarCommandItem | ToolbarComponentItem)[]} toolbar_items\n *\n * A ToolbarCommandItem must derive from a user command (see UserCommand)\n * specified by commandId. Properties defined in a toolbar item override those\n * from a user command.\n *\n * Example:\n *\n *     resources = {\n *         // see UserCommand\n *         user_commands: [\n *             {\n *                 id: myCommand,\n *                 run: myCommandFunction,\n *                 description: _t(\"My Command\"),\n *                 icon: \"fa-bug\",\n *             },\n *         ],\n *         // see ToolbarGroup\n *         toolbar_groups: [\n *             { id: \"myGroup\" },\n *         ],\n *         toolbar_items: [\n *             // See ToolbarCommandItem\n *             {\n *                 id: \"myButton\",\n *                 groupId: \"myGroup\",\n *                 commandId: \"myCommand\",\n *                 description: _t(\"My Toolbar Command Button\"), // overrides the user command's `description`\n *                 // `icon` is inferred from the user command\n *             },\n *             // See ToolbarComponentItem\n *             {\n *                 id: \"myComponentButton\",\n *                 groupId: \"myGroup\",\n *                 description: _t(\"My Toolbar Component Button\"),\n *                 Component: MyComponent,\n *                 props: { myProp: \"myValue\" },\n *             },\n *         ],\n *     };\n */\n\nexport class ToolbarPlugin extends Plugin {\n    static id = \"toolbar\";\n    static dependencies = [\"overlay\", \"selection\", \"userCommand\"];\n    static shared = [\"getToolbarInfo\"];\n    /** @type {import(\"plugins\").EditorResources} */\n    resources = {\n        selectionchange_handlers: this.handleSelectionChange.bind(this),\n        selection_leave_handlers: () => this.closeToolbar(),\n        step_added_handlers: () => this.updateToolbar(),\n        user_commands: {\n            id: \"expandToolbar\",\n            run: () => {\n                this.isToolbarExpanded = true;\n                this.updateToolbar();\n            },\n        },\n        toolbar_groups: [\n            withSequence(100, { id: \"expand_toolbar\", namespaces: [\"compact\"] }),\n            withSequence(30, { id: \"layout\" }),\n        ],\n        toolbar_items: {\n            id: \"expand_toolbar\",\n            groupId: \"expand_toolbar\",\n            commandId: \"expandToolbar\",\n            description: _t(\"Expand toolbar\"),\n            icon: \"oi-ellipsis-v\",\n        },\n        toolbar_namespace_providers: [\n            withSequence(100, (targetedNodes, editableSelection) =>\n                this.isToolbarVisible(targetedNodes, editableSelection) ? \"compact\" : undefined\n            ),\n        ],\n    };\n\n    setup() {\n        const groupIds = new Set();\n        for (const group of this.getResource(\"toolbar_groups\")) {\n            if (groupIds.has(group.id)) {\n                throw new Error(`Duplicate toolbar group id: ${group.id}`);\n            }\n            groupIds.add(group.id);\n        }\n        this.buttonGroups = this.getButtonGroups();\n        this.buttonsByNamespace = { DISABLED_NAMESPACE: [] };\n\n        this.isMobileToolbar = hasTouch() && window.visualViewport;\n\n        if (this.isMobileToolbar) {\n            this.overlay = new MobileToolbarOverlay(this.editable);\n        } else {\n            this.overlay = this.dependencies.overlay.createOverlay(Toolbar, {\n                positionOptions: {\n                    position: \"top-start\",\n                },\n                closeOnPointerdown: false,\n            });\n        }\n        this.state = reactive({ buttonGroups: [], namespace: undefined });\n\n        this.onSelectionChangeActive = true;\n        this.debouncedUpdateToolbar = debounce(this._updateToolbar, DELAY_TOOLBAR_OPEN);\n\n        if (this.isMobileToolbar) {\n            this.addDomListener(this.editable, \"pointerup\", () => {\n                // Collapse toolbar to compact mode when tapping outside of it\n                this.isToolbarExpanded = false;\n            });\n        } else {\n            // Mouse interaction behavior:\n            // Close toolbar on mousedown and prevent it from opening until mouseup.\n            this.addDomListener(this.editable, \"mousedown\", (ev) => {\n                this.closeToolbar(this.dependencies.selection.getSelectionData());\n                this.debouncedUpdateToolbar.cancel();\n                this.onSelectionChangeActive = false;\n            });\n            this.addGlobalDomListener(\"mouseup\", (ev) => {\n                if (ev.detail >= 2) {\n                    // Delayed open, waiting for a possible triple click.\n                    this.onSelectionChangeActive = true;\n                    this.debouncedUpdateToolbar();\n                } else {\n                    // Fast open, just wait for a possible selection change due\n                    // to mouseup.\n                    setTimeout(() => {\n                        this.updateToolbar();\n                        this.onSelectionChangeActive = true;\n                    });\n                }\n            });\n\n            // Keyboard interaction behavior:\n            // Close toolbar on keydown Arrows and prevent it from opening until\n            // keyup. Opening is debounced to avoid open/close between\n            // sequential keystrokes.\n            this.addDomListener(this.editable, \"keydown\", (ev) => {\n                // reason for \"key?\":\n                // On Chrome, if there is a password saved for a login page,\n                // a mouse click trigger a keydown event without any key\n                if (ev.key?.startsWith(\"Arrow\")) {\n                    this.closeToolbar(this.dependencies.selection.getSelectionData());\n                    this.onSelectionChangeActive = false;\n                }\n            });\n            this.addDomListener(this.editable, \"keyup\", (ev) => {\n                if (ev.key?.startsWith(\"Arrow\")) {\n                    this.onSelectionChangeActive = true;\n                    this.debouncedUpdateToolbar();\n                }\n            });\n        }\n        this.isToolbarExpanded = false;\n        this.toolbarProps = {\n            class: \"shadow rounded my-2\",\n            getSelection: () => this.dependencies.selection.getSelectionData(),\n            focusEditable: () => this.dependencies.selection.focusEditable(),\n            state: this.state,\n        };\n    }\n\n    destroy() {\n        this.debouncedUpdateToolbar.cancel();\n        this.updateToolbar.cancel();\n        this.overlay.close();\n        super.destroy();\n    }\n\n    /**\n     * @returns {ToolbarButton[]}\n     */\n    getButtons() {\n        /** @type {ToolbarItem[]} */\n        const toolbarItems = this.getResource(\"toolbar_items\");\n\n        /** @type {(item: ToolbarCommandItem) => ToolbarCommandButton} */\n        const commandItemToButton = (item) => {\n            const command = this.dependencies.userCommand.getCommand(item.commandId);\n            return composeToolbarButton(command, item);\n        };\n        /** @type {(item: ToolbarComponentItem) => ToolbarComponentButton} */\n        const componentItemToButton = (item) => ({\n            isAvailable: () => true,\n            ...item,\n            description:\n                item.description instanceof Function ? item.description : () => item.description,\n        });\n\n        return toolbarItems.map((item) =>\n            \"Component\" in item ? componentItemToButton(item) : commandItemToButton(item)\n        );\n    }\n\n    getButtonGroups() {\n        const buttons = this.getButtons();\n        /** @type {ToolbarGroup[]} */\n        const groups = this.getResource(\"toolbar_groups\");\n\n        return groups.map((group) => ({\n            ...omit(group, \"namespaces\"),\n            buttons: buttons\n                .filter((button) => button.groupId === group.id)\n                .map((button) => ({\n                    ...button,\n                    namespaces: button.namespaces || group.namespaces || [\"expanded\"],\n                })),\n        }));\n    }\n\n    /**\n     * @returns ToolbarButton[]\n     */\n    getButtonsForNamespace(namespace) {\n        if (this.buttonsByNamespace[namespace]) {\n            return this.buttonsByNamespace[namespace];\n        }\n        const button = this.buttonGroups.flatMap((group) =>\n            group.buttons.filter((btn) => btn.namespaces.includes(namespace))\n        );\n        this.buttonsByNamespace[namespace] = button;\n        return button;\n    }\n\n    getToolbarInfo() {\n        return {\n            buttonGroups: this.buttonGroups,\n        };\n    }\n\n    handleSelectionChange(selectionData) {\n        if (this.onSelectionChangeActive) {\n            this.updateToolbar(selectionData);\n        }\n    }\n\n    /**\n     * Different handlers might call updateToolbar (e.g. step added and\n     * selection change) in the same tick. To avoid unnecessary updates, we\n     * batch the calls.\n     */\n    updateToolbar = debounce(this._updateToolbar, 0, { trailing: true });\n    _updateToolbar(selectionData = this.dependencies.selection.getSelectionData()) {\n        // Prevent toolbar to open if the selection is not in the editable area,\n        // or if the selection is protected or protecting.\n        if (\n            !selectionData.currentSelectionIsInEditable ||\n            selectionData.documentSelectionIsProtected ||\n            selectionData.documentSelectionIsProtecting\n        ) {\n            this.closeToolbar();\n            return;\n        }\n        // Prevent toolbar to open if the selection is only non-editable nodes.\n        const targetedNodes = this.dependencies.selection.getTargetedNodes();\n        if (targetedNodes.every((node) => !this.dependencies.selection.isNodeEditable(node))) {\n            this.closeToolbar();\n            return;\n        }\n        // Determine the namespace to use\n        let currentNamespace = null;\n        let filteredtargetedNodes = [];\n        filteredtargetedNodes = this.getFilteredTargetedNodes(targetedNodes);\n        for (const fn of this.getResource(\"toolbar_namespace_providers\")) {\n            currentNamespace = fn(filteredtargetedNodes, selectionData.editableSelection);\n            if (currentNamespace) {\n                break;\n            }\n        }\n\n        if (currentNamespace === DISABLED_NAMESPACE) {\n            this.closeToolbar();\n            return;\n        }\n\n        if (currentNamespace === \"compact\" && this.isToolbarExpanded) {\n            currentNamespace = \"expanded\";\n        }\n\n        if (currentNamespace) {\n            this.state.namespace = currentNamespace;\n            // Do not reposition the toolbar if it's already open.\n            if (!this.overlay.isOpen) {\n                this.overlay.open({ props: this.toolbarProps });\n            }\n            this.updateButtonsStates(selectionData.editableSelection, filteredtargetedNodes);\n        } else {\n            this.closeToolbar();\n        }\n    }\n\n    getFilteredTargetedNodes(targetedNodes) {\n        return targetedNodes\n            .filter(\n                (node) =>\n                    this.dependencies.selection.isNodeEditable(node) &&\n                    (node.nodeType !== Node.TEXT_NODE || (!isEmptyTextNode(node) && !isZWS(node)))\n            )\n            .filter((node) => {\n                const element = closestElement(node);\n                const style = this.document.defaultView.getComputedStyle(element);\n                return style.display !== \"none\" && style.visibility !== \"hidden\";\n            });\n    }\n\n    isToolbarVisible(targetedNodes, editableSelection) {\n        if (this.isMobileToolbar) {\n            return true;\n        }\n\n        const isCollapsed = editableSelection.isCollapsed;\n        if (isCollapsed) {\n            return false;\n        }\n        // Only allow the toolbar to open if the selection contains visible selected characters.\n        const selectionText = editableSelection.textContent();\n        const textCleaned = selectionText.replace(/(\\r\\n|\\n|\\r|\\u200B|\\uFEFF)/gm, \"\");\n        if (textCleaned.length) {\n            return true;\n        }\n        // Even without textContent we display the toolbar if the selection contains a <br>\n        return targetedNodes.some(\n            (node) => node.nodeType === Node.ELEMENT_NODE && node.tagName === \"BR\"\n        );\n    }\n\n    /**\n     * @param {SelectionData} selectionData\n     */\n    closeToolbar(selectionData = null) {\n        if (!this.overlay.isOpen) {\n            return;\n        }\n        // TODO: refactor candidate : Remove data-prevent-closing-overlay\n        const anchor = selectionData?.documentSelectionIsInEditable\n            ? selectionData.editableSelection?.anchorNode\n            : document.getSelection()?.anchorNode;\n        const shouldPreventClosing =\n            anchor?.closest?.(\"[data-prevent-closing-overlay]\")?.dataset?.preventClosingOverlay ===\n            \"true\";\n        if (!shouldPreventClosing) {\n            this.overlay.close();\n            this.isToolbarExpanded = false;\n            this.state.namespace = null;\n        }\n    }\n\n    /**\n     * @param {EditorSelection} selection\n     * @param {Node[]} targetedNodes\n     */\n    updateButtonsStates(selection, targetedNodes) {\n        const availableButtons = this.getAvailableButtonsSet(selection);\n        this.state.buttonGroups = this.buttonGroups\n            .map((group) => ({\n                id: group.id,\n                buttons: group.buttons\n                    .filter((button) => availableButtons.has(button))\n                    .map((button) => ({\n                        id: button.id,\n                        description: button.description(selection, targetedNodes),\n                        isDisabled: !!button.isDisabled?.(selection, targetedNodes),\n                        ...(button.Component\n                            ? pick(button, \"Component\", \"props\")\n                            : {\n                                  ...pick(button, \"run\", \"icon\", \"text\"),\n                                  isActive: !!button.isActive?.(selection, targetedNodes),\n                              }),\n                    })),\n            }))\n            // Filter out groups left empty\n            .filter((group) => group.buttons.length > 0);\n    }\n\n    /**\n     * Get the set of available buttons for the current namespace and selection.\n     *\n     * @param {EditorSelection} selection\n     * @returns {Set<ToolbarButton>}\n     */\n    getAvailableButtonsSet(selection) {\n        if (this.state.namespace === \"compact\") {\n            return this.getAvailableButtonsCompact(selection);\n        }\n        const isAvailable = (button) => button.isAvailable(selection);\n        return new Set(this.getButtonsForNamespace(this.state.namespace).filter(isAvailable));\n    }\n\n    /**\n     * We only display the toolbar in its compact form if the expanded form is\n     * larger than a threshold, and larger than the compact version itself.\n     * Otherwise, we display the expanded toolbar directly.\n     *\n     * @param {EditorSelection} selection\n     * @returns {Set<ToolbarButton>}\n     */\n    getAvailableButtonsCompact(selection) {\n        const isAvailable = memoize((button) => button.isAvailable(selection));\n        const compact = this.getButtonsForNamespace(\"compact\").filter(isAvailable);\n        const expanded = this.getButtonsForNamespace(\"expanded\").filter(isAvailable);\n        const shouldDisplayCompactToolbar =\n            // Expanded version is big enough\n            expanded.length >= MIN_SIZE_FOR_COMPACT &&\n            // Expanded version is bigger than the compact version\n            expanded.length > compact.length;\n        if (shouldDisplayCompactToolbar) {\n            return new Set(compact);\n        }\n        this.state.namespace = \"expanded\";\n        return new Set(expanded);\n    }\n}\n\nclass MobileToolbarOverlay {\n    constructor(editable) {\n        this.isOpen = false;\n        this.overlayId = `mobile_toolbar_${Math.random().toString(16).slice(2)}`;\n        this.editable = editable;\n    }\n\n    open({ props }) {\n        props.class = \"shadow\";\n        if (!this.isOpen) {\n            const modal = this.editable.closest(\".o_modal_full\");\n            if (modal) {\n                // Same height of the toolbar\n                modal.style.paddingBottom = \"40px\";\n            }\n            registry.category(\"main_components\").add(this.overlayId, {\n                Component: ToolbarMobile,\n                props,\n            });\n            this.isOpen = true;\n        }\n    }\n\n    close() {\n        const modal = this.editable.closest(\".o_modal_full\");\n        if (modal) {\n            modal.style.paddingBottom = \"\";\n        }\n        registry.category(\"main_components\").remove(this.overlayId, \"MobileToolbar\");\n        this.isOpen = false;\n    }\n}\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { Plugin } from \"../plugin\";\nimport { VideoSelector } from \"./media/media_dialog/video_selector\";\n\nexport const YOUTUBE_URL_GET_VIDEO_ID =\n    /^(?:(?:https?:)?\\/\\/)?(?:(?:www|m)\\.)?(?:youtube\\.com|youtu\\.be)(?:\\/(?:[\\w-]+\\?v=|embed\\/|v\\/)?)([^\\s?&#]+)(?:\\S+)?$/i;\n\nexport class YoutubePlugin extends Plugin {\n    static id = \"youtube\";\n    static dependencies = [\"history\", \"dom\"];\n    /** @type {import(\"plugins\").EditorResources} */\n    resources = {\n        ...(this.config.allowVideo && {\n            paste_media_url_command_providers: this.getCommandForVideoUrlPaste.bind(this),\n        }),\n    };\n    /**\n     * @param {string} url\n     */\n    getCommandForVideoUrlPaste(url) {\n        const youtubeUrl = YOUTUBE_URL_GET_VIDEO_ID.exec(url);\n        if (youtubeUrl) {\n            // URL is a YouTube video.\n            return {\n                title: _t(\"Embed Youtube Video\"),\n                description: _t(\"Embed the youtube video in the document.\"),\n                icon: \"fa-youtube-play\",\n                run: async () => {\n                    const videoElement = await this.getYoutubeVideoElement(youtubeUrl[0]);\n                    this.dependencies.dom.insert(videoElement);\n                    this.dependencies.history.addStep();\n                },\n            };\n        }\n    }\n    // @todo @phoenix: Should this be in this plugin?\n    /**\n     * @param {string} url\n     * @returns {HTMLElement} saved video element or undefined if the URL\n     * is not a valid YouTube video URL.\n     */\n    async getYoutubeVideoElement(url) {\n        if (!URL.canParse(url)) {\n            return;\n        }\n        const parsedUrl = new URL(url);\n        const urlParams = parsedUrl.searchParams;\n        const start_from = urlParams.get(\"start\") || urlParams.get(\"t\");\n\n        const autoplay = urlParams.get(\"autoplay\") === \"1\";\n        const loop = urlParams.get(\"loop\") === \"1\";\n        const hide_controls = urlParams.get(\"controls\") === \"0\";\n        const hide_fullscreen = urlParams.get(\"fs\") === \"0\";\n\n        const videoData = await rpc(\"/html_editor/video_url/data\", {\n            video_url: url,\n            autoplay,\n            loop,\n            hide_controls,\n            hide_fullscreen,\n            start_from,\n        });\n        const savedVideo = this.createVideoElement(videoData);\n        savedVideo.classList.add(...VideoSelector.mediaSpecificClasses);\n        return savedVideo;\n    }\n\n    createVideoElement(videoData) {\n        return VideoSelector.createElements([{ src: videoData.embed_url }])[0];\n    }\n}\n", "const urlParams = new URLSearchParams(window.location.search);\nconst collaborationDebug = urlParams.get(\"collaborationDebug\");\nconst COLLABORATION_LOCALSTORAGE_KEY = \"odoo_editor_collaboration_debug\";\nif (typeof collaborationDebug === \"string\") {\n    if (collaborationDebug === \"false\") {\n        localStorage.removeItem(\n            COLLABORATION_LOCALSTORAGE_KEY,\n            urlParams.get(\"collaborationDebug\")\n        );\n    } else {\n        localStorage.setItem(COLLABORATION_LOCALSTORAGE_KEY, urlParams.get(\"collaborationDebug\"));\n    }\n}\nconst debugValue = localStorage.getItem(COLLABORATION_LOCALSTORAGE_KEY);\n\nconst debugShowLog = [\"\", \"true\", \"all\"].includes(debugValue);\nconst debugShowNotifications = debugValue === \"all\";\n\nconst baseNotificationMethods = {\n    ptp_request: async function (notification) {\n        const { requestId, requestName, requestPayload, requestTransport } =\n            notification.notificationPayload;\n        this._onRequest(\n            notification.fromPeerId,\n            requestId,\n            requestName,\n            requestPayload,\n            requestTransport\n        );\n    },\n    ptp_request_result: function (notification) {\n        const { requestId, result } = notification.notificationPayload;\n        // If not in _pendingRequestResolver, it means it has timeout.\n        if (this._pendingRequestResolver[requestId]) {\n            clearTimeout(this._pendingRequestResolver[requestId].rejectTimeout);\n            this._pendingRequestResolver[requestId].resolve(result);\n            delete this._pendingRequestResolver[requestId];\n        }\n    },\n\n    ptp_join: async function (notification) {\n        const peerId = notification.fromPeerId;\n        if (this.peersInfos[peerId] && this.peersInfos[peerId].peerConnection) {\n            return this.peersInfos[peerId];\n        }\n        this._createPeer(peerId);\n    },\n\n    rtc_signal_icecandidate: async function (notification) {\n        if (debugShowLog) {\n            console.log(`%creceive candidate`, \"background: darkgreen; color: white;\");\n        }\n        const peerInfos = this.peersInfos[notification.fromPeerId];\n        if (\n            !peerInfos ||\n            !peerInfos.peerConnection ||\n            peerInfos.peerConnection.connectionState === \"closed\"\n        ) {\n            console.groupCollapsed(\"=== ERROR: Handle Ice Candidate from undefined|closed ===\");\n            console.trace(peerInfos);\n            console.groupEnd();\n            return;\n        }\n        if (!peerInfos.peerConnection.remoteDescription) {\n            peerInfos.iceCandidateBuffer.push(notification.notificationPayload);\n        } else {\n            this._addIceCandidate(peerInfos, notification.notificationPayload);\n        }\n    },\n    rtc_signal_description: async function (notification) {\n        const description = notification.notificationPayload;\n        if (debugShowLog) {\n            console.log(\n                `%cdescription received:`,\n                \"background: blueviolet; color: white;\",\n                description\n            );\n        }\n\n        const peerInfos =\n            this.peersInfos[notification.fromPeerId] || this._createPeer(notification.fromPeerId);\n        const pc = peerInfos.peerConnection;\n\n        if (!pc || pc.connectionState === \"closed\") {\n            if (debugShowLog) {\n                console.groupCollapsed(\"=== ERROR: handle offer ===\");\n                console.log(\n                    \"An offer has been received for a non-existent peer connection - peer: \" +\n                        notification.fromPeerId\n                );\n                console.trace(pc && pc.connectionState);\n                console.groupEnd();\n            }\n            return;\n        }\n\n        // Skip if we already have an offer.\n        if (pc.signalingState === \"have-remote-offer\") {\n            return;\n        }\n\n        // If there is a racing conditing with the signaling offer (two\n        // being sent at the same time). We need one peer that abort by\n        // rollbacking to a stable signaling state where the other is\n        // continuing the process. The peer that is polite is the one that\n        // will rollback.\n        const isPolite =\n            (\"\" + notification.fromPeerId).localeCompare(\"\" + this._currentPeerId) === 1;\n        if (debugShowLog) {\n            console.log(\n                `%cisPolite: %c${isPolite}`,\n                \"background: deepskyblue;\",\n                `background:${isPolite ? \"green\" : \"red\"}`\n            );\n        }\n\n        const isOfferRacing =\n            description.type === \"offer\" &&\n            (peerInfos.makingOffer || pc.signalingState !== \"stable\");\n        // If there is a racing conditing with the signaling offer and the\n        // peer is impolite, we must not process this offer and wait for\n        // the answer for the signaling process to continue.\n        if (isOfferRacing && !isPolite) {\n            if (debugShowLog) {\n                console.log(\n                    `%creturn because isOfferRacing && !isPolite. pc.signalingState: ${pc.signalingState}`,\n                    \"background: red;\"\n                );\n            }\n            return;\n        }\n        if (debugShowLog) {\n            console.log(`%cisOfferRacing: ${isOfferRacing}`, \"background: red;\");\n            console.log(`%c SETREMOTEDESCRIPTION`, \"background: navy; color:white;\");\n        }\n        try {\n            await pc.setRemoteDescription(description);\n        } catch (e) {\n            if (e instanceof DOMException && e.name === \"InvalidStateError\") {\n                console.error(e);\n                return;\n            } else {\n                throw e;\n            }\n        }\n        if (peerInfos.iceCandidateBuffer.length) {\n            for (const candidate of peerInfos.iceCandidateBuffer) {\n                await this._addIceCandidate(peerInfos, candidate);\n            }\n            peerInfos.iceCandidateBuffer.splice(0);\n        }\n        if (description.type === \"offer\") {\n            const answerDescription = await pc.createAnswer();\n            try {\n                await pc.setLocalDescription(answerDescription);\n            } catch (e) {\n                if (e instanceof DOMException && e.name === \"InvalidStateError\") {\n                    console.error(e);\n                    return;\n                } else {\n                    throw e;\n                }\n            }\n            this.notifyPeer(notification.fromPeerId, \"rtc_signal_description\", pc.localDescription);\n        }\n    },\n};\n\nexport class PeerToPeer {\n    constructor(options) {\n        this.options = options;\n        this._currentPeerId = this.options.currentPeerId;\n        if (debugShowLog) {\n            console.log(\n                `%c currentPeerId:${this._currentPeerId}`,\n                \"background: blue; color: white;\"\n            );\n        }\n\n        // peerId -> PeerInfos\n        this.peersInfos = {};\n        this._lastRequestId = -1;\n        this._pendingRequestResolver = {};\n        this._stopped = false;\n    }\n\n    stop() {\n        this.closeAllConnections();\n        this._stopped = true;\n    }\n\n    getConnectedPeerIds() {\n        return Object.entries(this.peersInfos)\n            .filter(\n                ([id, infos]) =>\n                    infos.peerConnection &&\n                    infos.peerConnection.iceConnectionState === \"connected\" &&\n                    infos.dataChannel &&\n                    infos.dataChannel.readyState === \"open\"\n            )\n            .map(([id]) => id);\n    }\n\n    removePeer(peerId) {\n        if (debugShowLog) {\n            console.log(`%c REMOVE PEER ${peerId}`, \"background: chocolate;\");\n        }\n        this.notifySelf(\"ptp_remove\", peerId);\n        const peerInfos = this.peersInfos[peerId];\n        if (!peerInfos) {\n            return;\n        }\n        clearTimeout(peerInfos.fallbackTimeout);\n        clearTimeout(peerInfos.zombieTimeout);\n        peerInfos.dataChannel && peerInfos.dataChannel.close();\n        peerInfos.peerConnection && peerInfos.peerConnection.close();\n        delete this.peersInfos[peerId];\n    }\n\n    closeAllConnections() {\n        for (const peerId of Object.keys(this.peersInfos)) {\n            this.notifyAllPeers(\"ptp_disconnect\");\n            this.removePeer(peerId);\n        }\n    }\n\n    async notifyAllPeers(notificationName, notificationPayload, { transport = \"server\" } = {}) {\n        if (this._stopped) {\n            return;\n        }\n        const transportPayload = {\n            fromPeerId: this._currentPeerId,\n            notificationName,\n            notificationPayload,\n        };\n        if (transport === \"server\") {\n            await this.options.broadcastAll(transportPayload);\n        } else if (transport === \"rtc\") {\n            for (const cliendId of Object.keys(this.peersInfos)) {\n                this._channelNotify(cliendId, transportPayload);\n            }\n        } else {\n            throw new Error(\n                `Transport \"${transport}\" is not supported. Use \"server\" or \"rtc\" transport.`\n            );\n        }\n    }\n\n    notifyPeer(peerId, notificationName, notificationPayload, { transport = \"server\" } = {}) {\n        if (this._stopped) {\n            return;\n        }\n        if (debugShowNotifications) {\n            if (notificationName === \"ptp_request_result\") {\n                console.log(\n                    `%c${Date.now()} - REQUEST RESULT SEND: %c${transport}:${\n                        notificationPayload.requestId\n                    }:${this._currentPeerId.slice(\"-5\")}:${peerId.slice(\"-5\")}`,\n                    \"color: #aaa;font-weight:bold;\",\n                    \"color: #aaa;font-weight:normal\"\n                );\n            } else if (notificationName === \"ptp_request\") {\n                console.log(\n                    `%c${Date.now()} - REQUEST SEND: %c${transport}:${\n                        notificationPayload.requestName\n                    }|${notificationPayload.requestId}:${this._currentPeerId.slice(\n                        \"-5\"\n                    )}:${peerId.slice(\"-5\")}`,\n                    \"color: #aaa;font-weight:bold;\",\n                    \"color: #aaa;font-weight:normal\"\n                );\n            } else {\n                console.log(\n                    `%c${Date.now()} - NOTIFICATION SEND: %c${transport}:${notificationName}:${this._currentPeerId.slice(\n                        \"-5\"\n                    )}:${peerId.slice(\"-5\")}`,\n                    \"color: #aaa;font-weight:bold;\",\n                    \"color: #aaa;font-weight:normal\"\n                );\n            }\n        }\n        const transportPayload = {\n            fromPeerId: this._currentPeerId,\n            toPeerId: peerId,\n            notificationName,\n            notificationPayload,\n        };\n        if (transport === \"server\") {\n            this.options.broadcastAll(transportPayload);\n        } else if (transport === \"rtc\") {\n            this._channelNotify(peerId, transportPayload);\n        } else {\n            throw new Error(\n                `Transport \"${transport}\" is not supported. Use \"server\" or \"rtc\" transport.`\n            );\n        }\n    }\n\n    notifySelf(notificationName, notificationPayload) {\n        if (this._stopped) {\n            return;\n        }\n        return this.handleNotification({ notificationName, notificationPayload });\n    }\n\n    handleNotification(notification) {\n        if (this._stopped) {\n            return;\n        }\n        const isInternalNotification =\n            typeof notification.fromPeerId === \"undefined\" &&\n            typeof notification.toPeerId === \"undefined\";\n        if (\n            isInternalNotification ||\n            (notification.fromPeerId !== this._currentPeerId && !notification.toPeerId) ||\n            notification.toPeerId === this._currentPeerId\n        ) {\n            if (debugShowNotifications) {\n                if (notification.notificationName === \"ptp_request_result\") {\n                    console.log(\n                        `%c${Date.now()} - REQUEST RESULT RECEIVE: %c${\n                            notification.notificationPayload.requestId\n                        }:${notification.fromPeerId.slice(\"-5\")}:${notification.toPeerId.slice(\n                            \"-5\"\n                        )}`,\n                        \"color: #aaa;font-weight:bold;\",\n                        \"color: #aaa;font-weight:normal\"\n                    );\n                } else if (notification.notificationName === \"ptp_request\") {\n                    console.log(\n                        `%c${Date.now()} - REQUEST RECEIVE: %c${\n                            notification.notificationPayload.requestName\n                        }|${\n                            notification.notificationPayload.requestId\n                        }:${notification.fromPeerId.slice(\"-5\")}:${notification.toPeerId.slice(\n                            \"-5\"\n                        )}`,\n                        \"color: #aaa;font-weight:bold;\",\n                        \"color: #aaa;font-weight:normal\"\n                    );\n                } else {\n                    console.log(\n                        `%c${Date.now()} - NOTIFICATION RECEIVE: %c${\n                            notification.notificationName\n                        }:${notification.fromPeerId}:${notification.toPeerId}`,\n                        \"color: #aaa;font-weight:bold;\",\n                        \"color: #aaa;font-weight:normal\"\n                    );\n                }\n            }\n            try {\n                const baseMethod = baseNotificationMethods[notification.notificationName];\n                if (baseMethod) {\n                    return baseMethod.call(this, notification);\n                }\n                if (this.options.onNotification) {\n                    return this.options.onNotification(notification);\n                }\n            } catch (error) {\n                console.groupCollapsed(\"=== ERROR: On notification in collaboration ===\");\n                console.error(error);\n                console.groupEnd();\n            }\n        }\n    }\n\n    requestPeer(peerId, requestName, requestPayload, { transport = \"server\" } = {}) {\n        if (this._stopped) {\n            return;\n        }\n        return new Promise((resolve, reject) => {\n            const requestId = this._getRequestId();\n\n            const abort = (reason) => {\n                clearTimeout(rejectTimeout);\n                delete this._pendingRequestResolver[requestId];\n                reject(new RequestError(reason || \"Request was aborted.\"));\n            };\n            const rejectTimeout = setTimeout(\n                () => abort(\"Request took too long (more than 10 seconds).\"),\n                10000\n            );\n\n            this._pendingRequestResolver[requestId] = {\n                resolve,\n                rejectTimeout,\n                abort,\n            };\n\n            this.notifyPeer(\n                peerId,\n                \"ptp_request\",\n                {\n                    requestId,\n                    requestName,\n                    requestPayload,\n                    requestTransport: transport,\n                },\n                { transport }\n            );\n        });\n    }\n    abortCurrentRequests() {\n        for (const { abort } of Object.values(this._pendingRequestResolver)) {\n            abort();\n        }\n    }\n    _createPeer(peerId, { makeOffer = true } = {}) {\n        if (this._stopped) {\n            return;\n        }\n        if (debugShowLog) {\n            console.log(\"CREATE CONNECTION with peer id:\", peerId);\n        }\n        this.peersInfos[peerId] = {\n            makingOffer: false,\n            iceCandidateBuffer: [],\n            backoffFactor: 0,\n        };\n\n        if (!navigator.onLine) {\n            return this.peersInfos[peerId];\n        }\n        const pc = new RTCPeerConnection(this.options.peerConnectionConfig);\n\n        if (makeOffer) {\n            pc.onnegotiationneeded = async () => {\n                if (debugShowLog) {\n                    console.log(\n                        `%c NEGONATION NEEDED: ${pc.connectionState}`,\n                        \"background: deeppink;\"\n                    );\n                }\n                try {\n                    this.peersInfos[peerId].makingOffer = true;\n                    if (debugShowLog) {\n                        console.log(\n                            `%ccreating and sending an offer`,\n                            \"background: darkmagenta; color: white;\"\n                        );\n                    }\n                    const offer = await pc.createOffer();\n                    // Avoid race condition.\n                    if (pc.signalingState !== \"stable\") {\n                        return;\n                    }\n                    await pc.setLocalDescription(offer);\n                    this.notifyPeer(peerId, \"rtc_signal_description\", pc.localDescription);\n                } catch (err) {\n                    console.error(err);\n                } finally {\n                    this.peersInfos[peerId].makingOffer = false;\n                }\n            };\n        }\n        pc.onicecandidate = async (event) => {\n            if (event.candidate) {\n                this.notifyPeer(peerId, \"rtc_signal_icecandidate\", event.candidate);\n            }\n        };\n        pc.oniceconnectionstatechange = async () => {\n            if (debugShowLog) {\n                console.log(\"ICE STATE UPDATE: \" + pc.iceConnectionState);\n            }\n\n            switch (pc.iceConnectionState) {\n                case \"failed\":\n                case \"closed\":\n                    this.removePeer(peerId);\n                    break;\n                case \"disconnected\":\n                    if (navigator.onLine) {\n                        await this._recoverConnection(peerId, {\n                            delay: 3000,\n                            reason: \"ice connection disconnected\",\n                        });\n                    }\n                    break;\n                case \"connected\":\n                    this.peersInfos[peerId].backoffFactor = 0;\n                    break;\n            }\n        };\n        // This event does not work in FF. Let's try with oniceconnectionstatechange if it is sufficient.\n        pc.onconnectionstatechange = async () => {\n            if (debugShowLog) {\n                console.log(\"CONNECTION STATE UPDATE:\" + pc.connectionState);\n            }\n\n            switch (pc.connectionState) {\n                case \"failed\":\n                case \"closed\":\n                    this.removePeer(peerId);\n                    break;\n                case \"disconnected\":\n                    if (navigator.onLine) {\n                        await this._recoverConnection(peerId, {\n                            delay: 3000,\n                            reason: \"connection disconnected\",\n                        });\n                    }\n                    break;\n                case \"connected\":\n                case \"completed\":\n                    this.peersInfos[peerId].backoffFactor = 0;\n                    break;\n            }\n        };\n        pc.onicecandidateerror = async (error) => {\n            if (debugShowLog) {\n                console.groupCollapsed(\"=== ERROR: onIceCandidate ===\");\n                console.log(\n                    \"connectionState: \" +\n                        pc.connectionState +\n                        \" - iceState: \" +\n                        pc.iceConnectionState\n                );\n                console.trace(error);\n                console.groupEnd();\n            }\n            this._recoverConnection(peerId, { delay: 3000, reason: \"ice candidate error\" });\n        };\n        const dataChannel = pc.createDataChannel(\"notifications\", { negotiated: true, id: 1 });\n        let message = [];\n        dataChannel.onmessage = (event) => {\n            if (event.data !== \"-\") {\n                message.push(event.data);\n            } else {\n                this.handleNotification(JSON.parse(message.join(\"\")));\n                message = [];\n            }\n        };\n        dataChannel.onopen = (event) => {\n            this.notifySelf(\"rtc_data_channel_open\", {\n                connectionPeerId: peerId,\n            });\n        };\n\n        this.peersInfos[peerId].peerConnection = pc;\n        this.peersInfos[peerId].dataChannel = dataChannel;\n\n        return this.peersInfos[peerId];\n    }\n    async _addIceCandidate(peerInfos, candidate) {\n        const rtcIceCandidate = new RTCIceCandidate(candidate);\n        try {\n            await peerInfos.peerConnection.addIceCandidate(rtcIceCandidate);\n        } catch (error) {\n            // Ignored.\n            console.groupCollapsed(\"=== ERROR: ADD ICE CANDIDATE ===\");\n            console.trace(error);\n            console.groupEnd();\n        }\n    }\n\n    _channelNotify(peerId, transportPayload) {\n        if (this._stopped) {\n            return;\n        }\n        const peerInfo = this.peersInfos[peerId];\n        const dataChannel = peerInfo && peerInfo.dataChannel;\n\n        if (!dataChannel || dataChannel.readyState !== \"open\") {\n            if (peerInfo && !peerInfo.zombieTimeout) {\n                if (debugShowLog) {\n                    console.warn(\n                        `Impossible to communicate with peer ${peerId}. The connection will be killed in 10 seconds if the datachannel state has not changed.`\n                    );\n                }\n                this._killPotentialZombie(peerId);\n            }\n        } else {\n            const str = JSON.stringify(transportPayload);\n            const size = str.length;\n            const maxStringLength = 5000;\n            let from = 0;\n            let to = maxStringLength;\n            while (from < size) {\n                dataChannel.send(str.slice(from, to));\n                from = to;\n                to = to += maxStringLength;\n            }\n            dataChannel.send(\"-\");\n        }\n    }\n\n    _getRequestId() {\n        this._lastRequestId++;\n        return this._lastRequestId;\n    }\n\n    async _onRequest(fromPeerId, requestId, requestName, requestPayload, requestTransport) {\n        if (this._stopped) {\n            return;\n        }\n        const requestFunction = this.options.onRequest && this.options.onRequest[requestName];\n        const result = await requestFunction({\n            fromPeerId,\n            requestId,\n            requestName,\n            requestPayload,\n        });\n        this.notifyPeer(\n            fromPeerId,\n            \"ptp_request_result\",\n            { requestId, result },\n            { transport: requestTransport }\n        );\n    }\n    /**\n     * Attempts a connection recovery by updating the tracks, which will start\n     * a new transaction: negotiationneeded -> offer -> answer -> ...\n     *\n     * @private\n     * @param {Object} [param1]\n     * @param {number} [param1.delay] in ms\n     * @param {string} [param1.reason]\n     */\n    _recoverConnection(peerId, { delay = 0, reason = \"\" } = {}) {\n        if (this._stopped) {\n            this.removePeer(peerId);\n            return;\n        }\n        const peerInfos = this.peersInfos[peerId];\n        if (!peerInfos || peerInfos.fallbackTimeout) {\n            return;\n        }\n        const backoffFactor = this.peersInfos[peerId].backoffFactor;\n        const backoffDelay = delay * Math.pow(2, backoffFactor);\n        // Stop trying to recover the connection after 10 attempts.\n        if (backoffFactor > 10) {\n            if (debugShowLog) {\n                console.log(\n                    `%c STOP RTC RECOVERY: impossible to connect to peer ${peerId}: ${reason}`,\n                    \"background: darkred; color: white;\"\n                );\n            }\n            return;\n        }\n\n        peerInfos.fallbackTimeout = setTimeout(async () => {\n            peerInfos.fallbackTimeout = undefined;\n            const pc = peerInfos.peerConnection;\n            if (!pc || pc.iceConnectionState === \"connected\") {\n                return;\n            }\n            if ([\"connected\", \"closed\"].includes(pc.connectionState)) {\n                return;\n            }\n            // hard reset: recreating a RTCPeerConnection\n            if (debugShowLog) {\n                console.log(\n                    `%c RTC RECOVERY: calling back peer ${peerId} to salvage the connection ${pc.iceConnectionState} after ${backoffDelay}ms, reason: ${reason}`,\n                    \"background: darkorange; color: white;\"\n                );\n            }\n            this.removePeer(peerId);\n            const newPeerInfos = this._createPeer(peerId);\n            newPeerInfos.backoffFactor = backoffFactor + 1;\n        }, backoffDelay);\n    }\n    // todo: do we try to salvage the connection after killing the zombie ?\n    // Maybe the salvage should be done when the connection is dropped.\n    _killPotentialZombie(peerId) {\n        if (this._stopped) {\n            this.removePeer(peerId);\n            return;\n        }\n        const peerInfos = this.peersInfos[peerId];\n        if (!peerInfos || peerInfos.zombieTimeout) {\n            return;\n        }\n\n        // If there is no connection after 10 seconds, terminate.\n        peerInfos.zombieTimeout = setTimeout(() => {\n            if (peerInfos && peerInfos.dataChannel && peerInfos.dataChannel.readyState !== \"open\") {\n                if (debugShowLog) {\n                    console.log(`%c KILL ZOMBIE ${peerId}`, \"background: red;\");\n                }\n                this.removePeer(peerId);\n            } else {\n                if (debugShowLog) {\n                    console.log(`%c NOT A ZOMBIE ${peerId}`, \"background: green;\");\n                }\n            }\n        }, 10000);\n    }\n}\n\nexport class RequestError extends Error {\n    constructor(message) {\n        super(message);\n        this.name = \"RequestError\";\n    }\n}\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { user } from \"@web/core/user\";\nimport { Mutex } from \"@web/core/utils/concurrency\";\nimport { debounce } from \"@web/core/utils/timing\";\nimport { PeerToPeer, RequestError } from \"./PeerToPeer\";\nimport { ancestors } from \"@html_editor/utils/dom_traversal\";\nimport { childNodeIndex } from \"@html_editor/utils/position\";\n\n/**\n * @typedef {Object} CollaborationSelection\n * @property {import(\"@html_editor/core/history_plugin\").SerializedSelection} selection\n * @property {string} color\n * @property {string} peerId\n */\n\n// Time to consider a user offline in ms. This fixes the problem of the\n// navigator closing rtc connection when the mac laptop screen is closed.\n// const CONSIDER_OFFLINE_TIME = 1000;\n// Check wether the computer could be offline. This fixes the problem of the\n// navigator closing rtc connection when the mac laptop screen is closed.\n// This case happens on Mac OS on every browser when the user close it's laptop\n// screen. At first, the os/navigator closes all rtc connection, and after some\n// times, the os/navigator internet goes offline without triggering an\n// offline/online event.\n// However, if the laptop screen is open and the connection is properly remove\n// (e.g. disconnect wifi), the event is properly triggered.\n// const CHECK_OFFLINE_TIME = 1000;\n// const PTP_PEER_DISCONNECTED_STATES = [\"failed\", \"closed\", \"disconnected\"];\n\n// Time in ms to wait when trying to aggregate snapshots from other peers and\n// potentially recover from a missing step before trying to apply those\n// snapshots or recover from the server.\nconst PTP_MAX_RECOVERY_TIME = 500;\n\nconst REQUEST_ERROR = Symbol(\"REQUEST_ERROR\");\n\n// this is a local cache for ice server descriptions\nlet ICE_SERVERS = null;\n\n/**\n * @typedef { Object } CollaborationOdooShared\n * @property { CollaborationOdooPlugin['getPeerMetadata'] } getPeerMetadata\n */\n\nexport class CollaborationOdooPlugin extends Plugin {\n    static id = \"collaborationOdoo\";\n    static dependencies = [\"baseContainer\", \"history\", \"collaboration\", \"selection\"];\n    static shared = [\"getPeerMetadata\"];\n    /** @type {import(\"plugins\").EditorResources} */\n    resources = {\n        selectionchange_handlers: debounce(() => {\n            this.ptp?.notifyAllPeers(\n                \"oe_history_set_selection\",\n                this.getCurrentCollaborativeSelection(),\n                {\n                    transport: \"rtc\",\n                }\n            );\n        }, 50),\n        clean_for_save_handlers: ({ root }) => this.attachHistoryIds(root),\n        history_missing_parent_step_handlers: this.onHistoryMissingParentStep.bind(this),\n        history_reset_handlers: this.onReset.bind(this),\n        step_added_handlers: ({ step }) =>\n            this.ptp?.notifyAllPeers(\"oe_history_step\", step, { transport: \"rtc\" }),\n    };\n\n    setup() {\n        this.isDocumentStale = false;\n\n        this.ptpJoined = false;\n\n        // Each time a reset of the document is triggered, it is assigned a\n        // unique identifier. Since resetting the editor involves asynchronous\n        // requests, it is possible that subsequent resets are triggered before\n        // the previous one is complete. This property identifies the latest\n        // reset and can be compared against to cancel the processing of late\n        // responses from previous resets.\n        this.lastCollaborationResetId = 0;\n\n        // The ID is the latest step ID that the server knows through\n        // `data-last-history-steps`. We cannot save to the server if we do not\n        // have that ID in our history ids as it means that our version is\n        // stale.\n        this.serverLastStepId =\n            this.config.content && this.getLastHistoryStepId(this.config.content);\n\n        this.setupCollaboration(this.config.collaboration.collaborationChannel);\n\n        const collaborativeTrigger = this.config.collaboration.collaborativeTrigger;\n        this.joinPeerToPeer = this.joinPeerToPeer.bind(this);\n        if (collaborativeTrigger === \"start\") {\n            this.joinPeerToPeer();\n        } else if (\n            collaborativeTrigger === \"focus\" ||\n            typeof collaborativeTrigger === \"undefined\"\n        ) {\n            // Wait until editor is focused to join the peer to peer network.\n            this.editable.addEventListener(\"focus\", this.joinPeerToPeer);\n        }\n\n        stripHistoryIds(this.editable);\n    }\n    destroy() {\n        this.collaborationStopBus && this.collaborationStopBus();\n        // If peer to peer is initializing, wait for properly closing it.\n        if (this.peerToPeerLoading) {\n            this.peerToPeerLoading.then(() => {\n                this.stopPeerToPeer();\n            });\n        }\n        // todo: to implement\n        // clearInterval(this.collaborationInterval);\n        super.destroy();\n    }\n\n    stopPeerToPeer() {\n        this.joiningPtp = false;\n        this.ptpJoined = false;\n        this.resetCollabRequests();\n        this.ptp && this.ptp.stop();\n    }\n\n    getCurrentCollaborativeSelection() {\n        const selection = this.dependencies.selection.getEditableSelection();\n        return {\n            selection: this.dependencies.history.serializeSelection(selection),\n            peerId: this.config.collaboration.peerId,\n        };\n    }\n    setupCollaboration(collaborationChannel) {\n        const modelName = collaborationChannel.collaborationModelName;\n        const fieldName = collaborationChannel.collaborationFieldName;\n        const resId = collaborationChannel.collaborationResId;\n        const channelName = `editor_collaboration:${modelName}:${fieldName}:${resId}`;\n\n        if (\n            !(modelName && fieldName && resId)\n            // todo: handle this feature\n            // || Wysiwyg.activeCollaborationChannelNames.has(channelName)\n        ) {\n            return;\n        }\n\n        this.collaborationChannelName = channelName;\n        this.historyStepsBuffer = [];\n        // Wysiwyg.activeCollaborationChannelNames.add(channelName);\n\n        const collaborationBusListener = (payload) => {\n            if (\n                payload.model_name === modelName &&\n                payload.field_name === fieldName &&\n                payload.res_id === resId\n            ) {\n                if (payload.notificationName === \"html_field_write\") {\n                    this.onServerLastIdUpdate(payload.notificationPayload.last_step_id);\n                } else if (this.ptpJoined) {\n                    this.peerToPeerLoading.then(() => this.ptp.handleNotification(payload));\n                }\n            }\n        };\n        const { busService } = this.config.collaboration;\n        busService.subscribe(\"editor_collaboration\", collaborationBusListener);\n        busService.addChannel(this.collaborationChannelName);\n        this.collaborationStopBus = () => {\n            // Wysiwyg.activeCollaborationChannelNames.delete(this.collaborationChannelName);\n            busService.unsubscribe(\"editor_collaboration\", collaborationBusListener);\n            busService.deleteChannel(this.collaborationChannelName);\n        };\n\n        this.startCollaborationTime = new Date().getTime();\n\n        // this.checkConnectionChange = () => {\n        //     if (!this.ptp) {\n        //         return;\n        //     }\n        //     if (!navigator.onLine) {\n        //         this.signalOffline();\n        //     } else {\n        //         this.signalOnline();\n        //     }\n        // };\n\n        // window.addEventListener(\"online\", this.checkConnectionChange);\n        // window.addEventListener(\"offline\", this.checkConnectionChange);\n\n        // this.collaborationInterval = setInterval(async () => {\n        //     if (this.offlineTimeout || this.preSavePromise || !this.ptp) {\n        //         return;\n        //     }\n\n        //     const peersInfos = Object.values(this.ptp.peersInfos);\n        //     const couldBeDisconnected =\n        //         Boolean(peersInfos.length) &&\n        //         peersInfos.every((x) =>\n        //             PTP_PEER_DISCONNECTED_STATES.includes(\n        //                 x.peerConnection && x.peerConnection.connectionState\n        //             )\n        //         );\n\n        //     if (couldBeDisconnected) {\n        //         this.offlineTimeout = setTimeout(() => {\n        //             this.signalOffline();\n        //         }, CONSIDER_OFFLINE_TIME);\n        //     }\n        // }, CHECK_OFFLINE_TIME);\n\n        const loadPeerToPeer = async () => {\n            if (!ICE_SERVERS) {\n                ICE_SERVERS = await rpc(\"/html_editor/get_ice_servers\");\n            }\n\n            let iceServers = ICE_SERVERS;\n            if (!iceServers.length) {\n                iceServers = [\n                    {\n                        urls: [\"stun:stun1.l.google.com:19302\", \"stun:stun2.l.google.com:19302\"],\n                    },\n                ];\n            }\n            this.iceServers = iceServers;\n\n            this.ptp = this.getNewPtp();\n        };\n\n        this.peerToPeerLoading = loadPeerToPeer();\n    }\n\n    getNewPtp() {\n        const rpcMutex = new Mutex();\n        const { collaborationChannel } = this.config.collaboration;\n        const modelName = collaborationChannel.collaborationModelName;\n        const fieldName = collaborationChannel.collaborationFieldName;\n        const resId = collaborationChannel.collaborationResId;\n\n        // Wether or not the history has been sent or received at least\n        // once.\n        this.historySyncAtLeastOnce = false;\n\n        return new PeerToPeer({\n            peerConnectionConfig: { iceServers: this.iceServers },\n            currentPeerId: this.config.collaboration.peerId,\n            broadcastAll: (rpcData) =>\n                rpcMutex.exec(async () =>\n                    rpc(\"/html_editor/bus_broadcast\", {\n                        model_name: modelName,\n                        field_name: fieldName,\n                        res_id: resId,\n                        bus_data: rpcData,\n                    })\n                ),\n            onRequest: {\n                get_peer_metadata: this.getMetadata.bind(this),\n                get_missing_steps: (params) =>\n                    this.dependencies.collaboration.historyGetMissingSteps(params.requestPayload),\n                get_history_from_snapshot: () => this.getHistorySnapshot(),\n                get_collaborative_selection: () => this.getCurrentCollaborativeSelection(),\n                recover_document: (params) => {\n                    const { serverDocumentId, fromStepId } = params.requestPayload;\n                    if (\n                        !this.dependencies.collaboration.getBranchIds().includes(serverDocumentId)\n                    ) {\n                        return;\n                    }\n                    return {\n                        missingSteps: this.dependencies.collaboration.historyGetMissingSteps({\n                            fromStepId,\n                        }),\n                        snapshot: this.getHistorySnapshot(),\n                    };\n                },\n            },\n            onNotification: async (notification) => {\n                this.dispatchTo(\"collaboration_notification_handlers\", notification);\n                let { fromPeerId, notificationName, notificationPayload } = notification;\n                switch (notificationName) {\n                    case \"ptp_remove\":\n                        // todo: to implement\n                        // this.odooEditor.multiselectionRemove(notificationPayload);\n                        break;\n                    case \"ptp_disconnect\":\n                        this.ptp.removePeer(fromPeerId);\n                        // todo: to implement\n                        // this.odooEditor.multiselectionRemove(fromPeerId);\n                        break;\n                    case \"rtc_data_channel_open\": {\n                        fromPeerId = notificationPayload.connectionPeerId;\n                        const metadata = await this.requestPeer(\n                            fromPeerId,\n                            \"get_peer_metadata\",\n                            undefined,\n                            { transport: \"rtc\" }\n                        );\n                        if (metadata === REQUEST_ERROR) {\n                            return;\n                        }\n\n                        this.ptp.peersInfos[fromPeerId].metadata = metadata;\n\n                        if (!this.historySyncAtLeastOnce) {\n                            const localPeer = {\n                                id: this.config.collaboration.peerId,\n                                startTime: this.startCollaborationTime,\n                            };\n                            const remotePeer = {\n                                id: fromPeerId,\n                                startTime: metadata.startTime,\n                            };\n                            if (isPeerFirst(localPeer, remotePeer)) {\n                                this.historySyncAtLeastOnce = true;\n                                this.historySyncFinished = true;\n                            } else {\n                                this.resetCollabRequests();\n                                const response = await this.resetFromPeer(\n                                    fromPeerId,\n                                    this.lastCollaborationResetId\n                                );\n                                if (response === REQUEST_ERROR) {\n                                    return;\n                                }\n                            }\n                        } else {\n                            // Make both send their last step to each other to\n                            // ensure they are in sync.\n                            this.ptp.notifyAllPeers(\n                                \"oe_history_step\",\n                                this.dependencies.history.getHistorySteps().at(-1),\n                                { transport: \"rtc\" }\n                            );\n                            this.resetCollaborativeSelection(fromPeerId);\n                        }\n                        break;\n                    }\n                    case \"oe_history_step\":\n                        if (this.historySyncFinished) {\n                            this.dependencies.collaboration.onExternalHistorySteps([\n                                notificationPayload,\n                            ]);\n                        } else {\n                            this.historyStepsBuffer.push(notificationPayload);\n                        }\n                        break;\n                    case \"oe_history_set_selection\": {\n                        const peer = this.ptp.peersInfos[fromPeerId];\n                        if (!peer) {\n                            return;\n                        }\n                        const selection = notificationPayload;\n                        this.onExternalMultiselectionUpdate(selection);\n                        break;\n                    }\n                }\n            },\n        });\n    }\n    /**\n     * @param {string} peerId\n     */\n    getPeerMetadata(peerId) {\n        return this.ptp.peersInfos[peerId]?.metadata;\n    }\n    /**\n     * @param {CollaborationSelection} selection\n     */\n    onExternalMultiselectionUpdate(selection) {\n        this.dispatchTo(\"collaborative_selection_update_handlers\", selection);\n    }\n\n    async requestPeer(peerId, requestName, requestPayload, params) {\n        return this.ptp.requestPeer(peerId, requestName, requestPayload, params).catch((e) => {\n            if (e instanceof RequestError) {\n                return REQUEST_ERROR;\n            } else {\n                throw e;\n            }\n        });\n    }\n    getMetadata() {\n        const metadatas = {\n            startTime: this.startCollaborationTime,\n            peerName: user.name,\n        };\n        for (const cb of this.getResource(\"collaboration_peer_metadata_providers\")) {\n            Object.assign(metadatas, cb());\n        }\n        return metadatas;\n    }\n    /**\n     * Update the server document last step id and recover from a stale document\n     * if this peer does not have that step in its history.\n     */\n    onServerLastIdUpdate(last_step_id) {\n        this.serverLastStepId = last_step_id;\n        // Check if the current document is stale.\n        this.isDocumentStale = this.isLastDocumentStale();\n        if (this.isDocumentStale && this.ptpJoined) {\n            return this.recoverFromStaleDocument();\n        } else if (this.isDocumentStale && this.joiningPtp) {\n            // In case there is a stale document while a previous recovery is\n            // ongoing.\n            this.resetCollabRequests();\n            this.joinPeerToPeer();\n        }\n    }\n\n    joinPeerToPeer() {\n        this.editable.removeEventListener(\"focus\", this.joinPeerToPeer);\n        if (this.peerToPeerLoading) {\n            return this.peerToPeerLoading.then(async () => {\n                this.joiningPtp = true;\n                if (this.isDocumentStale) {\n                    const success = await this.resetFromServerAndResyncWithPeers();\n                    if (!success) {\n                        return;\n                    }\n                }\n                this.ptp.notifyAllPeers(\"ptp_join\");\n                this.joiningPtp = false;\n                this.ptpJoined = true;\n            });\n        }\n    }\n    isLastDocumentStale() {\n        if (!this.serverLastStepId) {\n            return false;\n        }\n        return !this.dependencies.collaboration.getBranchIds().includes(this.serverLastStepId);\n    }\n\n    /**\n     * Try to recover from a stale document.\n     *\n     * The strategy is:\n     *\n     * 1.  Try to get a converging document from the other peers.\n     *\n     * 1.1 By recovery from missing steps: it is the best possible case of\n     *     retrieval.\n     *\n     * 1.2 By recovery from snapshot: it reset the whole editor (destroying\n     *     changes and selection made by the user).\n     *\n     * 2. Reset from the server:\n     *    If the recovery from the other peers fails, reset from the server.\n     *\n     *    As we know we have a stale document, we need to reset it at least from\n     *    the server. We shouldn't wait too long for peers to respond because\n     *    the longer we wait for an unresponding peer, the longer a user can\n     *    edit a stale document.\n     *\n     *    The peers timeout is set to PTP_MAX_RECOVERY_TIME.\n     */\n    async recoverFromStaleDocument() {\n        return new Promise((resolve) => {\n            // 1. Try to recover a converging document from other peers.\n            const resetCollabCount = this.lastCollaborationResetId;\n\n            const allPeers = this.getPtpPeers().map((peer) => peer.id);\n\n            if (allPeers.length === 0) {\n                if (this.isDocumentStale) {\n                    this.showConflictDialog();\n                    resolve();\n                    return this.resetFromServerAndResyncWithPeers();\n                }\n            }\n\n            let hasRetrievalBudgetTimeout = false;\n            const snapshots = [];\n            let nbPendingResponses = allPeers.length;\n\n            const success = () => {\n                resolve();\n                clearTimeout(timeout);\n            };\n\n            for (const peerId of allPeers) {\n                this.requestPeer(\n                    peerId,\n                    \"recover_document\",\n                    {\n                        serverDocumentId: this.serverLastStepId,\n                        fromStepId: this.dependencies.collaboration.getBranchIds().at(-1),\n                    },\n                    { transport: \"rtc\" }\n                ).then((response) => {\n                    nbPendingResponses--;\n                    if (\n                        response === REQUEST_ERROR ||\n                        resetCollabCount !== this.lastCollaborationResetId ||\n                        hasRetrievalBudgetTimeout ||\n                        !response ||\n                        !this.isDocumentStale\n                    ) {\n                        if (nbPendingResponses <= 0) {\n                            processSnapshots();\n                        }\n                        return;\n                    }\n                    this.processMissingSteps(response.missingSteps);\n                    this.isDocumentStale = this.isLastDocumentStale();\n                    snapshots.push(response.snapshot);\n                    if (nbPendingResponses < 1) {\n                        processSnapshots();\n                    }\n                });\n            }\n\n            // Only process the snapshots after having received a response from all\n            // the peers or after PTP_MAX_RECOVERY_TIME in order to try to recover\n            // from missing steps.\n            const processSnapshots = async () => {\n                this.isDocumentStale = this.isLastDocumentStale();\n                if (!this.isDocumentStale) {\n                    return success();\n                }\n                if (snapshots[0]) {\n                    this.showConflictDialog();\n                }\n                for (const snapshot of snapshots) {\n                    this.applySnapshot(snapshot);\n                    this.isDocumentStale = this.isLastDocumentStale();\n                    // Prevent reseting from another snapshot if the document\n                    // converge.\n                    if (!this.isDocumentStale) {\n                        return success();\n                    }\n                }\n\n                // 2. If the document is still stale, try to recover from the server.\n                if (this.isDocumentStale) {\n                    this.showConflictDialog();\n                    await this.resetFromServerAndResyncWithPeers();\n                }\n\n                success();\n            };\n\n            // Wait PTP_MAX_RECOVERY_TIME to retrieve data from other peers to\n            // avoid reseting from the server if possible.\n            const timeout = setTimeout(() => {\n                if (resetCollabCount !== this.lastCollaborationResetId) {\n                    return;\n                }\n                hasRetrievalBudgetTimeout = true;\n                this.onRecoveryPeerTimeout(processSnapshots);\n            }, PTP_MAX_RECOVERY_TIME);\n        });\n    }\n\n    /**\n     * Get peer to peer peers.\n     */\n    getPtpPeers() {\n        const peers = Object.entries(this.ptp.peersInfos).map(([peerId, peerInfo]) => ({\n            id: peerId,\n            ...peerInfo,\n        }));\n        return peers.sort((a, b) => (isPeerFirst(a, b) ? -1 : 1));\n    }\n\n    getLastHistoryStepId(value) {\n        const matchId = value.match(/data-last-history-steps=\"[0-9,]*?([0-9]+)\"/);\n        return matchId && matchId[1];\n    }\n\n    resetCollabRequests() {\n        this.lastCollaborationResetId++;\n        // By aborting the current requests from ptp, we ensure that the ongoing\n        // `Wysiwyg.requestPeer` will return REQUEST_ERROR. Most requests that\n        // calls `Wysiwyg.requestPeer` might want to check if the response is\n        // REQUEST_ERROR.\n        this.ptp && this.ptp.abortCurrentRequests();\n    }\n    /**\n     * Reset the document from the server and resync with the peers.\n     */\n    async resetFromServerAndResyncWithPeers() {\n        let collaborationResetId = this.lastCollaborationResetId;\n        const record = await this.getCurrentRecord();\n        if (collaborationResetId !== this.lastCollaborationResetId) {\n            return;\n        }\n\n        const content =\n            record[this.config.collaboration.collaborationChannel.collaborationFieldName];\n        const lastHistoryId = content && this.getLastHistoryStepId(content);\n        // If a change was made in the document while retrieving it, the\n        // lastHistoryId will be different if the odoo bus did not have time to\n        // notify the user.\n        if (this.serverLastStepId !== lastHistoryId) {\n            // todo: instrument it to ensure it never happens\n            throw new Error(\n                \"Concurency detected while recovering from a stale document. The last history id of the server is different from the history id received by the html_field_write event.\"\n            );\n        }\n\n        this.isDocumentStale = false;\n        if (content) {\n            // content here is trusted\n            this.editable.innerHTML = content;\n        } else {\n            this.editable.replaceChildren(this.dependencies.baseContainer.createBaseContainer());\n        }\n        stripHistoryIds(this.editable);\n        this.dispatchTo(\"normalize_handlers\", this.editable);\n\n        this.dependencies.history.reset(content);\n\n        // After resetting from the server, try to resynchronise with a peer as\n        // if it was the first time connecting to a peer in order to retrieve a\n        // proper snapshot (e.g. This case could arise if we tried to recover\n        // from a peer but the timeout (PTP_MAX_RECOVERY_TIME) was reached\n        // before receiving a response).\n        this.historySyncAtLeastOnce = false;\n        this.resetCollabRequests();\n        collaborationResetId = this.lastCollaborationResetId;\n        this.startCollaborationTime = new Date().getTime();\n        await Promise.all(\n            this.getPtpPeers().map((peer) =>\n                // Reset from the fastest peer. The first peer to reset will set\n                // this.historySyncAtLeastOnce to true canceling the other peers\n                // resets.\n                this.resetFromPeer(peer.id, collaborationResetId)\n            )\n        );\n        return true;\n    }\n    onReset(content) {\n        // This ID correspond to the peer that initiated the document and set\n        // the initial oid for all nodes in the tree. It is not the same as\n        // document that had a step id at some point. If a step comes from a\n        // different history, we should not apply it.\n        this.historyShareId = Math.floor(Math.random() * Math.pow(2, 52)).toString();\n\n        const lastStepId = content && content.match(/data-last-history-steps=\"([\\d,]+)\"/)?.[1];\n        if (lastStepId) {\n            this.dependencies.collaboration.setInitialBranchStepId(lastStepId);\n        }\n    }\n\n    /**\n     * Process missing steps received from a peer.\n     *\n     * @private\n     * @param {Array<Object>|-1} missingSteps\n     * @return {Promise<boolean>} true if missing steps have been processed\n     */\n    async processMissingSteps(missingSteps) {\n        // If missing steps === -1, it means that either:\n        // - the step.peerId has a stale document\n        // - the step.peerId has a snapshot and does not includes the step in\n        //   its history\n        // - if another share history id\n        //   - because the step.peerId has reset from the server and\n        //     step.peerId is not synced with this peer\n        //   - because the step.peerId is in a network partition\n        if (missingSteps === -1 || !missingSteps.length) {\n            return false;\n        }\n        this.dependencies.collaboration.onExternalHistorySteps(missingSteps);\n        return true;\n    }\n    applySnapshot(snapshot) {\n        const { steps, historyIds, historyShareId } = snapshot;\n        // If there is no serverLastStepId, it means that we use a document\n        // that is not versionned yet.\n        const isStaleDocument =\n            this.serverLastStepId && !historyIds.includes(this.serverLastStepId);\n        if (isStaleDocument) {\n            return;\n        }\n        this.historyShareId = historyShareId;\n        this.historySyncAtLeastOnce = true;\n        this.dependencies.collaboration.resetFromSteps(steps, historyIds);\n\n        // todo: ensure that if the selection was not in the editable before the\n        // reset, it remains where it was after applying the snapshot.\n        return true;\n    }\n\n    /**\n     * Callback for when the timeout PTP_MAX_RECOVERY_TIME fires.\n     *\n     * Used to be hooked in tests.\n     *\n     * @param {Function} processSnapshots The snapshot processing function.\n     */\n    async onRecoveryPeerTimeout(processSnapshots) {\n        processSnapshots();\n    }\n    showConflictDialog() {\n        // todo: implement conflict dialog\n        // if (this.conflictDialogOpened) {\n        //     return;\n        // }\n        // const content = markup(this.odooEditor.editable.cloneNode(true).outerHTML);\n        // this.conflictDialogOpened = true;\n        // this.env.services.dialog.add(ConflictDialog, {\n        //     content,\n        //     close: () => (this.conflictDialogOpened = false),\n        // });\n    }\n\n    getHistorySnapshot() {\n        return Object.assign({}, this.dependencies.collaboration.getSnapshotSteps(), {\n            historyShareId: this.historyShareId,\n        });\n    }\n\n    async resetFromPeer(fromPeerId, resetCollabCount) {\n        this.historySyncFinished = false;\n        this.historyStepsBuffer = [];\n        const snapshot = await this.requestPeer(\n            fromPeerId,\n            \"get_history_from_snapshot\",\n            undefined,\n            { transport: \"rtc\" }\n        );\n        if (snapshot === REQUEST_ERROR) {\n            return REQUEST_ERROR;\n        }\n        if (resetCollabCount !== this.lastCollaborationResetId) {\n            return;\n        }\n        // Ensure that the history hasn't been synced by another peer before\n        // this `get_history_from_snapshot` finished.\n        if (this.historySyncAtLeastOnce) {\n            return;\n        }\n        const selection = this.dependencies.selection.getEditableSelection();\n        let anchorNodeIndexPath = this._getNodeIndexPath(selection.anchorNode);\n        let anchorOffset = selection.anchorOffset;\n        if (selection.anchorNode === this.editable) {\n            anchorNodeIndexPath = this._getNodeIndexPath(this.editable.firstChild);\n            anchorOffset = 0;\n        }\n        const applied = this.applySnapshot(snapshot);\n        if (!applied) {\n            return;\n        }\n        const anchorNode = this._getNodeFromIndexPath(anchorNodeIndexPath);\n        if (\n            this.dependencies.selection.isSelectionInEditable({ anchorNode, focusNode: anchorNode })\n        ) {\n            this.dependencies.selection.setSelection({\n                anchorNode,\n                anchorOffset,\n            });\n        }\n        this.historySyncFinished = true;\n        // In case there are steps received in the meantime, process them.\n        if (this.historyStepsBuffer.length) {\n            this.dependencies.collaboration.onExternalHistorySteps(this.historyStepsBuffer);\n            this.historyStepsBuffer = [];\n        }\n        this.editable.dispatchEvent(new CustomEvent(\"onHistoryResetFromPeer\"));\n        this.resetCollaborativeSelection(fromPeerId);\n    }\n\n    async resetCollaborativeSelection(fromPeerId) {\n        const remoteSelection = await this.requestPeer(\n            fromPeerId,\n            \"get_collaborative_selection\",\n            undefined,\n            { transport: \"rtc\" }\n        );\n        if (remoteSelection === REQUEST_ERROR) {\n            return;\n        }\n        if (remoteSelection) {\n            this.onExternalMultiselectionUpdate(remoteSelection);\n        }\n    }\n    async onHistoryMissingParentStep({ step, fromStepId }) {\n        if (!this.ptp) {\n            return;\n        }\n        const missingSteps = await this.requestPeer(\n            step.peerId,\n            \"get_missing_steps\",\n            {\n                fromStepId: fromStepId,\n                toStepId: step.id,\n            },\n            { transport: \"rtc\" }\n        );\n        if (missingSteps === REQUEST_ERROR) {\n            return;\n        }\n        this.processMissingSteps(\n            Array.isArray(missingSteps) ? missingSteps.concat(step) : missingSteps\n        );\n    }\n    async getCurrentRecord() {\n        const [record] = await this.config.collaboration.ormService.read(\n            this.config.collaboration.collaborationChannel.collaborationModelName,\n            [this.config.collaboration.collaborationChannel.collaborationResId],\n            [this.config.collaboration.collaborationChannel.collaborationFieldName]\n        );\n        return record;\n    }\n    attachHistoryIds(editable) {\n        const historyIds = this.dependencies.collaboration.getBranchIds().join(\",\");\n        const firstChild = editable.children[0];\n        if (firstChild) {\n            firstChild.setAttribute(\"data-last-history-steps\", historyIds);\n        }\n    }\n\n    /**\n     * Generates the path to a node as an array of indices, relative to a given ancestor.\n     *\n     * @param {Node} node - The node to trace the path for.\n     * @returns {number[]} The path as an array of child indices.\n     */\n    _getNodeIndexPath(node) {\n        return [node, ...ancestors(node, this.editable)].map((ancestor) =>\n            childNodeIndex(ancestor)\n        );\n    }\n    /**\n     * Finds a node in the DOM based on a path of child indices.\n     *\n     * @param {number[]} indexPath - The path as an array of child indices.\n     * @returns {Node|undefined} The node at the specified path, or null if not found.\n     */\n    _getNodeFromIndexPath(indexPath) {\n        return indexPath.reduceRight(\n            (node, index) => node?.childNodes?.[index],\n            this.editable.parentElement\n        );\n    }\n}\n\n/**\n * Check wether peerA is before peerB.\n */\nfunction isPeerFirst(peerA, peerB) {\n    if (peerA.startTime === peerB.startTime) {\n        return peerA.id.localeCompare(peerB.id) === -1;\n    }\n    if (peerA.startTime === undefined || peerB.startTime === undefined) {\n        return Boolean(peerA.startTime);\n    } else {\n        return peerA.startTime < peerB.startTime;\n    }\n}\n\nexport function stripHistoryIds(element) {\n    element\n        .querySelectorAll(\"[data-last-history-steps]\")\n        .forEach((el) => el.removeAttribute(\"data-last-history-steps\"));\n}\n", "import { Plugin } from \"@html_editor/plugin\";\n\n// 60 seconds\nexport const HISTORY_SNAPSHOT_INTERVAL = 1000 * 60;\n// 10 seconds\nconst HISTORY_SNAPSHOT_BUFFER_TIME = 1000 * 10;\n\n/**\n * @typedef { Object } CollaborationPluginConfig\n * @property { string } peerId\n *\n * @typedef { import(\"../../core/history_plugin\").HistoryStep } HistoryStep\n */\n\n/**\n * @typedef { Object } CollaborationShared\n * @property { CollaborationPlugin['getBranchIds'] } getBranchIds\n * @property { CollaborationPlugin['getSnapshotSteps'] } getSnapshotSteps\n * @property { CollaborationPlugin['historyGetMissingSteps'] } historyGetMissingSteps\n * @property { CollaborationPlugin['onExternalHistorySteps'] } onExternalHistorySteps\n * @property { CollaborationPlugin['resetFromSteps'] } resetFromSteps\n * @property { CollaborationPlugin['setInitialBranchStepId'] } setInitialBranchStepId\n */\n\n/**\n * @typedef {(() => void)[]} external_history_step_handlers\n */\n\nexport class CollaborationPlugin extends Plugin {\n    static id = \"collaboration\";\n    static dependencies = [\"history\", \"selection\", \"sanitize\"];\n    /** @type {import(\"plugins\").EditorResources} */\n    resources = {\n        /** Handlers */\n        history_cleaned_handlers: this.onHistoryClean.bind(this),\n        history_reset_handlers: this.onHistoryReset.bind(this),\n        step_added_handlers: ({ step }) => this.onStepAdded(step),\n\n        /** Overrides */\n        set_attribute_overrides: this.setAttribute.bind(this),\n\n        history_step_processors: this.processHistoryStep.bind(this),\n        unreversible_step_predicates: this.isUnreversibleStep.bind(this),\n    };\n    static shared = [\n        \"getBranchIds\",\n        \"getSnapshotSteps\",\n        \"historyGetMissingSteps\",\n        \"onExternalHistorySteps\",\n        \"resetFromSteps\",\n        \"setInitialBranchStepId\",\n    ];\n\n    /** @type { CollaborationPluginConfig['peerId'] } */\n    peerId = null;\n\n    setup() {\n        this.peerId = this.config.collaboration.peerId;\n        if (!this.peerId) {\n            throw new Error(\"The collaboration plugin requires a peerId\");\n        }\n        this._snapshotInterval = setInterval(() => {\n            this.makeSnapshot();\n        }, HISTORY_SNAPSHOT_INTERVAL);\n    }\n\n    destroy() {\n        super.destroy();\n        clearInterval(this._snapshotInterval);\n        this._snapshotInterval = false;\n    }\n\n    onHistoryClean() {\n        this.branchStepIds = [];\n    }\n    onHistoryReset() {\n        const firstStep = this.dependencies.history.getHistorySteps()[0];\n        this.snapshots = [{ step: firstStep }];\n    }\n    /**\n     * @param {HistoryStep} step\n     */\n    isUnreversibleStep(step) {\n        return step.peerId !== this.peerId;\n    }\n    /**\n     * @param {Node} node\n     * @param {string} attributeName\n     * @param {string} attributeValue\n     */\n    setAttribute(node, attributeName, attributeValue) {\n        if (attributeValue) {\n            this.safeSetAttribute(node, attributeName, attributeValue);\n            return true;\n        }\n    }\n\n    /**\n     * Get all the history ids for the current history branch.\n     */\n    getBranchIds() {\n        const steps = this.dependencies.history.getHistorySteps();\n        return (this.initialBranchStepId || \"\")\n            .split(\",\")\n            .concat(this.branchStepIds)\n            .concat(steps.map((s) => s.id));\n    }\n    /**\n     * Safely set an attribute on a node.\n     * @param {HTMLElement} node\n     * @param {string} attributeName\n     * @param {string} attributeValue\n     */\n    safeSetAttribute(node, attributeName, attributeValue) {\n        const clone = this.document.createElement(node.tagName);\n        clone.setAttribute(attributeName, attributeValue);\n        this.dependencies.sanitize.sanitize(clone);\n        if (clone.hasAttribute(attributeName)) {\n            node.setAttribute(attributeName, clone.getAttribute(attributeName));\n        } else {\n            node.removeAttribute(attributeName);\n        }\n    }\n\n    /**\n     * Apply external steps coming from the collaboration.\n     *\n     * @param {Object} newSteps External steps to be applied\n     */\n    onExternalHistorySteps(newSteps) {\n        let stepIndex = 0;\n        const selectionData = this.dependencies.selection.getSelectionData();\n\n        const steps = this.dependencies.history.getHistorySteps();\n        for (const newStep of newSteps) {\n            // todo: add a test that no 2 history_missing_parent_step_handlers\n            // are called in same stack.\n            const insertIndex = this.getInsertStepIndex(steps, newStep);\n            if (typeof insertIndex === \"undefined\") {\n                continue;\n            }\n            this.dependencies.history.addExternalStep(newStep, insertIndex);\n            stepIndex++;\n        }\n        if (selectionData.documentSelectionIsInEditable) {\n            this.dependencies.selection.rectifySelection(selectionData.editableSelection);\n        }\n\n        this.dispatchTo(\"external_history_step_handlers\");\n\n        // todo: ensure that if the selection was not in the editable before the\n        // reset, it remains where it was after applying the snapshot.\n\n        if (stepIndex) {\n            this.config.onChange?.();\n        }\n    }\n\n    /**\n     * @param {HistoryStep[]} steps\n     * @param {HistoryStep} newStep\n     */\n    getInsertStepIndex(steps, newStep) {\n        let index = steps.length - 1;\n        while (index >= 0 && steps[index].id !== newStep.previousStepId) {\n            // Skip steps that are already in the list.\n            if (steps[index].id === newStep.id) {\n                return;\n            }\n            index--;\n        }\n\n        // When the previousStepId is not present in the steps it\n        // could be either:\n        // - the previousStepId is before a snapshot of the same history\n        // - the previousStepId has not been received because peers were\n        //   disconnected at that time\n        // - the previousStepId is in another history (in case two totally\n        //   differents `steps` (but it should not arise)).\n        if (index < 0) {\n            const historySteps = steps;\n            let index = historySteps.length - 1;\n            // Get the last known step that we are sure the missing step\n            // peer has. It could either be a step that has the same\n            // peerId or the first step.\n            while (index !== 0) {\n                if (historySteps[index].peerId === newStep.peerId) {\n                    break;\n                }\n                index--;\n            }\n            const fromStepId = historySteps[index].id;\n            this.dispatchTo(\"history_missing_parent_step_handlers\", {\n                step: newStep,\n                fromStepId: fromStepId,\n            });\n            return;\n        }\n\n        let concurentSteps = [];\n        index++;\n        while (index < steps.length) {\n            if (steps[index].previousStepId === newStep.previousStepId) {\n                if (steps[index].id.localeCompare(newStep.id) === 1) {\n                    break;\n                } else {\n                    concurentSteps = [steps[index].id];\n                }\n            } else {\n                if (concurentSteps.includes(steps[index].previousStepId)) {\n                    concurentSteps.push(steps[index].id);\n                } else {\n                    break;\n                }\n            }\n            index++;\n        }\n\n        return index;\n    }\n\n    /**\n     * @param {Object} params\n     * @param {string} params.fromStepId\n     * @param {string} [params.toStepId]\n     */\n    historyGetMissingSteps({ fromStepId, toStepId }) {\n        const steps = this.dependencies.history.getHistorySteps();\n        const fromIndex = steps.findIndex((x) => x.id === fromStepId);\n        const toIndex = toStepId ? steps.findIndex((x) => x.id === toStepId) : steps.length;\n        if (fromIndex === -1 || toIndex === -1) {\n            return -1;\n        }\n        return steps.slice(fromIndex + 1, toIndex);\n    }\n\n    getSnapshotSteps() {\n        const historySteps = this.dependencies.history.getHistorySteps();\n        // If the current snapshot has no time, it means that there is the no\n        // other snapshot that have been made (either it is the one created upon\n        // initialization or reseted by history's resetFromSteps).\n        if (!this.snapshots[0].time) {\n            return { steps: historySteps, historyIds: this.getBranchIds() };\n        }\n        const snapshotSteps = [];\n        let snapshot;\n        if (this.snapshots[0].time + HISTORY_SNAPSHOT_BUFFER_TIME < Date.now()) {\n            snapshot = this.snapshots[0];\n        } else {\n            // this.snapshots[1] has being created at least 1 minute ago\n            // (HISTORY_SNAPSHOT_INTERVAL) or it is the first step.\n            snapshot = this.snapshots[1];\n        }\n        let index = historySteps.length - 1;\n        while (historySteps[index].id !== snapshot.step.id) {\n            snapshotSteps.push(historySteps[index]);\n            index--;\n        }\n        snapshotSteps.push(snapshot.step);\n        snapshotSteps.reverse();\n\n        return { steps: snapshotSteps, historyIds: this.getBranchIds() };\n    }\n    setInitialBranchStepId(stepId) {\n        this.initialBranchStepId = stepId;\n    }\n    resetFromSteps(steps, branchStepIds) {\n        this.dependencies.selection.resetSelection();\n        this.dependencies.history.resetFromSteps(steps);\n        this.snapshots = [{ step: steps[0] }];\n        this.branchStepIds = branchStepIds;\n\n        // @todo @phoenix: test that the hint are proprely handeled\n        // this._handleCommandHint();\n        // @todo @phoenix: make the multiselection\n        // this.multiselectionRefresh();\n        // @todo @phoenix: check it is still relevant\n        // this.dispatchEvent(new Event(\"resetFromSteps\"));\n    }\n\n    makeSnapshot() {\n        const historyLength = this.dependencies.history.getHistorySteps().length;\n        if (!this.lastSnapshotLength || this.lastSnapshotLength < historyLength) {\n            this.lastSnapshotLength = historyLength;\n            const step = this.dependencies.history.makeSnapshotStep();\n            const snapshot = {\n                time: Date.now(),\n                step: step,\n            };\n            this.snapshots = [snapshot, this.snapshots[0]];\n        }\n    }\n\n    /**\n     * @param {HistoryStep} step\n     */\n    onStepAdded(step) {\n        step.peerId = this.peerId;\n        this.dispatchTo(\"collaboration_step_added_handlers\", step);\n    }\n    /**\n     * @param {HistoryStep} step\n     */\n    processHistoryStep(step) {\n        step.peerId = this.peerId;\n        return step;\n    }\n}\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { closestBlock, isBlock } from \"@html_editor/utils/blocks\";\nimport { closestElement } from \"@html_editor/utils/dom_traversal\";\nimport { browser } from \"@web/core/browser/browser\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { user } from \"@web/core/user\";\n\n/**\n * @typedef {Object} SelectionInfo\n * @property {import(\"@html_editor/core/history_plugin\").SerializedSelection} selection\n * @property {string} color\n * @property {string} peerId\n * @property {string} peerName\n * @property {string} avatarPositionKey\n * @property {HTMLElement} avatarElement\n * @property {HTMLElement} avatarTargetElement\n */\n\nexport const AVATAR_SIZE = 25;\n\nexport class CollaborationSelectionAvatarPlugin extends Plugin {\n    static id = \"collaborationSelectionAvatar\";\n    static dependencies = [\"history\", \"position\", \"localOverlay\", \"collaborationOdoo\"];\n    /** @type {import(\"plugins\").EditorResources} */\n    resources = {\n        /** Handlers */\n        collaboration_notification_handlers: this.handleCollaborationNotification.bind(this),\n        external_history_step_handlers: this.refreshSelection.bind(this),\n        layout_geometry_change_handlers: this.refreshSelection.bind(this),\n        set_movable_element_handlers: this.disableAvatarForElement.bind(this),\n        unset_movable_element_handlers: this.enableAvatars.bind(this),\n        collaborative_selection_update_handlers: this.updateSelection.bind(this),\n\n        collaboration_peer_metadata_providers: () => ({ avatarUrl: this.avatarUrl }),\n    };\n\n    /** @type {Map<string, SelectionInfo>} */\n    selectionInfos = new Map();\n\n    setup() {\n        this.avatarOverlay = this.dependencies.localOverlay.makeLocalOverlay(\"oe-avatars-overlay\");\n        this.avatarsCountersOverlay = this.dependencies.localOverlay.makeLocalOverlay(\n            \"oe-avatars-counters-overlay\"\n        );\n        this.avatarUrl = `${\n            browser.location.origin\n        }/web/image?model=res.users&field=avatar_128&id=${encodeURIComponent(user.userId)}`;\n    }\n    handleCollaborationNotification({ notificationName, notificationPayload }) {\n        switch (notificationName) {\n            case \"ptp_remove\":\n                this.selectionInfos.delete(notificationPayload);\n                this.refreshSelection();\n        }\n    }\n\n    /**\n     * @param {import(\"./collaboration_odoo_plugin\").CollaborationSelection} selection\n     */\n    updateSelection(selection) {\n        /** @type {SelectionInfo} */\n        const savedSelection = this.selectionInfos.get(selection.peerId) || {};\n        const newSelection = Object.assign(savedSelection, selection);\n        this.selectionInfos.set(selection.peerId, newSelection);\n        this.drawPeerAvatar(newSelection);\n        this.updateAvatarCounters();\n    }\n    /**\n     * @param {SelectionInfo} selectionInfo\n     */\n    drawPeerAvatar(selectionInfo) {\n        const { selection, peerId } = selectionInfo;\n        const peerMetadata = this.dependencies.collaborationOdoo.getPeerMetadata(peerId);\n        if (!peerMetadata) {\n            return;\n        }\n        const { avatarUrl, peerName = _t(\"Anonymous\") } = peerMetadata;\n        const anchorNode = this.dependencies.history.getNodeById(selection.anchorNodeId);\n        const focusNode = this.dependencies.history.getNodeById(selection.focusNodeId);\n        if (!anchorNode || !focusNode || !anchorNode.isConnected || !focusNode.isConnected) {\n            return;\n        }\n        const anchorBlock =\n            closestElement(anchorNode, (el) => isBlock(el) && el.parentElement === this.editable) ||\n            closestBlock(anchorNode);\n        if (!anchorBlock) {\n            return;\n        }\n\n        const containerRect = this.avatarOverlay.getBoundingClientRect();\n\n        // Draw user avatar.\n        let avatarElement = selectionInfo.avatarElement;\n        if (!avatarElement) {\n            avatarElement = this.document.createElement(\"div\");\n            avatarElement.className = \"oe-collaboration-caret-avatar\";\n            avatarElement.style.display = \"none\";\n            const image = this.document.createElement(\"img\");\n            avatarElement.append(image);\n            image.onload = () => avatarElement.style.removeProperty(\"display\");\n            image.setAttribute(\"src\", avatarUrl);\n            image.classList.add(\"object-fit-cover\");\n        }\n        // Avoid re-appending the element in the dom.\n        if (!avatarElement.parentElement) {\n            this.avatarOverlay.append(avatarElement);\n        }\n        // Make sure data is up to date.\n        selectionInfo.avatarElement = avatarElement;\n        selectionInfo.peerName = peerName;\n        selectionInfo.avatarTargetElement = anchorBlock;\n        this.selectionInfos.set(peerId, selectionInfo);\n\n        const anchorBlockRect = anchorBlock.getBoundingClientRect();\n        const top = anchorBlockRect.y - containerRect.y;\n        avatarElement.style.top = top + \"px\";\n        const closestList = closestElement(anchorNode, \"ul, ol\"); // Prevent overlap bullets.\n        const anchorX = closestList ? closestList.getBoundingClientRect().x : anchorBlockRect.x;\n        const left = anchorX - containerRect.x - AVATAR_SIZE;\n        avatarElement.style.left = left + \"px\";\n        selectionInfo.avatarPositionKey = `${left}|${top}`;\n    }\n    updateAvatarCounters() {\n        const avatarsOverlaps = {};\n        for (const info of this.selectionInfos.values()) {\n            const key = info.avatarPositionKey;\n            avatarsOverlaps[key] = avatarsOverlaps[key] || new Set();\n            avatarsOverlaps[key].add(info);\n        }\n\n        // Render avatars overlap.\n        this.avatarsCountersOverlay.replaceChildren();\n        for (const [overlapKey, infos] of Object.entries(avatarsOverlaps)) {\n            const size = infos.size;\n            if (size > 1) {\n                const [left, top] = overlapKey.split(\"|\").map((n) => parseInt(n, 10));\n                const div = document.createElement(\"div\");\n                div.className = \"oe-overlapping-counter\";\n                div.style.left = left + 10 + \"px\";\n                div.style.top = top + 10 + \"px\";\n                div.innerText = size;\n                this.avatarsCountersOverlay.append(div);\n            }\n        }\n    }\n    refreshSelection() {\n        if (!this.selectionInfos.size) {\n            this.avatarOverlay.replaceChildren();\n        }\n        this.avatarsCountersOverlay.replaceChildren();\n        for (const selection of this.selectionInfos.values()) {\n            this.drawPeerAvatar(selection);\n        }\n        this.updateAvatarCounters();\n    }\n\n    disableAvatarForElement(element) {\n        this.enableAvatars();\n        for (const info of this.selectionInfos.values()) {\n            if (info.avatarTargetElement === element) {\n                if (!info.avatarElement.classList.contains(\"invisible\")) {\n                    info.avatarElement.classList.add(\"invisible\");\n                }\n            }\n        }\n    }\n    enableAvatars() {\n        for (const element of this.avatarOverlay.querySelectorAll(\n            \".oe-collaboration-caret-avatar.invisible\"\n        )) {\n            element.classList.remove(\"invisible\");\n        }\n    }\n}\n", "import { Plugin } from \"@html_editor/plugin\";\nimport {\n    getDeepestPosition,\n    isProtected,\n    isProtecting,\n    isUnprotecting,\n} from \"@html_editor/utils/dom_info\";\nimport { childNodes } from \"@html_editor/utils/dom_traversal\";\nimport { DIRECTIONS } from \"@html_editor/utils/position\";\nimport { getCursorDirection } from \"@html_editor/utils/selection\";\nimport { _t } from \"@web/core/l10n/translation\";\n\nexport class CollaborationSelectionPlugin extends Plugin {\n    static id = \"collaborationSelection\";\n    static dependencies = [\"history\", \"collaborationOdoo\", \"position\", \"localOverlay\"];\n    /** @type {import(\"plugins\").EditorResources} */\n    resources = {\n        /** Handlers */\n        collaboration_notification_handlers: this.handleCollaborationNotification.bind(this),\n        layout_geometry_change_handlers: this.refreshSelection.bind(this),\n        collaborative_selection_update_handlers: this.updateSelection.bind(this),\n\n        collaboration_peer_metadata_providers: () => ({ selectionColor: this.selectionColor }),\n    };\n    selectionInfos = new Map();\n\n    setup() {\n        this.selectionOverlay =\n            this.dependencies.localOverlay.makeLocalOverlay(\"oe-selections-container\");\n        this.selectionColor = `hsl(${(Math.random() * 360).toFixed(0)}, 75%, 50%)`;\n    }\n    handleCollaborationNotification({ notificationName, notificationPayload }) {\n        switch (notificationName) {\n            case \"ptp_remove\":\n                this.multiselectionRemove(notificationPayload);\n                this.selectionInfos.delete(notificationPayload);\n                break;\n        }\n    }\n    /**\n     * @param {import(\"./collaboration_odoo_plugin\").CollaborationSelection} selection\n     */\n    updateSelection(selection) {\n        this.selectionInfos.set(selection.peerId, selection);\n        this.drawPeerSelection(selection);\n    }\n    /**\n     * @param {import(\"./collaboration_odoo_plugin\").CollaborationSelection} selection\n     */\n    drawPeerSelection({ selection, peerId }) {\n        const peerMetadata = this.dependencies.collaborationOdoo.getPeerMetadata(peerId);\n        if (!peerMetadata) {\n            return;\n        }\n        const { selectionColor, peerName = _t(\"Anonymous\") } = peerMetadata;\n        this.multiselectionRemove(peerId);\n        let clientRects;\n\n        let anchorNode = this.dependencies.history.getNodeById(selection.anchorNodeId);\n        let focusNode = this.dependencies.history.getNodeById(selection.focusNodeId);\n        let anchorOffset = selection.anchorOffset;\n        let focusOffset = selection.focusOffset;\n        if (!anchorNode || !focusNode) {\n            anchorNode = this.editable.children[0];\n            focusNode = this.editable.children[0];\n            anchorOffset = 0;\n            focusOffset = 0;\n        }\n        const anchorTarget = childNodes(anchorNode).at(anchorOffset);\n        const focusTarget = childNodes(focusNode).at(focusOffset);\n        const protectionCheck = (node) =>\n            isProtecting(node) || (isProtected(node) && !isUnprotecting(node));\n        if (protectionCheck(anchorTarget) || protectionCheck(focusTarget)) {\n            // TODO @phoenix, TODO ABD: better handle collaborative selection\n            // on protected elements.\n            return;\n        }\n        if (anchorNode.isConnected && focusNode.isConnected) {\n            [anchorNode, anchorOffset] = getDeepestPosition(anchorNode, anchorOffset);\n            [focusNode, focusOffset] = getDeepestPosition(focusNode, focusOffset);\n        } else {\n            // todo: We should not be able to get here, this fixes multiples\n            // issues where we temporarily try to draw a an impossible\n            // selection. We should investigate the root cause of this issue.\n            anchorNode = this.editable.children[0];\n            focusNode = this.editable.children[0];\n            anchorOffset = 0;\n            focusOffset = 0;\n        }\n\n        const direction = getCursorDirection(anchorNode, anchorOffset, focusNode, focusOffset);\n        const range = new Range();\n        try {\n            if (direction === DIRECTIONS.RIGHT) {\n                range.setStart(anchorNode, anchorOffset);\n                range.setEnd(focusNode, focusOffset);\n            } else {\n                range.setStart(focusNode, focusOffset);\n                range.setEnd(anchorNode, anchorOffset);\n            }\n\n            clientRects = Array.from(range.getClientRects());\n        } catch {\n            // Changes in the dom might prevent the range to be instantiated\n            // (because of a removed node for example), in which case we ignore\n            // the range.\n            clientRects = [];\n        }\n        if (!clientRects.length) {\n            return;\n        }\n\n        // Draw rects (in case the selection is not collapsed).\n        const containerRect = this.selectionOverlay.getBoundingClientRect();\n        const indicators = clientRects.map(({ x, y, width, height }) => {\n            const rectElement = this.document.createElement(\"div\");\n            rectElement.style = `\n                position: absolute;\n                top: ${y - containerRect.y}px;\n                left: ${x - containerRect.x}px;\n                width: ${width}px;\n                height: ${height}px;\n                background-color: ${selectionColor};\n                opacity: 0.25;\n                pointer-events: none;\n            `;\n            rectElement.setAttribute(\"data-selection-peer-id\", peerId);\n            return rectElement;\n        });\n\n        // Draw carret.\n        const caretElement = this.document.createElement(\"div\");\n        caretElement.style = `border-left: 2px solid ${selectionColor}; position: absolute;`;\n        caretElement.setAttribute(\"data-selection-peer-id\", peerId);\n        caretElement.className = \"oe-collaboration-caret\";\n\n        // Draw carret top square.\n        const caretTopSquare = this.document.createElement(\"div\");\n        caretTopSquare.className = \"oe-collaboration-caret-top-square\";\n        caretTopSquare.style[\"background-color\"] = selectionColor;\n        caretTopSquare.setAttribute(\"data-peer-name\", peerName);\n        caretElement.append(caretTopSquare);\n\n        if (direction === DIRECTIONS.LEFT) {\n            const rect = clientRects[0];\n            caretElement.style.height = `${rect.height * 1.2}px`;\n            caretElement.style.top = `${rect.y - containerRect.y}px`;\n            caretElement.style.left = `${rect.x - containerRect.x}px`;\n        } else {\n            const rect = clientRects.at(-1);\n            caretElement.style.height = `${rect.height * 1.2}px`;\n            caretElement.style.top = `${rect.y - containerRect.y}px`;\n            caretElement.style.left = `${rect.right - containerRect.x}px`;\n        }\n        this.selectionOverlay.append(caretElement, ...indicators);\n    }\n\n    multiselectionRemove(peerId) {\n        const elements = this.selectionOverlay.querySelectorAll(\n            `[data-selection-peer-id=\"${peerId}\"]`\n        );\n        for (const element of elements) {\n            element.remove();\n        }\n    }\n    refreshSelection() {\n        this.selectionOverlay.replaceChildren();\n        for (const selection of this.selectionInfos.values()) {\n            this.drawPeerSelection(selection);\n        }\n    }\n}\n", "import {\n    applyObjectPropertyDifference,\n    getEmbeddedProps,\n    StateChangeManager,\n} from \"@html_editor/others/embedded_component_utils\";\nimport { Component, useState, useRef, onMounted, onWillDestroy } from \"@odoo/owl\";\n\nexport class EmbeddedCaptionComponent extends Component {\n    static template = \"html_editor.EmbeddedCaption\";\n\n    static props = {\n        image: { type: Element },\n        onUpdateCaption: { type: Function },\n        onEditorHistoryApply: { type: Function },\n        focusInput: { type: Boolean },\n        host: { type: Object },\n    };\n\n    setup() {\n        super.setup();\n        this.state = useState({\n            caption: \"\",\n            host: this.props.host,\n        });\n        this.captionInput = useRef(\"captionInput\");\n        if (this.props.focusInput) {\n            onMounted(() => {\n                this.captionInput.el.focus();\n            });\n        }\n        // Ensure the state, the attribute and the placeholder are in sync.\n        this.updateCaption();\n        const observer = new MutationObserver((mutations) => {\n            for (const mutation of mutations) {\n                if (mutation.type === \"attributes\" && mutation.attributeName === \"data-caption\") {\n                    this.updateCaption();\n                }\n            }\n        });\n        observer.observe(this.props.image, { attributes: true });\n        onWillDestroy(() => {\n            observer.disconnect();\n        });\n    }\n\n    updateCaption(caption = this.props.image.getAttribute(\"data-caption\")) {\n        if (caption !== this.state.caption) {\n            this.state.caption = caption;\n            this.props.onUpdateCaption(caption);\n        }\n    }\n\n    onInputBlur() {\n        // This is triggered before the selection changes. Wait before updating\n        // so when the history step triggers a normalization, it restores that\n        // new selection and not the old one.\n        setTimeout(() => {\n            if (this.captionInput.el) {\n                this.updateCaption(this.captionInput.el.value || \"\");\n            }\n        });\n    }\n\n    onInputKeyup(ev) {\n        if (ev.key === \"z\" && ev.ctrlKey && !this._appliedNativeHistory) {\n            this.props.onEditorHistoryApply(ev.shiftKey);\n        }\n        this._appliedNativeHistory = false;\n    }\n\n    onInputBeforeInput(ev) {\n        this._appliedNativeHistory = false;\n        if (ev.inputType === \"historyUndo\" || ev.inputType === \"historyRedo\") {\n            // Input elements handle their own history, but this event is not\n            // triggered if no changes were made to the input. So we handle the\n            // editor history on keyup in those cases, but let the browser do\n            // its thing otherwise.\n            this._appliedNativeHistory = true;\n        }\n    }\n}\n\nexport const captionEmbedding = {\n    name: \"caption\",\n    Component: EmbeddedCaptionComponent,\n    getProps: (host) => ({ host, ...getEmbeddedProps(host) }),\n    getStateChangeManager: (config) =>\n        new StateChangeManager(\n            Object.assign(config, {\n                propertyUpdater: {\n                    caption: (state, previous, next) => {\n                        applyObjectPropertyDifference(\n                            state,\n                            \"caption\",\n                            previous.caption,\n                            next.caption\n                        );\n                    },\n                },\n            })\n        ),\n};\n", "import {\n    applyObjectPropertyDifference,\n    getEmbeddedProps,\n    StateChangeManager,\n    useEmbeddedState,\n} from \"@html_editor/others/embedded_component_utils\";\nimport { useEffect, useRef, useState } from \"@odoo/owl\";\nimport { ReadonlyEmbeddedFileComponent } from \"@html_editor/others/embedded_components/core/file/readonly_file\";\n\nexport class EmbeddedFileComponent extends ReadonlyEmbeddedFileComponent {\n    static template = \"html_editor.EmbeddedFile\";\n\n    setup() {\n        super.setup();\n        // override the state by an embedded state.\n        this.state = useEmbeddedState(this.props.host);\n        this.fileModel.state = this.state;\n        this.localState = useState({\n            editFileName: false,\n        });\n        this.nameInput = useRef(\"nameInput\");\n        useEffect(\n            () => {\n                if (this.localState.editFileName) {\n                    this.nameInput.el.focus();\n                    this.nameInput.el.select();\n                }\n            },\n            () => [this.localState.editFileName]\n        );\n    }\n\n    onBlurNameInput(ev) {\n        this.localState.editFileName = false;\n        this.renameFile();\n    }\n\n    onFocusFileName(ev) {\n        this.localState.editFileName = true;\n    }\n\n    onKeydownNameInput(ev) {\n        if (ev.key !== \"Enter\") {\n            return;\n        } else {\n            ev.preventDefault();\n        }\n        if (this.renameFile()) {\n            this.localState.editFileName = false;\n            this.env.editorShared?.setSelectionAfter(this.props.host);\n        }\n    }\n\n    renameFile() {\n        let newName = this.nameInput.el.value;\n        if (!newName.length) {\n            return false;\n        }\n        if (newName === this.fileModel.filename) {\n            return true;\n        }\n        // filename is the name of the file as written in the editor by the\n        // user. It does not necessarily have the file extension.\n        this.fileModel.filename = newName;\n        if (this.fileModel.extension) {\n            const pattern = new RegExp(`\\\\.${this.fileModel.extension}$`, \"i\");\n            if (!newName.match(pattern)) {\n                newName += `.${this.fileModel.extension}`;\n            }\n        }\n        // name is the full name of the file (always with extension)\n        // and is used as the url queryParam when downloading it.\n        this.fileModel.name = newName;\n        return true;\n    }\n}\n\nexport const fileEmbedding = {\n    name: \"file\",\n    Component: EmbeddedFileComponent,\n    getProps: (host) => ({ host, ...getEmbeddedProps(host) }),\n    getStateChangeManager: (config) =>\n        new StateChangeManager(\n            Object.assign(config, {\n                propertyUpdater: {\n                    fileData: (state, previous, next) => {\n                        applyObjectPropertyDifference(\n                            state,\n                            \"fileData\",\n                            previous.fileData,\n                            next.fileData\n                        );\n                    },\n                },\n            })\n        ),\n};\n", "import { Component } from \"@odoo/owl\";\nimport { CopyButton } from \"@web/core/copy_button/copy_button\";\nimport { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\n\nexport const LANGUAGES = {\n    plaintext: \"Plain Text\",\n    markdown: \"Markdown\",\n    javascript: \"Javascript\",\n    typescript: \"Typescript\",\n    jsdoc: \"JSDoc\",\n    java: \"Java\",\n    python: \"Python\",\n    html: \"HTML\",\n    xml: \"XML\",\n    svg: \"SVG\",\n    json: \"JSON\",\n    css: \"CSS\",\n    sass: \"SASS\",\n    scss: \"SCSS\",\n    sql: \"SQL\",\n    diff: \"Diff\",\n};\n\nexport class CodeToolbar extends Component {\n    static template = \"html_editor.CodeToolbar\";\n    static props = {\n        target: { validate: (el) => el.nodeType === Node.ELEMENT_NODE },\n        getContent: { type: Function },\n        onLanguageChange: { type: Function },\n        currentLanguage: { type: String },\n    };\n    static components = { Dropdown, DropdownItem, CopyButton };\n\n    setup() {\n        super.setup();\n        this.languages = LANGUAGES;\n    }\n}\n", "import {\n    getEmbeddedProps,\n    StateChangeManager,\n    useEmbeddedState,\n} from \"@html_editor/others/embedded_component_utils\";\nimport { Component, onMounted, onWillStart, useEffect, useRef, useState } from \"@odoo/owl\";\nimport { loadBundle } from \"@web/core/assets\";\nimport { cookie } from \"@web/core/browser/cookie\";\nimport {\n    getPreValue,\n    highlightPre,\n} from \"../../core/syntax_highlighting/syntax_highlighting_utils\";\nimport { CodeToolbar } from \"./code_toolbar\";\n\nexport class EmbeddedSyntaxHighlightingComponent extends Component {\n    static template = \"html_editor.EmbeddedSyntaxHighlighting\";\n\n    static components = { CodeToolbar };\n    static props = {\n        value: { type: String },\n        languageId: { type: String },\n        onTextareaFocus: { type: Function },\n        host: { type: Object },\n    };\n\n    setup() {\n        super.setup();\n        this.state = useState({\n            host: this.props.host,\n            highlightedValue: \"\",\n        });\n        this.embeddedState = useEmbeddedState(this.props.host);\n        this.preRef = useRef(\"pre\");\n        this.textareaRef = useRef(\"textarea\");\n\n        onWillStart(() => this.loadPrism());\n        onMounted(() => {\n            this.pre = this.preRef.el;\n            this.textarea = this.textareaRef.el;\n            this.document = this.textarea.ownerDocument;\n            this.highlight();\n        });\n\n        useEffect(this.highlight.bind(this), () => [\n            this.embeddedState.value,\n            this.embeddedState.languageId,\n        ]);\n    }\n\n    /**\n     * Load the Prism library. This function exists only so it can be overridden\n     * in tests.\n     */\n    loadPrism() {\n        return loadBundle(\n            `html_editor.assets_prism${cookie.get(\"color_scheme\") === \"dark\" ? \"_dark\" : \"\"}`,\n            { targetDoc: this.props.host.ownerDocument }\n        );\n    }\n\n    /**\n     * Highlight the content of the pre.\n     */\n    highlight() {\n        const focus = this.document.activeElement === this.textarea;\n\n        highlightPre(this.pre, this.embeddedState.value, this.embeddedState.languageId);\n\n        // Ensure the values match.\n        const preValue = getPreValue(this.pre);\n        if (this.textarea.value !== preValue) {\n            this.textarea.value = preValue;\n        }\n        if (focus) {\n            this.textarea.focus({ preventScroll: true });\n            this.props.onTextareaFocus();\n        }\n        this.embeddedState.value = this.textarea.value;\n    }\n\n    onInput() {\n        this.textarea.focus();\n        this.props.onTextareaFocus();\n        this.embeddedState.value = this.textarea.value;\n    }\n\n    /**\n     * Handle tabulation in the textarea.\n     *\n     * @param {KeyboardEvent} ev\n     */\n    onKeydown(ev) {\n        if (ev.key === \"Tab\") {\n            ev.preventDefault();\n            const tabSize = +getComputedStyle(this.textarea).tabSize || 4;\n            const tab = \" \".repeat(tabSize);\n            const { selectionStart, selectionEnd } = this.textarea;\n            const collapsed = selectionStart === selectionEnd;\n            let start = this.textarea.value.slice(0, selectionStart).lastIndexOf(\"\\n\");\n            start = start === -1 ? 0 : start;\n            let newValue = \"\";\n            let spacesRemovedAtStart = 0;\n            if (ev.shiftKey) {\n                // Remove tabs.\n                let end = this.textarea.value\n                    .slice(selectionEnd, this.textarea.value.length)\n                    .indexOf(\"\\n\");\n                end = end === -1 ? 0 : end;\n                end = selectionEnd + end;\n                // From 0 to the last \\n before selection start.\n                newValue = this.textarea.value.slice(0, start);\n                // From the last \\n before selection start to selection end.\n                const regex = new RegExp(`(\\n|^)( |\\u00A0){1,${tabSize}}`, \"g\");\n                const startSlice = this.textarea.value.slice(start, selectionStart);\n                const cleanStartSlice = startSlice.replace(regex, \"$1\");\n                spacesRemovedAtStart = startSlice.length - cleanStartSlice.length;\n                newValue += cleanStartSlice;\n                newValue += this.textarea.value\n                    .slice(selectionStart, selectionEnd)\n                    .replace(regex, \"$1\");\n                newValue += this.textarea.value.slice(selectionEnd, end).replace(regex, \"$1\");\n                // From selection end to end.\n                newValue += this.textarea.value.slice(end, this.textarea.value.length);\n            } else {\n                // Insert tabs.\n                if (collapsed && /\\S/.test(this.textarea.value.slice(start, selectionStart))) {\n                    newValue =\n                        this.textarea.value.slice(0, selectionStart) +\n                        tab +\n                        this.textarea.value.slice(selectionStart, this.textarea.value.length);\n                } else {\n                    // From 0 to the last \\n before selection start.\n                    newValue = start ? this.textarea.value.slice(0, start) : tab;\n                    // From the last \\n before selection start to selection end.\n                    newValue += this.textarea.value\n                        .slice(start, selectionEnd)\n                        .replaceAll(\"\\n\", `\\n${tab}`);\n                    // From selection end to end.\n                    newValue += this.textarea.value.slice(selectionEnd, this.textarea.value.length);\n                }\n            }\n            const insertedChars = newValue.length - this.textarea.value.length;\n            this.textarea.value = newValue;\n            const newStart = selectionStart + (ev.shiftKey ? -spacesRemovedAtStart : tabSize);\n            const newEnd = collapsed ? newStart : selectionEnd + insertedChars;\n            this.textarea.setSelectionRange(newStart, newEnd, this.textarea.selectionDirection);\n            this.embeddedState.value = this.textarea.value;\n        }\n    }\n\n    /**\n     * Ensure the pre and textarea's scrolls match so they remain aligned.\n     */\n    onScroll() {\n        this.pre.scrollTop = this.textarea.scrollTop;\n        this.pre.scrollLeft = this.textarea.scrollLeft;\n    }\n\n    /**\n     * Change the language when selecting a new one via the code toolbar.\n     *\n     * @param {string} languageId\n     */\n    onLanguageChange(languageId) {\n        if (languageId && this.embeddedState.languageId !== languageId) {\n            this.textarea.focus();\n            this.props.onTextareaFocus();\n            this.embeddedState.languageId = languageId;\n        }\n    }\n}\n\nexport const syntaxHighlightingEmbedding = {\n    name: \"syntaxHighlighting\",\n    Component: EmbeddedSyntaxHighlightingComponent,\n    getProps: (host) => ({ host, ...getEmbeddedProps(host) }),\n    getStateChangeManager: (config) => new StateChangeManager(config),\n};\n", "import {\n    getEmbeddedProps,\n    StateChangeManager,\n    useEmbeddedState,\n} from \"@html_editor/others/embedded_component_utils\";\nimport { getVideoUrl } from \"@html_editor/utils/url\";\nimport {\n    Component,\n    onMounted,\n    onWillDestroy,\n    onWillUnmount,\n    useExternalListener,\n    useRef,\n} from \"@odoo/owl\";\nimport { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { useDropdownState } from \"@web/core/dropdown/dropdown_hooks\";\nimport { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\nimport { ReadonlyEmbeddedVideoComponent } from \"../../core/video/readonly_video\";\n\nexport class EmbeddedVideoComponent extends ReadonlyEmbeddedVideoComponent {\n    static template = \"html_editor.EmbeddedVideo\";\n    static props = {\n        platform: { type: String },\n        videoId: { type: String },\n        params: { type: Object, optional: true },\n        host: { type: HTMLElement },\n        createOverlay: { type: Function, optional: true },\n        focusEditable: { type: Function, optional: true },\n        addStep: { type: Function, optional: true },\n        openVideoSelectorDialog: { type: Function, optional: true },\n    };\n\n    setup() {\n        super.setup();\n        this.videoBlock = this.props.host;\n        this.state = useEmbeddedState(this.videoBlock);\n        this.dropdown = useDropdownState();\n\n        this.videoSettingsOverlay = this.props.createOverlay(VideoSettings, {\n            positionOptions: {\n                position: \"right-start\",\n            },\n            className: \"video-overlay\",\n            closeOnPointerdown: false,\n        });\n        this.iframeRef = useRef(\"iframeRef\");\n\n        useExternalListener(this.videoBlock, \"pointerenter\", () => {\n            this.videoSettingsOverlay.open({\n                target: this.videoBlock,\n                props: {\n                    videoBlock: this.videoBlock,\n                    overlay: this.videoSettingsOverlay,\n                    replaceVideo: () => {\n                        this.props.openVideoSelectorDialog((media) => {\n                            this.replaceVideo(media);\n                        }, this.iframeRef.el);\n                    },\n                    removeVideo: () => {\n                        this.videoBlock.remove();\n                        this.props.addStep();\n                    },\n                    focusEditable: this.props.focusEditable,\n                    dropdown: this.dropdown,\n                },\n            });\n        });\n\n        useExternalListener(this.videoBlock, \"pointerleave\", (e) => {\n            if (this.dropdown.isOpen || e.relatedTarget?.closest(\".video-overlay\")) {\n                return;\n            }\n            this.videoSettingsOverlay.close();\n        });\n\n        onWillDestroy(() => {\n            this.videoSettingsOverlay?.close();\n        });\n    }\n\n    get url() {\n        return getVideoUrl(this.state.platform, this.state.videoId, this.state.params).toString();\n    }\n\n    /**\n     * Replace a video in the editor\n     * @param {Object} media\n     */\n    replaceVideo(media) {\n        this.state.videoId = media.videoId;\n        this.state.platform = media.platform;\n        this.state.params = media.params;\n        this.props.focusEditable();\n    }\n}\n\nexport const videoEmbedding = {\n    name: \"video\",\n    Component: EmbeddedVideoComponent,\n    getProps: (host) => ({ host, ...getEmbeddedProps(host) }),\n    getStateChangeManager: (config) => new StateChangeManager(config),\n};\n\nexport class VideoSettings extends Component {\n    static template = \"html_editor.VideoSettings\";\n    static components = { Dropdown, DropdownItem };\n    static props = {\n        videoBlock: { type: HTMLElement },\n        overlay: { type: Object },\n        replaceVideo: { type: Function },\n        removeVideo: { type: Function },\n        focusEditable: { type: Function },\n        dropdown: { type: Object },\n    };\n\n    setup() {\n        this.menuRef = useRef(\"menuRef\");\n\n        onMounted(() => {\n            this.menuRef.el.addEventListener(\"pointerleave\", () => {\n                if (!this.props.dropdown.isOpen) {\n                    this.props.overlay.close();\n                }\n            });\n        });\n\n        useExternalListener(document, \"pointerdown\", (ev) => {\n            if (this.props.dropdown.isOpen) {\n                return;\n            }\n            this.props.overlay.close();\n        });\n\n        onWillUnmount(() => {\n            if (!this.props.videoBlock.isConnected) {\n                this.props.focusEditable();\n            }\n        });\n    }\n}\n", "import { fileEmbedding } from \"@html_editor/others/embedded_components/backend/file/file\";\nimport { captionEmbedding } from \"@html_editor/others/embedded_components/backend/caption/caption\";\nimport { readonlyFileEmbedding } from \"@html_editor/others/embedded_components/core/file/readonly_file\";\nimport {\n    readonlyTableOfContentEmbedding,\n    tableOfContentEmbedding,\n} from \"@html_editor/others/embedded_components/core/table_of_content/table_of_content\";\nimport { toggleBlockEmbedding } from \"@html_editor/others/embedded_components/core/toggle_block/toggle_block\";\nimport { videoEmbedding } from \"@html_editor/others/embedded_components/backend/video/video\";\nimport { readonlyVideoEmbedding } from \"@html_editor/others/embedded_components/core/video/readonly_video\";\nimport { syntaxHighlightingEmbedding } from \"@html_editor/others/embedded_components/backend/syntax_highlighting/syntax_highlighting\";\nimport { readonlySyntaxHighlightingEmbedding } from \"./core/syntax_highlighting/readonly_syntax_highlighting\";\n\nexport const MAIN_EMBEDDINGS = [\n    fileEmbedding,\n    tableOfContentEmbedding,\n    toggleBlockEmbedding,\n    videoEmbedding,\n    captionEmbedding,\n    syntaxHighlightingEmbedding,\n];\n\nexport const READONLY_MAIN_EMBEDDINGS = [\n    readonlyFileEmbedding,\n    readonlyTableOfContentEmbedding,\n    toggleBlockEmbedding,\n    readonlyVideoEmbedding,\n    captionEmbedding,\n    readonlySyntaxHighlightingEmbedding,\n];\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { closestBlock, isBlock } from \"@html_editor/utils/blocks\";\nimport { renderToElement } from \"@web/core/utils/render\";\nimport { unwrapContents } from \"@html_editor/utils/dom\";\nimport { closestElement } from \"@html_editor/utils/dom_traversal\";\nimport { EDITABLE_MEDIA_CLASS, isVisible } from \"@html_editor/utils/dom_info\";\nimport { boundariesOut, rightPos } from \"@html_editor/utils/position\";\nimport { findInSelection } from \"@html_editor/utils/selection\";\nimport { isHtmlContentSupported } from \"@html_editor/core/selection_plugin\";\n\nexport class CaptionPlugin extends Plugin {\n    static id = \"caption\";\n    static dependencies = [\n        \"image\",\n        \"split\",\n        \"history\",\n        \"embeddedComponents\",\n        \"selection\",\n        \"baseContainer\",\n    ];\n    /** @type {import(\"plugins\").EditorResources} */\n    resources = {\n        user_commands: [\n            {\n                id: \"toggleImageCaption\",\n                title: _t(\"Add/remove a caption\"),\n                run: this.toggleImageCaption.bind(this),\n                isAvailable: isHtmlContentSupported,\n            },\n        ],\n        toolbar_items: [\n            {\n                id: \"image_caption\",\n                description: _t(\"Add/remove a caption\"),\n                groupId: \"image_description\",\n                commandId: \"toggleImageCaption\",\n                text: \"Caption\",\n                isActive: () => this.hasImageCaption(this.dependencies.image.getTargetedImage()),\n            },\n        ],\n        clean_for_save_handlers: this.cleanForSave.bind(this),\n        mount_component_handlers: this.setupNewCaption.bind(this),\n        delete_handlers: this.afterDelete.bind(this),\n        delete_image_overrides: this.handleDeleteImage.bind(this),\n        after_save_media_dialog_handlers: this.onImageReplaced.bind(this),\n        hints: [{ selector: \"FIGCAPTION\", text: _t(\"Write a caption...\") }],\n        unsplittable_node_predicates: [\n            (node) => [\"FIGURE\", \"FIGCAPTION\"].includes(node.nodeName), // avoid merge\n        ],\n        image_name_predicates: [this.getImageName.bind(this)],\n        link_compatible_selection_predicates: [this.isLinkAllowedOnSelection.bind(this)],\n        // Consider a <figure> element as empty if it only contains a\n        // <figcaption> element (e.g. when its image has just been\n        // removed).\n        empty_node_predicates: (el) =>\n            el.matches?.(\"figure\") &&\n            el.children.length === 1 &&\n            el.children[0].matches(\"figcaption\"),\n        move_node_whitelist_selectors: \"figure\",\n    };\n\n    setup() {\n        for (const figure of this.editable.querySelectorAll(\"figure\")) {\n            // Embed the captions.\n            const image = figure.querySelector(\"img\");\n            figure.before(image);\n            const caption = figure.querySelector(\"figcaption\")?.textContent;\n            figure.remove();\n            this.addImageCaption(image, caption, false);\n        }\n    }\n\n    cleanForSave({ root }) {\n        for (const figure of root.querySelectorAll(\"figure\")) {\n            figure.removeAttribute(\"contenteditable\");\n            const image = figure.querySelector(\"img\");\n            // Remove embedding and convert caption attribute to text.\n            figure.querySelector(\"figcaption\").remove();\n            const caption = root.ownerDocument.createElement(\"figcaption\");\n            caption.textContent = image.getAttribute(\"data-caption\");\n            image.removeAttribute(\"data-caption\");\n            image.removeAttribute(\"data-caption-id\");\n            image.classList.remove(EDITABLE_MEDIA_CLASS);\n            image.after(caption);\n        }\n    }\n\n    hasImageCaption(image) {\n        if (!image) {\n            return;\n        }\n        const block = closestBlock(image);\n        return (\n            block.nodeName === \"FIGURE\" && !!block.querySelector(\"[data-embedded='caption'] input\")\n        );\n    }\n\n    toggleImageCaption(image = this.dependencies.image.getTargetedImage()) {\n        if (!image) {\n            return;\n        }\n        if (this.hasImageCaption(image)) {\n            this.removeImageCaption(image);\n        } else {\n            this.addImageCaption(image, image.getAttribute(\"data-caption\") || \"\");\n        }\n    }\n\n    getCaptionId() {\n        return \"\" + Math.floor(Math.random() * Date.now());\n    }\n\n    addImageCaption(image, captionText = \"\", focusInput = true) {\n        this.captionsBeingAdded ||= new Set();\n        // Move the image within a figure element.\n        const figure = this.document.createElement(\"figure\");\n        const link = image.parentElement.nodeName === \"A\" && image.parentElement;\n        if (link && (link.previousSibling || link.nextSibling)) {\n            // <p>wx<a><img/></a>yz</p> => <p>wx</p><p><a><img/></a></p><p>yz</p>\n            this.dependencies.split.splitAroundUntil(link, closestBlock(link));\n        } else if (\n            !link &&\n            (image.previousSibling || image.nextSibling) &&\n            closestBlock(image) !== this.editable\n        ) {\n            // <p>wx<img/>yz</p> => <p>wx</p><p><img/></p><p>yz</p>\n            const block = this.dependencies.split.splitAroundUntil(image, closestBlock(image));\n            if (isBlock(block.previousSibling) && !isVisible(block.previousSibling)) {\n                block.previousSibling.remove();\n            }\n            if (isBlock(block.nextSibling) && !isVisible(block.nextSibling)) {\n                block.nextSibling.remove();\n            }\n        }\n        // => <p><figure><img/></figure></p>\n        // or <p><a><figure><img/></figure></a></p>\n        image.before(figure);\n        figure.append(image);\n        if (!link && figure.parentElement !== this.editable) {\n            // => <figure><img/></figure></p>\n            // but still <p><a><figure><img/></figure></p>\n            unwrapContents(figure.parentElement);\n            // Figure is contenteditable=\"false\", so selection would jump\n            // to the nearest editable sibling <div>. Setting cursor at\n            // the end ensures caption input receives focus correctly.\n            this.dependencies.selection.setCursorEnd(figure);\n        }\n        // Set the caption and its ID.\n        const captionId = this.getCaptionId();\n        this.captionsBeingAdded.add(captionId);\n        image.setAttribute(\"data-caption-id\", captionId);\n        image.setAttribute(\"data-caption\", captionText || \"\");\n        // Ensure it's not possible to write inside the figure.\n        figure.setAttribute(\"contenteditable\", \"false\");\n        image.classList.add(EDITABLE_MEDIA_CLASS);\n        // Add the caption component.\n        // => <p><figure><img/><figcaption>...</figcaption></figure></p>\n        // or <p><a><figure><img/><figcaption>...</figcaption></figure></a></p>\n        const caption = renderToElement(\"html_editor.EmbeddedCaptionBlueprint\", {\n            embeddedProps: JSON.stringify({\n                id: captionId,\n                focusInput,\n            }),\n        });\n        figure.append(caption);\n        this.dependencies.history.addStep();\n        this.captionsBeingAdded.delete(captionId);\n    }\n\n    removeImageCaption(image) {\n        const figure = closestElement(image, \"figure\");\n        if (figure) {\n            figure.querySelector(\"figcaption\").remove();\n            if (closestBlock(figure.parentElement) === this.editable) {\n                const baseContainer = this.dependencies.baseContainer.createBaseContainer();\n                if (figure.parentElement.nodeName === \"A\") {\n                    figure.parentElement.before(baseContainer);\n                    baseContainer.append(figure.parentElement);\n                } else {\n                    figure.before(baseContainer);\n                    baseContainer.append(figure);\n                }\n            }\n            unwrapContents(figure);\n            image.removeAttribute(\"data-caption-id\"); // (keep the data-caption for if we toggle again)\n            image.classList.remove(EDITABLE_MEDIA_CLASS);\n            // Select the image.\n            const [anchorNode, anchorOffset, focusNode, focusOffset] = boundariesOut(image);\n            this.dependencies.selection.setSelection({\n                anchorNode,\n                anchorOffset,\n                focusNode,\n                focusOffset,\n            });\n            this.dependencies.selection.focusEditable();\n            this.dependencies.history.addStep();\n        }\n    }\n\n    setupNewCaption({ name, props }) {\n        if (name === \"caption\") {\n            const id = props.id;\n            delete props.id;\n            const image = this.editable.querySelector(`img[data-caption-id=\"${id}\"]`);\n            Object.assign(props, {\n                image,\n                onUpdateCaption: (caption = \"\") => {\n                    const figcaption = image.parentElement.querySelector(\"figcaption\");\n                    if (figcaption && figcaption.getAttribute(\"placeholder\") !== caption) {\n                        // Adapt the figcaption element's placeholder to the new\n                        // caption for screen reader users.\n                        figcaption.setAttribute(\"placeholder\", caption);\n                    }\n                    if (caption !== image.getAttribute(\"data-caption\")) {\n                        image.setAttribute(\"data-caption\", caption);\n                    }\n                    if (!this.captionsBeingAdded?.has(id)) {\n                        // If the caption is being added, we update without\n                        // adding a history step because it will be added at the\n                        // end of adding the caption, by `addImageCaption`.\n                        this.dependencies.history.addStep();\n                    }\n                },\n                onEditorHistoryApply: (redo = false) => {\n                    if (redo) {\n                        this.dependencies.history.redo();\n                    } else {\n                        this.dependencies.history.undo();\n                    }\n                },\n            });\n        }\n    }\n\n    getImageName(image) {\n        if (closestElement(image, \"figure\")) {\n            return image.getAttribute(\"data-caption\");\n        }\n    }\n\n    isLinkAllowedOnSelection() {\n        const figure = findInSelection(\n            this.dependencies.selection.getEditableSelection(),\n            \"figure\"\n        );\n        if (\n            figure &&\n            this.dependencies.selection\n                .getTargetedNodes()\n                .every((node) => closestElement(node, \"figure\") === figure)\n        ) {\n            return true;\n        }\n    }\n\n    onImageReplaced(media) {\n        const figure = closestElement(media, \"figure\");\n        if (media.nodeName === \"IMG\" && figure) {\n            const [anchorNode, anchorOffset] = rightPos(figure);\n            const caption = figure.querySelector(\"[data-embedded='caption'] input\")?.value;\n            figure.before(media);\n            figure.remove();\n            this.addImageCaption(media, caption, false);\n            this.dependencies.selection.setSelection({ anchorNode, anchorOffset });\n        }\n    }\n\n    afterDelete() {\n        const { anchorNode } = this.dependencies.selection.getEditableSelection();\n        const targetedNodes = this.dependencies.selection.getTargetedNodes();\n        for (const figure of this.editable.querySelectorAll(\"figure:not(:has(img))\")) {\n            const isSelectionInFigure = targetedNodes.includes(figure) || anchorNode === figure;\n            const sibling = figure.nextSibling || figure.previousSibling;\n            figure.remove();\n            if (isSelectionInFigure) {\n                // Note: this assumes the selection is collapsed after delete.\n                this.dependencies.selection.setSelection({\n                    anchorNode: sibling,\n                    anchorOffset: 0,\n                });\n            }\n        }\n    }\n\n    handleDeleteImage(image) {\n        const figure = closestElement(image, \"figure\");\n        if (figure) {\n            const sibling = figure.nextSibling || figure.previousSibling;\n            figure.remove();\n            this.dependencies.selection.setSelection({\n                anchorNode: sibling,\n                anchorOffset: 0,\n            });\n            this.dependencies.history.addStep();\n            return true;\n        }\n    }\n}\n", "import { DocumentSelector } from \"@html_editor/main/media/media_dialog/document_selector\";\nimport { renderToElement } from \"@web/core/utils/render\";\n\n/**\n * Override the @see DocumentSelector to render the uploaded file as embedded\n * component with editable file name and previewable file.\n */\nexport class EmbeddedFileDocumentsSelector extends DocumentSelector {\n    static mediaSpecificClasses = [];\n\n    /** @override */\n    static async renderFileElement(attachment) {\n        return renderEmbeddedFileBox(attachment);\n    }\n}\n\n/**\n * @param {Object} attachment\n * @returns {Element}\n */\nexport function renderEmbeddedFileBox(attachment) {\n    const dotSplit = attachment.name.split(\".\");\n    const extension = dotSplit.length > 1 ? dotSplit.pop() : undefined;\n    const fileData = {\n        access_token: attachment.access_token,\n        checksum: attachment.checksum,\n        extension,\n        filename: attachment.name,\n        id: attachment.id,\n        mimetype: attachment.mimetype,\n        name: attachment.name,\n        type: attachment.type,\n        url: attachment.url || \"\",\n    };\n    return renderToElement(\"html_editor.EmbeddedFileBlueprint\", {\n        embeddedProps: JSON.stringify({ fileData }),\n    });\n}\n", "import { nextLeaf } from \"@html_editor/utils/dom_info\";\nimport { isBlock } from \"@html_editor/utils/blocks\";\nimport {\n    EmbeddedFileDocumentsSelector,\n    renderEmbeddedFileBox,\n} from \"./embedded_file_documents_selector\";\nimport { FilePlugin } from \"@html_editor/main/media/file_plugin\";\nimport { closestElement } from \"@html_editor/utils/dom_traversal\";\n\n/**\n * This plugin is meant to replace the File plugin.\n */\nexport class EmbeddedFilePlugin extends FilePlugin {\n    static id = \"embeddedFile\";\n    static dependencies = [...super.dependencies, \"embeddedComponents\", \"selection\"];\n\n    // Extends the base class resources\n    /** @type {import(\"plugins\").EditorResources} */\n    resources = {\n        ...this.resources,\n        mount_component_handlers: this.setupNewFile.bind(this),\n    };\n\n    /** @override */\n    renderDownloadBox(attachment) {\n        return renderEmbeddedFileBox(attachment);\n    }\n\n    /** @override */\n    isUploadCommandAvailable({ anchorNode }) {\n        return (\n            super.isUploadCommandAvailable() &&\n            !closestElement(anchorNode, \"[data-embedded='clipboard']\")\n        );\n    }\n\n    /** @override */\n    get componentForMediaDialog() {\n        return EmbeddedFileDocumentsSelector;\n    }\n\n    setupNewFile({ name, env }) {\n        if (name === \"file\") {\n            Object.assign(env.editorShared, {\n                setSelectionAfter: (host) => {\n                    try {\n                        const leaf = nextLeaf(host, this.editable);\n                        if (!leaf) {\n                            return;\n                        }\n                        const leafEl = isBlock(leaf) ? leaf : leaf.parentElement;\n                        if (isBlock(leafEl) && leafEl.isContentEditable) {\n                            this.dependencies.selection.setSelection({\n                                anchorNode: leafEl,\n                                anchorOffset: 0,\n                            });\n                        }\n                    } catch {\n                        return;\n                    }\n                },\n            });\n        }\n    }\n}\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { withSequence } from \"@html_editor/utils/resource\";\nimport { getEmbeddedProps } from \"@html_editor/others/embedded_component_utils\";\nimport {\n    DEFAULT_LANGUAGE_ID,\n    getPreValue,\n    newlinesToLineBreaks,\n} from \"../../core/syntax_highlighting/syntax_highlighting_utils\";\nimport { removeInvisibleWhitespace } from \"@html_editor/utils/dom\";\n\nconst CODE_BLOCK_CLASS = \"o_syntax_highlighting\";\nconst CODE_BLOCK_SELECTOR = `div.${CODE_BLOCK_CLASS}`;\n\nexport class SyntaxHighlightingPlugin extends Plugin {\n    static id = \"syntaxHighlighting\";\n    static dependencies = [\n        \"overlay\",\n        \"history\",\n        \"selection\",\n        \"protectedNode\",\n        \"embeddedComponents\",\n    ];\n    /** @type {import(\"plugins\").EditorResources} */\n    resources = {\n        // Ensure focus can be preserved within the textarea:\n        is_node_editable_predicates: (node) => {\n            if (node?.classList?.contains(\"o_prism_source\")) {\n                return true;\n            }\n        },\n        system_attributes: \"data-syntax-highlighting-autofocus\",\n\n        /** Handlers */\n        mount_component_handlers: this.setupNewCodeBlock.bind(this),\n        normalize_handlers: (root) => this.addCodeBlocks(root, true),\n        post_undo_handlers: () => this.addCodeBlocks(this.editable, true),\n        post_redo_handlers: () => this.addCodeBlocks(this.editable, true),\n        clean_for_save_handlers: withSequence(0, ({ root }) => this.cleanForSave(root)),\n        before_set_tag_handlers: (el, newTagName, cursors) => {\n            if (newTagName.toLowerCase() === \"pre\") {\n                // Remove invisible whitespace that would become visible in a `<pre>` element.\n                removeInvisibleWhitespace(el, cursors);\n            }\n        },\n\n        /** Processors */\n        clipboard_content_processors: (clonedContent) => this.cleanForSave(clonedContent),\n    };\n\n    setup() {\n        this.addCodeBlocks();\n    }\n\n    cleanForSave(root) {\n        for (const codeBlock of root.querySelectorAll(\"div.o_syntax_highlighting\")) {\n            // Save only the `<pre>` element, with information to rebuild the\n            // embedded component, so the saved DOM is independent of this plugin.\n            const pre = codeBlock.querySelector(\"pre\");\n            pre.dataset.embedded = \"readonlySyntaxHighlighting\"; // Make it work in readonly.\n            const embeddedProps = getEmbeddedProps(codeBlock);\n            const value = embeddedProps.value;\n            pre.dataset.languageId = embeddedProps.languageId;\n            codeBlock.before(pre);\n            codeBlock.remove();\n            // Remove highlighting.\n            pre.textContent = value;\n            newlinesToLineBreaks(pre);\n        }\n    }\n\n    /**\n     * Take all `<pre>` element in the given `root` that aren't in an embedded\n     * syntax highlighting block, and replace them with an embedded syntax\n     * highlighting block. If `preserveFocus` is true, set the currently\n     * targeted `<pre>` element to be focused.\n     *\n     * @param {Element} [root = this.editable]\n     * @param {boolean} [preserveFocus = false]\n     */\n    addCodeBlocks(root = this.editable, preserveFocus = false) {\n        const targetedNodes = this.dependencies.selection.getTargetedNodes();\n        const nonEmbeddedPres = [...root.querySelectorAll(\"pre\")].filter(\n            (pre) => !pre.closest(CODE_BLOCK_SELECTOR)\n        );\n        for (const pre of nonEmbeddedPres) {\n            const isPreInSelection = !targetedNodes.some((node) => !pre.contains(node));\n            const embeddedProps = JSON.stringify({\n                value: getPreValue(pre),\n                languageId: pre.dataset.languageId || DEFAULT_LANGUAGE_ID,\n            });\n            const codeBlock = this.dependencies.embeddedComponents.renderBlueprintToElement(\n                \"html_editor.EmbeddedSyntaxHighlightingBlueprint\",\n                { embeddedProps },\n                () => {\n                    if (preserveFocus && isPreInSelection) {\n                        const textarea = codeBlock.querySelector(\"textarea\");\n                        if (textarea !== codeBlock.ownerDocument.activeElement) {\n                            textarea.focus();\n                            this.dependencies.history.stageFocus();\n                        }\n                    }\n                }\n            );\n            pre.before(codeBlock);\n            if (isPreInSelection) {\n                // Removing the pre will make us lose the selection. The DOM\n                // would try to set it in the root, which would get corrected,\n                // preventing us from directly writing inside the textarea.\n                this.document.getSelection().removeAllRanges();\n            }\n            pre.remove();\n        }\n    }\n\n    setupNewCodeBlock({ name, props }) {\n        if (name === \"syntaxHighlighting\") {\n            Object.assign(props, {\n                onTextareaFocus: () => this.dependencies.history.stageFocus(),\n            });\n            props.host.removeAttribute(\"data-syntax-highlighting-autofocus\");\n        }\n    }\n}\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { renderToElement } from \"@web/core/utils/render\";\nimport {\n    HEADINGS,\n    TableOfContentManager,\n} from \"@html_editor/others/embedded_components/core/table_of_content/table_of_content_manager\";\nimport { isHtmlContentSupported } from \"@html_editor/core/selection_plugin\";\n\nexport class TableOfContentPlugin extends Plugin {\n    static id = \"tableOfContent\";\n    static dependencies = [\"dom\", \"selection\", \"embeddedComponents\", \"link\", \"history\"];\n    /** @type {import(\"plugins\").EditorResources} */\n    resources = {\n        user_commands: [\n            {\n                id: \"insertTableOfContent\",\n                title: _t(\"Table of Contents\"),\n                description: _t(\"Highlight the structure (headings)\"),\n                icon: \"fa-bookmark\",\n                run: this.insertTableOfContent.bind(this),\n                isAvailable: isHtmlContentSupported,\n            },\n        ],\n        powerbox_items: [\n            {\n                categoryId: \"navigation\",\n                commandId: \"insertTableOfContent\",\n            },\n        ],\n\n        /** Handlers */\n        restore_savepoint_handlers: () => this.delayedUpdateTableOfContents(this.editable),\n        history_reset_handlers: () => this.delayedUpdateTableOfContents(this.editable),\n        history_reset_from_steps_handlers: () => this.delayedUpdateTableOfContents(this.editable),\n        step_added_handlers: ({ stepCommonAncestor }) =>\n            this.delayedUpdateTableOfContents(stepCommonAncestor),\n        external_step_added_handlers: this.delayedUpdateTableOfContents.bind(this, this.editable),\n        clean_for_save_handlers: this.cleanForSave.bind(this),\n        mount_component_handlers: this.setupNewToc.bind(this),\n\n        system_classes: [\"o_embedded_toc_header_highlight\"],\n    };\n\n    setup() {\n        this.manager = new TableOfContentManager({\n            el: this.editable,\n        });\n        this.alive = true;\n    }\n\n    insertTableOfContent() {\n        const tableOfContentBlueprint = renderToElement(\"html_editor.TableOfContentBlueprint\");\n        this.dependencies.dom.insert(tableOfContentBlueprint);\n        this.dependencies.history.addStep();\n    }\n\n    /**\n     * @param {HTMLElement} root\n     */\n    cleanForSave({ root }) {\n        for (const el of root.querySelectorAll(\".o_embedded_toc_header_highlight\")) {\n            el.classList.remove(\"o_embedded_toc_header_highlight\");\n        }\n    }\n\n    destroy() {\n        super.destroy();\n        this.alive = false;\n    }\n\n    delayedUpdateTableOfContents(element) {\n        const selector = HEADINGS.join(\",\");\n        if (!(!element || element.querySelector(selector) || element.closest(selector))) {\n            return;\n        }\n        if (this.updateTimeout) {\n            window.clearTimeout(this.updateTimeout);\n        }\n        this.updateTimeout = window.setTimeout(() => {\n            if (!this.alive) {\n                return;\n            }\n            this.manager.updateStructure();\n        }, 500);\n    }\n\n    setupNewToc({ name, props }) {\n        if (name === \"tableOfContent\") {\n            Object.assign(props, {\n                manager: this.manager,\n            });\n        }\n    }\n}\n", "import { getEmbeddedProps } from \"@html_editor/others/embedded_component_utils\";\nimport { Plugin } from \"@html_editor/plugin\";\nimport { isHtmlContentSupported } from \"@html_editor/core/selection_plugin\";\nimport { baseContainerGlobalSelector } from \"@html_editor/utils/base_container\";\nimport { closestBlock } from \"@html_editor/utils/blocks\";\nimport { isEmptyBlock, isParagraphRelatedElement } from \"@html_editor/utils/dom_info\";\nimport {\n    childNodes,\n    children,\n    closestElement,\n    firstLeaf,\n    lastLeaf,\n    selectElements,\n} from \"@html_editor/utils/dom_traversal\";\nimport { parseHTML } from \"@html_editor/utils/html\";\nimport { childNodeIndex, nodeSize } from \"@html_editor/utils/position\";\nimport { withSequence } from \"@html_editor/utils/resource\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { renderToString } from \"@web/core/utils/render\";\nimport { uuid } from \"@web/core/utils/strings\";\n\nconst toggleSelector = \"[data-embedded='toggleBlock']\";\nconst titleSelector = \"[data-embedded-editable='title']\";\nconst contentSelector = \"[data-embedded-editable='content']\";\n\nexport class ToggleBlockPlugin extends Plugin {\n    static id = \"toggleBlock\";\n    static dependencies = [\n        \"baseContainer\",\n        \"delete\",\n        \"dom\",\n        \"embeddedComponents\", // toggle is an embedded component.\n        \"history\",\n        \"selection\",\n        \"split\",\n    ];\n    /** @type {import(\"plugins\").EditorResources} */\n    resources = {\n        hints: [\n            withSequence(20, {\n                selector: `${toggleSelector} ${titleSelector} > *`,\n                text: _t(\"Toggle title\"),\n            }),\n            withSequence(10, {\n                selector: `${toggleSelector} ${contentSelector}:not(:focus) > ${baseContainerGlobalSelector}:only-child`,\n                text: _t(\"Add something inside this toggle\"),\n            }),\n        ],\n        hint_targets_providers: (selectionData, editable) => [\n            ...editable.querySelectorAll(\n                `${toggleSelector} ${contentSelector} > ${baseContainerGlobalSelector}:only-child`\n            ),\n        ],\n        move_node_blacklist_selectors: `${toggleSelector} ${titleSelector} *`,\n        selection_blocker_predicates: (blocker) => {\n            // Prevent the insertion of selection placeholders around toggle blocks.\n            if (blocker.nodeType === Node.ELEMENT_NODE && blocker.dataset.embedded === \"toggleBlock\") {\n                return false;\n            }\n        },\n        powerbox_items: [\n            {\n                commandId: \"insertToggleBlock\",\n                categoryId: \"structure\",\n            },\n        ],\n        shortcuts: [{ hotkey: \"control+enter\", commandId: \"switchToggleBlockState\" }],\n        user_commands: [\n            {\n                id: \"insertToggleBlock\",\n                title: _t(\"Toggle list\"),\n                description: _t(\"Hide Text under foldable toggles\"),\n                icon: \"fa-caret-square-o-right\",\n                isAvailable: (selection) =>\n                    isHtmlContentSupported(selection) &&\n                    !closestElement(selection.anchorNode, `${toggleSelector} ${titleSelector}`),\n                run: () => {\n                    this.insertToggleBlock();\n                },\n            },\n            {\n                id: \"switchToggleBlockState\",\n                run: this.manageToggleFromTitle.bind(this),\n            },\n        ],\n\n        normalize_handlers: withSequence(Infinity, this.normalize.bind(this)),\n\n        delete_backward_overrides: this.handleDeleteBackward.bind(this),\n        delete_forward_overrides: this.handleDeleteForward.bind(this),\n        shift_tab_overrides: this.handleShiftTab.bind(this),\n        split_element_block_overrides: withSequence(1, this.handleSplitElementBlock.bind(this)),\n        tab_overrides: this.handleTab.bind(this),\n\n        power_buttons_visibility_predicates: this.showPowerButtons.bind(this),\n\n        before_insert_processors: this.handleInsert.bind(this),\n    };\n\n    setup() {\n        this.preventDeleteBackwardContentEnd = false;\n        this.selectedToggleEmptyContentSet = new Set();\n    }\n\n    explodeToggle(toggle) {\n        const title = toggle.querySelector(titleSelector);\n        const content = toggle.querySelector(contentSelector);\n        let contentChildren = children(content);\n        if (contentChildren.length === 1 && isEmptyBlock(contentChildren[0])) {\n            contentChildren = [];\n        }\n        toggle.replaceWith(...children(title), ...contentChildren);\n    }\n\n    forceToggle(toggle, { showContent, restoreSelection } = {}) {\n        toggle.dispatchEvent(\n            new CustomEvent(\"forceToggle\", { detail: { showContent, restoreSelection } })\n        );\n    }\n\n    generateUniqueIds(toggles) {\n        for (const toggle of toggles) {\n            const props = getEmbeddedProps(toggle);\n            props.toggleBlockId = this.getUniqueIdentifier();\n            toggle.dataset.embeddedProps = JSON.stringify(props);\n        }\n    }\n\n    getClosestToggleContentInfo(node) {\n        const toggle = closestElement(node, toggleSelector);\n        const title = toggle?.querySelector(titleSelector);\n        const content = toggle?.querySelector(contentSelector);\n        return content?.contains(node) ? { content, title, toggle } : {};\n    }\n\n    getClosestToggleTitleInfo(node) {\n        const toggle = closestElement(node, toggleSelector);\n        const title = toggle?.querySelector(titleSelector);\n        const content = toggle?.querySelector(contentSelector);\n        return title?.contains(node) ? { content, title, toggle } : {};\n    }\n\n    getToggleFromTitleSelection() {\n        const selection = this.dependencies.selection.getEditableSelection();\n        if (!selection.anchorNode) {\n            return;\n        }\n        const { toggle } = this.getClosestToggleTitleInfo(selection.anchorNode);\n        return toggle;\n    }\n\n    getUniqueIdentifier() {\n        return uuid();\n    }\n\n    /**\n     * Handle all behaviors linked to the use of deleteBackward in the editor:\n     * 1. selection at start of title: explode the toggle and keep title and content as siblings\n     * 2. selection at end of content in a paragraph: unwraps from the toggle content\n     * 3. selection at start of content in a paragraph: merge the paragraph with the title\n     * 4. selection at start of paragraph after a toggle: merge the paragraph at content end\n     */\n    handleDeleteBackward(range) {\n        for (const handler of [\n            this.handleDeleteBackwardTitleStart,\n            this.handleDeleteBackwardContentEnd,\n            this.handleDeleteBackwardContentStart,\n            this.handleDeleteBackwardAfterToggle,\n        ]) {\n            if (handler.call(this, range)) {\n                return true;\n            }\n        }\n    }\n\n    handleDeleteBackwardContentEnd({ endContainer, endOffset }) {\n        const block = closestBlock(endContainer);\n        const isEmptyContainer = isEmptyBlock(endContainer);\n        const leaf = isEmptyContainer ? endContainer : firstLeaf(block);\n        const { toggle, content } = this.getClosestToggleContentInfo(endContainer);\n        if (\n            !content ||\n            endOffset !== 0 ||\n            childNodeIndex(block) === 0 ||\n            block.nextElementSibling ||\n            leaf !== endContainer ||\n            !isParagraphRelatedElement(block) ||\n            this.preventDeleteBackwardContentEnd\n        ) {\n            return;\n        }\n        toggle.after(block);\n        this.dependencies.selection.setCursorStart(block);\n        return true;\n    }\n\n    handleDeleteBackwardContentStart({ endContainer, endOffset }) {\n        const block = closestBlock(endContainer);\n        const leaf = isEmptyBlock(endContainer) ? endContainer : firstLeaf(block);\n        const { title, content } = this.getClosestToggleContentInfo(endContainer);\n        if (\n            !content ||\n            endOffset !== 0 ||\n            childNodeIndex(block) !== 0 ||\n            leaf !== endContainer ||\n            !isParagraphRelatedElement(block)\n        ) {\n            return;\n        }\n        title.append(block);\n        this.dependencies.selection.setCursorStart(block);\n        this.dependencies.delete.deleteBackward(\n            this.dependencies.selection.getEditableSelection(),\n            \"character\"\n        );\n        return true;\n    }\n\n    handleDeleteBackwardTitleStart({ endContainer, endOffset }) {\n        const block = closestBlock(endContainer);\n        const leaf = isEmptyBlock(endContainer) ? endContainer : firstLeaf(block);\n        const { toggle, title } = this.getClosestToggleTitleInfo(endContainer);\n        if (!title || endOffset !== 0 || childNodeIndex(block) !== 0 || leaf !== endContainer) {\n            return;\n        }\n        const cursors = this.dependencies.selection.preserveSelection();\n        this.explodeToggle(toggle);\n        cursors.restore();\n        return true;\n    }\n\n    handleDeleteBackwardAfterToggle({ endContainer, endOffset }) {\n        const block = closestBlock(endContainer);\n        const leaf = isEmptyBlock(endContainer) ? endContainer : firstLeaf(block);\n        const toggle = block?.previousSibling;\n        if (!toggle?.matches?.(toggleSelector) || endOffset !== 0 || leaf !== endContainer) {\n            return;\n        }\n        let target = toggle.querySelector(contentSelector);\n        if (target.parentElement.matches(\".d-none\")) {\n            if (!isParagraphRelatedElement(block)) {\n                return;\n            }\n            const title = toggle.querySelector(titleSelector);\n            target = title;\n        }\n        target.append(block);\n        this.dependencies.selection.setCursorStart(block);\n        this.preventDeleteBackwardContentEnd = true;\n        this.dependencies.delete.deleteBackward(\n            this.dependencies.selection.getEditableSelection(),\n            \"character\"\n        );\n        this.preventDeleteBackwardContentEnd = false;\n        return true;\n    }\n\n    /**\n     * Handle all behaviors linked to the use of deleteForward in the editor:\n     * 1. selection at end of title:\n     *   - (optional) explode a potential toggle at the start of content\n     *   - merge first paragraph from content with the title\n     * 2. selection at end of content in a paragraph:\n     *   - (optional) explode a potential sibling toggle\n     *   - merge a sibling paragraph at content end\n     * 3. selection at end of paragraph before a toggle: explode the toggle\n     */\n    handleDeleteForward(range) {\n        for (const handler of [\n            this.handleDeleteForwardTitleEnd,\n            this.handleDeleteForwardContentEnd,\n            this.handleDeleteForwardBeforeToggle,\n        ]) {\n            if (handler.call(this, range)) {\n                return true;\n            }\n        }\n    }\n\n    handleDeleteForwardContentEnd({ startContainer, startOffset }) {\n        const block = closestBlock(startContainer);\n        const isEmptyContainer = isEmptyBlock(startContainer);\n        const leaf = isEmptyContainer ? startContainer : lastLeaf(block);\n        const { toggle, content } = this.getClosestToggleContentInfo(startContainer);\n        if (\n            !content ||\n            !(\n                (isEmptyContainer && startOffset === 0) ||\n                startOffset === nodeSize(startContainer)\n            ) ||\n            block === this.editable ||\n            block.nextElementSibling ||\n            leaf !== startContainer ||\n            !isParagraphRelatedElement(block)\n        ) {\n            return;\n        }\n        let nextEl = toggle.nextSibling;\n        if (nextEl?.matches?.(toggleSelector)) {\n            this.explodeToggle(nextEl);\n            nextEl = toggle.nextSibling;\n        }\n        if (!isParagraphRelatedElement(nextEl)) {\n            return;\n        }\n        content.append(nextEl);\n        this.dependencies.selection.setCursorEnd(block);\n        this.dependencies.delete.deleteForward(\n            this.dependencies.selection.getEditableSelection(),\n            \"character\"\n        );\n        return true;\n    }\n\n    handleDeleteForwardTitleEnd({ startContainer, startOffset }) {\n        const block = closestBlock(startContainer);\n        const isEmptyContainer = isEmptyBlock(startContainer);\n        const leaf = isEmptyContainer ? startContainer : lastLeaf(block);\n        const { toggle, title, content } = this.getClosestToggleTitleInfo(startContainer);\n        if (\n            !title ||\n            !(\n                (isEmptyContainer && startOffset === 0) ||\n                startOffset === nodeSize(startContainer)\n            ) ||\n            block === this.editable ||\n            block.nextElementSibling ||\n            leaf !== startContainer\n        ) {\n            return;\n        }\n        let nextEl;\n        if (content.parentElement.matches(\".d-none\")) {\n            nextEl = toggle.nextSibling;\n            if (nextEl.matches?.(toggleSelector)) {\n                this.explodeToggle(nextEl);\n                nextEl = toggle.nextSibling;\n            }\n        } else {\n            nextEl = content.firstChild;\n            if (nextEl.matches?.(toggleSelector)) {\n                this.explodeToggle(nextEl);\n                nextEl = content.firstChild;\n            }\n        }\n        if (!isParagraphRelatedElement(nextEl)) {\n            return;\n        }\n        title.append(nextEl);\n        this.dependencies.selection.setCursorEnd(block);\n        this.dependencies.delete.deleteForward(\n            this.dependencies.selection.getEditableSelection(),\n            \"character\"\n        );\n        return true;\n    }\n\n    handleDeleteForwardBeforeToggle({ startContainer, startOffset }) {\n        const block = closestBlock(startContainer);\n        const isEmptyContainer = isEmptyBlock(startContainer);\n        const leaf = isEmptyContainer ? startContainer : lastLeaf(block);\n        const toggle = block.nextSibling;\n        if (\n            !toggle?.matches?.(toggleSelector) ||\n            !(\n                (isEmptyContainer && startOffset === 0) ||\n                startOffset === nodeSize(startContainer)\n            ) ||\n            leaf !== startContainer\n        ) {\n            return;\n        }\n        if (isEmptyBlock(block)) {\n            block.remove();\n            const title = toggle.querySelector(titleSelector);\n            this.dependencies.selection.setCursorStart(title.firstElementChild);\n        } else {\n            this.explodeToggle(toggle);\n            this.dependencies.selection.setCursorEnd(block);\n            this.dependencies.delete.deleteForward(\n                this.dependencies.selection.getEditableSelection(),\n                \"character\"\n            );\n        }\n        return true;\n    }\n\n    /**\n     * Generate new toggleBlockIds for every inserted toggle, to avoid duplicating\n     * copies.\n     */\n    handleInsert(insertContainer) {\n        const insertedToggles = insertContainer.querySelectorAll(toggleSelector);\n        this.generateUniqueIds(insertedToggles);\n        return insertContainer;\n    }\n\n    /**\n     * Shift + Tab in a toggle title will extract it from a potential parent\n     * toggle, and every child of that parent toggle content that is a nextSibling\n     * of the current toggle will be appended to the current toggle content.\n     */\n    handleShiftTab() {\n        const toggle = this.getToggleFromTitleSelection();\n        if (toggle) {\n            const closestToggleAncestor = closestElement(toggle.parentElement, toggleSelector);\n            if (closestToggleAncestor) {\n                const cursors = this.dependencies.selection.preserveSelection();\n                const ancestorContent = closestToggleAncestor.querySelector(contentSelector);\n                const containerContent = toggle.querySelector(contentSelector);\n                const siblings = childNodes(ancestorContent).filter(\n                    (node) =>\n                        toggle.compareDocumentPosition(node) & Node.DOCUMENT_POSITION_FOLLOWING\n                );\n                if (isEmptyBlock(containerContent.lastElementChild)) {\n                    containerContent.lastElementChild.remove();\n                }\n                containerContent.append(...siblings);\n                closestToggleAncestor.after(toggle);\n                this.forceToggle(toggle, { showContent: true, restoreSelection: cursors.restore });\n                this.dependencies.history.addStep();\n            }\n            return true;\n        }\n    }\n\n    /**\n     * This method handles the behavior when the user presses the Enter key.\n     * In the editor, when the user presses the Enter key, this splits the focused text block into 2\n     * separate ones. When the text block is split then it calls to all handlers so that they can trigger\n     * a specific behavior with the split block.\n     *\n     * This handler handles multiple cases:\n     *      1. The toggle title is currently empty (remove toggle)\n     *      2. Cursor is at the start of the toggle title (create new toggle before)\n     *      3. Cursor elsewhere in the toggle title (create new toggle after)\n     * @param {Object} param @see SplitPlugin.splitElementBlock\n     * @returns true if indeed handled by the method\n     */\n    handleSplitElementBlock({ targetNode, targetOffset, blockToSplit }) {\n        const { toggle, title, content } = this.getClosestToggleTitleInfo(targetNode);\n        if (title) {\n            const selection = this.dependencies.selection.getEditableSelection();\n            if (isEmptyBlock(selection.anchorNode)) {\n                const contentChildren = children(content);\n                if (contentChildren.length !== 1 || !isEmptyBlock(contentChildren[0])) {\n                    toggle.after(...children(content));\n                }\n                const baseContainer = this.dependencies.baseContainer.createBaseContainer();\n                baseContainer.appendChild(this.document.createElement(\"br\"));\n                toggle.replaceWith(baseContainer);\n                this.dependencies.selection.setCursorStart(baseContainer);\n                return true;\n            }\n            const insertBefore = targetOffset === 0 && blockToSplit.parentElement === title;\n            const [beforeSplit, afterSplit] = this.dependencies.split.splitElementBlock({\n                targetNode,\n                targetOffset,\n                blockToSplit,\n            });\n            if (beforeSplit && afterSplit) {\n                if (content.parentElement.matches(\".d-none\") || insertBefore) {\n                    const newToggle = this.renderToggleBlock();\n                    const newToggleBlock = newToggle.querySelector(toggleSelector);\n                    const newTitleEl = newToggle.querySelector(titleSelector);\n                    const dir = toggle.getAttribute(\"dir\");\n                    if (dir) {\n                        newToggleBlock.setAttribute(\"dir\", dir);\n                    }\n                    if (insertBefore) {\n                        toggle.before(newToggle);\n                        newTitleEl.replaceChildren(beforeSplit);\n                    } else {\n                        toggle.after(newToggle);\n                        newTitleEl.replaceChildren(afterSplit);\n                    }\n                } else {\n                    const firstChild = content.firstElementChild;\n                    if (isEmptyBlock(firstChild)) {\n                        firstChild.replaceWith(afterSplit);\n                    } else {\n                        content.prepend(afterSplit);\n                    }\n                }\n                this.dependencies.selection.setCursorStart(afterSplit);\n            }\n            return true;\n        }\n    }\n\n    /**\n     * Handles the tab behavior. This means that when we are inside a toggle title and we have a toggle\n     * as previous sibling of the embedded component, the current toggle is indented inside the content of\n     * the previous one.\n     */\n    handleTab() {\n        const toggle = this.getToggleFromTitleSelection();\n        if (toggle) {\n            const previousSibling = toggle.previousSibling;\n            if (previousSibling?.matches?.(toggleSelector)) {\n                const cursors = this.dependencies.selection.preserveSelection();\n                const previousSiblingContent = previousSibling.querySelector(contentSelector);\n                if (\n                    children(previousSiblingContent).length === 1 &&\n                    isEmptyBlock(previousSiblingContent.firstElementChild)\n                ) {\n                    previousSiblingContent.replaceChildren(toggle);\n                } else {\n                    previousSiblingContent.append(toggle);\n                }\n                const content = toggle.querySelector(contentSelector);\n                if (!content.parentElement.matches(\".d-none\")) {\n                    toggle.after(...children(content));\n                }\n                this.forceToggle(previousSibling, {\n                    showContent: true,\n                    restoreSelection: cursors.restore,\n                });\n                this.dependencies.history.addStep();\n            }\n            return true;\n        }\n    }\n\n    insertToggleBlock() {\n        const block = this.renderToggleBlock();\n        const target = block.querySelector(`${titleSelector} > ${baseContainerGlobalSelector}`);\n        this.dependencies.dom.insert(block);\n        this.dependencies.selection.setCursorStart(target);\n        this.dependencies.history.addStep();\n    }\n\n    manageToggleFromTitle() {\n        const toggle = this.getToggleFromTitleSelection();\n        if (!toggle) {\n            return;\n        }\n        this.forceToggle(toggle);\n    }\n\n    normalize(element) {\n        const cursors = this.dependencies.selection.preserveSelection();\n        let shouldRestoreCursor = false;\n        for (const titleChild of selectElements(\n            element,\n            `${toggleSelector} ${titleSelector} > *:first-child`\n        )) {\n            const title = titleChild.parentElement;\n            const toggle = closestElement(title, toggleSelector);\n            if (titleChild.nextElementSibling) {\n                const nodes = children(titleChild.parentElement);\n                title.replaceChildren(nodes.shift());\n                toggle.after(...nodes);\n                shouldRestoreCursor = true;\n            }\n            if (!isParagraphRelatedElement(titleChild)) {\n                toggle.after(titleChild);\n                shouldRestoreCursor = true;\n            }\n        }\n        if (shouldRestoreCursor) {\n            cursors.restore();\n        }\n        for (const emptyToggleNode of selectElements(\n            element,\n            `${toggleSelector} [data-embedded-editable]:empty`\n        )) {\n            const baseContainer = this.dependencies.baseContainer.createBaseContainer();\n            baseContainer.appendChild(this.document.createElement(\"br\"));\n            emptyToggleNode.replaceChildren(baseContainer);\n        }\n    }\n\n    renderToggleBlock() {\n        const baseContainer = this.dependencies.baseContainer.createBaseContainer();\n        return parseHTML(\n            this.document,\n            renderToString(\"html_editor.EmbeddedToggleBlockBlueprint\", {\n                baseContainerNodeName: baseContainer.nodeName,\n                baseContainerAttributes: {\n                    class: baseContainer.className,\n                },\n                embeddedProps: JSON.stringify({ toggleBlockId: this.getUniqueIdentifier() }),\n            })\n        );\n    }\n\n    showPowerButtons(selection) {\n        return (\n            selection.isCollapsed &&\n            !closestElement(selection.anchorNode, `${toggleSelector} ${titleSelector}`)\n        );\n    }\n}\n", "import { VideoPlugin } from \"@html_editor/main/media/video_plugin\";\nimport { EmbeddedVideoSelector } from \"./video_selector_dialog/embedded_video_selector\";\n\n/**\n * This plugin is meant to replace the Video plugin.\n */\nexport class EmbeddedVideoPlugin extends VideoPlugin {\n    static id = \"embeddedVideo\";\n    static dependencies = [\"embeddedComponents\", \"selection\", \"history\", \"overlay\", \"media\"];\n\n    // Extends the base class resources\n    /** @type {import(\"plugins\").EditorResources} */\n    resources = {\n        ...this.resources,\n        mount_component_handlers: this.extendEmbeddedVideoProps.bind(this),\n    };\n\n    /** @override */\n    get componentForMediaDialog() {\n        return EmbeddedVideoSelector;\n    }\n\n    /**\n     * @param {Object} props\n     */\n    extendEmbeddedVideoProps({ name, props }) {\n        if (name === \"video\") {\n            Object.assign(props, {\n                createOverlay: (Component, props = {}, options) =>\n                    this.dependencies.overlay.createOverlay(Component, props, options),\n                focusEditable: () => this.dependencies.selection.focusEditable(),\n                addStep: () => this.dependencies.history.addStep(),\n                openVideoSelectorDialog: (save, media) => {\n                    this.openVideoSelectorDialog(save, media);\n                },\n            });\n        }\n    }\n\n    /**\n     * Open media dialog allowing the user to insert a video\n     * @param {function} save\n     * @param {HTMLIFrameElement} iframe\n     */\n    openVideoSelectorDialog(save, iframe) {\n        this.dependencies.media.openMediaDialog({\n            node: iframe,\n            save: (elements, [media]) => {\n                if (media.src) {\n                    save(media);\n                }\n            },\n            visibleTabs: [\"VIDEOS\"],\n        });\n    }\n}\n", "import { YoutubePlugin } from \"@html_editor/main/youtube_plugin\";\nimport { EmbeddedVideoSelector } from \"./video_selector_dialog/embedded_video_selector\";\n\nexport class EmbeddedYoutubePlugin extends YoutubePlugin {\n    static id = \"embeddedYoutube\";\n    static dependencies = [...super.dependencies, \"embeddedComponents\"];\n\n    /** @override */\n    createVideoElement(videoData) {\n        const { video_id: videoId, platform, params } = videoData;\n        return EmbeddedVideoSelector.createElements([{ videoId, platform, params }])[0];\n    }\n}\n", "import { VideoSelector } from \"@html_editor/main/media/media_dialog/video_selector\";\nimport { renderToElement } from \"@web/core/utils/render\";\n\nexport class EmbeddedVideoSelector extends VideoSelector {\n    /** @override */\n    static createElements(selectedMedia) {\n        return selectedMedia.map((media) =>\n            renderToElement(\"html_editor.EmbeddedVideoBlueprint\", {\n                embeddedProps: JSON.stringify({\n                    videoId: media.videoId,\n                    platform: media.platform,\n                    params: media.params || {},\n                }),\n            })\n        );\n    }\n}\n", "import { nodeToTree } from \"@html_editor/core/history_plugin\";\nimport { Plugin } from \"@html_editor/plugin\";\nimport { withSequence } from \"@html_editor/utils/resource\";\nimport { memoize } from \"@web/core/utils/functions\";\nimport { renderToElement } from \"@web/core/utils/render\";\n\n/**\n * @typedef { Object } EmbeddedComponentShared\n * @property { EmbeddedComponentPlugin['renderBlueprintToElement'] } renderBlueprintToElement\n */\n\n/**\n * @typedef {((arg: { name, env, props }) => void)[]} mount_component_handlers\n * @typedef {(() => void)[]} post_mount_component_handlers\n */\n\n/**\n * This plugin is responsible with providing the API to manipulate/insert\n * sub components in an editor.\n */\nexport class EmbeddedComponentPlugin extends Plugin {\n    static id = \"embeddedComponents\";\n    static dependencies = [\"history\", \"protectedNode\", \"selection\"];\n    static shared = [\"renderBlueprintToElement\"];\n    /** @type {import(\"plugins\").EditorResources} */\n    resources = {\n        /** Handlers */\n        normalize_handlers: withSequence(0, this.normalize.bind(this)),\n        clean_for_save_handlers: ({ root }) => this.cleanForSave(root),\n        attribute_change_handlers: this.onChangeAttribute.bind(this),\n        restore_savepoint_handlers: () => this.handleComponents(this.editable),\n        history_reset_handlers: () => this.handleComponents(this.editable),\n        history_reset_from_steps_handlers: () => this.handleComponents(this.editable),\n        step_added_handlers: ({ stepCommonAncestor }) => this.handleComponents(stepCommonAncestor),\n        external_step_added_handlers: () => this.handleComponents(this.editable),\n\n        serializable_descendants_processors: this.processDescendantsToSerialize.bind(this),\n        attribute_change_processors: this.onChangeAttribute.bind(this),\n        savable_mutation_record_predicates: this.isMutationRecordSavable.bind(this),\n        move_node_whitelist_selectors: \"[data-embedded]\",\n    };\n\n    setup() {\n        this.components = new Set();\n        // map from node to component info\n        this.nodeMap = new WeakMap();\n        this.app = this.config.embeddedComponentInfo.app;\n        this.env = this.config.embeddedComponentInfo.env ?? {};\n        this.hostToStateChangeManagerMap = new WeakMap();\n        this.hostToOnComponentInsertedMap = new WeakMap();\n        this.embeddedComponents = memoize((embeddedComponents = []) => {\n            const result = {};\n            for (const embedding of embeddedComponents) {\n                // TODO ABD: Any embedding with the same name as another will overwrite it.\n                // File currently relies on this system. Change it ?\n                result[embedding.name] = embedding;\n            }\n            return result;\n        });\n        // First mount is done during history_reset_handlers which happens\n        // when start_edition_handlers are called.\n    }\n\n    isMutationRecordSavable(record) {\n        const info = this.nodeMap.get(record.target);\n        if (\n            info &&\n            record.type === \"attributes\" &&\n            record.attributeName === \"data-embedded-props\"\n        ) {\n            // This attribute is determined independently for each user\n            // through `data-embedded-state` attribute mutations.\n            return false;\n        }\n        return true;\n    }\n\n    /**\n     * @typedef {import(\"@html_editor/core/history_plugin\").Tree} Tree\n     *\n     * @param {Node} elem\n     * @param {Tree[]} serializableDescendants\n     * @returns {Tree[]}\n     */\n    processDescendantsToSerialize(elem, serializableDescendants) {\n        const embedding = this.getEmbedding(elem);\n        if (!embedding) {\n            return serializableDescendants;\n        }\n        return Object.values(embedding.getEditableDescendants?.(elem) || {}).map(nodeToTree);\n    }\n\n    handleComponents(elem) {\n        this.destroyRemovedComponents([...this.components]);\n        this.forEachEmbeddedComponentHost(elem, (host, embedding) => {\n            const info = this.nodeMap.get(host);\n            if (!info) {\n                this.mountComponent(host, embedding);\n            }\n        });\n    }\n\n    forEachEmbeddedComponentHost(elem, callback) {\n        const selector = `[data-embedded]`;\n        const targets = [...elem.querySelectorAll(selector)];\n        if (elem.matches(selector)) {\n            targets.unshift(elem);\n        }\n        for (const host of targets) {\n            const embedding = this.getEmbedding(host);\n            if (!embedding) {\n                continue;\n            }\n            callback(host, embedding);\n        }\n    }\n\n    getEmbedding(host) {\n        return this.embeddedComponents(this.getResource(\"embedded_components\"))[\n            host.dataset.embedded\n        ];\n    }\n\n    /**\n     * Apply an embedded state change received from `data-embedded-state`\n     * attribute. In some cases (undo/redo/revertStepsUntil history operations),\n     * the attribute has to be set to a new value, computed by the\n     * stateChangeManager.\n     *\n     * @param {Object} attributeChange @see HistoryPlugin\n     * @param { Object } options\n     * @param { boolean } options.forNewStep whether the mutation is being used\n     *        to create a new step\n     * @returns {string} new attribute value to set on the node, which might be\n     *        unchanged\n     */\n    onChangeAttribute(attributeChange, { forNewStep = false } = {}) {\n        const attributeValue = attributeChange.value;\n        let newAttributeValue;\n        if (attributeChange.attributeName === \"data-embedded-state\") {\n            const attrState = attributeChange.reverse\n                ? attributeChange.oldValue\n                : attributeChange.value;\n            const stateChangeManager = this.getStateChangeManager(attributeChange.target);\n            if (stateChangeManager) {\n                // onStateChanged returns undefined if no change is needed for\n                // the attribute value\n                newAttributeValue = stateChangeManager.onStateChanged(attrState, {\n                    reverse: attributeChange.reverse,\n                    forNewStep,\n                });\n            }\n        }\n        return newAttributeValue || attributeValue;\n    }\n\n    getStateChangeManager(host) {\n        const embedding = this.getEmbedding(host);\n        if (!(\"getStateChangeManager\" in embedding)) {\n            return null;\n        }\n        if (!this.hostToStateChangeManagerMap.has(host)) {\n            const config = {\n                host,\n                commitStateChanges: () => this.dependencies.history.addStep(),\n            };\n            const stateChangeManager = embedding.getStateChangeManager(config);\n            stateChangeManager.setup();\n            this.hostToStateChangeManagerMap.set(host, stateChangeManager);\n        }\n        return this.hostToStateChangeManagerMap.get(host);\n    }\n\n    mountComponent(\n        host,\n        { Component, getEditableDescendants, getProps, name, getStateChangeManager }\n    ) {\n        const props = getProps?.(host) || {};\n        const env = Object.create(this.env);\n        env.editorShared = {};\n        if (getStateChangeManager) {\n            env.getStateChangeManager = this.getStateChangeManager.bind(this);\n        }\n        if (getEditableDescendants) {\n            env.getEditableDescendants = getEditableDescendants;\n            // Enable the automatic selection restoration feature in @see useEditableDescendants\n            Object.assign(env.editorShared, {\n                selection: { ...this.dependencies.selection },\n            });\n        }\n        this.dispatchTo(\"mount_component_handlers\", { name, env, props });\n        const root = this.app.createRoot(Component, {\n            props,\n            env,\n        });\n        root.mount(host);\n        // Patch mount fiber to hook into the exact call stack where root is\n        // mounted (but before). This will remove host children synchronously\n        // just before adding the root rendered html.\n        const fiber = root.node.fiber;\n        const fiberComplete = fiber.complete;\n        fiber.complete = () => {\n            host.replaceChildren();\n            fiberComplete.call(fiber);\n            this.dispatchTo(\"post_mount_component_handlers\");\n        };\n        const onComponentInserted = this.extractOnComponentInserted(host);\n        if (onComponentInserted) {\n            // If a pending operation should be executed after the first mount\n            // of an inserted blueprint, add it as the last `onMounted` callback\n            root.node.mounted.push(onComponentInserted);\n        }\n        const info = {\n            root,\n            host,\n        };\n        this.components.add(info);\n        this.nodeMap.set(host, info);\n    }\n\n    destroyRemovedComponents(infos) {\n        // Avoid registering mutations if removed hosts are handled in\n        // the same microtask as when they were removed.\n        this.dependencies.history.ignoreDOMMutations(() => {\n            for (const info of infos) {\n                if (!this.editable.contains(info.host)) {\n                    const host = info.host;\n                    const display = host.style.display;\n                    const parentNode = host.parentNode;\n                    const clone = host.cloneNode(false);\n                    if (parentNode) {\n                        parentNode.replaceChild(clone, host);\n                    }\n                    host.style.display = \"none\";\n                    this.editable.after(host);\n                    this.destroyComponent(info);\n                    if (parentNode) {\n                        parentNode.replaceChild(host, clone);\n                    } else {\n                        host.remove();\n                    }\n                    host.style.display = display;\n                    if (!host.getAttribute(\"style\")) {\n                        host.removeAttribute(\"style\");\n                    }\n                }\n            }\n        });\n    }\n\n    deepDestroyComponent({ host }) {\n        const removed = [];\n        this.forEachEmbeddedComponentHost(host, (containedHost) => {\n            const info = this.nodeMap.get(containedHost);\n            if (info) {\n                if (this.editable.contains(containedHost)) {\n                    this.destroyComponent(info);\n                } else {\n                    removed.push(info);\n                }\n            }\n        });\n        this.destroyRemovedComponents(removed);\n    }\n\n    /**\n     * Should not be called directly as it will not handle recursivity and\n     * removed components @see deepDestroyComponent\n     */\n    destroyComponent({ root, host }) {\n        const { getEditableDescendants } = this.getEmbedding(host);\n        const editableDescendants = getEditableDescendants?.(host) || {};\n        root.destroy();\n        this.components.delete(arguments[0]);\n        this.nodeMap.delete(host);\n        host.append(...Object.values(editableDescendants));\n    }\n\n    destroy() {\n        super.destroy();\n        for (const info of [...this.components]) {\n            if (this.components.has(info)) {\n                this.deepDestroyComponent(info);\n            }\n        }\n    }\n\n    /**\n     * @param {String} template blueprint for the embedded Component\n     * @param {Object} [context] rendering context\n     * @param {Function} [onComponentInserted] function to be executed when\n     *        it is first mounted after it was inserted in the DOM. It will not\n     *        be executed if the blueprint is removed from the DOM before the\n     *        first mount nor if the component is mounted again afterwards.\n     * @returns {HTMLElement} host\n     */\n    renderBlueprintToElement(template, context = {}, onComponentInserted = undefined) {\n        const host = renderToElement(template, context);\n        if (onComponentInserted) {\n            this.hostToOnComponentInsertedMap.set(host, onComponentInserted);\n        }\n        return host;\n    }\n\n    extractOnComponentInserted(host) {\n        const onComponentInserted = this.hostToOnComponentInsertedMap.get(host);\n        this.hostToOnComponentInsertedMap.delete(host);\n        return onComponentInserted;\n    }\n\n    normalize(elem) {\n        this.forEachEmbeddedComponentHost(elem, (host, { getEditableDescendants }) => {\n            this.dependencies.protectedNode.setProtectingNode(host, true);\n            const editableDescendants = getEditableDescendants?.(host) || {};\n            for (const editableDescendant of Object.values(editableDescendants)) {\n                this.dependencies.protectedNode.setProtectingNode(editableDescendant, false);\n            }\n        });\n    }\n\n    cleanForSave(clone) {\n        this.forEachEmbeddedComponentHost(clone, (host, { getEditableDescendants }) => {\n            // In this case, host is a cloned element, there is no OWL root\n            // attached to it.\n            const editableDescendants = getEditableDescendants?.(host) || {};\n            host.replaceChildren();\n            for (const editableDescendant of Object.values(editableDescendants)) {\n                delete editableDescendant.dataset.oeProtected;\n                host.append(editableDescendant);\n            }\n            delete host.dataset.oeProtected;\n            delete host.dataset.embeddedState;\n        });\n    }\n}\n", "import { Component, useState } from \"@odoo/owl\";\n\nexport class QWebPicker extends Component {\n    static template = \"html_editor.QWebPicker\";\n    static props = [\"groups\", \"select\"];\n\n    setup() {\n        this.state = useState({ groups: this.props.groups });\n    }\n\n    onChange(ev) {\n        const [groupIndex, elementIndex] = ev.target.value.split(\",\");\n        this.props.select(this.state.groups[groupIndex][elementIndex].node);\n    }\n}\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { closestElement, selectElements } from \"@html_editor/utils/dom_traversal\";\nimport { leftPos, rightPos } from \"@html_editor/utils/position\";\nimport { QWebPicker } from \"./qweb_picker\";\nimport { isElement } from \"@html_editor/utils/dom_info\";\nimport { withSequence } from \"@html_editor/utils/resource\";\n\nconst isUnsplittableQWebElement = (node) =>\n    isElement(node) &&\n    (node.tagName === \"T\" ||\n        [\n            \"t-field\",\n            \"t-if\",\n            \"t-elif\",\n            \"t-else\",\n            \"t-foreach\",\n            \"t-value\",\n            \"t-esc\",\n            \"t-out\",\n            \"t-raw\",\n        ].some((attr) => node.getAttribute(attr)));\n\nconst PROTECTED_QWEB_SELECTOR = \"[t-esc], [t-raw], [t-out], [t-field]\";\nconst QWEB_DATA_ATTRIBUTES = [\n    \"data-oe-t-group\",\n    \"data-oe-t-inline\",\n    \"data-oe-t-selectable\",\n    \"data-oe-t-group-active\",\n];\nconst dataAttributesSelector = QWEB_DATA_ATTRIBUTES.map((attr) => `[${attr}]`).join(\", \");\n\nexport const isUnremovableQWebElement = (node) =>\n    node.getAttribute?.(\"t-set\") || node.getAttribute?.(\"t-call\");\n\nexport class QWebPlugin extends Plugin {\n    static id = \"qweb\";\n    static dependencies = [\"overlay\", \"protectedNode\", \"selection\"];\n    /** @type {import(\"plugins\").EditorResources} */\n    resources = {\n        /** Handlers */\n        selectionchange_handlers: withSequence(8, this.onSelectionChange.bind(this)),\n        clean_for_save_handlers: ({ root }) => {\n            this.clearDataAttributes(root);\n            for (const element of root.querySelectorAll(PROTECTED_QWEB_SELECTOR)) {\n                element.removeAttribute(\"contenteditable\");\n                delete element.dataset.oeProtected;\n            }\n        },\n        normalize_handlers: withSequence(0, this.normalize.bind(this)),\n\n        system_attributes: QWEB_DATA_ATTRIBUTES,\n        unremovable_node_predicates: isUnremovableQWebElement,\n        unsplittable_node_predicates: isUnsplittableQWebElement,\n        clipboard_content_processors: this.clearDataAttributes.bind(this),\n        legit_empty_link_predicates: (linkEl) =>\n            linkEl.getAttributeNames().some((name) => name.startsWith(\"t-\")),\n    };\n\n    setup() {\n        this.editable.classList.add(\"odoo-editor-qweb\");\n        this.picker = this.dependencies.overlay.createOverlay(QWebPicker, {\n            positionOptions: { position: \"top-start\" },\n        });\n        this.addDomListener(this.editable, \"click\", this.onClick);\n        this.groupIndex = 0;\n    }\n\n    isValidTargetForDomListener(ev) {\n        if (\n            ev.type === \"click\" &&\n            ev.target &&\n            closestElement(ev.target, PROTECTED_QWEB_SELECTOR)\n        ) {\n            // Allow clicking on a protected QWEB node to open the custom toolbar.\n            return true;\n        }\n        return super.isValidTargetForDomListener(ev);\n    }\n\n    /**\n     * @param { SelectionData } selectionData\n     */\n    onSelectionChange() {\n        if (this.picker.isOpen) {\n            this.picker.close();\n        }\n    }\n\n    normalize(root) {\n        this.normalizeInline(root);\n\n        for (const element of selectElements(root, PROTECTED_QWEB_SELECTOR)) {\n            this.dependencies.protectedNode.setProtectingNode(element, true);\n        }\n        this.applyGroupQwebBranching(root);\n    }\n\n    checkAllInline(el) {\n        return [...el.children].every((child) => {\n            if (child.tagName === \"T\") {\n                return this.checkAllInline(child);\n            } else {\n                return (\n                    child.nodeType !== Node.ELEMENT_NODE ||\n                    this.window.getComputedStyle(child).display === \"inline\"\n                );\n            }\n        });\n    }\n\n    normalizeInline(root) {\n        for (const el of selectElements(root, \"t\")) {\n            if (this.checkAllInline(el)) {\n                el.setAttribute(\"data-oe-t-inline\", \"true\");\n            }\n        }\n    }\n\n    getNodeGroups(node) {\n        const branchNode = node.closest(\"[data-oe-t-group]\");\n        if (!branchNode) {\n            return [];\n        }\n        const groupId = branchNode.getAttribute(\"data-oe-t-group\");\n        const group = [];\n        for (const node of branchNode.parentElement.querySelectorAll(\n            `[data-oe-t-group='${groupId}']`\n        )) {\n            let label = \"\";\n            if (node.hasAttribute(\"t-if\")) {\n                label = `if: ${node.getAttribute(\"t-if\")}`;\n            } else if (node.hasAttribute(\"t-elif\")) {\n                label = `elif: ${node.getAttribute(\"t-elif\")}`;\n            } else if (node.hasAttribute(\"t-else\")) {\n                label = \"else\";\n            }\n            group.push({\n                groupId,\n                node,\n                label,\n                isActive: node.getAttribute(\"data-oe-t-group-active\") === \"true\",\n            });\n        }\n        return this.getNodeGroups(branchNode.parentElement).concat([group]);\n    }\n\n    onClick(ev) {\n        if (this.picker.isOpen) {\n            this.picker.close();\n        }\n        if (ev.detail > 1) {\n            const selectionData = this.dependencies.selection.getSelectionData();\n            const selection = selectionData.documentSelection;\n            const qwebNode =\n                selection &&\n                selection.anchorNode &&\n                closestElement(selection.anchorNode, \"[t-field],[t-esc],[t-out]\");\n            if (qwebNode && this.editable.contains(qwebNode)) {\n                // select the whole qweb node\n                const [anchorNode, anchorOffset] = leftPos(qwebNode);\n                const [focusNode, focusOffset] = rightPos(qwebNode);\n                this.dependencies.selection.setSelection({\n                    anchorNode,\n                    anchorOffset,\n                    focusNode,\n                    focusOffset,\n                });\n            }\n        }\n        const targetNode = ev.target;\n        if (targetNode.closest(\"[data-oe-t-group]\")) {\n            this.selectNode(targetNode);\n        }\n    }\n\n    selectNode(node) {\n        const editableSelection = this.dependencies.selection.getSelectionData().editableSelection;\n        if (!editableSelection.isCollapsed) {\n            return;\n        }\n        this.selectedNode = node;\n        this.picker.open({\n            target: node,\n            props: {\n                groups: this.getNodeGroups(node),\n                select: this.select.bind(this),\n            },\n        });\n    }\n\n    applyGroupQwebBranching(root) {\n        const tNodes = selectElements(root, \"[t-if], [t-elif], [t-else]\");\n        const groupsEncounter = new Set();\n        for (const node of tNodes) {\n            const prevNode = node.previousElementSibling;\n\n            let groupId;\n            if (prevNode && !node.hasAttribute(\"t-if\")) {\n                // Make the first t-if selectable, if prevNode is not a t-if,\n                // it's already data-oe-t-selectable.\n                prevNode.setAttribute(\"data-oe-t-selectable\", \"true\");\n                groupId = parseInt(prevNode.getAttribute(\"data-oe-t-group\"));\n                node.setAttribute(\"data-oe-t-selectable\", \"true\");\n            } else {\n                groupId = this.groupIndex++;\n            }\n            groupsEncounter.add(groupId);\n            node.setAttribute(\"data-oe-t-group\", groupId);\n        }\n        for (const groupId of groupsEncounter) {\n            const isOneElementActive = root.querySelector(\n                `[data-oe-t-group='${groupId}'][data-oe-t-group-active]`\n            );\n            // If there is no element in groupId activated, activate the first\n            // one.\n            if (!isOneElementActive) {\n                const firstElementToActivate = selectElements(\n                    root,\n                    `[data-oe-t-group='${groupId}']`\n                ).next().value;\n                firstElementToActivate.setAttribute(\"data-oe-t-group-active\", \"true\");\n            }\n        }\n    }\n\n    select(node) {\n        const groupId = node.getAttribute(\"data-oe-t-group\");\n        const activeElement = node.parentElement.querySelector(\n            `[data-oe-t-group='${groupId}'][data-oe-t-group-active]`\n        );\n        if (activeElement === node) {\n            return;\n        }\n        activeElement.removeAttribute(\"data-oe-t-group-active\");\n        node.setAttribute(\"data-oe-t-group-active\", \"true\");\n        this.selectedNode = node;\n        this.picker.close();\n        this.selectNode(node);\n    }\n\n    clearDataAttributes(root) {\n        for (const node of root.querySelectorAll(dataAttributesSelector)) {\n            QWEB_DATA_ATTRIBUTES.forEach((attr) => node.removeAttribute(attr));\n        }\n    }\n}\n", "import { registry } from \"@web/core/registry\";\n\nexport const uploadLocalFileService = {\n    dependencies: [\"upload\", \"orm\"],\n    start(env, { upload: uploadService, orm }) {\n        const input = document.createElement(\"input\");\n        input.type = \"file\";\n\n        /**\n         * Open the system file selector and return the selected files.\n         *\n         * @param {Object} [options]\n         * @param {boolean} [options.multiple=true]\n         * @param {string} [options.accept]\n         * @returns {Promise<FileList>}\n         */\n        async function selectLocalFiles({ multiple, accept }) {\n            input.multiple = multiple;\n            input.accept = accept;\n            input.value = \"\"; // clear previously selected files\n\n            // Open system's file selector\n            input.click();\n\n            // Wait for user to select files or cancel.\n            await new Promise((resolve) => {\n                const resolveAndClear = () => {\n                    resolve();\n                    input.removeEventListener(\"change\", resolveAndClear);\n                    input.removeEventListener(\"cancel\", resolveAndClear);\n                };\n                // Detect file(s) selected\n                input.addEventListener(\"change\", resolveAndClear);\n                // Detect file selector closed without selecting files (cancel)\n                input.addEventListener(\"cancel\", resolveAndClear);\n            });\n            return input.files;\n        }\n\n        /**\n         * @param {FileList} files\n         * @param {Object} recordInfo\n         * @returns {Promise<Object[]>} attachments\n         */\n        async function filesToAttachments(files, { resModel, resId }) {\n            const attachments = [];\n            await uploadService.uploadFiles(files, { resModel, resId }, (attachment) => {\n                attachments.push(attachment);\n            });\n            return attachments;\n        }\n\n        /**\n         * Open the system file selector and upload the selected files.\n         *\n         * @param {Object} recordInfo\n         * @param {Object} [options]\n         * @param {string} [options.accept] Accepted file types\n         * @param {boolean} [options.multiple=false] Allow multiple files to be selected\n         * @param {boolean} [options.accessToken=false] Add access token to uploaded files\n         * @returns {Promise<Object[]>} attachments\n         */\n        async function upload(\n            { resId, resModel },\n            { accept = \"*/*\", multiple = false, accessToken = false } = {}\n        ) {\n            try {\n                const files = await selectLocalFiles({ multiple, accept });\n                const attachments = await filesToAttachments(files, { resModel, resId });\n                if (accessToken && attachments.length && !attachments[0].public) {\n                    await addAccessToken(attachments);\n                }\n                return attachments;\n            } catch {\n                // The upload service displays a either a notification or an\n                // error message in the progress toast.\n                return [];\n            }\n        }\n\n        /**\n         * @param {Object[]} attachments\n         * @returns {Promise<Object[]>}\n         */\n        async function addAccessToken(attachments) {\n            const accessTokens = await orm.call(\"ir.attachment\", \"generate_access_token\", [\n                attachments.map((a) => a.id),\n            ]);\n            attachments.forEach((attachment, index) => {\n                attachment.access_token = accessTokens[index];\n            });\n            return attachments;\n        }\n\n        /**\n         * @param {Object} attachments\n         * @param {Object} [options]\n         * @returns {string}\n         */\n        function getURL(attachment, { unique, download, accessToken } = {}) {\n            let url = `/web/content/${attachment.id}`;\n            const queryParams = [];\n            if (unique) {\n                queryParams.push(`unique=${encodeURIComponent(attachment.checksum)}`);\n            }\n            if (download) {\n                queryParams.push(\"download=true\");\n            }\n            if (accessToken && attachment.access_token) {\n                queryParams.push(`access_token=${attachment.access_token}`);\n            }\n            if (queryParams.length) {\n                url += `?${queryParams.join(\"&\")}`;\n            }\n            return url;\n        }\n\n        return { upload, addAccessToken, getURL };\n    },\n};\n\nregistry.category(\"services\").add(\"uploadLocalFiles\", uploadLocalFileService);\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { DynamicPlaceholderPopover } from \"@web/views/fields/dynamic_placeholder_popover\";\nimport { withSequence } from \"@html_editor/utils/resource\";\nimport { isHtmlContentSupported } from \"@html_editor/core/selection_plugin\";\n\n/**\n * @typedef {Object} DynamicPlaceholderShared\n * @property {DynamicPlaceholderPlugin['updateDphDefaultModel']} updateDphDefaultModel\n */\n\nexport class DynamicPlaceholderPlugin extends Plugin {\n    static id = \"dynamicPlaceholder\";\n    static dependencies = [\"overlay\", \"selection\", \"history\", \"dom\"];\n    static shared = [\"updateDphDefaultModel\"];\n    /** @type {import(\"plugins\").EditorResources} */\n    resources = {\n        user_commands: [\n            {\n                id: \"openDynamicPlaceholder\",\n                title: _t(\"Dynamic Placeholder\"),\n                description: _t(\"Insert a field\"),\n                icon: \"fa-hashtag\",\n                run: (params = {}) => this.open(params.resModel || this.defaultResModel),\n                isAvailable: isHtmlContentSupported,\n            },\n        ],\n        powerbox_categories: withSequence(60, {\n            id: \"marketing_tools\",\n            name: _t(\"Marketing Tools\"),\n        }),\n        powerbox_items: {\n            categoryId: \"marketing_tools\",\n            commandId: \"openDynamicPlaceholder\",\n        },\n        power_buttons: { commandId: \"openDynamicPlaceholder\" },\n    };\n    setup() {\n        this.defaultResModel = this.config.dynamicPlaceholderResModel;\n\n        /** @type {import(\"@html_editor/core/overlay_plugin\").Overlay} */\n        this.overlay = this.dependencies.overlay.createOverlay(DynamicPlaceholderPopover, {\n            hasAutofocus: true,\n            className: \"popover\",\n        });\n    }\n\n    /**\n     * @param {string} resModel\n     */\n    updateDphDefaultModel(resModel) {\n        this.defaultResModel = resModel;\n    }\n\n    /**\n     * @param {string} resModel\n     */\n    open(resModel) {\n        if (!resModel) {\n            return this.services.notification.add(\n                _t(\"You need to select a model before opening the dynamic placeholder selector.\"),\n                { type: \"danger\" }\n            );\n        }\n        this.overlay.open({\n            props: {\n                close: this.onClose.bind(this),\n                validate: this.onValidate.bind(this),\n                resModel: resModel,\n            },\n        });\n    }\n\n    /**\n     * @param {string} chain\n     * @param {string} defaultValue\n     * @param {string} fieldType\n     */\n    async onValidate(chain, defaultValue, fieldType) {\n        if (!chain) {\n            return;\n        }\n\n        const dynamicPlaceholder =\n            fieldType === \"datetime\"\n                ? await this._onValidateDatetime(chain, defaultValue)\n                : `object.${chain}`;\n\n        const t = document.createElement(\"T\");\n        t.setAttribute(\"t-out\", dynamicPlaceholder);\n        if (defaultValue?.length) {\n            t.innerText = defaultValue;\n        }\n\n        this.dependencies.dom.insert(t);\n        this.dependencies.history.addStep();\n    }\n\n    async _onValidateDatetime(chain, defaultValue) {\n        const partnerFields = await this.services.orm.call(\n            `${this.defaultResModel}`,\n            \"mail_get_partner_fields\",\n            [[]]\n        );\n\n        let dynamicPlaceholder = partnerFields.length\n            ? `format_datetime(object.${chain}, tz=object.${partnerFields[0]}.tz)`\n            : `format_datetime(object.${chain})`;\n\n        if (defaultValue) {\n            const safeDefaultValue = defaultValue.replace(/'/g, \"\\\\'\");\n            dynamicPlaceholder += ` or '${safeDefaultValue}'`;\n        }\n\n        return dynamicPlaceholder;\n    }\n\n    onClose() {\n        this.overlay.close();\n        this.dependencies.selection.focusEditable();\n    }\n}\n", "import { DynamicPlaceholderPlugin } from \"@html_editor/others/dynamic_placeholder_plugin\";\nimport { QWebPlugin } from \"@html_editor/others/qweb_plugin\";\n\nexport const DYNAMIC_PLACEHOLDER_PLUGINS = [DynamicPlaceholderPlugin, QWebPlugin];\n", "import { HtmlUpgradeManager } from \"@html_editor/html_migrations/html_upgrade_manager\";\nimport { stripVersion } from \"@html_editor/html_migrations/html_migrations_utils\";\nimport { stripHistoryIds } from \"@html_editor/others/collaboration/collaboration_odoo_plugin\";\nimport {\n    COLLABORATION_PLUGINS,\n    EMBEDDED_COMPONENT_PLUGINS,\n    MAIN_PLUGINS,\n    NO_EMBEDDED_COMPONENTS_FALLBACK_PLUGINS,\n} from \"@html_editor/plugin_sets\";\nimport { DYNAMIC_PLACEHOLDER_PLUGINS } from \"@html_editor/backend/plugin_sets\";\nimport {\n    MAIN_EMBEDDINGS,\n    READONLY_MAIN_EMBEDDINGS,\n} from \"@html_editor/others/embedded_components/embedding_sets\";\nimport { normalizeHTML } from \"@html_editor/utils/html\";\nimport { Wysiwyg } from \"@html_editor/wysiwyg\";\nimport { Component, markup, status, useRef, useState } from \"@odoo/owl\";\nimport { localization } from \"@web/core/l10n/localization\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { Mutex } from \"@web/core/utils/concurrency\";\nimport { useBus, useService } from \"@web/core/utils/hooks\";\nimport { useRecordObserver } from \"@web/model/relational_model/utils\";\nimport { standardFieldProps } from \"@web/views/fields/standard_field_props\";\nimport { TranslationButton } from \"@web/views/fields/translation_button\";\nimport { HtmlViewer } from \"@html_editor/components/html_viewer/html_viewer\";\nimport { EditorVersionPlugin } from \"@html_editor/core/editor_version_plugin\";\nimport { withSequence } from \"@html_editor/utils/resource\";\nimport { fixInvalidHTML, instanceofMarkup } from \"@html_editor/utils/sanitize\";\nimport { isHtmlContentSupported } from \"@html_editor/core/selection_plugin\";\n\nconst HTML_FIELD_METADATA_ATTRIBUTES = [\"data-last-history-steps\"];\n\n/**\n * Check whether the current value contains nodes that would break\n * on insertion inside an existing body.\n *\n * @returns {boolean} true if 'this.props.value' contains a node\n * that can only exist once per document.\n */\nfunction computeContainsComplexHTML(value) {\n    const domParser = new DOMParser();\n    if (!value) {\n        return false;\n    }\n    const parsedOriginal = domParser.parseFromString(value, \"text/html\");\n    return !!parsedOriginal.head.innerHTML.trim();\n}\n\nexport class HtmlField extends Component {\n    static template = \"html_editor.HtmlField\";\n    static props = {\n        ...standardFieldProps,\n        isCollaborative: { type: Boolean, optional: true },\n        collaborativeTrigger: { type: String, optional: true },\n        dynamicPlaceholder: { type: Boolean, optional: true, default: false },\n        dynamicPlaceholderModelReferenceField: { type: String, optional: true },\n        migrateHTML: { type: Boolean, optional: true },\n        cssReadonlyAssetId: { type: String, optional: true },\n        sandboxedPreview: { type: Boolean, optional: true },\n        codeview: { type: Boolean, optional: true },\n        editorConfig: { type: Object, optional: true },\n        embeddedComponents: { type: Boolean, optional: true },\n    };\n    static defaultProps = {\n        dynamicPlaceholder: false,\n    };\n    static components = {\n        Wysiwyg,\n        HtmlViewer,\n        TranslationButton,\n    };\n\n    setup() {\n        this.htmlUpgradeManager = new HtmlUpgradeManager();\n        this.mutex = new Mutex();\n\n        this.codeViewRef = useRef(\"codeView\");\n\n        const { model } = this.props.record;\n        useBus(model.bus, \"WILL_SAVE_URGENTLY\", () => this.commitChanges({ urgent: true }));\n        useBus(model.bus, \"NEED_LOCAL_CHANGES\", ({ detail }) =>\n            detail.proms.push(this.commitChanges())\n        );\n        this.busService = this.env.services.bus_service;\n        this.ormService = useService(\"orm\");\n\n        this.isDirty = false;\n        this.state = useState({\n            key: 0,\n            showCodeView: false,\n            containsComplexHTML: computeContainsComplexHTML(\n                this.props.record.data[this.props.name]\n            ),\n        });\n\n        useRecordObserver((record) => {\n            // Reset Wysiwyg when we discard or onchange value\n            const newValue = fixInvalidHTML(record.data[this.props.name]);\n            if (!this.isDirty) {\n                const value = normalizeHTML(newValue, this.clearElementToCompare.bind(this));\n                if (this.lastValue !== value) {\n                    this.state.key++;\n                    this.state.containsComplexHTML = computeContainsComplexHTML(newValue);\n                    this.lastValue = value;\n                }\n            }\n        });\n        useRecordObserver((record) => {\n            const value = record.data[this.props.dynamicPlaceholderModelReferenceField || \"model\"];\n            // update Dynamic Placeholder reference model\n            if (this.props.dynamicPlaceholder && this.editor) {\n                this.editor.shared.dynamicPlaceholder?.updateDphDefaultModel(value);\n            }\n        });\n    }\n\n    get value() {\n        const value = this.props.record.data[this.props.name] || \"\";\n        let newVal = fixInvalidHTML(value);\n        if (this.props.migrateHTML) {\n            newVal = this.htmlUpgradeManager.processForUpgrade(newVal, {\n                containsComplexHTML: this.state.containsComplexHTML,\n                env: this.env,\n            });\n        }\n        if (instanceofMarkup(value)) {\n            return markup(newVal);\n        }\n        return newVal;\n    }\n\n    get displayReadonly() {\n        return this.props.readonly || (this.sandboxedPreview && !this.state.showCodeView);\n    }\n\n    get wysiwygKey() {\n        return `${this.props.record.resId}_${this.state.key}`;\n    }\n\n    get sandboxedPreview() {\n        // @todo @phoenix maybe remove containsComplexHTML and alway use sandboxedPreview options\n        return this.props.sandboxedPreview || this.state.containsComplexHTML;\n    }\n\n    get isTranslatable() {\n        return this.props.record.fields[this.props.name].translate;\n    }\n\n    clearElementToCompare(element) {\n        if (this.props.isCollaborative) {\n            stripHistoryIds(element);\n        }\n        stripVersion(element);\n    }\n\n    async updateValue(value) {\n        this.lastValue = normalizeHTML(value, this.clearElementToCompare.bind(this));\n        this.isDirty = false;\n        await this.props.record.update({ [this.props.name]: value }).catch(() => {\n            this.isDirty = true;\n        });\n        this.props.record.model.bus.trigger(\"FIELD_IS_DIRTY\", this.isDirty);\n    }\n\n    async getEditorContent() {\n        const content = this.editor.getElContent();\n        const oldSrcToNewSrcMap = await this.editor.shared.imageSave?.savePendingImages(content);\n        // Update the actual editable if still in the DOM.\n        if (this.editor.editable && oldSrcToNewSrcMap) {\n            this.editor.editable\n                .querySelectorAll(\".o_b64_image_to_save, .o_modified_image_to_save\")\n                .forEach((unsavedImage) => {\n                    const oldSrc = unsavedImage.getAttribute(\"src\");\n                    if (oldSrcToNewSrcMap.has(oldSrc)) {\n                        unsavedImage.setAttribute(\"src\", oldSrcToNewSrcMap.get(oldSrc));\n                    }\n                    unsavedImage.classList.remove(\n                        \"o_b64_image_to_save\",\n                        \"o_modified_image_to_save\"\n                    );\n                });\n        }\n        return content;\n    }\n\n    async _commitChanges({ urgent }) {\n        if (status(this) === \"destroyed\") {\n            return;\n        }\n        if (this.isDirty) {\n            if (this.state.showCodeView) {\n                await this.updateValue(this.codeViewRef.el.value);\n                return;\n            }\n            if (urgent) {\n                await this.updateValue(this.editor.getContent());\n            }\n            const el = await this.getEditorContent();\n            const content = el.innerHTML;\n            this.clearElementToCompare(el);\n            const comparisonValue = el.innerHTML;\n            if (!urgent || (urgent && this.lastValue !== comparisonValue)) {\n                await this.updateValue(content);\n            }\n        }\n    }\n\n    async commitChanges({ urgent } = {}) {\n        if (urgent) {\n            return this._commitChanges({ urgent });\n        } else {\n            return this.mutex.exec(() => this._commitChanges({ urgent }));\n        }\n    }\n\n    onEditorLoad(editor) {\n        this.editor = editor;\n    }\n\n    onChange() {\n        this.isDirty = true;\n        this.props.record.model.bus.trigger(\"FIELD_IS_DIRTY\", true);\n    }\n\n    onBlur() {\n        return this.commitChanges();\n    }\n\n    async toggleCodeView() {\n        await this.commitChanges();\n        this.state.showCodeView = !this.state.showCodeView;\n        if (!this.state.showCodeView && this.editor) {\n            this.editor.editable.innerHTML = this.value;\n            this.editor.shared.history.addStep();\n        }\n    }\n\n    getConfig() {\n        const config = {\n            content: this.value,\n            Plugins: [\n                ...(this.props.migrateHTML ? [EditorVersionPlugin] : []),\n                ...MAIN_PLUGINS,\n                ...(this.props.isCollaborative ? COLLABORATION_PLUGINS : []),\n                ...(this.props.dynamicPlaceholder ? DYNAMIC_PLACEHOLDER_PLUGINS : []),\n                ...(this.props.embeddedComponents\n                    ? EMBEDDED_COMPONENT_PLUGINS\n                    : NO_EMBEDDED_COMPONENTS_FALLBACK_PLUGINS),\n            ],\n            classList: this.classList,\n            onChange: this.onChange.bind(this),\n            collaboration: this.props.isCollaborative && {\n                busService: this.busService,\n                ormService: this.ormService,\n                collaborativeTrigger: this.props.collaborativeTrigger,\n                collaborationChannel: {\n                    collaborationModelName: this.props.record.resModel,\n                    collaborationFieldName: this.props.name,\n                    collaborationResId: parseInt(this.props.record.resId),\n                },\n                peerId: this.generateId(),\n            },\n            dropImageAsAttachment: true, // @todo @phoenix always true ?\n            dynamicPlaceholder: this.props.dynamicPlaceholder,\n            dynamicPlaceholderResModel:\n                this.props.record.data[this.props.dynamicPlaceholderModelReferenceField || \"model\"],\n            direction: localization.direction || \"ltr\",\n            getRecordInfo: () => {\n                const { resModel, resId, data, fields, id } = this.props.record;\n                return { resModel, resId, data, fields, id };\n            },\n            resources: {},\n            ...this.props.editorConfig,\n        };\n\n        if (!(\"baseContainers\" in config)) {\n            config.baseContainers = [\"DIV\", \"P\"];\n        }\n\n        if (this.props.embeddedComponents) {\n            config.resources.embedded_components = [...MAIN_EMBEDDINGS];\n            config.embeddedComponentInfo = { app: this.__owl__.app, env: this.env };\n        }\n\n        const { sanitize_tags, sanitize } = this.props.record.fields[this.props.name];\n        if (\n            !(\"allowVideo\" in config) &&\n            !this.props.embeddedComponents &&\n            (sanitize_tags || (sanitize_tags === undefined && sanitize))\n        ) {\n            config.allowVideo = false; // Tag-sanitized fields remove videos.\n        }\n        if (this.props.codeview) {\n            config.resources = {\n                ...config.resources,\n                user_commands: [\n                    {\n                        id: \"codeview\",\n                        description: _t(\"Code view\"),\n                        icon: \"fa-code\",\n                        run: this.toggleCodeView.bind(this),\n                        isAvailable: isHtmlContentSupported,\n                    },\n                ],\n                toolbar_groups: withSequence(100, {\n                    id: \"codeview\",\n                }),\n                toolbar_items: {\n                    id: \"codeview\",\n                    groupId: \"codeview\",\n                    commandId: \"codeview\",\n                },\n            };\n        }\n        return config;\n    }\n\n    getReadonlyConfig() {\n        const config = {\n            value: this.value,\n            cssAssetId: this.props.cssReadonlyAssetId,\n            hasFullHtml: this.sandboxedPreview,\n        };\n        if (this.props.embeddedComponents) {\n            config.embeddedComponents = [...READONLY_MAIN_EMBEDDINGS];\n        }\n        return config;\n    }\n\n    generateId() {\n        // No need for secure random number.\n        return Math.floor(Math.random() * Math.pow(2, 52)).toString();\n    }\n}\n\nexport const htmlField = {\n    component: HtmlField,\n    displayName: _t(\"Html\"),\n    supportedTypes: [\"html\"],\n    extractProps({ attrs, options }, dynamicInfo) {\n        const editorConfig = {\n            mediaModalParams: {\n                useMediaLibrary: true,\n            },\n        };\n        if (attrs.placeholder) {\n            editorConfig.placeholder = attrs.placeholder;\n        }\n        if (options.height) {\n            editorConfig.height = `${options.height}px`;\n            editorConfig.classList = [\"overflow-auto\"];\n        }\n        if (\"allowImage\" in options) {\n            editorConfig.allowImage = Boolean(options.allowImage);\n        }\n        if (\"allowMediaDocuments\" in options) {\n            editorConfig.allowMediaDocuments = Boolean(options.allowMediaDocuments);\n        }\n        if (\"allowVideo\" in options) {\n            editorConfig.allowVideo = Boolean(options.allowVideo);\n        }\n        if (\"allowFile\" in options) {\n            editorConfig.allowFile = Boolean(options.allowFile);\n        }\n        if (\"allowChecklist\" in options) {\n            editorConfig.allowChecklist = Boolean(options.allowChecklist);\n        }\n        if (\"allowAttachmentCreation\" in options) {\n            editorConfig.allowImage = Boolean(options.allowAttachmentCreation);\n            editorConfig.allowFile = Boolean(options.allowAttachmentCreation);\n        }\n        if (\"baseContainers\" in options) {\n            editorConfig.baseContainers = options.baseContainers;\n        }\n        if (\"cleanEmptyStructuralContainers\" in options) {\n            editorConfig.cleanEmptyStructuralContainers = Boolean(\n                options.cleanEmptyStructuralContainers\n            );\n        }\n        return {\n            editorConfig,\n            isCollaborative: options.collaborative,\n            collaborativeTrigger: options.collaborative_trigger,\n            migrateHTML: \"migrateHTML\" in options ? Boolean(options.migrateHTML) : true,\n            dynamicPlaceholder: options.dynamic_placeholder,\n            dynamicPlaceholderModelReferenceField:\n                options.dynamic_placeholder_model_reference_field,\n            embeddedComponents:\n                \"embedded_components\" in options ? Boolean(options.embedded_components) : true,\n            sandboxedPreview: Boolean(options.sandboxedPreview),\n            cssReadonlyAssetId: options.cssReadonly,\n            codeview: Boolean(odoo.debug && options.codeview),\n        };\n    },\n};\n\nregistry.category(\"fields\").add(\"html\", htmlField, { force: true });\n\nexport function getHtmlFieldMetadata(content) {\n    const metadata = {};\n    for (const attribute of HTML_FIELD_METADATA_ATTRIBUTES) {\n        const regex = new RegExp(`${attribute}\\\\s*=\\\\s*\"([^\"]+)\"`);\n        metadata[attribute] = content.match(regex)?.[1];\n    }\n    return metadata;\n}\nexport function setHtmlFieldMetadata(content, metadata) {\n    const htmlContent = content.toString() || \"<div></div>\";\n    const parser = new DOMParser();\n    const contentDocument = parser.parseFromString(htmlContent, \"text/html\");\n    for (const [attribute, value] of Object.entries(metadata)) {\n        if (value) {\n            contentDocument.body.firstChild.setAttribute(attribute, value);\n        }\n    }\n    return contentDocument.body.innerHTML;\n}\n", "import { MAIN_PLUGINS } from \"@html_editor/plugin_sets\";\nimport { HtmlViewer } from \"@html_editor/components/html_viewer/html_viewer\";\nimport { EditorVersionPlugin } from \"@html_editor/core/editor_version_plugin\";\nimport { localization } from \"@web/core/l10n/localization\";\nimport { patch } from \"@web/core/utils/patch\";\nimport { PropertyValue } from \"@web/views/fields/properties/property_value\";\nimport { HtmlUpgradeManager } from \"@html_editor/html_migrations/html_upgrade_manager\";\nimport { normalizeHTML } from \"@html_editor/utils/html\";\nimport { Wysiwyg } from \"@html_editor/wysiwyg\";\nimport { user } from \"@web/core/user\";\nimport { useState, onWillStart, onWillUpdateProps } from \"@odoo/owl\";\n\npatch(PropertyValue.prototype, {\n    setup() {\n        this.htmlUpgradeManager = new HtmlUpgradeManager();\n        this.lastHtmlValue = this.propertyValue?.toString();\n        onWillStart(async () => {\n            this.htmlState.isPortalUser = await user.hasGroup(\"base.group_portal\");\n        });\n        this.htmlState = useState({ isPortalUser: false, key: 0 });\n\n        onWillUpdateProps((newProps) => {\n            const newValueStr = newProps.value?.toString();\n            if (newProps.type === \"html\" && newValueStr !== this.lastHtmlValue) {\n                this.htmlState.key += 1;\n                this.lastHtmlValue = newValueStr;\n            }\n        });\n\n        return super.setup();\n    },\n\n    get propertyValue() {\n        const value = super.propertyValue;\n        return this.props.type === \"html\"\n            ? this.htmlUpgradeManager.processForUpgrade(value || \"\")\n            : value;\n    },\n\n    onEditorLoad(editor) {\n        this.editor = editor;\n    },\n\n    async onEditorBlur() {\n        const value = this.editor.getContent();\n        if (normalizeHTML(value) !== normalizeHTML(this.lastHtmlValue)) {\n            this.onValueChange(value);\n            this.lastHtmlValue = value;\n        }\n    },\n\n    onWysiwygChange() {\n        if (!this.editor.editable.contains(document.activeElement)) {\n            // The DOM of the Wysiwyg have been changed, while the user is not editing\n            // (eg the chatgpt widget), mark the field as dirty\n            this.props.record.model.bus.trigger(\"FIELD_IS_DIRTY\", true);\n            this.onEditorBlur();\n        }\n    },\n\n    getConfig() {\n        let plugins = [...MAIN_PLUGINS, EditorVersionPlugin];\n        if (this.htmlState.isPortalUser) {\n            const toRemove = [\"file\", \"media\"];\n            plugins = plugins.filter(\n                (plugin) =>\n                    !toRemove.some((p) => plugin.id === p || plugin.dependencies.includes(p))\n            );\n        }\n\n        return {\n            content: this.propertyValue,\n            debug: !!this.env.debug,\n            direction: localization.direction || \"ltr\",\n            onChange: this.onWysiwygChange.bind(this),\n            placeholder: this.props.placeholder,\n            Plugins: plugins,\n            dropImageAsAttachment: true,\n            allowVideo: false,\n            getRecordInfo: () => {\n                const { resModel, resId, data, fields, id } = this.props.record;\n                return { resModel, resId, data, fields, id };\n            },\n        };\n    },\n\n    getReadonlyConfig() {\n        return {\n            value: this.propertyValue,\n            hasFullHtml: false,\n            cssAssetId: \"web.assets_frontend\",\n        };\n    },\n});\n\nPropertyValue.components = { ...PropertyValue.components, HtmlViewer, Wysiwyg };\n", "import { MediaDialog } from \"@html_editor/main/media/media_dialog/media_dialog\";\nimport { VideoSelector } from \"@html_editor/main/media/media_dialog/video_selector\";\nimport { _t } from \"@web/core/l10n/translation\";\n\nexport class CustomMediaDialog extends MediaDialog {\n    static defaultProps = {\n        ...MediaDialog.defaultProps,\n        extraTabs: [{ id: \"VIDEOS\", title: _t(\"Videos\"), Component: VideoSelector }],\n    };\n    async save() {\n        if (this.errorMessages[this.state?.activeTab]) {\n            this.notificationService.add(this.errorMessages[this.state.activeTab], {\n                type: \"danger\",\n            });\n            return;\n        }\n        if (this.state.activeTab == \"IMAGES\") {\n            const attachments = this.selectedMedia[this.state.activeTab];\n            const preloadedAttachments = attachments.filter((attachment) => attachment.res_model);\n            this.selectedMedia[this.state.activeTab] = attachments.filter(\n                (attachment) => !preloadedAttachments.includes(attachment)\n            );\n            if (this.selectedMedia[this.state.activeTab].length > 0) {\n                await super.save();\n                const newAttachments = this.selectedMedia[this.state.activeTab];\n                this.props.imageSave(newAttachments);\n            }\n            if (preloadedAttachments.length) {\n                this.props.imageSave(preloadedAttachments);\n            }\n        } else {\n            this.props.videoSave(this.selectedMedia[this.state.activeTab]);\n        }\n        this.props.close();\n    }\n}\n", "import { registry } from \"@web/core/registry\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { ImageField, imageField } from \"@web/views/fields/image/image_field\";\nimport { CustomMediaDialog } from \"./custom_media_dialog\";\nimport { getVideoUrl } from \"@html_editor/utils/url\";\n\nexport class X2ManyImageField extends ImageField {\n    static template = \"html_editor.ImageField\";\n    setup() {\n        super.setup();\n        this.orm = useService(\"orm\");\n        this.dialog = useService(\"dialog\");\n    }\n\n    /**\n     * New method and a new edit button is introduced here to overwrite,\n     * standard behavior of opening file input box in order to update a record.\n     */\n    onFileEdit(ev) {\n        const isVideo = this.props.record.data.video_url;\n        let mediaEl;\n        if (isVideo) {\n            mediaEl = document.createElement(\"img\");\n            mediaEl.dataset.src = this.props.record.data.video_url;\n        }\n        this.dialog.add(CustomMediaDialog, {\n            visibleTabs: [\"IMAGES\", \"VIDEOS\"],\n            media: mediaEl,\n            activeTab: isVideo ? \"VIDEOS\" : \"IMAGES\",\n            save: (el) => {}, // Simple rebound to fake its execution\n            imageSave: this.onImageSave.bind(this),\n            videoSave: this.onVideoSave.bind(this),\n        });\n    }\n\n    async onImageSave(attachment) {\n        const attachmentRecord = await this.orm.searchRead(\n            \"ir.attachment\",\n            [[\"id\", \"=\", attachment[0].id]],\n            [\"id\", \"datas\", \"name\"],\n            {}\n        );\n        if (!attachmentRecord[0].datas) {\n            // URL type attachments are mostly demo records which don't have any ir.attachment datas\n            // TODO: make it work with URL type attachments\n            return this.notification.add(\n                `Cannot add URL type attachment \"${attachmentRecord[0].name}\". Please try to reupload this image.`,\n                {\n                    type: \"warning\",\n                }\n            );\n        }\n        await this.props.record.update({\n            [this.props.name]: attachmentRecord[0].datas,\n            name: attachmentRecord[0].name,\n        });\n    }\n\n    async onVideoSave(videoInfo) {\n        const url = getVideoUrl(videoInfo[0].platform, videoInfo[0].videoId, videoInfo[0].params);\n        await this.props.record.update({\n            video_url: url.href,\n            name: videoInfo[0].platform + \" - [Video]\",\n        });\n    }\n\n    onFileRemove() {\n        const parentRecord = this.props.record._parentRecord.data;\n        parentRecord[this.env.parentField].delete(this.props.record);\n    }\n}\n\nexport const x2ManyImageField = {\n    ...imageField,\n    component: X2ManyImageField,\n};\n\nregistry.category(\"fields\").add(\"x2_many_image\", x2ManyImageField);\n", "import { registry } from \"@web/core/registry\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { X2ManyField, x2ManyField } from \"@web/views/fields/x2many/x2many_field\";\nimport { getVideoUrl } from \"@html_editor/utils/url\";\nimport { useChildSubEnv } from \"@odoo/owl\";\nimport { CustomMediaDialog } from \"./custom_media_dialog\";\n\nexport class X2ManyMediaViewer extends X2ManyField {\n    static template = \"html_editor.X2ManyMediaViewer\";\n    static props = {\n        ...X2ManyField.props,\n        convertToWebp: { type: Boolean, optional: true },\n    };\n\n    setup() {\n        super.setup();\n        this.dialogs = useService(\"dialog\");\n        this.orm = useService(\"orm\");\n        this.notification = useService(\"notification\");\n        this.supportedFields = [\"image_1920\", \"image_1024\", \"image_512\", \"image_256\", \"image_128\"];\n        useChildSubEnv({\n            parentField: this.props.name,\n        });\n    }\n\n    addMedia() {\n        this.dialogs.add(CustomMediaDialog, {\n            save: (el) => {}, // Simple rebound to fake its execution\n            multiImages: true,\n            visibleTabs: [\"IMAGES\", \"VIDEOS\"],\n            imageSave: this.onImageSave.bind(this),\n            videoSave: this.onVideoSave.bind(this),\n        });\n    }\n\n    onVideoSave(videoInfo) {\n        const url = getVideoUrl(videoInfo[0].platform, videoInfo[0].videoId, videoInfo[0].params);\n        const videoList = this.props.record.data[this.props.name];\n        videoList.addNewRecord({ position: \"bottom\" }).then((record) => {\n            record.update({ name: videoInfo[0].platform + \" - [Video]\", video_url: url.href });\n        });\n    }\n\n    async onImageSave(attachments) {\n        const attachmentIds = attachments.map((attachment) => attachment.id);\n        const attachmentRecords = await this.orm.searchRead(\n            \"ir.attachment\",\n            [[\"id\", \"in\", attachmentIds]],\n            [\"id\", \"datas\", \"name\", \"mimetype\"],\n            {}\n        );\n        for (const attachment of attachmentRecords) {\n            const imageList = this.props.record.data[this.props.name];\n            if (!attachment.datas) {\n                // URL type attachments are mostly demo records which don't have any ir.attachment datas\n                // TODO: make it work with URL type attachments\n                return this.notification.add(\n                    `Cannot add URL type attachment \"${attachment.name}\". Please try to reupload this image.`,\n                    {\n                        type: \"warning\",\n                    }\n                );\n            }\n            if (\n                this.props.convertToWebp &&\n                ![\"image/gif\", \"image/svg+xml\"].includes(attachment.mimetype)\n            ) {\n                // This method is widely adapted from onFileUploaded in ImageField.\n                // Upon change, make sure to verify whether the same change needs\n                // to be applied on both sides.\n                // Generate alternate sizes and format for reports.\n                const image = document.createElement(\"img\");\n                image.src = `data:${attachment.mimetype};base64,${attachment.datas}`;\n                await new Promise((resolve) => image.addEventListener(\"load\", resolve));\n\n                const originalSize = Math.max(image.width, image.height);\n                const smallerSizes = [1024, 512, 256, 128].filter((size) => size < originalSize);\n                let referenceId = undefined;\n\n                for (const size of [originalSize, ...smallerSizes]) {\n                    const ratio = size / originalSize;\n                    const canvas = document.createElement(\"canvas\");\n                    canvas.width = image.width * ratio;\n                    canvas.height = image.height * ratio;\n                    const ctx = canvas.getContext(\"2d\");\n                    ctx.drawImage(\n                        image,\n                        0,\n                        0,\n                        image.width,\n                        image.height,\n                        0,\n                        0,\n                        canvas.width,\n                        canvas.height\n                    );\n\n                    // WebP format\n                    const webpData = canvas.toDataURL(\"image/webp\").split(\",\")[1];\n                    const [resizedId] = await this.orm.call(\"ir.attachment\", \"create_unique\", [\n                        [\n                            {\n                                name: attachment.name.replace(/\\.[^/.]+$/, \".webp\"),\n                                description: size === originalSize ? \"\" : `resize: ${size}`,\n                                datas: webpData,\n                                res_id: referenceId,\n                                res_model: \"ir.attachment\",\n                                mimetype: \"image/webp\",\n                            },\n                        ],\n                    ]);\n\n                    referenceId = referenceId || resizedId;\n\n                    // JPEG format for compatibility\n                    const jpegData = canvas.toDataURL(\"image/jpeg\").split(\",\")[1];\n                    await this.orm.call(\"ir.attachment\", \"create_unique\", [\n                        [\n                            {\n                                name: attachment.name.replace(/\\.[^/.]+$/, \".jpg\"),\n                                description: `resize: ${size} - format: jpeg`,\n                                datas: jpegData,\n                                res_id: resizedId,\n                                res_model: \"ir.attachment\",\n                                mimetype: \"image/jpeg\",\n                            },\n                        ],\n                    ]);\n                }\n                const canvas = document.createElement(\"canvas\");\n                canvas.width = image.width;\n                canvas.height = image.height;\n                const ctx = canvas.getContext(\"2d\");\n                ctx.drawImage(image, 0, 0, image.width, image.height);\n\n                const webpData = canvas.toDataURL(\"image/webp\").split(\",\")[1];\n                attachment.datas = webpData;\n                attachment.mimetype = \"image/webp\";\n                attachment.name = attachment.name.replace(/\\.[^/.]+$/, \".webp\");\n            }\n\n            imageList.addNewRecord({ position: \"bottom\" }).then((record) => {\n                const activeFields = imageList.activeFields;\n                const updateData = {};\n                for (const field in activeFields) {\n                    if (attachment.datas && this.supportedFields.includes(field)) {\n                        updateData[field] = attachment.datas;\n                        updateData[\"name\"] = attachment.name;\n                    }\n                }\n                record.update(updateData);\n            });\n        }\n    }\n\n    async onAdd({ context, editable } = {}) {\n        this.addMedia();\n    }\n}\n\nexport const x2ManyMediaViewer = {\n    ...x2ManyField,\n    component: X2ManyMediaViewer,\n    extractProps: (\n        { attrs, relatedFields, viewMode, views, widget, options, string },\n        dynamicInfo\n    ) => {\n        const x2ManyFieldProps = x2ManyField.extractProps(\n            { attrs, relatedFields, viewMode, views, widget, options, string },\n            dynamicInfo\n        );\n        return {\n            ...x2ManyFieldProps,\n            convertToWebp: options.convert_to_webp,\n        };\n    },\n};\n\nregistry.category(\"fields\").add(\"x2_many_media_viewer\", x2ManyMediaViewer);\n", "/**\r\n* vkBeautify - javascript plugin to pretty-print or minify text in XML, JSON, CSS and SQL formats.\r\n*  \r\n* Version - 0.99.00.beta \r\n* Copyright (c) 2012 Vadim Kiryukhin\r\n* vkiryukhin @ gmail.com\r\n* http://www.eslinstructor.net/vkbeautify/\r\n* \r\n* Dual licensed under the MIT and GPL licenses:\r\n*   http://www.opensource.org/licenses/mit-license.php\r\n*   http://www.gnu.org/licenses/gpl.html\r\n*\r\n*   Pretty print\r\n*\r\n*        vkbeautify.xml(text [,indent_pattern]);\r\n*        vkbeautify.json(text [,indent_pattern]);\r\n*        vkbeautify.css(text [,indent_pattern]);\r\n*        vkbeautify.sql(text [,indent_pattern]);\r\n*\r\n*        @text - String; text to beatufy;\r\n*        @indent_pattern - Integer | String;\r\n*                Integer:  number of white spaces;\r\n*                String:   character string to visualize indentation ( can also be a set of white spaces )\r\n*   Minify\r\n*\r\n*        vkbeautify.xmlmin(text [,preserve_comments]);\r\n*        vkbeautify.jsonmin(text);\r\n*        vkbeautify.cssmin(text [,preserve_comments]);\r\n*        vkbeautify.sqlmin(text);\r\n*\r\n*        @text - String; text to minify;\r\n*        @preserve_comments - Bool; [optional];\r\n*                Set this flag to true to prevent removing comments from @text ( minxml and mincss functions only. )\r\n*\r\n*   Examples:\r\n*        vkbeautify.xml(text); // pretty print XML\r\n*        vkbeautify.json(text, 4 ); // pretty print JSON\r\n*        vkbeautify.css(text, '. . . .'); // pretty print CSS\r\n*        vkbeautify.sql(text, '----'); // pretty print SQL\r\n*\r\n*        vkbeautify.xmlmin(text, true);// minify XML, preserve comments\r\n*        vkbeautify.jsonmin(text);// minify JSON\r\n*        vkbeautify.cssmin(text);// minify CSS, remove comments ( default )\r\n*        vkbeautify.sqlmin(text);// minify SQL\r\n*\r\n*/\r\n\r\n(function() {\r\n\r\nfunction createShiftArr(step) {\r\n\r\n\tvar space = '    ';\r\n\t\r\n\tif ( isNaN(parseInt(step)) ) {  // argument is string\r\n\t\tspace = step;\r\n\t} else { // argument is integer\r\n\t\tswitch(step) {\r\n\t\t\tcase 1: space = ' '; break;\r\n\t\t\tcase 2: space = '  '; break;\r\n\t\t\tcase 3: space = '   '; break;\r\n\t\t\tcase 4: space = '    '; break;\r\n\t\t\tcase 5: space = '     '; break;\r\n\t\t\tcase 6: space = '      '; break;\r\n\t\t\tcase 7: space = '       '; break;\r\n\t\t\tcase 8: space = '        '; break;\r\n\t\t\tcase 9: space = '         '; break;\r\n\t\t\tcase 10: space = '          '; break;\r\n\t\t\tcase 11: space = '           '; break;\r\n\t\t\tcase 12: space = '            '; break;\r\n\t\t}\r\n\t}\r\n\r\n\tvar shift = ['\\n']; // array of shifts\r\n\tfor(ix=0;ix<100;ix++){\r\n\t\tshift.push(shift[ix]+space); \r\n\t}\r\n\treturn shift;\r\n}\r\n\r\nfunction vkbeautify(){\r\n\tthis.step = '    '; // 4 spaces\r\n\tthis.shift = createShiftArr(this.step);\r\n};\r\n\r\nvkbeautify.prototype.xml = function(text,step) {\r\n\r\n\tvar ar = text.replace(/>\\s{0,}</g,\"><\")\r\n\t\t\t\t .replace(/</g,\"~::~<\")\r\n\t\t\t\t .replace(/\\s*xmlns\\:/g,\"~::~xmlns:\")\r\n\t\t\t\t .replace(/\\s*xmlns\\=/g,\"~::~xmlns=\")\r\n\t\t\t\t .split('~::~'),\r\n\t\tlen = ar.length,\r\n\t\tinComment = false,\r\n\t\tdeep = 0,\r\n\t\tstr = '',\r\n\t\tix = 0,\r\n\t\tshift = step ? createShiftArr(step) : this.shift;\r\n\r\n\t\tfor(ix=0;ix<len;ix++) {\r\n\t\t\t// start comment or <![CDATA[...]]> or <!DOCTYPE //\r\n\t\t\tif(ar[ix].search(/<!/) > -1) { \r\n\t\t\t\tstr += shift[deep]+ar[ix];\r\n\t\t\t\tinComment = true; \r\n\t\t\t\t// end comment  or <![CDATA[...]]> //\r\n\t\t\t\tif(ar[ix].search(/-->/) > -1 || ar[ix].search(/\\]>/) > -1 || ar[ix].search(/!DOCTYPE/) > -1 ) { \r\n\t\t\t\t\tinComment = false; \r\n\t\t\t\t}\r\n\t\t\t} else \r\n\t\t\t// end comment  or <![CDATA[...]]> //\r\n\t\t\tif(ar[ix].search(/-->/) > -1 || ar[ix].search(/\\]>/) > -1) { \r\n\t\t\t\tstr += ar[ix];\r\n\t\t\t\tinComment = false; \r\n\t\t\t} else \r\n\t\t\t// <elm></elm> //\r\n\t\t\tif( /^<\\w/.exec(ar[ix-1]) && /^<\\/\\w/.exec(ar[ix]) &&\r\n\t\t\t\t/^<[\\w:\\-\\.\\,]+/.exec(ar[ix-1]) == /^<\\/[\\w:\\-\\.\\,]+/.exec(ar[ix])[0].replace('/','')) { \r\n\t\t\t\tstr += ar[ix];\r\n\t\t\t\tif(!inComment) deep--;\r\n\t\t\t} else\r\n\t\t\t // <elm> //\r\n\t\t\tif(ar[ix].search(/<\\w/) > -1 && ar[ix].search(/<\\//) == -1 && ar[ix].search(/\\/>/) == -1 ) {\r\n\t\t\t\tstr = !inComment ? str += shift[deep++]+ar[ix] : str += ar[ix];\r\n\t\t\t} else \r\n\t\t\t // <elm>...</elm> //\r\n\t\t\tif(ar[ix].search(/<\\w/) > -1 && ar[ix].search(/<\\//) > -1) {\r\n\t\t\t\tstr = !inComment ? str += shift[deep]+ar[ix] : str += ar[ix];\r\n\t\t\t} else \r\n\t\t\t// </elm> //\r\n\t\t\tif(ar[ix].search(/<\\//) > -1) { \r\n\t\t\t\tstr = !inComment ? str += shift[--deep]+ar[ix] : str += ar[ix];\r\n\t\t\t} else \r\n\t\t\t// <elm/> //\r\n\t\t\tif(ar[ix].search(/\\/>/) > -1 ) { \r\n\t\t\t\tstr = !inComment ? str += shift[deep]+ar[ix] : str += ar[ix];\r\n\t\t\t} else \r\n\t\t\t// <? xml ... ?> //\r\n\t\t\tif(ar[ix].search(/<\\?/) > -1) { \r\n\t\t\t\tstr += shift[deep]+ar[ix];\r\n\t\t\t} else \r\n\t\t\t// xmlns //\r\n\t\t\tif( ar[ix].search(/xmlns\\:/) > -1  || ar[ix].search(/xmlns\\=/) > -1) { \r\n\t\t\t\tstr += shift[deep]+ar[ix];\r\n\t\t\t} \r\n\t\t\t\r\n\t\t\telse {\r\n\t\t\t\tstr += ar[ix];\r\n\t\t\t}\r\n\t\t}\r\n\t\t\r\n\treturn  (str[0] == '\\n') ? str.slice(1) : str;\r\n}\r\n\r\nvkbeautify.prototype.json = function(text,step) {\r\n\r\n\tvar step = step ? step : this.step;\r\n\t\r\n\tif (typeof JSON === 'undefined' ) return text; \r\n\t\r\n\tif ( typeof text === \"string\" ) return JSON.stringify(JSON.parse(text), null, step);\r\n\tif ( typeof text === \"object\" ) return JSON.stringify(text, null, step);\r\n\t\t\r\n\treturn text; // text is not string nor object\r\n}\r\n\r\nvkbeautify.prototype.css = function(text, step) {\r\n\r\n\tvar ar = text.replace(/\\s{1,}/g,' ')\r\n\t\t\t\t.replace(/\\{/g,\"{~::~\")\r\n\t\t\t\t.replace(/\\}/g,\"~::~}~::~\")\r\n\t\t\t\t.replace(/\\;/g,\";~::~\")\r\n\t\t\t\t.replace(/\\/\\*/g,\"~::~/*\")\r\n\t\t\t\t.replace(/\\*\\//g,\"*/~::~\")\r\n\t\t\t\t.replace(/~::~\\s{0,}~::~/g,\"~::~\")\r\n\t\t\t\t.split('~::~'),\r\n\t\tlen = ar.length,\r\n\t\tdeep = 0,\r\n\t\tstr = '',\r\n\t\tix = 0,\r\n\t\tshift = step ? createShiftArr(step) : this.shift;\r\n\t\t\r\n\t\tfor(ix=0;ix<len;ix++) {\r\n\r\n\t\t\tif( /\\{/.exec(ar[ix]))  { \r\n\t\t\t\tstr += shift[deep++]+ar[ix];\r\n\t\t\t} else \r\n\t\t\tif( /\\}/.exec(ar[ix]))  { \r\n\t\t\t\tstr += shift[--deep]+ar[ix];\r\n\t\t\t} else\r\n\t\t\tif( /\\*\\\\/.exec(ar[ix]))  { \r\n\t\t\t\tstr += shift[deep]+ar[ix];\r\n\t\t\t}\r\n\t\t\telse {\r\n\t\t\t\tstr += shift[deep]+ar[ix];\r\n\t\t\t}\r\n\t\t}\r\n\t\treturn str.replace(/^\\n{1,}/,'');\r\n}\r\n\r\n//----------------------------------------------------------------------------\r\n\r\nfunction isSubquery(str, parenthesisLevel) {\r\n\treturn  parenthesisLevel - (str.replace(/\\(/g,'').length - str.replace(/\\)/g,'').length )\r\n}\r\n\r\nfunction split_sql(str, tab) {\r\n\r\n\treturn str.replace(/\\s{1,}/g,\" \")\r\n\r\n\t\t\t\t.replace(/ AND /ig,\"~::~\"+tab+tab+\"AND \")\r\n\t\t\t\t.replace(/ BETWEEN /ig,\"~::~\"+tab+\"BETWEEN \")\r\n\t\t\t\t.replace(/ CASE /ig,\"~::~\"+tab+\"CASE \")\r\n\t\t\t\t.replace(/ ELSE /ig,\"~::~\"+tab+\"ELSE \")\r\n\t\t\t\t.replace(/ END /ig,\"~::~\"+tab+\"END \")\r\n\t\t\t\t.replace(/ FROM /ig,\"~::~FROM \")\r\n\t\t\t\t.replace(/ GROUP\\s{1,}BY/ig,\"~::~GROUP BY \")\r\n\t\t\t\t.replace(/ HAVING /ig,\"~::~HAVING \")\r\n\t\t\t\t//.replace(/ SET /ig,\" SET~::~\")\r\n\t\t\t\t.replace(/ IN /ig,\" IN \")\r\n\t\t\t\t\r\n\t\t\t\t.replace(/ JOIN /ig,\"~::~JOIN \")\r\n\t\t\t\t.replace(/ CROSS~::~{1,}JOIN /ig,\"~::~CROSS JOIN \")\r\n\t\t\t\t.replace(/ INNER~::~{1,}JOIN /ig,\"~::~INNER JOIN \")\r\n\t\t\t\t.replace(/ LEFT~::~{1,}JOIN /ig,\"~::~LEFT JOIN \")\r\n\t\t\t\t.replace(/ RIGHT~::~{1,}JOIN /ig,\"~::~RIGHT JOIN \")\r\n\t\t\t\t\r\n\t\t\t\t.replace(/ ON /ig,\"~::~\"+tab+\"ON \")\r\n\t\t\t\t.replace(/ OR /ig,\"~::~\"+tab+tab+\"OR \")\r\n\t\t\t\t.replace(/ ORDER\\s{1,}BY/ig,\"~::~ORDER BY \")\r\n\t\t\t\t.replace(/ OVER /ig,\"~::~\"+tab+\"OVER \")\r\n\r\n\t\t\t\t.replace(/\\(\\s{0,}SELECT /ig,\"~::~(SELECT \")\r\n\t\t\t\t.replace(/\\)\\s{0,}SELECT /ig,\")~::~SELECT \")\r\n\t\t\t\t\r\n\t\t\t\t.replace(/ THEN /ig,\" THEN~::~\"+tab+\"\")\r\n\t\t\t\t.replace(/ UNION /ig,\"~::~UNION~::~\")\r\n\t\t\t\t.replace(/ USING /ig,\"~::~USING \")\r\n\t\t\t\t.replace(/ WHEN /ig,\"~::~\"+tab+\"WHEN \")\r\n\t\t\t\t.replace(/ WHERE /ig,\"~::~WHERE \")\r\n\t\t\t\t.replace(/ WITH /ig,\"~::~WITH \")\r\n\t\t\t\t\r\n\t\t\t\t//.replace(/\\,\\s{0,}\\(/ig,\",~::~( \")\r\n\t\t\t\t//.replace(/\\,/ig,\",~::~\"+tab+tab+\"\")\r\n\r\n\t\t\t\t.replace(/ ALL /ig,\" ALL \")\r\n\t\t\t\t.replace(/ AS /ig,\" AS \")\r\n\t\t\t\t.replace(/ ASC /ig,\" ASC \")\t\r\n\t\t\t\t.replace(/ DESC /ig,\" DESC \")\t\r\n\t\t\t\t.replace(/ DISTINCT /ig,\" DISTINCT \")\r\n\t\t\t\t.replace(/ EXISTS /ig,\" EXISTS \")\r\n\t\t\t\t.replace(/ NOT /ig,\" NOT \")\r\n\t\t\t\t.replace(/ NULL /ig,\" NULL \")\r\n\t\t\t\t.replace(/ LIKE /ig,\" LIKE \")\r\n\t\t\t\t.replace(/\\s{0,}SELECT /ig,\"SELECT \")\r\n\t\t\t\t.replace(/\\s{0,}UPDATE /ig,\"UPDATE \")\r\n\t\t\t\t.replace(/ SET /ig,\" SET \")\r\n\t\t\t\t\t\t\t\r\n\t\t\t\t.replace(/~::~{1,}/g,\"~::~\")\r\n\t\t\t\t.split('~::~');\r\n}\r\n\r\nvkbeautify.prototype.sql = function(text,step) {\r\n\r\n\tvar ar_by_quote = text.replace(/\\s{1,}/g,\" \")\r\n\t\t\t\t\t\t\t.replace(/\\'/ig,\"~::~\\'\")\r\n\t\t\t\t\t\t\t.split('~::~'),\r\n\t\tlen = ar_by_quote.length,\r\n\t\tar = [],\r\n\t\tdeep = 0,\r\n\t\ttab = this.step,//+this.step,\r\n\t\tinComment = true,\r\n\t\tinQuote = false,\r\n\t\tparenthesisLevel = 0,\r\n\t\tstr = '',\r\n\t\tix = 0,\r\n\t\tshift = step ? createShiftArr(step) : this.shift;;\r\n\r\n\t\tfor(ix=0;ix<len;ix++) {\r\n\t\t\tif(ix%2) {\r\n\t\t\t\tar = ar.concat(ar_by_quote[ix]);\r\n\t\t\t} else {\r\n\t\t\t\tar = ar.concat(split_sql(ar_by_quote[ix], tab) );\r\n\t\t\t}\r\n\t\t}\r\n\t\t\r\n\t\tlen = ar.length;\r\n\t\tfor(ix=0;ix<len;ix++) {\r\n\t\t\t\r\n\t\t\tparenthesisLevel = isSubquery(ar[ix], parenthesisLevel);\r\n\t\t\t\r\n\t\t\tif( /\\s{0,}\\s{0,}SELECT\\s{0,}/.exec(ar[ix]))  { \r\n\t\t\t\tar[ix] = ar[ix].replace(/\\,/g,\",\\n\"+tab+tab+\"\")\r\n\t\t\t} \r\n\t\t\t\r\n\t\t\tif( /\\s{0,}\\s{0,}SET\\s{0,}/.exec(ar[ix]))  { \r\n\t\t\t\tar[ix] = ar[ix].replace(/\\,/g,\",\\n\"+tab+tab+\"\")\r\n\t\t\t} \r\n\t\t\t\r\n\t\t\tif( /\\s{0,}\\(\\s{0,}SELECT\\s{0,}/.exec(ar[ix]))  { \r\n\t\t\t\tdeep++;\r\n\t\t\t\tstr += shift[deep]+ar[ix];\r\n\t\t\t} else \r\n\t\t\tif( /\\'/.exec(ar[ix]) )  { \r\n\t\t\t\tif(parenthesisLevel<1 && deep) {\r\n\t\t\t\t\tdeep--;\r\n\t\t\t\t}\r\n\t\t\t\tstr += ar[ix];\r\n\t\t\t}\r\n\t\t\telse  { \r\n\t\t\t\tstr += shift[deep]+ar[ix];\r\n\t\t\t\tif(parenthesisLevel<1 && deep) {\r\n\t\t\t\t\tdeep--;\r\n\t\t\t\t}\r\n\t\t\t} \r\n\t\t\tvar junk = 0;\r\n\t\t}\r\n\r\n\t\tstr = str.replace(/^\\n{1,}/,'').replace(/\\n{1,}/g,\"\\n\");\r\n\t\treturn str;\r\n}\r\n\r\n\r\nvkbeautify.prototype.xmlmin = function(text, preserveComments) {\r\n\r\n\tvar str = preserveComments ? text\r\n\t\t\t\t\t\t\t   : text.replace(/\\<![ \\r\\n\\t]*(--([^\\-]|[\\r\\n]|-[^\\-])*--[ \\r\\n\\t]*)\\>/g,\"\")\r\n\t\t\t\t\t\t\t\t\t .replace(/[ \\r\\n\\t]{1,}xmlns/g, ' xmlns');\r\n\treturn  str.replace(/>\\s{0,}</g,\"><\"); \r\n}\r\n\r\nvkbeautify.prototype.jsonmin = function(text) {\r\n\r\n\tif (typeof JSON === 'undefined' ) return text; \r\n\t\r\n\treturn JSON.stringify(JSON.parse(text), null, 0); \r\n\t\t\t\t\r\n}\r\n\r\nvkbeautify.prototype.cssmin = function(text, preserveComments) {\r\n\t\r\n\tvar str = preserveComments ? text\r\n\t\t\t\t\t\t\t   : text.replace(/\\/\\*([^*]|[\\r\\n]|(\\*+([^*/]|[\\r\\n])))*\\*+\\//g,\"\") ;\r\n\r\n\treturn str.replace(/\\s{1,}/g,' ')\r\n\t\t\t  .replace(/\\{\\s{1,}/g,\"{\")\r\n\t\t\t  .replace(/\\}\\s{1,}/g,\"}\")\r\n\t\t\t  .replace(/\\;\\s{1,}/g,\";\")\r\n\t\t\t  .replace(/\\/\\*\\s{1,}/g,\"/*\")\r\n\t\t\t  .replace(/\\*\\/\\s{1,}/g,\"*/\");\r\n}\r\n\r\nvkbeautify.prototype.sqlmin = function(text) {\r\n\treturn text.replace(/\\s{1,}/g,\" \").replace(/\\s{1,}\\(/,\"(\").replace(/\\s{1,}\\)/,\")\");\r\n}\r\n\r\nwindow.vkbeautify = new vkbeautify();\r\n\r\n})();\r\n\r\n", "//     idb-keyval.js 3.2.0\n//     https://github.com/jakearchibald/idb-keyval\n//     Copyright 2016, Jake Archibald\n//     Licensed under the Apache License, Version 2.0\n\nvar idbKeyval = (function (exports) {\n    'use strict';\n    \n    class Store {\n        constructor(dbName = 'keyval-store', storeName = 'keyval') {\n            this.storeName = storeName;\n            this._dbp = new Promise((resolve, reject) => {\n                const openreq = indexedDB.open(dbName, 1);\n                openreq.onerror = () => reject(openreq.error);\n                openreq.onsuccess = () => resolve(openreq.result);\n                // First time setup: create an empty object store\n                openreq.onupgradeneeded = () => {\n                    openreq.result.createObjectStore(storeName);\n                };\n            });\n        }\n        _withIDBStore(type, callback) {\n            return this._dbp.then(db => new Promise((resolve, reject) => {\n                const transaction = db.transaction(this.storeName, type);\n                transaction.oncomplete = () => resolve();\n                transaction.onabort = transaction.onerror = () => reject(transaction.error);\n                callback(transaction.objectStore(this.storeName));\n            }));\n        }\n    }\n    let store;\n    function getDefaultStore() {\n        if (!store)\n            store = new Store();\n        return store;\n    }\n    function get(key, store = getDefaultStore()) {\n        let req;\n        return store._withIDBStore('readonly', store => {\n            req = store.get(key);\n        }).then(() => req.result);\n    }\n    function set(key, value, store = getDefaultStore()) {\n        return store._withIDBStore('readwrite', store => {\n            store.put(value, key);\n        });\n    }\n    function del(key, store = getDefaultStore()) {\n        return store._withIDBStore('readwrite', store => {\n            store.delete(key);\n        });\n    }\n    function clear(store = getDefaultStore()) {\n        return store._withIDBStore('readwrite', store => {\n            store.clear();\n        });\n    }\n    function keys(store = getDefaultStore()) {\n        const keys = [];\n        return store._withIDBStore('readonly', store => {\n            // This would be store.getAllKeys(), but it isn't supported by Edge or Safari.\n            // And openKeyCursor isn't supported by Safari.\n            (store.openKeyCursor || store.openCursor).call(store).onsuccess = function () {\n                if (!this.result)\n                    return;\n                keys.push(this.result.key);\n                this.result.continue();\n            };\n        }).then(() => keys);\n    }\n    \n    exports.Store = Store;\n    exports.get = get;\n    exports.set = set;\n    exports.del = del;\n    exports.clear = clear;\n    exports.keys = keys;\n    \n    return exports;\n    \n    }({}));\n", "(function(){/*\n\n Copyright The Closure Library Authors.\n SPDX-License-Identifier: Apache-2.0\n*/\n'use strict';var D;function aa(a){var b=0;return function(){return b<a.length?{done:!1,value:a[b++]}:{done:!0}}}var ba=\"function\"==typeof Object.defineProperties?Object.defineProperty:function(a,b,c){if(a==Array.prototype||a==Object.prototype)return a;a[b]=c.value;return a};\nfunction ca(a){a=[\"object\"==typeof globalThis&&globalThis,a,\"object\"==typeof window&&window,\"object\"==typeof self&&self,\"object\"==typeof global&&global];for(var b=0;b<a.length;++b){var c=a[b];if(c&&c.Math==Math)return c}throw Error(\"Cannot find global object\");}var H=ca(this);function J(a,b){if(b)a:{var c=H;a=a.split(\".\");for(var d=0;d<a.length-1;d++){var f=a[d];if(!(f in c))break a;c=c[f]}a=a[a.length-1];d=c[a];b=b(d);b!=d&&null!=b&&ba(c,a,{configurable:!0,writable:!0,value:b})}}\nJ(\"Symbol\",function(a){function b(h){if(this instanceof b)throw new TypeError(\"Symbol is not a constructor\");return new c(d+(h||\"\")+\"_\"+f++,h)}function c(h,e){this.g=h;ba(this,\"description\",{configurable:!0,writable:!0,value:e})}if(a)return a;c.prototype.toString=function(){return this.g};var d=\"jscomp_symbol_\"+(1E9*Math.random()>>>0)+\"_\",f=0;return b});\nJ(\"Symbol.iterator\",function(a){if(a)return a;a=Symbol(\"Symbol.iterator\");for(var b=\"Array Int8Array Uint8Array Uint8ClampedArray Int16Array Uint16Array Int32Array Uint32Array Float32Array Float64Array\".split(\" \"),c=0;c<b.length;c++){var d=H[b[c]];\"function\"===typeof d&&\"function\"!=typeof d.prototype[a]&&ba(d.prototype,a,{configurable:!0,writable:!0,value:function(){return da(aa(this))}})}return a});function da(a){a={next:a};a[Symbol.iterator]=function(){return this};return a}\nfunction M(a){var b=\"undefined\"!=typeof Symbol&&Symbol.iterator&&a[Symbol.iterator];return b?b.call(a):{next:aa(a)}}function ea(a){if(!(a instanceof Array)){a=M(a);for(var b,c=[];!(b=a.next()).done;)c.push(b.value);a=c}return a}var fa=\"function\"==typeof Object.create?Object.create:function(a){function b(){}b.prototype=a;return new b},ha;\nif(\"function\"==typeof Object.setPrototypeOf)ha=Object.setPrototypeOf;else{var ia;a:{var ja={a:!0},ka={};try{ka.__proto__=ja;ia=ka.a;break a}catch(a){}ia=!1}ha=ia?function(a,b){a.__proto__=b;if(a.__proto__!==b)throw new TypeError(a+\" is not extensible\");return a}:null}var la=ha;\nfunction ma(a,b){a.prototype=fa(b.prototype);a.prototype.constructor=a;if(la)la(a,b);else for(var c in b)if(\"prototype\"!=c)if(Object.defineProperties){var d=Object.getOwnPropertyDescriptor(b,c);d&&Object.defineProperty(a,c,d)}else a[c]=b[c];a.ea=b.prototype}function na(){this.l=!1;this.i=null;this.h=void 0;this.g=1;this.s=this.m=0;this.j=null}function oa(a){if(a.l)throw new TypeError(\"Generator is already running\");a.l=!0}na.prototype.o=function(a){this.h=a};\nfunction pa(a,b){a.j={U:b,V:!0};a.g=a.m||a.s}na.prototype.return=function(a){this.j={return:a};this.g=this.s};function N(a,b,c){a.g=c;return{value:b}}function qa(a){this.g=new na;this.h=a}function ra(a,b){oa(a.g);var c=a.g.i;if(c)return sa(a,\"return\"in c?c[\"return\"]:function(d){return{value:d,done:!0}},b,a.g.return);a.g.return(b);return ta(a)}\nfunction sa(a,b,c,d){try{var f=b.call(a.g.i,c);if(!(f instanceof Object))throw new TypeError(\"Iterator result \"+f+\" is not an object\");if(!f.done)return a.g.l=!1,f;var h=f.value}catch(e){return a.g.i=null,pa(a.g,e),ta(a)}a.g.i=null;d.call(a.g,h);return ta(a)}function ta(a){for(;a.g.g;)try{var b=a.h(a.g);if(b)return a.g.l=!1,{value:b.value,done:!1}}catch(c){a.g.h=void 0,pa(a.g,c)}a.g.l=!1;if(a.g.j){b=a.g.j;a.g.j=null;if(b.V)throw b.U;return{value:b.return,done:!0}}return{value:void 0,done:!0}}\nfunction ua(a){this.next=function(b){oa(a.g);a.g.i?b=sa(a,a.g.i.next,b,a.g.o):(a.g.o(b),b=ta(a));return b};this.throw=function(b){oa(a.g);a.g.i?b=sa(a,a.g.i[\"throw\"],b,a.g.o):(pa(a.g,b),b=ta(a));return b};this.return=function(b){return ra(a,b)};this[Symbol.iterator]=function(){return this}}function O(a,b){b=new ua(new qa(b));la&&a.prototype&&la(b,a.prototype);return b}\nfunction va(a,b){a instanceof String&&(a+=\"\");var c=0,d=!1,f={next:function(){if(!d&&c<a.length){var h=c++;return{value:b(h,a[h]),done:!1}}d=!0;return{done:!0,value:void 0}}};f[Symbol.iterator]=function(){return f};return f}var wa=\"function\"==typeof Object.assign?Object.assign:function(a,b){for(var c=1;c<arguments.length;c++){var d=arguments[c];if(d)for(var f in d)Object.prototype.hasOwnProperty.call(d,f)&&(a[f]=d[f])}return a};J(\"Object.assign\",function(a){return a||wa});\nJ(\"Promise\",function(a){function b(e){this.h=0;this.i=void 0;this.g=[];this.o=!1;var g=this.j();try{e(g.resolve,g.reject)}catch(k){g.reject(k)}}function c(){this.g=null}function d(e){return e instanceof b?e:new b(function(g){g(e)})}if(a)return a;c.prototype.h=function(e){if(null==this.g){this.g=[];var g=this;this.i(function(){g.l()})}this.g.push(e)};var f=H.setTimeout;c.prototype.i=function(e){f(e,0)};c.prototype.l=function(){for(;this.g&&this.g.length;){var e=this.g;this.g=[];for(var g=0;g<e.length;++g){var k=\ne[g];e[g]=null;try{k()}catch(l){this.j(l)}}}this.g=null};c.prototype.j=function(e){this.i(function(){throw e;})};b.prototype.j=function(){function e(l){return function(q){k||(k=!0,l.call(g,q))}}var g=this,k=!1;return{resolve:e(this.C),reject:e(this.l)}};b.prototype.C=function(e){if(e===this)this.l(new TypeError(\"A Promise cannot resolve to itself\"));else if(e instanceof b)this.F(e);else{a:switch(typeof e){case \"object\":var g=null!=e;break a;case \"function\":g=!0;break a;default:g=!1}g?this.u(e):this.m(e)}};\nb.prototype.u=function(e){var g=void 0;try{g=e.then}catch(k){this.l(k);return}\"function\"==typeof g?this.G(g,e):this.m(e)};b.prototype.l=function(e){this.s(2,e)};b.prototype.m=function(e){this.s(1,e)};b.prototype.s=function(e,g){if(0!=this.h)throw Error(\"Cannot settle(\"+e+\", \"+g+\"): Promise already settled in state\"+this.h);this.h=e;this.i=g;2===this.h&&this.D();this.A()};b.prototype.D=function(){var e=this;f(function(){if(e.B()){var g=H.console;\"undefined\"!==typeof g&&g.error(e.i)}},1)};b.prototype.B=\nfunction(){if(this.o)return!1;var e=H.CustomEvent,g=H.Event,k=H.dispatchEvent;if(\"undefined\"===typeof k)return!0;\"function\"===typeof e?e=new e(\"unhandledrejection\",{cancelable:!0}):\"function\"===typeof g?e=new g(\"unhandledrejection\",{cancelable:!0}):(e=H.document.createEvent(\"CustomEvent\"),e.initCustomEvent(\"unhandledrejection\",!1,!0,e));e.promise=this;e.reason=this.i;return k(e)};b.prototype.A=function(){if(null!=this.g){for(var e=0;e<this.g.length;++e)h.h(this.g[e]);this.g=null}};var h=new c;b.prototype.F=\nfunction(e){var g=this.j();e.J(g.resolve,g.reject)};b.prototype.G=function(e,g){var k=this.j();try{e.call(g,k.resolve,k.reject)}catch(l){k.reject(l)}};b.prototype.then=function(e,g){function k(w,t){return\"function\"==typeof w?function(y){try{l(w(y))}catch(m){q(m)}}:t}var l,q,v=new b(function(w,t){l=w;q=t});this.J(k(e,l),k(g,q));return v};b.prototype.catch=function(e){return this.then(void 0,e)};b.prototype.J=function(e,g){function k(){switch(l.h){case 1:e(l.i);break;case 2:g(l.i);break;default:throw Error(\"Unexpected state: \"+\nl.h);}}var l=this;null==this.g?h.h(k):this.g.push(k);this.o=!0};b.resolve=d;b.reject=function(e){return new b(function(g,k){k(e)})};b.race=function(e){return new b(function(g,k){for(var l=M(e),q=l.next();!q.done;q=l.next())d(q.value).J(g,k)})};b.all=function(e){var g=M(e),k=g.next();return k.done?d([]):new b(function(l,q){function v(y){return function(m){w[y]=m;t--;0==t&&l(w)}}var w=[],t=0;do w.push(void 0),t++,d(k.value).J(v(w.length-1),q),k=g.next();while(!k.done)})};return b});\nJ(\"Object.is\",function(a){return a?a:function(b,c){return b===c?0!==b||1/b===1/c:b!==b&&c!==c}});J(\"Array.prototype.includes\",function(a){return a?a:function(b,c){var d=this;d instanceof String&&(d=String(d));var f=d.length;c=c||0;for(0>c&&(c=Math.max(c+f,0));c<f;c++){var h=d[c];if(h===b||Object.is(h,b))return!0}return!1}});\nJ(\"String.prototype.includes\",function(a){return a?a:function(b,c){if(null==this)throw new TypeError(\"The 'this' value for String.prototype.includes must not be null or undefined\");if(b instanceof RegExp)throw new TypeError(\"First argument to String.prototype.includes must not be a regular expression\");return-1!==this.indexOf(b,c||0)}});J(\"Array.prototype.keys\",function(a){return a?a:function(){return va(this,function(b){return b})}});var xa=this||self;\nfunction ya(a,b){a=a.split(\".\");var c=xa;a[0]in c||\"undefined\"==typeof c.execScript||c.execScript(\"var \"+a[0]);for(var d;a.length&&(d=a.shift());)a.length||void 0===b?c[d]&&c[d]!==Object.prototype[d]?c=c[d]:c=c[d]={}:c[d]=b};function za(a,b){b=String.fromCharCode.apply(null,b);return null==a?b:a+b}var Aa,Ba=\"undefined\"!==typeof TextDecoder,Ca,Da=\"undefined\"!==typeof TextEncoder;\nfunction Ea(a){if(Da)a=(Ca||(Ca=new TextEncoder)).encode(a);else{var b=void 0;b=void 0===b?!1:b;for(var c=0,d=new Uint8Array(3*a.length),f=0;f<a.length;f++){var h=a.charCodeAt(f);if(128>h)d[c++]=h;else{if(2048>h)d[c++]=h>>6|192;else{if(55296<=h&&57343>=h){if(56319>=h&&f<a.length){var e=a.charCodeAt(++f);if(56320<=e&&57343>=e){h=1024*(h-55296)+e-56320+65536;d[c++]=h>>18|240;d[c++]=h>>12&63|128;d[c++]=h>>6&63|128;d[c++]=h&63|128;continue}else f--}if(b)throw Error(\"Found an unpaired surrogate\");h=65533}d[c++]=\nh>>12|224;d[c++]=h>>6&63|128}d[c++]=h&63|128}}a=d.subarray(0,c)}return a};var Fa={},Ga=null;function Ha(a,b){void 0===b&&(b=0);Ja();b=Fa[b];for(var c=Array(Math.floor(a.length/3)),d=b[64]||\"\",f=0,h=0;f<a.length-2;f+=3){var e=a[f],g=a[f+1],k=a[f+2],l=b[e>>2];e=b[(e&3)<<4|g>>4];g=b[(g&15)<<2|k>>6];k=b[k&63];c[h++]=l+e+g+k}l=0;k=d;switch(a.length-f){case 2:l=a[f+1],k=b[(l&15)<<2]||d;case 1:a=a[f],c[h]=b[a>>2]+b[(a&3)<<4|l>>4]+k+d}return c.join(\"\")}\nfunction Ka(a){var b=a.length,c=3*b/4;c%3?c=Math.floor(c):-1!=\"=.\".indexOf(a[b-1])&&(c=-1!=\"=.\".indexOf(a[b-2])?c-2:c-1);var d=new Uint8Array(c),f=0;La(a,function(h){d[f++]=h});return d.subarray(0,f)}\nfunction La(a,b){function c(k){for(;d<a.length;){var l=a.charAt(d++),q=Ga[l];if(null!=q)return q;if(!/^[\\s\\xa0]*$/.test(l))throw Error(\"Unknown base64 encoding at char: \"+l);}return k}Ja();for(var d=0;;){var f=c(-1),h=c(0),e=c(64),g=c(64);if(64===g&&-1===f)break;b(f<<2|h>>4);64!=e&&(b(h<<4&240|e>>2),64!=g&&b(e<<6&192|g))}}\nfunction Ja(){if(!Ga){Ga={};for(var a=\"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789\".split(\"\"),b=[\"+/=\",\"+/\",\"-_=\",\"-_.\",\"-_\"],c=0;5>c;c++){var d=a.concat(b[c].split(\"\"));Fa[c]=d;for(var f=0;f<d.length;f++){var h=d[f];void 0===Ga[h]&&(Ga[h]=f)}}}};var Ma=\"function\"===typeof Uint8Array.prototype.slice,Na;function Oa(a,b,c){return b===c?Na||(Na=new Uint8Array(0)):Ma?a.slice(b,c):new Uint8Array(a.subarray(b,c))}var P=0,Q=0;function Pa(a,b){b=void 0===b?{}:b;b=void 0===b.v?!1:b.v;this.h=null;this.g=this.i=this.j=0;this.l=!1;this.v=b;a&&Qa(this,a)}function Qa(a,b){b=b.constructor===Uint8Array?b:b.constructor===ArrayBuffer?new Uint8Array(b):b.constructor===Array?new Uint8Array(b):b.constructor===String?Ka(b):b instanceof Uint8Array?new Uint8Array(b.buffer,b.byteOffset,b.byteLength):new Uint8Array(0);a.h=b;a.j=0;a.i=a.h.length;a.g=a.j}Pa.prototype.reset=function(){this.g=this.j};\nfunction Ra(a){var b=a.h,c=b[a.g],d=c&127;if(128>c)return a.g+=1,d;c=b[a.g+1];d|=(c&127)<<7;if(128>c)return a.g+=2,d;c=b[a.g+2];d|=(c&127)<<14;if(128>c)return a.g+=3,d;c=b[a.g+3];d|=(c&127)<<21;if(128>c)return a.g+=4,d;c=b[a.g+4];d|=(c&15)<<28;if(128>c)return a.g+=5,d>>>0;a.g+=5;128<=b[a.g++]&&128<=b[a.g++]&&128<=b[a.g++]&&128<=b[a.g++]&&a.g++;return d}\nfunction R(a){var b=a.h[a.g];var c=a.h[a.g+1];var d=a.h[a.g+2],f=a.h[a.g+3];a.g+=4;c=(b<<0|c<<8|d<<16|f<<24)>>>0;a=2*(c>>31)+1;b=c>>>23&255;c&=8388607;return 255==b?c?NaN:Infinity*a:0==b?a*Math.pow(2,-149)*c:a*Math.pow(2,b-150)*(c+Math.pow(2,23))}var Sa=[];function Ta(){this.g=new Uint8Array(64);this.h=0}Ta.prototype.push=function(a){if(!(this.h+1<this.g.length)){var b=this.g;this.g=new Uint8Array(Math.ceil(1+2*this.g.length));this.g.set(b)}this.g[this.h++]=a};Ta.prototype.length=function(){return this.h};Ta.prototype.end=function(){var a=this.g,b=this.h;this.h=0;return Oa(a,0,b)};function S(a,b){for(;127<b;)a.push(b&127|128),b>>>=7;a.push(b)};function Ua(a){var b={},c=void 0===b.N?!1:b.N;this.o={v:void 0===b.v?!1:b.v};this.N=c;b=this.o;Sa.length?(c=Sa.pop(),b&&(c.v=b.v),a&&Qa(c,a),a=c):a=new Pa(a,b);this.g=a;this.m=this.g.g;this.h=this.i=this.l=-1;this.j=!1}Ua.prototype.reset=function(){this.g.reset();this.h=this.l=-1};function Va(a){var b=a.g;(b=b.g==b.i)||(b=a.j)||(b=a.g,b=b.l||0>b.g||b.g>b.i);if(b)return!1;a.m=a.g.g;b=Ra(a.g);var c=b&7;if(0!=c&&5!=c&&1!=c&&2!=c&&3!=c&&4!=c)return a.j=!0,!1;a.i=b;a.l=b>>>3;a.h=c;return!0}\nfunction Wa(a){switch(a.h){case 0:if(0!=a.h)Wa(a);else{for(a=a.g;a.h[a.g]&128;)a.g++;a.g++}break;case 1:1!=a.h?Wa(a):(a=a.g,a.g+=8);break;case 2:if(2!=a.h)Wa(a);else{var b=Ra(a.g);a=a.g;a.g+=b}break;case 5:5!=a.h?Wa(a):(a=a.g,a.g+=4);break;case 3:b=a.l;do{if(!Va(a)){a.j=!0;break}if(4==a.h){a.l!=b&&(a.j=!0);break}Wa(a)}while(1);break;default:a.j=!0}}function Xa(a,b,c){var d=a.g.i,f=Ra(a.g);f=a.g.g+f;a.g.i=f;c(b,a);a.g.g=f;a.g.i=d;return b}\nfunction Ya(a){var b=Ra(a.g);a=a.g;var c=a.g;a.g+=b;a=a.h;var d;if(Ba)(d=Aa)||(d=Aa=new TextDecoder(\"utf-8\",{fatal:!1})),d=d.decode(a.subarray(c,c+b));else{b=c+b;for(var f=[],h=null,e,g,k;c<b;)e=a[c++],128>e?f.push(e):224>e?c>=b?f.push(65533):(g=a[c++],194>e||128!==(g&192)?(c--,f.push(65533)):f.push((e&31)<<6|g&63)):240>e?c>=b-1?f.push(65533):(g=a[c++],128!==(g&192)||224===e&&160>g||237===e&&160<=g||128!==((d=a[c++])&192)?(c--,f.push(65533)):f.push((e&15)<<12|(g&63)<<6|d&63)):244>=e?c>=b-2?f.push(65533):\n(g=a[c++],128!==(g&192)||0!==(e<<28)+(g-144)>>30||128!==((d=a[c++])&192)||128!==((k=a[c++])&192)?(c--,f.push(65533)):(e=(e&7)<<18|(g&63)<<12|(d&63)<<6|k&63,e-=65536,f.push((e>>10&1023)+55296,(e&1023)+56320))):f.push(65533),8192<=f.length&&(h=za(h,f),f.length=0);d=za(h,f)}return d};function Za(){this.h=[];this.i=0;this.g=new Ta}function $a(a,b){0!==b.length&&(a.h.push(b),a.i+=b.length)}function ab(a){var b=a.i+a.g.length();if(0===b)return new Uint8Array(0);b=new Uint8Array(b);for(var c=a.h,d=c.length,f=0,h=0;h<d;h++){var e=c[h];0!==e.length&&(b.set(e,f),f+=e.length)}c=a.g;d=c.h;0!==d&&(b.set(c.g.subarray(0,d),f),c.h=0);a.h=[b];return b}\nfunction T(a,b,c){if(null!=c){S(a.g,8*b+5);a=a.g;var d=c;d=(c=0>d?1:0)?-d:d;0===d?0<1/d?P=Q=0:(Q=0,P=2147483648):isNaN(d)?(Q=0,P=2147483647):3.4028234663852886E38<d?(Q=0,P=(c<<31|2139095040)>>>0):1.1754943508222875E-38>d?(d=Math.round(d/Math.pow(2,-149)),Q=0,P=(c<<31|d)>>>0):(b=Math.floor(Math.log(d)/Math.LN2),d*=Math.pow(2,-b),d=Math.round(8388608*d)&8388607,Q=0,P=(c<<31|b+127<<23|d)>>>0);c=P;a.push(c>>>0&255);a.push(c>>>8&255);a.push(c>>>16&255);a.push(c>>>24&255)}};var bb=\"function\"===typeof Uint8Array;function cb(a,b,c){if(null!=a)return\"object\"===typeof a?bb&&a instanceof Uint8Array?c(a):db(a,b,c):b(a)}function db(a,b,c){if(Array.isArray(a)){for(var d=Array(a.length),f=0;f<a.length;f++)d[f]=cb(a[f],b,c);Array.isArray(a)&&a.W&&eb(d);return d}d={};for(f in a)d[f]=cb(a[f],b,c);return d}function fb(a){return\"number\"===typeof a?isFinite(a)?a:String(a):a}var gb={W:{value:!0,configurable:!0}};\nfunction eb(a){Array.isArray(a)&&!Object.isFrozen(a)&&Object.defineProperties(a,gb);return a};var hb;function U(a,b,c){var d=hb;hb=null;a||(a=d);d=this.constructor.ca;a||(a=d?[d]:[]);this.j=d?0:-1;this.i=null;this.g=a;a:{d=this.g.length;a=d-1;if(d&&(d=this.g[a],null!==d&&\"object\"===typeof d&&d.constructor===Object)){this.l=a-this.j;this.h=d;break a}void 0!==b&&-1<b?(this.l=Math.max(b,a+1-this.j),this.h=null):this.l=Number.MAX_VALUE}if(c)for(b=0;b<c.length;b++)a=c[b],a<this.l?(a+=this.j,(d=this.g[a])?eb(d):this.g[a]=ib):(jb(this),(d=this.h[a])?eb(d):this.h[a]=ib)}var ib=Object.freeze(eb([]));\nfunction jb(a){var b=a.l+a.j;a.g[b]||(a.h=a.g[b]={})}function V(a,b,c){return-1===b?null:(void 0===c?0:c)||b>=a.l?a.h?a.h[b]:void 0:a.g[b+a.j]}function kb(a){var b=void 0===b?!1:b;var c=V(a,1,b);null==c&&(c=ib);c===ib&&(c=eb([]),W(a,1,c,b));return c}function X(a,b,c){a=V(a,b);a=null==a?a:+a;return null==a?void 0===c?0:c:a}function W(a,b,c,d){(void 0===d?0:d)||b>=a.l?(jb(a),a.h[b]=c):a.g[b+a.j]=c}\nfunction lb(a,b){a.i||(a.i={});var c=a.i[1];if(!c){var d=kb(a);c=[];for(var f=0;f<d.length;f++)c[f]=new b(d[f]);a.i[1]=c}return c}function mb(a,b,c,d){var f=lb(a,c);b=b?b:new c;a=kb(a);void 0!=d?(f.splice(d,0,b),a.splice(d,0,nb(b,!1))):(f.push(b),a.push(nb(b,!1)))}U.prototype.toJSON=function(){var a=nb(this,!1);return db(a,fb,Ha)};function nb(a,b){if(a.i)for(var c in a.i){var d=a.i[c];if(Array.isArray(d))for(var f=0;f<d.length;f++)d[f]&&nb(d[f],b);else d&&nb(d,b)}return a.g}\nU.prototype.toString=function(){return nb(this,!1).toString()};function ob(a,b){a=V(a,b);return null==a?0:a}function pb(a,b){a=V(a,b);return null==a?\"\":a};function qb(a,b){if(a=a.m){$a(b,b.g.end());for(var c=0;c<a.length;c++)$a(b,a[c])}}function rb(a,b){if(4==b.h)return!1;var c=b.m;Wa(b);b.N||(b=Oa(b.g.h,c,b.g.g),(c=a.m)?c.push(b):a.m=[b]);return!0};function Y(a,b){var c=void 0;return new (c||(c=Promise))(function(d,f){function h(k){try{g(b.next(k))}catch(l){f(l)}}function e(k){try{g(b[\"throw\"](k))}catch(l){f(l)}}function g(k){k.done?d(k.value):(new c(function(l){l(k.value)})).then(h,e)}g((b=b.apply(a,void 0)).next())})};function sb(a){U.call(this,a)}ma(sb,U);function tb(a,b){for(;Va(b);)switch(b.i){case 8:var c=Ra(b.g);W(a,1,c);break;case 21:c=R(b.g);W(a,2,c);break;case 26:c=Ya(b);W(a,3,c);break;case 34:c=Ya(b);W(a,4,c);break;default:if(!rb(a,b))return a}return a};function ub(a){U.call(this,a,-1,vb)}ma(ub,U);ub.prototype.addClassification=function(a,b){mb(this,a,sb,b)};var vb=[1];function wb(a){U.call(this,a)}ma(wb,U);function xb(a,b){for(;Va(b);)switch(b.i){case 13:var c=R(b.g);W(a,1,c);break;case 21:c=R(b.g);W(a,2,c);break;case 29:c=R(b.g);W(a,3,c);break;case 37:c=R(b.g);W(a,4,c);break;case 45:c=R(b.g);W(a,5,c);break;default:if(!rb(a,b))return a}return a};function yb(a){U.call(this,a,-1,zb)}ma(yb,U);var zb=[1];function Ab(a){U.call(this,a)}ma(Ab,U);function Bb(a,b,c){c=a.createShader(0===c?a.VERTEX_SHADER:a.FRAGMENT_SHADER);a.shaderSource(c,b);a.compileShader(c);if(!a.getShaderParameter(c,a.COMPILE_STATUS))throw Error(\"Could not compile WebGL shader.\\n\\n\"+a.getShaderInfoLog(c));return c};function Cb(a){return lb(a,sb).map(function(b){return{index:ob(b,1),Y:X(b,2),label:null!=V(b,3)?pb(b,3):void 0,displayName:null!=V(b,4)?pb(b,4):void 0}})};function Db(a){return{x:X(a,1),y:X(a,2),z:X(a,3),visibility:null!=V(a,4)?X(a,4):void 0}};function Eb(a,b){this.h=a;this.g=b;this.l=0}\nfunction Fb(a,b,c){Gb(a,b);if(\"function\"===typeof a.g.canvas.transferToImageBitmap)return Promise.resolve(a.g.canvas.transferToImageBitmap());if(c)return Promise.resolve(a.g.canvas);if(\"function\"===typeof createImageBitmap)return createImageBitmap(a.g.canvas);void 0===a.i&&(a.i=document.createElement(\"canvas\"));return new Promise(function(d){a.i.height=a.g.canvas.height;a.i.width=a.g.canvas.width;a.i.getContext(\"2d\",{}).drawImage(a.g.canvas,0,0,a.g.canvas.width,a.g.canvas.height);d(a.i)})}\nfunction Gb(a,b){var c=a.g;if(void 0===a.m){var d=Bb(c,\"\\n  attribute vec2 aVertex;\\n  attribute vec2 aTex;\\n  varying vec2 vTex;\\n  void main(void) {\\n    gl_Position = vec4(aVertex, 0.0, 1.0);\\n    vTex = aTex;\\n  }\",0),f=Bb(c,\"\\n  precision mediump float;\\n  varying vec2 vTex;\\n  uniform sampler2D sampler0;\\n  void main(){\\n    gl_FragColor = texture2D(sampler0, vTex);\\n  }\",1),h=c.createProgram();c.attachShader(h,d);c.attachShader(h,f);c.linkProgram(h);if(!c.getProgramParameter(h,c.LINK_STATUS))throw Error(\"Could not compile WebGL program.\\n\\n\"+\nc.getProgramInfoLog(h));d=a.m=h;c.useProgram(d);f=c.getUniformLocation(d,\"sampler0\");a.j={I:c.getAttribLocation(d,\"aVertex\"),H:c.getAttribLocation(d,\"aTex\"),da:f};a.s=c.createBuffer();c.bindBuffer(c.ARRAY_BUFFER,a.s);c.enableVertexAttribArray(a.j.I);c.vertexAttribPointer(a.j.I,2,c.FLOAT,!1,0,0);c.bufferData(c.ARRAY_BUFFER,new Float32Array([-1,-1,-1,1,1,1,1,-1]),c.STATIC_DRAW);c.bindBuffer(c.ARRAY_BUFFER,null);a.o=c.createBuffer();c.bindBuffer(c.ARRAY_BUFFER,a.o);c.enableVertexAttribArray(a.j.H);c.vertexAttribPointer(a.j.H,\n2,c.FLOAT,!1,0,0);c.bufferData(c.ARRAY_BUFFER,new Float32Array([0,1,0,0,1,0,1,1]),c.STATIC_DRAW);c.bindBuffer(c.ARRAY_BUFFER,null);c.uniform1i(f,0)}d=a.j;c.useProgram(a.m);c.canvas.width=b.width;c.canvas.height=b.height;c.viewport(0,0,b.width,b.height);c.activeTexture(c.TEXTURE0);a.h.bindTexture2d(b.glName);c.enableVertexAttribArray(d.I);c.bindBuffer(c.ARRAY_BUFFER,a.s);c.vertexAttribPointer(d.I,2,c.FLOAT,!1,0,0);c.enableVertexAttribArray(d.H);c.bindBuffer(c.ARRAY_BUFFER,a.o);c.vertexAttribPointer(d.H,\n2,c.FLOAT,!1,0,0);c.bindFramebuffer(c.DRAW_FRAMEBUFFER?c.DRAW_FRAMEBUFFER:c.FRAMEBUFFER,null);c.clearColor(0,0,0,0);c.clear(c.COLOR_BUFFER_BIT);c.colorMask(!0,!0,!0,!0);c.drawArrays(c.TRIANGLE_FAN,0,4);c.disableVertexAttribArray(d.I);c.disableVertexAttribArray(d.H);c.bindBuffer(c.ARRAY_BUFFER,null);a.h.bindTexture2d(0)}function Hb(a){this.g=a};var Ib=new Uint8Array([0,97,115,109,1,0,0,0,1,4,1,96,0,0,3,2,1,0,10,9,1,7,0,65,0,253,15,26,11]);function Jb(a,b){return b+a}function Kb(a,b){window[a]=b}function Lb(a){var b=document.createElement(\"script\");b.setAttribute(\"src\",a);b.setAttribute(\"crossorigin\",\"anonymous\");return new Promise(function(c){b.addEventListener(\"load\",function(){c()},!1);b.addEventListener(\"error\",function(){c()},!1);document.body.appendChild(b)})}\nfunction Mb(){return Y(this,function b(){return O(b,function(c){switch(c.g){case 1:return c.m=2,N(c,WebAssembly.instantiate(Ib),4);case 4:c.g=3;c.m=0;break;case 2:return c.m=0,c.j=null,c.return(!1);case 3:return c.return(!0)}})})}\nfunction Nb(a){this.g=a;this.listeners={};this.j={};this.F={};this.m={};this.s={};this.G=this.o=this.R=!0;this.C=Promise.resolve();this.P=\"\";this.B={};this.locateFile=a&&a.locateFile||Jb;if(\"object\"===typeof window)var b=window.location.pathname.toString().substring(0,window.location.pathname.toString().lastIndexOf(\"/\"))+\"/\";else if(\"undefined\"!==typeof location)b=location.pathname.toString().substring(0,location.pathname.toString().lastIndexOf(\"/\"))+\"/\";else throw Error(\"solutions can only be loaded on a web page or in a web worker\");\nthis.S=b;if(a.options){b=M(Object.keys(a.options));for(var c=b.next();!c.done;c=b.next()){c=c.value;var d=a.options[c].default;void 0!==d&&(this.j[c]=\"function\"===typeof d?d():d)}}}D=Nb.prototype;D.close=function(){this.i&&this.i.delete();return Promise.resolve()};function Ob(a,b){return void 0===a.g.files?[]:\"function\"===typeof a.g.files?a.g.files(b):a.g.files}\nfunction Pb(a){return Y(a,function c(){var d=this,f,h,e,g,k,l,q,v,w,t,y;return O(c,function(m){switch(m.g){case 1:f=d;if(!d.R)return m.return();h=Ob(d,d.j);return N(m,Mb(),2);case 2:e=m.h;if(\"object\"===typeof window)return Kb(\"createMediapipeSolutionsWasm\",{locateFile:d.locateFile}),Kb(\"createMediapipeSolutionsPackedAssets\",{locateFile:d.locateFile}),l=h.filter(function(u){return void 0!==u.data}),q=h.filter(function(u){return void 0===u.data}),v=Promise.all(l.map(function(u){var x=Qb(f,u.url);if(void 0!==\nu.path){var z=u.path;x=x.then(function(E){f.overrideFile(z,E);return Promise.resolve(E)})}return x})),w=Promise.all(q.map(function(u){return void 0===u.simd||u.simd&&e||!u.simd&&!e?Lb(f.locateFile(u.url,f.S)):Promise.resolve()})).then(function(){return Y(f,function x(){var z,E,F=this;return O(x,function(K){if(1==K.g)return z=window.createMediapipeSolutionsWasm,E=window.createMediapipeSolutionsPackedAssets,N(K,z(E),2);F.h=K.h;K.g=0})})}),t=function(){return Y(f,function x(){var z=this;return O(x,function(E){z.g.graph&&\nz.g.graph.url?E=N(E,Qb(z,z.g.graph.url),0):(E.g=0,E=void 0);return E})})}(),N(m,Promise.all([w,v,t]),7);if(\"function\"!==typeof importScripts)throw Error(\"solutions can only be loaded on a web page or in a web worker\");g=h.filter(function(u){return void 0===u.simd||u.simd&&e||!u.simd&&!e}).map(function(u){return f.locateFile(u.url,f.S)});importScripts.apply(null,ea(g));return N(m,createMediapipeSolutionsWasm(Module),6);case 6:d.h=m.h;d.l=new OffscreenCanvas(1,1);d.h.canvas=d.l;k=d.h.GL.createContext(d.l,\n{antialias:!1,alpha:!1,ba:\"undefined\"!==typeof WebGL2RenderingContext?2:1});d.h.GL.makeContextCurrent(k);m.g=4;break;case 7:d.l=document.createElement(\"canvas\");y=d.l.getContext(\"webgl2\",{});if(!y&&(y=d.l.getContext(\"webgl\",{}),!y))return alert(\"Failed to create WebGL canvas context when passing video frame.\"),m.return();d.D=y;d.h.canvas=d.l;d.h.createContext(d.l,!0,!0,{});case 4:d.i=new d.h.SolutionWasm,d.R=!1,m.g=0}})})}\nfunction Rb(a){return Y(a,function c(){var d=this,f,h,e,g,k,l,q,v;return O(c,function(w){if(1==w.g){if(d.g.graph&&d.g.graph.url&&d.P===d.g.graph.url)return w.return();d.o=!0;if(!d.g.graph||!d.g.graph.url){w.g=2;return}d.P=d.g.graph.url;return N(w,Qb(d,d.g.graph.url),3)}2!=w.g&&(f=w.h,d.i.loadGraph(f));h=M(Object.keys(d.B));for(e=h.next();!e.done;e=h.next())g=e.value,d.i.overrideFile(g,d.B[g]);d.B={};if(d.g.listeners)for(k=M(d.g.listeners),l=k.next();!l.done;l=k.next())q=l.value,Sb(d,q);v=d.j;d.j=\n{};d.setOptions(v);w.g=0})})}D.reset=function(){return Y(this,function b(){var c=this;return O(b,function(d){c.i&&(c.i.reset(),c.m={},c.s={});d.g=0})})};\nD.setOptions=function(a,b){var c=this;if(b=b||this.g.options){for(var d=[],f=[],h={},e=M(Object.keys(a)),g=e.next();!g.done;h={K:h.K,L:h.L},g=e.next()){var k=g.value;k in this.j&&this.j[k]===a[k]||(this.j[k]=a[k],g=b[k],void 0!==g&&(g.onChange&&(h.K=g.onChange,h.L=a[k],d.push(function(l){return function(){return Y(c,function v(){var w,t=this;return O(v,function(y){if(1==y.g)return N(y,l.K(l.L),2);w=y.h;!0===w&&(t.o=!0);y.g=0})})}}(h))),g.graphOptionXref&&(k={valueNumber:1===g.type?a[k]:0,valueBoolean:0===\ng.type?a[k]:!1,valueString:2===g.type?a[k]:\"\"},g=Object.assign(Object.assign(Object.assign({},{calculatorName:\"\",calculatorIndex:0}),g.graphOptionXref),k),f.push(g))))}if(0!==d.length||0!==f.length)this.o=!0,this.A=(void 0===this.A?[]:this.A).concat(f),this.u=(void 0===this.u?[]:this.u).concat(d)}};\nfunction Tb(a){return Y(a,function c(){var d=this,f,h,e,g,k,l,q;return O(c,function(v){switch(v.g){case 1:if(!d.o)return v.return();if(!d.u){v.g=2;break}f=M(d.u);h=f.next();case 3:if(h.done){v.g=5;break}e=h.value;return N(v,e(),4);case 4:h=f.next();v.g=3;break;case 5:d.u=void 0;case 2:if(d.A){g=new d.h.GraphOptionChangeRequestList;k=M(d.A);for(l=k.next();!l.done;l=k.next())q=l.value,g.push_back(q);d.i.changeOptions(g);g.delete();d.A=void 0}d.o=!1;v.g=0}})})}\nD.initialize=function(){return Y(this,function b(){var c=this;return O(b,function(d){return 1==d.g?N(d,Pb(c),2):3!=d.g?N(d,Rb(c),3):N(d,Tb(c),0)})})};function Qb(a,b){return Y(a,function d(){var f=this,h,e;return O(d,function(g){if(b in f.F)return g.return(f.F[b]);h=f.locateFile(b,\"\");e=fetch(h).then(function(k){return k.arrayBuffer()});f.F[b]=e;return g.return(e)})})}D.overrideFile=function(a,b){this.i?this.i.overrideFile(a,b):this.B[a]=b};D.clearOverriddenFiles=function(){this.B={};this.i&&this.i.clearOverriddenFiles()};\nD.send=function(a,b){return Y(this,function d(){var f=this,h,e,g,k,l,q,v,w,t;return O(d,function(y){switch(y.g){case 1:if(!f.g.inputs)return y.return();h=1E3*(void 0===b||null===b?performance.now():b);return N(y,f.C,2);case 2:return N(y,f.initialize(),3);case 3:e=new f.h.PacketDataList;g=M(Object.keys(a));for(k=g.next();!k.done;k=g.next())if(l=k.value,q=f.g.inputs[l]){a:{var m=f;var u=a[l];switch(q.type){case \"video\":var x=m.m[q.stream];x||(x=new Eb(m.h,m.D),m.m[q.stream]=x);m=x;0===m.l&&(m.l=m.h.createTexture());\nif(\"undefined\"!==typeof HTMLVideoElement&&u instanceof HTMLVideoElement){var z=u.videoWidth;x=u.videoHeight}else\"undefined\"!==typeof HTMLImageElement&&u instanceof HTMLImageElement?(z=u.naturalWidth,x=u.naturalHeight):(z=u.width,x=u.height);x={glName:m.l,width:z,height:x};z=m.g;z.canvas.width=x.width;z.canvas.height=x.height;z.activeTexture(z.TEXTURE0);m.h.bindTexture2d(m.l);z.texImage2D(z.TEXTURE_2D,0,z.RGBA,z.RGBA,z.UNSIGNED_BYTE,u);m.h.bindTexture2d(0);m=x;break a;case \"detections\":x=m.m[q.stream];\nx||(x=new Hb(m.h),m.m[q.stream]=x);m=x;m.data||(m.data=new m.g.DetectionListData);m.data.reset(u.length);for(x=0;x<u.length;++x){z=u[x];var E=m.data,F=E.setBoundingBox,K=x;var I=z.T;var r=new Ab;W(r,1,I.Z);W(r,2,I.$);W(r,3,I.height);W(r,4,I.width);W(r,5,I.rotation);W(r,6,I.X);var A=I=new Za;T(A,1,V(r,1));T(A,2,V(r,2));T(A,3,V(r,3));T(A,4,V(r,4));T(A,5,V(r,5));var C=V(r,6);if(null!=C&&null!=C){S(A.g,48);var p=A.g,B=C;C=0>B;B=Math.abs(B);var n=B>>>0;B=Math.floor((B-n)/4294967296);B>>>=0;C&&(B=~B>>>\n0,n=(~n>>>0)+1,4294967295<n&&(n=0,B++,4294967295<B&&(B=0)));P=n;Q=B;C=P;for(n=Q;0<n||127<C;)p.push(C&127|128),C=(C>>>7|n<<25)>>>0,n>>>=7;p.push(C)}qb(r,A);I=ab(I);F.call(E,K,I);if(z.O)for(E=0;E<z.O.length;++E)r=z.O[E],A=r.visibility?!0:!1,F=m.data,K=F.addNormalizedLandmark,I=x,r=Object.assign(Object.assign({},r),{visibility:A?r.visibility:0}),A=new wb,W(A,1,r.x),W(A,2,r.y),W(A,3,r.z),r.visibility&&W(A,4,r.visibility),p=r=new Za,T(p,1,V(A,1)),T(p,2,V(A,2)),T(p,3,V(A,3)),T(p,4,V(A,4)),T(p,5,V(A,5)),\nqb(A,p),r=ab(r),K.call(F,I,r);if(z.M)for(E=0;E<z.M.length;++E){F=m.data;K=F.addClassification;I=x;r=z.M[E];A=new sb;W(A,2,r.Y);r.index&&W(A,1,r.index);r.label&&W(A,3,r.label);r.displayName&&W(A,4,r.displayName);p=r=new Za;n=V(A,1);if(null!=n&&null!=n)if(S(p.g,8),C=p.g,0<=n)S(C,n);else{for(B=0;9>B;B++)C.push(n&127|128),n>>=7;C.push(1)}T(p,2,V(A,2));C=V(A,3);null!=C&&(C=Ea(C),S(p.g,26),S(p.g,C.length),$a(p,p.g.end()),$a(p,C));C=V(A,4);null!=C&&(C=Ea(C),S(p.g,34),S(p.g,C.length),$a(p,p.g.end()),$a(p,\nC));qb(A,p);r=ab(r);K.call(F,I,r)}}m=m.data;break a;default:m={}}}v=m;w=q.stream;switch(q.type){case \"video\":e.pushTexture2d(Object.assign(Object.assign({},v),{stream:w,timestamp:h}));break;case \"detections\":t=v;t.stream=w;t.timestamp=h;e.pushDetectionList(t);break;default:throw Error(\"Unknown input config type: '\"+q.type+\"'\");}}f.i.send(e);return N(y,f.C,4);case 4:e.delete(),y.g=0}})})};\nfunction Ub(a,b,c){return Y(a,function f(){var h,e,g,k,l,q,v=this,w,t,y,m,u,x,z,E;return O(f,function(F){switch(F.g){case 1:if(!c)return F.return(b);h={};e=0;g=M(Object.keys(c));for(k=g.next();!k.done;k=g.next())l=k.value,q=c[l],\"string\"!==typeof q&&\"texture\"===q.type&&void 0!==b[q.stream]&&++e;1<e&&(v.G=!1);w=M(Object.keys(c));k=w.next();case 2:if(k.done){F.g=4;break}t=k.value;y=c[t];if(\"string\"===typeof y)return z=h,E=t,N(F,Vb(v,t,b[y]),14);m=b[y.stream];if(\"detection_list\"===y.type){if(m){var K=\nm.getRectList();for(var I=m.getLandmarksList(),r=m.getClassificationsList(),A=[],C=0;C<K.size();++C){var p=K.get(C);a:{var B=new Ab;for(p=new Ua(p);Va(p);)switch(p.i){case 13:var n=R(p.g);W(B,1,n);break;case 21:n=R(p.g);W(B,2,n);break;case 29:n=R(p.g);W(B,3,n);break;case 37:n=R(p.g);W(B,4,n);break;case 45:n=R(p.g);W(B,5,n);break;case 48:for(var G=p.g,L=128,Ia=0,Z=n=0;4>Z&&128<=L;Z++)L=G.h[G.g++],Ia|=(L&127)<<7*Z;128<=L&&(L=G.h[G.g++],Ia|=(L&127)<<28,n|=(L&127)>>4);if(128<=L)for(Z=0;5>Z&&128<=L;Z++)L=\nG.h[G.g++],n|=(L&127)<<7*Z+3;if(128>L){G=Ia>>>0;L=n>>>0;if(n=L&2147483648)G=~G+1>>>0,L=~L>>>0,0==G&&(L=L+1>>>0);G=4294967296*L+(G>>>0);n=n?-G:G}else G.l=!0,n=void 0;W(B,6,n);break;default:if(!rb(B,p))break a}}B={Z:X(B,1),$:X(B,2),height:X(B,3),width:X(B,4),rotation:X(B,5,0),X:ob(B,6)};n=I.get(C);a:for(p=new yb,n=new Ua(n);Va(n);)switch(n.i){case 10:G=Xa(n,new wb,xb);mb(p,G,wb,void 0);break;default:if(!rb(p,n))break a}p=lb(p,wb).map(Db);G=r.get(C);a:for(n=new ub,G=new Ua(G);Va(G);)switch(G.i){case 10:n.addClassification(Xa(G,\nnew sb,tb));break;default:if(!rb(n,G))break a}B={T:B,O:p,M:Cb(n)};A.push(B)}K=A}else K=[];h[t]=K;F.g=7;break}if(\"proto_list\"===y.type){if(m){K=Array(m.size());for(I=0;I<m.size();I++)K[I]=m.get(I);m.delete()}else K=[];h[t]=K;F.g=7;break}if(void 0===m){F.g=3;break}if(\"float_list\"===y.type){h[t]=m;F.g=7;break}if(\"proto\"===y.type){h[t]=m;F.g=7;break}if(\"texture\"!==y.type)throw Error(\"Unknown output config type: '\"+y.type+\"'\");u=v.s[t];u||(u=new Eb(v.h,v.D),v.s[t]=u);return N(F,Fb(u,m,v.G),13);case 13:x=\nF.h,h[t]=x;case 7:y.transform&&h[t]&&(h[t]=y.transform(h[t]));F.g=3;break;case 14:z[E]=F.h;case 3:k=w.next();F.g=2;break;case 4:return F.return(h)}})})}function Vb(a,b,c){return Y(a,function f(){var h=this,e;return O(f,function(g){return\"number\"===typeof c||c instanceof Uint8Array||c instanceof h.h.Uint8BlobList?g.return(c):c instanceof h.h.Texture2dDataOut?(e=h.s[b],e||(e=new Eb(h.h,h.D),h.s[b]=e),g.return(Fb(e,c,h.G))):g.return(void 0)})})}\nfunction Sb(a,b){for(var c=b.name||\"$\",d=[].concat(ea(b.wants)),f=new a.h.StringList,h=M(b.wants),e=h.next();!e.done;e=h.next())f.push_back(e.value);h=a.h.PacketListener.implement({onResults:function(g){for(var k={},l=0;l<b.wants.length;++l)k[d[l]]=g.get(l);var q=a.listeners[c];q&&(a.C=Ub(a,k,b.outs).then(function(v){v=q(v);for(var w=0;w<b.wants.length;++w){var t=k[d[w]];\"object\"===typeof t&&t.hasOwnProperty&&t.hasOwnProperty(\"delete\")&&t.delete()}v&&(a.C=v)}))}});a.i.attachMultiListener(f,h);f.delete()}\nD.onResults=function(a,b){this.listeners[b||\"$\"]=a};ya(\"Solution\",Nb);ya(\"OptionType\",{BOOL:0,NUMBER:1,aa:2,0:\"BOOL\",1:\"NUMBER\",2:\"STRING\"});function Wb(a){void 0===a&&(a=0);switch(a){case 1:return\"selfie_segmentation_landscape.tflite\";default:return\"selfie_segmentation.tflite\"}}\nfunction Xb(a){var b=this;a=a||{};this.g=new Nb({locateFile:a.locateFile,files:function(c){return[{simd:!0,url:\"selfie_segmentation_solution_simd_wasm_bin.js\"},{simd:!1,url:\"selfie_segmentation_solution_wasm_bin.js\"},{data:!0,url:Wb(c.modelSelection)}]},graph:{url:\"selfie_segmentation.binarypb\"},listeners:[{wants:[\"segmentation_mask\",\"image_transformed\"],outs:{image:{type:\"texture\",stream:\"image_transformed\"},segmentationMask:{type:\"texture\",stream:\"segmentation_mask\"}}}],inputs:{image:{type:\"video\",\nstream:\"input_frames_gpu\"}},options:{useCpuInference:{type:0,graphOptionXref:{calculatorType:\"InferenceCalculator\",fieldName:\"use_cpu_inference\"},default:\"iPad Simulator;iPhone Simulator;iPod Simulator;iPad;iPhone;iPod\".split(\";\").includes(navigator.platform)||navigator.userAgent.includes(\"Mac\")&&\"ontouchend\"in document},selfieMode:{type:0,graphOptionXref:{calculatorType:\"GlScalerCalculator\",calculatorIndex:1,fieldName:\"flip_horizontal\"}},modelSelection:{type:1,graphOptionXref:{calculatorType:\"ConstantSidePacketCalculator\",\ncalculatorName:\"ConstantSidePacketCalculatorModelSelection\",fieldName:\"int_value\"},onChange:function(c){return Y(b,function f(){var h,e,g=this,k;return O(f,function(l){if(1==l.g)return h=Wb(c),e=\"third_party/mediapipe/modules/selfie_segmentation/\"+h,N(l,Qb(g.g,h),2);k=l.h;g.g.overrideFile(e,k);return l.return(!0)})})}}}})}D=Xb.prototype;D.close=function(){this.g.close();return Promise.resolve()};D.onResults=function(a){this.g.onResults(a)};\nD.initialize=function(){return Y(this,function b(){var c=this;return O(b,function(d){return N(d,c.g.initialize(),0)})})};D.reset=function(){this.g.reset()};D.send=function(a){return Y(this,function c(){var d=this;return O(c,function(f){return N(f,d.g.send(a),0)})})};D.setOptions=function(a){this.g.setOptions(a)};ya(\"SelfieSegmentation\",Xb);ya(\"VERSION\",\"0.1.1632777926\");}).call(this);", "import { markup } from \"@odoo/owl\";\n\nimport { htmlReplace, htmlReplaceAll } from \"@web/core/utils/html\";\n\n/**\n * Adds a span with a CSS class around chains of emojis in the message for styling purposes.\n *\n * Sequences of emojis are wrapped instead of individual ones to prevent compound emojis\n * such as \ud83d\udc69\ud83c\udfff = \ud83d\udc69 + \ud83c\udfff [dark skin tone character] from being separated.\n *\n * This will only match characters that have a different presentation from normal text, unlike \u00ae\n * For alternatives, see: https://www.unicode.org/reports/tr51/#Emoji_Properties_and_Data_Files\n *\n * @param {string|ReturnType<markup>} message a text message to format\n * @returns {ReturnType<markup>}\n */\nexport function formatText(message) {\n    message = htmlReplaceAll(\n        message,\n        /(\\p{Emoji_Presentation}+)/gu,\n        (_, compoundEmoji) => markup`<span class='o_mail_emoji'>${compoundEmoji}</span>`\n    );\n    message = htmlReplace(message, /(?:\\r\\n|\\r|\\n)/g, () => markup`<br>`);\n    return message;\n}\n", "import { useEffect } from \"@odoo/owl\";\nimport { patch } from \"@web/core/utils/patch\";\nimport { exprToBoolean } from \"@web/core/utils/strings\";\nimport { useDebounced } from \"@web/core/utils/timing\";\nimport { charField, CharField } from \"@web/views/fields/char/char_field\";\nimport { textField, TextField } from \"@web/views/fields/text/text_field\";\n\n/**\n * Support a key-based onchange in text fields.\n * The triggerOnChange method is debounced to run after given debounce delay\n * (or 2 seconds by default) when typing ends.\n *\n */\nconst onchangeOnKeydownMixin = () => ({\n    setup() {\n        super.setup(...arguments);\n\n        if (this.props.onchangeOnKeydown) {\n            const input = this.input || this.textareaRef;\n\n            const triggerOnChange = useDebounced(\n                this.triggerOnChange,\n                this.props.keydownDebounceDelay\n            );\n            useEffect(() => {\n                if (input.el) {\n                    input.el.addEventListener(\"keydown\", triggerOnChange);\n                    return () => {\n                        input.el.removeEventListener(\"keydown\", triggerOnChange);\n                    };\n                }\n            });\n        }\n    },\n\n    triggerOnChange() {\n        const input = this.input || this.textareaRef;\n        input.el.dispatchEvent(new Event(\"change\"));\n    },\n});\n\npatch(CharField.prototype, onchangeOnKeydownMixin());\npatch(TextField.prototype, onchangeOnKeydownMixin());\n\nCharField.props = {\n    ...CharField.props,\n    onchangeOnKeydown: { type: Boolean, optional: true },\n    keydownDebounceDelay: { type: Number, optional: true },\n};\n\nTextField.props = {\n    ...TextField.props,\n    onchangeOnKeydown: { type: Boolean, optional: true },\n    keydownDebounceDelay: { type: Number, optional: true },\n};\n\nconst charExtractProps = charField.extractProps;\ncharField.extractProps = (fieldInfo) => {\n    return Object.assign(charExtractProps(fieldInfo), {\n        onchangeOnKeydown: exprToBoolean(fieldInfo.attrs.onchange_on_keydown),\n        keydownDebounceDelay: fieldInfo.attrs.keydown_debounce_delay\n            ? Number(fieldInfo.attrs.keydown_debounce_delay)\n            : 2000,\n    });\n};\n\nconst textExtractProps = textField.extractProps;\ntextField.extractProps = (fieldInfo) => {\n    return Object.assign(textExtractProps(fieldInfo), {\n        onchangeOnKeydown: exprToBoolean(fieldInfo.attrs.onchange_on_keydown),\n        keydownDebounceDelay: fieldInfo.attrs.keydown_debounce_delay\n            ? Number(fieldInfo.attrs.keydown_debounce_delay)\n            : 2000,\n    });\n};\n", "import { ColumnProgress } from \"@web/views/view_components/column_progress\";\n\nexport class RottingColumnProgress extends ColumnProgress {\n    static template = \"mail.RottingColumnProgress\";\n    static props = {\n        ...ColumnProgress.props,\n        progressBarState: { type: Object },\n        onRotIconClicked: { type: Function },\n    };\n\n    getRottingGroupCount(group) {\n        const isRottingField = group._config.fields.is_rotting;\n        if (!isRottingField) {\n            return {};\n        }\n        return {\n            title: isRottingField.string,\n            value: group.list.records.filter((record) => record.data.is_rotting).length,\n        };\n    }\n\n    /**\n     * Checks that a filter verifying rotting status exists for the current set view.\n     * If that filter exists, it is toggled.\n     */\n    async onRottingIconClick() {\n        await this.props.onRotIconClicked(this.props.group);\n    }\n}\n", "import { patch } from \"@web/core/utils/patch\";\nimport { KanbanController } from \"@web/views/kanban/kanban_controller\";\nimport { rottingProgressBarPatch } from \"./rotting_progress_bar_hook\";\n\nexport class RottingKanbanController extends KanbanController {\n    setup() {\n        super.setup();\n        if (this.progressBarState) {\n            patch(this.progressBarState, rottingProgressBarPatch);\n        }\n    }\n\n    get progressBarAggregateFields() {\n        const res = super.progressBarAggregateFields;\n        if (this.props.fields.is_rotting) {\n            res.push(this.props.fields.is_rotting);\n        }\n        return res;\n    }\n}\n", "import { KanbanHeader } from \"@web/views/kanban/kanban_header\";\nimport { RottingColumnProgress } from \"./rotting_column_progress\";\n\nexport class RottingKanbanHeader extends KanbanHeader {\n    static template = \"mail.RottingKanbanHeader\";\n    static components = {\n        ...KanbanHeader.components,\n        ColumnProgress: RottingColumnProgress,\n    };\n\n    onRotIconClicked(group) {\n        this.props.progressBarState.toggleFilterRotten(group);\n    }\n}\n", "import { KanbanRecord } from \"@web/views/kanban/kanban_record\";\n\nexport class RottingKanbanRecord extends KanbanRecord {\n    /**\n     * @override\n     */\n    getRecordClasses() {\n        let classes = super.getRecordClasses();\n        if (this.props.record.data.is_rotting) {\n            classes += \" oe_kanban_card_rotting\";\n        }\n        return classes;\n    }\n}\n", "import { KanbanRenderer } from \"@web/views/kanban/kanban_renderer\";\nimport { RottingKanbanRecord } from \"./rotting_kanban_record\";\nimport { RottingKanbanHeader } from \"./rotting_kanban_header\";\n\nexport class RottingKanbanRenderer extends KanbanRenderer {\n    static components = {\n        ...KanbanRenderer.components,\n        KanbanRecord: RottingKanbanRecord,\n        KanbanHeader: RottingKanbanHeader,\n    };\n    /**\n     * @override\n     */\n    getGroupClasses(group, isGroupProcessing) {\n        let classes = super.getGroupClasses(group, isGroupProcessing);\n        if (this.props.progressBarState && this.props.progressBarState.rotIsFiltered[group.id]) {\n            classes += \" o_kanban_group_show o_kanban_group_show_rotting\";\n        }\n        return classes;\n    }\n}\n", "import { kanbanView } from \"@web/views/kanban/kanban_view\";\nimport { RottingKanbanController } from \"./rotting_kanban_controller\";\nimport { RottingKanbanRenderer } from \"./rotting_kanban_renderer\";\nimport { registry } from \"@web/core/registry\";\n\nexport const rottingKanbanView = {\n    ...kanbanView,\n    Controller: RottingKanbanController,\n    Renderer: RottingKanbanRenderer,\n};\n\nregistry.category(\"views\").add(\"rotting_kanban\", rottingKanbanView);\n", "export const rottingProgressBarPatch = {\n    rotIsFiltered: {},\n    async toggleFilterRotten(group) {\n        if (!this.rotIsFiltered[group.id]) {\n            await this.setFilterRotten(group);\n        } else {\n            await this.unsetFilterRotten(group);\n        }\n        group.model.notify();\n    },\n    async setFilterRotten(group) {\n        await group.applyFilter([[\"is_rotting\", \"=\", true]]);\n        this.rotIsFiltered[group.id] = group;\n        if (this.activeBars[group.serverValue]) {\n            delete this.activeBars[group.serverValue];\n        }\n    },\n    async unsetFilterRotten(group) {\n        await group.applyFilter(undefined);\n        delete this.rotIsFiltered[group.id];\n    },\n    /**\n     * @override\n     */\n    async selectBar(groupId, bar) {\n        if (this.rotIsFiltered[groupId]) {\n            delete this.rotIsFiltered[groupId];\n        }\n        return super.selectBar(groupId, bar);\n    },\n    /**\n     * @override\n     */\n    getGroupCount(group) {\n        if (this.rotIsFiltered[group.id]) {\n            return group.list.records.filter((record) => record.data.is_rotting).length;\n        }\n        return super.getGroupCount(group);\n    },\n};\n", "import {\n    statusBarDurationField,\n    StatusBarDurationField,\n} from \"@mail/views/fields/statusbar_duration/statusbar_duration_field\";\nimport { registry } from \"@web/core/registry\";\nimport { getRottingDaysTitle } from \"./rotting_widget\";\n\nexport class RottingStatusBarDurationField extends StatusBarDurationField {\n    static template = \"mail.RottingStatusBarDurationField\";\n\n    setup() {\n        super.setup();\n        this.title = getRottingDaysTitle(\n            this.env.model.config.resModel,\n            this.props.record.data.rotting_days\n        );\n    }\n}\n\nexport const rottingStatusBarDurationField = {\n    ...statusBarDurationField,\n    component: RottingStatusBarDurationField,\n};\n\nregistry.category(\"fields\").add(\"rotting_statusbar_duration\", rottingStatusBarDurationField);\n", "import { Component } from \"@odoo/owl\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { buildM2OFieldDescription, Many2OneField } from \"@web/views/fields/many2one/many2one_field\";\nimport { standardFieldProps } from \"@web/views/fields/standard_field_props\";\n\nexport function getRottingDaysTitle(modelName, rotDays) {\n    switch (modelName) {\n        case \"crm.lead\":\n            return _t(\"This lead has been stuck in this stage for %(numberOfDays)s days.\", {\n                numberOfDays: rotDays,\n            });\n        case \"hr.applicant\":\n            return _t(\"This applicant has been stuck in this stage for %(numberOfDays)s days.\", {\n                numberOfDays: rotDays,\n            });\n        case \"project.task\":\n            return _t(\"This task has been stuck in this stage for %(numberOfDays)s days.\", {\n                numberOfDays: rotDays,\n            });\n    }\n    return _t(\"This record has been stuck in this stage for %(numberOfDays)s days.\", {\n        numberOfDays: rotDays,\n    });\n}\n\nexport class KanbanRottingField extends Component {\n    static props = {\n        ...standardFieldProps,\n    };\n    static template = \"mail.KanbanRottingField\";\n\n    setup() {\n        // Preprocess all sentences as childless strings so they're easier to format in the DOM\n        this.dayCount = _t(\"%(numberOfDays)sd\", {\n            numberOfDays: this.props.record.data.rotting_days,\n        });\n\n        this.title = getRottingDaysTitle(\n            this.props.record.model.config.resModel,\n            this.props.record.data.rotting_days\n        );\n    }\n}\n\nexport class Many2OneFieldRotting extends Many2OneField {\n    static template = \"mail.Many2OneFieldRotting\";\n\n    setup() {\n        super.setup();\n        // As this widget is appended to another field's value, we display no additional title to prevent title overlap\n        this.dayCount = _t(\"%(numberOfDays)sd\", {\n            numberOfDays: this.props.record.data.rotting_days,\n        });\n    }\n}\n\nregistry.category(\"fields\").add(\"kanban.rotting\", {\n    component: KanbanRottingField,\n});\n\nregistry.category(\"fields\").add(\"list.badge_rotting\", {\n    ...buildM2OFieldDescription(Many2OneFieldRotting),\n});\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\n\nexport function manageMessages({ component, env }) {\n    const resId = component.model.root.resId;\n    if (!resId) {\n        return null; // No record\n    }\n    const description = _t(\"Messages\");\n    return {\n        type: \"item\",\n        description,\n        callback: () => {\n            env.services.action.doAction({\n                res_model: \"mail.message\",\n                name: description,\n                views: [\n                    [false, \"list\"],\n                    [false, \"form\"],\n                ],\n                type: \"ir.actions.act_window\",\n                domain: [\n                    [\"res_id\", \"=\", resId],\n                    [\"model\", \"=\", component.props.resModel],\n                ],\n                context: {\n                    default_res_model: component.props.resModel,\n                    default_res_id: resId,\n                },\n            });\n        },\n        sequence: 130,\n        section: \"record\",\n    };\n}\n\nregistry.category(\"debug\").category(\"form\").add(\"mail.manageMessages\", manageMessages);\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\n\nimport { markup } from \"@odoo/owl\";\nimport { delay } from \"@web/core/utils/concurrency\";\n\nregistry.category(\"web_tour.tours\").add(\"discuss_channel_tour\", {\n    url: \"/odoo\",\n    steps: () => [\n        {\n            isActive: [\"enterprise\"],\n            trigger: \"a[data-menu-xmlid='mail.menu_root_discuss']\",\n            content: _t(\"Open Discuss App\"),\n            tooltipPosition: \"bottom\",\n            run: \"click\",\n        },\n        {\n            trigger: \".o-mail-DiscussSearch-inputContainer\",\n            content: markup(\n                _t(\n                    \"<p>Channels make it easy to organize information across different topics and groups.</p> <p>Try to <b>create your first channel</b> (e.g. sales, marketing, product XYZ, after work party, etc).</p>\"\n                )\n            ),\n            tooltipPosition: \"bottom\",\n            run: \"click\",\n        },\n        {\n            trigger: \".o_command_palette_search input\",\n            content: markup(_t(\"<p>Create a channel here.</p>\")),\n            tooltipPosition: \"bottom\",\n            run: `edit SomeChannel_${new Date().getTime()}`,\n        },\n        {\n            trigger: \".o-mail-DiscussCommand-createChannel\",\n            content: markup(_t(\"<p>Create a public or private channel.</p>\")),\n            run: \"click\",\n            tooltipPosition: \"right\",\n        },\n        {\n            trigger: \".o-mail-Composer-input\",\n            content: markup(\n                _t(\n                    \"<p><b>Write a message</b> to the members of the channel here.</p> <p>You can notify someone with <i>'@'</i> or link another channel with <i>'#'</i>. Start your message with <i>'/'</i> to get the list of possible commands.</p>\"\n                )\n            ),\n            tooltipPosition: \"top\",\n            run: `edit SomeText_${new Date().getTime()}`,\n        },\n        {\n            trigger: \".o-mail-Composer-input\",\n            content: _t(\"Post your message on the thread\"),\n            tooltipPosition: \"top\",\n            run: \"press Enter\",\n        },\n        {\n            trigger: \".o-mail-Message[data-persistent] [title='Add Star']:not(:visible)\",\n            content: _t(\"Hover on your message and add a star\"),\n            tooltipPosition: \"top\",\n            async run(helpers) {\n                await delay(1000);\n                await helpers.click();\n            },\n        },\n        {\n            trigger: \"button[data-mailbox-id='starred']\",\n            content: _t(\n                \"Once a message has been starred, you can come back and review it at any time here.\"\n            ),\n            tooltipPosition: \"bottom\",\n            run: \"click\",\n        },\n        {\n            trigger: \".o-mail-DiscussSearch-inputContainer\",\n            content: markup(\n                _t(\n                    \"<p><b>Chat with coworkers</b> in real-time using direct messages.</p><p><i>You might need to invite users from the Settings app first.</i></p>\"\n                )\n            ),\n            tooltipPosition: \"bottom\",\n            run: \"click\",\n        },\n    ],\n});\n", "export * from \"./store\";\nexport * from \"./record\";\nexport * from \"./make_store\";\nexport { AND, OR, fields } from \"./misc\";\n", "import { markRaw, reactive, toRaw } from \"@odoo/owl\";\nimport { Store } from \"./store\";\nimport { STORE_SYM, isFieldDefinition, isMany, isRelation, modelRegistry } from \"./misc\";\nimport { Record } from \"./record\";\nimport { StoreInternal } from \"./store_internal\";\nimport { ModelInternal } from \"./model_internal\";\nimport { RecordInternal } from \"./record_internal\";\n\n/** @returns {import(\"models\").Store} */\nexport function makeStore(env, { localRegistry } = {}) {\n    const recordByLocalId = reactive(new Map());\n    // fake store for now, until it becomes a model\n    /** @type {import(\"models\").Store} */\n    let store = new Store();\n    store.env = env;\n    store.Model = Store;\n    store._ = markRaw(new StoreInternal());\n    store._raw = store;\n    store._proxyInternal = store;\n    store._proxy = store;\n    store.recordByLocalId = recordByLocalId;\n    Record.store = store;\n    /** @type {Object<string, typeof Record>} */\n    const Models = {};\n    const chosenModelRegistry = localRegistry ?? modelRegistry;\n    for (const [, _OgClass] of chosenModelRegistry.getEntries()) {\n        /** @type {typeof Record} */\n        const OgClass = _OgClass;\n        if (store[OgClass.getName()]) {\n            throw new Error(\n                `There must be no duplicated Model Names (duplicate found: ${OgClass.getName()})`\n            );\n        }\n        // classes cannot be made reactive because they are functions and they are not supported.\n        // work-around: make an object whose prototype is the class, so that static props become\n        // instance props.\n        /** @type {typeof Record} */\n        const Model = Object.create(OgClass);\n        // Produce another class with changed prototype, so that there are automatic get/set on relational fields\n        const Class = {\n            [OgClass.getName()]: class extends OgClass {\n                constructor() {\n                    super();\n                    this.setup();\n                    const record = this;\n                    record._raw = record;\n                    record.Model = Model;\n                    record._ = markRaw(\n                        record[STORE_SYM] ? new StoreInternal() : new RecordInternal()\n                    );\n                    const recordProxyInternal = new Proxy(record, {\n                        /**\n                         * @param {Record} record\n                         * @param {string} name\n                         * @param {Record} recordFullProxy\n                         */\n                        get(record, name, recordFullProxy) {\n                            recordFullProxy = record._.downgradeProxy(record, recordFullProxy);\n                            if (record._.gettingField || !Model._.fields.get(name)) {\n                                let res = Reflect.get(...arguments);\n                                if (typeof res === \"function\") {\n                                    res = res.bind(recordFullProxy);\n                                }\n                                return res;\n                            }\n                            if (Model._.fieldsCompute.get(name) && !Model._.fieldsEager.get(name)) {\n                                record._.fieldsComputeInNeed.set(name, true);\n                                if (record._.fieldsComputeOnNeed.get(name)) {\n                                    record._.compute(record, name);\n                                }\n                            }\n                            if (Model._.fieldsSort.get(name) && !Model._.fieldsEager.get(name)) {\n                                record._.fieldsSortInNeed.set(name, true);\n                                if (record._.fieldsSortOnNeed.get(name)) {\n                                    record._.sort(record, name);\n                                }\n                            }\n                            record._.gettingField = true;\n                            const val = recordFullProxy[name];\n                            record._.gettingField = false;\n                            if (isRelation(Model, name)) {\n                                const recordListFullProxy = val._proxy;\n                                if (isMany(Model, name)) {\n                                    return recordListFullProxy;\n                                }\n                                return recordListFullProxy[0];\n                            }\n                            return Reflect.get(record, name, recordFullProxy);\n                        },\n                        /**\n                         * @param {Record} record\n                         * @param {string} name\n                         */\n                        deleteProperty(record, name) {\n                            return store.MAKE_UPDATE(function recordDeleteProperty() {\n                                if (isRelation(Model, name)) {\n                                    const recordList = record[name];\n                                    recordList.clear();\n                                    return true;\n                                }\n                                return Reflect.deleteProperty(record, name);\n                            });\n                        },\n                        /**\n                         * Using record.update(data) is preferable for performance to batch process\n                         * when updating multiple fields at the same time.\n                         */\n                        set(record, name, val, receiver) {\n                            // ensure each field write goes through the updatingAttrs method exactly once\n                            if (record._.updatingAttrs.has(name)) {\n                                record[name] = val;\n                                return true;\n                            }\n                            return store.MAKE_UPDATE(function recordSet() {\n                                const reactiveSet = receiver !== record._proxyInternal;\n                                if (reactiveSet) {\n                                    record._.proxyUsed.set(name, true);\n                                }\n                                store._.updateFields(record, { [name]: val });\n                                if (reactiveSet) {\n                                    record._.proxyUsed.delete(name);\n                                }\n                                return true;\n                            });\n                        },\n                    });\n                    record._proxyInternal = recordProxyInternal;\n                    const recordProxy = reactive(recordProxyInternal);\n                    record._proxy = recordProxy;\n                    if (record?.[STORE_SYM]) {\n                        record.recordByLocalId = store.recordByLocalId;\n                        record._ = markRaw(toRaw(store._));\n                        store = record;\n                        Record.store = store;\n                    }\n                    for (const name of Model._.fields.keys()) {\n                        record._.prepareField(record, name, recordProxy);\n                    }\n                    return recordProxy;\n                }\n            },\n        }[OgClass.getName()];\n        Model._ = markRaw(new ModelInternal());\n        Object.assign(Model, {\n            Class,\n            records: reactive({}),\n        });\n        Models[Model.getName()] = Model;\n        store[Model.getName()] = Model;\n        // Detect fields with a dummy record and setup getter/setters on them\n        const obj = new OgClass();\n        obj.setup();\n        for (const [name, val] of Object.entries(obj)) {\n            if (isFieldDefinition(val)) {\n                Model._.prepareField(name, val);\n            }\n        }\n    }\n    // Sync inverse fields\n    for (const Model of Object.values(Models)) {\n        for (const name of Model._.fields.keys()) {\n            if (!isRelation(Model, name)) {\n                continue;\n            }\n            const targetModel = Model._.fieldsTargetModel.get(name);\n            const inverse = Model._.fieldsInverse.get(name);\n            if (targetModel && !Models[targetModel]) {\n                throw new Error(`No target model ${targetModel} exists`);\n            }\n            if (inverse) {\n                const OtherModel = Models[targetModel];\n                const rel2TargetModel = OtherModel._.fieldsTargetModel.get(inverse);\n                const rel2Inverse = OtherModel._.fieldsInverse.get(inverse);\n                if (rel2TargetModel && rel2TargetModel !== Model.getName()) {\n                    throw new Error(\n                        `Fields ${Models[\n                            targetModel\n                        ].getName()}.${inverse} has wrong targetModel. Expected: \"${Model.getName()}\" Actual: \"${rel2TargetModel}\"`\n                    );\n                }\n                if (rel2Inverse && rel2Inverse !== name) {\n                    throw new Error(\n                        `Fields ${Models[\n                            targetModel\n                        ].getName()}.${inverse} has wrong inverse. Expected: \"${name}\" Actual: \"${rel2Inverse}\"`\n                    );\n                }\n                OtherModel._.fieldsTargetModel.set(inverse, Model.getName());\n                OtherModel._.fieldsInverse.set(inverse, name);\n                // // FIXME: lazy fields are not working properly with inverse.\n                Model._.fieldsEager.set(name, true);\n                OtherModel._.fieldsEager.set(inverse, true);\n            }\n        }\n    }\n    /**\n     * store/_rawStore are assigned on models at next step, but they are\n     * required on Store model to make the initial store insert.\n     */\n    Object.assign(store.Store, { store, _rawStore: store });\n    // Make true store (as a model)\n    store = toRaw(store.Store.insert())._raw;\n    for (const Model of Object.values(Models)) {\n        Model._rawStore = store;\n        Model.store = store._proxy;\n        store._proxy[Model.getName()] = Model;\n    }\n    Object.assign(store, { Models, storeReady: true });\n    return store._proxy;\n}\n", "import { registry } from \"@web/core/registry\";\n\n/** @typedef {import(\"./record\").Record} Record */\n/** @typedef {import(\"./record_list\").RecordList} RecordList */\n\nexport const modelRegistry = registry.category(\"discuss.model\");\n\nexport const FIELD_DEFINITION_SYM = Symbol(\"field_definition\");\n/** @typedef {ATTR_SYM|MANY_SYM|ONE_SYM} FIELD_SYM */\nexport const ATTR_SYM = Symbol(\"attr\");\nexport const MANY_SYM = Symbol(\"many\");\nexport const ONE_SYM = Symbol(\"one\");\nexport const OR_SYM = Symbol(\"or\");\nconst AND_SYM = Symbol(\"and\");\nexport const IS_RECORD_SYM = Symbol(\"isRecord\");\nexport const IS_FIELD_SYM = Symbol(\"isField\");\n/** @deprecated equivalent to IS_DELETED_SYM */\nexport const IS_DELETING_SYM = Symbol(\"isDeleting\");\nexport const IS_DELETED_SYM = Symbol(\"isDeleted\");\nexport const STORE_SYM = Symbol(\"store\");\n\nexport function AND(...args) {\n    return [AND_SYM, ...args];\n}\nexport function OR(...args) {\n    return [OR_SYM, ...args];\n}\n\nexport function isCommand(data) {\n    return [\"ADD\", \"DELETE\", \"ADD.noinv\", \"DELETE.noinv\"].includes(data?.[0]?.[0]);\n}\n/**\n * @param {typeof import(\"./record\").Record} Model\n * @param {string} fieldName\n */\nexport function isOne(Model, fieldName) {\n    return Model._.fieldsOne.get(fieldName);\n}\n/**\n * @param {typeof import(\"./record\").Record} Model\n * @param {string} fieldName\n */\nexport function isMany(Model, fieldName) {\n    return Model._.fieldsMany.get(fieldName);\n}\n/** @param {Record} record */\nexport function isRecord(record) {\n    return Boolean(record?._?.[IS_RECORD_SYM]);\n}\n/**\n * @param {typeof import(\"./record\").Record} Model\n * @param {string} fieldName\n */\nexport function isRelation(Model, fieldName) {\n    return isMany(Model, fieldName) || isOne(Model, fieldName);\n}\nexport function isFieldDefinition(val) {\n    return val?.[FIELD_DEFINITION_SYM];\n}\n\nexport const fields = {\n    /**\n     * @template {keyof import(\"models\").Models} M\n     * @param {M} targetModel\n     * @param {Object} [param1={}]\n     * @param {(this: Record) => any} [param1.compute] if set, the value of this relational field is declarative and\n     *   is computed automatically. All reactive accesses recalls that function. The context of\n     *   the function is the record. Returned value is new value assigned to this field.\n     * @param {boolean} [param1.eager=false] when field is computed, determines whether the computation\n     *   of this field is eager or lazy. By default, fields are computed lazily, which means that\n     *   they are computed when dependencies change AND when this field is being used. In eager mode,\n     *   the field is immediately (re-)computed when dependencies changes, which matches the built-in\n     *   behaviour of OWL reactive.\n     * @param {string} [param1.inverse] if set, the name of field in targetModel that acts as the inverse.\n     * @param {(this: Record, r: import(\"models\").Models[M]) => void} [param1.onAdd] function that is called when a record is added\n     *   in the relation.\n     * @param {(this: Record, r: import(\"models\").Models[M]) => void} [param1.onDelete] function that is called when a record is removed\n     *   from the relation.\n     * @param {(this: Record) => void} [param1.onUpdate] function that is called when the field value is updated.\n     *   This is called at least once at record creation.\n     * @returns {import(\"models\").Models[M]}\n     */\n    One(targetModel, param1) {\n        return { ...param1, targetModel, [FIELD_DEFINITION_SYM]: true, [ONE_SYM]: true };\n    },\n    /**\n     * @template {keyof import(\"models\").Models} M\n     * @param {M} targetModel\n     * @param {Object} [param1={}]\n     * @param {(this: Record) => any} [param1.compute] if set, the value of this relational field is declarative and\n     *   is computed automatically. All reactive accesses recalls that function. The context of\n     *   the function is the record. Returned value is new value assigned to this field.\n     * @param {boolean} [param1.eager=false] when field is computed, determines whether the computation\n     *   of this field is eager or lazy. By default, fields are computed lazily, which means that\n     *   they are computed when dependencies change AND when this field is being used. In eager mode,\n     *   the field is immediately (re-)computed when dependencies changes, which matches the built-in\n     *   behaviour of OWL reactive.\n     * @param {string} [param1.inverse] if set, the name of field in targetModel that acts as the inverse.\n     * @param {(this: Record, r: import(\"models\").Models[M]) => void} [param1.onAdd] function that is called when a record is added\n     *   in the relation.\n     * @param {(this: Record, r: import(\"models\").Models[M]) => void} [param1.onDelete] function that is called when a record is removed\n     *   from the relation.\n     * @param {(this: Record) => void} [param1.onUpdate] function that is called when the field value is updated.\n     *   This is called at least once at record creation.\n     * @param {(this: Record, r1: import(\"models\").Models[M], r2: import(\"models\").Models[M]) => number} [param1.sort] if defined, this field\n     *   is automatically sorted by this function.\n     * @returns {import(\"models\").Models[M][]}\n     */\n    Many(targetModel, param1) {\n        return { ...param1, targetModel, [FIELD_DEFINITION_SYM]: true, [MANY_SYM]: true };\n    },\n    /**\n     * @template T\n     * @param {T} def\n     * @param {Object} [param1={}]\n     * @param {(this: Record) => any} [param1.compute] if set, the value of this attr field is declarative and\n     *   is computed automatically. All reactive accesses recalls that function. The context of\n     *   the function is the record. Returned value is new value assigned to this field.\n     * @param {boolean} [param1.eager=false] when field is computed, determines whether the computation\n     *   of this field is eager or lazy. By default, fields are computed lazily, which means that\n     *   they are computed when dependencies change AND when this field is being used. In eager mode,\n     *   the field is immediately (re-)computed when dependencies changes, which matches the built-in\n     *   behaviour of OWL reactive.\n     * @param {(this: Record) => void} [param1.onUpdate] function that is called when the field value is updated.\n     *   This is called at least once at record creation.\n     * @param {(this: Record, Object, Object) => number} [param1.sort] if defined, this field is automatically sorted\n     *   by this function.\n     * @param {'datetime'|'date'} [param1.type] if defined, automatically transform to a\n     * specific type.\n     * @returns {T}\n     */\n    Attr(def, param1) {\n        return { ...param1, [FIELD_DEFINITION_SYM]: true, [ATTR_SYM]: true, default: def };\n    },\n    /**\n     * HTML fields are ATTR that are automatically markup when the data being inserted is a markup.\n     *\n     * @param {string} def\n     * @param {Object} [param1={}]\n     * @param {(this: Record) => any} [param1.compute] if set, the value of this html field is declarative and\n     *   is computed automatically. All reactive accesses recalls that function. The context of\n     *   the function is the record. Returned value is new value assigned to this field.\n     * @param {boolean} [param1.eager=false] when field is computed, determines whether the computation\n     *   of this field is eager or lazy. By default, fields are computed lazily, which means that\n     *   they are computed when dependencies change AND when this field is being used. In eager mode,\n     *   the field is immediately (re-)computed when dependencies changes, which matches the built-in\n     *   behaviour of OWL reactive.\n     * @param {(this: Record) => void} [param1.onUpdate] function that is called when the field value is updated.\n     *   This is called at least once at record creation.\n     * @returns {string|markup }\n     */\n    Html(def, param1) {\n        const definition = {\n            ...param1,\n            [FIELD_DEFINITION_SYM]: true,\n            [ATTR_SYM]: true,\n            default: def,\n        };\n        definition.html = true;\n        return definition;\n    },\n    /**\n     * @param {Object} [param0={}]\n     * @param {(this: Record) => any} [param0.compute] if set, the value of this date field is declarative and\n     *   is computed automatically. All reactive accesses recalls that function. The context of\n     *   the function is the record. Returned value is new value assigned to this field.\n     * @param {boolean} [param0.eager=false] when field is computed, determines whether the computation\n     *   of this field is eager or lazy. By default, fields are computed lazily, which means that\n     *   they are computed when dependencies change AND when this field is being used. In eager mode,\n     *   the field is immediately (re-)computed when dependencies changes, which matches the built-in\n     *   behaviour of OWL reactive.\n     * @param {(this: Record) => void} [param0.onUpdate] function that is called when the field value is updated.\n     *   This is called at least once at record creation.\n     * @returns {luxon.DateTime}\n     */\n    Date(param0) {\n        return {\n            ...param0,\n            [FIELD_DEFINITION_SYM]: true,\n            [ATTR_SYM]: true,\n            type: \"date\",\n        };\n    },\n    /**\n     * @param {Object} [param0={}]\n     * @param {(this: Record) => any} [param0.compute] if set, the value of this datetime field is declarative and\n     *   is computed automatically. All reactive accesses recalls that function. The context of\n     *   the function is the record. Returned value is new value assigned to this field.\n     * @param {boolean} [param0.eager=false] when field is computed, determines whether the computation\n     *   of this field is eager or lazy. By default, fields are computed lazily, which means that\n     *   they are computed when dependencies change AND when this field is being used. In eager mode,\n     *   the field is immediately (re-)computed when dependencies changes, which matches the built-in\n     *   behaviour of OWL reactive.\n     * @param {(this: Record) => void} [param0.onUpdate] function that is called when the field value is updated.\n     *   This is called at least once at record creation.\n     * @returns {luxon.DateTime}\n     */\n    Datetime(param0) {\n        return {\n            ...param0,\n            [FIELD_DEFINITION_SYM]: true,\n            [ATTR_SYM]: true,\n            type: \"datetime\",\n        };\n    },\n};\n", "import { ATTR_SYM, MANY_SYM, ONE_SYM } from \"./misc\";\n\nexport class ModelInternal {\n    /** @type {Map<string, boolean>} */\n    fields = new Map();\n    /** @type {Map<string, boolean>} */\n    fieldsAttr = new Map();\n    /** @type {Map<string, boolean>} */\n    fieldsOne = new Map();\n    /** @type {Map<string, boolean>} */\n    fieldsMany = new Map();\n    /** @type {Map<string, boolean>} */\n    fieldsHtml = new Map();\n    /** @type {Map<string, string>} */\n    fieldsTargetModel = new Map();\n    /** @type {Map<string, () => any>} */\n    fieldsCompute = new Map();\n    /** @type {Map<string, boolean>} */\n    fieldsEager = new Map();\n    /** @type {Map<string, string>} */\n    fieldsInverse = new Map();\n    /** @type {Map<string, () => void>} */\n    fieldsOnAdd = new Map();\n    /** @type {Map<string, () => void>} */\n    fieldsOnDelete = new Map();\n    /** @type {Map<string, () => void>} */\n    fieldsOnUpdate = new Map();\n    /** @type {Map<string, () => number>} */\n    fieldsSort = new Map();\n    /** @type {Map<string, string>} */\n    fieldsType = new Map();\n\n    prepareField(fieldName, data) {\n        this.fields.set(fieldName, true);\n        if (data[ATTR_SYM]) {\n            this.fieldsAttr.set(fieldName, true);\n        }\n        if (data[ONE_SYM]) {\n            this.fieldsOne.set(fieldName, true);\n        }\n        if (data[MANY_SYM]) {\n            this.fieldsMany.set(fieldName, true);\n        }\n        for (const key in data) {\n            const value = data[key];\n            switch (key) {\n                case \"html\": {\n                    if (!value) {\n                        break;\n                    }\n                    this.fieldsHtml.set(fieldName, value);\n                    break;\n                }\n                case \"targetModel\": {\n                    this.fieldsTargetModel.set(fieldName, value);\n                    break;\n                }\n                case \"compute\": {\n                    this.fieldsCompute.set(fieldName, value);\n                    break;\n                }\n                case \"eager\": {\n                    if (!value) {\n                        break;\n                    }\n                    this.fieldsEager.set(fieldName, value);\n                    break;\n                }\n                case \"sort\": {\n                    this.fieldsSort.set(fieldName, value);\n                    break;\n                }\n                case \"inverse\": {\n                    this.fieldsInverse.set(fieldName, value);\n                    break;\n                }\n                case \"onAdd\": {\n                    this.fieldsOnAdd.set(fieldName, value);\n                    break;\n                }\n                case \"onDelete\": {\n                    this.fieldsOnDelete.set(fieldName, value);\n                    break;\n                }\n                case \"onUpdate\": {\n                    this.fieldsOnUpdate.set(fieldName, value);\n                    break;\n                }\n                case \"type\": {\n                    this.fieldsType.set(fieldName, value);\n                    break;\n                }\n            }\n        }\n    }\n}\n", "import { markup, toRaw } from \"@odoo/owl\";\nimport {\n    IS_DELETED_SYM,\n    OR_SYM,\n    isCommand,\n    isMany,\n    isOne,\n    isRecord,\n    isRelation,\n    modelRegistry,\n} from \"./misc\";\nimport { serializeDate, serializeDateTime } from \"@web/core/l10n/dates\";\n\n/** @typedef {import(\"./misc\").FieldDefinition} FieldDefinition */\n/** @typedef {import(\"./record_list\").RecordList} RecordList */\n/**\n * @typedef {Object} Ongoing\n * @property {Object} storeData Store insert-able data grouped by model names\n * @property {Set<string>} seenRecords A set of localIDs to track visited records\n * @property {boolean} depth Whether to recursively fetch deep data for all related records\n * @property {string[]} fields An array of field names to fetch, using dot notation (e.g., `\"persona.group_ids\"`).\n */\n\nconst Markup = markup().constructor;\n\nexport class Record {\n    /** @type {import(\"./model_internal\").ModelInternal} */\n    static _;\n    /** @type {import(\"./record_internal\").RecordInternal} */\n    _;\n    static id;\n    /** @type {import(\"@web/env\").OdooEnv} */\n    static env;\n    /** @type {import(\"@web/env\").OdooEnv} */\n    env;\n    /** @type {Object<string, Record>} */\n    static records;\n    /** @type {import(\"models\").Store} */\n    static store;\n    /** @param {() => any} fn */\n    static MAKE_UPDATE(fn) {\n        return this.store.MAKE_UPDATE(...arguments);\n    }\n    static onChange(record, name, cb) {\n        return this.store.onChange(...arguments);\n    }\n    static get(data) {\n        const Model = toRaw(this);\n        return this.records[Model.localId(data)];\n    }\n    static getName() {\n        return this._name || this.name;\n    }\n    static register(localRegistry) {\n        if (localRegistry) {\n            // Record-specific tests use local registry as to not affect other tests\n            localRegistry.add(this.getName(), this);\n        } else {\n            modelRegistry.add(this.getName(), this);\n        }\n    }\n    static localId(data) {\n        const Model = toRaw(this);\n        let idStr;\n        if (typeof data === \"object\" && data !== null) {\n            idStr = Model._localId(Model.id, data);\n        } else {\n            idStr = data; // non-object data => single id\n        }\n        return `${Model.getName()},${idStr}`;\n    }\n    get localId() {\n        return toRaw(this)._.localId;\n    }\n    static _localId(expr, data, { brackets = false } = {}) {\n        const Model = toRaw(this);\n        if (!Array.isArray(expr)) {\n            if (Model._.fields.get(expr)) {\n                if (Model._.fieldsMany.get(expr)) {\n                    throw new Error(\"Using a fields.Many() as id is not (yet) supported\");\n                }\n                if (!isRelation(Model, expr)) {\n                    return data[expr];\n                }\n                if (isCommand(data[expr])) {\n                    // Note: only fields.One is supported\n                    const [cmd, data2] = data[expr].at(-1);\n                    if (cmd === \"DELETE\") {\n                        return undefined;\n                    } else {\n                        return `(${data2?.localId})`;\n                    }\n                }\n                // relational field (note: optional when OR)\n                if (isRecord(data[expr])) {\n                    return `(${data[expr]?.localId})`;\n                }\n                const TargetModelName = Model._.fieldsTargetModel.get(expr);\n                return `(${Model.store[TargetModelName].get(data[expr])?.localId})`;\n            }\n            return data[expr];\n        }\n        const vals = [];\n        for (let i = 1; i < expr.length; i++) {\n            vals.push(Model._localId(expr[i], data, { brackets: true }));\n        }\n        let res = vals.join(expr[0] === OR_SYM ? \" OR \" : \" AND \");\n        if (brackets) {\n            res = `(${res})`;\n        }\n        return res;\n    }\n    static _retrieveIdFromData(data) {\n        const Model = toRaw(this);\n        const res = {};\n        function _deepRetrieve(expr2) {\n            if (typeof expr2 === \"string\") {\n                if (isCommand(data[expr2])) {\n                    // Note: only fields.One() is supported\n                    const [cmd, data2] = data[expr2].at(-1);\n                    return Object.assign(res, {\n                        [expr2]:\n                            cmd === \"DELETE\"\n                                ? undefined\n                                : cmd === \"DELETE.noinv\"\n                                ? [[\"DELETE.noinv\", data2]]\n                                : cmd === \"ADD.noinv\"\n                                ? [[\"ADD.noinv\", data2]]\n                                : data2,\n                    });\n                }\n                return Object.assign(res, { [expr2]: data[expr2] });\n            }\n            if (expr2 instanceof Array) {\n                for (const expr of this.id) {\n                    if (typeof expr === \"symbol\") {\n                        continue;\n                    }\n                    _deepRetrieve(expr);\n                }\n            }\n        }\n        if (Model.id === undefined) {\n            return res;\n        }\n        if (typeof Model.id === \"string\") {\n            if (typeof data !== \"object\" || data === null) {\n                return { [Model.id]: data }; // non-object data => single id\n            }\n            if (isCommand(data[Model.id])) {\n                // Note: only fields.One is supported\n                const [cmd, data2] = data[Model.id].at(-1);\n                return Object.assign(res, {\n                    [Model.id]:\n                        cmd === \"DELETE\"\n                            ? undefined\n                            : cmd === \"DELETE.noinv\"\n                            ? [[\"DELETE.noinv\", data2]]\n                            : cmd === \"ADD.noinv\"\n                            ? [[\"ADD.noinv\", data2]]\n                            : data2,\n                });\n            }\n            return { [Model.id]: data[Model.id] };\n        }\n        for (const expr of Model.id) {\n            if (typeof expr === \"symbol\") {\n                continue;\n            }\n            _deepRetrieve(expr);\n        }\n        return res;\n    }\n    /**\n     * Technical attribute, DO NOT USE in business code.\n     * This class is almost equivalent to current class of model,\n     * except this is a function, so we can new() it, whereas\n     * `this` is not, because it's an object.\n     * (in order to comply with OWL reactivity)\n     *\n     * @type {typeof Record}\n     */\n    static Class;\n    /**\n     * This method is almost equivalent to new Class, except that it properly\n     * setup relational fields of model with get/set, @see Class\n     *\n     * @returns {Record}\n     */\n    static new(data, ids) {\n        const Model = toRaw(this);\n        const store = Model._rawStore;\n        return store.MAKE_UPDATE(function RecordNew() {\n            const recordProxy = new Model.Class();\n            const record = toRaw(recordProxy)._raw;\n            Object.assign(record._, { localId: Model.localId(ids) });\n            Object.assign(recordProxy, { ...ids });\n            Model.records[record.localId] = recordProxy;\n            if (record.Model.getName() === \"Store\") {\n                Object.assign(record, {\n                    env: Model._rawStore.env,\n                    recordByLocalId: Model._rawStore.recordByLocalId,\n                });\n            }\n            Model._rawStore.recordByLocalId.set(record.localId, recordProxy);\n            for (const fieldName of record.Model._.fields.keys()) {\n                record._.requestCompute?.(record, fieldName);\n                record._.requestSort?.(record, fieldName);\n            }\n            return recordProxy;\n        });\n    }\n    /** @returns {Record|Record[]} */\n    static insert(data, options = {}) {\n        const ModelFullProxy = this;\n        const Model = toRaw(ModelFullProxy);\n        const store = Model._rawStore;\n        return store.MAKE_UPDATE(function RecordInsert() {\n            const isMulti = Array.isArray(data);\n            if (!isMulti) {\n                data = [data];\n            }\n            const res = data.map(function RecordInsertMap(d) {\n                return Model._insert.call(ModelFullProxy, d, options);\n            });\n            if (!isMulti) {\n                return res[0];\n            }\n            return res;\n        });\n    }\n    /** @returns {Record} */\n    static _insert(data) {\n        const ModelFullProxy = this;\n        const Model = toRaw(ModelFullProxy);\n        const recordFullProxy = Model.preinsert.call(ModelFullProxy, data);\n        const record = toRaw(recordFullProxy)._raw;\n        record.update.call(record._proxy, data);\n        return recordFullProxy;\n    }\n    /** @returns {Record} */\n    static preinsert(data) {\n        const ModelFullProxy = this;\n        const Model = toRaw(ModelFullProxy);\n        const ids = Model._retrieveIdFromData(data);\n        for (const name in ids) {\n            if (\n                ids[name] &&\n                !isRecord(ids[name]) &&\n                !isCommand(ids[name]) &&\n                isRelation(Model, name)\n            ) {\n                // preinsert that record in relational field,\n                // as it is required to make current local id\n                ids[name] = Model._rawStore[Model._.fieldsTargetModel.get(name)].preinsert(\n                    ids[name]\n                );\n            }\n        }\n        return Model.get.call(ModelFullProxy, data) ?? Model.new(data, ids);\n    }\n\n    /** @returns {import(\"models\").Store} */\n    get store() {\n        return toRaw(this)._raw.Model._rawStore._proxy;\n    }\n    /** @returns {import(\"models\").Store} */\n    get _rawStore() {\n        return toRaw(this)._raw.Model._rawStore;\n    }\n    /**\n     * Technical attribute, contains the Model entry in the store.\n     * This is almost the same as the class, except it's an object\n     * (so it works with OWL reactivity), and it's the actual object\n     * that store the records.\n     *\n     * Indeed, `this.constructor.records` is there to initiate `records`\n     * on the store entry, but the class `static records` is not actually\n     * used because it's non-reactive, and we don't want to persistently\n     * store records on class, to make sure different tests do not share\n     * records.\n     *\n     * @type {typeof Record}\n     */\n    Model;\n    /** @type {string} */\n    /** @type {this} */\n    _raw;\n    /** @type {this} */\n    _proxyInternal;\n    /** @type {this} */\n    _proxy;\n\n    setup() {}\n\n    update(data) {\n        const record = toRaw(this)._raw;\n        const store = record._rawStore;\n        return store.MAKE_UPDATE(function recordUpdate() {\n            if (typeof data === \"object\" && data !== null) {\n                store._.updateFields(record, data);\n            } else {\n                if (Array.isArray(record.Model.id)) {\n                    throw new Error(\n                        `Cannot insert \"${data}\" on model \"${record.Model.getName()}\": this model doesn't support single-id data!`\n                    );\n                }\n                // update on single-id data\n                store._.updateFields(record, { [record.Model.id]: data });\n            }\n        });\n    }\n\n    delete() {\n        const record = toRaw(this)._raw;\n        const store = record._rawStore;\n        return store.MAKE_UPDATE(function recordDelete() {\n            store._.ADD_QUEUE(\"delete\", record);\n        });\n    }\n\n    exists() {\n        return !this[IS_DELETED_SYM];\n    }\n\n    /** @param {Record} record */\n    eq(record) {\n        return toRaw(this)._raw === toRaw(record)?._raw;\n    }\n\n    /** @param {Record} record */\n    notEq(record) {\n        return !this.eq(record);\n    }\n\n    /** @param {Record[]|RecordList} collection */\n    in(collection) {\n        if (!collection) {\n            return false;\n        }\n        return collection.some((record) => toRaw(record)._raw.eq(this));\n    }\n\n    /** @param {Record[]|RecordList} collection */\n    notIn(collection) {\n        return !this.in(collection);\n    }\n\n    /**\n     * Converts the current record and its related data into Store insert-able data.\n     * @param {Array<string> | { depth: boolean }} options Configuration options or an array of field names.\n     * @returns {Object} A data object grouped by model names.\n     */\n    toData(options = { depth: false }) {\n        const prefix = this._getActualModelName();\n        const ongoing = {\n            seenRecords: new Set(),\n            storeData: {},\n            depth: options.depth,\n            fields: undefined,\n        };\n        if (Array.isArray(options)) {\n            ongoing.fields = options.map((field) => `${prefix}.${field}`);\n        }\n        this._toData(ongoing, prefix);\n        return ongoing.storeData;\n    }\n\n    _cleanupData(data) {\n        const fieldsToDelete = [\n            \"_\",\n            \"_fieldsValue\",\n            \"_proxy\",\n            \"_proxyInternal\",\n            \"_raw\",\n            \"env\",\n            \"Model\",\n        ];\n        fieldsToDelete.forEach((field) => delete data[field]);\n    }\n\n    _getActualModelName() {\n        return this.Model.getName();\n    }\n\n    /**\n     * @param {Ongoing} ongoing The ongoing data conversion state.\n     * @param {string} [prefix] The prefix for the current field (used for nested fields).\n     */\n    _toData(ongoing, prefix = undefined) {\n        if (ongoing.depth && ongoing.seenRecords.has(this.localId)) {\n            return;\n        }\n        ongoing.seenRecords.add(this.localId);\n\n        const recordProxy = this;\n        const record = toRaw(recordProxy)._raw;\n        const Model = record.Model;\n        const data = { ...recordProxy };\n        for (const name of Model._.fields.keys()) {\n            const fullFieldName = prefix ? `${prefix}.${name}` : name;\n            if (isMany(Model, name)) {\n                data[name] = record._proxyInternal[name].map((recordProxy) => {\n                    const record = toRaw(recordProxy)._raw;\n                    return record._toDataRelationalRecord.call(\n                        record._proxyInternal,\n                        ongoing,\n                        fullFieldName\n                    );\n                });\n            } else if (isOne(Model, name)) {\n                const otherRecord = toRaw(record._proxyInternal[name])?._raw;\n                data[name] = otherRecord?._toDataRelationalRecord.call(\n                    otherRecord._proxyInternal,\n                    ongoing,\n                    fullFieldName\n                );\n            } else {\n                // fields.Attr()\n                const value = recordProxy[name];\n                if (Model._.fieldsType.get(name) === \"datetime\" && value) {\n                    data[name] = serializeDateTime(value);\n                } else if (Model._.fieldsType.get(name) === \"date\" && value) {\n                    data[name] = serializeDate(value);\n                } else if (Model._.fieldsHtml.get(name) && value instanceof Markup) {\n                    data[name] = [\"markup\", value.toString()];\n                } else {\n                    data[name] = value;\n                }\n            }\n        }\n\n        this._cleanupData(data);\n        const pyModelName = record._getActualModelName();\n        ongoing.storeData[pyModelName] ||= [];\n        ongoing.storeData[pyModelName].push(data);\n    }\n\n    /**\n     * @param {Ongoing} ongoing The ongoing data conversion state.\n     * @param {string} prefix The prefix for the current field (used for nested fields).\n     * @returns {Object} A data object grouped by model names.\n     */\n    _toDataRelationalRecord(ongoing, prefix = undefined) {\n        const data = this.Model._retrieveIdFromData(this);\n        if (ongoing.depth || ongoing.fields?.some((field) => field.startsWith(prefix))) {\n            this._toData(ongoing, prefix);\n        }\n        for (const [name, val] of Object.entries(data)) {\n            if (isRecord(val)) {\n                data[name] = val._toDataRelationalRecord(ongoing, prefix);\n            }\n        }\n        return data;\n    }\n}\nRecord.register();\n", "/** @typedef {import(\"./record\").Record} Record */\n/** @typedef {import(\"./record_list\").RecordList} RecordList */\n\nimport { onChange } from \"@mail/utils/common/misc\";\nimport { IS_DELETED_SYM, IS_RECORD_SYM, isRelation } from \"./misc\";\nimport { RecordList } from \"./record_list\";\nimport { reactive, toRaw } from \"@odoo/owl\";\nimport { RecordUses } from \"./record_uses\";\n\nexport class RecordInternal {\n    [IS_RECORD_SYM] = true;\n    // Note: state of fields in Maps rather than object is intentional for improved performance.\n    /**\n     * For computed field, determines whether the field is computing its value.\n     *\n     * @type {Map<string, boolean>}\n     */\n    fieldsComputing = new Map();\n    /**\n     * On lazy-sorted field, determines whether the field should be (re-)sorted\n     * when it's needed (i.e. accessed). Eager sorted fields are immediately re-sorted at end of update cycle,\n     * whereas lazy sorted fields wait extra for them being needed.\n     *\n     * @type {Map<string, boolean>}\n     */\n    fieldsSortOnNeed = new Map();\n    /**\n     * On lazy sorted-fields, determines whether this field is needed (i.e. accessed).\n     *\n     * @type {Map<string, boolean>}\n     */\n    fieldsSortInNeed = new Map();\n    /**\n     * For sorted field, determines whether the field is sorting its value.\n     *\n     * @type {Map<string, boolean>}\n     */\n    fieldsSorting = new Map();\n    /**\n     * On lazy computed-fields, determines whether this field is needed (i.e. accessed).\n     *\n     * @type {Map<string, boolean>}\n     */\n    fieldsComputeInNeed = new Map();\n    /**\n     * on lazy-computed field, determines whether the field should be (re-)computed\n     * when it's needed (i.e. accessed). Eager computed fields are immediately re-computed at end of update cycle,\n     * whereas lazy computed fields wait extra for them being needed.\n     *\n     * @type {Map<string, boolean>}\n     */\n    fieldsComputeOnNeed = new Map();\n    /** @type {Map<string, () => void>} */\n    fieldsOnUpdateObserves = new Map();\n    /** @type {Map<string, this>} */\n    fieldsSortProxy2 = new Map();\n    /** @type {Map<string, this>} */\n    fieldsComputeProxy2 = new Map();\n    uses = new RecordUses();\n    updatingAttrs = new Map();\n    proxyUsed = new Map();\n    /** @type {string} */\n    localId;\n    gettingField = false;\n\n    /**\n     * @param {Record} record\n     * @param {string} fieldName\n     * @param {Record} recordProxy\n     */\n    prepareField(record, fieldName, recordProxy) {\n        const self = this;\n        const Model = toRaw(record).Model;\n        if (isRelation(Model, fieldName)) {\n            // Relational fields contain symbols for detection in original class.\n            // This constructor is called on genuine records:\n            // - 'one' fields => undefined\n            // - 'many' fields => RecordList\n            // record[name]?.[0] is ONE_SYM or MANY_SYM\n            const recordList = new RecordList();\n            Object.assign(recordList._, {\n                name: fieldName,\n                owner: record,\n            });\n            Object.assign(recordList, {\n                _raw: recordList,\n                _store: record.store,\n            });\n            record[fieldName] = recordList;\n        } else {\n            record[fieldName] = record[fieldName].default;\n        }\n        if (Model._.fieldsCompute.get(fieldName)) {\n            if (!Model._.fieldsEager.get(fieldName)) {\n                onChange(recordProxy, fieldName, () => {\n                    if (this.fieldsComputing.get(fieldName)) {\n                        /**\n                         * Use a reactive to reset the computeInNeed flag when there is\n                         * a change. This assumes when other reactive are still\n                         * observing the value, its own callback will reset the flag to\n                         * true through the proxy getters.\n                         */\n                        this.fieldsComputeInNeed.delete(fieldName);\n                    }\n                });\n                // reset flags triggered by registering onChange\n                this.fieldsComputeInNeed.delete(fieldName);\n                this.fieldsSortInNeed.delete(fieldName);\n            }\n            const cb = function computeObserver() {\n                self.requestCompute(record, fieldName);\n            };\n            const computeProxy2 = reactive(recordProxy, cb);\n            this.fieldsComputeProxy2.set(fieldName, computeProxy2);\n        }\n        if (Model._.fieldsSort.get(fieldName)) {\n            if (!Model._.fieldsEager.get(fieldName)) {\n                onChange(recordProxy, fieldName, () => {\n                    if (this.fieldsSorting.get(fieldName)) {\n                        /**\n                         * Use a reactive to reset the inNeed flag when there is a\n                         * change. This assumes if another reactive is still observing\n                         * the value, its own callback will reset the flag to true\n                         * through the proxy getters.\n                         */\n                        this.fieldsSortInNeed.delete(fieldName);\n                    }\n                });\n                // reset flags triggered by registering onChange\n                this.fieldsComputeInNeed.delete(fieldName);\n                this.fieldsSortInNeed.delete(fieldName);\n            }\n            const sortProxy2 = reactive(recordProxy, function sortObserver() {\n                self.requestSort(record, fieldName);\n            });\n            this.fieldsSortProxy2.set(fieldName, sortProxy2);\n        }\n        if (Model._.fieldsOnUpdate.get(fieldName)) {\n            const store = Model.store;\n            store._onChange(recordProxy, fieldName, (obs) => {\n                this.fieldsOnUpdateObserves.set(fieldName, obs);\n                if (store._.UPDATE !== 0) {\n                    store._.ADD_QUEUE(\"onUpdate\", record, fieldName);\n                } else {\n                    this.onUpdate(record, fieldName);\n                }\n            });\n        }\n    }\n\n    requestCompute(record, fieldName, { force = false } = {}) {\n        if (record[IS_DELETED_SYM]) {\n            return;\n        }\n        const Model = record.Model;\n        if (!Model._.fieldsCompute.get(fieldName)) {\n            return;\n        }\n        const store = record._rawStore;\n        if (store._.UPDATE !== 0 && !force) {\n            store._.ADD_QUEUE(\"compute\", record, fieldName);\n        } else {\n            if (Model._.fieldsEager.get(fieldName) || this.fieldsComputeInNeed.get(fieldName)) {\n                this.compute(record, fieldName);\n            } else {\n                this.fieldsComputeOnNeed.set(fieldName, true);\n            }\n        }\n    }\n    requestSort(record, fieldName, { force } = {}) {\n        if (record[IS_DELETED_SYM]) {\n            return;\n        }\n        const Model = record.Model;\n        if (!Model._.fieldsSort.get(fieldName)) {\n            return;\n        }\n        const store = record._rawStore;\n        if (store._.UPDATE !== 0 && !force) {\n            store._.ADD_QUEUE(\"sort\", record, fieldName);\n        } else {\n            if (Model._.fieldsEager.get(fieldName) || this.fieldsSortInNeed.get(fieldName)) {\n                this.sort(record, fieldName);\n            } else {\n                this.fieldsSortOnNeed.set(fieldName, true);\n            }\n        }\n    }\n    /**\n     * @param {Record} record\n     * @param {string} fieldName\n     */\n    compute(record, fieldName) {\n        const Model = record.Model;\n        const store = record._rawStore;\n        this.fieldsComputing.set(fieldName, true);\n        this.fieldsComputeOnNeed.delete(fieldName);\n        let computedValue;\n        try {\n            computedValue = Model._.fieldsCompute\n                .get(fieldName)\n                .call(this.fieldsComputeProxy2.get(fieldName));\n        } catch (err) {\n            store.handleError(err);\n        }\n        store._.updateFields(record, {\n            [fieldName]: computedValue,\n        });\n        this.fieldsComputing.delete(fieldName);\n    }\n    /**\n     * @param {Record} record\n     * @param {string} fieldName\n     */\n    sort(record, fieldName) {\n        const Model = record.Model;\n        if (!Model._.fieldsSort.get(fieldName)) {\n            return;\n        }\n        const store = record._rawStore;\n        this.fieldsSortOnNeed.delete(fieldName);\n        this.fieldsSorting.set(fieldName, true);\n        const proxy2Sort = this.fieldsSortProxy2.get(fieldName);\n        const func = Model._.fieldsSort.get(fieldName).bind(proxy2Sort);\n        if (isRelation(Model, fieldName)) {\n            try {\n                store._.sortRecordList(proxy2Sort[fieldName]._proxy, func);\n            } catch (err) {\n                store.handleError(err);\n            }\n        } else {\n            // sort on copy of list so that reactive observers not triggered while sorting\n            const copy = [...proxy2Sort[fieldName]];\n            copy.sort(func);\n            const hasChanged = copy.some((item, index) => item !== record[fieldName][index]);\n            if (hasChanged) {\n                proxy2Sort[fieldName] = copy;\n            }\n        }\n        this.fieldsSorting.delete(fieldName);\n    }\n    onUpdate(record, fieldName) {\n        const store = record._rawStore;\n        const Model = record.Model;\n        if (!Model._.fieldsOnUpdate.get(fieldName)) {\n            return;\n        }\n        /**\n         * Forward internal proxy for performance as onUpdate does not\n         * need reactive (observe is called separately).\n         */\n        try {\n            Model._.fieldsOnUpdate.get(fieldName).call(record._proxyInternal);\n        } catch (err) {\n            store.handleError(err);\n        }\n        this.fieldsOnUpdateObserves.get(fieldName)?.();\n    }\n    /**\n     * The internal reactive is only necessary to trigger outer reactives when\n     * writing on it. As it has no callback, reading through it has no effect,\n     * except slowing down performance and complexifying the stack.\n     */\n    downgradeProxy(record, fullProxy) {\n        return record._proxy === fullProxy ? record._proxyInternal : fullProxy;\n    }\n}\n", "import { markRaw, reactive, toRaw } from \"@odoo/owl\";\nimport { isRecord } from \"./misc\";\n\n/** @param {RecordList} reclist */\nfunction getInverse(reclist) {\n    return reclist._.owner.Model._.fieldsInverse.get(reclist._.name);\n}\n\n/** @param {RecordList} reclist */\nfunction getTargetModel(reclist) {\n    return reclist._.owner.Model._.fieldsTargetModel.get(reclist._.name);\n}\n\n/** @param {RecordList} reclist */\nfunction isComputeField(reclist) {\n    return reclist._.owner.Model._.fieldsCompute.get(reclist._.name);\n}\n\n/** @param {RecordList} reclist */\nfunction isSortField(reclist) {\n    return reclist._.owner.Model._.fieldsSort.get(reclist._.name);\n}\n\n/** @param {RecordList} reclist */\nfunction isEager(reclist) {\n    return reclist._.owner.Model._.fieldsEager.get(reclist._.name);\n}\n\n/** @param {RecordList} reclist */\nfunction setComputeInNeed(reclist) {\n    reclist._.owner._.fieldsComputeInNeed.set(reclist._.name, true);\n}\n\n/** @param {RecordList} reclist */\nfunction setSortInNeed(reclist) {\n    reclist._.owner._.fieldsSortInNeed.set(reclist._.name, true);\n}\n\n/** @param {RecordList} reclist */\nfunction isComputeOnNeed(reclist) {\n    return reclist._.owner._.fieldsComputeOnNeed.get(reclist._.name);\n}\n\n/** @param {RecordList} reclist */\nfunction isSortOnNeed(reclist) {\n    return reclist._.owner._.fieldsSortOnNeed.get(reclist._.name);\n}\n\n/** @param {RecordList} reclist */\nfunction computeField(reclist) {\n    reclist._.owner._.compute(reclist._.owner, reclist._.name);\n}\n\n/** @param {RecordList} reclist */\nfunction sortField(reclist) {\n    reclist._.owner._.sort(reclist._.owner, reclist._.name);\n}\n\n/** @param {RecordList} reclist */\nfunction isOne(reclist) {\n    return reclist._.owner.Model._.fieldsOne.get(reclist._.name);\n}\n\nexport class RecordListInternal {\n    /** @type {string} */\n    name;\n    /** @type {Record} */\n    owner;\n\n    /**\n     * Version of add() that does not update the inverse.\n     * This is internally called when inserting (with intent to add)\n     * on relational field with inverse, to prevent infinite loops.\n     *\n     * @param {RecordList} recordList\n     * @param {...Record}\n     */\n    addNoinv(recordList, ...records) {\n        const self = this;\n        const store = recordList._store;\n        if (isOne(recordList)) {\n            const last = records.at(-1);\n            if (isRecord(last) && last.in(recordList)) {\n                return;\n            }\n            const record = self.insert(\n                recordList,\n                last,\n                function recordList_AddNoInvOneInsert(record) {\n                    if (record.localId !== recordList.data[0]) {\n                        const old = recordList._proxy.at(-1);\n                        recordList._proxy.data.pop();\n                        old?._.uses.delete(recordList);\n                        recordList._proxy.data.push(record.localId);\n                        self.syncLength(recordList);\n                        record._.uses.add(recordList);\n                    }\n                },\n                { inv: false }\n            );\n            store._.ADD_QUEUE(\"onAdd\", self.owner, self.name, record);\n            return;\n        }\n        for (const val of records) {\n            if (isRecord(val) && val.in(recordList)) {\n                continue;\n            }\n            const record = self.insert(\n                recordList,\n                val,\n                function recordList_AddNoInvManyInsert(record) {\n                    if (recordList.data.indexOf(record.localId) === -1) {\n                        recordList._proxy.data.push(record.localId);\n                        self.syncLength(recordList);\n                        record._.uses.add(recordList);\n                    }\n                },\n                { inv: false }\n            );\n            store._.ADD_QUEUE(\"onAdd\", self.owner, self.name, record);\n        }\n    }\n    /** @param {R[]|any[]} data */\n    assign(recordList, data) {\n        const self = this;\n        const store = recordList._store;\n        return store.MAKE_UPDATE(function recordListAssign() {\n            /** @type {Record[]|Set<Record>|RecordList<Record|any[]>} */\n            const collection = isRecord(data) ? [data] : data;\n            // data and collection could be same record list,\n            // save before clear to not push mutated recordlist that is empty\n            const vals = [...collection];\n            const oldRecords = recordList._proxyInternal.slice\n                .call(recordList._proxy)\n                .map((recordProxy) => toRaw(recordProxy)._raw);\n            const newRecords = vals.map((val) =>\n                self.insert(recordList, val, function recordListAssignInsert(record) {\n                    if (record.notIn(oldRecords)) {\n                        record._.uses.add(recordList);\n                        store._.ADD_QUEUE(\"onAdd\", self.owner, self.name, record);\n                    }\n                })\n            );\n            const inverse = getInverse(recordList);\n            for (const oldRecord of oldRecords) {\n                if (oldRecord.notIn(newRecords)) {\n                    oldRecord._.uses.delete(recordList);\n                    store._.ADD_QUEUE(\"onDelete\", self.owner, self.name, oldRecord);\n                    if (inverse) {\n                        oldRecord[inverse].delete(self.owner);\n                    }\n                }\n            }\n            recordList._proxy.data = newRecords.map((newRecord) => newRecord.localId);\n            recordList._.syncLength(recordList);\n        });\n    }\n    /**\n     * Version of delete() that does not update the inverse.\n     * This is internally called when inserting (with intent to delete)\n     * on relational field with inverse, to prevent infinite loops.\n     *\n     * @param {RecordList} recordList\n     * @param {...Record}\n     */\n    deleteNoinv(recordList, ...records) {\n        const self = this;\n        const store = recordList._store;\n        for (const val of records) {\n            const record = this.insert(\n                recordList,\n                val,\n                function recordList_DeleteNoInv_Insert(record) {\n                    const index = recordList.data.indexOf(record.localId);\n                    if (index !== -1) {\n                        const old = recordList._proxy.at(-1);\n                        recordList.splice.call(recordList._proxy, index, 1);\n                        self.syncLength(recordList);\n                        old._.uses.delete(recordList);\n                    }\n                },\n                { inv: false }\n            );\n            store._.ADD_QUEUE(\"onDelete\", self.owner, self.name, record);\n        }\n    }\n    /**\n     * The internal reactive is only necessary to trigger outer reactives when\n     * writing on it. As it has no callback, reading through it has no effect,\n     * except slowing down performance and complexifying the stack.\n     *\n     * @param {RecordList} recordList\n     * @param {RecordList} fullProxy\n     */\n    downgradeProxy(recordList, fullProxy) {\n        return recordList._proxy === fullProxy ? recordList._proxyInternal : fullProxy;\n    }\n    /**\n     * @param {RecordList} recordList\n     * @param {R|any} val\n     * @param {(R) => void} [fn] function that is called in-between preinsert and\n     *   insert. Preinsert only inserted what's needed to make record, while\n     *   insert finalize with all remaining data.\n     * @param {boolean} [inv=true] whether the inverse should be added or not.\n     *   It is always added except when during an insert on a relational field,\n     *   in order to avoid infinite loop.\n     * @param {\"ADD\"|\"DELETE} [mode=\"ADD\"] the mode of insert on the relation.\n     *   Important to match the inverse. Most of the time it's \"ADD\", that is when\n     *   inserting the relation the inverse should be added. Exception when the insert\n     *   comes from deletion, we want to \"DELETE\".\n     */\n    insert(recordList, val, fn, { inv = true, mode = \"ADD\" } = {}) {\n        const inverse = getInverse(recordList);\n        const targetModel = getTargetModel(recordList);\n        if (typeof val !== \"object\") {\n            if (Array.isArray(recordList._store[targetModel].id)) {\n                throw new Error(\n                    `Cannot insert \"${val}\" on relational field \"${recordList._.owner.Model.getName()}/${\n                        recordList._.name\n                    }\": target model \"${targetModel}\" doesn't support single-id data!`\n                );\n            }\n            // single-id data\n            val = { [recordList._store[targetModel].id]: val };\n        }\n        if (inverse && inv) {\n            // special command to call addNoinv/deleteNoInv, to prevent infinite loop\n            const target = isRecord(val) && val._raw === val ? val._proxy : val;\n            target[inverse] = [[mode === \"ADD\" ? \"ADD.noinv\" : \"DELETE.noinv\", recordList._.owner]];\n        }\n        /** @type {R} */\n        let newRecordProxy;\n        if (!isRecord(val)) {\n            newRecordProxy = recordList._store[targetModel].preinsert(val);\n        } else {\n            newRecordProxy = val;\n        }\n        const newRecord = toRaw(newRecordProxy)._raw;\n        fn?.(newRecord);\n        if (!isRecord(val)) {\n            // was preinserted, fully insert now\n            recordList._store[targetModel].insert(val);\n        }\n        return newRecord;\n    }\n    /**\n     * Sync reclist.data length with array length, as to not introduce confusion while debugging\n     *\n     * @param {RecordList} reclist\n     */\n    syncLength(reclist) {\n        reclist.length = reclist.data.length;\n    }\n}\n\n/** * @template {Record} R */\nexport class RecordList extends Array {\n    /** @type {import(\"models\").Store} */\n    _store;\n    /** @type {string[]} */\n    data = [];\n    /** @type {this} */\n    _raw;\n    /** @type {this} */\n    _proxyInternal;\n    /** @type {this} */\n    _proxy;\n    _ = markRaw(new RecordListInternal());\n\n    constructor() {\n        super();\n        const recordList = this;\n        recordList._raw = recordList;\n        const recordListProxyInternal = new Proxy(recordList, {\n            /** @param {RecordList<R>} receiver */\n            get(recordList, name, recordListFullProxy) {\n                recordListFullProxy = recordList._.downgradeProxy(recordList, recordListFullProxy);\n                if (\n                    typeof name === \"symbol\" ||\n                    Object.keys(recordList).includes(name) ||\n                    Object.prototype.hasOwnProperty.call(recordList.constructor.prototype, name)\n                ) {\n                    let res = Reflect.get(...arguments);\n                    if (typeof res === \"function\") {\n                        res = res.bind(recordListFullProxy);\n                    }\n                    return res;\n                }\n                if (isComputeField(recordList) && !isEager(recordList)) {\n                    setComputeInNeed(recordList);\n                    if (isComputeOnNeed(recordList)) {\n                        computeField(recordList);\n                    }\n                }\n                if (name === \"length\") {\n                    return recordListFullProxy.data.length;\n                }\n                if (isSortField(recordList) && !isEager(recordList)) {\n                    setSortInNeed(recordList);\n                    if (isSortOnNeed(recordList)) {\n                        sortField(recordList);\n                    }\n                }\n                if (typeof name !== \"symbol\" && !window.isNaN(parseInt(name))) {\n                    // support for \"array[index]\" syntax\n                    const index = parseInt(name);\n                    return recordListFullProxy._store.recordByLocalId.get(\n                        recordListFullProxy.data[index]\n                    );\n                }\n                // Attempt an unimplemented array method call\n                const array = [...recordList[Symbol.iterator].call(recordListFullProxy)];\n                return array[name]?.bind(array);\n            },\n            /** @param {RecordList<R>} recordListProxy */\n            set(recordList, name, val, recordListProxy) {\n                const store = recordList._store;\n                return store.MAKE_UPDATE(function recordListSet() {\n                    if (typeof name !== \"symbol\" && !window.isNaN(parseInt(name))) {\n                        // support for \"array[index] = r3\" syntax\n                        const index = parseInt(name);\n                        recordList._.insert(\n                            recordList,\n                            val,\n                            function recordListSet_Insert(newRecord) {\n                                const oldRecord = toRaw(\n                                    toRaw(recordList._store.recordByLocalId).get(\n                                        recordList.data[index]\n                                    )\n                                )._raw;\n                                recordListProxy.data[index] = newRecord?.localId;\n                                if (oldRecord && oldRecord.notEq(newRecord)) {\n                                    oldRecord._.uses.delete(recordList);\n                                }\n                                store._.ADD_QUEUE(\n                                    \"onDelete\",\n                                    recordList._.owner,\n                                    recordList._.name,\n                                    oldRecord\n                                );\n                                const inverse = getInverse(recordList);\n                                if (inverse) {\n                                    oldRecord[inverse].delete(recordList._.owner);\n                                }\n                                if (newRecord) {\n                                    newRecord._.uses.add(recordList);\n                                    store._.ADD_QUEUE(\n                                        \"onAdd\",\n                                        recordList._.owner,\n                                        recordList._.name,\n                                        newRecord\n                                    );\n                                    if (inverse) {\n                                        newRecord[inverse].add?.(recordList._.owner);\n                                    }\n                                }\n                            }\n                        );\n                    } else if (name === \"length\") {\n                        const newLength = parseInt(val);\n                        if (newLength !== recordList.data.length) {\n                            if (newLength < recordList.data.length) {\n                                recordList.splice.call(\n                                    recordListProxy,\n                                    newLength,\n                                    recordList.length - newLength\n                                );\n                            }\n                            recordListProxy.data.length = newLength;\n                            recordList._.syncLength(recordList);\n                        }\n                    } else {\n                        return Reflect.set(recordList, name, val, recordListProxy);\n                    }\n                    return true;\n                });\n            },\n        });\n        recordList._proxyInternal = recordListProxyInternal;\n        recordList._proxy = reactive(recordListProxyInternal);\n        return recordList;\n    }\n    /** @param {R[]} records */\n    push(...records) {\n        const recordList = toRaw(this)._raw;\n        const recordListFullProxy = recordList._.downgradeProxy(recordList, this);\n        const store = recordList._store;\n        return store.MAKE_UPDATE(function recordListPush() {\n            for (const val of records) {\n                const record = recordList._.insert(\n                    recordList,\n                    val,\n                    function recordListPushInsert(record) {\n                        recordList._proxy.data.push(record.localId);\n                        recordList._.syncLength(recordList);\n                        record._.uses.add(recordList);\n                    }\n                );\n                store._.ADD_QUEUE(\"onAdd\", recordList._.owner, recordList._.name, record);\n                const inverse = getInverse(recordList);\n                if (inverse) {\n                    record[inverse].add(recordList._.owner);\n                }\n            }\n            return recordListFullProxy.data.length;\n        });\n    }\n    /** @returns {R} */\n    pop() {\n        const recordList = toRaw(this)._raw;\n        const recordListFullProxy = recordList._.downgradeProxy(recordList, this);\n        const store = recordList._store;\n        return store.MAKE_UPDATE(function recordListPop() {\n            /** @type {R} */\n            const oldRecordProxy = recordListFullProxy.at(-1);\n            if (oldRecordProxy) {\n                recordList.splice.call(recordListFullProxy, recordListFullProxy.length - 1, 1);\n            }\n            return oldRecordProxy;\n        });\n    }\n    /** @returns {R} */\n    shift() {\n        const recordList = toRaw(this)._raw;\n        const recordListFullProxy = recordList._.downgradeProxy(recordList, this);\n        const store = recordList._store;\n        return store.MAKE_UPDATE(function recordListShift() {\n            const recordProxy = recordListFullProxy._store.recordByLocalId.get(\n                recordListFullProxy.data.shift()\n            );\n            recordList._.syncLength(recordList);\n            if (!recordProxy) {\n                return;\n            }\n            const record = toRaw(recordProxy)._raw;\n            record._.uses.delete(recordList);\n            store._.ADD_QUEUE(\"onDelete\", recordList._.owner, recordList._.name, record);\n            const inverse = getInverse(recordList);\n            if (inverse) {\n                record[inverse].delete(recordList._.owner);\n            }\n            return recordProxy;\n        });\n    }\n    /** @param {R[]} records */\n    unshift(...records) {\n        const recordList = toRaw(this)._raw;\n        const recordListFullProxy = recordList._.downgradeProxy(recordList, this);\n        const store = recordList._store;\n        return store.MAKE_UPDATE(function recordListUnshift() {\n            for (let i = records.length - 1; i >= 0; i--) {\n                const record = recordList._.insert(recordList, records[i], (record) => {\n                    recordList._proxy.data.unshift(record.localId);\n                    recordList._.syncLength(recordList);\n                    record._.uses.add(recordList);\n                });\n                store._.ADD_QUEUE(\"onAdd\", recordList._.owner, recordList._.name, record);\n                const inverse = getInverse(recordList);\n                if (inverse) {\n                    record[inverse].add(recordList._.owner);\n                }\n            }\n            return recordListFullProxy.data.length;\n        });\n    }\n    /** @param {R} recordProxy */\n    indexOf(recordProxy) {\n        const recordList = toRaw(this)._raw;\n        const recordListFullProxy = recordList._.downgradeProxy(recordList, this);\n        return recordListFullProxy.data.indexOf(toRaw(recordProxy)?._raw.localId);\n    }\n    /**\n     * @param {number} [start]\n     * @param {number} [deleteCount]\n     * @param {...R} [newRecordsProxy]\n     */\n    splice(start, deleteCount, ...newRecordsProxy) {\n        const recordList = toRaw(this)._raw;\n        const recordListFullProxy = recordList._.downgradeProxy(recordList, this);\n        const store = recordList._store;\n        return store.MAKE_UPDATE(function recordListSplice() {\n            const oldRecordsProxy = recordList._proxyInternal.slice.call(\n                recordListFullProxy,\n                start,\n                start + deleteCount\n            );\n            const list = recordListFullProxy.data.slice(); // splice on copy of list so that reactive observers not triggered while splicing\n            list.splice(\n                start,\n                deleteCount,\n                ...newRecordsProxy.map((newRecordProxy) => toRaw(newRecordProxy)._raw.localId)\n            );\n            if (isOne(recordList) && start === 0 && deleteCount === 1) {\n                // avoid replacing whole list, to avoid triggering observers too much\n                if (list.length === 0) {\n                    recordList._proxy.data.pop();\n                } else {\n                    recordList._proxy.data[0] = list[0];\n                }\n            } else {\n                recordList._proxy.data = list;\n            }\n            recordList._.syncLength(recordList);\n            for (const oldRecordProxy of oldRecordsProxy) {\n                const oldRecord = toRaw(oldRecordProxy)._raw;\n                oldRecord._.uses.delete(recordList);\n                store._.ADD_QUEUE(\"onDelete\", recordList._.owner, recordList._.name, oldRecord);\n                const inverse = getInverse(recordList);\n                if (inverse) {\n                    oldRecord[inverse].delete(recordList._.owner);\n                }\n            }\n            for (const newRecordProxy of newRecordsProxy) {\n                const newRecord = toRaw(newRecordProxy)._raw;\n                newRecord._.uses.add(recordList);\n                store._.ADD_QUEUE(\"onAdd\", recordList._.owner, recordList._.name, newRecord);\n                const inverse = getInverse(recordList);\n                if (inverse) {\n                    newRecord[inverse].add(recordList._.owner);\n                }\n            }\n        });\n    }\n    /** @param {(a: R, b: R) => boolean} func */\n    sort(func) {\n        const recordList = toRaw(this)._raw;\n        const recordListFullProxy = recordList._.downgradeProxy(recordList, this);\n        const store = recordList._store;\n        return store.MAKE_UPDATE(function recordListSort() {\n            recordList._store._.sortRecordList(recordListFullProxy, func);\n            return recordListFullProxy;\n        });\n    }\n    /** @param {...R[]|...RecordList[R]} collections */\n    concat(...collections) {\n        const recordList = toRaw(this)._raw;\n        const recordListFullProxy = recordList._.downgradeProxy(recordList, this);\n        return recordListFullProxy.data\n            .map((localId) => recordListFullProxy._store.recordByLocalId.get(localId))\n            .concat(...collections.map((c) => [...c]));\n    }\n    /**\n     * @param {...R}\n     * @returns {R|R[]} the added record(s)\n     */\n    add(...records) {\n        const recordList = toRaw(this)._raw;\n        const store = recordList._store;\n        return store.MAKE_UPDATE(function recordListAdd() {\n            if (isOne(recordList)) {\n                const last = records.at(-1);\n                if (isRecord(last) && recordList.data.includes(toRaw(last)._raw.localId)) {\n                    return last;\n                }\n                return recordList._.insert(\n                    recordList,\n                    last,\n                    function recordListAddInsertOne(record) {\n                        if (record.localId !== recordList.data[0]) {\n                            recordList.splice.call(recordList._proxy, 0, 1, record);\n                        }\n                    }\n                );\n            }\n            const res = [];\n            for (const val of records) {\n                if (isRecord(val) && recordList.data.includes(val.localId)) {\n                    continue;\n                }\n                const rec = recordList._.insert(\n                    recordList,\n                    val,\n                    function recordListAddInsertMany(record) {\n                        if (recordList.data.indexOf(record.localId) === -1) {\n                            recordList.push.call(recordList._proxy, record);\n                        }\n                    }\n                );\n                res.push(rec);\n            }\n            return res.length === 1 ? res[0] : res;\n        });\n    }\n    /** @param {...R}  */\n    delete(...records) {\n        const recordList = toRaw(this)._raw;\n        const store = recordList._store;\n        return store.MAKE_UPDATE(function recordListDelete() {\n            for (const val of records) {\n                recordList._.insert(\n                    recordList,\n                    val,\n                    function recordListDelete_Insert(record) {\n                        const index = recordList.data.indexOf(record.localId);\n                        if (index !== -1) {\n                            recordList.splice.call(recordList._proxy, index, 1);\n                        }\n                    },\n                    { mode: \"DELETE\" }\n                );\n            }\n        });\n    }\n    clear() {\n        const recordList = toRaw(this)._raw;\n        const store = recordList._store;\n        return store.MAKE_UPDATE(function recordListClear() {\n            while (recordList.data.length > 0) {\n                recordList.pop.call(recordList._proxy);\n            }\n        });\n    }\n    /** @yields {R} */\n    *[Symbol.iterator]() {\n        const recordList = toRaw(this)._raw;\n        const recordListFullProxy = recordList._.downgradeProxy(recordList, this);\n        for (const localId of recordListFullProxy.data) {\n            yield recordListFullProxy._store.recordByLocalId.get(localId);\n        }\n    }\n    /** @param {number} index */\n    at(index) {\n        // this custom implement of \"at\" is slightly faster than auto-calling unimplement array method\n        const recordList = toRaw(this)._raw;\n        const recordListFullProxy = recordList._.downgradeProxy(recordList, this);\n        return recordListFullProxy._store.recordByLocalId.get(recordListFullProxy.data.at(index));\n    }\n}\n", "export class RecordUses {\n    /**\n     * Track the uses of a record. Each record contains a single `RecordUses`:\n     * - Key: localId of record that uses current record\n     * - Value: Map where key is relational field name, and value is number\n     *          of time current record is present in this relation.\n     *\n     * @type {Map<string, Map<string, number>>}}\n     */\n    data = new Map();\n    /** @param {RecordList} list */\n    add(list) {\n        const record = list._.owner;\n        if (!this.data.has(record.localId)) {\n            this.data.set(record.localId, new Map());\n        }\n        const use = this.data.get(record.localId);\n        if (!use.get(list._.name)) {\n            use.set(list._.name, 0);\n        }\n        use.set(list._.name, use.get(list._.name) + 1);\n    }\n    /** @param {RecordList} list */\n    delete(list) {\n        const record = list._.owner;\n        if (!this.data.has(record.localId)) {\n            return;\n        }\n        const use = this.data.get(record.localId);\n        if (!use.get(list._.name)) {\n            return;\n        }\n        use.set(list._.name, use.get(list._.name) - 1);\n        if (use.get(list._.name) === 0) {\n            use.delete(list._.name);\n        }\n    }\n}\n", "import { Record } from \"./record\";\nimport { STORE_SYM, modelRegistry } from \"./misc\";\nimport { reactive, toRaw } from \"@odoo/owl\";\n\n/** @typedef {import(\"./record_list\").RecordList} RecordList */\n\nexport const storeInsertFns = {\n    makeContext(store) {},\n    getActualModelName(store, ctx, pyOrJsModelName) {\n        return pyOrJsModelName;\n    },\n    getExtraFieldsFromModel(store) {},\n};\n\nexport class Store extends Record {\n    /** @type {import(\"./store_internal\").StoreInternal} */\n    _;\n    [STORE_SYM] = true;\n    /** @type {Map<string, Record>} */\n    recordByLocalId;\n    storeReady = false;\n    /**\n     * @param {string} localId\n     * @returns {Record}\n     */\n    get(localId) {\n        return this.recordByLocalId.get(localId);\n    }\n\n    handleError(err) {\n        this._.ERRORS.push(err);\n    }\n\n    warnErrors = true;\n\n    /** @param {() => any} fn */\n    MAKE_UPDATE(fn) {\n        this._.UPDATE++;\n        let res;\n        try {\n            res = fn();\n        } catch (err) {\n            this.handleError(err);\n        }\n        this._.UPDATE--;\n        const deletingRecordsByLocalId = new Map();\n        if (this._.UPDATE === 0) {\n            // pretend an increased update cycle so that nothing in queue creates many small update cycles\n            this._.UPDATE++;\n            while (\n                this._.FC_QUEUE.size > 0 ||\n                this._.FS_QUEUE.size > 0 ||\n                this._.FA_QUEUE.size > 0 ||\n                this._.FD_QUEUE.size > 0 ||\n                this._.FU_QUEUE.size > 0 ||\n                this._.RO_QUEUE.size > 0 ||\n                this._.RD_QUEUE.size > 0 ||\n                this._.RHD_QUEUE.size > 0\n            ) {\n                const FC_QUEUE = new Map(this._.FC_QUEUE);\n                const FS_QUEUE = new Map(this._.FS_QUEUE);\n                const FA_QUEUE = new Map(this._.FA_QUEUE);\n                const FD_QUEUE = new Map(this._.FD_QUEUE);\n                const FU_QUEUE = new Map(this._.FU_QUEUE);\n                const RO_QUEUE = new Map(this._.RO_QUEUE);\n                const RD_QUEUE = new Map(this._.RD_QUEUE);\n                const RHD_QUEUE = new Map(this._.RHD_QUEUE);\n                this._.FC_QUEUE.clear();\n                this._.FS_QUEUE.clear();\n                this._.FA_QUEUE.clear();\n                this._.FD_QUEUE.clear();\n                this._.FU_QUEUE.clear();\n                this._.RO_QUEUE.clear();\n                this._.RD_QUEUE.clear();\n                this._.RHD_QUEUE.clear();\n                while (FC_QUEUE.size > 0) {\n                    /** @type {[Record, Map<string, true>]} */\n                    const [record, recMap] = FC_QUEUE.entries().next().value;\n                    FC_QUEUE.delete(record);\n                    for (const fieldName of recMap.keys()) {\n                        record._.requestCompute(record, fieldName, { force: true });\n                    }\n                }\n                while (FS_QUEUE.size > 0) {\n                    /** @type {[Record, Map<string, true>]} */\n                    const [record, recMap] = FS_QUEUE.entries().next().value;\n                    FS_QUEUE.delete(record);\n                    for (const fieldName of recMap.keys()) {\n                        record._.requestSort(record, fieldName, { force: true });\n                    }\n                }\n                while (FA_QUEUE.size > 0) {\n                    /** @type {[Record, Map<string, Map<Record, true>>]} */\n                    const [record, recMap] = FA_QUEUE.entries().next().value;\n                    FA_QUEUE.delete(record);\n                    while (recMap.size > 0) {\n                        /** @type {[string, Map<Record, true>]} */\n                        const [fieldName, fieldMap] = recMap.entries().next().value;\n                        recMap.delete(fieldName);\n                        const onAdd = record.Model._.fieldsOnAdd.get(fieldName);\n                        for (const addedRec of fieldMap.keys()) {\n                            try {\n                                onAdd?.call(record._proxy, addedRec._proxy);\n                            } catch (err) {\n                                this.handleError(err);\n                            }\n                        }\n                    }\n                }\n                while (FD_QUEUE.size > 0) {\n                    /** @type {[Record, Map<string, Map<Record, true>>]} */\n                    const [record, recMap] = FD_QUEUE.entries().next().value;\n                    FD_QUEUE.delete(record);\n                    while (recMap.size > 0) {\n                        /** @type {[string, Map<Record, true>]} */\n                        const [fieldName, fieldMap] = recMap.entries().next().value;\n                        recMap.delete(fieldName);\n                        const onDelete = record.Model._.fieldsOnDelete.get(fieldName);\n                        for (const removedRec of fieldMap.keys()) {\n                            try {\n                                onDelete?.call(record._proxy, removedRec._proxy);\n                            } catch (err) {\n                                this.handleError(err);\n                            }\n                        }\n                    }\n                }\n                while (FU_QUEUE.size > 0) {\n                    /** @type {[Record, Map<string, true>]} */\n                    const [record, map] = FU_QUEUE.entries().next().value;\n                    FU_QUEUE.delete(record);\n                    for (const fieldName of map.keys()) {\n                        record._.onUpdate(record, fieldName);\n                    }\n                }\n                while (RO_QUEUE.size > 0) {\n                    /** @type {Map<Function, true>} */\n                    const cb = RO_QUEUE.keys().next().value;\n                    RO_QUEUE.delete(cb);\n                    try {\n                        cb();\n                    } catch (err) {\n                        this.handleError(err);\n                    }\n                }\n                while (RD_QUEUE.size > 0) {\n                    /** @type {Record} */\n                    const record = RD_QUEUE.keys().next().value;\n                    RD_QUEUE.delete(record);\n                    for (const [localId, names] of record._.uses.data.entries()) {\n                        for (const [name2, count] of names.entries()) {\n                            const existingRecordProxyInternal = toRaw(this.recordByLocalId).get(\n                                localId\n                            );\n                            const usingRecord =\n                                (existingRecordProxyInternal &&\n                                    toRaw(existingRecordProxyInternal)?._raw) ||\n                                deletingRecordsByLocalId.get(localId);\n                            if (!usingRecord) {\n                                // record already deleted, clean inverses\n                                record._.uses.data.delete(localId);\n                                continue;\n                            }\n                            for (let c = 0; c < count; c++) {\n                                usingRecord[name2].delete(record);\n                            }\n                        }\n                    }\n                    deletingRecordsByLocalId.set(record.localId, record);\n                    this.recordByLocalId.delete(record.localId);\n                    this._.ADD_QUEUE(\"hard_delete\", toRaw(record));\n                }\n                while (RHD_QUEUE.size > 0) {\n                    // effectively delete the record\n                    /** @type {Record} */\n                    const record = RHD_QUEUE.keys().next().value;\n                    RHD_QUEUE.delete(record);\n                    deletingRecordsByLocalId.delete(record.localId);\n                }\n            }\n            this._.UPDATE--;\n            if (this._.ERRORS.length) {\n                if (this.warnErrors) {\n                    console.warn(\"Store data insert aborted due to following errors:\");\n                    for (const err of this._.ERRORS) {\n                        console.warn(err);\n                    }\n                }\n                const [error1] = this._.ERRORS;\n                this._.ERRORS = [];\n                throw error1;\n            }\n        }\n        return res;\n    }\n    /**\n     * @template T\n     * @param {T} [dataByModelName={}]\n     * @param {Object} [options={}]\n     * @returns {{ [K in keyof T]: import(\"models\").Models[K][] }}\n     */\n    insert(dataByModelName = {}, options = {}) {\n        const store = this;\n        const ctx = storeInsertFns.makeContext(store);\n        Record.MAKE_UPDATE(function storeInsert() {\n            const recordsDataToDelete = [];\n            for (const [pyOrJsModelName, data] of Object.entries(dataByModelName)) {\n                const modelName = storeInsertFns.getActualModelName(store, ctx, pyOrJsModelName);\n                if (!store[modelName]) {\n                    console.warn(`store.insert() received data for unknown model \u201c${modelName}\u201d.`);\n                    continue;\n                }\n                const insertData = [];\n                for (const vals of Array.isArray(data) ? data : [data]) {\n                    const extraFields = storeInsertFns.getExtraFieldsFromModel(\n                        store,\n                        pyOrJsModelName\n                    );\n                    if (extraFields) {\n                        Object.assign(vals, extraFields);\n                    }\n                    if (vals._DELETE) {\n                        delete vals._DELETE;\n                        recordsDataToDelete.push([modelName, vals]);\n                    } else {\n                        insertData.push(vals);\n                    }\n                }\n                store[modelName].insert(insertData, options);\n            }\n            // Delete after all inserts to make sure a relation potentially registered before the\n            // delete doesn't re-add the deleted record by mistake.\n            for (const [modelName, vals] of recordsDataToDelete) {\n                store[modelName].get(vals)?.delete();\n            }\n        });\n    }\n    onChange(record, name, cb) {\n        return this._onChange(record, name, (observe) => {\n            const fn = () => {\n                observe();\n                try {\n                    cb();\n                } catch (err) {\n                    this.handleError(err);\n                }\n            };\n            if (this._.UPDATE !== 0) {\n                if (!this._.RO_QUEUE.has(fn)) {\n                    this._.RO_QUEUE.set(fn, true);\n                }\n            } else {\n                fn();\n            }\n        });\n    }\n    /**\n     * Version of onChange where the callback receives observe function as param.\n     * This is useful when there's desire to postpone calling the callback function,\n     * in which the observe is also intended to have its invocation postponed.\n     *\n     * @param {Record} record\n     * @param {string|string[]} key\n     * @param {(observe: Function) => any} callback\n     * @returns {function} function to call to stop observing changes\n     */\n    _onChange(record, key, callback) {\n        let proxy;\n        function _observe() {\n            // access proxy[key] only once to avoid triggering reactive get() many times\n            const val = proxy[key];\n            if (typeof val === \"object\" && val !== null) {\n                void Object.keys(val);\n            }\n            if (Array.isArray(val)) {\n                void val.length;\n                void toRaw(val).forEach.call(val, (i) => i);\n            }\n        }\n        if (Array.isArray(key)) {\n            for (const k of key) {\n                this._onChange(record, k, callback);\n            }\n            return;\n        }\n        let ready = true;\n        proxy = reactive(record, () => {\n            if (ready) {\n                callback(_observe);\n            }\n        });\n        _observe();\n        return () => {\n            ready = false;\n        };\n    }\n    _cleanupData(data) {\n        super._cleanupData(data);\n        if (this._getActualModelName() === \"Store\") {\n            delete data.Models;\n            for (const [name] of modelRegistry.getEntries()) {\n                delete data[name];\n            }\n        }\n    }\n}\n", "/** @typedef {import(\"./record\").Record} Record */\n/** @typedef {import(\"./record_list\").RecordList} RecordList */\n\nimport { htmlEscape, markup, toRaw } from \"@odoo/owl\";\nimport { RecordInternal } from \"./record_internal\";\nimport { deserializeDate, deserializeDateTime } from \"@web/core/l10n/dates\";\nimport { IS_DELETED_SYM, IS_DELETING_SYM, isCommand, isMany } from \"./misc\";\n\nconst Markup = markup().constructor;\n\nexport class StoreInternal extends RecordInternal {\n    /** @type {Map<import(\"./record\").Record, Map<string, true>>} */\n    FC_QUEUE = new Map(); // field-computes\n    /** @type {Map<import(\"./record\").Record, Map<string, true>>} */\n    FS_QUEUE = new Map(); // field-sorts\n    /** @type {Map<import(\"./record\").Record, Map<string, Map<import(\"./record\").Record, true>>>} */\n    FA_QUEUE = new Map(); // field-onadds\n    /** @type {Map<import(\"./record\").Record, Map<string, Map<import(\"./record\").Record, true>>>} */\n    FD_QUEUE = new Map(); // field-ondeletes\n    /** @type {Map<import(\"./record\").Record, Map<string, true>>} */\n    FU_QUEUE = new Map(); // field-onupdates\n    /** @type {Map<Function, true>} */\n    RO_QUEUE = new Map(); // record-onchanges\n    /** @type {Map<Record, true>} */\n    RD_QUEUE = new Map(); // record-deletes\n    /** @type {Map<Record, true>} */\n    RHD_QUEUE = new Map(); // record-hard-deletes\n    ERRORS = [];\n    UPDATE = 0;\n\n    /**\n     * @param {\"compute\"|\"sort\"|\"onAdd\"|\"onDelete\"|\"onUpdate\"|\"hard_delete\"} type\n     * @param {...any} params\n     */\n    ADD_QUEUE(type, ...params) {\n        switch (type) {\n            case \"delete\": {\n                /** @type {import(\"./record\").Record} */\n                const [record] = params;\n                if (!this.RD_QUEUE.has(record)) {\n                    this.RD_QUEUE.set(record, true);\n                }\n                break;\n            }\n            case \"compute\": {\n                /** @type {[import(\"./record\").Record, string]} */\n                const [record, fieldName] = params;\n                let recMap = this.FC_QUEUE.get(record);\n                if (!recMap) {\n                    recMap = new Map();\n                    this.FC_QUEUE.set(record, recMap);\n                }\n                recMap.set(fieldName, true);\n                break;\n            }\n            case \"sort\": {\n                /** @type {[import(\"./record\").Record, string]} */\n                const [record, fieldName] = params;\n                let recMap = this.FS_QUEUE.get(record);\n                if (!recMap) {\n                    recMap = new Map();\n                    this.FS_QUEUE.set(record, recMap);\n                }\n                recMap.set(fieldName, true);\n                break;\n            }\n            case \"onAdd\": {\n                /** @type {[import(\"./record\").Record, string, import(\"./record\").Record]} */\n                const [record, fieldName, addedRec] = params;\n                const Model = record.Model;\n                if (Model._.fieldsSort.get(fieldName)) {\n                    this.ADD_QUEUE(\"sort\", record, fieldName);\n                }\n                if (!Model._.fieldsOnAdd.get(fieldName)) {\n                    return;\n                }\n                let recMap = this.FA_QUEUE.get(record);\n                if (!recMap) {\n                    recMap = new Map();\n                    this.FA_QUEUE.set(record, recMap);\n                }\n                let fieldMap = recMap.get(fieldName);\n                if (!fieldMap) {\n                    fieldMap = new Map();\n                    recMap.set(fieldName, fieldMap);\n                }\n                fieldMap.set(addedRec, true);\n                break;\n            }\n            case \"onDelete\": {\n                /** @type {[import(\"./record\").Record, string, import(\"./record\").Record]} */\n                const [record, fieldName, removedRec] = params;\n                const Model = record.Model;\n                if (!Model._.fieldsOnDelete.get(fieldName)) {\n                    return;\n                }\n                let recMap = this.FD_QUEUE.get(record);\n                if (!recMap) {\n                    recMap = new Map();\n                    this.FD_QUEUE.set(record, recMap);\n                }\n                let fieldMap = recMap.get(fieldName);\n                if (!fieldMap) {\n                    fieldMap = new Map();\n                    recMap.set(fieldName, fieldMap);\n                }\n                fieldMap.set(removedRec, true);\n                break;\n            }\n            case \"onUpdate\": {\n                /** @type {[import(\"./record\").Record, string]} */\n                const [record, fieldName] = params;\n                let recMap = this.FU_QUEUE.get(record);\n                if (!recMap) {\n                    recMap = new Map();\n                    this.FU_QUEUE.set(record, recMap);\n                }\n                recMap.set(fieldName, true);\n                break;\n            }\n            case \"hard_delete\": {\n                /** @type {import(\"./record\").Record} */\n                const [record] = params;\n                record._[IS_DELETING_SYM] = true;\n                record._proxy[IS_DELETED_SYM] = true;\n                delete record.Model.records[record.localId];\n                if (!this.RHD_QUEUE.has(record)) {\n                    this.RHD_QUEUE.set(record, true);\n                }\n                break;\n            }\n        }\n    }\n    /** @param {RecordList<Record>} recordListFullProxy */\n    sortRecordList(recordListFullProxy, func) {\n        const recordList = toRaw(recordListFullProxy)._raw;\n        // sort on copy of list so that reactive observers not triggered while sorting\n        const recordsFullProxy = recordListFullProxy.data.map((localId) =>\n            recordListFullProxy._store.recordByLocalId.get(localId)\n        );\n        recordsFullProxy.sort(func);\n        const data = recordsFullProxy.map((recordFullProxy) => toRaw(recordFullProxy)._raw.localId);\n        const hasChanged = recordList.data.some((localId, i) => localId !== data[i]);\n        if (hasChanged) {\n            recordListFullProxy.data = data;\n        }\n    }\n    /**\n     * @param {Record} record\n     * @param {string} fieldName\n     * @param {any} value\n     */\n    updateAttr(record, fieldName, value) {\n        const Model = record.Model;\n        const fieldType = Model._.fieldsType.get(fieldName);\n        const fieldHtml = Model._.fieldsHtml.get(fieldName);\n        // ensure each field write goes through the proxy exactly once to trigger reactives\n        const targetRecord = record._.proxyUsed.has(fieldName) ? record : record._proxy;\n        let shouldChange = record[fieldName] !== value;\n        if (fieldType === \"datetime\" && value) {\n            if (!(value instanceof luxon.DateTime)) {\n                value = deserializeDateTime(value);\n            }\n            shouldChange = !record[fieldName] || !value.equals(record[fieldName]);\n        }\n        if (fieldType === \"date\" && value) {\n            if (!(value instanceof luxon.DateTime)) {\n                value = deserializeDate(value);\n            }\n            shouldChange = !record[fieldName] || !value.equals(record[fieldName]);\n        }\n        let newValue = value;\n        if (fieldHtml) {\n            newValue =\n                Array.isArray(value) && value[0] === \"markup\"\n                    ? value[1]\n                        ? markup(value[1])\n                        : \"\"\n                    : value\n                    ? htmlEscape(value)\n                    : \"\";\n            shouldChange =\n                record[fieldName]?.toString() !== newValue?.toString() ||\n                record[fieldName] instanceof Markup != newValue instanceof Markup;\n        }\n        if (shouldChange) {\n            record._.updatingAttrs.set(fieldName, true);\n            targetRecord[fieldName] = newValue;\n            record._.updatingAttrs.delete(fieldName);\n        }\n    }\n    /**\n     * @param {Record} record\n     * @param {Object} vals\n     */\n    updateFields(record, vals) {\n        const fieldEntries = Object.entries(vals).concat(\n            Object.getOwnPropertySymbols(vals).map((sym) => [sym, vals[sym]])\n        );\n        for (const [fieldName, value] of fieldEntries) {\n            if (!record.Model._.fields.get(fieldName) || record.Model._.fieldsAttr.get(fieldName)) {\n                this.updateAttr(record, fieldName, value);\n            } else {\n                this.updateRelation(record, fieldName, value);\n            }\n        }\n    }\n    /**\n     * @param {Record} record\n     * @param {string} fieldName\n     * @param {any} value\n     */\n    updateRelation(record, fieldName, value) {\n        /** @type {RecordList<Record>} */\n        const recordList = record[fieldName];\n        if (isMany(record.Model, fieldName)) {\n            this.updateRelationMany(recordList, value);\n        } else {\n            this.updateRelationOne(recordList, value);\n        }\n    }\n    /**\n     * @param {RecordList} recordList\n     * @param {any} value\n     */\n    updateRelationMany(recordList, value) {\n        if (isCommand(value)) {\n            for (const [cmd, cmdData] of value) {\n                if (Array.isArray(cmdData)) {\n                    for (const item of cmdData) {\n                        if (cmd === \"ADD\") {\n                            recordList.add(item);\n                        } else if (cmd === \"ADD.noinv\") {\n                            recordList._.addNoinv(recordList, item);\n                        } else if (cmd === \"DELETE.noinv\") {\n                            recordList._.deleteNoinv(recordList, item);\n                        } else {\n                            recordList.delete(item);\n                        }\n                    }\n                } else {\n                    if (cmd === \"ADD\") {\n                        recordList.add(cmdData);\n                    } else if (cmd === \"ADD.noinv\") {\n                        recordList._.addNoinv(recordList, cmdData);\n                    } else if (cmd === \"DELETE.noinv\") {\n                        recordList._.deleteNoinv(recordList, cmdData);\n                    } else {\n                        recordList.delete(cmdData);\n                    }\n                }\n            }\n        } else if ([null, false, undefined].includes(value)) {\n            recordList.clear();\n        } else if (!Array.isArray(value)) {\n            recordList._.assign(recordList, [value]);\n        } else {\n            recordList._.assign(recordList, value);\n        }\n    }\n    /**\n     * @param {RecordList} recordList\n     * @param {any} value\n     * @returns {boolean} whether the value has changed\n     */\n    updateRelationOne(recordList, value) {\n        if (isCommand(value)) {\n            const [cmd, cmdData] = value.at(-1);\n            if (cmd === \"ADD\") {\n                recordList.add(cmdData);\n            } else if (cmd === \"ADD.noinv\") {\n                recordList._.addNoinv(recordList, cmdData);\n            } else if (cmd === \"DELETE.noinv\") {\n                recordList._.deleteNoinv(recordList, cmdData);\n            } else {\n                recordList.delete(cmdData);\n            }\n        } else if ([null, false, undefined].includes(value)) {\n            recordList.clear();\n        } else {\n            recordList.add(value);\n        }\n    }\n}\n", "import { isRecord, STORE_SYM } from \"@mail/model/misc\";\nimport { Component, toRaw } from \"@odoo/owl\";\nimport { DropdownState } from \"@web/core/dropdown/dropdown_hooks\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { Reactive } from \"@web/core/utils/reactive\";\n\nexport const ACTION_TAGS = Object.freeze({\n    DANGER: \"DANGER\",\n    SUCCESS: \"SUCCESS\",\n    IMPORTANT_BADGE: \"IMPORTANT_BADGE\",\n    WARNING_BADGE: \"WARNING_BADGE\",\n    CALL_LAYOUT: \"CALL_LAYOUT\",\n    JOIN_LEAVE_CALL: \"JOIN_LEAVE_CALL\",\n});\n\n/** @typedef {import(\"@odoo/owl\").Component} Component */\n/** @typedef {import(\"@mail/model/record\").Record} Record */\n/** @typedef {Component|Record} ActionOwner */\n\n/**\n * @typedef {Object} ActionDefinition\n * @property {boolean|(action: Action) => boolean} [badge]\n * @property {string|(action: Action) => string} [badgeIcon]\n * @property {string|(action: Action) => string} [badgeText]\n * @property {Object|(action: Action) => Object} [btnAttrs]\n * @property {string|(action: Action) => string} [btnClass]\n * @property {Component} [component]\n * @property {boolean|(action: Action) => boolean} [componentCondition=true]\n * @property {(action: Action) => Component<Props, Env>} [componentProps]\n * @property {boolean|(action: Action) => boolean} [disabledCondition]\n * @property {boolean} [dropdown]\n * @property {Component|(action: Action) => Component} [dropdownComponent]\n * @property {Object|(action: Action) => Object} [dropdownComponentProps]\n * @property {string|(action: Action) => string} [dropdownMenuClass]\n * @property {string|(action: Action) => string} [dropdownPosition]\n * @property {DropdownState|(action: Action) => DropdownState} [dropdownState]\n * @property {string|(action: Action) => string} [dropdownTemplate]\n * @property {Object|(action: Action) => Object} [dropdownTemplateParams]\n * @property {string|(action: Action) => string} [hotkey]\n * @property {string|(action: Action) => string} [icon]\n * @property {boolean|(action: Action) => boolean} [inlineName=false]\n * @property {boolean|(action: Action) => boolean} [isActive]\n * @property {string|(action: Action) => string} [name]\n * @property {(action: Action, ev: Event) => void} [onSelected]\n * @property {number|(action: Action) => number} [sequence]\n * @property {boolean|(action: Action) => boolean} [sequenceGroup]\n * @property {boolean|(action: Action) => boolean} [sequenceQuick]\n * @property {() => void} [setup]\n * @property {string|string[]|(action: Action) => string|string[]} [tags]\n */\n\nexport class Action {\n    /** @type {ActionDefinition}  User-defined explicit definition of this action */\n    definition;\n    /** @type {ActionOwner} Entity that is using this action */\n    owner;\n    /** @type {string} Unique id of this action. */\n    id;\n    /** @type {import(\"models\").Store} */\n    store;\n\n    /** param `store` is required for actions made with new Action() by hand in components and outside component.setup() */\n    constructor({ owner, id, definition, store }) {\n        this.definition = definition;\n        this.id = id;\n        this.owner = owner;\n        const rawOwner = toRaw(owner);\n        this.store =\n            store ??\n            (rawOwner[STORE_SYM]\n                ? owner\n                : isRecord(owner)\n                ? owner.store\n                : useService(\"mail.store\"));\n    }\n\n    get params() {\n        return { action: this, store: this.store, owner: this.owner };\n    }\n\n    /** @param {Action} action @returns {boolean|undefined} */\n    _badge(action) {}\n    /** Condition for showing badge on this action */\n    get badge() {\n        return (\n            this._badge(this.params) ??\n            (typeof this.definition.badge === \"function\"\n                ? this.definition.badge.call(this, this.params)\n                : this.definition.badge)\n        );\n    }\n\n    /** @param {Action} action @returns {string|undefined} */\n    _badgeIcon(action) {}\n    /** When action shows badge @see badge this property tells the icon inside badge */\n    get badgeIcon() {\n        return (\n            this._badgeIcon(this.params) ??\n            (typeof this.definition.badgeIcon === \"function\"\n                ? this.definition.badgeIcon.call(this, this.params)\n                : this.definition.badgeIcon)\n        );\n    }\n\n    /** @param {Action} action @returns {string|undefined} */\n    _badgeText(action) {}\n    /** When action shows badge @see badge this property tells the text inside badge. */\n    get badgeText() {\n        return (\n            this._badgeText(this.params) ??\n            (typeof this.definition.badgeText === \"function\"\n                ? this.definition.badgeText.call(this, this.params)\n                : this.definition.badgeText)\n        );\n    }\n\n    /** @param {Action} action @returns {Object|undefined} */\n    _btnAttrs(action) {}\n    get btnAttrs() {\n        return (\n            this._btnAttrs(this.params) ??\n            (typeof this.definition.btnAttrs === \"function\"\n                ? this.definition.btnAttrs.call(this, this.params)\n                : this.definition.btnAttrs)\n        );\n    }\n\n    /** @param {Action} action @returns {string|undefined} */\n    _btnClass(action) {}\n    get btnClass() {\n        return (\n            this._btnClass(this.params) ??\n            (typeof this.definition.btnClass === \"function\"\n                ? this.definition.btnClass.call(this, this.params)\n                : this.definition.btnClass)\n        );\n    }\n\n    /** @param {Action} action @returns {Component|undefined} */\n    _component(action) {}\n    /** When provided, this component is mounted for this action. UI/UX of action is fully managed by the component */\n    get component() {\n        return this._component(this.params) ?? this.definition.component;\n    }\n\n    /** @param {Action} action @returns {boolean|undefined} */\n    _componentCondition(action) {}\n    /** When provided, action.component is conditionally picked based on this condition. When condition is false, the usual UI/UX of action from other explicit definitions is chosen */\n    get componentCondition() {\n        return (\n            this._componentCondition(this.params) ??\n            (typeof this.definition.componentCondition === \"function\"\n                ? this.definition.componentCondition.call(this, this.params)\n                : this.definition.componentCondition ?? true)\n        );\n    }\n\n    /** @param {Action} action @returns {Object|undefined} */\n    _componentProps(action) {}\n    /** Props to pass to the component of this action. */\n    get componentProps() {\n        return (\n            this._componentProps(this.params) ??\n            this.definition.componentProps?.call(this, this.params)\n        );\n    }\n\n    /** @param {Action} action @returns {boolean|undefined} */\n    _condition(action) {}\n    /** Condition for availability of this action */\n    get condition() {\n        return (\n            this._condition(this.params) ??\n            (typeof this.definition.condition === \"function\"\n                ? this.definition.condition.call(this, this.params)\n                : this.definition.condition ?? true)\n        );\n    }\n\n    /** @param {Action} action @returns {boolean|undefined} */\n    _disabledCondition(action) {}\n    /** Condition to disable the button of this action (but still display it). */\n    get disabledCondition() {\n        return Boolean(\n            this._disabledCondition(this.params) ??\n                this.definition.disabledCondition?.call(this, this.params)\n        );\n    }\n\n    /** @param {Action} action @returns {boolean|undefined} */\n    _dropdown(action) {}\n    /** Determines whether this action opens a dropdown on selection. */\n    get dropdown() {\n        return this._dropdown(this.params) ?? this.definition.dropdown;\n    }\n\n    /** @param {Action} action @returns {Component|undefined} */\n    _dropdownComponent(action) {}\n    /** When action is a dropdown @see dropdown, this determines an optional component to use for the content slot */\n    get dropdownComponent() {\n        return (\n            this._dropdownComponent(this.params) ??\n            (typeof this.definition.dropdownComponent === \"function\" &&\n            Object.getPrototypeOf(this.definition.dropdownComponent) !== Component\n                ? this.definition.dropdownComponent.call(this, this.params)\n                : this.definition.dropdownComponent)\n        );\n    }\n\n    /** @param {Action} action @returns {Object|undefined} */\n    _dropdownComponentProps(action) {}\n    /** When action is a dropdown @see dropdown, this determines optional props to pass to component of the content slot of dropdown. */\n    get dropdownComponentProps() {\n        return (\n            this._dropdownComponentProps(this.params) ??\n            (typeof this.definition.dropdownComponentProps === \"function\"\n                ? this.definition.dropdownComponentProps.call(this, this.params)\n                : this.definition.dropdownComponentProps)\n        );\n    }\n\n    /** @param {Action} action @returns {string|undefined} */\n    _dropdownMenuClass(action) {}\n    /** When action is a dropdown @see dropdown, this determines an optional menu class for the dropdown, in addition to default dropdown menu classes */\n    get dropdownMenuClass() {\n        return (\n            this._dropdownMenuClass(this.params) ??\n            (typeof this.definition.dropdownMenuClass === \"function\"\n                ? this.definition.dropdownMenuClass.call(this, this.params)\n                : this.definition.dropdownMenuClass)\n        );\n    }\n\n    /** @param {Action} action @returns {string|undefined} */\n    _dropdownPosition(action) {}\n    /** When action is a dropdown @see dropdown, this determines the preferred position of the dropdown */\n    get dropdownPosition() {\n        return (\n            this._dropdownPosition(this.params) ??\n            (typeof this.definition.dropdownPosition === \"function\"\n                ? this.definition.dropdownPosition.call(this, this.params)\n                : this.definition.dropdownPosition)\n        );\n    }\n\n    /** @param {Action} action @returns {DropdownState|undefined} */\n    _dropdownState(action) {}\n    /** When action is a dropdown @see dropdown, this determines the preferred position of the dropdown */\n    get dropdownState() {\n        return (\n            this._dropdownState(this.params) ??\n            (typeof this.definition.dropdownState === \"function\"\n                ? this.definition.dropdownState.call(this, this.params)\n                : this.definition.dropdownState)\n        );\n    }\n\n    /** @param {Action} action @returns {string|undefined} */\n    _dropdownTemplate(action) {}\n    /** When action is a dropdown @see dropdown, this determines an optional template to use for the content slot */\n    get dropdownTemplate() {\n        return (\n            this._dropdownTemplate(this.params) ??\n            (typeof this.definition.dropdownTemplate === \"function\"\n                ? this.definition.dropdownTemplate.call(this, this.params)\n                : this.definition.dropdownTemplate)\n        );\n    }\n\n    /** @param {Action} action @returns {Object|undefined} */\n    _dropdownTemplateParams(action) {}\n    /**\n     * When action is a dropdown @see dropdown, this determines optional params to pass to template of the content slot of dropdown.\n     * The params are provided to template in object `templateParams` with named parameters as given by explicit definition.\n     * For example: `{ myParam1: 1 }` is retrieved in template with `templateParams.myParam1`.\n     */\n    get dropdownTemplateParams() {\n        return (\n            this._dropdownTemplateParams(this.params) ??\n            (typeof this.definition.dropdownTemplateParams === \"function\"\n                ? this.definition.dropdownTemplateParams.call(this, this.params)\n                : this.definition.dropdownTemplateParams)\n        );\n    }\n\n    /** @param {Action} action @returns {string|undefined} */\n    _hotkey(action) {}\n    /** Determines whether this action has a keyboard hotkey to trigger the onSelected */\n    get hotkey() {\n        return (\n            this._hotkey(this.params) ??\n            (typeof this.definition.hotkey === \"function\"\n                ? this.definition.hotkey.call(this, this.params)\n                : this.definition.hotkey)\n        );\n    }\n\n    /** @param {Action} action @returns {string|Object|undefined} */\n    _icon(action) {}\n    /**\n     * Icon for the button this action.\n     * - When a string, this is considered an icon as classname (.fa and .oi).\n     * - When an object with property `template`, this is an icon rendered in template.\n     *   Template params are provided in `params` and passed to template as a `t-set=\"templateParams\"`\n     */\n    get icon() {\n        return (\n            this._icon(this.params) ??\n            (typeof this.definition.icon === \"function\"\n                ? this.definition.icon.call(this, this.params)\n                : this.definition.icon)\n        );\n    }\n\n    /** @param {Action} action @returns {string|undefined} */\n    _inlineName(action) {}\n    /** If set, when action is used in inline, shows action name in addition to icon. */\n    get inlineName() {\n        return (\n            this._inlineName(this.params) ??\n            (typeof this.definition.inlineName === \"function\"\n                ? this.definition.inlineName.call(this, this.params)\n                : this.definition.inlineName) ??\n            false\n        );\n    }\n\n    /** @param {Action} action @returns {boolean|undefined} */\n    _isActive(action) {}\n    /** States whether this action is currently active. */\n    get isActive() {\n        return (\n            this._isActive(this.params) ??\n            (typeof this.definition.isActive === \"function\"\n                ? this.definition.isActive.call(this, this.params)\n                : this.definition.isActive)\n        );\n    }\n\n    /** @param {Action} action @returns {string|undefined} */\n    _name(action) {}\n    /** Name of this action, displayed to the user. */\n    get name() {\n        return (\n            this._name(this.params) ??\n            (typeof this.definition.name === \"function\"\n                ? this.definition.name.call(this, this.params)\n                : this.definition.name)\n        );\n    }\n\n    /** @param {Action} action @param {Event} ev @returns {true|undefined} */\n    _onSelected(action, ev) {}\n    /** Action to execute when this action is selected @param {Event} ev */\n    onSelected(ev) {\n        return (\n            this._onSelected(this.params, ev) ??\n            this.definition.onSelected?.call(this, this.params, ev)\n        );\n    }\n\n    /** @param {Action} action @returns {number|undefined} */\n    _sequence(action) {}\n    /** Determines the order of this action (smaller first). */\n    get sequence() {\n        return (\n            this._sequence(this.params) ??\n            (typeof this.definition.sequence === \"function\"\n                ? this.definition.sequence.call(this, this.params)\n                : this.definition.sequence)\n        );\n    }\n\n    /** @param {Action} action @returns {number|undefined} */\n    _sequenceGroup(action) {}\n    get sequenceGroup() {\n        return (\n            this._sequenceGroup(this.params) ??\n            (typeof this.definition.sequenceGroup === \"function\"\n                ? this.definition.sequenceGroup.call(this, this.params)\n                : this.definition.sequenceGroup)\n        );\n    }\n\n    /** @param {Action} action @returns {number|undefined} */\n    _sequenceQuick(action) {}\n    get sequenceQuick() {\n        return (\n            this._sequenceQuick(this.params) ??\n            (typeof this.definition.sequenceQuick === \"function\"\n                ? this.definition.sequenceQuick.call(this, this.params)\n                : this.definition.sequenceQuick)\n        );\n    }\n\n    /** @param {Action} action @returns {true|undefined} */\n    _setup(action) {}\n    /** setup is executed when the owner is being setup. */\n    setup() {\n        return this._setup(this.params) ?? this.definition.setup?.call(this, this.params);\n    }\n\n    /** @param {Action} action @returns {string|string[]|undefined} */\n    _tags(action) {}\n    /** If set, list of tags of this action. */\n    get tags() {\n        const res =\n            this._tags(this.params) ??\n            (typeof this.definition.tags === \"function\"\n                ? this.definition.tags.call(this, this.params)\n                : this.definition.tags);\n        return Array.isArray(res) ? res : [res];\n    }\n\n    get tagClassNames() {\n        return this.tags.map((tag) => `o-tag-${tag}`).join(\" \");\n    }\n}\n\nexport class UseActions extends Reactive {\n    ActionClass = Action;\n    /** @type {Component} */\n    component;\n    /** @type {Map<string, Action>} */\n    moreActions = new Map();\n    /** @type {Action[]} */\n    transformedActions;\n    /** @type {import(\"models\").Store} */\n    store;\n\n    constructor(component, transformedActions, store) {\n        super();\n        this.component = component;\n        this.transformedActions = transformedActions;\n        this.store = store;\n    }\n\n    /**\n     * @typedef {Object} MoreActionSpecificDefinition\n     * @property {Action[]|Array<Action[]>} actions\n     */\n    /** @typedef {ActionDefinition & MoreActionSpecificDefinition} MoreActionDefinition */\n    /** @param {MoreActionDefinition} [data] */\n    more(data = {}, id) {\n        let moreAction = toRaw(this).moreActions.get(id);\n        if (moreAction) {\n            moreAction = this.moreActions.get(id);\n            moreAction.definition.actions = data.actions;\n        } else {\n            moreAction = new this.ActionClass({\n                owner: this.component,\n                id: `more-action:${id}`,\n                definition: {\n                    ...data,\n                    dropdown: true,\n                    dropdownState: new DropdownState(),\n                    icon: data?.icon ?? \"oi oi-ellipsis-v\",\n                    isActive: ({ action }) => action.dropdownState.isOpen,\n                    isMoreAction: true,\n                    sequence: data.sequence ?? 1000,\n                },\n                store: this.store,\n            });\n            toRaw(this).moreActions.set(data.id, moreAction);\n        }\n        return moreAction;\n    }\n\n    get actions() {\n        const actions = this.transformedActions\n            .filter((action) => action.condition)\n            .sort((a1, a2) => a1.sequence - a2.sequence);\n        return actions;\n    }\n\n    get partition() {\n        const actions = this.transformedActions.filter((action) => action.condition);\n        const quick = actions\n            .filter((a) => a.sequenceQuick)\n            .sort((a1, a2) => a1.sequenceQuick - a2.sequenceQuick);\n        const grouped = actions.filter((a) => a.sequenceGroup);\n        const groups = {};\n        for (const a of grouped) {\n            if (!(a.sequenceGroup in groups)) {\n                groups[a.sequenceGroup] = [];\n            }\n            groups[a.sequenceGroup].push(a);\n        }\n        const sortedGroups = Object.entries(groups).sort(\n            ([groupId1], [groupId2]) => groupId1 - groupId2\n        );\n        for (const [, actions] of sortedGroups) {\n            actions.sort((a1, a2) => a1.sequence - a2.sequence);\n        }\n        const group = sortedGroups.map(([groupId, actions]) => actions);\n        const other = actions\n            .filter((a) => !a.sequenceQuick && !a.sequenceGroup)\n            .sort((a1, a2) => a1.sequence - a2.sequence);\n        return { quick, group, other };\n    }\n}\n", "import { CallDropdown } from \"@mail/discuss/call/common/call_dropdown\";\nimport { attClassObjectToString } from \"@mail/utils/common/format\";\nimport { Component, onWillUnmount } from \"@odoo/owl\";\nimport { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\n\nimport { useService } from \"@web/core/utils/hooks\";\n\nconst actionListProps = [\n    \"inline?\",\n    \"dropdown?\",\n    \"fw?\",\n    \"hasBtnBg?\",\n    \"odooControlPanelSwitchStyle?\",\n    \"thread?\",\n];\n\nclass Action extends Component {\n    static props = [\n        \"action\",\n        \"group?\",\n        \"isFirstInGroup?\",\n        \"isLastInGroup?\",\n        \"style?\",\n        ...actionListProps,\n    ];\n    static defaultProps = { fw: true };\n    static components = { Action, DropdownItem };\n    static template = \"mail.Action\";\n\n    get ActionList() {\n        return ActionList;\n    }\n\n    get Dropdown() {\n        if (this.env.inDiscussCallView?.isPip) {\n            return CallDropdown;\n        }\n        return Dropdown;\n    }\n\n    setup() {\n        super.setup();\n        this.store = useService(\"mail.store\");\n        this.ui = useService(\"ui\");\n        this.attClassObjectToString = attClassObjectToString;\n        if (this.props.action.definition?.isMoreAction) {\n            onWillUnmount(() => {\n                this.props.action.dropdownState.close();\n            });\n        }\n    }\n\n    get action() {\n        return this.props.action;\n    }\n\n    get hasBtnBg() {\n        return this.props.odooControlPanelSwitchStyle || this.props.hasBtnBg;\n    }\n\n    onSelected(action, ev) {\n        action.onSelected?.(ev);\n        this.env.inCallDropdown?.close();\n    }\n}\n\nexport class ActionList extends Component {\n    static components = { Action };\n    static props = [\"actions\", \"groupClass?\", ...actionListProps];\n    static template = \"mail.ActionList\";\n\n    getActionProps(action, group, { index, isFirstInGroup, isLastInGroup } = {}) {\n        return {\n            action,\n            group,\n            isFirstInGroup,\n            isLastInGroup,\n            ...Object.fromEntries(\n                actionListProps.map((propName) => {\n                    const actualPropName = propName.endsWith(\"?\")\n                        ? propName.substring(0, propName.length - 1)\n                        : propName;\n                    return [actualPropName, this.props[actualPropName]];\n                })\n            ),\n            style: `z-index: ${group.length - index}`,\n        };\n    }\n\n    setup() {\n        super.setup();\n        this.store = useService(\"mail.store\");\n        this.ui = useService(\"ui\");\n        this.actionListProps = actionListProps;\n    }\n\n    get groups() {\n        let groups;\n        if (this.props.actions.find((i) => Array.isArray(i))) {\n            groups = this.props.actions;\n        } else {\n            groups = [this.props.actions];\n        }\n        return groups.filter((group) => group.length); // don't show empty groups\n    }\n\n    get hasBtnBg() {\n        return this.props.odooControlPanelSwitchStyle || this.props.hasBtnBg;\n    }\n}\n", "import { fields, Record } from \"@mail/core/common/record\";\nimport { assignDefined } from \"@mail/utils/common/misc\";\n\nexport class Activity extends Record {\n    static _name = \"mail.activity\";\n    static id = \"id\";\n    /**\n     * @param {Object} data\n     * @param {Object} [param1]\n     * @param {boolean} param1.broadcast\n     * @returns {import(\"models\").Activity}\n     */\n    static _insert(data, { broadcast = true } = {}) {\n        /** @type {import(\"models\").Activity} */\n        const activity = this.preinsert(data);\n        assignDefined(activity, data);\n        if (broadcast) {\n            this.store.activityBroadcastChannel?.postMessage({\n                type: \"INSERT\",\n                payload: activity.serialize(),\n            });\n        }\n        return activity;\n    }\n\n    /** @type {number} */\n    id;\n    /** @type {boolean} */\n    active;\n    /** @type {string} */\n    activity_category;\n    activity_type_id = fields.One(\"mail.activity.type\");\n    /** @type {string|false} */\n    activity_decoration;\n    /** @type {Object[]} */\n    attachment_ids;\n    /** @type {boolean} */\n    can_write;\n    /** @type {'suggest'|'trigger'} */\n    chaining_type;\n    create_date = fields.Datetime();\n    create_uid = fields.One(\"res.users\");\n    date_deadline = fields.Date();\n    date_done = fields.Date();\n    /** @type {string} */\n    display_name;\n    /** @type {boolean} */\n    has_recommended_activities;\n    /** @type {string} */\n    feedback;\n    /** @type {string} */\n    icon = \"fa-tasks\";\n    mail_template_ids = fields.Many(\"mail.template\");\n    note = fields.Html(\"\");\n    /** @type {string} */\n    res_model;\n    /** @type {[number, string]} */\n    res_model_id;\n    /** @type {number} */\n    res_id;\n    /** @type {string} */\n    res_name;\n    /** @type {'overdue'|'planned'|'today'} */\n    state;\n    /** @type {string} */\n    summary;\n    user_id = fields.One(\"res.users\");\n    /** @type {string} */\n    write_date;\n    /** @type {[number, string]} */\n    write_uid;\n\n    serialize() {\n        return JSON.parse(JSON.stringify(this.toData([\"user_id\"])));\n    }\n}\n\nActivity.register();\n", "import { Gif } from \"@mail/core/common/gif\";\n\nimport { Component } from \"@odoo/owl\";\nimport { isMobileOS } from \"@web/core/browser/feature_detection\";\n\nimport { ConfirmationDialog } from \"@web/core/confirmation_dialog/confirmation_dialog\";\nimport { download } from \"@web/core/network/download\";\nimport { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { useDropdownState } from \"@web/core/dropdown/dropdown_hooks\";\nimport { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\nimport { useFileViewer } from \"@web/core/file_viewer/file_viewer_hook\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { url } from \"@web/core/utils/urls\";\n\nclass Actions extends Component {\n    static components = { Dropdown, DropdownItem };\n    static props = [\"actions\"];\n    static template = \"mail.Actions\";\n\n    setup() {\n        super.setup();\n        this.actionsMenuState = useDropdownState();\n    }\n}\n\n/**\n * @typedef {Object} Props\n * @property {import(\"models\").Attachment[]} attachments\n * @property {function} unlinkAttachment\n * @property {ReturnType<import('@mail/core/common/message_search_hook').useMessageSearch>} [messageSearch]\n * @extends {Component<Props, Env>}\n */\nexport class AttachmentList extends Component {\n    static components = { Actions, Gif };\n    static props = [\"attachments\", \"unlinkAttachment\", \"messageSearch?\"];\n    static template = \"mail.AttachmentList\";\n\n    setup() {\n        super.setup();\n        this.ui = useService(\"ui\");\n        this.dialog = useService(\"dialog\");\n        this.fileViewer = useFileViewer();\n        this.actionsMenuState = useDropdownState();\n        this.isMobileOS = isMobileOS();\n    }\n\n    /**\n     * @param {import(\"models\").Attachment} attachment\n     */\n    getImageUrl(attachment) {\n        if (attachment.uploading && attachment.tmpUrl) {\n            return attachment.tmpUrl;\n        }\n        return url(attachment.urlRoute, {\n            ...attachment.urlQueryParams,\n        });\n    }\n\n    /**\n     * @param {import(\"models\").Attachment} attachment\n     */\n    canDownload(attachment) {\n        return !attachment.uploading && !this.env.inComposer;\n    }\n\n    /**\n     * @param {import(\"models\").Attachment} attachment\n     */\n    onClickDownload(attachment) {\n        download({\n            data: {},\n            url: attachment.downloadUrl,\n        });\n    }\n\n    /**\n     * @param {import(\"models\").Attachment} attachment\n     */\n    onClickUnlink(attachment) {\n        if (this.env.inComposer) {\n            return this.props.unlinkAttachment(attachment);\n        }\n        this.dialog.add(ConfirmationDialog, {\n            body: _t('Do you really want to delete \"%s\"?', attachment.name),\n            cancel: () => {},\n            confirm: () => this.onConfirmUnlink(attachment),\n        });\n    }\n\n    onClickAttachment(attachment) {\n        this.fileViewer.open(attachment, this.props.attachments);\n    }\n\n    /**\n     * @param {import(\"models\").Attachment} attachment\n     */\n    onConfirmUnlink(attachment) {\n        this.props.unlinkAttachment(attachment);\n    }\n\n    onImageLoaded() {\n        this.env.onImageLoaded?.();\n    }\n\n    get isInChatWindowAndIsAlignedRight() {\n        return this.env.inChatWindow && this.env.alignedRight;\n    }\n\n    get isInChatWindowAndIsAlignedLeft() {\n        return this.env.inChatWindow && !this.env.alignedRight;\n    }\n\n    getActions(attachment) {\n        const res = [];\n        if (this.showDelete) {\n            res.push({\n                label: _t(\"Remove\"),\n                icon: \"fa fa-trash\",\n                onSelect: () => this.onClickUnlink(attachment),\n            });\n        }\n        if (this.canDownload(attachment)) {\n            res.push({\n                label: _t(\"Download\"),\n                icon: \"fa fa-download\",\n                onSelect: () => this.onClickDownload(attachment),\n            });\n        }\n        return res;\n    }\n\n    get showDelete() {\n        // in the composer they should all be implicitly deletable\n        if (this.env.inComposer) {\n            return true;\n        }\n        if (!this.attachment.isDeletable) {\n            return false;\n        }\n        // in messages users are expected to delete the message instead of just the attachment\n        return (\n            !this.env.message ||\n            this.env.message.hasTextContent ||\n            (this.env.message && this.props.attachments.length > 1)\n        );\n    }\n\n    /**\n     * @param {import(\"models\").Attachment} attachment\n     */\n    showUploaded(attachment) {\n        return !attachment.isImage && !attachment.uploading && this.env.inComposer;\n    }\n}\n", "import { fields, Record } from \"@mail/core/common/record\";\nimport { assignDefined } from \"@mail/utils/common/misc\";\nimport { generatePdfThumbnail } from \"@mail/utils/common/pdf_thumbnail\";\n\nimport { FileModelMixin } from \"@web/core/file_viewer/file_model\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { imageUrl, url } from \"@web/core/utils/urls\";\n\nexport class Attachment extends FileModelMixin(Record) {\n    static _name = \"ir.attachment\";\n    static id = \"id\";\n    static new() {\n        /** @type {import(\"models\").Attachment} */\n        const attachment = super.new(...arguments);\n        Record.onChange(attachment, [\"extension\", \"name\"], () => {\n            if (!attachment.extension && attachment.name) {\n                attachment.extension = attachment.name.split(\".\").pop();\n            }\n        });\n        return attachment;\n    }\n\n    composer = fields.One(\"Composer\", { inverse: \"attachments\" });\n    thread = fields.One(\"Thread\", { inverse: \"attachments\" });\n    /** @type {string} */\n    raw_access_token;\n    res_name;\n    /** @type {string} */\n    thumbnail_access_token;\n    message = fields.One(\"mail.message\", { inverse: \"attachment_ids\" });\n    /** @type {string} */\n    ownership_token;\n    create_date = fields.Datetime();\n    has_thumbnail = fields.Attr(undefined, {\n        onUpdate() {\n            if (\n                this.isPdf &&\n                !this.has_thumbnail &&\n                (this.store.self.main_user_id?.share === false || this.ownership_token)\n            ) {\n                this.setPdfThumbnail();\n            }\n        },\n    });\n\n    get thumbnailUrl() {\n        return imageUrl(\n            \"ir.attachment\",\n            this.id,\n            \"thumbnail\",\n            assignDefined(\n                {},\n                {\n                    access_token: this.thumbnail_access_token,\n                    crop: \"top\",\n                    height: 110,\n                    unique: this.checksum,\n                    width: 180,\n                }\n            )\n        );\n    }\n\n    get gifPaused() {\n        return this.thread ? !this.thread.isFocused : !this.composer?.isFocused;\n    }\n\n    get isDeletable() {\n        if (this.message && this.store.self.main_user_id?.share !== false) {\n            return this.message.editable;\n        }\n        return true;\n    }\n\n    get monthYear() {\n        if (!this.create_date) {\n            return undefined;\n        }\n        return `${this.create_date.monthLong}, ${this.create_date.year}`;\n    }\n\n    get uploading() {\n        return this.id < 0;\n    }\n\n    /** Remove the given attachment globally. */\n    delete() {\n        if (this.tmpUrl) {\n            URL.revokeObjectURL(this.tmpUrl);\n        }\n        super.delete();\n    }\n\n    /**\n     * Delete the given attachment on the server as well as removing it\n     * globally.\n     */\n    async remove() {\n        if (this.id > 0) {\n            await rpc(\n                \"/mail/attachment/delete\",\n                assignDefined({ attachment_id: this.id }, { access_token: this.ownership_token })\n            );\n        }\n        this.delete();\n    }\n\n    get previewName() {\n        return this.voice ? _t(\"Voice Message\") : this.name || \"\";\n    }\n\n    async setPdfThumbnail() {\n        const { isPdfValid, thumbnail } = await generatePdfThumbnail(\n            url(\n                `/mail/attachment/pdf_first_page/${this.id}`,\n                assignDefined({}, { access_token: this.ownership_token })\n            )\n        );\n        if (isPdfValid !== undefined) {\n            rpc(\n                `/mail/attachment/update_thumbnail`,\n                assignDefined(\n                    { attachment_id: this.id, thumbnail },\n                    { access_token: this.ownership_token }\n                )\n            );\n        }\n    }\n}\n\nAttachment.register();\n", "import { EventBus } from \"@odoo/owl\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { Deferred } from \"@web/core/utils/concurrency\";\n\nexport class AttachmentUploadService {\n    constructor(env, services) {\n        this.setup(env, services);\n    }\n\n    setup(env, services) {\n        this.env = env;\n        this.fileUploadService = services[\"file_upload\"];\n        /** @type {import(\"@mail/core/common/store_service\").Store} */\n        this.store = services[\"mail.store\"];\n        this.notificationService = services[\"notification\"];\n\n        this.nextId = -1;\n        this.abortByAttachmentId = new Map();\n        this.deferredByAttachmentId = new Map();\n        this.uploadingAttachmentIds = new Set();\n        this._fileUploadBus = new EventBus();\n        /** @type {Map<number, {composer: import(\"models\").Composer, thread: import(\"models\").Thread}>} */\n        this.targetsByTmpId = new Map();\n        this.fileUploadService.bus.addEventListener(\n            \"FILE_UPLOAD_ADDED\",\n            ({ detail: { upload } }) => {\n                const tmpId = parseInt(upload.data.get(\"temporary_id\"));\n                if (!this.uploadingAttachmentIds.has(tmpId)) {\n                    return;\n                }\n                const { thread, composer } = this.targetsByTmpId.get(tmpId);\n                const tmpUrl = upload.data.get(\"tmp_url\");\n                this.abortByAttachmentId.set(tmpId, upload.xhr.abort.bind(upload.xhr));\n                const attachment = this.store[\"ir.attachment\"].insert(\n                    this._makeAttachmentData(upload, tmpId, composer ? undefined : thread, tmpUrl)\n                );\n                composer?.attachments.push(attachment);\n            }\n        );\n        this.fileUploadService.bus.addEventListener(\n            \"FILE_UPLOAD_LOADED\",\n            ({ detail: { upload } }) => {\n                const tmpId = parseInt(upload.data.get(\"temporary_id\"));\n                if (!this.uploadingAttachmentIds.has(tmpId)) {\n                    return;\n                }\n                const def = this.deferredByAttachmentId.get(tmpId);\n                if (upload.xhr.status === 413) {\n                    this.notificationService.add(_t(\"File too large\"), { type: \"danger\" });\n                    def.resolve();\n                    this._cleanupUploading(tmpId);\n                    return;\n                }\n                if (upload.xhr.status !== 200) {\n                    this.notificationService.add(_t(\"Server error\"), { type: \"danger\" });\n                    def.resolve();\n                    this._cleanupUploading(tmpId);\n                    return;\n                }\n                const response = JSON.parse(upload.xhr.response);\n                if (response.error) {\n                    this.notificationService.add(response.error, { type: \"danger\" });\n                    def.resolve();\n                    this._cleanupUploading(tmpId);\n                    return;\n                }\n                const { thread, composer } = this.targetsByTmpId.get(tmpId);\n                this._processLoaded(thread, composer, response, tmpId, def);\n            }\n        );\n        this.fileUploadService.bus.addEventListener(\n            \"FILE_UPLOAD_ERROR\",\n            ({ detail: { upload } }) => {\n                const tmpId = parseInt(upload.data.get(\"temporary_id\"));\n                if (!this.uploadingAttachmentIds.has(tmpId)) {\n                    return;\n                }\n                this.deferredByAttachmentId.get(tmpId).resolve();\n                this._cleanupUploading(tmpId);\n            }\n        );\n    }\n\n    _processLoaded(thread, composer, { data }, tmpId, def) {\n        const { store_data, attachment_id } = data;\n        this.store.insert(store_data);\n        /** @type {import(\"models\").Attachment} */\n        const attachment = this.store[\"ir.attachment\"].get(attachment_id);\n        if (composer) {\n            const index = composer.attachments.findIndex(({ id }) => id === tmpId);\n            if (index >= 0) {\n                composer.attachments[index] = attachment;\n            } else {\n                composer.attachments.push(attachment);\n            }\n        }\n        def.resolve(attachment);\n        this._fileUploadBus.trigger(\"UPLOAD\", thread);\n        this._cleanupUploading(tmpId);\n    }\n\n    _cleanupUploading(tmpId) {\n        this.abortByAttachmentId.delete(tmpId);\n        this.deferredByAttachmentId.delete(tmpId);\n        this.uploadingAttachmentIds.delete(tmpId);\n        this.targetsByTmpId.delete(tmpId);\n        this.store[\"ir.attachment\"].get(tmpId)?.remove();\n    }\n\n    getUploadURL(thread) {\n        return \"/mail/attachment/upload\";\n    }\n\n    async unlink(attachment) {\n        if (this.uploadingAttachmentIds.has(attachment.id)) {\n            const deferred = this.deferredByAttachmentId.get(attachment.id);\n            const abort = this.abortByAttachmentId.get(attachment.id);\n            this._cleanupUploading(attachment.id);\n            deferred.resolve();\n            abort();\n            return;\n        }\n        await attachment.remove();\n    }\n\n    async upload(thread, composer, file, options) {\n        const tmpId = this.nextId--;\n        const tmpURL = URL.createObjectURL(file);\n        return this._upload(thread, composer, file, options, tmpId, tmpURL);\n    }\n\n    async _upload(thread, composer, file, options, tmpId, tmpURL) {\n        this.targetsByTmpId.set(tmpId, { composer, thread });\n        this.uploadingAttachmentIds.add(tmpId);\n        await this.fileUploadService\n            .upload(this.getUploadURL(thread), [file], {\n                buildFormData: (formData) => {\n                    this._buildFormData(formData, tmpURL, thread, composer, tmpId, options);\n                },\n            })\n            .catch((e) => {\n                if (e.name !== \"AbortError\") {\n                    throw e;\n                }\n            });\n        const uploadDoneDeferred = new Deferred();\n        this.deferredByAttachmentId.set(tmpId, uploadDoneDeferred);\n        return uploadDoneDeferred;\n    }\n\n    /**\n     * @param {import(\"models\").Thread} thread\n     * @param {() => void} onFileUploaded\n     */\n    onFileUploaded(thread, onFileUploaded) {\n        this._fileUploadBus.addEventListener(\"UPLOAD\", ({ detail }) => {\n            if (thread.eq(detail)) {\n                onFileUploaded();\n            }\n        });\n    }\n\n    _buildFormData(formData, tmpURL, thread, composer, tmpId, options) {\n        formData.append(\"thread_id\", thread.id);\n        formData.append(\"tmp_url\", tmpURL);\n        formData.append(\"thread_model\", thread.model);\n        formData.append(\"is_pending\", Boolean(composer));\n        formData.append(\"temporary_id\", tmpId);\n        if (options?.activity) {\n            formData.append(\"activity_id\", options.activity.id);\n        }\n        return formData;\n    }\n\n    _makeAttachmentData(upload, tmpId, thread, tmpUrl) {\n        const attachmentData = {\n            id: tmpId,\n            mimetype: upload.type,\n            name: upload.title,\n            thread,\n            extension: upload.title.split(\".\").pop(),\n            uploading: true,\n            tmpUrl,\n        };\n        return attachmentData;\n    }\n}\n\nexport const attachmentUploadService = {\n    dependencies: [\"file_upload\", \"mail.store\", \"notification\"],\n    start(env, services) {\n        return new AttachmentUploadService(env, services);\n    },\n};\n\nregistry.category(\"services\").add(\"mail.attachment_upload\", attachmentUploadService);\n", "import { useState } from \"@odoo/owl\";\n\nimport { useService } from \"@web/core/utils/hooks\";\n\nexport function dataUrlToBlob(data, type) {\n    const binData = window.atob(data);\n    const uiArr = new Uint8Array(binData.length);\n    uiArr.forEach((_, index) => (uiArr[index] = binData.charCodeAt(index)));\n    return new Blob([uiArr], { type });\n}\n\nexport class AttachmentUploader {\n    constructor(thread, { composer } = {}) {\n        this.attachmentUploadService = useService(\"mail.attachment_upload\");\n        Object.assign(this, { thread, composer });\n    }\n\n    uploadData({ data, name, type }, options) {\n        const file = new File([dataUrlToBlob(data, type)], name, { type });\n        return this.uploadFile(file, options);\n    }\n\n    async uploadFile(file, options) {\n        return this.attachmentUploadService.upload(this.thread, this.composer, file, options);\n    }\n\n    async unlink(attachment) {\n        await this.attachmentUploadService.unlink(attachment);\n    }\n}\n\n/**\n * @param {import(\"models\").Thread} thread\n * @param {Object} [param1={}]\n * @param {import(\"models\").Composer} [param1.composer]\n * @param {function} [param1.onFileUploaded]\n */\nexport function useAttachmentUploader(thread, { composer, onFileUploaded } = {}) {\n    return useState(new AttachmentUploader(...arguments));\n}\n", "import {\n    Component,\n    onMounted,\n    onWillUnmount,\n    onWillUpdateProps,\n    useComponent,\n    useEffect,\n    useRef,\n    useState,\n} from \"@odoo/owl\";\n\nimport { useService } from \"@web/core/utils/hooks\";\nimport { deepEqual } from \"@web/core/utils/objects\";\nimport { hidePDFJSButtons } from \"@web/core/utils/pdfjs\";\n\nclass AbstractAttachmentView extends Component {\n    static template = \"mail.AttachmentView\";\n    static components = {};\n    static props = [\"threadId\", \"threadModel\"];\n\n    setup() {\n        super.setup();\n        this.store = useService(\"mail.store\");\n        this.uiService = useService(\"ui\");\n        this.iframeViewerPdfRef = useRef(\"iframeViewerPdf\");\n        this.state = useState({\n            /** @type {import(\"models\").Thread|undefined} */\n            thread: undefined,\n        });\n        useEffect(\n            (el) => {\n                if (el) {\n                    hidePDFJSButtons(this.iframeViewerPdfRef.el);\n                }\n            },\n            () => [this.iframeViewerPdfRef.el]\n        );\n        this.updateFromProps(this.props);\n        onWillUpdateProps((props) => this.updateFromProps(props));\n    }\n\n    onClickNext() {\n        const index = this.state.thread.attachmentsInWebClientView.findIndex((attachment) =>\n            attachment.eq(this.state.thread.message_main_attachment_id)\n        );\n        this.state.thread.setMainAttachmentFromIndex(\n            index >= this.state.thread.attachmentsInWebClientView.length - 1 ? 0 : index + 1\n        );\n    }\n\n    onClickPrevious() {\n        const index = this.state.thread.attachmentsInWebClientView.findIndex((attachment) =>\n            attachment.eq(this.state.thread.message_main_attachment_id)\n        );\n        this.state.thread.setMainAttachmentFromIndex(\n            index <= 0 ? this.state.thread.attachmentsInWebClientView.length - 1 : index - 1\n        );\n    }\n\n    updateFromProps(props) {\n        this.state.thread = this.store.Thread.insert({\n            id: props.threadId,\n            model: props.threadModel,\n        });\n    }\n\n    get displayName() {\n        return this.state.thread.message_main_attachment_id.name;\n    }\n\n    onClickPopout() {}\n}\n\n/*\n * AttachmentView inside popout window.\n * Popout features disabled as this only makes sense in the non-popout AttachmentView.\n */\nexport class PopoutAttachmentView extends AbstractAttachmentView {\n    static template = \"mail.PopoutAttachmentView\";\n}\n\nexport function usePopoutAttachment() {\n    const component = useComponent();\n    const uiService = useService(\"ui\");\n    const mailPopoutService = useService(\"mail.popout\");\n\n    function attachmentViewParentElementClassList() {\n        const attachmentViewEl = document.querySelector(\".o-mail-Attachment\");\n        let parentElementClassList;\n        if ((parentElementClassList = attachmentViewEl?.parentElement?.classList)) {\n            return parentElementClassList;\n        }\n        return null;\n    }\n\n    function showAttachmentView() {\n        const parentElementClassList = attachmentViewParentElementClassList();\n        const hiddenClass = \"d-none\";\n        if (parentElementClassList?.contains(hiddenClass)) {\n            parentElementClassList.remove(hiddenClass);\n        }\n    }\n\n    function hideAttachmentView() {\n        const parentElementClassList = attachmentViewParentElementClassList();\n        const hiddenClass = \"d-none\";\n        if (!parentElementClassList?.contains(hiddenClass)) {\n            parentElementClassList?.add(hiddenClass);\n        }\n    }\n\n    function extractPopoutProps(props) {\n        return {\n            threadId: props.threadId,\n            threadModel: props.threadModel,\n        };\n    }\n\n    function popout() {\n        mailPopoutService.addHooks(\n            () => {\n                hideAttachmentView();\n                uiService.bus.trigger(\"resize\");\n            },\n            () => {\n                showAttachmentView();\n                uiService.bus.trigger(\"resize\");\n            }\n        );\n        mailPopoutService.popout(PopoutAttachmentView, extractPopoutProps(component.props));\n    }\n\n    function updatePopout(newProps = component.props) {\n        if (mailPopoutService.externalWindow) {\n            hideAttachmentView();\n            mailPopoutService.popout(PopoutAttachmentView, extractPopoutProps(newProps));\n        }\n    }\n\n    function resetPopout() {\n        mailPopoutService.reset();\n    }\n\n    onMounted(updatePopout);\n    onWillUpdateProps((props) => {\n        const oldProps = extractPopoutProps(component.props);\n        const newProps = extractPopoutProps(props);\n        if (!deepEqual(oldProps, newProps)) {\n            updatePopout(newProps);\n        }\n    });\n    onWillUnmount(resetPopout);\n    return {\n        popout,\n        updatePopout,\n        resetPopout,\n    };\n}\n\n/**\n * @typedef {Object} Props\n * @property {number} threadId\n * @property {string} threadModel\n * @extends {Component<Props, Env>}\n */\nexport class AttachmentView extends AbstractAttachmentView {\n    setup() {\n        super.setup();\n        this.attachmentPopout = usePopoutAttachment();\n    }\n\n    onClickPopout() {\n        this.attachmentPopout.popout();\n    }\n}\n", "import { Component, useRef, useState, onWillUpdateProps, onMounted } from \"@odoo/owl\";\n\nimport { useAutoresize } from \"@web/core/utils/autoresize\";\n\nexport class AutoresizeInput extends Component {\n    static template = \"mail.AutoresizeInput\";\n    static props = {\n        autofocus: { type: Boolean, optional: true },\n        className: { type: String, optional: true },\n        enabled: { optional: true },\n        onValidate: { type: Function, optional: true },\n        placeholder: { type: String, optional: true },\n        value: { type: String, optional: true },\n    };\n    static defaultProps = {\n        autofocus: false,\n        className: \"\",\n        enabled: true,\n        onValidate: () => {},\n        placeholder: \"\",\n    };\n\n    setup() {\n        super.setup();\n        this.state = useState({\n            value: this.props.value,\n            isFocused: false,\n        });\n        this.inputRef = useRef(\"input\");\n        onWillUpdateProps((nextProps) => {\n            if (this.props.value !== nextProps.value) {\n                this.state.value = nextProps.value;\n            }\n        });\n        useAutoresize(this.inputRef);\n        onMounted(() => {\n            if (this.props.autofocus) {\n                this.inputRef.el.focus();\n                this.inputRef.el.setSelectionRange(-1, -1);\n            }\n        });\n    }\n\n    /**\n     * @param {KeyboardEvent} ev\n     */\n    onKeydownInput(ev) {\n        switch (ev.key) {\n            case \"Enter\":\n                this.inputRef.el.blur();\n                break;\n            case \"Escape\":\n                ev.stopPropagation();\n                this.state.value = this.props.value;\n                this.inputRef.el.blur();\n                break;\n        }\n    }\n\n    onBlurInput() {\n        this.state.isFocused = false;\n        this.props.onValidate(this.state.value);\n    }\n}\n", "import { Record } from \"@mail/core/common/record\";\n\nexport class CannedResponse extends Record {\n    static _name = \"mail.canned.response\";\n    static id = \"id\";\n\n    /** @type {number} */\n    id;\n    /** @type {string} */\n    source;\n    /** @type {string} */\n    substitution;\n}\n\nCannedResponse.register();\n", "import { ImStatus } from \"@mail/core/common/im_status\";\n\nimport { Component, useEffect, useRef, useState, useSubEnv } from \"@odoo/owl\";\n\nimport { useChildRef, useService } from \"@web/core/utils/hooks\";\nimport { useHover } from \"@mail/utils/common/hooks\";\nimport { usePopover } from \"@web/core/popover/popover_hook\";\nimport { CountryFlag } from \"@mail/core/common/country_flag\";\nimport { isMobileOS } from \"@web/core/browser/feature_detection\";\n\nclass ChatBubblePreview extends Component {\n    static props = [\"chatWindow\", \"close\"];\n    static template = \"mail.ChatBubblePreview\";\n\n    /** @returns {import(\"models\").Thread} */\n    get thread() {\n        return this.props.chatWindow.thread;\n    }\n\n    get previewText() {\n        const lastMessage = this.thread?.newestPersistentOfAllMessage;\n        if (!lastMessage) {\n            return false;\n        }\n        return lastMessage.previewText;\n    }\n}\n\n/**\n * @typedef {Object} Props\n * @extends {Component<Props, Env>}\n */\nexport class ChatBubble extends Component {\n    static components = { CountryFlag, ImStatus };\n    static props = [\"chatWindow\"];\n    static template = \"mail.ChatBubble\";\n\n    setup() {\n        super.setup();\n        this.store = useService(\"mail.store\");\n        const popoverRef = useChildRef();\n        this.isMobileOS = isMobileOS();\n        this.popover = usePopover(ChatBubblePreview, {\n            animation: false,\n            position: \"left-middle\",\n            popoverClass:\n                \"dropdown-menu bg-view border-0 p-0 overflow-visible o-rounded-bubble mx-1\",\n            ref: popoverRef,\n        });\n        this.env.bus.addEventListener(\"ChatBubble:preview-will-open\", ({ detail }) => {\n            if (detail === this) {\n                return;\n            }\n            this.popover.close();\n        });\n        this.hover = useHover([\"root\", popoverRef], {\n            onHover: () => {\n                this.env.bus.trigger(\"ChatBubble:preview-will-open\", this);\n                this.popover.open(this.rootRef.el, { chatWindow: this.props.chatWindow });\n            },\n            onAway: () => this.popover.close(),\n        });\n        this.rootRef = useRef(\"root\");\n        this.state = useState({ bouncing: false });\n        useEffect(\n            (importantCounter) => {\n                this.state.bouncing = Boolean(importantCounter);\n            },\n            () => [this.thread?.importantCounter]\n        );\n        useSubEnv({ inChatBubble: true });\n    }\n\n    /** @returns {import(\"models\").Thread} */\n    get thread() {\n        return this.props.chatWindow.thread;\n    }\n\n    get showImStatus() {\n        return (\n            this.thread?.correspondent?.im_status &&\n            this.thread.correspondent.im_status !== \"offline\"\n        );\n    }\n}\n", "import { ChatWindow } from \"@mail/core/common/chat_window\";\nimport { ActionList } from \"@mail/core/common/action_list\";\nimport { useHover, useMovable } from \"@mail/utils/common/hooks\";\nimport { Component, useEffect, useExternalListener, useRef, useState } from \"@odoo/owl\";\n\nimport { browser } from \"@web/core/browser/browser\";\nimport { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { useDropdownState } from \"@web/core/dropdown/dropdown_hooks\";\nimport { registry } from \"@web/core/registry\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { ChatBubble } from \"./chat_bubble\";\nimport { isMobileOS } from \"@web/core/browser/feature_detection\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { Action } from \"@mail/core/common/action\";\n\nexport class ChatHub extends Component {\n    static components = { ActionList, ChatBubble, ChatWindow, Dropdown };\n    static props = [];\n    static template = \"mail.ChatHub\";\n\n    get chatHub() {\n        return this.store.chatHub;\n    }\n\n    setup() {\n        super.setup();\n        this.store = useService(\"mail.store\");\n        this.ui = useService(\"ui\");\n        this.busMonitoring = useService(\"bus.monitoring_service\");\n        this.bubblesHover = useHover(\"bubbles\");\n        this.moreHover = useHover([\"more-button\", \"more-menu\"], {\n            onHover: () => (this.more.isOpen = true),\n            onAway: () => (this.more.isOpen = false),\n        });\n        this.options = useDropdownState();\n        this.more = useDropdownState();\n        this.ref = useRef(\"bubbles\");\n        this.position = useState({\n            dragged: false,\n            isDragging: false,\n            top: \"unset\",\n            left: \"unset\",\n            bottom: `${this.chatHub.BUBBLE_OUTER}px;`,\n            right: `${this.chatHub.BUBBLE_OUTER + this.chatHub.BUBBLE_START}px;`,\n        });\n        this.onResize();\n        useExternalListener(browser, \"resize\", this.onResize);\n        useEffect(() => {\n            if (this.chatHub.folded.length && this.store.channels?.status === \"not_fetched\") {\n                this.store.channels.fetch();\n            }\n        });\n        useMovable({\n            enable: () => this.chatHub.compact || !this.chatHub.opened.length,\n            cursor: \"grabbing\",\n            ref: this.ref,\n            elements: \".o-mail-ChatHub-bubbles\",\n            onDragStart: () => {\n                this.more.close();\n                this.options.close();\n                this.position.isDragging = true;\n                this.position.dragged = true;\n            },\n            onDragEnd: () => (this.position.isDragging = false),\n            onDrop: this.onDrop.bind(this),\n        });\n        this.env.bus.addEventListener(\"ChatWindow:will-open\", () => {\n            this.resetPosition();\n        });\n    }\n\n    get optionActions() {\n        const actions = [];\n        if (this.chatHub.showConversations && !this.chatHub.compact) {\n            actions.push(\n                new Action({\n                    owner: this,\n                    id: \"hide-all\",\n                    definition: {\n                        name: _t(\"Hide all conversations\"),\n                        icon: \"fa fa-eye-slash\",\n                        onSelected: () => this.chatHub.hideAll(),\n                    },\n                    store: this.store,\n                }),\n                new Action({\n                    owner: this,\n                    id: \"close-all\",\n                    definition: {\n                        name: _t(\"Close all conversations\"),\n                        icon: \"oi oi-close\",\n                        onSelected: () => this.chatHub.closeAll(),\n                    },\n                    store: this.store,\n                })\n            );\n        }\n        if (this.position.dragged) {\n            actions.push(\n                new Action({\n                    owner: this,\n                    id: \"reset-position\",\n                    definition: {\n                        name: _t(\"Reset initial position\"),\n                        icon: \"fa fa-undo\",\n                        onSelected: () => this.resetPosition(),\n                    },\n                    store: this.store,\n                })\n            );\n        }\n        return actions;\n    }\n\n    get isMobileOS() {\n        return isMobileOS();\n    }\n\n    onDrop({ top, left }) {\n        this.position.bottom = \"unset\";\n        this.position.right = \"unset\";\n        this.position.top = `${top}px`;\n        this.position.left = `${left}px`;\n    }\n\n    onResize() {\n        this.chatHub.onRecompute();\n    }\n\n    resetPosition() {\n        this.position.top = \"unset\";\n        this.position.left = \"unset\";\n        this.position.bottom = `${this.chatHub.BUBBLE_OUTER}px;`;\n        this.position.right = `${this.chatHub.BUBBLE_OUTER + this.chatHub.BUBBLE_START}px;`;\n        this.position.dragged = false;\n        this.options.close();\n    }\n\n    get compactCounter() {\n        let counter = 0;\n        const cws = this.chatHub.opened.concat(this.chatHub.folded);\n        for (const chatWindow of cws) {\n            counter += chatWindow.thread.importantCounter > 0 ? 1 : 0;\n        }\n        return counter;\n    }\n\n    get hiddenCounter() {\n        let counter = 0;\n        for (const chatWindow of this.chatHub.folded.slice(this.chatHub.maxFolded)) {\n            counter += chatWindow.thread.importantCounter > 0 ? 1 : 0;\n        }\n        return counter;\n    }\n\n    /** @deprecated */\n    get displayConversations() {\n        return this.chatHub.showConversations && !this.chatHub.compact;\n    }\n\n    /** @deprecated */\n    get isShown() {\n        return true;\n    }\n\n    /** @deprecated */\n    shouldDisplayChatWindow(cw) {\n        return cw.canShow;\n    }\n\n    expand() {\n        this.chatHub.compact = false;\n        this.more.isOpen = this.chatHub.folded.length > this.chatHub.maxFolded;\n        if (this.chatHub.opened.length > 0) {\n            this.resetPosition();\n        }\n    }\n}\n\nexport const chatHubService = {\n    dependencies: [\"bus.monitoring_service\", \"mail.store\", \"ui\"],\n    start() {\n        registry.category(\"main_components\").add(\"mail.ChatHub\", { Component: ChatHub });\n    },\n};\nregistry.category(\"services\").add(\"mail.chat_hub\", chatHubService);\n", "import { browser } from \"@web/core/browser/browser\";\nimport { fields, Record } from \"./record\";\n\nimport { Deferred, Mutex } from \"@web/core/utils/concurrency\";\n\nexport const CHAT_HUB_KEY = \"mail.ChatHub\";\nconst CHAT_HUB_COMPACT_LS = \"mail.user_setting.chathub_compact\";\n\nexport class ChatHub extends Record {\n    BUBBLE = 56; // same value as $o-mail-ChatHub-bubblesWidth\n    BUBBLE_START = 15; // same value as $o-mail-ChatHub-bubblesStart\n    BUBBLE_LIMIT = 7;\n    BUBBLE_OUTER = 10; // same value as $o-mail-ChatHub-bubblesMargin\n    WINDOW_GAP = 10; // for a single end, multiply by 2 for left and right together.\n    WINDOW_INBETWEEN = 5;\n    WINDOW = 380; // same value as $o-mail-ChatWindow-width\n\n    /** @returns {import(\"models\").ChatHub} */\n    static new() {\n        /** @type {import(\"models\").ChatHub} */\n        const chatHub = super.new(...arguments);\n        browser.addEventListener(\"storage\", (ev) => {\n            if (ev.key === CHAT_HUB_KEY) {\n                chatHub.load(ev.newValue || undefined);\n            } else if (ev.key === null) {\n                chatHub.load();\n            }\n            if (ev.key === CHAT_HUB_COMPACT_LS) {\n                chatHub.compact = ev.newValue === \"true\";\n            }\n        });\n        chatHub\n            .load(browser.localStorage.getItem(CHAT_HUB_KEY) ?? undefined)\n            .then(() => chatHub.initPromise.resolve());\n        return chatHub;\n    }\n\n    compact = fields.Attr(false, {\n        compute() {\n            return browser.localStorage.getItem(CHAT_HUB_COMPACT_LS) === \"true\";\n        },\n        /** @this {import(\"models\").Chathub} */\n        onUpdate() {\n            if (this.compact) {\n                browser.localStorage.setItem(CHAT_HUB_COMPACT_LS, this.compact.toString());\n            } else {\n                browser.localStorage.removeItem(CHAT_HUB_COMPACT_LS);\n            }\n        },\n    });\n    canShowOpened = fields.Many(\"ChatWindow\");\n    canShowFolded = fields.Many(\"ChatWindow\");\n    /** From left to right. Right-most will actually be folded */\n    opened = fields.Many(\"ChatWindow\", {\n        inverse: \"hubAsOpened\",\n        /** @this {import(\"models\").ChatHub} */\n        onAdd(r) {\n            this.onRecompute();\n        },\n    });\n    /** From top to bottom. Bottom-most will actually be hidden */\n    folded = fields.Many(\"ChatWindow\", { inverse: \"hubAsFolded\" });\n    initPromise = new Deferred();\n    preFirstFetchPromise = new Deferred();\n    loadMutex = new Mutex();\n\n    async closeAll() {\n        await this.initPromise;\n        const promises = [];\n        for (const cw of [...this.opened, ...this.folded]) {\n            promises.push(cw.close({ notifyState: false }));\n        }\n        await Promise.all(promises);\n        this.save(); // sync only once at the end\n    }\n\n    hideAll() {\n        for (const cw of this.opened) {\n            cw.bypassCompact = false;\n        }\n        this.compact = true;\n    }\n\n    onRecompute() {\n        while (this.opened.length > this.maxOpened) {\n            const cw = this.opened.pop();\n            this.folded.unshift(cw);\n        }\n    }\n\n    async load(str = \"{}\") {\n        await this.loadMutex.exec(() => this._load(str));\n    }\n\n    async _load(str) {\n        /** @type {{ opened: Object[], folded: Object[] }} */\n        const { opened = [], folded = [] } = JSON.parse(str);\n        const hasInvalidData =\n            opened.some((data) => !data.id || !data.model) ||\n            folded.some((data) => !data.id || !data.model);\n        if (hasInvalidData) {\n            opened.length = 0;\n            folded.length = 0;\n            browser.localStorage.removeItem(CHAT_HUB_KEY);\n        }\n        const getThread = (data) => this.store.Thread.getOrFetch(data, [\"display_name\"]);\n        const openPromises = opened.map(getThread);\n        const foldPromises = folded.map(getThread);\n        this.preFirstFetchPromise.resolve();\n        const foldThreads = await Promise.all(foldPromises);\n        const openThreads = await Promise.all(openPromises);\n        /** @param {import(\"models\").Thread[]} threads */\n        const insertChatWindows = (threads) =>\n            threads\n                .filter((thread) => thread?.model === \"discuss.channel\")\n                .map((thread) => this.store.ChatWindow.insert({ thread }));\n        const toFold = insertChatWindows(foldThreads);\n        const toOpen = insertChatWindows(openThreads);\n        // close first to make room for others\n        for (const chatWindow of [...this.opened, ...this.folded]) {\n            if (chatWindow.notIn(toOpen) && chatWindow.notIn(toFold)) {\n                chatWindow.close({ force: true, notifyState: false });\n            }\n        }\n        // folded before opened because if there are too many opened they will be added to folded\n        this.folded = toFold;\n        this.opened = toOpen;\n    }\n\n    get maxOpened() {\n        const chatBubblesWidth = this.BUBBLE_START + this.BUBBLE + this.BUBBLE_OUTER * 2;\n        const startGap = this.store.env.services.ui.isSmall ? 0 : this.WINDOW_GAP;\n        const endGap = this.store.env.services.ui.isSmall ? 0 : this.WINDOW_GAP;\n        const available = browser.innerWidth - startGap - endGap - chatBubblesWidth;\n        const maxAmountWithoutHidden = Math.max(\n            1,\n            Math.floor(available / (this.WINDOW + this.WINDOW_INBETWEEN))\n        );\n        return maxAmountWithoutHidden;\n    }\n\n    get maxFolded() {\n        const chatBubbleSpace = this.BUBBLE_START + this.BUBBLE + this.BUBBLE_OUTER * 2;\n        return Math.min(this.BUBBLE_LIMIT, Math.floor(browser.innerHeight / chatBubbleSpace));\n    }\n\n    save() {\n        browser.localStorage.setItem(\n            CHAT_HUB_KEY,\n            JSON.stringify({\n                opened: this.opened.map((cw) => ({ id: cw.thread.id, model: cw.thread.model })),\n                folded: this.folded.map((cw) => ({ id: cw.thread.id, model: cw.thread.model })),\n            })\n        );\n    }\n\n    showConversations = fields.Attr(false, {\n        compute() {\n            return this.canShowOpened.length + this.canShowFolded.length > 0;\n        },\n    });\n}\n\nChatHub.register();\n", "import { ActionList } from \"@mail/core/common/action_list\";\nimport { Composer } from \"@mail/core/common/composer\";\nimport { ImStatus } from \"@mail/core/common/im_status\";\nimport { Thread } from \"@mail/core/common/thread\";\nimport { AutoresizeInput } from \"@mail/core/common/autoresize_input\";\nimport { CountryFlag } from \"@mail/core/common/country_flag\";\nimport { useThreadActions } from \"@mail/core/common/thread_actions\";\nimport { ThreadIcon } from \"@mail/core/common/thread_icon\";\nimport { useHover, useMessageScrolling } from \"@mail/utils/common/hooks\";\nimport { isEventHandled } from \"@web/core/utils/misc\";\n\nimport { Component, toRaw, useChildSubEnv, useRef, useState, useSubEnv } from \"@odoo/owl\";\n\nimport { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { localization } from \"@web/core/l10n/localization\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { Typing } from \"@mail/discuss/typing/common/typing\";\nimport { getActiveHotkey } from \"@web/core/hotkeys/hotkey_service\";\nimport { isMobileOS } from \"@web/core/browser/feature_detection\";\n\n/**\n * @typedef {Object} Props\n * @property {import(\"models\").ChatWindow} chatWindow\n * @property {boolean} [right]\n * @extends {Component<Props, Env>}\n */\nexport class ChatWindow extends Component {\n    static components = {\n        ActionList,\n        CountryFlag,\n        Dropdown,\n        Thread,\n        Composer,\n        ThreadIcon,\n        ImStatus,\n        AutoresizeInput,\n        Typing,\n    };\n    static props = [\"chatWindow\", \"right?\"];\n    static template = \"mail.ChatWindow\";\n\n    setup() {\n        super.setup();\n        useSubEnv({ inChatWindow: true });\n        this.store = useService(\"mail.store\");\n        this.messageHighlight = useMessageScrolling();\n        this.state = useState({\n            actionsMenuOpened: false,\n            jumpThreadPresent: 0,\n            editingGuestName: false,\n            editingName: false,\n        });\n        this.ui = useService(\"ui\");\n        this.contentRef = useRef(\"content\");\n        this.threadActions = useThreadActions({ thread: () => this.thread });\n        this.actionsMenuButtonHover = useHover(\"actionsMenuButton\");\n        this.parentChannelHover = useHover(\"parentChannel\");\n        this.isMobileOS = isMobileOS();\n\n        useChildSubEnv({\n            closeActionPanel: () => this.threadActions.activeAction?.close(),\n            messageHighlight: this.messageHighlight,\n        });\n    }\n\n    get composerType() {\n        if (this.thread.model !== \"discuss.channel\") {\n            return \"note\";\n        }\n        return undefined;\n    }\n\n    get hasActionsMenu() {\n        return (\n            this.partitionedActions.group.length > 0 ||\n            this.partitionedActions.other.length > 0 ||\n            (this.ui.isSmall && this.partitionedActions.quick.length > 2) ||\n            (!this.ui.isSmall && this.partitionedActions.quick.length > 3)\n        );\n    }\n\n    get thread() {\n        return this.props.chatWindow.thread;\n    }\n\n    get showImStatus() {\n        return this.thread?.channel_type === \"chat\" && this.thread.correspondent;\n    }\n\n    get attClass() {\n        return {\n            \"w-100 h-100 o-mobile\": this.ui.isSmall,\n            \"o-rounded-bubble border border-dark mb-2\": !this.ui.isSmall,\n        };\n    }\n\n    get style() {\n        const textDirection = localization.direction;\n        const offsetFrom = textDirection === \"rtl\" ? \"left\" : \"right\";\n        const visibleOffset = this.ui.isSmall ? 0 : this.props.right;\n        const oppositeFrom = offsetFrom === \"right\" ? \"left\" : \"right\";\n        return `${offsetFrom}: ${visibleOffset}px; ${oppositeFrom}: auto;`;\n    }\n\n    onKeydown(ev) {\n        const chatWindow = toRaw(this.props.chatWindow);\n        if (ev.key === \"Escape\" && this.threadActions.activeAction) {\n            this.threadActions.activeAction.close();\n            ev.stopPropagation();\n            return;\n        }\n        if (ev.target.closest(\".o-dropdown\") || ev.target.closest(\".o-dropdown--menu\")) {\n            return;\n        }\n        ev.stopPropagation(); // not letting home menu steal my CTRL-C\n        switch (getActiveHotkey(ev)) {\n            case \"escape\":\n                if (\n                    isEventHandled(ev, \"NavigableList.close\") ||\n                    isEventHandled(ev, \"Composer.discard\")\n                ) {\n                    return;\n                }\n                if (this.state.editingName) {\n                    this.state.editingName = false;\n                    return;\n                }\n                this.close({ escape: true });\n                break;\n            case \"tab\": {\n                const index = this.store.chatHub.opened.findIndex((cw) => cw.eq(chatWindow));\n                if (index === this.store.chatHub.opened.length - 1) {\n                    this.store.chatHub.opened[0].focus({ jumpToNewMessage: true });\n                } else {\n                    this.store.chatHub.opened[index + 1].focus({ jumpToNewMessage: true });\n                }\n                break;\n            }\n            case \"control+k\":\n                this.store.env.services.command.openMainPalette({ searchValue: \"@\" });\n                ev.preventDefault();\n                break;\n        }\n    }\n\n    onClickHeader(ev) {\n        if (\n            this.ui.isSmall ||\n            this.state.editingName ||\n            this.props.chatWindow.actionsDisabled ||\n            isEventHandled(ev, \"ThreadAction.onSelected\")\n        ) {\n            return;\n        }\n        this.toggleFold();\n    }\n\n    toggleFold() {\n        const chatWindow = toRaw(this.props.chatWindow);\n        if (this.state.actionsMenuOpened) {\n            return;\n        }\n        chatWindow.fold();\n    }\n\n    close(options) {\n        const chatWindow = toRaw(this.props.chatWindow);\n        chatWindow.close(options);\n    }\n\n    get actionsMenuTitleText() {\n        return _t(\"Open Actions Menu\");\n    }\n\n    async renameThread(name) {\n        const thread = toRaw(this.thread);\n        await thread.rename(name);\n        this.state.editingName = false;\n    }\n\n    async renameGuest(name) {\n        const newName = name.trim();\n        if (this.store.self.name !== newName) {\n            await this.store.self.updateGuestName(newName);\n        }\n        this.state.editingGuestName = false;\n    }\n\n    async onActionsMenuStateChanged(isOpen) {\n        // await new Promise(setTimeout); // wait for bubbling header\n        this.state.actionsMenuOpened = isOpen;\n    }\n}\n", "import { fields, Record } from \"@mail/core/common/record\";\n\n/** @typedef {{ thread?: import(\"models\").Thread }} ChatWindowData */\n\nexport class ChatWindow extends Record {\n    static id = \"thread\";\n\n    actionsDisabled = false;\n    bypassCompact = false;\n    thread = fields.One(\"Thread\", { inverse: \"chat_window\" });\n    autofocus = 0;\n    jumpToNewMessage = 0;\n    hidden = false;\n    /** Whether the chat window was created from the messaging menu */\n    fromMessagingMenu = false;\n    hubAsOpened = fields.One(\"ChatHub\", { inverse: \"opened\" });\n    hubAsFolded = fields.One(\"ChatHub\", { inverse: \"folded\" });\n    hubAsCanShowOpened = fields.One(\"ChatHub\", {\n        inverse: \"canShowOpened\",\n        /** @this {import(\"models\").ChatWindow} */\n        compute() {\n            if (this.canShow && this.hubAsOpened) {\n                return this.store.chatHub;\n            }\n        },\n    });\n    hubAsCanShowFolded = fields.One(\"ChatHub\", {\n        inverse: \"canShowFolded\",\n        /** @this {import(\"models\").ChatWindow} */\n        compute() {\n            if (this.canShow && this.hubAsFolded) {\n                return this.store.chatHub;\n            }\n        },\n    });\n\n    get displayName() {\n        return this.thread?.displayName;\n    }\n\n    get isOpen() {\n        return Boolean(this.hubAsOpened);\n    }\n\n    canShow = fields.Attr(true, {\n        compute() {\n            return this.computeCanShow();\n        },\n    });\n\n    computeCanShow() {\n        if (this.store.env.services.ui.isSmall) {\n            return !this.hubAsFolded || !this.store.discuss?.isActive;\n        }\n        return !this.store.discuss?.isActive;\n    }\n\n    async close(options = {}) {\n        await this.store.chatHub.initPromise;\n        const { escape = false } = options;\n        options.notifyState ??= true;\n        const chatHub = this.store.chatHub;\n        const indexAsOpened = chatHub.opened.findIndex((w) => w.eq(this));\n        this.store.chatHub.opened.delete(this);\n        this.store.chatHub.folded.delete(this);\n        if (options.notifyState) {\n            this.store.chatHub.save();\n        }\n        if (escape && indexAsOpened !== -1 && chatHub.opened.length > 0) {\n            chatHub.opened[indexAsOpened === 0 ? 0 : indexAsOpened - 1].focus();\n        }\n        this._onClose(options);\n        this.delete();\n    }\n\n    focus({ jumpToNewMessage = false } = {}) {\n        this.autofocus++;\n        if (jumpToNewMessage) {\n            this.jumpToNewMessage++;\n        }\n    }\n\n    async fold() {\n        await this.store.chatHub.initPromise;\n        this.store.chatHub.opened.delete(this);\n        this.store.chatHub.folded.delete(this);\n        this.store.chatHub.folded.unshift(this);\n        this.store.chatHub.save();\n        this.bypassCompact = false;\n    }\n\n    async open({\n        focus = false,\n        notifyState = true,\n        jumpToNewMessage = false,\n        swapOpened = true,\n    } = {}) {\n        await this.store.chatHub.initPromise;\n        this.store.env.bus.trigger(\"ChatWindow:will-open\");\n        this.store.chatHub.folded.delete(this);\n        if (swapOpened || !this.store.chatHub.opened.includes(this)) {\n            this.store.chatHub.opened.delete(this);\n            this.store.chatHub.opened.unshift(this);\n        }\n        if (notifyState) {\n            this.store.chatHub.save();\n        }\n        if (focus) {\n            this.focus({ jumpToNewMessage });\n        }\n    }\n\n    _onClose() {}\n}\n\nChatWindow.register();\n", "import { AttachmentList } from \"@mail/core/common/attachment_list\";\nimport { useAttachmentUploader } from \"@mail/core/common/attachment_uploader_hook\";\nimport { useCustomDropzone } from \"@web/core/dropzone/dropzone_hook\";\nimport { MailAttachmentDropzone } from \"@mail/core/common/mail_attachment_dropzone\";\nimport { MessageConfirmDialog } from \"@mail/core/common/message_confirm_dialog\";\nimport { NavigableList } from \"@mail/core/common/navigable_list\";\nimport { MAIL_PLUGINS, MAIL_SMALL_UI_PLUGINS } from \"@mail/core/common/plugin/plugin_sets\";\nimport { useSuggestion } from \"@mail/core/common/suggestion_hook\";\nimport { useSelection } from \"@mail/utils/common/hooks\";\nimport { isDragSourceExternalFile } from \"@mail/utils/common/misc\";\n\nimport { Wysiwyg } from \"@html_editor/wysiwyg\";\n\nimport { rpc } from \"@web/core/network/rpc\";\nimport { isEventHandled, markEventHandled } from \"@web/core/utils/misc\";\nimport { browser } from \"@web/core/browser/browser\";\nimport { useDebounced } from \"@web/core/utils/timing\";\n\nimport {\n    Component,\n    markup,\n    onMounted,\n    onWillUnmount,\n    useChildSubEnv,\n    useEffect,\n    useRef,\n    useState,\n    useExternalListener,\n    toRaw,\n    EventBus,\n    reactive,\n} from \"@odoo/owl\";\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport {\n    createElementWithContent,\n    htmlJoin,\n    isHtmlEmpty,\n    isMarkup,\n    setElementContent,\n} from \"@web/core/utils/html\";\nimport { FileUploader } from \"@web/views/fields/file_handler\";\nimport { isEmail } from \"@web/core/utils/strings\";\nimport { isDisplayStandalone, isIOS, isMobileOS } from \"@web/core/browser/feature_detection\";\nimport { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\nimport { useComposerActions } from \"@mail/core/common/composer_actions\";\nimport { ActionList } from \"@mail/core/common/action_list\";\nimport { lastLeaf } from \"@html_editor/utils/dom_traversal\";\n\nconst EDIT_CLICK_TYPE = {\n    CANCEL: \"cancel\",\n    SAVE: \"save\",\n};\n\n/**\n * @typedef {Object} Props\n * @property {import(\"models\").Composer} composer\n * @property {'compact'|'normal'|'extended'} [mode] default: 'normal'\n * @property {'message'|'note'|false} [type] default: false\n * @property {string} [placeholder]\n * @property {string} [className]\n * @property {function} [onDiscardCallback]\n * @property {function} [onPostCallback]\n * @property {number} [autofocus]\n * @property {import(\"@web/core/utils/hooks\").Ref} [dropzoneRef]\n * @extends {Component<Props, Env>}\n */\nexport class Composer extends Component {\n    static components = {\n        ActionList,\n        AttachmentList,\n        Dropdown,\n        DropdownItem,\n        FileUploader,\n        NavigableList,\n        Wysiwyg,\n    };\n    static defaultProps = {\n        mode: \"normal\",\n        className: \"\",\n        sidebar: true,\n        showFullComposer: true,\n        allowUpload: true,\n    };\n    static props = [\n        \"composer\",\n        \"autofocus?\",\n        \"onCloseFullComposerCallback?\",\n        \"onDiscardCallback?\",\n        \"onPostCallback?\",\n        \"mode?\",\n        \"placeholder?\",\n        \"dropzoneRef?\",\n        \"className?\",\n        \"sidebar?\",\n        \"type?\",\n        \"showFullComposer?\",\n        \"allowUpload?\",\n    ];\n    static template = \"mail.Composer\";\n\n    setup() {\n        super.setup();\n        /** @type {import(\"@html_editor/editor\").Editor} */\n        this.editor = undefined;\n        this.isMobileOS = isMobileOS();\n        this.isIosPwa = isIOS() && isDisplayStandalone();\n        this.store = useService(\"mail.store\");\n        this.composerActions = useComposerActions({ composer: () => this.props.composer });\n        this.EDIT_CLICK_TYPE = EDIT_CLICK_TYPE;\n        this.OR_PRESS_SEND_KEYBIND = _t(\"or press %(send_keybind)s\", {\n            send_keybind: htmlJoin(\n                this.sendKeybinds.map((key) => markup`<samp>${key}</samp>`),\n                \" + \"\n            ),\n        });\n        this.attachmentUploader = useAttachmentUploader(\n            this.thread ?? this.props.composer.message.thread,\n            { composer: this.props.composer }\n        );\n        this.ui = useService(\"ui\");\n        this.composerService = useService(\"mail.composer\");\n        this.ref = useRef(\"textarea\");\n        this.fakeTextarea = useRef(\"fakeTextarea\");\n        this.inputContainerRef = useRef(\"input-container\");\n        this.pickerContainerRef = useRef(\"picker-container\");\n        this.state = useState({\n            active: true,\n            isFullComposerOpen: false,\n        });\n        this.root = useRef(\"root\");\n        this.fullComposerBus = new EventBus();\n        this.selection = useSelection({\n            refName: \"textarea\",\n            model: this.props.composer.selection,\n            preserveOnClickAwayPredicate: async (ev) => {\n                // Let event be handled by bubbling handlers first.\n                await new Promise(setTimeout);\n                return (\n                    !this.isEventTrusted(ev) ||\n                    isEventHandled(ev, \"sidebar.openThread\") ||\n                    isEventHandled(ev, \"emoji.selectEmoji\") ||\n                    isEventHandled(ev, \"Composer.onClickAddEmoji\") ||\n                    isEventHandled(ev, \"composer.clickOnAddAttachment\") ||\n                    isEventHandled(ev, \"composer.selectSuggestion\") ||\n                    isEventHandled(ev, \"composer.clickInsertCannedResponse\")\n                );\n            },\n        });\n        this.suggestion = useSuggestion();\n        this.markEventHandled = markEventHandled;\n        this.onDropFile = this.onDropFile.bind(this);\n        this.saveContentDebounced = useDebounced(this.saveContent, 5000, {\n            execBeforeUnmount: true,\n        });\n        this.updateFromEditor = false;\n        useExternalListener(window, \"beforeunload\", this.saveContent.bind(this));\n        useExternalListener(\n            window,\n            \"click\",\n            (ev) => {\n                if (\n                    this.ui.isSmall &&\n                    this.composerActions.activePicker &&\n                    this.pickerContainerRef.el &&\n                    ev.target !== this.pickerContainerRef.el &&\n                    !this.pickerContainerRef.el.contains(ev.target)\n                ) {\n                    this.composerActions.activePicker.close?.();\n                }\n            },\n            { capture: true }\n        );\n        if (this.props.dropzoneRef) {\n            useCustomDropzone(\n                this.props.dropzoneRef,\n                MailAttachmentDropzone,\n                {\n                    extraClass: \"o-mail-Composer-dropzone\",\n                    onDrop: this.onDropFile,\n                },\n                () =>\n                    this.props.allowUpload &&\n                    (!this.store.rtc.state.isFullscreen || this.env.inMeetingView)\n            );\n        }\n        useChildSubEnv({ inComposer: true });\n        useEffect(\n            (focus) => {\n                if (focus && this.ref.el) {\n                    this.selection.restore();\n                    this.ref.el.focus();\n                }\n                if (focus && this.editor) {\n                    this.editor.shared.selection.focusEditable();\n                }\n            },\n            () => [this.props.autofocus + this.props.composer.autofocus, this.props.placeholder]\n        );\n        useEffect(\n            () => {\n                if (this.props.composer.replyToMessage) {\n                    this.props.composer.autofocus++;\n                }\n            },\n            () => [this.props.composer.replyToMessage]\n        );\n        useEffect(\n            () => {\n                if (this.fakeTextarea.el?.scrollHeight) {\n                    let wasEmpty = false;\n                    if (!this.fakeTextarea.el.value) {\n                        wasEmpty = true;\n                        this.fakeTextarea.el.value = \"0\";\n                    }\n                    this.ref.el.style.height = this.fakeTextarea.el.scrollHeight + \"px\";\n                    if (wasEmpty) {\n                        this.fakeTextarea.el.value = \"\";\n                    }\n                }\n                this.saveContentDebounced();\n            },\n            () => [this.props.composer.composerText, this.ref.el]\n        );\n        useEffect(\n            () => {\n                if (!this.props.composer.forceCursorMove) {\n                    return;\n                }\n                this.selection.restore();\n                this.props.composer.forceCursorMove = false;\n            },\n            () => [this.props.composer.forceCursorMove]\n        );\n        onMounted(() => {\n            this.ref.el?.scrollTo({ top: 0, behavior: \"instant\" });\n            if (!this.props.composer.composerText) {\n                this.restoreContent();\n            }\n        });\n        onWillUnmount(() => {\n            this.props.composer.isFocused = false;\n        });\n        const composerProxy = reactive(this.props.composer, () => {\n            if (this.status === 2 /* DESTROYED */) {\n                return;\n            }\n            const composerHtml = composerProxy.composerHtml;\n            if (this.updateFromEditor) {\n                return;\n            }\n            if (!this.editor?.editable) {\n                return;\n            }\n            setElementContent(this.editor.editable, composerHtml);\n            this.editor.shared.selection.setCursorEnd(lastLeaf(this.editor.editable));\n            this.editor.shared.history.addStep();\n        });\n        void composerProxy.composerHtml; // start observing\n    }\n\n    get areAllActionsDisabled() {\n        return false;\n    }\n\n    get isMultiUpload() {\n        return true;\n    }\n\n    get placeholder() {\n        if (this.props.placeholder) {\n            return this.props.placeholder;\n        }\n        if (this.thread) {\n            if (this.thread.channel_type === \"channel\") {\n                const threadName = this.thread.displayName;\n                if (this.thread.parent_channel_id) {\n                    return _t('Message \"%(subChannelName)s\"', {\n                        subChannelName: threadName,\n                    });\n                }\n                return _t(\"Message #%(threadName)s\u2026\", { threadName });\n            }\n            return _t(\"Message %(thread name)s\u2026\", { \"thread name\": this.thread.displayName });\n        }\n        return \"\";\n    }\n\n    get wysiwygConfig() {\n        return {\n            content: this.props.composer.composerHtml,\n            placeholder: this.placeholder,\n            Plugins: this.ui.isSmall ? MAIL_SMALL_UI_PLUGINS : MAIL_PLUGINS,\n            composerPluginDependencies: {\n                onBeforePaste: (selection, ev) => this.onPaste(ev),\n                onFocusin: this.onFocusin.bind(this),\n                onFocusout: this.onFocusout.bind(this),\n                onInput: this.onInput.bind(this),\n                onKeydown: this.onKeydown.bind(this),\n            },\n            classList: [\"o-mail-Composer-html\"],\n            onChange: () => this.onChangeWysiwygContent(),\n            onEditorReady: () => {\n                this.editor.shared.selection.setCursorEnd(lastLeaf(this.editor.editable));\n                this.editor.shared.history.addStep();\n            },\n        };\n    }\n\n    onClickCancelOrSaveEditText(ev) {\n        const composer = toRaw(this.props.composer);\n        if (composer.message && ev.target.dataset?.type === EDIT_CLICK_TYPE.CANCEL) {\n            this.props.onDiscardCallback(ev);\n        }\n        if (composer.message && ev.target.dataset?.type === EDIT_CLICK_TYPE.SAVE) {\n            this.editMessage(ev);\n        }\n    }\n\n    get CANCEL_OR_SAVE_EDIT_TEXT() {\n        const tags = {\n            open_samp: markup`<samp>`,\n            close_samp: markup`</samp>`,\n            open_em: markup`<em>`,\n            close_em: markup`</em>`,\n            open_cancel: markup`<button class=\"btn btn-link fst-italic p-0 align-baseline\" data-type=\"${EDIT_CLICK_TYPE.CANCEL}\">`,\n            close_cancel: markup`</button>`,\n            open_save: markup`<button class=\"btn btn-link fst-italic p-0 align-baseline\" data-type=\"${EDIT_CLICK_TYPE.SAVE}\">`,\n            close_save: markup`</button>`,\n        };\n        return this.env.inChatter\n            ? _t(\n                  \"%(open_samp)sEscape%(close_samp)s %(open_em)sto %(open_cancel)scancel%(close_cancel)s%(close_em)s, %(open_samp)sCTRL-Enter%(close_samp)s %(open_em)sto %(open_save)ssave%(close_save)s%(close_em)s\",\n                  tags\n              )\n            : _t(\n                  \"%(open_samp)sEscape%(close_samp)s %(open_em)sto %(open_cancel)scancel%(close_cancel)s%(close_em)s, %(open_samp)sEnter%(close_samp)s %(open_em)sto %(open_save)ssave%(close_save)s%(close_em)s\",\n                  tags\n              );\n    }\n\n    get SEND_TEXT() {\n        if (this.props.composer.message) {\n            return _t(\"Save editing\");\n        }\n        return this.props.type === \"note\" ? _t(\"Log\") : _t(\"Send\");\n    }\n\n    get sendKeybinds() {\n        return this.env.inChatter ? [_t(\"CTRL\"), _t(\"Enter\")] : [_t(\"Enter\")];\n    }\n\n    get showComposerAvatar() {\n        return !this.compact && this.props.sidebar;\n    }\n\n    get thread() {\n        return this.props.composer.targetThread;\n    }\n\n    get allowUpload() {\n        return this.props.allowUpload;\n    }\n\n    get message() {\n        return this.props.composer.message ?? null;\n    }\n\n    get extraData() {\n        return this.thread.rpcParams;\n    }\n\n    get isSendButtonDisabled() {\n        const attachments = this.props.composer.attachments;\n        return (\n            !this.state.active ||\n            (isHtmlEmpty(this.props.composer.composerHtml) && attachments.length === 0) ||\n            attachments.some(({ uploading }) => Boolean(uploading))\n        );\n    }\n\n    get hasSuggestions() {\n        return Boolean(this.suggestion?.state.items);\n    }\n\n    get navigableListProps() {\n        const props = {\n            anchorRef: this.inputContainerRef.el,\n            position: this.env.inChatter ? \"bottom-fit\" : \"top-fit\",\n            onSelect: (ev, option) => {\n                this.suggestion.insert(option);\n                markEventHandled(ev, \"composer.selectSuggestion\");\n            },\n            isLoading: !!this.suggestion.search.term && this.suggestion.state.isFetching,\n            options: [],\n        };\n        if (!this.hasSuggestions) {\n            return props;\n        }\n        const suggestions = this.suggestion.state.items.suggestions;\n        switch (this.suggestion.state.items.type) {\n            case \"Partner\":\n                return {\n                    ...props,\n                    optionTemplate: \"mail.Composer.suggestionPartner\",\n                    options: suggestions.map((suggestion) => {\n                        if (suggestion.isSpecial) {\n                            return {\n                                ...suggestion,\n                                group: 1,\n                                optionTemplate: \"mail.Composer.suggestionSpecial\",\n                                classList: \"o-mail-Composer-suggestion\",\n                            };\n                        } else if (suggestion.Model.getName() === \"res.role\") {\n                            return {\n                                label: suggestion.name,\n                                role: suggestion,\n                                thread: this.thread,\n                                optionTemplate: \"mail.Composer.suggestionRole\",\n                                classList: \"o-mail-Composer-suggestion\",\n                            };\n                        } else {\n                            return {\n                                label: this.thread?.getPersonaName(suggestion) ?? suggestion.name,\n                                thread: this.thread,\n                                partner: suggestion,\n                                classList: \"o-mail-Composer-suggestion\",\n                            };\n                        }\n                    }),\n                };\n            case \"Thread\":\n                return {\n                    ...props,\n                    optionTemplate: \"mail.Composer.suggestionThread\",\n                    options: suggestions.map((suggestion) => ({\n                        label: suggestion.fullNameWithParent,\n                        thread: suggestion,\n                        classList: \"o-mail-Composer-suggestion\",\n                    })),\n                };\n            case \"ChannelCommand\":\n                return {\n                    ...props,\n                    optionTemplate: \"mail.Composer.suggestionChannelCommand\",\n                    options: suggestions.map((suggestion) => ({\n                        label: suggestion.name,\n                        help: suggestion.help,\n                        classList: \"o-mail-Composer-suggestion\",\n                    })),\n                };\n            case \"mail.canned.response\":\n                return {\n                    ...props,\n                    optionTemplate: \"mail.Composer.suggestionCannedResponse\",\n                    options: suggestions.map((suggestion) => ({\n                        cannedResponse: suggestion,\n                        source: suggestion.source,\n                        label: suggestion.substitution,\n                        classList: \"o-mail-Composer-suggestion\",\n                    })),\n                };\n            case \"emoji\":\n                return {\n                    ...props,\n                    optionTemplate: \"mail.Composer.suggestionEmoji\",\n                    options: suggestions.map((suggestion) => ({\n                        emoji: suggestion,\n                        label: suggestion.codepoints,\n                    })),\n                };\n            default:\n                return props;\n        }\n    }\n\n    onDropFile(ev) {\n        if (isDragSourceExternalFile(ev.dataTransfer)) {\n            for (const file of ev.dataTransfer.files) {\n                this.attachmentUploader.uploadFile(file);\n            }\n        }\n    }\n\n    onCloseFullComposerCallback(isDiscard) {\n        if (this.props.onCloseFullComposerCallback) {\n            this.props.onCloseFullComposerCallback(isDiscard);\n        } else {\n            this.thread?.fetchNewMessages();\n        }\n    }\n\n    onInput(ev) {\n        if (!this.props.composer.isDirty) {\n            this.props.composer.isDirty = true;\n        }\n    }\n\n    /**\n     * This doesn't work on firefox https://bugzilla.mozilla.org/show_bug.cgi?id=1699743\n     */\n    onPaste(ev) {\n        if (!this.allowUpload) {\n            return;\n        }\n        if (!ev.clipboardData?.items) {\n            return;\n        }\n        if (ev.clipboardData.files.length === 0) {\n            return;\n        }\n        ev.preventDefault();\n        for (const file of ev.clipboardData.files) {\n            this.attachmentUploader.uploadFile(file);\n        }\n    }\n\n    onKeydown(ev) {\n        const composer = toRaw(this.props.composer);\n        switch (ev.key) {\n            case \"ArrowUp\":\n                if (!this.env.inChatter && composer.composerText === \"\" && composer.thread) {\n                    const messageToEdit = composer.thread.lastEditableMessageOfSelf;\n                    if (messageToEdit) {\n                        messageToEdit.enterEditMode(this.props.composer.thread);\n                    }\n                }\n                break;\n            case \"Enter\": {\n                if (isEventHandled(ev, \"NavigableList.select\") || !this.state.active) {\n                    ev.preventDefault();\n                    return;\n                }\n                if (this.isMobileOS || ev.isComposing) {\n                    return;\n                }\n                const shouldPost = this.env.inChatter ? ev.ctrlKey : !ev.shiftKey;\n                if (!shouldPost) {\n                    return;\n                }\n                ev.preventDefault(); // to prevent useless return\n                if (composer.message) {\n                    this.editMessage();\n                } else {\n                    this.sendMessage();\n                }\n                break;\n            }\n            case \"Escape\":\n                if (isEventHandled(ev, \"NavigableList.close\")) {\n                    return;\n                }\n                if (this.props.onDiscardCallback) {\n                    this.props.onDiscardCallback();\n                    markEventHandled(ev, \"Composer.discard\");\n                }\n                break;\n        }\n    }\n\n    get fullComposerAdditionalContext() {\n        // To be overridden by inheriting classes\n        return {};\n    }\n\n    async onClickFullComposer(ev) {\n        const allRecipients = [...this.thread.suggestedRecipients];\n        if (this.props.type !== \"note\") {\n            allRecipients.push(...this.thread.additionalRecipients);\n            // auto-create partners:\n            const newPartners = allRecipients.filter((recipient) => !recipient.partner_id);\n            if (newPartners.length !== 0) {\n                const recipientEmails = [];\n                newPartners.forEach((recipient) => {\n                    recipientEmails.push(recipient.email);\n                });\n                const partners = await rpc(\"/mail/partner/from_email\", {\n                    thread_model: this.thread.model,\n                    thread_id: this.thread.id,\n                    emails: recipientEmails,\n                });\n                for (const index in partners) {\n                    const partnerData = partners[index];\n                    const partner = this.store[\"res.partner\"].insert(partnerData);\n                    const email = recipientEmails[index];\n                    const recipient = allRecipients.find((recipient) => recipient.email === email);\n                    recipient.partner_id = partner.id;\n                }\n            }\n        }\n        const attachmentIds = this.props.composer.attachments.map((attachment) => attachment.id);\n        let default_body = this.props.composer.composerHtml;\n        if (isHtmlEmpty(default_body)) {\n            const composer = toRaw(this.props.composer);\n            // Reset signature when recovering an empty body.\n            composer.emailAddSignature = true;\n        }\n        let signature = this.thread.effectiveSelf.main_user_id?.signature;\n        if (signature) {\n            const divElement = document.createElement(\"div\");\n            divElement.setAttribute(\"data-o-mail-quote\", \"1\");\n            divElement.append(\n                document.createElement(\"br\"),\n                document.createTextNode(\"-- \"),\n                document.createElement(\"br\"),\n                ...createElementWithContent(\"div\", signature).childNodes\n            );\n            signature = markup(divElement.outerHTML);\n        }\n        default_body = this.formatDefaultBodyForFullComposer(\n            default_body,\n            this.props.composer.emailAddSignature ? signature : \"\"\n        );\n        const context = {\n            default_attachment_ids: attachmentIds,\n            default_body,\n            default_email_add_signature: false,\n            default_model: this.thread.model,\n            default_partner_ids:\n                this.props.type === \"note\"\n                    ? []\n                    : allRecipients.map((recipient) => recipient.partner_id),\n            default_res_ids: [this.thread.id],\n            default_subtype_xmlid: this.props.type === \"note\" ? \"mail.mt_note\" : \"mail.mt_comment\",\n            clicked_on_full_composer: true,\n            // Changed in 18.2+: finally get rid of autofollow, following should be done manually\n            ...this.fullComposerAdditionalContext,\n        };\n        const action = {\n            name: this.props.type === \"note\" ? _t(\"Log note\") : _t(\"Compose Email\"),\n            type: \"ir.actions.act_window\",\n            res_model: \"mail.compose.message\",\n            view_mode: \"form\",\n            views: [[false, \"form\"]],\n            target: \"new\",\n            context: context,\n        };\n        const options = {\n            onClose: (args) => {\n                // args === { dismiss: true } : click on 'X' or press escape\n                // args === { special: true } : click on 'discard'\n                const accidentalDiscard = args?.dismiss;\n                const isDiscard = accidentalDiscard || args?.special;\n                // otherwise message is posted (args === [undefined])\n                if (!isDiscard && this.props.composer.thread.model === \"mail.box\") {\n                    this.notifySendFromMailbox();\n                }\n                if (accidentalDiscard) {\n                    this.fullComposerBus.trigger(\"ACCIDENTAL_DISCARD\", {\n                        onAccidentalDiscard: (isEmpty) => {\n                            if (!isEmpty) {\n                                this.saveContent();\n                                this.restoreContent();\n                            }\n                        },\n                    });\n                } else {\n                    this.clear();\n                }\n                this.props.composer.replyToMessage = undefined;\n                this.onCloseFullComposerCallback(isDiscard);\n                this.state.isFullComposerOpen = false;\n                // Use another event bus so that no message is sent to the\n                // closed composer.\n                this.fullComposerBus = new EventBus();\n            },\n            props: {\n                fullComposerBus: this.fullComposerBus,\n            },\n        };\n        await this.env.services.action.doAction(action, options);\n        this.state.isFullComposerOpen = true;\n    }\n\n    /**\n     * @param {string|ReturnType<markup>} defaultBody\n     * @param {string|ReturnType<markup>} [signature=\"\"]\n     * @returns {ReturnType<markup>}\n     */\n    formatDefaultBodyForFullComposer(defaultBody, signature = \"\") {\n        if (signature) {\n            defaultBody = markup`${defaultBody}<br>${signature}`;\n        }\n        return markup`<div>${defaultBody}</div>`; // as to not wrap in <p> by html_sanitize\n    }\n\n    clear() {\n        this.props.composer.clear();\n        browser.localStorage.removeItem(this.props.composer.localId);\n    }\n\n    notifySendFromMailbox() {\n        this.env.services.notification.add(_t('Message posted on \"%s\"', this.thread.displayName), {\n            type: \"info\",\n        });\n    }\n\n    isEventTrusted(ev) {\n        // Allow patching during tests\n        return ev.isTrusted;\n    }\n\n    async processMessage(cb) {\n        if (this.props.composer.attachments.some(({ uploading }) => uploading)) {\n            this.env.services.notification.add(_t(\"Please wait while the file is uploading.\"), {\n                type: \"warning\",\n            });\n        } else if (this.canProcessMessage) {\n            if (!this.state.active) {\n                return;\n            }\n            this.state.active = false;\n            await cb(this.props.composer.composerHtml);\n            if (this.props.onPostCallback) {\n                this.props.onPostCallback();\n            }\n            this.clear();\n            this.state.active = true;\n            this.ref.el?.focus();\n        }\n    }\n\n    get canProcessMessage() {\n        return (\n            !isHtmlEmpty(this.props.composer.composerHtml) ||\n            this.props.composer.attachments.length > 0 ||\n            (this.message && this.message.attachment_ids.length > 0)\n        );\n    }\n\n    async sendMessage() {\n        const composer = toRaw(this.props.composer);\n        this.composerActions.activePicker?.close?.();\n        if (composer.message) {\n            this.editMessage();\n            return;\n        }\n        if (this.props.type !== \"note\") {\n            const allRecipients = [\n                ...composer.thread.suggestedRecipients,\n                ...composer.thread.additionalRecipients,\n            ];\n            if (allRecipients.some((recipient) => !recipient.email || !isEmail(recipient.email))) {\n                return;\n            }\n        }\n        await this.processMessage(async (value) => {\n            await this._sendMessage(value, this.postData, this.extraData);\n        });\n    }\n\n    get postData() {\n        const composer = toRaw(this.props.composer);\n        return {\n            attachments: composer.attachments || [],\n            emailAddSignature: composer.emailAddSignature,\n            isNote: this.props.type === \"note\",\n            mentionedChannels: composer.mentionedChannels || [],\n            mentionedPartners: composer.mentionedPartners || [],\n            mentionedRoles: composer.mentionedRoles || [],\n            cannedResponseIds: composer.cannedResponses.map((c) => c.id),\n            parentId: this.props.composer.replyToMessage?.id,\n        };\n    }\n\n    /**\n     * @typedef postData\n     * @property {import(\"models\").Attachment[]} attachments\n     * @property {boolean} isNote\n     * @property {number} parentId\n     * @property {integer[]} mentionedChannelIds\n     * @property {integer[]} mentionedPartnerIds\n     */\n\n    /**\n     * @param {ReturnType<markup>} value message body\n     * @param {postData} postData Message meta data info\n     * @param {extraData} extraData Message extra meta data info needed by other modules\n     */\n    async _sendMessage(value, postData, extraData) {\n        const thread = toRaw(this.props.composer.thread);\n        const postThread = toRaw(this.thread);\n        const post = postThread.post.bind(postThread, value, postData, extraData);\n        let message;\n        if (postThread.model === \"discuss.channel\") {\n            // feature of (optimistic) temp message\n            post();\n        } else {\n            message = await post();\n        }\n        if (thread.model === \"mail.box\") {\n            this.notifySendFromMailbox();\n        }\n        this.suggestion?.clearRawMentions();\n        this.suggestion?.clearCannedResponses();\n        this.props.composer.replyToMessage = undefined;\n        this.props.composer.emailAddSignature = true;\n        this.props.composer.thread.additionalRecipients = [];\n        return message;\n    }\n\n    async editMessage() {\n        const composer = toRaw(this.props.composer);\n        if (!this.askDeleteFromEdit) {\n            await this.processMessage(async (value) =>\n                composer.message.edit(value, composer.attachments, {\n                    mentionedChannels: composer.mentionedChannels,\n                    mentionedPartners: composer.mentionedPartners,\n                    mentionedRoles: composer.mentionedRoles,\n                })\n            );\n        } else {\n            this.env.services.dialog.add(\n                MessageConfirmDialog,\n                {\n                    message: composer.message,\n                    onConfirm: () =>\n                        this.message.remove({\n                            removeFromThread: this.shouldHideFromMessageListOnDelete,\n                        }),\n                    prompt: _t(\"Are you sure you want to bid farewell to this message forever?\"),\n                },\n                { context: this }\n            );\n        }\n        this.suggestion?.clearRawMentions();\n    }\n\n    get askDeleteFromEdit() {\n        const composer = toRaw(this.props.composer);\n        return !composer.composerText && composer.message.attachment_ids.length === 0;\n    }\n\n    onClickInsertCannedResponse(ev) {\n        markEventHandled(ev, \"composer.clickInsertCannedResponse\");\n        const composer = toRaw(this.props.composer);\n        if (this.editor) {\n            if (!isHtmlEmpty(this.props.composer.composerHtml)) {\n                this.editor.shared.dom.insert(\" \");\n            }\n            this.editor.shared.dom.insert(\"::\");\n            this.editor.shared.history.addStep();\n        } else {\n            const composerText = composer.composerText;\n            const firstPart = composerText.slice(0, composer.selection.start);\n            const secondPart = composerText.slice(composer.selection.end, composerText.length);\n            const toInsertPart = firstPart.length === 0 || firstPart.at(-1) === \" \" ? \"::\" : \" ::\";\n            composer.composerText = firstPart + toInsertPart + secondPart;\n            this.selection.moveCursor((firstPart + toInsertPart).length);\n        }\n        if (!this.ui.isSmall || !this.env.inChatter) {\n            composer.autofocus++;\n        }\n    }\n\n    onChangeWysiwygContent() {\n        this.updateFromEditor = true;\n        // markup: editor content is trusted\n        this.props.composer.composerHtml = markup(this.editor.getContent());\n        if (!this.props.composer.isDirty) {\n            this.props.composer.isDirty = true;\n        }\n        this.updateFromEditor = false;\n    }\n\n    onLoadWysiwyg(editor) {\n        this.editor = editor;\n    }\n\n    addEmoji(str) {\n        const composer = toRaw(this.props.composer);\n        if (this.editor) {\n            this.editor.shared.dom.insert(str);\n            this.editor.shared.history.addStep();\n        } else {\n            const composerText = composer.composerText;\n            const firstPart = composerText.slice(0, composer.selection.start);\n            const secondPart = composerText.slice(composer.selection.end, composerText.length);\n            composer.composerText = firstPart + str + secondPart;\n            this.selection.moveCursor((firstPart + str).length);\n        }\n        if (this.ui.isSmall && !this.env.inChatter) {\n            return false;\n        } else {\n            composer.autofocus++;\n        }\n    }\n\n    onFocusin() {\n        const composer = toRaw(this.props.composer);\n        composer.isFocused = true;\n        if (\n            composer.thread?.scrollTop === \"bottom\" &&\n            !composer.thread.scrollUnread &&\n            !composer.thread.markedAsUnread\n        ) {\n            composer.thread?.markAsRead();\n        }\n    }\n\n    onFocusout(ev) {\n        if (\n            [EDIT_CLICK_TYPE.CANCEL, EDIT_CLICK_TYPE.SAVE].includes(ev.relatedTarget?.dataset?.type)\n        ) {\n            // Edit or Save most likely clicked: early return as to not re-render (which prevents click)\n            return;\n        }\n        this.props.composer.isFocused = false;\n    }\n\n    saveContent() {\n        const composer = toRaw(this.props.composer);\n        const saveContentToLocalStorage = ({\n            composerHtml,\n            emailAddSignature,\n            replyToMessageId,\n        }) => {\n            browser.localStorage.setItem(\n                composer.localId,\n                JSON.stringify({\n                    emailAddSignature,\n                    replyToMessageId,\n                    composerHtml: isMarkup(composerHtml) ? [\"markup\", composerHtml] : composerHtml,\n                })\n            );\n        };\n        if (this.state.isFullComposerOpen) {\n            this.fullComposerBus.trigger(\"SAVE_CONTENT\", {\n                onSaveContent: saveContentToLocalStorage,\n            });\n        } else {\n            saveContentToLocalStorage({\n                composerHtml: composer.composerHtml,\n                emailAddSignature: true,\n                replyToMessageId: composer.replyToMessage?.id,\n            });\n        }\n    }\n\n    restoreContent() {\n        const composer = toRaw(this.props.composer);\n        let config;\n        try {\n            config = JSON.parse(browser.localStorage.getItem(composer.localId));\n        } catch {\n            browser.localStorage.removeItem(composer.localId);\n        }\n        if (!config) {\n            return;\n        }\n        if (!isHtmlEmpty(config.composerHtml)) {\n            composer.emailAddSignature = config.emailAddSignature;\n            composer.composerHtml = config.composerHtml;\n        }\n        if (Number.isInteger(config.replyToMessageId)) {\n            composer.replyToMessage = this.store[\"mail.message\"].insert(config.replyToMessageId);\n        }\n    }\n\n    get shouldHideFromMessageListOnDelete() {\n        return false;\n    }\n}\n", "import { toRaw, useComponent, useEffect, useRef, useState } from \"@odoo/owl\";\nimport { useEmojiPicker } from \"@web/core/emoji_picker/emoji_picker\";\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { markEventHandled } from \"@web/core/utils/misc\";\nimport { Action, UseActions } from \"@mail/core/common/action\";\nimport { useService } from \"@web/core/utils/hooks\";\n\nexport const composerActionsRegistry = registry.category(\"mail.composer/actions\");\n\n/** @typedef {import(\"@odoo/owl\").Component} Component */\n/** @typedef {import(\"@mail/core/common/action\").ActionDefinition} ActionDefinition */\n/** @typedef {import(\"models\").Composer} Composer */\n/**\n * @typedef {Object} ComposerActionSpecificDefinition\n * @property {boolean|(comp: Component) => boolean} [condition=true]\n * @property {boolean} [isPicker]\n * @property {string|(comp: Component) => string} [pickerName]\n */\n/**\n * @typedef {ActionDefinition & ComposerActionSpecificDefinition} ComposerActionDefinition\n */\n\n/**\n * @param {string} id\n * @param {ComposerActionDefinition} definition\n */\nexport function registerComposerAction(id, definition) {\n    composerActionsRegistry.add(id, definition);\n}\n\nexport function pickerOnClick(component, action, ev) {\n    let anchorEl;\n    if (component.ui.isSmall) {\n        anchorEl = component.pickerTargetRef.el;\n    } else if (!anchorEl) {\n        if (action.sequenceQuick) {\n            anchorEl = component.quickActionsRef.el;\n        } else {\n            anchorEl = component.moreActionsRef.el ?? component.extraActionsRef.el;\n        }\n    }\n    const previousPicker = component.getActivePicker();\n    previousPicker?.close();\n    if (toRaw(previousPicker) === toRaw(action.picker)) {\n        component.setActivePicker(null);\n    } else {\n        component.setActivePicker(action.picker);\n        component.getActivePicker().open({ el: anchorEl });\n    }\n}\n\nexport function pickerSetup(action, func) {\n    const component = useComponent();\n    component.pickerTargetRef = useRef(\"picker-target\");\n    component.quickActionsRef = useRef(\"quick-actions\");\n    component.moreActionsRef = useRef(\"more-actions\");\n    component.extraActionsRef = useRef(\"extra-actions\");\n    action.ref = useRef(action.id);\n    action.picker = func();\n}\n\nregisterComposerAction(\"send-message\", {\n    btnClass: ({ action }) => (action.isActive ? \"o-sendMessageActive o-text-white shadow-sm\" : \"\"),\n    condition: ({ composer, owner, store }) =>\n        (store.env.isSmall && composer.message) || (!owner.env.inChatter && !composer.message),\n    disabledCondition: ({ owner }) => owner.isSendButtonDisabled,\n    icon: \"fa fa-paper-plane-o\",\n    isActive: ({ owner }) => owner.sendMessageState.active,\n    name: ({ composer, owner }) =>\n        composer.message\n            ? _t(\"Save editing\")\n            : composer.targetThread?.model === \"discuss.channel\"\n            ? _t(\"Send\")\n            : owner.props.type === \"note\"\n            ? _t(\"Log\")\n            : _t(\"Send\"),\n    onSelected: ({ owner }) => owner.sendMessage(),\n    setup: ({ owner }) => {\n        owner.sendMessageState = useState({ active: false });\n        useEffect(\n            () => {\n                owner.sendMessageState.active = !owner.isSendButtonDisabled;\n            },\n            () => [owner.isSendButtonDisabled]\n        );\n    },\n    sequenceQuick: 30,\n});\nregisterComposerAction(\"add-emoji\", {\n    icon: \"fa fa-smile-o\",\n    isPicker: true,\n    pickerName: _t(\"Emoji\"),\n    name: _t(\"Add Emojis\"),\n    onSelected({ owner }, ev) {\n        pickerOnClick(owner, this, ev);\n        markEventHandled(ev, \"Composer.onClickAddEmoji\");\n    },\n    setup({ owner }) {\n        pickerSetup(this, () =>\n            useEmojiPicker(\n                undefined,\n                {\n                    onSelect: (emoji) => owner.addEmoji(emoji),\n                    onClose: () => owner.setActivePicker(null),\n                },\n                { arrow: false }\n            )\n        );\n    },\n    sequenceQuick: 20,\n});\nregisterComposerAction(\"upload-files\", {\n    condition: ({ owner }) => owner.allowUpload,\n    icon: \"fa fa-paperclip\",\n    name: _t(\"Attach Files\"),\n    onSelected: ({ composer: comp, owner }, ev) => {\n        owner.fileUploaderRef.el?.click();\n        const composer = toRaw(comp);\n        markEventHandled(ev, \"composer.clickOnAddAttachment\");\n        composer.autofocus++;\n    },\n    setup: ({ owner }) => (owner.fileUploaderRef = useRef(\"file-uploader\")),\n    sequence: 20,\n});\nregisterComposerAction(\"open-full-composer\", {\n    condition: ({ composer, owner }) =>\n        owner.props.showFullComposer &&\n        composer.targetThread &&\n        composer.targetThread.model !== \"discuss.channel\" &&\n        !owner.env.inFrontendPortalChatter,\n    hotkey: \"shift+c\",\n    icon: \"fa fa-expand\",\n    name: _t(\"Open Full Composer\"),\n    onSelected: ({ owner }) => owner.onClickFullComposer(),\n    sequence: 30,\n});\nregisterComposerAction(\"add-canned-response\", {\n    condition: ({ composer, store }) =>\n        store.hasCannedResponses &&\n        composer.targetThread &&\n        store.env.services[\"mail.suggestion\"]\n            .getSupportedDelimiters(composer.targetThread)\n            .find(([delimiter]) => delimiter === \"::\"),\n    icon: \"fa fa-file-text-o\",\n    name: _t(\"Insert a Canned response\"),\n    onSelected: ({ owner }, ev) => owner.onClickInsertCannedResponse(ev),\n    sequence: 5,\n});\n\nexport class ComposerAction extends Action {\n    /** @type {() => Composer} */\n    composerFn;\n\n    /**\n     * @param {Object} param0\n     * @param {Composer|() => Composer} composer\n     */\n    constructor({ composer }) {\n        super(...arguments);\n        this.composerFn = typeof composer === \"function\" ? composer : () => composer;\n    }\n\n    get params() {\n        return Object.assign(super.params, { composer: this.composerFn() });\n    }\n\n    get isPicker() {\n        return this.definition.isPicker;\n    }\n\n    get pickerName() {\n        return typeof this.definition.pickerName === \"function\"\n            ? this.definition.pickerName(this._component)\n            : this.definition.pickerName;\n    }\n}\n\nclass UseComposerActions extends UseActions {\n    get partition() {\n        const res = super.partition;\n        const actions = this.transformedActions.filter((action) => action.condition);\n        const groupedPickers = Object.groupBy(\n            actions.filter((a) => a.isPicker),\n            (a) => (a.sequenceQuick ? \"quick\" : \"other\")\n        );\n        groupedPickers.quick?.sort((a1, a2) => a1.sequenceQuick - a2.sequenceQuick);\n        groupedPickers.other?.sort((a1, a2) => a1.sequence - a2.sequence);\n        const pickers = (groupedPickers.other ?? []).concat(groupedPickers.quick ?? []);\n        return Object.assign(res, { pickers });\n    }\n}\n\n/**\n * @param {Object} [params0={}]\n * @param {Composer|() => Composer} composer\n */\nexport function useComposerActions({ composer } = {}) {\n    const component = useComponent();\n    const transformedActions = composerActionsRegistry\n        .getEntries()\n        .map(\n            ([id, definition]) => new ComposerAction({ owner: component, id, definition, composer })\n        );\n    for (const action of transformedActions) {\n        action.setup();\n    }\n    const state = useState(\n        new UseComposerActions(component, transformedActions, useService(\"mail.store\"))\n    );\n    component.getActivePicker = () => state.activePicker;\n    component.setActivePicker = (newActivePicker) => (state.activePicker = newActivePicker);\n    return state;\n}\n", "import { fields, OR, Record } from \"@mail/core/common/record\";\nimport {\n    convertBrToLineBreak,\n    getNonEditableMentions,\n    prettifyMessageText,\n} from \"@mail/utils/common/format\";\nimport { markup } from \"@odoo/owl\";\nimport { isHtmlEmpty } from \"@web/core/utils/html\";\n\nexport class Composer extends Record {\n    static id = OR(\"thread\", \"message\");\n\n    clear() {\n        this.attachments.length = 0;\n        this.replyToMessage = undefined;\n        this.composerHtml = markup(\"<div class='o-paragraph'><br></div>\");\n        Object.assign(this.selection, {\n            start: 0,\n            end: 0,\n            direction: \"none\",\n        });\n    }\n\n    /**\n     * @param {string} text - text to insert\n     * @param {number} position - insertion position\n     * @param {Object} [options]\n     * @param {boolean} [options.moveCursorToEnd=false] - If true, place cursor at end of composerText\n     */\n    insertText(text, position, { moveCursorToEnd = false } = {}) {\n        const before = this.composerText.substring(0, position);\n        const after = this.composerText.substring(position);\n        this.composerText = before + text + after;\n        this.selection.start = before.length + text.length;\n        if (moveCursorToEnd) {\n            this.selection.start = this.composerText.length;\n        }\n        this.selection.end = this.selection.start;\n        this.forceCursorMove = true;\n    }\n\n    attachments = fields.Many(\"ir.attachment\");\n    /** @type {boolean} */\n    emailAddSignature = true;\n    message = fields.One(\"mail.message\");\n    mentionedPartners = fields.Many(\"res.partner\");\n    mentionedRoles = fields.Many(\"res.role\");\n    mentionedChannels = fields.Many(\"Thread\");\n    cannedResponses = fields.Many(\"mail.canned.response\");\n    isDirty = false;\n    composerText = fields.Attr(\"\", {\n        onUpdate() {\n            if (this.updateFrom === \"html\") {\n                this.updateFrom = undefined;\n                return;\n            }\n            const validMentions = this.store.getMentionsFromText(this.composerText, {\n                mentionedChannels: this.mentionedChannels,\n                mentionedPartners: this.mentionedPartners,\n                mentionedRoles: this.mentionedRoles,\n                thread: this.targetThread,\n            });\n            const prettifiedHtml = prettifyMessageText(this.composerText, {\n                validMentions,\n                thread: this.targetThread,\n            });\n            if (this.composerHtml.toString() !== prettifiedHtml.toString()) {\n                this.updateFrom = \"text\";\n                this.composerHtml = prettifiedHtml;\n            }\n        },\n    });\n    composerHtml = fields.Html(markup(\"<div class='o-paragraph'><br></div>\"), {\n        compute() {\n            if (this.syncHtmlWithMessage) {\n                return (\n                    getNonEditableMentions(this.message.body) ||\n                    markup(\"<div class='o-paragraph'><br></div>\")\n                );\n            }\n            return this.composerHtml;\n        },\n        onUpdate() {\n            if (this.updateFrom === \"text\") {\n                this.updateFrom = undefined;\n                return;\n            }\n            const prettifiedText = isHtmlEmpty(this.composerHtml)\n                ? \"\"\n                : convertBrToLineBreak(this.composerHtml);\n            if (this.composerText !== prettifiedText) {\n                this.updateFrom = \"html\";\n                this.composerText = prettifiedText;\n            }\n        },\n    });\n    thread = fields.One(\"Thread\");\n    /** @type {{ start: number, end: number, direction: \"forward\" | \"backward\" | \"none\"}}*/\n    selection = {\n        start: 0,\n        end: 0,\n        direction: \"none\",\n    };\n    /** @type {boolean} */\n    forceCursorMove;\n    isFocused = fields.Attr(false, {\n        /** @this {import(\"models\").Composer} */\n        onUpdate() {\n            if (this.thread) {\n                if (this.isFocused) {\n                    this.thread.isFocusedCounter++;\n                } else {\n                    this.thread.isFocusedCounter--;\n                }\n            }\n        },\n    });\n    autofocus = 0;\n    replyToMessage = fields.One(\"mail.message\", { inverse: \"composerAsReplyToMessage\" });\n    /** @type {\"text\" | \"html\" | undefined} */\n    updateFrom = undefined;\n\n    get syncHtmlWithMessage() {\n        return this.message && !this.isDirty;\n    }\n\n    get targetThread() {\n        return this.replyToMessage?.thread ?? this.thread ?? this.message?.thread ?? null;\n    }\n}\n\nComposer.register();\n", "import { reactive } from \"@odoo/owl\";\nimport { registry } from \"@web/core/registry\";\n\nexport const composerService = {\n    dependencies: [\"mail.store\", \"legacy_multi_tab\"],\n    /**\n     * Enable Html composer with: odoo.__WOWL_DEBUG__.root.env.services[\"mail.composer\"].setHtmlComposer()\n     * @param {import(\"@web/env\").OdooEnv}\n     * @param {Partial<import(\"services\").Services>} services\n     */\n    start(env, { legacy_multi_tab }) {\n        const state = reactive({\n            htmlEnabled: legacy_multi_tab.getSharedValue(\"mail.html_composer.enabled\", false),\n            setHtmlComposer() {\n                if (state.htmlEnabled) {\n                    return;\n                }\n                state.htmlEnabled = true;\n                legacy_multi_tab.setSharedValue(\"mail.html_composer.enabled\", true);\n            },\n            setTextComposer() {\n                if (!state.htmlEnabled) {\n                    return;\n                }\n                state.htmlEnabled = false;\n                legacy_multi_tab.setSharedValue(\"mail.html_composer.enabled\", false);\n            },\n        });\n\n        legacy_multi_tab.bus.addEventListener(\"shared_value_updated\", ({ detail }) => {\n            if (detail.key === \"mail.html_composer.enabled\") {\n                state.htmlEnabled = JSON.parse(detail.newValue);\n            }\n        });\n\n        return state;\n    },\n};\n\nregistry.category(\"services\").add(\"mail.composer\", composerService);\n", "import { Component } from \"@odoo/owl\";\n\n/**\n * @typedef {Object} Props\n * @extends {Component<Props, Env>}\n */\nexport class CountryFlag extends Component {\n    static props = [\"country\", \"class?\"];\n    static template = \"mail.CountryFlag\";\n}\n", "import { Record } from \"@mail/core/common/record\";\n\nexport class Country extends Record {\n    static id = \"id\";\n    static _name = \"res.country\";\n\n    /** @type {string} */\n    code;\n    /** @type {number} */\n    id;\n    /** @type {string} */\n    name;\n\n    get flagUrl() {\n        if (!this.code) {\n            return false;\n        }\n        return `/base/static/img/country_flags/${encodeURIComponent(this.code.toLowerCase())}.png`;\n    }\n}\n\nCountry.register();\n", "import { fields, Record } from \"@mail/core/common/record\";\n\nimport { Deferred } from \"@web/core/utils/concurrency\";\n\n/**\n * This class represents a specific and unique request coming from the client to the server, and it\n * also holds the corresponding response coming from the server.\n * It is useful when batching requests together to determine which data to return for a specific\n * request. It basically does the un-batching by referencing the given id. The rest of the data\n * being grouped together in the store, flattened and with no duplicate records/fields, no matter\n * how many requests are returning the same data.\n * Instances of the class are created by the fetch method on each call, and they are deleted once\n * they are resolved with their data. This class should not be used directly under typical use.\n */\nexport class DataResponse extends Record {\n    static id = \"id\";\n    static _lastId = 0;\n\n    static createRequest() {\n        return this.insert({ id: ++this._lastId });\n    }\n\n    /** @type {number} */\n    id;\n    /**\n     * When set to true, this data request is resolved as soon as its RPC returns, even if there was\n     * no actual data for this request inside it. This is useful for fetch request that only fills\n     * the store but does not need to wait any particular value.\n     */\n    _autoResolve = false;\n    /**\n     * Promise that is resolved with the data when the data request is complete.\n     */\n    _resultDef = new Deferred();\n    /**\n     * When set to true, resolves this data request with the current values of the fields as data,\n     * and then deletes the data request.\n     *\n     * @type {boolean}\n     */\n    _resolve = fields.Attr(undefined, {\n        /** @this {import(\"models\").DataResponse} */\n        onUpdate() {\n            if (this._resolve) {\n                this._resultDef.resolve({ ...this });\n                this.delete();\n            }\n        },\n    });\n    /*\n     * Fields are contextual to each data request. They are generically added here to benefit from\n     * fields behavior as well as auto-complete, but their meaning depends on each data request.\n     * Existing fields defined here should be used in new data requests if they fit the purpose, and\n     * other fields can be added if necessary.\n     */\n    attachments = fields.Many(\"ir.attachment\");\n    channel = fields.One(\"Thread\");\n    channels = fields.Many(\"Thread\");\n    /** @type {number} */\n    count;\n    message = fields.One(\"mail.message\");\n    partners = fields.Many(\"res.partner\");\n}\n\nDataResponse.register();\n", "import { Component } from \"@odoo/owl\";\nimport { isMobileOS } from \"@web/core/browser/feature_detection\";\n\n/**\n * @typedef {Object} Props\n * @property {string} date\n * @property {string} [className]\n */\nexport class DateSection extends Component {\n    static template = \"mail.DateSection\";\n    static props = [\"date\", \"className?\"];\n\n    get isMobileOS() {\n        return isMobileOS();\n    }\n}\n", "import { fields, Record } from \"@mail/core/common/record\";\n\nexport class DiscussCallHistory extends Record {\n    static id = \"id\";\n    static _name = \"discuss.call.history\";\n\n    /** @type {number} */\n    id;\n    end_dt = fields.Datetime();\n    /** @type {number|undefined} */\n    duration_hour;\n}\nDiscussCallHistory.register();\n", "import { registry } from \"@web/core/registry\";\n\nexport const discussComponentRegistry = registry.category(\"discuss.component\");\n", "import { fields, Record } from \"@mail/core/common/record\";\nimport { markRaw } from \"@odoo/owl\";\n\nimport { _t } from \"@web/core/l10n/translation\";\n\nexport class Failure extends Record {\n    static nextId = markRaw({ value: 1 });\n    static id = \"id\";\n\n    notifications = fields.Many(\"mail.notification\", {\n        /** @this {import(\"models\").Failure} */\n        onUpdate() {\n            if (this.notifications.length === 0) {\n                this.delete();\n            } else {\n                this.store.failures.add(this);\n            }\n        },\n    });\n    get modelName() {\n        return this.notifications?.[0]?.mail_message_id?.thread?.modelName;\n    }\n    get resModel() {\n        return this.notifications?.[0]?.mail_message_id?.thread?.model;\n    }\n    get resIds() {\n        return new Set([\n            ...this.notifications\n                .map((notif) => notif.mail_message_id?.thread?.id)\n                .filter((id) => !!id),\n        ]);\n    }\n    lastMessage = fields.One(\"mail.message\", {\n        /** @this {import(\"models\").Failure} */\n        compute() {\n            let lastMsg = this.notifications[0]?.mail_message_id;\n            for (const notification of this.notifications) {\n                if (lastMsg?.id < notification.mail_message_id?.id) {\n                    lastMsg = notification.mail_message_id;\n                }\n            }\n            return lastMsg;\n        },\n    });\n    /** @type {'sms' | 'email'} */\n    get type() {\n        return this.notifications?.[0]?.notification_type;\n    }\n    get status() {\n        return this.notifications?.[0]?.notification_status;\n    }\n\n    get iconSrc() {\n        return \"/mail/static/src/img/smiley/mailfailure.svg\";\n    }\n\n    get body() {\n        if (this.notifications.length === 1 && this.lastMessage?.thread) {\n            return _t(\"An error occurred when sending an email on \u201c%(record_name)s\u201d\", {\n                record_name: this.lastMessage.thread.display_name,\n            });\n        }\n        return _t(\"An error occurred when sending an email\");\n    }\n\n    get datetime() {\n        return this.lastMessage?.datetime;\n    }\n}\n\nFailure.register();\n", "import { fields, Record } from \"@mail/core/common/record\";\nimport { rpc } from \"@web/core/network/rpc\";\n\nexport class Follower extends Record {\n    static _name = \"mail.followers\";\n    static id = \"id\";\n\n    thread = fields.One(\"Thread\");\n    /** @type {number} */\n    id;\n    /** @type {boolean} */\n    is_active;\n    partner_id = fields.One(\"res.partner\");\n    subtype_ids = fields.Many(\"mail.message.subtype\");\n\n    /** @returns {boolean} */\n    get isEditable() {\n        const hasWriteAccess = this.thread ? this.thread.hasWriteAccess : false;\n        return this.partner_id.eq(this.store.self_partner)\n            ? this.thread.hasReadAccess\n            : hasWriteAccess;\n    }\n\n    async remove() {\n        const data = await rpc(\"/mail/thread/unsubscribe\", {\n            res_model: this.thread.model,\n            res_id: this.thread.id,\n            partner_ids: [this.partner_id.id],\n        });\n        this.store.insert(data);\n    }\n\n    removeRecipient() {\n        this.thread.recipients.delete(this);\n    }\n}\n\nFollower.register();\n", "import { Component, useState } from \"@odoo/owl\";\n\nimport { Deferred, KeepLast } from \"@web/core/utils/concurrency\";\nimport { memoize } from \"@web/core/utils/functions\";\n\n/**\n * @typedef {Object} Props\n * @property {string} src\n * @property {string} [alt]\n * @property {string} [class]\n * @property {string} [loading]\n * @property {((event: Event) => void)} [onLoad]\n * @property {((event: Event) => void)} [onClick]\n * @property {boolean} [paused]\n * @property {string} [style]\n * @extends {Component<Props, Env>}\n */\nexport class Gif extends Component {\n    static template = \"mail.Gif\";\n    static props = {\n        src: String,\n        alt: { type: String, optional: true },\n        class: { type: String, optional: true },\n        loading: { type: String, optional: true },\n        onLoad: { type: Function, optional: true },\n        onClick: { type: Function, optional: true },\n        paused: { type: Boolean, optional: true },\n        style: { type: String, optional: true },\n    };\n    static components = {};\n\n    generateGifSnapshot = memoize(async (src) => {\n        const deferred = new Deferred();\n        const image = document.createElement(\"img\");\n        if (new URL(src).origin !== location.origin) {\n            image.crossOrigin = \"anonymous\";\n        }\n        image.src = src;\n        image.onload = () => {\n            const canvas = document.createElement(\"canvas\");\n            canvas.width = image.width;\n            canvas.height = image.height;\n            canvas.getContext(\"2d\").drawImage(image, 0, 0, image.width, image.height);\n            deferred.resolve(canvas.toDataURL(\"image/gif\"));\n        };\n        return deferred;\n    });\n\n    setup() {\n        this.state = useState({ snapshot: null });\n        this.keepLast = new KeepLast();\n    }\n\n    onLoad() {\n        this.props.onLoad?.(...arguments);\n        this.keepLast\n            .add(this.generateGifSnapshot(this.props.src))\n            .then((snapshot) => (this.state.snapshot = snapshot));\n    }\n}\n", "import { Component } from \"@odoo/owl\";\nimport { Typing } from \"@mail/discuss/typing/common/typing\";\n\nexport class ImStatus extends Component {\n    static props = [\"persona?\", \"className?\", \"style?\", \"member?\", \"slots?\", \"size?\"];\n    static template = \"mail.ImStatus\";\n    static defaultProps = { className: \"\", style: \"\", size: \"lg\" };\n    static components = { Typing };\n\n    get persona() {\n        return this.props.persona ?? this.props.member?.persona;\n    }\n}\n", "import { Component } from \"@odoo/owl\";\nimport { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\nimport { ImStatus } from \"./im_status\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { rpc } from \"@web/core/network/rpc\";\n\nexport class ImStatusDropdown extends Component {\n    static components = { Dropdown, DropdownItem, ImStatus };\n    static props = [];\n    static template = \"mail.ImStatusDropdown\";\n\n    setup() {\n        this.store = useService(\"mail.store\");\n        this.readableImStatusByCode = {\n            online: _t(\"Online\"),\n            away: _t(\"Away\"),\n            busy: _t(\"Do Not Disturb\"),\n            offline: _t(\"Offline\"),\n        };\n    }\n\n    setManualImStatus(status) {\n        rpc(\"/mail/set_manual_im_status\", { status });\n    }\n\n    get readableImStatus() {\n        const imStatus = this.store.self.im_status || \"offline\";\n        for (const status in this.readableImStatusByCode) {\n            if (imStatus.includes(status)) {\n                return this.readableImStatusByCode[status];\n            }\n        }\n        return _t(\"Unknown Status\");\n    }\n}\n\nexport function imStatusItem(env) {\n    return {\n        type: \"component\",\n        contentComponent: ImStatusDropdown,\n        sequence: 45,\n    };\n}\n\nregistry.category(\"user_menuitems\").add(\"im_status\", imStatusItem);\n", "import { browser } from \"@web/core/browser/browser\";\nimport { registry } from \"@web/core/registry\";\n\nexport const AWAY_DELAY = 30 * 60 * 1000; // 30 minutes\n\n/**\n * This service keeps the user's presence up to date with the server. When the\n * connection to the server is established, the user's presence is updated. If\n * another device or browser updates the user's presence, the presence is sent to\n * the server if relevant (e.g., another device is away or offline, but this one\n * is online).\n *\n * To receive updates through the bus, subscribe to presence channels\n * (e.g., subscribe to `odoo-presence-res.partner_3-token` to receive updates about\n * this partner). Token is optional and can be used to grant access to the presence\n * channel if the user is not allowed to read the partner's presence.\n * See `_get_im_status_access_token`.\n */\nexport const imStatusService = {\n    dependencies: [\"bus_service\", \"presence\"],\n    // and cyclic dependecy with \"mail.store\" (both services should be merged together)\n\n    start(env, { bus_service, presence }) {\n        let lastSentInactivity;\n        let becomeAwayTimeout;\n\n        const updateBusPresence = () => {\n            lastSentInactivity = presence.getInactivityPeriod();\n            startAwayTimeout();\n            bus_service.send(\"update_presence\", { inactivity_period: lastSentInactivity });\n        };\n\n        const startAwayTimeout = () => {\n            clearTimeout(becomeAwayTimeout);\n            const awayTime = AWAY_DELAY - presence.getInactivityPeriod();\n            if (awayTime > 0) {\n                becomeAwayTimeout = browser.setTimeout(() => updateBusPresence(), awayTime);\n            }\n        };\n        bus_service.addEventListener(\"BUS:CONNECT\", () => updateBusPresence(), { once: true });\n        bus_service.subscribe(\n            \"bus.bus/im_status_updated\",\n            async ({ presence_status, im_status, partner_id, guest_id, debounce = true }) => {\n                const store = env.services[\"mail.store\"];\n                const partner = store[\"res.partner\"].get(partner_id);\n                const guest = store[\"mail.guest\"].get(guest_id);\n                if (!partner && !guest) {\n                    return; // Do not store unknown persona's status\n                }\n                if (debounce) {\n                    partner?.debouncedSetImStatus(im_status);\n                    guest?.debouncedSetImStatus(im_status);\n                } else {\n                    partner?.updateImStatus(im_status);\n                    guest?.updateImStatus(im_status);\n                }\n                if (partner?.eq(store.self_partner) || guest?.eq(store.self_guest)) {\n                    const isOnline = presence.getInactivityPeriod() < AWAY_DELAY;\n                    if ((presence_status === \"away\" && isOnline) || presence_status === \"offline\") {\n                        updateBusPresence();\n                    }\n                }\n            }\n        );\n        presence.bus.addEventListener(\"presence\", () => {\n            if (lastSentInactivity >= AWAY_DELAY) {\n                updateBusPresence();\n            }\n            startAwayTimeout();\n        });\n        return { updateBusPresence };\n    },\n};\n\nregistry.category(\"services\").add(\"im_status\", imStatusService);\n", "import { Gif } from \"@mail/core/common/gif\";\nimport { LinkPreviewConfirmDelete } from \"@mail/core/common/link_preview_confirm_delete\";\n\nimport { Component, useEffect, useRef, useState } from \"@odoo/owl\";\n\nimport { useService } from \"@web/core/utils/hooks\";\n\n/**\n * @typedef {Object} Props\n * @property {import(\"models\").LinkPreview} linkPreview\n * @property {import(\"models\").Message} [message]\n * @property {Boolean} [gifPaused]\n * @property {function} [delete] Function bound to the delete button\n * @property {function} [deleteAll] Function bound to the delete all button\n * @extends {Component<Props, Env>}\n */\nexport class LinkPreview extends Component {\n    static template = \"mail.LinkPreview\";\n    static props = [\"linkPreview\", \"delete?\", \"deleteAll?\", \"gifPaused?\", \"message?\"];\n    static components = { Gif };\n\n    setup() {\n        super.setup();\n        this.dialogService = useService(\"dialog\");\n        this.state = useState({ startVideo: false, videoLoaded: false });\n        this.videoRef = useRef(\"video\");\n        useEffect(\n            (el) => {\n                if (el) {\n                    el.onload = () => (this.state.videoLoaded = true);\n                }\n            },\n            () => [this.videoRef.el]\n        );\n    }\n\n    onClick() {\n        this.dialogService.add(LinkPreviewConfirmDelete, {\n            linkPreview: this.props.linkPreview,\n            delete: this.props.delete,\n            deleteAll: this.props.deleteAll,\n            LinkPreview,\n        });\n    }\n\n    onImageLoaded() {\n        this.env.onImageLoaded?.();\n    }\n}\n", "import { Component } from \"@odoo/owl\";\n\nimport { Dialog } from \"@web/core/dialog/dialog\";\nimport { useService } from \"@web/core/utils/hooks\";\n\n/**\n * @typedef {Object} Props\n * @property {import(\"models\").LinkPreview} linkPreview\n * @property {function} [delete] Function bound to the delete button\n * @property {function} [deleteAll] Function bound to the delete all button\n * @property {function} close\n * @property {Component} LinkPreviewListComponent\n * @extends {Component<Props, Env>}\n */\nexport class LinkPreviewConfirmDelete extends Component {\n    static components = { Dialog };\n    static props = [\"linkPreview\", \"delete\", \"deleteAll?\", \"close\", \"LinkPreview\"];\n    static template = \"mail.LinkPreviewConfirmDelete\";\n\n    setup() {\n        super.setup();\n        this.store = useService(\"mail.store\");\n    }\n\n    get message() {\n        return this.props.linkPreview.message_id;\n    }\n\n    onClickOk() {\n        this.props.delete();\n        this.props.close();\n    }\n\n    onClickDeleteAll() {\n        this.props.deleteAll?.();\n        this.props.close();\n    }\n\n    onClickCancel() {\n        this.props.close();\n    }\n}\n", "import { fields, Record } from \"@mail/core/common/record\";\nimport { convertToEmbedURL } from \"@mail/utils/common/misc\";\n\nconst VIDEO_EXTENSIONS = new Set([\"mp4\", \"mov\", \"avi\", \"mkv\", \"webm\", \"mpeg\", \"mpg\", \"ogv\", \"3gp\"]);\n\nexport class LinkPreview extends Record {\n    static _name = \"mail.link.preview\";\n    static id = \"id\";\n\n    /** @type {number} */\n    id;\n    message_link_preview_ids = fields.Many(\"mail.message.link.preview\", {\n        inverse: \"link_preview_id\",\n    });\n    /** @type {string} */\n    image_mimetype;\n    /** @type {string} */\n    og_description;\n    /** @type {string} */\n    og_image;\n    /** @type {string} */\n    og_mimetype;\n    /** @type {string} */\n    og_title;\n    /** @type {string} */\n    og_type;\n    /** @type {string} */\n    og_site_name;\n    /** @type {string} */\n    source_url;\n\n    get isGif() {\n        return [this.og_mimetype, this.image_mimetype].includes(\"image/gif\");\n    }\n\n    get imageUrl() {\n        return this.og_image ? this.og_image : this.source_url;\n    }\n\n    get isImage() {\n        return Boolean(this.image_mimetype || this.og_mimetype === \"image/gif\");\n    }\n\n    get isVideo() {\n        let fileExt;\n        if (this.og_title) {\n            fileExt = this.og_title.split(\".\").pop();\n        }\n        return (\n            VIDEO_EXTENSIONS.has(fileExt) ||\n            Boolean(!this.isImage && this.og_type && this.og_type.startsWith(\"video\"))\n        );\n    }\n\n    get isCard() {\n        return !this.isImage && !this.isVideo;\n    }\n\n    get videoURL() {\n        const { url } = convertToEmbedURL(this.source_url);\n        return url;\n    }\n\n    get videoProvider() {\n        const { provider } = convertToEmbedURL(this.source_url);\n        return provider;\n    }\n}\n\nLinkPreview.register();\n", "import { Record } from \"@mail/core/common/record\";\n\nexport class MailActivityType extends Record {\n    static _name = \"mail.activity.type\";\n    static id = \"id\";\n\n    /** @type {number} */\n    id;\n    /** @type {string} */\n    name;\n}\n\nMailActivityType.register();\n", "import { Component } from \"@odoo/owl\";\nimport { Dropzone } from \"@web/core/dropzone/dropzone\";\n\nexport class MailAttachmentDropzone extends Component {\n    static template = \"mail.MailAttachmentDropzone\";\n    static components = { Dropzone };\n    static props = Dropzone.props;\n}\n", "import { reactive } from \"@odoo/owl\";\n\nimport { registry } from \"@web/core/registry\";\n\nexport class MailCoreCommon {\n    /**\n     * @param {import(\"@web/env\").OdooEnv} env\n     * @param {import(\"services\").ServiceFactories} services\n     */\n    constructor(env, services) {\n        this.env = env;\n        this.busService = services.bus_service;\n        this.store = services[\"mail.store\"];\n    }\n\n    setup() {\n        this.busService.subscribe(\"ir.attachment/delete\", (payload) => {\n            const { id: attachmentId, message: messageData } = payload;\n            if (messageData) {\n                this.store[\"mail.message\"].insert(messageData);\n            }\n            const attachment = this.store[\"ir.attachment\"].get(attachmentId);\n            attachment?.delete();\n        });\n        this.busService.subscribe(\"mail.message/delete\", (payload, { id: notifId }) => {\n            for (const messageId of payload.message_ids) {\n                const message = this.store[\"mail.message\"].get(messageId);\n                if (!message) {\n                    continue;\n                }\n                this.env.bus.trigger(\"mail.message/delete\", { message, notifId });\n                message.delete();\n            }\n        });\n        this.busService.subscribe(\"mail.message/toggle_star\", (payload, metadata) =>\n            this._handleNotificationToggleStar(payload, metadata)\n        );\n        this.busService.subscribe(\"res.users.settings\", (payload) => {\n            if (payload) {\n                this.store.settings.update(payload);\n            }\n        });\n        this.busService.subscribe(\"mail.record/insert\", (payload) => {\n            this.store.insert(payload);\n        });\n    }\n\n    _handleNotificationToggleStar(payload, metadata) {\n        const { message_ids: messageIds, starred } = payload;\n        this.store[\"mail.message\"].insert(messageIds.map((id) => ({ id, starred })));\n    }\n}\n\nexport const mailCoreCommon = {\n    dependencies: [\"bus_service\", \"mail.store\"],\n    /**\n     * @param {import(\"@web/env\").OdooEnv} env\n     * @param {import(\"services\").ServiceFactories} services\n     */\n    start(env, services) {\n        const mailCoreCommon = reactive(new MailCoreCommon(env, services));\n        mailCoreCommon.setup();\n        return mailCoreCommon;\n    },\n};\n\nregistry.category(\"services\").add(\"mail.core.common\", mailCoreCommon);\n", "import { Component, reactive } from \"@odoo/owl\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { registry } from \"@web/core/registry\";\n\nconst DEFAULT_ID = Symbol(\"default\");\n\nexport class MailFullscreen extends Component {\n    static props = [\"component\", \"props?\"];\n    static template = \"mail.Fullscreen\";\n\n    setup() {\n        super.setup();\n        this.fullscreen = useService(\"mail.fullscreen\");\n    }\n}\n\nexport const fullscreenService = {\n    start(env) {\n        const state = reactive({ enter, exit, id: undefined, closeOverlay: undefined });\n        async function exit(id = state.id) {\n            if (!id || id !== state.id) {\n                return;\n            }\n            state.closeOverlay?.();\n            state.id = undefined;\n            state.closeOverlay = undefined;\n            const fullscreenElement =\n                document.webkitFullscreenElement || document.fullscreenElement;\n            if (fullscreenElement) {\n                if (document.exitFullscreen) {\n                    await document.exitFullscreen();\n                } else if (document.mozCancelFullScreen) {\n                    await document.mozCancelFullScreen();\n                } else if (document.webkitCancelFullScreen) {\n                    await document.webkitCancelFullScreen();\n                }\n            }\n        }\n        /**\n         * @param component\n         * @param {object} [options]\n         * @param [options.props]\n         * @param {any} [options.id]\n         * @param {boolean} [options.keepBrowserHeader] - Optional flag to specify whether to keep\n         * the browser's header (address bar, tabs, etc.) visible.\n         * @param {string} [options.rootId] - Optional root id to pass to the overlay.\n         * @returns {Promise<void>}\n         */\n        async function enter(\n            component,\n            { keepBrowserHeader = false, props, rootId, id = DEFAULT_ID } = {}\n        ) {\n            state.closeOverlay?.();\n            state.id = id;\n            state.closeOverlay = env.services.overlay.add(\n                MailFullscreen,\n                { component, props },\n                { rootId }\n            );\n            const el = document.body;\n            if (keepBrowserHeader) {\n                return;\n            }\n            try {\n                if (el.requestFullscreen) {\n                    await el.requestFullscreen();\n                } else if (el.mozRequestFullScreen) {\n                    await el.mozRequestFullScreen();\n                } else if (el.webkitRequestFullscreen) {\n                    await el.webkitRequestFullscreen();\n                }\n            } catch {\n                // doing nothing, we're just in non-native fullscreen.\n            }\n        }\n        window.addEventListener(\"fullscreenchange\", () => {\n            const isFullscreen = Boolean(\n                document.webkitFullscreenElement || document.fullscreenElement\n            );\n            if (!isFullscreen) {\n                state.exit();\n            }\n        });\n        return state;\n    },\n};\n\nregistry.category(\"services\").add(\"mail.fullscreen\", fullscreenService);\n", "import { fields, Record } from \"@mail/core/common/record\";\nimport { imageUrl } from \"@web/core/utils/urls\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { debounce } from \"@web/core/utils/timing\";\nimport { Store } from \"@mail/core/common/store_service\";\n\nconst TRANSPARENT_AVATAR =\n    \"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAQAAABpN6lAAAAAqElEQVR42u3QMQEAAAwCoNm/9GJ4CBHIjYsAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBDQ9+KgAIHd5IbMAAAAAElFTkSuQmCC\";\nconst { DateTime } = luxon;\n\n/**\n * @typedef {'offline' | 'bot' | 'online' | 'away' | 'im_partner' | undefined} ImStatus\n * @typedef Data\n * @property {number} id\n * @property {string} name\n * @property {string} email\n * @property {ImStatus} im_status\n */\n\nexport class MailGuest extends Record {\n    static id = \"id\";\n    static _name = \"mail.guest\";\n    static new() {\n        const record = super.new(...arguments);\n        record.debouncedSetImStatus = debounce(\n            (newStatus) => record.updateImStatus(newStatus),\n            Store.IM_STATUS_DEBOUNCE_DELAY\n        );\n        return record;\n    }\n\n    /** @type {string} */\n    avatar_128_access_token;\n    /** @type {number} */\n    id;\n    debouncedSetImStatus;\n    monitorPresence = fields.Attr(false, {\n        compute() {\n            return this.store.env.services.bus_service.isActive && this.id > 0;\n        },\n    });\n    _triggerPresenceSubscription = fields.Attr(null, {\n        compute() {\n            return this.monitorPresence && this.presenceChannel;\n        },\n        onUpdate() {\n            if (this.previousPresencechannel) {\n                this.store.env.services.bus_service.deleteChannel(this.previousPresencechannel);\n            }\n            if (this._triggerPresenceSubscription) {\n                this.store.env.services.bus_service.addChannel(this.presenceChannel);\n            }\n            this.previousPresencechannel = this.presenceChannel;\n        },\n        eager: true,\n    });\n    /** @type {string} */\n    name;\n    country_id = fields.One(\"res.country\");\n    /** @type {string} */\n    email;\n    /** @type {ImStatus} */\n    im_status = fields.Attr(null, {\n        onUpdate() {\n            if (this.eq(this.store.self_guest) && this.im_status === \"offline\" && this.id < 0) {\n                this.store.env.services.im_status.updateBusPresence();\n            }\n        },\n    });\n    /** @type {string|undefined} */\n    im_status_access_token;\n\n    /** @type {luxon.DateTime} */\n    offline_since = fields.Datetime();\n    /** @type {string|undefined} */\n    previousPresencechannel;\n    presenceChannel = fields.Attr(null, {\n        compute() {\n            const channel = `odoo-presence-mail.guest_${this.id}`;\n            if (this.im_status_access_token) {\n                return `${channel}-${this.im_status_access_token}`;\n            }\n            return channel;\n        },\n    });\n    write_date = fields.Datetime();\n\n    get avatarUrl() {\n        const accessTokenParam = {};\n        if (this.store.self.main_user_id?.share !== false) {\n            accessTokenParam.access_token = this.avatar_128_access_token;\n        }\n        if (this.id === -1) {\n            return TRANSPARENT_AVATAR;\n        }\n        return imageUrl(\"mail.guest\", this.id, \"avatar_128\", {\n            ...accessTokenParam,\n            unique: this.write_date,\n        });\n    }\n\n    async updateGuestName(name) {\n        await rpc(\"/mail/guest/update_name\", {\n            guest_id: this.id,\n            name,\n        });\n    }\n\n    updateImStatus(newStatus) {\n        if (newStatus === \"offline\") {\n            this.offline_since = DateTime.now();\n        }\n        this.im_status = newStatus;\n    }\n}\n\nMailGuest.register();\n", "import { Record } from \"@mail/model/record\";\n\nexport class MailMessageSubtype extends Record {\n    static id = \"id\";\n    static _name = \"mail.message.subtype\";\n\n    /** @type {string} */\n    description;\n    /** @type {number} */\n    id;\n    /** @type {string} */\n    name;\n}\nMailMessageSubtype.register();\n", "import { App } from \"@odoo/owl\";\n\nimport { browser } from \"@web/core/browser/browser\";\nimport { appTranslateFn } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { getTemplate } from \"@web/core/templates\";\n\nconst DEFAULT_ID = Symbol(\"default\");\n\nexport const mailPopoutService = {\n    /**\n     * To be overridden to add specific assets to call PiP.\n     * @param [Window] window the window on which we may add assets\n     */\n    async addAssets(window) {},\n\n    start(env) {\n        /**\n         * @type {Map<any, { externalWindow: Window|null, hooks: { beforePopout?: Function, afterPopoutClosed?: Function, app: App } }>}\n         */\n        const popouts = new Map();\n\n        /**\n         * Reset the external window to its initial state:\n         * - Reset the external window header from main window (for appropriate title and other meta data)\n         * - clear the external window's document body\n         * - destroy the current app mounted on the window\n         * @param {any} id - The ID of the popout instance to reset\n         * @param {Object} [options]\n         * @param {Boolean} [options.useAlternativeAssets]\n         */\n        async function reset(id, { useAlternativeAssets } = {}) {\n            const popout = popouts.get(id);\n            if (!popout) {\n                return;\n            }\n            const doc = popout.externalWindow?.document;\n            if (doc) {\n                doc.head.textContent = \"\";\n                if (useAlternativeAssets) {\n                    await mailPopoutService.addAssets(popout.externalWindow);\n                } else {\n                    doc.write(window.document.head.outerHTML);\n                }\n                doc.body = doc.createElement(\"body\");\n            }\n            if (popout.app) {\n                popout.app.destroy();\n                popout.app = null;\n            }\n        }\n\n        /**\n         * Poll the external window to detect when it is closed.\n         * the afterPopoutClosed hook (afterFn) is then called after the window is closed\n         */\n        async function pollClosedWindow(id) {\n            while (popouts.get(id)?.externalWindow) {\n                const popout = popouts.get(id);\n                await new Promise((r) => setTimeout(r, 1000));\n                if (popout.externalWindow?.closed) {\n                    const hooks = popout.hooks;\n                    hooks?.afterPopoutClosed?.();\n                    popout.externalWindow = null;\n                }\n            }\n        }\n\n        /**\n         * @param id\n         * @param component\n         * @param {Object} param2\n         * @param {Object} [param2.props]\n         * @param {Object} [param2.options]\n         *      If only one of width or height is provided, the other is calculated based on the aspect ratio.\n         *      If neither is provided, a default height of 320p is used.\n         * @param {number} [param2.options.width] - The width of the popout window.\n         * @param {number} [param2.options.height] - The height of the popout window.\n         * @param {number} [param2.options.aspectRatio=16/9] - The aspect ratio of the popout window.\n         * @param {boolean} [param2.options.useAlternativeAssets]\n         * @returns {Promise<Window|null>}\n         */\n        async function pip(\n            id,\n            component,\n            {\n                props,\n                options: { width, height, aspectRatio = 16 / 9, useAlternativeAssets = false } = {},\n            } = {}\n        ) {\n            const popout = popouts.get(id);\n            let externalWindow = popout.externalWindow;\n            if (!externalWindow || externalWindow.closed) {\n                const hooks = popout.hooks;\n                hooks?.beforePopout?.();\n                height =\n                    height || (width ? width / aspectRatio : Math.min(240, window.innerHeight));\n                width = width || height * aspectRatio;\n                if (window.documentPictureInPicture) {\n                    externalWindow = await window.documentPictureInPicture.requestWindow({\n                        width,\n                        height,\n                    });\n                } else {\n                    externalWindow = browser.open(\n                        \"about:blank\",\n                        \"_blank\",\n                        `popup=yes,width=${width},height=${height}`\n                    );\n                }\n                popout.externalWindow = externalWindow;\n                pollClosedWindow(id);\n            }\n            await reset(id, { useAlternativeAssets });\n            popout.app = new App(component, {\n                name: \"Popout\",\n                env: Object.assign({}, env, {\n                    /**\n                     * Some sub components may need a reference to the external window to\n                     * access window information such as its dimensions, or to attach event listeners.\n                     */\n                    pipWindow: externalWindow,\n                }),\n                props,\n                getTemplate,\n                translatableAttributes: [\"data-tooltip\"],\n                translateFn: appTranslateFn,\n            });\n            popout.app.mount(externalWindow.document.body);\n            return externalWindow;\n        }\n\n        /**\n         * Mounts the passed component (with its props) on an external window.\n         * If the external window does not exist, it is created.\n         */\n        function popout(id, component, props) {\n            const popout = popouts.get(id);\n            let externalWindow = popout.externalWindow;\n            if (!externalWindow || externalWindow.closed) {\n                const hooks = popout.hooks;\n                hooks?.beforePopout?.();\n                externalWindow = browser.open(\"about:blank\", \"_blank\", \"popup=yes\");\n                window.addEventListener(\"beforeunload\", () => {\n                    if (externalWindow && !externalWindow.closed) {\n                        externalWindow.close();\n                    }\n                });\n                popout.externalWindow = externalWindow;\n                pollClosedWindow(id);\n            }\n            reset(id);\n            popout.app = new App(component, {\n                name: \"Popout\",\n                env,\n                props,\n                getTemplate,\n                translatableAttributes: [\"data-tooltip\"],\n                translateFn: appTranslateFn,\n            });\n            popout.app.mount(externalWindow.document.body);\n            return externalWindow;\n        }\n\n        function getExternalWindow(id) {\n            const externalWindow = popouts.get(id)?.externalWindow;\n            return externalWindow && !externalWindow.closed ? externalWindow : null;\n        }\n\n        function addHooks(id, hooks) {\n            const popout = popouts.get(id);\n            popout.hooks = hooks;\n        }\n\n        /**\n         * Creates an ID-aware popout manager for a specific ID.\n         * This allows using multiple popout instances with different IDs,\n         *\n         * @param {any} id - An identifier for this popout instance\n         */\n        function createManager(id = DEFAULT_ID) {\n            popouts.set(id, {\n                externalWindow: null,\n                hooks: {},\n            });\n            return {\n                /**\n                 * Registers hooks for this popout instance.\n                 * @param {Function} beforePopout - called before the external window is created.\n                 * @param {Function} afterPopoutClosed - called after the external window is closed.\n                 */\n                addHooks(beforePopout = () => {}, afterPopoutClosed = () => {}) {\n                    addHooks(id, { beforePopout, afterPopoutClosed });\n                },\n\n                /**\n                 * Creates a picture-in-picture window and mounts the component\n                 * @param component - The component to be mounted.\n                 * @param {Props} props - The props of the component.\n                 * @returns {Promise<Window>} The external window\n                 */\n                async pip(component, props) {\n                    return pip(id, component, props);\n                },\n\n                /**\n                 * Creates a popup window and mounts the component\n                 * @param component - The component to be mounted.\n                 * @param {Props} props - The props of the component.\n                 * @returns {Window} The external window\n                 */\n                popout(component, props) {\n                    return popout(id, component, props);\n                },\n\n                /**\n                 * Resets this popout instance to its initial state\n                 */\n                reset() {\n                    reset(id);\n                },\n\n                /**\n                 * Gets the external window for this ID\n                 * @returns {Window|null} The external window or null if closed/doesn't exist\n                 */\n                get externalWindow() {\n                    return getExternalWindow(id);\n                },\n\n                /**\n                 * Gets the ID of this manager\n                 * @returns {any} The ID\n                 */\n                get id() {\n                    return id;\n                },\n            };\n        }\n\n        return Object.assign(createManager(), { createManager });\n    },\n};\n\nregistry.category(\"services\").add(\"mail.popout\", mailPopoutService);\n", "import { Record } from \"@mail/model/record\";\n\nexport class MailTemplate extends Record {\n    static id = \"id\";\n    static _name = \"mail.template\";\n    /** @type {number} */\n    id;\n    /** @type {String} */\n    name;\n}\nMailTemplate.register();\n", "import { AttachmentList } from \"@mail/core/common/attachment_list\";\nimport { Composer } from \"@mail/core/common/composer\";\nimport { ImStatus } from \"@mail/core/common/im_status\";\nimport { MessageInReply } from \"@mail/core/common/message_in_reply\";\nimport { MessageLinkPreviewList } from \"@mail/core/common/message_link_preview_list\";\nimport { MessageNotificationPopover } from \"@mail/core/common/message_notification_popover\";\nimport { MessageReactionMenu } from \"@mail/core/common/message_reaction_menu\";\nimport { MessageReactions } from \"@mail/core/common/message_reactions\";\nimport { RelativeTime } from \"@mail/core/common/relative_time\";\nimport { htmlToTextContentInline } from \"@mail/utils/common/format\";\nimport { isEventHandled, markEventHandled } from \"@web/core/utils/misc\";\nimport { renderToElement } from \"@web/core/utils/render\";\n\nimport {\n    Component,\n    onMounted,\n    onPatched,\n    onWillDestroy,\n    onWillUpdateProps,\n    toRaw,\n    useChildSubEnv,\n    useEffect,\n    useRef,\n    useState,\n    useSubEnv,\n} from \"@odoo/owl\";\n\nimport { ActionSwiper } from \"@web/core/action_swiper/action_swiper\";\nimport { hasTouch, isMobileOS } from \"@web/core/browser/feature_detection\";\nimport { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { useDropdownState } from \"@web/core/dropdown/dropdown_hooks\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { usePopover } from \"@web/core/popover/popover_hook\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { createElementWithContent } from \"@web/core/utils/html\";\nimport { getOrigin, url } from \"@web/core/utils/urls\";\nimport { useMessageActions } from \"./message_actions\";\nimport { discussComponentRegistry } from \"./discuss_component_registry\";\nimport { NotificationMessage } from \"./notification_message\";\nimport { useLongPress } from \"@mail/utils/common/hooks\";\nimport { ActionList } from \"@mail/core/common/action_list\";\nimport { loadCssFromBundle } from \"@mail/utils/common/misc\";\n\n/**\n * @typedef {Object} Props\n * @property {boolean} [hasActions=true]\n * @property {boolean} [highlighted]\n * @property {function} [onParentMessageClick]\n * @property {import(\"models\").Message} message\n * @property {boolean} [squashed]\n * @property {import(\"models\").Thread} [thread]\n * @property {ReturnType<import('@mail/core/common/message_search_hook').useMessageSearch>} [messageSearch]\n * @property {String} [className]\n * @extends {Component<Props, Env>}\n */\nexport class Message extends Component {\n    // This is the darken version of #71639e\n    static SHADOW_LINK_COLOR = \"#66598f\";\n    static SHADOW_HIGHLIGHT_COLOR = \"#e99d00bf\";\n    static SHADOW_LINK_HOVER_COLOR = \"#564b79\";\n    static components = {\n        ActionList,\n        ActionSwiper,\n        AttachmentList,\n        Composer,\n        Dropdown,\n        ImStatus,\n        MessageInReply,\n        MessageLinkPreviewList,\n        MessageReactions,\n        Popover: MessageNotificationPopover,\n        RelativeTime,\n        NotificationMessage,\n    };\n    static defaultProps = {\n        hasActions: true,\n        isInChatWindow: false,\n        showDates: true,\n    };\n    static props = [\n        \"asCard?\",\n        \"registerMessageRef?\",\n        \"hasActions?\",\n        \"isInChatWindow?\",\n        \"onParentMessageClick?\",\n        \"message\",\n        \"previousMessage?\",\n        \"squashed?\",\n        \"thread?\",\n        \"messageSearch?\",\n        \"className?\",\n        \"showDates?\",\n        \"isFirstMessage?\",\n        \"isReadOnly?\",\n    ];\n    static template = \"mail.Message\";\n\n    setup() {\n        super.setup();\n        this.store = useService(\"mail.store\");\n        this.popover = usePopover(this.constructor.components.Popover, { position: \"top\" });\n        this.state = useState({\n            isHovered: false,\n            isClicked: false,\n            expandOptions: false,\n            emailHeaderOpen: false,\n        });\n        /** @type {ShadowRoot} */\n        this.shadowRoot;\n        this.root = useRef(\"root\");\n        if (isMobileOS()) {\n            useLongPress(\"root\", {\n                action: () => this.openMobileActions(),\n                predicate: () => !this.isEditing,\n            });\n        }\n        onWillUpdateProps((nextProps) => {\n            this.props.registerMessageRef?.(this.props.message, null);\n        });\n        onMounted(() => this.props.registerMessageRef?.(this.props.message, this.root));\n        onPatched(() => this.props.registerMessageRef?.(this.props.message, this.root));\n        onWillDestroy(() => this.props.registerMessageRef?.(this.props.message, null));\n        this.hasTouch = hasTouch;\n        this.messageBody = useRef(\"body\");\n        this.messageActions = useMessageActions({\n            message: () => this.message,\n            thread: () => this.props.thread,\n        });\n        this.shadowBody = useRef(\"shadowBody\");\n        this.dialog = useService(\"dialog\");\n        this.ui = useService(\"ui\");\n        this.openReactionMenu = this.openReactionMenu.bind(this);\n        this.optionsDropdown = useDropdownState();\n        useSubEnv({ inMessage: true });\n        useChildSubEnv({\n            message: this.props.message,\n            alignedRight: this.isAlignedRight,\n        });\n        onMounted(() => {\n            if (this.shadowBody.el) {\n                this.shadowRoot = this.shadowBody.el.attachShadow({ mode: \"open\" });\n                const color = this.store.isOdooWhiteTheme ? \"dark\" : \"white\";\n                loadCssFromBundle(this.shadowRoot, \"mail.assets_message_email\");\n                const shadowStyle = document.createElement(\"style\");\n                shadowStyle.textContent = `\n                    * {\n                        background-color: transparent !important;\n                        color: ${color} !important;\n                    }\n                    a, a * {\n                        color: ${this.constructor.SHADOW_LINK_COLOR} !important;\n                    }\n                    a:hover, a *:hover {\n                        color: ${this.constructor.SHADOW_LINK_HOVER_COLOR} !important;\n                    }\n                    .o-mail-Message-searchHighlight {\n                        background: ${this.constructor.SHADOW_HIGHLIGHT_COLOR} !important;\n                    }\n                `;\n                if (!this.store.isOdooWhiteTheme) {\n                    this.shadowRoot.appendChild(shadowStyle);\n                }\n                const ellipsisStyle = document.createElement(\"style\");\n                ellipsisStyle.textContent = `\n                    .o-mail-ellipsis {\n                        min-width: 2.7ch;\n                        background-color: ButtonFace;\n                        border-radius: 50rem;\n                        border: 0;\n                        display: block\n                        font: -moz-button;\n                        font-size: .75rem;\n                        font-weight: 500;\n                        line-height: 1.1;\n                        cursor: pointer;\n                        padding: 0 4px;\n                        vertical-align: top;\n                        color #ffffff;\n                        text-decoration: none;\n                        text-align: center;\n                        &:hover {\n                            background-color: -moz-buttonhoverface;\n                        }\n                    }\n                `;\n                this.shadowRoot.appendChild(ellipsisStyle);\n            }\n        });\n        useEffect(\n            () => {\n                if (this.shadowBody.el) {\n                    const bodyEl = createElementWithContent(\n                        \"span\",\n                        this.message.showTranslation\n                            ? this.message.richTranslationValue\n                            : this.props.messageSearch?.highlight(this.message.richBody) ??\n                                  this.message.richBody\n                    );\n                    this.prepareMessageBody(bodyEl);\n                    this.shadowRoot.appendChild(bodyEl);\n                    return () => {\n                        this.shadowRoot.removeChild(bodyEl);\n                    };\n                }\n            },\n            () => [\n                this.message.showTranslation,\n                this.message.richTranslationValue,\n                this.props.messageSearch?.searchTerm,\n                this.message.richBody,\n                this.isEditing,\n            ]\n        );\n        useEffect(\n            () => {\n                if (!this.isEditing) {\n                    this.prepareMessageBody(this.messageBody.el);\n                }\n            },\n            () => [this.isEditing, this.message.richBody]\n        );\n    }\n\n    computeActions() {\n        const allActions = this.messageActions.actions;\n        const quickActions = allActions.slice(\n            0,\n            allActions.length > this.quickActionCount\n                ? this.quickActionCount - 1\n                : this.quickActionCount\n        );\n        const moreActions =\n            allActions.length > this.quickActionCount\n                ? allActions.slice(this.quickActionCount - 1)\n                : false;\n        const moreAction = moreActions?.length\n            ? this.messageActions.more({\n                  actions: moreActions,\n                  dropdownMenuClass: \"o-mail-Message-moreMenu\",\n                  dropdownPosition: this.isAlignedRight\n                      ? this.message.threadAsNewest\n                          ? \"left-end\"\n                          : \"left-start\"\n                      : this.message.threadAsNewest\n                      ? \"right-end\"\n                      : \"right-start\",\n                  name: this.expandText,\n              })\n            : undefined;\n        const actions = moreAction ? [...quickActions, moreAction] : quickActions;\n        if (this.isAlignedRight) {\n            actions.reverse();\n        }\n        this.state.moreAction = moreAction;\n        this.quickActions = quickActions;\n        this.actions = actions;\n    }\n\n    get attClass() {\n        return {\n            \"user-select-none o-isMobileOS\": isMobileOS(),\n            [this.props.className]: true,\n            \"o-card p-2 ps-1 mx-1 mt-1 mb-1 border border-dark rounded-2\": this.props.asCard,\n            \"pt-1\": !this.props.asCard && !this.props.squashed,\n            \"o-pt-0_5\": !this.props.asCard && this.props.squashed,\n            \"o-selfAuthored\": this.message.isSelfAuthored && !this.env.messageCard,\n            \"o-selected\": this.props.message.composerAsReplyToMessage?.thread.eq(this.props.thread),\n            \"o-squashed\": this.props.squashed,\n            \"mt-1\":\n                !this.props.squashed &&\n                this.props.thread &&\n                !this.env.messageCard &&\n                !this.props.asCard,\n            \"px-1\": this.props.isInChatWindow,\n            \"o-actionMenuMobileOpen\": this.ui.isSmall && this.optionsDropdown.isOpen,\n            \"o-editing\": this.isEditing,\n        };\n    }\n\n    get authorAvatarAttClass() {\n        return {\n            \"object-fit-contain\": this.props.message.author_id?.is_company,\n            \"object-fit-cover\": !this.props.message.author_id?.is_company,\n        };\n    }\n\n    get authorAvatarUrl() {\n        if (\n            this.message.message_type &&\n            this.message.message_type.includes(\"email\") &&\n            !this.message.author_id &&\n            !this.message.author_guest_id\n        ) {\n            return url(\"/mail/static/src/img/email_icon.png\");\n        }\n        if (this.message.author) {\n            return this.message.author.avatarUrl;\n        }\n        return this.store.DEFAULT_AVATAR;\n    }\n\n    get expandText() {\n        return _t(\"Expand\");\n    }\n\n    get isEditing() {\n        return !this.props.isReadOnly && this.props.message.composer;\n    }\n\n    get message() {\n        return this.props.message;\n    }\n\n    /** Max amount of quick actions, including \"...\" */\n    get quickActionCount() {\n        if (isMobileOS()) {\n            return 1;\n        }\n        return this.env.inChatWindow || this.env.inMeetingChat ? 2 : 4;\n    }\n\n    get showSubtypeDescription() {\n        return (\n            this.message.subtype_id?.description &&\n            this.message.subtype_id.description.toLowerCase() !==\n                htmlToTextContentInline(this.message.body || \"\").toLowerCase()\n        );\n    }\n\n    get messageTypeText() {\n        if (this.props.message.message_type === \"notification\") {\n            return _t(\"System notification\");\n        }\n        if (this.props.message.message_type === \"auto_comment\") {\n            return _t(\"Automated message\");\n        }\n        if (this.props.message.message_type === \"out_of_office\") {\n            return _t(\"Out-of-office message\");\n        }\n        if (\n            !this.props.message.isDiscussion &&\n            this.props.message.message_type !== \"user_notification\"\n        ) {\n            return _t(\"Note\");\n        }\n        return _t(\"Message\");\n    }\n\n    get isActive() {\n        return (\n            this.state.isHovered ||\n            this.state.isClicked ||\n            this.emojiPicker?.isOpen ||\n            Boolean(this.state.moreAction?.isActive)\n        );\n    }\n\n    get isAlignedRight() {\n        return Boolean(this.env.inChatWindow && this.props.message.isSelfAuthored);\n    }\n\n    get isMobileOS() {\n        return isMobileOS();\n    }\n\n    get isPersistentMessageFromAnotherThread() {\n        return (\n            !this.message.is_transient &&\n            !this.message.isPending &&\n            this.message.thread &&\n            this.message.thread.notEq(this.props.thread)\n        );\n    }\n\n    get translatedFromText() {\n        return _t(\"(Translated from: %(language)s)\", { language: this.message.translationSource });\n    }\n\n    get translationFailureText() {\n        return _t(\"(Translation Failure: %(error)s)\", { error: this.message.translationErrors });\n    }\n\n    onMouseenter() {\n        this.state.isHovered = true;\n    }\n\n    onMouseleave() {\n        this.state.isHovered = false;\n        this.state.isClicked = null;\n    }\n\n    /**\n     * @returns {boolean}\n     */\n    get shouldDisplayAuthorName() {\n        if (!this.env.inChatWindow) {\n            return true;\n        }\n        if (this.message.isSelfAuthored) {\n            return false;\n        }\n        if (this.props.thread.channel_type === \"chat\") {\n            return false;\n        }\n        return true;\n    }\n\n    async onClickAttachmentUnlink(attachment) {\n        await toRaw(attachment).remove();\n    }\n\n    /**\n     * @param {MouseEvent} ev\n     */\n    async onClick(ev) {\n        if (this.store.handleClickOnLink(ev, this.props.thread)) {\n            return;\n        }\n        if (\n            !isEventHandled(ev, \"Message.ClickAuthor\") &&\n            !isEventHandled(ev, \"Message.ClickFailure\")\n        ) {\n            if (this.state.isClicked) {\n                this.state.isClicked = false;\n            } else {\n                this.state.isClicked = true;\n                document.body.addEventListener(\n                    \"click\",\n                    () => {\n                        this.state.isClicked = false;\n                    },\n                    { capture: true, once: true }\n                );\n            }\n        }\n    }\n\n    /** @param {HTMLElement} bodyEl */\n    prepareMessageBody(bodyEl) {\n        if (!bodyEl) {\n            return;\n        }\n        const editedEl = bodyEl.querySelector(\".o-mail-Message-edited\");\n        editedEl?.replaceChildren(renderToElement(\"mail.Message.edited\"));\n        const channelLinks = bodyEl.querySelectorAll(\"a.o_channel_redirect\");\n        this.store.handleValidChannelMention(Array.from(channelLinks));\n        for (const el of bodyEl.querySelectorAll(\".o_message_redirect\")) {\n            // only transform links targetting the same database\n            if (el.getAttribute(\"href\")?.startsWith(getOrigin())) {\n                const message = this.store[\"mail.message\"].get(el.dataset.oeId);\n                if (message?.thread?.displayName) {\n                    el.classList.add(\"o_message_redirect_transformed\");\n                    el.replaceChildren(renderToElement(\"mail.Message.messageLink\", { message }));\n                }\n            }\n        }\n    }\n\n    getAuthorAttClass() {\n        return { \"opacity-50\": this.message.isPending };\n    }\n\n    getAvatarContainerAttClass() {\n        return {\n            \"opacity-50\": this.message.isPending,\n            \"o-inChatWindow\": this.env.inChatWindow,\n        };\n    }\n\n    exitEditMode() {\n        this.message.exitEditMode(this.props.thread);\n    }\n\n    onClickNotification(ev) {\n        const message = toRaw(this.message);\n        if (message.failureNotifications.length > 0) {\n            markEventHandled(ev, \"Message.ClickFailure\");\n        }\n        this.popover.open(ev.target, { message });\n    }\n\n    /** @param {MouseEvent} [ev] */\n    openMobileActions(ev) {\n        if (!isMobileOS()) {\n            return;\n        }\n        ev?.stopPropagation();\n        this.optionsDropdown.open();\n    }\n\n    openReactionMenu(reaction) {\n        const message = toRaw(this.props.message);\n        this.dialog.add(\n            MessageReactionMenu,\n            { message, initialReaction: reaction },\n            { context: this }\n        );\n    }\n\n    async onClickToggleTranslation() {\n        toRaw(this.props.message).onClickToggleTranslation();\n    }\n\n    get shouldHideFromMessageListOnDelete() {\n        return false;\n    }\n}\n\ndiscussComponentRegistry.add(\"Message\", Message);\n", "import { toRaw, useComponent, useState } from \"@odoo/owl\";\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { download } from \"@web/core/network/download\";\nimport { registry } from \"@web/core/registry\";\nimport { discussComponentRegistry } from \"./discuss_component_registry\";\nimport { Deferred } from \"@web/core/utils/concurrency\";\nimport { Action, ACTION_TAGS, UseActions } from \"@mail/core/common/action\";\nimport { useEmojiPicker } from \"@web/core/emoji_picker/emoji_picker\";\nimport { QuickReactionMenu } from \"@mail/core/common/quick_reaction_menu\";\nimport { isMobileOS } from \"@web/core/browser/feature_detection\";\nimport { useService } from \"@web/core/utils/hooks\";\n\nconst { DateTime } = luxon;\n\nexport const messageActionsRegistry = registry.category(\"mail.message/actions\");\n\n/** @typedef {import(\"@odoo/owl\").Component} Component */\n/** @typedef {import(\"@mail/core/common/action\").ActionDefinition} ActionDefinition */\n/** @typedef {import(\"models\").Message} Message */\n/** @typedef {import(\"models\").Thread} Thread */\n/**\n * @typedef {Object} MessageActionSpecificDefinition\n * @property {boolean|(comp: Component) => boolean} [condition=true]\n */\n/**\n * @typedef {ActionDefinition & MessageActionSpecificDefinition} MessageActionDefinition\n */\n/**\n * @param {string} id\n * @param {MessageActionDefinition} definition\n */\nexport function registerMessageAction(id, definition) {\n    messageActionsRegistry.add(id, definition);\n}\n\nregisterMessageAction(\"reaction\", {\n    component: QuickReactionMenu,\n    componentProps: ({ message, owner }) => ({\n        message,\n        action: messageActionsRegistry.get(\"reaction\"),\n        messageActive: owner.isActive,\n    }),\n    componentCondition: () => !isMobileOS(),\n    condition: ({ message, thread }) => message.canAddReaction(thread),\n    icon: \"oi oi-smile-add\",\n    name: _t(\"Add a Reaction\"),\n    onSelected({ owner }) {\n        return owner.reactionPicker.open({\n            el: owner.root?.el?.querySelector(`[name=\"${this.id}\"]`),\n        });\n    },\n    setup: ({ message, owner, thread }) =>\n        (owner.reactionPicker = useEmojiPicker(undefined, {\n            onSelect: (emoji) => {\n                const reaction = message.reactions.find(\n                    ({ content, personas }) =>\n                        content === emoji && thread.effectiveSelf.in(personas)\n                );\n                if (!reaction) {\n                    message.react(emoji);\n                }\n            },\n        })),\n    sequence: 10,\n});\nregisterMessageAction(\"reply-to\", {\n    condition: ({ message: msg, thread: thr }) => {\n        const message = toRaw(msg);\n        const thread = toRaw(thr);\n        return (\n            message.canReplyTo(thread) ||\n            (![\"discuss.channel\", \"mail.box\"].includes(thread?.model) &&\n                message.isNote &&\n                !message.isSelfAuthored)\n        );\n    },\n    icon: \"fa fa-reply\",\n    name: _t(\"Reply\"),\n    onSelected: ({ message: msg, owner, thread: thr }) => {\n        const message = toRaw(msg);\n        const thread = toRaw(thr);\n        const composer = thread.composer;\n        if (message.eq(composer.replyToMessage)) {\n            composer.replyToMessage = undefined;\n            return;\n        }\n        if ([\"discuss.channel\", \"mail.box\"].includes(thread.model)) {\n            composer.replyToMessage = message;\n        }\n        if (thread.model === \"discuss.channel\") {\n            return;\n        }\n        if (!message.isSelfAuthored && message.model !== \"discuss.channel\") {\n            const mentionText = `@${message.authorName} `;\n            if (!composer.composerText.includes(mentionText)) {\n                composer.mentionedPartners.add(message.author);\n                composer.insertText(mentionText, 0, { moveCursorToEnd: true });\n            }\n        }\n        owner.env.inChatter?.toggleComposer(\"note\", { force: true });\n    },\n    sequence: ({ message, store, thread }) =>\n        thread?.eq(store.inbox) || message.isSelfAuthored ? 55 : 20,\n});\nregisterMessageAction(\"toggle-star\", {\n    condition: ({ message }) => message.canToggleStar,\n    icon: ({ message }) => (message.starred ? \"fa fa-star o-mail-Message-starred\" : \"fa fa-star-o\"),\n    name: ({ message }) => (message.starred ? _t(\"Remove Star\") : _t(\"Add Star\")),\n    onSelected: ({ message }) => message.toggleStar(),\n    sequence: 30,\n});\nregisterMessageAction(\"mark-as-read\", {\n    condition: ({ store, thread }) => thread?.eq(store.inbox),\n    icon: \"fa fa-check\",\n    name: _t(\"Mark as Read\"),\n    onSelected: ({ message }) => message.setDone(),\n    sequence: 40,\n});\nregisterMessageAction(\"reactions\", {\n    condition: ({ message }) => message.reactions.length,\n    icon: \"fa fa-smile-o\",\n    name: _t(\"View Reactions\"),\n    onSelected: ({ owner }) => owner.openReactionMenu(),\n    sequence: 50,\n});\nregisterMessageAction(\"unfollow\", {\n    condition: ({ message, thread }) => message.canUnfollow(thread),\n    icon: \"fa fa-user-times\",\n    name: _t(\"Unfollow\"),\n    onSelected: ({ message }) => message.unfollow(),\n    sequence: 60,\n});\nregisterMessageAction(\"edit\", {\n    condition: ({ message }) => message.editable,\n    icon: \"fa fa-pencil\",\n    name: _t(\"Edit\"),\n    onSelected: ({ message, owner, thread }) => {\n        message.enterEditMode(thread);\n        owner.optionsDropdown?.close();\n    },\n    sequence: ({ message }) => (message.isSelfAuthored ? 20 : 55),\n});\nregisterMessageAction(\"delete\", {\n    condition: ({ message }) => message.editable,\n    icon: \"fa fa-trash\",\n    name: _t(\"Delete\"),\n    onSelected: async ({ message: msg, owner, store }) => {\n        const message = toRaw(msg);\n        const def = new Deferred();\n        store.env.services.dialog.add(\n            discussComponentRegistry.get(\"MessageConfirmDialog\"),\n            {\n                message,\n                prompt: _t(\"Are you sure you want to bid farewell to this message forever?\"),\n                onConfirm: () => {\n                    def.resolve(true);\n                    message.remove({\n                        removeFromThread: owner.shouldHideFromMessageListOnDelete,\n                    });\n                },\n            },\n            { context: owner, onClose: () => def.resolve(false) }\n        );\n        return def;\n    },\n    sequence: 120,\n    tags: ACTION_TAGS.DANGER,\n});\nregisterMessageAction(\"download_files\", {\n    condition: ({ message, store }) =>\n        message.attachment_ids.length > 1 && store.self.main_user_id?.share === false,\n    icon: \"fa fa-download\",\n    name: _t(\"Download Files\"),\n    onSelected: ({ message }) =>\n        download({\n            data: {\n                file_ids: message.attachment_ids.map((rec) => rec.id),\n                zip_name: `attachments_${DateTime.local().toFormat(\"HHmmddMMyyyy\")}.zip`,\n            },\n            url: \"/mail/attachment/zip\",\n        }),\n    sequence: 55,\n});\nregisterMessageAction(\"toggle-translation\", {\n    condition: ({ message }) => message.isTranslatable(message.thread),\n    icon: ({ message }) =>\n        `fa fa-language ${message.showTranslation ? \"o-mail-Message-translated\" : \"\"}`,\n    name: ({ message }) => (message.showTranslation ? _t(\"Revert\") : _t(\"Translate\")),\n    onSelected: ({ message }) => message.onClickToggleTranslation(),\n    sequence: 100,\n});\nregisterMessageAction(\"copy-message\", {\n    condition: ({ message }) => isMobileOS() && !message.isBodyEmpty,\n    onSelected: ({ message }) => message.copyMessageText(),\n    name: _t(\"Copy to Clipboard\"),\n    icon: \"fa fa-copy\",\n    sequence: 30,\n});\nregisterMessageAction(\"copy-link\", {\n    condition: ({ message, thread }) =>\n        message.message_type &&\n        message.message_type !== \"user_notification\" &&\n        thread &&\n        (!thread.access_token || thread.hasReadAccess),\n    icon: \"fa fa-link\",\n    name: _t(\"Copy Link\"),\n    onSelected: ({ message }) => message.copyLink(),\n    sequence: 110,\n});\n\nexport class MessageAction extends Action {\n    /** @type {() => Message} */\n    messageFn;\n    /** @type {() => Thread} */\n    threadFn;\n    /**\n     * @param {Object} param0\n     * @param {Thread|() => Thread} thread\n     */\n    constructor({ message, thread }) {\n        super(...arguments);\n        this.messageFn = typeof message === \"function\" ? message : () => message;\n        this.threadFn = typeof thread === \"function\" ? thread : () => thread;\n    }\n\n    get params() {\n        return Object.assign(super.params, { message: this.messageFn(), thread: this.threadFn() });\n    }\n}\n\nclass UseMessageActions extends UseActions {\n    ActionClass = MessageAction;\n}\n\n/**\n * @param {Object} [params0={}]\n * @param {Message|() => Message} [message]\n * @param {Thread|() => Thread} [thread] when set, the thread the message is being viewed\n */\nexport function useMessageActions({ message, thread } = {}) {\n    const component = useComponent();\n    const transformedActions = messageActionsRegistry\n        .getEntries()\n        .map(\n            ([id, definition]) =>\n                new MessageAction({ owner: component, id, definition, message, thread })\n        );\n    for (const action of transformedActions) {\n        action.setup();\n    }\n    const state = useState(\n        new UseMessageActions(component, transformedActions, useService(\"mail.store\"))\n    );\n    return state;\n}\n", "import { Message } from \"@mail/core/common/message\";\nimport { useVisible } from \"@mail/utils/common/hooks\";\n\nimport { Component, useSubEnv } from \"@odoo/owl\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { useService } from \"@web/core/utils/hooks\";\n\n/**\n * @typedef {Object} Props\n * @property {string} [emptyText]\n * @property {import(\"@mail/core/common/message_model\").Message[]} messages\n * @property {ReturnType<import('@mail/core/common/message_search_hook').useMessageSearch>} [messageSearch]\n * @property {function} [loadMore]\n * @property {string} mode\n * @property {function} [onClickJump]\n * @property {function} [onLoadMoreVisible]\n * @property {boolean} [showEmpty]\n * @property {import(\"@mail/core/common/thread_model\").Thread} thread\n * @extends {Component<Props, Env>}\n */\nexport class MessageCardList extends Component {\n    static components = { Message };\n    static props = [\n        \"emptyText?\",\n        \"messages\",\n        \"messageSearch?\",\n        \"loadMore?\",\n        \"mode\",\n        \"onClickJump?\",\n        \"onLoadMoreVisible?\",\n        \"showEmpty?\",\n        \"thread\",\n    ];\n    static template = \"mail.MessageCardList\";\n\n    setup() {\n        super.setup();\n        this.ui = useService(\"ui\");\n        useSubEnv({ messageCard: true });\n        useVisible(\"load-more\", (isVisible) => {\n            if (isVisible) {\n                this.props.onLoadMoreVisible?.();\n            }\n        });\n    }\n\n    /**\n     * Highlight the given message and scrolls to it. In small mode, the\n     * pin/search menus are closed beforewards\n     *\n     * @param {import('@mail/core/common/message_model').Message} message\n     */\n    async onClickJump(message) {\n        this.props.onClickJump?.();\n        if (this.ui.isSmall || this.env.inChatWindow || this.env.inMeetingView) {\n            this.env.pinMenu?.close();\n            this.env.searchMenu?.close();\n            this.env.inMeetingView?.openChat();\n        }\n        // Give the time for menus to close before scrolling to the message.\n        await new Promise((resolve) => setTimeout(() => requestAnimationFrame(resolve)));\n        await this.env.messageHighlight?.highlightMessage(message, this.props.thread);\n    }\n\n    get emptyText() {\n        return this.props.emptyText ?? _t(\"No messages found\");\n    }\n}\n", "import { Component } from \"@odoo/owl\";\n\nimport { Dialog } from \"@web/core/dialog/dialog\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { discussComponentRegistry } from \"./discuss_component_registry\";\n\nexport class MessageConfirmDialog extends Component {\n    static components = { Dialog };\n    static props = [\n        \"close\",\n        \"confirmColor?\",\n        \"confirmText?\",\n        \"message\",\n        \"prompt\",\n        \"size?\",\n        \"title?\",\n        \"onConfirm\",\n    ];\n    static defaultProps = {\n        confirmColor: \"btn-primary\",\n        confirmText: _t(\"Delete\"),\n        size: \"xl\",\n        title: _t(\"Send this message to the great trash can in the sky?\"),\n    };\n    static template = \"mail.MessageConfirmDialog\";\n\n    get messageComponent() {\n        return discussComponentRegistry.get(\"Message\");\n    }\n\n    onClickConfirm() {\n        this.props.onConfirm();\n        this.props.close();\n    }\n}\n\ndiscussComponentRegistry.add(\"MessageConfirmDialog\", MessageConfirmDialog);\n", "import { Component } from \"@odoo/owl\";\n\nimport { useService } from \"@web/core/utils/hooks\";\nimport { url } from \"@web/core/utils/urls\";\n\nexport class MessageInReply extends Component {\n    static props = [\"class?\", \"message\", \"onClick?\"];\n    static defaultProps = { class: \"\" };\n    static template = \"mail.MessageInReply\";\n\n    setup() {\n        super.setup();\n        this.store = useService(\"mail.store\");\n    }\n\n    get authorAvatarUrl() {\n        if (\n            this.props.message.message_type &&\n            this.props.message.message_type.includes(\"email\") &&\n            !this.props.message.author_id &&\n            !this.props.message.author_guest_id\n        ) {\n            return url(\"/mail/static/src/img/email_icon.png\");\n        }\n\n        if (this.props.message.parent_id.author) {\n            return this.props.message.parent_id.author.avatarUrl;\n        }\n\n        return this.store.DEFAULT_AVATAR;\n    }\n}\n", "import { LinkPreview } from \"@mail/core/common/link_preview\";\n\nimport { Component } from \"@odoo/owl\";\n\n/**\n * @typedef {Object} Props\n * @property {import(\"models\").MessageLinkPreview[]} messageLinkPreviews\n * @extends {Component<Props, Env>}\n */\nexport class MessageLinkPreviewList extends Component {\n    static template = \"mail.MessageLinkPreviewList\";\n    static props = [\"messageLinkPreviews\"];\n    static components = { LinkPreview };\n}\n", "import { fields, Record } from \"@mail/core/common/record\";\n\nimport { rpc } from \"@web/core/network/rpc\";\n\nexport class MessageLinkPreview extends Record {\n    static _name = \"mail.message.link.preview\";\n    static id = \"id\";\n\n    message_id = fields.One(\"mail.message\", { inverse: \"message_link_preview_ids\" });\n    link_preview_id = fields.One(\"mail.link.preview\", { inverse: \"message_link_preview_ids\" });\n\n    get gifPaused() {\n        return !this.message_id.thread?.isFocused;\n    }\n\n    hide() {\n        rpc(\"/mail/link_preview/hide\", { message_link_preview_ids: [this.id] });\n    }\n}\n\nMessageLinkPreview.register();\n", "import { isEmptyBlock } from \"@html_editor/utils/dom_info\";\n\nimport { fields, Record } from \"@mail/core/common/record\";\nimport {\n    EMOJI_REGEX,\n    convertBrToLineBreak,\n    decorateEmojis,\n    generateEmojisOnHtml,\n    getNonEditableMentions,\n    htmlToTextContentInline,\n} from \"@mail/utils/common/format\";\n\nimport { browser } from \"@web/core/browser/browser\";\nimport { router } from \"@web/core/browser/router\";\nimport { loadEmoji } from \"@web/core/emoji_picker/emoji_picker\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { user } from \"@web/core/user\";\nimport { createDocumentFragmentFromContent, createElementWithContent } from \"@web/core/utils/html\";\nimport { url } from \"@web/core/utils/urls\";\n\nimport { markup } from \"@odoo/owl\";\n\nconst { DateTime } = luxon;\nexport class Message extends Record {\n    static _name = \"mail.message\";\n    static id = \"id\";\n\n    /** @param {Object} data */\n    update(data) {\n        super.update(data);\n        if (this.isNotification && !this.notificationType) {\n            const htmlBody = createDocumentFragmentFromContent(this.body);\n            this.notificationType = htmlBody.querySelector(\".o_mail_notification\")?.dataset.oeType;\n        }\n    }\n\n    attachment_ids = fields.Many(\"ir.attachment\", { inverse: \"message\" });\n    author_id = fields.One(\"res.partner\");\n    author_guest_id = fields.One(\"mail.guest\");\n    get author() {\n        return this.author_id || this.author_guest_id;\n    }\n    body = fields.Html(\"\");\n    call_history_ids = fields.Many(\"discuss.call.history\");\n    richBody = fields.Html(\"\", {\n        compute() {\n            if (!this.store.emojiLoader.loaded) {\n                loadEmoji();\n            }\n            return decorateEmojis(this.body) ?? \"\";\n        },\n    });\n    richTranslationValue = fields.Html(\"\", {\n        compute() {\n            if (!this.store.emojiLoader.loaded) {\n                loadEmoji();\n            }\n            return decorateEmojis(this.translationValue) ?? \"\";\n        },\n    });\n    composer = fields.One(\"Composer\", { inverse: \"message\", onDelete: (r) => r.delete() });\n    composerAsReplyToMessage = fields.One(\"Composer\", { inverse: \"replyToMessage\" });\n    date = fields.Datetime();\n    /** @type {string} */\n    default_subject;\n    /** @type {boolean} */\n    edited = fields.Attr(false, {\n        compute() {\n            return Boolean(\n                // \".o-mail-Message-edited\" is the class added by the mail.thread in _message_update_content\n                // when the message is edited\n                createDocumentFragmentFromContent(this.body).querySelector(\".o-mail-Message-edited\")\n            );\n        },\n    });\n    hasLink = fields.Attr(false, {\n        compute() {\n            if (this.isBodyEmpty) {\n                return false;\n            }\n            const div = createElementWithContent(\"div\", this.body);\n            return Boolean(div.querySelector(\"a:not([data-oe-model])\"));\n        },\n    });\n    hasMailNotificationSummary = fields.Attr(false, {\n        compute() {\n            return Boolean(\n                createDocumentFragmentFromContent(this.body).querySelector(\n                    '[summary=\"o_mail_notification\"]'\n                )\n            );\n        },\n    });\n    /** @type {number|string} */\n    id;\n    /** @type {Array[Array[string]]} */\n    incoming_email_cc;\n    /** @type {Array[Array[string]]} */\n    incoming_email_to;\n    get isDiscussion() {\n        return this.store.mt_comment?.eq(this.subtype_id);\n    }\n    get isNote() {\n        return this.store.mt_note?.eq(this.subtype_id);\n    }\n    /** @type {boolean} */\n    is_transient;\n    message_link_preview_ids = fields.Many(\"mail.message.link.preview\", { inverse: \"message_id\" });\n    /** @type {number[]} */\n    parent_id = fields.One(\"mail.message\");\n    /**\n     * When set, this temporary/pending message failed message post, and the\n     * value is a callback to re-attempt to post the message.\n     *\n     * @type {() => {} | undefined}\n     */\n    postFailRedo = undefined;\n    reactions = fields.Many(\"MessageReactions\", {\n        inverse: \"message\",\n        /**\n         * @param {import(\"models\").MessageReactions} r1\n         * @param {import(\"models\").MessageReactions} r2\n         */\n        sort: (r1, r2) => r1.sequence - r2.sequence,\n    });\n    notification_ids = fields.Many(\"mail.notification\", { inverse: \"mail_message_id\" });\n    partner_ids = fields.Many(\"res.partner\");\n    subtype_id = fields.One(\"mail.message.subtype\");\n    thread = fields.One(\"Thread\");\n    threadAsNeedaction = fields.One(\"Thread\", {\n        compute() {\n            if (this.needaction) {\n                return this.thread;\n            }\n        },\n    });\n    threadAsNewest = fields.One(\"Thread\");\n    threadAsInEdition = fields.One(\"Thread\", {\n        compute() {\n            if (this.composer) {\n                return this.thread;\n            }\n        },\n    });\n    scheduledDatetime = fields.Datetime();\n    onlyEmojis = fields.Attr(false, {\n        compute() {\n            const bodyWithoutTags = createElementWithContent(\"div\", this.body).textContent;\n            const withoutEmojis = bodyWithoutTags.replace(EMOJI_REGEX, \"\");\n            return (\n                bodyWithoutTags.length > 0 &&\n                bodyWithoutTags.match(EMOJI_REGEX) &&\n                withoutEmojis.trim().length === 0\n            );\n        },\n    });\n    /** @type {string} */\n    subject;\n    /** @type {Object[]} */\n    trackingValues = [];\n    /** @type {string|undefined} */\n    translationValue;\n    /** @type {string|undefined} */\n    translationSource;\n    /** @type {string|undefined} */\n    translationErrors;\n    /** @type {string} */\n    message_type;\n    /** @type {string|undefined} */\n    notificationType;\n    create_date = fields.Datetime();\n    write_date = fields.Datetime();\n    /** @type {undefined|Boolean} */\n    needaction;\n    starred = false;\n    showTranslation = false;\n\n    /**\n     * True if the backend would technically allow edition\n     * @returns {boolean}\n     */\n    get allowsEdition() {\n        return this.store.self.main_user_id?.is_admin || this.isSelfAuthored;\n    }\n\n    get bubbleColor() {\n        if (this.message_type === \"notification\") {\n            return undefined;\n        }\n        if (!this.isSelfAuthored && !this.isNote && !this.isHighlightedFromMention) {\n            return \"blue\";\n        }\n        if (this.isSelfAuthored && !this.isNote && !this.isHighlightedFromMention) {\n            return \"green\";\n        }\n        if (this.isHighlightedFromMention) {\n            return \"orange\";\n        }\n        return undefined;\n    }\n\n    get editable() {\n        if (this.isEmpty || !this.allowsEdition) {\n            return false;\n        }\n        return this.message_type === \"comment\";\n    }\n\n    get dateDay() {\n        let dateDay = this.datetime.toLocaleString(DateTime.DATE_MED);\n        if (dateDay === DateTime.now().toLocaleString(DateTime.DATE_MED)) {\n            dateDay = _t(\"Today\");\n        }\n        return dateDay;\n    }\n\n    get dateSimple() {\n        return this.datetime\n            .toLocaleString(DateTime.TIME_SIMPLE, {\n                locale: user.lang,\n            })\n            .replace(\"\u202f\", \" \"); // so that AM/PM are properly wrapped\n    }\n\n    get dateSimpleWithDay() {\n        const userLocale = { locale: user.lang };\n        if (this.datetime.hasSame(DateTime.now(), \"day\")) {\n            return this.datetime.toLocaleString(DateTime.TIME_SIMPLE, userLocale);\n        }\n        if (this.datetime.hasSame(DateTime.now().minus({ day: 1 }), \"day\")) {\n            return _t(\"Yesterday at %(time)s\", {\n                time: this.datetime.toLocaleString(DateTime.TIME_SIMPLE, userLocale),\n            });\n        }\n        if (this.datetime?.year === DateTime.now().year) {\n            return this.datetime.toLocaleString(\n                { ...DateTime.DATETIME_MED, year: undefined },\n                userLocale\n            );\n        }\n        return this.datetime.toLocaleString({ ...DateTime.DATETIME_MED }, userLocale);\n    }\n\n    get datetime() {\n        return this.date || DateTime.now();\n    }\n\n    /**\n     * Get the effective persona performing actions on this message.\n     * Priority order: logged-in user, portal partner (token-authenticated), guest.\n     *\n     * @returns {import(\"models\").Persona}\n     */\n    get effectiveSelf() {\n        return this.thread?.effectiveSelf ?? this.store.self;\n    }\n\n    get datetimeShort() {\n        return this.datetime.toLocaleString(DateTime.DATETIME_SHORT_WITH_SECONDS);\n    }\n\n    get isSelfMentioned() {\n        return this.effectiveSelf.in(this.partner_ids);\n    }\n\n    get isHighlightedFromMention() {\n        return this.isSelfMentioned && this.thread?.model === \"discuss.channel\";\n    }\n\n    isSelfAuthored = fields.Attr(false, {\n        compute() {\n            return Boolean(this.author?.eq(this.effectiveSelf));\n        },\n    });\n\n    isPending = false;\n\n    get hasActions() {\n        return !this.is_transient;\n    }\n\n    get isNotification() {\n        return this.message_type === \"notification\" && this.thread?.model === \"discuss.channel\";\n    }\n\n    get isSubjectSimilarToThreadName() {\n        if (!this.subject || !this.thread || !this.thread.display_name) {\n            return false;\n        }\n        const regexPrefix = /^((re|fw|fwd)\\s*:\\s*)*/i;\n        const cleanedThreadName = this.thread.display_name.replace(regexPrefix, \"\");\n        const cleanedSubject = this.subject.replace(regexPrefix, \"\");\n        return cleanedSubject === cleanedThreadName;\n    }\n\n    get isSubjectDefault() {\n        const name = this.thread?.display_name;\n        const threadName = name ? name.trim().toLowerCase() : \"\";\n        const defaultSubject = this.default_subject ? this.default_subject.toLowerCase() : \"\";\n        const candidates = new Set([defaultSubject, threadName]);\n        return candidates.has(this.subject?.toLowerCase());\n    }\n\n    get persistent() {\n        return Number.isInteger(this.id);\n    }\n\n    get resUrl() {\n        return url(router.stateToUrl({ model: this.thread.model, resId: this.thread.id }));\n    }\n\n    isTranslatable(thread) {\n        return (\n            !this.isEmpty &&\n            !this.isBodyEmpty &&\n            !this.hasMailNotificationSummary &&\n            this.store.hasMessageTranslationFeature &&\n            ![\"discuss.channel\", \"mail.box\"].includes(thread?.model)\n        );\n    }\n\n    get hasTextContent() {\n        return !this.isBodyEmpty || this.edited;\n    }\n\n    isEmpty = fields.Attr(false, {\n        /** @this {import(\"models\").Message} */\n        compute() {\n            return this.computeIsEmpty();\n        },\n    });\n    isBodyEmpty = fields.Attr(undefined, {\n        compute() {\n            return !this.body || isEmptyBlock(createElementWithContent(\"div\", this.body));\n        },\n    });\n\n    computeIsEmpty() {\n        return (\n            this.isBodyEmpty &&\n            this.attachment_ids.length === 0 &&\n            this.trackingValues.length === 0 &&\n            !this.subtype_id?.description\n        );\n    }\n\n    /**\n     * Determines if the link preview is actually the main content of the\n     * message. Meaning:\n     * - The link is the only part of the message body.\n     * - There is only one link in the message body.\n     * - The link preview is of image type.\n     */\n    get linkPreviewSquash() {\n        return (\n            this.store.hasLinkPreviewFeature &&\n            this.body &&\n            this.body.startsWith(\"<a\") &&\n            this.body.endsWith(\"/a>\") &&\n            this.body.match(/<\\/a>/im)?.length === 1 &&\n            this.message_link_preview_ids.length === 1 &&\n            this.message_link_preview_ids[0].link_preview_id.isImage\n        );\n    }\n\n    /**\n     * This is the preferred way to display the name of the author of a message.\n     */\n    get authorName() {\n        if (this.author) {\n            return this.getPersonaName(this.author);\n        }\n        return this.email_from;\n    }\n\n    get notificationHidden() {\n        return false;\n    }\n\n    inlineBody = fields.Html(\"\", {\n        /** @this {import(\"models\").Message} */\n        compute() {\n            if (this.notificationType === \"call\") {\n                return _t(\"%(caller)s started a call\", { caller: this.authorName });\n            }\n            if (this.notificationType === \"thread_deletion\") {\n                return _t('%(user)s deleted the thread \"%(thread_name)s\"', {\n                    user: this.authorName,\n                    thread_name: decorateEmojis(htmlToTextContentInline(this.body)),\n                });\n            }\n            if (this.notificationType === \"channel_rename\") {\n                const name = htmlToTextContentInline(this.body);\n                const params = { user: this.authorName, name: markup`<b>${name}</b>` };\n                return this.thread?.parent_channel_id\n                    ? _t(\"%(user)s changed the thread name to %(name)s\", params)\n                    : _t(\"%(user)s changed the channel name to %(name)s\", params);\n            }\n            if (this.isEmpty) {\n                return _t(\"This message has been removed\");\n            }\n            if (!this.body) {\n                return \"\";\n            }\n            return decorateEmojis(htmlToTextContentInline(this.body));\n        },\n    });\n\n    get notificationIcon() {\n        switch (this.notificationType) {\n            case \"pin\":\n                return \"fa fa-thumb-tack\";\n            case \"call\":\n                return \"fa fa-phone\";\n        }\n        return null;\n    }\n\n    get failureNotifications() {\n        return this.notification_ids.filter((notification) => notification.isFailure);\n    }\n\n    get scheduledDateSimple() {\n        return this.scheduledDatetime.toLocaleString(DateTime.TIME_SIMPLE, {\n            locale: user.lang,\n        });\n    }\n\n    get canToggleStar() {\n        return Boolean(\n            !this.is_transient &&\n                !this.isPending &&\n                this.thread &&\n                this.store.self_partner?.main_user_id?.share === false &&\n                this.persistent\n        );\n    }\n\n    get hasOnlyAttachments() {\n        return this.isBodyEmpty && this.attachment_ids.length > 0;\n    }\n\n    previewText = fields.Html(\"\", {\n        /** @this {import(\"models\").Message} */\n        compute() {\n            if (!this.hasOnlyAttachments) {\n                return this.inlineBody || this.subtype_id?.description;\n            }\n            const { attachment_ids: attachments } = this;\n            if (!attachments || attachments.length === 0) {\n                return \"\";\n            }\n            switch (attachments.length) {\n                case 1:\n                    return attachments[0].previewName;\n                case 2:\n                    return _t(\"%(file1)s and %(file2)s\", {\n                        file1: attachments[0].previewName,\n                        file2: attachments[1].previewName,\n                        count: attachments.length - 1,\n                    });\n                default:\n                    return _t(\"%(file1)s and %(count)s other attachments\", {\n                        file1: attachments[0].previewName,\n                        count: attachments.length - 1,\n                    });\n            }\n        },\n    });\n\n    get previewIcon() {\n        const { attachment_ids: attachments } = this;\n        if (!attachments || attachments.length === 0) {\n            return \"\";\n        }\n        const firstAttachment = attachments[0];\n        switch (true) {\n            case firstAttachment.isImage:\n                return \"fa-picture-o\";\n            case firstAttachment.mimetype === \"audio/mpeg\":\n                return firstAttachment.voice ? \"fa-microphone\" : \"fa-headphones\";\n            case firstAttachment.isVideo:\n                return \"fa-video-camera\";\n            default:\n                return \"fa-file\";\n        }\n    }\n\n    /** @param {import(\"models\").Thread} thread the thread where the message is shown */\n    canAddReaction(thread) {\n        return Boolean(\n            !this.is_transient &&\n                !this.isPending &&\n                this.thread?.can_react &&\n                !this.thread.isTransient\n        );\n    }\n\n    /** @param {import(\"models\").Thread} thread the thread where the message is shown */\n    canReplyTo(thread) {\n        return (\n            [\"discuss.channel\", \"mail.box\"].includes(thread?.model) &&\n            this.message_type !== \"user_notification\"\n        );\n    }\n\n    /** @param {import(\"models\").Thread} thread the thread where the message is shown */\n    canUnfollow(thread) {\n        return Boolean(this.thread?.selfFollower && thread?.model === \"mail.box\");\n    }\n\n    async copyLink() {\n        let notification = _t(\"Message Link Copied!\");\n        let type = \"info\";\n        try {\n            await browser.navigator.clipboard.writeText(url(`/mail/message/${this.id}`));\n        } catch {\n            notification = _t(\"Message Link Copy Failed (Permission denied?)!\");\n            type = \"danger\";\n        }\n        this.store.env.services.notification.add(notification, { type });\n    }\n\n    async copyMessageText() {\n        const messageBody = convertBrToLineBreak(this.body);\n        try {\n            await browser.navigator.clipboard.writeText(messageBody);\n        } catch {\n            this.store.env.services.notification.add(\n                _t(\"Message Copy Failed (Permission denied?)!\"),\n                { type: \"danger\" }\n            );\n        }\n        this.store.env.services.notification.add(_t(\"Message Copied!\"), { type: \"info\" });\n    }\n\n    async edit(\n        body,\n        attachments = [],\n        { mentionedChannels = [], mentionedPartners = [], mentionedRoles = [] } = {}\n    ) {\n        const bodyEl = createElementWithContent(\"div\", this.body);\n        bodyEl.querySelector(\"span.o-mail-Message-edited\")?.remove();\n        if (\n            createElementWithContent(\"div\", body).innerHTML === bodyEl.innerHTML &&\n            attachments.length === 0\n        ) {\n            return;\n        }\n        const validMentions = this.store.getMentionsFromText(body, {\n            mentionedChannels,\n            mentionedPartners,\n            mentionedRoles,\n            thread: this.thread,\n        });\n        const hadLink = this.hasLink; // to remove old previews if message no longer contains any link\n        const updateData = {\n            attachment_ids: attachments\n                .concat(this.attachment_ids)\n                .map((attachment) => attachment.id),\n            attachment_tokens: attachments\n                .concat(this.attachment_ids)\n                .map((attachment) => attachment.ownership_token),\n            body: await generateEmojisOnHtml(body),\n            partner_ids: validMentions?.partners?.map((partner) => partner.id),\n            role_ids: validMentions?.roles?.map((role) => role.id),\n        };\n        this.store.fillPartnersMentionToken(updateData);\n        const data = await rpc(\"/mail/message/update_content\", {\n            message_id: this.id,\n            update_data: updateData,\n            ...this.thread.rpcParams,\n        });\n        this.store.insert(data);\n        if ((hadLink || this.hasLink) && this.store.hasLinkPreviewFeature) {\n            rpc(\"/mail/link_preview\", { message_id: this.id }, { silent: true });\n        }\n        return data;\n    }\n\n    /** @param {import(\"models\").Thread} thread the thread where the message is being viewed when starting edition */\n    async enterEditMode(thread) {\n        const doc = createDocumentFragmentFromContent(this.body);\n        const validChannels = (\n            await Promise.all(\n                Array.from(\n                    doc.querySelectorAll(\".o_channel_redirect[data-oe-model='discuss.channel']\")\n                ).map(async (el) =>\n                    this.store.Thread.getOrFetch({ id: el.dataset.oeId, model: \"discuss.channel\" })\n                )\n            )\n        ).filter((channel) => channel?.exists());\n        const text = convertBrToLineBreak(this.body);\n        if (thread?.messageInEdition) {\n            thread.messageInEdition.composer = undefined;\n        }\n        this.composer = {\n            composerHtml: getNonEditableMentions(this.body),\n            mentionedChannels: validChannels,\n            mentionedPartners: this.partner_ids,\n            selection: {\n                start: text.length,\n                end: text.length,\n                direction: \"none\",\n            },\n        };\n    }\n\n    /** @param {import(\"models\").Thread} thread the thread where the message is being viewed when stopping edition */\n    exitEditMode(thread) {\n        const threadAsInEdition = this.threadAsInEdition;\n        this.composer = undefined;\n        if (threadAsInEdition && threadAsInEdition.eq(thread)) {\n            threadAsInEdition.composer.autofocus++;\n        }\n    }\n\n    /**\n     * Provide fallback to displayName in the absence of a thread\n     *\n     * @param {import(\"models\").Persona} persona\n     * @returns {string}\n     */\n    getPersonaName(persona) {\n        return this.thread?.getPersonaName(persona) || persona?.displayName || persona?.name;\n    }\n\n    async onClickToggleTranslation() {\n        if (!this.translationValue) {\n            const { error, lang_name, body } = await rpc(\"/mail/message/translate\", {\n                message_id: this.id,\n            });\n            this.translationValue = body && markup(body);\n            this.translationSource = lang_name;\n            this.translationErrors = error;\n        }\n        this.showTranslation = !this.showTranslation && Boolean(this.translationValue);\n    }\n\n    async react(content) {\n        this.store.insert(\n            await rpc(\n                \"/mail/message/reaction\",\n                {\n                    action: \"add\",\n                    content,\n                    message_id: this.id,\n                    ...this.thread.rpcParams,\n                },\n                { silent: true }\n            )\n        );\n    }\n\n    async remove({ removeFromThread = false } = {}) {\n        const data = await rpc(\"/mail/message/update_content\", {\n            message_id: this.id,\n            update_data: this.removeParams,\n            ...this.thread.rpcParams,\n        });\n        this.store.insert(data);\n        if (this.thread && removeFromThread) {\n            this.thread.messages = this.thread.messages.filter((message) => message.notEq(this));\n        }\n        this.composer = undefined;\n        return data;\n    }\n\n    get removeParams() {\n        return {\n            attachment_ids: [],\n            attachment_tokens: [],\n            body: \"\",\n            partner_ids: [],\n        };\n    }\n\n    async setDone() {\n        await this.store.env.services.orm.silent.call(\"mail.message\", \"set_message_done\", [\n            [this.id],\n        ]);\n    }\n\n    async toggleStar() {\n        this.store.insert(\n            await this.store.env.services.orm.silent.call(\n                \"mail.message\",\n                \"toggle_message_starred\",\n                [[this.id]]\n            )\n        );\n    }\n\n    async unfollow() {\n        if (this.needaction) {\n            await this.setDone();\n        }\n        const thread = this.thread;\n        await thread.selfFollower.remove();\n        this.store.env.services.notification.add(\n            _t('You are no longer following \"%(thread_name)s\".', {\n                thread_name: thread.display_name,\n            }),\n            { type: \"success\" }\n        );\n    }\n\n    hideAllLinkPreviews() {\n        rpc(\"/mail/link_preview/hide\", {\n            message_link_preview_ids: this.message_link_preview_ids.map((lpm) => lpm.id),\n        });\n    }\n}\n\nMessage.register();\n", "import { Component } from \"@odoo/owl\";\n\nexport class MessageNotificationPopover extends Component {\n    static template = \"mail.MessageNotificationPopover\";\n    static props = [\"message\", \"close?\"];\n}\n", "import { useHover } from \"@mail/utils/common/hooks\";\nimport { Component } from \"@odoo/owl\";\nimport { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { useDropdownState } from \"@web/core/dropdown/dropdown_hooks\";\nimport { loadEmoji } from \"@web/core/emoji_picker/emoji_picker\";\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { useService } from \"@web/core/utils/hooks\";\n\nexport class MessageReactionList extends Component {\n    static template = \"mail.MessageReactionList\";\n    static components = { Dropdown };\n    static props = [\"message\", \"openReactionMenu\", \"reaction\"];\n\n    setup() {\n        super.setup();\n        this.loadEmoji = loadEmoji;\n        this.store = useService(\"mail.store\");\n        this.ui = useService(\"ui\");\n        this.preview = useDropdownState();\n        this.hover = useHover([\"reactionButton\", \"reactionList\"], {\n            onHover: () => (this.preview.isOpen = true),\n            onAway: () => (this.preview.isOpen = false),\n            stateObserver: () => [this.preview?.isOpen],\n        });\n    }\n\n    /** @param {import(\"models\").MessageReactions} reaction */\n    previewText(reaction) {\n        const { count, content: emoji } = reaction;\n        const personNames = reaction.personas\n            .slice(0, 3)\n            .map((persona) => this.props.message.getPersonaName(persona));\n        const shortcode =\n            this.store.emojiLoader.loaded?.emojiValueToShortcodes?.[emoji]?.[0] ?? \"?\";\n        switch (count) {\n            case 1:\n                return _t(\"%(emoji)s reacted by %(person)s\", {\n                    emoji: shortcode,\n                    person: personNames[0],\n                });\n            case 2:\n                return _t(\"%(emoji)s reacted by %(person1)s and %(person2)s\", {\n                    emoji: shortcode,\n                    person1: personNames[0],\n                    person2: personNames[1],\n                });\n            case 3:\n                return _t(\"%(emoji)s reacted by %(person1)s, %(person2)s, and %(person3)s\", {\n                    emoji: shortcode,\n                    person1: personNames[0],\n                    person2: personNames[1],\n                    person3: personNames[2],\n                });\n            case 4:\n                return _t(\n                    \"%(emoji)s reacted by %(person1)s, %(person2)s, %(person3)s, and 1 other\",\n                    {\n                        emoji: shortcode,\n                        person1: personNames[0],\n                        person2: personNames[1],\n                        person3: personNames[2],\n                    }\n                );\n            default:\n                return _t(\n                    \"%(emoji)s reacted by %(person1)s, %(person2)s, %(person3)s, and %(count)s others\",\n                    {\n                        count: count - 3,\n                        emoji: shortcode,\n                        person1: personNames[0],\n                        person2: personNames[1],\n                        person3: personNames[2],\n                    }\n                );\n        }\n    }\n\n    hasSelfReacted(reaction) {\n        return this.props.message.effectiveSelf.in(reaction.personas);\n    }\n\n    onClickReaction(reaction) {\n        if (!this.props.message.canAddReaction()) {\n            return;\n        }\n        if (this.hasSelfReacted(reaction)) {\n            reaction.remove();\n        } else {\n            this.props.message.react(reaction.content);\n        }\n    }\n\n    onContextMenu(ev) {\n        if (this.ui.isSmall) {\n            ev.preventDefault();\n            this.props.openReactionMenu();\n        }\n    }\n\n    onClickReactionList(reaction) {\n        this.preview.isOpen = false; // closes dropdown immediately as to not recover focus after dropdown closes\n        this.props.openReactionMenu(reaction);\n    }\n}\n", "import { loadEmoji } from \"@web/core/emoji_picker/emoji_picker\";\nimport { onExternalClick } from \"@mail/utils/common/hooks\";\n\nimport { Component, onMounted, useEffect, useExternalListener, useRef, useState } from \"@odoo/owl\";\n\nimport { Dialog } from \"@web/core/dialog/dialog\";\nimport { useService } from \"@web/core/utils/hooks\";\n\nexport class MessageReactionMenu extends Component {\n    static props = [\"close\", \"message\", \"initialReaction?\"];\n    static components = { Dialog };\n    static template = \"mail.MessageReactionMenu\";\n\n    setup() {\n        super.setup();\n        this.root = useRef(\"root\");\n        this.store = useService(\"mail.store\");\n        this.ui = useService(\"ui\");\n        this.state = useState({\n            reaction: this.props.initialReaction\n                ? this.props.initialReaction\n                : this.props.message.reactions[0],\n        });\n        useExternalListener(document, \"keydown\", this.onKeydown);\n        onExternalClick(\"root\", () => this.props.close());\n        useEffect(\n            () => {\n                const activeReaction = this.props.message.reactions.find(\n                    ({ content }) => content === this.state.reaction.content\n                );\n                if (this.props.message.reactions.length === 0) {\n                    this.props.close();\n                } else if (!activeReaction) {\n                    this.state.reaction = this.props.message.reactions[0];\n                }\n            },\n            () => [this.props.message.reactions.length]\n        );\n        onMounted(() => {\n            if (!this.store.emojiLoader.loaded) {\n                loadEmoji();\n            }\n        });\n    }\n\n    onKeydown(ev) {\n        switch (ev.key) {\n            case \"Escape\":\n                this.props.close();\n                break;\n            case \"q\":\n                this.props.close();\n                break;\n            default:\n                return;\n        }\n    }\n\n    getEmojiShortcode(reaction) {\n        return this.store.emojiLoader.loaded?.emojiValueToShortcodes?.[reaction.content][0] ?? \"?\";\n    }\n\n    get contentClass() {\n        const attClass = {\n            \"o-mail-MessageReactionMenu h-50 d-flex\": true,\n            \"position-absolute bottom-0\": this.store.useMobileView,\n        };\n        return Object.entries(attClass)\n            .filter(([classNames, value]) => value)\n            .map(([classNames]) => classNames)\n            .join(\" \");\n    }\n}\n", "import { Component, useRef } from \"@odoo/owl\";\n\nimport { useMessageActions } from \"@mail/core/common/message_actions\";\nimport { MessageReactionList } from \"@mail/core/common/message_reaction_list\";\nimport { QuickReactionMenu } from \"@mail/core/common/quick_reaction_menu\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { useEmojiPicker } from \"@web/core/emoji_picker/emoji_picker\";\nimport { isMobileOS } from \"@web/core/browser/feature_detection\";\n\nexport class MessageReactions extends Component {\n    static props = [\"message\", \"openReactionMenu\"];\n    static template = \"mail.MessageReactions\";\n    static components = { MessageReactionList, QuickReactionMenu };\n\n    setup() {\n        super.setup();\n        this.store = useService(\"mail.store\");\n        this.ui = useService(\"ui\");\n        this.addRef = useRef(\"add\");\n        this.isMobileOS = isMobileOS();\n        this.messageActions = useMessageActions({ message: () => this.props.message });\n        this.emojiPicker = useEmojiPicker(this.addRef, {\n            onSelect: (emoji) => {\n                const reaction = this.props.message.reactions.find(\n                    ({ content, personas }) =>\n                        content === emoji && this.props.message.effectiveSelf.in(personas)\n                );\n                if (!reaction) {\n                    this.props.message.react(emoji);\n                }\n            },\n        });\n    }\n}\n", "import { AND, fields, Record } from \"@mail/core/common/record\";\nimport { rpc } from \"@web/core/network/rpc\";\n\nexport class MessageReactions extends Record {\n    static id = AND(\"message\", \"content\");\n\n    /** @type {string} */\n    content;\n    /** @type {number} */\n    count;\n    guests = fields.Many(\"mail.guest\");\n    message = fields.One(\"mail.message\");\n    partners = fields.Many(\"res.partner\");\n    personas = fields.Attr([], {\n        compute() {\n            return [...this.partners, ...this.guests];\n        },\n    });\n    /** @type {number} */\n    sequence;\n\n    async remove() {\n        this.store.insert(\n            await rpc(\n                \"/mail/message/reaction\",\n                {\n                    action: \"remove\",\n                    content: this.content,\n                    message_id: this.message.id,\n                    ...this.message.thread.rpcParams,\n                },\n                { silent: true }\n            )\n        );\n    }\n}\n\nMessageReactions.register();\n", "import { useSequential } from \"@mail/utils/common/hooks\";\nimport { useState, onWillUnmount, markup } from \"@odoo/owl\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { createDocumentFragmentFromContent } from \"@web/core/utils/html\";\nimport { escapeRegExp } from \"@web/core/utils/strings\";\n\nexport const HIGHLIGHT_CLASS = \"o-mail-Message-searchHighlight\";\n\n/**\n * @param {string} searchTerm\n * @param {string} target\n */\nexport function searchHighlight(searchTerm, target) {\n    if (!searchTerm) {\n        return target;\n    }\n    const htmlDoc = createDocumentFragmentFromContent(target);\n    for (const term of searchTerm.split(\" \")) {\n        const regexp = new RegExp(`(${escapeRegExp(term)})`, \"gi\");\n        // Special handling for '\n        // Note: browsers use XPath 1.0, so uses concat() rather than ||\n        const split = term.toLowerCase().split(\"'\");\n        let lowercase = split.map((s) => `'${s}'`).join(', \"\\'\", ');\n        let uppercase = lowercase.toUpperCase();\n        if (split.length > 1) {\n            lowercase = `concat(${lowercase})`;\n            uppercase = `concat(${uppercase})`;\n        }\n        const matchs = htmlDoc.evaluate(\n            `//*[text()[contains(translate(., ${uppercase}, ${lowercase}), ${lowercase})]]`, // Equivalent to `.toLowerCase()` on all searched chars\n            htmlDoc,\n            null,\n            XPathResult.ORDERED_NODE_SNAPSHOT_TYPE\n        );\n        for (let i = 0; i < matchs.snapshotLength; i++) {\n            const element = matchs.snapshotItem(i);\n            const newNode = [];\n            for (const node of element.childNodes) {\n                const match = node.textContent.match(regexp);\n                if (node.nodeType === Node.TEXT_NODE && match?.length > 0) {\n                    let curIndex = 0;\n                    for (const match of node.textContent.matchAll(regexp)) {\n                        const start = htmlDoc.createTextNode(\n                            node.textContent.slice(curIndex, match.index)\n                        );\n                        newNode.push(start);\n                        const span = htmlDoc.createElement(\"span\");\n                        span.setAttribute(\"class\", HIGHLIGHT_CLASS);\n                        span.textContent = match[0];\n                        newNode.push(span);\n                        curIndex = match.index + match[0].length;\n                    }\n                    const end = htmlDoc.createTextNode(node.textContent.slice(curIndex));\n                    newNode.push(end);\n                } else {\n                    newNode.push(node);\n                }\n            }\n            element.replaceChildren(...newNode);\n        }\n    }\n    return markup(htmlDoc.body.innerHTML);\n}\n\n/** @param {import('models').Thread} thread */\nexport function useMessageSearch(thread) {\n    const store = useService(\"mail.store\");\n    const sequential = useSequential();\n    const state = useState({\n        thread,\n        async search(before = false) {\n            if (this.searchTerm) {\n                this.searching = true;\n                const data = await sequential(() =>\n                    store.searchMessagesInThread(\n                        this.searchTerm,\n                        this.thread,\n                        before,\n                        this.is_notification\n                    )\n                );\n                if (!data) {\n                    return;\n                }\n                const { count, loadMore, messages } = data;\n                this.searched = true;\n                this.searching = false;\n                this.count = count;\n                this.loadMore = loadMore;\n                if (before) {\n                    this.messages.push(...messages);\n                } else {\n                    this.messages = messages;\n                }\n            } else {\n                this.clear();\n            }\n        },\n        count: 0,\n        clear() {\n            this.messages = [];\n            this.searched = false;\n            this.searching = false;\n            this.searchTerm = undefined;\n        },\n        /** @type {true | false | undefined} */\n        is_notification: undefined,\n        loadMore: false,\n        /** @type {import('@mail/core/common/message_model').Message[]} */\n        messages: [],\n        /** @type {string|undefined} */\n        searchTerm: undefined,\n        searched: false,\n        searching: false,\n        /** @param {string} target */\n        highlight: (target) => searchHighlight(state.searchTerm, target),\n    });\n    onWillUnmount(() => {\n        state.clear();\n    });\n    return state;\n}\n", "import { ImStatus } from \"@mail/core/common/im_status\";\nimport { onExternalClick } from \"@mail/utils/common/hooks\";\nimport { markEventHandled, isEventHandled } from \"@web/core/utils/misc\";\n\nimport { Component, useEffect, useExternalListener, useRef, useState } from \"@odoo/owl\";\n\nimport { getActiveHotkey } from \"@web/core/hotkeys/hotkey_service\";\nimport { usePosition } from \"@web/core/position/position_hook\";\nimport { useService } from \"@web/core/utils/hooks\";\n\nexport class NavigableList extends Component {\n    static components = { ImStatus };\n    static template = \"mail.NavigableList\";\n    static props = {\n        anchorRef: { optional: true },\n        class: { type: String, optional: true },\n        onSelect: { type: Function },\n        options: { type: Array },\n        optionTemplate: { type: String, optional: true },\n        position: { type: String, optional: true },\n        closeOnSelect: { type: Boolean, optional: true },\n        isLoading: { type: Boolean, optional: true },\n    };\n    static defaultProps = {\n        position: \"bottom\",\n        closeOnSelect: true,\n        isLoading: false,\n    };\n\n    setup() {\n        super.setup();\n        this.rootRef = useRef(\"root\");\n        this.state = useState({\n            activeIndex: null,\n            open: false,\n            showLoading: false,\n        });\n        this.hotkey = useService(\"hotkey\");\n        this.hotkeysToRemove = [];\n\n        useExternalListener(window, \"keydown\", this.onKeydown, true);\n        onExternalClick(\"root\", async (ev) => {\n            // Let event be handled by bubbling handlers first.\n            await new Promise(setTimeout);\n            if (isEventHandled(ev, \"composer.onClickTextarea\")) {\n                return;\n            }\n            this.close();\n        });\n        // position and size\n        usePosition(\"root\", () => this.props.anchorRef, { position: this.props.position });\n        useEffect(\n            () => {\n                this.open();\n            },\n            () => [this.props]\n        );\n        useEffect(\n            () => {\n                if (!this.props.isLoading) {\n                    clearTimeout(this.loadingTimeoutId);\n                    this.state.showLoading = false;\n                } else if (!this.loadingTimeoutId) {\n                    this.loadingTimeoutId = setTimeout(() => (this.state.showLoading = true), 2000);\n                }\n            },\n            () => [this.props.isLoading]\n        );\n    }\n\n    get show() {\n        return Boolean(this.state.open && (this.props.isLoading || this.props.options.length));\n    }\n\n    get sortedOptions() {\n        return this.props.options.sort((o1, o2) => (o1.group ?? 0) - (o2.group ?? 0));\n    }\n\n    open() {\n        this.state.open = true;\n        this.state.activeIndex = null;\n        this.navigate(\"first\");\n    }\n\n    close() {\n        if (this.props.closeOnSelect) {\n            this.state.open = false;\n            this.state.activeIndex = null;\n        }\n    }\n\n    selectOption(ev, index, params = {}) {\n        const option = this.props.options[index];\n        if (!option) {\n            return;\n        }\n        if (option.unselectable) {\n            this.close();\n            return;\n        }\n        this.props.onSelect(ev, option, {\n            ...params,\n        });\n        this.close();\n    }\n\n    navigate(direction) {\n        if (this.props.options.length === 0) {\n            return;\n        }\n        const activeOptionId = this.state.activeIndex !== null ? this.state.activeIndex : 0;\n        let targetId = undefined;\n        switch (direction) {\n            case \"first\":\n                targetId = 0;\n                break;\n            case \"last\":\n                targetId = this.props.options.length - 1;\n                break;\n            case \"previous\":\n                targetId = activeOptionId - 1;\n                if (targetId < 0) {\n                    this.navigate(\"last\");\n                    return;\n                }\n                break;\n            case \"next\":\n                targetId = activeOptionId + 1;\n                if (targetId > this.props.options.length - 1) {\n                    this.navigate(\"first\");\n                    return;\n                }\n                break;\n            default:\n                return;\n        }\n        this.state.activeIndex = targetId;\n    }\n\n    onKeydown(ev) {\n        if (!this.show) {\n            return;\n        }\n        const hotkey = getActiveHotkey(ev);\n        switch (hotkey) {\n            case \"enter\":\n                markEventHandled(ev, \"NavigableList.select\");\n                if (this.state.activeIndex === null) {\n                    this.close();\n                    return;\n                }\n                this.selectOption(ev, this.state.activeIndex);\n                break;\n            case \"escape\":\n                markEventHandled(ev, \"NavigableList.close\");\n                this.close();\n                break;\n            case \"tab\":\n                this.navigate(this.state.activeIndex === null ? \"first\" : \"next\");\n                break;\n            case \"arrowup\":\n                this.navigate(this.state.activeIndex === null ? \"first\" : \"previous\");\n                break;\n            case \"arrowdown\":\n                this.navigate(this.state.activeIndex === null ? \"first\" : \"next\");\n                break;\n            default:\n                return;\n        }\n        if (this.props.options.length !== 0) {\n            ev.stopPropagation();\n        }\n        ev.preventDefault();\n    }\n\n    onOptionMouseEnter(index) {}\n}\n", "import {\n    Component,\n    onMounted,\n    onPatched,\n    onWillDestroy,\n    onWillUpdateProps,\n    useRef,\n} from \"@odoo/owl\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { escape } from \"@web/core/utils/strings\";\n\nexport class NotificationMessage extends Component {\n    static template = \"mail.NotificationMessage\";\n    static props = [\"message\", \"thread\", \"registerMessageRef?\"];\n\n    setup() {\n        super.setup();\n        this.root = useRef(\"root\");\n        onWillUpdateProps((nextProps) => {\n            this.props.registerMessageRef?.(this.props.message, null);\n        });\n        onMounted(() => this.props.registerMessageRef?.(this.props.message, this.root));\n        onPatched(() => this.props.registerMessageRef?.(this.props.message, this.root));\n        onWillDestroy(() => this.props.registerMessageRef?.(this.props.message, null));\n        this.escape = escape;\n        this.store = useService(\"mail.store\");\n    }\n\n    /**\n     * @param {MouseEvent} ev\n     */\n    async onClickNotificationMessage(ev) {\n        this.store.handleClickOnLink(ev, this.props.thread);\n        const { oeType, oeId } = ev.target.dataset;\n        if (oeType === \"highlight\") {\n            await this.env.messageHighlight?.highlightMessage(\n                this.store[\"mail.message\"].insert({\n                    id: Number(oeId),\n                    res_id: this.props.thread.id,\n                    model: this.props.thread.model,\n                    thread: this.props.thread,\n                }),\n                this.props.thread\n            );\n        }\n    }\n\n    get message() {\n        return this.props.message;\n    }\n\n    get callInformation() {\n        const history = this.message.call_history_ids[0];\n        if (history?.duration_hour === undefined || !history?.end_dt) {\n            return _t(\"%(author)s started a call.\", { author: this.message.authorName });\n        }\n        let duration = luxon.Duration.fromObject({\n            seconds: Math.max(1, Math.round(history.duration_hour * 3600)),\n        }).shiftTo(\"hours\", \"minutes\", \"seconds\");\n        if (duration.hours || duration.minutes) {\n            duration = duration.set({ seconds: 0 });\n        }\n        const units = Object.entries(duration.toObject())\n            .filter(([unit, amount]) => amount != 0)\n            .map(([unit, amount]) => unit);\n        return _t(\"Call lasted %(duration)s.\", {\n            duration: duration.shiftTo(...units).toHuman({ unitDisplay: \"short\" }),\n        });\n    }\n}\n", "import { fields, Record } from \"@mail/core/common/record\";\n\nimport { _t } from \"@web/core/l10n/translation\";\n\nexport class Notification extends Record {\n    static _name = \"mail.notification\";\n    static id = \"id\";\n\n    /** @type {number} */\n    id;\n    mail_message_id = fields.One(\"mail.message\", {\n        onDelete() {\n            this.delete();\n        },\n    });\n    /** @type {string} */\n    notification_status;\n    /** @type {string} */\n    notification_type;\n    mail_email_address;\n    failure = fields.One(\"Failure\", {\n        inverse: \"notifications\",\n        /** @this {import(\"models\").Notification} */\n        compute() {\n            const thread = this.mail_message_id?.thread;\n            if (!this.mail_message_id?.isSelfAuthored) {\n                return;\n            }\n            const failure = Object.values(this.store.Failure.records).find(\n                (f) =>\n                    f.resModel === thread?.model &&\n                    f.type === this.notification_type &&\n                    (f.resModel !== \"discuss.channel\" || f.resIds.has(thread?.id))\n            );\n            return this.isFailure\n                ? {\n                      id: failure ? failure.id : this.store.Failure.nextId.value++,\n                  }\n                : false;\n        },\n        eager: true,\n    });\n    /** @type {string} */\n    failure_type;\n    get failureMessage() {\n        switch (this.failure_type) {\n            case \"mail_smtp\":\n                return _t(\"Connection failed\");\n            case \"mail_bounce\":\n                return _t(\"Bounce\");\n            case \"mail_email_invalid\":\n                return _t(\"Invalid email address\");\n            case \"mail_email_missing\":\n                return _t(\"Missing email address\");\n            case \"mail_from_invalid\":\n                return _t(\"Invalid from address\");\n            case \"mail_from_missing\":\n                return _t(\"Missing from address\");\n            case \"mail_spam\":\n                return _t(\"Detected As Spam\");\n            default:\n                return _t(\"Exception\");\n        }\n    }\n    res_partner_id = fields.One(\"res.partner\");\n\n    /**\n     * Get the translate string of the failure type only\n     * when it corresponds to a failure type\n     * that is automatically cancelled before sending.\n     *\n     * @returns {string}\n     */\n    get autoCanceledFailureType() {\n        switch (this.failure_type) {\n            case \"mail_bl\":\n                return _t(\"Blacklisted Address\");\n            case \"mail_dup\":\n                return _t(\"Duplicated Email\");\n            case \"mail_optout\":\n                return _t(\"Opted Out\");\n        }\n        return \"\";\n    }\n\n    get isFailure() {\n        return [\"exception\", \"bounce\"].includes(this.notification_status);\n    }\n\n    get icon() {\n        if (this.isFailure) {\n            return \"fa fa-envelope\";\n        }\n        return \"fa fa-envelope-o\";\n    }\n\n    get label() {\n        return \"\";\n    }\n\n    get isFollowerNotification() {\n        return this.mail_message_id.thread.followers.some(\n            (follower) => follower.partner_id.id === this.res_partner_id.id\n        );\n    }\n\n    get statusIcon() {\n        switch (this.notification_status) {\n            case \"process\":\n                return \"fa fa-hourglass-half\";\n            case \"pending\":\n                return \"fa fa-paper-plane-o\";\n            case \"sent\":\n                return \"fa fa-check\";\n            case \"bounce\":\n                return \"fa fa-exclamation\";\n            case \"exception\":\n                return \"fa fa-times text-danger\";\n            case \"ready\":\n                return \"fa fa-send-o\";\n            case \"canceled\":\n                if (this.autoCanceledFailureType) {\n                    return \"fa fa-remove\";\n                }\n                return \"fa fa-trash-o\";\n        }\n        return \"\";\n    }\n\n    get statusTitle() {\n        switch (this.notification_status) {\n            case \"process\":\n                return _t(\"Processing\");\n            case \"pending\":\n                return _t(\"Sent\");\n            case \"sent\":\n                return _t(\"Delivered\");\n            case \"bounce\":\n                return _t(\"Bounced\");\n            case \"exception\":\n                return _t(\"Error\");\n            case \"ready\":\n                return _t(\"Queued\");\n            case \"canceled\":\n                return this.autoCanceledFailureType || _t(\"Cancelled\");\n        }\n        return \"\";\n    }\n}\n\nNotification.register();\n", "import { reactive } from \"@odoo/owl\";\n\nimport { browser } from \"@web/core/browser/browser\";\nimport {\n    isAndroidApp,\n    isDisplayStandalone,\n    isIOS,\n    isIosApp,\n} from \"@web/core/browser/feature_detection\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\n\nasync function getIosPwaPermission() {\n    if (browser.location.protocol !== \"https:\") {\n        return \"denied\";\n    }\n    const registration = await browser.navigator.serviceWorker?.getRegistration();\n    return (await registration?.pushManager.permissionState()) ?? \"prompt\";\n}\n\nexport const notificationPermissionService = {\n    dependencies: [\"notification\"],\n\n    _normalizePermission(permission) {\n        switch (permission) {\n            case \"default\":\n                return \"prompt\";\n            case undefined:\n                return \"denied\";\n            default:\n                return permission;\n        }\n    },\n\n    /**\n     * @param {import(\"@web/env\").OdooEnv} env\n     * @param {import(\"services\").ServiceFactories} services\n     */\n    async start(env, services) {\n        const notification = services.notification;\n        let permission;\n        try {\n            if (isIOS() && isDisplayStandalone()) {\n                permission = { state: await getIosPwaPermission() };\n            } else if (isIOS()) {\n                permission = { state: \"denied\" };\n            } else {\n                permission = await browser.navigator?.permissions?.query({\n                    name: \"notifications\",\n                });\n            }\n        } catch {\n            // noop\n        }\n        const state = reactive({\n            /** @type {\"prompt\" | \"granted\" | \"denied\"} */\n            permission:\n                isIosApp() || isAndroidApp()\n                    ? \"denied\"\n                    : this._normalizePermission(\n                          permission?.state ?? browser.Notification?.permission\n                      ),\n            requestPermission: async () => {\n                if (browser.Notification && state.permission === \"prompt\") {\n                    state.permission = this._normalizePermission(\n                        await browser.Notification.requestPermission()\n                    );\n                    if (state.permission === \"denied\") {\n                        notification.add(_t(\"Odoo will not send notifications on this device.\"), {\n                            type: \"warning\",\n                            title: _t(\"Notifications blocked\"),\n                        });\n                    } else if (state.permission === \"granted\") {\n                        notification.add(_t(\"Odoo will send notifications on this device!\"), {\n                            type: \"success\",\n                            title: _t(\"Notifications allowed\"),\n                        });\n                    }\n                }\n            },\n        });\n        if (permission && !isIOS()) {\n            permission.addEventListener(\"change\", () => (state.permission = permission.state));\n        }\n        return state;\n    },\n};\n\nregistry.category(\"services\").add(\"mail.notification.permission\", notificationPermissionService);\n", "import { browser } from \"@web/core/browser/browser\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\n\nconst PREVIEW_MSG_MAX_SIZE = 350; // optimal for native English speakers\n\n/**\n * @typedef {Messaging} Messaging\n */\nexport class OutOfFocusService {\n    /**\n     * @param {import(\"@web/env\").OdooEnv} env\n     * @param {import(\"services\").ServiceFactories} services\n     */\n    constructor(env, services) {\n        this.setup(env, services);\n    }\n\n    setup(env, services) {\n        this.env = env;\n        this.audio = undefined;\n        this.multiTab = services.multi_tab;\n        this.notificationService = services.notification;\n        this.soundEffectService = services[\"mail.sound_effects\"];\n        /** @type {import(\"models\").Store} */\n        this.store = services[\"mail.store\"];\n        this.closeFuncs = [];\n    }\n\n    async notify(message, thread) {\n        const modelsHandleByPush = [\"mail.thread\", \"discuss.channel\"];\n        if (\n            modelsHandleByPush.includes(message.thread?.model) &&\n            (await this.hasServiceWorkInstalledAndPushSubscriptionActive())\n        ) {\n            return;\n        }\n        const author = message.author;\n        let notificationTitle;\n        let icon = \"/mail/static/src/img/odoobot_transparent.png\";\n        if (!author) {\n            notificationTitle = _t(\"New message\");\n        } else {\n            icon = author.avatarUrl;\n            if (message.thread?.channel_type === \"channel\") {\n                notificationTitle = _t(\"%(author name)s from %(channel name)s\", {\n                    \"author name\": message.authorName,\n                    \"channel name\": message.thread.displayName,\n                });\n            } else {\n                notificationTitle = message.authorName;\n            }\n        }\n        const notificationContent = message.previewText\n            .toString()\n            .substring(0, PREVIEW_MSG_MAX_SIZE);\n        await this.sendNotification({\n            message: notificationContent,\n            sound: message.thread?.model === \"discuss.channel\",\n            title: notificationTitle,\n            type: \"info\",\n            icon,\n        });\n    }\n\n    async hasServiceWorkInstalledAndPushSubscriptionActive() {\n        const registration = await browser.navigator.serviceWorker?.getRegistration();\n        if (registration) {\n            const pushManager = await registration.pushManager;\n            if (pushManager) {\n                const subscription = await pushManager.getSubscription();\n                return !!subscription;\n            }\n        }\n        return false;\n    }\n\n    /**\n     * Send a notification, preferably a native one. If native\n     * notifications are disable or unavailable on the current\n     * platform, fallback on the notification service.\n     *\n     * @param {Object} param0\n     * @param {string} [param0.message] The body of the\n     * notification.\n     * @param {string} [param0.title] The title of the notification.\n     * @param {string} [param0.type] The type to be passed to the no\n     * service when native notifications can't be sent.\n     * @param {string} [param0.icon] The icon to be displayed in the\n     * notification.\n     */\n    async sendNotification({ message, sound = true, title, type, icon }) {\n        if (!this.canSendNativeNotification || !(await this.multiTab.isOnMainTab())) {\n            if (sound) {\n                this._playSound();\n            }\n            return;\n        }\n        try {\n            this.sendNativeNotification(title, message, icon, { sound });\n        } catch (error) {\n            // Notification without Serviceworker in Chrome Android doesn't works anymore\n            // So we fallback to the notification service in this case\n            // https://bugs.chromium.org/p/chromium/issues/detail?id=481856\n            if (error.message.includes(\"ServiceWorkerRegistration\")) {\n                this.sendOdooNotification(message, { sound, title, type });\n            } else {\n                throw error;\n            }\n        }\n    }\n\n    /**\n     * @param {string} message\n     * @param {Object} options\n     */\n    async sendOdooNotification(message, options) {\n        const { sound } = options;\n        delete options.sound;\n        this.closeFuncs.push(this.notificationService.add(message, options));\n        if (this.closeFuncs.length > 3) {\n            this.closeFuncs.shift()();\n        }\n        if (sound) {\n            this._playSound();\n        }\n    }\n\n    /**\n     * @param {string} title\n     * @param {string} message\n     */\n    sendNativeNotification(title, message, icon, { sound = true } = {}) {\n        const notification = new Notification(title, {\n            body: message,\n            icon,\n        });\n        notification.addEventListener(\"click\", ({ target: notification }) => {\n            window.focus();\n            notification.close();\n        });\n        if (sound) {\n            this._playSound();\n        }\n    }\n\n    async _playSound() {\n        if (\n            this.canPlayAudio &&\n            this.store.settings.messageSound &&\n            (await this.multiTab.isOnMainTab())\n        ) {\n            this.soundEffectService.play(\"new-message\");\n        }\n    }\n\n    get canPlayAudio() {\n        return typeof Audio !== \"undefined\";\n    }\n\n    get canSendNativeNotification() {\n        return Boolean(window.Notification && window.Notification.permission === \"granted\");\n    }\n}\n\nexport const outOfFocusService = {\n    dependencies: [\"multi_tab\", \"notification\", \"mail.sound_effects\", \"mail.store\"],\n    /**\n     * @param {import(\"@web/env\").OdooEnv} env\n     * @param {import(\"services\").ServiceFactories} services\n     */\n    start(env, services) {\n        const service = new OutOfFocusService(env, services);\n        return service;\n    },\n};\n\nregistry.category(\"services\").add(\"mail.out_of_focus\", outOfFocusService);\n", "import { cleanTerm } from \"@mail/utils/common/format\";\nimport { registry } from \"@web/core/registry\";\n\n/**\n * Registry of functions to sort partner suggestions.\n * The expected value is a function with the following\n * signature:\n *     (partner1: Partner, partner2: Partner, { env: OdooEnv, searchTerm: string, thread?: Thread , context?: Object}) => number|undefined\n */\nexport const partnerCompareRegistry = registry.category(\"mail.partner_compare\");\n\npartnerCompareRegistry.add(\n    \"mail.archived-last-except-odoobot\",\n    (p1, p2) => {\n        const p1active = p1.active || p1.eq(p1.store.odoobot);\n        const p2active = p2.active || p2.eq(p2.store.odoobot);\n        if (!p1active && p2active) {\n            return 1;\n        }\n        if (!p2active && p1active) {\n            return -1;\n        }\n    },\n    { sequence: 5 }\n);\n\npartnerCompareRegistry.add(\n    \"mail.internal-users\",\n    (p1, p2) => {\n        const isAInternalUser = p1.main_user_id?.share === false;\n        const isBInternalUser = p2.main_user_id?.share === false;\n        if (isAInternalUser && !isBInternalUser) {\n            return -1;\n        }\n        if (!isAInternalUser && isBInternalUser) {\n            return 1;\n        }\n    },\n    { sequence: 35 }\n);\n\npartnerCompareRegistry.add(\n    \"mail.followers\",\n    (p1, p2, { thread }) => {\n        if (thread) {\n            const followerList = [...thread.followers];\n            if (thread.selfFollower) {\n                followerList.push(thread.selfFollower);\n            }\n            const isFollower1 = followerList.some((follower) => p1.eq(follower.partner_id));\n            const isFollower2 = followerList.some((follower) => p2.eq(follower.partner_id));\n            if (isFollower1 && !isFollower2) {\n                return -1;\n            }\n            if (!isFollower1 && isFollower2) {\n                return 1;\n            }\n        }\n    },\n    { sequence: 45 }\n);\n\npartnerCompareRegistry.add(\n    \"mail.name\",\n    (p1, p2, { searchTerm }) => {\n        const cleanedName1 = cleanTerm(p1.name);\n        const cleanedName2 = cleanTerm(p2.name);\n        if (cleanedName1.startsWith(searchTerm) && !cleanedName2.startsWith(searchTerm)) {\n            return -1;\n        }\n        if (!cleanedName1.startsWith(searchTerm) && cleanedName2.startsWith(searchTerm)) {\n            return 1;\n        }\n        if (cleanedName1 < cleanedName2) {\n            return -1;\n        }\n        if (cleanedName1 > cleanedName2) {\n            return 1;\n        }\n    },\n    { sequence: 50 }\n);\n\npartnerCompareRegistry.add(\n    \"mail.email\",\n    (p1, p2, { searchTerm }) => {\n        const cleanedEmail1 = cleanTerm(p1.email);\n        const cleanedEmail2 = cleanTerm(p2.email);\n        if (cleanedEmail1.startsWith(searchTerm) && !cleanedEmail1.startsWith(searchTerm)) {\n            return -1;\n        }\n        if (!cleanedEmail2.startsWith(searchTerm) && cleanedEmail2.startsWith(searchTerm)) {\n            return 1;\n        }\n        if (cleanedEmail1 < cleanedEmail2) {\n            return -1;\n        }\n        if (cleanedEmail1 > cleanedEmail2) {\n            return 1;\n        }\n    },\n    { sequence: 55 }\n);\n\npartnerCompareRegistry.add(\"mail.id\", (p1, p2) => p1.id - p2.id, { sequence: 75 });\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { baseContainerGlobalSelector } from \"@html_editor/utils/base_container\";\nimport { isEmptyBlock } from \"@html_editor/utils/dom_info\";\nimport { childNodes } from \"@html_editor/utils/dom_traversal\";\nimport { withSequence } from \"@html_editor/utils/resource\";\n\n/**\n * This plugin works with the composer used in Discuss, ChatWindow and Chatter.\n * For the full composer, it is using HtmlComposerMessageField.\n */\nexport class MailComposerPlugin extends Plugin {\n    static id = \"mail_composer\";\n    static dependencies = [\"clipboard\", \"hint\", \"input\", \"selection\"];\n    resources = {\n        before_paste_handlers: this.config.composerPluginDependencies.onBeforePaste.bind(this),\n        bypass_paste_image_files: () => true,\n        create_link_handlers: (linkEl) => (linkEl.target = \"_blank\"),\n        hints: [\n            withSequence(1, {\n                selector: `.odoo-editor-editable > ${baseContainerGlobalSelector}:only-child`,\n                text: this.config.placeholder,\n            }),\n        ],\n        hint_targets_providers: (selectionData, editable) => {\n            const el = editable.firstChild;\n            if (\n                !selectionData.documentSelectionIsInEditable &&\n                childNodes(editable).length === 1 &&\n                isEmptyBlock(el) &&\n                el.matches(baseContainerGlobalSelector)\n            ) {\n                return [el];\n            } else {\n                return [];\n            }\n        },\n        input_handlers: this.config.composerPluginDependencies.onInput.bind(this),\n    };\n\n    setup() {\n        this.addDomListener(\n            this.editable,\n            \"keydown\",\n            this.config.composerPluginDependencies.onKeydown\n        );\n        this.addDomListener(\n            this.editable,\n            \"focusin\",\n            this.config.composerPluginDependencies.onFocusin\n        );\n        this.addDomListener(\n            this.editable,\n            \"focusout\",\n            this.config.composerPluginDependencies.onFocusout\n        );\n    }\n}\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { closestElement } from \"@html_editor/utils/dom_traversal\";\nimport { generateThreadMentionElement } from \"@mail/utils/common/format\";\n\nexport class MentionPlugin extends Plugin {\n    static id = \"mention\";\n    static dependencies = [\"baseContainer\", \"selection\", \"history\", \"protectedNode\"];\n    resources = {\n        selectionchange_handlers: this.detectMentions.bind(this),\n        is_node_editable_predicates: (node) => {\n            for (const { selector } of this.MENTION_SELECTORS) {\n                if (closestElement(node, selector)) {\n                    return true;\n                }\n            }\n        },\n        select_all_overrides: this.selectAll.bind(this),\n    };\n\n    setup() {\n        super.setup();\n        /** @type {import(\"models\").Store} */\n        this.store = this.services[\"mail.store\"];\n    }\n\n    /**\n     * Extend the selection to include whole mention elements at the borders\n     * so that it doesn't get stuck into the contenteditable=false\n     */\n    selectAll({ anchorNode, anchorOffset, focusNode, focusOffset }) {\n        const SELECTOR = this.MENTION_SELECTORS.map(({ selector }) => selector).join(\", \");\n        if (closestElement(anchorNode, SELECTOR)) {\n            const startMention = closestElement(anchorNode, SELECTOR);\n            anchorNode = startMention.parentNode;\n            anchorOffset = Array.prototype.indexOf.call(anchorNode.childNodes, startMention);\n        }\n        if (closestElement(focusNode, SELECTOR)) {\n            const endMention = closestElement(focusNode, SELECTOR);\n            focusNode = endMention.parentNode;\n            focusOffset = Array.prototype.indexOf.call(focusNode.childNodes, endMention) + 1;\n        }\n        this.dependencies.selection.setSelection({\n            anchorNode,\n            anchorOffset,\n            focusNode,\n            focusOffset,\n        });\n    }\n\n    get MENTION_SELECTORS() {\n        return [\n            {\n                selector: \"a.o_channel_redirect\",\n                checker: (el) => this.isValidChannelMentionElement(el),\n                validMentionsHandler: (channelLinks) => {\n                    this.store.handleValidChannelMention(channelLinks);\n                    this.dependencies.history.addStep();\n                },\n            },\n            {\n                selector: \"a.o_mail_redirect\",\n                checker: (el) => true,\n            },\n            {\n                selector: \"a.o-discuss-mention\",\n                checker: (el) => true,\n            },\n            {\n                selector: \"a.o_mail_redirect\",\n                checker: () => true,\n            },\n            {\n                selector: \"a.o-discuss-mention\",\n                checker: () => true,\n            },\n        ];\n    }\n\n    async detectMentions(ev) {\n        for (const { selector, checker, validMentionsHandler } of this.MENTION_SELECTORS) {\n            const mentionLinks = Array.from(this.editable.querySelectorAll(selector)) || [];\n            const validMentionLinks = (\n                await Promise.all(\n                    mentionLinks.map(async (el) => ({\n                        el,\n                        isValid: await checker(el),\n                    }))\n                )\n            )\n                .filter(({ isValid }) => isValid)\n                .map(({ el }) => el);\n            this.prepareValidMentionLinks(validMentionLinks);\n            validMentionsHandler?.(validMentionLinks);\n        }\n    }\n\n    prepareValidMentionLinks(validMentionLinks) {\n        for (const el of validMentionLinks) {\n            this.dependencies.protectedNode.setProtectingNode(el, true);\n            // if el's parent is odoo-editor-editable, which happens when the html is computed or set with setContent,\n            // considering the mention blocks are protected and not editable.\n            // This will lead to issues where the mention cannot be deleted or edited properly.\n            // In this case, we wrap the mention with a base container.\n            if (el.parentElement === this.editable) {\n                const baseContainer = this.dependencies.baseContainer.createBaseContainer();\n                baseContainer.appendChild(el.cloneNode(true));\n                this.editable.replaceChild(baseContainer, el);\n                this.dependencies.history.addStep();\n            }\n        }\n    }\n\n    async isValidChannelMentionElement(el) {\n        if (el.dataset.oeModel !== \"discuss.channel\") {\n            return false;\n        }\n        const channel = await this.store.Thread.getOrFetch({\n            model: \"discuss.channel\",\n            id: Number(el.dataset.oeId),\n        });\n        if (!channel) {\n            return false;\n        }\n        const validChannelMention = generateThreadMentionElement(channel);\n        return (\n            validChannelMention.getAttribute(\"href\") === el.getAttribute(\"href\") &&\n            [...validChannelMention.classList].every((cls) => el.classList.contains(cls))\n        );\n    }\n}\n", "import { ChatGPTTranslatePlugin } from \"@html_editor/main/chatgpt/chatgpt_translate_plugin\";\nimport { ColorPlugin } from \"@html_editor/main/font/color_plugin\";\nimport { CORE_PLUGINS } from \"@html_editor/plugin_sets\";\nimport { FeffPlugin } from \"@html_editor/main/feff_plugin\";\nimport { HintPlugin } from \"@html_editor/main/hint_plugin\";\nimport { InlineCodePlugin } from \"@html_editor/main/inline_code\";\nimport { LinkPastePlugin } from \"@html_editor/main/link/link_paste_plugin\";\nimport { LinkPlugin } from \"@html_editor/main/link/link_plugin\";\nimport { ShortCutPlugin } from \"@html_editor/core/shortcut_plugin\";\nimport { TabulationPlugin } from \"@html_editor/main/tabulation_plugin\";\nimport { ToolbarPlugin } from \"@html_editor/main/toolbar/toolbar_plugin\";\n\nimport { MailComposerPlugin } from \"@mail/core/common/plugin/mail_composer_plugin\";\nimport { MentionPlugin } from \"@mail/core/common/plugin/mention_plugin\";\nimport { ProtectedNodePlugin } from \"@html_editor/core/protected_node_plugin\";\n\nexport const MAIL_CORE_PLUGINS = [\n    ...CORE_PLUGINS,\n    ChatGPTTranslatePlugin,\n    ColorPlugin,\n    FeffPlugin,\n    HintPlugin,\n    InlineCodePlugin,\n    LinkPastePlugin,\n    LinkPlugin,\n    MailComposerPlugin,\n    MentionPlugin,\n    ProtectedNodePlugin,\n    ShortCutPlugin,\n    TabulationPlugin,\n];\n\nexport const MAIL_PLUGINS = [...MAIL_CORE_PLUGINS, ToolbarPlugin];\nexport const MAIL_SMALL_UI_PLUGINS = MAIL_CORE_PLUGINS;\n", "import { Component, useExternalListener, useRef, useState } from \"@odoo/owl\";\nimport { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { useDropdownState } from \"@web/core/dropdown/dropdown_hooks\";\nimport { loadEmoji, useEmojiPicker } from \"@web/core/emoji_picker/emoji_picker\";\nimport { useService } from \"@web/core/utils/hooks\";\n\n/**\n * @typedef {Object} Props\n * @property {Object} action\n * @property {Object} [classNames]\n * @property {import(\"models\").Message} message\n * @property {boolean} [messageActive]\n * @extends {Component<Props, Env>}\n */\nexport class QuickReactionMenu extends Component {\n    static template = \"mail.QuickReactionMenu\";\n    static props = {\n        action: Object,\n        classNames: { type: Object, optional: true },\n        message: Object,\n        messageActive: { type: Boolean, optional: true },\n    };\n    static components = { Dropdown };\n    static DEFAULT_EMOJIS = [\"\ud83d\udc4d\", \"\u2764\ufe0f\", \"\ud83e\udd23\", \"\ud83d\ude2f\", \"\ud83d\ude05\", \"\ud83d\ude4f\"];\n\n    setup() {\n        this.toggle = useRef(\"toggle\");\n        this.store = useService(\"mail.store\");\n        this.ui = useService(\"ui\");\n        this.picker = useEmojiPicker(\n            null,\n            { onSelect: this.toggleReaction.bind(this), class: \"overflow-hidden rounded-2\" },\n            {\n                position: \"bottom-middle\",\n                popoverClass: \"o-mail-QuickReactionMenu-pickerPopover\",\n            }\n        );\n        this.dropdown = useState(\n            useDropdownState({\n                onClose: () => {\n                    const currentThread = this.env.getCurrentThread?.();\n                    if (!currentThread || currentThread.notEq(this.props.message.thread)) {\n                        return;\n                    }\n                    if (currentThread.messageInEdition) {\n                        currentThread.messageInEdition.composer.autofocus++;\n                    } else {\n                        currentThread.composer.autofocus++;\n                    }\n                },\n            })\n        );\n        this.frequentEmojiService = useService(\"web.frequent.emoji\");\n        useExternalListener(window, \"keydown\", async (ev) => {\n            if (\n                !this.dropdown.isOpen ||\n                this.picker.isOpen ||\n                !this.toggle.el?.contains(ev.target) ||\n                [\"Shift\", \"Control\", \"Meta\", \"Alt\"].includes(ev.key)\n            ) {\n                return;\n            }\n            this.togglePicker(ev.key);\n        });\n    }\n\n    togglePicker(initialSearchTerm) {\n        if (this.picker.isOpen) {\n            this.picker.close();\n        } else {\n            this.picker.open(this.toggle, { initialSearchTerm });\n        }\n    }\n\n    getEmojiShortcode(emoji) {\n        return this.store.emojiLoader.loaded?.emojiValueToShortcodes?.[emoji][0] ?? \"?\";\n    }\n\n    onClick() {\n        if (!this.store.emojiLoader.isLoaded) {\n            loadEmoji();\n        }\n        if (this.ui.isSmall) {\n            this.props.action.onSelected();\n        } else {\n            this.picker.close();\n            if (this.dropdown.isOpen) {\n                this.dropdown.close();\n            } else {\n                this.dropdown.open();\n            }\n        }\n    }\n\n    toggleReaction(emoji) {\n        const reaction = this.props.message.reactions.find(\n            (r) => r.content === emoji && this.props.message.effectiveSelf.in(r.personas)\n        );\n        if (reaction) {\n            reaction.remove();\n        } else {\n            this.props.message.react(emoji);\n            this.frequentEmojiService.incrementEmojiUsage(emoji);\n        }\n        this.dropdown.close();\n        this.picker.close();\n    }\n\n    get attClass() {\n        const invisible =\n            typeof this.props.messageActive === \"boolean\" &&\n            !this.props.messageActive &&\n            !this.dropdown.isOpen &&\n            !this.picker.isOpen;\n        return {\n            ...this.props.classNames,\n            \"o-open\": this.dropdown.isOpen,\n            invisible,\n            visible: !invisible,\n        };\n    }\n\n    reactedBySelf(emoji) {\n        return this.props.message.reactions.some(\n            (r) => r.content === emoji && this.props.message.effectiveSelf.in(r.personas)\n        );\n    }\n\n    get mostFrequentEmojis() {\n        const numberOfEmojis = 6;\n        const mostFrequent = this.frequentEmojiService.getMostFrequent(numberOfEmojis);\n        return mostFrequent.concat(\n            QuickReactionMenu.DEFAULT_EMOJIS.filter((emoji) => !mostFrequent.includes(emoji)).slice(\n                0,\n                Math.max(0, numberOfEmojis - mostFrequent.length)\n            )\n        );\n    }\n\n    get navigationOptions() {\n        return {\n            // Bypass nested dropdown behavior to allow initial focus.\n            onUpdated: (navigator) => {\n                if (!navigator.activeItem) {\n                    navigator.items[0]?.setActive();\n                }\n            },\n            hotkeys: {\n                arrowright: (navigator) => navigator.next(),\n                arrowleft: (navigator) => navigator.previous(),\n                // Disable up and down navigation as it does not make sense for horizontal menu.\n                arrowdown: null,\n                arrowup: null,\n            },\n        };\n    }\n}\n", "export * from \"@mail/model/export\";\n", "import { Component, onWillDestroy, onWillUpdateProps, xml } from \"@odoo/owl\";\n\nimport { _t } from \"@web/core/l10n/translation\";\n\nconst MINUTE = 60 * 1000;\nconst HOUR = 60 * MINUTE;\n\nexport class RelativeTime extends Component {\n    static props = [\"datetime\"];\n    static template = xml`<t t-esc=\"relativeTime\"/>`;\n\n    setup() {\n        super.setup();\n        this.timeout = null;\n        this.computeRelativeTime(this.props.datetime);\n        onWillDestroy(() => clearTimeout(this.timeout));\n        onWillUpdateProps((nextProps) => {\n            clearTimeout(this.timeout);\n            this.computeRelativeTime(nextProps.datetime);\n        });\n    }\n\n    computeRelativeTime(datetime) {\n        if (!datetime) {\n            this.relativeTime = \"\";\n            return;\n        }\n        const delta = Date.now() - datetime.ts;\n        const absDelta = Math.abs(delta);\n        if (absDelta < 45 * 1000) {\n            this.relativeTime = delta < 0 ? _t(\"in a few seconds\") : _t(\"now\");\n        } else {\n            this.relativeTime = datetime.toRelative();\n        }\n        const updateDelay = absDelta < MINUTE ? absDelta : absDelta < HOUR ? MINUTE : HOUR;\n        this.timeout = setTimeout(() => {\n            this.computeRelativeTime(this.props.datetime);\n            this.render();\n        }, updateDelay);\n    }\n}\n", "import { Record } from \"@mail/core/common/record\";\n\nexport class ResCompany extends Record {\n    static _name = \"res.company\";\n    static id = \"id\";\n\n    /** @type {number} */\n    id;\n    /** @type {string} */\n    name;\n}\n\nResCompany.register();\n", "import { fields, Record } from \"@mail/core/common/record\";\n\nexport class ResGroups extends Record {\n    static _name = \"res.groups\";\n    static id = \"id\";\n    /** @type {string} */\n    full_name;\n    partners = fields.Many(\"res.partner\", { inverse: \"group_ids\" });\n    privilege_id = fields.One(\"res.groups.privilege\");\n}\n\nResGroups.register();\n", "import { Record } from \"@mail/core/common/record\";\n\nexport class ResGroupsPrivilege extends Record {\n    static _name = \"res.groups.privilege\";\n}\n\nResGroupsPrivilege.register();\n", "import { Record } from \"@mail/core/common/record\";\n\nexport class ResLang extends Record {\n    static id = \"id\";\n    static _name = \"res.lang\";\n\n    /** @type {number} */\n    id;\n    /** @type {string} */\n    name;\n}\n\nResLang.register();\n", "import { Store } from \"@mail/core/common/store_service\";\nimport { fields, Record } from \"@mail/core/common/record\";\nimport { imageUrl } from \"@web/core/utils/urls\";\nimport { debounce } from \"@web/core/utils/timing\";\n\nconst { DateTime } = luxon;\n\nexport class ResPartner extends Record {\n    static id = \"id\";\n    static _name = \"res.partner\";\n    static new() {\n        const record = super.new(...arguments);\n        record.debouncedSetImStatus = debounce(\n            (newStatus) => record.updateImStatus(newStatus),\n            Store.IM_STATUS_DEBOUNCE_DELAY\n        );\n        return record;\n    }\n\n    _triggerPresenceSubscription = fields.Attr(null, {\n        compute() {\n            return this.monitorPresence && this.presenceChannel;\n        },\n        onUpdate() {\n            if (this.previousPresencechannel) {\n                this.store.env.services.bus_service.deleteChannel(this.previousPresencechannel);\n            }\n            if (this._triggerPresenceSubscription) {\n                this.store.env.services.bus_service.addChannel(this.presenceChannel);\n            }\n            this.previousPresencechannel = this.presenceChannel;\n        },\n        eager: true,\n    });\n    /** @type {string} */\n    avatar_128_access_token;\n    /** @type {string} */\n    commercial_company_name;\n    country_id = fields.One(\"res.country\");\n    debouncedSetImStatus;\n    /** @type {string} */\n    email;\n    /**\n     * function = job position (Frenchism)\n     *\n     * @type {string}\n     */\n    function;\n    group_ids = fields.Many(\"res.groups\", { inverse: \"partners\" });\n    /** @type {number} */\n    id;\n    /** @type {ImStatus} */\n    im_status = fields.Attr(null, {\n        onUpdate() {\n            if (this.eq(this.store.self_partner) && this.im_status === \"offline\") {\n                this.store.env.services.im_status.updateBusPresence();\n            }\n        },\n    });\n    /** @type {string|undefined} */\n    im_status_access_token;\n    /** @type {boolean | undefined} */\n    is_company;\n    /** @type {boolean} */\n    is_public;\n    main_user_id = fields.One(\"res.users\");\n    monitorPresence = fields.Attr(false, {\n        compute() {\n            if (!this.store.env.services.bus_service.isActive || this.id <= 0) {\n                return false;\n            }\n            return this.im_status !== \"im_partner\" && !this.is_public;\n        },\n    });\n    /** @type {string} */\n    name;\n    /** @type {string} */\n    display_name;\n    /** @type {string} */\n    phone;\n    /** @type {luxon.DateTime} */\n    offline_since = fields.Datetime();\n    presenceChannel = fields.Attr(null, {\n        compute() {\n            const channel = `odoo-presence-res.partner_${this.id}`;\n            if (this.im_status_access_token) {\n                return channel + `-${this.im_status_access_token}`;\n            }\n            return channel;\n        },\n    });\n    /** @type {string|undefined} */\n    previousPresencechannel;\n    write_date = fields.Datetime();\n\n    /**\n     * @deprecated\n     *\n     * `store.menuThreads` uses this field to filter threads based on search\n     * terms. For each computation, the `menuThread` field is marked as needing a\n     * recompute, which can lead to excessive recursion\u2014sometimes even exceeding the\n     * call stack size. This computation is simple enough that it doesn\u2019t need a\n     * compute and has been replaced by a getter. To override the display name\n     * computation, override the displayName getter.\n     */\n    _computeDisplayName() {\n        return this.name || this.display_name;\n    }\n\n    get avatarUrl() {\n        const accessTokenParam = {};\n        if (this.store.self.main_user_id?.share !== false) {\n            accessTokenParam.access_token = this.avatar_128_access_token;\n        }\n        return imageUrl(\"res.partner\", this.id, \"avatar_128\", {\n            ...accessTokenParam,\n            unique: this.write_date,\n        });\n    }\n\n    get displayName() {\n        return this._computeDisplayName();\n    }\n\n    searchChat() {\n        return Object.values(this.store.Thread.records).find(\n            (thread) => thread.channel_type === \"chat\" && thread.correspondent?.persona.eq(this)\n        );\n    }\n\n    updateImStatus(newStatus) {\n        if (newStatus === \"offline\") {\n            this.offline_since = DateTime.now();\n        }\n        this.im_status = newStatus;\n    }\n}\n\nResPartner.register();\n", "import { Record } from \"@mail/core/common/record\";\n\nexport class ResRole extends Record {\n    static id = \"id\";\n    static _name = \"res.role\";\n\n    /** @type {number} */\n    id;\n    /** @type {string} */\n    name;\n}\n\nResRole.register();\n", "import { fields, Record } from \"@mail/core/common/record\";\n\nexport class ResUsers extends Record {\n    static _name = \"res.users\";\n    static id = \"id\";\n\n    /** @type {number} */\n    id;\n    company_id = fields.One(\"res.company\");\n    /** @type {string} */\n    get email() {\n        return this.partner_id?.email;\n    }\n    /** @type {string} */\n    im_status;\n    /** @type {boolean} */\n    is_admin;\n    /** @type {string} */\n    get name() {\n        return this.partner_id?.name;\n    }\n    /** @type {\"email\" | \"inbox\"} */\n    notification_type;\n    partner_id = fields.One(\"res.partner\");\n    /** @type {string} */\n    get phone() {\n        return this.partner_id?.phone;\n    }\n    /** @type {boolean} false when the user is an internal user, true otherwise */\n    share;\n    /** @type {ReturnType<import(\"@odoo/owl\").markup>|string|undefined} */\n    signature = fields.Html(undefined);\n}\n\nResUsers.register();\n", "import { Component, useExternalListener, useState } from \"@odoo/owl\";\nimport { browser } from \"@web/core/browser/browser\";\nimport { useAutofocus } from \"@web/core/utils/hooks\";\nimport { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\nimport { _t } from \"@web/core/l10n/translation\";\n\n/**\n * @typedef {Object} SearchFilter\n * @property {string} label\n * @property {string} name\n * @property {true|false|undefined} [is_notification]\n */\n\n/**\n * @typedef {Object} Props\n * @property {ReturnType<typeof import(\"@mail/core/common/message_search_hook\").useMessageSearch>} messageSearch\n * @property {import(\"@mail/core/common/thread_model\").Thread} thread\n * @property {function} [closeSearch]\n * @extends {Component<Props, Env>}\n */\nexport class SearchMessageInput extends Component {\n    static template = \"mail.SearchMessageInput\";\n    static props = [\"closeSearch?\", \"messageSearch\", \"thread\"];\n    static components = { Dropdown, DropdownItem };\n\n    setup() {\n        super.setup();\n        this.state = useState({ searchTerm: \"\", searchedTerm: \"\" });\n        useAutofocus();\n        useExternalListener(\n            browser,\n            \"keydown\",\n            (ev) => {\n                if (ev.key === \"Escape\") {\n                    this.props.closeSearch?.();\n                }\n            },\n            { capture: true }\n        );\n    }\n\n    search() {\n        this.props.messageSearch.searchTerm = this.state.searchTerm;\n        this.props.messageSearch.search();\n        this.state.searchedTerm = this.state.searchTerm;\n    }\n\n    clear() {\n        this.state.searchTerm = \"\";\n        this.state.searchedTerm = this.state.searchTerm;\n        this.props.messageSearch.clear();\n        this.props.closeSearch?.();\n    }\n\n    onKeydownSearch(ev) {\n        if (ev.key !== \"Enter\") {\n            return;\n        }\n        if (!this.state.searchTerm) {\n            this.clear();\n        } else {\n            this.search();\n        }\n    }\n\n    /** @param {SearchFilter} searchFilter */\n    onChangeSearchFilter(searchFilter) {\n        if (searchFilter.is_notification !== this.props.messageSearch.is_notification) {\n            this.props.messageSearch.is_notification = searchFilter.is_notification;\n            if (this.state.searchTerm) {\n                this.search();\n            }\n        }\n    }\n\n    /** @returns {SearchFilter[]} */\n    get searchFilters() {\n        return [\n            {\n                label: \"all\",\n                name: _t(\"All\"),\n                is_notification: undefined,\n            },\n            {\n                label: \"conversations\",\n                name: _t(\"Conversations\"),\n                is_notification: false,\n            },\n            {\n                label: \"tracked_changes\",\n                name: _t(\"Tracked Changes\"),\n                is_notification: true,\n            },\n        ];\n    }\n}\n", "import { Component } from \"@odoo/owl\";\nimport { MessageCardList } from \"./message_card_list\";\nimport { _t } from \"@web/core/l10n/translation\";\n\n/**\n * @typedef {Object} Props\n * @property {import(\"@mail/core/common/thread_model\").Thread} thread\n * @property {Object} [messaageSearch]\n * @property {function} [onClickJump]\n * @property {function} [loadMore]\n */\nexport class SearchMessageResult extends Component {\n    static template = \"mail.SearchMessageResult\";\n    static components = { MessageCardList };\n    static props = [\"thread\", \"messageSearch\", \"onClickJump?\"];\n\n    get MESSAGE_FOUND() {\n        if (this.props.messageSearch.messages.length === 0) {\n            return false;\n        }\n        return _t(\"%s messages found\", this.props.messageSearch.count);\n    }\n\n    onLoadMoreVisible() {\n        const before = this.props.messageSearch?.messages\n            ? Math.min(...this.props.messageSearch.messages.map((message) => message.id))\n            : false;\n        this.props.messageSearch.search(before);\n    }\n}\n", "import { Component, onWillUpdateProps } from \"@odoo/owl\";\nimport { ActionPanel } from \"@mail/discuss/core/common/action_panel\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { SearchMessageInput } from \"@mail/core/common/search_message_input\";\nimport { SearchMessageResult } from \"@mail/core/common/search_message_result\";\nimport { useMessageSearch } from \"./message_search_hook\";\n\n/**\n * @typedef {Object} Props\n * @property {import(\"@mail/core/common/thread_model\").Thread} thread\n */\nexport class SearchMessagesPanel extends Component {\n    static template = \"mail.SearchMessagesPanel\";\n    static components = { ActionPanel, SearchMessageInput, SearchMessageResult };\n    static props = [\"thread\"];\n\n    setup() {\n        super.setup();\n        this.store = useService(\"mail.store\");\n        this.messageSearch = this.env.messageSearch ?? useMessageSearch(this.props.thread);\n        onWillUpdateProps((nextProps) => {\n            if (this.props.thread.notEq(nextProps.thread)) {\n                this.env.searchMenu?.close();\n            }\n        });\n    }\n\n    get title() {\n        return _t(\"Search Message\");\n    }\n}\n", "import { hasHardwareAcceleration } from \"@mail/utils/common/misc\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { browser } from \"@web/core/browser/browser\";\nimport { fields, Record } from \"./record\";\nimport { debounce } from \"@web/core/utils/timing\";\nimport { rpc } from \"@web/core/network/rpc\";\n\nconst MESSAGE_SOUND = \"mail.user_setting.message_sound\";\n\nexport class Settings extends Record {\n    id;\n\n    static new() {\n        const record = super.new(...arguments);\n        record.onStorage = record.onStorage.bind(record);\n        browser.addEventListener(\"storage\", record.onStorage);\n        return record;\n    }\n\n    setup() {\n        super.setup();\n        this.saveVoiceThresholdDebounce = debounce(() => {\n            browser.localStorage.setItem(\n                \"mail_user_setting_voice_threshold\",\n                this.voiceActivationThreshold.toString()\n            );\n        }, 2000);\n        this.hasCanvasFilterSupport =\n            typeof document.createElement(\"canvas\").getContext(\"2d\").filter !== \"undefined\";\n        this._loadLocalSettings();\n    }\n\n    delete() {\n        browser.removeEventListener(\"storage\", this.onStorage);\n        super.delete(...arguments);\n    }\n\n    // Notification settings\n    /**\n     * @type {\"mentions\"|\"all\"|\"no_notif\"}\n     */\n    channel_notifications = fields.Attr(\"mentions\", {\n        compute() {\n            return this.channel_notifications === false ? \"mentions\" : this.channel_notifications;\n        },\n    });\n    messageSound = fields.Attr(true, {\n        compute() {\n            return browser.localStorage.getItem(MESSAGE_SOUND) !== \"false\";\n        },\n        /** @this {import(\"models\").Settings} */\n        onUpdate() {\n            if (this.messageSound) {\n                browser.localStorage.removeItem(MESSAGE_SOUND);\n            } else {\n                browser.localStorage.setItem(MESSAGE_SOUND, \"false\");\n            }\n        },\n    });\n    useCallAutoFocus = fields.Attr(true, {\n        /** @this {import(\"models\").Settings} */\n        compute() {\n            return !browser.localStorage.getItem(\"mail_user_setting_disable_call_auto_focus\");\n        },\n        /** @this {import(\"models\").Settings} */\n        onUpdate() {\n            if (this.useCallAutoFocus) {\n                browser.localStorage.removeItem(\"mail_user_setting_disable_call_auto_focus\");\n                return;\n            }\n            browser.localStorage.setItem(\"mail_user_setting_disable_call_auto_focus\", \"true\");\n        },\n    });\n\n    // Voice settings\n    // DeviceId of the audio input selected by the user\n    audioInputDeviceId = \"\";\n    audioOutputDeviceId = \"\";\n    cameraInputDeviceId = \"\";\n    use_push_to_talk = false;\n    voice_active_duration = 200;\n    volumes = fields.Many(\"Volume\");\n    volumeSettingsTimeouts = new Map();\n    // Normalized [0, 1] volume at which the voice activation system must consider the user as \"talking\".\n    voiceActivationThreshold = 0.05;\n    // true if listening to keyboard input to register the push to talk key.\n    isRegisteringKey = false;\n    push_to_talk_key;\n\n    // Video settings\n    backgroundBlurAmount = 10;\n    edgeBlurAmount = 10;\n    showOnlyVideo = false;\n    useBlur = fields.Attr(false, {\n        compute() {\n            return browser.localStorage.getItem(\"mail_user_setting_use_blur\") === \"true\";\n        },\n        /** @this {import(\"models\").Settings} */\n        onUpdate() {\n            if (this.useBlur) {\n                browser.localStorage.setItem(\"mail_user_setting_use_blur\", \"true\");\n            } else {\n                browser.localStorage.removeItem(\"mail_user_setting_use_blur\");\n            }\n        },\n    });\n    blurPerformanceWarning = fields.Attr(false, {\n        compute() {\n            const rtc = this.store.rtc;\n            if (!rtc || !this.useBlur) {\n                return false;\n            }\n            return this.useBlur && rtc.state?.cameraTrack && !hasHardwareAcceleration();\n        },\n    });\n    cameraFacingMode = undefined;\n\n    logRtc = false;\n    /**\n     * @returns {Object} MediaTrackConstraints\n     */\n    get audioConstraints() {\n        const constraints = {\n            echoCancellation: true,\n            noiseSuppression: true,\n        };\n        if (this.audioInputDeviceId) {\n            constraints.deviceId = this.audioInputDeviceId;\n        }\n        return constraints;\n    }\n\n    get cameraConstraints() {\n        const constraints = {\n            width: 1280,\n        };\n        if (this.cameraFacingMode) {\n            constraints.facingMode = this.cameraFacingMode;\n        } else if (this.cameraInputDeviceId) {\n            constraints.deviceId = this.cameraInputDeviceId;\n        }\n        return constraints;\n    }\n\n    get NOTIFICATIONS() {\n        return [\n            {\n                label: \"all\",\n                name: _t(\"All Messages\"),\n            },\n            {\n                label: \"mentions\",\n                name: _t(\"Mentions Only\"),\n            },\n            {\n                label: \"no_notif\",\n                name: _t(\"Nothing\"),\n            },\n        ];\n    }\n\n    get MUTES() {\n        return [\n            {\n                label: \"15_mins\",\n                value: 15,\n                name: _t(\"For 15 minutes\"),\n            },\n            {\n                label: \"1_hour\",\n                value: 60,\n                name: _t(\"For 1 hour\"),\n            },\n            {\n                label: \"3_hours\",\n                value: 180,\n                name: _t(\"For 3 hours\"),\n            },\n            {\n                label: \"8_hours\",\n                value: 480,\n                name: _t(\"For 8 hours\"),\n            },\n            {\n                label: \"24_hours\",\n                value: 1440,\n                name: _t(\"For 24 hours\"),\n            },\n            {\n                label: \"forever\",\n                value: -1,\n                name: _t(\"Until I turn it back on\"),\n            },\n        ];\n    }\n\n    getMuteUntilText(dt) {\n        if (dt) {\n            return dt.year <= luxon.DateTime.now().year + 2\n                ? _t(`Until %s`, dt.toLocaleString(luxon.DateTime.DATETIME_MED))\n                : _t(\"Until I turn it back on\");\n        }\n        return undefined;\n    }\n\n    /**\n     * @param {string} custom_notifications\n     * @param {import(\"models\").Thread} thread\n     */\n    async setCustomNotifications(custom_notifications, thread = undefined) {\n        return rpc(\"/discuss/settings/custom_notifications\", {\n            custom_notifications:\n                !thread && custom_notifications === \"mentions\" ? false : custom_notifications,\n            channel_id: thread?.id,\n        });\n    }\n\n    /**\n     * @param {integer|false} minutes\n     * @param {import(\"models\").Thread} thread\n     */\n    async setMuteDuration(minutes, thread = undefined) {\n        return rpc(\"/discuss/settings/mute\", {\n            minutes,\n            channel_id: thread?.id,\n        });\n    }\n\n    /**\n     * @param {String} audioInputDeviceId\n     */\n    async setAudioInputDevice(audioInputDeviceId) {\n        this.audioInputDeviceId = audioInputDeviceId;\n        browser.localStorage.setItem(\"mail_user_setting_audio_input_device_id\", audioInputDeviceId);\n    }\n    /**\n     * @param {String} audioOutputDeviceId\n     */\n    async setAudioOutputDevice(audioOutputDeviceId) {\n        this.audioOutputDeviceId = audioOutputDeviceId;\n        browser.localStorage.setItem(\n            \"mail_user_setting_audio_output_device_id\",\n            audioOutputDeviceId\n        );\n    }\n    /**\n     * @param {String} cameraInputDeviceId\n     */\n    async setCameraInputDevice(cameraInputDeviceId) {\n        this.cameraFacingMode = undefined;\n        this.cameraInputDeviceId = cameraInputDeviceId;\n        browser.localStorage.setItem(\n            \"mail_user_setting_camera_input_device_id\",\n            cameraInputDeviceId\n        );\n    }\n    /**\n     * @param {string} value\n     */\n    setDelayValue(value) {\n        this.voice_active_duration = parseInt(value, 10);\n        this._saveSettings();\n    }\n    /**\n     * @param {event} ev\n     */\n    async setPushToTalkKey(ev) {\n        const nonElligibleKeys = new Set([\"Shift\", \"Control\", \"Alt\", \"Meta\"]);\n        let pushToTalkKey = `${ev.shiftKey || \"\"}.${ev.ctrlKey || ev.metaKey || \"\"}.${\n            ev.altKey || \"\"\n        }`;\n        if (!nonElligibleKeys.has(ev.key)) {\n            pushToTalkKey += `.${ev.key === \" \" ? \"Space\" : ev.key}`;\n        }\n        this.push_to_talk_key = pushToTalkKey;\n        this._saveSettings();\n    }\n    /**\n     * @param {Object} param0\n     * @param {number} [param0.partnerId]\n     * @param {number} [param0.guestId]\n     * @param {number} param0.volume\n     */\n    async saveVolumeSetting({ partnerId, guestId, volume }) {\n        if (!this.store.self_partner) {\n            return;\n        }\n        const key = `${partnerId}_${guestId}`;\n        if (this.volumeSettingsTimeouts.get(key)) {\n            browser.clearTimeout(this.volumeSettingsTimeouts.get(key));\n        }\n        this.volumeSettingsTimeouts.set(\n            key,\n            browser.setTimeout(\n                this._onSaveVolumeSettingTimeout.bind(this, { key, partnerId, guestId, volume }),\n                5000\n            )\n        );\n    }\n    /**\n     * @param {float} voiceActivationThreshold\n     */\n    setThresholdValue(voiceActivationThreshold) {\n        this.voiceActivationThreshold = voiceActivationThreshold;\n        this.saveVoiceThresholdDebounce();\n    }\n\n    // methods\n\n    buildKeySet({ shiftKey, ctrlKey, altKey, key }) {\n        const keys = new Set();\n        if (key) {\n            keys.add(key === \"Meta\" ? \"Alt\" : key);\n        }\n        if (shiftKey) {\n            keys.add(\"Shift\");\n        }\n        if (ctrlKey) {\n            keys.add(\"Control\");\n        }\n        if (altKey) {\n            keys.add(\"Alt\");\n        }\n        return keys;\n    }\n\n    /**\n     * @param {event} ev\n     * @param {Object} param1\n     */\n    isPushToTalkKey(ev) {\n        if (!this.use_push_to_talk || !this.push_to_talk_key) {\n            return false;\n        }\n        const [shiftKey, ctrlKey, altKey, key] = this.push_to_talk_key.split(\".\");\n        const settingsKeySet = this.buildKeySet({ shiftKey, ctrlKey, altKey, key });\n        const eventKeySet = this.buildKeySet({\n            shiftKey: ev.shiftKey,\n            ctrlKey: ev.ctrlKey,\n            altKey: ev.altKey,\n            key: ev.key,\n        });\n        if (ev.type === \"keydown\") {\n            return [...settingsKeySet].every((key) => eventKeySet.has(key));\n        }\n        return settingsKeySet.has(ev.key === \"Meta\" ? \"Alt\" : ev.key);\n    }\n    pushToTalkKeyFormat() {\n        if (!this.push_to_talk_key) {\n            return;\n        }\n        const [shiftKey, ctrlKey, altKey, key] = this.push_to_talk_key.split(\".\");\n        return {\n            shiftKey: !!shiftKey,\n            ctrlKey: !!ctrlKey,\n            altKey: !!altKey,\n            key: key || false,\n        };\n    }\n    setPushToTalk(value) {\n        this.use_push_to_talk = value;\n        this._saveSettings();\n    }\n    /**\n     * @private\n     */\n    _loadLocalSettings() {\n        const voiceActivationThresholdString = browser.localStorage.getItem(\n            \"mail_user_setting_voice_threshold\"\n        );\n        this.voiceActivationThreshold = voiceActivationThresholdString\n            ? parseFloat(voiceActivationThresholdString)\n            : this.voiceActivationThreshold;\n        this.audioInputDeviceId = browser.localStorage.getItem(\n            \"mail_user_setting_audio_input_device_id\"\n        );\n        this.audioOutputDeviceId = browser.localStorage.getItem(\n            \"mail_user_setting_audio_output_device_id\"\n        );\n        this.cameraInputDeviceId = browser.localStorage.getItem(\n            \"mail_user_setting_camera_input_device_id\"\n        );\n        this.showOnlyVideo =\n            browser.localStorage.getItem(\"mail_user_setting_show_only_video\") === \"true\";\n        const backgroundBlurAmount = browser.localStorage.getItem(\n            \"mail_user_setting_background_blur_amount\"\n        );\n        this.backgroundBlurAmount = backgroundBlurAmount ? parseInt(backgroundBlurAmount) : 10;\n        const edgeBlurAmount = browser.localStorage.getItem(\"mail_user_setting_edge_blur_amount\");\n        this.edgeBlurAmount = edgeBlurAmount ? parseInt(edgeBlurAmount) : 10;\n        this.useCallAutoFocus = !browser.localStorage.getItem(\n            \"mail_user_setting_disable_call_auto_focus\"\n        );\n    }\n    /**\n     * @private\n     */\n    async _onSaveGlobalSettingsTimeout() {\n        this.globalSettingsTimeout = undefined;\n        await this.store.env.services.orm.call(\n            \"res.users.settings\",\n            \"set_res_users_settings\",\n            [[this.id]],\n            {\n                new_settings: {\n                    push_to_talk_key: this.push_to_talk_key,\n                    use_push_to_talk: this.use_push_to_talk,\n                    voice_active_duration: this.voice_active_duration,\n                },\n            }\n        );\n    }\n    /**\n     * @param {Object} param0\n     * @param {String} param0.key\n     * @param {number} [param0.partnerId]\n     * @param {number} param0.volume\n     */\n    async _onSaveVolumeSettingTimeout({ key, partnerId, guestId, volume }) {\n        this.volumeSettingsTimeouts.delete(key);\n        await this.store.env.services.orm.call(\n            \"res.users.settings\",\n            \"set_volume_setting\",\n            [[this.id], partnerId, volume],\n            { guest_id: guestId }\n        );\n    }\n    onStorage(ev) {\n        if (ev.key === MESSAGE_SOUND) {\n            this.messageSound = ev.newValue !== \"false\";\n        }\n        if (ev.key === \"mail_user_setting_use_blur\") {\n            this.useBlur = ev.newValue === \"true\";\n        }\n    }\n    /**\n     * @private\n     */\n    async _saveSettings() {\n        if (!this.store.self_partner) {\n            return;\n        }\n        browser.clearTimeout(this.globalSettingsTimeout);\n        this.globalSettingsTimeout = browser.setTimeout(\n            () => this._onSaveGlobalSettingsTimeout(),\n            2000\n        );\n    }\n}\n\nSettings.register();\n", "import { browser } from \"@web/core/browser/browser\";\nimport { registry } from \"@web/core/registry\";\nimport { url } from \"@web/core/utils/urls\";\n\nexport class SoundEffects {\n    /**\n     * @param {import(\"@web/env\").OdooEnv} env\n     */\n    constructor(env) {\n        this.soundEffects = {\n            \"call-join\": { defaultVolume: 0.75, path: \"/mail/static/src/audio/call-join\" },\n            \"call-leave\": { defaultVolume: 0.75, path: \"/mail/static/src/audio/call-leave\" },\n            \"earphone-off\": { defaultVolume: 0.15, path: \"/mail/static/src/audio/earphone-off\" },\n            \"earphone-on\": { defaultVolume: 0.15, path: \"/mail/static/src/audio/earphone-on\" },\n            \"mic-off\": { defaultVolume: 0.2, path: \"/mail/static/src/audio/mic-off\" },\n            \"mic-on\": { defaultVolume: 0.2, path: \"/mail/static/src/audio/mic-on\" },\n            \"ptt-press\": { defaultVolume: 0.1, path: \"/mail/static/src/audio/ptt-press\" },\n            \"ptt-release\": { defaultVolume: 0.1, path: \"/mail/static/src/audio/ptt-release\" },\n            \"call-invitation\": {\n                defaultVolume: 0.5,\n                path: \"/mail/static/src/audio/call-invitation\",\n            },\n            \"new-message\": { defaultVolume: 1, path: \"/mail/static/src/audio/new-message\" },\n            \"screen-sharing\": {\n                defaultVolume: 0.75,\n                path: \"/mail/static/src/audio/screen-sharing\",\n            },\n            \"member-leave\": { defaultVolume: 0.5, path: \"/mail/static/src/audio/channel_01_out\" },\n        };\n    }\n\n    /**\n     * @param {String} param0 soundEffectName\n     * @param {Object} param1\n     * @param {boolean} [param1.loop] true if we want to make the audio loop, will only stop if stop() is called\n     * @param {float} [param1.volume] the volume percentage in decimal to play this sound.\n     *   If not provided, uses the default volume of this sound effect.\n     */\n    play(soundEffectName, { loop = false, volume } = {}) {\n        if (typeof browser.Audio === \"undefined\") {\n            return;\n        }\n        const soundEffect = this.soundEffects[soundEffectName];\n        if (!soundEffect) {\n            return;\n        }\n        if (!soundEffect.audio) {\n            const audio = new browser.Audio();\n            const ext = audio.canPlayType(\"audio/ogg; codecs=vorbis\") ? \".ogg\" : \".mp3\";\n            audio.src = url(soundEffect.path + ext);\n            soundEffect.audio = audio;\n        }\n        if (!soundEffect.audio.paused) {\n            soundEffect.audio.pause();\n        }\n        soundEffect.audio.currentTime = 0;\n        soundEffect.audio.loop = loop;\n        soundEffect.audio.volume = volume ?? soundEffect.defaultVolume ?? 1;\n        Promise.resolve(soundEffect.audio.play()).catch(() => {});\n    }\n    /**\n     * Resets the audio to the start of the track and pauses it.\n     * @param {String} [soundEffectName]\n     */\n    stop(soundEffectName) {\n        const soundEffect = this.soundEffects[soundEffectName];\n        if (soundEffect) {\n            if (soundEffect.audio) {\n                soundEffect.audio.pause();\n                soundEffect.audio.currentTime = 0;\n            }\n        } else {\n            for (const soundEffect of Object.values(this.soundEffects)) {\n                if (soundEffect.audio) {\n                    soundEffect.audio.pause();\n                    soundEffect.audio.currentTime = 0;\n                }\n            }\n        }\n    }\n}\n\nexport const soundEffects = {\n    /**\n     * @param {import(\"@web/env\").OdooEnv} env\n     */\n    start(env) {\n        return new SoundEffects(env);\n    },\n};\n\nregistry.category(\"services\").add(\"mail.sound_effects\", soundEffects);\n", "import { Store as BaseStore, fields, makeStore, storeInsertFns } from \"@mail/core/common/record\";\nimport { threadCompareRegistry } from \"@mail/core/common/thread_compare\";\nimport { cleanTerm, generateEmojisOnHtml, prettifyMessageText } from \"@mail/utils/common/format\";\nimport { compareDatetime } from \"@mail/utils/common/misc\";\n\nimport { reactive } from \"@odoo/owl\";\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { registry } from \"@web/core/registry\";\nimport { user } from \"@web/core/user\";\nimport { Deferred, Mutex } from \"@web/core/utils/concurrency\";\nimport { renderToElement } from \"@web/core/utils/render\";\nimport { debounce } from \"@web/core/utils/timing\";\nimport { session } from \"@web/session\";\nimport { browser } from \"@web/core/browser/browser\";\nimport { loader } from \"@web/core/emoji_picker/emoji_picker\";\nimport { patch } from \"@web/core/utils/patch\";\nimport { isMobileOS } from \"@web/core/browser/feature_detection\";\nimport { getOrigin } from \"@web/core/utils/urls\";\nimport { cookie } from \"@web/core/browser/cookie\";\n\n/**\n * @typedef {{isSpecial: boolean, channel_types: string[], label: string, displayName: string, description: string}} SpecialMention\n */\n\nlet prevLastMessageId = null;\nlet temporaryIdOffset = 0.01;\n\nexport const pyToJsModels = {\n    \"discuss.channel\": \"Thread\",\n    \"mail.thread\": \"Thread\",\n};\n\nexport const addFieldsByPyModel = {\n    \"discuss.channel\": { model: \"discuss.channel\" },\n};\n\npatch(storeInsertFns, {\n    makeContext(store) {\n        if (!(store instanceof Store)) {\n            return super.makeContext(...arguments);\n        }\n        return { pyModels: Object.values(pyToJsModels) };\n    },\n    getActualModelName(store, ctx, pyOrJsModelName) {\n        if (!(store instanceof Store)) {\n            return super.getActualModelName(...arguments);\n        }\n        if (ctx.pyModels.includes(pyOrJsModelName)) {\n            console.warn(\n                `store.insert() should receive the python model name instead of \u201c${pyOrJsModelName}\u201d.`\n            );\n        }\n        return pyToJsModels[pyOrJsModelName] || pyOrJsModelName;\n    },\n    getExtraFieldsFromModel(store, pyOrJsModelName) {\n        if (!(store instanceof Store)) {\n            return super.getExtraFieldsFromModel(...arguments);\n        }\n        return addFieldsByPyModel[pyOrJsModelName];\n    },\n});\n\nexport class Store extends BaseStore {\n    static FETCH_DATA_DEBOUNCE_DELAY = 1;\n    static OTHER_LONG_TYPING = 60000;\n    static IM_STATUS_DEBOUNCE_DELAY = 1000;\n\n    FETCH_LIMIT = 30;\n    DEFAULT_AVATAR = \"/mail/static/src/img/smiley/avatar.jpg\";\n    isReady = new Deferred();\n    /** This is the current logged partner / guest */\n    self_partner = fields.One(\"res.partner\");\n    self_guest = fields.One(\"mail.guest\");\n    get self() {\n        return this.self_partner || this.self_guest;\n    }\n    allChannels = fields.Many(\"Thread\", {\n        inverse: \"storeAsAllChannels\",\n        onUpdate() {\n            const busService = this.store.env.services.bus_service;\n            if (!busService.isActive && this.allChannels.some((t) => !t.isTransient)) {\n                busService.start();\n            }\n        },\n    });\n    /**\n     * Indicates whether the current user is using the application through the\n     * public page.\n     */\n    inPublicPage = false;\n    odoobot = fields.One(\"res.partner\");\n    useMobileView = fields.Attr(undefined, {\n        compute() {\n            return this.store.env.services.ui.isSmall || isMobileOS();\n        },\n    });\n    users = {};\n    /** @type {number} */\n    internalUserGroupId;\n    mt_comment = fields.One(\"mail.message.subtype\");\n    mt_note = fields.One(\"mail.message.subtype\");\n    /** @type {boolean} */\n    hasMessageTranslationFeature;\n    hasLinkPreviewFeature = true;\n    // messaging menu\n    menu = { counter: 0 };\n    chatHub = fields.One(\"ChatHub\", { compute: () => ({}) });\n    failures = fields.Many(\"Failure\", {\n        /**\n         * @param {import(\"models\").Failure} f1\n         * @param {import(\"models\").Failure} f2\n         */\n        sort: (f1, f2) => f2.lastMessage?.id - f1.lastMessage?.id,\n    });\n    settings = fields.One(\"Settings\");\n    emojiLoader = loader;\n\n    /** @type {[[string, any, import(\"models\").DataResponse]]} */\n    fetchParams = [];\n    fetchReadonly = true;\n    fetchSilent = true;\n\n    cannedReponses = this.makeCachedFetchData(\"mail.canned.response\");\n\n    specialMentions = [\n        {\n            isSpecial: true,\n            label: \"everyone\",\n            channel_types: [\"channel\", \"group\"],\n            displayName: \"Everyone\",\n            description: _t(\"Notify everyone\"),\n        },\n    ];\n\n    isNotificationPermissionDismissed = fields.Attr(false, {\n        compute() {\n            return (\n                browser.localStorage.getItem(\"mail.user_setting.push_notification_dismissed\") ===\n                \"true\"\n            );\n        },\n        /** @this {import(\"models\").DiscussApp} */\n        onUpdate() {\n            if (this.isNotificationPermissionDismissed) {\n                browser.localStorage.setItem(\n                    \"mail.user_setting.push_notification_dismissed\",\n                    \"true\"\n                );\n            } else {\n                browser.localStorage.removeItem(\"mail.user_setting.push_notification_dismissed\");\n            }\n        },\n    });\n\n    messagePostMutex = new Mutex();\n\n    menuThreads = fields.Many(\"Thread\", {\n        /** @this {import(\"models\").Store} */\n        compute() {\n            /** @type {import(\"models\").Thread[]} */\n            const searchTerm = cleanTerm(this.discuss.searchTerm);\n            let threads = Object.values(this.Thread.records).filter(\n                (thread) =>\n                    (thread.displayToSelf ||\n                        (thread.needactionMessages.length > 0 && thread.model !== \"mail.box\")) &&\n                    cleanTerm(thread.displayName).includes(searchTerm)\n            );\n            const tab = this.discuss.activeTab;\n            if (tab === \"inbox\") {\n                threads = threads.filter(({ channel_type }) =>\n                    this.tabToThreadType(\"mailbox\").includes(channel_type)\n                );\n            } else if (tab === \"starred\") {\n                threads = [this.starred];\n            } else if (tab !== \"notification\") {\n                threads = threads.filter(({ channel_type }) =>\n                    this.tabToThreadType(tab).includes(channel_type)\n                );\n            }\n            return threads;\n        },\n        /**\n         * @this {import(\"models\").Store}\n         * @param {import(\"models\").Thread} thread1\n         * @param {import(\"models\").Thread} thread2\n         */\n        sort(thread1, thread2) {\n            const compareFunctions = threadCompareRegistry.getAll();\n            for (const fn of compareFunctions) {\n                const result = fn(thread1, thread2);\n                if (result !== undefined) {\n                    return result;\n                }\n            }\n            return thread2.localId > thread1.localId ? 1 : -1;\n        },\n    });\n\n    shouldSimulateDarkTheme(ctx) {\n        return (\n            (ctx?.env?.inDiscussCallView ||\n                ctx?.env?.inCallInvitation ||\n                ctx?.env.isDiscussPipBanner ||\n                ctx?.env?.inWelcomePage) &&\n            this.isOdooWhiteTheme &&\n            !ctx?.env.inMeetingSideActions &&\n            !ctx?.env.inDiscussActionPanel\n        );\n    }\n\n    discussDropdownMenuClass(ctx) {\n        const res = [\"o-discuss-dropdownMenu\", \"d-flex\", \"flex-column\", \"border-secondary\"];\n        if (this.shouldSimulateDarkTheme(ctx)) {\n            res.push(\"o-simulateDarkTheme\");\n        }\n        return res.join(\" \");\n    }\n\n    standaloneInboxMessages = fields.Many(\"mail.message\", {\n        compute() {\n            const messages = this.store.inbox.messages.filter((m) => !m.thread);\n            return messages.sort(\n                (m1, m2) => compareDatetime(m2.datetime, m1.datetime) || m2.id - m1.id\n            );\n        },\n    });\n\n    /**\n     * @param {Object} params post message data\n     * @param {import(\"models\").Message} tmpMessage the associated temporary message\n     */\n    async doMessagePost(params, tmpMessage) {\n        return this.messagePostMutex.exec(async () => {\n            let res;\n            try {\n                res = await rpc(\"/mail/message/post\", params, { silent: true });\n            } catch (err) {\n                if (!tmpMessage) {\n                    throw err;\n                }\n                tmpMessage.postFailRedo = () => {\n                    tmpMessage.postFailRedo = undefined;\n                    tmpMessage.thread.messages.delete(tmpMessage);\n                    tmpMessage.thread.messages.add(tmpMessage);\n                    this.doMessagePost(params, tmpMessage);\n                };\n            }\n            return res;\n        });\n    }\n\n    /**\n     * @param {string} name\n     * @param {any} params\n     * @param {Object} [options={}]\n     * @param {boolean} [options.requestData=false] when set to true, the return promise will\n     *  resolve only when the requested data are returned (the data might come later, from another\n     *  RPC or a bus notification for example). When set to false (the default), the return promise\n     *  will resolve as soon as the RPC is done. This is intended to be true only for requests that\n     *  will be resolved server side with `resolve_data_request`.\n     * @param {boolean} [options.readonly=true] when set to false, the server will open a read-write\n     *  cursor to process this request which is necessary if the request is expected to change data.\n     * @param {boolean} [options.silent=true]\n     * @returns {Deferred}\n     */\n    async fetchStoreData(\n        name,\n        params,\n        { requestData = false, readonly = true, silent = true } = {}\n    ) {\n        const dataRequest = this.DataResponse.createRequest();\n        dataRequest._autoResolve = !requestData;\n        this.fetchParams.push([name, params, dataRequest]);\n        this.fetchReadonly = this.fetchReadonly && readonly;\n        this.fetchSilent = this.fetchSilent && silent;\n        this._fetchStoreDataDebounced();\n        return dataRequest._resultDef;\n    }\n\n    /** Import data received from init_messaging */\n    async initialize() {\n        await this.fetchStoreData(\"init_messaging\");\n        this.isReady.resolve();\n    }\n\n    /**\n     * Create a cacheable version of the `fetchStoreData` method. The result of the\n     * request is cached once acquired. In case of failure, the deferred is\n     * rejected and the cache is reset allowing to retry the request when\n     * calling the function again.\n     *\n     * @param {string} name\n     * @param {*} params Parameters to pass to the `fetchStoreData` method.\n     * @returns {{\n     *      fetch: () => ReturnType<Store[\"fetchStoreData\"]>,\n     *      status: \"not_fetched\"|\"fetching\"|\"fetched\"\n     * }}\n     */\n    makeCachedFetchData(name, params) {\n        let def = null;\n        const r = reactive({\n            status: \"not_fetched\",\n            fetch: () => {\n                if ([\"fetching\", \"fetched\"].includes(r.status)) {\n                    return def;\n                }\n                r.status = \"fetching\";\n                def = new Deferred();\n                this.fetchStoreData(name, params).then(\n                    (result) => {\n                        r.status = \"fetched\";\n                        def.resolve(result);\n                    },\n                    (error) => {\n                        r.status = \"not_fetched\";\n                        def.reject(error);\n                    }\n                );\n                return def;\n            },\n        });\n        return r;\n    }\n\n    _fetchStoreDataDebounced() {\n        const fetchParams = this.fetchParams;\n        this._fetchStoreDataRpc(\n            fetchParams.map(([name, params, dataRequest]) => {\n                if (dataRequest._autoResolve) {\n                    /**\n                     * Auto-resolve requests don't need to pass any data request id as the server is\n                     * expected to not return anything specific for them. It would work if id are\n                     * given but it's more bytes on the network and more noise in the logs/tests.\n                     */\n                    if (params !== undefined) {\n                        return [name, params];\n                    } else {\n                        // In a similar reasoning, also remove empty params.\n                        return name;\n                    }\n                } else {\n                    return [name, params, dataRequest.id];\n                }\n            })\n        ).then(\n            (data) => {\n                this.insert(data);\n                for (const [, , dataRequest] of fetchParams) {\n                    if (dataRequest._autoResolve) {\n                        dataRequest._resolve = true;\n                    }\n                }\n            },\n            (error) => {\n                for (const [, , dataRequest] of fetchParams) {\n                    dataRequest._resultDef.reject(error);\n                }\n            }\n        );\n        this.fetchParams = [];\n        this.fetchReadonly = true;\n        this.fetchSilent = true;\n    }\n\n    _fetchStoreDataRpc(fetchParams) {\n        return rpc(\n            this.fetchReadonly ? \"/mail/data\" : \"/mail/action\",\n            { fetch_params: fetchParams, context: user.context },\n            { silent: this.fetchSilent }\n        );\n    }\n\n    async startMeeting() {\n        const thread = await this.createGroupChat({\n            default_display_mode: \"video_full_screen\",\n            partners_to: [this.self.id],\n        });\n        await this.store.chatHub.initPromise;\n        this.ChatWindow.get(thread)?.update({ autofocus: 0 });\n        await this.env.services[\"discuss.rtc\"].toggleCall(thread, { camera: true });\n        if (this.rtc.selfSession) {\n            this.rtc.enterFullscreen({ autoOpenAction: \"invite-people\" });\n        }\n    }\n\n    /**\n     * @param {'chat' | 'group'} tab\n     * @returns Thread types matching the given tab.\n     */\n    tabToThreadType(tab) {\n        return tab === \"chat\" ? [\"chat\", \"group\"] : [tab];\n    }\n\n    handleClickOnLink(ev, thread) {\n        const link = ev.target.closest(\"a\");\n        if (!link) {\n            return;\n        }\n        const model = link.dataset.oeModel;\n        const id = Number(link.dataset.oeId);\n        if (link.classList.contains(\"o_channel_redirect\") && model && id) {\n            ev.preventDefault();\n            this.Thread.getOrFetch({ model, id }).then((thread) => {\n                if (thread) {\n                    thread.open({ focus: true });\n                } else {\n                    this.env.services.notification.add(_t(\"This thread is no longer available.\"), {\n                        type: \"danger\",\n                    });\n                }\n            });\n            return true;\n        } else if (link.classList.contains(\"o_mail_redirect\") && id) {\n            ev.preventDefault();\n            this.onClickPartnerMention(ev, id);\n            return true;\n        } else if (link.classList.contains(\"o_message_redirect\")) {\n            const message = this[\"mail.message\"].get(id);\n            const targetThread = message?.thread;\n            const showAccessError = () =>\n                this.env.services.notification.add(_t(\"This conversation isn\u2019t available.\"), {\n                    type: \"danger\",\n                });\n            if (targetThread) {\n                targetThread.checkReadAccess().then((hasAccess) => {\n                    if (hasAccess) {\n                        targetThread.highlightMessage = message;\n                        let isOpen = targetThread.eq(thread);\n                        if (!isOpen) {\n                            isOpen = targetThread.open({ focus: true, swapOpened: false });\n                        }\n                        if (!isOpen) {\n                            window.open(link.href);\n                        }\n                    } else {\n                        if (this.self_partner) {\n                            showAccessError();\n                        } else {\n                            window.open(link.href);\n                        }\n                    }\n                });\n                ev.preventDefault();\n                return true;\n            } else if (link.getAttribute(\"href\")?.startsWith(getOrigin())) {\n                showAccessError();\n                ev.preventDefault();\n                return true;\n            }\n        }\n        return false;\n    }\n\n    setup() {\n        super.setup();\n        this._fetchStoreDataDebounced = debounce(\n            this._fetchStoreDataDebounced,\n            Store.FETCH_DATA_DEBOUNCE_DELAY\n        );\n    }\n\n    /** Provides an override point for when the store service has started. */\n    onStarted() {\n        this.isOdooWhiteTheme = cookie.get(\"color_scheme\") !== \"dark\" || this.inPublicPage;\n        navigator.serviceWorker?.addEventListener(\"message\", ({ data = {} }) => {\n            const { type, payload } = data;\n            if (type === \"notification-display-request\") {\n                const { correlationId, model, res_id } = payload;\n                const thread = this.Thread.get({ model, id: res_id });\n                let isTabFocused;\n                try {\n                    isTabFocused = parent.document.hasFocus();\n                } catch {\n                    // assumes tab not focused: parent.document from iframe triggers CORS error\n                }\n                // Prevent duplicate inbox push notifications since they're already handled by\n                // `mail.message/inbox` bus notifications, and the `modelsHandleByPush` heuristic\n                // in `out_of_focus_service.js` isn't reliable enough to detect these cases.\n                const isInbox =\n                    this.store.self.main_user_id?.notification_type === \"inbox\" &&\n                    model !== \"discuss.channel\";\n                if ((isTabFocused && thread?.isDisplayed) || isInbox) {\n                    navigator.serviceWorker.controller?.postMessage({\n                        type: \"notification-display-response\",\n                        payload: { correlationId },\n                    });\n                }\n            }\n            if (type === \"notification-displayed\") {\n                this.onPushNotificationDisplayed(payload);\n            }\n        });\n    }\n\n    onPushNotificationDisplayed(payload) {\n        if ([\"mail.thread\", \"discuss.channel\"].includes(payload.model)) {\n            this.env.services[\"mail.out_of_focus\"]._playSound();\n        }\n    }\n\n    /**\n     * Search and fetch for a partner with a given user or partner id.\n     * @param {Object} param0\n     * @param {number} param0.userId\n     * @param {number} param0.partnerId\n     * @returns {Promise<import(\"models\").Thread | undefined>}\n     */\n    async getChat({ userId, partnerId }) {\n        const partner = await this.getPartner({ userId, partnerId });\n        if (!partner) {\n            return;\n        }\n        let chat = partner.searchChat();\n        if (!chat?.self_member_id?.is_pinned) {\n            chat = await this.joinChat(partner.id);\n        }\n        if (!chat) {\n            this.env.services.notification.add(\n                _t(\"An unexpected error occurred during the creation of the chat.\"),\n                { type: \"warning\" }\n            );\n            return;\n        }\n        return chat;\n    }\n\n    fillPartnersMentionToken(postData) {\n        postData.partner_ids_mention_token ||= {};\n        for (const pid of postData.partner_ids) {\n            const partner = this[\"res.partner\"].get(pid);\n            if (partner?.mention_token) {\n                postData.partner_ids_mention_token[pid] = partner.mention_token;\n            }\n        }\n    }\n\n    /** @returns {number} */\n    getLastMessageId() {\n        return Object.values(this[\"mail.message\"].records).reduce(\n            (lastMessageId, message) => Math.max(lastMessageId, message.id),\n            0\n        );\n    }\n\n    handleValidChannelMention(channelLinks) {\n        for (const linkEl of channelLinks.filter(\n            (el) => !el.querySelector(\".fa-comments-o, .fa-hashtag\")\n        )) {\n            const text = linkEl.textContent.substring(1); // remove '#' prefix\n            const icon = linkEl.classList.contains(\"o_channel_redirect_asThread\")\n                ? \"fa fa-comments-o\"\n                : \"fa fa-hashtag\";\n            const iconEl = renderToElement(\"mail.Message.mentionedChannelIcon\", { icon });\n            linkEl.replaceChildren(iconEl);\n            linkEl.insertAdjacentText(\"beforeend\", ` ${text}`);\n        }\n    }\n\n    getMentionsFromText(\n        body,\n        { mentionedChannels = [], mentionedPartners = [], mentionedRoles = [], thread } = {}\n    ) {\n        const validMentions = {};\n        validMentions.threads = mentionedChannels.filter((thread) => {\n            if (thread.parent_channel_id) {\n                return body.includes(\n                    `#${thread.parent_channel_id.displayName} > ${thread.displayName}`\n                );\n            }\n            return body.includes(`#${thread.displayName}`);\n        });\n        validMentions.partners = mentionedPartners.filter((partner) =>\n            body.includes(`@${thread?.getPersonaName(partner) ?? partner.name}`)\n        );\n        validMentions.roles = mentionedRoles.filter((role) => body.includes(`@${role.name}`));\n        validMentions.specialMentions = this.specialMentions\n            .filter((special) => body.includes(`@${special.label}`))\n            .map((special) => special.label);\n        return validMentions;\n    }\n\n    /**\n     * Get the parameters to pass to the message post route.\n     */\n    async getMessagePostParams({ body, postData, thread }) {\n        const {\n            attachments,\n            cannedResponseIds,\n            emailAddSignature,\n            isNote,\n            mentionedChannels,\n            mentionedPartners,\n            mentionedRoles,\n        } = postData;\n        const subtype = isNote ? \"mail.mt_note\" : \"mail.mt_comment\";\n        const validMentions = this.getMentionsFromText(body, {\n            mentionedChannels,\n            mentionedPartners,\n            mentionedRoles,\n            thread,\n        });\n        const partner_ids = validMentions?.partners.map((partner) => partner.id) ?? [];\n        const role_ids = validMentions?.roles.map((role) => role.id) ?? [];\n        const recipientEmails = [];\n        if (!isNote) {\n            const allRecipients = [...thread.suggestedRecipients, ...thread.additionalRecipients];\n            const recipientIds = allRecipients\n                .filter((recipient) => recipient.persona)\n                .map((recipient) => recipient.persona.id);\n            allRecipients\n                .filter((recipient) => !recipient.persona)\n                .forEach((recipient) => {\n                    recipientEmails.push(recipient.email);\n                });\n            partner_ids.push(...recipientIds);\n        }\n        postData = {\n            body: await generateEmojisOnHtml(body),\n            email_add_signature: emailAddSignature,\n            message_type: \"comment\",\n            subtype_xmlid: subtype,\n        };\n        if (attachments.length) {\n            postData.attachment_ids = attachments.map(({ id }) => id);\n        }\n        if (partner_ids.length) {\n            Object.assign(postData, { partner_ids });\n            this.fillPartnersMentionToken(postData);\n        }\n        if (role_ids.length) {\n            Object.assign(postData, { role_ids });\n        }\n        if (thread.model === \"discuss.channel\" && validMentions?.specialMentions.length) {\n            postData.special_mentions = validMentions.specialMentions;\n        }\n        if (attachments.length) {\n            postData.attachment_tokens = attachments.map(\n                (attachment) => attachment.ownership_token\n            );\n        }\n        if (recipientEmails.length) {\n            postData.partner_emails = recipientEmails;\n        }\n        const params = {\n            // Changed in 18.2+: finally get rid of autofollow, following should be done manually\n            post_data: postData,\n            thread_id: thread.id,\n            thread_model: thread.model,\n        };\n        if (cannedResponseIds?.length) {\n            params.canned_response_ids = cannedResponseIds;\n        }\n        return params;\n    }\n\n    getNextTemporaryId() {\n        const lastMessageId = this.getLastMessageId();\n        if (prevLastMessageId === lastMessageId) {\n            temporaryIdOffset += 0.01;\n        } else {\n            prevLastMessageId = lastMessageId;\n            temporaryIdOffset = 0.01;\n        }\n        return lastMessageId + temporaryIdOffset;\n    }\n\n    /**\n     * Search and fetch for a partner with a given user or partner id.\n     * @param {Object} param0\n     * @param {number} param0.userId\n     * @param {number} param0.partnerId\n     * @returns {Promise<import(\"models\").Persona> | undefined}\n     */\n    async getPartner({ userId, partnerId }) {\n        if (userId) {\n            let user = this.users[userId];\n            if (!user) {\n                this.users[userId] = { id: userId };\n                user = this.users[userId];\n            }\n            if (!user.partner_id) {\n                const [userData] = await this.env.services.orm.silent.read(\n                    \"res.users\",\n                    [user.id],\n                    [\"partner_id\"],\n                    { context: { active_test: false } }\n                );\n                if (userData) {\n                    user.partner_id = userData.partner_id[0];\n                }\n            }\n            if (!user.partner_id) {\n                this.env.services.notification.add(_t(\"You can only chat with existing users.\"), {\n                    type: \"warning\",\n                });\n                return;\n            }\n            partnerId = user.partner_id;\n        }\n        if (partnerId) {\n            const partner = this[\"res.partner\"].insert({ id: partnerId });\n            if (!partner.main_user_id) {\n                const [userId] = await this.env.services.orm.silent.search(\n                    \"res.users\",\n                    [[\"partner_id\", \"=\", partnerId]],\n                    { context: { active_test: false } }\n                );\n                if (!userId) {\n                    this.env.services.notification.add(\n                        _t(\"You can only chat with partners that have a dedicated user.\"),\n                        { type: \"info\" }\n                    );\n                    return;\n                }\n                if (!partner.main_user_id) {\n                    partner.main_user_id = userId;\n                }\n            }\n            return partner;\n        }\n    }\n\n    async joinChat(id, forceOpen = false) {\n        const { channel } = await this.fetchStoreData(\n            \"/discuss/get_or_create_chat\",\n            { partners_to: [id] },\n            { readonly: false, requestData: true }\n        );\n        if (forceOpen) {\n            await channel.open({ focus: true });\n        }\n        return channel;\n    }\n\n    async openChat(person) {\n        const chat = await this.getChat(person);\n        chat?.open({ focus: true });\n    }\n\n    openDocument({ id, model }) {\n        this.env.services.action.doAction({\n            type: \"ir.actions.act_window\",\n            res_model: model,\n            views: [[false, \"form\"]],\n            res_id: id,\n        });\n    }\n\n    /**\n     * @param {MouseEvent} ev - Click event triggering the popover.\n     * @param {number} id - Partner Id of mentioned partner.\n     */\n    onClickPartnerMention(ev, id) {\n        this.openChat({ partnerId: id });\n    }\n\n    /**\n     * @param {string} searchTerm\n     * @param {Thread} thread\n     * @param {number} before\n     * @param {true|false|undefined} is_notification\n     */\n    async searchMessagesInThread(searchTerm, thread, before, is_notification) {\n        const { count, data, messages } = await rpc(thread.getFetchRoute(), {\n            ...thread.getFetchParams(),\n            fetch_params: {\n                is_notification,\n                search_term: await prettifyMessageText(searchTerm), // formatted like message_post\n                before,\n            },\n        });\n        this.insert(data);\n        return {\n            count,\n            loadMore: messages.length === this.FETCH_LIMIT,\n            messages: this[\"mail.message\"].insert(messages),\n        };\n    }\n}\nStore.register();\n\nexport const storeService = {\n    dependencies: [\"bus_service\", \"im_status\", \"ui\", \"popover\"],\n    /**\n     * @param {import(\"@web/env\").OdooEnv} env\n     * @param {import(\"services\").ServiceFactories} services\n     * @returns {import(\"models\").Store}\n     */\n    start(env, services) {\n        const store = makeStore(env);\n        store.insert(session.storeData);\n        /**\n         * Add defaults for `self` and `settings` because in livechat there could be no user and no\n         * guest yet (both undefined at init), but some parts of the code that loosely depend on\n         * these values will still be executed immediately. Providing a dummy default is enough to\n         * avoid crashes, the actual values being filled at livechat init when they are necessary.\n         */\n        store.self_guest ??= { id: -1 };\n        store.settings ??= {};\n        store.initialize();\n        store.onStarted();\n        return store;\n    },\n};\n\nregistry.category(\"services\").add(\"mail.store\", storeService);\n", "import { isContentEditable, isTextNode } from \"@html_editor/utils/dom_info\";\nimport { rightPos } from \"@html_editor/utils/position\";\nimport {\n    generatePartnerMentionElement,\n    generateRoleMentionElement,\n    generateSpecialMentionElement,\n    generateThreadMentionElement,\n} from \"@mail/utils/common/format\";\nimport { status, useComponent, useEffect, useState } from \"@odoo/owl\";\nimport { ConnectionAbortedError } from \"@web/core/network/rpc\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { useDebounced } from \"@web/core/utils/timing\";\nimport { createTextNode } from \"@web/core/utils/xml\";\n\nexport const DELAY_FETCH = 250;\n\nexport class UseSuggestion {\n    constructor(comp) {\n        this.comp = comp;\n        this.fetchSuggestions = useDebounced(this.fetchSuggestions.bind(this), DELAY_FETCH);\n        useEffect(\n            () => {\n                this.update();\n                if (this.search.position === undefined || !this.search.delimiter) {\n                    return; // nothing else to fetch\n                }\n                if (!this.composer.store.self_partner) {\n                    return; // guests cannot access fetch suggestion method\n                }\n                if (\n                    this.lastFetchedSearch?.count === 0 &&\n                    (!this.search.delimiter || this.isSearchMoreSpecificThanLastFetch)\n                ) {\n                    return; // no need to fetch since this is more specific than last and last had no result\n                }\n                this.fetchSuggestions();\n            },\n            () => [this.search.delimiter, this.search.position, this.search.term]\n        );\n        useEffect(\n            () => {\n                this.detect();\n            },\n            () => [\n                this.composer.selection.start,\n                this.composer.selection.end,\n                this.composer.composerText,\n                this.composer.composerHtml,\n            ]\n        );\n    }\n    /** @type {import(\"@mail/core/common/composer\").Composer} */\n    comp;\n    get composer() {\n        return this.comp.props.composer;\n    }\n    suggestionService = useService(\"mail.suggestion\");\n    state = useState({\n        count: 0,\n        items: undefined,\n        isFetching: false,\n    });\n    search = {\n        delimiter: undefined,\n        position: undefined,\n        term: \"\",\n    };\n    lastFetchedSearch;\n    get isSearchMoreSpecificThanLastFetch() {\n        return (\n            this.lastFetchedSearch.delimiter === this.search.delimiter &&\n            this.search.term.startsWith(this.lastFetchedSearch.term) &&\n            this.lastFetchedSearch.position >= this.search.position\n        );\n    }\n    clearRawMentions() {\n        this.composer.mentionedChannels.length = 0;\n        this.composer.mentionedPartners.length = 0;\n        this.composer.mentionedRoles.length = 0;\n    }\n    clearCannedResponses() {\n        this.composer.cannedResponses = [];\n    }\n    clearSearch() {\n        Object.assign(this.search, {\n            delimiter: undefined,\n            position: undefined,\n            term: \"\",\n        });\n        this.state.items = undefined;\n    }\n    detect() {\n        let start = 0;\n        let end = 0;\n        let text = \"\";\n        if (this.comp.composerService.htmlEnabled) {\n            const selection = this.comp.editor.shared.selection.getEditableSelection();\n            if (\n                !isTextNode(selection.startContainer) ||\n                !isContentEditable(selection.startContainer) ||\n                !selection.isCollapsed\n            ) {\n                this.clearSearch();\n                return;\n            }\n            start = selection.startOffset;\n            end = selection.endOffset;\n            text = selection.anchorNode.textContent;\n        } else {\n            start = this.composer.selection.start;\n            end = this.composer.selection.end;\n            text = this.composer.composerText;\n        }\n        if (start !== end) {\n            // avoid interfering with multi-char selection\n            this.clearSearch();\n            return;\n        }\n        const candidatePositions = [];\n        // consider the chars before the current cursor position\n        let numberOfSpaces = 0;\n        for (let index = start - 1; index >= 0; --index) {\n            if (/\\s/.test(text[index])) {\n                numberOfSpaces++;\n                if (numberOfSpaces === 2) {\n                    // The consideration stops after the second space since\n                    // a majority of partners have a two-word name. This\n                    // removes the need to check for mentions following a\n                    // delimiter used earlier in the content.\n                    break;\n                }\n            }\n            candidatePositions.push(index);\n        }\n        // keep the current delimiter if it is still valid\n        if (this.search.position !== undefined && this.search.position < start) {\n            candidatePositions.push(this.search.position);\n        }\n        const supportedDelimiters = this.suggestionService.getSupportedDelimiters(\n            this.thread,\n            this.comp.env\n        );\n        for (const candidatePosition of candidatePositions) {\n            if (candidatePosition < 0 || candidatePosition >= text.length) {\n                continue;\n            }\n\n            const findAppropriateDelimiter = () => {\n                let goodCandidate;\n                for (const [delimiter, allowedPosition, minCharCountAfter] of supportedDelimiters) {\n                    if (\n                        text.substring(candidatePosition).startsWith(delimiter) && // delimiter is used\n                        (allowedPosition === undefined || allowedPosition === candidatePosition) && // delimiter is allowed position\n                        (minCharCountAfter === undefined ||\n                            start - candidatePosition - delimiter.length + 1 > minCharCountAfter) && // delimiter is allowed (enough custom char typed after)\n                        (!goodCandidate || delimiter.length > goodCandidate) // delimiter is more specific\n                    ) {\n                        goodCandidate = delimiter;\n                    }\n                }\n                return goodCandidate;\n            };\n\n            const candidateDelimiter = findAppropriateDelimiter();\n            if (!candidateDelimiter) {\n                continue;\n            }\n            const charBeforeCandidate = text[candidatePosition - 1];\n            if (charBeforeCandidate && !/\\s/.test(charBeforeCandidate)) {\n                continue;\n            }\n            Object.assign(this.search, {\n                delimiter: candidateDelimiter,\n                position: candidatePosition,\n                term: text.substring(candidatePosition + candidateDelimiter.length, start),\n            });\n            this.state.count++;\n            return;\n        }\n        this.clearSearch();\n    }\n    get thread() {\n        return this.composer.thread || this.composer.message?.thread;\n    }\n    insert(option) {\n        let position = this.search.position + 1;\n        if (\n            [\":\", \"::\"].includes(this.search.delimiter) ||\n            (this.comp.composerService.htmlEnabled && this.search.delimiter !== \"/\")\n        ) {\n            position = this.search.position;\n        }\n        if (this.comp.composerService.htmlEnabled) {\n            const { startContainer, endContainer, endOffset } =\n                this.comp.editor.shared.selection.getEditableSelection();\n            this.comp.editor.shared.selection.setSelection({\n                anchorNode: startContainer,\n                anchorOffset: position,\n                focusNode: endContainer,\n                focusOffset: endOffset,\n            });\n        }\n        if (option.partner) {\n            this.composer.mentionedPartners.add({ id: option.partner.id });\n        } else if (option.role) {\n            this.composer.mentionedRoles.add(option.role);\n        } else if (option.thread) {\n            this.composer.mentionedChannels.add({ model: \"discuss.channel\", id: option.thread.id });\n        } else if (option.cannedResponse) {\n            this.composer.cannedResponses.push(option.cannedResponse);\n        }\n        if (this.comp.composerService.htmlEnabled) {\n            let inlineElement;\n            if (option.partner) {\n                inlineElement = generatePartnerMentionElement(option.partner, this.thread);\n            } else if (option.isSpecial) {\n                inlineElement = generateSpecialMentionElement(option.label);\n            } else if (option.role) {\n                inlineElement = generateRoleMentionElement(option.role);\n            } else if (option.thread) {\n                inlineElement = generateThreadMentionElement(option.thread);\n            } else {\n                inlineElement = createTextNode(option.label);\n            }\n            this.comp.editor.shared.dom.insert(inlineElement);\n            const [anchorNode, anchorOffset] = rightPos(inlineElement);\n            this.comp.editor.shared.selection.setSelection({ anchorNode, anchorOffset });\n            this.comp.editor.shared.dom.insert(\"\\u00A0\");\n            this.comp.editor.shared.history.addStep();\n        } else {\n            // remove the user-typed search delimiter\n            this.composer.composerText =\n                this.composer.composerText.substring(0, position) +\n                this.composer.composerText.substring(this.composer.selection.end);\n            this.clearSearch();\n            this.composer.insertText(`${option.label} `, position);\n        }\n    }\n    update() {\n        if (!this.search.delimiter) {\n            return;\n        }\n        const { type, suggestions } = this.suggestionService.searchSuggestions(this.search, {\n            thread: this.thread,\n        });\n        if (!suggestions.length) {\n            this.state.items = undefined;\n            return;\n        }\n        // arbitrary limit to avoid displaying too many elements at once\n        // ideally a load more mechanism should be introduced\n        const limit = 8;\n        suggestions.length = Math.min(suggestions.length, limit);\n        this.state.items = { type, suggestions };\n    }\n\n    async fetchSuggestions() {\n        if (!this.thread || status(this.comp) === \"destroyed\") {\n            return;\n        }\n        let resetFetchingState = true;\n        try {\n            this.abortController?.abort();\n            this.abortController = new AbortController();\n            this.state.isFetching = true;\n            await this.suggestionService.fetchSuggestions(this.search, {\n                thread: this.thread,\n                abortSignal: this.abortController.signal,\n            });\n        } catch (e) {\n            this.lastFetchedSearch = null;\n            if (e instanceof ConnectionAbortedError) {\n                resetFetchingState = false;\n                return;\n            }\n            throw e;\n        } finally {\n            if (resetFetchingState) {\n                this.state.isFetching = false;\n            }\n        }\n        if (!this.thread || status(this.comp) === \"destroyed\") {\n            return;\n        }\n        this.update();\n        this.lastFetchedSearch = {\n            ...this.search,\n            count: this.state.items?.suggestions.length ?? 0,\n        };\n        if (!this.state.items?.suggestions.length) {\n            this.clearSearch();\n        }\n    }\n}\n\nexport function useSuggestion() {\n    return new UseSuggestion(useComponent());\n}\n", "import { partnerCompareRegistry } from \"@mail/core/common/partner_compare\";\nimport { cleanTerm } from \"@mail/utils/common/format\";\nimport { toRaw } from \"@odoo/owl\";\nimport { loadEmoji } from \"@web/core/emoji_picker/emoji_picker\";\n\nimport { registry } from \"@web/core/registry\";\nimport { fuzzyLookup } from \"@web/core/utils/search\";\n\nexport class SuggestionService {\n    /**\n     * @param {import(\"@web/env\").OdooEnv} env\n     * @param {import(\"services\").ServiceFactories} services\n     */\n    constructor(env, services) {\n        this.env = env;\n        this.orm = services.orm;\n        this.store = services[\"mail.store\"];\n        this.composer = services[\"mail.composer\"];\n        this.emojis;\n    }\n\n    /**\n     * Returns list of supported delimiters, each supported\n     * delimiter is in an array [a, b, c] where:\n     * - a: chars to trigger\n     * - b: (optional) if set, the exact position in composer text input to allow using this delimiter\n     * - c: (optional) if set, this is the minimum amount of extra char after delimiter to allow using this delimiter\n     *\n     * @param {import('models').Thread} thread\n     * @returns {Array<[string, number, number]>}\n     */\n    getSupportedDelimiters(thread, env) {\n        if (env?.inFrontendPortalChatter) {\n            return [[\":\", undefined, 2]];\n        }\n        return [[\"@\"], [\"#\"], [\"::\"], [\":\", undefined, 2]];\n    }\n\n    async fetchSuggestions({ delimiter, term }, { thread, abortSignal } = {}) {\n        const cleanedSearchTerm = cleanTerm(term);\n        switch (delimiter) {\n            case \"@\":\n                await this.fetchPartnersRoles(cleanedSearchTerm, thread, { abortSignal });\n                break;\n            case \"#\":\n                await this.fetchThreads(cleanedSearchTerm, { abortSignal });\n                break;\n            case \"::\":\n                await this.store.cannedReponses.fetch();\n                break;\n            case \":\": {\n                const { emojis } = await loadEmoji();\n                this.emojis = emojis;\n                break;\n            }\n        }\n    }\n\n    /**\n     * Make an ORM call with a cancellable signal. Usefull to abort fetch\n     * requests from outside of the suggestion service.\n     *\n     * @param {String} model\n     * @param {String} method\n     * @param {Array} args\n     * @param {Object} kwargs\n     * @param {Object} options\n     * @param {AbortSignal} options.abortSignal\n     */\n    makeOrmCall(model, method, args, kwargs, { abortSignal } = {}) {\n        return new Promise((res, rej) => {\n            const req = this.orm.silent.call(model, method, args, kwargs);\n            const onAbort = () => {\n                try {\n                    req.abort();\n                } catch (e) {\n                    rej(e);\n                }\n            };\n            abortSignal?.addEventListener(\"abort\", onAbort);\n            req.then(res)\n                .catch(rej)\n                .finally(() => abortSignal?.removeEventListener(\"abort\", onAbort));\n        });\n    }\n    /**\n     * @param {string} term\n     * @param {import(\"models\").Thread} [thread]\n     */\n    async fetchPartnersRoles(term, thread, { abortSignal } = {}) {\n        const kwargs = { search: term };\n        if (thread?.model === \"discuss.channel\") {\n            kwargs.channel_id = thread.id;\n        }\n        const data = await this.makeOrmCall(\n            \"res.partner\",\n            thread?.model === \"discuss.channel\"\n                ? \"get_mention_suggestions_from_channel\"\n                : \"get_mention_suggestions\",\n            [],\n            kwargs,\n            { abortSignal }\n        );\n        this.store.insert(data);\n    }\n\n    /**\n     * @param {string} term\n     */\n    async fetchThreads(term, { abortSignal } = {}) {\n        const data = await this.makeOrmCall(\n            \"discuss.channel\",\n            \"get_mention_suggestions\",\n            [],\n            { search: term },\n            { abortSignal }\n        );\n        this.store.insert(data);\n    }\n\n    searchCannedResponseSuggestions(cleanedSearchTerm) {\n        const cannedResponses = Object.values(this.store[\"mail.canned.response\"].records).filter(\n            (cannedResponse) => cleanTerm(cannedResponse.source).includes(cleanedSearchTerm)\n        );\n        const sortFunc = (c1, c2) => {\n            const cleanedName1 = cleanTerm(c1.source);\n            const cleanedName2 = cleanTerm(c2.source);\n            if (\n                cleanedName1.startsWith(cleanedSearchTerm) &&\n                !cleanedName2.startsWith(cleanedSearchTerm)\n            ) {\n                return -1;\n            }\n            if (\n                !cleanedName1.startsWith(cleanedSearchTerm) &&\n                cleanedName2.startsWith(cleanedSearchTerm)\n            ) {\n                return 1;\n            }\n            if (cleanedName1 < cleanedName2) {\n                return -1;\n            }\n            if (cleanedName1 > cleanedName2) {\n                return 1;\n            }\n            return c1.id - c2.id;\n        };\n        return {\n            type: \"mail.canned.response\",\n            suggestions: cannedResponses.sort(sortFunc),\n        };\n    }\n\n    searchEmojisSuggestions(cleanedSearchTerm) {\n        let emojis = [];\n        if (this.emojis && cleanedSearchTerm) {\n            emojis = fuzzyLookup(cleanedSearchTerm, this.emojis, (emoji) => emoji.shortcodes);\n        }\n        return {\n            type: \"emoji\",\n            suggestions: emojis,\n        };\n    }\n\n    /**\n     * Returns suggestions that match the given search term from specified type.\n     *\n     * @param {Object} [param0={}]\n     * @param {String} [param0.delimiter] can be one one of the following: [\"@\", \"#\"]\n     * @param {String} [param0.term]\n     * @param {Object} [options={}]\n     * @param {Integer} [options.thread] prioritize and/or restrict\n     *  result in the context of given thread\n     * @returns {{ type: String, suggestions: Array }}\n     */\n    searchSuggestions({ delimiter, term }, { thread } = {}) {\n        thread = toRaw(thread);\n        const cleanedSearchTerm = cleanTerm(term);\n        switch (delimiter) {\n            case \"@\": {\n                const partners = this.searchPartnerSuggestions(cleanedSearchTerm, thread);\n                const roles = this.searchRoleSuggestions(cleanedSearchTerm);\n                return {\n                    type: \"Partner\",\n                    suggestions: [...partners.suggestions, ...roles.suggestions],\n                };\n            }\n            case \"#\":\n                return this.searchChannelSuggestions(cleanedSearchTerm);\n            case \"::\":\n                return this.searchCannedResponseSuggestions(cleanedSearchTerm);\n            case \":\":\n                return this.searchEmojisSuggestions(cleanedSearchTerm);\n        }\n        return {\n            type: undefined,\n            suggestions: [],\n        };\n    }\n\n    searchRoleSuggestions(cleanedSearchTerm) {\n        const roles = Object.values(this.store[\"res.role\"].records).filter((role) =>\n            cleanTerm(role.name).includes(cleanedSearchTerm)\n        );\n        const sortFunc = (r1, r2) => {\n            const cleanedName1 = cleanTerm(r1.name);\n            const cleanedName2 = cleanTerm(r2.name);\n            if (\n                cleanedName1.startsWith(cleanedSearchTerm) &&\n                !cleanedName2.startsWith(cleanedSearchTerm)\n            ) {\n                return -1;\n            }\n            if (\n                !cleanedName1.startsWith(cleanedSearchTerm) &&\n                cleanedName2.startsWith(cleanedSearchTerm)\n            ) {\n                return 1;\n            }\n            if (cleanedName1 < cleanedName2) {\n                return -1;\n            }\n            if (cleanedName1 > cleanedName2) {\n                return 1;\n            }\n            return r1.id - r2.id;\n        };\n        return {\n            suggestions: roles.sort(sortFunc),\n        };\n    }\n\n    isSuggestionValid(partner, thread) {\n        return (\n            (this.store.self_partner?.main_user_id?.share === false || partner.mention_token) &&\n            partner.notEq(this.store.odoobot)\n        );\n    }\n\n    getPartnerSuggestions(thread) {\n        return Object.values(this.store[\"res.partner\"].records).filter((partner) =>\n            this.isSuggestionValid(partner, thread)\n        );\n    }\n\n    searchPartnerSuggestions(cleanedSearchTerm, thread) {\n        const partners = this.getPartnerSuggestions(thread);\n        const suggestions = [];\n        for (const partner of partners) {\n            if (!partner.name) {\n                continue;\n            }\n            if (\n                cleanTerm(partner.name).includes(cleanedSearchTerm) ||\n                (partner.email && cleanTerm(partner.email).includes(cleanedSearchTerm))\n            ) {\n                suggestions.push(partner);\n            }\n        }\n        suggestions.push(\n            ...this.store.specialMentions.filter(\n                (special) =>\n                    thread &&\n                    special.channel_types.includes(thread.channel_type) &&\n                    cleanedSearchTerm.length >= Math.min(4, special.label.length) &&\n                    (special.label.startsWith(cleanedSearchTerm) ||\n                        cleanTerm(special.description.toString()).includes(cleanedSearchTerm))\n            )\n        );\n        return {\n            type: \"Partner\",\n            suggestions: [...this.sortPartnerSuggestions(suggestions, cleanedSearchTerm, thread)],\n        };\n    }\n\n    /**\n     * @param {[import(\"models\").Persona | import(\"@mail/core/common/store_service\").SpecialMention]} [partners]\n     * @param {String} [searchTerm]\n     * @param {import(\"models\").Thread} thread\n     * @returns {[import(\"models\").Persona]}\n     */\n    sortPartnerSuggestions(partners, searchTerm = \"\", thread = undefined) {\n        const cleanedSearchTerm = cleanTerm(searchTerm);\n        const compareFunctions = partnerCompareRegistry.getAll();\n        const context = this.sortPartnerSuggestionsContext(thread);\n        return partners.sort((p1, p2) => {\n            p1 = toRaw(p1);\n            p2 = toRaw(p2);\n            if (p1.isSpecial || p2.isSpecial) {\n                return 0;\n            }\n            for (const fn of compareFunctions) {\n                const result = fn(p1, p2, {\n                    env: this.env,\n                    searchTerm: cleanedSearchTerm,\n                    thread,\n                    context,\n                });\n                if (result !== undefined) {\n                    return result;\n                }\n            }\n        });\n    }\n\n    sortPartnerSuggestionsContext() {\n        return {};\n    }\n\n    searchChannelSuggestions(cleanedSearchTerm) {\n        const suggestionList = Object.values(this.store.Thread.records).filter(\n            (thread) =>\n                thread.channel_type === \"channel\" &&\n                thread.displayName &&\n                cleanTerm(thread.displayName).includes(cleanedSearchTerm)\n        );\n        const sortFunc = (c1, c2) => {\n            const isPublicChannel1 = c1.channel_type === \"channel\" && !c2.group_public_id;\n            const isPublicChannel2 = c2.channel_type === \"channel\" && !c2.group_public_id;\n            if (isPublicChannel1 && !isPublicChannel2) {\n                return -1;\n            }\n            if (!isPublicChannel1 && isPublicChannel2) {\n                return 1;\n            }\n            if (c1.hasSelfAsMember && !c2.hasSelfAsMember) {\n                return -1;\n            }\n            if (!c1.hasSelfAsMember && c2.hasSelfAsMember) {\n                return 1;\n            }\n            const cleanedDisplayName1 = cleanTerm(c1.displayName);\n            const cleanedDisplayName2 = cleanTerm(c2.displayName);\n            if (\n                cleanedDisplayName1.startsWith(cleanedSearchTerm) &&\n                !cleanedDisplayName2.startsWith(cleanedSearchTerm)\n            ) {\n                return -1;\n            }\n            if (\n                !cleanedDisplayName1.startsWith(cleanedSearchTerm) &&\n                cleanedDisplayName2.startsWith(cleanedSearchTerm)\n            ) {\n                return 1;\n            }\n            if (cleanedDisplayName1 < cleanedDisplayName2) {\n                return -1;\n            }\n            if (cleanedDisplayName1 > cleanedDisplayName2) {\n                return 1;\n            }\n            return c1.id - c2.id;\n        };\n        return {\n            type: \"Thread\",\n            suggestions: suggestionList.sort(sortFunc),\n        };\n    }\n}\n\nexport const suggestionService = {\n    dependencies: [\"orm\", \"mail.store\", \"mail.composer\"],\n    /**\n     * @param {import(\"@web/env\").OdooEnv} env\n     * @param {import(\"services\").ServiceFactories} services\n     */\n    start(env, services) {\n        return new SuggestionService(env, services);\n    },\n};\n\nregistry.category(\"services\").add(\"mail.suggestion\", suggestionService);\n", "import { DateSection } from \"@mail/core/common/date_section\";\nimport { Message } from \"@mail/core/common/message\";\nimport { NotificationMessage } from \"./notification_message\";\nimport { Record } from \"@mail/core/common/record\";\nimport { useVisible } from \"@mail/utils/common/hooks\";\n\nimport {\n    Component,\n    markRaw,\n    onMounted,\n    onWillDestroy,\n    onWillPatch,\n    onWillUnmount,\n    onWillUpdateProps,\n    reactive,\n    toRaw,\n    useChildSubEnv,\n    useEffect,\n    useRef,\n    useState,\n} from \"@odoo/owl\";\nimport { browser } from \"@web/core/browser/browser\";\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { Transition } from \"@web/core/transition\";\nimport { Deferred } from \"@web/core/utils/concurrency\";\nimport { useBus, useRefListener, useService } from \"@web/core/utils/hooks\";\nimport { escape } from \"@web/core/utils/strings\";\n\nexport const PRESENT_VIEWPORT_THRESHOLD = 1;\n/**\n * @typedef {Object} Props\n * @property {boolean} [isInChatWindow=false]\n * @property {number} [jumpPresent=0]\n * @property {number} [jumpToNewMessage=0]\n * @property {\"asc\"|\"desc\"} [order=\"asc\"]\n * @property {import(\"models\").Thread} thread\n * @property {string} [searchTerm]\n * @property {import(\"@web/core/utils/hooks\").Ref} [scrollRef]\n * @extends {Component<Props, Env>}\n */\nexport class Thread extends Component {\n    static components = { Message, NotificationMessage, Transition, DateSection };\n    static props = [\n        \"autofocus?\",\n        \"showDates?\",\n        \"isInChatWindow?\",\n        \"jumpPresent?\",\n        \"jumpToNewMessage?\",\n        \"thread\",\n        \"order?\",\n        \"scrollRef?\",\n        \"showEmptyMessage?\",\n        \"showJumpPresent?\",\n        \"messageActions?\",\n    ];\n    static defaultProps = {\n        isInChatWindow: false,\n        jumpPresent: 0,\n        order: \"asc\",\n        showDates: true,\n        showEmptyMessage: true,\n        showJumpPresent: true,\n        messageActions: true,\n    };\n    static template = \"mail.Thread\";\n\n    /** @type {Deferred} */\n    smoothScrollingDeferred;\n    /** @type {number} */\n    smoothScrollingTimeout;\n    isSmoothScrolling = false;\n\n    setup() {\n        super.setup();\n        this.escape = escape;\n        this.applyScroll = this.applyScroll.bind(this);\n        this.saveScroll = this.saveScroll.bind(this);\n        this.onScroll = this.onScroll.bind(this);\n        this.registerMessageRef = this.registerMessageRef.bind(this);\n        this.store = useService(\"mail.store\");\n        this.ui = useService(\"ui\");\n        this.state = useState({\n            isFocused: false,\n            isReplyingTo: false,\n            mountedAndLoaded: false,\n            showJumpPresent: false,\n            scrollTop: null,\n        });\n        this.lastJumpPresent = this.props.jumpPresent;\n        this.orm = useService(\"orm\");\n        /** @type {ReturnType<import('@mail/utils/common/hooks').useMessageScrolling>|null} */\n        this.messageHighlight = this.env.messageHighlight\n            ? useState(this.env.messageHighlight)\n            : null;\n        this.scrollingToHighlight = false;\n        this.refByMessageId = reactive(new Map(), () => {\n            this.scrollToHighlighted();\n        });\n        useEffect(\n            () => {\n                this.scrollToHighlighted();\n            },\n            () => [this.messageHighlight?.highlightedMessageId]\n        );\n        this.present = useRef(\"load-newer\");\n        this.jumpPresentRef = useRef(\"jump-present\");\n        this.root = useRef(\"messages\");\n        this.visibleState = useVisible(\"messages\", () => {\n            this.updateShowJumpPresent();\n        });\n        /**\n         * This is the reference element with the scrollbar. The reference can\n         * either be the chatter scrollable (if chatter) or the thread\n         * scrollable (in other cases).\n         */\n        this.scrollableRef = this.props.scrollRef ?? this.root;\n        useRefListener(\n            this.scrollableRef,\n            \"scrollend\",\n            () => (this.state.scrollTop = this.scrollableRef.el.scrollTop)\n        );\n        this.loadOlderState = useVisible(\n            \"load-older\",\n            async () => {\n                await Promise.all([\n                    this.messageHighlight?.scrollPromise,\n                    this.smoothScrollingDeferred,\n                ]);\n                if (this.loadOlderState.isVisible) {\n                    toRaw(this.props.thread).fetchMoreMessages();\n                }\n            },\n            { ready: false }\n        );\n        this.loadNewerState = useVisible(\n            \"load-newer\",\n            async () => {\n                await Promise.all([\n                    this.messageHighlight?.scrollPromise,\n                    this.smoothScrollingDeferred,\n                ]);\n                if (this.loadNewerState.isVisible) {\n                    toRaw(this.props.thread).fetchMoreMessages(\"newer\");\n                }\n            },\n            { ready: false }\n        );\n        this.presentThresholdState = useVisible(\"present-treshold\", () =>\n            this.updateShowJumpPresent()\n        );\n        this.setupScroll();\n        useEffect(\n            (focus) => {\n                if (focus && this.state.mountedAndLoaded) {\n                    this.root.el.focus();\n                }\n            },\n            () => [this.props.autofocus + this.props.thread.autofocus, this.state.mountedAndLoaded]\n        );\n        useEffect(\n            () => {\n                this.computeJumpPresentPosition();\n            },\n            () => [this.jumpPresentRef.el, this.viewportEl]\n        );\n        useEffect(\n            () => this.updateShowJumpPresent(),\n            () => [this.props.thread.loadNewer]\n        );\n        useEffect(\n            () => {\n                if (this.props.jumpPresent !== this.lastJumpPresent) {\n                    this.jumpToPresent({ immediate: true });\n                }\n            },\n            () => [this.props.jumpPresent]\n        );\n        useEffect(\n            () => {\n                if (this.props.thread.highlightMessage && this.state.mountedAndLoaded) {\n                    this.messageHighlight?.highlightMessage(\n                        this.props.thread.highlightMessage,\n                        this.props.thread\n                    );\n                    this.props.thread.highlightMessage = null;\n                }\n            },\n            () => [this.props.thread.highlightMessage, this.state.mountedAndLoaded]\n        );\n        useEffect(\n            () => {\n                if (!this.state.mountedAndLoaded) {\n                    return;\n                }\n                this.updateShowJumpPresent();\n            },\n            () => [this.state.mountedAndLoaded]\n        );\n        onMounted(() => {\n            if (!this.env.chatter || this.env.chatter?.fetchMessages) {\n                if (this.env.chatter) {\n                    this.env.chatter.fetchMessages = false;\n                }\n                this.fetchMessages();\n            }\n        });\n        onWillUnmount(() => {\n            if (this.state.isFocused) {\n                this.props.thread.isFocusedCounter--;\n            }\n        });\n        useEffect(\n            (isLoaded) => {\n                this.state.mountedAndLoaded = isLoaded;\n            },\n            /**\n             * Observe `mountedAndLoaded` as well because it might change from\n             * other parts of the code without `useEffect` detecting any change\n             * for `isLoaded`, and it should still be reset when patching.\n             */\n            () => [this.props.thread.isLoaded, this.state.mountedAndLoaded]\n        );\n        useEffect(\n            () => {\n                if (!this.props.jumpToNewMessage) {\n                    return;\n                }\n                const el = this.refByMessageId.get(\n                    this.props.thread.self_member_id.new_message_separator_ui - 1\n                )?.el;\n                if (el) {\n                    el.querySelector(\".o-mail-Message-jumpTarget\").scrollIntoView({\n                        behavior: \"instant\",\n                        block: \"center\",\n                    });\n                }\n            },\n            () => [this.props.jumpToNewMessage]\n        );\n        useBus(this.env.bus, \"MAIL:RELOAD-THREAD\", ({ detail }) => {\n            const { model, id } = this.props.thread;\n            if (detail.model === model && detail.id === id) {\n                toRaw(this.props.thread).fetchNewMessages();\n            }\n        });\n        onWillUpdateProps((nextProps) => {\n            if (nextProps.thread.notEq(this.props.thread)) {\n                this.lastJumpPresent = nextProps.jumpPresent;\n            }\n            if (!this.env.chatter || this.env.chatter?.fetchMessages) {\n                if (this.env.chatter) {\n                    this.env.chatter.fetchMessages = false;\n                }\n                toRaw(nextProps.thread).fetchNewMessages();\n            }\n        });\n    }\n\n    computeJumpPresentPosition() {\n        if (!this.viewportEl || !this.jumpPresentRef.el) {\n            return;\n        }\n        const width = this.viewportEl.clientWidth;\n        const height = this.viewportEl.clientHeight;\n        const computedStyle = window.getComputedStyle(this.viewportEl);\n        const ps = parseInt(computedStyle.getPropertyValue(\"padding-left\"));\n        const pe = parseInt(computedStyle.getPropertyValue(\"padding-right\"));\n        const pt = parseInt(computedStyle.getPropertyValue(\"padding-top\"));\n        const pb = parseInt(computedStyle.getPropertyValue(\"padding-bottom\"));\n        this.jumpPresentRef.el.style.transform = `translate(${\n            this.env.inChatter ? 22 : width - ps - pe - 22\n        }px, ${\n            this.env.inChatter && !this.env.inChatter.aside\n                ? -22\n                : height - pt - pb - (this.env.inChatter?.aside ? 75 : 0)\n        }px)`;\n    }\n\n    /**\n     * The scroll on a message list is managed in several different ways.\n     *\n     * 1. When the user first accesses a thread with unread messages, or when\n     *    the user goes back to a thread with new unread messages, it should\n     *    scroll to the position of the first unread message if there is one.\n     * 2. When loading older or newer messages, the messages already on screen\n     *    should visually stay in place. When the extra messages are added at\n     *    the bottom (chatter loading older, or channel loading newer) the same\n     *    scroll top position should be kept, and when the extra messages are\n     *    added at the top (chatter loading newer, or channel loading older),\n     *    the extra height from the extra messages should be compensated in the\n     *    scroll position.\n     * 3. When the scroll is at the bottom, it should stay at the bottom when\n     *    there is a change of height: new messages, images loaded, ...\n     * 4. When the user goes back and forth between threads, it should restore\n     *    the last scroll position of each thread.\n     * 5. When currently highlighting a message it takes priority to allow the\n     *    highlighted message to be scrolled to.\n     */\n    setupScroll() {\n        /**\n         * Last scroll value that was automatically set. This prevents from\n         * setting the same value 2 times in a row. This is not supposed to have\n         * an effect, unless the value was changed from outside in the meantime,\n         * in which case resetting the value would incorrectly override the\n         * other change. This should give enough time to scroll/resize event to\n         * register the new scroll value.\n         */\n        this.lastSetValue = undefined;\n        /**\n         * The snapshot mechanism (point 2) should only apply after the messages\n         * have been loaded and displayed at least once. Technically this is\n         * after the first patch following when `mountedAndLoaded` is true. This\n         * is what this variable holds.\n         */\n        this.loadedAndPatched = false;\n        /**\n         * The snapshot of current scrollTop and scrollHeight for the purpose\n         * of keeping messages in place when loading older/newer (point 2).\n         */\n        this.snapshot = undefined;\n        /**\n         * The newest message that is already rendered, useful to detect\n         * whether newer messages have been loaded since last render to decide\n         * when to apply the snapshot to keep messages in place (point 2).\n         */\n        this.newestPersistentMessage = undefined;\n        /**\n         * The oldest message that is already rendered, useful to detect\n         * whether older messages have been loaded since last render to decide\n         * when to apply the snapshot to keep messages in place (point 2).\n         */\n        this.oldestPersistentMessage = undefined;\n        /**\n         * Whether it was possible to load newer messages in the last rendered\n         * state, useful to decide when to apply the snapshot to keep messages\n         * in place (point 2).\n         */\n        this.loadNewer = undefined;\n        /**\n         * These states need to be immediately reset when the value changes on\n         * the record, because the transition is important, not only the final\n         * value. If resetting is depending on the update cycle, it can happen\n         * that the value quickly changes and then back again before there is\n         * any mounting/patching, and the change would therefore be undetected.\n         */\n        let stopOnChange = Record.onChange(this.props.thread, \"isLoaded\", () => {\n            if (!this.props.thread.isLoaded || !this.state.mountedAndLoaded) {\n                this.reset();\n            }\n        });\n        onWillUpdateProps((nextProps) => {\n            if (nextProps.thread.notEq(this.props.thread)) {\n                stopOnChange();\n                stopOnChange = Record.onChange(nextProps.thread, \"isLoaded\", () => {\n                    if (!nextProps.thread.isLoaded || !this.state.mountedAndLoaded) {\n                        this.reset();\n                    }\n                });\n            }\n        });\n        onWillDestroy(() => stopOnChange());\n        onWillPatch(() => {\n            if (!this.loadedAndPatched) {\n                return;\n            }\n            this.snapshot = {\n                scrollHeight: this.scrollableRef.el.scrollHeight,\n                scrollTop: this.scrollableRef.el.scrollTop,\n            };\n        });\n        useEffect(this.applyScroll);\n        useChildSubEnv({\n            getCurrentThread: () => this.props.thread,\n            onImageLoaded: this.applyScroll,\n        });\n        const observer = new ResizeObserver(() => {\n            this.computeJumpPresentPosition();\n            this.applyScroll();\n        });\n        useEffect(\n            (el, mountedAndLoaded) => {\n                if (el && mountedAndLoaded) {\n                    el.addEventListener(\"scroll\", this.onScroll);\n                    observer.observe(el);\n                    return () => {\n                        observer.unobserve(el);\n                        el.removeEventListener(\"scroll\", this.onScroll);\n                    };\n                }\n            },\n            () => [this.scrollableRef.el, this.state.mountedAndLoaded]\n        );\n    }\n\n    applyScroll() {\n        if (!this.props.thread.isLoaded || !this.state.mountedAndLoaded) {\n            this.reset();\n            return;\n        }\n        // Use toRaw() to prevent scroll check from triggering renders.\n        const thread = toRaw(this.props.thread);\n        this.applyScrollContextually(thread);\n        this.snapshot = undefined;\n        this.newestPersistentMessage = thread.newestPersistentMessage;\n        this.oldestPersistentMessage = thread.oldestPersistentMessage;\n        this.loadNewer = thread.loadNewer;\n        if (!this.loadedAndPatched) {\n            this.loadedAndPatched = true;\n            this.loadOlderState.ready = true;\n            this.loadNewerState.ready = true;\n        }\n    }\n\n    /** @param {import(\"models\").Thread} thread */\n    applyScrollContextually(thread) {\n        const olderMessages = thread.oldestPersistentMessage?.id < this.oldestPersistentMessage?.id;\n        const newerMessages = thread.newestPersistentMessage?.id > this.newestPersistentMessage?.id;\n        const messagesAtTop =\n            (this.props.order === \"asc\" && olderMessages) ||\n            (this.props.order === \"desc\" && newerMessages);\n        const messagesAtBottom =\n            (this.props.order === \"desc\" && olderMessages) ||\n            (this.props.order === \"asc\" &&\n                newerMessages &&\n                (this.loadNewer ||\n                    typeof thread.scrollTop !== \"string\" ||\n                    !thread.scrollTop?.includes(\"bottom\")));\n        if (this.snapshot && messagesAtTop) {\n            this.setScroll(\n                this.snapshot.scrollTop +\n                    this.scrollableRef.el.scrollHeight -\n                    this.snapshot.scrollHeight\n            );\n        } else if (this.snapshot && messagesAtBottom) {\n            this.setScroll(this.snapshot.scrollTop);\n        } else if (\n            !this.env.messageHighlight?.highlightedMessageId &&\n            thread.scrollTop !== undefined\n        ) {\n            let value;\n            if (typeof thread.scrollTop === \"string\" && thread.scrollTop?.includes(\"bottom\")) {\n                value =\n                    this.props.order === \"asc\"\n                        ? this.scrollableRef.el.scrollHeight - this.scrollableRef.el.clientHeight\n                        : 0;\n            } else {\n                value =\n                    this.props.order === \"asc\"\n                        ? thread.scrollTop\n                        : this.scrollableRef.el.scrollHeight -\n                          thread.scrollTop -\n                          this.scrollableRef.el.clientHeight;\n            }\n            if (\n                (this.lastSetValue === undefined || Math.abs(this.lastSetValue - value) > 1) &&\n                !this.isSmoothScrolling\n            ) {\n                this.setScroll(value, {\n                    smooth:\n                        typeof thread.scrollTop === \"string\" &&\n                        thread.scrollTop?.includes(\"smooth\"),\n                });\n            }\n        }\n    }\n\n    fetchMessages() {\n        toRaw(this.props.thread).fetchNewMessages();\n    }\n\n    get viewportEl() {\n        let viewportEl = this.scrollableRef.el;\n        if (viewportEl && viewportEl.clientHeight > browser.innerHeight) {\n            while (viewportEl && viewportEl.clientHeight > browser.innerHeight) {\n                viewportEl = viewportEl.parentElement;\n            }\n        }\n        return viewportEl;\n    }\n\n    get PRESENT_THRESHOLD() {\n        const threshold = (this.viewportEl?.clientHeight ?? 0) * PRESENT_VIEWPORT_THRESHOLD;\n        return this.state.showJumpPresent ? threshold - 200 : threshold;\n    }\n\n    updateShowJumpPresent() {\n        this.state.showJumpPresent =\n            this.visibleState.isVisible &&\n            (this.props.thread.loadNewer || this.presentThresholdState.isVisible === false);\n    }\n\n    onClickLoadOlder() {\n        this.props.thread.fetchMoreMessages();\n    }\n\n    async onClickPreferences() {\n        const actionDescription = await this.orm.call(\"res.users\", \"action_get\");\n        actionDescription.res_id = this.store.self.main_user_id?.id;\n        this.env.services.action.doAction(actionDescription);\n    }\n\n    onFocusin() {\n        this.state.isFocused = true;\n        this.props.thread.isFocusedCounter++;\n        const thread = toRaw(this.props.thread);\n        if (thread?.scrollTop === \"bottom\" && !thread.scrollUnread && !thread.markedAsUnread) {\n            thread?.markAsRead();\n        }\n    }\n\n    onFocusout() {\n        this.state.isFocused = false;\n        this.props.thread.isFocusedCounter--;\n    }\n\n    getMessageClassName(message) {\n        return !message.isNotification && this.messageHighlight?.highlightedMessageId === message.id\n            ? \"o-highlighted bg-view shadow-lg pb-1\"\n            : \"\";\n    }\n\n    async jumpToPresent({ immediate = false } = {}) {\n        this.messageHighlight?.clear();\n        if (!immediate || this.props.thread.loadNewer) {\n            await this.props.thread.loadAround();\n            this.props.thread.loadNewer = false;\n            this.state.showJumpPresent = false;\n        }\n        this.props.thread.scrollTop = immediate ? \"bottom\" : \"bottom-smooth\";\n    }\n\n    registerMessageRef(message, ref) {\n        if (!ref) {\n            this.refByMessageId.delete(message.id);\n            return;\n        }\n        this.refByMessageId.set(message.id, markRaw(ref));\n    }\n\n    reset() {\n        this.state.mountedAndLoaded = false;\n        this.loadOlderState.ready = false;\n        this.loadNewerState.ready = false;\n        this.lastSetValue = undefined;\n        this.snapshot = undefined;\n        this.newestPersistentMessage = undefined;\n        this.oldestPersistentMessage = undefined;\n        this.loadedAndPatched = false;\n        this.loadNewer = false;\n    }\n\n    isSquashed(msg, prevMsg) {\n        if (this.props.thread.model === \"mail.box\") {\n            return false;\n        }\n        if (!prevMsg || prevMsg.message_type === \"notification\" || this.env.inChatter) {\n            return false;\n        }\n\n        if (!msg.author?.eq(prevMsg.author)) {\n            return false;\n        }\n        if (!msg.thread?.eq(prevMsg.thread)) {\n            return false;\n        }\n        if (msg.isNote) {\n            return false;\n        }\n        return msg.datetime.ts - prevMsg.datetime.ts < 5 * 60 * 1000;\n    }\n\n    get isAtBottom() {\n        if (this.loadNewer) {\n            return false;\n        }\n        return this.props.order === \"asc\"\n            ? this.scrollableRef.el.scrollHeight -\n                  this.scrollableRef.el.scrollTop -\n                  this.scrollableRef.el.clientHeight <\n                  30\n            : this.scrollableRef.el.scrollTop < 30;\n    }\n\n    onScroll() {\n        const thread = toRaw(this.props.thread);\n        if (\n            this.isAtBottom &&\n            !thread.markedAsUnread &&\n            thread.isFocused &&\n            !thread.markingAsRead\n        ) {\n            thread.markAsRead();\n        }\n        this.saveScroll();\n    }\n\n    saveScroll() {\n        const thread = toRaw(this.props.thread);\n        const isBottom = this.isAtBottom;\n        if (isBottom) {\n            thread.scrollTop = \"bottom\";\n        } else {\n            thread.scrollTop =\n                this.props.order === \"asc\"\n                    ? this.scrollableRef.el.scrollTop\n                    : this.scrollableRef.el.scrollHeight -\n                      this.scrollableRef.el.scrollTop -\n                      this.scrollableRef.el.clientHeight;\n        }\n    }\n\n    async scrollToHighlighted() {\n        if (!this.messageHighlight?.highlightedMessageId || this.scrollingToHighlight) {\n            return;\n        }\n        const el = this.refByMessageId.get(this.messageHighlight.highlightedMessageId)?.el;\n        if (el) {\n            this.scrollingToHighlight = true;\n\n            await this.messageHighlight.startupDeferred;\n            this.messageHighlight\n                .scrollTo(el.querySelector(\".o-mail-Message-jumpTarget\"))\n                .then(() => (this.scrollingToHighlight = false));\n        }\n    }\n\n    get orderedMessages() {\n        const messages = this.state.mountedAndLoaded\n            ? this.props.thread.messages\n            : this.props.thread.phantomMessages;\n        return this.props.order === \"asc\" ? [...messages] : [...messages].reverse();\n    }\n\n    get showLoadOlder() {\n        return (\n            this.props.thread.loadOlder &&\n            this.props.thread.isLoaded &&\n            !this.props.thread.isTransient &&\n            !this.props.thread.hasLoadingFailed &&\n            !this.messageHighlight?.initiated &&\n            !this.messageHighlight?.highlightedMessageId\n        );\n    }\n\n    setScroll(value, { smooth = false } = {}) {\n        if (smooth) {\n            clearTimeout(this.smoothScrollingTimeout);\n            this.isSmoothScrolling = true;\n            this.smoothScrollingDeferred = new Deferred();\n            const onSmoothScrollingEnd = () => {\n                this.smoothScrollingDeferred.resolve();\n                this.smoothScrollingDeferred = undefined;\n                this.isSmoothScrolling = false;\n            };\n            if (\"onscrollend\" in window) {\n                document.addEventListener(\"scrollend\", onSmoothScrollingEnd, {\n                    capture: true,\n                    once: true,\n                });\n            } else {\n                // To remove when safari will support the \"scrollend\" event.\n                this.smoothScrollingTimeout = setTimeout(onSmoothScrollingEnd, 250);\n            }\n        }\n        this.scrollableRef.el.scrollTo({ behavior: smooth ? \"smooth\" : undefined, top: value });\n        this.lastSetValue = value;\n        this.messageHighlight?.startupDeferred?.resolve();\n        this.saveScroll();\n    }\n\n    get showStartMessage() {\n        return (\n            this.state.mountedAndLoaded &&\n            [\"channel\", \"group\", \"chat\"].includes(this.props.thread.channel_type)\n        );\n    }\n\n    get startMessageTitle() {\n        const channelName = this.props.thread.name;\n        if (this.props.thread.parent_channel_id) {\n            return channelName;\n        }\n        if (this.props.thread.channel_type === \"channel\") {\n            return _t(\"Welcome to #%(channelName)s!\", { channelName });\n        }\n        return this.props.thread.displayName;\n    }\n\n    get startMessageSubtitle() {\n        if (this.props.thread.parent_channel_id) {\n            const authorName = Object.values(this.store[\"res.partner\"].records).find((partner) =>\n                partner.main_user_id?.eq(this.props.thread.create_uid)\n            )?.name;\n            if (authorName) {\n                return _t(\"Started by %(authorName)s\", { authorName });\n            }\n        }\n        if (this.props.thread.channel_type === \"channel\") {\n            return _t(\"This is the start of the #%(channelName)s channel\", {\n                channelName: this.props.thread.name,\n            });\n        }\n        if (this.props.thread.channel_type === \"group\") {\n            return _t(\"This is the start of %(conversationName)s group\", {\n                conversationName: this.props.thread.displayName,\n            });\n        }\n        return _t(\"This is the start of your direct chat with %(userName)s\", {\n            userName: this.props.thread.displayName,\n        });\n    }\n}\n", "import { useSubEnv, useComponent, useState } from \"@odoo/owl\";\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { SearchMessagesPanel } from \"@mail/core/common/search_messages_panel\";\nimport { markEventHandled } from \"@web/core/utils/misc\";\nimport { Action, ACTION_TAGS, UseActions } from \"@mail/core/common/action\";\nimport { MeetingChat } from \"@mail/discuss/call/common/meeting_chat\";\nimport { useService } from \"@web/core/utils/hooks\";\n\nexport const threadActionsRegistry = registry.category(\"mail.thread/actions\");\n\n/** @typedef {import(\"@odoo/owl\").Component} Component */\n/** @typedef {import(\"@mail/core/common/action\").ActionDefinition} ActionDefinition */\n/** @typedef {import(\"models\").Thread} Thread */\n/**\n * @typedef {Object} ThreadActionSpecificDefinition\n * @property {Component} [actionPanelComponent]\n * @property {(Component) => Object} [actionPanelComponentProps]\n * @property {(Component) => void} [close]\n * @property {boolean|(comp: Component) => boolean} [condition=true]\n * @property {string|(comp: Component) => string} [nameClass]\n * @property {(comp: Component) => void} [open]\n * @property {(comp: Component) => string} [panelOuterClass]\n * @property {boolean} [toggle]\n */\n\n/**\n * @typedef {ActionDefinition & ThreadActionSpecificDefinition} ThreadActionDefinition\n */\n\n/**\n * @param {string} id\n * @param {ThreadActionDefinition} definition\n */\nexport function registerThreadAction(id, definition) {\n    threadActionsRegistry.add(id, definition);\n}\n\nregisterThreadAction(\"fold-chat-window\", {\n    condition: ({ owner }) => owner.props.chatWindow && !owner.isDiscussSidebarChannelActions,\n    icon: \"oi oi-fw oi-minus\",\n    name: ({ owner }) => (!owner.props.chatWindow?.isOpen ? _t(\"Open\") : _t(\"Fold\")),\n    open: ({ owner }) => owner.toggleFold(),\n    displayActive: ({ owner }) => !owner.props.chatWindow?.isOpen,\n    sequence: 99,\n    sequenceQuick: 20,\n});\nregisterThreadAction(\"rename-thread\", {\n    condition: ({ owner, thread }) =>\n        thread &&\n        owner.props.chatWindow?.isOpen &&\n        (thread.is_editable || thread.channel_type === \"chat\") &&\n        !owner.isDiscussSidebarChannelActions,\n    icon: \"fa fa-fw fa-pencil\",\n    name: _t(\"Rename Thread\"),\n    open: ({ owner }) => (owner.state.editingName = true),\n    sequence: 30,\n    sequenceGroup: 20,\n});\nregisterThreadAction(\"close\", {\n    condition: ({ owner }) => owner.props.chatWindow && !owner.isDiscussSidebarChannelActions,\n    icon: \"oi fa-fw oi-close\",\n    name: _t(\"Close Chat Window (ESC)\"),\n    open: ({ owner }) => owner.close(),\n    sequence: 100,\n    sequenceQuick: 10,\n});\nregisterThreadAction(\"search-messages\", {\n    actionPanelComponent: SearchMessagesPanel,\n    condition: ({ owner, thread }) =>\n        [\"discuss.channel\", \"mail.box\"].includes(thread?.model) &&\n        (!owner.props.chatWindow || owner.props.chatWindow.isOpen) &&\n        !owner.isDiscussSidebarChannelActions,\n    hotkey: \"f\",\n    panelOuterClass: \"o-mail-SearchMessagesPanel bg-inherit\",\n    icon: \"oi oi-fw oi-search\",\n    name: ({ action }) => (action.isActive ? _t(\"Close Search\") : _t(\"Search Messages\")),\n    sequence: 20,\n    sequenceGroup: 20,\n    setup: ({ action }) =>\n        useSubEnv({\n            searchMenu: {\n                open: () => action.open(),\n                close: () => {\n                    if (action.isActive) {\n                        action.close();\n                    }\n                },\n            },\n        }),\n    toggle: true,\n});\nregisterThreadAction(\"meeting-chat\", {\n    actionPanelComponent: MeetingChat,\n    badge: ({ thread }) => thread.isUnread,\n    badgeIcon: ({ thread }) => !thread.importantCounter && \"fa fa-circle text-700\",\n    badgeText: ({ thread }) => thread.importantCounter || undefined,\n    condition: ({ owner }) => owner.env.inMeetingView,\n    icon: \"fa fa-fw fa-comments\",\n    name: _t(\"Chat\"),\n    panelOuterClass: \"bg-100 border border-secondary\",\n    sequence: 30,\n    toggle: true,\n    tags: ({ thread }) => {\n        const tags = [];\n        if (thread.importantCounter) {\n            tags.push(ACTION_TAGS.IMPORTANT_BADGE);\n        }\n        return tags;\n    },\n});\n\nexport class ThreadAction extends Action {\n    /** Determines whether this is a popover linked to this action. */\n    popover = null;\n    /** @type {() => Thread} */\n    threadFn;\n\n    /**\n     * @param {Object} param0\n     * @param {Thread|() => Thread} thread\n     */\n    constructor({ thread }) {\n        super(...arguments);\n        this.threadFn = typeof thread === \"function\" ? thread : () => thread;\n    }\n\n    get params() {\n        return Object.assign(super.params, { thread: this.threadFn() });\n    }\n\n    /** Optional component that is used as action panel of this component, i.e. when action is active. */\n    get actionPanelComponent() {\n        return this.definition.actionPanelComponent;\n    }\n\n    /** Condition to display the action panel component of this action. */\n    get actionPanelComponentCondition() {\n        return this.isActive && this.actionPanelComponent && this.condition && !this.popover;\n    }\n\n    /** Props to pass to the action panel component of this action. */\n    get actionPanelComponentProps() {\n        return this.definition.actionPanelComponentProps?.call(this, this.params);\n    }\n\n    /** Closes this action. */\n    close() {\n        if (this.toggle) {\n            this.owner.threadActions.activeAction = this.owner.threadActions.actionStack.pop();\n        }\n        this.definition.close?.call(this, this.params);\n    }\n\n    /** States whether this action is currently active. */\n    get isActive() {\n        return this.id === this.owner.threadActions.activeAction?.id;\n    }\n\n    /** ClassName on name of this action */\n    get nameClass() {\n        return typeof this.definition.nameClass === \"function\"\n            ? this.definition.nameClass.call(this, this.params)\n            : this.definition.nameClass;\n    }\n\n    /**\n     * @override\n     * @param {MouseEvent} [ev]\n     * @param {object} [param0]\n     * @param {boolean} [param0.keepPrevious] Whether the previous action\n     * should be kept so that closing the current action goes back\n     * to the previous one.\n     * */\n    onSelected(ev, { keepPrevious } = {}) {\n        if (ev) {\n            markEventHandled(ev, \"ThreadAction.onSelected\");\n        }\n        if (this.toggle && this.isActive) {\n            this.close();\n        } else {\n            this.open({ keepPrevious });\n        }\n    }\n\n    /**\n     * Opens this action.\n     *\n     * @param {object} [param0]\n     * @param {boolean} [param0.keepPrevious] Whether the previous action\n     * should be kept so that closing the current action goes back\n     * to the previous one.\n     * */\n    open({ keepPrevious } = {}) {\n        if (this.toggle) {\n            if (this.owner.threadActions.activeAction) {\n                if (keepPrevious) {\n                    this.owner.threadActions.actionStack.push(\n                        this.owner.threadActions.activeAction\n                    );\n                } else {\n                    this.owner.threadActions.activeAction.close();\n                }\n            }\n            this.owner.threadActions.activeAction = this;\n        }\n        this.definition.open?.call(this, this.params);\n    }\n\n    get panelOuterClass() {\n        return typeof this.definition.panelOuterClass === \"function\"\n            ? this.definition.panelOuterClass.call(this, this.params)\n            : this.definition.panelOuterClass;\n    }\n\n    /** Determines whether this action is a one time effect or can be toggled (on or off). */\n    get toggle() {\n        return this.definition.toggle;\n    }\n}\n\nclass UseThreadActions extends UseActions {\n    ActionClass = ThreadAction;\n    actionStack = [];\n    activeAction = null;\n}\n\n/**\n * @param {Object} [params0={}]\n * @param {Thread|() => Thread} thread\n */\nexport function useThreadActions({ thread } = {}) {\n    const component = useComponent();\n    const transformedActions = threadActionsRegistry\n        .getEntries()\n        .map(([id, definition]) => new ThreadAction({ owner: component, id, definition, thread }));\n    for (const action of transformedActions) {\n        action.setup();\n    }\n    return useState(new UseThreadActions(component, transformedActions, useService(\"mail.store\")));\n}\n", "import { registry } from \"@web/core/registry\";\n\n/**\n * Registry of functions to sort threads in messaging menu.\n * The expected value is a function with the following\n * signature:\n *     (thread1: Thread, thread2: Thread) => number | undefined\n */\nexport const threadCompareRegistry = registry.category(\"mail.thread_compare\");\n\nthreadCompareRegistry.add(\n    \"mail.needaction\",\n    /**\n     * @param {import(\"models\").Thread thread1}\n     * @param {import(\"models\").Thread thread2}\n     */\n    (thread1, thread2) => {\n        const aNeedaction = thread1.needactionMessages.length;\n        const bNeedaction = thread2.needactionMessages.length;\n        if (aNeedaction > 0 && bNeedaction === 0) {\n            return -1;\n        }\n        if (bNeedaction > 0 && aNeedaction === 0) {\n            return 1;\n        }\n    },\n    { sequence: 10 }\n);\n\nthreadCompareRegistry.add(\n    \"mail.message-datetime\",\n    /**\n     * @param {import(\"models\").Thread thread1}\n     * @param {import(\"models\").Thread thread2}\n     */\n    (thread1, thread2) => {\n        const aMessageDatetime = thread1.newestPersistentOfAllMessage?.datetime;\n        const bMessageDateTime = thread2.newestPersistentOfAllMessage?.datetime;\n        if (!aMessageDatetime && bMessageDateTime) {\n            return 1;\n        }\n        if (!bMessageDateTime && aMessageDatetime) {\n            return -1;\n        }\n        if (aMessageDatetime && bMessageDateTime && aMessageDatetime !== bMessageDateTime) {\n            return bMessageDateTime - aMessageDatetime;\n        }\n    },\n    { sequence: 40 }\n);\n", "import { useService } from \"@web/core/utils/hooks\";\n\nimport { Component } from \"@odoo/owl\";\nimport { Thread } from \"./thread_model\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { ImStatus } from \"./im_status\";\n\n/**\n * @typedef {Object} Props\n * @property {import(\"models\").Thread} thread\n * @property {string} size\n * @property {string} className\n * @extends {Component<Props, Env>}\n */\nexport class ThreadIcon extends Component {\n    static template = \"mail.ThreadIcon\";\n    static components = { ImStatus };\n    static props = {\n        thread: { type: Thread },\n        size: { optional: true, validate: (size) => [\"small\", \"medium\", \"large\"].includes(size) },\n        className: { type: String, optional: true },\n        title: { type: Boolean, optional: true },\n    };\n    static defaultProps = {\n        size: \"medium\",\n        className: \"\",\n        title: true,\n    };\n\n    setup() {\n        super.setup();\n        this.store = useService(\"mail.store\");\n    }\n\n    get correspondent() {\n        return this.props.thread.correspondent;\n    }\n\n    get defaultChatIcon() {\n        return {\n            class: \"fa fa-question-circle opacity-75\",\n            title: _t(\"No IM status available\"),\n        };\n    }\n}\n", "import { AND, fields, Record } from \"@mail/core/common/record\";\nimport { generateEmojisOnHtml } from \"@mail/utils/common/format\";\nimport { assignDefined } from \"@mail/utils/common/misc\";\nimport { rpc } from \"@web/core/network/rpc\";\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { user } from \"@web/core/user\";\nimport { Deferred } from \"@web/core/utils/concurrency\";\n\n/**\n * @typedef SuggestedRecipient\n * @property {string} email\n * @property {import(\"models\").Persona|false} persona\n * @property {string} lang\n * @property {string} reason\n */\n\nexport class Thread extends Record {\n    static id = AND(\"model\", \"id\");\n    /**\n     * @param {string} localId\n     * @returns {string}\n     */\n    static localIdToActiveId(localId) {\n        if (!localId) {\n            return undefined;\n        }\n        // Transform \"Thread,<model> AND <id>\" to \"<model>_<id>\"\"\n        return localId.split(\",\").slice(1).join(\"_\").replace(\" AND \", \"_\");\n    }\n    static async getOrFetch(data, fieldNames = []) {\n        let thread = this.get(data);\n        if (\n            data.id > 0 &&\n            (!thread || fieldNames.some((fieldName) => thread[fieldName] === undefined))\n        ) {\n            await this.store.fetchStoreData(\"mail.thread\", {\n                thread_model: data.model,\n                thread_id: data.id,\n                request_list: fieldNames,\n            });\n            thread = this.get(data);\n            if (!thread?.exists()) {\n                return;\n            }\n        }\n        return thread;\n    }\n\n    autofocus = 0;\n    create_uid = fields.One(\"res.users\");\n    /** @type {number} */\n    id;\n    /** @type {string} */\n    uuid;\n    /** @type {string} */\n    model;\n    allMessages = fields.Many(\"mail.message\", {\n        inverse: \"thread\",\n    });\n    storeAsAllChannels = fields.One(\"Store\", {\n        compute() {\n            if (this.model === \"discuss.channel\") {\n                return this.store;\n            }\n        },\n        eager: true,\n    });\n    /** @type {boolean} */\n    areAttachmentsLoaded = false;\n    group_public_id = fields.One(\"res.groups\");\n    attachments = fields.Many(\"ir.attachment\", {\n        /**\n         * @param {import(\"models\").Attachment} a1\n         * @param {import(\"models\").Attachment} a2\n         */\n        sort: (a1, a2) => (a1.id < a2.id ? 1 : -1),\n    });\n    get allowedToLeaveChannelTypes() {\n        return [\"channel\", \"group\"];\n    }\n    get canLeave() {\n        return (\n            this.allowedToLeaveChannelTypes.includes(this.channel_type) &&\n            this.group_ids.length === 0 &&\n            this.store.self_partner\n        );\n    }\n    get allowedToUnpinChannelTypes() {\n        return [\"chat\"];\n    }\n    get canUnpin() {\n        return (\n            this.parent_channel_id || this.allowedToUnpinChannelTypes.includes(this.channel_type)\n        );\n    }\n    /** @type {boolean} */\n    can_react = true;\n    chat_window = fields.One(\"ChatWindow\", {\n        inverse: \"thread\",\n    });\n    close_chat_window = fields.Attr(undefined, {\n        /** @this {import(\"models\").Thread} */\n        onUpdate() {\n            if (this.close_chat_window) {\n                this.close_chat_window = undefined;\n                this.closeChatWindow({ force: true });\n            }\n        },\n    });\n    composer = fields.One(\"Composer\", {\n        compute: () => ({}),\n        inverse: \"thread\",\n        onDelete: (r) => r.delete(),\n    });\n    counter = 0;\n    counter_bus_id = 0;\n    /** @type {string} */\n    description;\n    /** @type {string} */\n    display_name;\n    displayToSelf = fields.Attr(false, {\n        compute() {\n            return (\n                this.self_member_id?.is_pinned ||\n                ([\"channel\", \"group\"].includes(this.channel_type) &&\n                    this.hasSelfAsMember &&\n                    !this.parent_channel_id)\n            );\n        },\n        onUpdate() {\n            this.onPinStateUpdated();\n        },\n    });\n    followers = fields.Many(\"mail.followers\", {\n        /** @this {import(\"models\").Thread} */\n        onAdd(r) {\n            r.thread = this;\n        },\n        onDelete: (r) => r.delete(),\n    });\n    selfFollower = fields.One(\"mail.followers\", {\n        /** @this {import(\"models\").Thread} */\n        onAdd(r) {\n            r.thread = this;\n        },\n        onDelete: (r) => r.delete(),\n    });\n    /** @type {integer|undefined} */\n    followersCount;\n    loadOlder = false;\n    loadNewer = false;\n    get importantCounter() {\n        if (this.model === \"mail.box\") {\n            return this.counter;\n        }\n        return this.message_needaction_counter;\n    }\n    isDisplayed = fields.Attr(false, {\n        compute() {\n            return this.computeIsDisplayed();\n        },\n        onUpdate() {\n            this.isDisplayedOnUpdate();\n        },\n    });\n    isDisplayedOnUpdate() {}\n    get isFocused() {\n        return this.isFocusedCounter !== 0;\n    }\n    isFocusedCounter = fields.Attr(0, {\n        onUpdate() {\n            if (this.isFocusedCounter < 0) {\n                this.isFocusedCounter = 0;\n            }\n        },\n    });\n    isLoadingAttachments = false;\n    isLoadedDeferred = new Deferred();\n    isLoaded = fields.Attr(false, {\n        /** @this {import(\"models\").Thread} */\n        onUpdate() {\n            if (this.isLoaded) {\n                this.isLoadedDeferred.resolve();\n            } else {\n                const def = this.isLoadedDeferred;\n                this.isLoadedDeferred = new Deferred();\n                this.isLoadedDeferred.then(() => def.resolve());\n            }\n        },\n    });\n    message_main_attachment_id = fields.One(\"ir.attachment\");\n    message_needaction_counter = 0;\n    message_needaction_counter_bus_id = 0;\n    messageInEdition = fields.One(\"mail.message\", { inverse: \"threadAsInEdition\" });\n    /**\n     * Contains continuous sequence of messages to show in message list.\n     * Messages are ordered from older to most recent.\n     * There should not be any hole in this list: there can be unknown\n     * messages before start and after end, but there should not be any\n     * unknown in-between messages.\n     *\n     * Content should be fetched and inserted in a controlled way.\n     */\n    messages = fields.Many(\"mail.message\");\n    /**\n     * Phantom messages is a snapshot of `messages` while the thread is being loaded.\n     * In other words: when thread is not loaded or loading, phantom messages are the\n     * messages before thread loading.\n     */\n    phantomMessages = fields.Many(\"mail.message\");\n    /** @type {string} */\n    modelName;\n    /** @type {string} */\n    module_icon;\n    /**\n     * Contains messages received from the bus that are not yet inserted in\n     * `messages` list. This is a temporary storage to ensure nothing is lost\n     * when fetching newer messages.\n     */\n    pendingNewMessages = fields.Many(\"mail.message\");\n    needactionMessages = fields.Many(\"mail.message\", {\n        inverse: \"threadAsNeedaction\",\n        sort: (message1, message2) => message1.id - message2.id,\n    });\n    // FIXME: should be in the portal/frontend bundle but live chat can be loaded\n    // before portal resulting in the field not being properly initialized.\n    portal_partner = fields.One(\"res.partner\");\n    status = \"new\";\n    /**\n     * Stored scoll position of thread from top in ASC order.\n     *\n     * @type {number|'bottom'}\n     */\n    scrollTop = \"bottom\";\n    transientMessages = fields.Many(\"mail.message\");\n    /* The additional recipients are the recipients that are manually added\n     * by the user by using the \"To\" field of the Chatter. */\n    additionalRecipients = fields.Attr([]);\n    /* The suggested recipients are the recipients that are suggested by the\n     * current model and includes the recipients of the last message. (e.g: for\n     * a crm lead, the model will suggest the customer associated to the lead). */\n    suggestedRecipients = fields.Attr([]);\n    /** @type {String[]|undefined} */\n    partner_fields;\n    /** @type {String|undefined} */\n    primary_email_field;\n    hasLoadingFailed = false;\n    canPostOnReadonly;\n    /** @type {Boolean} */\n    is_editable;\n    /** @type {Boolean} */\n    isLocallyPinned = fields.Attr(false, {\n        onUpdate() {\n            this.onPinStateUpdated();\n        },\n    });\n    /** @type {\"not_fetched\"|\"pending\"|\"fetched\"} */\n    fetchMembersState = \"not_fetched\";\n    /** @type {integer|null} */\n    highlightMessage = fields.One(\"mail.message\");\n    /** @type {String|undefined} */\n    access_token;\n    /** @type {String|undefined} */\n    hash;\n    /**\n     * Partner id for non channel threads\n     *  @type {integer|undefined}\n     */\n    pid;\n\n    get accessRestrictedToGroupText() {\n        if (!this.group_public_id?.full_name) {\n            return false;\n        }\n        return _t('Access restricted to group \"%(groupFullName)s\"', {\n            groupFullName: this.group_public_id.full_name,\n        });\n    }\n\n    get busChannel() {\n        return `${this.model}_${this.id}`;\n    }\n\n    get followersFullyLoaded() {\n        return (\n            this.followersCount ===\n            (this.selfFollower ? this.followers.length + 1 : this.followers.length)\n        );\n    }\n\n    get attachmentsInWebClientView() {\n        const attachments = this.attachments.filter(\n            (attachment) => (attachment.isPdf || attachment.isImage) && !attachment.uploading\n        );\n        attachments.sort((a1, a2) => a2.id - a1.id);\n        return attachments;\n    }\n\n    get isUnread() {\n        return this.needactionMessages.length > 0;\n    }\n\n    get typesAllowingCalls() {\n        return [\"chat\", \"channel\", \"group\"];\n    }\n\n    get allowCalls() {\n        return (\n            !this.isTransient &&\n            this.typesAllowingCalls.includes(this.channel_type) &&\n            !this.correspondent?.persona.eq(this.store.odoobot)\n        );\n    }\n\n    /**\n     * Return the name of the given persona to display in the context of this\n     * thread.\n     *\n     * @param {import(\"models\").Persona} persona\n     * @returns {string}\n     */\n    getPersonaName(persona) {\n        return persona?.displayName || persona?.name;\n    }\n\n    get hasAttachmentPanel() {\n        return this.model === \"discuss.channel\";\n    }\n\n    get isChatChannel() {\n        return [\"chat\", \"group\"].includes(this.channel_type);\n    }\n\n    get supportsCustomChannelName() {\n        return this.isChatChannel && this.channel_type !== \"group\";\n    }\n\n    get displayName() {\n        return this.display_name;\n    }\n\n    computeIsDisplayed() {\n        return this.store.ChatWindow.get({ thread: this })?.isOpen;\n    }\n\n    get avatarUrl() {\n        return this.module_icon ?? this.store.DEFAULT_AVATAR;\n    }\n\n    get allowDescription() {\n        return [\"channel\", \"group\"].includes(this.channel_type);\n    }\n\n    get fullNameWithParent() {\n        const text = this.parent_channel_id\n            ? `${this.parent_channel_id.displayName} > ${this.displayName}`\n            : this.displayName;\n        return text;\n    }\n\n    get isTransient() {\n        return !this.id || this.id < 0;\n    }\n\n    get lastEditableMessageOfSelf() {\n        const editableMessagesBySelf = this.nonEmptyMessages.filter(\n            (message) => message.isSelfAuthored && message.editable\n        );\n        if (editableMessagesBySelf.length > 0) {\n            return editableMessagesBySelf.at(-1);\n        }\n        return null;\n    }\n\n    get needactionCounter() {\n        return this.message_needaction_counter;\n    }\n\n    newestMessage = fields.One(\"mail.message\", {\n        inverse: \"threadAsNewest\",\n        compute() {\n            return this.messages.at(-1);\n        },\n    });\n\n    get newestPersistentMessage() {\n        return this.messages.findLast((msg) => Number.isInteger(msg.id));\n    }\n\n    newestPersistentAllMessages = fields.Many(\"mail.message\", {\n        compute() {\n            const allPersistentMessages = this.allMessages.filter((message) =>\n                Number.isInteger(message.id)\n            );\n            allPersistentMessages.sort((m1, m2) => m2.id - m1.id);\n            return allPersistentMessages;\n        },\n    });\n\n    newestPersistentOfAllMessage = fields.One(\"mail.message\", {\n        compute() {\n            return this.newestPersistentAllMessages[0];\n        },\n    });\n\n    get oldestPersistentMessage() {\n        return this.messages.find((msg) => Number.isInteger(msg.id));\n    }\n\n    onPinStateUpdated() {}\n\n    get invitationLink() {\n        if (!this.uuid || this.channel_type === \"chat\") {\n            return undefined;\n        }\n        return `${window.location.origin}/chat/${this.id}/${this.uuid}`;\n    }\n\n    get isEmpty() {\n        return this.messages.length === 0;\n    }\n\n    get nonEmptyMessages() {\n        return this.messages.filter((message) => !message.isEmpty);\n    }\n\n    get persistentMessages() {\n        return this.messages.filter((message) => !message.is_transient && !message.isPending);\n    }\n\n    get prefix() {\n        return this.isChatChannel ? \"@\" : \"#\";\n    }\n\n    get rpcParams() {\n        return {};\n    }\n\n    async checkReadAccess() {\n        await this.store.Thread.getOrFetch(this, [\"hasReadAccess\"]);\n        return this.hasReadAccess;\n    }\n\n    executeCommand(command, body = \"\") {\n        return this.store.env.services.orm.call(\n            \"discuss.channel\",\n            command.methodName,\n            [[this.id]],\n            { body }\n        );\n    }\n\n    /** @param {{after: Number, before: Number}} */\n    async fetchMessages({ after, around, before } = {}) {\n        this.status = \"loading\";\n        if (![\"mail.box\", \"discuss.channel\"].includes(this.model) && !this.id) {\n            this.isLoaded = true;\n            return [];\n        }\n        let res;\n        try {\n            res = await this.fetchMessagesData({ after, around, before });\n            this.hasLoadingFailed = false;\n        } catch (e) {\n            this.hasLoadingFailed = true;\n            this.isLoaded = true;\n            this.status = \"ready\";\n            throw e;\n        }\n        this.store.insert(res.data);\n        const msgs = this.store[\"mail.message\"].insert(res.messages.reverse());\n        this.isLoaded = true;\n        this.status = \"ready\";\n        return msgs;\n    }\n\n    /** @param {{after: Number, before: Number}} */\n    async fetchMessagesData({ after, around, before } = {}) {\n        // ordered messages received: newest to oldest\n        return await rpc(this.getFetchRoute(), {\n            ...this.getFetchParams(),\n            fetch_params: {\n                limit:\n                    !around && around !== 0 ? this.store.FETCH_LIMIT : this.store.FETCH_LIMIT * 2,\n                after,\n                around,\n                before,\n            },\n        });\n    }\n\n    /** @param {\"older\"|\"newer\"} epoch */\n    async fetchMoreMessages(epoch = \"older\") {\n        if (\n            this.status === \"loading\" ||\n            (epoch === \"older\" && !this.loadOlder) ||\n            (epoch === \"newer\" && !this.loadNewer)\n        ) {\n            return;\n        }\n        const before = epoch === \"older\" ? this.oldestPersistentMessage?.id : undefined;\n        const after = epoch === \"newer\" ? this.newestPersistentMessage?.id : undefined;\n        let fetched = [];\n        try {\n            fetched = await this.fetchMessages({ after, before });\n        } catch {\n            return;\n        }\n        if (\n            (after !== undefined && !this.messages.some((message) => message.id === after)) ||\n            (before !== undefined && !this.messages.some((message) => message.id === before))\n        ) {\n            // there might have been a jump to message during RPC fetch.\n            // Abort feeding messages as to not put holes in message list.\n            return;\n        }\n        const alreadyKnownMessages = new Set(this.messages.map(({ id }) => id));\n        const messagesToAdd = fetched.filter((message) => !alreadyKnownMessages.has(message.id));\n        if (epoch === \"older\") {\n            this.messages.unshift(...messagesToAdd);\n        } else {\n            this.messages.push(...messagesToAdd);\n        }\n        if (fetched.length < this.store.FETCH_LIMIT) {\n            if (epoch === \"older\") {\n                this.loadOlder = false;\n            } else if (epoch === \"newer\") {\n                this.loadNewer = false;\n                const missingMessages = this.pendingNewMessages.filter(\n                    ({ id }) => !alreadyKnownMessages.has(id)\n                );\n                if (missingMessages.length > 0) {\n                    this.messages.push(...missingMessages);\n                    this.messages.sort((m1, m2) => m1.id - m2.id);\n                }\n            }\n        }\n        this._enrichMessagesWithTransient();\n        this.pendingNewMessages = [];\n    }\n\n    /**\n     * Get the effective persona performing actions on this thread.\n     * Priority order: logged-in user, portal partner (token-authenticated), guest.\n     *\n     * @returns {import(\"models\").Persona}\n     */\n    get effectiveSelf() {\n        return this.store.self_partner || this.store.self_guest;\n    }\n\n    async fetchNewMessages() {\n        if (\n            this.status === \"loading\" ||\n            (this.isLoaded && [\"discuss.channel\", \"mail.box\"].includes(this.model))\n        ) {\n            return;\n        }\n        const after = this.isLoaded ? this.newestPersistentMessage?.id : undefined;\n        let fetched = [];\n        try {\n            fetched = await this.fetchMessages({ after });\n        } catch {\n            return;\n        }\n        // feed messages\n        // could have received a new message as notification during fetch\n        // filter out already fetched (e.g. received as notification in the meantime)\n        let startIndex;\n        if (after === undefined) {\n            startIndex = 0;\n        } else {\n            const afterIndex = this.messages.findIndex((message) => message.id === after);\n            if (afterIndex === -1) {\n                // there might have been a jump to message during RPC fetch.\n                // Abort feeding messages as to not put holes in message list.\n                return;\n            } else {\n                startIndex = afterIndex + 1;\n            }\n        }\n        const alreadyKnownMessages = new Set(this.messages.map((m) => m.id));\n        const filtered = fetched.filter(\n            (message) =>\n                !alreadyKnownMessages.has(message.id) &&\n                (this.persistentMessages.length === 0 ||\n                    message.id < this.oldestPersistentMessage.id ||\n                    message.id > this.newestPersistentMessage.id)\n        );\n        this.messages.splice(startIndex, 0, ...filtered);\n        Object.assign(this, {\n            loadOlder:\n                after === undefined && fetched.length === this.store.FETCH_LIMIT\n                    ? true\n                    : after === undefined && fetched.length !== this.store.FETCH_LIMIT\n                    ? false\n                    : this.loadOlder,\n        });\n    }\n\n    getFetchParams() {\n        if (this.model === \"discuss.channel\") {\n            return { channel_id: this.id };\n        }\n        if (this.model === \"mail.box\") {\n            return {};\n        }\n        return {\n            thread_id: this.id,\n            thread_model: this.model,\n            ...this.rpcParams,\n        };\n    }\n\n    getFetchRoute() {\n        if (this.model === \"discuss.channel\") {\n            return \"/discuss/channel/messages\";\n        }\n        if (this.model === \"mail.box\" && this.id === \"inbox\") {\n            return `/mail/inbox/messages`;\n        }\n        if (this.model === \"mail.box\" && this.id === \"starred\") {\n            return `/mail/starred/messages`;\n        }\n        if (this.model === \"mail.box\" && this.id === \"history\") {\n            return `/mail/history/messages`;\n        }\n        return this.fetchRouteChatter;\n    }\n\n    get fetchRouteChatter() {\n        return \"/mail/thread/messages\";\n    }\n\n    /**\n     * Get ready to jump to a message in a thread. This method will fetch the\n     * messages around the message to jump to if required, and update the thread\n     * messages accordingly.\n     *\n     * @param {import(\"models\").Message} [messageId] if not provided, load around newest message\n     */\n    async loadAround(messageId) {\n        if (\n            this.status === \"loading\" ||\n            (this.isLoaded && this.messages.some(({ id }) => id === messageId))\n        ) {\n            return;\n        }\n        this.isLoaded = false;\n        this.scrollTop = undefined;\n        try {\n            this.phantomMessages = this.messages;\n            this.messages = await this.fetchMessages({ around: messageId });\n            this.phantomMessages = [];\n        } catch {\n            this.isLoaded = true;\n            return;\n        }\n        this.isLoaded = true;\n        this.loadNewer = messageId !== undefined ? true : false;\n        this.loadOlder = true;\n        const limit =\n            !messageId && messageId !== 0 ? this.store.FETCH_LIMIT : this.store.FETCH_LIMIT * 2;\n        if (this.messages.length < limit) {\n            const olderMessagesCount = this.messages.filter(({ id }) => id < messageId).length;\n            const newerMessagesCount = this.messages.filter(({ id }) => id > messageId).length;\n            if (olderMessagesCount < limit / 2 - 1) {\n                this.loadOlder = false;\n            }\n            if (newerMessagesCount < limit / 2) {\n                this.loadNewer = false;\n            }\n        }\n        this._enrichMessagesWithTransient();\n    }\n\n    async markAllMessagesAsRead() {\n        await this.store.env.services.orm.silent.call(\"mail.message\", \"mark_all_as_read\", [\n            [\n                [\"model\", \"=\", this.model],\n                [\"res_id\", \"=\", this.id],\n            ],\n        ]);\n        this.message_needaction_counter = 0;\n    }\n\n    async markAsFetched() {\n        await this.store.env.services.orm.silent.call(\"discuss.channel\", \"channel_fetched\", [\n            [this.id],\n        ]);\n    }\n\n    /**\n     * @param {Object} [options] used in overrides\n     */\n    markAsRead(options) {\n        const newestPersistentMessage = this.newestPersistentOfAllMessage;\n        if (!newestPersistentMessage && !this.isLoaded) {\n            this.isLoadedDeferred\n                .then(() => new Promise(setTimeout))\n                .then(() => this.markAsRead(options));\n            return;\n        }\n        if (this.message_needaction_counter > 0) {\n            this.markAllMessagesAsRead();\n        }\n    }\n\n    /** @param {string} data base64 representation of the binary */\n    async notifyAvatarToServer(data) {\n        await rpc(\"/discuss/channel/update_avatar\", {\n            channel_id: this.id,\n            data,\n        });\n    }\n\n    async notifyDescriptionToServer(description) {\n        this.description = description;\n        return this.store.env.services.orm.call(\n            \"discuss.channel\",\n            \"channel_change_description\",\n            [[this.id]],\n            { description }\n        );\n    }\n\n    /** @param {import(\"models\").Message} message */\n    onNewSelfMessage(message) {}\n\n    /**\n     * @param {Object} [options]\n     * @return {boolean} true if the thread was opened, false otherwise\n     */\n    open(options) {\n        return false;\n    }\n\n    async openChatWindow({ focus = false, fromMessagingMenu, bypassCompact, swapOpened } = {}) {\n        const thread = await this.store.Thread.getOrFetch(this);\n        if (!thread) {\n            return;\n        }\n        await this.store.chatHub.initPromise;\n        const cw = this.store.ChatWindow.insert(\n            assignDefined({ thread: this }, { fromMessagingMenu, bypassCompact })\n        );\n        cw.open({ focus, swapOpened });\n        return cw;\n    }\n\n    async closeChatWindow(options = {}) {\n        await this.store.chatHub.initPromise;\n        const chatWindow = this.store.ChatWindow.get({ thread: this });\n        await chatWindow?.close({ notifyState: false, ...options });\n    }\n\n    /** @param {string} name */\n    async rename(name) {\n        const newName = name.trim();\n        if (\n            newName !== this.displayName &&\n            ((newName && this.channel_type === \"channel\") || this.isChatChannel)\n        ) {\n            if (this.channel_type === \"channel\" || this.channel_type === \"group\") {\n                this.name = newName;\n                await this.store.env.services.orm.call(\n                    \"discuss.channel\",\n                    \"channel_rename\",\n                    [[this.id]],\n                    { name: newName }\n                );\n            } else if (this.supportsCustomChannelName) {\n                if (this.self_member_id) {\n                    this.self_member_id.custom_channel_name = newName;\n                }\n                await this.store.env.services.orm.call(\n                    \"discuss.channel\",\n                    \"channel_set_custom_name\",\n                    [[this.id]],\n                    { name: newName }\n                );\n            }\n        }\n    }\n\n    addOrReplaceMessage(message, tmpMsg) {\n        // The message from other personas (not self) should not replace the tmpMsg\n        if (tmpMsg && tmpMsg.in(this.messages) && this.effectiveSelf.eq(message.author)) {\n            this.messages.splice(this.messages.indexOf(tmpMsg), 1, message);\n            return;\n        }\n        this.messages.add(message);\n    }\n\n    /**\n     *  @param {ReturnType<import(\"@odoo/owl\").markup>} body\n     *  @param {Object} extraData\n     */\n    async post(body, postData = {}, extraData = {}) {\n        let tmpMsg;\n        postData.attachments = postData.attachments ? [...postData.attachments] : []; // to not lose them on composer clear\n        const { attachments, parentId } = postData;\n        const params = await this.store.getMessagePostParams({ body, postData, thread: this });\n        Object.assign(params, extraData);\n        const tmpId = this.store.getNextTemporaryId();\n        params.context = { ...user.context, ...params.context, temporary_id: tmpId };\n        if (parentId) {\n            params.post_data.parent_id = parentId;\n        }\n        if (this.model !== \"discuss.channel\") {\n            params.thread_id = this.id;\n            params.thread_model = this.model;\n        } else {\n            const tmpData = {\n                id: tmpId,\n                attachment_ids: attachments,\n                res_id: this.id,\n                model: \"discuss.channel\",\n            };\n            if (this.store.self_partner) {\n                tmpData.author_id = this.store.self_partner;\n            } else {\n                tmpData.author_guest_id = this.store.self_guest;\n            }\n            if (parentId) {\n                tmpData.parent_id = this.store[\"mail.message\"].get(parentId);\n            }\n            tmpMsg = this.store[\"mail.message\"].insert({\n                ...tmpData,\n                body: await generateEmojisOnHtml(body),\n                isPending: true,\n                thread: this,\n            });\n            this.messages.push(tmpMsg);\n            this.onNewSelfMessage(tmpMsg);\n        }\n        const data = await this.store.doMessagePost(params, tmpMsg);\n        if (!data) {\n            return;\n        }\n        this.store.insert(data.store_data);\n        /** @type {import(\"models\").Message} */\n        const message = this.store[\"mail.message\"].get(data.message_id);\n        this.addOrReplaceMessage(message, tmpMsg);\n        this.onNewSelfMessage(message);\n        // Only delete the temporary message now that seen_message_id is updated\n        // to avoid flickering.\n        tmpMsg?.delete();\n        if (message.hasLink && this.store.hasLinkPreviewFeature) {\n            rpc(\"/mail/link_preview\", { message_id: message.id }, { silent: true });\n        }\n        return message;\n    }\n\n    /** @param {number} index */\n    async setMainAttachmentFromIndex(index) {\n        this.message_main_attachment_id = this.attachmentsInWebClientView[index];\n        await this.store.env.services.orm.call(\"ir.attachment\", \"register_as_main_attachment\", [\n            this.message_main_attachment_id.id,\n        ]);\n    }\n\n    /**\n     * Following a load more or load around, listing of messages contains persistent messages.\n     * Transient messages are missing, so this function puts known transient messages at the\n     * right place in message list of thread.\n     */\n    _enrichMessagesWithTransient() {\n        for (const message of this.transientMessages) {\n            if (message.id < this.oldestPersistentMessage && !this.loadOlder) {\n                this.messages.unshift(message);\n            } else if (message.id > this.newestPersistentMessage && !this.loadNewer) {\n                this.messages.push(message);\n            } else {\n                let afterIndex = this.messages.findIndex((msg) => msg.id > message.id);\n                if (afterIndex === -1) {\n                    afterIndex = this.messages.length + 1;\n                }\n                this.messages.splice(afterIndex - 1, 0, message);\n            }\n        }\n    }\n\n    async leaveChannel({ force = false } = {}) {\n        if (\n            this.channel_type !== \"group\" &&\n            this.create_uid?.eq(this.store.self.main_user_id) &&\n            !force\n        ) {\n            await this.askLeaveConfirmation(\n                _t(\"You are the administrator of this channel. Are you sure you want to leave?\")\n            );\n        }\n        if (this.channel_type === \"group\" && !force) {\n            await this.askLeaveConfirmation(\n                _t(\n                    \"You are about to leave this group conversation and will no longer have access to it unless you are invited again. Are you sure you want to continue?\"\n                )\n            );\n        }\n        await this.closeChatWindow();\n        await this.store.env.services.orm.silent.call(\"discuss.channel\", \"action_unfollow\", [\n            this.id,\n        ]);\n    }\n\n    _getActualModelName() {\n        return this.model === \"discuss.channel\" ? \"discuss.channel\" : \"mail.thread\";\n    }\n}\n\nThread.register();\n", "import { OR, fields, Record } from \"./record\";\n\nexport class Volume extends Record {\n    static id = OR(\"partner_id\", \"guest_id\");\n\n    partner_id = fields.One(\"res.partner\");\n    guest_id = fields.One(\"mail.guest\");\n    get persona() {\n        return this.partner_id || this.guest_id;\n    }\n    volume = 1;\n}\n\nVolume.register();\n", "import { DiscussSidebar } from \"@mail/core/public_web/discuss_sidebar\";\nimport { useMessageScrolling } from \"@mail/utils/common/hooks\";\n\nimport { Component, useRef, useExternalListener, useEffect, useSubEnv } from \"@odoo/owl\";\nimport { getActiveHotkey } from \"@web/core/hotkeys/hotkey_service\";\n\nimport { useService } from \"@web/core/utils/hooks\";\nimport { DiscussContent } from \"@mail/core/public_web/discuss_content\";\nimport { MessagingMenu } from \"@mail/core/public_web/messaging_menu\";\n\nexport class Discuss extends Component {\n    static components = {\n        DiscussContent,\n        DiscussSidebar,\n        MessagingMenu,\n    };\n    static props = {\n        hasSidebar: { type: Boolean, optional: true },\n        thread: { optional: true },\n    };\n    static defaultProps = { hasSidebar: true };\n    static template = \"mail.Discuss\";\n\n    setup() {\n        super.setup();\n        this.store = useService(\"mail.store\");\n        this.messageHighlight = useMessageScrolling();\n        this.root = useRef(\"root\");\n        this.orm = useService(\"orm\");\n        this.effect = useService(\"effect\");\n        this.ui = useService(\"ui\");\n        useSubEnv({\n            inDiscussApp: true,\n            messageHighlight: this.messageHighlight,\n        });\n        useExternalListener(\n            window,\n            \"keydown\",\n            (ev) => {\n                if (getActiveHotkey(ev) === \"escape\" && !this.thread?.composer?.isFocused) {\n                    if (this.thread?.composer) {\n                        this.thread.composer.autofocus++;\n                    }\n                }\n                if (getActiveHotkey(ev) === \"control+k\") {\n                    this.store.env.services.command.openMainPalette({ searchValue: \"@\" });\n                    ev.preventDefault();\n                    ev.stopPropagation();\n                }\n            },\n            { capture: true }\n        );\n        if (this.store.inPublicPage) {\n            useEffect(\n                (thread, isSmall) => {\n                    if (!thread) {\n                        return;\n                    }\n                    if (isSmall) {\n                        this.thread\n                            .openChatWindow({ focus: true })\n                            .then((chatWindow) => (this.chatWindow = chatWindow));\n                    } else {\n                        this.chatWindow?.close();\n                    }\n                },\n                () => [this.thread, this.ui.isSmall]\n            );\n        }\n    }\n\n    get thread() {\n        return this.props.thread || this.store.discuss.thread;\n    }\n}\n", "import { fields, Record } from \"@mail/core/common/record\";\nimport { browser } from \"@web/core/browser/browser\";\n\nconst NO_MEMBERS_DEFAULT_OPEN_LS = \"mail.user_setting.no_members_default_open\";\nexport const DISCUSS_SIDEBAR_COMPACT_LS = \"mail.user_setting.discuss_sidebar_compact\";\nexport const LAST_DISCUSS_ACTIVE_ID_LS = \"mail.user_setting.discuss_last_active_id\";\n\nexport class DiscussApp extends Record {\n    INSPECTOR_WIDTH = 300;\n    COMPACT_SIDEBAR_WIDTH = 60;\n    /** @type {'notification'|'channel'|'chat'|'livechat'|'inbox'} */\n    activeTab = \"notification\";\n    searchTerm = \"\";\n    isActive = false;\n    isMemberPanelOpenByDefault = fields.Attr(true, {\n        compute() {\n            return browser.localStorage.getItem(NO_MEMBERS_DEFAULT_OPEN_LS) !== \"true\";\n        },\n        /** @this {import(\"models\").DiscussApp} */\n        onUpdate() {\n            if (this.isMemberPanelOpenByDefault) {\n                browser.localStorage.removeItem(NO_MEMBERS_DEFAULT_OPEN_LS);\n            } else {\n                browser.localStorage.setItem(NO_MEMBERS_DEFAULT_OPEN_LS, \"true\");\n            }\n        },\n    });\n    isSidebarCompact = fields.Attr(false, {\n        compute() {\n            return browser.localStorage.getItem(DISCUSS_SIDEBAR_COMPACT_LS) === \"true\";\n        },\n        /** @this {import(\"models\").DiscussApp} */\n        onUpdate() {\n            if (this.isSidebarCompact) {\n                browser.localStorage.setItem(\n                    DISCUSS_SIDEBAR_COMPACT_LS,\n                    this.isSidebarCompact.toString()\n                );\n            } else {\n                browser.localStorage.removeItem(DISCUSS_SIDEBAR_COMPACT_LS);\n            }\n        },\n    });\n    lastActiveId = fields.Attr(undefined, {\n        /** @this {import(\"models\").DiscussApp} */\n        compute() {\n            return browser.localStorage.getItem(LAST_DISCUSS_ACTIVE_ID_LS) ?? undefined;\n        },\n        /** @this {import(\"models\").DiscussApp} */\n        onUpdate() {\n            if (this.lastActiveId) {\n                browser.localStorage.setItem(LAST_DISCUSS_ACTIVE_ID_LS, this.lastActiveId);\n            } else {\n                browser.localStorage.removeItem(LAST_DISCUSS_ACTIVE_ID_LS);\n            }\n        },\n    });\n    thread = fields.One(\"Thread\", {\n        /** @this {import(\"models\").DiscussApp} */\n        onUpdate() {\n            this._threadOnUpdate();\n        },\n    });\n    hasRestoredThread = false;\n\n    static new() {\n        const record = super.new(...arguments);\n        record.onStorage = record.onStorage.bind(record);\n        browser.addEventListener(\"storage\", record.onStorage);\n        return record;\n    }\n\n    delete() {\n        browser.removeEventListener(\"storage\", this.onStorage);\n        super.delete(...arguments);\n    }\n\n    onStorage(ev) {\n        if (ev.key === DISCUSS_SIDEBAR_COMPACT_LS) {\n            this.isSidebarCompact = ev.newValue === \"true\";\n        }\n        if (ev.key === NO_MEMBERS_DEFAULT_OPEN_LS) {\n            this.isMemberPanelOpenByDefault = ev.newValue !== \"true\";\n        }\n    }\n\n    _threadOnUpdate() {\n        this.lastActiveId = this.store.Thread.localIdToActiveId(this.thread?.localId);\n    }\n}\n\nDiscussApp.register();\n", "import { Discuss } from \"@mail/core/public_web/discuss\";\n\nimport { Component, onMounted, onWillStart, onWillUnmount, onWillUpdateProps } from \"@odoo/owl\";\n\nimport { registry } from \"@web/core/registry\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { router } from \"@web/core/browser/router\";\n\n/**\n * @typedef {Object} Props\n * @property {Object} action\n * @property {Object} action.context\n * @property {number} [action.context.active_id]\n * @property {Object} [action.params]\n * @property {number} [action.params.active_id]\n * @extends {Component<Props, Env>}\n */\nexport class DiscussClientAction extends Component {\n    static components = { Discuss };\n    static props = [\"*\"];\n    static template = \"mail.DiscussClientAction\";\n\n    setup() {\n        super.setup();\n        this.store = useService(\"mail.store\");\n        onWillStart(() => {\n            // bracket to avoid blocking rendering with restore promise\n            this.restoreDiscussThread(this.props);\n        });\n        onWillUpdateProps((nextProps) => {\n            // bracket to avoid blocking rendering with restore promise\n            this.restoreDiscussThread(nextProps);\n        });\n        onMounted(() => (this.store.discuss.isActive = true));\n        onWillUnmount(() => (this.store.discuss.isActive = false));\n    }\n\n    getActiveId(props) {\n        return (\n            props.action.context.active_id ??\n            props.action.params?.active_id ??\n            this.store.Thread.localIdToActiveId(this.store.discuss.thread?.localId) ??\n            (this.env.services.ui.isSmall ? undefined : this.store.discuss.lastActiveId)\n        );\n    }\n\n    /** @param {string} [rawActiveId] */\n    parseActiveId(rawActiveId) {\n        if (!rawActiveId) {\n            return undefined;\n        }\n        const [model, id] = rawActiveId.split(\"_\");\n        if (model === \"mail.box\") {\n            return [\"mail.box\", id];\n        }\n        return [model, parseInt(id)];\n    }\n\n    /**\n     * Restore the discuss thread according to the active_id in the action if\n     * necessary.\n     *\n     * @param {Props} props\n     */\n    async restoreDiscussThread(props) {\n        const rawActiveId = this.getActiveId(props);\n        const parsedActiveId = this.parseActiveId(rawActiveId);\n        if (!parsedActiveId) {\n            this.store.discuss.thread = undefined;\n            this.store.discuss.hasRestoredThread = true;\n            const odoobotChat = this.store.odoobot?.searchChat();\n            const selfMember = odoobotChat?.self_member_id;\n            if (odoobotChat && selfMember?.is_pinned && !selfMember.seen_message_id) {\n                odoobotChat.setAsDiscussThread(false);\n            }\n            return;\n        }\n        const [model, id] = parsedActiveId;\n        const activeThread = await this.store.Thread.getOrFetch({ model, id });\n        if (activeThread && activeThread.notEq(this.store.discuss.thread)) {\n            const highlight_message_id =\n                props.action?.params?.highlight_message_id || router.current.highlight_message_id;\n            if (highlight_message_id) {\n                activeThread.highlightMessage = highlight_message_id;\n                delete props.action?.params?.highlight_message_id;\n                delete router.current?.highlight_message_id;\n            }\n            activeThread.setAsDiscussThread(false);\n        }\n        this.store.discuss.hasRestoredThread = true;\n    }\n}\n\nregistry.category(\"actions\").add(\"mail.action_discuss\", DiscussClientAction);\n", "import { Component, useEffect, useRef, useState } from \"@odoo/owl\";\n\nimport { useThreadActions } from \"@mail/core/common/thread_actions\";\nimport { AutoresizeInput } from \"@mail/core/common/autoresize_input\";\nimport { ActionList } from \"@mail/core/common/action_list\";\nimport { Thread } from \"@mail/core/common/thread\";\nimport { ThreadIcon } from \"@mail/core/common/thread_icon\";\nimport { Composer } from \"@mail/core/common/composer\";\nimport { ImStatus } from \"@mail/core/common/im_status\";\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { FileUploader } from \"@web/views/fields/file_handler\";\nimport { useService } from \"@web/core/utils/hooks\";\n\nexport class DiscussContent extends Component {\n    static components = {\n        ActionList,\n        AutoresizeInput,\n        Thread,\n        ThreadIcon,\n        Composer,\n        FileUploader,\n        ImStatus,\n    };\n    static props = [\"thread?\"];\n    static template = \"mail.DiscussContent\";\n\n    setup() {\n        super.setup();\n        this.store = useService(\"mail.store\");\n        this.ui = useService(\"ui\");\n        this.notification = useService(\"notification\");\n        this.threadActions = useThreadActions({ thread: () => this.thread });\n        this.root = useRef(\"root\");\n        this.state = useState({ jumpThreadPresent: 0 });\n        this.isDiscussContent = true;\n        useEffect(\n            () => this.actionPanelAutoOpenFn(),\n            () => [this.thread]\n        );\n    }\n\n    actionPanelAutoOpenFn() {\n        const memberListAction = this.threadActions.actions.find((a) => a.id === \"member-list\");\n        if (!memberListAction) {\n            return;\n        }\n        if (this.store.discuss.isMemberPanelOpenByDefault) {\n            if (!this.threadActions.activeAction) {\n                memberListAction.open();\n            } else if (this.threadActions.activeAction === memberListAction) {\n                return; // no-op (already open)\n            } else {\n                this.store.discuss.isMemberPanelOpenByDefault = false;\n            }\n        }\n    }\n\n    get thread() {\n        return this.props.thread || this.store.discuss.thread;\n    }\n\n    get showImStatus() {\n        return this.thread.channel_type === \"chat\";\n    }\n\n    get showThreadAvatar() {\n        return [\"channel\", \"group\", \"chat\"].includes(this.thread.channel_type);\n    }\n\n    get isThreadAvatarEditable() {\n        return (\n            !this.thread.parent_channel_id &&\n            this.thread.is_editable &&\n            [\"channel\", \"group\"].includes(this.thread.channel_type)\n        );\n    }\n\n    async onFileUploaded(file) {\n        await this.thread.notifyAvatarToServer(file.data);\n        this.notification.add(_t(\"The avatar has been updated!\"), { type: \"success\" });\n    }\n\n    async renameGuest(name) {\n        const newName = name.trim();\n        if (this.store.self.name !== newName) {\n            await this.store.self.updateGuestName(newName);\n        }\n    }\n\n    async renameThread(name) {\n        await this.thread.rename(name);\n    }\n\n    async updateThreadDescription(description) {\n        const newDescription = description.trim();\n        if (!newDescription && !this.thread.description) {\n            return;\n        }\n        if (newDescription !== this.thread.description) {\n            await this.thread.notifyDescriptionToServer(newDescription);\n        }\n    }\n}\n", "import { useHover } from \"@mail/utils/common/hooks\";\nimport { Component } from \"@odoo/owl\";\nimport { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { useDropdownState } from \"@web/core/dropdown/dropdown_hooks\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { useService } from \"@web/core/utils/hooks\";\n\n/**\n * @typedef {Object} Props\n * @extends {Component<Props, Env>}\n */\nexport class DiscussSearch extends Component {\n    static template = \"mail.DiscussSearch\";\n    static props = [\"class?\"];\n    static components = { Dropdown };\n\n    setup() {\n        super.setup();\n        this.store = useService(\"mail.store\");\n        this.command = useService(\"command\");\n        this.ui = useService(\"ui\");\n        this.searchHover = useHover([\"search-btn\", \"search-floating\"], {\n            onHover: () => {\n                if (this.store.discuss.isSidebarCompact) {\n                    this.searchFloating.isOpen = true;\n                }\n            },\n            onAway: () => {\n                if (this.store.discuss.isSidebarCompact) {\n                    this.searchFloating.isOpen = false;\n                }\n            },\n        });\n        this.meetingHover = useHover([\"meeting-btn\", \"meeting-floating\"], {\n            onHover: () => {\n                if (this.store.discuss.isSidebarCompact) {\n                    this.meetingFloating.isOpen = true;\n                }\n            },\n            onAway: () => {\n                if (this.store.discuss.isSidebarCompact) {\n                    this.meetingFloating.isOpen = false;\n                }\n            },\n        });\n        this.searchFloating = useDropdownState();\n        this.meetingFloating = useDropdownState();\n    }\n\n    get class() {\n        if (typeof this.props.class === \"object\" && this.props.class !== null) {\n            return Object.entries(this.props.class)\n                .filter(([_, val]) => val)\n                .map(([key, _]) => key)\n                .join(\" \");\n        }\n        return this.props.class;\n    }\n\n    get newMeetingText() {\n        return _t(\"New Meeting\");\n    }\n\n    onClickNewMeeting() {\n        this.store.startMeeting();\n        if (this.env.inMessagingMenu) {\n            this.env.inMessagingMenu.dropdown.close();\n        }\n    }\n\n    onClickSearchConversations() {\n        this.command.openMainPalette({ searchValue: \"@\" });\n    }\n}\n", "import { Component, onMounted, useSubEnv } from \"@odoo/owl\";\nimport { ActionList } from \"../common/action_list\";\nimport { DiscussSearch } from \"./discuss_search\";\n\nimport { registry } from \"@web/core/registry\";\nimport { ResizablePanel } from \"@web/core/resizable_panel/resizable_panel\";\nimport { useService } from \"@web/core/utils/hooks\";\n\nexport const discussSidebarItemsRegistry = registry.category(\"mail.discuss_sidebar_items\");\n\n/**\n * @typedef {Object} Props\n * @extends {Component<Props, Env>}\n */\nexport class DiscussSidebar extends Component {\n    static template = \"mail.DiscussSidebar\";\n    static props = {};\n    static components = { ActionList, DiscussSearch, ResizablePanel };\n\n    setup() {\n        super.setup();\n        this.store = useService(\"mail.store\");\n        this.ui = useService(\"ui\");\n        useSubEnv({ inDiscussSidebar: true });\n        onMounted(() => {\n            this.mounted = true;\n        });\n    }\n\n    get discussSidebarItems() {\n        return discussSidebarItemsRegistry.getAll();\n    }\n\n    onResize(width) {\n        if (!this.mounted) {\n            return; // ignore resize from mount not triggered by user\n        }\n        this.store.discuss.isSidebarCompact = width <= 100;\n    }\n}\n", "import { CountryFlag } from \"@mail/core/common/country_flag\";\nimport { ImStatus } from \"@mail/core/common/im_status\";\nimport { NotificationItem } from \"@mail/core/public_web/notification_item\";\nimport { useDiscussSystray } from \"@mail/utils/common/hooks\";\n\nimport { Component, useExternalListener, useRef, useState, useSubEnv } from \"@odoo/owl\";\n\nimport { hasTouch, isDisplayStandalone, isIOS } from \"@web/core/browser/feature_detection\";\nimport { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { useDropdownState } from \"@web/core/dropdown/dropdown_hooks\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { getActiveHotkey } from \"@web/core/hotkeys/hotkey_service\";\nimport { DiscussContent } from \"./discuss_content\";\n\nexport class MessagingMenu extends Component {\n    static components = { CountryFlag, DiscussContent, Dropdown, NotificationItem, ImStatus };\n    static props = [];\n    static template = \"mail.MessagingMenu\";\n\n    setup() {\n        super.setup();\n        this.isIosPwa = isIOS() && isDisplayStandalone();\n        this.discussSystray = useDiscussSystray();\n        this.store = useService(\"mail.store\");\n        this.hasTouch = hasTouch;\n        this.ui = useService(\"ui\");\n        this.state = useState({\n            activeIndex: null,\n            adding: false,\n        });\n        this.dropdown = useDropdownState();\n        this.notificationList = useRef(\"notification-list\");\n        useSubEnv({ inMessagingMenu: { dropdown: this.dropdown } });\n\n        useExternalListener(window, \"keydown\", this.onKeydown, true);\n    }\n\n    onClickThread(isMarkAsRead, thread, message) {\n        if (!isMarkAsRead) {\n            if (message?.needaction && message.message_type === \"user_notification\") {\n                this.store.inbox.highlightMessage = message;\n                this.store.inbox.open();\n                return;\n            }\n            thread.open({ focus: true, fromMessagingMenu: true, bypassCompact: true });\n            this.dropdown.close();\n            return;\n        }\n        this.markAsRead(thread);\n    }\n\n    onClickInboxMsg(isMarkAsRead, msg) {\n        if (!isMarkAsRead) {\n            this.store.inbox.highlightMessage = msg;\n            this.env.services.action.doAction({\n                tag: \"mail.action_discuss\",\n                type: \"ir.actions.client\",\n                context: { active_id: \"mail.box_inbox\" },\n            });\n            return;\n        }\n        msg.setDone();\n    }\n\n    markAsRead(thread) {\n        if (thread.needactionMessages.length > 0) {\n            thread.markAllMessagesAsRead();\n        }\n    }\n\n    navigate(direction) {\n        if (this.notificationItems.length === 0) {\n            return;\n        }\n        const activeOptionId = this.state.activeIndex !== null ? this.state.activeIndex : 0;\n        let targetId = undefined;\n        switch (direction) {\n            case \"first\":\n                targetId = 0;\n                break;\n            case \"last\":\n                targetId = this.notificationItems.length - 1;\n                break;\n            case \"previous\":\n                targetId = activeOptionId - 1;\n                if (targetId < 0) {\n                    this.navigate(\"last\");\n                    return;\n                }\n                break;\n            case \"next\":\n                targetId = activeOptionId + 1;\n                if (targetId > this.notificationItems.length - 1) {\n                    this.navigate(\"first\");\n                    return;\n                }\n                break;\n            default:\n                return;\n        }\n        this.state.activeIndex = targetId;\n        this.notificationItems[targetId]?.scrollIntoView({ block: \"nearest\" });\n    }\n\n    onKeydown(ev) {\n        if (!this.dropdown.isOpen) {\n            return;\n        }\n        const hotkey = getActiveHotkey(ev);\n        switch (hotkey) {\n            case \"enter\":\n                if (this.state.activeIndex === null) {\n                    return;\n                }\n                this.notificationItems[this.state.activeIndex].click();\n                break;\n            case \"tab\":\n                this.navigate(this.state.activeIndex === null ? \"first\" : \"next\");\n                break;\n            case \"arrowup\":\n                this.navigate(this.state.activeIndex === null ? \"first\" : \"previous\");\n                break;\n            case \"arrowdown\":\n                this.navigate(this.state.activeIndex === null ? \"first\" : \"next\");\n                break;\n            default:\n                return;\n        }\n        ev.preventDefault();\n        ev.stopPropagation();\n    }\n\n    get notificationItems() {\n        return this.notificationList.el?.children ?? [];\n    }\n\n    get threads() {\n        return this.store.menuThreads;\n    }\n\n    get visibleStandaloneMessages() {\n        const tab = this.store.discuss.activeTab;\n        if (tab !== \"notification\") {\n            return [];\n        }\n        if (this.store.discuss.searchTerm) {\n            return [];\n        }\n        return this.store.standaloneInboxMessages;\n    }\n\n    /**\n     * @type {{ id: string, icon: string, label: string }[]}\n     */\n    get _tabs() {\n        return [\n            {\n                counter: this.store.discuss.chats.threadsWithCounter.length,\n                icon: \"oi oi-users\",\n                id: \"chat\",\n                label: _t(\"Chats\"),\n                sequence: 20,\n            },\n            {\n                channelHasUnread: Boolean(this.store.discuss.unreadChannels.length),\n                counter: this.store.discuss.channels.threadsWithCounter.length,\n                icon: \"fa fa-hashtag\",\n                id: \"channel\",\n                label: _t(\"Channels\"),\n                sequence: 40,\n            },\n        ];\n    }\n\n    get tabs() {\n        return this._tabs.sort((t1, t2) => t1.sequence - t2.sequence);\n    }\n\n    onClickNavTab(tabId) {\n        if (this.store.discuss.activeTab === tabId) {\n            return;\n        }\n        this.store.discuss.activeTab = tabId;\n        if (\n            this.store.discuss.activeTab === \"inbox\" &&\n            (!this.store.discuss.thread || this.store.discuss.thread.model !== \"mail.box\")\n        ) {\n            this.store.inbox.setAsDiscussThread();\n        }\n        if (this.store.discuss.activeTab === \"starred\") {\n            this.store.starred.setAsDiscussThread();\n        }\n        if (![\"inbox\", \"starred\"].includes(this.store.discuss.activeTab)) {\n            this.store.discuss.thread = undefined;\n        }\n    }\n\n    canUnpinItem(thread) {\n        return thread.canUnpin && thread.self_member_id?.message_unread_counter === 0;\n    }\n}\n\nregistry\n    .category(\"systray\")\n    .add(\"mail.messaging_menu\", { Component: MessagingMenu }, { sequence: 25 });\n", "import { isToday } from \"@mail/utils/common/dates\";\nimport { useHover } from \"@mail/utils/common/hooks\";\n\nimport { Component, useRef, useSubEnv } from \"@odoo/owl\";\n\nimport { ActionSwiper } from \"@web/core/action_swiper/action_swiper\";\nimport { useService } from \"@web/core/utils/hooks\";\n\nconst { DateTime } = luxon;\n\nexport class NotificationItem extends Component {\n    static components = { ActionSwiper };\n    static props = [\n        \"counter?\",\n        \"datetime?\",\n        \"first?\",\n        \"hasMarkAsReadButton?\",\n        \"iconSrc?\",\n        \"important?\",\n        \"muted?\",\n        \"onClick\",\n        \"onSwipeLeft?\",\n        \"onSwipeRight?\",\n        \"slots?\",\n        \"isActive?\",\n        \"nameMaxLine?\",\n        \"textMaxLine?\",\n        \"thread?\",\n    ];\n    static defaultProps = {\n        counter: 0,\n        muted: 0,\n    };\n    static template = \"mail.NotificationItem\";\n\n    setup() {\n        super.setup();\n        this.isToday = isToday;\n        this.DateTime = DateTime;\n        this.ui = useService(\"ui\");\n        this.store = useService(\"mail.store\");\n        this.markAsReadRef = useRef(\"markAsRead\");\n        this.rootHover = useHover(\"root\");\n        useSubEnv({ inNotificationItem: true });\n    }\n\n    get dateText() {\n        if (isToday(this.props.datetime)) {\n            return this.props.datetime?.toLocaleString(DateTime.TIME_SIMPLE);\n        }\n        if (this.props.datetime?.year === DateTime.now().year) {\n            return this.props.datetime?.toLocaleString({ month: \"short\", day: \"numeric\" });\n        }\n        return this.props.datetime?.toLocaleString(DateTime.DATE_MED);\n    }\n\n    onClick(ev) {\n        this.props.onClick(this.markAsReadRef.el?.contains(ev.target));\n    }\n\n    webkitLineClamp(maxLine) {\n        return `\n            display: -webkit-box;\n            overflow: hidden;\n            -webkit-box-orient: vertical;\n            -webkit-line-clamp: ${maxLine};\n        `;\n    }\n}\n", "import { OutOfFocusService, outOfFocusService } from \"@mail/core/common/out_of_focus_service\";\nimport { patch } from \"@web/core/utils/patch\";\n\npatch(OutOfFocusService.prototype, {\n    setup(env, services) {\n        super.setup(env, services);\n        this.titleService = services.title;\n        this.counter = 0;\n        this.contributingMessageLocalIds = new Set();\n        env.bus.addEventListener(\"window_focus\", () => this.onWindowFocus());\n    },\n    clearUnreadMessage() {\n        this.counter = 0;\n        this.contributingMessageLocalIds.clear();\n        this.titleService.setCounters({ discuss: undefined });\n    },\n    notify(message) {\n        if (this.contributingMessageLocalIds.has(message.localId)) {\n            return;\n        }\n        this.contributingMessageLocalIds.add(message.localId);\n        this.counter++;\n        this.titleService.setCounters({ discuss: this.counter });\n        super.notify(...arguments);\n    },\n    onWindowFocus() {\n        this.clearUnreadMessage();\n    },\n});\noutOfFocusService.dependencies = [...outOfFocusService.dependencies, \"title\"];\n", "import { Store, storeService } from \"@mail/core/common/store_service\";\nimport { fields } from \"@mail/core/common/record\";\nimport { router } from \"@web/core/browser/router\";\nimport { patch } from \"@web/core/utils/patch\";\n\npatch(Store.prototype, {\n    setup() {\n        super.setup(...arguments);\n        this.discuss = fields.One(\"DiscussApp\");\n        /** @type {number|undefined} */\n        this.action_discuss_id;\n    },\n    onStarted() {\n        super.onStarted(...arguments);\n        this.discuss = { activeTab: \"notification\" };\n        this.env.bus.addEventListener(\n            \"discuss.channel/new_message\",\n            ({ detail: { channel, message, silent } }) => {\n                if (this.env.services.ui.isSmall || message.isSelfAuthored || silent) {\n                    return;\n                }\n                channel.notifyMessageToUser(message);\n            }\n        );\n    },\n});\n\npatch(storeService, {\n    start(env, services) {\n        const store = super.start(...arguments);\n        const discussActionIds = [\"mail.action_discuss\", \"discuss\"];\n        if (store.action_discuss_id) {\n            discussActionIds.push(store.action_discuss_id);\n        }\n        store.discuss.isActive ||= discussActionIds.includes(router.current.action);\n        services.ui.bus.addEventListener(\"resize\", () => {\n            store.discuss.activeTab = \"notification\";\n            if (services.ui.isSmall && store.discuss.thread?.channel_type) {\n                store.discuss.activeTab = store.discuss.thread.channel_type;\n            }\n        });\n        return store;\n    },\n});\n", "import { registerThreadAction } from \"@mail/core/common/thread_actions\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { ACTION_TAGS } from \"@mail/core/common/action\";\n\nregisterThreadAction(\"leave\", {\n    condition: ({ owner, thread }) =>\n        (thread?.canLeave || thread?.canUnpin) && !owner.isDiscussContent,\n    icon: \"fa fa-fw fa-sign-out\",\n    name: ({ thread }) => (thread.canLeave ? _t(\"Leave Channel\") : _t(\"Unpin Conversation\")),\n    open: ({ thread }) => (thread.canLeave ? thread.leaveChannel() : thread.unpin()),\n    partition: ({ owner }) => owner.env.inChatWindow,\n    sequence: 10,\n    sequenceGroup: 40,\n    tags: ACTION_TAGS.DANGER,\n});\n", "import { patch } from \"@web/core/utils/patch\";\nimport { Thread } from \"@mail/core/common/thread_model\";\nimport { router } from \"@web/core/browser/router\";\nimport { ConfirmationDialog } from \"@web/core/confirmation_dialog/confirmation_dialog\";\nimport { _t } from \"@web/core/l10n/translation\";\n\npatch(Thread.prototype, {\n    /**\n     * Handle the notification of a new message based on the notification setting of the user.\n     * Thread on mute:\n     * 1. No longer see the unread status: the bold text disappears and the channel name fades out.\n     * 2. Without sound + need action counter.\n     * Thread Notification Type:\n     * All messages:All messages sound + need action counter\n     * Mentions:Only mention sounds + need action counter\n     * Nothing: No sound + need action counter\n     *\n     * @param {import(\"models\").Message} message\n     */\n    async notifyMessageToUser(message) {\n        const channel_notifications =\n            this.self_member_id?.custom_notifications || this.store.settings.channel_notifications;\n        if (\n            !this.self_member_id?.mute_until_dt &&\n            !this.store.self.im_status.includes(\"busy\") &&\n            (this.channel_type !== \"channel\" ||\n                (this.channel_type === \"channel\" &&\n                    (channel_notifications === \"all\" ||\n                        (channel_notifications === \"mentions\" &&\n                            message.partner_ids?.includes(this.store.self)))))\n        ) {\n            if (this.model === \"discuss.channel\" && this.inChathubOnNewMessage) {\n                await this.store.chatHub.initPromise;\n                let chatWindow = this.store.ChatWindow.get({ thread: this });\n                if (!chatWindow) {\n                    chatWindow = this.store.ChatWindow.insert({ thread: this });\n                    if (\n                        this.autoOpenChatWindowOnNewMessage &&\n                        this.store.chatHub.opened.length < this.store.chatHub.maxOpened\n                    ) {\n                        chatWindow.open();\n                    } else {\n                        chatWindow.fold();\n                    }\n                }\n            }\n            if (this.notifyWhenOutOfFocus) {\n                this.store.env.services[\"mail.out_of_focus\"].notify(message, this);\n            }\n        }\n    },\n    /** Condition for whether the conversation should become present in chat hub on new message */\n    get inChathubOnNewMessage() {\n        return !this.store.discuss.isActive;\n    },\n    get autoOpenChatWindowOnNewMessage() {\n        return false;\n    },\n    get notifyWhenOutOfFocus() {\n        return true;\n    },\n    /** @param {boolean} pushState */\n    setAsDiscussThread(pushState) {\n        if (pushState === undefined) {\n            pushState = this.notEq(this.store.discuss.thread);\n        }\n        this.store.discuss.thread = this;\n        this.store.discuss.activeTab = !this.store.env.services.ui.isSmall\n            ? \"notification\"\n            : this.model === \"mail.box\"\n            ? this.store.self.main_user_id?.notification_type === \"inbox\"\n                ? \"inbox\"\n                : \"starred\"\n            : [\"chat\", \"group\"].includes(this.channel_type)\n            ? \"chat\"\n            : \"channel\";\n        if (pushState) {\n            this.setActiveURL();\n        }\n        if (\n            this.store.env.services.ui.isSmall &&\n            this.model !== \"mail.box\" &&\n            !this.store.is_welcome_page_displayed\n        ) {\n            this.open({ focus: true });\n        }\n    },\n\n    setActiveURL() {\n        const activeId =\n            typeof this.id === \"string\" ? `mail.box_${this.id}` : `discuss.channel_${this.id}`;\n        router.pushState({ active_id: activeId });\n        if (\n            this.store.action_discuss_id &&\n            this.store.env.services.action?.currentController?.action.id ===\n                this.store.action_discuss_id\n        ) {\n            // Keep the action stack up to date (used by breadcrumbs).\n            this.store.env.services.action.currentController.action.context.active_id = activeId;\n        }\n    },\n    async unpin() {\n        this.isLocallyPinned = false;\n        if (this.eq(this.store.discuss.thread)) {\n            router.replaceState({ active_id: undefined });\n        }\n        if (this.model === \"discuss.channel\" && this.self_member_id?.is_pinned !== false) {\n            await this.store.env.services.orm.silent.call(\n                \"discuss.channel\",\n                \"channel_pin\",\n                [this.id],\n                { pinned: false }\n            );\n        }\n    },\n    /** @param {string} body */\n    async askLeaveConfirmation(body) {\n        await new Promise((resolve) => {\n            this.store.env.services.dialog.add(ConfirmationDialog, {\n                body: body,\n                confirmLabel: _t(\"Leave Conversation\"),\n                confirm: resolve,\n                cancel: () => {},\n            });\n        });\n    },\n});\n", "import { patch } from \"@web/core/utils/patch\";\nimport { Message } from \"@mail/core/common/message\";\nimport { onWillUnmount } from \"@odoo/owl\";\n\npatch(Message.prototype, {\n    setup() {\n        super.setup(...arguments);\n        this.state.lastReadMoreIndex = 0;\n        this.state.isReadMoreByIndex = new Map();\n        onWillUnmount(() => {\n            this.messageBody.el?.querySelector(\".o-mail-ellipsis\")?.remove();\n        });\n    },\n\n    /**\n     * @override\n     * @param {HTMLElement} bodyEl\n     */\n    prepareMessageBody(bodyEl) {\n        if (!bodyEl) {\n            return;\n        }\n        super.prepareMessageBody(...arguments);\n        Array.from(bodyEl.querySelectorAll(\".o-mail-ellipsis\")).forEach((el) => el.remove());\n        this.insertEllipsisbtn(bodyEl);\n    },\n\n    /**\n     * Modifies the message to add the 'ellipsis button' functionality\n     * All element nodes with 'data-o-mail-quote' attribute are concerned.\n     * All text nodes after a ``#stopSpelling`` element are concerned.\n     * Those text nodes need to be wrapped in a span (toggle functionality).\n     * All consecutive elements are joined in one 'ellipsis button'.\n     *\n     * @param {HTMLElement} bodyEl\n     */\n    insertEllipsisbtn(bodyEl) {\n        /**\n         * @param {HTMLElement} e\n         * @param {string} selector\n         */\n        function prevAll(e, selector) {\n            const res = [];\n            while ((e = e.previousElementSibling)) {\n                if (e.matches(selector)) {\n                    res.push(e);\n                }\n            }\n            return res;\n        }\n\n        /**\n         * @param {HTMLElement} e\n         * @param {string} selector\n         */\n        function prev(e, selector) {\n            while ((e = e.previousElementSibling)) {\n                if (e.matches(selector)) {\n                    return e;\n                }\n            }\n        }\n\n        /** @param {HTMLElement} el */\n        function hide(el) {\n            el.dataset.oMailDisplay = el.style.display;\n            el.style.display = \"none\";\n        }\n\n        /**\n         * @param {HTMLElement} el\n         * @param {boolean} condition\n         */\n        function toggle(el, condition = false) {\n            if (condition) {\n                let newDisplay = el.dataset.oMailDisplay;\n                if (newDisplay === \"none\") {\n                    newDisplay = null;\n                }\n                el.style.display = newDisplay;\n            } else {\n                hide(el);\n            }\n        }\n\n        const groups = [];\n        let ellipsisNodes;\n        const ELEMENT_NODE = 1;\n        const TEXT_NODE = 3;\n        /** @type {ChildNode[]} childrenEl */\n        const childrenEl = Array.from(bodyEl.childNodes).filter(\n            /** @param {ChildNode} childEl */\n            function (childEl) {\n                return (\n                    childEl.nodeType === ELEMENT_NODE ||\n                    (childEl.nodeType === TEXT_NODE && childEl.nodeValue.trim())\n                );\n            }\n        );\n        for (const childEl of childrenEl) {\n            // Hide Text nodes if \"stopSpelling\"\n            if (\n                childEl.nodeType === TEXT_NODE &&\n                prevAll(childEl, '[id*=\"stopSpelling\"]').length > 0\n            ) {\n                // Convert Text nodes to Element nodes\n                const newChildEl = document.createElement(\"span\");\n                newChildEl.textContent = childEl.textContent;\n                newChildEl.dataset.oMailQuote = \"1\";\n                childEl.parentNode.replaceChild(newChildEl, childEl);\n            }\n            // Create array for each 'read more' with nodes to toggle\n            if (\n                (childEl.nodeType === ELEMENT_NODE && childEl.getAttribute(\"data-o-mail-quote\")) ||\n                (childEl.nodeName === \"BR\" && prev(childEl, '[data-o-mail-quote=\"1\"]'))\n            ) {\n                if (!ellipsisNodes) {\n                    ellipsisNodes = [];\n                    groups.push(ellipsisNodes);\n                }\n                hide(childEl);\n                ellipsisNodes.push(childEl);\n            } else {\n                ellipsisNodes = undefined;\n                this.insertEllipsisbtn(childEl);\n            }\n        }\n\n        for (const group of groups) {\n            const index = this.state.lastReadMoreIndex++;\n            const ellipsisbtnEl = document.createElement(\"button\");\n            ellipsisbtnEl.className = \"o-mail-ellipsis badge rounded-pill border-0 py-0 px-1\";\n            const iconellipsisEl = document.createElement(\"i\");\n            iconellipsisEl.className = \"oi oi-ellipsis-h oi-large\";\n            ellipsisbtnEl.append(iconellipsisEl);\n            group[0].parentNode.insertBefore(ellipsisbtnEl, group[0]);\n            // Toggle All next nodes\n            if (!this.state.isReadMoreByIndex.has(index)) {\n                this.state.isReadMoreByIndex.set(index, true);\n            }\n            const updateFromState = () => {\n                const isReadMore = this.state.isReadMoreByIndex.get(index);\n                for (const childEl of group) {\n                    hide(childEl);\n                    toggle(childEl, !isReadMore);\n                }\n            };\n            ellipsisbtnEl.addEventListener(\"click\", (e) => {\n                e.preventDefault();\n                this.state.isReadMoreByIndex.set(index, !this.state.isReadMoreByIndex.get(index));\n                updateFromState();\n            });\n            updateFromState();\n        }\n    },\n});\n", "import { useAttachmentUploader } from \"@mail/core/common/attachment_uploader_hook\";\nimport { ActivityMailTemplate } from \"@mail/core/web/activity_mail_template\";\nimport { ActivityMarkAsDone } from \"@mail/core/web/activity_markasdone_popover\";\nimport { computeDelay, getMsToTomorrow } from \"@mail/utils/common/dates\";\nimport { AvatarCardPopover } from \"@mail/discuss/web/avatar_card/avatar_card_popover\";\n\nimport { Component, onMounted, onWillUnmount, useState } from \"@odoo/owl\";\n\nimport { browser } from \"@web/core/browser/browser\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { usePopover } from \"@web/core/popover/popover_hook\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { FileUploader } from \"@web/views/fields/file_handler\";\n\n/**\n * @typedef {Object} Props\n * @property {import(\"models\").Activity} activity\n * @property {function} onActivityChanged\n * @property {function} reloadParentView\n * @extends {Component<Props, Env>}\n */\nexport class Activity extends Component {\n    static components = { ActivityMailTemplate, FileUploader };\n    static props = [\"activity\", \"onActivityChanged\", \"reloadParentView\"];\n    static template = \"mail.Activity\";\n\n    setup() {\n        super.setup();\n        this.storeService = useService(\"mail.store\");\n        this.state = useState({ showDetails: false });\n        this.markDonePopover = usePopover(ActivityMarkAsDone, { position: \"right\" });\n        this.avatarCard = usePopover(AvatarCardPopover);\n        onMounted(() => {\n            this.updateDelayAtNight();\n        });\n        onWillUnmount(() => browser.clearTimeout(this.updateDelayMidnightTimeout));\n        this.attachmentUploader = useAttachmentUploader(this.thread);\n    }\n\n    get displayName() {\n        if (this.props.activity.summary) {\n            return _t(\"\u201c%s\u201d\", this.props.activity.summary);\n        }\n        return this.props.activity.display_name;\n    }\n\n    updateDelayAtNight() {\n        browser.clearTimeout(this.updateDelayMidnightTimeout);\n        this.updateDelayMidnightTimeout = browser.setTimeout(\n            () => this.render(),\n            getMsToTomorrow() + 100\n        ); // Make sure there is no race condition\n    }\n\n    get delay() {\n        return computeDelay(this.props.activity.date_deadline);\n    }\n\n    toggleDetails() {\n        this.state.showDetails = !this.state.showDetails;\n    }\n\n    async onClickMarkAsDone(ev) {\n        if (this.markDonePopover.isOpen) {\n            this.markDonePopover.close();\n            return;\n        }\n        this.markDonePopover.open(ev.currentTarget, {\n            activity: this.props.activity,\n            hasHeader: true,\n            onActivityChanged: this.props.onActivityChanged,\n        });\n    }\n\n    async onFileUploaded(data) {\n        const thread = this.thread;\n        const { id: attachmentId } = await this.attachmentUploader.uploadData(data, {\n            activity: this.props.activity,\n        });\n        await this.props.activity.markAsDone([attachmentId]);\n        this.props.onActivityChanged(thread);\n        await thread.fetchNewMessages();\n    }\n\n    onClickAvatar(ev) {\n        if (!this.props.activity.user_id) {\n            return;\n        }\n        const target = ev.currentTarget;\n        if (!this.avatarCard.isOpen) {\n            this.avatarCard.open(target, {\n                id: this.props.activity.user_id.id,\n            });\n        }\n    }\n\n    async edit() {\n        const thread = this.thread;\n        await this.props.activity.edit();\n        this.props.onActivityChanged(thread);\n    }\n\n    async unlink() {\n        const thread = this.thread;\n        this.props.activity.remove();\n        await this.env.services.orm.unlink(\"mail.activity\", [this.props.activity.id]);\n        this.props.onActivityChanged(thread);\n    }\n\n    get thread() {\n        return this.env.services[\"mail.store\"].Thread.insert({\n            model: this.props.activity.res_model,\n            id: this.props.activity.res_id,\n        });\n    }\n\n    /**\n     * @param {MouseEvent} ev\n     */\n    async onClick(ev) {\n        this.storeService.handleClickOnLink(ev, this.thread);\n    }\n}\n", "import { ActivityListPopover } from \"@mail/core/web/activity_list_popover\";\n\nimport { Component, useEnv, useRef } from \"@odoo/owl\";\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { usePopover } from \"@web/core/popover/popover_hook\";\n\nexport class ActivityButton extends Component {\n    static props = {\n        record: { type: Object },\n    };\n    static template = \"mail.ActivityButton\";\n\n    setup() {\n        super.setup();\n        this.popover = usePopover(ActivityListPopover, { position: \"bottom-start\" });\n        this.buttonRef = useRef(\"button\");\n        this.env = useEnv();\n        this.defaultActivityStateClass = \"text-muted\";\n        this.defaultActivityDecorationClass = \"fa-clock-o btn-link text-dark\";\n    }\n\n    get buttonClass() {\n        const classes = [];\n        switch (this.props.record.data.activity_state) {\n            case \"overdue\":\n                classes.push(\"text-danger\");\n                break;\n            case \"today\":\n                classes.push(\"text-warning\");\n                break;\n            case \"planned\":\n                classes.push(\"text-success\");\n                break;\n            default:\n                if (this.defaultActivityStateClass) {\n                    classes.push(this.activityStateClass);\n                }\n                break;\n        }\n        switch (this.props.record.data.activity_exception_decoration) {\n            case \"warning\":\n                classes.push(\"text-warning\");\n                classes.push(this.props.record.data.activity_exception_icon);\n                break;\n            case \"danger\":\n                classes.push(\"text-danger\");\n                classes.push(this.props.record.data.activity_exception_icon);\n                break;\n            default: {\n                const { activity_ids, activity_type_icon } = this.props.record.data;\n                if (activity_ids.records.length) {\n                    classes.push(activity_type_icon || \"fa-tasks\");\n                    break;\n                }\n                classes.push(this.defaultActivityDecorationClass);\n                break;\n            }\n        }\n        return classes.join(\" \");\n    }\n\n    get title() {\n        if (this.props.record.data.activity_exception_decoration) {\n            return _t(\"Warning\");\n        }\n        if (this.props.record.data.activity_summary) {\n            return this.props.record.data.activity_summary;\n        }\n        if (this.props.record.data.activity_type_id) {\n            return this.props.record.data.activity_type_id.display_name;\n        }\n        return _t(\"Show activities\");\n    }\n\n    async onClick() {\n        if (this.popover.isOpen) {\n            this.popover.close();\n        } else {\n            const resId = this.props.record.resId;\n            const selectedRecords = this.env?.model?.root?.selection ?? [];\n            const selectedIds = selectedRecords.map((r) => r.resId);\n            // If the current record is not selected, ignore the selection\n            const resIds =\n                selectedIds.includes(resId) && selectedIds.length > 1 ? selectedIds : undefined;\n            this.popover.open(this.buttonRef.el, {\n                activityIds: this.props.record.data.activity_ids.currentIds,\n                onActivityChanged: (thread) => {\n                    const recordToLoad = resIds ? selectedRecords : [this.props.record];\n                    recordToLoad.forEach((r) => r.load());\n                    this.onActivityChanged();\n                    this.popover.close();\n                },\n                resId,\n                resIds,\n                resModel: this.props.record.resModel,\n            });\n        }\n    }\n\n    /** Add custom behavior on activity changed */\n    onActivityChanged() {}\n}\n", "import { ActivityListPopoverItem } from \"@mail/core/web/activity_list_popover_item\";\nimport { compareDatetime } from \"@mail/utils/common/misc\";\n\nimport { Component, onWillUpdateProps } from \"@odoo/owl\";\n\nimport { useService } from \"@web/core/utils/hooks\";\n\n/**\n * @typedef {Object} Props\n * @property {number[]} activityIds\n * @property {function} close\n * @property {number} [defaultActivityTypeId]\n * @property {function} onActivityChanged\n * @property {number} resId\n * @property {string} resModel\n * @extends {Component<Props, Env>}\n */\nexport class ActivityListPopover extends Component {\n    static components = { ActivityListPopoverItem };\n    static props = [\n        \"activityIds\",\n        \"close\",\n        \"defaultActivityTypeId?\",\n        \"onActivityChanged\",\n        \"resId\",\n        /** Ids of record selection used to schedule activities in batch; it must include resId. */\n        \"resIds?\",\n        \"resModel\",\n    ];\n    static template = \"mail.ActivityListPopover\";\n\n    setup() {\n        super.setup();\n        this.orm = useService(\"orm\");\n        this.store = useService(\"mail.store\");\n        this.updateFromProps(this.props);\n        onWillUpdateProps((props) => this.updateFromProps(props));\n    }\n\n    get activities() {\n        /** @type {import(\"models\").Activity[]} */\n        const allActivities = Object.values(this.store[\"mail.activity\"].records);\n        return allActivities\n            .filter((activity) => this.props.activityIds.includes(activity.id))\n            .sort((a, b) => compareDatetime(a.date_deadline, b.date_deadline) || a.id - b.id);\n    }\n\n    onClickAddActivityButton() {\n        this.store\n            .scheduleActivity(\n                this.props.resModel,\n                this.props.resIds ? this.props.resIds : [this.props.resId],\n                this.props.defaultActivityTypeId\n            )\n            .then(() => this.props.onActivityChanged());\n        this.props.close();\n    }\n\n    get doneActivities() {\n        return this.activities.filter((activity) => activity.state === \"done\");\n    }\n\n    get overdueActivities() {\n        return this.activities.filter((activity) => activity.state === \"overdue\");\n    }\n\n    get plannedActivities() {\n        return this.activities.filter((activity) => activity.state === \"planned\");\n    }\n\n    get todayActivities() {\n        return this.activities.filter((activity) => activity.state === \"today\");\n    }\n\n    async updateFromProps(props) {\n        const data = await this.orm.silent.call(\"mail.activity\", \"activity_format\", [\n            props.activityIds,\n        ]);\n        this.store.insert(data);\n    }\n}\n", "import { useAttachmentUploader } from \"@mail/core/common/attachment_uploader_hook\";\nimport { ActivityMailTemplate } from \"@mail/core/web/activity_mail_template\";\nimport { ActivityMarkAsDone } from \"@mail/core/web/activity_markasdone_popover\";\nimport { computeDelay } from \"@mail/utils/common/dates\";\n\nimport { Component, useState } from \"@odoo/owl\";\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { FileUploader } from \"@web/views/fields/file_handler\";\n\n/**\n * @typedef {Object} Props\n * @property {import(\"models\").Activity} activity\n * @property {function} [onActivityChanged]\n * @property {function} [onClickDoneAndScheduleNext]\n * @property {function} onClickEditActivityButton\n * @extends {Component<Props, Env>}\n */\nexport class ActivityListPopoverItem extends Component {\n    static components = { ActivityMailTemplate, ActivityMarkAsDone, FileUploader };\n    static props = [\n        \"activity\",\n        \"onActivityChanged?\",\n        \"onClickDoneAndScheduleNext?\",\n        \"onClickEditActivityButton?\",\n    ];\n    static template = \"mail.ActivityListPopoverItem\";\n\n    setup() {\n        super.setup();\n        this.state = useState({ hasMarkDoneView: false });\n        if (this.props.activity.activity_category === \"upload_file\") {\n            this.attachmentUploader = useAttachmentUploader(\n                this.env.services[\"mail.store\"].Thread.insert({\n                    model: this.props.activity.res_model,\n                    id: this.props.activity.res_id,\n                })\n            );\n        }\n        this.closeMarkAsDone = this.closeMarkAsDone.bind(this);\n    }\n\n    closeMarkAsDone() {\n        this.state.hasMarkDoneView = false;\n    }\n\n    get delayLabel() {\n        const diff = computeDelay(this.props.activity.date_deadline);\n        if (diff === 0) {\n            return _t(\"Today\");\n        } else if (diff === -1) {\n            return _t(\"Yesterday\");\n        } else if (diff < 0) {\n            return _t(\"%s days overdue\", Math.round(Math.abs(diff)));\n        } else if (diff === 1) {\n            return _t(\"Tomorrow\");\n        } else {\n            return _t(\"Due in %s days\", Math.round(Math.abs(diff)));\n        }\n    }\n\n    get hasCancelButton() {\n        const activity = this.props.activity;\n        return activity.state !== \"done\" && activity.can_write;\n    }\n\n    get hasEditButton() {\n        const activity = this.props.activity;\n        return activity.state !== \"done\" && activity.can_write;\n    }\n\n    get hasFileUploader() {\n        const activity = this.props.activity;\n        return activity.state !== \"done\" && activity.activity_category === \"upload_file\";\n    }\n\n    get hasMarkDoneButton() {\n        return this.props.activity.state !== \"done\" && !this.hasFileUploader;\n    }\n\n    onClickEditActivityButton() {\n        this.props.onClickEditActivityButton();\n        this.props.activity.edit().then(() => this.props.onActivityChanged?.());\n    }\n\n    onClickMarkAsDone() {\n        this.state.hasMarkDoneView = !this.state.hasMarkDoneView;\n    }\n\n    async onFileUploaded(data) {\n        const { id: attachmentId } = await this.attachmentUploader.uploadData(data, {\n            activity: this.props.activity,\n        });\n        await this.props.activity.markAsDone([attachmentId]);\n        this.props.onActivityChanged?.();\n    }\n\n    unlink() {\n        this.props.activity.remove();\n        this.env.services.orm\n            .unlink(\"mail.activity\", [this.props.activity.id])\n            .then(() => this.props.onActivityChanged?.());\n    }\n}\n", "import { Component } from \"@odoo/owl\";\n\nimport { useService } from \"@web/core/utils/hooks\";\nimport { _t } from \"@web/core/l10n/translation\";\n\n/**\n * @typedef {Object} Props\n * @property {import(\"models\").Activity} activity\n * @property {function} [onClickButtons]\n * @property {function} [onActivityChanged]\n * @extends {Component<Props, Env>}\n */\nexport class ActivityMailTemplate extends Component {\n    static defaultProps = {\n        onClickButtons: () => {},\n    };\n    static props = [\"activity\", \"onClickButtons?\", \"onActivityChanged?\"];\n    static template = \"mail.ActivityMailTemplate\";\n\n    setup() {\n        super.setup();\n        this.store = useService(\"mail.store\");\n    }\n\n    /**\n     * @param {MouseEvent} ev\n     * @param {Object} mailTemplate\n     */\n    onClickPreview(ev, mailTemplate) {\n        ev.stopPropagation();\n        ev.preventDefault();\n        this.props.onClickButtons();\n        const action = {\n            name: _t(\"Compose Email\"),\n            type: \"ir.actions.act_window\",\n            res_model: \"mail.compose.message\",\n            views: [[false, \"form\"]],\n            target: \"new\",\n            context: {\n                default_res_ids: [this.props.activity.res_id],\n                default_model: this.props.activity.res_model,\n                default_subtype_xmlid: \"mail.mt_comment\",\n                default_template_id: mailTemplate.id,\n                force_email: true,\n            },\n        };\n        const thread = this.store.Thread.insert({\n            model: this.props.activity.res_model,\n            id: this.props.activity.res_id,\n        });\n        this.env.services.action.doAction(action, {\n            onClose: () => this.props.onActivityChanged?.(thread),\n        });\n    }\n\n    /**\n     * @param {MouseEvent} ev\n     * @param {Object} mailTemplate\n     */\n    async onClickSend(ev, mailTemplate) {\n        ev.stopPropagation();\n        ev.preventDefault();\n        this.props.onClickButtons();\n        const thread = this.store.Thread.insert({\n            model: this.props.activity.res_model,\n            id: this.props.activity.res_id,\n        });\n        await this.env.services.orm.call(this.props.activity.res_model, \"activity_send_mail\", [\n            [this.props.activity.res_id],\n            mailTemplate.id,\n        ]);\n        this.props.onActivityChanged?.(thread);\n    }\n}\n", "import { Component, onMounted, useExternalListener, useRef } from \"@odoo/owl\";\n\nexport class ActivityMarkAsDone extends Component {\n    static template = \"mail.ActivityMarkAsDone\";\n    static props = [\n        \"activity\",\n        \"close?\",\n        \"hasHeader?\",\n        \"onClickDoneAndScheduleNext?\",\n        \"onActivityChanged\",\n    ];\n    static defaultProps = {\n        hasHeader: false,\n    };\n\n    get isSuggested() {\n        return this.props.activity.chaining_type === \"suggest\";\n    }\n\n    setup() {\n        super.setup();\n        this.textArea = useRef(\"textarea\");\n        onMounted(() => {\n            this.textArea.el.focus();\n        });\n        useExternalListener(window, \"keydown\", this.onKeydown);\n    }\n\n    onKeydown(ev) {\n        if (ev.key === \"Escape\" && this.props.close) {\n            this.props.close();\n        }\n    }\n\n    async onClickDone() {\n        const { res_id, res_model } = this.props.activity;\n        const thread = this.env.services[\"mail.store\"].Thread.insert({\n            model: res_model,\n            id: res_id,\n        });\n        await this.props.activity.markAsDone();\n        this.props.onActivityChanged(thread);\n        await thread.fetchNewMessages();\n    }\n\n    async onClickDoneAndScheduleNext() {\n        const { res_id, res_model } = this.props.activity;\n        const thread = this.env.services[\"mail.store\"].Thread.insert({\n            model: res_model,\n            id: res_id,\n        });\n        if (this.props.onClickDoneAndScheduleNext) {\n            this.props.onClickDoneAndScheduleNext();\n        }\n        if (this.props.close) {\n            this.props.close();\n        }\n        const action = await this.props.activity.markAsDoneAndScheduleNext();\n        thread.fetchNewMessages();\n        this.props.onActivityChanged(thread);\n        if (!action) {\n            return;\n        }\n        await new Promise((resolve) => {\n            this.env.services.action.doAction(action, {\n                onClose: resolve,\n            });\n        });\n        this.props.onActivityChanged(thread);\n    }\n}\n", "import { Component } from \"@odoo/owl\";\n\nimport { useDiscussSystray } from \"@mail/utils/common/hooks\";\nimport { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { useDropdownState } from \"@web/core/dropdown/dropdown_hooks\";\nimport { registry } from \"@web/core/registry\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { Domain } from \"@web/core/domain\";\nimport { user } from \"@web/core/user\";\nimport { useCommand } from \"@web/core/commands/command_hook\";\nimport { _t } from \"@web/core/l10n/translation\";\n\nexport class ActivityMenu extends Component {\n    static components = { Dropdown };\n    static props = [];\n    static template = \"mail.ActivityMenu\";\n\n    setup() {\n        super.setup();\n        this.discussSystray = useDiscussSystray();\n        this.store = useService(\"mail.store\");\n        this.action = useService(\"action\");\n        this.userId = user.userId;\n        this.ui = useService(\"ui\");\n        this.dropdown = useDropdownState();\n        useCommand(_t(\"Activity\"), () => this.store.scheduleActivity(false, false), {\n            category: \"activity\",\n            hotkey: \"alt+shift+a\",\n            global: true,\n            hotkeyOptions: { bypassEditableProtection: true },\n            isAvailable: () =>\n                !this.ui.activeElement.querySelector(\n                    \"[data-hotkey='shift+a'], .o_mail_activity_schedule_wizard\"\n                ),\n        });\n    }\n\n    onBeforeOpen() {\n        this.store.fetchStoreData(\"systray_get_activities\");\n    }\n\n    availableViews(group) {\n        return [\n            [false, \"kanban\"],\n            [false, \"list\"],\n            [false, \"form\"],\n            [false, \"activity\"],\n        ];\n    }\n\n    openActivityGroup(group, filter = \"all\", newWindow) {\n        this.dropdown.close();\n        const context = {\n            // Necessary because activity_ids of mail.activity.mixin has auto_join\n            // So, duplicates are faking the count and \"Load more\" doesn't show up\n            force_search_count: 1,\n            search_default_filter_activities_my: 1,\n        };\n        if (group.model === \"mail.activity\") {\n            this.action.doAction(\"mail.mail_activity_without_access_action\", {\n                newWindow,\n                additionalContext: {\n                    active_ids: group.activity_ids,\n                    active_model: \"mail.activity\",\n                },\n            });\n            return;\n        }\n\n        if (filter === \"all\") {\n            context[\"search_default_activities_overdue\"] = 1;\n            context[\"search_default_activities_today\"] = 1;\n        } else if (filter === \"overdue\") {\n            context[\"search_default_activities_overdue\"] = 1;\n        } else if (filter === \"today\") {\n            context[\"search_default_activities_today\"] = 1;\n        } else if (filter === \"upcoming_all\") {\n            context[\"search_default_activities_upcoming_all\"] = 1;\n        }\n\n        let domain = [];\n        if (group.domain) {\n            domain = Domain.and([domain, group.domain]).toList();\n        }\n        const views = this.availableViews(group);\n\n        this.action.doAction(\n            {\n                context,\n                domain,\n                name: group.name,\n                res_model: group.model,\n                search_view_id: [false],\n                type: \"ir.actions.act_window\",\n                views,\n            },\n            {\n                newWindow,\n                clearBreadcrumbs: true,\n                viewType: group.view_type,\n            }\n        );\n    }\n\n    openMyActivities(newWindow) {\n        this.dropdown.close();\n        this.action.doAction(\"mail.mail_activity_action_my\", {\n            newWindow,\n            clearBreadcrumbs: true,\n        });\n    }\n}\n\nregistry\n    .category(\"systray\")\n    .add(\"mail.activity_menu\", { Component: ActivityMenu }, { sequence: 20 });\n", "import { Activity } from \"@mail/core/common/activity_model\";\nimport { formatDate, formatDateTime } from \"@web/core/l10n/dates\";\nimport { _t } from \"@web/core/l10n/translation\";\n\nimport { patch } from \"@web/core/utils/patch\";\n\npatch(Activity.prototype, {\n    setup() {\n        super.setup(...arguments);\n    },\n    get dateDeadlineFormatted() {\n        return formatDate(this.date_deadline);\n    },\n    get dateDoneFormatted() {\n        return formatDate(this.date_done);\n    },\n    get dateCreateFormatted() {\n        return formatDateTime(this.create_date);\n    },\n    async edit() {\n        await new Promise((resolve) =>\n            this.store.env.services.action.doAction(\n                {\n                    type: \"ir.actions.act_window\",\n                    name: _t(\"Schedule Activity\"),\n                    res_model: \"mail.activity\",\n                    view_mode: \"form\",\n                    views: [[false, \"form\"]],\n                    target: \"new\",\n                    res_id: this.id,\n                    context: {\n                        default_res_model: this.res_model,\n                        default_res_id: this.res_id,\n                        dialog_size: \"large\",\n                    },\n                },\n                {\n                    onClose: resolve,\n                }\n            )\n        );\n    },\n    /** @param {number[]} attachmentIds */\n    async markAsDone(attachmentIds = []) {\n        await this.store.env.services.orm.call(\"mail.activity\", \"action_feedback\", [[this.id]], {\n            attachment_ids: attachmentIds,\n            feedback: this.feedback,\n        });\n        this.store.activityBroadcastChannel?.postMessage({\n            type: \"RELOAD_CHATTER\",\n            payload: { id: this.res_id, model: this.res_model },\n        });\n    },\n    /** @returns {Promise<import(\"@web/webclient/actions/action_service\").ActionDescription>} */\n    async markAsDoneAndScheduleNext() {\n        const action = await this.store.env.services.orm.call(\n            \"mail.activity\",\n            \"action_feedback_schedule_next\",\n            [[this.id]],\n            { feedback: this.feedback }\n        );\n        this.activityBroadcastChannel?.postMessage({\n            type: \"RELOAD_CHATTER\",\n            payload: { id: this.res_id, model: this.res_model },\n        });\n        return action;\n    },\n    remove({ broadcast = true } = {}) {\n        this.delete();\n        if (broadcast) {\n            this.activityBroadcastChannel?.postMessage({\n                type: \"DELETE\",\n                payload: { id: this.id },\n            });\n        }\n    },\n});\n", "import { ChatWindow } from \"@mail/core/common/chat_window_model\";\n\nimport { patch } from \"@web/core/utils/patch\";\n\npatch(ChatWindow.prototype, {\n    async _onClose(options) {\n        if (\n            this.store.env.services.ui.isSmall &&\n            !this.store.discuss.isActive &&\n            this.fromMessagingMenu\n        ) {\n            // If we are in mobile and discuss is not open, it means the\n            // chat window was opened from the messaging menu. In that\n            // case it should be re-opened to simulate it was always\n            // there in the background.\n            document.querySelector(\".o_menu_systray i[aria-label='Messages']\")?.click();\n            // ensure messaging menu is opened before chat window is closed\n            await Promise.resolve();\n        }\n        await super._onClose(...arguments);\n    },\n});\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\n\n// Add an activity category for the command palette\nregistry.category(\"command_categories\").add(\"activity\", {}, { sequence: 45 });\n\nconst commandProviderRegistry = registry.category(\"command_provider\");\n\ncommandProviderRegistry.add(\"activity\", {\n    provide: (env, options) => [\n        {\n            name: _t(\"Show My Activities\"),\n            category: \"activity\",\n            action() {\n                env.services.action.doAction(\"mail.mail_activity_action_my\", {\n                    target: \"current\",\n                    clearBreadcrumbs: true,\n                });\n            },\n        },\n        {\n            name: _t(\"Show All Activities\"),\n            category: \"activity\",\n            action() {\n                env.services.action.doAction(\"mail.mail_activity_action\", {\n                    target: \"current\",\n                    clearBreadcrumbs: true,\n                });\n            },\n        },\n    ],\n});\n", "import { wrapInlinesInBlocks } from \"@html_editor/utils/dom\";\nimport { childNodes } from \"@html_editor/utils/dom_traversal\";\n\nimport { Composer } from \"@mail/core/common/composer\";\n\nimport { markup } from \"@odoo/owl\";\n\nimport { createDocumentFragmentFromContent } from \"@web/core/utils/html\";\nimport { patch } from \"@web/core/utils/patch\";\nimport { renderToElement } from \"@web/core/utils/render\";\n\npatch(Composer.prototype, {\n    /**\n     * Construct an editor friendly html representation of the body.\n     *\n     * @param {string|ReturnType<markup>} defaultBody\n     * @param {string|ReturnType<markup>} [signature=\"\"]\n     * @returns {ReturnType<markup>}\n     */\n    formatDefaultBodyForFullComposer(defaultBody, signature = \"\") {\n        const fragment = createDocumentFragmentFromContent(defaultBody).body;\n        if (!fragment.firstChild) {\n            fragment.append(document.createElement(\"BR\"));\n        }\n        if (signature) {\n            const signatureEl = renderToElement(\"html_editor.Signature\", {\n                signature,\n                signatureClass: \"o-signature-container\",\n            });\n            fragment.append(signatureEl);\n        }\n        const container = document.createElement(\"DIV\");\n        container.append(...childNodes(fragment));\n        wrapInlinesInBlocks(container, { baseContainerNodeName: \"DIV\" });\n        return markup(container.innerHTML);\n    },\n});\n", "import { Dialog } from \"@web/core/dialog/dialog\";\nimport { patch } from \"@web/core/utils/patch\";\n\npatch(Dialog.prototype, {\n    /**\n     * @override\n     */\n    onEscape() {\n        if (this.data.model === \"mail.compose.message\") {\n            return;\n        }\n        super.onEscape();\n    },\n});\n", "import { useEffect } from \"@odoo/owl\";\n\nimport { Discuss } from \"@mail/core/public_web/discuss\";\nimport { MessagingMenu } from \"@mail/core/public_web/messaging_menu\";\n\nimport { ControlPanel } from \"@web/search/control_panel/control_panel\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { patch } from \"@web/core/utils/patch\";\n\nObject.assign(Discuss.components, { ControlPanel, MessagingMenu });\n\npatch(Discuss.prototype, {\n    setup() {\n        super.setup();\n        this.prevInboxCounter = this.store.inbox.counter;\n        useEffect(\n            (threadName) => {\n                if (threadName) {\n                    this.env.config?.setDisplayName(threadName);\n                }\n            },\n            () => [this.thread?.displayName]\n        );\n        useEffect(\n            () => {\n                if (\n                    this.thread?.id === \"inbox\" &&\n                    this.prevInboxCounter !== this.store.inbox.counter &&\n                    this.store.inbox.counter === 0\n                ) {\n                    this.effect.add({\n                        message: _t(\"Congratulations, your inbox is empty!\"),\n                        type: \"rainbow_man\",\n                        fadeout: \"fast\",\n                    });\n                }\n                this.prevInboxCounter = this.store.inbox.counter;\n            },\n            () => [this.store.inbox.counter]\n        );\n    },\n});\n", "import { ThreadIcon } from \"@mail/core/common/thread_icon\";\nimport { discussSidebarItemsRegistry } from \"@mail/core/public_web/discuss_sidebar\";\nimport { useHover } from \"@mail/utils/common/hooks\";\n\nimport { Component, useRef } from \"@odoo/owl\";\nimport { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { useDropdownState } from \"@web/core/dropdown/dropdown_hooks\";\n\nimport { useService } from \"@web/core/utils/hooks\";\nimport { markEventHandled } from \"@web/core/utils/misc\";\n\nexport class Mailbox extends Component {\n    static template = \"mail.Mailbox\";\n    static props = [\"mailbox\"];\n    static components = { Dropdown, ThreadIcon };\n\n    setup() {\n        super.setup();\n        this.store = useService(\"mail.store\");\n        this.hover = useHover([\"root\", \"floating\"], {\n            onHover: () => {\n                if (this.store.discuss.isSidebarCompact) {\n                    this.floating.isOpen = true;\n                }\n            },\n            onAway: () => {\n                if (this.store.discuss.isSidebarCompact) {\n                    this.floating.isOpen = false;\n                }\n            },\n        });\n        this.floating = useDropdownState();\n        this.rootRef = useRef(\"root\");\n    }\n\n    /** @returns {import(\"models\").Thread} */\n    get mailbox() {\n        return this.props.mailbox;\n    }\n\n    /** @param {MouseEvent} ev */\n    openThread(ev) {\n        markEventHandled(ev, \"sidebar.openThread\");\n        this.mailbox.setAsDiscussThread();\n    }\n}\n\n/**\n * @typedef {Object} Props\n * @extends {Component<Props, Env>}\n */\nexport class DiscussSidebarMailboxes extends Component {\n    static template = \"mail.DiscussSidebarMailboxes\";\n    static props = {};\n    static components = { Mailbox };\n\n    setup() {\n        super.setup();\n        this.store = useService(\"mail.store\");\n    }\n}\n\ndiscussSidebarItemsRegistry.add(\"mailbox\", DiscussSidebarMailboxes, { sequence: 20 });\n", "import { useService } from \"@web/core/utils/hooks\";\nimport { Component } from \"@odoo/owl\";\nimport { FollowerSubtypeDialog } from \"@mail/core/web/follower_subtype_dialog\";\nimport { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\n\n/**\n * @typedef {Object} Props\n * @property {import(\"models\").Follower} follower\n * @property {Function} [onFollowerChanged]\n * @property {Function} [close]\n * @extends {Component<Props, Env>}\n */\nexport class Follower extends Component {\n    static template = \"mail.Follower\";\n    static props = [\"follower\", \"onFollowerChanged?\", \"close?\"];\n    static components = { DropdownItem };\n\n    setup() {\n        this.store = useService(\"mail.store\");\n    }\n\n    onClickDetails() {\n        this.store.openDocument({\n            id: this.props.follower.partner_id.id,\n            model: \"res.partner\"\n        });\n        this.props.close?.();\n    }\n\n    async onClickEdit() {\n        this.env.services.dialog.add(FollowerSubtypeDialog, {\n            follower: this.props.follower,\n            onFollowerChanged: () => this.props.onFollowerChanged?.(),\n        });\n        this.props.close?.();\n    }\n\n    async onClickRemove() {\n        await this.props.follower.remove();\n        this.props.onFollowerChanged?.();\n    }\n}\n", "import { Component } from \"@odoo/owl\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { useVisible } from \"@mail/utils/common/hooks\";\nimport { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\nimport { Follower } from \"@mail/core/web/follower\";\nimport { FollowerSubtypeDialog } from \"@mail/core/web/follower_subtype_dialog\";\n\n/**\n * @typedef {Object} Props\n * @property {function} [onAddFollowers]\n * @property {function} [onFollowerChanged]\n * @property {import('@mail/core/common/thread_model').Thread} thread\n * @extends {Component<Props, Env>}\n */\n\nexport class FollowerList extends Component {\n    static template = \"mail.FollowerList\";\n    static components = { DropdownItem, Follower };\n    static props = [\"onAddFollowers?\", \"onFollowerChanged?\", \"thread\", \"dropdown\"];\n\n    setup() {\n        super.setup();\n        this.action = useService(\"action\");\n        this.store = useService(\"mail.store\");\n        useVisible(\"load-more\", (isVisible) => {\n            if (isVisible) {\n                this.props.thread.loadMoreFollowers();\n            }\n        });\n    }\n\n    onClickAddFollowers() {\n        const action = {\n            type: \"ir.actions.act_window\",\n            res_model: \"mail.followers.edit\",\n            view_mode: \"form\",\n            views: [[false, \"form\"]],\n            name: _t(\"Add followers to this document\"),\n            target: \"new\",\n            context: {\n                default_res_model: this.props.thread.model,\n                default_res_ids: [this.props.thread.id],\n                dialog_size: \"medium\",\n                form_view_ref: \"mail.mail_followers_list_edit_form\",\n            },\n        };\n        this.action.doAction(action, {\n            onClose: () => {\n                this.props.onAddFollowers?.();\n            },\n        });\n    }\n\n    async onClickFollow() {\n        this.props.thread.follow();\n        this.props.onFollowerChanged?.();\n    }\n\n    async onClickUnfollow() {\n        if (this.props.thread.selfFollower) {\n            await this.props.thread.selfFollower.remove();\n            this.props.onFollowerChanged?.();\n        }\n    }\n\n    async onClickEdit() {\n        this.env.services.dialog.add(FollowerSubtypeDialog, {\n            follower: this.props.thread.selfFollower,\n            onFollowerChanged: () => this.props.onFollowerChanged?.(),\n        });\n        this.props.dropdown.close();\n    }\n}\n", "import { rpc } from \"@web/core/network/rpc\";\nimport { Component, onWillStart, useState } from \"@odoo/owl\";\n\nimport { Dialog } from \"@web/core/dialog/dialog\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { useService } from \"@web/core/utils/hooks\";\n\n/**\n * @typedef {Object} Props\n * @property {function} close\n * @property {import(\"models\").Follower} follower\n * @property {function} onFollowerChanged\n * @extends {Component<Props, Env>}\n */\nexport class FollowerSubtypeDialog extends Component {\n    static components = { Dialog };\n    static props = [\"close\", \"follower\", \"onFollowerChanged\"];\n    static template = \"mail.FollowerSubtypeDialog\";\n\n    setup() {\n        super.setup();\n        this.store = useService(\"mail.store\");\n        this.state = useState({\n            /** @type {import(\"models\").MailMessageSubtype[]} */\n            subtypes: [],\n        });\n        onWillStart(async () => {\n            const { store_data, subtype_ids } = await rpc(\"/mail/read_subscription_data\", {\n                follower_id: this.props.follower.id,\n            });\n            this.store.insert(store_data);\n            this.state.subtypes = subtype_ids.map((id) =>\n                this.store[\"mail.message.subtype\"].get(id)\n            );\n        });\n    }\n\n    /**\n     * @param {Event} ev\n     * @param {SubtypeData} subtype\n     */\n    onChangeCheckbox(ev, subtype) {\n        if (ev.target.checked) {\n            this.props.follower.subtype_ids.add(subtype);\n        } else {\n            this.props.follower.subtype_ids.delete(subtype);\n        }\n    }\n\n    async onClickApply() {\n        const selectedSubtypes = this.state.subtypes.filter((s) =>\n            s.in(this.props.follower.subtype_ids)\n        );\n        if (selectedSubtypes.length === 0) {\n            await this.props.follower.remove();\n        } else {\n            await this.env.services.orm.call(\n                this.props.follower.thread.model,\n                \"message_subscribe\",\n                [[this.props.follower.thread.id]],\n                {\n                    partner_ids: [this.props.follower.partner_id.id],\n                    subtype_ids: selectedSubtypes.map((subtype) => subtype.id),\n                }\n            );\n            if (this.store.mt_comment.notIn(selectedSubtypes)) {\n                this.props.follower.removeRecipient();\n            }\n            this.env.services.notification.add(\n                _t(\"The subscription preferences were successfully applied.\"),\n                { type: \"success\" }\n            );\n        }\n        this.props.onFollowerChanged();\n        this.props.close();\n    }\n\n    get title() {\n        return _t(\"Edit Subscription of %(name)s\", { name: this.props.follower.partner_id.name });\n    }\n}\n", "import { ColumnProgress } from \"@web/views/view_components/column_progress\";\n\nexport class MailColumnProgress extends ColumnProgress {\n    static props = {\n        ...ColumnProgress.props,\n        aggregateOn: { type: Object, optional: true },\n    };\n    static template = \"mail.ColumnProgress\";\n}\n", "import { registry } from \"@web/core/registry\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport {\n    many2ManyBinaryField,\n    Many2ManyBinaryField,\n} from \"@web/views/fields/many2many_binary/many2many_binary_field\";\n\nexport class MailComposerAttachmentList extends Many2ManyBinaryField {\n    static template = \"mail.MailComposerAttachmentList\";\n    /** @override */\n    setup() {\n        super.setup();\n        this.mailStore = useService(\"mail.store\");\n        this.attachmentUploadService = useService(\"mail.attachment_upload\");\n    }\n    /**\n     * @override\n     * @param {integer} fileId\n     */\n    async onFileRemove(fileId) {\n        super.onFileRemove(fileId);\n        const attachment = this.mailStore[\"ir.attachment\"].insert(fileId);\n        await this.attachmentUploadService.unlink(attachment);\n        this.env.fullComposerBus.trigger(\"ATTACHMENT_REMOVED\", {\n            id: attachment.id,\n        });\n    }\n}\n\nexport const mailComposerAttachmentList = {\n    ...many2ManyBinaryField,\n    component: MailComposerAttachmentList,\n};\n\nregistry.category(\"fields\").add(\"mail_composer_attachment_list\", mailComposerAttachmentList);\n", "import { dataUrlToBlob } from \"@mail/core/common/attachment_uploader_hook\";\nimport { registry } from \"@web/core/registry\";\nimport { standardFieldProps } from \"@web/views/fields/standard_field_props\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { useX2ManyCrud } from \"@web/views/fields/relational_utils\";\n\nimport { Component } from \"@odoo/owl\";\nimport { FileUploader } from \"@web/views/fields/file_handler\";\n\nexport class MailComposerAttachmentSelector extends Component {\n    static template = \"mail.MailComposerAttachmentSelector\";\n    static components = { FileUploader };\n    static props = { ...standardFieldProps };\n\n    setup() {\n        this.mailStore = useService(\"mail.store\");\n        this.attachmentUploadService = useService(\"mail.attachment_upload\");\n        this.operations = useX2ManyCrud(() => {\n            return this.props.record.data[\"attachment_ids\"];\n        }, true);\n    }\n\n    /** @param {Object} data */\n    async onFileUploaded({ data, name, type }) {\n        let resIds;\n        if (this.props.record.resModel === \"mail.scheduled.message\") {\n            resIds = [this.props.record.data.res_id.resId];\n        } else {\n            resIds = JSON.parse(this.props.record.data.res_ids);\n        }\n        const thread = await this.mailStore.Thread.insert({\n            model: this.props.record.data.model,\n            id: resIds[0],\n        });\n        const file = new File([dataUrlToBlob(data, type)], name, { type });\n        const attachment = await this.attachmentUploadService.upload(thread, thread.composer, file);\n        if (attachment) {\n            await this.operations.saveRecord([attachment.id]);\n        }\n    }\n}\n\nexport const mailComposerAttachmentSelector = {\n    component: MailComposerAttachmentSelector,\n};\n\nregistry\n    .category(\"fields\")\n    .add(\"mail_composer_attachment_selector\", mailComposerAttachmentSelector);\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { Component } from \"@odoo/owl\";\n\n\nexport class MailComposerBccPopover extends Component {\n    static template = \"mail.MailComposerBccPopover\";\n    static props = [\"records\", \"close?\"];\n\n    /**\n     * @param {Record} record\n     * @returns {string}\n     **/\n    getRecipientText(record) {\n        return _t(\"%(name)s <%(email)s>\", {\n            name: record.data.name,\n            email: record.data.email_normalized\n        });\n    }\n}\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { standardFieldProps } from \"@web/views/fields/standard_field_props\";\nimport { user } from \"@web/core/user\";\nimport { useService } from \"@web/core/utils/hooks\";\n\nimport { Component, useState, onWillStart } from \"@odoo/owl\";\nimport { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\nimport { SelectCreateDialog } from \"@web/views/view_dialogs/select_create_dialog\";\n\n\nexport class MailComposerTemplateSelector extends Component {\n    static template = \"mail.MailComposerTemplateSelector\";\n    static components = { Dropdown, DropdownItem };\n    static props = { ...standardFieldProps };\n\n    setup() {\n        this.action = useService(\"action\");\n        this.orm = useService(\"orm\");\n        this.limit = 80;\n\n        const { context } = this.props.record.evalContext;\n        this.state = useState({\n            hideMailTemplateManagementOptions: context?.hide_mail_template_management_options,\n        });\n\n        onWillStart(() => {\n            this.fetchTemplates();\n        });\n    }\n\n    async fetchTemplates() {\n        const fields = [\"display_name\"];\n        const templates = await this.orm.searchRead(\"mail.template\", [\n            [\"model\", \"=\", this.props.record.data.render_model],\n            [\"user_id\", \"=\", user.userId]\n        ], fields, { limit: this.limit });\n        if (templates.length < this.limit) {\n            templates.push(...await this.orm.searchRead(\"mail.template\", [\n                [\"model\", \"=\", this.props.record.data.render_model],\n                [\"user_id\", \"=\", false]\n            ], fields, { limit: this.limit - templates.length }));\n        }\n        this.state.templates = templates;\n    }\n\n    /**\n     * @param {Object} template\n     * @param {integer} template.id\n     * @param {string} template.display_name\n     */\n    async onLoadTemplate(template) {\n        await this.props.record.update({\n            template_id: { id: template.id },\n        });\n    }\n\n    async onSaveTemplate() {\n        if (!(await this.props.record.save())) {\n            return;\n        }\n        await this.action.doActionButton({\n            type: \"object\",\n            name: \"open_template_creation_wizard\",\n            resId: this.props.record.resId,\n            resModel: this.props.record.resModel\n        });\n    }\n\n    async onManageTemplateBtnClick() {\n        const action = await this.action.loadAction(\"mail.action_email_template_tree_all\");\n        action.context = {\n            search_default_my_templates: 1,\n            search_default_model: this.props.record.data.model,\n            default_model: this.props.record.data.model,\n            default_user_id: user.userId,\n        };\n        this.action.doAction(action);\n    }\n\n    onSelectTemplateSearchMoreBtnClick() {\n        this.env.services.dialog.add(SelectCreateDialog, {\n            resModel: \"mail.template\",\n            title: _t(\"Select a Template\"),\n            multiSelect: false,\n            noCreate: true,\n            domain: [[\"model\", \"=\", this.props.record.data.render_model]],\n            onSelected: async templateIds => {\n                await this.props.record.update({\n                    template_id: { id: templateIds[0] },\n                });\n            },\n        });\n    }\n}\n\nexport const mailComposerTemplateSelector = {\n    component: MailComposerTemplateSelector,\n    fieldDependencies: [\n        { name: \"can_edit_body\", type: \"boolean\" },\n        { name: \"render_model\", type: \"string\" },\n    ],\n};\n\nregistry.category(\"fields\").add(\"mail_composer_template_selector\", mailComposerTemplateSelector);\n", "import { patch } from \"@web/core/utils/patch\";\nimport { MailCoreCommon } from \"@mail/core/common/mail_core_common_service\";\n\npatch(MailCoreCommon.prototype, {\n    _handleNotificationToggleStar(payload, metadata) {\n        super._handleNotificationToggleStar(payload, metadata);\n        const { id: notifId } = metadata;\n        const { message_ids: messageIds, starred } = payload;\n        for (const id of messageIds) {\n            const message = this.store[\"mail.message\"].get({ id });\n            const starredBox = this.store.starred;\n            if (starred) {\n                if (notifId > starredBox.counter_bus_id) {\n                    starredBox.counter++;\n                }\n                starredBox.messages.add(message);\n            } else {\n                if (notifId > starredBox.counter_bus_id) {\n                    starredBox.counter--;\n                }\n                starredBox.messages.delete(message);\n            }\n        }\n    },\n});\n", "import { reactive } from \"@odoo/owl\";\n\nimport { registry } from \"@web/core/registry\";\n\nexport class MailCoreWeb {\n    /**\n     * @param {import(\"@web/env\").OdooEnv} env\n     * @param {import(\"services\").ServiceFactories} services\n     */\n    constructor(env, services) {\n        this.env = env;\n        this.busService = services.bus_service;\n        this.store = services[\"mail.store\"];\n    }\n\n    setup() {\n        this.busService.subscribe(\"mail.activity/updated\", (payload, { id: notifId }) => {\n            if (notifId <= this.store.activity_counter_bus_id) {\n                return;\n            }\n            let countDiff = 0;\n            if (\"count_diff\" in payload) {\n                countDiff = payload.count_diff;\n            } else if (payload.activity_created) {\n                countDiff = 1;\n            } else if (payload.activity_deleted) {\n                countDiff = -1;\n            }\n            this.store.activityCounter += countDiff;\n        });\n        this.env.bus.addEventListener(\"mail.message/delete\", ({ detail: { message, notifId } }) => {\n            if (message.needaction && notifId > this.store.inbox.counter_bus_id) {\n                this.store.inbox.counter--;\n            }\n            if (message.starred && notifId > this.store.starred.counter_bus_id) {\n                this.store.starred.counter--;\n            }\n        });\n        this.busService.subscribe(\"mail.message/inbox\", (payload, { id: notifId }) => {\n            const { message_id: messageId, store_data } = payload;\n            this.store.insert(store_data);\n            /** @type {import(\"models\").Message} */\n            const message = this.store[\"mail.message\"].get(messageId);\n            const inbox = this.store.inbox;\n            if (notifId > inbox.counter_bus_id) {\n                inbox.counter++;\n            }\n            inbox.messages.add(message);\n            if (message.thread && notifId > message.thread.message_needaction_counter_bus_id) {\n                message.thread.message_needaction_counter++;\n            }\n            this.store.env.services[\"mail.out_of_focus\"].notify(message);\n        });\n        this.busService.subscribe(\"mail.message/mark_as_read\", (payload, { id: notifId }) => {\n            const { message_ids: messageIds, needaction_inbox_counter } = payload;\n            const inbox = this.store.inbox;\n            for (const messageId of messageIds) {\n                // We need to ignore all not yet known messages because we don't want them\n                // to be shown partially as they would be linked directly to cache.\n                // Furthermore, server should not send back all messageIds marked as read\n                // but something like last read messageId or something like that.\n                // (just imagine you mark 1000 messages as read ... )\n                const message = this.store[\"mail.message\"].get(messageId);\n                if (!message) {\n                    continue;\n                }\n                // update thread counter (before removing message from Inbox, to ensure isNeedaction check is correct)\n                const thread = message.thread;\n                if (\n                    thread &&\n                    message.needaction &&\n                    notifId > thread.message_needaction_counter_bus_id\n                ) {\n                    thread.message_needaction_counter--;\n                }\n                // move messages from Inbox to history\n                message.needaction = false;\n                inbox.messages.delete({ id: messageId });\n                const history = this.store.history;\n                history.messages.add(message);\n            }\n            if (notifId > inbox.counter_bus_id) {\n                inbox.counter = needaction_inbox_counter;\n                inbox.counter_bus_id = notifId;\n            }\n            if (inbox.counter > inbox.messages.length) {\n                inbox.fetchMoreMessages();\n            }\n        });\n    }\n}\n\nexport const mailCoreWeb = {\n    dependencies: [\"bus_service\", \"mail.store\"],\n    /**\n     * @param {import(\"@web/env\").OdooEnv} env\n     * @param {import(\"services\").ServiceFactories} services\n     */\n    start(env, services) {\n        const mailCoreWeb = reactive(new MailCoreWeb(env, services));\n        mailCoreWeb.setup();\n        return mailCoreWeb;\n    },\n};\n\nregistry.category(\"services\").add(\"mail.core.web\", mailCoreWeb);\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { Component, useEffect, useState } from \"@odoo/owl\";\nimport { useService, useAutofocus } from \"@web/core/utils/hooks\";\n\nimport { NavigableList } from \"@mail/core/common/navigable_list\";\nimport { useSequential } from \"@mail/utils/common/hooks\";\n\nexport class MentionList extends Component {\n    static template = \"mail.MentionList\";\n    static components = { NavigableList };\n    static props = {\n        onSelect: { type: Function },\n        close: { type: Function, optional: true },\n        type: { type: String },\n    };\n    static defaultProps = {\n        close: () => {},\n    };\n\n    setup() {\n        super.setup();\n        this.state = useState({\n            searchTerm: \"\",\n            options: [],\n            isFetching: false,\n        });\n        this.orm = useService(\"orm\");\n        this.store = useService(\"mail.store\");\n        this.suggestionService = useService(\"mail.suggestion\");\n        this.sequential = useSequential();\n        this.ref = useAutofocus({ mobile: true });\n\n        useEffect(\n            () => {\n                if (!this.state.searchTerm) {\n                    this.state.options = [];\n                    return;\n                }\n                this.sequential(async () => {\n                    this.state.isFetching = true;\n                    try {\n                        await this.suggestionService.fetchSuggestions({\n                            delimiter: this.props.type === \"partner\" ? \"@\" : \"#\",\n                            term: this.state.searchTerm,\n                        });\n                    } finally {\n                        this.state.isFetching = false;\n                    }\n                    const { suggestions } = this.suggestionService.searchSuggestions({\n                        delimiter: this.props.type === \"partner\" ? \"@\" : \"#\",\n                        term: this.state.searchTerm,\n                    });\n                    this.state.options = suggestions;\n                });\n            },\n            () => [this.state.searchTerm]\n        );\n    }\n\n    get placeholder() {\n        switch (this.props.type) {\n            case \"channel\":\n                return _t(\"Search for a channel...\");\n            case \"partner\":\n                return _t(\"Search for a user...\");\n            default:\n                return _t(\"Search...\");\n        }\n    }\n\n    get navigableListProps() {\n        const props = {\n            anchorRef: this.ref.el,\n            position: \"bottom-fit\",\n            isLoading: !!this.state.searchTerm && this.state.isFetching,\n            onSelect: (...args) => {\n                this.props.onSelect(...args);\n                this.props.close();\n            },\n            options: [],\n        };\n        switch (this.props.type) {\n            case \"partner\":\n                this.state.options.forEach((option) => {\n                    props.options.push({\n                        label: option.name,\n                        partner: option,\n                    });\n                });\n                break;\n            case \"channel\": {\n                this.state.options.forEach((option) => {\n                    props.options.push({\n                        label: option.name,\n                        channel: option,\n                    });\n                });\n                break;\n            }\n        }\n        return props;\n    }\n\n    onKeydown(ev) {\n        switch (ev.key) {\n            case \"Escape\": {\n                this.props.close();\n                break;\n            }\n        }\n    }\n}\n", "import { getNonEditableMentions, parseEmail } from \"@mail/utils/common/format\";\nimport { registerMessageAction } from \"@mail/core/common/message_actions\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { renderToMarkup } from \"@web/core/utils/render\";\nimport { rpc } from \"@web/core/network/rpc\";\n\nexport function messageActionOpenFullComposer(title, context, component) {\n    const message = component.props.message;\n    const thread = component.props.thread;\n    const action = {\n        name: title,\n        type: \"ir.actions.act_window\",\n        res_model: \"mail.compose.message\",\n        view_mode: \"form\",\n        views: [[false, \"form\"]],\n        target: \"new\",\n        context: {\n            ...context,\n            default_model: thread.model,\n            default_res_ids: [thread.id],\n            default_subject: message.subject || message.default_subject,\n            default_subtype_xmlid: \"mail.mt_comment\",\n        },\n    };\n    component.env.services.action.doAction(action, {\n        onClose: () => thread.fetchNewMessages(),\n    });\n}\n\nregisterMessageAction(\"reply-all\", {\n    condition: ({ message, thread }) => message.canReplyAll(thread),\n    icon: \"fa fa-reply\",\n    name: _t(\"Reply All\"),\n    onSelected: async ({ message, owner, thread }) => {\n        const recipients = await rpc(\"/mail/thread/recipients\", {\n            thread_model: thread.model,\n            thread_id: thread.id,\n            message_id: message.id,\n        });\n        const recipientIds = recipients.map((r) => r.id);\n        const emailFrom = message.author_id?.email || message.email_from;\n        const [name, email] = parseEmail(emailFrom);\n        const datetime = _t(\"%(date)s at %(time)s\", {\n            date: message.datetime.toFormat(\"ccc, MMM d, yyyy\"),\n            time: message.datetime.toFormat(\"hh:mm a\"),\n        });\n        const body = renderToMarkup(\"mail.Message.bodyInReply\", {\n            body: getNonEditableMentions(message.body),\n            date: datetime,\n            email,\n            message,\n            name: name || email,\n        });\n        const context = {\n            default_body: body,\n            default_composition_mode: \"comment\",\n            default_composition_comment_option: \"reply_all\",\n            default_partner_ids: recipientIds,\n        };\n        messageActionOpenFullComposer(_t(\"Reply All\"), context, owner);\n    },\n    sequence: 71,\n});\nregisterMessageAction(\"forward\", {\n    condition: ({ message, thread }) => message.canForward(thread),\n    icon: \"fa fa-share\",\n    name: _t(\"Forward\"),\n    onSelected: async ({ message, owner, store }) => {\n        const emailFrom = message.author_id?.email || message.email_from;\n        const [name, email] = parseEmail(emailFrom);\n        const datetime = _t(\"%(date)s at %(time)s\", {\n            date: message.datetime.toFormat(\"ccc, MMM d, yyyy\"),\n            time: message.datetime.toFormat(\"hh:mm a\"),\n        });\n        const body = renderToMarkup(\"mail.Message.bodyInForward\", {\n            body: getNonEditableMentions(message.body),\n            date: datetime,\n            email,\n            message,\n            name: name || email,\n        });\n        const attachmentIds = message.attachment_ids.map((a) => a.id);\n        const newAttachmentIds = await store.env.services.orm.call(\n            \"ir.attachment\",\n            \"copy\",\n            [attachmentIds],\n            {\n                default: { res_model: \"mail.compose.message\", res_id: 0 },\n            }\n        );\n        const context = {\n            default_attachment_ids: newAttachmentIds,\n            default_body: body,\n            default_composition_mode: \"comment\",\n            default_composition_comment_option: \"forward\",\n        };\n        messageActionOpenFullComposer(_t(\"Forward Message\"), context, owner);\n    },\n    sequence: 72,\n});\n", "import { Message } from \"@mail/core/common/message_model\";\n\nimport { patch } from \"@web/core/utils/patch\";\n\n/** @type {import(\"models\").Message} */\nconst messagePatch = {\n    /** @param {import(\"models\").Thread} thread the thread where the message is shown */\n    canReplyAll(thread) {\n        return this.canForward(thread) && !this.isNote;\n    },\n    /** @param {import(\"models\").Thread} thread */\n    canForward(thread) {\n        if (!thread) {\n            return false;\n        }\n        return (\n            ![\"discuss.channel\", \"mail.box\"].includes(thread.model) &&\n            [\"comment\", \"email\"].includes(this.message_type)\n        );\n    },\n};\npatch(Message.prototype, messagePatch);\n", "import { Message } from \"@mail/core/common/message\";\nimport { markEventHandled } from \"@web/core/utils/misc\";\n\nimport {\n    deserializeDate,\n    deserializeDateTime,\n    formatDate,\n    formatDateTime,\n} from \"@web/core/l10n/dates\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport {\n    formatChar,\n    formatFloat,\n    formatInteger,\n    formatMonetary,\n    formatText,\n} from \"@web/views/fields/formatters\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { usePopover } from \"@web/core/popover/popover_hook\";\nimport { patch } from \"@web/core/utils/patch\";\nimport { AvatarCardPopover } from \"@mail/discuss/web/avatar_card/avatar_card_popover\";\nimport { messageActionOpenFullComposer } from \"@mail/core/web/message_actions_patch\";\n\npatch(Message.prototype, {\n    setup() {\n        super.setup(...arguments);\n        this.action = useService(\"action\");\n        this.avatarCard = usePopover(AvatarCardPopover);\n    },\n    get authorAvatarAttClass() {\n        return {\n            ...super.authorAvatarAttClass,\n            \"o_redirect cursor-pointer\": this.hasAuthorClickable(),\n        };\n    },\n    getAuthorAttClass() {\n        return {\n            ...super.getAuthorAttClass(),\n            \"cursor-pointer o-hover-text-underline\": this.hasAuthorClickable(),\n        };\n    },\n    getAuthorText() {\n        return this.hasAuthorClickable() ? _t(\"Open card\") : undefined;\n    },\n    getAvatarContainerAttClass() {\n        return {\n            ...super.getAvatarContainerAttClass(),\n            \"cursor-pointer\": this.hasAuthorClickable(),\n        };\n    },\n    hasAuthorClickable() {\n        return this.message.author_id?.main_user_id;\n    },\n    onClickAuthor(ev) {\n        if (this.hasAuthorClickable()) {\n            markEventHandled(ev, \"Message.ClickAuthor\");\n            const target = ev.currentTarget;\n            if (!this.avatarCard.isOpen) {\n                this.avatarCard.open(target, {\n                    id: this.message.author_id.main_user_id.id,\n                });\n            }\n        }\n    },\n\n    /** @deprecated */\n    async onClickMessageForward() {\n        await this.messageActions.actions.find((a) => a.name === \"forward\")?.onClick();\n    },\n\n    /** @deprecated */\n    async onClickMessageReplyAll() {\n        await this.messageActions.actions.find((a) => a.name === \"reply-all\")?.onClick();\n    },\n\n    /** @deprecated */\n    openFullComposer(name, context) {\n        messageActionOpenFullComposer(name, context, this);\n    },\n\n    openRecord() {\n        this.message.thread.open({ focus: true });\n        this.message.thread.highlightMessage = this.message;\n    },\n\n    /**\n     * @returns {string}\n     */\n    formatTracking(trackingFieldInfo, trackingValue) {\n        switch (trackingFieldInfo.fieldType) {\n            case \"boolean\":\n                return trackingValue ? _t(\"Yes\") : _t(\"No\");\n            /**\n             * many2one formatter exists but is expecting id/display_name or data\n             * object but only the target record name is known in this context.\n             *\n             * Selection formatter exists but requires knowing all\n             * possibilities and they are not given in this context.\n             */\n            case \"char\":\n            case \"many2one\":\n            case \"selection\":\n                return formatChar(trackingValue);\n            case \"date\": {\n                const value = trackingValue ? deserializeDate(trackingValue) : trackingValue;\n                return formatDate(value);\n            }\n            case \"datetime\": {\n                const value = trackingValue ? deserializeDateTime(trackingValue) : trackingValue;\n                return formatDateTime(value);\n            }\n            case \"float\":\n                return formatFloat(trackingValue, { digits: trackingFieldInfo.floatPrecision });\n            case \"integer\":\n                return formatInteger(trackingValue);\n            case \"text\":\n                return formatText(trackingValue);\n            case \"monetary\":\n                return formatMonetary(trackingValue, {\n                    currencyId: trackingFieldInfo.currencyId,\n                });\n            default:\n                return trackingValue;\n        }\n    },\n\n    /**\n     * @returns {string}\n     */\n    formatTrackingOrNone(trackingFieldInfo, trackingValue) {\n        const formattedValue = this.formatTracking(trackingFieldInfo, trackingValue);\n        return formattedValue\n            ? this.props.messageSearch?.highlight(formattedValue) ?? formattedValue\n            : _t(\"None\");\n    },\n});\n", "import { MessagingMenu } from \"@mail/core/public_web/messaging_menu\";\nimport { onExternalClick } from \"@mail/utils/common/hooks\";\nimport { useEffect } from \"@odoo/owl\";\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { patch } from \"@web/core/utils/patch\";\nimport { MessagingMenuQuickSearch } from \"@mail/core/web/messaging_menu_quick_search\";\n\nObject.assign(MessagingMenu.components, { MessagingMenuQuickSearch });\n\npatch(MessagingMenu.prototype, {\n    setup() {\n        super.setup();\n        this.action = useService(\"action\");\n        this.pwa = useService(\"pwa\");\n        this.notification = useService(\"mail.notification.permission\");\n        Object.assign(this.state, {\n            searchOpen: false,\n        });\n\n        onExternalClick(\"selector\", () => Object.assign(this.state, { adding: false }));\n        useEffect(\n            () => {\n                if (\n                    this.store.discuss.searchTerm &&\n                    this.lastSearchTerm !== this.store.discuss.searchTerm &&\n                    this.state.activeIndex\n                ) {\n                    this.state.activeIndex = 0;\n                }\n                if (!this.store.discuss.searchTerm) {\n                    this.state.activeIndex = null;\n                }\n                this.lastSearchTerm = this.store.discuss.searchTerm;\n            },\n            () => [this.store.discuss.searchTerm]\n        );\n        useEffect(\n            () => {\n                if (!this.dropdown.isOpen) {\n                    this.state.activeIndex = null;\n                }\n            },\n            () => [this.dropdown.isOpen]\n        );\n    },\n    beforeOpen() {\n        this.state.searchOpen = false;\n        this.store.discuss.searchTerm = \"\";\n        this.store.isReady.then(() => {\n            if (\n                !this.store.inbox.isLoaded &&\n                this.store.inbox.status !== \"loading\" &&\n                this.store.inbox.counter !== this.store.inbox.messages.length\n            ) {\n                this.store.inbox.fetchNewMessages();\n            }\n        });\n    },\n    get canPromptToInstall() {\n        return this.pwa.canPromptToInstall;\n    },\n    get hasPreviews() {\n        return (\n            this.threads.length > 0 ||\n            this.visibleStandaloneMessages.length > 0 ||\n            (this.store.failures.length > 0 && this.store.discuss.activeTab === \"notification\") ||\n            (this.shouldAskPushPermission && this.store.discuss.activeTab === \"notification\") ||\n            (this.canPromptToInstall && this.store.discuss.activeTab === \"notification\")\n        );\n    },\n    get installationRequest() {\n        return {\n            body: _t(\"Come here often? Install the app for quick and easy access!\"),\n            displayName: _t(\"Install Odoo\"),\n            onClick: () => {\n                this.pwa.show();\n            },\n            iconSrc: this.store.odoobot.avatarUrl,\n            partner: this.store.odoobot,\n            isShown: this.store.discuss.activeTab === \"notification\" && this.canPromptToInstall,\n        };\n    },\n    get notificationRequest() {\n        return {\n            body: _t(\"Stay tuned! Enable push notifications to never miss a message.\"),\n            displayName: _t(\"Turn on notifications\"),\n            iconSrc: this.store.odoobot.avatarUrl,\n            partner: this.store.odoobot,\n            isShown:\n                this.store.discuss.activeTab === \"notification\" && this.shouldAskPushPermission,\n        };\n    },\n    get _tabs() {\n        return [\n            {\n                icon: \"fa fa-bell-o\",\n                activeIcon: \"fa fa-bell\",\n                id: \"notification\",\n                label: _t(\"Notifications\"),\n                sequence: 10,\n            },\n            {\n                counter:\n                    this.store.self.main_user_id?.notification_type === \"inbox\"\n                        ? this.store.inbox.counter\n                        : this.store.starred.counter,\n                icon:\n                    this.store.self.main_user_id?.notification_type === \"inbox\"\n                        ? \"fa fa-inbox\"\n                        : \"fa fa-star-o\",\n                activeIcon:\n                    this.store.self.main_user_id?.notification_type !== \"inbox\" && \"fa fa-star\",\n                id:\n                    this.store.self.main_user_id?.notification_type === \"inbox\"\n                        ? \"inbox\"\n                        : \"starred\",\n                label:\n                    this.store.self.main_user_id?.notification_type === \"inbox\"\n                        ? _t(\"Inbox\")\n                        : _t(\"Starred\"),\n                sequence: 100,\n            },\n            ...super._tabs,\n        ];\n    },\n    /** @param {import(\"models\").Failure} failure */\n    onClickFailure(failure) {\n        const threadIds = new Set(\n            failure.notifications.map(({ mail_message_id: message }) => message.thread.id)\n        );\n        if (threadIds.size === 1) {\n            const message = failure.notifications[0].mail_message_id;\n            this.openThread(message.thread);\n        } else {\n            this.openFailureView(failure);\n            this.dropdown.close();\n        }\n    },\n    async openThread(thread) {\n        thread.open({ focus: true, fromMessagingMenu: true });\n        this.dropdown.close();\n    },\n    openFailureView(failure) {\n        if (failure.type !== \"email\") {\n            return;\n        }\n        this.action.doAction({\n            name: _t(\"Mail Failures\"),\n            type: \"ir.actions.act_window\",\n            view_mode: \"kanban,list,form\",\n            views: [\n                [false, \"kanban\"],\n                [false, \"list\"],\n                [false, \"form\"],\n            ],\n            target: \"current\",\n            res_model: failure.resModel,\n            domain: [[\"message_has_error\", \"=\", true]],\n            context: { create: false },\n        });\n    },\n    cancelNotifications(failure) {\n        return this.env.services.orm.call(failure.resModel, \"notify_cancel_by_type\", [], {\n            notification_type: failure.type,\n        });\n    },\n    toggleSearch() {\n        this.store.discuss.searchTerm = \"\";\n        this.state.searchOpen = !this.state.searchOpen;\n    },\n    get counter() {\n        let value =\n            this.store.globalCounter +\n            this.store.failures.reduce((acc, f) => acc + parseInt(f.notifications.length), 0);\n        if (this.canPromptToInstall) {\n            value++;\n        }\n        if (this.shouldAskPushPermission) {\n            value++;\n        }\n        return value;\n    },\n    get shouldAskPushPermission() {\n        return (\n            this.notification.permission === \"prompt\" &&\n            !this.store.isNotificationPermissionDismissed\n        );\n    },\n    getFailureNotificationName(failure) {\n        if (failure.type === \"email\") {\n            return _t(\"Email Failure: %(modelName)s\", { modelName: failure.modelName });\n        }\n        return _t(\"Failure: %(modelName)s\", { modelName: failure.modelName });\n    },\n});\n", "import { onExternalClick } from \"@mail/utils/common/hooks\";\nimport { Component } from \"@odoo/owl\";\nimport { getActiveHotkey } from \"@web/core/hotkeys/hotkey_service\";\n\nimport { useAutofocus, useService } from \"@web/core/utils/hooks\";\n\nexport class MessagingMenuQuickSearch extends Component {\n    static components = {};\n    static props = [\"onClose\"];\n    static template = \"mail.MessagingMenuQuickSearch\";\n\n    setup() {\n        super.setup();\n        this.store = useService(\"mail.store\");\n        useAutofocus();\n        onExternalClick(\"search\", () => this.props.onClose());\n    }\n\n    onKeydownInput(ev) {\n        const hotkey = getActiveHotkey(ev);\n        if (hotkey === \"escape\") {\n            ev.stopPropagation();\n            ev.preventDefault();\n            this.props.onClose();\n        }\n    }\n}\n", "import { useService } from \"@web/core/utils/hooks\";\n\nexport const helpers = {\n    SUPPORTED_M2X_AVATAR_MODELS: [\"res.users\", \"res.partner\"],\n    buildOpenChatParams: (resModel, id) => ({\n        userId: resModel === \"res.users\" ? id : undefined,\n        partnerId: resModel === \"res.partner\" ? id : undefined,\n    }),\n};\n\nexport function useOpenChat(resModel) {\n    const store = useService(\"mail.store\");\n    if (!helpers.SUPPORTED_M2X_AVATAR_MODELS.includes(resModel)) {\n        throw new Error(\n            `This widget is only supported on many2one and many2many fields pointing to ${JSON.stringify(\n                helpers.SUPPORTED_M2X_AVATAR_MODELS\n            )}`\n        );\n    }\n    return async (id) => {\n        store.openChat(helpers.buildOpenChatParams(resModel, id));\n    };\n}\n", "import { OutOfFocusService } from \"@mail/core/common/out_of_focus_service\";\nimport { patch } from \"@web/core/utils/patch\";\n\npatch(OutOfFocusService.prototype, {\n    onWindowFocus() {\n        super.onWindowFocus();\n        this.store.updateAppBadge();\n    },\n});\n", "import { parseEmail } from \"@mail/utils/common/format\";\nimport { AutoComplete } from \"@web/core/autocomplete/autocomplete\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { isEmail } from \"@web/core/utils/strings\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { useSelectCreate } from \"@web/views/fields/relational_utils\";\n\nimport { rpc } from \"@web/core/network/rpc\";\nimport { usePopover } from \"@web/core/popover/popover_hook\";\nimport { useTagNavigation } from \"@web/core/record_selectors/tag_navigation_hook\";\nimport { uniqueId } from \"@web/core/utils/functions\";\nimport { RecipientsPopover } from \"./recipients_popover\";\nimport { RecipientsInputTagsList } from \"./recipients_input_tags_list\";\n\nimport { Component } from \"@odoo/owl\";\n\nexport class RecipientsInput extends Component {\n    static template = \"mail.RecipientsInput\";\n    static components = { AutoComplete, RecipientsInputTagsList };\n    static props = {\n        thread: { type: Object },\n    };\n\n    setup() {\n        this.orm = useService(\"orm\");\n        this.action = useService(\"action\");\n        this.store = useService(\"mail.store\");\n        this.popover = usePopover(RecipientsPopover, { position: \"bottom-middle\" });\n        useTagNavigation(\"recipientsInputRef\", {\n            delete: this.deleteTagByIndex.bind(this),\n        });\n\n        this.openListViewToSelectResPartner = useSelectCreate({\n            resModel: \"res.partner\",\n            activeActions: {\n                create: false,\n                link: true, // Unable multi-select\n            },\n            /** @param {Object} resIds */\n            onSelected: async (resIds) => {\n                const partners = await this.orm.searchRead(\n                    \"res.partner\",\n                    [[\"id\", \"in\", Array.from(resIds)]],\n                    [\"email\", \"id\", \"lang\", \"name\"]\n                );\n                for (const partner of partners) {\n                    this.insertAdditionalRecipient({\n                        email: partner.email,\n                        name: partner.name,\n                        partner_id: partner.id,\n                    });\n                }\n            },\n        });\n    }\n\n    deleteTagByIndex(index) {\n        const tags = this.getTagsFromMailThread();\n        if (tags[index]) {\n            tags[index].onDelete();\n        }\n    }\n\n    getAutoCompleteSources() {\n        return [\n            {\n                placeholder: _t(\"Loading...\"),\n                /** @param {string} term */\n                options: async (term) => {\n                    const partnerIds = new Set();\n                    const recipients = this.getAllMailThreadRecipients();\n\n                    for (const recipient of recipients) {\n                        if (recipient.partner_id) {\n                            partnerIds.add(recipient.partner_id);\n                        }\n                    }\n\n                    const options = [];\n                    const [name, email] = term ? parseEmail(term) : [\"\", \"\"];\n\n                    const limit = 8;\n                    const matches = await this.orm.searchRead(\n                        \"res.partner\",\n                        [\n                            [\"id\", \"not in\", Array.from(partnerIds)],\n                            \"|\",\n                            [\"name\", \"ilike\", name],\n                            email ? [\"email_normalized\", \"ilike\", email] : [0, \"=\", 1], // if no email, use a false leaf\n                        ],\n                        [\"email\", \"id\", \"lang\", \"name\"],\n                        { limit }\n                    );\n\n                    options.push(\n                        ...matches.map((match) => ({\n                            label: match.email\n                                ? _t(\"%(partner_name)s <%(partner_email)s>\", {\n                                      partner_name: match.name || _t(\"Unnamed\"),\n                                      partner_email: match.email,\n                                  })\n                                : match.name || _t(\"Unnamed\"),\n                            onSelect: () => {\n                                this.insertAdditionalRecipient({\n                                    email: match.email,\n                                    name: match.name,\n                                    partner_id: match.id,\n                                });\n                            },\n                        }))\n                    );\n\n                    if (matches.length >= limit) {\n                        options.push({\n                            label: _t(\"Search More...\"),\n                            cssClass: \"o_m2o_dropdown_option o_m2o_dropdown_option_search_more\",\n                            onSelect: () => {\n                                this.openListViewToSelectResPartner({});\n                            },\n                        });\n                    }\n\n                    const createOption = {\n                        cssClass: \"o_m2o_dropdown_option o_m2o_dropdown_option_create\",\n                        label: _t(\"Create %s\", name),\n                    };\n\n                    if (isEmail(email)) {\n                        createOption.onSelect = async () => {\n                            const partners = await rpc(\"/mail/partner/from_email\", {\n                                thread_model: this.props.thread.model,\n                                thread_id: this.props.thread.id,\n                                emails: [term],\n                            });\n                            if (partners.length) {\n                                const partner = partners[0];\n                                this.insertAdditionalRecipient({\n                                    email: partner.email,\n                                    name: partner.name,\n                                    partner_id: partner.id,\n                                });\n                            } else {\n                                this.insertAdditionalRecipient({\n                                    email,\n                                    name,\n                                    partner_id: false,\n                                });\n                            }\n                        };\n                    } else {\n                        createOption.onSelect = async () => {\n                            const [partnerId] = await this.orm.create(\"res.partner\", [\n                                { name, email },\n                            ]);\n                            this.insertAdditionalRecipient({\n                                email,\n                                name,\n                                partner_id: partnerId,\n                            });\n                        };\n                    }\n                    options.push(createOption);\n                    return options;\n                },\n            },\n        ];\n    }\n\n    /** @returns {Object} */\n    getTagsFromMailThread() {\n        const tags = [];\n        const createTagForRecipient = (recipient, recipientField) => {\n            const title = `${recipient.name || _t(\"Unnamed\")} ${\n                recipient.email ? \"<\" + recipient.email + \">\" : \"\"\n            }`;\n            title.trim();\n            tags.push({\n                id: uniqueId(\"tag_\"),\n                resId: recipient.partner_id,\n                canEdit: true,\n                text: recipient.name || recipient.email || _t(\"Unnamed\"),\n                name: recipient.name || _t(\"Unnamed\"),\n                email: recipient.email,\n                title,\n                onClick: (ev) => {\n                    if (recipient.partner_id && recipient.email) {\n                        const viewProfileBtnOverride = () => {\n                            const action = {\n                                type: \"ir.actions.act_window\",\n                                res_model: \"res.partner\",\n                                res_id: recipient.partner_id,\n                                views: [[false, \"form\"]],\n                                target: \"current\",\n                            };\n                            this.action.doAction(action);\n                        };\n                        this.popover.open(ev.target, {\n                            viewProfileBtnOverride,\n                            id: recipient.partner_id,\n                        });\n                    }\n                },\n                onDelete: () => {\n                    this.props.thread[recipientField] = this.props.thread[recipientField].filter(\n                        (additionalOrSuggestedRecipient) =>\n                            additionalOrSuggestedRecipient.partner_id !== recipient.partner_id ||\n                            additionalOrSuggestedRecipient.email !== recipient.email\n                    );\n                },\n            });\n        };\n        for (const recipient of this.props.thread.suggestedRecipients) {\n            createTagForRecipient(recipient, \"suggestedRecipients\");\n        }\n        for (const recipient of this.props.thread.additionalRecipients) {\n            createTagForRecipient(recipient, \"additionalRecipients\");\n        }\n        return tags;\n    }\n\n    /** @return {Array[SuggestedRecipient]}*/\n    getAllMailThreadRecipients() {\n        return [\n            ...this.props.thread.suggestedRecipients,\n            ...this.props.thread.additionalRecipients,\n        ];\n    }\n\n    /**\n     * This method updates a recipient with a new email address.\n     * @param {string} emailNormalized email address to be set on the partner. The address is not a mailbox\n     * notation and only address, e.g. \"Raoulette <raoulette@gmail.com>\" is not accepted but \"raoulette@gmail.com\"\n     * is accepted as input.\n     * @param {number} recipientPartnerId ID of the partner to update\n     */\n    async updateRecipient(emailNormalized, recipientPartnerId) {\n        await this.orm.write(\"res.partner\", [recipientPartnerId], { email: emailNormalized });\n        const allRecipients = this.getAllMailThreadRecipients();\n        allRecipients.some((oldRecipient) => {\n            if (oldRecipient.partner_id === recipientPartnerId) {\n                oldRecipient.email = emailNormalized;\n                return true;\n            }\n        });\n    }\n\n    /**\n     * @param {SuggestedRecipient} recipient\n     * @returns {boolean}\n     */\n    hasRecipient(recipient) {\n        return this.getAllMailThreadRecipients().some(\n            (current) => current.email === recipient.email\n        );\n    }\n\n    /** @param {SuggestedRecipient} recipient */\n    insertAdditionalRecipient(recipient) {\n        if (this.hasRecipient(recipient)) {\n            return;\n        }\n        this.props.thread.additionalRecipients.push(recipient);\n    }\n\n    /** @returns {string} */\n    getPlaceholder() {\n        const hasRecipients =\n            this.props.thread.suggestedRecipients.length ||\n            this.props.thread.additionalRecipients.length;\n        return hasRecipients ? \"\" : _t(\"Followers only\");\n    }\n}\n", "import { usePopover } from \"@web/core/popover/popover_hook\";\nimport { TagsList } from \"@web/core/tags_list/tags_list\";\nimport { RecipientsInputTagsListPopover } from \"./recipients_input_tags_list_popover\";\n\nimport { onWillUpdateProps, toRaw, useEffect, useRef, useState } from \"@odoo/owl\";\n\n/**\n * Override of the TagsList so that the email address of each recipients can be checked.\n * If a recipient doesn't have an email address set to its partner a popover is opened below the corresponding\n * Tag.\n */\nexport class RecipientsInputTagsList extends TagsList {\n    static template = \"web.RecipientsInputTagsList\";\n    static props = {\n        ...TagsList.props,\n        updateRecipient: { type: Function, optional: true },\n    };\n    static defaultProps = { ...TagsList.defaultProps, updateRecipient: () => {} };\n    setup() {\n        this.popover = usePopover(RecipientsInputTagsListPopover, {\n            closeOnClickAway: false,\n            position: \"bottom-middle\",\n        });\n        this.tagToUpdateRef = useRef(\"tagToUpdate\");\n        this.state = useState({\n            tagToUpdate: this.getFirstTagToUpdate(this.props.tags),\n        });\n        onWillUpdateProps((nextProps) => {\n            this.state.tagToUpdate = this.getFirstTagToUpdate(nextProps.tags);\n        });\n        useEffect(\n            () => {\n                if (this.state.tagToUpdate && this.tagToUpdateRef.el) {\n                    this.updateTag();\n                } else if (this.popover.isOpen) {\n                    this.popover.close();\n                }\n            },\n            () => [this.state.tagToUpdate, this.tagToUpdateRef.el]\n        );\n    }\n\n    getFirstTagToUpdate(tags) {\n        for (const tag of tags) {\n            if (!tag.email) {\n                return tag;\n            }\n        }\n    }\n\n    tagEquals(tag1, tag2) {\n        return toRaw(tag1) === toRaw(tag2);\n    }\n\n    updateTag() {\n        this.popover.open(this.tagToUpdateRef.el, {\n            tagToUpdate: this.state.tagToUpdate,\n            onUpdateTag: (newEmail) =>\n                this.props.updateRecipient(newEmail, this.state.tagToUpdate.resId),\n        });\n    }\n}\n", "import { parseEmail } from \"@mail/utils/common/format\";\nimport { getActiveHotkey } from \"@web/core/hotkeys/hotkey_service\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { isEmail } from \"@web/core/utils/strings\";\n\nimport { Component, useExternalListener, useRef, useState } from \"@odoo/owl\";\n/**\n * This class represents the popover opened when we detect that one of our recipients is missing an email\n * address in the RecipientsInput. It allows the user to correct this error and update the partner\n * with an email address.\n */\nexport class RecipientsInputTagsListPopover extends Component {\n    static props = {\n        tagToUpdate: { type: Object },\n        onUpdateTag: { type: Function },\n        close: { type: Function },\n    };\n    static template = \"mail.RecipientsInputTagsListPopover\";\n\n    setup() {\n        this.orm = useService(\"orm\");\n        this.state = useState({ value: \"\" });\n        this.popoverRef = useRef(\"tagsListPopoverRef\");\n        useExternalListener(window, \"click\", (ev) => {\n            if (!this.popoverRef.el?.contains(ev.target)) {\n                this.discardTag();\n            }\n        });\n    }\n\n    onKeydown(ev) {\n        const hotkey = getActiveHotkey(ev);\n        this.state.error = false;\n        if (hotkey === \"enter\") {\n            this.updateTag();\n        }\n        if (hotkey === \"escape\") {\n            this.discardTag();\n        }\n    }\n\n    updateTag() {\n        if (!this.isValidEmail) {\n            this.state.error = true;\n            return;\n        }\n        this.props.onUpdateTag(this.state.value);\n        this.props.close();\n    }\n\n    discardTag() {\n        this.props.tagToUpdate.onDelete();\n        this.props.close();\n    }\n\n    get isValidEmail() {\n        const value = parseEmail(this.state.value);\n        const name = value ? value[0] : \"\";\n        return isEmail(name);\n    }\n}\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { useService } from \"@web/core/utils/hooks\";\n\nimport { Component, onWillStart } from \"@odoo/owl\";\n\n/**\n * This popover is used to show a card with details for the recipients' partner with its name,\n * email and phone number. The card can also redirect the user to the `res.partner` form view if he\n * wants more details or edit said partner.\n */\nexport class RecipientsPopover extends Component {\n    static template = \"mail.RecipientsPopover\";\n    static props = {\n        id: { type: Number, required: true },\n        close: { type: Function, required: true },\n        viewProfileBtnOverride: { type: Function },\n    };\n\n    setup() {\n        this.orm = useService(\"orm\");\n        onWillStart(async () => {\n            [this.partner] = await this.orm.read(\"res.partner\", [this.props.id], this.fieldNames);\n        });\n    }\n\n    get name() {\n        return this.partner.name || this.partner.display_name || _t(\"Unnamed\");\n    }\n\n    get phone() {\n        return this.partner.phone;\n    }\n\n    get email() {\n        return this.partner.email_normalized || this.partner.email;\n    }\n\n    get fieldNames() {\n        return [\"name\", \"email_normalized\", \"email\", \"phone\", \"display_name\"];\n    }\n\n    onClickViewProfile() {\n        this.props.close();\n        this.props.viewProfileBtnOverride();\n    }\n}\n", "import { fields } from \"@mail/core/common/record\";\nimport { Store } from \"@mail/core/common/store_service\";\nimport { browser } from \"@web/core/browser/browser\";\nimport { _t } from \"@web/core/l10n/translation\";\n\nimport { patch } from \"@web/core/utils/patch\";\n\nconst unread_store = (() => {\n    if (!window.idbKeyval) {\n        return undefined;\n    }\n    return new window.idbKeyval.Store(\"odoo-mail-unread-db\", \"odoo-mail-unread-store\");\n})();\n\n/** @type {import(\"models\").Store} */\nconst StorePatch = {\n    setup() {\n        super.setup(...arguments);\n        this.activityCounter = 0;\n        this.activity_counter_bus_id = 0;\n        /** @type {Object[]} */\n        this.activityGroups = fields.Attr([], {\n            onUpdate() {\n                this.onUpdateActivityGroups();\n            },\n            sort(g1, g2) {\n                /**\n                 * Sort by model ID ASC but always place the activity group for \"mail.activity\" model at\n                 * the end (other activities).\n                 */\n                const getSortId = (activityGroup) =>\n                    activityGroup.model === \"mail.activity\" ? Number.MAX_VALUE : activityGroup.id;\n                return getSortId(g1) - getSortId(g2);\n            },\n        });\n        this.globalCounter = fields.Attr(0, {\n            compute() {\n                return this.computeGlobalCounter();\n            },\n            onUpdate() {\n                this.updateAppBadge();\n            },\n            eager: true,\n        });\n        this.inbox = fields.One(\"Thread\");\n        this.starred = fields.One(\"Thread\");\n        this.history = fields.One(\"Thread\");\n    },\n    computeGlobalCounter() {\n        return this.inbox?.counter ?? 0;\n    },\n    async initialize() {\n        await Promise.all([\n            this.fetchStoreData(\"failures\"),\n            this.fetchStoreData(\"systray_get_activities\"),\n            super.initialize(...arguments),\n        ]);\n    },\n    onPushNotificationDisplayed() {\n        super.onPushNotificationDisplayed(...arguments);\n        this.updateAppBadge();\n    },\n    onStarted() {\n        super.onStarted(...arguments);\n        this.inbox = {\n            display_name: _t(\"Inbox\"),\n            id: \"inbox\",\n            model: \"mail.box\",\n        };\n        this.starred = {\n            display_name: _t(\"Starred messages\"),\n            id: \"starred\",\n            model: \"mail.box\",\n        };\n        this.history = {\n            display_name: _t(\"History\"),\n            id: \"history\",\n            model: \"mail.box\",\n        };\n        try {\n            // useful for synchronizing activity data between multiple tabs\n            this.activityBroadcastChannel = new browser.BroadcastChannel(\"mail.activity.channel\");\n            this.activityBroadcastChannel.onmessage =\n                this._onActivityBroadcastChannelMessage.bind(this);\n        } catch {\n            // BroadcastChannel API is not supported (e.g. Safari < 15.4), so disabling it.\n            this.activityBroadcastChannel = null;\n        }\n    },\n    onUpdateActivityGroups() {},\n    /**\n     * @param {string} resModel\n     * @param {number[]} resIds\n     * @param {number|undefined} defaultActivityTypeId\n     */\n    async scheduleActivity(resModel, resIds, defaultActivityTypeId = undefined) {\n        const context = {\n            active_model: resModel,\n            active_ids: resIds,\n            active_id: resIds[0],\n            ...(defaultActivityTypeId !== undefined\n                ? { default_activity_type_id: defaultActivityTypeId }\n                : {}),\n        };\n        await new Promise((resolve) =>\n            this.env.services.action.doAction(\n                {\n                    type: \"ir.actions.act_window\",\n                    name:\n                        resIds && resIds.length > 1\n                            ? _t(\"Schedule Activity On Selected Records\")\n                            : _t(\"Schedule Activity\"),\n                    res_model: \"mail.activity.schedule\",\n                    view_mode: \"form\",\n                    views: [[false, \"form\"]],\n                    target: \"new\",\n                    context,\n                },\n                {\n                    onClose: resolve,\n                    additionalContext: {\n                        dialog_size: \"large\",\n                    },\n                }\n            )\n        );\n    },\n    updateAppBadge() {\n        if (unread_store) {\n            window.idbKeyval.set(\"unread\", this.globalCounter, unread_store);\n            Promise.resolve(navigator.setAppBadge?.(this.globalCounter)).catch(() => {}); // FIXME: Illegal invocation error in HOOT\n        }\n    },\n    /**\n     * @param {object} param0\n     * @param {{ type: \"INSERT\"|\"DELETE\"|\"RELOAD_CHATTER\", payload: Partial<import(\"models\").Activity> }} param0.data\n     */\n    _onActivityBroadcastChannelMessage({ data }) {\n        switch (data.type) {\n            case \"INSERT\":\n                this.insert(data.payload, { broadcast: false });\n                break;\n            case \"DELETE\": {\n                const activity = this[\"mail.activity\"].insert(data.payload, { broadcast: false });\n                activity.remove({ broadcast: false });\n                break;\n            }\n            case \"RELOAD_CHATTER\": {\n                const thread = this.Thread.insert({\n                    model: data.payload.model,\n                    id: data.payload.id,\n                });\n                thread.fetchNewMessages();\n                break;\n            }\n        }\n    },\n    async unstarAll() {\n        // apply the change immediately for faster feedback\n        this.store.starred.counter = 0;\n        this.store.starred.messages = [];\n        await this.env.services.orm.call(\"mail.message\", \"unstar_all\");\n    },\n    handleClickOnLink(ev, thread) {\n        const model = ev.target.dataset.oeModel;\n        const id = Number(ev.target.dataset.oeId);\n        const isLinkHandledBySuper = super.handleClickOnLink(...arguments);\n        if (!isLinkHandledBySuper && ev.target.tagName === \"A\" && id && model) {\n            ev.preventDefault();\n            Promise.resolve(\n                this.env.services.action.doAction({\n                    type: \"ir.actions.act_window\",\n                    res_model: model,\n                    views: [[false, \"form\"]],\n                    res_id: id,\n                })\n            ).then(() => this.onLinkFollowed(thread));\n            return true;\n        }\n        return false;\n    },\n    /** @param {import(\"models\").Thread} fromThread */\n    onLinkFollowed(fromThread) {},\n};\npatch(Store.prototype, StorePatch);\n", "import { registerThreadAction } from \"@mail/core/common/thread_actions\";\n\nimport { _t } from \"@web/core/l10n/translation\";\n\nregisterThreadAction(\"mark-all-read\", {\n    condition: ({ owner, thread }) =>\n        thread?.id === \"inbox\" && !owner.isDiscussSidebarChannelActions,\n    disabledCondition: ({ thread }) => thread.isEmpty,\n    open: ({ store }) => store.env.services.orm.silent.call(\"mail.message\", \"mark_all_as_read\"),\n    sequence: 1,\n    name: _t(\"Mark all read\"),\n});\nregisterThreadAction(\"unstar-all\", {\n    condition: ({ owner, thread }) =>\n        thread?.id === \"starred\" && !owner.isDiscussSidebarChannelActions,\n    disabledCondition: ({ thread }) => thread.isEmpty,\n    open: ({ store }) => store.unstarAll(),\n    sequence: 2,\n    name: _t(\"Unstar all\"),\n});\n", "import { Thread } from \"@mail/core/common/thread_model\";\n\nimport { patch } from \"@web/core/utils/patch\";\nimport { fields } from \"../common/record\";\nimport { compareDatetime } from \"@mail/utils/common/misc\";\nimport { rpc } from \"@web/core/network/rpc\";\n\n/** @type {import(\"models\").Thread} */\nconst threadPatch = {\n    setup() {\n        super.setup();\n        /** @type {number|undefined} */\n        this.recipientsCount = undefined;\n        this.recipients = fields.Many(\"mail.followers\");\n        this.activities = fields.Many(\"mail.activity\", {\n            sort: (a, b) => compareDatetime(a.date_deadline, b.date_deadline) || a.id - b.id,\n            onDelete(r) {\n                r.remove();\n            },\n        });\n        /** @type {boolean} */\n        this.isDisplayedInDiscussAppDesktop = fields.Attr(undefined, {\n            /** @this {import(\"models\").Thread} */\n            compute() {\n                if (this.store.discuss.isActive && !this.store.env.services.ui.isSmall) {\n                    return this.eq(this.store.discuss.thread);\n                }\n                return false;\n            },\n        });\n    },\n    get recipientsFullyLoaded() {\n        return this.recipientsCount === this.recipients.length;\n    },\n    computeIsDisplayed() {\n        return this.isDisplayedInDiscussAppDesktop || super.computeIsDisplayed();\n    },\n    async loadMoreFollowers() {\n        const data = await this.store.env.services.orm.call(this.model, \"message_get_followers\", [\n            [this.id],\n            this.followers.at(-1).id,\n        ]);\n        this.store.insert(data);\n    },\n    async loadMoreRecipients() {\n        const data = await this.store.env.services.orm.call(\n            this.model,\n            \"message_get_followers\",\n            [[this.id], this.recipients.at(-1).id],\n            { filter_recipients: true }\n        );\n        this.store.insert(data);\n    },\n    /** @override */\n    open(options) {\n        const res = super.open(...arguments);\n        if (res) {\n            return res;\n        }\n        if (this.model === \"mail.box\") {\n            if (this.store.discuss.isActive) {\n                this.setAsDiscussThread();\n            } else {\n                this.store.env.services.action.doAction({\n                    context: { active_id: `mail.box_${this.id}` },\n                    tag: \"mail.action_discuss\",\n                    type: \"ir.actions.client\",\n                });\n            }\n        } else {\n            this.store.env.services.action.doAction({\n                type: \"ir.actions.act_window\",\n                res_id: this.id,\n                res_model: this.model,\n                views: [[false, \"form\"]],\n            });\n        }\n        return true;\n    },\n    async unpin() {\n        await this.store.chatHub.initPromise;\n        const chatWindow = this.store.ChatWindow.get({ thread: this });\n        await chatWindow?.close();\n        await super.unpin(...arguments);\n    },\n    async follow() {\n        const data = await rpc(\"/mail/thread/subscribe\", {\n            res_model: this.model,\n            res_id: this.id,\n            partner_ids: [this.store.self.id],\n        });\n        this.store.insert(data);\n    },\n};\npatch(Thread.prototype, threadPatch);\n", "const { DateTime } = luxon;\n\n/**\n * @param {luxon.DateTime} datetime\n */\nexport function computeDelay(datetime) {\n    if (!datetime) {\n        return 0;\n    }\n    const today = DateTime.now().startOf(\"day\");\n    return datetime.diff(today, \"days\").days;\n}\n\nexport function getMsToTomorrow() {\n    const now = new Date();\n    const night = new Date(\n        now.getFullYear(),\n        now.getMonth(),\n        now.getDate() + 1, // the next day\n        0,\n        0,\n        0 // at 00:00:00 hours\n    );\n    return night.getTime() - now.getTime();\n}\n\nexport function isToday(datetime) {\n    if (!datetime) {\n        return false;\n    }\n    return (\n        datetime.toLocaleString(DateTime.DATE_FULL) ===\n        DateTime.now().toLocaleString(DateTime.DATE_FULL)\n    );\n}\n", "import { htmlEscape, markup } from \"@odoo/owl\";\n\nimport { router } from \"@web/core/browser/router\";\nimport { loadEmoji, loader } from \"@web/core/emoji_picker/emoji_picker\";\nimport { normalize } from \"@web/core/l10n/utils\";\nimport {\n    createDocumentFragmentFromContent,\n    createElementWithContent,\n    htmlFormatList,\n    htmlJoin,\n    htmlReplace,\n    htmlReplaceAll,\n    htmlTrim,\n    setElementContent,\n} from \"@web/core/utils/html\";\nimport { escapeRegExp } from \"@web/core/utils/strings\";\nimport { getOrigin } from \"@web/core/utils/urls\";\nimport { setAttributes } from \"@web/core/utils/xml\";\n\nconst urlRegexp =\n    /\\b(?:https?:\\/\\/\\d{1,3}(?:\\.\\d{1,3}){3}|(?:https?:\\/\\/|(?:www\\.))[-a-z0-9@:%._+~#=\\u00C0-\\u024F\\u1E00-\\u1EFF]{1,256}\\.[a-z]{2,13})\\b(?:[-a-z0-9@:%_+~#?&[\\]^|{}`\\\\'$//=\\u00C0-\\u024F\\u1E00-\\u1EFF]|[.]*[-a-z0-9@:%_+~#?&[\\]^|{}`\\\\'$//=\\u00C0-\\u024F\\u1E00-\\u1EFF]|,(?!$| )|\\.(?!$| |\\.)|;(?!$| ))*/gi;\nconst messageUrlRegExp = new RegExp(`^${escapeRegExp(getOrigin())}/mail/message/(\\\\d+)$`);\n\n/**\n * @param {string|ReturnType<markup>} rawBody\n * @param {Object} validMentions\n * @param {import(\"models\").Persona[]} validMentions.partners\n * @returns {Promise<string|ReturnType<markup>>}\n */\nexport function prettifyMessageText(rawBody, { validMentions = {}, thread } = {}) {\n    if (rawBody instanceof markup().constructor) {\n        // markup is already \"pretty\"\n        return rawBody;\n    }\n    let body = htmlTrim(rawBody);\n    body = htmlReplace(body, /(\\r|\\n){2,}/g, () => markup`<br/><br/>`);\n    body = htmlReplace(body, /(\\r|\\n)/g, () => markup`<br/>`);\n    body = htmlReplace(body, /&nbsp;/g, () => \" \");\n    body = htmlTrim(body);\n    // This message will be received from the mail composer as html content\n    // subtype but the urls will not be linkified. If the mail composer\n    // takes the responsibility to linkify the urls we end up with double\n    // linkification a bit everywhere. Ideally we want to keep the content\n    // as text internally and only make html enrichment at display time but\n    // the current design makes this quite hard to do.\n    body = generateMentionsLinks(body, { ...validMentions, thread });\n    body = parseAndTransform(body, addLink);\n    return body;\n}\n\n/**\n * @param {string|ReturnType<markup>} htmlBody\n */\nexport async function generateEmojisOnHtml(htmlBody, { allowEmojiLoading = true } = {}) {\n    let body = htmlBody;\n    if (allowEmojiLoading || odoo.loader.modules.get(\"@web/core/emoji_picker/emoji_data\")) {\n        body = await _generateEmojisOnHtml(body);\n    }\n    return body;\n}\n\n/**\n * @param {string|ReturnType<markup>} rawBody\n * @param {Object} validMentions\n * @param {import(\"models\").Persona[]} validMentions.partners\n */\nexport async function prettifyMessageContent(\n    rawBody,\n    { validMentions = [], allowEmojiLoading = true } = {}\n) {\n    let body = prettifyMessageText(rawBody, { validMentions });\n    body = await generateEmojisOnHtml(body, { allowEmojiLoading });\n    return body;\n}\n\n/**\n * WARNING: this is not enough to unescape potential XSS contained in htmlString, transformFunction\n * should handle it or it should be handled after/before calling parseAndTransform. So if the result\n * of this function is used in a t-raw, be very careful.\n *\n * @param {string|ReturnType<markup>} htmlString\n * @param {function} transformFunction\n * @returns {ReturnType<markup>}\n */\nexport function parseAndTransform(htmlString, transformFunction) {\n    const div = document.createElement(\"div\");\n    try {\n        setElementContent(div, htmlString);\n    } catch {\n        div.appendChild(createElementWithContent(\"pre\", htmlString));\n    }\n    return _parseAndTransform(Array.from(div.childNodes), transformFunction);\n}\n\n/**\n * @param {Node[]} nodes\n * @param {function} transformFunction with:\n *   param node\n *   param function\n *   return string\n * @return {ReturnType<markup>}\n */\nfunction _parseAndTransform(nodes, transformFunction) {\n    if (!nodes) {\n        return;\n    }\n    return htmlJoin(\n        Object.values(nodes).map((node) =>\n            transformFunction(node, function () {\n                return _parseAndTransform(node.childNodes, transformFunction);\n            })\n        )\n    );\n}\n\n/**\n * @param {string} text\n * @return {ReturnType<markup>} linkified text\n */\nfunction linkify(text) {\n    let curIndex = 0;\n    let result = \"\";\n    let match;\n    while ((match = urlRegexp.exec(text)) !== null) {\n        result = htmlJoin([result, text.slice(curIndex, match.index)]);\n        // Decode the url first, in case it's already an encoded url\n        const inputUrl = decodeURI(match[0]);\n        const url = !/^https?:\\/\\//i.test(inputUrl) ? \"http://\" + inputUrl : inputUrl;\n        const link = document.createElement(\"a\");\n        setAttributes(link, {\n            target: \"_blank\",\n            rel: \"noreferrer noopener\",\n            href: encodeURI(url),\n        });\n        link.textContent = inputUrl;\n        const messageMatch = messageUrlRegExp.exec(url);\n        if (messageMatch !== null) {\n            setAttributes(link, {\n                \"data-oe-id\": messageMatch[1],\n                \"data-oe-model\": \"mail.message\",\n            });\n            link.classList.add(\"o_message_redirect\");\n        }\n        // markup: outerHTML is safe when used as a node\n        result = htmlJoin([result, markup(link.outerHTML)]);\n        curIndex = match.index + match[0].length;\n    }\n    return htmlJoin([result, text.slice(curIndex)]);\n}\n\n/**\n * @param {Node} node\n * @param {function} transformFunction\n * @return {ReturnType<markup>}\n */\nexport function addLink(node, transformChildren) {\n    if (node.nodeType === 3) {\n        // text node\n        const linkified = linkify(node.textContent);\n        if (linkified.toString() !== node.textContent) {\n            const div = createElementWithContent(\"div\", linkified);\n            for (const childNode of [...div.childNodes]) {\n                node.parentNode.insertBefore(childNode, node);\n            }\n            node.parentNode.removeChild(node);\n            return linkified;\n        }\n        return node.textContent;\n    }\n    if (node.tagName === \"A\") {\n        return markup(node.outerHTML);\n    }\n    transformChildren();\n    return markup(node.outerHTML);\n}\n\nfunction generateMentionElement({ className, id, model, text }) {\n    const link = document.createElement(\"a\");\n    setAttributes(link, {\n        href: router.stateToUrl({ model: model, resId: id }),\n        class: className,\n        \"data-oe-id\": id,\n        \"data-oe-model\": model,\n        \"data-oe-protected\": \"true\",\n        target: \"_blank\",\n        contenteditable: \"false\",\n    });\n    link.textContent = text;\n    return link;\n}\n\n/**\n * @param {import(\"models\").ResPartner} partner\n * @param {import(\"models\").Thread} thread\n */\nexport function generatePartnerMentionElement(partner, thread) {\n    return generateMentionElement({\n        className: \"o_mail_redirect\",\n        id: partner.id,\n        model: \"res.partner\",\n        text: `@${thread?.getPersonaName(partner) ?? partner.name}`,\n    });\n}\n\n/** @param {import(\"models\").ResRole} role */\nexport function generateRoleMentionElement(role) {\n    return generateMentionElement({\n        className: \"o-discuss-mention\",\n        id: role.id,\n        model: \"res.role\",\n        text: `@${role.name}`,\n    });\n}\n\n/** @param {string} label */\nexport function generateSpecialMentionElement(label) {\n    const link = document.createElement(\"a\");\n    setAttributes(link, {\n        class: \"o-discuss-mention\",\n        \"data-oe-protected\": \"true\",\n        contenteditable: \"false\",\n    });\n    link.textContent = `@${label}`;\n    return link;\n}\n\n/** @param {import(\"models\").Thread} thread */\nexport function generateThreadMentionElement(thread) {\n    return generateMentionElement({\n        className: `o_channel_redirect${\n            thread.parent_channel_id ? \" o_channel_redirect_asThread\" : \"\"\n        }`,\n        id: thread.id,\n        model: \"discuss.channel\",\n        text: `#${thread.fullNameWithParent}`,\n    });\n}\n\n/**\n * @param {string|ReturnType<markup>} body\n * @param {Object} param1\n * @param {import(\"models\").ResPartner[]} param1.partners\n * @param {import(\"models\").ResRole[]} param1.roles\n * @param {import(\"models\").Thread[]} param1.threads\n * @param {string[]} param1.specialMentions\n * @param {import(\"models\").Thread} param1.thread\n * @return {ReturnType<markup>}\n */\nfunction generateMentionsLinks(\n    body,\n    { partners = [], roles = [], threads = [], specialMentions = [], thread }\n) {\n    const mentions = [];\n    for (const partner of partners) {\n        const placeholder = `@-mention-partner-${partner.id}`;\n        const text = `@${thread?.getPersonaName(partner) ?? partner.name}`;\n        mentions.push({\n            link: generatePartnerMentionElement(partner, thread),\n            placeholder,\n        });\n        body = htmlReplace(body, text, placeholder);\n    }\n    for (const thread of threads) {\n        const placeholder = `#-mention-channel-${thread.id}`;\n        const text = `#${thread.fullNameWithParent}`;\n        mentions.push({\n            link: generateThreadMentionElement(thread),\n            placeholder,\n        });\n        body = htmlReplace(body, text, placeholder);\n    }\n    for (const special of specialMentions) {\n        const text = `@${special}`;\n        const placeholder = `@-mention-special-${special}`;\n        mentions.push({\n            link: generateSpecialMentionElement(special),\n            placeholder,\n        });\n        body = htmlReplace(body, text, placeholder);\n    }\n    for (const role of roles) {\n        const placeholder = `@-mention-role-${role.id}`;\n        const text = `@${role.name}`;\n        mentions.push({\n            link: generateRoleMentionElement(role),\n            placeholder,\n        });\n        body = htmlReplace(body, text, placeholder);\n    }\n    for (const mention of mentions) {\n        const link = mention.link;\n        // markup: outerHTML is safe when used as a node\n        body = htmlReplace(body, mention.placeholder, markup(link.outerHTML));\n    }\n    return htmlEscape(body);\n}\n\n/**\n * @private\n * @param {string|ReturnType<markup>} htmlString\n * @returns {Promise<ReturnType<markup>>}\n */\nasync function _generateEmojisOnHtml(htmlString) {\n    const { emojis } = await loadEmoji();\n    for (const emoji of emojis) {\n        for (const source of [...emoji.shortcodes, ...emoji.emoticons]) {\n            const escapedSource = htmlEscape(String(source));\n            const regexp = new RegExp(\n                \"(\\\\s|^)(\" + escapeRegExp(escapedSource) + \")(?=\\\\s|$|<)\",\n                \"g\"\n            );\n            htmlString = htmlReplace(htmlString, regexp, (_, group1) => group1 + emoji.codepoints);\n        }\n    }\n    return htmlEscape(htmlString);\n}\n\n/**\n * @param {string|ReturnType<markup>} body\n * @returns {ReturnType<markup>}\n */\nexport function getNonEditableMentions(body) {\n    const doc = createDocumentFragmentFromContent(body);\n    for (const block of doc.body.querySelectorAll(\".o_mail_reply_hide\")) {\n        block.classList.remove(\"o_mail_reply_hide\");\n    }\n    // for mentioned partner\n    for (const mention of doc.body.querySelectorAll(\".o_mail_redirect\")) {\n        mention.setAttribute(\"contenteditable\", false);\n    }\n    // for mentioned channel\n    for (const mention of doc.body.querySelectorAll(\".o_channel_redirect\")) {\n        mention.setAttribute(\"contenteditable\", false);\n    }\n    // for special mentions\n    for (const mention of doc.body.querySelectorAll(\".o-discuss-mention\")) {\n        mention.setAttribute(\"contenteditable\", false);\n    }\n    return markup(doc.body.innerHTML);\n}\n\n/**\n * @param {string|ReturnType<markup>} htmlString\n * @returns {string}\n */\nexport function htmlToTextContentInline(htmlString) {\n    htmlString = htmlReplace(htmlString, /<br\\s*\\/?>/gi, () => \" \");\n    const div = document.createElement(\"div\");\n    try {\n        setElementContent(div, htmlString);\n    } catch {\n        div.appendChild(createElementWithContent(\"pre\", htmlString));\n    }\n    return div.textContent\n        .trim()\n        .replace(/[\\n\\r]/g, \"\")\n        .replace(/\\s\\s+/g, \" \");\n}\n\nexport function convertBrToLineBreak(str) {\n    str = htmlReplace(str, /<br\\s*\\/?>/gi, () => \"\\n\");\n    return createDocumentFragmentFromContent(str).body.textContent;\n}\n\nexport function cleanTerm(term) {\n    return typeof term === \"string\" ? normalize(term) : \"\";\n}\n\n/**\n * Parses text to find email: Tagada <address@mail.fr> -> [Tagada, address@mail.fr] or False\n *\n * @param {string} text\n * @returns {[string,string|boolean]|false}\n */\nexport function parseEmail(text) {\n    if (!text) {\n        return;\n    }\n    let result = text.match(/\"?(.*?)\"? <(.*@.*)>/);\n    if (result) {\n        const name = (result[1] || \"\").trim().replace(/(^\"|\"$)/g, \"\");\n        return [name, (result[2] || \"\").trim()];\n    }\n    result = text.match(/(.*@.*)/);\n    if (result) {\n        return [String(result[1] || \"\").trim(), String(result[1] || \"\").trim()];\n    }\n    return [text, false];\n}\n\nexport const EMOJI_REGEX = /\\p{Emoji_Presentation}|\\p{Emoji}\\uFE0F|\\u200d/gu;\n\n/**\n * Wrap emojis present in the given text with a title and return a safe HTML\n * string.\n *\n * @param {string|ReturnType<markup>} content\n * @returns {ReturnType<markup>}\n */\nexport function decorateEmojis(content) {\n    if (!loader.loaded || !content) {\n        return content;\n    }\n    const doc = createDocumentFragmentFromContent(content);\n    const nodes = doc.evaluate(\n        \".//text()\",\n        doc.body,\n        null,\n        XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE,\n        null\n    );\n    for (let i = 0; i < nodes.snapshotLength; i++) {\n        const node = nodes.snapshotItem(i);\n        const span = document.createElement(\"span\");\n        setElementContent(\n            span,\n            htmlReplaceAll(node.textContent, loader.loaded.emojiRegex, (codepoints) =>\n                markup(\n                    `<span class=\"o-mail-emoji\" title=\"${htmlFormatList(\n                        loader.loaded.emojiValueToShortcodes[codepoints],\n                        { style: \"unit-narrow\" }\n                    )}\">${htmlEscape(codepoints)}</span>`\n                )\n            )\n        );\n        node.replaceWith(...span.childNodes);\n    }\n    return markup(doc.body.innerHTML);\n}\n\n/**\n * Converts an object of key/value to string, where object represents a attClass with OWL syntax object\n * and value is evaluation of each key.\n * Example: \"attClassObjectToString({ a: 1, b: 0, c: 1 })\" converts to \"a c\".\n */\nexport function attClassObjectToString(obj) {\n    return Object.entries(obj)\n        .filter(([_, val]) => val)\n        .map(([key, _]) => key)\n        .join(\" \");\n}\n", "import {\n    Component,\n    onMounted,\n    onPatched,\n    onWillUnmount,\n    toRaw,\n    useComponent,\n    useEffect,\n    useRef,\n    useState,\n    useSubEnv,\n    xml,\n} from \"@odoo/owl\";\n\nimport { monitorAudio } from \"@mail/utils/common/media_monitoring\";\nimport { browser } from \"@web/core/browser/browser\";\nimport { OVERLAY_SYMBOL } from \"@web/core/overlay/overlay_container\";\nimport { Deferred } from \"@web/core/utils/concurrency\";\nimport { makeDraggableHook } from \"@web/core/utils/draggable_hook_builder_owl\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { useService } from \"@web/core/utils/hooks\";\n\nexport function useLazyExternalListener(target, eventName, handler, eventParams) {\n    const boundHandler = handler.bind(useComponent());\n    let t;\n    onMounted(() => {\n        t = target();\n        if (!t) {\n            return;\n        }\n        t.addEventListener(eventName, boundHandler, eventParams);\n    });\n    onPatched(() => {\n        const t2 = target();\n        if (t !== t2) {\n            if (t) {\n                t.removeEventListener(eventName, boundHandler, eventParams);\n            }\n            if (t2) {\n                t2.addEventListener(eventName, boundHandler, eventParams);\n            }\n            t = t2;\n        }\n    });\n    onWillUnmount(() => {\n        if (!t) {\n            return;\n        }\n        t.removeEventListener(eventName, boundHandler, eventParams);\n    });\n}\n\nexport function onExternalClick(refName, cb) {\n    let downTarget, upTarget;\n    const ref = useRef(refName);\n    function onClick(ev) {\n        if (ref.el && !ref.el.contains(ev.composedPath()[0])) {\n            cb(ev, { downTarget, upTarget });\n            upTarget = downTarget = null;\n        }\n    }\n    function onMousedown(ev) {\n        downTarget = ev.target;\n    }\n    function onMouseup(ev) {\n        upTarget = ev.target;\n    }\n    onMounted(() => {\n        document.body.addEventListener(\"mousedown\", onMousedown, true);\n        document.body.addEventListener(\"mouseup\", onMouseup, true);\n        document.body.addEventListener(\"click\", onClick, true);\n    });\n    onWillUnmount(() => {\n        document.body.removeEventListener(\"mousedown\", onMousedown, true);\n        document.body.removeEventListener(\"mouseup\", onMouseup, true);\n        document.body.removeEventListener(\"click\", onClick, true);\n    });\n}\n\n/**\n * Hook that allows to determine precisely when refs are (mouse-)hovered.\n * Should provide a list of ref names, and can add callbacks when elements are\n * hovered-in (onHover), hovered-out (onAway), hovering for some time (onHovering).\n *\n * @param {string | string[] | Function} refNames name of refs that determine whether this is in state \"hovering\".\n *   ref name that end with \"*\" means it takes parented HTML node into account too. Useful for floating\n *   menu where dropdown menu container is not accessible. Function type is for useChildRef support.\n * @param {Object} param1\n * @param {() => void} [param1.onHover] callback when hovering the ref names.\n * @param {() => void} [param1.onAway] callback when stop hovering the ref names.\n * @param {number, () => void} [param1.onHovering] array where 1st param is duration until start hovering\n *   and function to be executed at this delay duration after hovering is kept true.\n * @param {() => Array} [param1.stateObserver] when provided, function that, when called, returns list of\n *   reactive state related to presence of targets' el. This is used to help the hook detect when the targets\n *   are removed from DOM, to properly mark the hovered target as non-hovered.\n * @returns {({ isHover: boolean })}\n */\nexport function useHover(refNames, { onHover, onAway, stateObserver, onHovering } = {}) {\n    refNames = Array.isArray(refNames) ? refNames : [refNames];\n    const targets = [];\n    let wasHovering = false;\n    let hoveringTimeout;\n    let awayTimeout;\n    let lastHoveredTarget;\n    for (const refName of refNames) {\n        if (typeof refName === \"function\") {\n            // Special case: useChildRef support\n            targets.push({ ref: refName });\n            continue;\n        }\n        targets.push({ ref: useRef(refName) });\n    }\n    const state = useState({\n        set isHover(newIsHover) {\n            if (this._isHover !== newIsHover) {\n                this._isHover = newIsHover;\n                this._count++;\n            }\n        },\n        get isHover() {\n            void this._count;\n            return this._isHover;\n        },\n        _contains: [],\n        _count: 0,\n        _isHover: false,\n        _targets: targets,\n        addTarget(target) {\n            state._targets.push(target);\n            const handleMouseenter = (ev) => onmouseenter(ev);\n            const handleMouseleave = (ev) => onmouseleave(ev);\n            target.ref.el.addEventListener(\"mouseenter\", handleMouseenter, true);\n            target.ref.el.addEventListener(\"mouseleave\", handleMouseleave, true);\n            return () => {\n                target.ref.el.removeEventListener(\"mouseenter\", handleMouseenter, true);\n                target.ref.el.removeEventListener(\"mouseleave\", handleMouseleave, true);\n                const idx = state._targets.findIndex((t) => t === target);\n                if (idx) {\n                    state._targets.splice(idx, 1);\n                }\n            };\n        },\n    });\n    function setHover(hovering) {\n        if (hovering && !wasHovering) {\n            state.isHover = true;\n            clearTimeout(awayTimeout);\n            clearTimeout(hoveringTimeout);\n            if (typeof onHover === \"function\") {\n                onHover();\n            }\n            if (Array.isArray(onHovering)) {\n                const [delay, cb] = onHovering;\n                hoveringTimeout = setTimeout(() => {\n                    cb();\n                }, delay);\n            }\n        } else if (!hovering) {\n            state.isHover = false;\n            clearTimeout(awayTimeout);\n            if (typeof onAway === \"function\") {\n                awayTimeout = setTimeout(() => {\n                    clearTimeout(hoveringTimeout);\n                    onAway();\n                }, 100);\n            }\n        }\n        wasHovering = hovering;\n    }\n    function onmouseenter(ev) {\n        if (state.isHover) {\n            return;\n        }\n        for (const target of toRaw(state)._targets) {\n            if (!target.ref.el) {\n                continue;\n            }\n            if (target.ref.el.contains(ev.target)) {\n                setHover(true);\n                lastHoveredTarget = target;\n                return;\n            }\n        }\n        for (const contains of state._contains) {\n            if (contains(ev.target)) {\n                setHover(true);\n                return;\n            }\n        }\n    }\n    function onmouseleave(ev) {\n        if (!state.isHover) {\n            return;\n        }\n        for (const target of toRaw(state._targets)) {\n            if (!target.ref.el) {\n                continue;\n            }\n            if (target.ref.el.contains(ev.relatedTarget)) {\n                return;\n            }\n        }\n        for (const contains of state._contains) {\n            if (contains(ev.relatedTarget)) {\n                return;\n            }\n        }\n        setHover(false);\n        lastHoveredTarget = null;\n    }\n\n    for (const target of targets) {\n        useLazyExternalListener(\n            () => target.ref.el,\n            \"mouseenter\",\n            (ev) => onmouseenter(ev),\n            true\n        );\n        useLazyExternalListener(\n            () => target.ref.el,\n            \"mouseleave\",\n            (ev) => onmouseleave(ev),\n            true\n        );\n    }\n\n    if (stateObserver) {\n        useEffect((open) => {\n            // Note: stateObserver is essentially used with useDropdownState()?.isOpen.\n            // While isOpen can become false, the ref.el can still be there for a short period of time.\n            // Relying on isOpen becoming false forces good syncing of isHover state on dropdown close.\n            if ((lastHoveredTarget && !lastHoveredTarget.ref.el) || !open) {\n                setHover(false);\n                lastHoveredTarget = null;\n            }\n        }, stateObserver);\n    }\n    return state;\n}\n\nexport class UseHoverOverlay extends Component {\n    static props = [\"slots\", \"hover\"];\n    static template = xml`<div t-ref=\"root\"><t t-slot=\"default\"/></div>`;\n\n    setup() {\n        super.setup();\n        this.root = useRef(\"root\");\n        const overlayContains = toRaw(this.env[OVERLAY_SYMBOL].contains);\n        let removeTarget;\n        onMounted(() => {\n            this.props.hover._contains.push(overlayContains);\n            removeTarget = this.props.hover.addTarget({\n                ref: { el: this.root.el.closest(\".o-overlay-item\") },\n            });\n        });\n        onWillUnmount(() => {\n            const idx = this.props.hover._contains.find((c) => c === overlayContains);\n            if (idx) {\n                this.props.hover._contains.splice(idx, 1);\n            }\n            removeTarget?.();\n        });\n    }\n}\n\n/**\n * Hook that execute the callback function each time the scrollable element hit\n * the bottom minus the threshold.\n *\n * @param {string} refName scrollable t-ref name to observe\n * @param {function} callback function to execute when scroll hit the bottom minus the threshold\n * @param {number} threshold number of threshold pixel to trigger the callback\n */\nexport function useOnBottomScrolled(refName, callback, threshold = 1) {\n    const ref = useRef(refName);\n    function onScroll() {\n        if (Math.abs(ref.el.scrollTop + ref.el.clientHeight - ref.el.scrollHeight) < threshold) {\n            callback();\n        }\n    }\n    onMounted(() => {\n        ref.el?.addEventListener(\"scroll\", onScroll);\n    });\n    onWillUnmount(() => {\n        ref.el?.removeEventListener(\"scroll\", onScroll);\n    });\n}\n\n/**\n * @param {string} refName\n * @param {function} [cb]\n */\nexport function useVisible(refName, cb, { ready = true } = {}) {\n    const ref = useRef(refName);\n    const state = useState({\n        isVisible: undefined,\n        ready,\n    });\n    function setValue(value) {\n        state.isVisible = value;\n        cb?.(state.isVisible);\n    }\n    const observer = new IntersectionObserver((entries) => {\n        setValue(entries.at(-1).isIntersecting);\n    });\n    useEffect(\n        (el, ready) => {\n            if (el && ready) {\n                observer.observe(el);\n                return () => {\n                    setValue(undefined);\n                    observer.unobserve(el);\n                };\n            }\n        },\n        () => [ref.el, state.ready]\n    );\n    return state;\n}\n\n/**\n * @typedef {Object} MessageScrolling\n * @property {function} clear\n * @property {function} highlightMessage\n * @property {number|null} highlightedMessageId\n * @returns {MessageScrolling}\n */\nexport function useMessageScrolling(duration = 2000) {\n    let timeout;\n    const state = useState({\n        clear() {\n            if (this.highlightedMessageId) {\n                browser.clearTimeout(timeout);\n                timeout = null;\n                this.highlightedMessageId = null;\n            }\n        },\n        /**\n         * @param {import(\"models\").Message} message\n         * @param {import(\"models\").Thread} thread\n         */\n        async highlightMessage(message, thread) {\n            if (thread.model !== \"mail.box\" && thread.notEq(message.thread)) {\n                return;\n            }\n            state.initiated = true;\n            let messageScrollDirection;\n            if (message.notIn(thread.messages)) {\n                messageScrollDirection = message.id < thread.messages[0]?.id ? \"top\" : \"bottom\";\n                await thread.loadAround(message.id);\n            }\n            const lastHighlightedMessageId = state.highlightedMessageId;\n            this.clear();\n            if (lastHighlightedMessageId === message.id) {\n                // Give some time for the state to update.\n                await new Promise(setTimeout);\n            }\n            thread.scrollTop = messageScrollDirection === \"top\" ? \"bottom\" : undefined;\n            if (thread.scrollTop === \"bottom\") {\n                state.startupDeferred = new Deferred();\n                await state.startupDeferred;\n                state.startupDeferred = null;\n            }\n            state.highlightedMessageId = message.id;\n            state.initiated = false;\n            timeout = browser.setTimeout(() => this.clear(), duration);\n        },\n        initiated: false,\n        /**\n         * Deferred during highlight startup, i.e. highlight is initiated but isn't scrolling yet\n         * Useful to set correct starting condition to initiate scroll to highlight, like scroll to bottom.\n         */\n        startupDeferred: null,\n        /** Deferred during scrolling to highlight */\n        scrollPromise: null,\n        /**\n         * Scroll the element into view and expose a promise that will resolved\n         * once the scroll is done.\n         *\n         * @param {Element} el\n         */\n        scrollTo(el) {\n            state.scrollPromise?.resolve();\n            const scrollPromise = new Deferred();\n            state.scrollPromise = scrollPromise;\n            if (\"onscrollend\" in window) {\n                document.addEventListener(\"scrollend\", scrollPromise.resolve, {\n                    capture: true,\n                    once: true,\n                });\n            } else {\n                // To remove when safari will support the \"scrollend\" event.\n                setTimeout(scrollPromise.resolve, 250);\n            }\n            el.scrollIntoView({ behavior: \"smooth\", block: \"center\" });\n            return scrollPromise;\n        },\n        highlightedMessageId: null,\n    });\n    return state;\n}\n\nexport function useMicrophoneVolume() {\n    let isClosed = false;\n    let audioTrack = null;\n    let disconnectAudioMonitor;\n    let audioMonitorPromise;\n    const store = useService(\"mail.store\");\n    const state = useState({\n        isReady: true,\n        isActive: false,\n        value: 0,\n        toggle: async () => {\n            if (!state.isReady) {\n                return;\n            }\n            state.isReady = false;\n            disconnectAudioMonitor?.();\n            disconnectAudioMonitor = undefined;\n            if (audioTrack) {\n                audioTrack.stop();\n                audioTrack = null;\n                state.isReady = true;\n                state.isActive = false;\n                state.value = 0;\n                return;\n            }\n            let track;\n            try {\n                const audioStream = await browser.navigator.mediaDevices.getUserMedia({\n                    audio: store.settings.audioConstraints,\n                });\n                track = audioStream.getAudioTracks()[0];\n            } catch {\n                store.env.services.notification.add(\n                    _t('\"%(hostname)s\" requires microphone access', {\n                        hostname: browser.location.host,\n                    }),\n                    { type: \"warning\" }\n                );\n                return;\n            }\n            if (isClosed) {\n                track.stop();\n                return;\n            }\n            audioMonitorPromise = monitorAudio(track, {\n                onTic: (value) => {\n                    state.value = value;\n                },\n                processInterval: 100,\n            });\n            disconnectAudioMonitor = await audioMonitorPromise;\n            audioTrack = track;\n            state.isActive = true;\n            state.isReady = true;\n        },\n    });\n    onWillUnmount(async () => {\n        isClosed = true;\n        await audioMonitorPromise;\n        audioTrack?.stop();\n        disconnectAudioMonitor?.();\n    });\n    return state;\n}\n\nexport function useSelection({ refName, model, preserveOnClickAwayPredicate = () => false }) {\n    const ui = useService(\"ui\");\n    const ref = useRef(refName);\n    function onSelectionChange() {\n        const activeElement = ref.el?.getRootNode().activeElement;\n        if (activeElement && activeElement === ref.el) {\n            Object.assign(model, {\n                start: ref.el.selectionStart,\n                end: ref.el.selectionEnd,\n                direction: ref.el.selectionDirection,\n            });\n        }\n    }\n    onExternalClick(refName, async (ev) => {\n        if (await preserveOnClickAwayPredicate(ev)) {\n            return;\n        }\n        if (!ref.el) {\n            return;\n        }\n        Object.assign(model, {\n            start: ref.el.value.length,\n            end: ref.el.value.length,\n            direction: ref.el.selectionDirection,\n        });\n    });\n    onMounted(() => {\n        document.addEventListener(\"selectionchange\", onSelectionChange);\n        document.addEventListener(\"input\", onSelectionChange);\n    });\n    onWillUnmount(() => {\n        document.removeEventListener(\"selectionchange\", onSelectionChange);\n        document.removeEventListener(\"input\", onSelectionChange);\n    });\n    return {\n        restore() {\n            ref.el?.setSelectionRange(model.start, model.end, model.direction);\n        },\n        moveCursor(position) {\n            model.start = model.end = position;\n            if (ref.el && !ui.isSmall) {\n                // In mobile, selection seems to adjust correctly.\n                // Don't programmatically adjust, otherwise it shows soft keyboard!\n                ref.el.selectionStart = ref.el.selectionEnd = position;\n            }\n        },\n    };\n}\n\nexport function useSequential() {\n    let inProgress = false;\n    let nextFunction;\n    let nextResolve;\n    let nextReject;\n    async function call() {\n        const resolve = nextResolve;\n        const reject = nextReject;\n        const func = nextFunction;\n        nextResolve = undefined;\n        nextReject = undefined;\n        nextFunction = undefined;\n        inProgress = true;\n        try {\n            const data = await func();\n            resolve(data);\n        } catch (e) {\n            reject(e);\n        }\n        inProgress = false;\n        if (nextFunction && nextResolve) {\n            call();\n        }\n    }\n    return (func) => {\n        nextResolve?.();\n        const prom = new Promise((resolve, reject) => {\n            nextResolve = resolve;\n            nextReject = reject;\n        });\n        nextFunction = func;\n        if (!inProgress) {\n            call();\n        }\n        return prom;\n    };\n}\n\nexport function useDiscussSystray() {\n    const ui = useService(\"ui\");\n    return {\n        class: \"o-mail-DiscussSystray-class\",\n        get contentClass() {\n            return `d-flex flex-column flex-grow-1 ${\n                ui.isSmall ? \"overflow-auto o-scrollbar-thin w-100 mh-100\" : \"\"\n            }`;\n        },\n        get menuClass() {\n            return `p-0 o-mail-DiscussSystray ${\n                ui.isSmall\n                    ? \"o-mail-systrayFullscreenDropdownMenu start-0 w-100 mh-100 d-flex flex-column mt-0 border-0 shadow-lg\"\n                    : \"\"\n            }`;\n        },\n    };\n}\n\nexport const useMovable = makeDraggableHook({\n    name: \"useMovable\",\n    onWillStartDrag({ ctx, addCleanup, addStyle, getRect }) {\n        ctx.current.container = document.createElement(\"div\");\n        addStyle(ctx.current.container, {\n            position: \"fixed\",\n            top: 0,\n            bottom: 0,\n            left: 0,\n            right: 0,\n        });\n        ctx.current.element.after(ctx.current.container);\n        addCleanup(() => ctx.current.container.remove());\n    },\n    onDragStart: () => true,\n    onDragEnd: () => true,\n    onDrop({ ctx, getRect }) {\n        const { top, left } = getRect(ctx.current.element);\n        return { top, left };\n    },\n});\n\nexport const LONG_PRESS_DELAY = 400;\n\n/**\n * Subscribes to long press events on the element matching the given ref name.\n * It internally prevents false positives caused by scroll gestures.\n *\n * @param {string} refName The ref name of the element to listen for long presses on.\n * @param {Object} options\n * @param {() => void} [options.action] Function called when a long press is detected.\n * @param {() => boolean} [options.predicate] Optional function to enable long press detection.\n */\nexport function useLongPress(refName, { action, predicate = () => true } = {}) {\n    const MOVE_TRESHOLD = 10;\n    const ref = useRef(refName);\n    let timer = null;\n    let startX = 0;\n    let startY = 0;\n\n    function reset() {\n        clearTimeout(timer);\n        timer = null;\n    }\n    useLazyExternalListener(\n        () => ref.el,\n        \"touchstart\",\n        (ev) => {\n            if (!predicate()) {\n                return;\n            }\n            const touch = ev.touches[0];\n            startX = touch.clientX;\n            startY = touch.clientY;\n            timer = setTimeout(() => {\n                action();\n                reset();\n            }, LONG_PRESS_DELAY);\n        }\n    );\n    useLazyExternalListener(\n        () => ref.el,\n        \"touchmove\",\n        (ev) => {\n            if (!timer) {\n                return;\n            }\n            const touch = ev.touches[0];\n            const dx = touch.screenX - startX;\n            const dy = touch.screenY - startY;\n            if (Math.hypot(dx, dy) > MOVE_TRESHOLD) {\n                reset();\n            }\n        }\n    );\n    useLazyExternalListener(() => ref.el, \"touchend\", reset);\n    useLazyExternalListener(() => ref.el, \"touchcancel\", reset);\n}\n\nexport const inDiscussCallViewProps = [\"isPip?\"];\nexport function useInDiscussCallView() {\n    const component = useComponent();\n    useSubEnv({\n        inDiscussCallView: {\n            get isPip() {\n                return component.props.isPip;\n            },\n        },\n    });\n}\n", "// Broad human voice range of frequencies in hz.\nconst HUMAN_VOICE_FREQUENCY_RANGE = [80, 1000];\n\n//------------------------------------------------------------------------------\n// Public\n//------------------------------------------------------------------------------\n\n/**\n * monitors the activity of an audio mediaStreamTrack\n *\n * @param {MediaStreamTrack} track\n * @param {Object} [processorOptions] options for the audio processor\n * @param {Array<number>} [processorOptions.frequencyRange] the range of frequencies to monitor in hz\n * @param {number} [processorOptions.minimumActiveCycles] how many cycles have to pass since the\n *          last time the threshold was exceeded to go back to inactive state, this prevents\n *          stuttering when the speech volume oscillates around the threshold value.\n * @param {function(boolean):void} [processorOptions.onThreshold] a function to be called when the threshold is passed\n * @param {function(number):void} [processorOptions.onTic] a function to be called at each tics\n * @param {number} [processorOptions.volumeThreshold] the normalized minimum value for audio detection\n * @returns {Object} returnValue\n * @returns {function} returnValue.disconnect callback to cleanly end the monitoring\n */\nexport async function monitorAudio(track, processorOptions) {\n    // cloning the track so it is not affected by the enabled change of the original track.\n    const monitoredTrack = track.clone();\n    monitoredTrack.enabled = true;\n    const stream = new window.MediaStream([monitoredTrack]);\n    const AudioContext = window.AudioContext || window.webkitAudioContext;\n    if (!AudioContext) {\n        throw \"missing audio context\";\n    }\n    const audioContext = new AudioContext();\n    const source = audioContext.createMediaStreamSource(stream);\n\n    let processor;\n    try {\n        processor = await _loadAudioWorkletProcessor(source, audioContext, processorOptions);\n    } catch {\n        // In case Worklets are not supported by the browser (eg: Safari)\n        processor = _loadScriptProcessor(source, audioContext, processorOptions);\n    }\n\n    return async () => {\n        processor.disconnect();\n        source.disconnect();\n        monitoredTrack.stop();\n        try {\n            await audioContext.close();\n        } catch (e) {\n            if (e.name === \"InvalidStateError\") {\n                return; // the audio context is already closed\n            }\n            throw e;\n        }\n    };\n}\n\n//------------------------------------------------------------------------------\n// Private\n//------------------------------------------------------------------------------\n\n/**\n * @param {MediaStreamSource} source\n * @param {AudioContext} audioContext\n * @param {Object} [param2] options\n * @returns {Object} returnValue\n * @returns {function} returnValue.disconnect disconnect callback\n */\nfunction _loadScriptProcessor(\n    source,\n    audioContext,\n    {\n        frequencyRange = HUMAN_VOICE_FREQUENCY_RANGE,\n        minimumActiveCycles = 30,\n        onThreshold,\n        onTic,\n        volumeThreshold = 0.3,\n    } = {}\n) {\n    // audio setup\n    const bitSize = 1024;\n    const analyser = audioContext.createAnalyser();\n    source.connect(analyser);\n    const scriptProcessorNode = audioContext.createScriptProcessor(bitSize, 1, 1);\n    analyser.connect(scriptProcessorNode);\n    analyser.fftsize = bitSize;\n    scriptProcessorNode.connect(audioContext.destination);\n\n    // timing variables\n    const processInterval = 50; // how many ms between each computation\n    const intervalInFrames = (processInterval / 1000) * analyser.context.sampleRate;\n    let nextUpdateFrame = processInterval;\n\n    // process variables\n    let activityBuffer = 0;\n    let wasAboveThreshold = undefined;\n    let isAboveThreshold = false;\n\n    scriptProcessorNode.onaudioprocess = () => {\n        // throttles down the processing tic rate\n        nextUpdateFrame -= bitSize;\n        if (nextUpdateFrame >= 0) {\n            return;\n        }\n        nextUpdateFrame += intervalInFrames;\n\n        // computes volume and threshold\n        const normalizedVolume = getFrequencyAverage(\n            analyser,\n            frequencyRange[0],\n            frequencyRange[1]\n        );\n        if (normalizedVolume >= volumeThreshold) {\n            activityBuffer = minimumActiveCycles;\n        } else if (normalizedVolume < volumeThreshold && activityBuffer > 0) {\n            activityBuffer--;\n        }\n        isAboveThreshold = activityBuffer > 0;\n\n        onTic?.(normalizedVolume);\n        if (wasAboveThreshold !== isAboveThreshold) {\n            wasAboveThreshold = isAboveThreshold;\n            onThreshold?.(isAboveThreshold);\n        }\n    };\n    return {\n        disconnect: () => {\n            analyser.disconnect();\n            scriptProcessorNode.disconnect();\n            scriptProcessorNode.onaudioprocess = null;\n        },\n    };\n}\n\n/**\n * @param {MediaStreamSource} source\n * @param {AudioContext} audioContext\n * @param {Object} [param2] options\n * @returns {Object} returnValue\n * @returns {function} returnValue.disconnect disconnect callback\n */\nasync function _loadAudioWorkletProcessor(\n    source,\n    audioContext,\n    {\n        frequencyRange = HUMAN_VOICE_FREQUENCY_RANGE,\n        minimumActiveCycles = 10,\n        onThreshold,\n        onTic,\n        volumeThreshold = 0.3,\n        normalizationParameters = { boost: 1, shift: 0.6 },\n    } = {}\n) {\n    await audioContext.resume();\n    // Safari does not support Worklet.addModule\n    await audioContext.audioWorklet.addModule(\"/mail/rtc/audio_worklet_processor_v2\");\n    const thresholdProcessor = new window.AudioWorkletNode(audioContext, \"audio-processor\", {\n        processorOptions: {\n            minimumActiveCycles,\n            volumeThreshold,\n            frequencyRange,\n            normalizationParameters,\n            postAllTics: !!onTic,\n        },\n    });\n    source.connect(thresholdProcessor);\n    thresholdProcessor.port.onmessage = (event) => {\n        const { isAboveThreshold, volume } = event.data;\n        if (isAboveThreshold !== undefined) {\n            onThreshold?.(isAboveThreshold);\n        }\n        if (volume !== undefined) {\n            onTic?.(volume);\n        }\n    };\n    return {\n        disconnect: () => {\n            thresholdProcessor.disconnect();\n        },\n    };\n}\n\n/**\n * @param {AnalyserNode} analyser\n * @param {number} lowerFrequency lower bound for relevant frequencies to monitor\n * @param {number} higherFrequency upper bound for relevant frequencies to monitor\n * @returns {number} normalized [0...1] average quantity of the relevant frequencies\n */\nfunction getFrequencyAverage(analyser, lowerFrequency, higherFrequency) {\n    const frequencies = new window.Uint8Array(analyser.frequencyBinCount);\n    analyser.getByteFrequencyData(frequencies);\n    const sampleRate = analyser.context.sampleRate;\n    const startIndex = _getFrequencyIndex(lowerFrequency, sampleRate, analyser.frequencyBinCount);\n    const endIndex = _getFrequencyIndex(higherFrequency, sampleRate, analyser.frequencyBinCount);\n    const count = endIndex - startIndex;\n    let sum = 0;\n    for (let index = startIndex; index < endIndex; index++) {\n        sum += frequencies[index] / 255;\n    }\n    if (!count) {\n        return 0;\n    }\n    return sum / count;\n}\n\n/**\n * @param {number} targetFrequency in Hz\n * @param {number} sampleRate the sample rate of the audio\n * @param {number} binCount AnalyserNode.frequencyBinCount\n * @returns {number} the index of the targetFrequency within binCount\n */\nfunction _getFrequencyIndex(targetFrequency, sampleRate, binCount) {\n    const index = Math.round((targetFrequency / (sampleRate / 2)) * binCount);\n    return Math.min(Math.max(0, index), binCount);\n}\n", "import { reactive } from \"@odoo/owl\";\nimport { AssetsLoadingError, getBundle } from \"@web/core/assets\";\nimport { memoize } from \"@web/core/utils/functions\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { effect } from \"@web/core/utils/reactive\";\n\nexport function assignDefined(obj, data, keys = Object.keys(data)) {\n    for (const key of keys) {\n        if (data[key] !== undefined) {\n            obj[key] = data[key];\n        }\n    }\n    return obj;\n}\n\nexport function assignGetter(obj, data) {\n    const properties = Object.fromEntries(\n        Object.entries(data).map(([getterName, getterFn]) => [\n            getterName,\n            {\n                get: getterFn,\n                set: () => {}, // avoids Proxy \"trap returned falsish\" error\n            },\n        ])\n    );\n    Object.defineProperties(obj, properties);\n}\n\nexport function assignIn(obj, data, keys = Object.keys(data)) {\n    for (const key of keys) {\n        if (key in data) {\n            obj[key] = data[key];\n        }\n    }\n    return obj;\n}\n\n/**\n * @template T\n * @param {T[]} list\n * @param {number} target\n * @param {(item: T) => number} [itemToCompareVal]\n * @returns {T}\n */\nexport function nearestGreaterThanOrEqual(list, target, itemToCompareVal) {\n    const findNext = (left, right, next) => {\n        if (left > right) {\n            return next;\n        }\n        const index = Math.floor((left + right) / 2);\n        const item = list[index];\n        const val = itemToCompareVal?.(item) ?? item;\n        if (val === target) {\n            return item;\n        } else if (val > target) {\n            return findNext(left, index - 1, item);\n        } else {\n            return findNext(index + 1, right, next);\n        }\n    };\n    return findNext(0, list.length - 1, null);\n}\n\nexport const mailGlobal = {\n    isInTest: false,\n};\n\n/**\n * Use `rpc` instead.\n *\n * @deprecated\n */\nexport function rpcWithEnv() {\n    return rpc;\n}\n\n// todo: move this some other place in the future\nexport function isDragSourceExternalFile(dataTransfer) {\n    const dragDataType = dataTransfer.types;\n    if (dragDataType.constructor === window.DOMStringList) {\n        return dragDataType.contains(\"Files\");\n    }\n    if (dragDataType.constructor === Array) {\n        return dragDataType.includes(\"Files\");\n    }\n    return false;\n}\n\n/**\n * @param {Object} target\n * @param {string|string[]} key\n * @param {Function} callback\n */\nexport function onChange(target, key, callback) {\n    let proxy;\n    function _observe() {\n        // access proxy[key] only once to avoid triggering reactive get() many times\n        const val = proxy[key];\n        if (typeof val === \"object\" && val !== null) {\n            void Object.keys(val);\n        }\n        if (Array.isArray(val)) {\n            void val.length;\n            void val.forEach((i) => i);\n        }\n    }\n    if (Array.isArray(key)) {\n        for (const k of key) {\n            onChange(target, k, callback);\n        }\n        return;\n    }\n    proxy = reactive(target, () => {\n        _observe();\n        callback();\n    });\n    _observe();\n    return proxy;\n}\n\n/**\n * @param {MediaStream} [stream]\n */\nexport function closeStream(stream) {\n    stream?.getTracks?.().forEach((track) => track.stop());\n}\n\n/**\n * Compare two Luxon datetime.\n *\n * @param {import(\"@web/core/l10n/dates\").NullableDateTime} date1\n * @param {import(\"@web/core/l10n/dates\").NullableDateTime} date2\n * @returns {number} Negative if date1 is less than date2, positive if date1 is\n *  greater than date2, and 0 if they are equal.\n */\nexport function compareDatetime(date1, date2) {\n    if (date1?.ts === date2?.ts) {\n        return 0;\n    }\n    if (!date1) {\n        return -1;\n    }\n    if (!date2) {\n        return 1;\n    }\n    return date1.ts - date2.ts;\n}\n\n/**\n * Compares two version strings.\n *\n * @param {string} v1 - The first version string to compare.\n * @param {string} v2 - The second version string to compare.\n * @return {number} -1 if v1 is less than v2, 1 if v1 is greater than v2, and 0 if they are equal.\n */\nfunction compareVersion(v1, v2) {\n    const parts1 = v1.split(\".\");\n    const parts2 = v2.split(\".\");\n\n    for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) {\n        const num1 = parseInt(parts1[i]) || 0;\n        const num2 = parseInt(parts2[i]) || 0;\n        if (num1 < num2) {\n            return -1;\n        }\n        if (num1 > num2) {\n            return 1;\n        }\n    }\n    return 0;\n}\n\n/**\n * Return a version object that can be compared to other version strings.\n *\n * @param {string} v The version string to evaluate.\n */\nexport function parseVersion(v) {\n    return {\n        isLowerThan(other) {\n            return compareVersion(v, other) < 0;\n        },\n    };\n}\n\n/**\n * Converts a given URL from platforms like YouTube, Google Drive, Instagram,\n * etc., into their embed format. This function extracts the necessary video ID\n * or content identifier from the input URL and returns the corresponding embed\n * URL for that platform.\n *\n * @param {string} url\n */\nexport function convertToEmbedURL(url) {\n    const ytRegex = /^.*(youtu.be\\/|v\\/|u\\/\\w\\/|embed\\/|live\\/|watch\\?v=|&v=)([^#&?]*).*/;\n    const ytMatch = url.match(ytRegex);\n    if (ytMatch?.length === 3) {\n        const youtubeURL = new URL(`/embed/${ytMatch[2]}`, \"https://www.youtube.com\");\n        youtubeURL.searchParams.set(\"autoplay\", \"1\");\n        return { url: youtubeURL.toString(), provider: \"youtube\" };\n    }\n    const gdriveRegex = /(?:drive\\.google\\.com\\/(?:file\\/d\\/|open\\?id=|uc\\?id=))([^/?&]+)/;\n    const gdriveMatch = url.match(gdriveRegex);\n    if (gdriveMatch?.length === 2) {\n        const gdriveURL = new URL(`/file/d/${gdriveMatch[1]}/preview`, \"https://drive.google.com\");\n        return { url: gdriveURL.toString(), provider: \"google-drive\" };\n    }\n    return { url: null, provider: null };\n}\n\n/**\n * Checks if the browser supports hardware acceleration for video processing.\n *\n * @returns {boolean} True if hardware acceleration is supported, false otherwise.\n */\nexport const hasHardwareAcceleration = memoize(() => {\n    const canvas = document.createElement(\"canvas\");\n    const gl =\n        canvas.getContext(\"webgl2\") ||\n        canvas.getContext(\"webgl\") ||\n        canvas.getContext(\"experimental-webgl\");\n    if (!gl) {\n        // WebGL support is typically required for hardware acceleration.\n        return false;\n    }\n    const debugInfo = gl.getExtension(\"WEBGL_debug_renderer_info\");\n    if (debugInfo) {\n        const renderer = gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL);\n        if (/swiftshader|llvmpipe|software/i.test(renderer)) {\n            // These renderers indicate software-based rendering instead of hardware acceleration.\n            return false;\n        }\n    }\n    return true;\n});\n\n/**\n * Runs a reactive effect whenever the dependencies change. The effect receives\n * the current values returned by `dependencies`. If the effect returns a\n * cleanup function, it is run before the next execution.\n *\n * @template {object[]} T\n * @param {Object} options\n * @param {(...dependencies: any[]) => void | (() => void)} options.effect The\n *        effect callback. May return a cleanup function.\n * @param {(...args: [...T]) => Object|Array} options.dependencies Returns an array of\n *        values to track. The effect is called only if these values change.\n * @param {[...T]} options.reactiveTargets Objects that the effect depends on.\n */\nexport function effectWithCleanup({ effect: effectFn, dependencies, reactiveTargets }) {\n    let cleanup;\n    let prevDependencies;\n    effect((...deps) => {\n        const nextDependencies = dependencies(...deps);\n        const changed =\n            !prevDependencies ||\n            (Array.isArray(nextDependencies)\n                ? nextDependencies.some((v, i) => v !== prevDependencies[i])\n                : Object.keys(nextDependencies).some(\n                      (key) => nextDependencies[key] !== prevDependencies[key]\n                  ));\n        if (changed) {\n            prevDependencies = Array.isArray(nextDependencies)\n                ? [...nextDependencies]\n                : { ...nextDependencies };\n            cleanup?.();\n            cleanup = Array.isArray(nextDependencies)\n                ? effectFn(...nextDependencies)\n                : effectFn({ ...nextDependencies });\n        }\n    }, reactiveTargets);\n}\n\n/**\n * A thin wrapper around `effectWithCleanup` that debounces the cleanup phase:\n * setup runs immediately when activated, while cleanup is delayed until the\n * predicate remains false for `delay` ms.\n *\n * Setup is executed again only after cleanup has completed, ensuring symmetry\n * between setup and cleanup.\n *\n * @template T - type of reactive targets\n * @template D - type of dependencies\n * @param {Object} options\n * @param {(dependencies: D) => (() => void)} options.effect Function called\n * when the predicate becomes true and the effect is not active. Receives the\n * values returned by `dependencies`.\n * @param {number} options.delay Debounce delay in milliseconds before running\n * cleanup.\n * @param {(...targets: T) => D} options.dependencies Function returning an\n * array of values tracked by the effect; passed to setup/cleanup.\n * @param {(...targets: T) => boolean} options.predicate Function returning a\n * boolean to determine whether the effect should be activated.\n * @param {[...T]} options.reactiveTargets Array of reactive objects that the\n * effect depends on.\n */\nexport function effectWithDebouncedCleanup({\n    delay,\n    dependencies,\n    effect: effectFn,\n    predicate,\n    reactiveTargets,\n}) {\n    let timeout;\n    let active = false;\n    let cleanup;\n    effectWithCleanup({\n        effect(ctx) {\n            const { predicate, ...deps } = ctx;\n            if (!predicate) {\n                return;\n            }\n            clearTimeout(timeout);\n            if (!active) {\n                cleanup = effectFn(deps);\n                active = true;\n            }\n            return () => {\n                timeout = setTimeout(() => {\n                    cleanup();\n                    active = false;\n                }, delay);\n            };\n        },\n        dependencies: (...targets) => ({\n            predicate: predicate(...targets),\n            ...dependencies(...targets),\n        }),\n        reactiveTargets,\n    });\n}\n\n/**\n * @param {HTMLElement} targetNode\n * @param {string} bundleName\n */\nexport async function loadCssFromBundle(targetNode, bundleName) {\n    try {\n        const res = await getBundle(bundleName);\n        for (const url of res.cssLibs) {\n            const link = document.createElement(\"link\");\n            link.rel = \"stylesheet\";\n            link.href = url;\n            targetNode.appendChild(link);\n            await new Promise((res, rej) => {\n                link.addEventListener(\"load\", res);\n                link.addEventListener(\"error\", rej);\n            });\n        }\n    } catch (e) {\n        if (e instanceof AssetsLoadingError && e.cause instanceof TypeError) {\n            // an AssetsLoadingError caused by a TypeError means that the\n            // fetch request has been cancelled by the browser. It can occur\n            // when the user changes page, or navigate away from the website\n            // client action, so the iframe is unloaded. In this case, we\n            // don't care abour reporting the error, it is actually a normal\n            // situation.\n            return new Promise(() => {});\n        } else {\n            throw e;\n        }\n    }\n}\n", "import { loadPDFJSAssets } from \"@web/core/utils/pdfjs\";\n\nexport async function generatePdfThumbnail(pdfUrl, options = { height: 256, width: 256 }) {\n    let initialWorkerSrc = false,\n        isPdfValid,\n        pdf,\n        thumbnail;\n    try {\n        await loadPDFJSAssets();\n        // Force usage of worker to avoid hanging the tab.\n        initialWorkerSrc = globalThis.pdfjsLib.GlobalWorkerOptions.workerSrc;\n        globalThis.pdfjsLib.GlobalWorkerOptions.workerSrc =\n            \"/web/static/lib/pdfjs/build/pdf.worker.js\";\n    } catch {\n        return { thumbnail, pdfEnabled: false };\n    }\n    try {\n        // Support for blob url\n        if (pdfUrl.startsWith(\"blob:\")) {\n            pdfUrl = URL.createObjectURL(pdfUrl);\n            pdf = await globalThis.pdfjsLib.getDocument(pdfUrl).promise;\n            URL.revokeObjectURL(pdfUrl);\n        } else {\n            pdf = await globalThis.pdfjsLib.getDocument(pdfUrl).promise;\n        }\n    } catch (_error) {\n        if (_error.status === 415) {\n            isPdfValid = false;\n        } else if (\n            _error.name !== \"UnexpectedResponseException\" &&\n            _error.status &&\n            _error.status !== 403\n        ) {\n            pdf = undefined;\n        }\n    } finally {\n        // Restore pdfjs's state\n        globalThis.pdfjsLib.GlobalWorkerOptions.workerSrc = initialWorkerSrc;\n    }\n    if (pdf) {\n        isPdfValid = true;\n        const page = await pdf.getPage(1);\n        // Render first page onto a canvas\n        const viewPort = page.getViewport({ scale: 1 });\n        const canvas = document.createElement(\"canvas\");\n        canvas.width = options.width;\n        canvas.height = options.height;\n        const scale = canvas.width / viewPort.width;\n        await page.render({\n            canvasContext: canvas.getContext(\"2d\"),\n            viewport: page.getViewport({ scale }),\n        }).promise;\n        thumbnail = canvas.toDataURL(\"image/jpeg\").replace(\"data:image/jpeg;base64,\", \"\");\n    }\n    return { isPdfValid, thumbnail, pdfEnabled: true };\n}\n", "import { Composer } from \"@mail/core/common/composer\";\nimport { Thread } from \"@mail/core/common/thread\";\n\nimport {\n    Component,\n    onMounted,\n    onWillUpdateProps,\n    useChildSubEnv,\n    useRef,\n    useState,\n} from \"@odoo/owl\";\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { useThrottleForAnimation } from \"@web/core/utils/timing\";\n\n/**\n * @typedef {Object} Props\n * @extends {Component<Props, Env>}\n */\nexport class Chatter extends Component {\n    static template = \"mail.Chatter\";\n    static components = { Thread, Composer };\n    static props = [\"composer?\", \"threadId?\", \"threadModel\", \"twoColumns?\"];\n    static defaultProps = { composer: true, threadId: false, twoColumns: false };\n\n    setup() {\n        this.store = useService(\"mail.store\");\n        this.state = useState({\n            jumpThreadPresent: 0,\n            /** @type {import(\"models\").Thread} */\n            thread: undefined,\n            aside: false,\n            disabled: !this.props.threadId,\n        });\n        this.rootRef = useRef(\"root\");\n        this.onScrollDebounced = useThrottleForAnimation(this.onScroll);\n        useChildSubEnv(this.childSubEnv);\n\n        onMounted(this._onMounted);\n        onWillUpdateProps((nextProps) => {\n            this.state.disabled = !nextProps.threadId;\n            if (\n                this.props.threadId !== nextProps.threadId ||\n                this.props.threadModel !== nextProps.threadModel\n            ) {\n                this.changeThread(nextProps.threadModel, nextProps.threadId);\n            }\n            if (!this.env.chatter || this.env.chatter?.fetchThreadData) {\n                if (this.env.chatter) {\n                    this.env.chatter.fetchThreadData = false;\n                }\n                this.load(this.state.thread, this.requestList);\n            }\n        });\n    }\n\n    get afterPostRequestList() {\n        return [\"messages\"];\n    }\n\n    get childSubEnv() {\n        return { inChatter: this.state };\n    }\n\n    get onCloseFullComposerRequestList() {\n        return [\"messages\"];\n    }\n\n    get requestList() {\n        return [];\n    }\n\n    changeThread(threadModel, threadId) {\n        this.state.thread = this.store.Thread.insert({ model: threadModel, id: threadId });\n        if (threadId === false) {\n            if (this.state.thread.messages.length === 0) {\n                this.state.thread.messages.push({\n                    id: this.store.getNextTemporaryId(),\n                    author_id: this.state.thread.effectiveSelf,\n                    body: _t(\"Creating a new record...\"),\n                    message_type: \"notification\",\n                    thread: this.state.thread,\n                    trackingValues: [],\n                    res_id: threadId,\n                    model: threadModel,\n                });\n            }\n        }\n    }\n\n    /**\n     * Fetch data for the thread according to the request list.\n     * @param {import(\"models\").Thread} thread\n     * @param {string[]} requestList\n     */\n    async load(thread, requestList) {\n        if (!thread.id || !this.state.thread?.eq(thread)) {\n            return;\n        }\n        await thread.fetchThreadData(requestList);\n    }\n\n    onCloseFullComposerCallback() {\n        this.load(this.state.thread, this.onCloseFullComposerRequestList);\n    }\n\n    _onMounted() {\n        this.changeThread(this.props.threadModel, this.props.threadId);\n        if (!this.env.chatter || this.env.chatter?.fetchThreadData) {\n            if (this.env.chatter) {\n                this.env.chatter.fetchThreadData = false;\n            }\n            this.load(this.state.thread, this.requestList);\n        }\n    }\n\n    onPostCallback() {\n        this.state.jumpThreadPresent++;\n        // Load new messages to fetch potential new messages from other users (useful due to lack of auto-sync in chatter).\n        this.load(this.state.thread, this.afterPostRequestList);\n    }\n\n    onScroll() {\n        this.state.isTopStickyPinned = this.rootRef.el.scrollTop !== 0;\n    }\n}\n", "import { Composer } from \"@mail/core/common/composer\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { patch } from \"@web/core/utils/patch\";\n\npatch(Composer.prototype, {\n    get placeholder() {\n        if (this.thread && this.thread.model !== \"discuss.channel\" && !this.props.placeholder) {\n            if (this.props.type === \"message\") {\n                return _t(\"Send a message to followers\u2026\");\n            } else {\n                return _t(\"Log an internal note\u2026\");\n            }\n        }\n        return super.placeholder;\n    },\n});\n", "import { Thread } from \"@mail/core/common/thread_model\";\nimport { patch } from \"@web/core/utils/patch\";\n\npatch(Thread.prototype, {\n    /** @param {string[]} requestList */\n    async fetchThreadData(requestList) {\n        if (requestList.includes(\"messages\")) {\n            this.fetchNewMessages();\n        }\n        await this.store.fetchStoreData(\"mail.thread\", {\n            access_params: this.rpcParams,\n            request_list: requestList,\n            thread_id: this.id,\n            thread_model: this.model,\n        });\n    },\n});\n", "import { ScheduledMessage } from \"@mail/chatter/web/scheduled_message\";\nimport { Activity } from \"@mail/core/web/activity\";\nimport { AttachmentList } from \"@mail/core/common/attachment_list\";\nimport { Chatter } from \"@mail/chatter/web_portal/chatter\";\nimport { FollowerList } from \"@mail/core/web/follower_list\";\nimport { assignGetter, isDragSourceExternalFile } from \"@mail/utils/common/misc\";\nimport { useAttachmentUploader } from \"@mail/core/common/attachment_uploader_hook\";\nimport { useCustomDropzone } from \"@web/core/dropzone/dropzone_hook\";\nimport { useHover, useMessageScrolling } from \"@mail/utils/common/hooks\";\nimport { MailAttachmentDropzone } from \"@mail/core/common/mail_attachment_dropzone\";\nimport { RecipientsInput } from \"@mail/core/web/recipients_input\";\nimport { SearchMessageInput } from \"@mail/core/common/search_message_input\";\nimport { SearchMessageResult } from \"@mail/core/common/search_message_result\";\nimport { KeepLast } from \"@web/core/utils/concurrency\";\nimport { status, useEffect } from \"@odoo/owl\";\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { browser } from \"@web/core/browser/browser\";\nimport { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { FileUploader } from \"@web/views/fields/file_handler\";\nimport { patch } from \"@web/core/utils/patch\";\nimport { useDropdownState } from \"@web/core/dropdown/dropdown_hooks\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { useMessageSearch } from \"@mail/core/common/message_search_hook\";\nimport { usePopoutAttachment } from \"@mail/core/common/attachment_view\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { useRecordObserver } from \"@web/model/relational_model/utils\";\n\nexport const DELAY_FOR_SPINNER = 1000;\n\nObject.assign(Chatter.components, {\n    Activity,\n    AttachmentList,\n    Dropdown,\n    FileUploader,\n    FollowerList,\n    RecipientsInput,\n    ScheduledMessage,\n    SearchMessageInput,\n    SearchMessageResult,\n});\n\nChatter.props.push(\n    \"close?\",\n    \"compactHeight?\",\n    \"has_activities?\",\n    \"hasAttachmentPreview?\",\n    \"hasParentReloadOnActivityChanged?\",\n    \"hasParentReloadOnAttachmentsChanged?\",\n    \"hasParentReloadOnFollowersUpdate?\",\n    \"hasParentReloadOnMessagePosted?\",\n    \"highlightMessageId?\",\n    \"isAttachmentBoxVisibleInitially?\",\n    \"isChatterAside?\",\n    \"isInFormSheetBg?\",\n    \"saveRecord?\",\n    \"record?\"\n);\n\nObject.assign(Chatter.defaultProps, {\n    compactHeight: false,\n    has_activities: true,\n    hasAttachmentPreview: false,\n    hasParentReloadOnActivityChanged: false,\n    hasParentReloadOnAttachmentsChanged: false,\n    hasParentReloadOnFollowersUpdate: false,\n    hasParentReloadOnMessagePosted: false,\n    isAttachmentBoxVisibleInitially: false,\n    isChatterAside: false,\n    isInFormSheetBg: true,\n});\n\n/**\n * @type {import(\"@mail/chatter/web_portal/chatter\").Chatter }\n * @typedef {Object} Props\n * @property {function} [close]\n */\npatch(Chatter.prototype, {\n    setup() {\n        this.messageHighlight = useMessageScrolling();\n        super.setup(...arguments);\n        this.orm = useService(\"orm\");\n        this.keepLastSuggestedRecipientsUpdate = new KeepLast();\n        /** @deprecated equivalent to partner_fields and primary_email_field on thread */\n        this.mailImpactingFields = { recordFields: [], emailFields: [] };\n        useRecordObserver((record) => this.updateRecipients(record));\n        this.attachmentPopout = usePopoutAttachment();\n        Object.assign(this.state, {\n            composerType: false,\n            isAttachmentBoxOpened: this.props.isAttachmentBoxVisibleInitially,\n            isSearchOpen: false,\n            showActivities: true,\n            showAttachmentLoading: false,\n            showScheduledMessages: true,\n        });\n        this.messageSearch = useMessageSearch();\n        this.attachmentUploader = useAttachmentUploader(\n            this.store.Thread.insert({ model: this.props.threadModel, id: this.props.threadId })\n        );\n        this.unfollowHover = useHover(\"unfollow\");\n        this.followerListDropdown = useDropdownState();\n        /** @type {number|null} */\n        this.loadingAttachmentTimeout = null;\n        useCustomDropzone(\n            this.rootRef,\n            MailAttachmentDropzone,\n            {\n                extraClass: \"o-mail-Chatter-dropzone\",\n                /** @param {Event} ev */\n                onDrop: async (ev) => {\n                    if (this.state.composerType) {\n                        return;\n                    }\n                    if (isDragSourceExternalFile(ev.dataTransfer)) {\n                        const files = [...ev.dataTransfer.files];\n                        if (!this.state.thread.id) {\n                            const saved = await this.props.saveRecord?.();\n                            if (!saved) {\n                                return;\n                            }\n                        }\n                        Promise.all(\n                            files.map((file) => this.attachmentUploader.uploadFile(file))\n                        ).then(() => {\n                            if (this.props.hasParentReloadOnAttachmentsChanged) {\n                                this.reloadParentView();\n                            }\n                        });\n                        this.state.isAttachmentBoxOpened = true;\n                    }\n                },\n            },\n            () => !this.store.meetingViewOpened || this.env.inMeetingView\n        );\n        useEffect(\n            () => {\n                if (!this.state.thread) {\n                    return;\n                }\n                browser.clearTimeout(this.loadingAttachmentTimeout);\n                if (this.state.thread?.isLoadingAttachments) {\n                    this.loadingAttachmentTimeout = browser.setTimeout(\n                        () => (this.state.showAttachmentLoading = true),\n                        DELAY_FOR_SPINNER\n                    );\n                } else {\n                    this.state.showAttachmentLoading = false;\n                    this.state.isAttachmentBoxOpened =\n                        this.state.isAttachmentBoxOpened ||\n                        (this.props.isAttachmentBoxVisibleInitially && this.attachments.length > 0);\n                }\n                return () => browser.clearTimeout(this.loadingAttachmentTimeout);\n            },\n            () => [this.state.thread, this.state.thread?.isLoadingAttachments]\n        );\n        useEffect(\n            () => {\n                if (\n                    this.state.thread &&\n                    ![\"new\", \"loading\"].includes(this.state.thread.status) &&\n                    this.attachments.length === 0\n                ) {\n                    this.state.isAttachmentBoxOpened = false;\n                }\n            },\n            () => [this.state.thread?.status, this.attachments]\n        );\n        useEffect(\n            () => {\n                this.state.aside = this.props.isChatterAside;\n            },\n            () => [this.props.isChatterAside]\n        );\n    },\n\n    async updateRecipients(record, mode = this.state.composerType) {\n        if (!record) {\n            return;\n        }\n        // Hack: Make the useRecordObserver subscribe to the record changes\n        Object.keys(record.data).forEach((field) => record.data[field]);\n        const partnerIds = []; // Ensure that we don't have duplicates\n        let email;\n        this.mailImpactingFields.recordFields.forEach((field) => {\n            const value = record._changes[field];\n            if (record.data[field] !== undefined && value) {\n                partnerIds.push(value.id);\n            }\n        });\n        this.mailImpactingFields.emailFields.forEach((field) => {\n            const value = record._changes[field];\n            if (record.data[field] !== undefined && value) {\n                email = value;\n                return;\n            }\n        });\n        if ((!partnerIds.length && !email) || mode !== \"message\" || status(this) === \"destroyed\") {\n            return;\n        }\n        const recipients = await this.keepLastSuggestedRecipientsUpdate.add(\n            rpc(\"/mail/thread/recipients/get_suggested_recipients\", {\n                thread_model: this.props.threadModel,\n                thread_id: this.props.threadId,\n                partner_ids: partnerIds,\n                main_email: email,\n            })\n        );\n        if (status(this) === \"destroyed\" && !this.state.thread) {\n            return;\n        }\n        this.state.thread.suggestedRecipients = recipients.map((result) => ({\n            email: result.email,\n            partner_id: result.partner_id,\n            name: result.name || result.email,\n        }));\n        this.state.thread.additionalRecipients = this.state.thread.additionalRecipients.filter(\n            (additionalRecipient) =>\n                this.state.thread.suggestedRecipients.every(\n                    (suggestedRecipient) =>\n                        suggestedRecipient.partner_id !== additionalRecipient.partner_id\n                )\n        );\n    },\n\n    /**\n     * @returns {import(\"models\").Activity[]}\n     */\n    get activities() {\n        return this.state.thread?.activities ?? [];\n    },\n\n    get afterPostRequestList() {\n        return [\n            ...super.afterPostRequestList,\n            \"followers\",\n            \"scheduledMessages\",\n            \"suggestedRecipients\",\n        ];\n    },\n\n    get attachments() {\n        return this.state.thread?.attachments ?? [];\n    },\n\n    get childSubEnv() {\n        const res = Object.assign(super.childSubEnv, { messageHighlight: this.messageHighlight });\n        assignGetter(res.inChatter, { aside: () => this.props.isChatterAside });\n        Object.assign(res.inChatter, { toggleComposer: this.toggleComposer.bind(this) });\n        return res;\n    },\n\n    get followerButtonLabel() {\n        return _t(\"Show Followers\");\n    },\n\n    get followingText() {\n        return _t(\"Following\");\n    },\n\n    /**\n     * @returns {boolean}\n     */\n    get isDisabled() {\n        return !this.state.thread.id || !this.state.thread?.hasReadAccess;\n    },\n\n    get onCloseFullComposerRequestList() {\n        return [...super.onCloseFullComposerRequestList, \"scheduledMessages\"];\n    },\n\n    get requestList() {\n        return [\n            ...super.requestList,\n            \"activities\",\n            \"attachments\",\n            \"contact_fields\",\n            \"followers\",\n            \"scheduledMessages\",\n            \"suggestedRecipients\",\n        ];\n    },\n\n    get scheduledMessages() {\n        return this.state.thread?.scheduledMessages ?? [];\n    },\n\n    get unfollowText() {\n        return _t(\"Unfollow\");\n    },\n\n    changeThread(threadModel, threadId) {\n        super.changeThread(...arguments);\n        this.attachmentUploader.thread = this.state.thread;\n        if (threadId === false) {\n            this.state.composerType = false;\n        } else {\n            this.onThreadCreated?.(this.state.thread);\n            this.onThreadCreated = null;\n            this.messageSearch.thread = this.state.thread;\n            this.closeSearch();\n        }\n    },\n\n    closeSearch() {\n        this.messageSearch.clear();\n        this.state.isSearchOpen = false;\n    },\n\n    /** @override */\n    async load(thread, requestList) {\n        await super.load(...arguments);\n        if (!thread.id || !this.state.thread?.eq(thread)) {\n            return;\n        }\n        this.mailImpactingFields = {\n            emailFields: this.state.thread.primary_email_field\n                ? [this.state.thread.primary_email_field]\n                : [],\n            recordFields: this.state.thread.partner_fields || [],\n        };\n        this.updateRecipients(this.props.record);\n    },\n\n    onActivityChanged(thread) {\n        this.load(thread, [...this.requestList, \"messages\"]);\n        if (this.props.hasParentReloadOnActivityChanged) {\n            this.reloadParentView();\n        }\n    },\n\n    onAddFollowers() {\n        this.load(this.state.thread, [\"followers\", \"suggestedRecipients\"]);\n        if (this.props.hasParentReloadOnFollowersUpdate) {\n            this.reloadParentView();\n        }\n    },\n\n    onClickAddAttachments() {\n        if (this.attachments.length === 0) {\n            return;\n        }\n        this.state.isAttachmentBoxOpened = !this.state.isAttachmentBoxOpened;\n        if (this.state.isAttachmentBoxOpened) {\n            this.rootRef.el.scrollTop = 0;\n            this.state.thread.scrollTop = \"bottom\";\n        }\n    },\n\n    async onClickAttachFile(ev) {\n        if (this.state.thread.id) {\n            return;\n        }\n        const saved = await this.props.saveRecord?.();\n        if (!saved) {\n            return false;\n        }\n    },\n\n    onClickSearch() {\n        this.state.composerType = false;\n        this.state.isSearchOpen = !this.state.isSearchOpen;\n    },\n\n    onCloseFullComposerCallback(isDiscard) {\n        this.toggleComposer();\n        super.onCloseFullComposerCallback();\n        if (!isDiscard) {\n            this.reloadParentView();\n        }\n    },\n\n    onFollowerChanged() {\n        document.body.click(); // hack to close dropdown\n        this.reloadParentView();\n    },\n\n    _onMounted() {\n        super._onMounted();\n        if (this.state.thread && this.props.highlightMessageId) {\n            this.state.thread.highlightMessage = this.props.highlightMessageId;\n        }\n    },\n\n    onPostCallback() {\n        if (this.props.hasParentReloadOnMessagePosted) {\n            this.reloadParentView();\n        }\n        this.toggleComposer();\n        super.onPostCallback();\n    },\n\n    onScheduledMessageChanged(thread) {\n        // reload messages as well as a scheduled message could have been sent\n        this.load(thread, [\"scheduledMessages\", \"messages\"]);\n        // sending a message could trigger another action (eg. move so to quotation sent)\n        this.reloadParentView();\n    },\n\n    onSuggestedRecipientAdded(thread) {\n        this.load(thread, [\"suggestedRecipients\"]);\n    },\n\n    async onUploaded(data) {\n        await this.attachmentUploader.uploadData(data);\n        if (this.props.hasParentReloadOnAttachmentsChanged) {\n            this.reloadParentView();\n        }\n        this.state.isAttachmentBoxOpened = true;\n        if (this.rootRef.el) {\n            this.rootRef.el.scrollTop = 0;\n        }\n        this.state.thread.scrollTop = \"bottom\";\n    },\n\n    async reloadParentView() {\n        await this.props.saveRecord?.();\n        if (this.props.record) {\n            await this.props.record.load();\n        }\n    },\n\n    async scheduleActivity() {\n        this.closeSearch();\n        const schedule = async (thread) => {\n            await this.store.scheduleActivity(thread.model, [thread.id]);\n            this.load(thread, [\"activities\", \"messages\"]);\n            if (this.props.hasParentReloadOnActivityChanged) {\n                await this.reloadParentView();\n            }\n        };\n        if (this.state.thread.id) {\n            schedule(this.state.thread);\n        } else {\n            this.onThreadCreated = schedule;\n            this.props.saveRecord?.();\n        }\n    },\n\n    toggleActivities() {\n        this.state.showActivities = !this.state.showActivities;\n    },\n\n    toggleComposer(mode = false, { force = false } = {}) {\n        this.closeSearch();\n        const toggle = async () => {\n            if (!force && this.state.composerType === mode) {\n                this.state.composerType = false;\n            } else {\n                if (mode === \"message\") {\n                    await this.updateRecipients(this.props.record, mode);\n                }\n                this.state.composerType = mode;\n            }\n        };\n        if (this.state.thread.id) {\n            toggle();\n        } else {\n            this.onThreadCreated = toggle;\n            this.props.saveRecord?.();\n        }\n    },\n\n    toggleScheduledMessages() {\n        this.state.showScheduledMessages = !this.state.showScheduledMessages;\n    },\n\n    async unlinkAttachment(attachment) {\n        await this.attachmentUploader.unlink(attachment);\n        if (this.props.hasParentReloadOnAttachmentsChanged) {\n            this.reloadParentView();\n        }\n    },\n\n    popoutAttachment() {\n        this.attachmentPopout.popout();\n    },\n});\n", "import { patch } from \"@web/core/utils/patch\";\nimport { FormArchParser } from \"@web/views/form/form_arch_parser\";\n\npatch(FormArchParser.prototype, {\n    parse(xmlDoc, models, modelName) {\n        const result = super.parse(...arguments);\n        result.has_activities = Boolean(models[modelName].has_activities);\n        return result;\n    },\n});\n", "import { registry } from \"@web/core/registry\";\nimport { patch } from \"@web/core/utils/patch\";\nimport { append, createElement, extractAttributes, setAttributes } from \"@web/core/utils/xml\";\nimport { FormCompiler } from \"@web/views/form/form_compiler\";\n\n/** @this {FormCompiler} */\nfunction compileChatter(node, params) {\n    const chatterContainerXml = createElement(\"t\");\n    setAttributes(chatterContainerXml, {\n        \"t-component\": \"__comp__.mailComponents.Chatter\",\n        has_activities: \"__comp__.props.archInfo.has_activities\",\n        hasAttachmentPreview: Boolean(\n            this.templates.FormRenderer.querySelector(\".o_attachment_preview\")\n        ),\n        hasParentReloadOnActivityChanged: Boolean(node.getAttribute(\"reload_on_activity\")),\n        hasParentReloadOnAttachmentsChanged: Boolean(node.getAttribute(\"reload_on_attachment\")),\n        hasParentReloadOnFollowersUpdate: Boolean(node.getAttribute(\"reload_on_follower\")),\n        hasParentReloadOnMessagePosted: Boolean(node.getAttribute(\"reload_on_post\")),\n        isAttachmentBoxVisibleInitially: Boolean(node.getAttribute(\"open_attachments\")),\n        threadId: \"__comp__.props.record.resId or undefined\",\n        threadModel: \"__comp__.props.record.resModel\",\n        record: \"__comp__.props.record\",\n        saveRecord: \"() => __comp__.save and __comp__.save()\",\n        highlightMessageId: \"__comp__.highlightMessageId\",\n    });\n    const chatterContainerHookXml = createElement(\"div\");\n    chatterContainerHookXml.classList.add(\"o-mail-ChatterContainer\", \"o-mail-Form-chatter\");\n    setAttributes(chatterContainerHookXml, { \"t-if\": \"!__comp__.env.inDialog\" });\n    append(chatterContainerHookXml, chatterContainerXml);\n    return chatterContainerHookXml;\n}\n\nfunction compileAttachmentPreview(node, params) {\n    const webClientViewAttachmentViewContainerHookXml = createElement(\"div\");\n    webClientViewAttachmentViewContainerHookXml.classList.add(\"o_attachment_preview\");\n    const webClientViewAttachmentViewContainerXml = createElement(\"t\");\n    setAttributes(webClientViewAttachmentViewContainerXml, {\n        \"t-component\": \"__comp__.mailComponents.AttachmentView\",\n        threadId: \"__comp__.props.record.resId or undefined\",\n        threadModel: \"__comp__.props.record.resModel\",\n    });\n    append(webClientViewAttachmentViewContainerHookXml, webClientViewAttachmentViewContainerXml);\n    return webClientViewAttachmentViewContainerHookXml;\n}\n\nregistry.category(\"form_compilers\").add(\"chatter_compiler\", {\n    selector: \"chatter\",\n    fn: compileChatter,\n});\n\nregistry.category(\"form_compilers\").add(\"attachment_preview_compiler\", {\n    selector: \"div.o_attachment_preview\",\n    fn: compileAttachmentPreview,\n});\n\npatch(FormCompiler.prototype, {\n    compile(node, params) {\n        const res = super.compile(node, params);\n        const chatterContainerHookXml = res.querySelector(\".o-mail-Form-chatter\");\n        if (!chatterContainerHookXml) {\n            return res; // no chatter, keep the result as it is\n        }\n        const chatterContainerXml = chatterContainerHookXml.querySelector(\n            \"t[t-component='__comp__.mailComponents.Chatter']\"\n        );\n        setAttributes(chatterContainerXml, {\n            isChatterAside: \"false\",\n            isInFormSheetBg: \"false\",\n            saveRecord: \"__comp__.props.saveRecord\",\n        });\n        if (chatterContainerHookXml.parentNode.classList.contains(\"o_form_sheet\")) {\n            return res; // if chatter is inside sheet, keep it there\n        }\n        const formSheetBgXml = res.querySelector(\".o_form_sheet_bg\");\n        const parentXml = formSheetBgXml && formSheetBgXml.parentNode;\n        if (!parentXml) {\n            return res; // miss-config: a sheet-bg is required for the rest\n        }\n\n        const webClientViewAttachmentViewHookXml = res.querySelector(\".o_attachment_preview\");\n        const hasPreview = !!webClientViewAttachmentViewHookXml;\n        if (webClientViewAttachmentViewHookXml) {\n            // in sheet bg (attachment viewer present)\n            setAttributes(webClientViewAttachmentViewHookXml, {\n                \"t-if\": `__comp__.mailLayout(${hasPreview}).includes(\"COMBO\")`,\n            });\n            const sheetBgChatterContainerHookXml = chatterContainerHookXml.cloneNode(true);\n            sheetBgChatterContainerHookXml.classList.add(\"o-isInFormSheetBg\", \"w-auto\");\n            setAttributes(sheetBgChatterContainerHookXml, {\n                \"t-if\": `__comp__.mailLayout(${hasPreview}) == \"COMBO\"`,\n            });\n            append(formSheetBgXml, sheetBgChatterContainerHookXml);\n            const sheetBgChatterContainerXml = sheetBgChatterContainerHookXml.querySelector(\n                \"t[t-component='__comp__.mailComponents.Chatter']\"\n            );\n            setAttributes(sheetBgChatterContainerXml, {\n                isInFormSheetBg: \"true\",\n                isChatterAside: \"false\",\n            });\n        }\n        // after sheet bg (standard position, either aside or below)\n        setAttributes(chatterContainerXml, {\n            isInFormSheetBg: `[\"COMBO\", \"BOTTOM_CHATTER\"].includes(__comp__.mailLayout(${hasPreview}))`,\n            isChatterAside: `[\"SIDE_CHATTER\", \"EXTERNAL_COMBO_XXL\", \"EXTERNAL_COMBO\"].includes(__comp__.mailLayout(${hasPreview}))`,\n        });\n        const { [\"t-if\"]: tIf } = extractAttributes(chatterContainerHookXml, [\"t-if\"]);\n        setAttributes(chatterContainerHookXml, {\n            \"t-if\": `${\n                tIf ? tIf : \"true\"\n            } and (![\"COMBO\", \"NONE\"].includes(__comp__.mailLayout(${hasPreview})))`, // opposite of sheetBgChatterContainerHookXml\n            \"t-attf-class\": `{{ [\"SIDE_CHATTER\", \"EXTERNAL_COMBO_XXL\"].includes(__comp__.mailLayout(${hasPreview})) ? \"o-aside w-print-100\" : \"mt-4 mt-md-0\" }}`,\n        });\n        append(parentXml, chatterContainerHookXml);\n        return res;\n    },\n});\n", "import { EventBus, useSubEnv } from \"@odoo/owl\";\n\nimport { x2ManyCommands } from \"@web/core/orm_service\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { createDocumentFragmentFromContent } from \"@web/core/utils/html\";\nimport { patch } from \"@web/core/utils/patch\";\nimport { FormController } from \"@web/views/form/form_controller\";\n\nFormController.props = {\n    ...FormController.props,\n    fullComposerBus: { type: EventBus, optional: true },\n};\n\npatch(FormController.prototype, {\n    setup() {\n        super.setup(...arguments);\n        if (this.env.services[\"mail.store\"]) {\n            this.mailStore = useService(\"mail.store\");\n        }\n        useSubEnv({\n            chatter: {\n                fetchThreadData: true,\n                fetchMessages: true,\n            },\n        });\n    },\n    onWillLoadRoot(nextConfiguration) {\n        super.onWillLoadRoot(...arguments);\n        this.env.chatter.fetchThreadData = true;\n        this.env.chatter.fetchMessages = true;\n        const isSameThread =\n            this.model.root?.resId === nextConfiguration.resId &&\n            this.model.root?.resModel === nextConfiguration.resModel;\n        if (isSameThread) {\n            // not first load\n            const { resModel, resId } = this.model.root;\n            this.env.bus.trigger(\"MAIL:RELOAD-THREAD\", { model: resModel, id: resId });\n        }\n    },\n\n    async onWillSaveRecord(record, changes) {\n        if (record.resModel === \"mail.compose.message\") {\n            const doc = createDocumentFragmentFromContent(changes.body);\n            const partnerElements = doc.querySelectorAll('[data-oe-model=\"res.partner\"]');\n            const partnerIds = Array.from(partnerElements).map((element) =>\n                parseInt(element.dataset.oeId)\n            );\n            if (partnerIds.length) {\n                if (changes.partner_ids[0] && changes.partner_ids[0][0] === x2ManyCommands.SET) {\n                    partnerIds.push(...changes.partner_ids[0][2]);\n                }\n                changes.partner_ids.push(...partnerIds.map((pid) => x2ManyCommands.link(pid)));\n            }\n        }\n    },\n});\n", "import { AttachmentView } from \"@mail/core/common/attachment_view\";\nimport { Chatter } from \"@mail/chatter/web_portal/chatter\";\n\nimport { onMounted, onWillUnmount, useState } from \"@odoo/owl\";\n\nimport { browser } from \"@web/core/browser/browser\";\nimport { router } from \"@web/core/browser/router\";\nimport { SIZES } from \"@web/core/ui/ui_service\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { patch } from \"@web/core/utils/patch\";\nimport { useDebounced } from \"@web/core/utils/timing\";\nimport { FormRenderer } from \"@web/views/form/form_renderer\";\n\npatch(FormRenderer.prototype, {\n    setup() {\n        super.setup();\n        this.mailComponents = {\n            AttachmentView,\n            Chatter,\n        };\n        this.highlightMessageId = router.current.highlight_message_id;\n        this.messagingState = useState({\n            /** @type {import(\"models\").Thread} */\n            thread: undefined,\n        });\n        if (this.env.services[\"mail.store\"]) {\n            this.mailStore = useService(\"mail.store\");\n        }\n        this.uiService = useService(\"ui\");\n        this.mailPopoutService = useService(\"mail.popout\");\n\n        this.onResize = useDebounced(this.render, 200);\n        onMounted(() => browser.addEventListener(\"resize\", this.onResize));\n        onWillUnmount(() => browser.removeEventListener(\"resize\", this.onResize));\n    },\n    /**\n     * @returns {boolean}\n     */\n    hasFile() {\n        if (!this.mailStore || !this.props.record.resId) {\n            return false;\n        }\n        this.messagingState.thread = this.mailStore.Thread.insert({\n            id: this.props.record.resId,\n            model: this.props.record.resModel,\n        });\n        return this.messagingState.thread.attachmentsInWebClientView.length > 0;\n    },\n    mailLayout(hasAttachmentContainer) {\n        const xxl = this.uiService.size >= SIZES.XXL;\n        const hasFile = this.hasFile();\n        const hasChatter = !!this.mailStore;\n        const hasExternalWindow = !!this.mailPopoutService.externalWindow;\n        if (hasExternalWindow && hasFile && hasAttachmentContainer) {\n            if (xxl) {\n                return \"EXTERNAL_COMBO_XXL\"; // chatter on the side, attachment in separate tab\n            }\n            return \"EXTERNAL_COMBO\"; // chatter on the bottom, attachment in separate tab\n        }\n        if (hasChatter) {\n            if (xxl) {\n                if (hasAttachmentContainer && hasFile) {\n                    return \"COMBO\"; // chatter on the bottom, attachment on the side\n                }\n                return \"SIDE_CHATTER\"; // chatter on the side, no attachment\n            }\n            return \"BOTTOM_CHATTER\"; // chatter on the bottom, no attachment\n        }\n        return \"NONE\";\n    },\n});\n", "import { formView } from \"@web/views/form/form_view\";\nimport { registry } from \"@web/core/registry\";\nimport { EventBus, toRaw, useEffect, useRef, useSubEnv } from \"@odoo/owl\";\nimport { useCustomDropzone } from \"@web/core/dropzone/dropzone_hook\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { useX2ManyCrud } from \"@web/views/fields/relational_utils\";\nimport { MailAttachmentDropzone } from \"@mail/core/common/mail_attachment_dropzone\";\n\nexport class MailComposerFormController extends formView.Controller {\n    static props = {\n        ...formView.Controller.props,\n        fullComposerBus: { type: EventBus, optional: true },\n    };\n    static defaultProps = { fullComposerBus: new EventBus() };\n    setup() {\n        super.setup();\n        toRaw(this.env.dialogData).model = \"mail.compose.message\";\n        useSubEnv({\n            fullComposerBus: this.props.fullComposerBus,\n        });\n    }\n}\n\nexport class MailComposerFormRenderer extends formView.Renderer {\n    setup() {\n        super.setup();\n        this.orm = useService(\"orm\");\n        // Autofocus the visible editor in edition mode.\n        this.root = useRef(\"compiled_view_root\");\n        useEffect(\n            (isInEdition, el) => {\n                if (\n                    el &&\n                    isInEdition &&\n                    this.props.record.data.composition_comment_option === \"reply_all\"\n                ) {\n                    const element = el.querySelector(\".note-editable[contenteditable]\");\n                    if (element) {\n                        element.focus();\n                        document.dispatchEvent(new Event(\"selectionchange\", {}));\n                    }\n                }\n            },\n            () => [this.props.record.isInEdition, this.root.el, this.props.record.resId]\n        );\n\n        const getActiveMailThreads = () =>\n            JSON.parse(this.props.record.data.res_ids).map((resId) => {\n                const thread = this.mailStore.Thread.insert({\n                    model: this.props.record.data.model,\n                    id: resId,\n                });\n                return thread;\n            });\n\n        // Add file dropzone on full mail composer:\n        this.attachmentUploadService = useService(\"mail.attachment_upload\");\n        this.operations = useX2ManyCrud(() => this.props.record.data[\"attachment_ids\"], true);\n\n        useCustomDropzone(this.root, MailAttachmentDropzone, {\n            /** @param {Event} event */\n            onDrop: async (event) => {\n                for (const thread of getActiveMailThreads()) {\n                    for (const file of event.dataTransfer.files) {\n                        const attachment = await this.attachmentUploadService.upload(\n                            thread,\n                            thread.composer,\n                            file\n                        );\n                        await this.operations.saveRecord([attachment.id]);\n                    }\n                }\n            },\n        });\n\n        /** @param {function} */\n        const onCloseWizardModal = (callback) => {\n            this.env.dialogData.dismiss = callback;\n        };\n\n        onCloseWizardModal(async () => {\n            const selectedPartnerIds = this.props.record.data.partner_ids.currentIds;\n            const selectedPartners = await this.orm.searchRead(\n                \"res.partner\",\n                [[\"id\", \"in\", selectedPartnerIds]],\n                [\"email\", \"id\", \"lang\", \"name\"]\n            );\n\n            /**\n             * @param {SuggestedRecipient} recipient\n             * @returns {SuggestedRecipient}\n             */\n            const updateRecipientWithCorrespondingPartner = (recipient) => {\n                const partner = selectedPartners.find(\n                    (partner) => partner.id === recipient.id || partner.email === recipient.email\n                );\n                if (partner) {\n                    return {\n                        ...recipient,\n                        email: partner.email,\n                        lang: partner.lang,\n                        name: partner.name,\n                        partner_id: partner.id,\n                    };\n                }\n                return recipient;\n            };\n\n            /**\n             * @param {SuggestedRecipient} recipient\n             * @returns {boolean}\n             */\n            const isRecipientSelectedFromFullMailComposer = (recipient) =>\n                selectedPartnerIds.includes(recipient.partner_id);\n\n            for (const thread of getActiveMailThreads()) {\n                // Update the recipient lists:\n                thread.suggestedRecipients = thread.suggestedRecipients.map(\n                    updateRecipientWithCorrespondingPartner\n                );\n                thread.additionalRecipients = thread.additionalRecipients.map(\n                    updateRecipientWithCorrespondingPartner\n                );\n\n                // Remove the recipients that got removed from the composer:\n                thread.suggestedRecipients = thread.suggestedRecipients.filter(\n                    isRecipientSelectedFromFullMailComposer\n                );\n                thread.additionalRecipients = thread.additionalRecipients.filter(\n                    isRecipientSelectedFromFullMailComposer\n                );\n\n                // Add the recipients that got added to the composer:\n                for (const partner of selectedPartners) {\n                    const allRecipients = [\n                        ...thread.suggestedRecipients,\n                        ...thread.additionalRecipients,\n                    ];\n                    if (!allRecipients.some((recipient) => recipient.partner_id === partner.id)) {\n                        thread.additionalRecipients.push({\n                            email: partner.email,\n                            lang: partner.lang,\n                            name: partner.name,\n                            partner_id: partner.id,\n                        });\n                    }\n                }\n            }\n        });\n    }\n}\n\nregistry.category(\"views\").add(\"mail_composer_form\", {\n    ...formView,\n    Controller: MailComposerFormController,\n    Renderer: MailComposerFormRenderer,\n});\n", "import { formView } from \"@web/views/form/form_view\";\nimport { registry } from \"@web/core/registry\";\nimport { useService } from \"@web/core/utils/hooks\";\n\n\nexport class MailComposerSaveTemplateFormController extends formView.Controller {\n    /** @override */\n    setup() {\n        super.setup();\n        this.actionService = useService(\"action\");\n    }\n\n    /** @override */\n    async afterExecuteActionButton(clickParams) {\n        if (clickParams.special !== \"cancel\") {\n            return await super.afterExecuteActionButton(...arguments);\n        }\n        await this.actionService.doActionButton({\n            type: \"object\",\n            name: \"cancel_save_template\",\n            resId: this.model.root.resId,\n            resModel: this.model.root.resModel,\n        });\n    }\n}\n\nregistry.category(\"views\").add(\"mail_composer_save_template_form\", {\n    ...formView,\n    Controller: MailComposerSaveTemplateFormController,\n});\n", "import { AttachmentList } from \"@mail/core/common/attachment_list\";\nimport { RelativeTime } from \"@mail/core/common/relative_time\";\nimport { AvatarCardPopover } from \"@mail/discuss/web/avatar_card/avatar_card_popover\";\nimport { ConfirmationDialog } from \"@web/core/confirmation_dialog/confirmation_dialog\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { usePopover } from \"@web/core/popover/popover_hook\";\nimport { useService } from \"@web/core/utils/hooks\";\n\nimport { Component, useState } from \"@odoo/owl\";\n\nexport const SCHEDULED_MESSAGE_TRUNCATE_THRESHOLD = 50; // arbitrary, ~ 1 line on large screen\n\nexport class ScheduledMessage extends Component {\n    static props = {\n        onScheduledMessageChanged: Function,\n        scheduledMessage: Object,\n    };\n    static template = \"mail.ScheduledMessage\";\n    static components = {\n        AttachmentList,\n        RelativeTime,\n    };\n\n    setup() {\n        super.setup();\n        this.state = useState({\n            readMore: false,\n        });\n        this.avatarCard = usePopover(AvatarCardPopover);\n        this.dialogService = useService(\"dialog\");\n    }\n\n    get isShort() {\n        return (\n            this.props.scheduledMessage.textContent.length < SCHEDULED_MESSAGE_TRUNCATE_THRESHOLD\n        );\n    }\n\n    get scheduledDate() {\n        return this.props.scheduledMessage.scheduled_date.toLocaleString(\n            luxon.DateTime.DATETIME_SHORT\n        );\n    }\n\n    get truncatedMessage() {\n        return (\n            this.props.scheduledMessage.textContent.substring(\n                0,\n                SCHEDULED_MESSAGE_TRUNCATE_THRESHOLD\n            ) + \"...\"\n        );\n    }\n\n    async cancel() {\n        const thread = this.props.scheduledMessage.thread;\n        await this.props.scheduledMessage.cancel();\n        this.props.onScheduledMessageChanged(thread);\n    }\n\n    onClick(ev) {\n        this.props.scheduledMessage.store.handleClickOnLink(ev, this.props.scheduledMessage.thread);\n    }\n\n    async onClickAttachmentUnlink(attachment) {\n        attachment.remove();\n    }\n\n    onClickAuthor(ev) {\n        if (!this.avatarCard.isOpen) {\n            this.avatarCard.open(ev.currentTarget, {\n                id: this.props.scheduledMessage.author_id.main_user_id?.id,\n            });\n        }\n    }\n\n    onClickCancel() {\n        this.dialogService.add(ConfirmationDialog, {\n            body: _t(\"Are you sure you want to cancel the scheduled message?\"),\n            cancel: () => {},\n            cancelLabel: _t(\"Close\"),\n            confirm: this.cancel.bind(this),\n            confirmLabel: _t(\"Cancel Message\"),\n        });\n    }\n\n    async onClickEdit() {\n        await this.props.scheduledMessage.edit();\n        this.props.onScheduledMessageChanged(this.props.scheduledMessage.thread);\n    }\n\n    async onClickSendNow() {\n        await this.props.scheduledMessage.send();\n        this.props.onScheduledMessageChanged(this.props.scheduledMessage.thread);\n    }\n}\n", "import { fields, Record } from \"@mail/core/common/record\";\nimport { htmlToTextContentInline } from \"@mail/utils/common/format\";\nimport { _t } from \"@web/core/l10n/translation\";\n\nexport class ScheduledMessage extends Record {\n    static _name = \"mail.scheduled.message\";\n    static id = \"id\";\n    /** @type {Object.<number, import(\"models\").ScheduledMessage>} */\n    static records = {};\n    /** @returns {import(\"models\").ScheduledMessage} */\n    static get(data) {\n        return super.get(data);\n    }\n    /** @type {number} */\n    id;\n    attachment_ids = fields.Many(\"ir.attachment\");\n    author_id = fields.One(\"res.partner\");\n    body = fields.Html(\"\");\n    /** @type {boolean} */\n    composition_batch;\n    scheduled_date = fields.Datetime();\n    /** @type {boolean} */\n    is_note;\n    textContent = fields.Attr(false, {\n        compute() {\n            if (!this.body) {\n                return \"\";\n            }\n            return htmlToTextContentInline(this.body);\n        },\n    });\n    thread = fields.One(\"Thread\");\n    // Editors of the records can delete scheduled messages\n    get deletable() {\n        return this.store.self.main_user_id?.is_admin || this.thread.hasWriteAccess;\n    }\n\n    get editable() {\n        return this.store.self.main_user_id?.is_admin || this.isSelfAuthored;\n    }\n\n    get isSelfAuthored() {\n        return this.author_id.eq(this.store.self);\n    }\n\n    get isSubjectThreadName() {\n        return (\n            this.thread.display_name?.trim().toLowerCase() === this.subject?.trim().toLowerCase()\n        );\n    }\n\n    /**\n     * Cancel the scheduled message.\n     */\n    async cancel() {\n        await this.store.env.services.orm.unlink(\"mail.scheduled.message\", [this.id]);\n        this.delete();\n    }\n\n    /**\n     * Open the mail_compose_mesage form view to allow edition of the scheduled message.\n     * If the message has already been sent, displays a notification instead.\n     */\n    async edit() {\n        let action;\n        try {\n            action = await this.store.env.services.orm.call(\n                \"mail.scheduled.message\",\n                \"open_edit_form\",\n                [this.id]\n            );\n        } catch {\n            this.notifyAlreadySent();\n            return;\n        }\n        return new Promise((resolve) =>\n            this.store.env.services.action.doAction(action, { onClose: resolve })\n        );\n    }\n\n    notifyAlreadySent() {\n        this.store.env.services.notification.add(_t(\"This message has already been sent.\"), {\n            type: \"warning\",\n        });\n    }\n\n    /**\n     * Send the scheduled message directly\n     */\n    async send() {\n        try {\n            await this.store.env.services.orm.call(\"mail.scheduled.message\", \"post_message\", [\n                this.id,\n            ]);\n        } catch {\n            // already sent (by someone else or by cron)\n            return;\n        }\n    }\n}\n\nScheduledMessage.register();\n", "import { fields } from \"@mail/core/common/record\";\nimport { Thread } from \"@mail/core/common/thread_model\";\nimport { compareDatetime } from \"@mail/utils/common/misc\";\nimport \"@mail/chatter/web_portal/thread_model_patch\";\n\nimport { patch } from \"@web/core/utils/patch\";\n\n/** @type {import(\"models\").Thread} */\nconst threadPatch = {\n    setup() {\n        super.setup();\n        this.scheduledMessages = fields.Many(\"mail.scheduled.message\", {\n            sort: (a, b) => compareDatetime(a.scheduled_date, b.scheduled_date) || a.id - b.id,\n            inverse: \"thread\",\n        });\n    },\n\n    /** @param {string[]} requestList */\n    async fetchThreadData(requestList) {\n        this.isLoadingAttachments =\n            this.isLoadingAttachments || requestList.includes(\"attachments\");\n        await super.fetchThreadData(requestList);\n        if (!this.message_main_attachment_id && this.attachmentsInWebClientView.length > 0) {\n            this.setMainAttachmentFromIndex(0);\n        }\n    },\n};\npatch(Thread.prototype, threadPatch);\n", "import { CalendarRenderer } from \"@web/views/calendar/calendar_renderer\";\nimport { ActivityCalendarCommonRender } from \"./calendar_common/activity_calendar_common_renderer\";\nimport { ActivityCalendarYearRenderer } from \"./calendar_year/activity_calendar_year_renderer\";\n\nexport class ActivityCalendarRender extends CalendarRenderer {\n    static components = {\n        ...CalendarRenderer.components,\n        day: ActivityCalendarCommonRender,\n        week: ActivityCalendarCommonRender,\n        month: ActivityCalendarCommonRender,\n        year: ActivityCalendarYearRenderer,\n    };\n}\n", "import { registry } from \"@web/core/registry\";\nimport { calendarView } from \"@web/views/calendar/calendar_view\";\nimport { ActivityCalendarRender } from \"./activity_calendar_renderer\";\n\nconst activityCalendarView = {\n    ...calendarView,\n    Renderer: ActivityCalendarRender,\n};\n\nregistry.category(\"views\").add(\"activity_calendar\", activityCalendarView);\n", "import { useService } from \"@web/core/utils/hooks\";\nimport { CalendarCommonPopover } from \"@web/views/calendar/calendar_common/calendar_common_popover\";\n\nexport class ActivityCalendarCommonPopover extends CalendarCommonPopover {\n    static subTemplates = {\n        ...CalendarCommonPopover.subTemplates,\n        footer: \"mail.ActivityCalendarCommonPopover.footer\",\n    };\n    setup() {\n        this.orm = useService(\"orm\");\n        this.actionService = useService(\"action\");\n    }\n\n    async openRecord() {\n        const action = await this.orm.call(\"mail.activity\", \"action_open_document\", [\n            this.props.record.rawRecord.id,\n        ]);\n        this.actionService.doAction(action);\n    }\n}\n", "import { CalendarCommonRenderer } from \"@web/views/calendar/calendar_common/calendar_common_renderer\";\nimport { ActivityCalendarCommonPopover } from \"./activity_calendar_common_popover\";\n\nexport class ActivityCalendarCommonRender extends CalendarCommonRenderer {\n    static components = {\n        ...CalendarCommonRenderer.components,\n        Popover: ActivityCalendarCommonPopover,\n    };\n}\n", "import { useService } from \"@web/core/utils/hooks\";\nimport { CalendarYearPopover } from \"@web/views/calendar/calendar_year/calendar_year_popover\";\n\nexport class ActivityCalendarYearPopover extends CalendarYearPopover {\n    setup() {\n        super.setup();\n        this.orm = useService(\"orm\");\n        this.actionService = useService(\"action\");\n    }\n\n    async onRecordClick(record) {\n        const action = await this.orm.call(\"mail.activity\", \"action_open_document\", [\n            record.rawRecord.id,\n        ]);\n        this.actionService.doAction(action);\n    }\n}\n", "import { CalendarYearRenderer } from \"@web/views/calendar/calendar_year/calendar_year_renderer\";\nimport { ActivityCalendarYearPopover } from \"./activity_calendar_year_popover\";\n\nexport class ActivityCalendarYearRenderer extends CalendarYearRenderer {\n    static components = {\n        ...CalendarYearRenderer.components,\n        Popover: ActivityCalendarYearPopover,\n    };\n}\n", "import { Component } from \"@odoo/owl\";\n\nimport { registry } from \"@web/core/registry\";\nimport { standardFieldProps } from \"@web/views/fields/standard_field_props\";\n\nclass ActivityException extends Component {\n    static props = standardFieldProps;\n    static template = \"mail.ActivityException\";\n    static fieldDependencies = [{ name: \"activity_exception_icon\", type: \"char\" }];\n\n    get textClass() {\n        if (this.props.record.data[this.props.name]) {\n            return (\n                \"text-\" +\n                this.props.record.data[this.props.name] +\n                \" fa \" +\n                this.props.record.data.activity_exception_icon\n            );\n        }\n        return undefined;\n    }\n}\n\nObject.assign(ActivityException, {\n    props: standardFieldProps,\n    template: \"mail.ActivityException\",\n});\n\nregistry.category(\"fields\").add(\"activity_exception\", {\n    component: ActivityException,\n    fieldDependencies: ActivityException.fieldDependencies,\n    label: false,\n});\n", "import { useComponent } from \"@odoo/owl\";\n\nimport { useCommand } from \"@web/core/commands/command_hook\";\nimport { Domain } from \"@web/core/domain\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { user } from \"@web/core/user\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { getFieldDomain } from \"@web/model/relational_model/utils\";\n\n/**\n * Use this hook to add \"Assign to..\" and \"Assign/Unassign me\" to the command palette.\n */\n\nexport function useAssignUserCommand() {\n    const component = useComponent();\n    const orm = useService(\"orm\");\n    const type = component.props.record.fields[component.props.name].type;\n    if (component.relation !== \"res.users\") {\n        return;\n    }\n\n    const getCurrentIds = () => {\n        if (type === \"many2one\" && component.props.record.data[component.props.name]) {\n            return [component.props.record.data[component.props.name].id];\n        } else if (type === \"many2many\") {\n            return component.props.record.data[component.props.name].currentIds;\n        }\n        return [];\n    };\n\n    const add = async (record) => {\n        if (type === \"many2one\") {\n            component.props.record.update({ [component.props.name]: {\n                id: record[0],\n                display_name: record[1],\n            } });\n        } else if (type === \"many2many\") {\n            component.props.record.data[component.props.name].linkTo(record[0], {\n                display_name: record[1],\n            });\n        }\n    };\n\n    const remove = async (record) => {\n        if (type === \"many2one\") {\n            component.props.record.update({ [component.props.name]: false });\n        } else if (type === \"many2many\") {\n            component.props.record.data[component.props.name].unlinkFrom(record[0]);\n        }\n    };\n\n    const provide = async (env, options) => {\n        const value = options.searchValue.trim();\n        let domain = getFieldDomain(\n            component.props.record,\n            component.props.name,\n            component.props.domain\n        );\n        const context = component.props.context;\n        if (type === \"many2many\") {\n            const selectedUserIds = getCurrentIds();\n            if (selectedUserIds.length) {\n                domain = Domain.and([domain, [[\"id\", \"not in\", selectedUserIds]]]).toList();\n            }\n        }\n        component._pendingRpc?.abort(false);\n        component._pendingRpc = orm.call(component.relation, \"name_search\", [], {\n            name: value,\n            domain: domain,\n            operator: \"ilike\",\n            limit: 80,\n            context,\n        });\n        const searchResult = await component._pendingRpc;\n        component._pendingRpc = null;\n        return searchResult.map((record) => ({\n            name: record[1],\n            action: add.bind(null, record),\n        }));\n    };\n    const options = {\n        category: \"smart_action\",\n        global: true,\n        identifier: component.props.string,\n    };\n    if (component.props.record.id !== component.props.record.model.root.id) {\n        // Only List View\n        options.isAvailable = () =>\n            component.props.record.model.multiEdit && component.props.record.selected;\n    } else {\n        options.isAvailable = () => true;\n    }\n    useCommand(\n        _t(\"Assign to ...\"),\n        () => ({\n            configByNameSpace: {\n                default: {\n                    emptyMessage: _t(\"No users found\"),\n                },\n            },\n            placeholder: _t(\"Select a user...\"),\n            providers: [\n                {\n                    provide,\n                },\n            ],\n        }),\n        {\n            ...options,\n            hotkey: \"alt+i\",\n        }\n    );\n\n    useCommand(\n        _t(\"Assign to me\"),\n        () => {\n            add([user.userId, user.name]);\n        },\n        {\n            ...options,\n            isAvailable: () => options.isAvailable() && !getCurrentIds().includes(user.userId),\n            hotkey: \"alt+shift+i\",\n        }\n    );\n    if (component.props.record.id === component.props.record.model.root.id) {\n        // Only Form View\n        useCommand(\n            _t(\"Unassign from me\"),\n            () => {\n                remove([user.userId, user.name]);\n            },\n            {\n                ...options,\n                isAvailable: () => options.isAvailable() && getCurrentIds().includes(user.userId),\n                hotkey: \"alt+shift+i\",\n            }\n        );\n    } else {\n        if (type === \"many2one\") {\n            useCommand(\n                _t(\"Unassign\"),\n                () => {\n                    remove([user.userId, user.name]);\n                },\n                {\n                    ...options,\n                    isAvailable: () => options.isAvailable() && getCurrentIds().length > 0,\n                    hotkey: \"alt+shift+u\",\n                }\n            );\n        } else {\n            useCommand(\n                _t(\"Unassign from me\"),\n                () => {\n                    remove([user.userId, user.name]);\n                },\n                {\n                    ...options,\n                    isAvailable: () =>\n                        options.isAvailable() && getCurrentIds().includes(user.userId),\n                    hotkey: \"alt+shift+u\",\n                }\n            );\n        }\n    }\n}\n", "import { usePopover } from \"@web/core/popover/popover_hook\";\nimport { AvatarCardPopover } from \"@mail/discuss/web/avatar_card/avatar_card_popover\";\n\nimport { Component } from \"@odoo/owl\";\n\nexport class Avatar extends Component {\n    static template = \"mail.Avatar\";\n    static components = { Popover: AvatarCardPopover };\n    static props = {\n        resModel: { type: String },\n        resId: { type: Number },\n        canOpenPopover: { type: Boolean, optional: true },\n        cssClass: { type: [String, Object], optional: true },\n        displayName: { type: String, optional: true },\n        noSpacing: { type: Boolean, optional: true },\n    };\n    static defaultProps = {\n        canOpenPopover: true,\n    };\n\n    setup() {\n        this.avatarCard = usePopover(this.constructor.components.Popover);\n    }\n\n    get canOpenPopover() {\n        return this.props.canOpenPopover && !this.env.isSmall && !!this.props.resId;\n    }\n\n    get popoverProps() {\n        return {\n            id: this.props.resId,\n            model: this.props.resModel,\n        };\n    }\n\n    onClickAvatar(ev) {\n        const target = ev.currentTarget;\n        if (!this.avatarCard.isOpen && this.canOpenPopover) {\n            this.avatarCard.open(target, this.popoverProps);\n        }\n    }\n}\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { Many2XAutocomplete } from \"@web/views/fields/relational_utils\";\nimport { AvatarUserFormViewDialog } from \"@mail/views/web/view_dialog/avatar_user_form_view_dialog\";\n\nexport class Many2XAvatarUserAutocomplete extends Many2XAutocomplete {\n    get actionSuggestions() {\n        return [\n            {\n                enabled: () => this.activeActions.create,\n                build: (request) => {\n                    const label = request\n                        ? _t(`Invite \"%s\"`, request)\n                        : _t(\"Invite teammates via email\");\n                    return {\n                        cssClass:\n                            \"o_m2o_dropdown_option o_m2o_dropdown_option_create text-indent-3\",\n                        label: label,\n                        data: { slotName: \"inviteTeammates\", label: label },\n                        onSelect: () => this.slowCreate(request),\n                    };\n                },\n            },\n            {\n                enabled: this.addSearchMoreSuggestion.bind(this),\n                build: this.buildSearchMoreSuggestion.bind(this),\n            },\n        ];\n    }\n\n    get createDialog() {\n        return AvatarUserFormViewDialog;\n    }\n\n    get createDialogSize() {\n        return \"md\";\n    }\n\n    slowCreate(request) {\n        return this.openMany2X({\n            context: this.getCreationContext(request),\n            nextRecordsContext: this.props.context,\n            title: _t(\"Invite teammates\"),\n        });\n    }\n}\n", "import { EmojisFieldCommon } from \"@mail/views/web/fields/emojis_field_common/emojis_field_common\";\n\nimport { useRef } from \"@odoo/owl\";\n\nimport { registry } from \"@web/core/registry\";\nimport { CharField, charField } from \"@web/views/fields/char/char_field\";\n\n/**\n * Extension of the FieldChar that will add emojis support\n */\nexport class EmojisCharField extends EmojisFieldCommon(CharField) {\n    static template = \"mail.EmojisCharField\";\n    static components = { ...CharField.components };\n    setup() {\n        super.setup();\n        this.targetEditElement = useRef(\"input\");\n        this._setupOverride();\n    }\n\n    get shouldTrim() {\n        return false;\n    }\n}\n\nexport const emojisCharField = {\n    ...charField,\n    component: EmojisCharField,\n    additionalClasses: [...(charField.additionalClasses || []), \"o_field_text\"],\n};\n\nregistry.category(\"fields\").add(\"char_emojis\", emojisCharField);\n", "import { useEmojiPicker } from \"@web/core/emoji_picker/emoji_picker\";\n\nimport { useRef } from \"@odoo/owl\";\n\n/*\n * Common code for EmojisTextField and EmojisCharField\n */\nexport const EmojisFieldCommon = (T) =>\n    class EmojisFieldCommon extends T {\n        /**\n         * Create an emoji textfield view to enable opening an emoji popover\n         */\n        _setupOverride() {\n            this.emojiPicker = useEmojiPicker(\n                useRef(\"emojisButton\"),\n                {\n                    onSelect: (codepoints) => {\n                        const originalContent = this.targetEditElement.el.value;\n                        const start = this.targetEditElement.el.selectionStart;\n                        const end = this.targetEditElement.el.selectionEnd;\n                        const left = originalContent.slice(0, start);\n                        const right = originalContent.slice(end, originalContent.length);\n                        this.targetEditElement.el.value = left + codepoints + right;\n                        // trigger onInput from input_field hook to set field as dirty\n                        this.targetEditElement.el.dispatchEvent(new InputEvent(\"input\"));\n                        // keydown serves to both commit the changes in input_field and trigger onchange for some fields\n                        this.targetEditElement.el.dispatchEvent(new KeyboardEvent(\"keydown\"));\n                        this.targetEditElement.el.focus();\n                        const newCursorPos = start + codepoints.length;\n                        this.targetEditElement.el.setSelectionRange(newCursorPos, newCursorPos);\n                        if (this._emojiAdded) {\n                            this._emojiAdded();\n                        }\n                    },\n                },\n                {\n                    position: \"bottom\",\n                }\n            );\n        }\n    };\n", "import { EmojisFieldCommon } from \"@mail/views/web/fields/emojis_field_common/emojis_field_common\";\n\nimport { registry } from \"@web/core/registry\";\nimport { TextField, textField } from \"@web/views/fields/text/text_field\";\n\n/**\n * Extension of the FieldText that will add emojis support\n */\nexport class EmojisTextField extends EmojisFieldCommon(TextField) {\n    static template = \"mail.EmojisTextField\";\n    static components = { ...TextField.components };\n    setup() {\n        super.setup();\n        this.targetEditElement = this.textareaRef;\n        this._setupOverride();\n    }\n}\n\nexport const emojisTextField = {\n    ...textField,\n    component: EmojisTextField,\n    additionalClasses: [...(textField.additionalClasses || []), \"o_field_text\"],\n};\n\nregistry.category(\"fields\").add(\"text_emojis\", emojisTextField);\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { fillEmpty } from \"@html_editor/utils/dom\";\nimport { isEmptyBlock } from \"@html_editor/utils/dom_info\";\nimport { closestElement, selectElements } from \"@html_editor/utils/dom_traversal\";\nimport { renderToElement } from \"@web/core/utils/render\";\n\nexport class ContentExpandablePlugin extends Plugin {\n    static id = \"contentexpandable\";\n    static dependencies = [\"protectedNode\", \"selection\"];\n    resources = {\n        clean_for_save_handlers: ({ root }) => this.cleanForSave(root),\n        delete_backward_overrides: this.deleteBackward.bind(this),\n        move_node_blacklist_selectors: \".o_mail_reply_container, .o_mail_reply_container *\",\n    };\n\n    setup() {\n        this.insertReplyContent();\n    }\n\n    deleteBackward({ endContainer }) {\n        const closestReplyContainer = closestElement(endContainer, \".o_mail_reply_container\");\n        if (closestReplyContainer && isEmptyBlock(closestReplyContainer)) {\n            const parentEl = closestReplyContainer.parentElement;\n            closestReplyContainer.remove();\n            fillEmpty(parentEl);\n            return true;\n        }\n    }\n\n    /**\n     * @override\n     */\n    isValidTargetForDomListener(ev) {\n        if (\n            ev.type === \"click\" &&\n            ev.target &&\n            closestElement(ev.target, \".o-mail-Message-viewMore-btn\")\n        ) {\n            // Allow clicking on the viewMore button even if it is protected.\n            return true;\n        }\n        return super.isValidTargetForDomListener(ev);\n    }\n\n    insertReplyContent() {\n        const ele = this.editable.querySelector(\".o_mail_reply_container\");\n        if (!ele) {\n            return;\n        }\n        this.dependencies.protectedNode.setProtectingNode(ele, true);\n        for (const subEl of ele.querySelectorAll(\":scope > .o_mail_reply_content\")) {\n            this.dependencies.protectedNode.setProtectingNode(subEl, false);\n            subEl.classList.add(\"d-none\");\n        }\n        const mailQuoteElement = this.editable.querySelectorAll('*[data-o-mail-quote=\"1\"]');\n        for (const element of mailQuoteElement) {\n            element.removeAttribute(\"data-o-mail-quote\");\n            element.removeAttribute(\"style\");\n        }\n        const buttonTemplate = renderToElement(\"mail.ExpandableButton\");\n        const button = buttonTemplate.querySelector(\".o-mail-Message-viewMore-btn\");\n        this.addDomListener(button, \"click\", this.onClickViewButton);\n        ele.prepend(buttonTemplate);\n    }\n\n    onClickViewButton(ev) {\n        const ele = closestElement(ev.target, \".o_mail_reply_container\");\n        if (!ele) {\n            return;\n        }\n        for (const subEl of ele.querySelectorAll(\":scope > .o_mail_reply_content\")) {\n            subEl.classList.toggle(\"d-none\");\n        }\n        closestElement(ev.target, \".o-mail-Message-viewMore-container\")?.remove();\n    }\n\n    cleanForSave(root) {\n        for (const el of selectElements(root, \".o_mail_reply_container\")) {\n            delete el.dataset.oeProtected;\n            for (const subEl of el.querySelectorAll(\".o_mail_reply_content\")) {\n                delete subEl.dataset.oeProtected;\n                subEl.classList.remove(\"d-none\");\n            }\n            el.querySelector(\".o-mail-Message-viewMore-container\")?.remove();\n            el.setAttribute(\"data-o-mail-quote\", \"1\");\n        }\n    }\n}\n", "import { DYNAMIC_PLACEHOLDER_PLUGINS } from \"@html_editor/backend/plugin_sets\";\nimport { isEmpty } from \"@html_editor/utils/dom_info\";\nimport { registry } from \"@web/core/registry\";\nimport { useBus } from \"@web/core/utils/hooks\";\nimport { HtmlMailField, htmlMailField } from \"../html_mail_field/html_mail_field\";\nimport { MentionPlugin } from \"./mention_plugin\";\nimport { ContentExpandablePlugin } from \"./content_expandable_plugin\";\nimport { fillEmpty } from \"@html_editor/utils/dom\";\nimport { markup } from \"@odoo/owl\";\n\nexport class HtmlComposerMessageField extends HtmlMailField {\n    setup() {\n        super.setup();\n        if (this.env.fullComposerBus) {\n            useBus(this.env.fullComposerBus, \"ACCIDENTAL_DISCARD\", (ev) => {\n                const elContent = this.getNoSignatureElContent();\n                ev.detail.onAccidentalDiscard(isEmpty(elContent));\n            });\n            useBus(this.env.fullComposerBus, \"SAVE_CONTENT\", (ev) => {\n                const emailAddSignature = Boolean(\n                    this.editor.editable.querySelector(\".o-signature-container\")\n                );\n                const composerHtml = markup(this.getNoSignatureElContent().innerHTML);\n                ev.detail.onSaveContent({ composerHtml, emailAddSignature });\n            });\n            useBus(this.env.fullComposerBus, \"ATTACHMENT_REMOVED\", (ev) => {\n                const attachmentElements = this.editor.editable.querySelectorAll(\n                    `[data-attachment-id=\"${ev.detail.id}\"]`\n                );\n                attachmentElements.forEach((element) => {\n                    const parent = element.parentElement;\n                    element.remove();\n                    fillEmpty(parent);\n                });\n                this.editor.shared.history.addStep();\n            });\n        }\n    }\n\n    getConfig() {\n        const config = super.getConfig(...arguments);\n        config.Plugins = [...config.Plugins, MentionPlugin];\n        if (this.props.record.data.composition_comment_option === \"reply_all\") {\n            config.Plugins.push(ContentExpandablePlugin);\n        }\n        if (!this.props.record.data.composition_batch) {\n            config.Plugins = config.Plugins.filter(\n                (plugin) => !DYNAMIC_PLACEHOLDER_PLUGINS.includes(plugin)\n            );\n        }\n        config.onAttachmentChange = (attachment) => {\n            // This only needs to happen for the composer for now\n            if (\n                !(\n                    this.props.record.fieldNames.includes(\"attachment_ids\") &&\n                    this.props.record.resModel === \"mail.compose.message\"\n                )\n            ) {\n                return;\n            }\n            this.props.record.data.attachment_ids.linkTo(attachment.id, attachment);\n        };\n        return config;\n    }\n\n    getNoSignatureElContent() {\n        const elContent = this.editor.getElContent();\n        for (const el of elContent.querySelectorAll(\".o-signature-container\")) {\n            el.remove();\n        }\n        return elContent;\n    }\n}\n\nexport const htmlComposerMessageField = {\n    ...htmlMailField,\n    additionalClasses: [...htmlMailField.additionalClasses, \"ps-0\", \"o_mail_composer_message\"],\n    component: HtmlComposerMessageField,\n};\n\nregistry.category(\"fields\").add(\"html_composer_message\", htmlComposerMessageField);\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { MentionList } from \"@mail/core/web/mention_list\";\nimport { router } from \"@web/core/browser/router\";\nimport { renderToElement } from \"@web/core/utils/render\";\nimport { url } from \"@web/core/utils/urls\";\n\nexport class MentionPlugin extends Plugin {\n    static id = \"mention\";\n    static dependencies = [\"overlay\", \"dom\", \"history\", \"input\", \"selection\"];\n\n    resources = {\n        beforeinput_handlers: this.onBeforeInput.bind(this),\n    };\n\n    setup() {\n        this.mentionList = this.dependencies.overlay.createOverlay(MentionList, {\n            hasAutofocus: true,\n            className: \"popover\",\n        });\n    }\n\n    onSelect(ev, option) {\n        this.dependencies.selection.focusEditable();\n        const mentionBlock = renderToElement(\"mail.Wysiwyg.mentionLink\", {\n            option,\n            href: url(\n                router.stateToUrl({\n                    model: option.partner ? \"res.partner\" : \"discuss.channel\",\n                    resId: option.partner ? option.partner.id : option.channel.id,\n                })\n            ),\n        });\n        const nameNode = this.document.createTextNode(\n            `${option.partner ? \"@\" : \"#\"}${option.label}`\n        );\n        mentionBlock.appendChild(nameNode);\n        this.historySavePointRestore();\n        this.dependencies.dom.insert(mentionBlock);\n        this.dependencies.history.addStep();\n    }\n\n    onBeforeInput(ev) {\n        if (ev.data === \"@\" || ev.data === \"#\") {\n            this.historySavePointRestore = this.dependencies.history.makeSavePoint();\n            this.mentionList.open({\n                props: {\n                    onSelect: this.onSelect.bind(this),\n                    type: ev.data === \"@\" ? \"partner\" : \"channel\",\n                    close: () => {\n                        this.mentionList.close();\n                    },\n                },\n            });\n        }\n    }\n}\n", "import { isBlock } from \"@html_editor/utils/blocks\";\nimport { getAdjacentPreviousSiblings } from \"@html_editor/utils/dom_traversal\";\nimport { blendColors } from \"@web/core/utils/colors\";\n\nfunction parentsGet(node, root = undefined) {\n    const parents = [];\n    while (node) {\n        parents.unshift(node);\n        if (node === root) {\n            break;\n        }\n        node = node.parentNode;\n    }\n    return parents;\n}\n\nfunction commonParentGet(node1, node2, root = undefined) {\n    if (!node1 || !node2) {\n        return null;\n    }\n    const n1p = parentsGet(node1, root);\n    const n2p = parentsGet(node2, root);\n    while (n1p.length > 1 && n1p[1] === n2p[1]) {\n        n1p.shift();\n        n2p.shift();\n    }\n    // Check  in case at least one of them is not in the DOM.\n    return n1p[0] === n2p[0] ? n1p[0] : null;\n}\n\n//--------------------------------------------------------------------------\n// Constants\n//--------------------------------------------------------------------------\n\nconst RE_COL_MATCH = /(^| )col(-[\\w\\d]+)*( |$)/;\nconst RE_OFFSET_MATCH = /(^| )offset(-[\\w\\d]+)*( |$)/;\nconst RE_PADDING_MATCH = /[ ]*padding[^;]*;/g;\nconst RE_PADDING = /([\\d.]+)/;\nconst RE_WHITESPACE = /[\\s\\u200b]*/;\nconst SELECTORS_IGNORE = /(^\\*$|:hover|:before|:after|:active|:link|::|')|@page/; // :not(:has) should be legal\nconst CONVERT_INLINE_BLACKLIST_CLASSES = [\"o_mail_redirect\"];\n// CSS properties relating to font, which Outlook seem to have trouble inheriting.\nconst FONT_PROPERTIES_TO_INHERIT = [\n    \"color\",\n    \"font-size\",\n    \"font-family\",\n    \"font-weight\",\n    \"font-style\",\n    \"text-decoration\",\n    \"text-transform\",\n    \"text-align\",\n];\n// Attributes all tables should have in a mailing.\nexport const TABLE_ATTRIBUTES = {\n    cellspacing: 0,\n    cellpadding: 0,\n    border: 0,\n    width: \"100%\",\n    align: \"center\",\n    role: \"presentation\",\n};\n// Cancel tables default styles.\nexport const TABLE_STYLES = {\n    \"border-collapse\": \"collapse\",\n    \"text-align\": \"inherit\",\n    \"font-size\": \"unset\",\n    \"line-height\": \"inherit\",\n};\n\nexport const BASIC_THEME_TABLE_STYLES = {\n    \"background-color\": \"transparent\",\n    color: \"inherit\",\n};\n\nconst GROUPED_STYLES = {\n    border: [\n        \"border-top-width\",\n        \"border-right-width\",\n        \"border-bottom-width\",\n        \"border-left-width\",\n        \"border-top-style\",\n        \"border-right-style\",\n        \"border-bottom-style\",\n        \"border-left-style\",\n        \"border-top-color\",\n        \"border-right-color\",\n        \"border-bottom-color\",\n        \"border-left-color\",\n    ],\n    padding: [\"padding-top\", \"padding-bottom\", \"padding-left\", \"padding-right\"],\n    margin: [\"margin-top\", \"margin-bottom\", \"margin-left\", \"margin-right\"],\n    \"border-radius\": [\n        \"border-top-left-radius\",\n        \"border-top-right-radius\",\n        \"border-bottom-right-radius\",\n        \"border-bottom-left-radius\",\n    ],\n};\n\n//--------------------------------------------------------------------------\n// Public\n//--------------------------------------------------------------------------\n\n/**\n * Convert snippets and mailing bodies to tables.\n *\n * @param {HTMLElement} element\n */\nexport function addTables(element) {\n    const isInBasicTheme = Boolean(element.querySelector(\".o_layout.o_basic_theme\"));\n    for (const snippet of element.querySelectorAll(\".o_mail_snippet_general, .o_layout\")) {\n        if (isInBasicTheme) {\n            for (const [property, value] of Object.entries(BASIC_THEME_TABLE_STYLES)) {\n                snippet.style.setProperty(property, value);\n            }\n        }\n        // Convert all snippets and the mailing itself into table > tr > td\n        const table = _createTable(snippet.attributes);\n\n        const row = document.createElement(\"tr\");\n        let col = document.createElement(\"td\");\n        row.appendChild(col);\n        if (snippet.classList.contains(\"o_basic_theme\")) {\n            const div = document.createElement(\"div\");\n            div.classList.add(\"o_apple_wrapper_padding\");\n            col.appendChild(div);\n            col = div;\n            const style = document.createElement(\"style\");\n            // We create a nested media query because it's only supported by a\n            // handful of clients, including Apple Mail, and we actually only\n            // want this for Apple Mail.\n            const padding = \"34px\"; // This is what's needed to align the content with Apple Mail's header.\n            style.textContent =\n                `@media{@media{.o_basic_theme div.o_apple_wrapper_padding{padding:${snippet.style.padding};}}}` +\n                `@media(min-width:737px){@media{@media{.o_basic_theme div.o_apple_wrapper_padding{padding-left:${padding};}}}}`;\n            div.before(style);\n        }\n        table.appendChild(row);\n\n        for (const child of [...snippet.childNodes]) {\n            col.appendChild(child);\n        }\n        snippet.before(table);\n        snippet.remove();\n\n        // If snippet doesn't have a table as child, wrap its contents in one.\n        const childTables = [...col.children].filter((child) => child.nodeName === \"TABLE\");\n        if (!childTables.length) {\n            const tableB = _createTable();\n            const rowB = document.createElement(\"tr\");\n            const colB = document.createElement(\"td\");\n\n            rowB.appendChild(colB);\n            tableB.appendChild(rowB);\n            for (const child of [...col.childNodes]) {\n                colB.appendChild(child);\n            }\n            col.appendChild(tableB);\n        }\n    }\n}\n/**\n * Convert CSS display for attachment link to real image.\n * Without this post process, the display depends on the CSS and the picture\n * does not appear when we use the html without css (to send by email for e.g.)\n *\n * @param {HTMLElement} element\n */\nfunction attachmentThumbnailToLinkImg(element) {\n    const links = [\n        ...element.querySelectorAll(`a[href*=\"/web/content/\"][data-mimetype]:empty`),\n    ].filter((link) => RE_WHITESPACE.test(link.textContent));\n    for (const link of links) {\n        const image = document.createElement(\"img\");\n        image.setAttribute(\n            \"src\",\n            _getStylePropertyValue(link, \"background-image\").replace(/(^url\\(['\"])|(['\"]\\)$)/g, \"\")\n        );\n        // Note: will trigger layout thrashing.\n        image.setAttribute(\"height\", Math.max(1, _getHeight(link)));\n        image.setAttribute(\"width\", Math.max(1, _getWidth(link)));\n        link.prepend(image);\n    }\n}\n/**\n * Convert Bootstrap rows and columns to actual tables.\n *\n * Note: Because of the limited support of media queries in emails, this doesn't\n * support the mixing and matching of column options (e.g., \"col-4 col-sm-6\" and\n * \"col col-4\" aren't supported).\n *\n * @param {Element} element\n */\nexport function bootstrapToTable(element) {\n    // First give all rows in columns a separate container parent.\n    for (const rowInColumn of [...element.querySelectorAll(\".row\")].filter((row) =>\n        RE_COL_MATCH.test(row.parentElement.className)\n    )) {\n        const parentColumn = rowInColumn.parentElement;\n        const previous = rowInColumn.previousElementSibling;\n        if (previous && previous.classList.contains(\"o_fake_table\")) {\n            // If a container was already created there, append to it.\n            previous.append(rowInColumn);\n        } else {\n            _wrap(rowInColumn, \"div\", \"o_fake_table\");\n        }\n        // Bootstrap rows have negative left and right margins, which are not\n        // supported by GMail and Outlook. Add up the padding of the column with\n        // the negative margin of the row to get the correct padding.\n        const rowStyle = getComputedStyle(rowInColumn);\n        const columnStyle = getComputedStyle(parentColumn);\n        for (const side of [\"left\", \"right\"]) {\n            const negativeMargin = +rowStyle[`margin-${side}`].replace(\"px\", \"\");\n            const columnPadding = +columnStyle[`padding-${side}`].replace(\"px\", \"\");\n            if (negativeMargin < 0 && columnPadding >= Math.abs(negativeMargin)) {\n                parentColumn.style[`padding-${side}`] = `${columnPadding + negativeMargin}px`;\n                rowInColumn.style[`margin-${side}`] = 0;\n            }\n        }\n    }\n\n    // These containers from the mass mailing masonry snippet require full\n    // height contents, which is only possible if the table itself has a set\n    // height. We also need to restyle it because of the change in structure.\n    for (const masonryTopInnerContainer of element.querySelectorAll(\n        \".s_masonry_block > .container\"\n    )) {\n        masonryTopInnerContainer.style.setProperty(\"height\", \"100%\");\n    }\n    for (const masonryGrid of element.querySelectorAll(\".o_masonry_grid_container\")) {\n        masonryGrid.style.setProperty(\"padding\", 0);\n        for (const fakeTable of [...masonryGrid.children].filter((c) =>\n            c.classList.contains(\"o_fake_table\")\n        )) {\n            fakeTable.style.setProperty(\"height\", _getHeight(fakeTable) + \"px\");\n        }\n    }\n    for (const masonryRow of element.querySelectorAll(\n        \".o_masonry_grid_container > .o_fake_table > .row.h-100\"\n    )) {\n        masonryRow.style.removeProperty(\"height\");\n        masonryRow.parentElement.style.setProperty(\"height\", \"100%\");\n    }\n\n    const containers = element.querySelectorAll(\".container, .container-fluid, .o_fake_table\");\n    // Capture the widths of the containers before manipulating it.\n    for (const container of containers) {\n        container.setAttribute(\"o-temp-width\", _getWidth(container));\n    }\n    // Now convert all containers with rows to tables.\n    for (const container of [...containers].filter((n) =>\n        [...n.children].some((c) => c.classList.contains(\"row\"))\n    )) {\n        // The width of the table was stored in a temporary attribute. Fetch it\n        // for use in `_applyColspan` and remove the attribute at the end.\n        const containerWidth = parseFloat(container.getAttribute(\"o-temp-width\"));\n\n        // TABLE\n        const table = _createTable(container.attributes);\n        for (const child of [...container.childNodes]) {\n            table.append(child);\n        }\n        table.classList.remove(\"container\", \"container-fluid\", \"o_fake_table\");\n        if (!table.className) {\n            table.removeAttribute(\"class\");\n        }\n        container.before(table);\n        container.remove();\n\n        // ROWS\n        // First give all siblings of rows a separate row/col parent combo.\n        for (const row of [...table.children].filter(\n            (child) => isBlock(child) && !child.classList.contains(\"row\")\n        )) {\n            const newCol = _wrap(row, \"div\", \"col-12\");\n            _wrap(newCol, \"div\", \"row\");\n        }\n\n        for (const bootstrapRow of [...table.children].filter((c) => c.classList.contains(\"row\"))) {\n            const tr = document.createElement(\"tr\");\n            for (const attr of bootstrapRow.attributes) {\n                tr.setAttribute(attr.name, attr.value);\n            }\n            tr.classList.remove(\"row\");\n            if (!tr.className) {\n                tr.removeAttribute(\"class\");\n            }\n            for (const child of [...bootstrapRow.childNodes]) {\n                tr.append(child);\n            }\n            bootstrapRow.before(tr);\n            bootstrapRow.remove();\n\n            // COLUMNS\n            const bootstrapColumns = [...tr.children].filter(\n                (column) => column.className && column.className.match(RE_COL_MATCH)\n            );\n\n            // 1. Replace generic \"col\" classes with specific \"col-n\", computed\n            //    by sharing the available space between them.\n            const flexColumns = bootstrapColumns.filter(\n                (column) => !/\\d/.test(column.className.match(RE_COL_MATCH)[0] || \"0\")\n            );\n            const colTotalSize = bootstrapColumns\n                .map((child) => _getColumnSize(child) + _getColumnOffsetSize(child))\n                .reduce((a, b) => a + b, 0);\n            const colSize = Math.max(1, Math.round((12 - colTotalSize) / flexColumns.length));\n            for (const flexColumn of flexColumns) {\n                flexColumn.classList.remove(flexColumn.className.match(RE_COL_MATCH)[0].trim());\n                flexColumn.classList.add(`col-${colSize}`);\n            }\n\n            // 2. Create and fill up the row(s) with grid(s).\n            // Create new, empty columns for column offsets.\n            let columnIndex = 0;\n            for (const bootstrapColumn of [...bootstrapColumns]) {\n                const offsetSize = _getColumnOffsetSize(bootstrapColumn);\n                if (offsetSize) {\n                    const newColumn = document.createElement(\"div\");\n                    newColumn.classList.add(`col-${offsetSize}`);\n                    bootstrapColumn.classList.remove(\n                        bootstrapColumn.className.match(RE_OFFSET_MATCH)[0].trim()\n                    );\n                    bootstrapColumn.before(newColumn);\n                    bootstrapColumns.splice(columnIndex, 0, newColumn);\n                    columnIndex++;\n                }\n                columnIndex++;\n            }\n            let grid = _createColumnGrid();\n            let gridIndex = 0;\n            let currentRow = tr.cloneNode();\n            tr.after(currentRow);\n            let currentCol;\n            columnIndex = 0;\n            for (const bootstrapColumn of bootstrapColumns) {\n                const columnSize = _getColumnSize(bootstrapColumn);\n                if (gridIndex + columnSize < 12) {\n                    currentCol = grid[gridIndex];\n                    _applyColspan(currentCol, columnSize, containerWidth);\n                    gridIndex += columnSize;\n                    if (columnIndex === bootstrapColumns.length - 1) {\n                        // We handled all the columns but there is still space\n                        // in the row. Insert the columns and fill the row.\n                        _applyColspan(grid[gridIndex], 12 - gridIndex, containerWidth);\n                        currentRow.append(...grid.filter((td) => td.getAttribute(\"colspan\")));\n                    }\n                } else if (gridIndex + columnSize === 12) {\n                    // Finish the row.\n                    currentCol = grid[gridIndex];\n                    _applyColspan(currentCol, columnSize, containerWidth);\n                    currentRow.append(...grid.filter((td) => td.getAttribute(\"colspan\")));\n                    if (columnIndex !== bootstrapColumns.length - 1) {\n                        // The row was filled before we handled all of its\n                        // columns. Create a new one and start again from there.\n                        const previousRow = currentRow;\n                        currentRow = currentRow.cloneNode();\n                        previousRow.after(currentRow);\n                        grid = _createColumnGrid();\n                        gridIndex = 0;\n                    }\n                } else {\n                    // Fill the row with what was in the grid before it\n                    // overflowed.\n                    _applyColspan(grid[gridIndex], 12 - gridIndex, containerWidth);\n                    currentRow.append(...grid.filter((td) => td.getAttribute(\"colspan\")));\n                    // Start a new row that starts with the current col.\n                    const previousRow = currentRow;\n                    currentRow = currentRow.cloneNode();\n                    previousRow.after(currentRow);\n                    grid = _createColumnGrid();\n                    currentCol = grid[0];\n                    _applyColspan(currentCol, columnSize, containerWidth);\n                    gridIndex = columnSize;\n                    if (columnIndex === bootstrapColumns.length - 1 && gridIndex < 12) {\n                        // We handled all the columns but there is still space\n                        // in the row. Insert the columns and fill the row.\n                        _applyColspan(grid[gridIndex], 12 - gridIndex, containerWidth);\n                        currentRow.append(...grid.filter((td) => td.getAttribute(\"colspan\")));\n                    }\n                }\n                if (currentCol) {\n                    for (const attr of bootstrapColumn.attributes) {\n                        if (attr.name !== \"colspan\") {\n                            currentCol.setAttribute(attr.name, attr.value);\n                        }\n                    }\n                    const colMatch = bootstrapColumn.className.match(RE_COL_MATCH);\n                    currentCol.classList.remove(colMatch[0].trim());\n                    if (!currentCol.className) {\n                        currentCol.removeAttribute(\"class\");\n                    }\n                    for (const child of [...bootstrapColumn.childNodes]) {\n                        currentCol.append(child);\n                    }\n                    // Adapt width to colspan.\n                    _applyColspan(currentCol, +currentCol.getAttribute(\"colspan\"), containerWidth);\n                }\n                columnIndex++;\n            }\n            tr.remove(); // row was cloned and inserted already\n        }\n    }\n    for (const table of element.querySelectorAll(\"table\")) {\n        table.removeAttribute(\"o-temp-width\");\n    }\n    // Merge tables in tds into one common table, each in its own row.\n    const tds = [...element.querySelectorAll(\"td\")]\n        .filter(\n            (td) =>\n                td.children.length > 1 &&\n                [...td.children].every((child) => child.nodeName === \"TABLE\")\n        )\n        .reverse();\n    for (const td of tds) {\n        const table = _createTable();\n        const trs = [...td.children]\n            .map((child) => _wrap(child, \"td\"))\n            .map((wrappedChild) => _wrap(wrappedChild, \"tr\"));\n        trs[0].before(table);\n        table.append(...trs);\n    }\n}\n/**\n * Convert Bootstrap cards to table structures.\n *\n * @param {Element} element\n */\nexport function cardToTable(element) {\n    for (const card of element.querySelectorAll(\".card\")) {\n        const table = _createTable(card.attributes);\n        table.style.removeProperty(\"overflow\");\n        const cardImgTopSuperRows = [];\n        for (const child of [...card.childNodes]) {\n            const row = document.createElement(\"tr\");\n            const col = document.createElement(\"td\");\n            if (![\"IMG\", \"A\"].includes(child.nodeName) && isBlock(child)) {\n                for (const attr of child.attributes) {\n                    col.setAttribute(attr.name, attr.value);\n                }\n                for (const descendant of [...child.childNodes]) {\n                    col.append(descendant);\n                }\n                child.remove();\n            } else if (child.nodeType === Node.TEXT_NODE) {\n                if (child.textContent.replace(RE_WHITESPACE, \"\").length) {\n                    col.append(child);\n                } else {\n                    continue;\n                }\n            } else {\n                col.append(child);\n            }\n            const subTable = _createTable();\n            const superRow = document.createElement(\"tr\");\n            const superCol = document.createElement(\"td\");\n            row.append(col);\n            subTable.append(row);\n            superCol.append(subTable);\n            superRow.append(superCol);\n            table.append(superRow);\n            if (child.nodeType === Node.ELEMENT_NODE) {\n                const hasImgTop = [child, ...child.querySelectorAll(\".card-img-top\")].some(\n                    (node) =>\n                        node.classList &&\n                        node.classList.contains(\"card-img-top\") &&\n                        node.closest &&\n                        node.closest(\".card\") === table\n                );\n                if (hasImgTop) {\n                    // Collect .card-img-top superRows to manipulate their heights.\n                    cardImgTopSuperRows.push(superRow);\n                }\n            }\n        }\n        // We expect successive .card-img-top to have the same height so the\n        // bodies of the cards are aligned. This achieves that without flexboxes\n        // by forcing the height of the smallest card:\n        const smallestCardImgRow = Math.min(\n            0,\n            ...cardImgTopSuperRows.map((row) => row.clientHeight)\n        );\n        for (const row of cardImgTopSuperRows) {\n            row.style.height = smallestCardImgRow + \"px\";\n        }\n        card.before(table);\n        card.remove();\n    }\n}\n/**\n * Convert CSS style to inline style (leave the classes on elements but forces\n * the style they give as inline style).\n *\n * @param {HTMLElement} $element\n * @param {Object} cssRules\n */\nexport function classToStyle(element, cssRules) {\n    const writes = [];\n    const nodeToRules = new Map();\n    const rulesToProcess = [];\n    for (const rule of cssRules) {\n        const nodes = element.querySelectorAll(rule.selector);\n        if (nodes.length) {\n            rulesToProcess.push(rule);\n        }\n        for (const node of nodes) {\n            const nodeRules = nodeToRules.get(node);\n            if (!nodeRules) {\n                nodeToRules.set(node, [rule]);\n            } else {\n                nodeRules.push(rule);\n            }\n        }\n    }\n    _computeStyleAndSpecificityOnRules(rulesToProcess);\n    for (const rules of nodeToRules.values()) {\n        rules.sort((a, b) => a.specificity - b.specificity);\n    }\n\n    for (const node of nodeToRules.keys()) {\n        const nodeRules = nodeToRules.get(node);\n        const css = nodeRules ? _getMatchedCSSRules(node, nodeRules) : {};\n        // Flexbox\n        for (const styleName of node.style) {\n            if (styleName.includes(\"flex\") || `${node.style[styleName]}`.includes(\"flex\")) {\n                writes.push(() => {\n                    node.style[styleName] = \"\";\n                });\n            }\n        }\n\n        // Do not apply css that would override inline styles (which are prioritary).\n        let style = node.getAttribute(\"style\") || \"\";\n        // Outlook doesn't support inline !important\n        style = style.replace(/!important/g, \"\");\n        for (const [key, value] of Object.entries(css)) {\n            if (!new RegExp(`(^|;)\\\\s*${key}[ :]`).test(style)) {\n                style = `${key}:${value};${style}`;\n            }\n        }\n        style = correctBorderAttributes(style);\n        if (Object.keys(style || {}).length === 0) {\n            writes.push(() => {\n                node.removeAttribute(\"style\");\n            });\n        } else {\n            writes.push(() => {\n                node.setAttribute(\"style\", style);\n                if (node.style.width) {\n                    node.setAttribute(\"width\", node.style.width.replace(\"px\", \"\").trim());\n                }\n            });\n        }\n\n        if (node.nodeName === \"IMG\") {\n            writes.push(() => {\n                // Media list images should not have an inline height\n                if (node.classList.contains(\"s_media_list_img\")) {\n                    node.style.removeProperty(\"height\");\n                }\n                // Protect aspect ratio when resizing in mobile.\n                if (\n                    node.style.getPropertyValue(\"width\") === \"100%\" &&\n                    node.style.getPropertyValue(\"object-fit\") === \"\"\n                ) {\n                    node.style.setProperty(\"object-fit\", \"cover\");\n                }\n            });\n        }\n        // Apple Mail\n        if (node.nodeName === \"TD\" && !node.childNodes.length) {\n            // Append non-breaking spaces to empty table cells.\n            writes.push(() => {\n                node.appendChild(document.createTextNode(\"\\u00A0\"));\n            });\n        }\n        // Outlook\n        if (\n            node.nodeName === \"A\" &&\n            node.classList.contains(\"btn\") &&\n            !node.classList.contains(\"btn-link\") &&\n            !node.children.length\n        ) {\n            writes.push(() => {\n                node.before(\n                    createMso(`<table align=\"center\" border=\"0\"\n                    role=\"presentation\" cellpadding=\"0\" cellspacing=\"0\"\n                    style=\"border-radius: 6px; border-collapse: separate !important;\">\n                        <tbody>\n                            <tr>\n                                <td style=\"${node.style.cssText\n                                    .replace(RE_PADDING_MATCH, \"\")\n                                    .replaceAll('\"', \"&quot;\")}\" ${\n                        node.parentElement.style.textAlign === \"center\" ? 'align=\"center\" ' : \"\"\n                    }bgcolor=\"${blendColors(node.style.backgroundColor)}\">\n                    `)\n                );\n                node.after(\n                    createMso(`</td>\n                        </tr>\n                    </tbody>\n                </table>`)\n                );\n            });\n        } else if (\n            node.nodeName === \"IMG\" &&\n            node.classList.contains(\"mx-auto\") &&\n            node.classList.contains(\"d-block\")\n        ) {\n            writes.push(() => {\n                _wrap(node, \"p\", \"o_outlook_hack\", \"text-align:center;margin:0\");\n            });\n        }\n\n        // Compute dynamic styles (var, calc).\n        writes.push(() => {\n            let computedStyle;\n            for (const styleName of node.style) {\n                const styleValue = node.style.getPropertyValue(styleName);\n                if (styleValue.includes(\"var(\") || styleValue.includes(\"calc(\")) {\n                    computedStyle = computedStyle || getComputedStyle(node);\n                    const prop = styleValue.includes(\"var(\")\n                        ? styleValue.replace(/var\\((.*)\\)/, \"$1\")\n                        : styleName;\n                    const value =\n                        computedStyle.getPropertyValue(prop) ||\n                        computedStyle.getPropertyValue(styleName);\n                    node.style.setProperty(styleName, value);\n                }\n            }\n        });\n\n        // Fix inheritance of font properties on Outlook.\n        writes.push(() => {\n            const propsToConvert = FONT_PROPERTIES_TO_INHERIT.filter(\n                (prop) => node.style[prop] === \"inherit\"\n            );\n            if (propsToConvert.length) {\n                const computedStyle = getComputedStyle(node);\n                for (const prop of propsToConvert) {\n                    node.style.setProperty(prop, computedStyle[prop]);\n                }\n            }\n        });\n\n        const matchedBlacklistRules = nodeRules?.filter((rule) =>\n            CONVERT_INLINE_BLACKLIST_CLASSES.some(\n                (cls) => rule.selector.includes(cls) && node.classList.contains(cls)\n            )\n        );\n\n        const blacklistedStyles = {};\n        for (const rule of matchedBlacklistRules) {\n            for (const [key, value] of Object.entries(rule.style)) {\n                if (\n                    !blacklistedStyles[key] ||\n                    !blacklistedStyles[key].includes(\"important\") ||\n                    value.includes(\"important\")\n                ) {\n                    blacklistedStyles[key] = value;\n                }\n            }\n        }\n\n        for (const [key, value] of Object.entries(blacklistedStyles)) {\n            if (value && value.endsWith(\"important\")) {\n                blacklistedStyles[key] = value.replace(/\\s*!important\\s*$/, \"\");\n            }\n        }\n\n        // Find styles to remove if they are from a blacklisted class and match\n        // existing styles.\n        const stylesToRemove = Object.fromEntries(\n            Object.entries(css).filter(([key, value]) => blacklistedStyles[key] === value)\n        );\n        // Remove style from blacklisted classes.\n        writes.push(() => {\n            for (const [key] of Object.entries(stylesToRemove)) {\n                if (node.style[key]) {\n                    node.style.removeProperty(key);\n                }\n            }\n        });\n    }\n    writes.forEach((fn) => fn());\n}\n/**\n * Add styles to all table rows and columns, that are necessary for them to be\n * responsive. This only works if columns have a max-width so the styles are\n * only applied to columns where that is the case.\n *\n * @param {Element} element\n */\nfunction enforceTablesResponsivity(element) {\n    // Trying this: https://www.litmus.com/blog/mobile-responsive-email-stacking/\n    const trs = [...element.querySelectorAll(\".o_mail_wrapper tr\")]\n        .filter((tr) => [...tr.children].some((td) => td.classList.contains(\"o_converted_col\")))\n        .reverse();\n    for (const tr of trs) {\n        const commonTable = _createTable();\n        commonTable.style.height = \"100%\";\n        const commonTr = document.createElement(\"tr\");\n        const commonTd = document.createElement(\"td\");\n        commonTr.appendChild(commonTd);\n        commonTable.appendChild(commonTr);\n        const tds = [...tr.children].filter((child) => child.nodeName === \"TD\");\n        let index = 0;\n        for (const td of tds) {\n            const width = td.style.maxWidth;\n            const div = document.createElement(\"div\");\n            div.style.display = \"inline-block\";\n            div.style.verticalAlign = \"top\";\n            div.classList.add(\"o_stacking_wrapper\");\n            commonTd.appendChild(div);\n            const newTable = _createTable();\n            newTable.style.width = width;\n            newTable.classList.add(\"o_stacking_wrapper\");\n            div.appendChild(newTable);\n            const newTr = document.createElement(\"tr\");\n            newTable.appendChild(newTr);\n            newTr.appendChild(td);\n            td.style.width = \"100%\";\n            td.removeAttribute(\"width\");\n            if (index === 0) {\n                div.before(\n                    createMso(`\n                    <table cellpadding=\"0\" cellspacing=\"0\" border=\"0\" role=\"presentation\" style=\"width: 100%;\">\n                        <tr>\n                            <td valign=\"top\" style=\"width: ${width};\">`)\n                );\n            } else {\n                div.before(createMso(`</td><td valign=\"top\" style=\"width: ${width};\">`));\n            }\n            if (index === tds.length - 1) {\n                div.after(createMso(`</td></tr></table>`));\n            }\n            index++;\n        }\n        const topTd = document.createElement(\"td\");\n        topTd.appendChild(commonTable);\n        tr.prepend(topTd);\n    }\n}\n// Masonry has crazy nested tables that require some extra treatment.\nfunction handleMasonry(element) {\n    const masonryTrs = element.querySelectorAll(\".s_masonry_block tr\");\n    for (const tr of masonryTrs) {\n        const height = _getHeight(tr);\n        const tds = [...tr.children].filter((child) => child.nodeName === \"TD\");\n        const tdsWithTable = tds.filter((td) =>\n            [...td.children].some((child) => child.nodeName === \"TABLE\")\n        );\n        if (tdsWithTable.length) {\n            // TODO: this seems a duplicate of the other o_desktop_h100 set below.\n            // Set the cells' heights to fill their parents.\n            for (const tdWithTable of tdsWithTable) {\n                tdWithTable.classList.add(\"o_desktop_h100\");\n                tdWithTable.style.setProperty(\"height\", \"100%\");\n            }\n            // We also have to set the same height on the cells' sibling TDs.\n            tds.forEach((td) => td.style.setProperty(\"height\", height + \"px\"));\n        }\n        // Sometimes Masonry declares rows with a height of 100% but with\n        // columns that overfit the grid. In these cases, we split the rows into\n        // multiple rows so we need to adapt their heights for them to be\n        // divided equally.\n        const trSiblings = [...tr.parentElement.children].filter(\n            (child) => child.nodeName === \"TR\"\n        );\n        if (\n            trSiblings.length > 1 &&\n            (tr.classList.contains(\"h-100\") || tr.style.getPropertyValue(\"height\") === \"100%\")\n        ) {\n            tr.style.setProperty(\"height\", `${_getHeight(tr.parentElement) / trSiblings.length}px`);\n        }\n    }\n    for (const tr of masonryTrs) {\n        const height = tr.style.height.includes(\"px\")\n            ? parseFloat(tr.style.height.replace(\"px\", \"\").trim())\n            : _getHeight(tr);\n        tr.closest(\"table\").classList.add(\"o_desktop_h100\");\n        tr.classList.add(\"o_desktop_h100\");\n        for (const td of [...tr.children].filter((child) => child.nodeName === \"TD\")) {\n            td.classList.add(\"o_desktop_h100\");\n            td.style.setProperty(\"height\", \"100%\");\n            const childrenNames = [...td.children].map((child) => child.nodeName);\n            if (!childrenNames.includes(\"TABLE\")) {\n                // Hack that makes vertical-align possible within an inline-block.\n                const wrapper = document.createElement(\"div\");\n                wrapper.style.setProperty(\"display\", \"inline-block\");\n                wrapper.style.setProperty(\"width\", \"100%\");\n                // Transfer color to wrapper for Outlook on MacOS/iOS.\n                const tdStyle = getComputedStyle(td);\n                wrapper.style.setProperty(\"color\", tdStyle.color);\n                const firstNonCommentChild = [...td.childNodes].find(\n                    (child) => child.nodeType !== Node.COMMENT_NODE\n                );\n                let anchor;\n                if (firstNonCommentChild) {\n                    anchor = getAdjacentPreviousSiblings(firstNonCommentChild)\n                        .filter((sib) => sib.nodeType !== Node.TEXT_NODE)\n                        .shift();\n                }\n                for (const child of [...td.childNodes].filter(\n                    (child) => child.nodeType !== Node.COMMENT_NODE\n                )) {\n                    wrapper.append(child);\n                }\n                anchor ? anchor.after(wrapper) : td.append(wrapper);\n                const centeringSpan = document.createElement(\"span\");\n                centeringSpan.style.setProperty(\"height\", \"100%\");\n                centeringSpan.style.setProperty(\"display\", \"inline-block\");\n                centeringSpan.style.setProperty(\"vertical-align\", \"middle\");\n                td.prepend(centeringSpan);\n                // Height on cells should be applied in pixels.\n                if (td.style.height.includes(\"%\")) {\n                    const newHeight =\n                        (height * parseFloat(td.style.height.replace(\"%\").trim())) / 100;\n                    td.style.setProperty(\"height\", newHeight + \"px\");\n                    // Spread height down for responsivity\n                    td.style.setProperty(\"max-height\", newHeight + \"px\");\n                    wrapper.style.setProperty(\"max-height\", newHeight + \"px\");\n                    if (\n                        wrapper.childElementCount === 1 &&\n                        wrapper.firstElementChild.nodeName === \"IMG\" &&\n                        wrapper.firstElementChild.style.height === \"100%\"\n                    ) {\n                        wrapper.firstElementChild.style.setProperty(\"max-height\", newHeight + \"px\");\n                    }\n                }\n            }\n        }\n    }\n}\n/**\n * Modify the styles of images so they are responsive.\n *\n * @param {Element} element\n */\nfunction enforceImagesResponsivity(element) {\n    // Images with 100% height in cells should preserve that height and the\n    // height of the row should be applied to the cell.\n    for (const image of element.querySelectorAll(\"td > img\")) {\n        const td = image.parentElement;\n        if (\n            td.childElementCount === 1 &&\n            (image.classList.contains(\"h-100\") ||\n                _getStylePropertyValue(image, \"height\") === \"100%\")\n        ) {\n            td.style.setProperty(\"height\", _getHeight(td.parentElement) + \"px\");\n            image.style.setProperty(\"height\", \"100%\");\n        }\n    }\n    // Remove the height attribute in card images so they can resize\n    // responsively, but leave it for Outlook.\n    for (const image of element.querySelectorAll('img[width=\"100%\"][height]')) {\n        image.before(createMso(image.outerHTML));\n        image.classList.add(\"mso-hide\");\n        image.removeAttribute(\"height\");\n    }\n}\n/**\n * Convert the contents of an element area into content\n * that is widely compatible with email clients.\n *\n * @param {HTMLElement} element\n * @param {Object[]} cssRules Array<{selector: string;\n *                            style: {[styleName]: string};\n *                            specificity: number;}>\n */\nexport async function toInline(element, cssRules) {\n    // Fix card-img-top heights (must happen before we transform everything).\n    for (const imgTop of element.querySelectorAll(\".card-img-top\")) {\n        imgTop.style.setProperty(\"height\", _getHeight(imgTop) + \"px\");\n    }\n\n    // Fix empty element heights to be always visible as they might have borders\n    // (used as separation) and can be rendered with height 0px.\n    // like having empty div with % height and display inline-block.\n    for (const el of element.querySelectorAll(\".o_not_editable[class*='border-']:empty\")) {\n        el.style.height = getComputedStyle(el).height;\n    }\n\n    attachmentThumbnailToLinkImg(element);\n    fontToImg(element);\n    await svgToPng(element);\n\n    // Fix img-fluid for Outlook.\n    for (const image of element.querySelectorAll(\"img.img-fluid\")) {\n        const width = _getWidth(image);\n        const clone = image.cloneNode();\n        clone.setAttribute(\"width\", width);\n        clone.style.setProperty(\"width\", width + \"px\");\n        clone.style.removeProperty(\"max-width\");\n        image.before(createMso(clone.outerHTML));\n        _hideForOutlook(image);\n    }\n\n    classToStyle(element, cssRules);\n    bootstrapToTable(element);\n    cardToTable(element);\n    listGroupToTable(element);\n    addTables(element);\n    handleMasonry(element);\n    const rootFontSizeProperty = getComputedStyle(element.ownerDocument.documentElement).fontSize;\n    const rootFontSize = parseFloat(rootFontSizeProperty.replace(/[^\\d.]/g, \"\"));\n    normalizeRem(element, rootFontSize);\n    enforceImagesResponsivity(element);\n    enforceTablesResponsivity(element);\n    flattenBackgroundImages(element);\n    formatTables(element);\n    normalizeColors(element);\n    responsiveToStaticForOutlook(element);\n    // Fix Outlook image rendering bug.\n    for (const attributeName of [\"width\", \"height\"]) {\n        const images = element.querySelectorAll(\"img\");\n        for (const image of images) {\n            if (image.style[attributeName] !== \"auto\") {\n                const value =\n                    image.getAttribute(attributeName) ||\n                    (attributeName === \"height\" && image.offsetHeight) ||\n                    (attributeName === \"width\" ? _getWidth(image) : _getHeight(image));\n                if (value) {\n                    image.setAttribute(attributeName, value);\n                    image.style.setProperty(attributeName, value + \"px\");\n                }\n            }\n        }\n    }\n    // Fix mx-auto on images in table cells.\n    for (const centeredImage of element.querySelectorAll(\"td > img.mx-auto\")) {\n        if (centeredImage.parentElement.children.length === 1) {\n            centeredImage.parentElement.style.setProperty(\"text-align\", \"center\");\n        }\n    }\n\n    // Remove contenteditable attributes\n    [element, ...element.querySelectorAll(\"[contenteditable]\")].forEach((node) =>\n        node.removeAttribute(\"contenteditable\")\n    );\n\n    // Hide replaced cells on Outlook\n    element.querySelectorAll(\".mso-hide\").forEach(_hideForOutlook);\n\n    // Replace double quotes in font-family styles with simple quotes (and\n    // simply remove these styles from images).\n    element\n        .querySelectorAll(\"[style*=font-family]\")\n        .forEach((n) =>\n            n.nodeName === \"IMG\"\n                ? n.style.removeProperty(\"font-family\")\n                : n.setAttribute(\"style\", n.getAttribute(\"style\").replaceAll('\"', \"'\"))\n        );\n\n    element\n        .querySelectorAll(\".o_converted_col\")\n        .forEach((node) => node.classList.remove(\"o_converted_col\"));\n}\n/**\n * Take all elements with a `background-image` style and convert them to `vml`\n * for Outlook. Also remove data-bg-src to avoid Gmail cutting the html.\n *\n * @param {Element} element\n */\nfunction flattenBackgroundImages(element) {\n    const backgroundImages = [...element.querySelectorAll(\"*[style*=background-image]\")]\n        .filter((el) => !el.closest(\".mso-hide\"))\n        .reverse();\n    for (const backgroundImage of backgroundImages) {\n        const vml = _backgroundImageToVml(backgroundImage);\n        if (vml) {\n            // Put the Outlook version after the original one in an mso conditional.\n            backgroundImage.after(createMso(vml));\n            // Hide the original element for Outlook.\n            backgroundImage.classList.add(\"mso-hide\");\n        }\n        if (backgroundImage.hasAttribute(\"data-bg-src\")) {\n            // Remove data-bg-src as it is not needed for email rendering and\n            // can cause Gmail to cut the email prematurely if the attributes\n            // contain an image in the form of a long base64 string.\n            backgroundImage.removeAttribute(\"data-bg-src\");\n        }\n    }\n}\n/**\n * Convert font icons to images.\n *\n * @param {HTMLElement} element - the element in which the font icons have to be\n *                           converted to images\n */\nfunction fontToImg(element) {\n    const { fonts } = odoo.loader.modules.get(\"@html_editor/utils/fonts\");\n\n    for (const font of element.querySelectorAll(\".fa\")) {\n        let icon, content;\n        fonts.fontIcons.find((fontIcon) =>\n            fonts.getCssSelectors(fontIcon.parser).find((data) => {\n                if (font.matches(data.selector.replace(/::?before/g, \"\"))) {\n                    icon = data.names[0].split(\"-\").shift();\n                    content = data.css.match(/content:\\s*['\"]?(.)['\"]?/)[1];\n                    return true;\n                }\n            })\n        );\n        if (content) {\n            const color = _getStylePropertyValue(font, \"color\").replace(/\\s/g, \"\");\n            let backgroundColoredElement = font;\n            let bg, isTransparent;\n            do {\n                bg = _getStylePropertyValue(backgroundColoredElement, \"background-color\").replace(\n                    /\\s/g,\n                    \"\"\n                );\n                isTransparent = bg === \"transparent\" || bg === \"rgba(0,0,0,0)\";\n                backgroundColoredElement = backgroundColoredElement.parentElement;\n            } while (isTransparent && backgroundColoredElement);\n            if (bg === \"rgba(0,0,0,0)\" && isTransparent) {\n                // default on white rather than black background since opacity\n                // is not supported.\n                bg = \"rgb(255,255,255)\";\n            }\n            const style = font.getAttribute(\"style\");\n            const width = _getWidth(font);\n            const height = _getHeight(font);\n            const lineHeight = _getStylePropertyValue(font, \"line-height\");\n            // Compute the padding.\n            // First get the dimensions of the icon itself (::before)\n            font.style.setProperty(\"height\", \"fit-content\");\n            font.style.setProperty(\"width\", \"fit-content\");\n            font.style.setProperty(\"line-height\", \"normal\");\n            const intrinsicWidth = _getWidth(font);\n            const intrinsicHeight = _getHeight(font);\n            const hPadding = width && intrinsicWidth && (width - intrinsicWidth) / 2;\n            const vPadding = height && intrinsicHeight && (height - intrinsicHeight) / 2;\n            let padding = \"\";\n            if (hPadding || vPadding) {\n                padding = vPadding ? vPadding + \"px \" : \"0 \";\n                padding += hPadding ? hPadding + \"px\" : \"0\";\n            }\n            const image = document.createElement(\"img\");\n            image.setAttribute(\"width\", intrinsicWidth);\n            image.setAttribute(\"height\", intrinsicHeight);\n            image.setAttribute(\n                \"src\",\n                `/mail/font_to_img/${content.charCodeAt(0)}/${encodeURIComponent(\n                    color\n                )}/${encodeURIComponent(bg)}/${Math.max(1, Math.round(intrinsicWidth))}x${Math.max(\n                    1,\n                    Math.round(intrinsicHeight)\n                )}`\n            );\n            image.setAttribute(\"data-class\", font.getAttribute(\"class\"));\n            image.setAttribute(\"data-style\", style);\n            image.setAttribute(\"style\", style);\n            image.style.setProperty(\"box-sizing\", \"border-box\"); // keep the fontawesome's dimensions\n            image.style.setProperty(\"line-height\", lineHeight);\n            image.style.setProperty(\"width\", intrinsicWidth + \"px\");\n            image.style.setProperty(\"height\", intrinsicHeight + \"px\");\n            image.style.setProperty(\"vertical-align\", \"unset\"); // undo Bootstrap's default (middle).\n            if (!padding) {\n                image.style.setProperty(\"margin\", _getStylePropertyValue(font, \"margin\"));\n            }\n            // For rounded images, apply the rounded border to a wrapper, make\n            // sure it doesn't get applied to the image itself so the image\n            // doesn't get cropped in the process.\n            const wrapper = document.createElement(\"span\");\n            wrapper.style.setProperty(\"display\", \"inline-block\");\n            wrapper.append(image);\n            font.before(wrapper);\n            if (font.classList.contains(\"mx-auto\")) {\n                wrapper.parentElement.style.textAlign = \"center\";\n            }\n            font.remove();\n            wrapper.style.setProperty(\"padding\", padding);\n            const wrapperWidth =\n                width +\n                [\"left\", \"right\"].reduce(\n                    (sum, side) =>\n                        sum +\n                        (+_getStylePropertyValue(image, `margin-${side}`).replace(\"px\", \"\") || 0),\n                    0\n                );\n            wrapper.style.setProperty(\"width\", wrapperWidth + \"px\");\n            wrapper.style.setProperty(\"height\", height + \"px\");\n            wrapper.style.setProperty(\"vertical-align\", \"text-bottom\");\n            wrapper.style.setProperty(\"background-color\", image.style.backgroundColor);\n            wrapper.setAttribute(\n                \"class\",\n                \"oe_unbreakable \" + // prevent sanitize from grouping image wrappers\n                    font\n                        .getAttribute(\"class\")\n                        .replace(new RegExp(\"(^|\\\\s+)\" + icon + \"(-[^\\\\s]+)?\", \"gi\"), \"\") // remove inline font-awsome style\n            );\n        } else {\n            font.remove();\n        }\n    }\n}\n/**\n * Format table styles so they display well in most mail clients. This implies\n * moving table paddings to its cells, adding tbody (with canceled styles) where\n * needed, and adding pixel heights to parents of elements with percent heights.\n *\n * @param {HTMLElement} element\n */\nexport function formatTables(element) {\n    const writes = [];\n    for (const table of element.querySelectorAll(\n        \"table.o_mail_snippet_general, .o_mail_snippet_general table\"\n    )) {\n        const tablePaddingTop = parseFloat(\n            _getStylePropertyValue(table, \"padding-top\").match(RE_PADDING)[1]\n        );\n        const tablePaddingRight = parseFloat(\n            _getStylePropertyValue(table, \"padding-right\").match(RE_PADDING)[1]\n        );\n        const tablePaddingBottom = parseFloat(\n            _getStylePropertyValue(table, \"padding-bottom\").match(RE_PADDING)[1]\n        );\n        const tablePaddingLeft = parseFloat(\n            _getStylePropertyValue(table, \"padding-left\").match(RE_PADDING)[1]\n        );\n        const rows = [...table.querySelectorAll(\"tr\")].filter(\n            (tr) => tr.closest(\"table\") === table\n        );\n        const columns = [...table.querySelectorAll(\"td\")].filter(\n            (td) => td.closest(\"table\") === table\n        );\n        for (const column of columns) {\n            const columnsInRow = [...column.closest(\"tr\").querySelectorAll(\"td\")].filter(\n                (td) => td.closest(\"table\") === table\n            );\n            const columnIndex = columnsInRow.findIndex((col) => col === column);\n            const rowIndex = rows.findIndex((row) => row === column.closest(\"tr\"));\n\n            if (!rowIndex) {\n                const match = _getStylePropertyValue(column, \"padding-top\").match(RE_PADDING);\n                const columnPaddingTop = match ? parseFloat(match[1]) : 0;\n                writes.push(() => {\n                    column.style[\"padding-top\"] = `${columnPaddingTop + tablePaddingTop}px`;\n                });\n            }\n            if (columnIndex === columnsInRow.length - 1) {\n                const match = _getStylePropertyValue(column, \"padding-right\").match(RE_PADDING);\n                const columnPaddingRight = match ? parseFloat(match[1]) : 0;\n                writes.push(() => {\n                    column.style[\"padding-right\"] = `${columnPaddingRight + tablePaddingRight}px`;\n                });\n            }\n            if (rowIndex === rows.length - 1) {\n                const match = _getStylePropertyValue(column, \"padding-bottom\").match(RE_PADDING);\n                const columnPaddingBottom = match ? parseFloat(match[1]) : 0;\n                writes.push(() => {\n                    column.style[\"padding-bottom\"] = `${\n                        columnPaddingBottom + tablePaddingBottom\n                    }px`;\n                });\n            }\n            if (!columnIndex) {\n                const match = _getStylePropertyValue(column, \"padding-left\").match(RE_PADDING);\n                const columnPaddingLeft = match ? parseFloat(match[1]) : 0;\n                writes.push(() => {\n                    column.style[\"padding-left\"] = `${columnPaddingLeft + tablePaddingLeft}px`;\n                });\n            }\n        }\n        writes.push(() => {\n            table.style.removeProperty(\"padding\");\n        });\n    }\n    writes.forEach((fn) => fn());\n    // Ensure a tbody in every table and cancel its default style.\n    for (const table of [...element.querySelectorAll(\"table\")].filter(\n        (n) => ![...n.children].some((c) => c.nodeName === \"TBODY\")\n    )) {\n        const contents = [...table.childNodes];\n        const tbody = document.createElement(\"tbody\");\n        tbody.style.setProperty(\"vertical-align\", \"top\");\n        table.prepend(tbody);\n        tbody.append(...contents);\n    }\n    // Children will only take 100% height if the parent has a height property.\n    for (const node of [...element.querySelectorAll(\"*\")].filter(\n        (n) =>\n            n.style &&\n            n.style.getPropertyValue(\"height\") === \"100%\" &&\n            (!n.parentElement.style.getPropertyValue(\"height\") ||\n                n.parentElement.style.getPropertyValue(\"height\").includes(\"%\"))\n    )) {\n        let parent = node.parentElement;\n        let height = parent.style.getPropertyValue(\"height\");\n        while (parent && height && height.includes(\"%\")) {\n            parent = parent.parentElement;\n            height = parent.style.getPropertyValue(\"height\");\n        }\n        if (parent) {\n            parent.style.setProperty(\"height\", parent.getBoundingClientRect().height);\n        }\n    }\n    // Align self and justify content don't work on table cells.\n    for (const cell of element.querySelectorAll(\"td\")) {\n        const alignSelf = cell.style.alignSelf;\n        const justifyContent = cell.style.justifyContent;\n        if (\n            alignSelf === \"start\" ||\n            justifyContent === \"start\" ||\n            justifyContent === \"flex-start\"\n        ) {\n            cell.style.verticalAlign = \"top\";\n        } else if (alignSelf === \"center\" || justifyContent === \"center\") {\n            const parentCell = cell.parentElement.closest(\"td\");\n            const parentTable = cell.closest(\"table\");\n            if (parentCell) {\n                parentTable.style.height = _getHeight(parentCell) + \"px\";\n            }\n            cell.style.verticalAlign = \"middle\";\n        } else if (\n            alignSelf === \"end\" ||\n            justifyContent === \"end\" ||\n            justifyContent === \"flex-end\"\n        ) {\n            cell.style.verticalAlign = \"bottom\";\n        }\n    }\n    // Align items doesn't work on table rows.\n    for (const row of element.querySelectorAll(\"tr\")) {\n        const alignItems = row.style.alignItems;\n        if (alignItems === \"flex-start\") {\n            row.style.verticalAlign = \"top\";\n        } else if (alignItems === \"center\") {\n            row.style.verticalAlign = \"middle\";\n        } else if (alignItems === \"flex-end\" || alignItems === \"baseline\") {\n            row.style.verticalAlign = \"bottom\";\n        } else if (alignItems === \"stretch\") {\n            const columns = [...row.querySelectorAll(\"td.o_converted_col\")];\n            if (columns.length > 1) {\n                const commonAncestor = commonParentGet(columns[0], columns[1]);\n                const biggestHeight = commonAncestor.clientHeight;\n                for (const column of columns) {\n                    column.style.height = biggestHeight + \"px\";\n                }\n            }\n        }\n    }\n    // Tables don't properly inherit certain styles from their ancestors in Outlook.\n    for (const table of element.querySelectorAll(\"table\")) {\n        const propsToConvert = FONT_PROPERTIES_TO_INHERIT.filter(\n            (prop) => table.style[prop] === \"inherit\" || !table.style[prop]\n        );\n        if (propsToConvert.length) {\n            for (const prop of propsToConvert) {\n                let ancestor = table;\n                while (ancestor && (!ancestor.style[prop] || ancestor.style[prop] === \"inherit\")) {\n                    ancestor = ancestor.parentElement;\n                }\n                if (ancestor) {\n                    table.style.setProperty(prop, ancestor.style[prop]);\n                }\n            }\n        }\n    }\n}\n/**\n * Parse through the given document's stylesheets, preprocess(*) them and return\n * the result as an array of objects, each containing a selector string , a\n * style object and a specificity number. Preprocessing involves grouping\n * whatever rules can be grouped together and precomputing their specificity so\n * as to sort them appropriately.\n *\n * @param {Document} doc\n * @returns {Object[]} Array<{selector: string;\n *                            style: {[styleName]: string};\n *                            specificity: number;}>\n */\nexport function getCSSRules(doc) {\n    const cssRules = [];\n    for (const sheet of [...doc.styleSheets, ...doc.adoptedStyleSheets]) {\n        // try...catch because browser may not able to enumerate rules for cross-domain sheets\n        let rules;\n        try {\n            rules = sheet.rules || sheet.cssRules;\n        } catch (e) {\n            console.log(\"Can't read the css rules of: \" + sheet.href, e);\n            continue;\n        }\n        for (const rule of rules || []) {\n            const subRules = [rule];\n            const conditionText = rule.conditionText;\n            const minWidthMatch = conditionText && conditionText.match(/\\(min-width *: *(\\d+)/);\n            const minWidth = minWidthMatch && +(minWidthMatch[1] || \"0\");\n            if (minWidth && minWidth >= 768) {\n                // Large min-width media queries should be included.\n                // eg., .container has a default max-width for all screens.\n                let mediaRules;\n                try {\n                    mediaRules = rule.rules || rule.cssRules;\n                    subRules.push(...mediaRules);\n                } catch (e) {\n                    console.log(`Can't read the css rules of: ${sheet.href} (${conditionText})`, e);\n                }\n            }\n            for (const subRule of subRules) {\n                const selectorText = subRule.selectorText || \"\";\n                // Split selectors, making sure not to split at commas in parentheses.\n                for (const selector of splitSelectorAroundCommasOutsideParentheses(selectorText)) {\n                    if (selector && !SELECTORS_IGNORE.test(selector)) {\n                        cssRules.push({ selector: selector.trim(), rawRule: subRule });\n                        if (selector === \"body\") {\n                            // The top element of a mailing has the class\n                            // 'o_layout'. Give it the body's styles so they can\n                            // trickle down.\n                            cssRules.push({\n                                selector: \".o_layout\",\n                                rawRule: subRule,\n                                specificity: 1,\n                            });\n                        }\n                    }\n                }\n            }\n        }\n    }\n\n    return cssRules;\n}\n/**\n * Convert Bootstrap list groups and their items to table structures.\n *\n * @param {Element} element\n */\nexport function listGroupToTable(element) {\n    for (const listGroup of element.querySelectorAll(\".list-group\")) {\n        let table;\n        if (listGroup.querySelectorAll(\".list-group-item\").length) {\n            table = _createTable(listGroup.attributes);\n        } else {\n            table = listGroup.cloneNode();\n            for (const attr of listGroup.attributes) {\n                table.setAttribute(attr.name, attr.value);\n            }\n        }\n        for (const child of [...listGroup.childNodes]) {\n            if (child.classList && child.classList.contains(\"list-group-item\")) {\n                // List groups are <ul>s that render like tables. Their\n                // li.list-group-item children should translate to tr > td.\n                const row = document.createElement(\"tr\");\n                const col = document.createElement(\"td\");\n                for (const attr of child.attributes) {\n                    col.setAttribute(attr.name, attr.value);\n                }\n                col.append(...child.childNodes);\n                col.classList.remove(\"list-group-item\");\n                if (!col.className) {\n                    col.removeAttribute(\"class\");\n                }\n                row.append(col);\n                table.append(row);\n                child.remove();\n            } else if (child.nodeName === \"LI\") {\n                table.append(...child.childNodes);\n            } else {\n                table.append(child);\n            }\n        }\n        table.classList.remove(\"list-group\");\n        if (!table.className) {\n            table.removeAttribute(\"class\");\n        }\n        if (listGroup.nodeName === \"TD\") {\n            listGroup.append(table);\n            listGroup.classList.remove(\"list-group\");\n            if (!listGroup.className) {\n                listGroup.removeAttribute(\"class\");\n            }\n        } else {\n            listGroup.before(table);\n            listGroup.remove();\n        }\n    }\n}\n/**\n * Convert all styles containing rgb colors to hexadecimal colors.\n * Note: ignores rgba colors, which are not supported in Microsoft Outlook.\n *\n * @param {HTMLElement} element\n */\nexport function normalizeColors(element) {\n    for (const node of element.querySelectorAll('[style*=\"rgb\"]')) {\n        const rgbaMatch = node.getAttribute(\"style\").match(/rgba?\\(([\\d.]+\\s*,?\\s*){3,4}\\)/g);\n        for (const rgb of rgbaMatch || []) {\n            node.setAttribute(\n                \"style\",\n                node.getAttribute(\"style\").replace(rgb, blendColors(rgb, node))\n            );\n        }\n    }\n}\n/**\n * Convert all css values that use the rem unit to px.\n *\n * @param {HTMLElement} $element\n * @param {Number} rootFontSize=16 The font size of the root element, in pixels\n */\nexport function normalizeRem(element, rootFontSize = 16) {\n    for (const node of element.querySelectorAll('[style*=\"rem\"]')) {\n        const remMatch = node.getAttribute(\"style\").match(/[\\d.]+\\s*rem/g);\n        for (const rem of remMatch || []) {\n            const remValue = parseFloat(rem.replace(/[^\\d.]/g, \"\"));\n            const pxValue = Math.round(remValue * rootFontSize * 100) / 100;\n            node.setAttribute(\"style\", node.getAttribute(\"style\").replace(rem, pxValue + \"px\"));\n        }\n    }\n}\n\n/**\n * This replaces column html with a dumbed down, Outlook-compliant version of\n * them just for Outlook so while not responsive, these columns still display OK\n * on Outlook.\n *\n * @param {Element} element\n */\nfunction responsiveToStaticForOutlook(element) {\n    // Replace the responsive tables with static ones for Outlook\n    for (const td of element.querySelectorAll(\"td.o_converted_col:not(.mso-hide)\")) {\n        const tdStyle = td.getAttribute(\"style\") || \"\";\n        const msoAttributes = [...td.attributes].filter(\n            (attr) => attr.name !== \"style\" && attr.name !== \"width\"\n        );\n        const msoWidth = td.style.getPropertyValue(\"max-width\");\n        const msoStyles = tdStyle.replace(/(^| |max-)width:[^;]*;\\s*/g, \"\");\n        const outlookTd = document.createElement(\"td\");\n        for (const attribute of msoAttributes) {\n            outlookTd.setAttribute(attribute.name, td.getAttribute(attribute.name));\n        }\n        if (msoWidth) {\n            outlookTd.setAttribute(\"width\", (\"\" + msoWidth).replace(\"px\", \"\").trim());\n            outlookTd.setAttribute(\"style\", `${msoStyles}width: ${msoWidth};`);\n        } else {\n            outlookTd.setAttribute(\"style\", msoStyles);\n        }\n        if (td.closest(\".s_masonry_block\")) {\n            outlookTd.style.padding = 0; // Not sure why this is needed.\n        }\n        // Outlook doesn't support left/right padding on images. When the image\n        // is the only child of its parent, apply said padding to the parent.\n        if (td.children.length === 1 && td.firstElementChild.nodeName === \"IMG\") {\n            const tdComputedStyle = getComputedStyle(td);\n            for (const side of [\"left\", \"right\"]) {\n                if (td.firstElementChild.style.width === \"100%\") {\n                    const prop = `padding-${side}`;\n                    const imagePadding = +td.firstElementChild.style[prop].replace(\"px\", \"\");\n                    if (imagePadding > 0) {\n                        const tdPadding = +tdComputedStyle[prop].replace(\"px\", \"\") || 0;\n                        outlookTd.style[prop] = tdPadding + imagePadding + \"px\";\n                    }\n                }\n            }\n        }\n        // The opening tag of `outlookTd` is for Outlook.\n        td.before(createMso(outlookTd.outerHTML.replace(\"</td>\", \"\")));\n        // The opening tag of `td` is for the others.\n        _hideForOutlook(td, \"opening\");\n    }\n}\n/**\n * Convert images of type svg to png.\n *\n * @param {HTMLElement} element\n */\nasync function svgToPng(element) {\n    for (const svg of element.querySelectorAll('img[src*=\".svg\"]')) {\n        // Make sure the svg is loaded before we convert it.\n        await new Promise((resolve) => {\n            svg.onload = () => resolve();\n            if (svg.complete) {\n                resolve();\n            }\n        });\n        const image = document.createElement(\"img\");\n        const canvas = document.createElement(\"CANVAS\");\n        const width = _getWidth(svg);\n        const height = _getHeight(svg);\n\n        canvas.setAttribute(\"width\", width);\n        canvas.setAttribute(\"height\", height);\n        canvas.getContext(\"2d\").drawImage(svg, 0, 0, width, height);\n\n        for (const attribute of svg.attributes) {\n            image.setAttribute(attribute.name, attribute.value);\n        }\n\n        image.setAttribute(\"src\", canvas.toDataURL(\"png\"));\n        image.setAttribute(\"width\", width);\n        image.setAttribute(\"height\", height);\n\n        svg.before(image);\n        svg.remove();\n    }\n}\n\n//--------------------------------------------------------------------------\n// Private\n//--------------------------------------------------------------------------\n\n/**\n * Take an element and apply a colspan to it. In this context, this implies to\n * also apply a width to it, that corresponds to the colspan.\n *\n * @param {Element} element\n * @param {number} colspan\n * @param {number} tableWidth\n */\nfunction _applyColspan(element, colspan, tableWidth) {\n    element.setAttribute(\"colspan\", colspan);\n    const widthPercentage = +element.getAttribute(\"colspan\") / 12;\n    // Round to 2 decimal places.\n    const width = Math.round(tableWidth * widthPercentage * 100) / 100;\n    element.style.setProperty(\"max-width\", width + \"px\");\n    element.classList.add(\"o_converted_col\");\n}\n/**\n * Take an element with a background image and return a string containing the\n * VML code to display the same image properly in Outlook, with its contents\n * inside.\n * Note that this assumes:\n *   - background-size: cover,\n *   - background-repeat: no-repeat,\n *   - size 100%\n *   - content is centered x/y\n * TODO: centering span probably not needed with `v-text-anchor:middle` present.\n *\n * @param {Element} backgroundImage\n * @returns {string}\n */\nfunction _backgroundImageToVml(backgroundImage) {\n    const matches = backgroundImage.style.backgroundImage.match(/url\\(\"?(.+?)\"?\\)/);\n    const url = matches && matches[1];\n    if (url) {\n        // Create the outer structure.\n        const clone = backgroundImage.cloneNode(true);\n        const div = document.createElement(\"div\");\n        div.replaceChildren(...clone.childNodes);\n        [\n            [\"fontSize\", 0],\n            [\"height\", \"100%\"],\n            [\"width\", \"100%\"],\n        ].forEach(([k, v]) => (div.style[k] = v));\n        const vmlContent = document.createElement(\"div\");\n        vmlContent.append(div);\n\n        // Preserve important inherited properties without ancestor context.\n        const style = getComputedStyle(backgroundImage);\n        for (const prop of FONT_PROPERTIES_TO_INHERIT) {\n            div.style[prop] = backgroundImage.style[prop] || style[prop];\n        }\n        [...div.children].forEach((child) =>\n            child.style.setProperty(\"font-size\", child.style.fontSize || style.fontSize)\n        );\n\n        // Prepare the top element for hosting the VML image.\n        for (const prop of [\n            \"background\",\n            \"background-image\",\n            \"background-repeat\",\n            \"background-size\",\n        ]) {\n            clone.style.removeProperty(prop);\n        }\n        clone.style.padding = 0;\n        clone.className = clone.className.replace(/p[bt]\\d+/g, \"\"); // Remove padding classes.\n        clone.setAttribute(\"background\", url);\n        clone.setAttribute(\"valign\", \"middle\");\n\n        // Create the VML structure, with the content of the original element inside.\n        const [width, height] = [_getWidth(backgroundImage), _getHeight(backgroundImage)];\n        const vml =\n            `<v:image xmlns:v=\"urn:schemas-microsoft-com:vml\" fill=\"true\" stroke=\"false\" ` +\n            `style=\"border: 0; display: inline-block; width: ${width}px; height: ${height}px;\" src=\"${url}\"/>\n        <v:rect xmlns:v=\"urn:schemas-microsoft-com:vml\" fill=\"true\" stroke=\"false\" ` +\n            `style=\"border: 0; display: inline-block; position: absolute; width:${width}px; height:${height}px; v-text-anchor:middle;\">\n            <v:fill opacity=\"0%\" color=\"#000000\"/>\n            <v:textbox inset=\"0,0,0,0\">\n                <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\">\n                    <tr>\n                        <td width=\"${width}\" align=\"center\" style=\"text-align: center;\">${vmlContent.outerHTML}</td>\n                    </tr>\n                </table>\n            </v:textbox>\n        </v:rect>`;\n\n        // Wrap the VML in the original opening and closing tags.\n        return `${clone.outerHTML.replace(\n            /<\\/[\\w-]+>[\\s\\n]*$/,\n            \"\"\n        )}${vml}</${clone.nodeName.toLowerCase()}>`;\n    }\n}\n/**\n * Take a selector and return its specificity according to the w3 specification.\n *\n * @see http://www.w3.org/TR/css3-selectors/#specificity\n * @param {string} selector\n * @returns number\n */\nfunction _computeSpecificity(selector) {\n    let a = 0;\n    selector = selector.replace(/#[a-z0-9_-]+/gi, () => {\n        a++;\n        return \"\";\n    });\n    let b = 0;\n    selector = selector.replace(/(\\.[a-z0-9_-]+)|(\\[.*?\\])/gi, () => {\n        b++;\n        return \"\";\n    });\n    let c = 0;\n    selector = selector.replace(/(^|\\s+|:+)[a-z0-9_-]+/gi, (a) => {\n        if (!a.includes(\":not(\")) {\n            c++;\n        }\n        return \"\";\n    });\n    return a * 100 + b * 10 + c;\n}\n/**\n * Take all the rules and modify them to contain information on their\n * specificity and to have normalized style.\n *\n * @see _computeSpecificity\n * @see _normalizeStyle\n * @param {Object} cssRules\n */\nfunction _computeStyleAndSpecificityOnRules(cssRules) {\n    for (const cssRule of cssRules) {\n        if (!cssRule.style && cssRule.rawRule.style) {\n            const style = _normalizeStyle(cssRule.rawRule.style);\n            if (Object.keys(style).length) {\n                Object.assign(cssRule, {\n                    style,\n                    specificity: _computeSpecificity(cssRule.selector),\n                });\n            } else {\n                Object.assign(cssRule, {\n                    specificity: 0,\n                });\n            }\n        }\n    }\n}\n/**\n * Return an array of twelve table cells as JQuery elements.\n *\n * @returns {Element[]}\n */\nfunction _createColumnGrid() {\n    return new Array(12).fill().map(() => document.createElement(\"td\"));\n}\n/**\n * Return a comment element with the given content, wrapped in an mso condition.\n *\n * @param {string} content\n * @returns {Comment}\n */\nexport function createMso(content = \"\") {\n    // We remove comments having opposite condition from the one we will insert\n    // We remove comment tags having the same condition\n    const showRegex = /<!--\\[if\\s+mso\\]>([\\s\\S]*?)<!\\[endif\\]-->/g;\n    const hideRegex = /<!--\\[if\\s+!mso\\]>([\\s\\S]*?)<!\\[endif\\]-->/g;\n    let contentToInsert = content;\n    contentToInsert = contentToInsert.replace(showRegex, (matchedContent, group) => group);\n    contentToInsert = contentToInsert.replace(hideRegex, \"\");\n    return document.createComment(`[if mso]>${contentToInsert}<![endif]`);\n}\n/**\n * Return a table element, with its default styles and attributes, as well as\n * the applicable given attributes, if any.\n *\n * @see TABLE_ATTRIBUTES\n * @see TABLE_STYLES\n * @param {NamedNodeMap | Attr[]} [attributes] default: []\n * @returns {Element}\n */\nfunction _createTable(attributes = []) {\n    const table = document.createElement(\"table\");\n    Object.entries(TABLE_ATTRIBUTES).forEach(([att, value]) => table.setAttribute(att, value));\n    for (const attr of attributes) {\n        if (!(attr.name === \"width\" && attr.value === \"100%\")) {\n            table.setAttribute(attr.name, attr.value);\n        }\n    }\n    table.style.setProperty(\"width\", \"100%\", \"important\");\n    if (table.classList.contains(\"o_layout\")) {\n        // The top mailing element inherits the body's font size and line-height\n        // and should keep them.\n        const layoutStyles = { ...TABLE_STYLES };\n        delete layoutStyles[\"font-size\"];\n        delete layoutStyles[\"line-height\"];\n        Object.entries(layoutStyles).forEach(([att, value]) => (table.style[att] = value));\n    } else {\n        for (const styleName in TABLE_STYLES) {\n            if (!(\"style\" in attributes && attributes.style.value.includes(styleName + \":\"))) {\n                table.style[styleName] = TABLE_STYLES[styleName];\n            }\n        }\n    }\n    return table;\n}\n/**\n * Take a Bootstrap grid column element and return its size, computed by using\n * its Bootstrap classes.\n *\n * @see RE_COL_MATCH\n * @param {Element} column\n * @returns {number}\n */\nfunction _getColumnSize(column) {\n    const colMatch = column.className.match(RE_COL_MATCH);\n    const colOptions = colMatch[2] && colMatch[2].substr(1).split(\"-\");\n    const colSize =\n        (colOptions && (colOptions.length === 2 ? +colOptions[1] : +colOptions[0])) || 0;\n    return colSize;\n}\n/**\n * Take a Bootstrap grid column element and return its offset size, computed by\n * using its Bootstrap classes.\n *\n * @see RE_OFFSET_MATCH\n * @param {Element} column\n * @returns {number}\n */\nfunction _getColumnOffsetSize(column) {\n    const offsetMatch = column.className.match(RE_OFFSET_MATCH);\n    const offsetOptions = offsetMatch && offsetMatch[2] && offsetMatch[2].substr(1).split(\"-\");\n    const offsetSize =\n        (offsetOptions && (offsetOptions.length === 2 ? +offsetOptions[1] : +offsetOptions[0])) ||\n        0;\n    return offsetSize;\n}\n/**\n * Return the CSS rules which applies on an element, tweaked so that they are\n * browser/mail client ok.\n *\n * @param {Node} node\n * @param {Object[]} Array<{selector: string;\n *                          style: {[styleName]: string};\n *                          specificity: number;}>\n * @returns {Object} {[styleName]: string}\n */\nfunction _getMatchedCSSRules(node, cssRules) {\n    node.matches =\n        node.matches ||\n        node.webkitMatchesSelector ||\n        node.mozMatchesSelector ||\n        node.msMatchesSelector ||\n        node.oMatchesSelector;\n\n    const styles = cssRules.map((rule) => removeBlacklistedStyles(rule, node)).filter(Boolean);\n\n    // Add inline styles at the highest specificity.\n    if (node.style.length) {\n        const inlineStyles = {};\n        for (const styleName of node.style) {\n            inlineStyles[styleName] = node.style[styleName];\n        }\n        styles.push(inlineStyles);\n    }\n\n    const processedStyle = {};\n    for (const style of styles) {\n        for (const [key, value] of Object.entries(style)) {\n            if (\n                !processedStyle[key] ||\n                !processedStyle[key].includes(\"important\") ||\n                value.includes(\"important\")\n            ) {\n                processedStyle[key] = value;\n            }\n        }\n    }\n\n    for (const [key, value] of Object.entries(processedStyle)) {\n        if (value && value.endsWith(\"important\")) {\n            processedStyle[key] = value.replace(/\\s*!important\\s*$/, \"\");\n        }\n    }\n\n    // When a grouped style (e.g., border-width, margin, padding) uses a CSS variable\n    // (e.g., var(--some-variable)), its substyles (e.g., margin-left, padding-top)\n    // won't have explicit values in CSSRule's style property. The grouped style itself\n    // also won't appear directly. To prevent losing these styles, we add the substyles\n    // explicitly using their computed values.\n    const computedStyle = getComputedStyle(node);\n    for (const groupName in GROUPED_STYLES) {\n        // We exclude the 'margin' and 'padding' styles from force apply because\n        // it's common that they have a value set by auto which doesn't make sense to\n        // force their computed value.\n        const force = !groupName.includes(\"margin\") && !groupName.includes(\"padding\");\n        const hasSubStyleApplied = GROUPED_STYLES[groupName].some(\n            (styleName) => styleName in processedStyle\n        );\n        if (!force && hasSubStyleApplied) {\n            continue;\n        }\n        for (const styleName of GROUPED_STYLES[groupName]) {\n            const styleValue = computedStyle.getPropertyValue(styleName);\n            if (styleValue && typeof styleValue === \"string\" && styleValue.length) {\n                processedStyle[styleName] = styleValue;\n            }\n        }\n    }\n\n    if (\n        processedStyle.display === \"block\" &&\n        !(node.classList && node.classList.contains(\"oe-nested\"))\n    ) {\n        delete processedStyle.display;\n    }\n    if (!processedStyle[\"box-sizing\"]) {\n        processedStyle[\"box-sizing\"] = \"border-box\"; // This is by default with Bootstrap.\n    }\n\n    // The css generates all the attributes separately and not in simplified\n    // form. In order to have a better compatibility (outlook for example) we\n    // simplify the css tags. e.g. border-left-style: none; border-bottom-s ....\n    // will be simplified in border-style = none\n    for (const info of [\n        { name: \"margin\" },\n        { name: \"padding\" },\n        { name: \"border\", suffix: \"-style\", defaultValue: \"none\" },\n    ]) {\n        const positions = [\"top\", \"right\", \"bottom\", \"left\"];\n        const positionalKeys = positions.map(\n            (position) => `${info.name}-${position}${info.suffix || \"\"}`\n        );\n        const styles = positionalKeys.map((key) => processedStyle[key]).filter((s) => s);\n        const hasVariableStyle = styles.some(\n            (style) => style.includes(\"calc(\") || style.includes(\"var(\")\n        );\n        const inherits = positionalKeys.some((key) =>\n            [\"inherit\", \"initial\"].includes((processedStyle[key] || \"\").trim())\n        );\n        if (styles.length && !hasVariableStyle && !inherits) {\n            const propertyName = `${info.name}${info.suffix || \"\"}`;\n            processedStyle[propertyName] = positionalKeys.every(\n                (key) => processedStyle[positionalKeys[0]] === processedStyle[key]\n            )\n                ? (processedStyle[propertyName] = processedStyle[positionalKeys[0]]) // top = right = bottom = left => property: [top];\n                : positionalKeys\n                      .map((key) => processedStyle[key] || info.defaultValue || 0)\n                      .join(\" \"); // property: [top] [right] [bottom] [left];\n            for (const prop of positionalKeys) {\n                delete processedStyle[prop];\n            }\n        }\n    }\n\n    if (processedStyle[\"border-bottom-left-radius\"]) {\n        processedStyle[\"border-radius\"] = processedStyle[\"border-bottom-left-radius\"];\n        delete processedStyle[\"border-bottom-left-radius\"];\n        delete processedStyle[\"border-bottom-right-radius\"];\n        delete processedStyle[\"border-top-left-radius\"];\n        delete processedStyle[\"border-top-right-radius\"];\n    }\n\n    // If the border styling is initial we remove it to simplify the css tags\n    // for compatibility. Also, since we do not send a css style tag, the\n    // initial value of the border is useless.\n    for (const styleName in processedStyle) {\n        if (styleName.includes(\"border\") && processedStyle[styleName] === \"initial\") {\n            delete processedStyle[styleName];\n        }\n    }\n\n    // text-decoration rule is decomposed in -line, -color and -style. This is\n    // however not supported by many browser/mail clients and the editor does\n    // not allow to change -color and -style rule anyway\n    if (processedStyle[\"text-decoration-line\"]) {\n        processedStyle[\"text-decoration\"] = processedStyle[\"text-decoration-line\"];\n        delete processedStyle[\"text-decoration-line\"];\n        delete processedStyle[\"text-decoration-color\"];\n        delete processedStyle[\"text-decoration-style\"];\n        delete processedStyle[\"text-decoration-thickness\"];\n    }\n\n    // flexboxes are not supported in Windows Outlook\n    for (const styleName in processedStyle) {\n        if (styleName.includes(\"flex\") || `${processedStyle[styleName]}`.includes(\"flex\")) {\n            delete processedStyle[styleName];\n        }\n    }\n\n    return processedStyle;\n}\nlet lastComputedStyleElement;\nlet lastComputedStyle;\n/**\n * Return the value of the given style property on the given element. This\n * caches the last computed style so if it's called several times in a row for\n * the same element, we don't recompute it every time.\n *\n * @param {Element} element\n * @param {string} propertyName\n * @returns\n */\nfunction _getStylePropertyValue(element, propertyName) {\n    const computedStyle =\n        lastComputedStyleElement === element ? lastComputedStyle : getComputedStyle(element);\n    lastComputedStyleElement = element;\n    lastComputedStyle = computedStyle;\n    return computedStyle[propertyName] || element.style.getPropertyValue(propertyName);\n}\n/**\n * Equivalent to JQuery's `width` method. Returns the element's visible width.\n *\n * @param {Element} element\n * @returns {Number}\n */\nfunction _getWidth(element) {\n    return parseFloat(getComputedStyle(element).width.replace(\"px\", \"\")) || 0;\n}\n/**\n * Equivalent to JQuery's `height` method. Returns the element's visible height.\n *\n * @param {Element} element\n * @returns {Number}\n */\nfunction _getHeight(element) {\n    return parseFloat(getComputedStyle(element).height.replace(\"px\", \"\")) || 0;\n}\n/**\n * Hides the given node (or just its opening/closing tag) for Outlook with mso\n * conditional comments and, if needed, mso hide style.\n *\n * @param {Node} node\n * @param {false|'opening'|'closing'} [onlyHideTag=false]\n */\nfunction _hideForOutlook(node, onlyHideTag = false) {\n    if (!onlyHideTag) {\n        let style = (node.getAttribute(\"style\") || \"\").trim();\n        if (style && !style.endsWith(\";\")) {\n            style += \";\";\n        }\n        node.setAttribute(\"style\", `${style} mso-hide: all;`);\n    }\n    node[onlyHideTag === \"closing\" ? \"append\" : \"before\"](document.createComment(\"[if !mso]><!\"));\n    node[onlyHideTag === \"opening\" ? \"prepend\" : \"after\"](document.createComment(\"<![endif]\"));\n}\n/**\n * Take a css style declaration return a \"normalized\" version of it (as a\n * standard object) for the purposes of emails. This means removing its styles\n * that are invalid, describe animations or aren't standard css (webkit\n * extensions). It also involves adding the \"!important\" suffix to styles that\n * have that priority, so they can be handled without access to the full\n * declaration.\n *\n * @param {CSSStyleDeclaration} style\n * @returns {Object} {[styleName]: string}\n */\nfunction _normalizeStyle(style) {\n    const normalizedStyle = {};\n    for (const styleName of style) {\n        const value = style[styleName];\n        if (\n            value &&\n            !styleName.includes(\"animation\") &&\n            !styleName.includes(\"-webkit\") &&\n            typeof value === \"string\"\n        ) {\n            const normalizedStyleName = styleName.replace(/-(.)/g, (a, b) => b.toUpperCase());\n            normalizedStyle[styleName] = style[normalizedStyleName];\n            if (style.getPropertyPriority(styleName) === \"important\") {\n                normalizedStyle[styleName] += \" !important\";\n            }\n        }\n    }\n    return normalizedStyle;\n}\n/**\n * Wrap a given element into a new parent, in place.\n *\n * @param {Element} element\n * @param {string} wrapperTag\n * @param {string} [wrapperClass] optional class to apply to the wrapper\n * @param {string} [wrapperStyle] optional style to apply to the wrapper\n * @returns {Element} the wrapper\n */\nfunction _wrap(element, wrapperTag, wrapperClass, wrapperStyle) {\n    const wrapper = document.createElement(wrapperTag);\n    if (wrapperClass) {\n        wrapper.className = wrapperClass;\n    }\n    if (wrapperStyle) {\n        wrapper.style.cssText = wrapperStyle;\n    }\n    element.parentElement.insertBefore(wrapper, element);\n    wrapper.append(element);\n    return wrapper;\n}\n\nfunction isBlacklistedStyle(node, selector, key) {\n    return (\n        node.matches(\"table, thead, tbody, tfoot, tr, td, th\") &&\n        [\"table\", \"thead\", \"tbody\", \"tfoot\", \"tr\", \"td\", \"th\"].some((elName) =>\n            selector.includes(elName)\n        ) &&\n        key.includes(\"color\")\n    );\n}\n\nfunction removeBlacklistedStyles(rule, node) {\n    if (!rule.style) {\n        return rule.style;\n    }\n    const styles = {};\n    for (const [key, value] of Object.entries(rule.style)) {\n        if (isBlacklistedStyle(node, rule.selector, key)) {\n            continue;\n        }\n        styles[key] = value;\n    }\n    return styles;\n}\n\nexport function splitSelectorAroundCommasOutsideParentheses(selector) {\n    if (selector.indexOf(\",\") === -1) {\n        return [selector].filter(Boolean);\n    }\n    const result = [];\n    let start = 0;\n    let depth = 0;\n    let inString;\n    for (let i = 0; i < selector.length; i++) {\n        const char = selector[i];\n        if (inString) {\n            if (char === inString && selector[i - 1] !== \"\\\\\") {\n                inString = undefined;\n            }\n            continue;\n        }\n        switch (char) {\n            case \"'\":\n            case '\"':\n                inString = char;\n                break;\n            case \"(\":\n                depth++;\n                break;\n            case \")\":\n                depth--;\n                if (depth < 0) {\n                    return [selector];\n                }\n                break;\n            case \",\":\n                if (depth === 0) {\n                    result.push(selector.slice(start, i));\n                    start = i + 1;\n                }\n                break;\n        }\n    }\n    if (depth > 0) {\n        return [selector];\n    }\n    result.push(selector.slice(start));\n    return result.filter(Boolean);\n}\n\n/**\n * Corrects the `border-style` attribute in the provided inline style string.\n * This is specifically for Outlook, which displays borders even when their widths are set to 0px.\n * If all border widths are 0, the function updates `border-style` to `none`.\n *\n * @param {string} style - The inline style string to correct.\n * @returns {string} - The corrected inline style string.\n */\nfunction correctBorderAttributes(style) {\n    const stylesObject = style\n        .replace(/\\s+/g, \" \")\n        .split(\";\")\n        .reduce((styles, styleString) => {\n            const [attribute, value] = styleString.split(\":\").map((str) => str.trim());\n            if (attribute) {\n                styles[attribute] = value;\n            }\n            return styles;\n        }, {});\n\n    const BORDER_WIDTHS_ATTRIBUTES = [\n        \"border-bottom-width\",\n        \"border-left-width\",\n        \"border-right-width\",\n        \"border-top-width\",\n    ];\n\n    const isBorderStyleApplied = BORDER_WIDTHS_ATTRIBUTES.some(\n        (attribute) => attribute in stylesObject\n    );\n\n    if (!isBorderStyleApplied) {\n        return style;\n    }\n\n    const totalBorderWidth = BORDER_WIDTHS_ATTRIBUTES.reduce((totalWidth, attribute) => {\n        const widthValue = stylesObject[attribute] || \"0px\";\n        const numericWidth = parseFloat(widthValue.replace(\"px\", \"\")) || 0;\n        return totalWidth + numericWidth;\n    }, 0);\n\n    if (totalBorderWidth === 0) {\n        let correctedStyle = style.trim();\n        if (correctedStyle.slice(-1) != \";\") {\n            correctedStyle += \";\";\n        }\n        correctedStyle = correctedStyle.replace(\n            /(;|^)\\s*border-style\\s*:[^;]*(;|$)|$/,\n            \"$1border-style:none$2\"\n        );\n        return correctedStyle;\n    }\n\n    if (/border-style\\s*:/i.test(style)) {\n        return style;\n    }\n    return style.trim().replace(/;?$/, \"; border-style: solid;\");\n}\n", "import { HtmlField, htmlField } from \"@html_editor/fields/html_field\";\nimport { registry } from \"@web/core/registry\";\nimport { getCSSRules, toInline } from \"./convert_inline\";\nimport { ColumnPlugin } from \"@html_editor/main/column_plugin\";\n\nconst cssRulesByElement = new WeakMap();\n\nexport class HtmlMailField extends HtmlField {\n    /**\n     * @param {WeakMap} cssRulesByElement\n     * @param {Editor} editor\n     * @param {HTMLElement} el\n     */\n    static async getInlinedEditorContent(cssRulesByElement, editor, el) {\n        if (!cssRulesByElement.has(editor.editable)) {\n            cssRulesByElement.set(editor.editable, getCSSRules(editor.document));\n        }\n        const cssRules = cssRulesByElement.get(editor.editable);\n        // Insert the cloned element inside an DOM so we can get its computed style.\n        editor.editable.after(el);\n        el.classList.remove(\"odoo-editor-editable\");\n        await toInline(el, cssRules);\n        el.remove();\n    }\n\n    async getEditorContent() {\n        const el = await super.getEditorContent();\n        await HtmlMailField.getInlinedEditorContent(cssRulesByElement, this.editor, el);\n        return el;\n    }\n\n    getConfig() {\n        const config = super.getConfig();\n        config.dropImageAsAttachment = false;\n        config.Plugins = config.Plugins.filter((plugin) => plugin !== ColumnPlugin);\n        return config;\n    }\n}\n\nexport const htmlMailField = {\n    ...htmlField,\n    component: HtmlMailField,\n    additionalClasses: [\"o_field_html\"],\n    extractProps({ attrs, options }, dynamicInfo) {\n        const props = htmlField.extractProps({ attrs, options }, dynamicInfo);\n        props.editorConfig.allowChecklist = false;\n        props.embeddedComponents = false;\n        return props;\n    },\n};\n\nregistry.category(\"fields\").add(\"html_mail\", htmlMailField);\n", "import { ActivityButton } from \"@mail/core/web/activity_button\";\n\nimport { Component } from \"@odoo/owl\";\n\nimport { registry } from \"@web/core/registry\";\nimport { standardFieldProps } from \"@web/views/fields/standard_field_props\";\n\nexport class KanbanActivity extends Component {\n    static components = { ActivityButton };\n    // used in children, in particular in ActivityButton\n    static fieldDependencies = [\n        {\n            name: \"activity_exception_decoration\",\n            type: \"selection\",\n            selection: [(\"warning\", \"Alert\"), (\"danger\", \"Error\")],\n        },\n        { name: \"activity_exception_icon\", type: \"char\" },\n        { name: \"activity_state\", type: \"selection\" },\n        { name: \"activity_summary\", type: \"char\" },\n        { name: \"activity_type_icon\", type: \"char\" },\n        { name: \"activity_type_id\", type: \"many2one\", relation: \"mail.activity.type\" },\n    ];\n    static props = standardFieldProps;\n    static template = \"mail.KanbanActivity\";\n}\n\nexport const kanbanActivity = {\n    component: KanbanActivity,\n    fieldDependencies: KanbanActivity.fieldDependencies,\n};\n\nregistry.category(\"fields\").add(\"kanban_activity\", kanbanActivity);\n", "import { ActivityButton } from \"@mail/core/web/activity_button\";\n\nimport { Component } from \"@odoo/owl\";\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { standardFieldProps } from \"@web/views/fields/standard_field_props\";\n\nclass ListActivityButton extends ActivityButton {\n    static props = {\n        ...ActivityButton.props,\n        slots: Object,\n    };\n    static template = \"mail.ListActivityButton\";\n\n    setup() {\n        super.setup();\n        this.defaultActivityStateClass = \"\";\n        this.defaultActivityDecorationClass = \"fa-clock-o\";\n    }\n}\n\nexport class ListActivity extends Component {\n    static components = { ActivityButton: ListActivityButton };\n    // also used in children, in particular in ActivityButton\n    static fieldDependencies = [\n        { name: \"activity_exception_decoration\", type: \"selection\", selection: [] },\n        { name: \"activity_exception_icon\", type: \"char\" },\n        { name: \"activity_state\", type: \"selection\", selection: [] },\n        { name: \"activity_summary\", type: \"char\" },\n        { name: \"activity_type_icon\", type: \"char\" },\n        { name: \"activity_type_id\", type: \"many2one\", relation: \"mail.activity.type\" },\n    ];\n    static props = standardFieldProps;\n    static template = \"mail.ListActivity\";\n\n    get summaryText() {\n        if (this.props.record.data.activity_exception_decoration) {\n            return _t(\"Warning\");\n        }\n        if (this.props.record.data.activity_summary) {\n            return this.props.record.data.activity_summary;\n        }\n        if (this.props.record.data.activity_type_id) {\n            return this.props.record.data.activity_type_id.display_name;\n        }\n        return undefined;\n    }\n}\n\nexport const listActivity = {\n    component: ListActivity,\n    fieldDependencies: ListActivity.fieldDependencies,\n    displayName: _t(\"List Activity\"),\n    supportedTypes: [\"one2many\"],\n};\n\nregistry.category(\"fields\").add(\"list_activity\", listActivity);\n", "import { useAssignUserCommand } from \"@mail/views/web/fields/assign_user_command_hook\";\n\nimport { registry } from \"@web/core/registry\";\nimport { TagsList } from \"@web/core/tags_list/tags_list\";\nimport { usePopover } from \"@web/core/popover/popover_hook\";\nimport { AvatarCardPopover } from \"@mail/discuss/web/avatar_card/avatar_card_popover\";\nimport {\n    Many2ManyTagsAvatarField,\n    many2ManyTagsAvatarField,\n    ListMany2ManyTagsAvatarField,\n    listMany2ManyTagsAvatarField,\n    KanbanMany2ManyTagsAvatarField,\n    kanbanMany2ManyTagsAvatarField,\n    KanbanMany2ManyTagsAvatarFieldTagsList,\n} from \"@web/views/fields/many2many_tags_avatar/many2many_tags_avatar_field\";\nimport { Many2XAvatarUserAutocomplete } from \"../avatar_autocomplete/avatar_many2x_autocomplete\";\n\nexport class Many2ManyAvatarUserTagsList extends TagsList {\n    static template = \"mail.Many2ManyAvatarUserTagsList\";\n}\n\nconst WithUserChatter = (T) =>\n    class UserChatterMixin extends T {\n        setup() {\n            super.setup(...arguments);\n            if (this.props.withCommand) {\n                useAssignUserCommand();\n            }\n            this.avatarCard = usePopover(AvatarCardPopover);\n        }\n\n        displayAvatarCard(record) {\n            return [\"res.users\", \"res.partner\"].includes(this.relation);\n        }\n\n        getAvatarCardProps(record) {\n            return {\n                id: record.resId,\n                model: this.relation,\n            };\n        }\n\n        getTagProps(record) {\n            return {\n                ...super.getTagProps(...arguments),\n                onImageClicked: (ev) => {\n                    if (!this.displayAvatarCard(record)) {\n                        return;\n                    }\n                    const target = ev.currentTarget;\n                    if (\n                        !this.avatarCard.isOpen ||\n                        (this.lastOpenedId && record.resId !== this.lastOpenedId)\n                    ) {\n                        this.avatarCard.open(target, this.getAvatarCardProps(record));\n                        this.lastOpenedId = record.resId;\n                    }\n                },\n            };\n        }\n    };\n\nexport class Many2ManyTagsAvatarUserField extends WithUserChatter(Many2ManyTagsAvatarField) {\n    static template = \"mail.Many2ManyTagsAvatarUserField\";\n    static components = {\n        ...Many2ManyTagsAvatarField.components,\n        TagsList: Many2ManyAvatarUserTagsList,\n        Many2XAutocomplete: Many2XAvatarUserAutocomplete,\n    };\n}\n\nexport const many2ManyTagsAvatarUserField = {\n    ...many2ManyTagsAvatarField,\n    component: Many2ManyTagsAvatarUserField,\n    additionalClasses: [\"o_field_many2many_tags_avatar\"],\n};\n\nregistry.category(\"fields\").add(\"many2many_avatar_user\", many2ManyTagsAvatarUserField);\n\nexport class KanbanMany2ManyAvatarUserTagsList extends KanbanMany2ManyTagsAvatarFieldTagsList {\n    static template = \"mail.KanbanMany2ManyAvatarUserTagsList\";\n}\n\nexport class KanbanMany2ManyTagsAvatarUserField extends WithUserChatter(\n    KanbanMany2ManyTagsAvatarField\n) {\n    static template = \"mail.KanbanMany2ManyTagsAvatarUserField\";\n    static components = {\n        ...KanbanMany2ManyTagsAvatarField.components,\n        TagsList: KanbanMany2ManyAvatarUserTagsList,\n    };\n    get displayText() {\n        return !this.props.readonly;\n    }\n}\nexport const kanbanMany2ManyTagsAvatarUserField = {\n    ...kanbanMany2ManyTagsAvatarField,\n    component: KanbanMany2ManyTagsAvatarUserField,\n    additionalClasses: [\"o_field_many2many_tags_avatar\", \"o_field_many2many_tags_avatar_kanban\"],\n};\nregistry.category(\"fields\").add(\"kanban.many2many_avatar_user\", kanbanMany2ManyTagsAvatarUserField);\n\nexport class ListMany2ManyTagsAvatarUserField extends WithUserChatter(\n    ListMany2ManyTagsAvatarField\n) {\n    static template = \"mail.ListMany2ManyTagsAvatarUserField\";\n    static components = {\n        ...ListMany2ManyTagsAvatarField.components,\n        TagsList: Many2ManyAvatarUserTagsList,\n        Many2XAutocomplete: Many2XAvatarUserAutocomplete,\n    };\n\n    get displayText() {\n        return this.props.record.data[this.props.name].records.length === 1 || !this.props.readonly;\n    }\n}\n\nexport const listMany2ManyTagsAvatarUserField = {\n    ...listMany2ManyTagsAvatarField,\n    component: ListMany2ManyTagsAvatarUserField,\n    listViewWidth: [120],\n    additionalClasses: [\"o_field_many2many_tags_avatar\", \"o_field_many2many_tags_avatar_list\"],\n};\n\nregistry.category(\"fields\").add(\"list.many2many_avatar_user\", listMany2ManyTagsAvatarUserField);\nregistry\n    .category(\"fields\")\n    .add(\"activity.many2many_avatar_user\", kanbanMany2ManyTagsAvatarUserField);\n", "import { RecipientsInputTagsList } from \"@mail/core/web/recipients_input_tags_list\";\nimport { RecipientsPopover } from \"@mail/core/web/recipients_popover\";\nimport { parseEmail } from \"@mail/utils/common/format\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { evaluateBooleanExpr } from \"@web/core/py_js/py\";\nimport { registry } from \"@web/core/registry\";\nimport { usePopover } from \"@web/core/popover/popover_hook\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport {\n    Many2ManyTagsField,\n    many2ManyTagsField,\n} from \"@web/views/fields/many2many_tags/many2many_tags_field\";\nimport { Many2XAutocomplete } from \"@web/views/fields/relational_utils\";\n\nexport class FieldMany2ManyTagsEmailTagsList extends RecipientsInputTagsList {\n    static template = \"FieldMany2ManyTagsEmailTagsList\";\n}\n\nexport class FieldMany2ManyTagsEmailMany2xAutocomplete extends Many2XAutocomplete {\n    /**\n     * @override\n     * @param {string} value\n     * @returns {Object}\n     */\n    getCreationContext(value) {\n        const [name, email] = value ? parseEmail(value) : [\"\", \"\"];\n        const context = super.getCreationContext(name);\n        if (email) {\n            context[\"default_email\"] = email;\n        }\n        return context;\n    }\n}\n\nexport class FieldMany2ManyTagsEmail extends Many2ManyTagsField {\n    static template = \"FieldMany2ManyTagsEmailTags\";\n    static components = {\n        ...FieldMany2ManyTagsEmail.components,\n        TagsList: FieldMany2ManyTagsEmailTagsList,\n        Many2XAutocomplete: FieldMany2ManyTagsEmailMany2xAutocomplete,\n    };\n    static props = {\n        ...Many2ManyTagsField.props,\n        context: { type: Object, optional: true },\n        canEditTags: { type: Boolean, optional: true },\n    };\n\n    setup() {\n        super.setup();\n        if (this.quickCreate) {\n            this.quickCreate = this.quickCreateRecipient.bind(this);\n        }\n        this.openedDialogs = 0;\n        this.recordsIdsToAdd = [];\n\n        this.recipientsPopover = usePopover(RecipientsPopover);\n        this.actionService = useService(\"action\");\n\n        const update = this.update;\n        this.update = async (object) => {\n            await update(object);\n        };\n    }\n\n    get tags() {\n        // Add email to our tags\n        const tags = super.tags;\n        const emailByResId = this.props.record.data[this.props.name].records.reduce(\n            (acc, record) => {\n                acc[record.resId] = record.data.email;\n                return acc;\n            },\n            {}\n        );\n        tags.forEach((tag) => {\n            tag.email = emailByResId[tag.resId];\n            tag.name = tag.text;\n            tag.title = tag.text;\n        });\n        return tags;\n    }\n\n    /**\n     * @override\n     * @param {Record} record\n     * @returns {Object}\n     */\n    getTagProps(record) {\n        return {\n            ...super.getTagProps(record),\n            text:\n                record.data.name || record.data.email || record.data.display_name || _t(\"Unnamed\"),\n            onClick: (ev) => this.onTagClick(ev, record),\n        };\n    }\n\n    /**\n     * @param {Event} event\n     * @param {Record} record\n     */\n    onTagClick(event, record) {\n        const viewProfileBtnOverride = () => {\n            const action = {\n                type: \"ir.actions.act_window\",\n                res_model: \"res.partner\",\n                res_id: record.resId,\n                views: [[false, \"form\"]],\n                target: \"current\",\n            };\n            this.actionService.doAction(action);\n        };\n        this.recipientsPopover.open(event.target, {\n            id: record.resId,\n            viewProfileBtnOverride,\n        });\n    }\n\n    async quickCreateRecipient(request) {\n        const [name, email] = parseEmail(request);\n        const [partnerId] = await this.orm.create(\"res.partner\", [{ name, email }]);\n        return this.props.record.data[this.props.name].addAndRemove({ add: [partnerId] });\n    }\n\n    async updateRecipient(newEmail, partnerId) {\n        const list = this.props.record.data[this.props.name];\n        const partnerRecord = list.records.find((r) => r.resId === partnerId);\n        partnerRecord.canSaveOnUpdate = true;\n        return partnerRecord.update({ email: newEmail }, { save: true });\n    }\n}\n\nexport const fieldMany2ManyTagsEmail = {\n    ...many2ManyTagsField,\n    component: FieldMany2ManyTagsEmail,\n    supportedOptions: [\n        ...many2ManyTagsField.supportedOptions,\n        {\n            label: _t(\"Edit Tags\"),\n            name: \"edit_tags\",\n            type: \"boolean\",\n        },\n    ],\n    extractProps({ options, attrs }, dynamicInfo) {\n        const props = many2ManyTagsField.extractProps(...arguments);\n        props.context = dynamicInfo.context;\n        const hasEditPermission = attrs.can_write ? evaluateBooleanExpr(attrs.can_write) : true;\n        props.canEditTags = options.edit_tags ? hasEditPermission : false;\n        return props;\n    },\n    relatedFields: (fieldInfo) => [\n        ...many2ManyTagsField.relatedFields(fieldInfo),\n        { name: \"email\", type: \"char\", readonly: false },\n        { name: \"name\", type: \"char\" },\n    ],\n    additionalClasses: [\"o_field_many2many_tags\"],\n};\n\nregistry.category(\"fields\").add(\"many2many_tags_email\", fieldMany2ManyTagsEmail);\n", "import { Component } from \"@odoo/owl\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { computeM2OProps, KanbanMany2One } from \"@web/views/fields/many2one/many2one\";\nimport {\n    buildM2OFieldDescription,\n    extractM2OFieldProps,\n    m2oSupportedOptions,\n    Many2OneField,\n} from \"@web/views/fields/many2one/many2one_field\";\nimport { Avatar } from \"../avatar/avatar\";\n\nexport class KanbanMany2OneAvatarUserField extends Component {\n    static template = \"mail.KanbanMany2OneAvatarUserField\";\n    static components = { Avatar, KanbanMany2One };\n    static props = {\n        ...Many2OneField.props,\n        displayAvatarName: { type: Boolean, optional: true },\n    };\n\n    get displayName() {\n        return this.props.displayAvatarName && this.value ? this.value.display_name : \"\";\n    }\n\n    get m2oProps() {\n        return computeM2OProps(this.props);\n    }\n\n    get value() {\n        return this.props.record.data[this.props.name];\n    }\n}\n\n/** @type {import(\"registries\").FieldsRegistryItemShape} */\nconst fieldDescr = {\n    ...buildM2OFieldDescription(KanbanMany2OneAvatarUserField),\n    additionalClasses: [\"o_field_many2one_avatar_kanban\", \"o_field_many2one_avatar\"],\n    extractProps(staticInfo, dynamicInfo) {\n        return {\n            ...extractM2OFieldProps(staticInfo, dynamicInfo),\n            displayAvatarName: staticInfo.options.display_avatar_name || false,\n            readonly: dynamicInfo.readonly,\n        };\n    },\n    supportedOptions: [\n        ...m2oSupportedOptions,\n        {\n            label: _t(\"Display avatar name\"),\n            name: \"display_avatar_name\",\n            type: \"boolean\",\n        },\n    ],\n};\n\nregistry.category(\"fields\").add(\"activity.many2one_avatar_user\", fieldDescr);\nregistry.category(\"fields\").add(\"kanban.many2one_avatar_user\", fieldDescr);\n", "import { useAssignUserCommand } from \"@mail/views/web/fields/assign_user_command_hook\";\n\nimport { Component } from \"@odoo/owl\";\nimport { registry } from \"@web/core/registry\";\nimport { computeM2OProps, Many2One } from \"@web/views/fields/many2one/many2one\";\nimport {\n    buildM2OFieldDescription,\n    extractM2OFieldProps,\n    Many2OneField,\n} from \"@web/views/fields/many2one/many2one_field\";\nimport { Avatar } from \"../avatar/avatar\";\nimport { Many2XAvatarUserAutocomplete } from \"../avatar_autocomplete/avatar_many2x_autocomplete\";\n\nexport class Many2OneAvatarUser extends Many2One {\n    static components = {\n        ...Many2One.components,\n        Many2XAutocomplete: Many2XAvatarUserAutocomplete,\n    };\n}\n\nexport class Many2OneAvatarUserField extends Component {\n    static template = \"mail.Many2OneAvatarUserField\";\n    static components = { Avatar, Many2OneAvatarUser };\n    static props = {\n        ...Many2OneField.props,\n        withCommand: { type: Boolean },\n    };\n\n    setup() {\n        if (this.props.withCommand) {\n            useAssignUserCommand();\n        }\n    }\n\n    get m2oProps() {\n        return computeM2OProps(this.props);\n    }\n\n    get relation() {\n        // This getter is used by `useAssignUserCommand`\n        return this.props.record.fields[this.props.name].relation;\n    }\n\n    get value() {\n        return this.props.record.data[this.props.name];\n    }\n}\n\nexport const many2OneAvatarUserField = {\n    ...buildM2OFieldDescription(Many2OneAvatarUserField),\n    additionalClasses: [\"o_field_many2one_avatar\"],\n    extractProps(staticInfo, dynamicInfo) {\n        return {\n            ...extractM2OFieldProps(staticInfo, dynamicInfo),\n            withCommand: [\"form\", \"list\"].includes(staticInfo.viewType),\n            canOpen: \"no_open\" in staticInfo.options\n                ? !staticInfo.options.no_open\n                : staticInfo.viewType === \"form\",\n        };\n    },\n    listViewWidth: [110],\n};\nregistry.category(\"fields\").add(\"many2one_avatar_user\", many2OneAvatarUserField);\n", "import { useOpenChat } from \"@mail/core/web/open_chat_hook\";\n\nimport { TagsList } from \"@web/core/tags_list/tags_list\";\nimport { patch } from \"@web/core/utils/patch\";\nimport { PropertyValue } from \"@web/views/fields/properties/property_value\";\n\n/**\n * Allow to open the chatter of the user when we click on the avatar of a Many2one\n * property (like we do for many2one_avatar_user widget).\n */\npatch(PropertyValue.prototype, {\n    setup() {\n        super.setup();\n\n        if (this.env.services[\"mail.store\"]) {\n            // work only for the res.users model\n            this.openChat = useOpenChat(\"res.users\");\n        }\n    },\n\n    _onAvatarClicked() {\n        if (this.openChat && this.showAvatar && this.props.comodel === \"res.users\") {\n            this.openChat(this.props.value.id);\n        }\n    },\n});\n\n/**\n * Allow to open the chatter of the user when we click on the avatar of a Many2many\n * property (like we do for many2many_avatar_user widget).\n */\nexport class Many2manyPropertiesTagsList extends TagsList {\n    static template = \"mail.Many2manyPropertiesTagsList\";\n\n    setup() {\n        super.setup();\n        if (this.env.services[\"mail.store\"]) {\n            this.openChat = useOpenChat(\"res.users\");\n        }\n    }\n\n    _onAvatarClicked(tagIndex) {\n        const tag = this.props.tags[tagIndex];\n        if (this.openChat && tag.comodel === \"res.users\") {\n            this.openChat(tag.id);\n        }\n    }\n}\n\nPropertyValue.components = {\n    ...PropertyValue.components,\n    TagsList: Many2manyPropertiesTagsList,\n};\n", "import { DateTimeInput } from \"@web/core/datetime/datetime_input\";\nimport { Dialog } from \"@web/core/dialog/dialog\";\nimport { today } from \"@web/core/l10n/dates\";\n\nimport { Component, useState } from \"@odoo/owl\";\n\nexport class ScheduledDateDialog extends Component {\n    static template = \"mail.ScheduledDateDialog\";\n    static props = {\n        close: Function,\n        isRemovable: { type: Boolean },\n        save: Function,\n        scheduledDate: { type: luxon.DateTime, optional: true },\n    };\n    static components = {\n        DateTimeInput,\n        Dialog,\n    };\n\n    setup() {\n        const now = luxon.DateTime.now();\n        this.tomorrowMorning = today().plus({ days: 1 }).set({ hour: 8 });\n        this.tomorrowAfternoon = this.tomorrowMorning.set({ hour: 13 });\n        this.mondayMorning = today()\n            .plus({ days: (1 - today().weekday + 7) % 7 || 7 })\n            .set({ hour: 8 });\n\n        this.state = useState({\n            customDateTime: now\n                .plus({ hours: 1 })\n                .set({ minutes: Math.ceil(now.minute / 5) * 5, seconds: 0, milliseconds: 0 }),\n            selectedOption: undefined,\n        });\n\n        if (!this.props.scheduledDate || this.props.scheduledDate.equals(this.tomorrowMorning)) {\n            this.state.selectedOption = \"morning\";\n        } else if (this.props.scheduledDate.equals(this.tomorrowAfternoon)) {\n            this.state.selectedOption = \"afternoon\";\n        } else if (this.props.scheduledDate.equals(this.mondayMorning)) {\n            this.state.selectedOption = \"monday\";\n        } else {\n            this.state.selectedOption = \"custom\";\n            this.state.customDateTime = this.props.scheduledDate;\n        }\n        this.dateTimeFormat = {\n            day: \"numeric\",\n            hour: \"numeric\",\n            minute: \"numeric\",\n            month: \"short\",\n        };\n    }\n\n    get dateTimePickerProps() {\n        return {\n            minDate: luxon.DateTime.now(),\n            onSelect: (value) => (this.state.customDateTime = value),\n            type: \"datetime\",\n            value: this.state.customDateTime,\n            rounding: 1,\n        };\n    }\n\n    get scheduledDate() {\n        if (this.state.selectedOption === \"morning\") {\n            return this.tomorrowMorning;\n        } else if (this.state.selectedOption === \"afternoon\") {\n            return this.tomorrowAfternoon;\n        } else if (this.state.selectedOption === \"monday\") {\n            return this.mondayMorning;\n        } else {\n            return this.state.customDateTime;\n        }\n    }\n\n    clear() {\n        this.props.save(false);\n        this.props.close();\n    }\n\n    save() {\n        this.props.save(this.scheduledDate);\n        this.props.close();\n    }\n}\n", "import { ScheduledDateDialog } from \"./scheduled_date_dialog\";\nimport { deserializeDateTime, serializeDateTime } from \"@web/core/l10n/dates\";\nimport { registry } from \"@web/core/registry\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { standardFieldProps } from \"@web/views/fields/standard_field_props\";\n\nimport { Component } from \"@odoo/owl\";\n\n/**\n * Widgets used to display and select the scheduled date in the composer (in monocomment mode)\n * and in the mail_scheduled_message form view.\n * There are two different widgets because the composer uses a text field to store the\n * scheduled date whereas the mail_scheduled_message model uses a datetime field.\n */\n\nclass ScheduledDateFieldCommon extends Component {\n    static props = standardFieldProps;\n    static template = \"mail.ScheduledDateField\";\n\n    setup() {\n        super.setup();\n        this.dialog = useService(\"dialog\");\n        this.dateTimeFormat = {\n            day: \"numeric\",\n            hour: \"numeric\",\n            minute: \"numeric\",\n            month: \"short\",\n        };\n    }\n\n    onClick(ev) {\n        this.dialog.add(ScheduledDateDialog, {\n            save: (scheduledDate) => this.setScheduledDate(scheduledDate),\n            isRemovable: this.isRemovable,\n            scheduledDate: this.scheduledDate,\n        });\n        // prevents the button to look focused (text-info to look darker) when closing the dialog\n        ev.currentTarget.blur();\n    }\n}\n\nclass TextScheduledDateField extends ScheduledDateFieldCommon {\n    setup() {\n        super.setup();\n        this.isRemovable = true;\n    }\n\n    get scheduledDate() {\n        return (\n            (this.props.record.data[this.props.name] || undefined) &&\n            deserializeDateTime(this.props.record.data[this.props.name])\n        );\n    }\n\n    setScheduledDate(scheduledDate) {\n        this.props.record.update({\n            scheduled_date: scheduledDate ? serializeDateTime(scheduledDate) : \"\",\n        });\n    }\n}\n\nconst textScheduledDateField = {\n    component: TextScheduledDateField,\n};\nregistry.category(\"fields\").add(\"text_scheduled_date\", textScheduledDateField);\n\nclass DatetimeScheduledDateField extends ScheduledDateFieldCommon {\n    setup() {\n        super.setup();\n        this.isRemovable = false;\n    }\n\n    get scheduledDate() {\n        return this.props.record.data[this.props.name];\n    }\n\n    setScheduledDate(scheduledDate) {\n        this.props.record.update({ scheduled_date: scheduledDate });\n    }\n}\n\nconst datetimeScheduledDateField = {\n    component: DatetimeScheduledDateField,\n};\nregistry.category(\"fields\").add(\"datetime_scheduled_date\", datetimeScheduledDateField);\n", "import { Component } from \"@odoo/owl\";\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { CharField } from \"@web/views/fields/char/char_field\";\n\nexport class ShortcutCharField extends Component {\n    static template = \"mail.ShortcutCharField\";\n    static components = { CharField };\n    static props = { ...CharField.props };\n\n    get charProps() {\n        return {\n            ...this.props,\n            placeholder: _t(\"e.g. hello\"),\n        };\n    }\n}\n\nregistry.category(\"fields\").add(\"shortcut\", {\n    component: ShortcutCharField,\n});\n", "import { useService } from \"@web/core/utils/hooks\";\nimport { KanbanController } from \"@web/views/kanban/kanban_controller\";\n\nexport class MailActivityMyKanbanController extends KanbanController {\n    setup() {\n        super.setup();\n        this.store = useService(\"mail.store\");\n    }\n\n    async createRecord() {\n        return this.store\n            .scheduleActivity(\n                this.props.resModel != \"mail.activity\" ? this.props.resModel : false,\n                false\n            )\n            .then(async () => {\n                // Refresh view once new activity has been added\n                await this.model.root.load();\n            });\n    }\n}\n", "import { kanbanView } from \"@web/views/kanban/kanban_view\";\nimport { MailActivityMyKanbanController } from \"./mail_activity_my_kanban_controller\";\nimport { registry } from \"@web/core/registry\";\n\nexport const mailActivityMyKanbanView = {\n    ...kanbanView,\n    Controller: MailActivityMyKanbanController,\n};\n\nregistry.category(\"views\").add(\"mail_activity_my_kanban\", mailActivityMyKanbanView);\n", "import { useService } from \"@web/core/utils/hooks\";\nimport { ListController } from \"@web/views/list/list_controller\";\n\nexport class ArchiveDisabledListController extends ListController {\n    setup() {\n        super.setup();\n        this.archiveEnabled = false;\n        this.store = useService(\"mail.store\");\n    }\n\n    async createRecord() {\n        return this.store\n            .scheduleActivity(\n                this.props.resModel != \"mail.activity\" ? this.props.resModel : false,\n                false\n            )\n            .then(async () => {\n                // Refresh view once new activity has been added\n                await this.model.root.load();\n            });\n    }\n}\n", "import { registry } from \"@web/core/registry\";\nimport { listView } from \"@web/views/list/list_view\";\nimport { ArchiveDisabledListController } from \"./archive_disabled_list_controller\";\n\nexport const archiveDisabledListView = {\n    ...listView,\n    Controller: ArchiveDisabledListController,\n};\n\nregistry.category(\"views\").add(\"archive_disabled_activity_list\", archiveDisabledListView);\n", "import { Component } from \"@odoo/owl\";\nimport { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\nimport { registry } from \"@web/core/registry\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { standardWidgetProps } from \"@web/views/widgets/standard_widget_props\";\nconst { DateTime } = luxon;\n\n/**\n * This widget displays a small dropdown allowing users to reschedule the\n * selected activity to certain dates in the near future.\n */\n\n// Version of the widget to use on mail.activity lists\nexport class MailActivityListRescheduleDropdown extends Component {\n    static components = { Dropdown, DropdownItem };\n    static props = {\n        ...standardWidgetProps,\n    };\n    static template = \"mail.MailActivityListRescheduleDropdown\";\n\n    setup() {\n        this.orm = useService(\"orm\");\n        this.action = useService(\"action\");\n        const today = DateTime.now().startOf(\"day\");\n        this.targetDays = {\n            today: {\n                displayDay: today.weekdayShort,\n                actionName: \"action_reschedule_today\",\n            },\n            tomorrow: {\n                displayDay: today.plus({ days: 1 }).weekdayShort,\n                actionName: \"action_reschedule_tomorrow\",\n            },\n            nextWeek: {\n                displayDay: today.plus({ weeks: 1 }).startOf(\"week\").weekdayShort,\n                actionName: \"action_reschedule_nextweek\",\n            },\n        };\n    }\n\n    async rescheduleActivity(click, actionName) {\n        await this.action.doActionButton({\n            type: \"object\",\n            name: actionName,\n            resModel: this.props.record.resModel,\n            resId: this.props.record.resId,\n            onClose: async () => {\n                await this.props.record.model.root.load();\n                this.props.record.model.notify();\n            },\n        });\n        return this.props.record;\n    }\n}\n\n// Version of the widget to use on lists of records inheriting from mail.activity.mixin\nexport class MailActivityMixinListRescheduleDropdown extends MailActivityListRescheduleDropdown {\n    static template = \"mail.MailActivityMixinListRescheduleDropdown\";\n    setup() {\n        super.setup();\n        this.targetDays.today.actionName = \"action_reschedule_my_next_today\";\n        this.targetDays.tomorrow.actionName = \"action_reschedule_my_next_tomorrow\";\n        this.targetDays.nextWeek.actionName = \"action_reschedule_my_next_nextweek\";\n    }\n}\n\nregistry.category(\"view_widgets\").add(\"mail_activity_list_reschedule_dropdown\", {\n    component: MailActivityListRescheduleDropdown,\n    listViewWidth: [50, 100],\n});\n\nregistry.category(\"view_widgets\").add(\"mail_activity_mixin_list_reschedule_dropdown\", {\n    component: MailActivityMixinListRescheduleDropdown,\n    listViewWidth: [50, 100],\n});\n", "import { patch } from \"@web/core/utils/patch\";\nimport { ListRenderer } from \"@web/views/list/list_renderer\";\n\npatch(ListRenderer.prototype, {\n    getPropertyFieldColumns(_, list) {\n        const columns = super.getPropertyFieldColumns(...arguments);\n        for (const column of columns) {\n            const { relation, type } = list.fields[column.name];\n            if (relation === \"res.users\") {\n                column.widget =\n                    type === \"many2one\" ? \"many2one_avatar_user\" : \"many2many_avatar_user\";\n            }\n        }\n        return columns;\n    },\n});\n", "import { SampleServer } from \"@web/model/sample_server\";\nimport { patch } from \"@web/core/utils/patch\";\n\n/**\n * If `activity_exception_decoration` is set, 'Warning' is displayed\n * instead of the last activity, and we don't want to see a bunch of\n * 'Warning's in a list.\n */\npatch(SampleServer.prototype, {\n    _getRandomSelectionValue(modelName, field) {\n        if (field.name === \"activity_exception_decoration\") {\n            return false;\n        }\n        return super._getRandomSelectionValue(...arguments);\n    },\n});\n", "import { FormViewDialog } from \"@web/views/view_dialogs/form_view_dialog\";\nimport { onMounted } from \"@odoo/owl\";\n\nexport class AvatarUserFormViewDialog extends FormViewDialog {\n    setup() {\n        super.setup();\n        Object.assign(this.viewProps, {\n            buttonTemplate: this.props.isToMany\n                ? \"mail.UserFormViewDialog.ToMany.buttons\"\n                : \"mail.UserFormViewDialog.ToOne.buttons\",\n        });\n\n        onMounted(() => {\n            setTimeout(() => {\n                const input = this.modalRef.el.querySelector(\"#name_0\");\n                if (input) {\n                    input.focus();\n                }\n            });\n        });\n    }\n}\n", "import { browser } from \"@web/core/browser/browser\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { patch } from \"@web/core/utils/patch\";\nimport { WebClient } from \"@web/webclient/webclient\";\nimport { onWillDestroy } from \"@odoo/owl\";\nimport { _t } from \"@web/core/l10n/translation\";\n\nconst USER_DEVICES_MODEL = \"mail.push.device\";\n\npatch(WebClient.prototype, {\n    /**\n     * @override\n     */\n    setup() {\n        super.setup();\n        this.orm = useService(\"orm\");\n        this.notification = useService(\"notification\");\n        if (this._canSendNativeNotification) {\n            this.env.bus.addEventListener(\"WEB_CLIENT_READY\", () => this._subscribePush(), {\n                once: true,\n            });\n        }\n        if (browser.navigator.permissions) {\n            let notificationPerm;\n            const onPermissionChange = () => {\n                if (this._canSendNativeNotification) {\n                    this._subscribePush();\n                } else {\n                    this._unsubscribePush();\n                }\n            };\n            browser.navigator.permissions.query({ name: \"notifications\" }).then((perm) => {\n                notificationPerm = perm;\n                notificationPerm.addEventListener(\"change\", onPermissionChange);\n            });\n            onWillDestroy(() => {\n                notificationPerm?.removeEventListener(\"change\", onPermissionChange);\n            });\n        }\n    },\n    /**\n     *\n     * @returns {boolean}\n     * @private\n     */\n    get _canSendNativeNotification() {\n        return browser.Notification?.permission === \"granted\";\n    },\n\n    /**\n     * Subscribe device from push notification\n     *\n     * @private\n     * @return {Promise<void>}\n     */\n    async _subscribePush(numberTry = 1) {\n        await this.serviceWorkerActivatedDeferred;\n        const pushManager = await this.pushManager();\n        if (!pushManager) {\n            return;\n        }\n        let subscription = await pushManager.getSubscription();\n        const previousEndpoint = browser.localStorage.getItem(`${USER_DEVICES_MODEL}_endpoint`);\n        // This may occur if the subscription was refreshed by the browser,\n        // but it may also happen if the subscription has been revoked or lost.\n        if (!subscription) {\n            try {\n                subscription = await pushManager.subscribe({\n                    userVisibleOnly: true,\n                    applicationServerKey: await this._getApplicationServerKey(),\n                });\n            } catch (error) {\n                console.warn(error);\n                this.notification.add(error.message, {\n                    title: _t(\"Failed to enable push notifications\"),\n                    type: \"danger\",\n                    sticky: true,\n                });\n                if (await navigator.brave?.isBrave()) {\n                    this.notification.add(\n                        _t(\n                            \"Brave: enable 'Google Services for Push Messaging' to enable push notifications\"\n                        ),\n                        {\n                            type: \"warning\",\n                            sticky: true,\n                        }\n                    );\n                }\n                return;\n            }\n            browser.localStorage.setItem(`${USER_DEVICES_MODEL}_endpoint`, subscription.endpoint);\n        }\n        const kwargs = subscription.toJSON();\n        if (previousEndpoint && subscription.endpoint !== previousEndpoint) {\n            kwargs.previous_endpoint = previousEndpoint;\n        }\n        try {\n            kwargs.vapid_public_key = this._arrayBufferToBase64(\n                subscription.options.applicationServerKey\n            );\n            await this.orm.call(USER_DEVICES_MODEL, \"register_devices\", [], kwargs);\n        } catch (e) {\n            const invalidVapidErrorClass = \"odoo.addons.mail.tools.jwt.InvalidVapidError\";\n            const warningMessage = \"Error sending subscription information to the server\";\n            if (e.data?.name === invalidVapidErrorClass) {\n                const MAX_TRIES = 2;\n                if (numberTry < MAX_TRIES) {\n                    await subscription.unsubscribe();\n                    this._subscribePush(numberTry + 1);\n                } else {\n                    console.warn(warningMessage);\n                }\n            } else {\n                console.warn(`${warningMessage}: ${e.data?.debug}`);\n            }\n        }\n    },\n\n    /**\n     * Unsubscribe device from push notification\n     *\n     * @private\n     * @return {Promise<void>}\n     */\n    async _unsubscribePush() {\n        await this.serviceWorkerActivatedDeferred;\n        const pushManager = await this.pushManager();\n        if (!pushManager) {\n            return;\n        }\n        const subscription = await pushManager.getSubscription();\n        if (!subscription) {\n            return;\n        }\n        await this.orm.call(USER_DEVICES_MODEL, \"unregister_devices\", [], {\n            endpoint: subscription.endpoint,\n        });\n        await subscription.unsubscribe();\n        browser.localStorage.removeItem(`${USER_DEVICES_MODEL}_endpoint`);\n    },\n\n    /**\n     * Retrieve the PushManager interface of the Push API provides a way to receive notifications from third-party\n     * servers as well as request URLs for push notifications.\n     *\n     * @return {Promise<PushManager>}\n     */\n    async pushManager() {\n        const registration = await browser.navigator.serviceWorker?.getRegistration();\n        return registration?.pushManager;\n    },\n\n    /**\n     *\n     * The Application Server Key is need to be an Uint8Array.\n     * This format is used when the exchanging secret key between client and server.\n     * This base64 to Uint8Array implementation is inspired by https://github.com/gbhasha/base64-to-uint8array\n     *\n     * @private\n     * @return {Uint8Array}\n     */\n    async _getApplicationServerKey() {\n        const vapid_public_key_base64 = await this.orm.call(\n            USER_DEVICES_MODEL,\n            \"get_web_push_vapid_public_key\"\n        );\n        const padding = \"=\".repeat((4 - (vapid_public_key_base64.length % 4)) % 4);\n        const base64 = (vapid_public_key_base64 + padding).replace(/-/g, \"+\").replace(/_/g, \"/\");\n        const rawData = atob(base64);\n        const outputArray = new Uint8Array(rawData.length);\n        for (let i = 0; i < rawData.length; ++i) {\n            outputArray[i] = rawData.charCodeAt(i);\n        }\n        return outputArray;\n    },\n\n    /**\n     * Convert an ArrayBuffer to a base64 string without padding\n     * @param buffer {ArrayBuffer}\n     * @return {string}\n     * @private\n     */\n    _arrayBufferToBase64(buffer) {\n        const bytes = new Uint8Array(buffer);\n        let binary = \"\";\n        for (let i = 0; i < bytes.byteLength; i++) {\n            binary += String.fromCharCode(bytes[i]);\n        }\n        return window.btoa(binary).replaceAll(\"+\", \"-\").replaceAll(\"/\", \"_\").replaceAll(\"=\", \"\");\n    },\n});\n", "import { attClassObjectToString } from \"@mail/utils/common/format\";\nimport { Component, useSubEnv } from \"@odoo/owl\";\nimport { ResizablePanel } from \"@web/core/resizable_panel/resizable_panel\";\nimport { useForwardRefToParent, useService } from \"@web/core/utils/hooks\";\n\n/**\n * @typedef {Object} Props\n * @prop {string} title\n * @prop {Object} [slots]\n * @extends {Component<Props, Env>}\n */\nexport class ActionPanel extends Component {\n    static template = \"mail.ActionPanel\";\n    static components = { ResizablePanel };\n    static props = [\n        \"contentRef?\",\n        \"icon?\",\n        \"title?\",\n        \"resizable?\",\n        \"slots?\",\n        \"initialWidth?\",\n        \"minWidth?\",\n    ];\n    static defaultProps = { contentPadding: true, resizable: true };\n\n    setup() {\n        super.setup();\n        this.store = useService(\"mail.store\");\n        this.ui = useService(\"ui\");\n        useForwardRefToParent(\"contentRef\");\n        useSubEnv({ inDiscussActionPanel: true });\n    }\n\n    get classNames() {\n        return attClassObjectToString({\n            \"o-mail-ActionPanel overflow-auto o-scrollbar-thin d-flex flex-column flex-shrink-0 position-relative py-2 pt-0 h-100 bg-inherit\": true,\n            \"o-mail-ActionPanel-chatter\": this.env.inChatter,\n            \"o-chatWindow\": this.env.inChatWindow,\n            \"px-2\": !this.env.inChatter && !this.env.inMeetingChat,\n            rounded: !this.props.resizable,\n        });\n    }\n\n    get minWidth() {\n        return this.props.minWidth;\n    }\n\n    get initialWidth() {\n        return this.props.initialWidth;\n    }\n}\n", "import { Attachment } from \"@mail/core/common/attachment_model\";\n\nimport { patch } from \"@web/core/utils/patch\";\n\n/** @type {import(\"models\").Attachment} */\nconst attachmentPatch = {\n    get isDeletable() {\n        if (this.message && this.thread?.model === \"discuss.channel\") {\n            return this.message.editable;\n        }\n        return super.isDeletable;\n    },\n};\npatch(Attachment.prototype, attachmentPatch);\n", "import { DateSection } from \"@mail/core/common/date_section\";\nimport { ActionPanel } from \"@mail/discuss/core/common/action_panel\";\nimport { AttachmentList } from \"@mail/core/common/attachment_list\";\n\nimport { Component, onWillStart, onWillUpdateProps } from \"@odoo/owl\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { useSequential, useVisible } from \"@mail/utils/common/hooks\";\n\n/**\n * @typedef {Object} Props\n * @property {import(\"models\").Thread} thread\n * @extends {Component<Props, Env>}\n */\nexport class AttachmentPanel extends Component {\n    static components = { ActionPanel, AttachmentList, DateSection };\n    static props = [\"thread\"];\n    static template = \"mail.AttachmentPanel\";\n\n    setup() {\n        super.setup();\n        this.sequential = useSequential();\n        this.store = useService(\"mail.store\");\n        this.ormService = useService(\"orm\");\n        this.attachmentUploadService = useService(\"mail.attachment_upload\");\n        onWillStart(() => {\n            this.props.thread.fetchMoreAttachments();\n        });\n        onWillUpdateProps((nextProps) => {\n            if (nextProps.thread.notEq(this.props.thread)) {\n                nextProps.thread.fetchMoreAttachments();\n            }\n        });\n        useVisible(\"load-older\", (isVisible) => {\n            if (isVisible) {\n                this.props.thread.fetchMoreAttachments();\n            }\n        });\n    }\n\n    /**\n     * @return {Object<string, import(\"models\").Attachment[]>}\n     */\n    get attachmentsByDate() {\n        const attachmentsByDate = {};\n        for (const attachment of this.props.thread.attachments) {\n            const attachments = attachmentsByDate[attachment.monthYear] ?? [];\n            attachments.push(attachment);\n            attachmentsByDate[attachment.monthYear] = attachments;\n        }\n        return attachmentsByDate;\n    }\n}\n", "import { Component } from \"@odoo/owl\";\n\n/**\n * @typedef {Object} Props\n * @prop {(persona: import(\"models\").Persona) => string} [avatarClass]\n * Function used to determine extra classes for the avatars.\n * @prop {Array} personas List of personas to display in the stack.\n * @prop {\"v\"|\"h\"} [direction] Determine the direction of the\n * stack (vertical or horizontal).\n * @prop {number} [max] Maximum number of personas to display in the stack. An\n * hidden count will be displayed if there are more personas than max.\n * @prop {number} [size] Size of the avatars, in pixel.\n * @extends {Component<Props, Env>}\n */\nexport class AvatarStack extends Component {\n    static template = \"mail.AvatarStack\";\n    static props = {\n        containerClass: { type: String, optional: true },\n        direction: { type: String, optional: true, validate: (d) => [\"v\", \"h\"].includes(d) },\n        avatarClass: { type: Function, optional: true },\n        max: { type: Number, optional: true },\n        onClick: { type: Function, optional: true },\n        personas: Array,\n        size: { type: Number, optional: true },\n        slots: { optional: true },\n    };\n    static defaultProps = {\n        avatarClass: () => \"\",\n        onClick: () => {},\n        max: 4,\n        size: 24,\n        direction: \"h\",\n    };\n\n    getStyle(index) {\n        const styles = [\n            \"box-sizing: content-box\",\n            `height: ${this.props.size}px`,\n            \"margin: 1px\",\n            `padding: 1.5px`,\n            `width: ${this.props.size}px`,\n            `z-index: ${this.props.personas.length - index}`,\n        ];\n        if (index !== 0) {\n            // Compute cumulative offset,\n            const marginDirection = this.props.direction === \"v\" ? \"top\" : \"left\";\n            styles.push(`margin-${marginDirection}: -${this.props.size / 4.5}px`);\n        }\n        return styles.join(\";\");\n    }\n}\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\n\nconst commandRegistry = registry.category(\"discuss.channel_commands\");\n\ncommandRegistry\n    .add(\"help\", {\n        help: _t(\"Show a helper message\"),\n        methodName: \"execute_command_help\",\n    })\n    .add(\"leave\", {\n        help: _t(\"Leave this channel\"),\n        methodName: \"execute_command_leave\",\n    })\n    .add(\"who\", {\n        channel_types: [\"channel\", \"chat\", \"group\"],\n        help: _t(\"List users in the current channel\"),\n        methodName: \"execute_command_who\",\n    });\n", "import { ImStatus } from \"@mail/core/common/im_status\";\nimport { ActionPanel } from \"@mail/discuss/core/common/action_panel\";\n\nimport { Component, onMounted, onWillStart, useEffect, useRef, useState } from \"@odoo/owl\";\n\nimport { useSequential } from \"@mail/utils/common/hooks\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { useDebounced } from \"@web/core/utils/timing\";\n\nexport class ChannelInvitation extends Component {\n    static components = { ImStatus, ActionPanel };\n    static defaultProps = { hasSizeConstraints: false };\n    static props = [\n        \"autofocus?\",\n        \"hasSizeConstraints?\",\n        \"thread?\",\n        \"close?\",\n        \"className?\",\n        \"state?\",\n    ];\n    static template = \"discuss.ChannelInvitation\";\n\n    setup() {\n        super.setup();\n        this.orm = useService(\"orm\");\n        this.store = useService(\"mail.store\");\n        this.rtc = useService(\"discuss.rtc\");\n        this.notification = useService(\"notification\");\n        this.suggestionService = useService(\"mail.suggestion\");\n        this.ui = useService(\"ui\");\n        this.inputRef = useRef(\"input\");\n        this.sequential = useSequential();\n        this.state = useState({\n            searchResultCount: 0,\n            searchStr: \"\",\n            selectableEmails: [],\n            selectablePartners: [],\n            selectedEmails: [],\n            selectedPartners: [],\n            sentEmails: new Set(),\n        });\n        this.debouncedFetchPartnersToInvite = useDebounced(\n            this.fetchPartnersToInvite.bind(this),\n            250\n        );\n        onWillStart(() => {\n            if (this.store.self_partner) {\n                this.fetchPartnersToInvite();\n            }\n        });\n        onMounted(() => {\n            if (this.store.self_partner && this.props.thread) {\n                this.inputRef.el.focus();\n            }\n        });\n        useEffect(\n            () => {\n                if (this.props.autofocus) {\n                    this.inputRef.el?.focus();\n                }\n            },\n            () => [this.props.autofocus]\n        );\n    }\n\n    get selectablePartners() {\n        return this.props.state?.selectablePartners ?? this.state.selectablePartners;\n    }\n\n    set selectablePartners(partners) {\n        if (this.props.state?.selectablePartners) {\n            this.props.state.selectablePartners = partners;\n        } else {\n            this.state.selectablePartners = partners;\n        }\n    }\n\n    get selectedPartners() {\n        return this.props.state?.selectedPartners ?? this.state.selectedPartners;\n    }\n\n    set selectedPartners(partners) {\n        if (this.props.state?.selectedPartners) {\n            this.props.state.selectedPartners = partners;\n        } else {\n            this.state.selectedPartners = partners;\n        }\n    }\n\n    get searchStr() {\n        return this.props.state?.searchStr ?? this.state.searchStr;\n    }\n\n    set searchStr(newSearchStr) {\n        if (this.props.state?.searchStr !== undefined) {\n            this.props.state.searchStr = newSearchStr;\n        } else {\n            this.state.searchStr = newSearchStr;\n        }\n    }\n\n    get showingResultNarrowText() {\n        return _t(\n            \"Showing %(result_count)s results out of %(total_count)s. Narrow your search to see more choices.\",\n            {\n                result_count: this.selectablePartners.length,\n                total_count: this.state.searchResultCount,\n            }\n        );\n    }\n\n    get searchPlaceholder() {\n        if (this.props.thread?.allow_invite_by_email) {\n            return _t(\"Invite people or email\");\n        }\n        return _t(\"Search people to invite\");\n    }\n\n    async fetchPartnersToInvite() {\n        const results = await this.sequential(() =>\n            this.orm.call(\"res.partner\", \"search_for_channel_invite\", [\n                this.searchStr,\n                this.props.thread?.id ?? false,\n            ])\n        );\n        if (!results) {\n            return;\n        }\n        this.store.insert(results.store_data);\n        const selectablePartners = results.partner_ids.map((id) =>\n            this.store[\"res.partner\"].get(id)\n        );\n        this.selectablePartners = this.suggestionService.sortPartnerSuggestions(\n            selectablePartners,\n            this.searchStr,\n            this.props.thread\n        );\n        this.state.searchResultCount = results[\"count\"];\n        const selectableEmails = this.state.selectedEmails.filter((addr) =>\n            addr.includes(this.searchStr)\n        );\n        if (results.selectable_email) {\n            selectableEmails.push(results.selectable_email);\n        }\n        if (results.email_already_sent) {\n            this.state.sentEmails.add(results.selectable_email);\n        }\n        this.state.selectableEmails = [...new Set(selectableEmails)];\n    }\n\n    onInput() {\n        this.searchStr = this.inputRef.el.value;\n        this.debouncedFetchPartnersToInvite();\n    }\n\n    onClickSelectablePartner(partner) {\n        if (partner.in(this.selectedPartners)) {\n            const index = this.selectedPartners.indexOf(partner);\n            if (index !== -1) {\n                this.selectedPartners.splice(index, 1);\n            }\n            return;\n        }\n        this.selectedPartners.push(partner);\n    }\n\n    onClickSelectableEmail(email) {\n        const index = this.state.selectedEmails.indexOf(email);\n        if (index !== -1) {\n            this.state.selectedEmails.splice(index, 1);\n            return;\n        }\n        this.state.selectedEmails.push(email);\n    }\n\n    onClickSelectedPartner(partner) {\n        const index = this.selectedPartners.indexOf(partner);\n        this.selectedPartners.splice(index, 1);\n    }\n\n    onClickSelectedEmail(email) {\n        const index = this.state.selectedEmails.indexOf(email);\n        this.state.selectedEmails.splice(index, 1);\n    }\n\n    onFocusInvitationLinkInput(ev) {\n        ev.target.select();\n    }\n\n    async onClickCopy(ev) {\n        let notification = _t(\"Invitation link copied!\");\n        let type = \"success\";\n        const clipboard = this.env.inDiscussCallView?.isPip\n            ? this.rtc.pipService.pipWindow?.navigator.clipboard\n            : navigator.clipboard;\n        try {\n            await clipboard.writeText(this.props.thread.invitationLink);\n        } catch {\n            notification = _t(\"Invitation link copy failed (Permission denied?)!\");\n            type = \"danger\";\n        }\n        this.notification.add(notification, { type });\n    }\n\n    async onClickInvite() {\n        if (this.props.thread.channel_type === \"chat\") {\n            const partnerIds = this.selectedPartners.map((partner) => partner.id);\n            if (this.props.thread.correspondent?.partner_id) {\n                partnerIds.unshift(this.props.thread.correspondent.partner_id.id);\n            }\n            await this.store.startChat(partnerIds);\n            return;\n        }\n        const invitePromises = [];\n        if (this.selectedPartners.length) {\n            invitePromises.push(\n                this.orm.call(\"discuss.channel\", \"add_members\", [[this.props.thread.id]], {\n                    partner_ids: this.selectedPartners.map((partner) => partner.id),\n                    invite_to_rtc_call: this.rtc.state.channel?.eq(this.props.thread),\n                })\n            );\n        }\n        if (this.state.selectedEmails.length) {\n            invitePromises.push(\n                this.orm.call(\"discuss.channel\", \"invite_by_email\", [this.props.thread.id], {\n                    emails: this.state.selectedEmails,\n                })\n            );\n        }\n        await Promise.all(invitePromises);\n        this.state.selectedEmails = [];\n        this.state.selectedPartners = [];\n        this.props.close?.();\n    }\n\n    get invitationButtonText() {\n        if (!this.props.thread) {\n            return \"\";\n        }\n        if (this.props.thread.channel_type === \"channel\") {\n            return _t(\"Invite\");\n        } else if (this.props.thread.channel_type === \"group\") {\n            return _t(\"Invite to Group Chat\");\n        } else if (this.props.thread.channel_type === \"chat\") {\n            if (this.props.thread.correspondent?.persona.eq(this.store.self)) {\n                if (this.selectedPartners.length === 0) {\n                    return _t(\"Invite\");\n                }\n                if (this.selectedPartners.length === 1) {\n                    const alreadyChat = Object.values(this.store.Thread.records).some(\n                        (thread) =>\n                            thread.channel_type === \"chat\" &&\n                            thread.correspondent?.partner_id?.eq(this.selectedPartners[0])\n                    );\n                    if (alreadyChat) {\n                        return _t(\"Go to conversation\");\n                    }\n                    return _t(\"Start a Conversation\");\n                }\n            }\n            return _t(\"Create Group Chat\");\n        }\n        return _t(\"Invite\");\n    }\n}\n", "import { ImStatus } from \"@mail/core/common/im_status\";\nimport { ActionPanel } from \"@mail/discuss/core/common/action_panel\";\n\nimport { Component, onWillUpdateProps, onWillStart } from \"@odoo/owl\";\nimport { _t } from \"@web/core/l10n/translation\";\n\nimport { useService } from \"@web/core/utils/hooks\";\n\nexport class ChannelMemberList extends Component {\n    static components = { ImStatus, ActionPanel };\n    static props = [\"thread\", \"openChannelInvitePanel\", \"className?\"];\n    static template = \"discuss.ChannelMemberList\";\n\n    setup() {\n        super.setup();\n        this.store = useService(\"mail.store\");\n        onWillStart(() => {\n            if (this.props.thread.fetchMembersState === \"not_fetched\") {\n                this.props.thread.fetchChannelMembers();\n            }\n        });\n        onWillUpdateProps((nextProps) => {\n            if (nextProps.thread.fetchMembersState === \"not_fetched\") {\n                nextProps.thread.fetchChannelMembers();\n            }\n        });\n    }\n\n    get onlineSectionText() {\n        return _t(\"Online - %(online_count)s\", {\n            online_count: this.props.thread.onlineMembers.length,\n        });\n    }\n\n    get offlineSectionText() {\n        return _t(\"Offline - %(offline_count)s\", {\n            offline_count: this.props.thread.offlineMembers.length,\n        });\n    }\n\n    canOpenChatWith(member) {\n        if (this.store.inPublicPage) {\n            return false;\n        }\n        if (member.guest_id) {\n            return false;\n        }\n        return true;\n    }\n\n    onClickAvatar(ev, member) {\n        if (!this.canOpenChatWith(member)) {\n            return;\n        }\n        this.store.openChat({ partnerId: member.partner_id.id });\n    }\n}\n", "import { Store } from \"@mail/core/common/store_service\";\nimport { fields, Record } from \"@mail/core/common/record\";\n\nimport { browser } from \"@web/core/browser/browser\";\nimport { deserializeDateTime } from \"@web/core/l10n/dates\";\nimport { user } from \"@web/core/user\";\n\nconst { DateTime } = luxon;\n\nexport class ChannelMember extends Record {\n    static _name = \"discuss.channel.member\";\n    static id = \"id\";\n\n    /** @type {string} */\n    create_date;\n    /** @type {string} */\n    custom_channel_name;\n    /**\n     * false means using the custom_notifications from user settings.\n     *\n     * @type {false|\"all\"|\"mentions\"|\"no_notif\"}\n     */\n    custom_notifications;\n    /** @type {number} */\n    id;\n    is_pinned = fields.Attr(undefined, {\n        compute() {\n            return (\n                !this.unpin_dt ||\n                (this.last_interest_dt && this.last_interest_dt >= this.unpin_dt) ||\n                (this.channel_id?.last_interest_dt &&\n                    this.channel_id?.last_interest_dt >= this.unpin_dt)\n            );\n        },\n        /** @this {import(\"models\").ChannelMember} */\n        onUpdate() {\n            this.channel_id?.onPinStateUpdated();\n        },\n    });\n    last_interest_dt = fields.Datetime();\n    last_seen_dt = fields.Datetime();\n    guest_id = fields.One(\"mail.guest\");\n    partner_id = fields.One(\"res.partner\");\n    get persona() {\n        return this.partner_id || this.guest_id;\n    }\n    channel_id = fields.One(\"Thread\", { inverse: \"channel_member_ids\" });\n    threadAsSelf = fields.One(\"Thread\", {\n        compute() {\n            if (this.store.self?.eq(this.persona)) {\n                return this.channel_id;\n            }\n        },\n    });\n    fetched_message_id = fields.One(\"mail.message\");\n    seen_message_id = fields.One(\"mail.message\");\n    hideUnreadBanner = false;\n    message_unread_counter = fields.Attr(0, {\n        /** @this {import(\"models\").ChannelMember} */\n        onUpdate() {\n            if (\n                this.message_unread_counter === 0 ||\n                !this.channel_id?.isDisplayed ||\n                this.channel_id?.scrollTop !== \"bottom\" ||\n                this.channel_id.markedAsUnread ||\n                !this.channel_id.isFocused\n            ) {\n                this.message_unread_counter_ui = this.message_unread_counter;\n            }\n        },\n    });\n    message_unread_counter_ui = 0;\n    message_unread_counter_bus_id = 0;\n    mute_until_dt = fields.Datetime();\n    new_message_separator = fields.Attr(null, {\n        /** @this {import(\"models\").ChannelMember} */\n        onUpdate() {\n            if (!this.channel_id?.isDisplayed) {\n                this.new_message_separator_ui = this.new_message_separator;\n            }\n        },\n    });\n    new_message_separator_ui = null;\n    isTyping = false;\n    is_typing_dt = fields.Datetime({\n        onUpdate() {\n            browser.clearTimeout(this.typingTimeoutId);\n            if (\n                !this.is_typing_dt ||\n                DateTime.now().diff(this.is_typing_dt).milliseconds > Store.OTHER_LONG_TYPING\n            ) {\n                this.isTyping = false;\n            }\n            if (this.isTyping) {\n                this.typingTimeoutId = browser.setTimeout(\n                    () => (this.isTyping = false),\n                    Store.OTHER_LONG_TYPING\n                );\n            }\n        },\n    });\n    threadAsTyping = fields.One(\"Thread\", {\n        compute() {\n            return this.isTyping ? this.channel_id : undefined;\n        },\n        eager: true,\n        onDelete() {\n            browser.clearTimeout(this.typingTimeoutId);\n        },\n    });\n    /** @type {number} */\n    typingTimeoutId;\n    unpin_dt = fields.Datetime();\n\n    get name() {\n        if (this.guest_id) {\n            return this.guest_id.name;\n        }\n        return this.channel_id.getPersonaName(this.partner_id);\n    }\n\n    get avatarUrl() {\n        return this.partner_id?.avatarUrl || this.guest_id?.avatarUrl;\n    }\n\n    get im_status() {\n        return this.partner_id?.im_status || this.guest_id?.im_status;\n    }\n\n    /**\n     * @returns {string}\n     */\n    getLangName() {\n        return this.persona.lang_name;\n    }\n\n    get memberSince() {\n        return this.create_date ? deserializeDateTime(this.create_date) : undefined;\n    }\n\n    /**\n     * @param {import(\"models\").Message} message\n     */\n    hasSeen(message) {\n        return this.persona.eq(message.author) || this.seen_message_id?.id >= message.id;\n    }\n    get lastSeenDt() {\n        return this.last_seen_dt\n            ? this.last_seen_dt.toLocaleString(DateTime.TIME_24_SIMPLE, {\n                  locale: user.lang,\n              })\n            : undefined;\n    }\n}\n\nChannelMember.register();\n", "import { ActionPanel } from \"@mail/discuss/core/common/action_panel\";\n\nimport { Component } from \"@odoo/owl\";\n\nimport { rpc } from \"@web/core/network/rpc\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { useService } from \"@web/core/utils/hooks\";\n\nexport class DeleteThreadDialog extends Component {\n    static components = { ActionPanel };\n    static props = [\"thread\", \"close\"];\n    static template = \"discuss.DeleteThreadDialog\";\n\n    setup() {\n        super.setup();\n        this.store = useService(\"mail.store\");\n    }\n\n    async onConfirmation() {\n        let toOpenThread;\n        const threadName = this.props.thread.name;\n        if (this.store.discuss?.thread?.eq(this.props.thread) || this.env.inChatWindow) {\n            toOpenThread = this.props.thread.parent_channel_id;\n        }\n        await rpc(\"/discuss/channel/sub_channel/delete\", {\n            sub_channel_id: this.props.thread.id,\n        });\n        if (toOpenThread?.exists()) {\n            toOpenThread.open();\n        }\n        this.props.close();\n        this.env.services.notification.add(\n            _t('Thread \"%(thread_name)s\" has been deleted', { thread_name: threadName }),\n            { type: \"info\" }\n        );\n    }\n}\n", "import { markup, reactive } from \"@odoo/owl\";\n\nimport { registry } from \"@web/core/registry\";\n\nexport class DiscussCoreCommon {\n    /**\n     * @param {import(\"@web/env\").OdooEnv} env\n     * @param {import(\"services\").ServiceFactories} services\n     */\n    constructor(env, services) {\n        this.busService = services.bus_service;\n        this.env = env;\n        this.notificationService = services.notification;\n        this.orm = services.orm;\n        this.presence = services.presence;\n        this.store = services[\"mail.store\"];\n    }\n\n    setup() {\n        this.busService.subscribe(\"discuss.channel/delete\", (payload, metadata) => {\n            const thread = this.store.Thread.insert({\n                id: payload.id,\n                model: \"discuss.channel\",\n            });\n            this._handleNotificationChannelDelete(thread, metadata);\n        });\n        this.busService.subscribe(\"discuss.channel/new_message\", (payload, metadata) => {\n            // Insert should always be done before any async operation. Indeed,\n            // awaiting before the insertion could lead to overwritting newer\n            // state coming from more recent `mail.record/insert` notifications.\n            this.store.insert(payload.data);\n            this._handleNotificationNewMessage(payload, metadata);\n        });\n        this.busService.subscribe(\"discuss.channel/transient_message\", (payload) => {\n            const { body, channel_id } = payload;\n            const lastMessageId = this.store.getLastMessageId();\n            const message = this.store[\"mail.message\"].insert({\n                author_id: this.store.odoobot,\n                body: markup(body),\n                id: lastMessageId + 0.01,\n                subtype_id: this.store.mt_note,\n                is_transient: true,\n                thread: { id: channel_id, model: \"discuss.channel\" },\n            });\n            message.thread.messages.push(message);\n            message.thread.transientMessages.push(message);\n        });\n        this.busService.subscribe(\"discuss.channel.member/fetched\", (payload) => {\n            const { channel_id, id, last_message_id, partner_id } = payload;\n            this.store[\"discuss.channel.member\"].insert({\n                id,\n                fetched_message_id: { id: last_message_id },\n                partner_id: { id: partner_id },\n                thread: { id: channel_id, model: \"discuss.channel\" },\n            });\n        });\n        this.env.bus.addEventListener(\"mail.message/delete\", ({ detail: { message, notifId } }) => {\n            if (message.thread) {\n                const { self_member_id } = message.thread;\n                if (\n                    message.id > self_member_id?.seen_message_id.id &&\n                    notifId > self_member_id.message_unread_counter_bus_id\n                ) {\n                    self_member_id.message_unread_counter--;\n                }\n            }\n        });\n    }\n\n    /**\n     * @param {import(\"models\").Thread} thread\n     * @param {{ notifId: number}} metadata\n     */\n    async _handleNotificationChannelDelete(thread, metadata) {\n        await thread.closeChatWindow({ force: true });\n        thread.messages.splice(0, thread.messages.length);\n        thread.delete();\n    }\n\n    async _handleNotificationNewMessage(payload, { id: notifId }) {\n        const { data, id: channelId, silent, temporary_id } = payload;\n        const channel = await this.store.Thread.getOrFetch({\n            model: \"discuss.channel\",\n            id: channelId,\n        });\n        if (!channel) {\n            return;\n        }\n        const message = this.store[\"mail.message\"].get(data[\"mail.message\"][0]);\n        if (!message) {\n            return;\n        }\n        if (message.notIn(channel.messages)) {\n            if (!channel.loadNewer) {\n                channel.addOrReplaceMessage(message, this.store[\"mail.message\"].get(temporary_id));\n            } else if (channel.status === \"loading\") {\n                channel.pendingNewMessages.push(message);\n            }\n            if (message.isSelfAuthored) {\n                channel.onNewSelfMessage(message);\n            } else {\n                if (channel.isDisplayed && channel.self_member_id?.new_message_separator_ui === 0) {\n                    channel.self_member_id.new_message_separator_ui = message.id;\n                }\n                if (!channel.isDisplayed && channel.self_member_id) {\n                    channel.scrollUnread = true;\n                }\n                if (\n                    notifId > channel.self_member_id?.message_unread_counter_bus_id &&\n                    !message.isNotification\n                ) {\n                    channel.self_member_id.message_unread_counter++;\n                }\n            }\n        }\n        if (\n            channel.channel_type !== \"channel\" &&\n            this.store.self_partner &&\n            channel.self_member_id\n        ) {\n            // disabled on non-channel threads and\n            // on \"channel\" channels for performance reasons\n            channel.markAsFetched();\n        }\n        if (\n            !channel.loadNewer &&\n            !message.isSelfAuthored &&\n            channel.composer.isFocused &&\n            this.store.self_partner &&\n            channel.newestPersistentMessage?.eq(channel.newestMessage) &&\n            !channel.markedAsUnread\n        ) {\n            channel.markAsRead();\n        }\n        this.env.bus.trigger(\"discuss.channel/new_message\", { channel, message, silent });\n        const authorMember = channel.channel_member_ids.find((member) =>\n            member.persona?.eq(message.author)\n        );\n        if (authorMember) {\n            authorMember.seen_message_id = message;\n        }\n    }\n}\n\nexport const discussCoreCommon = {\n    dependencies: [\n        \"bus_service\",\n        \"mail.out_of_focus\",\n        \"mail.store\",\n        \"notification\",\n        \"orm\",\n        \"presence\",\n    ],\n    /**\n     * @param {import(\"@web/env\").OdooEnv} env\n     * @param {import(\"services\").ServiceFactories} services\n     */\n    start(env, services) {\n        const discussCoreCommon = reactive(new DiscussCoreCommon(env, services));\n        discussCoreCommon.setup(env, services);\n        return discussCoreCommon;\n    },\n};\n\nregistry.category(\"services\").add(\"discuss.core.common\", discussCoreCommon);\n", "import { Component, useState } from \"@odoo/owl\";\nimport { useService } from \"@web/core/utils/hooks\";\n\nexport class DiscussNotificationSettings extends Component {\n    static props = {};\n    static template = \"mail.DiscussNotificationSettings\";\n\n    setup() {\n        this.store = useService(\"mail.store\");\n        this.state = useState({\n            selectedDuration: false,\n        });\n    }\n\n    onChangeMessageSound() {\n        this.store.settings.messageSound = !this.store.settings.messageSound;\n    }\n}\n", "import { Component, xml } from \"@odoo/owl\";\nimport { registry } from \"@web/core/registry\";\n\nimport { DiscussNotificationSettings } from \"@mail/discuss/core/common/discuss_notification_settings\";\n\nexport class DiscussNotificationSettingsClientAction extends Component {\n    static components = { DiscussNotificationSettings };\n    static props = [\"*\"];\n    static template = xml`\n        <div class=\"o-mail-DiscussNotificationSettingsClientAction mx-3 my-2\">\n            <DiscussNotificationSettings/>\n        </div>\n    `;\n}\n\nregistry\n    .category(\"actions\")\n    .add(\"mail.discuss_notification_settings_action\", DiscussNotificationSettingsClientAction);\n", "import { MailGuest } from \"@mail/core/common/mail_guest_model\";\nimport { fields } from \"@mail/core/common/record\";\n\nimport { patch } from \"@web/core/utils/patch\";\n\n/** @type {import(\"models\").MailGuest} */\nconst mailGuestPatch = {\n    setup() {\n        super.setup();\n        this.channelMembers = fields.Many(\"discuss.channel.member\");\n    },\n};\npatch(MailGuest.prototype, mailGuestPatch);\n", "import { registerMessageAction } from \"@mail/core/common/message_actions\";\n\nimport { toRaw } from \"@odoo/owl\";\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { rpc } from \"@web/core/network/rpc\";\n\nregisterMessageAction(\"set-new-message-separator\", {\n    condition: ({ message, thread }) =>\n        thread &&\n        thread.self_member_id &&\n        thread.eq(message.thread) &&\n        !message.hasNewMessageSeparator &&\n        message.persistent,\n    icon: \"fa fa-eye-slash\",\n    name: _t(\"Mark as Unread\"),\n    onSelected: ({ message: msg }) => {\n        const message = toRaw(msg);\n        const selfMember = message.thread?.self_member_id;\n        if (selfMember) {\n            selfMember.new_message_separator = message.id;\n            selfMember.new_message_separator_ui = selfMember.new_message_separator;\n        }\n        message.thread.markedAsUnread = true;\n        rpc(\"/discuss/channel/set_new_message_separator\", {\n            channel_id: message.thread.id,\n            message_id: message.id,\n        });\n    },\n    sequence: 70,\n});\n", "import { Message } from \"@mail/core/common/message_model\";\nimport { fields } from \"@mail/core/common/record\";\n\nimport { patch } from \"@web/core/utils/patch\";\n\n/** @type {import(\"models\").Message} */\nconst messagePatch = {\n    setup() {\n        super.setup();\n        this.hasEveryoneSeen = fields.Attr(false, {\n            /** @this {import(\"models\").Message} */\n            compute() {\n                return this.thread?.membersThatCanSeen.every((m) => m.hasSeen(this));\n            },\n        });\n        this.hasNewMessageSeparator = fields.Attr(false, {\n            compute() {\n                // compute for caching the value and not re-rendering all\n                // messages when new_message_separator changes\n                return this.thread?.self_member_id?.new_message_separator === this.id;\n            },\n        });\n        this.hasSomeoneFetched = fields.Attr(false, {\n            /** @this {import(\"models\").Message} */\n            compute() {\n                return this.thread?.channel_member_ids.some(\n                    (m) => m.persona.notEq(this.author) && m.fetched_message_id?.id >= this.id\n                );\n            },\n        });\n        this.hasSomeoneSeen = fields.Attr(false, {\n            /** @this {import(\"models\").Message} */\n            compute() {\n                return this.thread?.membersThatCanSeen\n                    .filter((member) => member.persona.notEq(this.author))\n                    .some((m) => m.hasSeen(this));\n            },\n        });\n        this.isMessagePreviousToLastSelfMessageSeenByEveryone = fields.Attr(false, {\n            /** @this {import(\"models\").Message} */\n            compute() {\n                if (!this.thread?.lastSelfMessageSeenByEveryone) {\n                    return false;\n                }\n                return this.id < this.thread.lastSelfMessageSeenByEveryone.id;\n            },\n        });\n        /** @type {Promise<Thread>[]} @deprecated */\n        this.mentionedChannelPromises = [];\n        this.threadAsFirstUnread = fields.One(\"Thread\", { inverse: \"firstUnreadMessage\" });\n    },\n    /** @returns {import(\"models\").ChannelMember[]} */\n    get channelMemberHaveSeen() {\n        return this.thread.membersThatCanSeen.filter(\n            (m) => m.hasSeen(this) && m.persona.notEq(this.author)\n        );\n    },\n    /**\n     * @override\n     */\n    async edit(\n        body,\n        attachments = [],\n        { mentionedChannels = [], mentionedPartners = [], mentionedRoles = [] } = {}\n    ) {\n        return await super.edit(body, attachments, {\n            mentionedChannels,\n            mentionedPartners,\n            mentionedRoles,\n        });\n    },\n};\npatch(Message.prototype, messagePatch);\n", "import { Message } from \"@mail/core/common/message\";\nimport { MessageSeenIndicator } from \"@mail/discuss/core/common/message_seen_indicator\";\n\nimport { patch } from \"@web/core/utils/patch\";\n\nMessage.components = { ...Message.components, MessageSeenIndicator };\n\n/** @type {Message} */\nconst messagePatch = {\n    get showSeenIndicator() {\n        return this.props.message.isSelfAuthored && this.props.thread?.hasSeenFeature;\n    },\n};\npatch(Message.prototype, messagePatch);\n", "import { Component, useExternalListener, useRef } from \"@odoo/owl\";\nimport { Dialog } from \"@web/core/dialog/dialog\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { browser } from \"@web/core/browser/browser\";\n\nclass MessageSeenIndicatorDialog extends Component {\n    static components = { Dialog };\n    static template = \"mail.MessageSeenIndicatorDialog\";\n    static props = [\"message\", \"close?\"];\n\n    setup() {\n        super.setup();\n        this.contentRef = useRef(\"content\");\n        useExternalListener(\n            browser,\n            \"click\",\n            (ev) => {\n                if (!this.contentRef?.el.contains(ev.target)) {\n                    this.props.close();\n                }\n            },\n            true\n        );\n    }\n}\n\n/**\n * @typedef {Object} Props\n * @property {import(\"models\").Message} message\n * @property {import(\"models\").Thread} thread\n * @extends {Component<Props, Env>}\n */\nexport class MessageSeenIndicator extends Component {\n    static template = \"mail.MessageSeenIndicator\";\n    static props = [\"message\", \"thread\", \"className?\"];\n\n    setup() {\n        super.setup();\n        this.dialog = useService(\"dialog\");\n    }\n\n    get summary() {\n        if (this.props.message.hasEveryoneSeen) {\n            if (this.props.thread.channel_member_ids.length === 2) {\n                return _t(\"Seen by %(user)s\", { user: this.props.thread.correspondent.name });\n            }\n            return _t(\"Seen by everyone\");\n        }\n        const seenMembers = this.props.message.channelMemberHaveSeen;\n        const [user1, user2, user3] = seenMembers.map((member) => member.name);\n        switch (seenMembers.length) {\n            case 0:\n                return _t(\"Sent\");\n            case 1:\n                return _t(\"Seen by %(user)s\", { user: user1 });\n            case 2:\n                return _t(\"Seen by %(user1)s and %(user2)s\", { user1, user2 });\n            case 3:\n                return _t(\"Seen by %(user1)s, %(user2)s and %(user3)s\", { user1, user2, user3 });\n            case 4:\n                return _t(\"Seen by %(user1)s, %(user2)s, %(user3)s and 1 other\", {\n                    user1,\n                    user2,\n                    user3,\n                });\n            default:\n                return _t(\"Seen by %(user1)s, %(user2)s, %(user3)s and %(count)s others\", {\n                    user1,\n                    user2,\n                    user3,\n                    count: seenMembers.length - 3,\n                });\n        }\n    }\n\n    openDialog() {\n        if (this.props.message.channelMemberHaveSeen.length === 0) {\n            return;\n        }\n        this.dialog.add(MessageSeenIndicatorDialog, { message: this.props.message });\n    }\n}\n", "import { Component, xml } from \"@odoo/owl\";\nimport { ActionPanel } from \"@mail/discuss/core/common/action_panel\";\nimport { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { DiscussNotificationSettingsClientAction } from \"./discuss_notification_settings_client_action\";\nimport { Dialog } from \"@web/core/dialog/dialog\";\n\nclass NotificationDialog extends Component {\n    static props = [\"close?\"];\n    static components = { Dialog, DiscussNotificationSettingsClientAction };\n    static template = xml`\n        <Dialog size=\"'md'\" footer=\"false\">\n            <DiscussNotificationSettingsClientAction/>\n        </Dialog>\n    `;\n}\n\nexport class NotificationSettings extends Component {\n    static components = { ActionPanel, Dropdown, DropdownItem };\n    static props = [\"hasSizeConstraints?\", \"thread\", \"close?\", \"className?\"];\n    static template = \"discuss.NotificationSettings\";\n\n    setup() {\n        this.store = useService(\"mail.store\");\n        this.dialog = useService(\"dialog\");\n        this.ui = useService(\"ui\");\n    }\n\n    setMute(minutes) {\n        this.store.settings.setMuteDuration(minutes, this.props.thread);\n        this.props.close?.();\n    }\n\n    onClickAllConversationsMuted() {\n        this.dialog.add(NotificationDialog);\n    }\n}\n", "import { partnerCompareRegistry } from \"@mail/core/common/partner_compare\";\n\npartnerCompareRegistry.add(\n    \"discuss.recent-chats\",\n    (p1, p2, { env, context }) => {\n        const recentChatPartnerIds =\n            context.recentChatPartnerIds || env.services[\"mail.store\"].getRecentChatPartnerIds();\n        const recentChatIndex_p1 = recentChatPartnerIds.findIndex(\n            (partnerId) => partnerId === p1.id\n        );\n        const recentChatIndex_p2 = recentChatPartnerIds.findIndex(\n            (partnerId) => partnerId === p2.id\n        );\n        if (recentChatIndex_p1 !== -1 && recentChatIndex_p2 === -1) {\n            return -1;\n        } else if (recentChatIndex_p1 === -1 && recentChatIndex_p2 !== -1) {\n            return 1;\n        } else if (recentChatIndex_p1 < recentChatIndex_p2) {\n            return -1;\n        } else if (recentChatIndex_p1 > recentChatIndex_p2) {\n            return 1;\n        }\n    },\n    { sequence: 25 }\n);\n\npartnerCompareRegistry.add(\n    \"discuss.members\",\n    (p1, p2, { thread, context: { memberPartnerIds } }) => {\n        if (thread?.model === \"discuss.channel\") {\n            const isMember1 = memberPartnerIds.has(p1.id);\n            const isMember2 = memberPartnerIds.has(p2.id);\n            if (isMember1 && !isMember2) {\n                return -1;\n            }\n            if (!isMember1 && isMember2) {\n                return 1;\n            }\n        }\n    },\n    { sequence: 40 }\n);\n", "import { ResPartner } from \"@mail/core/common/res_partner_model\";\nimport { fields } from \"@mail/core/common/record\";\n\nimport { patch } from \"@web/core/utils/patch\";\n\n/** @type {import(\"models\").Persona} */\nconst resPartnerPatch = {\n    setup() {\n        super.setup();\n        this.channelMembers = fields.Many(\"discuss.channel.member\");\n    },\n};\npatch(ResPartner.prototype, resPartnerPatch);\n", "import { Store } from \"@mail/core/common/store_service\";\nimport { compareDatetime } from \"@mail/utils/common/misc\";\n\nimport { patch } from \"@web/core/utils/patch\";\nimport { debounce } from \"@web/core/utils/timing\";\n\n/** @type {import(\"models\").Store} */\nconst storeServicePatch = {\n    /** @override */\n    setup() {\n        super.setup();\n        /** @type {Map<number, Deferred>} */\n        this.channelIdsFetchingDeferred = new Map();\n        /**\n         * Defines channel types that have the message seen indicator/info feature.\n         * @see `discuss.channel`._types_allowing_seen_infos()\n         *\n         * @type {string[]}\n         */\n        this.channel_types_with_seen_infos = [];\n        this.updateBusSubscription = debounce(\n            () => this.env.services.bus_service.forceUpdateChannels(),\n            0\n        );\n    },\n    get onlineMemberStatuses() {\n        return [\"away\", \"bot\", \"busy\", \"online\"];\n    },\n    /**\n     * @param {Object} param0\n     * @param {string} param0.default_display_mode\n     * @param {number[]} param0.partners_to\n     * @param {string} param0.name\n     * @returns {Promise<import(\"models\").Thread>}\n     */\n    async createGroupChat({ default_display_mode, partners_to, name }) {\n        const { channel } = await this.fetchStoreData(\n            \"/discuss/create_group\",\n            { default_display_mode, partners_to, name },\n            { readonly: false, requestData: true }\n        );\n        await channel.open({ focus: true });\n        return channel;\n    },\n    /** @param {number} channelId */\n    async fetchChannel(channelId) {\n        const fetchParam = this.fetchParams.find(([name]) => name === \"discuss.channel\");\n        if (fetchParam) {\n            const [, channelIds, dataRequest] = fetchParam;\n            channelIds.push(channelId);\n            await dataRequest._resultDef;\n        } else {\n            await this.fetchStoreData(\"discuss.channel\", [channelId]);\n        }\n    },\n    /**\n     * List of known partner ids with a direct chat, ordered\n     * by most recent interest (1st item being the most recent)\n     *\n     * @returns {number[]}\n     */\n    getRecentChatPartnerIds() {\n        return Object.values(this.Thread.records)\n            .filter((thread) => thread.channel_type === \"chat\" && thread.correspondent?.partner_id)\n            .sort((a, b) => compareDatetime(b.lastInterestDt, a.lastInterestDt) || b.id - a.id)\n            .map((thread) => thread.correspondent.partner_id.id);\n    },\n    /**\n     * @param {import(\"models\").ChannelMember} m1\n     * @param {import(\"models\").ChannelMember} m2\n     */\n    sortMembers(m1, m2) {\n        return m1.name?.localeCompare(m2.name) || m1.id - m2.id;\n    },\n    /** @param {number[]} partnerIds */\n    async startChat(partnerIds) {\n        const partners_to = [...new Set([this.self.id, ...partnerIds])];\n        if (partners_to.length === 1) {\n            const chat = await this.joinChat(partners_to[0], true);\n            chat.open({ focus: true, bypassCompact: true });\n        } else if (partners_to.length === 2) {\n            const correspondentId = partners_to.find(\n                (partnerId) => partnerId !== this.store.self.id\n            );\n            const chat = await this.joinChat(correspondentId, true);\n            chat.open({ focus: true, bypassCompact: true });\n        } else {\n            await this.createGroupChat({ partners_to });\n        }\n    },\n};\n\npatch(Store.prototype, storeServicePatch);\n", "import { SuggestionService } from \"@mail/core/common/suggestion_service\";\nimport { cleanTerm } from \"@mail/utils/common/format\";\n\nimport { registry } from \"@web/core/registry\";\nimport { patch } from \"@web/core/utils/patch\";\n\nconst commandRegistry = registry.category(\"discuss.channel_commands\");\n\n/** @type {SuggestionService} */\nconst suggestionServicePatch = {\n    getSupportedDelimiters(thread, env) {\n        const res = super.getSupportedDelimiters(...arguments);\n        return thread?.model === \"discuss.channel\" ? [...res, [\"/\", 0]] : res;\n    },\n    /**\n     * @override\n     */\n    isSuggestionValid(partner, thread) {\n        if (thread?.model === \"discuss.channel\" && partner.eq(this.store.odoobot)) {\n            return true;\n        }\n        return super.isSuggestionValid(...arguments);\n    },\n    /**\n     * @override\n     */\n    getPartnerSuggestions(thread) {\n        const isNonPublicChannel =\n            thread &&\n            (thread.channel_type === \"group\" ||\n                thread.channel_type === \"chat\" ||\n                (thread.channel_type === \"channel\" &&\n                    (thread.parent_channel_id || thread).group_public_id));\n        if (isNonPublicChannel) {\n            // Only return the channel members when in the context of a\n            // group restricted channel. Indeed, the message with the mention\n            // would be notified to the mentioned partner, so this prevents\n            // from inadvertently leaking the private message to the\n            // mentioned partner.\n            const partnersById = new Map(\n                [\n                    ...thread.channel_member_ids,\n                    ...(thread.parent_channel_id?.channel_member_ids ?? []),\n                ]\n                    .filter((m) => m.partner_id)\n                    .map((m) => [m.partner_id.id, m.partner_id])\n            );\n            if (thread.channel_type === \"channel\") {\n                const group = (thread.parent_channel_id || thread).group_public_id;\n                group.partners.forEach((partner) => partnersById.set(partner.id, partner));\n            }\n            return Array.from(partnersById.values());\n        } else {\n            return super.getPartnerSuggestions(...arguments);\n        }\n    },\n    /**\n     * @override\n     */\n    searchSuggestions({ delimiter, term }, { thread } = {}) {\n        if (delimiter === \"/\") {\n            return this.searchChannelCommand(cleanTerm(term), thread);\n        }\n        return super.searchSuggestions(...arguments);\n    },\n    searchChannelCommand(cleanedSearchTerm, thread) {\n        if (!thread.model === \"discuss.channel\") {\n            // channel commands are channel specific\n            return;\n        }\n        const commands = commandRegistry\n            .getEntries()\n            .filter(([name, command]) => {\n                if (!cleanTerm(name).includes(cleanedSearchTerm)) {\n                    return false;\n                }\n                if (command.channel_types) {\n                    return command.channel_types.includes(thread.channel_type);\n                }\n                return true;\n            })\n            .map(([name, command]) => ({\n                channel_types: command.channel_types,\n                help: command.help,\n                id: command.id,\n                name,\n            }));\n        const sortFunc = (c1, c2) => {\n            if (c1.channel_types && !c2.channel_types) {\n                return -1;\n            }\n            if (!c1.channel_types && c2.channel_types) {\n                return 1;\n            }\n            const cleanedName1 = cleanTerm(c1.name);\n            const cleanedName2 = cleanTerm(c2.name);\n            if (\n                cleanedName1.startsWith(cleanedSearchTerm) &&\n                !cleanedName2.startsWith(cleanedSearchTerm)\n            ) {\n                return -1;\n            }\n            if (\n                !cleanedName1.startsWith(cleanedSearchTerm) &&\n                cleanedName2.startsWith(cleanedSearchTerm)\n            ) {\n                return 1;\n            }\n            if (cleanedName1 < cleanedName2) {\n                return -1;\n            }\n            if (cleanedName1 > cleanedName2) {\n                return 1;\n            }\n            return c1.id - c2.id;\n        };\n        return {\n            type: \"ChannelCommand\",\n            suggestions: commands.sort(sortFunc),\n        };\n    },\n    /** @override */\n    sortPartnerSuggestionsContext(thread) {\n        return Object.assign(super.sortPartnerSuggestionsContext(), {\n            recentChatPartnerIds: this.store.getRecentChatPartnerIds(),\n            memberPartnerIds: new Set(\n                thread?.channel_member_ids\n                    .filter((member) => member.partner_id)\n                    .map((member) => member.partner_id.id)\n            ),\n        });\n    },\n};\npatch(SuggestionService.prototype, suggestionServicePatch);\n", "import { ACTION_TAGS } from \"@mail/core/common/action\";\nimport { registerThreadAction } from \"@mail/core/common/thread_actions\";\nimport { AttachmentPanel } from \"@mail/discuss/core/common/attachment_panel\";\nimport { ChannelInvitation } from \"@mail/discuss/core/common/channel_invitation\";\nimport { ChannelMemberList } from \"@mail/discuss/core/common/channel_member_list\";\nimport { DeleteThreadDialog } from \"@mail/discuss/core/common/delete_thread_dialog\";\nimport { NotificationSettings } from \"@mail/discuss/core/common/notification_settings\";\n\nimport { Component, xml } from \"@odoo/owl\";\n\nimport { Dialog } from \"@web/core/dialog/dialog\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { usePopover } from \"@web/core/popover/popover_hook\";\n\nclass ChannelActionDialog extends Component {\n    static props = [\"title\", \"contentComponent\", \"contentProps\", \"close?\"];\n    static components = { Dialog };\n    static template = xml`\n        <Dialog size=\"'md'\" title=\"props.title\" footer=\"false\" contentClass=\"'o-bg-body'\" bodyClass=\"'p-1'\">\n            <t t-component=\"props.contentComponent\" t-props=\"props.contentProps\"/>\n        </Dialog>\n    `;\n}\n\nregisterThreadAction(\"notification-settings\", {\n    actionPanelComponent: NotificationSettings,\n    condition: ({ owner, store, thread }) =>\n        thread?.model === \"discuss.channel\" &&\n        store.self_partner &&\n        (!owner.props.chatWindow || owner.props.chatWindow.isOpen),\n    setup({ owner }) {\n        if (!owner.props.chatWindow) {\n            this.popover = usePopover(NotificationSettings, {\n                onClose: () => this.close(),\n                position: \"bottom-end\",\n                fixedPosition: true,\n                popoverClass: this.panelOuterClass,\n            });\n        }\n    },\n    open({ owner, store, thread }) {\n        if (owner.isDiscussSidebarChannelActions || owner.env.inMeetingView) {\n            store.env.services.dialog?.add(ChannelActionDialog, {\n                title: thread.name,\n                contentComponent: NotificationSettings,\n                contentProps: { thread },\n            });\n        } else {\n            this.popover?.open(owner.root.el.querySelector(`[name=\"${this.id}\"]`), {\n                hasSizeConstraints: true,\n                thread,\n            });\n        }\n    },\n    close: ({ action }) => action.popover?.close(),\n    icon: ({ thread }) =>\n        thread.self_member_id?.mute_until_dt\n            ? \"fa fa-fw text-danger fa-bell-slash\"\n            : \"fa fa-fw fa-bell\",\n    name: _t(\"Notification Settings\"),\n    panelOuterClass: \"bg-100 border border-secondary\",\n    sequence: 10,\n    sequenceGroup: 30,\n    toggle: true,\n});\nregisterThreadAction(\"attachments\", {\n    actionPanelComponent: AttachmentPanel,\n    condition: ({ owner, thread }) =>\n        thread?.hasAttachmentPanel &&\n        (!owner.props.chatWindow || owner.props.chatWindow.isOpen) &&\n        !owner.isDiscussSidebarChannelActions,\n    icon: \"fa fa-fw fa-paperclip\",\n    name: _t(\"Attachments\"),\n    sequence: 10,\n    sequenceGroup: 10,\n    toggle: true,\n});\nregisterThreadAction(\"invite-people\", {\n    actionPanelComponent: ChannelInvitation,\n    actionPanelComponentProps: ({ action }) => ({ close: () => action.close() }),\n    close: ({ action }) => action.popover?.close(),\n    condition: ({ owner, thread }) =>\n        thread?.model === \"discuss.channel\" &&\n        (!owner.props.chatWindow || owner.props.chatWindow.isOpen),\n    panelOuterClass: ({ owner }) =>\n        `o-discuss-ChannelInvitation ${\n            owner.props.chatWindow ? \"bg-inherit\" : \"\"\n        } bg-100 border border-secondary`,\n    icon: \"oi oi-fw oi-user-plus\",\n    name: _t(\"Invite People\"),\n    open({ owner, store, thread }) {\n        if (owner.isDiscussSidebarChannelActions) {\n            store.env.services.dialog?.add(ChannelActionDialog, {\n                title: thread.displayName,\n                contentComponent: ChannelInvitation,\n                contentProps: {\n                    autofocus: true,\n                    thread,\n                    close: () => store.env.services.dialog.closeAll(),\n                },\n            });\n        } else if (!owner.env.inMeetingView) {\n            this.popover?.open(owner.root.el.querySelector(`[name=\"${this.id}\"]`), {\n                hasSizeConstraints: true,\n                thread,\n            });\n        }\n    },\n    sequence: ({ owner }) => (owner.isDiscussSidebarChannelActions ? 20 : 10),\n    sequenceGroup: 20,\n    setup({ owner }) {\n        if (!owner.props.chatWindow && !owner.env.inMeetingView) {\n            this.popover = usePopover(ChannelInvitation, {\n                onClose: () => this.close(),\n                popoverClass: this.panelOuterClass,\n            });\n        }\n    },\n    toggle: true,\n});\nregisterThreadAction(\"member-list\", {\n    actionPanelComponent: ChannelMemberList,\n    actionPanelComponentProps: ({ owner }) => ({\n        openChannelInvitePanel({ keepPrevious } = {}) {\n            owner.threadActions.actions\n                .find(({ id }) => id === \"invite-people\")\n                ?.open({ keepPrevious });\n        },\n    }),\n    condition: ({ owner, thread }) =>\n        thread?.hasMemberList &&\n        (!owner.props.chatWindow || owner.props.chatWindow.isOpen) &&\n        !owner.isDiscussSidebarChannelActions,\n    panelOuterClass: \"o-discuss-ChannelMemberList bg-inherit\",\n    icon: \"oi oi-fw oi-users\",\n    name: _t(\"Members\"),\n    close: ({ owner, store }) => {\n        if (owner.env.inDiscussApp) {\n            store.discuss.isMemberPanelOpenByDefault = false;\n        }\n    },\n    open: ({ owner, store }) => {\n        if (owner.env.inDiscussApp) {\n            store.discuss.isMemberPanelOpenByDefault = true;\n        }\n    },\n    sequence: 30,\n    sequenceGroup: 10,\n    toggle: true,\n});\nregisterThreadAction(\"mark-read\", {\n    condition: ({ owner, thread }) =>\n        thread?.self_member_id &&\n        thread.self_member_id.message_unread_counter > 0 &&\n        !thread.self_member_id.mute_until_dt &&\n        owner.isDiscussSidebarChannelActions,\n    open: ({ owner }) => owner.thread.markAsRead(),\n    icon: \"fa fa-fw fa-check\",\n    name: _t(\"Mark Read\"),\n    sequence: 10,\n    sequenceGroup: 20,\n});\nregisterThreadAction(\"delete-thread\", {\n    actionPanelComponent: DeleteThreadDialog,\n    actionPanelComponentProps({ action }) {\n        return { close: () => action.close() };\n    },\n    condition({ owner, store, thread }) {\n        return (\n            thread?.parent_channel_id &&\n            store.self.main_user_id?.eq(thread.create_uid) &&\n            !owner.isDiscussContent\n        );\n    },\n    panelOuterClass: \"bg-100\",\n    icon: \"fa fa-fw fa-trash\",\n    iconLarge: \"fa fa-fw fa-lg fa-trash\",\n    name: _t(\"Delete Thread\"),\n    close: ({ action }) => action.popover?.close(),\n    toggle: true,\n    open: ({ action, owner, store, thread }) => {\n        if (owner.isDiscussSidebarChannelActions) {\n            store.env.services.dialog?.add(ChannelActionDialog, {\n                title: thread.name,\n                contentComponent: DeleteThreadDialog,\n                contentProps: {\n                    close: () => store.env.services.dialog.closeAll(),\n                    thread,\n                },\n            });\n        }\n    },\n    sequence: ({ owner }) => (owner.props.chatWindow ? 50 : 40),\n    sequenceGroup: 40,\n    tags: [ACTION_TAGS.DANGER],\n});\n", "import { threadCompareRegistry } from \"@mail/core/common/thread_compare\";\nimport { compareDatetime } from \"@mail/utils/common/misc\";\n\nthreadCompareRegistry.add(\n    \"mail.unread\",\n    /**\n     * @param {import(\"models\").Thread thread1}\n     * @param {import(\"models\").Thread thread2}\n     */\n    (thread1, thread2) => {\n        const aUnread = thread1.self_member_id?.message_unread_counter;\n        const bUnread = thread2.self_member_id?.message_unread_counter;\n        if (aUnread > 0 && bUnread === 0) {\n            return -1;\n        }\n        if (bUnread > 0 && aUnread === 0) {\n            return 1;\n        }\n    },\n    { sequence: 20 }\n);\n\nthreadCompareRegistry.add(\n    \"mail.last-interest\",\n    /**\n     * @param {import(\"models\").Thread thread1}\n     * @param {import(\"models\").Thread thread2}\n     */\n    (thread1, thread2) => {\n        const aLastInterestDt = thread1.lastInterestDt;\n        const bLastInterestDt = thread2.lastInterestDt;\n        if (aLastInterestDt && bLastInterestDt) {\n            const res = compareDatetime(bLastInterestDt, aLastInterestDt);\n            if (res !== 0) {\n                return res;\n            }\n        }\n    },\n    { sequence: 30 }\n);\n", "import { fields } from \"@mail/core/common/record\";\nimport { Thread } from \"@mail/core/common/thread_model\";\nimport { useSequential } from \"@mail/utils/common/hooks\";\nimport {\n    compareDatetime,\n    effectWithCleanup,\n    nearestGreaterThanOrEqual,\n} from \"@mail/utils/common/misc\";\nimport { _t } from \"@web/core/l10n/translation\";\n\nimport { formatList } from \"@web/core/l10n/utils\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { registry } from \"@web/core/registry\";\nimport { Deferred } from \"@web/core/utils/concurrency\";\nimport { createElementWithContent } from \"@web/core/utils/html\";\nimport { patch } from \"@web/core/utils/patch\";\nimport { imageUrl } from \"@web/core/utils/urls\";\n\nconst commandRegistry = registry.category(\"discuss.channel_commands\");\n\n/** @type {typeof Thread} */\nconst threadStaticPatch = {\n    new() {\n        const thread = super.new(...arguments);\n        // Handles subscriptions for non-members. Subscriptions for channels\n        // that the user is a member of are handled by\n        // `ir_websocket@_build_bus_channel_list`.\n        effectWithCleanup({\n            effect(busChannel, busService) {\n                if (busService && busChannel) {\n                    busService.addChannel(busChannel);\n                    return () => busService.deleteChannel(busChannel);\n                }\n            },\n            dependencies: (thread) => [\n                thread.shouldSubscribeToBusChannel && thread.busChannel,\n                thread.store.env.services.bus_service,\n            ],\n            reactiveTargets: [thread],\n        });\n        return thread;\n    },\n    async getOrFetch(data, fieldNames = []) {\n        if (data.model !== \"discuss.channel\" || data.id < 1) {\n            return super.getOrFetch(...arguments);\n        }\n        const thread = this.store.Thread.get({ id: data.id, model: data.model });\n        if (thread?.fetchChannelInfoState === \"fetched\") {\n            return Promise.resolve(thread);\n        }\n        const fetchChannelInfoDeferred = this.store.channelIdsFetchingDeferred.get(data.id);\n        if (fetchChannelInfoDeferred) {\n            return fetchChannelInfoDeferred;\n        }\n        const def = new Deferred();\n        this.store.channelIdsFetchingDeferred.set(data.id, def);\n        this.store.fetchChannel(data.id).then(\n            () => {\n                this.store.channelIdsFetchingDeferred.delete(data.id);\n                const thread = this.store.Thread.get({ id: data.id, model: data.model });\n                if (thread?.exists()) {\n                    thread.fetchChannelInfoState = \"fetched\";\n                    def.resolve(thread);\n                } else {\n                    def.resolve();\n                }\n            },\n            () => {\n                this.store.channelIdsFetchingDeferred.delete(data.id);\n                const thread = this.store.Thread.get({ id: data.id, model: data.model });\n                if (thread?.exists()) {\n                    def.reject(thread);\n                } else {\n                    def.reject();\n                }\n            }\n        );\n        return def;\n    },\n};\npatch(Thread, threadStaticPatch);\n\n/** @type {import(\"models\").Thread} */\nconst threadPatch = {\n    setup() {\n        super.setup();\n        this.channel_member_ids = fields.Many(\"discuss.channel.member\", {\n            inverse: \"channel_id\",\n            onDelete: (r) => r.delete(),\n            sort: (m1, m2) => m1.id - m2.id,\n        });\n        this.correspondent = fields.One(\"discuss.channel.member\", {\n            /** @this {import(\"models\").Thread} */\n            compute() {\n                return this.computeCorrespondent();\n            },\n        });\n        this.correspondentCountry = fields.One(\"res.country\", {\n            /** @this {import(\"models\").Thread} */\n            compute() {\n                return this.correspondent?.persona?.country_id ?? this.country_id;\n            },\n        });\n        /** @type {\"video_full_screen\"|undefined} */\n        this.default_display_mode = undefined;\n        /** @type {Deferred<Thread|undefined>} */\n        this.fetchChannelInfoDeferred = undefined;\n        /** @type {\"not_fetched\"|\"fetching\"|\"fetched\"} */\n        this.fetchChannelInfoState = \"not_fetched\";\n        this.group_ids = fields.Many(\"res.groups\");\n        this.hasOtherMembersTyping = fields.Attr(false, {\n            /** @this {import(\"models\").Thread} */\n            compute() {\n                return this.otherTypingMembers.length > 0;\n            },\n        });\n        this.hasSeenFeature = fields.Attr(false, {\n            /** @this {import(\"models\").Thread} */\n            compute() {\n                return this.store.channel_types_with_seen_infos.includes(this.channel_type);\n            },\n        });\n        this.firstUnreadMessage = fields.One(\"mail.message\", {\n            /** @this {import(\"models\").Thread} */\n            compute() {\n                if (!this.self_member_id) {\n                    return null;\n                }\n                const messages = this.messages.filter((m) => !m.isNotification);\n                const separator = this.self_member_id.new_message_separator_ui;\n                if (separator === 0 && !this.loadOlder) {\n                    return messages[0];\n                }\n                if (!separator || messages.length === 0 || messages.at(-1).id < separator) {\n                    return null;\n                }\n                // try to find a perfect match according to the member's separator\n                let message = this.store[\"mail.message\"].get({ id: separator });\n                if (!message || this.notEq(message.thread)) {\n                    message = nearestGreaterThanOrEqual(messages, separator, (msg) => msg.id);\n                }\n                return message;\n            },\n            inverse: \"threadAsFirstUnread\",\n        });\n        this.invited_member_ids = fields.Many(\"discuss.channel.member\");\n        this.last_interest_dt = fields.Datetime();\n        this.lastInterestDt = fields.Datetime({\n            /** @this {import(\"models\").Thread} */\n            compute() {\n                const selfMemberLastInterestDt = this.self_member_id?.last_interest_dt;\n                const lastInterestDt = this.last_interest_dt;\n                return compareDatetime(selfMemberLastInterestDt, lastInterestDt) > 0\n                    ? selfMemberLastInterestDt\n                    : lastInterestDt;\n            },\n        });\n        this.lastMessageSeenByAllId = fields.Attr(undefined, {\n            /** @this {import(\"models\").Thread} */\n            compute() {\n                if (!this.hasSeenFeature) {\n                    return;\n                }\n                return this.channel_member_ids.reduce((lastMessageSeenByAllId, member) => {\n                    if (member.notEq(this.selfMember) && member.seen_message_id) {\n                        return lastMessageSeenByAllId\n                            ? Math.min(lastMessageSeenByAllId, member.seen_message_id.id)\n                            : member.seen_message_id.id;\n                    } else {\n                        return lastMessageSeenByAllId;\n                    }\n                }, undefined);\n            },\n        });\n        this.lastSelfMessageSeenByEveryone = fields.One(\"mail.message\", {\n            compute() {\n                if (!this.lastMessageSeenByAllId) {\n                    return false;\n                }\n                let res;\n                // starts from most recent persistent messages to find early\n                for (let i = this.persistentMessages.length - 1; i >= 0; i--) {\n                    const message = this.persistentMessages[i];\n                    if (!message.isSelfAuthored) {\n                        continue;\n                    }\n                    if (message.id > this.lastMessageSeenByAllId) {\n                        continue;\n                    }\n                    res = message;\n                    break;\n                }\n                return res;\n            },\n        });\n        this.markReadSequential = useSequential();\n        this.markedAsUnread = false;\n        this.markingAsRead = false;\n        /** @type {number|undefined} */\n        this.member_count = undefined;\n        /** @type {string} name: only for channel. For generic thread, @see display_name */\n        this.name = undefined;\n        this.channel_name_member_ids = fields.Many(\"discuss.channel.member\");\n        this.onlineMembers = fields.Many(\"discuss.channel.member\", {\n            /** @this {import(\"models\").Thread} */\n            compute() {\n                return this.channel_member_ids\n                    .filter((member) => this.store.onlineMemberStatuses.includes(member.im_status))\n                    .sort((m1, m2) => this.store.sortMembers(m1, m2)); // FIXME: sort are prone to infinite loop (see test \"Display livechat custom name in typing status\")\n            },\n        });\n        this.offlineMembers = fields.Many(\"discuss.channel.member\", {\n            compute() {\n                return this._computeOfflineMembers().sort(\n                    (m1, m2) => this.store.sortMembers(m1, m2) // FIXME: sort are prone to infinite loop (see test \"Display livechat custom name in typing status\")\n                );\n            },\n        });\n        this.otherTypingMembers = fields.Many(\"discuss.channel.member\", {\n            /** @this {import(\"models\").Thread} */\n            compute() {\n                return this.typingMembers.filter((member) => !member.persona?.eq(this.store.self));\n            },\n        });\n        this.self_member_id = fields.One(\"discuss.channel.member\", {\n            inverse: \"threadAsSelf\",\n        });\n        this.scrollUnread = true;\n        // memberBusSubscription\n        this.toggleBusSubscription = fields.Attr(false, {\n            /** @this {import(\"models\").Thread} */\n            compute() {\n                return (\n                    this.model === \"discuss.channel\" &&\n                    this.self_member_id?.memberSince >=\n                        this.store.env.services.bus_service.startedAt\n                );\n            },\n            onUpdate() {\n                this.store.updateBusSubscription();\n            },\n        });\n        this.typingMembers = fields.Many(\"discuss.channel.member\", { inverse: \"threadAsTyping\" });\n    },\n    /** @returns {import(\"models\").ChannelMember[]} */\n    _computeOfflineMembers() {\n        return this.channel_member_ids.filter(\n            (member) => !this.store.onlineMemberStatuses.includes(member.im_status)\n        );\n    },\n    /** Equivalent to DiscussChannel._allow_invite_by_email */\n    get allow_invite_by_email() {\n        return (\n            this.channel_type === \"group\" ||\n            (this.channel_type === \"channel\" && !this.group_public_id)\n        );\n    },\n    get areAllMembersLoaded() {\n        return this.member_count === this.channel_member_ids.length;\n    },\n    get avatarUrl() {\n        if (this.channel_type === \"channel\" || this.channel_type === \"group\") {\n            return imageUrl(\"discuss.channel\", this.id, \"avatar_128\", {\n                unique: this.avatar_cache_key,\n            });\n        }\n        if (this.channel_type === \"chat\" && this.correspondent) {\n            return this.correspondent.avatarUrl;\n        }\n        return super.avatarUrl;\n    },\n    get showCorrespondentCountry() {\n        return false;\n    },\n    /** @override */\n    async checkReadAccess() {\n        const res = await super.checkReadAccess();\n        if (!res && this.model === \"discuss.channel\") {\n            // channel is assumed to be readable if its channel_type is known\n            return this.channel_type;\n        }\n        return res;\n    },\n    /** @returns {import(\"models\").ChannelMember} */\n    computeCorrespondent() {\n        if (this.channel_type === \"channel\") {\n            return undefined;\n        }\n        const correspondents = this.correspondents;\n        if (correspondents.length === 1) {\n            // 2 members chat.\n            return correspondents[0];\n        }\n        if (correspondents.length === 0 && this.channel_member_ids.length === 1) {\n            // Self-chat.\n            return this.channel_member_ids[0];\n        }\n        return undefined;\n    },\n    /** @returns {import(\"models\").ChannelMember[]} */\n    get correspondents() {\n        return this.channel_member_ids.filter(({ persona }) => persona?.notEq(this.store.self));\n    },\n    get displayName() {\n        if (this.supportsCustomChannelName && this.self_member_id?.custom_channel_name) {\n            return this.self_member_id.custom_channel_name;\n        }\n        if (this.channel_type === \"chat\" && this.correspondent) {\n            return this.correspondent.name;\n        }\n        if (this.channel_name_member_ids.length && !this.name) {\n            const nameParts = this.channel_name_member_ids\n                .sort((m1, m2) => m1.id - m2.id)\n                .slice(0, 3)\n                .map((member) => member.name);\n            if (this.member_count > 3) {\n                const remaining = this.member_count - 3;\n                nameParts.push(remaining === 1 ? _t(\"1 other\") : _t(\"%s others\", remaining));\n            }\n            return formatList(nameParts);\n        }\n        if (this.model === \"discuss.channel\" && this.name) {\n            return this.name;\n        }\n        return super.displayName;\n    },\n    async fetchChannelMembers() {\n        if (this.fetchMembersState === \"pending\") {\n            return;\n        }\n        const previousState = this.fetchMembersState;\n        this.fetchMembersState = \"pending\";\n        const known_member_ids = this.channel_member_ids.map((channelMember) => channelMember.id);\n        let data;\n        try {\n            data = await rpc(\"/discuss/channel/members\", {\n                channel_id: this.id,\n                known_member_ids: known_member_ids,\n            });\n        } catch (e) {\n            this.fetchMembersState = previousState;\n            throw e;\n        }\n        this.fetchMembersState = \"fetched\";\n        this.store.insert(data);\n    },\n    async fetchMoreAttachments(limit = 30) {\n        if (this.isLoadingAttachments || this.areAttachmentsLoaded) {\n            return;\n        }\n        this.isLoadingAttachments = true;\n        try {\n            const data = await rpc(\"/discuss/channel/attachments\", {\n                before: Math.min(...this.attachments.map(({ id }) => id)),\n                channel_id: this.id,\n                limit,\n            });\n            this.store.insert(data.store_data);\n            if (data.count < limit) {\n                this.areAttachmentsLoaded = true;\n            }\n        } finally {\n            this.isLoadingAttachments = false;\n        }\n    },\n    get hasMemberList() {\n        return [\"channel\", \"group\"].includes(this.channel_type);\n    },\n    get hasSelfAsMember() {\n        return Boolean(this.self_member_id);\n    },\n    /** @override */\n    get importantCounter() {\n        if (this.isChatChannel && this.self_member_id?.message_unread_counter_ui) {\n            return this.self_member_id.message_unread_counter_ui;\n        }\n        if (this.discussAppCategory?.id === \"channels\") {\n            if (this.store.settings.channel_notifications === \"no_notif\") {\n                return 0;\n            }\n            if (\n                this.store.settings.channel_notifications === \"all\" &&\n                !this.self_member_id?.mute_until_dt\n            ) {\n                return this.self_member_id?.message_unread_counter_ui;\n            }\n        }\n        return super.importantCounter;\n    },\n    /** @override */\n    isDisplayedOnUpdate() {\n        super.isDisplayedOnUpdate(...arguments);\n        if (!this.self_member_id) {\n            return;\n        }\n        if (!this.isDisplayed) {\n            this.self_member_id.new_message_separator_ui =\n                this.self_member_id.new_message_separator;\n            this.markedAsUnread = false;\n        }\n    },\n    get isUnread() {\n        return this.self_member_id?.message_unread_counter > 0 || super.isUnread;\n    },\n    /** @override */\n    markAsRead() {\n        super.markAsRead(...arguments);\n        if (!this.self_member_id) {\n            return;\n        }\n        const newestPersistentMessage = this.newestPersistentOfAllMessage;\n        if (!newestPersistentMessage) {\n            return;\n        }\n        const alreadyReadBySelf =\n            this.self_member_id.seen_message_id?.id >= newestPersistentMessage.id &&\n            this.self_member_id.new_message_separator > newestPersistentMessage.id;\n        if (alreadyReadBySelf) {\n            return;\n        }\n        this.markReadSequential(async () => {\n            this.markingAsRead = true;\n            return rpc(\n                \"/discuss/channel/mark_as_read\",\n                {\n                    channel_id: this.id,\n                    last_message_id: newestPersistentMessage.id,\n                },\n                { silent: true }\n            ).catch((e) => {\n                if (e.code !== 404) {\n                    throw e;\n                }\n            });\n        }).then(() => (this.markingAsRead = false));\n    },\n    /**\n     * To be overridden.\n     * The purpose is to exclude technical channel_member_ids like bots and avoid\n     * \"wrong\" seen message indicator\n     * @returns {import(\"models\").ChannelMember[]}\n     */\n    get membersThatCanSeen() {\n        return this.channel_member_ids;\n    },\n    /** @override */\n    get needactionCounter() {\n        return this.isChatChannel\n            ? this.self_member_id?.message_unread_counter ?? 0\n            : super.needactionCounter;\n    },\n    /** @override */\n    onNewSelfMessage(message) {\n        if (!this.self_member_id || message.id < this.self_member_id.seen_message_id?.id) {\n            return;\n        }\n        this.self_member_id.seen_message_id = message;\n        this.self_member_id.new_message_separator = message.id + 1;\n        this.self_member_id.new_message_separator_ui = this.self_member_id.new_message_separator;\n        this.markedAsUnread = false;\n    },\n    /** @override */\n    open(options) {\n        if (this.model === \"discuss.channel\") {\n            const res = this.openChannel();\n            if (res) {\n                return res;\n            }\n            this.openChatWindow(options);\n            return true;\n        }\n        return super.open(...arguments);\n    },\n    /**\n     * @returns {boolean} true if the channel was opened, false otherwise\n     */\n    openChannel() {\n        return false;\n    },\n    /** @param {string} body */\n    async post(body) {\n        const textContent = createElementWithContent(\"div\", body).textContent.trim();\n        if (this.model === \"discuss.channel\" && textContent.startsWith(\"/\")) {\n            const [firstWord] = textContent.substring(1).split(/\\s/);\n            const command = commandRegistry.get(firstWord, false);\n            if (\n                command &&\n                (!command.channel_types || command.channel_types.includes(this.channel_type))\n            ) {\n                await this.executeCommand(command, textContent);\n                return;\n            }\n        }\n        return super.post(...arguments);\n    },\n    get shouldSubscribeToBusChannel() {\n        return Boolean(\n            this.model === \"discuss.channel\" &&\n                !this.isTransient &&\n                !this.self_member_id &&\n                (this.isLocallyPinned || this.chat_window?.isOpen)\n        );\n    },\n    get showUnreadBanner() {\n        return this.self_member_id?.message_unread_counter_ui > 0;\n    },\n    get unknownMembersCount() {\n        return (this.member_count ?? 0) - this.channel_member_ids.length;\n    },\n};\npatch(Thread.prototype, threadPatch);\n", "import { Thread } from \"@mail/core/common/thread\";\n\nimport { useEffect, toRaw } from \"@odoo/owl\";\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { patch } from \"@web/core/utils/patch\";\n\n/** @type {Thread} */\nconst threadPatch = {\n    setup() {\n        super.setup(...arguments);\n        useEffect(\n            (loadNewer, mountedAndLoaded) => {\n                if (\n                    loadNewer ||\n                    !mountedAndLoaded ||\n                    !this.props.thread.self_member_id ||\n                    !this.scrollableRef.el\n                ) {\n                    return;\n                }\n                const el = this.scrollableRef.el;\n                if (Math.abs(el.scrollTop + el.clientHeight - el.scrollHeight) <= 1) {\n                    this.props.thread.self_member_id.hideUnreadBanner = true;\n                }\n            },\n            () => [this.props.thread.loadNewer, this.state.mountedAndLoaded, this.state.scrollTop]\n        );\n    },\n    /** @override */\n    applyScrollContextually(thread) {\n        if (thread.self_member_id && thread.scrollUnread) {\n            if (thread.firstUnreadMessage) {\n                const messageEl = this.refByMessageId.get(thread.firstUnreadMessage.id)?.el;\n                if (!messageEl) {\n                    return;\n                }\n                const messageCenter =\n                    messageEl.offsetTop -\n                    this.scrollableRef.el.offsetHeight / 2 +\n                    messageEl.offsetHeight / 2;\n                this.setScroll(messageCenter);\n            } else {\n                const scrollTop =\n                    this.props.order === \"asc\"\n                        ? this.scrollableRef.el.scrollHeight - this.scrollableRef.el.clientHeight\n                        : 0;\n                this.setScroll(scrollTop);\n            }\n            thread.scrollUnread = false;\n            if (this.isAtBottom && !thread.markedAsUnread && thread.isFocused) {\n                thread.markAsRead();\n            }\n        } else {\n            super.applyScrollContextually(...arguments);\n        }\n    },\n    /** @override */\n    fetchMessages() {\n        if (this.props.thread.self_member_id && this.props.thread.scrollUnread) {\n            toRaw(this.props.thread).loadAround(\n                this.props.thread.self_member_id.new_message_separator\n            );\n        } else {\n            super.fetchMessages();\n        }\n    },\n    get newMessageBannerText() {\n        if (this.props.thread.self_member_id?.message_unread_counter > 1) {\n            return _t(\"%s new messages\", this.props.thread.self_member_id.message_unread_counter);\n        }\n        return _t(\"1 new message\");\n    },\n    async onClickUnreadMessagesBanner() {\n        await this.props.thread.loadAround(\n            this.props.thread.self_member_id.new_message_separator_ui\n        );\n        this.messageHighlight?.highlightMessage(\n            this.props.thread.firstUnreadMessage,\n            this.props.thread\n        );\n    },\n};\npatch(Thread.prototype, threadPatch);\n", "import { patch } from \"@web/core/utils/patch\";\nimport { ActionPanel } from \"@mail/discuss/core/common/action_panel\";\n\npatch(ActionPanel.prototype, {\n    get initialWidth() {\n        return super.initialWidth || this.store.discuss.INSPECTOR_WIDTH;\n    },\n    get minWidth() {\n        return super.minWidth || this.store.discuss.INSPECTOR_WIDTH;\n    },\n});\n", "import { Component } from \"@odoo/owl\";\nimport { registry } from \"@web/core/registry\";\nimport { useService } from \"@web/core/utils/hooks\";\n\n/**\n * @typedef {Object} Props\n * @extends {Component<Props, Env>}\n */\nexport class BusConnectionAlert extends Component {\n    static template = \"mail.BusConnectionAlert\";\n    static props = {};\n\n    setup() {\n        this.busMonitoring = useService(\"bus.monitoring_service\");\n        this.store = useService(\"mail.store\");\n    }\n}\n\nexport const connectionAlertService = {\n    dependencies: [\"bus.monitoring_service\", \"mail.store\"],\n    start() {\n        registry\n            .category(\"main_components\")\n            .add(\"bus.ConnectionAlert\", { Component: BusConnectionAlert });\n    },\n};\nregistry.category(\"services\").add(\"bus.connection_alert\", connectionAlertService);\n", "import { compareDatetime } from \"@mail/utils/common/misc\";\nimport { fields, Record } from \"@mail/core/common/record\";\nimport { browser } from \"@web/core/browser/browser\";\n\nexport class DiscussAppCategory extends Record {\n    static id = \"id\";\n\n    /**\n     * @param {import(\"models\").Thread} t1\n     * @param {import(\"models\").Thread} t2\n     */\n    sortThreads(t1, t2) {\n        if (this.id === \"channels\") {\n            return String.prototype.localeCompare.call(t1.name, t2.name);\n        }\n        if (this.id === \"chats\") {\n            return compareDatetime(t2.lastInterestDt, t1.lastInterestDt) || t2.id - t1.id;\n        }\n    }\n\n    get isVisible() {\n        return (\n            !this.hidden &&\n            (!this.hideWhenEmpty ||\n                this.threads.some((thread) => thread.displayToSelf || thread.isLocallyPinned))\n        );\n    }\n\n    /** @type {string} */\n    extraClass;\n    /** @string */\n    icon;\n    /** @string */\n    id;\n    /** @type {string} */\n    name;\n    // Hide categories from the devtools if really bothered.\n    hidden = fields.Attr(undefined, {\n        compute() {\n            return Boolean(localStorage.getItem(`mail.sidebar_category_${this.id}_hidden`));\n        },\n        onUpdate() {\n            if (!this.hidden && this.hidden !== undefined) {\n                localStorage.removeItem(`mail.sidebar_category_${this.id}_hidden`);\n            } else {\n                localStorage.setItem(`mail.sidebar_category_${this.id}_hidden`, true);\n            }\n        },\n        eager: true,\n    });\n    hideWhenEmpty = false;\n    canView = false;\n    app = fields.One(\"DiscussApp\", {\n        compute() {\n            return this.store.discuss;\n        },\n    });\n    _openLocally = false;\n    localStateKey = fields.Attr(null, {\n        compute() {\n            if (this.saveStateToServer) {\n                return null;\n            }\n            return `discuss_sidebar_category_${this.id}_open`;\n        },\n        onUpdate() {\n            if (this.localStateKey) {\n                this._openLocally = JSON.parse(\n                    browser.localStorage.getItem(this.localStateKey) ?? \"true\"\n                );\n            }\n        },\n    });\n    /** @type {number} */\n    sequence;\n\n    get open() {\n        return this.saveStateToServer\n            ? this.store.settings[this.serverStateKey]\n            : this._openLocally;\n    }\n\n    get saveStateToServer() {\n        return this.serverStateKey && this.store.self?.main_user_id?.share === false;\n    }\n\n    set open(value) {\n        if (this.saveStateToServer) {\n            this.store.settings[this.serverStateKey] = value;\n            this.store.env.services.orm.call(\n                \"res.users.settings\",\n                \"set_res_users_settings\",\n                [[this.store.settings.id]],\n                {\n                    new_settings: {\n                        [this.serverStateKey]: value,\n                    },\n                }\n            );\n        } else {\n            this._openLocally = value;\n            browser.localStorage.setItem(this.localStateKey, value);\n        }\n    }\n\n    /** @type {string} */\n    serverStateKey;\n    threads = fields.Many(\"Thread\", {\n        sort(t1, t2) {\n            return this.sortThreads(t1, t2);\n        },\n        inverse: \"discussAppCategory\",\n    });\n    threadsWithCounter = fields.Many(\"Thread\", { inverse: \"categoryAsThreadWithCounter\" });\n}\n\nDiscussAppCategory.register();\n", "import { fields } from \"@mail/core/common/record\";\nimport { DiscussApp } from \"@mail/core/public_web/discuss_app_model\";\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { patch } from \"@web/core/utils/patch\";\n\nconst discussAppPatch = {\n    setup() {\n        super.setup(...arguments);\n        this.allCategories = fields.Many(\"DiscussAppCategory\", {\n            inverse: \"app\",\n            sort: (c1, c2) =>\n                c1.sequence !== c2.sequence\n                    ? c1.sequence - c2.sequence\n                    : c1.name.localeCompare(c2.name),\n        });\n        this.channels = fields.One(\"DiscussAppCategory\", {\n            compute() {\n                return {\n                    addTitle: _t(\"Add or join a channel\"),\n                    canView: true,\n                    extraClass: \"o-mail-DiscussSidebarCategory-channel\",\n                    icon: \"fa fa-hashtag\",\n                    id: \"channels\",\n                    name: _t(\"Channels\"),\n                    sequence: 10,\n                    serverStateKey: \"is_discuss_sidebar_category_channel_open\",\n                };\n            },\n            eager: true,\n        });\n        this.chats = fields.One(\"DiscussAppCategory\", {\n            compute() {\n                return this.computeChats();\n            },\n            eager: true,\n        });\n        this.unreadChannels = fields.Many(\"Thread\", { inverse: \"appAsUnreadChannels\" });\n    },\n    computeChats() {\n        return {\n            addTitle: _t(\"Start a conversation\"),\n            canView: false,\n            extraClass: \"o-mail-DiscussSidebarCategory-chat\",\n            icon: \"oi oi-users\",\n            id: \"chats\",\n            name: _t(\"Direct messages\"),\n            sequence: 30,\n            serverStateKey: \"is_discuss_sidebar_category_chat_open\",\n        };\n    },\n};\npatch(DiscussApp.prototype, discussAppPatch);\n", "import { DiscussClientAction } from \"@mail/core/public_web/discuss_client_action\";\n\nimport { patch } from \"@web/core/utils/patch\";\n\npatch(DiscussClientAction.prototype, {\n    async restoreDiscussThread() {\n        await this.store.channels.fetch();\n        return super.restoreDiscussThread(...arguments);\n    },\n    parseActiveId(rawActiveId) {\n        if (typeof rawActiveId === \"number\") {\n            return [\"discuss.channel\", rawActiveId];\n        }\n        const parsedActiveId = super.parseActiveId(rawActiveId);\n        if (!parsedActiveId) {\n            return parsedActiveId;\n        }\n        const [model, id] = parsedActiveId;\n        if (model === \"mail.channel\") {\n            // legacy format (sent in old emails, shared links, ...)\n            return [\"discuss.channel\", id];\n        }\n        return [model, id];\n    },\n});\n", "import { cleanTerm } from \"@mail/utils/common/format\";\n\nimport { Component, useState } from \"@odoo/owl\";\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { ImStatus } from \"@mail/core/common/im_status\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { Dialog } from \"@web/core/dialog/dialog\";\nimport { ChannelInvitation } from \"../common/channel_invitation\";\n\nconst commandSetupRegistry = registry.category(\"command_setup\");\nconst commandProviderRegistry = registry.category(\"command_provider\");\n\nconst NEW_CHANNEL = \"NEW_CHANNEL\";\nconst NEW_GROUP_CHAT = \"NEW_GROUP_CHAT\";\n\nclass CreateChatDialog extends Component {\n    static components = { ChannelInvitation, Dialog };\n    static props = [\"close\", \"name?\"];\n    static template = \"mail.CreateChatDialog\";\n\n    setup() {\n        super.setup();\n        this.store = useService(\"mail.store\");\n        this.invitePeopleState = useState({\n            selectablePartners: [],\n            selectedPartners: [],\n            searchStr: this.props.name,\n        });\n    }\n\n    get createText() {\n        if (this.invitePeopleState.selectedPartners.length === 1) {\n            return _t(\"Open Chat\");\n        }\n        return _t(\"Create Group Chat\");\n    }\n\n    onClickConfirm() {\n        const selectedPartnersId = this.invitePeopleState.selectedPartners.map((p) => p.id);\n        const partners_to = [...new Set([this.store.self.id, ...selectedPartnersId])];\n        if (partners_to.length === 1) {\n            this.store.createGroupChat({ partners_to });\n        } else {\n            this.store.startChat(partners_to);\n        }\n        this.props.close();\n    }\n}\n\nclass CreateChannelDialog extends Component {\n    static components = { Dialog };\n    static props = [\"close\", \"name?\"];\n    static template = \"mail.CreateChannelDialog\";\n\n    setup() {\n        super.setup();\n        this.store = useService(\"mail.store\");\n        this.orm = useService(\"orm\");\n        this.state = useState({ name: this.props.name || \"\", isInvalid: false });\n    }\n\n    /** @param {KeyboardEvent} ev */\n    onKeydown(ev) {\n        switch (ev.key) {\n            case \"Enter\":\n                this.onClickConfirm();\n                break;\n            default:\n                this.state.isInvalid = false;\n        }\n    }\n\n    async onClickConfirm() {\n        const name = this.state.name.trim();\n        if (!name) {\n            this.state.isInvalid = true;\n            return;\n        }\n        await makeNewChannel(name, this.store);\n        this.props.close();\n    }\n}\n\nclass DiscussCommand extends Component {\n    static components = { ImStatus };\n    static template = \"mail.DiscussCommand\";\n    static props = {\n        counter: { type: Number, optional: true },\n        executeCommand: Function,\n        imgUrl: { String, optional: true },\n        name: String,\n        persona: { type: Object, optional: true },\n        channel: { type: Object, optional: true },\n        action: { type: Object, optional: true },\n        searchValue: String,\n        slots: Object,\n    };\n\n    setup() {\n        super.setup();\n        this.store = useService(\"mail.store\");\n        this.ui = useService(\"ui\");\n    }\n}\n\n// -----------------------------------------------------------------------------\n// add @ namespace + provider\n// -----------------------------------------------------------------------------\ncommandSetupRegistry.add(\"@\", {\n    debounceDelay: 200,\n    emptyMessage: _t(\"No conversation found\"),\n    name: _t(\"conversations\"),\n    placeholder: _t(\"Search a conversation\"),\n});\n\n/**\n * @param {string} name\n * @param {import(\"models\").Store} store\n */\nasync function makeNewChannel(name, store) {\n    const { channel } = await store.fetchStoreData(\n        \"/discuss/create_channel\",\n        { name, group_id: store.internalUserGroupId },\n        { readonly: false, requestData: true }\n    );\n    await channel.open({ focus: true, bypassCompact: true });\n}\n\nexport class DiscussCommandPalette {\n    /**\n     * @param {import(\"@web/env\").OdooEnv} env\n     * @param {import(\"services\").ServiceFactories} env.services\n     */\n    constructor(env, options) {\n        this.env = env;\n        this.options = options;\n        this.dialog = env.services.dialog;\n        /** @type {import(\"models\").Store} */\n        this.store = env.services[\"mail.store\"];\n        this.orm = env.services.orm;\n        this.suggestion = env.services[\"mail.suggestion\"];\n        this.ui = env.services.ui;\n        this.commands = [];\n        this.options = options;\n        this.cleanedTerm = cleanTerm(this.options.searchValue);\n    }\n\n    async fetch() {\n        await this.store.channels.fetch(); // FIXME: needed to search group chats without explicit name\n        await this.store.searchConversations(this.cleanedTerm);\n    }\n\n    /** @param {Record[]} [filtered] persona or thread to filters, e.g. being build already in a category in a patch such as MENTIONS or RECENT */\n    buildResults(filtered) {\n        const TOTAL_LIMIT = this.ui.isSmall ? 7 : 10;\n        const remaining = TOTAL_LIMIT - (filtered ? filtered.size : 0);\n        let partners = [];\n        if (this.store.self_partner) {\n            partners = Object.values(this.store[\"res.partner\"].records).filter(\n                (partner) =>\n                    partner.main_user_id?.share === false &&\n                    cleanTerm(partner.displayName).includes(this.cleanedTerm) &&\n                    (!filtered || !filtered.has(partner))\n            );\n            partners = this.suggestion\n                .sortPartnerSuggestions(partners, this.cleanedTerm)\n                .slice(0, TOTAL_LIMIT);\n        }\n        const selfPartner = this.store.self_partner?.in(partners)\n            ? this.store.self_partner\n            : undefined;\n        if (selfPartner) {\n            // selfPersona filtered here to put at the bottom as lowest priority\n            partners = partners.filter((p) => p.notEq(selfPartner));\n        }\n        const channels = Object.values(this.store.Thread.records)\n            .filter(\n                (thread) =>\n                    thread.channel_type &&\n                    thread.channel_type !== \"chat\" &&\n                    cleanTerm(thread.displayName).includes(this.cleanedTerm) &&\n                    (!filtered || !filtered.has(thread))\n            )\n            .sort((c1, c2) => {\n                if (c1.self_member_id && !c2.self_member_id) {\n                    return -1;\n                } else if (!c1.self_member_id && c2.self_member_id) {\n                    return 1;\n                }\n                return c1.id - c2.id;\n            })\n            .slice(0, TOTAL_LIMIT);\n        // balance remaining: half personas, half channels\n        const elligiblePersonas = [];\n        const elligibleChannels = [];\n        let i = 0;\n        while ((channels.length || partners.length) && i < remaining) {\n            const p = partners.shift();\n            const c = channels.shift();\n            if (p) {\n                elligiblePersonas.push(p);\n                i++;\n            }\n            if (i >= remaining) {\n                break;\n            }\n            if (c) {\n                elligibleChannels.push(c);\n                i++;\n            }\n        }\n        for (const persona of elligiblePersonas) {\n            this.commands.push(this.makeDiscussCommand(persona));\n        }\n        for (const channel of elligibleChannels) {\n            this.commands.push(this.makeDiscussCommand(channel));\n        }\n        if (selfPartner && i < remaining) {\n            // put self persona as lowest priority item\n            this.commands.push(this.makeDiscussCommand(selfPartner));\n        }\n    }\n\n    makeDiscussCommand(threadOrPersona, category) {\n        if (threadOrPersona?.Model?.name === \"Thread\") {\n            /** @type {import(\"models\").Thread} */\n            const thread = threadOrPersona;\n            return {\n                Component: DiscussCommand,\n                action: async () => {\n                    const channel = await this.store.Thread.getOrFetch(thread);\n                    channel.open({ focus: true, bypassCompact: true });\n                },\n                name: thread.displayName,\n                category,\n                props: {\n                    imgUrl: thread.parent_channel_id?.avatarUrl ?? thread.avatarUrl,\n                    channel: thread.channel_type !== \"chat\" ? thread : undefined,\n                    persona:\n                        thread.channel_type === \"chat\" ? thread.correspondent.persona : undefined,\n                    counter: thread.importantCounter,\n                },\n            };\n        }\n        if (threadOrPersona?.Model?._name === \"res.partner\") {\n            /** @type {import(\"models\").Persona} */\n            const persona = threadOrPersona;\n            const chat = persona.searchChat();\n            return {\n                Component: DiscussCommand,\n                action: () => {\n                    this.store.openChat({ partnerId: persona.id });\n                },\n                name: persona.displayName,\n                category,\n                props: {\n                    imgUrl: persona.avatarUrl,\n                    persona,\n                    counter: chat ? chat.importantCounter : undefined,\n                },\n            };\n        }\n        if (threadOrPersona === NEW_CHANNEL) {\n            return {\n                Component: DiscussCommand,\n                action: async () => {\n                    const name = this.options.searchValue.trim();\n                    if (name) {\n                        await makeNewChannel(name, this.store);\n                    } else {\n                        this.dialog.add(CreateChannelDialog);\n                    }\n                },\n                name: _t(\"Create Channel\"),\n                className: \"o-mail-DiscussCommand-createChannel d-flex\",\n                props: { action: { icon: \"fa fa-fw fa-hashtag\", searchValueSuffix: true } },\n            };\n        }\n        if (threadOrPersona === NEW_GROUP_CHAT) {\n            const name = this.options.searchValue.trim();\n            return {\n                Component: DiscussCommand,\n                action: () => {\n                    this.dialog.add(CreateChatDialog, { name });\n                },\n                name: _t(\"Create Chat\"),\n                className: \"d-flex\",\n                props: { action: { icon: \"oi fa-fw oi-users\" } },\n            };\n        }\n        throw new Error(`Unsupported use of makeDiscussCommand(\"${threadOrPersona}\")`);\n    }\n}\n\ncommandProviderRegistry.add(\"find_or_start_conversation\", {\n    namespace: \"@\",\n    async provide(env, options) {\n        const palette = new DiscussCommandPalette(env, options);\n        await palette.fetch();\n        palette.buildResults();\n        palette.commands.slice(0, 8);\n        if (!palette.store.inPublicPage) {\n            palette.commands.push(palette.makeDiscussCommand(NEW_CHANNEL));\n            palette.commands.push(palette.makeDiscussCommand(NEW_GROUP_CHAT));\n        }\n        return palette.commands;\n    },\n});\n", "import { DiscussContent } from \"@mail/core/public_web/discuss_content\";\nimport { Call } from \"@mail/discuss/call/common/call\";\nimport { PipBanner } from \"@mail/discuss/call/common/pip_banner\";\nimport { useService } from \"@web/core/utils/hooks\";\n\nimport { patch } from \"@web/core/utils/patch\";\n\nObject.assign(DiscussContent.components, { Call, PipBanner });\n\npatch(DiscussContent.prototype, {\n    setup() {\n        super.setup(...arguments);\n        this.rtc = useService(\"discuss.rtc\");\n    },\n});\n", "import { reactive } from \"@odoo/owl\";\nimport { browser } from \"@web/core/browser/browser\";\nimport { _t } from \"@web/core/l10n/translation\";\n\nimport { registry } from \"@web/core/registry\";\n\nexport class DiscussCorePublicWeb {\n    /**\n     * @param {import(\"@web/env\").OdooEnv} env\n     * @param {import(\"services\").ServiceFactories} services\n     */\n    constructor(env, services) {\n        this.env = env;\n        this.store = services[\"mail.store\"];\n        this.busService = services.bus_service;\n        this.notificationService = services.notification;\n        this.rtcService = services[\"discuss.rtc\"];\n        try {\n            this.sidebarCategoriesBroadcast = new browser.BroadcastChannel(\n                \"discuss_core_public_web.sidebar_categories\"\n            );\n            this.sidebarCategoriesBroadcast.addEventListener(\n                \"message\",\n                ({ data: { id, open } }) => {\n                    const category = this.store.DiscussAppCategory.get(id);\n                    if (category) {\n                        category.open = open;\n                    }\n                }\n            );\n        } catch {\n            // BroadcastChannel API is not supported (e.g. Safari < 15.4), so disabling it.\n        }\n        this.busService.subscribe(\"discuss.channel/joined\", async (payload) => {\n            const {\n                data,\n                channel_id,\n                invite_to_rtc_call,\n                invited_by_user_id: invitedByUserId,\n            } = payload;\n            this.store.insert(data);\n            await this.store.fetchChannel(channel_id);\n            const thread = this.store.Thread.get({ id: channel_id, model: \"discuss.channel\" });\n            if (\n                thread &&\n                invitedByUserId &&\n                invitedByUserId !== this.store.self.main_user_id?.id &&\n                !invite_to_rtc_call\n            ) {\n                this.notificationService.add(\n                    _t(\"You have been invited to #%s\", thread.displayName),\n                    { type: \"info\" }\n                );\n            }\n        });\n        browser.navigator.serviceWorker?.addEventListener(\n            \"message\",\n            async ({ data: { action, data } }) => {\n                if (action === \"OPEN_CHANNEL\") {\n                    const channel = await this.store.Thread.getOrFetch({\n                        model: \"discuss.channel\",\n                        id: data.id,\n                    });\n                    channel?.open({ focus: true });\n                    if (!data.joinCall || !channel || this.rtcService.state.channel?.eq(channel)) {\n                        return;\n                    }\n                    if (this.rtcService.state.channel) {\n                        await this.rtcService.leaveCall();\n                    }\n                    this.rtcService.joinCall(channel);\n                } else if (action === \"POST_RTC_LOGS\") {\n                    const logs = data || {};\n                    logs.odooInfo = odoo.info;\n                    const string = JSON.stringify(logs);\n                    const blob = new Blob([string], { type: \"application/json\" });\n                    const downloadLink = document.createElement(\"a\");\n                    const now = luxon.DateTime.now().toFormat(\"yyyy-LL-dd_HH-mm\");\n                    downloadLink.download = `RtcLogs_${now}.json`;\n                    const url = URL.createObjectURL(blob);\n                    downloadLink.href = url;\n                    downloadLink.click();\n                    URL.revokeObjectURL(url);\n                }\n            }\n        );\n    }\n\n    /**\n     * Send the state of a category to the other tabs.\n     *\n     * @param {import(\"models\").DiscussAppCategory} category\n     */\n    broadcastCategoryState(category) {\n        this.sidebarCategoriesBroadcast?.postMessage({ id: category.id, open: category.open });\n    }\n}\n\nexport const discussCorePublicWeb = {\n    dependencies: [\"bus_service\", \"discuss.rtc\", \"mail.store\", \"notification\"],\n    /**\n     * @param {import(\"@web/env\").OdooEnv} env\n     * @param {import(\"services\").ServiceFactories} services\n     */\n    start(env, services) {\n        return reactive(new DiscussCorePublicWeb(env, services));\n    },\n};\n\nregistry.category(\"services\").add(\"discuss.core.public.web\", discussCorePublicWeb);\n", "import { CountryFlag } from \"@mail/core/common/country_flag\";\nimport { ImStatus } from \"@mail/core/common/im_status\";\nimport { ThreadIcon } from \"@mail/core/common/thread_icon\";\nimport { discussSidebarItemsRegistry } from \"@mail/core/public_web/discuss_sidebar\";\nimport { DiscussSidebarChannelActions } from \"@mail/discuss/core/public_web/discuss_sidebar_channel_actions\";\nimport { useHover, UseHoverOverlay } from \"@mail/utils/common/hooks\";\n\nimport { Component, useSubEnv } from \"@odoo/owl\";\n\nimport { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { useDropdownState } from \"@web/core/dropdown/dropdown_hooks\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { markEventHandled } from \"@web/core/utils/misc\";\n\nexport const discussSidebarChannelIndicatorsRegistry = registry.category(\n    \"mail.discuss_sidebar_channel_indicators\"\n);\n\nexport class DiscussSidebarSubchannel extends Component {\n    static template = \"mail.DiscussSidebarSubchannel\";\n    static props = [\"thread\", \"isFirst?\"];\n    static components = { DiscussSidebarChannelActions, Dropdown, UseHoverOverlay };\n\n    setup() {\n        super.setup();\n        this.store = useService(\"mail.store\");\n        this.hover = useHover([\"root\"], {\n            onHover: () => {\n                if (this.store.discuss.isSidebarCompact) {\n                    this.floating.isOpen = true;\n                }\n            },\n            onAway: () => {\n                if (this.store.discuss.isSidebarCompact) {\n                    this.floating.isOpen = false;\n                }\n            },\n            stateObserver: () => [this.floating?.isOpen],\n        });\n        this.floating = useDropdownState();\n        this.showingActions = useDropdownState();\n    }\n\n    get actionsTitle() {\n        return _t(\"Thread Actions\");\n    }\n\n    get thread() {\n        return this.props.thread;\n    }\n\n    /** @param {MouseEvent} ev */\n    openThread(ev, thread) {\n        markEventHandled(ev, \"sidebar.openThread\");\n        thread.open();\n    }\n}\n\nexport class DiscussSidebarChannel extends Component {\n    static template = \"mail.DiscussSidebarChannel\";\n    static props = [\"thread\"];\n    static components = {\n        CountryFlag,\n        DiscussSidebarChannelActions,\n        DiscussSidebarSubchannel,\n        Dropdown,\n        ImStatus,\n        ThreadIcon,\n        UseHoverOverlay,\n    };\n\n    setup() {\n        super.setup();\n        this.store = useService(\"mail.store\");\n        this.hover = useHover([\"root\"], {\n            onHover: () => {\n                if (this.store.discuss.isSidebarCompact) {\n                    this.floating.isOpen = true;\n                }\n            },\n            onAway: () => {\n                if (this.store.discuss.isSidebarCompact) {\n                    this.floating.isOpen = false;\n                }\n            },\n            stateObserver: () => [this.floating?.isOpen],\n        });\n        this.floating = useDropdownState();\n        this.showingActions = useDropdownState();\n    }\n\n    get actionsTitle() {\n        if (this.thread.channel_type === \"channel\") {\n            return _t(\"Channel Actions\");\n        }\n        return _t(\"Chat Actions\");\n    }\n\n    get attClass() {\n        return {\n            \"bg-inherit\": this.thread.notEq(this.store.discuss.thread),\n            \"o-active\": this.thread.eq(this.store.discuss.thread),\n            \"o-unread\":\n                this.thread.self_member_id?.message_unread_counter > 0 &&\n                !this.thread.self_member_id?.mute_until_dt,\n            \"border-bottom-0 rounded-bottom-0\": this.bordered,\n            \"opacity-50\": this.thread.self_member_id?.mute_until_dt,\n            \"position-relative justify-content-center o-compact mt-0 p-1\":\n                this.store.discuss.isSidebarCompact,\n            \"px-0\": !this.store.discuss.isSidebarCompact,\n        };\n    }\n\n    get attClassContainer() {\n        return {\n            \"border border-dark rounded-2 o-bordered\": this.bordered,\n            \"o-compact\": this.store.discuss.isSidebarCompact,\n        };\n    }\n\n    get bordered() {\n        return (\n            this.store.discuss.isSidebarCompact &&\n            Boolean(this.env.filteredThreads?.(this.thread.sub_channel_ids)?.length)\n        );\n    }\n\n    get indicators() {\n        return discussSidebarChannelIndicatorsRegistry.getAll();\n    }\n\n    get itemNameAttClass() {\n        return {\n            \"o-unread fw-bolder\":\n                this.thread.self_member_id?.message_unread_counter > 0 &&\n                !this.thread.self_member_id?.mute_until_dt,\n            \"opacity-75 opacity-100-hover\":\n                this.thread.self_member_id?.message_unread_counter === 0 ||\n                this.thread.self_member_id?.mute_until_dt,\n        };\n    }\n\n    /** @returns {import(\"models\").Thread} */\n    get thread() {\n        return this.props.thread;\n    }\n\n    get threadAvatarAttClass() {\n        return {};\n    }\n\n    get subChannels() {\n        return this.env.filteredThreads?.(this.thread.sub_channel_ids) ?? [];\n    }\n\n    showThread(sub) {\n        if (sub.eq(this.store.discuss.thread)) {\n            return true;\n        }\n        if (!this.thread.discussAppCategory.open) {\n            return false;\n        }\n        if (\n            !this.thread.self_member_id?.mute_until_dt ||\n            sub.self_member_id?.message_unread_counter > 0\n        ) {\n            return true;\n        }\n        return (\n            this.isSelfOrThreadActive &&\n            !(this.thread.self_member_id?.mute_until_dt && sub.self_member_id?.mute_until_dt)\n        );\n    }\n\n    get isSelfOrThreadActive() {\n        return (\n            this.thread.eq(this.store.discuss.thread) ||\n            this.store.discuss.thread?.in(this.subChannels)\n        );\n    }\n\n    /** @param {MouseEvent} ev */\n    openThread(ev, thread) {\n        markEventHandled(ev, \"sidebar.openThread\");\n        thread.open();\n    }\n}\n\nexport class DiscussSidebarCategory extends Component {\n    static template = \"mail.DiscussSidebarCategory\";\n    static props = [\"category\"];\n    static components = { Dropdown };\n\n    setup() {\n        super.setup();\n        this.store = useService(\"mail.store\");\n        this.discusscorePublicWebService = useService(\"discuss.core.public.web\");\n        this.hover = useHover([\"root\", \"floating\"], {\n            onHover: () => {\n                if (this.store.discuss.isSidebarCompact) {\n                    this.onHover(true);\n                }\n            },\n            onAway: () => {\n                if (this.store.discuss.isSidebarCompact) {\n                    this.onHover(false);\n                }\n            },\n        });\n        this.floating = useDropdownState();\n    }\n\n    onHover(hovering) {\n        this.floating.isOpen = hovering;\n    }\n\n    /** @returns {import(\"models\").DiscussAppCategory} */\n    get category() {\n        return this.props.category;\n    }\n\n    get actions() {\n        return [];\n    }\n\n    toggle() {\n        if (this.store.channels.status === \"fetching\") {\n            return;\n        }\n        this.category.open = !this.category.open;\n        this.discusscorePublicWebService.broadcastCategoryState(this.category);\n    }\n}\n\n/**\n * @typedef {Object} Props\n * @extends {Component<Props, Env>}\n */\nexport class DiscussSidebarCategories extends Component {\n    static template = \"mail.DiscussSidebarCategories\";\n    static props = {};\n    static components = {\n        DiscussSidebarCategory,\n        DiscussSidebarChannel,\n        Dropdown,\n    };\n\n    setup() {\n        super.setup();\n        this.store = useService(\"mail.store\");\n        this.discusscorePublicWebService = useService(\"discuss.core.public.web\");\n        this.orm = useService(\"orm\");\n        useSubEnv({\n            filteredThreads: (threads) => this.filteredThreads(threads),\n        });\n    }\n\n    filteredThreads(threads) {\n        return threads.filter((thread) => thread.displayInSidebar);\n    }\n}\n\ndiscussSidebarItemsRegistry.add(\"channels\", DiscussSidebarCategories, { sequence: 30 });\n", "import { ActionList } from \"@mail/core/common/action_list\";\nimport { useThreadActions } from \"@mail/core/common/thread_actions\";\n\nimport { Component } from \"@odoo/owl\";\nimport { useService } from \"@web/core/utils/hooks\";\n/**\n * @typedef {Object} Props\n * @property {import(\"@mail/core/common/thread_model\").Thread} thread\n * @property {function} [close]\n * @extends {Component<Props, Env>}\n */\nexport class DiscussSidebarChannelActions extends Component {\n    static template = \"mail.DiscussSidebarChannelActions\";\n    static props = [\"thread\"];\n    static components = { ActionList };\n\n    setup() {\n        this.store = useService(\"mail.store\");\n        this.isDiscussSidebarChannelActions = true;\n        this.threadActions = useThreadActions({ thread: () => this.thread });\n    }\n\n    get thread() {\n        return this.props.thread;\n    }\n}\n", "import { registerMessageAction } from \"@mail/core/common/message_actions\";\nimport { _t } from \"@web/core/l10n/translation\";\n\nregisterMessageAction(\"create-or-view-thread\", {\n    condition: ({ message, store, thread }) =>\n        message.thread?.eq(thread) &&\n        message.thread.hasSubChannelFeature &&\n        store.self.main_user_id?.share === false,\n    icon: \"fa fa-comments-o\",\n    onSelected: ({ message }) => {\n        if (message.linkedSubChannel) {\n            message.linkedSubChannel.open({ focus: true });\n        } else {\n            message.thread.createSubChannel({ initialMessage: message });\n        }\n    },\n    name: ({ message }) => (message.linkedSubChannel ? _t(\"View Thread\") : _t(\"Create Thread\")),\n    sequence: 75,\n});\n", "import { Message } from \"@mail/core/common/message_model\";\nimport { fields } from \"@mail/model/misc\";\nimport { patch } from \"@web/core/utils/patch\";\n\npatch(Message.prototype, {\n    setup() {\n        super.setup(...arguments);\n        this.linkedSubChannel = fields.One(\"Thread\", { inverse: \"from_message_id\" });\n    },\n});\n", "import { Message } from \"@mail/core/common/message\";\nimport { SubChannelPreview } from \"@mail/discuss/core/public_web/sub_channel_preview\";\n\nObject.assign(Message.components, { SubChannelPreview });\n", "import { MessagingMenu } from \"@mail/core/public_web/messaging_menu\";\n\nimport { patch } from \"@web/core/utils/patch\";\n\npatch(MessagingMenu.prototype, {\n    /** @override */\n    markAsRead(thread) {\n        super.markAsRead(...arguments);\n        if (thread.model === \"discuss.channel\") {\n            thread.markAsRead();\n        }\n    },\n});\n", "import { Store } from \"@mail/core/common/store_service\";\nimport { useSequential } from \"@mail/utils/common/hooks\";\nimport { rpc } from \"@web/core/network/rpc\";\n\nimport { patch } from \"@web/core/utils/patch\";\n\n/** @type {import(\"models\").Store} */\nconst StorePatch = {\n    setup() {\n        super.setup(...arguments);\n        this.channels = this.makeCachedFetchData(\"channels_as_member\");\n        this.fetchSsearchConversationsSequential = useSequential();\n    },\n    /** @param {string} searchValue */\n    async searchConversations(searchValue) {\n        const data = await this.fetchSsearchConversationsSequential(async () => {\n            const data = await rpc(\"/discuss/search\", { term: searchValue });\n            return data;\n        });\n        this.insert(data);\n    },\n};\npatch(Store.prototype, StorePatch);\n", "import { NotificationItem } from \"@mail/core/public_web/notification_item\";\nimport { ActionPanel } from \"@mail/discuss/core/common/action_panel\";\nimport { SubChannelPreview } from \"@mail/discuss/core/public_web/sub_channel_preview\";\nimport { useSequential, useVisible } from \"@mail/utils/common/hooks\";\nimport { Component, useEffect, useRef, useState } from \"@odoo/owl\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { useAutofocus, useService } from \"@web/core/utils/hooks\";\nimport { fuzzyLookup } from \"@web/core/utils/search\";\n\n/**\n * @typedef {Object} Props\n * @property {import(\"@mail/core/common/thread_model\").Thread} thread\n * @property {function} [close]\n * @extends {Component<Props, Env>}\n */\nexport class SubChannelList extends Component {\n    static template = \"mail.SubChannelList\";\n    static components = { ActionPanel, NotificationItem, SubChannelPreview };\n\n    static props = [\"thread\", \"close?\"];\n\n    setup() {\n        this.store = useService(\"mail.store\");\n        this.state = useState({\n            loading: false,\n            searchTerm: \"\",\n            lastSearchTerm: \"\",\n            searching: false,\n            subChannels: this.props.thread.sub_channel_ids,\n        });\n        this.searchRef = useRef(\"search\");\n        this.sequential = useSequential();\n        useAutofocus({ refName: \"search\" });\n        this.loadMoreState = useVisible(\"load-more\", (isVisible) => {\n            if (isVisible) {\n                this.props.thread.loadMoreSubChannels({\n                    searchTerm: this.state.searching ? this.state.searchTerm : undefined,\n                });\n            }\n        });\n        useEffect(\n            (searchTerm) => {\n                if (!searchTerm) {\n                    this.clearSearch();\n                }\n            },\n            () => [this.state.searchTerm]\n        );\n    }\n\n    get NO_THREAD_FOUND() {\n        return _t(`No thread named \"%(thread_name)s\"`, { thread_name: this.state.lastSearchTerm });\n    }\n\n    async onClickSubThread(subThread) {\n        if (!subThread.hasSelfAsMember) {\n            await rpc(\"/discuss/channel/join\", { channel_id: subThread.id });\n        }\n        subThread.open({ focus: true });\n        if (this.env.inChatWindow) {\n            this.props.close?.();\n        }\n    }\n\n    clearSearch() {\n        this.state.searchTerm = \"\";\n        this.state.lastSearchTerm = \"\";\n        this.state.searching = false;\n        this.state.loading = false;\n        this.state.subChannels = this.props.thread.sub_channel_ids;\n    }\n\n    onKeydownSearch(ev) {\n        if (ev.key === \"Enter\") {\n            this.search();\n        }\n    }\n\n    async onClickCreate() {\n        await this.props.thread.createSubChannel({ name: this.state.searchTerm });\n        this._refreshSubChannelList();\n        this.props.close?.();\n    }\n\n    async search() {\n        if (!this.state.searchTerm) {\n            return;\n        }\n        this.sequential(async () => {\n            this.state.searching = true;\n            this.state.loading = true;\n            try {\n                await this.props.thread.loadMoreSubChannels({\n                    searchTerm: this.state.searchTerm,\n                });\n                if (this.state.searching) {\n                    this._refreshSubChannelList();\n                    this.state.lastSearchTerm = this.state.searchTerm;\n                }\n            } finally {\n                this.state.loading = false;\n            }\n        });\n    }\n\n    _refreshSubChannelList() {\n        this.state.subChannels = fuzzyLookup(\n            this.state.searchTerm ?? \"\",\n            this.props.thread.sub_channel_ids,\n            ({ name }) => name\n        );\n    }\n}\n", "import { isToday } from \"@mail/utils/common/dates\";\nimport { Component } from \"@odoo/owl\";\n\nconst { DateTime } = luxon;\n\nexport class SubChannelPreview extends Component {\n    static template = \"mail.SubChannelPreview\";\n    static props = [\"class?\", \"onClick?\", \"thread\"];\n\n    dateText(message) {\n        if (isToday(message.datetime)) {\n            return message.datetime?.toLocaleString(DateTime.TIME_SIMPLE);\n        }\n        return message.datetime?.toLocaleString(DateTime.DATE_MED);\n    }\n\n    get thread() {\n        return this.props.thread;\n    }\n\n    onClick() {\n        this.props.onClick?.();\n    }\n}\n", "import { registerThreadAction } from \"@mail/core/common/thread_actions\";\nimport { SubChannelList } from \"@mail/discuss/core/public_web/sub_channel_list\";\nimport { useChildSubEnv } from \"@odoo/owl\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { usePopover } from \"@web/core/popover/popover_hook\";\n\nregisterThreadAction(\"show-threads\", {\n    actionPanelComponent: SubChannelList,\n    actionPanelComponentProps: ({ action }) => ({ close: () => action.close() }),\n    close: ({ action }) => action.popover?.close(),\n    condition: ({ owner, thread }) =>\n        (thread?.hasSubChannelFeature || thread?.parent_channel_id?.hasSubChannelFeature) &&\n        !owner.isDiscussSidebarChannelActions,\n    icon: \"fa fa-fw fa-comments-o\",\n    name: _t(\"Threads\"),\n    setup({ owner, store }) {\n        if (owner.env.inDiscussApp && !store.env.isSmall) {\n            this.popover = usePopover(SubChannelList, {\n                onClose: () => this.close(),\n                fixedPosition: true,\n                popoverClass: this.panelOuterClass,\n            });\n        }\n        useChildSubEnv({ subChannelMenu: { open: () => this.open() } });\n    },\n    open({ owner, thread }) {\n        const channel = thread?.parent_channel_id || thread;\n        this.popover?.open(owner.root.el.querySelector(`[name=\"${this.id}\"]`), { thread: channel });\n    },\n    panelOuterClass: \"bg-100 border border-secondary\",\n    sequence: ({ owner }) => (owner.props.chatWindow ? 40 : 5),\n    sequenceGroup: 10,\n    toggle: true,\n});\n", "import { Thread } from \"@mail/core/common/thread_model\";\nimport { fields } from \"@mail/model/misc\";\nimport { compareDatetime } from \"@mail/utils/common/misc\";\nimport { rpc } from \"@web/core/network/rpc\";\n\nimport { patch } from \"@web/core/utils/patch\";\n\n/** @type {import(\"models\").Thread} */\nconst threadPatch = {\n    setup() {\n        super.setup(...arguments);\n        this.appAsUnreadChannels = fields.One(\"DiscussApp\", {\n            compute() {\n                return this.channel_type === \"channel\" && this.isUnread ? this.store.discuss : null;\n            },\n        });\n        this.categoryAsThreadWithCounter = fields.One(\"DiscussAppCategory\", {\n            compute() {\n                return this.displayInSidebar && this.importantCounter > 0\n                    ? this.discussAppCategory\n                    : null;\n            },\n        });\n        this.discussAppCategory = fields.One(\"DiscussAppCategory\", {\n            compute() {\n                return this._computeDiscussAppCategory();\n            },\n        });\n        this.from_message_id = fields.One(\"mail.message\");\n        this.parent_channel_id = fields.One(\"Thread\", {\n            onDelete() {\n                this.delete();\n            },\n        });\n        this.sub_channel_ids = fields.Many(\"Thread\", {\n            inverse: \"parent_channel_id\",\n            sort: (a, b) => compareDatetime(b.lastInterestDt, a.lastInterestDt) || b.id - a.id,\n        });\n        this.displayInSidebar = fields.Attr(false, {\n            compute() {\n                return this._computeDisplayInSidebar();\n            },\n        });\n        this.loadSubChannelsDone = false;\n        /** @type {import(\"models\").Thread|null} */\n        this.lastSubChannelLoaded = null;\n    },\n    get canLeave() {\n        return !this.parent_channel_id && super.canLeave;\n    },\n    _computeDisplayInSidebar() {\n        return (\n            this.displayToSelf ||\n            this.isLocallyPinned ||\n            this.sub_channel_ids.some((t) => t.displayInSidebar)\n        );\n    },\n    _computeDiscussAppCategory() {\n        if (this.parent_channel_id) {\n            return;\n        }\n        if ([\"group\", \"chat\"].includes(this.channel_type)) {\n            return this.store.discuss.chats;\n        }\n        if (this.channel_type === \"channel\") {\n            return this.store.discuss.channels;\n        }\n    },\n    get allowCalls() {\n        return super.allowCalls && !this.parent_channel_id;\n    },\n    get hasSubChannelFeature() {\n        return [\"channel\", \"group\"].includes(this.channel_type);\n    },\n    get isEmpty() {\n        return !this.from_message_id && super.isEmpty;\n    },\n    /**\n     * @param {Object} [param0={}]\n     * @param {import(\"models\").Message} [param0.initialMessage]\n     * @param {string} [param0.name]\n     */\n    async createSubChannel({ initialMessage, name } = {}) {\n        const { store_data, sub_channel } = await rpc(\"/discuss/channel/sub_channel/create\", {\n            parent_channel_id: this.parent_channel_id?.id || this.id,\n            from_message_id: initialMessage?.id,\n            name,\n        });\n        this.store.insert(store_data);\n        this.store.Thread.get({ model: \"discuss.channel\", id: sub_channel }).open({ focus: true });\n    },\n    /**\n     * @param {*} param0\n     * @param {string} [param0.searchTerm]\n     * @returns {Promise<import(\"models\").Thread[]|undefined>}\n     */\n    async loadMoreSubChannels({ searchTerm } = {}) {\n        if (this.loadSubChannelsDone) {\n            return;\n        }\n        const limit = 30;\n        const { store_data, sub_channel_ids } = await rpc(\"/discuss/channel/sub_channel/fetch\", {\n            before: this.lastSubChannelLoaded?.id,\n            limit,\n            parent_channel_id: this.id,\n            search_term: searchTerm,\n        });\n        this.store.insert(store_data);\n        const threads = sub_channel_ids.map((subChannelId) =>\n            this.store.Thread.get({ model: \"discuss.channel\", id: subChannelId })\n        );\n\n        if (searchTerm) {\n            // Ignore holes in the sub-channel list that may arise when\n            // searching for a specific term.\n            return;\n        }\n        const subChannels = threads.filter((thread) => this.eq(thread.parent_channel_id));\n        this.lastSubChannelLoaded = subChannels.reduce(\n            (min, channel) => (!min || channel.id < min.id ? channel : min),\n            this.lastSubChannelLoaded\n        );\n        if (subChannels.length < limit) {\n            this.loadSubChannelsDone = true;\n        }\n        return subChannels;\n    },\n    onPinStateUpdated() {\n        super.onPinStateUpdated();\n        if (this.self_member_id?.is_pinned) {\n            this.isLocallyPinned = false;\n        }\n        if (!this.self_member_id?.is_pinned && !this.isLocallyPinned) {\n            this.sub_channel_ids.forEach((c) => (c.isLocallyPinned = false));\n        }\n    },\n    /** @override */\n    openChannel() {\n        if (this.store.discuss.isActive && !this.store.env.services.ui.isSmall) {\n            this.setAsDiscussThread();\n            return true;\n        }\n        return super.openChannel();\n    },\n    setAsDiscussThread() {\n        super.setAsDiscussThread(...arguments);\n        if (!this.displayToSelf && this.model === \"discuss.channel\") {\n            this.isLocallyPinned = true;\n        }\n    },\n};\npatch(Thread.prototype, threadPatch);\n", "import { Thread } from \"@mail/core/common/thread\";\nimport { patch } from \"@web/core/utils/patch\";\n\npatch(Thread.prototype, {\n    get orderedMessages() {\n        const result = super.orderedMessages;\n        if (this.props.thread.from_message_id) {\n            if (this.props.order === \"asc\") {\n                result.unshift(this.props.thread.from_message_id);\n            } else {\n                result.push(this.props.thread.from_message_id);\n            }\n        }\n        return result;\n    },\n});\n", "import { ImStatus } from \"@mail/core/common/im_status\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { patch } from \"@web/core/utils/patch\";\nimport { BurgerMenu } from \"@web/webclient/burger_menu/burger_menu\";\n\nObject.assign(BurgerMenu.components, { ImStatus });\n\npatch(BurgerMenu.prototype, {\n    setup() {\n        super.setup();\n        this.store = useService(\"mail.store\");\n    },\n});\n", "import { ChannelMemberList } from \"@mail/discuss/core/common/channel_member_list\";\nimport { AvatarCardPopover } from \"@mail/discuss/web/avatar_card/avatar_card_popover\";\n\nimport { usePopover } from \"@web/core/popover/popover_hook\";\nimport { patch } from \"@web/core/utils/patch\";\n\npatch(ChannelMemberList.prototype, {\n    setup() {\n        super.setup(...arguments);\n        this.avatarCard = usePopover(AvatarCardPopover, {\n            position: \"right\",\n        });\n    },\n    onClickAvatar(ev, member) {\n        if (!this.canOpenChatWith(member)) {\n            return;\n        }\n        if (!this.avatarCard.isOpen) {\n            this.avatarCard.open(ev.currentTarget, {\n                id: member.partner_id.main_user_id?.id,\n            });\n        }\n    },\n});\nObject.assign(ChannelMemberList.components, { AvatarCardPopover });\n", "import { DiscussCommandPalette } from \"@mail/discuss/core/public_web/discuss_command_palette\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { patch } from \"@web/core/utils/patch\";\n\nconst commandCategoryRegistry = registry.category(\"command_categories\");\n\nconst DISCUSS_MENTIONED = \"DISCUSS_MENTIONED\";\nconst DISCUSS_RECENT = \"DISCUSS_RECENT\";\n\ncommandCategoryRegistry\n    .add(DISCUSS_MENTIONED, { namespace: \"@\", name: _t(\"Mentions\") }, { sequence: 10 })\n    .add(DISCUSS_RECENT, { namespace: \"@\", name: _t(\"Recent\") }, { sequence: 20 });\n\npatch(DiscussCommandPalette.prototype, {\n    buildResults() {\n        const importantChannels = this.store.getSelfImportantChannels();\n        const recentChannels = this.store.getSelfRecentChannels();\n        const mentionedSet = new Set();\n        const recentSet = new Set();\n        const CATEGORY_LIMIT = 3;\n        if (!this.cleanedTerm) {\n            const limitedMentioned = importantChannels.slice(0, CATEGORY_LIMIT);\n            for (const channel of limitedMentioned) {\n                this.commands.push(this.makeDiscussCommand(channel, DISCUSS_MENTIONED));\n                if (channel.channel_type === \"chat\") {\n                    mentionedSet.add(channel.correspondent.persona);\n                } else {\n                    mentionedSet.add(channel);\n                }\n            }\n            const limitedRecent = recentChannels\n                .filter(\n                    (channel) =>\n                        !mentionedSet.has(channel) &&\n                        !mentionedSet.has(channel.correspondent?.persona)\n                )\n                .slice(0, CATEGORY_LIMIT);\n            for (const channel of limitedRecent) {\n                this.commands.push(this.makeDiscussCommand(channel, DISCUSS_RECENT));\n                if (channel.channel_type === \"chat\") {\n                    recentSet.add(channel.correspondent.persona);\n                } else {\n                    recentSet.add(channel);\n                }\n            }\n        }\n        super.buildResults(new Set([...mentionedSet, ...recentSet]));\n    },\n});\n", "import { DiscussCoreCommon } from \"@mail/discuss/core/common/discuss_core_common_service\";\n\nimport { patch } from \"@web/core/utils/patch\";\n\n/** @type {DiscussCoreCommon} */\nconst discussCoreCommon = {\n    async _handleNotificationNewMessage(...args) {\n        // initChannelsUnreadCounter becomes unreliable\n        await this.store.channels.fetch();\n        return super._handleNotificationNewMessage(...args);\n    },\n};\n\npatch(DiscussCoreCommon.prototype, discussCoreCommon);\n", "import { reactive } from \"@odoo/owl\";\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\n\nexport class DiscussCoreWeb {\n    /**\n     * @param {import(\"@web/env\").OdooEnv} env\n     * @param {import(\"services\").ServiceFactories} services\n     */\n    constructor(env, services) {\n        this.env = env;\n        this.busService = services.bus_service;\n        this.notificationService = services.notification;\n        this.ui = services.ui;\n        this.store = services[\"mail.store\"];\n        this.multiTab = services.multi_tab;\n    }\n\n    setup() {\n        this.busService.subscribe(\"res.users/connection\", async ({ partnerId, username }) => {\n            // If the current user invited a new user, and the new user is\n            // connecting for the first time while the current user is present\n            // then open a chat for the current user with the new user.\n            const notification = _t(\n                \"%(user)s connected. This is their first connection. Wish them luck.\",\n                { user: username }\n            );\n            this.notificationService.add(notification, { type: \"info\" });\n            if (!(await this.multiTab.isOnMainTab())) {\n                return;\n            }\n            const chat = await this.store.getChat({ partnerId });\n            if (chat && !this.ui.isSmall) {\n                chat.openChatWindow({ focus: false });\n            }\n        });\n        this.env.bus.addEventListener(\"mail.message/delete\", ({ detail: { message } }) => {\n            if (message.thread?.model === \"discuss.channel\") {\n                // initChannelsUnreadCounter becomes unreliable\n                this.store.channels.fetch();\n            }\n        });\n    }\n}\n\nexport const discussCoreWeb = {\n    dependencies: [\"bus_service\", \"mail.store\", \"notification\", \"ui\", \"multi_tab\"],\n    /**\n     * @param {import(\"@web/env\").OdooEnv} env\n     * @param {import(\"services\").ServiceFactories} services\n     */\n    start(env, services) {\n        const discussCoreWeb = reactive(new DiscussCoreWeb(env, services));\n        discussCoreWeb.setup();\n        return discussCoreWeb;\n    },\n};\n\nregistry.category(\"services\").add(\"discuss.core.web\", discussCoreWeb);\n", "import { patch } from \"@web/core/utils/patch\";\nimport { DiscussSidebarCategory } from \"../public_web/discuss_sidebar_categories\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { useService } from \"@web/core/utils/hooks\";\n\n/** @type {import(\"@mail/discuss/core/public_web/discuss_sidebar_categories\").DiscussSidebarCategory} */\nconst DiscussSidebarCategoryPatch = {\n    setup() {\n        super.setup();\n        this.actionService = useService(\"action\");\n    },\n    open() {\n        if (this.category.id === \"channels\") {\n            this.actionService.doAction({\n                name: _t(\"Public Channels\"),\n                type: \"ir.actions.act_window\",\n                res_model: \"discuss.channel\",\n                views: [\n                    [false, \"kanban\"],\n                    [false, \"list\"],\n                    [false, \"form\"],\n                ],\n                domain: [\n                    [\"channel_type\", \"=\", \"channel\"],\n                    [\"parent_channel_id\", \"=\", false],\n                ],\n            });\n        }\n    },\n    get actions() {\n        const actions = super.actions;\n        if (this.category.canView) {\n            actions.push({\n                onSelect: () => this.open(),\n                label: _t(\"View or join channels\"),\n                icon: \"fa fa-cog\",\n            });\n        }\n        return actions;\n    },\n};\n\npatch(DiscussSidebarCategory.prototype, DiscussSidebarCategoryPatch);\n", "import { DiscussSearch } from \"@mail/core/public_web/discuss_search\";\nimport { MessagingMenu } from \"@mail/core/public_web/messaging_menu\";\nimport { patch } from \"@web/core/utils/patch\";\nimport { useService } from \"@web/core/utils/hooks\";\n\nObject.assign(MessagingMenu.components, { DiscussSearch });\n\npatch(MessagingMenu.prototype, {\n    setup() {\n        super.setup();\n        this.command = useService(\"command\");\n    },\n    beforeOpen() {\n        const res = super.beforeOpen(...arguments);\n        this.store.channels.fetch();\n        return res;\n    },\n    onClickNewMessage() {\n        this.command.openMainPalette({ searchValue: \"@\" });\n        if (!this.ui.isSmall && !this.env.inDiscussApp) {\n            this.dropdown.close();\n        }\n    },\n});\n", "import { Store } from \"@mail/core/common/store_service\";\nimport { compareDatetime } from \"@mail/utils/common/misc\";\nimport { AvatarCardPopover } from \"@mail/discuss/web/avatar_card/avatar_card_popover\";\n\nimport { patch } from \"@web/core/utils/patch\";\n\n/** @type {import(\"models\").Store} */\nconst StorePatch = {\n    setup() {\n        super.setup(...arguments);\n        this.initChannelsUnreadCounter = 0;\n    },\n    computeGlobalCounter() {\n        if (!this.Thread) {\n            return super.computeGlobalCounter();\n        }\n        const channelsContribution =\n            this.channels.status !== \"fetched\"\n                ? this.initChannelsUnreadCounter\n                : Object.values(this.Thread.records).filter(\n                      (thread) =>\n                          thread.displayToSelf &&\n                          !thread.self_member_id?.mute_until_dt &&\n                          (thread.self_member_id?.message_unread_counter ||\n                              thread.message_needaction_counter)\n                  ).length;\n        // Needactions are already counted in the super call, but we want to discard them for channel so that there is only +1 per channel.\n        const channelsNeedactionCounter = Object.values(this.Thread.records).reduce(\n            (acc, thread) =>\n                acc + (thread.model === \"discuss.channel\" ? thread.message_needaction_counter : 0),\n            0\n        );\n        return super.computeGlobalCounter() + channelsContribution + channelsNeedactionCounter;\n    },\n    /** @returns {import(\"models\").Thread[]} */\n    getSelfImportantChannels() {\n        return this.getSelfRecentChannels().filter((channel) => channel.importantCounter > 0);\n    },\n    /** @returns {import(\"models\").Thread[]} */\n    getSelfRecentChannels() {\n        return Object.values(this.Thread.records)\n            .filter((thread) => thread.model === \"discuss.channel\" && thread.self_member_id)\n            .sort((a, b) => compareDatetime(b.lastInterestDt, a.lastInterestDt) || b.id - a.id);\n    },\n    onStarted() {\n        super.onStarted();\n        if (this.discuss.isActive) {\n            this.channels.fetch();\n        }\n    },\n    onLinkFollowed(fromThread) {\n        super.onLinkFollowed(...arguments);\n        if (!this.env.isSmall && fromThread?.model === \"discuss.channel\") {\n            fromThread.open({ focus: false });\n        }\n    },\n    /**\n     * @override\n     * @param {MouseEvent} ev\n     * @param {number} id\n     */\n    onClickPartnerMention(ev, id) {\n        this.env.services.popover.add(ev.target, AvatarCardPopover, {\n            id,\n            model: \"res.partner\",\n        });\n    },\n};\npatch(Store.prototype, StorePatch);\n", "import { registerThreadAction } from \"@mail/core/common/thread_actions\";\n\nimport { _t } from \"@web/core/l10n/translation\";\n\nregisterThreadAction(\"expand-discuss\", {\n    condition: ({ owner, store, thread }) =>\n        thread &&\n        owner.props.chatWindow?.isOpen &&\n        thread.model === \"discuss.channel\" &&\n        !store.env.services.ui.isSmall &&\n        !owner.isDiscussSidebarChannelActions,\n    icon: \"fa fa-fw fa-expand\",\n    name: _t(\"Open in Discuss\"),\n    open({ owner, store, thread }) {\n        store.env.services.action.doAction(\n            {\n                type: \"ir.actions.client\",\n                tag: \"mail.action_discuss\",\n            },\n            {\n                clearBreadcrumbs: owner.env.services[\"home_menu\"]?.hasHomeMenu,\n                additionalContext: { active_id: thread.id },\n            }\n        );\n    },\n    sequence: 10,\n    sequenceGroup: 5,\n});\nregisterThreadAction(\"advanced-settings\", {\n    condition: ({ owner, thread }) => thread && owner.isDiscussSidebarChannelActions,\n    open: ({ owner, store, thread }) => {\n        store.env.services.action.doAction({\n            type: \"ir.actions.act_window\",\n            res_model: \"discuss.channel\",\n            views: [[false, \"form\"]],\n            res_id: thread.id,\n            target: \"current\",\n        });\n    },\n    icon: \"fa fa-fw fa-gear\",\n    name: _t(\"Advanced Settings\"),\n    sequence: 20,\n    sequenceGroup: 30,\n});\n", "import { Thread } from \"@mail/core/common/thread_model\";\n\nimport { patch } from \"@web/core/utils/patch\";\n\npatch(Thread.prototype, {\n    onPinStateUpdated() {\n        super.onPinStateUpdated();\n        if (!this.displayToSelf && !this.isLocallyPinned && this.eq(this.store.discuss.thread)) {\n            if (this.store.discuss.isActive) {\n                const newThread =\n                    this.store.discuss.channels.threads.find(\n                        (thread) => thread.displayToSelf || thread.isLocallyPinned\n                    ) || this.store.inbox;\n                newThread.setAsDiscussThread();\n            } else {\n                this.store.discuss.thread = undefined;\n            }\n        }\n    },\n});\n", "import { ImStatus } from \"@mail/core/common/im_status\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { patch } from \"@web/core/utils/patch\";\nimport { UserMenu } from \"@web/webclient/user_menu/user_menu\";\n\nObject.assign(UserMenu.components, { ImStatus });\n\npatch(UserMenu.prototype, {\n    setup() {\n        super.setup();\n        this.store = useService(\"mail.store\");\n    },\n});\n", "import { closeStream } from \"@mail/utils/common/misc\";\n\nimport { browser } from \"@web/core/browser/browser\";\n\nconst FPS = 30; // Frames per second for the blurred background stream\n\nfunction drawAndBlurImageOnCanvas(image, blurAmount, canvas) {\n    canvas.width = image.width;\n    canvas.height = image.height;\n    if (blurAmount === 0) {\n        canvas.getContext(\"2d\").drawImage(image, 0, 0, image.width, image.height);\n        return;\n    }\n    canvas.getContext(\"2d\").clearRect(0, 0, image.width, image.height);\n    canvas.getContext(\"2d\").save();\n    // FIXME : Does not work on safari https://bugs.webkit.org/show_bug.cgi?id=198416\n    canvas.getContext(\"2d\").filter = `blur(${blurAmount}px)`;\n    canvas.getContext(\"2d\").drawImage(image, 0, 0, image.width, image.height);\n    canvas.getContext(\"2d\").restore();\n}\n\nexport class BlurManager {\n    canvas = document.createElement(\"canvas\");\n    canvasBlur = document.createElement(\"canvas\");\n    canvasMask = document.createElement(\"canvas\");\n    canvasStream;\n    isVideoDataLoaded = false;\n    rejectStreamPromise;\n    resolveStreamPromise;\n    selfieSegmentation = new window.SelfieSegmentation({\n        locateFile: (file) => {\n            return `https://cdn.jsdelivr.net/npm/@mediapipe/selfie_segmentation@0.1/${file}`;\n        },\n    });\n    /**\n     * Promise or undefined, based on the input stream, resolved when selfieSegmentation has started painting on the canvas,\n     * resolves into a web.MediaStream that is the blurred version of the input stream.\n     */\n    stream;\n    video = document.createElement(\"video\");\n    worker;\n\n    constructor(\n        stream,\n        { backgroundBlur = 10, edgeBlur = 10, modelSelection = 1, selfieMode = false } = {}\n    ) {\n        this.edgeBlur = edgeBlur;\n        this.backgroundBlur = backgroundBlur;\n        this._onVideoPlay = this._onVideoPlay.bind(this);\n        this.video.addEventListener(\"loadeddata\", this._onVideoPlay);\n        this.canvas.getContext(\"2d\"); // canvas.captureStream() doesn't work on firefox before getContext() is called.\n        this.canvasStream = this.canvas.captureStream();\n        let rejectStreamPromise;\n        let resolveStreamPromise;\n        Object.assign(this, {\n            stream: new Promise((resolve, reject) => {\n                rejectStreamPromise = reject;\n                resolveStreamPromise = resolve;\n            }),\n            rejectStreamPromise,\n            resolveStreamPromise,\n        });\n        try {\n            this.worker = new Worker(\"/mail/static/src/discuss/call/common/tick_worker.js\");\n            this.worker.onmessage = (e) => this._handleWorkerMessage(e);\n            this.worker.onerror = () => {\n                this._terminateWorker();\n                this._requestFrame();\n            };\n        } catch {\n            this.worker = null;\n        }\n        this.video.srcObject = stream;\n        this.video.load();\n        this.selfieSegmentation.setOptions({\n            selfieMode,\n            modelSelection,\n        });\n        this.selfieSegmentation.onResults((r) => this._onSelfieSegmentationResults(r));\n        this.video.autoplay = true;\n        Promise.resolve(this.video.play()).catch(() => {});\n    }\n\n    close() {\n        this.video.removeEventListener(\"loadeddata\", this._onVideoPlay);\n        this.video.srcObject = null;\n        this.isVideoDataLoaded = false;\n        this.selfieSegmentation.reset();\n        closeStream(this.canvasStream);\n        this.canvasStream = null;\n        this._terminateWorker();\n        if (this.rejectStreamPromise) {\n            this.rejectStreamPromise(\n                new Error(\"The source stream was removed before the beginning of the blur process\")\n            );\n        }\n    }\n\n    /**\n     * @private\n     * @param {MessageEvent} e\n     */\n    async _handleWorkerMessage(e) {\n        if (e.data.command === \"tick\") {\n            await this._onFrame();\n            this.worker.postMessage({ command: \"tock\" });\n        }\n    }\n\n    /**\n     * @private\n     */\n    _terminateWorker() {\n        if (this.worker) {\n            this.worker.postMessage({ command: \"stop\" });\n            this.worker.terminate();\n        }\n        this.worker = null;\n    }\n\n    _drawWithCompositing(image, compositeOperation) {\n        this.canvas.getContext(\"2d\").globalCompositeOperation = compositeOperation;\n        this.canvas.getContext(\"2d\").drawImage(image, 0, 0);\n    }\n\n    /**\n     * @private\n     */\n    _onVideoPlay() {\n        this.isVideoDataLoaded = true;\n        if (this.worker) {\n            this.worker.postMessage({ command: \"start\", fps: FPS });\n        } else {\n            this._requestFrame();\n        }\n    }\n\n    /**\n     * @private\n     */\n    async _onFrame() {\n        if (!this.selfieSegmentation) {\n            return;\n        }\n        if (!this.video) {\n            return;\n        }\n        if (!this.isVideoDataLoaded) {\n            return;\n        }\n        await this.selfieSegmentation.send({ image: this.video });\n    }\n\n    /**\n     * @private\n     */\n    _onSelfieSegmentationResults(results) {\n        drawAndBlurImageOnCanvas(results.image, this.backgroundBlur, this.canvasBlur);\n        this.canvas.width = this.canvasBlur.width;\n        this.canvas.height = this.canvasBlur.height;\n        drawAndBlurImageOnCanvas(results.segmentationMask, this.edgeBlur, this.canvasMask);\n        this.canvas.getContext(\"2d\").save();\n        this.canvas\n            .getContext(\"2d\")\n            .drawImage(results.image, 0, 0, this.canvas.width, this.canvas.height);\n        this._drawWithCompositing(this.canvasMask, \"destination-in\");\n        this._drawWithCompositing(this.canvasBlur, \"destination-over\");\n        this.canvas.getContext(\"2d\").restore();\n        if (this.resolveStreamPromise) {\n            this.resolveStreamPromise(this.canvasStream);\n            this.resolveStreamPromise = null;\n        }\n    }\n\n    /**\n     * @private\n     */\n    _requestFrame() {\n        if (!this.isVideoDataLoaded) {\n            return;\n        }\n        browser.requestAnimationFrame(async () => {\n            await this._onFrame();\n            if (!this.worker) {\n                browser.setTimeout(() => this._requestFrame(), Math.floor(1000 / FPS));\n            }\n        });\n    }\n}\n", "import { CallDropdown } from \"@mail/discuss/call/common/call_dropdown\";\n\nimport { Component } from \"@odoo/owl\";\n\nimport { useService } from \"@web/core/utils/hooks\";\n\nexport class BlurPerformanceWarning extends Component {\n    static template = \"discuss.BlurPerformanceWarning\";\n    static props = {};\n    static components = { CallDropdown };\n\n    setup() {\n        this.store = useService(\"mail.store\");\n    }\n\n    onClickClose() {\n        this.store.settings.blurPerformanceWarning = false;\n    }\n}\n", "import { BlurPerformanceWarning } from \"@mail/discuss/call/common/blur_performance_warning\";\nimport { CallActionList } from \"@mail/discuss/call/common/call_action_list\";\nimport { CallParticipantCard } from \"@mail/discuss/call/common/call_participant_card\";\nimport { PttAdBanner } from \"@mail/discuss/call/common/ptt_ad_banner\";\n\nimport { Component, onMounted, onPatched, onWillUnmount, toRaw, useRef, useState } from \"@odoo/owl\";\n\nimport { browser } from \"@web/core/browser/browser\";\nimport { isMobileOS } from \"@web/core/browser/feature_detection\";\nimport { useHotkey } from \"@web/core/hotkeys/hotkey_hook\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { isEventHandled, markEventHandled } from \"@web/core/utils/misc\";\nimport { useCallActions } from \"@mail/discuss/call/common/call_actions\";\nimport { ActionList } from \"@mail/core/common/action_list\";\nimport { ACTION_TAGS } from \"@mail/core/common/action\";\nimport { inDiscussCallViewProps, useInDiscussCallView } from \"@mail/utils/common/hooks\";\n\n/**\n * @typedef CardData\n * @property {string} key\n * @property {import(\"models\").RtcSession} session\n * @property {MediaStream} videoStream\n * @property {import(\"models\").ChannelMember} [member]\n */\n\n/**\n * @typedef {Object} Props\n * @property {import(\"models\").Thread} thread\n * @property {boolean} [compact]\n * @extends {Component<Props, Env>}\n */\nexport class Call extends Component {\n    static components = {\n        ActionList,\n        BlurPerformanceWarning,\n        CallActionList,\n        CallParticipantCard,\n        PttAdBanner,\n    };\n    static props = [\"thread?\", \"compact?\", \"hasOverlay?\", ...inDiscussCallViewProps];\n    static defaultProps = { hasOverlay: true };\n    static template = \"discuss.Call\";\n\n    overlayTimeout;\n\n    setup() {\n        super.setup();\n        this.grid = useRef(\"grid\");\n        this.root = useRef(\"root\");\n        this.notification = useService(\"notification\");\n        this.rtc = useService(\"discuss.rtc\");\n        this.isMobileOs = isMobileOS();\n        this.ui = useService(\"ui\");\n        this.state = useState({\n            sidebar: false,\n            tileWidth: 0,\n            tileHeight: 0,\n            columnCount: 0,\n            overlay: false,\n            /** @type {CardData|undefined} */\n            insetCard: undefined,\n        });\n        this.store = useService(\"mail.store\");\n        this.callActions = useCallActions({ thread: () => this.channel });\n        onMounted(() => {\n            this.resizeObserver = new ResizeObserver(() => this.arrangeTiles());\n            this.resizeObserver.observe(this.grid.el);\n            this.arrangeTiles();\n        });\n        onPatched(() => this.arrangeTiles());\n        onWillUnmount(() => {\n            this.resizeObserver.disconnect();\n            browser.clearTimeout(this.overlayTimeout);\n        });\n        useHotkey(\"shift+d\", () => this.rtc.toggleDeafen());\n        useHotkey(\"shift+m\", () => this.rtc.toggleMicrophone());\n        useInDiscussCallView();\n    }\n\n    get layoutActions() {\n        if (!this.isActiveCall) {\n            return [];\n        }\n        return this.callActions.actions.filter((action) =>\n            action.tags.includes(ACTION_TAGS.CALL_LAYOUT)\n        );\n    }\n\n    get isFullSize() {\n        return this.props.isPip || this.rtc.state.isFullscreen;\n    }\n\n    get isActiveCall() {\n        return Boolean(this.channel.eq(this.rtc.channel));\n    }\n\n    get minimized() {\n        if (this.rtc.state.isFullscreen || !this.channel || this.channel.activeRtcSession) {\n            return false;\n        }\n        if (!this.isActiveCall || this.channel.videoCount === 0 || this.props.compact) {\n            return true;\n        }\n        return false;\n    }\n\n    get channel() {\n        return this.props.thread || this.rtc.channel;\n    }\n\n    /** @returns {CardData[]} */\n    get visibleMainCards() {\n        const activeSession = this.channel.activeRtcSession;\n        if (!activeSession) {\n            this.state.insetCard = undefined;\n            return this.channel.visibleCards;\n        }\n        const type = activeSession.mainVideoStreamType;\n        if (type === \"screen\" || activeSession.is_screen_sharing_on) {\n            this.setInset(activeSession, type === \"camera\" ? \"screen\" : \"camera\");\n        } else {\n            this.state.insetCard = undefined;\n        }\n        return [\n            {\n                key: \"session_\" + activeSession.id,\n                session: activeSession,\n                type,\n                videoStream: activeSession.getStream(type),\n            },\n        ];\n    }\n\n    /**\n     * @param {import(\"models\").RtcSession} session\n     * @param {String} [videoType]\n     */\n    setInset(session, videoType) {\n        const key = \"session_\" + session.id;\n        if (toRaw(this.state).insetCard?.key === key) {\n            this.state.insetCard.type = videoType;\n            this.state.insetCard.videoStream = session.getStream(videoType);\n        } else {\n            this.state.insetCard = {\n                key,\n                session,\n                type: videoType,\n                videoStream: session.getStream(videoType),\n            };\n        }\n    }\n\n    get hasCallNotifications() {\n        return Boolean(\n            (!this.props.compact || this.rtc.state.isFullscreen) &&\n                this.isActiveCall &&\n                this.rtc.notifications.size\n        );\n    }\n\n    get hasSidebarButton() {\n        return Boolean(\n            this.channel.activeRtcSession &&\n                this.state.overlay &&\n                (!this.props.compact || this.rtc.state.isFullscreen)\n        );\n    }\n\n    get isControllerFloating() {\n        return this.rtc.state.isFullscreen || (this.channel.activeRtcSession && !this.ui.isSmall);\n    }\n\n    onMouseleaveMain(ev) {\n        if (ev.relatedTarget && ev.relatedTarget.closest(\".o-dropdown--menu\")) {\n            // the overlay should not be hidden when the cursor leaves to enter the controller dropdown\n            return;\n        }\n        this.state.overlay = false;\n    }\n\n    onMousemoveMain(ev) {\n        if (isEventHandled(ev, \"CallMain.MousemoveOverlay\")) {\n            return;\n        }\n        this.showOverlay();\n    }\n\n    onMousemoveOverlay(ev) {\n        markEventHandled(ev, \"CallMain.MousemoveOverlay\");\n        this.state.overlay = true;\n        browser.clearTimeout(this.overlayTimeout);\n    }\n\n    showOverlay() {\n        this.state.overlay = true;\n        browser.clearTimeout(this.overlayTimeout);\n        this.overlayTimeout = browser.setTimeout(() => {\n            this.state.overlay = false;\n        }, 3000);\n    }\n\n    arrangeTiles() {\n        if (!this.grid.el) {\n            return;\n        }\n        this.grid.el.style.setProperty(\"--width\", \"0\");\n        this.grid.el.style.setProperty(\"--height\", \"0\");\n        const { width, height } = this.grid.el.getBoundingClientRect();\n        const aspectRatio = this.minimized ? 1 : 16 / 9;\n        const tileCount = this.grid.el.children.length;\n        let optimal = {\n            area: 0,\n            columnCount: 0,\n            tileHeight: 0,\n            tileWidth: 0,\n        };\n        for (let columnCount = 1; columnCount <= tileCount; columnCount++) {\n            const rowCount = Math.ceil(tileCount / columnCount);\n            const potentialHeight = width / (columnCount * aspectRatio);\n            const potentialWidth = height / rowCount;\n            let tileHeight;\n            let tileWidth;\n            if (potentialHeight > potentialWidth) {\n                tileHeight = Math.floor(potentialWidth);\n                tileWidth = Math.floor(tileHeight * aspectRatio);\n            } else {\n                tileWidth = Math.floor(width / columnCount);\n                tileHeight = Math.floor(tileWidth / aspectRatio);\n            }\n            const area = tileHeight * tileWidth;\n            if (area <= optimal.area) {\n                continue;\n            }\n            optimal = {\n                area,\n                columnCount,\n                tileHeight,\n                tileWidth,\n            };\n        }\n        Object.assign(this.state, {\n            tileWidth: optimal.tileWidth,\n            tileHeight: optimal.tileHeight,\n            columnCount: optimal.columnCount,\n        });\n        this.grid.el.style.setProperty(\"--width\", `${this.state.tileWidth}px`);\n        this.grid.el.style.setProperty(\"--height\", `${this.state.tileHeight}px`);\n    }\n}\n", "import { Component, onWillRender, toRaw, useRef } from \"@odoo/owl\";\n\nimport { isMobileOS } from \"@web/core/browser/feature_detection\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { useCallActions } from \"@mail/discuss/call/common/call_actions\";\nimport { usePopover } from \"@web/core/popover/popover_hook\";\nimport { Tooltip } from \"@web/core/tooltip/tooltip\";\nimport { CALL_PROMOTE_FULLSCREEN } from \"@mail/discuss/call/common/thread_model_patch\";\nimport { ActionList } from \"@mail/core/common/action_list\";\nimport { ACTION_TAGS } from \"@mail/core/common/action\";\n\nexport class CallActionList extends Component {\n    static components = { ActionList };\n    static props = [\"thread\", \"compact?\"];\n    static template = \"discuss.CallActionList\";\n\n    setup() {\n        super.setup();\n        this.store = useService(\"mail.store\");\n        this.rtc = useService(\"discuss.rtc\");\n        this.pipService = useService(\"discuss.pip_service\");\n        this.callActions = useCallActions({ thread: () => this.props.thread });\n        this.more = useRef(\"more\");\n        this.root = useRef(\"root\");\n        this.popover = usePopover(Tooltip, {\n            position: \"top-middle\",\n        });\n        onWillRender(() => {\n            const partition = toRaw(this.callActions).partition;\n            const other = partition.other.filter((a) => !a.tags.includes(ACTION_TAGS.CALL_LAYOUT));\n            const group2 = [];\n            for (const groupActions of partition.group) {\n                const filtered = groupActions.filter(\n                    (a) => !a.tags.includes(ACTION_TAGS.CALL_LAYOUT)\n                );\n                const sequenceGroup = filtered[0].sequenceGroup;\n                const maxQuickActions = sequenceGroup === 200 ? 1 : 4;\n                const quickActions = filtered.slice(0, maxQuickActions);\n                const moreActions = filtered.slice(maxQuickActions);\n                const newGroup = moreActions?.length\n                    ? [\n                          ...quickActions,\n                          this.callActions.more(\n                              {\n                                  actions: moreActions,\n                                  dropdownMenuClass: \"m-0 mb-1\",\n                                  dropdownPosition: \"top-end\",\n                                  name: this.MORE,\n                              },\n                              sequenceGroup\n                          ),\n                      ]\n                    : quickActions;\n                group2.push(newGroup);\n            }\n            this.actions = [...group2, other];\n        });\n    }\n\n    /** @deprecated */\n    get isPromotingFullscreen() {\n        return Boolean(\n            !this.env.pipWindow &&\n                this.props.thread.promoteFullscreen === CALL_PROMOTE_FULLSCREEN.ACTIVE\n        );\n    }\n\n    get MORE() {\n        return _t(\"More\");\n    }\n\n    get isOfActiveCall() {\n        return Boolean(this.props.thread.eq(this.rtc.channel));\n    }\n\n    get isSmall() {\n        return Boolean(this.props.compact && this.rtc.state.isFullscreen);\n    }\n\n    get isMobileOS() {\n        return isMobileOS();\n    }\n\n    onMouseenterMore() {\n        if (this.isPromotingFullscreen) {\n            this.popover.open(this.more.el, { tooltip: _t(\"Enter full screen!\") });\n            this.props.thread.promoteFullscreen = CALL_PROMOTE_FULLSCREEN.DISCARDED;\n        }\n    }\n\n    onMouseleaveMore() {\n        if (this.popover.isOpen) {\n            this.popover.close();\n        }\n    }\n\n    onClickMore() {\n        if (this.popover.isOpen) {\n            this.popover.close();\n        }\n    }\n}\n", "import { Action, ACTION_TAGS, UseActions } from \"@mail/core/common/action\";\nimport { useComponent, useState } from \"@odoo/owl\";\nimport { isMobileOS } from \"@web/core/browser/feature_detection\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { QuickVoiceSettings } from \"./quick_voice_settings\";\nimport { QuickVideoSettings } from \"./quick_video_settings\";\nimport { attClassObjectToString } from \"@mail/utils/common/format\";\nimport { CALL_PROMOTE_FULLSCREEN } from \"./thread_model_patch\";\n\nexport const callActionsRegistry = registry.category(\"discuss.call/actions\");\nexport const CALL_ICON_DEAFEN = \"fa fa-deaf\";\nexport const CALL_ICON_MUTED = \"fa fa-microphone-slash\";\n\n/** @typedef {import(\"@mail/core/common/action\").ActionDefinition} ActionDefinition */\n\n/**\n * @typedef {Object} CallActionSpecificDefinition\n * @property {boolean} [isTracked]\n */\n\n/**\n * @typedef {ActionDefinition & CallActionSpecificDefinition} CallActionDefinition\n */\n\n/**\n * @param {string} id\n * @param {CallActionDefinition} definition\n */\nexport function registerCallAction(id, definition) {\n    callActionsRegistry.add(id, definition);\n}\n\nexport const muteAction = {\n    badge: ({ owner, store }) =>\n        !owner.env.inCallMenu && store.rtc.microphonePermission !== \"granted\",\n    badgeIcon: \"fa fa-exclamation\",\n    condition: ({ store, thread }) => thread?.eq(store.rtc?.channel),\n    name: ({ store }) => (store.rtc.selfSession.isMute ? _t(\"Unmute\") : _t(\"Mute\")),\n    isActive: ({ store }) =>\n        (store.rtc.selfSession?.isMute && store.rtc.microphonePermission === \"granted\") ||\n        store.rtc.selfSession?.is_deaf,\n    isTracked: true,\n    icon: ({ action, store }) =>\n        action.isActive\n            ? store.rtc.selfSession?.is_deaf\n                ? CALL_ICON_DEAFEN\n                : CALL_ICON_MUTED\n            : \"fa fa-microphone\",\n    hotkey: \"shift+m\",\n    onSelected: ({ store }) => store.rtc.toggleMicrophone(),\n    sequence: 10,\n    sequenceGroup: 100,\n    tags: ({ action, store }) => {\n        const tags = [];\n        if (action.isActive) {\n            tags.push(ACTION_TAGS.DANGER);\n        }\n        if (store.rtc.microphonePermission !== \"granted\") {\n            tags.push(ACTION_TAGS.DANGER, ACTION_TAGS.WARNING_BADGE);\n        }\n        return tags;\n    },\n};\nregisterCallAction(\"mute\", muteAction);\nexport const quickActionSettings = {\n    condition: ({ owner, store, thread }) =>\n        !owner.env.inCallMenu && thread?.eq(store.rtc?.channel),\n    dropdown: true,\n    dropdownComponent: QuickVoiceSettings,\n    dropdownMenuClass: \"p-2\",\n    dropdownPosition: \"top-end\",\n    icon: \"oi oi-chevron-up o-xsmaller\",\n    name: _t(\"Voice Settings\"),\n    sequence: 15,\n    sequenceGroup: 100,\n};\nregisterCallAction(\"quick-voice-settings\", quickActionSettings);\nregisterCallAction(\"deafen\", {\n    condition: ({ owner }) => owner.env.inCallMenu,\n    name: ({ store }) => (store.rtc.selfSession.is_deaf ? _t(\"Undeafen\") : _t(\"Deafen\")),\n    isActive: ({ store }) => store.rtc.selfSession?.is_deaf,\n    isTracked: true,\n    icon: ({ action }) => (action.isActive ? CALL_ICON_DEAFEN : \"fa fa-headphones\"),\n    hotkey: \"shift+d\",\n    onSelected: ({ store }) => store.rtc.toggleDeafen(),\n    sequence: 10,\n    sequenceGroup: 110,\n    tags: ({ action }) => (action.isActive ? ACTION_TAGS.DANGER : undefined),\n});\nexport const cameraOnAction = {\n    badge: ({ owner, store }) => !owner.env.inCallMenu && store.rtc.cameraPermission !== \"granted\",\n    badgeIcon: \"fa fa-exclamation\",\n    condition: ({ store, thread }) => thread?.eq(store.rtc?.channel),\n    disabledCondition: ({ store }) => store.rtc?.isRemote,\n    name: ({ store }) =>\n        store.rtc?.isRemote\n            ? _t(\"Camera is unavailable outside the call tab.\")\n            : store.rtc.selfSession.is_camera_on\n            ? _t(\"Stop camera\")\n            : _t(\"Turn camera on\"),\n    isActive: ({ store }) => store.rtc.selfSession?.is_camera_on,\n    isTracked: true,\n    icon: \"fa fa-video-camera\",\n    onSelected: ({ owner, store }) => store.rtc.toggleVideo(\"camera\", { env: owner.env }),\n    sequence: 10,\n    sequenceGroup: 120,\n    tags: ({ action, store }) => {\n        const tags = [];\n        if (action.isActive) {\n            tags.push(ACTION_TAGS.SUCCESS);\n        }\n        if (store.rtc.cameraPermission !== \"granted\") {\n            tags.push(ACTION_TAGS.DANGER, ACTION_TAGS.WARNING_BADGE);\n        }\n        return tags;\n    },\n};\nregisterCallAction(\"camera-on\", cameraOnAction);\nexport const quickVideoSettings = {\n    condition: ({ owner, store, thread }) =>\n        !owner.env.inCallMenu && thread?.eq(store.rtc?.channel),\n    dropdown: true,\n    dropdownComponent: QuickVideoSettings,\n    dropdownMenuClass: \"p-2\",\n    dropdownPosition: \"top-end\",\n    icon: \"oi oi-chevron-up o-xsmaller\",\n    name: _t(\"Video Settings\"),\n    sequence: 15,\n    sequenceGroup: 120,\n};\nregisterCallAction(\"quick-video-settings\", quickVideoSettings);\nexport const switchCameraAction = {\n    condition: ({ store, thread }) =>\n        thread?.eq(store.rtc?.channel) && isMobileOS() && store.rtc.selfSession?.is_camera_on,\n    name: _t(\"Switch Camera\"),\n    isActive: false,\n    icon: \"fa fa-refresh\",\n    onSelected: ({ store }) => store.rtc.toggleCameraFacingMode(),\n    sequence: 40,\n    sequenceGroup: 100,\n};\nregisterCallAction(\"switch-camera\", switchCameraAction);\nregisterCallAction(\"raise-hand\", {\n    condition: ({ store, thread }) => thread?.eq(store.rtc?.channel),\n    name: ({ store }) => (store.rtc.selfSession.raisingHand ? _t(\"Lower Hand\") : _t(\"Raise Hand\")),\n    isActive: ({ store }) => store.rtc.selfSession?.raisingHand,\n    isTracked: true,\n    icon: \"fa fa-hand-paper-o\",\n    onSelected: ({ store }) => store.rtc.raiseHand(!store.rtc.selfSession.raisingHand),\n    sequence: 50,\n    sequenceGroup: 200,\n});\nregisterCallAction(\"share-screen\", {\n    condition: ({ store, thread }) => thread?.eq(store.rtc?.channel) && !isMobileOS(),\n    disabledCondition: ({ store }) => store.rtc?.isRemote,\n    name: ({ store }) =>\n        store.rtc?.isRemote\n            ? _t(\"Screen sharing is unavailable outside the call tab.\")\n            : store.rtc.selfSession.is_screen_sharing_on\n            ? _t(\"Stop Sharing Screen\")\n            : _t(\"Share Screen\"),\n    isTracked: true,\n    isActive: ({ store }) => store.rtc.selfSession?.is_screen_sharing_on,\n    icon: \"fa fa-desktop\",\n    onSelected: ({ owner, store }) => store.rtc.toggleVideo(\"screen\", { env: owner.env }),\n    sequence: 40,\n    sequenceGroup: 200,\n    tags: ({ action }) => (action.isActive ? ACTION_TAGS.SUCCESS : undefined),\n});\nregisterCallAction(\"auto-focus\", {\n    condition: ({ owner, store, thread }) =>\n        !owner.env.inCallMenu && thread?.eq(store.rtc?.channel),\n    name: ({ store }) =>\n        store.settings.useCallAutoFocus ? _t(\"Disable speaker autofocus\") : _t(\"Autofocus speaker\"),\n    isActive: ({ store }) => store.settings?.useCallAutoFocus,\n    icon: ({ action }) => (action.isActive ? \"fa fa-eye\" : \"fa fa-eye-slash\"),\n    onSelected: ({ store }) => (store.settings.useCallAutoFocus = !store.settings.useCallAutoFocus),\n    sequence: 50,\n    sequenceGroup: 200,\n});\n/** @deprecated Blur background action is replaced by @see QuickVideoSettings menu item \"Blur background\" */\nexport const blurBackgroundAction = {\n    condition: false,\n    name: ({ store }) => (store.settings.useBlur ? _t(\"Remove Blur\") : _t(\"Blur Background\")),\n    isActive: ({ store }) => store?.settings?.useBlur,\n    icon: \"fa fa-photo\",\n    onSelected: ({ store }) => (store.settings.useBlur = !store.settings.useBlur),\n    sequence: 60,\n    sequenceGroup: 200,\n};\nregisterCallAction(\"fullscreen\", {\n    btnClass: ({ owner, thread }) =>\n        attClassObjectToString({\n            \"o-discuss-CallActionList-pulse\": Boolean(\n                !owner.env.pipWindow && thread.promoteFullscreen === CALL_PROMOTE_FULLSCREEN.ACTIVE\n            ),\n        }),\n    condition: ({ store, thread }) => thread?.eq(store.rtc?.channel),\n    name: ({ store }) => (store.rtc.state.isFullscreen ? _t(\"Exit Fullscreen\") : _t(\"Fullscreen\")),\n    isActive: ({ store }) => store.rtc.state.isFullscreen,\n    icon: ({ action }) => (action.isActive ? \"fa fa-compress\" : \"fa fa-expand\"),\n    onSelected: ({ store }) => {\n        if (store.rtc.state.isFullscreen) {\n            store.rtc.exitFullscreen();\n        } else {\n            store.rtc.closePip();\n            store.rtc.enterFullscreen();\n        }\n    },\n    sequence: 80,\n    tags: ACTION_TAGS.CALL_LAYOUT,\n});\nregisterCallAction(\"picture-in-picture\", {\n    condition: ({ owner, store, thread }) =>\n        !owner.env.inCallMenu &&\n        thread?.eq(store.rtc?.channel) &&\n        store.env.services[\"discuss.pip_service\"] &&\n        !store.env?.isSmall,\n    disabledCondition: ({ store }) => store.rtc?.isRemote,\n    name: ({ store }) =>\n        store.rtc?.state.isPipMode ? _t(\"Exit Picture in Picture\") : _t(\"Picture in Picture\"),\n    isActive: ({ store }) => store.rtc?.state.isPipMode,\n    icon: \"oi oi-launch\",\n    onSelected: ({ owner, store }) => {\n        const isPipMode = store.rtc?.state.isPipMode;\n        if (isPipMode) {\n            store.rtc.closePip();\n        } else {\n            store.rtc.openPip({ context: owner });\n        }\n    },\n    sequence: 70,\n    tags: ACTION_TAGS.CALL_LAYOUT,\n});\nexport const acceptWithCamera = {\n    condition: ({ thread }) =>\n        thread?.self_member_id?.rtc_inviting_session_id?.is_camera_on &&\n        typeof thread?.useCameraByDefault !== \"boolean\",\n    disabledCondition: ({ store }) => store.rtc?.state.hasPendingRequest,\n    name: _t(\"Accept with camera\"),\n    icon: \"fa fa-video-camera\",\n    onSelected: ({ store, thread }) => store.rtc.toggleCall(thread, { camera: true }),\n    sequence: 100,\n    sequenceGroup: 300,\n    tags: [ACTION_TAGS.JOIN_LEAVE_CALL, ACTION_TAGS.SUCCESS],\n};\nregisterCallAction(\"accept-with-camera\", acceptWithCamera);\nregisterCallAction(\"join-back\", {\n    btnClass: ({ owner }) =>\n        attClassObjectToString({\n            \"text-nowrap pe-2 rounded-pill\": true,\n            \"mx-1\": !owner.env.inCallInvitation,\n        }),\n    condition: ({ store, thread }) =>\n        !thread?.eq(store.rtc?.channel) && typeof thread?.useCameraByDefault === \"boolean\",\n    disabledCondition: ({ store }) => store.rtc?.state.hasPendingRequest,\n    icon: ({ thread }) => (thread.useCameraByDefault ? \"fa fa-video-camera\" : \"fa fa-phone\"),\n    inlineName: ({ owner }) => (owner.env.inCallInvitation ? undefined : _t(\"Join\")),\n    name: ({ thread }) => (thread.useCameraByDefault ? _t(\"Join Video Call\") : _t(\"Join Call\")),\n    onSelected: ({ store, thread }) =>\n        store.rtc.toggleCall(thread, { camera: thread.useCameraByDefault }),\n    sequence: 110,\n    sequenceGroup: 300,\n    tags: [ACTION_TAGS.JOIN_LEAVE_CALL, ACTION_TAGS.SUCCESS],\n});\nregisterCallAction(\"join-with-camera\", {\n    btnClass: \"text-nowrap\",\n    condition: ({ store, thread }) =>\n        !thread?.eq(store.rtc?.channel) &&\n        !thread?.self_member_id?.rtc_inviting_session_id &&\n        typeof thread?.useCameraByDefault !== \"boolean\",\n    disabledCondition: ({ store }) => store.rtc?.state.hasPendingRequest,\n    name: _t(\"Join Video Call\"),\n    icon: \"fa fa-video-camera\",\n    onSelected: ({ store, thread }) => store.rtc.toggleCall(thread, { camera: true }),\n    sequence: 120,\n    sequenceGroup: 300,\n    tags: [ACTION_TAGS.JOIN_LEAVE_CALL, ACTION_TAGS.SUCCESS],\n});\nexport const joinAction = {\n    condition: ({ store, thread }) =>\n        !thread?.eq(store.rtc?.channel) && typeof thread?.useCameraByDefault !== \"boolean\",\n    disabledCondition: ({ store }) => store.rtc?.state.hasPendingRequest,\n    name: _t(\"Join Call\"),\n    icon: \"fa fa-phone\",\n    onSelected: ({ store, thread }, ev) => store.rtc.toggleCall(thread),\n    sequence: 130,\n    sequenceGroup: 300,\n    tags: [ACTION_TAGS.JOIN_LEAVE_CALL, ACTION_TAGS.SUCCESS],\n};\nregisterCallAction(\"join\", joinAction);\nexport const rejectAction = {\n    btnClass: ({ owner, thread }) =>\n        attClassObjectToString({\n            \"pe-2 rounded-pill\": typeof thread?.useCameraByDefault === \"boolean\",\n            \"mx-1\": !owner.env.inCallInvitation && typeof thread?.useCameraByDefault === \"boolean\",\n        }),\n    condition: ({ thread }) => thread?.self_member_id?.rtc_inviting_session_id,\n    disabledCondition: ({ store }) => store.rtc?.state.hasPendingRequest,\n    icon: \"oi oi-close\",\n    inlineName: ({ owner, thread }) =>\n        !owner.env.inCallInvitation && typeof thread?.useCameraByDefault === \"boolean\"\n            ? _t(\"Reject\")\n            : undefined,\n    name: _t(\"Reject\"),\n    onSelected: ({ store, thread }) => {\n        if (store.rtc.state.hasPendingRequest) {\n            return;\n        }\n        store.rtc.leaveCall(thread);\n    },\n    sequence: 140,\n    sequenceGroup: 300,\n    tags: [ACTION_TAGS.JOIN_LEAVE_CALL, ACTION_TAGS.DANGER],\n};\nregisterCallAction(\"reject\", rejectAction);\nregisterCallAction(\"disconnect\", {\n    condition: ({ store, thread }) =>\n        thread?.eq(store.rtc?.channel) && !thread?.self_member_id?.rtc_inviting_session_id,\n    disabledCondition: ({ store }) => store.rtc?.state.hasPendingRequest,\n    name: _t(\"Disconnect\"),\n    icon: \"fa fa-phone\",\n    onSelected: ({ store, thread }) => store.rtc.toggleCall(thread),\n    sequence: 150,\n    sequenceGroup: 300,\n    tags: [ACTION_TAGS.JOIN_LEAVE_CALL, ACTION_TAGS.DANGER],\n});\n\nexport class CallAction extends Action {\n    /** @type {() => Thread} */\n    threadFn;\n\n    /**\n     * @param {Object} param0\n     * @param {Thread|() => Thread} thread\n     */\n    constructor({ thread }) {\n        super(...arguments);\n        this.threadFn = typeof thread === \"function\" ? thread : () => thread;\n    }\n\n    get params() {\n        return Object.assign(super.params, { thread: this.threadFn() });\n    }\n\n    get isTracked() {\n        return this.definition.isTracked;\n    }\n}\n\nclass UseCallActions extends UseActions {\n    ActionClass = CallAction;\n}\n\n/**\n * @param {Object} [params0={}]\n * @param {Thread|() => Thread} thread\n */\nexport function useCallActions({ thread } = {}) {\n    const component = useComponent();\n    const transformedActions = callActionsRegistry\n        .getEntries()\n        .map(([id, definition]) => new CallAction({ owner: component, id, definition, thread }));\n    return useState(new UseCallActions(component, transformedActions, useService(\"mail.store\")));\n}\n", "import { registry } from \"@web/core/registry\";\nimport { Call } from \"@mail/discuss/call/common/call\";\nimport { Meeting } from \"@mail/discuss/call/common/meeting\";\n\n/**\n * Registry used to access components while avoiding cycling dependencies.\n */\nconst callComponentsRegistry = registry.category(\"discuss.call/components\");\n\ncallComponentsRegistry.add(\"Call\", Call).add(\"Meeting\", Meeting);\n", "import { Component, onMounted, onWillUnmount, useState } from \"@odoo/owl\";\n\nimport { browser } from \"@web/core/browser/browser\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { useService } from \"@web/core/utils/hooks\";\n\nimport { CONNECTION_TYPES } from \"@mail/discuss/call/common/rtc_service\";\n\nconst PROTOCOLS_TEXT = { host: \"HOST\", srflx: \"STUN\", prflx: \"STUN\", relay: \"TURN\" };\n\nexport class CallContextMenu extends Component {\n    static props = [\"rtcSession\", \"close?\"];\n    static template = \"discuss.CallContextMenu\";\n\n    updateStatsTimeout;\n    rtcConnectionTypes = CONNECTION_TYPES;\n\n    setup() {\n        super.setup();\n        this.store = useService(\"mail.store\");\n        this.rtc = useService(\"discuss.rtc\");\n        this.state = useState({\n            downloadStats: {},\n            uploadStats: {},\n            producerStats: {},\n            peerStats: {},\n            rangeVolume: this.volume,\n        });\n        onMounted(() => {\n            if (!this.env.debug) {\n                return;\n            }\n            this.updateStats();\n            this.updateStatsTimeout = browser.setInterval(() => this.updateStats(), 3000);\n        });\n        onWillUnmount(() => browser.clearInterval(this.updateStatsTimeout));\n    }\n\n    get isSelf() {\n        return this.rtc.selfSession?.eq(this.props.rtcSession);\n    }\n\n    get inboundConnectionTypeText() {\n        const candidateType =\n            this.rtc.state.connectionType === CONNECTION_TYPES.SERVER\n                ? this.state.downloadStats.remoteCandidateType\n                : this.state.peerStats.remoteCandidateType;\n        return this.formatProtocol(candidateType);\n    }\n\n    get outboundConnectionTypeText() {\n        const candidateType =\n            this.rtc.state.connectionType === CONNECTION_TYPES.SERVER\n                ? this.state.uploadStats.localCandidateType\n                : this.state.peerStats.localCandidateType;\n        return this.formatProtocol(candidateType);\n    }\n\n    get volume() {\n        return this.store.settings.getVolume(this.props.rtcSession);\n    }\n\n    /**\n     * @param {string} candidateType\n     * @returns {string} a formatted string that describes the connection type e.g: \"prflx (STUN)\"\n     */\n    formatProtocol(candidateType) {\n        if (!candidateType) {\n            return _t(\"no connection\");\n        }\n        return _t(\"%(candidateType)s (%(protocol)s)\", {\n            candidateType,\n            protocol: PROTOCOLS_TEXT[candidateType],\n        });\n    }\n\n    async updateStats() {\n        if (this.rtc.localSession?.eq(this.props.rtcSession)) {\n            if (this.rtc.sfuClient) {\n                const { uploadStats, downloadStats, ...producerStats } =\n                    await this.rtc.sfuClient.getStats();\n                if (!uploadStats || !downloadStats) {\n                    return;\n                }\n                const formattedUploadStats = {};\n                for (const value of uploadStats.values?.() || []) {\n                    switch (value.type) {\n                        case \"candidate-pair\":\n                            if (value.state === \"succeeded\" && value.localCandidateId) {\n                                formattedUploadStats.localCandidateType =\n                                    uploadStats.get(value.localCandidateId)?.candidateType || \"\";\n                                formattedUploadStats.availableOutgoingBitrate =\n                                    value.availableOutgoingBitrate;\n                            }\n                            break;\n                        case \"transport\":\n                            formattedUploadStats.dtlsState = value.dtlsState;\n                            formattedUploadStats.iceState = value.iceState;\n                            formattedUploadStats.packetsSent = value.packetsSent;\n                            break;\n                    }\n                }\n                const formattedDownloadStats = {};\n                for (const value of downloadStats.values?.() || []) {\n                    switch (value.type) {\n                        case \"candidate-pair\":\n                            if (value.state === \"succeeded\" && value.localCandidateId) {\n                                formattedDownloadStats.remoteCandidateType =\n                                    downloadStats.get(value.remoteCandidateId)?.candidateType || \"\";\n                            }\n                            break;\n                        case \"transport\":\n                            formattedDownloadStats.dtlsState = value.dtlsState;\n                            formattedDownloadStats.iceState = value.iceState;\n                            formattedDownloadStats.packetsReceived = value.packetsReceived;\n                            break;\n                    }\n                }\n                const formattedProducerStats = {};\n                for (const [type, stat] of Object.entries(producerStats)) {\n                    const currentTypeStats = {};\n                    for (const value of stat.values()) {\n                        switch (value.type) {\n                            case \"codec\":\n                                currentTypeStats.codec = value.mimeType;\n                                currentTypeStats.clockRate = value.clockRate;\n                                break;\n                        }\n                    }\n                    formattedProducerStats[type] = currentTypeStats;\n                }\n                this.state.uploadStats = formattedUploadStats;\n                this.state.downloadStats = formattedDownloadStats;\n                this.state.producerStats = formattedProducerStats;\n            }\n            return;\n        }\n        this.state.peerStats = await this.rtc.p2pService.getFormattedStats(\n            this.props.rtcSession.id\n        );\n    }\n\n    onChangeVolume(ev) {\n        const volume = Number(ev.target.value);\n        this.rtc.setVolume(this.props.rtcSession, volume);\n    }\n}\n", "import { Component, useRef, useState, useExternalListener, useSubEnv } from \"@odoo/owl\";\nimport { useNavigation } from \"@web/core/navigation/navigation\";\nimport { usePosition } from \"@web/core/position/position_hook\";\n\n/**\n * CallDropdown is an alternative to the web popover for calls to make them available\n * in cases where they cannot be overlays (main components), such as in picture-in-picture mode.\n */\nexport class CallDropdown extends Component {\n    static template = \"discuss.CallDropdown\";\n    static props = {\n        position: { type: String, optional: true },\n        class: { type: String, optional: true },\n        menuClass: { type: String, optional: true },\n        slots: { optional: true },\n        openByDefault: { type: Boolean, optional: true },\n        state: { type: Object, optional: true },\n    };\n    static defaultProps = {\n        position: \"bottom\",\n        class: \"\",\n        menuClass: \"\",\n        openByDefault: false,\n    };\n\n    setup() {\n        super.setup();\n        this.triggerRef = useRef(\"trigger\");\n        this.menuRef = useRef(\"menu\");\n        this.state = useState({ isOpen: this.props.openByDefault });\n        usePosition(\"menu\", () => this.triggerRef.el, {\n            position: this.props.position,\n            margin: 4,\n            flip: true,\n        });\n        useExternalListener(this.window, \"click\", this.onClickAway, { capture: true });\n        useExternalListener(this.window, \"keydown\", this.onKeydown);\n        useSubEnv({ inCallDropdown: { close: () => this.close() } });\n        this.navigation = useNavigation(this.menuRef, {\n            isNavigationAvailable: () => this.state.isOpen,\n            getItems: () => {\n                if (this.state.isOpen && this.menuRef.el) {\n                    return this.menuRef.el.querySelectorAll(\n                        \":scope .o-navigable, :scope .o-dropdown\"\n                    );\n                }\n                return [];\n            },\n        });\n    }\n\n    get window() {\n        return this.env.pipWindow || window;\n    }\n\n    get isOpen() {\n        return this.state.isOpen;\n    }\n\n    toggle() {\n        this.isOpen ? this.close() : this.open();\n    }\n\n    open() {\n        this.state.isOpen = true;\n    }\n\n    close() {\n        this.state.isOpen = false;\n    }\n\n    handleClick(ev) {\n        ev.preventDefault();\n        ev.stopPropagation();\n        this.toggle();\n    }\n\n    onClickAway(ev) {\n        if (!this.isOpen) {\n            return;\n        }\n        const isOutsideClick =\n            !this.triggerRef.el?.contains(ev.target) && !this.menuRef.el?.contains(ev.target);\n        if (isOutsideClick) {\n            this.close();\n        }\n    }\n\n    onClickMenu(ev) {\n        ev.stopPropagation();\n    }\n\n    onKeydown(ev) {\n        if (ev.key === \"Escape\" && this.isOpen) {\n            ev.preventDefault();\n            this.close();\n        }\n    }\n}\n", "import { Component } from \"@odoo/owl\";\n\n/**\n * @typedef {Object} Props\n * @property {Function} onClose\n * @extends {Component<Props, Env>}\n */\nexport class CallInfiniteMirroringWarning extends Component {\n    static template = \"discuss.CallInfiniteMirroringWarning\";\n    static props = {\n        onClose: { type: Function },\n    };\n}\n", "import { Action, ACTION_TAGS } from \"@mail/core/common/action\";\nimport { ActionList } from \"@mail/core/common/action_list\";\nimport {\n    acceptWithCamera,\n    CallAction,\n    joinAction,\n    rejectAction,\n} from \"@mail/discuss/call/common/call_actions\";\nimport { CallPreview } from \"@mail/discuss/call/common/call_preview\";\n\nimport { Component, useState, useSubEnv } from \"@odoo/owl\";\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { useService } from \"@web/core/utils/hooks\";\n\nexport class CallInvitation extends Component {\n    static props = [\"thread\"];\n    static template = \"discuss.CallInvitation\";\n    static components = { ActionList, CallPreview };\n\n    setup() {\n        super.setup();\n        this.rtc = useService(\"discuss.rtc\");\n        this.store = useService(\"mail.store\");\n        this.ui = useService(\"ui\");\n        this.state = useState({\n            activateCamera: 0,\n            activateMicrophone: 0,\n            showCameraPreview: false,\n            hasCamera: false,\n            hasMicrophone: this.rtc.microphonePermission === \"granted\",\n        });\n        useSubEnv({ inCallInvitation: true });\n    }\n\n    joinCall() {\n        this.rtc.toggleCall(this.props.thread, {\n            audio: this.state.hasMicrophone,\n            camera: this.state.hasCamera,\n        });\n    }\n\n    get acceptOrRejectActions() {\n        const joinUpdated = {\n            ...joinAction,\n            btnClass: joinAction.btnClass + \" o-me-0_5\",\n            onSelected: () => this.joinCall(),\n        };\n        const acceptWithCameraUpdated = {\n            ...acceptWithCamera,\n            btnClass: acceptWithCamera.btnClass + \" o-me-0_5\",\n            onSelected: () => {\n                this.state.hasCamera = true;\n                this.joinCall();\n            },\n            condition: true,\n        };\n        return [\n            new CallAction({\n                id: \"accept-with-camera\",\n                definition: acceptWithCameraUpdated,\n                owner: this,\n                store: this.store,\n                thread: this.props.thread,\n            }),\n            new CallAction({\n                id: \"join\",\n                definition: joinUpdated,\n                owner: this,\n                store: this.store,\n                thread: this.props.thread,\n            }),\n            new CallAction({\n                id: \"reject\",\n                definition: rejectAction,\n                owner: this,\n                store: this.store,\n                thread: this.props.thread,\n            }),\n        ];\n    }\n\n    get otherActions() {\n        return [\n            new Action({\n                id: \"toggle-camera-preview\",\n                definition: {\n                    name: () =>\n                        this.state.showCameraPreview\n                            ? _t(\"Hide camera preview\")\n                            : _t(\"Show camera preview\"),\n                    icon: () =>\n                        this.state.showCameraPreview ? \"fa fa-chevron-up\" : \"fa fa-chevron-down\",\n                    onSelected: () => {\n                        this.state.showCameraPreview = !this.state.showCameraPreview;\n                        if (this.rtc.cameraPermission !== \"denied\") {\n                            this.state.activateCamera++;\n                        }\n                        if (this.state.hasMicrophone) {\n                            this.state.activateMicrophone++;\n                        }\n                    },\n                    tags: () => [ACTION_TAGS.CALL_LAYOUT],\n                },\n                store: this.store,\n            }),\n        ];\n    }\n\n    get avatarTitle() {\n        const channelName = this.props.thread.displayName;\n        if (this.props.thread.channel_type === \"chat\") {\n            return _t(\"View chat with %(channel_name)s\", { channel_name: channelName });\n        }\n        return _t(\"View the %(channel_name)s channel\", { channel_name: channelName });\n    }\n\n    get inviter() {\n        return this.props.thread.self_member_id?.rtc_inviting_session_id?.channel_member_id;\n    }\n\n    get incomingCallText() {\n        if (this.props.thread.channel_type === \"chat\" || !this.inviter) {\n            return _t(\"Incoming call\");\n        }\n        return _t(\"Incoming call from %(inviter)s\", { inviter: this.inviter.name });\n    }\n\n    /** @param {{ microphone?: boolean, camera?: boolean }} settings */\n    onCallSettingsChanged(settings) {\n        if (settings.microphone !== undefined) {\n            this.state.hasMicrophone = settings.microphone;\n        }\n        if (settings.camera !== undefined) {\n            this.state.hasCamera = settings.camera;\n        }\n    }\n}\n", "import { CallInvitation } from \"@mail/discuss/call/common/call_invitation\";\nimport { onChange } from \"@mail/utils/common/misc\";\n\nimport { Component } from \"@odoo/owl\";\n\nimport { registry } from \"@web/core/registry\";\nimport { useService } from \"@web/core/utils/hooks\";\n\nexport class CallInvitations extends Component {\n    static props = [];\n    static components = { CallInvitation };\n    static template = \"discuss.CallInvitations\";\n\n    setup() {\n        super.setup();\n        this.rtc = useService(\"discuss.rtc\");\n        this.store = useService(\"mail.store\");\n    }\n}\n\nexport const callInvitationsService = {\n    dependencies: [\"discuss.rtc\", \"mail.store\", \"overlay\"],\n    start(env, services) {\n        const store = services[\"mail.store\"];\n        let removeOverlay;\n        const onChangeRingingThreadsLength = () => {\n            if (store.ringingThreads.length > 0) {\n                if (!removeOverlay) {\n                    removeOverlay = services.overlay.add(CallInvitations, {});\n                }\n            } else {\n                removeOverlay?.();\n                removeOverlay = undefined;\n            }\n        };\n        onChangeRingingThreadsLength();\n        onChange(store.ringingThreads, \"length\", onChangeRingingThreadsLength);\n    },\n};\nregistry.category(\"services\").add(\"discuss.call_invitations\", callInvitationsService);\n", "import { Component, useSubEnv } from \"@odoo/owl\";\n\nimport { ActionList } from \"@mail/core/common/action_list\";\nimport { useCallActions } from \"@mail/discuss/call/common/call_actions\";\n\nimport { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { registry } from \"@web/core/registry\";\nimport { useService } from \"@web/core/utils/hooks\";\n\nexport class CallMenu extends Component {\n    static props = [];\n    static template = \"discuss.CallMenu\";\n    static components = { ActionList, Dropdown };\n    setup() {\n        super.setup();\n        this.rtc = useService(\"discuss.rtc\");\n        this.callActions = useCallActions({ thread: () => this.rtc.channel });\n        this.isEnterprise = odoo.info && odoo.info.isEnterprise;\n        useSubEnv({ inCallMenu: true });\n    }\n\n    get icon() {\n        const res = this.rtc.callActions.find(\n            (action) => action.id === this.rtc.lastSelfCallAction\n        )?.icon;\n        return (typeof res === \"function\" ? res() : res) ?? \"fa fa-microphone\";\n    }\n}\n\nregistry.category(\"systray\").add(\"discuss.CallMenu\", { Component: CallMenu }, { sequence: 100 });\n", "import { CallContextMenu } from \"@mail/discuss/call/common/call_context_menu\";\nimport { CallParticipantVideo } from \"@mail/discuss/call/common/call_participant_video\";\nimport { CallDropdown } from \"@mail/discuss/call/common/call_dropdown\";\nimport { CONNECTION_TYPES } from \"@mail/discuss/call/common/rtc_service\";\nimport { useHover } from \"@mail/utils/common/hooks\";\nimport { isEventHandled } from \"@web/core/utils/misc\";\nimport { browser } from \"@web/core/browser/browser\";\nimport { isMobileOS } from \"@web/core/browser/feature_detection\";\n\nimport { Component, onMounted, onWillUnmount, useRef, useExternalListener } from \"@odoo/owl\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { rpc } from \"@web/core/network/rpc\";\n\nconst HIDDEN_CONNECTION_STATES = new Set([\"connected\", \"completed\"]);\n\nexport class CallParticipantCard extends Component {\n    static props = [\n        \"className\",\n        \"cardData\",\n        \"thread\",\n        \"minimized?\",\n        \"inset?\",\n        \"isSidebarItem?\",\n        \"compact?\",\n    ];\n    static components = { CallParticipantVideo, CallContextMenu, CallDropdown };\n    static template = \"discuss.CallParticipantCard\";\n\n    setup() {\n        super.setup();\n        this.contextMenuAnchorRef = useRef(\"contextMenuAnchor\");\n        this.root = useRef(\"root\");\n        this.rtc = useService(\"discuss.rtc\");\n        this.store = useService(\"mail.store\");\n        this.ui = useService(\"ui\");\n        this.rootHover = useHover(\"root\");\n        this.resumeStreamHover = useHover(\"resumeStream\");\n        this.isMobileOS = isMobileOS();\n        this.dragPos = undefined;\n        this.isDrag = false;\n        this.parentBoundingRect = undefined;\n        onMounted(() => {\n            if (!this.rtcSession) {\n                return;\n            }\n            this.rtc.updateVideoDownload(this.rtcSession, {\n                viewCountIncrement: 1,\n            });\n        });\n        onWillUnmount(() => {\n            if (!this.rtcSession) {\n                return;\n            }\n            this.rtc.updateVideoDownload(this.rtcSession, {\n                viewCountIncrement: -1,\n            });\n        });\n        useExternalListener(browser, \"fullscreenchange\", this.onFullScreenChange);\n    }\n\n    get isContextMenuAvailable() {\n        return (\n            this.isOfActiveCall &&\n            (this.rtcSession.notEq(this.rtc.selfSession) ||\n                (this.env.debug && this.rtc.state.connectionType === CONNECTION_TYPES.SERVER))\n        );\n    }\n\n    get isRemoteVideo() {\n        if (!this.rtcSession) {\n            return false;\n        }\n        return (\n            this.rtc.isRemote &&\n            (this.rtcSession.is_screen_sharing_on || this.rtcSession.is_camera_on)\n        );\n    }\n\n    get isSmall() {\n        return Boolean(\n            this.props.isSidebarItem || this.ui.isSmall || this.props.minimized || this.props.inset\n        );\n    }\n\n    get window() {\n        return this.env.pipWindow || window;\n    }\n\n    get showLiveLabel() {\n        if (this.props.isSidebarItem) {\n            return false;\n        }\n        if (this.props.cardData.type === \"screen\") {\n            if (this.props.inset) {\n                return true;\n            } else {\n                return (\n                    !this.rtcSession.eq(this.rtcSession.channel.activeRtcSession) &&\n                    !this.props.minimized\n                );\n            }\n        }\n        return false;\n    }\n\n    get showRemoteWarning() {\n        return !this.props.minimized && !this.props.inset && this.isRemoteVideo;\n    }\n\n    get rtcSession() {\n        return this.props.cardData.session;\n    }\n\n    get channelMember() {\n        return this.rtcSession ? this.rtcSession.channel_member_id : this.props.cardData.member;\n    }\n\n    get isOfActiveCall() {\n        return Boolean(this.rtcSession && this.rtcSession.channel?.eq(this.rtc.channel));\n    }\n\n    get showConnectionState() {\n        if (\n            !this.rtcSession ||\n            !this.rtc.isHost ||\n            !this.isOfActiveCall ||\n            HIDDEN_CONNECTION_STATES.has(this.rtcSession.connectionState)\n        ) {\n            return false;\n        }\n        if (this.rtc.state.connectionType === CONNECTION_TYPES.SERVER) {\n            return this.rtcSession.eq(this.rtc?.selfSession);\n        } else {\n            return this.rtcSession.notEq(this.rtc?.selfSession);\n        }\n    }\n\n    /**\n     * @deprecated use `showConnectionState` instead\n     */\n    get showServerState() {\n        return false;\n    }\n\n    get name() {\n        return this.channelMember?.name;\n    }\n\n    get hasMediaError() {\n        return (\n            this.isOfActiveCall &&\n            Boolean(this.rtcSession?.videoError || this.rtcSession?.audioError)\n        );\n    }\n\n    get hasVideo() {\n        return Boolean(this.props.cardData.videoStream);\n    }\n\n    get isTalking() {\n        return Boolean(\n            this.rtcSession && this.rtcSession.isActuallyTalking && !this.rtc.selfSession?.is_deaf\n        );\n    }\n\n    get hasRaisingHand() {\n        const screenStream = this.rtcSession.videoStreams.get(\"screen\");\n        return Boolean(\n            this.rtcSession.raisingHand &&\n                (!screenStream || screenStream !== this.props.cardData.videoStream)\n        );\n    }\n\n    get isActiveRtcSession() {\n        return this.rtcSession && this.rtcSession.eq(this.rtcSession.channel?.activeRtcSession);\n    }\n\n    async onClick(ev) {\n        if (isEventHandled(ev, \"CallParticipantCard.clickVolumeAnchor\")) {\n            return;\n        }\n        if (this.isDrag) {\n            this.isDrag = false;\n            return;\n        }\n        if (this.rtcSession) {\n            const channel = this.rtcSession.channel;\n            this.rtcSession.mainVideoStreamType = this.props.cardData.type;\n            if (this.rtcSession.eq(channel.activeRtcSession) && !this.props.inset) {\n                channel.activeRtcSession = undefined;\n                this.rtcSession.mainVideoStreamType = undefined;\n            } else {\n                const activeRtcSession = channel.activeRtcSession;\n                const currentMainVideoType = this.rtcSession.mainVideoStreamType;\n                channel.activeRtcSession = this.rtcSession;\n                if (this.props.inset && activeRtcSession) {\n                    this.props.inset(activeRtcSession, currentMainVideoType);\n                }\n            }\n            return;\n        }\n        await rpc(\"/mail/rtc/channel/cancel_call_invitation\", {\n            channel_id: this.props.thread.id,\n            member_ids: [this.channelMember.id],\n        });\n    }\n\n    async onClickReplay() {\n        this.env.bus.trigger(\"RTC-SERVICE:PLAY_MEDIA\");\n    }\n\n    onMouseDown() {\n        if (!this.props.inset) {\n            return;\n        }\n        const onMousemove = (ev) => this.drag(ev);\n        const onMouseup = () => {\n            const insetEl = this.root.el;\n            const bottomOffset = this.env.inChatWindow ? this.window.innerHeight * 0.05 : 0; // 5vh in pixels\n            if (parseInt(insetEl.style.left) < insetEl.parentNode.offsetWidth / 2) {\n                insetEl.style.left = \"1vh\";\n                insetEl.style.right = \"\";\n            } else {\n                insetEl.style.left = \"\";\n                insetEl.style.right = \"1vh\";\n            }\n            if (\n                parseInt(insetEl.style.top) <\n                (insetEl.parentNode.offsetHeight - bottomOffset) / 2\n            ) {\n                insetEl.style.top = \"1vh\";\n                insetEl.style.bottom = \"\";\n            } else {\n                insetEl.style.bottom = this.env.inChatWindow ? \"5vh\" : \"1vh\";\n                insetEl.style.top = \"unset\";\n            }\n            this.dragPos = undefined;\n            this.parentBoundingRect = undefined;\n            document.removeEventListener(\"mouseup\", onMouseup);\n            document.removeEventListener(\"mousemove\", onMousemove);\n        };\n        document.addEventListener(\"mouseup\", onMouseup);\n        document.addEventListener(\"mousemove\", onMousemove);\n    }\n\n    onTouchMove(ev) {\n        if (!this.props.inset) {\n            return;\n        }\n        this.drag(ev);\n    }\n\n    drag(ev) {\n        this.isDrag = true;\n        const insetEl = this.root.el;\n        const parent = insetEl.parentNode;\n        const boundingRect =\n            this.parentBoundingRect || (this.parentBoundingRect = parent.getBoundingClientRect());\n        const bottomOffset = this.env.inChatWindow ? this.window.innerHeight * 0.05 : 0; // 5vh in pixels\n        const clientX = Math.max((ev.clientX ?? ev.touches[0].clientX) - boundingRect.left, 0);\n        const clientY = Math.max((ev.clientY ?? ev.touches[0].clientY) - boundingRect.top, 0);\n        if (!this.dragPos) {\n            this.dragPos = { posX: clientX, posY: clientY };\n        }\n        const dX = this.dragPos.posX - clientX;\n        const dY = this.dragPos.posY - clientY;\n        const widthOffset = parent.offsetWidth - insetEl.clientWidth;\n        const heightOffset = parent.offsetHeight - insetEl.clientHeight - bottomOffset;\n        this.dragPos.posX = Math.min(clientX, widthOffset);\n        this.dragPos.posY = Math.min(clientY, heightOffset);\n        insetEl.style.left = Math.min(Math.max(insetEl.offsetLeft - dX, 0), widthOffset) + \"px\";\n        insetEl.style.top = Math.min(Math.max(insetEl.offsetTop - dY, 0), heightOffset) + \"px\";\n    }\n\n    onFullScreenChange() {\n        this.root.el.style = \"left:''; top:''\";\n    }\n}\n", "import { Component, onMounted, onPatched, status, useExternalListener, useRef } from \"@odoo/owl\";\nimport { useService } from \"@web/core/utils/hooks\";\n\n/**\n * @typedef {Object} Props\n * @property {import(\"models\").RtcSession} session\n * @extends {Component<Props, Env>}\n */\nexport class CallParticipantVideo extends Component {\n    static props = [\"session\", \"type\", \"inset?\"];\n    static template = \"discuss.CallParticipantVideo\";\n\n    setup() {\n        super.setup();\n        this.rtc = useService(\"discuss.rtc\");\n        this.store = useService(\"mail.store\");\n        this.root = useRef(\"root\");\n        onMounted(() => this._update());\n        onPatched(() => this._update());\n        useExternalListener(this.env.bus, \"RTC-SERVICE:PLAY_MEDIA\", async () => {\n            await this.play();\n        });\n    }\n\n    _update() {\n        if (!this.root.el) {\n            return;\n        }\n        if (!this.props.session || !this.props.session.getStream(this.props.type)) {\n            this.root.el.srcObject = undefined;\n        } else {\n            this.root.el.srcObject = this.props.session.getStream(this.props.type);\n        }\n        this.root.el.load();\n    }\n\n    async play() {\n        try {\n            await this.root.el?.play?.();\n            this.props.session.videoError = undefined;\n        } catch (error) {\n            if (status(this) === \"destroyed\") {\n                return;\n            }\n            this.props.session.videoError = error.name;\n        }\n    }\n\n    async onVideoLoadedMetaData() {\n        await this.play();\n    }\n}\n", "import { Component } from \"@odoo/owl\";\nimport { Dialog } from \"@web/core/dialog/dialog\";\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { useService } from \"@web/core/utils/hooks\";\n\nexport class CallPermissionDialog extends Component {\n    static components = { Dialog };\n    static props = {\n        close: Function,\n        media: {\n            type: String,\n            validate: (s) => [\"camera\", \"microphone\"].includes(s),\n        },\n        useMicrophone: Function,\n        useCamera: Function,\n    };\n    static template = \"discuss.CallPermissionDialog\";\n\n    setup() {\n        this.rtc = useService(\"discuss.rtc\");\n    }\n\n    async onClickUseMicrophone() {\n        if (await this.rtc.askForBrowserPermission({ audio: true })) {\n            await this.props.useMicrophone();\n        }\n        this.props.close();\n    }\n\n    async onClickUseCamera() {\n        if (await this.rtc.askForBrowserPermission({ video: true })) {\n            await this.props.useCamera();\n        }\n        this.props.close();\n    }\n\n    async onClickUseMicAndCamera() {\n        if (await this.rtc.askForBrowserPermission({ audio: true, video: true })) {\n            await Promise.all([this.props.useMicrophone(), this.props.useCamera()]);\n        }\n        this.props.close();\n    }\n\n    get primaryActionText() {\n        return this.props.media === \"camera\" ? _t(\"Use Camera\") : _t(\"Use Microphone\");\n    }\n\n    get permissionPrompt() {\n        if (this.props.media === \"microphone\") {\n            return _t(\"Do you want people to hear you in the meeting?\");\n        }\n        return _t(\"Do you want people to see you in the meeting?\");\n    }\n\n    get permissionNote() {\n        return _t(\"You can still turn off your %s anytime.\", this.props.media);\n    }\n}\n", "import { Action, ACTION_TAGS } from \"@mail/core/common/action\";\nimport { ActionList } from \"@mail/core/common/action_list\";\nimport {\n    cameraOnAction,\n    muteAction,\n    quickActionSettings,\n    quickVideoSettings,\n} from \"@mail/discuss/call/common/call_actions\";\nimport { CallPermissionDialog } from \"@mail/discuss/call/common/call_permission_dialog\";\nimport { closeStream, onChange } from \"@mail/utils/common/misc\";\n\nimport { Component, onWillDestroy, status, useEffect, useRef, useState } from \"@odoo/owl\";\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { useService } from \"@web/core/utils/hooks\";\n\n/**\n * @typedef {Object} Props\n * @property {Number} [activateCamera]\n * @property {Number} [activateMicrophone]\n * @property {({ microphone?: boolean, camera?: boolean }) => void} [onSettingsChanged]\n * @extends {Component<Props, Env>}\n */\nexport class CallPreview extends Component {\n    static template = \"mail.CallPreview\";\n    static props = [\"activateCamera?\", \"activateMicrophone?\", \"onSettingsChanged?\"];\n    static components = { ActionList };\n\n    setup() {\n        this.dialog = useService(\"dialog\");\n        this.notification = useService(\"notification\");\n        this.rtc = useService(\"discuss.rtc\");\n        this.store = useService(\"mail.store\");\n        this.state = useState({ audioStream: null, blurManager: null, videoStream: null });\n        this.audioRef = useRef(\"audio\");\n        this.videoRef = useRef(\"video\");\n        useEffect(\n            (videoEl, audioEl, audioStream, videoStream, blurStream) => {\n                if (audioEl && !audioEl.srcObject && audioStream) {\n                    audioEl.srcObject = audioStream;\n                }\n                if (videoEl && !videoEl.srcObject && videoStream) {\n                    videoEl.srcObject = blurStream ?? videoStream;\n                }\n            },\n            () => [\n                this.videoRef.el,\n                this.audioRef.el,\n                this.state.audioStream,\n                this.state.videoStream,\n                this.state.blurManager?.stream,\n            ]\n        );\n        if (this.hasRtcSupport) {\n            onChange(this.rtc, \"microphonePermission\", () => {\n                if (this.rtc.microphonePermission !== \"granted\") {\n                    this.disableMicrophone();\n                }\n            });\n            onChange(this.rtc, \"cameraPermission\", () => {\n                if (this.rtc.cameraPermission !== \"granted\") {\n                    this.disableCamera();\n                }\n            });\n            onChange(this.store.settings, \"audioInputDeviceId\", () => {\n                if (this.state.audioStream) {\n                    closeStream(this.state.audioStream);\n                    this.enableMicrophone();\n                }\n            });\n            onChange(this.store.settings, \"cameraInputDeviceId\", () => {\n                if (this.state.videoStream) {\n                    closeStream(this.state.videoStream);\n                    this.enableCamera();\n                }\n            });\n            onChange(this.store.settings, \"audioOutputDeviceId\", (deviceId) => {\n                this.audioRef.el?.setSinkId?.(deviceId).catch(() => {});\n            });\n            onChange(this.store.settings, \"useBlur\", () => {\n                if (this.store.settings.useBlur) {\n                    this.enableBlur();\n                } else {\n                    this.disableBlur();\n                }\n            });\n            onChange(this.store.settings, [\"edgeBlurAmount\", \"backgroundBlurAmount\"], () => {\n                if (this.state.blurManager) {\n                    this.state.blurManager.edgeBlur = this.store.settings.edgeBlurAmount;\n                    this.state.blurManager.backgroundBlur =\n                        this.store.settings.backgroundBlurAmount;\n                }\n            });\n            onWillDestroy(() => {\n                closeStream(this.state.audioStream);\n                closeStream(this.state.videoStream);\n            });\n            useEffect(\n                (activateCamera) => {\n                    if (activateCamera > 0 && !this.state.videoStream) {\n                        this.enableCamera();\n                    }\n                },\n                () => [this.props.activateCamera]\n            );\n            useEffect(\n                (activateMicrophone) => {\n                    if (activateMicrophone > 0 && !this.state.audioStream) {\n                        this.enableMicrophone();\n                    }\n                },\n                () => [this.props.activateMicrophone]\n            );\n        }\n    }\n\n    get hasRtcSupport() {\n        return Boolean(\n            navigator.mediaDevices && navigator.mediaDevices.getUserMedia && window.MediaStream\n        );\n    }\n\n    get actions() {\n        const cameraOnActionUpdated = {\n            ...cameraOnAction,\n            name: () => (this.state.videoStream ? _t(\"Stop camera\") : _t(\"Turn camera on\")),\n            isActive: () => this.state.videoStream,\n            onSelected: () => this.toggleCamera(),\n            tags: (...args) => {\n                const tags = cameraOnAction.tags?.(...args) ?? [];\n                if (!args[0].action.isActive) {\n                    tags.push(ACTION_TAGS.DANGER);\n                }\n                return tags;\n            },\n        };\n        const muteActionUpdated = {\n            ...muteAction,\n            isActive: () => !this.state.audioStream,\n            name: ({ action }) => (action.isActive ? _t(\"Unmute\") : _t(\"Mute\")),\n            onSelected: () => this.toggleMic(),\n        };\n        return [\n            [\n                new Action({\n                    id: \"toggle-microphone\",\n                    owner: this,\n                    definition: muteActionUpdated,\n                    store: this.store,\n                }),\n                new Action({\n                    id: \"audio-settings\",\n                    owner: this,\n                    definition: quickActionSettings,\n                    store: this.store,\n                }),\n            ],\n            [\n                new Action({\n                    id: \"toggle-camera\",\n                    owner: this,\n                    definition: cameraOnActionUpdated,\n                    store: this.store,\n                }),\n                new Action({\n                    id: \"video-settings\",\n                    owner: this,\n                    definition: quickVideoSettings,\n                    store: this.store,\n                }),\n            ],\n        ];\n    }\n\n    async enableMicrophone() {\n        if (\n            this.rtc.microphonePermission !== \"granted\" &&\n            !(await this.rtc.askForBrowserPermission({ audio: true }))\n        ) {\n            return;\n        }\n        this.state.audioStream = await navigator.mediaDevices.getUserMedia({\n            audio: this.store.settings.audioConstraints,\n        });\n        if (status(this) === \"destroyed\") {\n            closeStream(this.state.audioStream);\n            return;\n        }\n        if (this.audioRef.el) {\n            this.audioRef.el.srcObject = this.state.audioStream;\n        }\n        this.props.onSettingsChanged?.({ microphone: true });\n    }\n\n    disableMicrophone() {\n        closeStream(this.state.audioStream);\n        this.state.audioStream = null;\n        if (this.audioRef.el) {\n            this.audioRef.el.srcObject = null;\n        }\n        this.props.onSettingsChanged?.({ microphone: false });\n    }\n\n    async toggleMic() {\n        if (this.state.audioStream) {\n            this.disableMicrophone();\n            return;\n        }\n        if (this.rtc.microphonePermission === \"prompt\") {\n            this.dialog.add(CallPermissionDialog, {\n                media: \"microphone\",\n                useMicrophone: () => this.enableMicrophone(),\n                useCamera: () => this.enableCamera(),\n            });\n            return;\n        }\n        await this.enableMicrophone();\n    }\n\n    async enableCamera() {\n        if (\n            this.rtc.cameraPermission !== \"granted\" &&\n            !(await this.rtc.askForBrowserPermission({ video: true }))\n        ) {\n            return;\n        }\n        this.state.videoStream = await navigator.mediaDevices.getUserMedia({\n            video: this.store.settings.cameraConstraints,\n        });\n        if (!this.videoRef.el) {\n            return;\n        }\n        if (status(this) === \"destroyed\") {\n            closeStream(this.state.videoStream);\n            return;\n        }\n        if (this.videoRef.el) {\n            this.videoRef.el.srcObject = this.state.videoStream;\n        }\n        this.props.onSettingsChanged?.({ camera: true });\n        if (this.store.settings.useBlur) {\n            await this.enableBlur();\n        }\n    }\n\n    disableCamera() {\n        closeStream(this.state.videoStream);\n        this.state.videoStream = null;\n        this.state.blurManager?.close();\n        this.state.blurManager = undefined;\n        if (this.videoRef.el) {\n            this.videoRef.el.srcObject = null;\n        }\n        this.props.onSettingsChanged?.({ camera: false });\n    }\n\n    async toggleCamera() {\n        if (this.state.videoStream) {\n            this.disableCamera();\n            return;\n        }\n        if (this.rtc.cameraPermission === \"prompt\") {\n            this.dialog.add(CallPermissionDialog, {\n                media: \"camera\",\n                useMicrophone: () => this.enableMicrophone(),\n                useCamera: () => this.enableCamera(),\n            });\n            return;\n        }\n        await this.enableCamera();\n    }\n\n    async enableBlur() {\n        this.store.settings.useBlur = true;\n        if (!this.videoRef.el) {\n            return;\n        }\n        try {\n            this.state.blurManager = await this.rtc.applyBlurEffect(this.state.videoStream);\n            this.videoRef.el.srcObject = await this.state.blurManager.stream;\n        } catch (_e) {\n            this.notification.add(_e.message, { type: \"warning\" });\n            this.disableBlur();\n        }\n    }\n\n    disableBlur() {\n        this.store.settings.useBlur = false;\n        if (this.videoRef.el) {\n            this.videoRef.el.srcObject = this.state.videoStream;\n        }\n        this.state.blurManager?.close();\n        this.state.blurManager = undefined;\n    }\n\n    toggleBlur() {\n        if (this.state.blurManager) {\n            this.disableBlur();\n            return;\n        }\n        this.enableBlur();\n    }\n}\n", "import { Component, onWillStart, useExternalListener, useState, xml } from \"@odoo/owl\";\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { browser } from \"@web/core/browser/browser\";\nimport { debounce } from \"@web/core/utils/timing\";\nimport { isMobileOS } from \"@web/core/browser/feature_detection\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { useMicrophoneVolume } from \"@mail/utils/common/hooks\";\nimport { ActionPanel } from \"@mail/discuss/core/common/action_panel\";\nimport { DeviceSelect } from \"@mail/discuss/call/common/device_select\";\nimport { Dialog } from \"@web/core/dialog/dialog\";\n\nexport class CallSettings extends Component {\n    static template = \"discuss.CallSettings\";\n    static props = [\"withActionPanel?\", \"*\"];\n    static defaultProps = {\n        withActionPanel: true,\n    };\n    static components = { ActionPanel, DeviceSelect };\n\n    setup() {\n        super.setup();\n        this.notification = useService(\"notification\");\n        this.store = useService(\"mail.store\");\n        this.rtc = useService(\"discuss.rtc\");\n        this.microphoneVolume = useMicrophoneVolume();\n        this.state = useState({\n            userDevices: [],\n        });\n        this.pttExtService = useService(\"discuss.ptt_extension\");\n        this.saveBackgroundBlurAmount = debounce(() => {\n            browser.localStorage.setItem(\n                \"mail_user_setting_background_blur_amount\",\n                this.store.settings.backgroundBlurAmount.toString()\n            );\n        }, 2000);\n        this.saveEdgeBlurAmount = debounce(() => {\n            browser.localStorage.setItem(\n                \"mail_user_setting_edge_blur_amount\",\n                this.store.settings.edgeBlurAmount.toString()\n            );\n        }, 2000);\n        useExternalListener(browser, \"keydown\", this._onKeyDown, { capture: true });\n        useExternalListener(browser, \"keyup\", this._onKeyUp, { capture: true });\n        onWillStart(async () => {\n            if (!browser.navigator.mediaDevices) {\n                // zxing-js: isMediaDevicesSuported or canEnumerateDevices is false.\n                this.notification.add(\n                    _t(\"Media devices unobtainable. SSL might not be set up properly.\"),\n                    { type: \"warning\" }\n                );\n                console.warn(\"Media devices unobtainable. SSL might not be set up properly.\");\n                return;\n            }\n            this.state.userDevices = await browser.navigator.mediaDevices.enumerateDevices();\n        });\n    }\n\n    get stopText() {\n        return _t(\"Stop\");\n    }\n\n    get testText() {\n        return _t(\"Test\");\n    }\n\n    get pushToTalkKeyText() {\n        const { shiftKey, ctrlKey, altKey, key } = this.store.settings.pushToTalkKeyFormat();\n        const f = (k, name) => (k ? name : \"\");\n        const keys = [f(ctrlKey, \"Ctrl\"), f(altKey, \"Alt\"), f(shiftKey, \"Shift\"), key].filter(\n            Boolean\n        );\n        return keys.join(\" + \");\n    }\n\n    get isMobileOS() {\n        return isMobileOS();\n    }\n\n    _onKeyDown(ev) {\n        if (!this.store.settings.isRegisteringKey) {\n            return;\n        }\n        ev.stopPropagation();\n        ev.preventDefault();\n        this.store.settings.setPushToTalkKey(ev);\n    }\n\n    _onKeyUp(ev) {\n        if (!this.store.settings.isRegisteringKey) {\n            return;\n        }\n        ev.stopPropagation();\n        ev.preventDefault();\n        this.store.settings.isRegisteringKey = false;\n    }\n\n    onChangeLogRtc(ev) {\n        this.store.settings.logRtc = ev.target.checked;\n    }\n\n    onChangeSelectAudioInput(ev) {\n        this.store.settings.setAudioInputDevice(ev.target.value);\n    }\n\n    onClickDownloadLogs() {\n        this.rtc.dumpLogs({ download: true });\n    }\n\n    onClickRegisterKeyButton() {\n        this.store.settings.isRegisteringKey = !this.store.settings.isRegisteringKey;\n    }\n\n    onChangeDelay(ev) {\n        this.store.settings.setDelayValue(ev.target.value);\n    }\n\n    onChangeBlur(ev) {\n        this.store.settings.useBlur = ev.target.checked;\n    }\n\n    onChangeShowOnlyVideo(ev) {\n        const showOnlyVideo = ev.target.checked;\n        this.store.settings.showOnlyVideo = showOnlyVideo;\n        browser.localStorage.setItem(\n            \"mail_user_setting_show_only_video\",\n            this.store.settings.showOnlyVideo\n        );\n        const activeRtcSessions = this.store.allActiveRtcSessions;\n        if (showOnlyVideo && activeRtcSessions) {\n            activeRtcSessions\n                .filter((rtcSession) => !rtcSession.videoStream)\n                .forEach((rtcSession) => {\n                    rtcSession.channel.activeRtcSession = undefined;\n                });\n        }\n    }\n\n    onChangeBackgroundBlurAmount(ev) {\n        this.store.settings.backgroundBlurAmount = Number(ev.target.value);\n        this.saveBackgroundBlurAmount();\n    }\n\n    onChangeEdgeBlurAmount(ev) {\n        this.store.settings.edgeBlurAmount = Number(ev.target.value);\n        this.saveEdgeBlurAmount();\n    }\n}\n\nexport class CallSettingsDialog extends Component {\n    static template = xml`\n        <Dialog size=\"medium\" footer=\"false\" title.translate=\"Voice &amp; Video Settings\">\n            <CallSettings withActionPanel=\"false\"/>\n        </Dialog>\n    `;\n    static props = [\"*\"];\n    static components = { CallSettings, Dialog };\n}\n", "import { ChannelMember } from \"@mail/discuss/core/common/channel_member_model\";\nimport { fields } from \"@mail/core/common/record\";\n\nimport { browser } from \"@web/core/browser/browser\";\nimport { patch } from \"@web/core/utils/patch\";\n\nChannelMember.CANCEL_CALL_INVITE_DELAY = 30000;\n/** @type {import(\"models\").ChannelMember} */\nconst ChannelMemberPatch = {\n    setup() {\n        super.setup(...arguments);\n        this.rtc_inviting_session_id = fields.One(\"discuss.channel.rtc.session\", {\n            /** @this {import(\"models\").ChannelMember} */\n            onAdd(r) {\n                if (!this.channel_id) {\n                    return;\n                }\n                this.channel_id.rtc_session_ids.add(r);\n                this.store.ringingThreads.add(this.channel_id);\n                this.channel_id.cancelRtcInvitationTimeout = browser.setTimeout(() => {\n                    this.store.env.services[\"discuss.rtc\"].leaveCall(this.channel_id);\n                }, ChannelMember.CANCEL_CALL_INVITE_DELAY);\n            },\n            /** @this {import(\"models\").ChannelMember} */\n            onDelete() {\n                if (!this.channel_id) {\n                    return;\n                }\n                browser.clearTimeout(this.channel_id.cancelRtcInvitationTimeout);\n                this.store.ringingThreads.delete(this.channel_id);\n            },\n        });\n        this.rtcSession = fields.One(\"discuss.channel.rtc.session\");\n    },\n};\npatch(ChannelMember.prototype, ChannelMemberPatch);\n", "import { ChatWindow } from \"@mail/core/common/chat_window\";\nimport { Call } from \"@mail/discuss/call/common/call\";\nimport { PipBanner } from \"@mail/discuss/call/common/pip_banner\";\nimport { useService } from \"@web/core/utils/hooks\";\n\nimport { patch } from \"@web/core/utils/patch\";\n\nObject.assign(ChatWindow.components, { Call, PipBanner });\n\npatch(ChatWindow.prototype, {\n    setup() {\n        super.setup(...arguments);\n        this.rtc = useService(\"discuss.rtc\");\n    },\n});\n", "import { Component, onWillDestroy, onWillStart, useState } from \"@odoo/owl\";\n\nimport { browser } from \"@web/core/browser/browser\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { isBrowserChrome } from \"@web/core/browser/feature_detection\";\n\nconst deviceKind = new Set([\"audioinput\", \"videoinput\", \"audiooutput\"]);\n\nexport class DeviceSelect extends Component {\n    static props = {\n        kind: {\n            type: String,\n            validate: (string) => deviceKind.has(string),\n        },\n    };\n    static template = \"discuss.CallDeviceSelect\";\n\n    setup() {\n        super.setup();\n        this.store = useService(\"mail.store\");\n        this.notification = useService(\"notification\");\n        this.state = useState({\n            userDevices: [],\n        });\n        this.abortController = new AbortController();\n        this.isBrowserChrome = isBrowserChrome();\n        onWillStart(async () => {\n            if (!browser.navigator.mediaDevices) {\n                // zxing-js: isMediaDevicesSuported or canEnumerateDevices is false.\n                this.notification.add(\n                    _t(\"Media devices unobtainable. SSL might not be set up properly.\"),\n                    { type: \"warning\" }\n                );\n                console.warn(\"Media devices unobtainable. SSL might not be set up properly.\");\n                return;\n            }\n            await this.updateDevicesList();\n            this.setupEventListeners();\n        });\n        onWillDestroy(() => {\n            this.abortController.abort();\n        });\n    }\n\n    async updateDevicesList() {\n        this.state.userDevices = await browser.navigator.mediaDevices.enumerateDevices();\n    }\n\n    async setupEventListeners() {\n        const boundHandler = this.updateDevicesList.bind(this);\n        const signal = this.abortController.signal;\n\n        browser.navigator.mediaDevices.addEventListener(\"devicechange\", boundHandler, { signal });\n        if (this.props.kind == \"videoinput\") {\n            const cameraPermission = await browser.navigator.permissions.query({ name: \"camera\" });\n            cameraPermission.addEventListener(\"change\", boundHandler, { signal });\n        } else {\n            const microphonePermission = await browser.navigator.permissions.query({\n                name: \"microphone\",\n            });\n            microphonePermission.addEventListener(\"change\", boundHandler, { signal });\n        }\n    }\n\n    async showPermissionDialog(kind) {\n        if (kind === \"videoinput\") {\n            if (this.store.rtc.cameraPermission === \"denied\") {\n                this.store.rtc.showMediaUnavailableWarning({ camera: true });\n            } else {\n                this.store.rtc.showMediaPermissionDialog(\"camera\");\n                return;\n            }\n        } else {\n            if (this.store.rtc.microphonePermission === \"denied\") {\n                this.store.rtc.showMediaUnavailableWarning({ microphone: true });\n            } else {\n                this.store.rtc.showMediaPermissionDialog(\"microphone\");\n                return;\n            }\n        }\n    }\n\n    isSelected(id) {\n        switch (this.props.kind) {\n            case \"audioinput\":\n                return this.store.settings.audioInputDeviceId === id;\n            case \"videoinput\":\n                return this.store.settings.cameraInputDeviceId === id;\n            case \"audiooutput\":\n                return this.store.settings.audioOutputDeviceId === id;\n        }\n    }\n\n    onChangeSelectAudioInput(ev) {\n        switch (this.props.kind) {\n            case \"audioinput\":\n                this.store.settings.setAudioInputDevice(ev.target.value);\n                return;\n            case \"videoinput\":\n                this.store.settings.setCameraInputDevice(ev.target.value);\n                return;\n            case \"audiooutput\":\n                this.store.settings.setAudioOutputDevice(ev.target.value);\n                return;\n        }\n    }\n\n    isPermissionGranted(kind) {\n        if (kind === \"videoinput\") {\n            return this.store.rtc.cameraPermission === \"granted\";\n        }\n        return this.store.rtc.microphonePermission === \"granted\";\n    }\n}\n", "import { Component } from \"@odoo/owl\";\nimport { registry } from \"@web/core/registry\";\nimport { CallSettings } from \"@mail/discuss/call/common/call_settings\";\n\nexport class DiscussCallSettingsClientAction extends Component {\n    static components = { CallSettings };\n    static props = [\"*\"];\n    static template = \"mail.DiscussCallSettingsClientAction\";\n}\n\nregistry\n    .category(\"actions\")\n    .add(\"mail.discuss_call_settings_action\", DiscussCallSettingsClientAction);\n", "import { registry } from \"@web/core/registry\";\nimport { PeerToPeer } from \"@mail/discuss/call/common/peer_to_peer\";\n\nexport const discussP2P = {\n    dependencies: [\"bus_service\"],\n    /**\n     * @param {import(\"@web/env\").OdooEnv} env\n     * @param {import(\"services\").ServiceFactories} services\n     */\n    start(env, services) {\n        const p2p = new PeerToPeer({\n            logLevel: env.debug ? \"info\" : undefined,\n            notificationRoute: \"/mail/rtc/session/notify_call_members\",\n        });\n        services[\"bus_service\"].subscribe(\n            \"discuss.channel.rtc.session/peer_notification\",\n            ({ sender, notifications }) => {\n                for (const content of notifications) {\n                    p2p.handleNotification(sender, content);\n                }\n            }\n        );\n        return p2p;\n    },\n};\n\nregistry.category(\"services\").add(\"discuss.p2p\", discussP2P);\n", "import { MailGuest } from \"@mail/core/common/mail_guest_model\";\nimport { fields } from \"@mail/model/misc\";\nimport { patch } from \"@web/core/utils/patch\";\n\npatch(MailGuest.prototype, {\n    setup() {\n        super.setup(...arguments);\n        this.currentRtcSession = fields.One(\"discuss.channel.rtc.session\", { inverse: \"guest_id\" });\n    },\n});\n", "import { Composer } from \"@mail/core/common/composer\";\nimport { Thread } from \"@mail/core/common/thread\";\nimport { Call } from \"@mail/discuss/call/common/call\";\nimport { CallActionList } from \"@mail/discuss/call/common/call_action_list\";\nimport { ChannelInvitation } from \"@mail/discuss/core/common/channel_invitation\";\nimport {\n    inDiscussCallViewProps,\n    useInDiscussCallView,\n    useMessageScrolling,\n} from \"@mail/utils/common/hooks\";\n\nimport { Component, onMounted, onWillUnmount, useChildSubEnv, useSubEnv } from \"@odoo/owl\";\n\nimport { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { MeetingSideActions } from \"./meeting_side_actions\";\nimport { useThreadActions } from \"@mail/core/common/thread_actions\";\nimport { useMessageSearch } from \"@mail/core/common/message_search_hook\";\n\n/** @typedef {\"chat\"|\"invite\"} MeetingPanel */\n\n/**\n * @typedef {Object} Props\n * @property {ThreadActionDefinition.id} [autoOpenAction]\n * @extends {Component<Props, Env>}\n */\nexport class Meeting extends Component {\n    static template = \"mail.Meeting\";\n    static props = [\"autoOpenAction?\", ...inDiscussCallViewProps];\n    static components = {\n        Call,\n        CallActionList,\n        ChannelInvitation,\n        Composer,\n        Dropdown,\n        MeetingSideActions,\n        Thread,\n    };\n\n    setup() {\n        this.store = useService(\"mail.store\");\n        this.ui = useService(\"ui\");\n        this.rtc = useService(\"discuss.rtc\");\n        onMounted(() => {\n            if (this.props.autoOpenAction) {\n                this.threadActions.actions\n                    .find((a) => a.id === this.props.autoOpenAction)\n                    ?.onSelected();\n            }\n        });\n        useInDiscussCallView();\n        useSubEnv({\n            inMeetingView: {\n                openChat: () =>\n                    this.threadActions.actions\n                        .find((action) => action.id === \"meeting-chat\")\n                        ?.open(),\n            },\n        });\n        this.threadActions = useThreadActions({ thread: () => this.thread });\n        this.messageHighlight = useMessageScrolling();\n        this.messageSearch = useMessageSearch(this.thread);\n        useChildSubEnv({\n            closeActionPanel: () => this.threadActions.activeAction?.close(),\n            messageHighlight: this.messageHighlight,\n            messageSearch: this.messageSearch,\n        });\n        onMounted(() => (this.store.meetingViewOpened = true));\n        onWillUnmount(() => (this.store.meetingViewOpened = false));\n    }\n\n    get thread() {\n        return this.store.rtc.channel;\n    }\n}\n", "import { Composer } from \"@mail/core/common/composer\";\nimport { Thread } from \"@mail/core/common/thread\";\nimport { ActionPanel } from \"@mail/discuss/core/common/action_panel\";\nimport { Typing } from \"@mail/discuss/typing/common/typing\";\n\nimport { Component, useState, useSubEnv } from \"@odoo/owl\";\n\nimport { isMobileOS } from \"@web/core/browser/feature_detection\";\nimport { useChildRef, useService } from \"@web/core/utils/hooks\";\n\n/**\n * @typedef {Object} Props\n * @property {import(\"models\").Thread} [thread]\n * @extends {Component<Props, Env>}\n */\nexport class MeetingChat extends Component {\n    static template = \"mail.MeetingChat\";\n    static components = {\n        ActionPanel,\n        Composer,\n        Thread,\n        Typing,\n    };\n    static props = [\"thread?\"];\n\n    setup() {\n        this.store = useService(\"mail.store\");\n        this.ui = useService(\"ui\");\n        this.rtc = useService(\"discuss.rtc\");\n        this.state = useState({ jumpPresent: 0 });\n        this.panelContentRef = useChildRef();\n        this.isMobileOS = isMobileOS();\n        useSubEnv({ inMeetingChat: true });\n    }\n\n    get thread() {\n        return this.store.rtc.channel;\n    }\n}\n", "import { ActionList } from \"@mail/core/common/action_list\";\n\nimport { Component, useSubEnv } from \"@odoo/owl\";\n\nimport { useService } from \"@web/core/utils/hooks\";\n\n/** @typedef {\"chat\"|\"invite\"} MeetingPanel */\n\n/**\n * @typedef {Object} Props\n * @property {import(\"@mail/core/common/thread_actions\").UseThreadActions} threadActions\n * @extends {Component<Props, Env>}\n */\nexport class MeetingSideActions extends Component {\n    static template = \"mail.MeetingSideActions\";\n    static props = [\"threadActions\"];\n    static components = { ActionList };\n\n    setup() {\n        this.store = useService(\"mail.store\");\n        useSubEnv({ inMeetingSideActions: true });\n    }\n\n    computeActions() {\n        const quickThreadActionIds = [\"invite-people\", \"meeting-chat\"];\n        const threadActions = this.props.threadActions;\n        const { quick, other, group } = threadActions.partition;\n        const partitionedActions = {\n            quick: quick.filter((action) => !quickThreadActionIds.includes(action.id)),\n            other: other.filter((action) => !quickThreadActionIds.includes(action.id)),\n            group: group\n                .map((group) => group.filter((action) => !quickThreadActionIds.includes(action.id)))\n                .filter((g) => g.length > 0),\n        };\n        const actions = threadActions.actions.filter((action) =>\n            quickThreadActionIds.includes(action.id)\n        );\n        actions.push(\n            threadActions.more({\n                actions: [\n                    partitionedActions.quick,\n                    partitionedActions.other,\n                    ...partitionedActions.group,\n                ],\n            })\n        );\n        this.actions = actions;\n    }\n}\n", "import { rpc } from \"@web/core/network/rpc\";\nimport { Deferred } from \"@web/core/utils/concurrency\";\nimport { browser } from \"@web/core/browser/browser\";\n\nexport const STREAM_TYPE = Object.freeze({\n    AUDIO: \"audio\",\n    CAMERA: \"camera\",\n    SCREEN: \"screen\",\n});\nexport const UPDATE_EVENT = Object.freeze({\n    BROADCAST: \"broadcast\",\n    CONNECTION_CHANGE: \"connection_change\",\n    DISCONNECT: \"disconnect\",\n    INFO_CHANGE: \"info_change\",\n    RECOVERY: \"recovery\",\n    TRACK: \"track\",\n});\nconst LOG_LEVEL = Object.freeze({\n    NONE: \"none\",\n    DEBUG: \"debug\",\n    INFO: \"info\",\n    WARN: \"warn\",\n    ERROR: \"error\",\n});\nconst INTERNAL_EVENT = Object.freeze({\n    ANSWER: \"answer\",\n    BROADCAST: \"broadcast\",\n    DISCONNECT: \"disconnect\",\n    ICE_CANDIDATE: \"ice-candidate\",\n    INFO: \"info\",\n    OFFER: \"offer\",\n    TRACK_CHANGE: \"trackChange\",\n});\nconst ORDERED_TRANSCEIVER_TYPES = [STREAM_TYPE.AUDIO, STREAM_TYPE.CAMERA, STREAM_TYPE.SCREEN];\nconst DEFAULT_BUS_BATCH_DELAY = 100;\nconst INITIAL_RECONNECT_DELAY = 2_000 + Math.random() * 1_000; // the initial delay between reconnection attempts\nconst MAXIMUM_RECONNECT_DELAY = 25_000 + Math.random() * 5_000; // the longest delay possible between reconnection attempts\nconst INVALID_ICE_CONNECTION_STATES = new Set([\"disconnected\", \"failed\", \"closed\"]);\nconst IS_CLIENT_RTC_COMPATIBLE = Boolean(window.RTCPeerConnection && window.MediaStream);\nconst DEFAULT_ICE_SERVERS = [\n    { urls: [\"stun:stun1.l.google.com:19302\", \"stun:stun2.l.google.com:19302\"] },\n];\nconst DEFAULT_NOTIFICATION_ROUTE = \"/mail/rtc/session/notify_call_members\";\n\n/**\n * @typedef {Object} Media\n * @property {MediaStreamTrack | null} track the track of the associated RtcRtpTransceiver, its presence does not\n *     imply active streaming as it exists for the whole lifetime transceiver (since webRTC 'unified plan').\n * @property {boolean} active represents whether the remote (peer) is actively streaming this track\n * @property {boolean} accepted represents whether the local (current user) wants to download this track\n */\n\n/**\n * @typedef {Object} Info (sealed)\n * @property {boolean} isSelfMuted\n * @property {boolean} isRaisingHand\n * @property {boolean} isDeaf\n * @property {boolean} isTalking\n * @property {boolean} isCameraOn\n * @property {boolean} isScreenSharingOn\n */\n\nexport class Peer {\n    /** @type {number} */\n    id;\n    /** @type {RTCPeerConnection} */\n    connection;\n    /** @type {number} */\n    connectRetryDelay = INITIAL_RECONNECT_DELAY;\n    sequence = 0;\n    /** @type {RTCDataChannel} */\n    dataChannel;\n    hasPriority = false;\n    isBuildingOffer = false;\n    isBuildingAnswer = false;\n    /** @type {Object<STREAM_TYPE[keyof STREAM_TYPE], Media>} */\n    medias = Object.seal({\n        [STREAM_TYPE.AUDIO]: {\n            track: null,\n            active: false,\n            accepted: true,\n        },\n        [STREAM_TYPE.SCREEN]: {\n            track: null,\n            active: false,\n            accepted: true,\n        },\n        [STREAM_TYPE.CAMERA]: {\n            track: null,\n            active: false,\n            accepted: true,\n        },\n    });\n    /**\n     * @param {number} id\n     * @param {Object} param2\n     * @param {RTCPeerConnection} param2.connection\n     * @param {RTCDataChannel} param2.dataChannel\n     * @param {boolean} hasPriority true if this peer offers should have priority in case of collisions\n     * @param {number} [connectRetryDelay=INITIAL_RECONNECT_DELAY]\n     */\n    constructor(\n        id,\n        {\n            connection,\n            dataChannel,\n            hasPriority = false,\n            connectRetryDelay = INITIAL_RECONNECT_DELAY,\n            sequence = 0,\n        }\n    ) {\n        this.id = id;\n        this.connection = connection;\n        this.dataChannel = dataChannel;\n        this.hasPriority = hasPriority;\n        this.connectRetryDelay = connectRetryDelay;\n        this.sequence = sequence;\n        this.ready = new Deferred();\n    }\n\n    disconnect() {\n        if (this.connection) {\n            const RTCRtpSenders = this.connection.getSenders();\n            for (const sender of RTCRtpSenders) {\n                try {\n                    this.connection.removeTrack(sender);\n                } catch {\n                    // ignore error\n                }\n            }\n            for (const transceiver of this.connection.getTransceivers()) {\n                try {\n                    transceiver.stop();\n                } catch {\n                    // transceiver may already be stopped by the remote.\n                }\n            }\n        }\n        this.ready.resolve?.();\n        this.connection?.close();\n        this.connection = undefined;\n        this.dataChannel?.close();\n        this.dataChannel = undefined;\n        for (const media of Object.values(this.medias)) {\n            media.track?.stop();\n        }\n    }\n    /**\n     * @param {{STREAM_TYPE[keyof STREAM_TYPE]}} streamType\n     * @param {boolean} canUpload whether this transceiver needs upload capability (outbound stream)\n     * @returns {RTCRtpTransceiverDirection}\n     */\n    getRecommendedTransceiverDirection(streamType, canUpload = false) {\n        if (this.medias[streamType].accepted) {\n            return canUpload ? \"sendrecv\" : \"recvonly\";\n        } else {\n            return canUpload ? \"sendonly\" : \"inactive\";\n        }\n    }\n    /**\n     * @param {STREAM_TYPE[keyof STREAM_TYPE]} streamType\n     * @returns {RTCRtpTransceiver | undefined} the transceiver used for this trackKind.\n     */\n    getTransceiver(streamType) {\n        if (!this.connection) {\n            // may be disconnected\n            return;\n        }\n        const transceivers = this.connection.getTransceivers();\n        return transceivers[ORDERED_TRANSCEIVER_TYPES.indexOf(streamType)];\n    }\n    /**\n     * @param {RTCRtpTransceiver} transceiver\n     * @returns {STREAM_TYPE[keyof STREAM_TYPE]}\n     */\n    getTransceiverStreamType(transceiver) {\n        const transceivers = this.connection.getTransceivers();\n        return ORDERED_TRANSCEIVER_TYPES[transceivers.indexOf(transceiver)];\n    }\n}\n\n/**\n * This class represents a network of peers and handles peer to peer connections.\n *\n *  @fires PeerToPeer#update\n */\nexport class PeerToPeer extends EventTarget {\n    /** @type {number} */\n    selfId;\n    /** @type {number}*/\n    channelId;\n    /** @type {Map<number, Peer>}*/\n    peers = new Map();\n    /**\n     * Predicate to check if we accept the offer from a peer, this can be useful if we want to prevent\n     * negotiations for connections that we do not want to manage.\n     *\n     * @param {number} id the id of the peer to check if we accept the offer\n     * @param {number} sequence the sequence of the offer, it indicates the order of the connection\n     */\n    acceptOffer = async (id, sequence) => true;\n    /** @type {number} */\n    _batchDelay = DEFAULT_BUS_BATCH_DELAY;\n    /** @type {Info} */\n    _localInfo = Object.seal({\n        isSelfMuted: false,\n        isRaisingHand: false,\n        isDeaf: false,\n        isTalking: false,\n        isCameraOn: false,\n        isScreenSharingOn: false,\n    });\n    /** @type {String[]} */\n    _iceServers;\n    _isPendingNotify = false;\n    _notificationsToSend = new Map();\n    _isAntiGlareEnabled = true;\n    /**\n     * id of notification transaction\n     * @type {number}\n     */\n    _tmpNotificationId = 0;\n    /**\n     * by peer ID\n     * @type {Map<timeoutID>}\n     */\n    _recoverTimeouts = new Map();\n    /** @type {String} */\n    _notificationRoute;\n    /** @type {boolean} */\n    _isStreamingEnabled = true;\n    /** @type {Object<STREAM_TYPE[keyof STREAM_TYPE], MediaStreamTrack | null>} */\n    _tracks = Object.seal({\n        [STREAM_TYPE.AUDIO]: null,\n        [STREAM_TYPE.SCREEN]: null,\n        [STREAM_TYPE.CAMERA]: null,\n    });\n    _loggingFunctions = {\n        [LOG_LEVEL.DEBUG]: () => {},\n        [LOG_LEVEL.INFO]: () => {},\n        [LOG_LEVEL.WARN]: () => {},\n        [LOG_LEVEL.ERROR]: () => {},\n    };\n    get isActive() {\n        return Boolean(this.selfId !== undefined && this.channelId !== undefined);\n    }\n    /**\n     * @param {object} [options]\n     * @param {String} [options.notificationRoute] the route used to communicate with the odoo server\n     * @param {LOG_LEVEL[keyof LOG_LEVEL]} [options.logLevel=LOG_LEVEL.NONE]\n     * @param {boolean} [options.antiGlare=true] whether or not to use the rollback feature to manage offer collisions,\n     *        ids provided for peers should be comparable for this feature to work.\n     * @param {number} [options.batchDelay=DEFAULT_BUS_BATCH_DELAY]\n     * @param {boolean} [options.enableStreaming=true] whether or not setting the peer connections with audio and video\n     *        transceivers to allow streaming features.\n     */\n    constructor({\n        notificationRoute = DEFAULT_NOTIFICATION_ROUTE,\n        logLevel = LOG_LEVEL.ERROR,\n        batchDelay = DEFAULT_BUS_BATCH_DELAY,\n        antiGlare = true,\n        enableStreaming = true,\n    } = {}) {\n        super();\n        this._isStreamingEnabled = enableStreaming;\n        this._isAntiGlareEnabled = antiGlare;\n        this._notificationRoute = notificationRoute;\n        this._batchDelay = batchDelay;\n        this.setLoggingLevel(logLevel);\n    }\n\n    /**\n     * @param {any} selfId should be comparable to benefit from the anti glare (offer collisions)\n     * @param {any} channelId\n     * @param {object} [options]\n     * @param {Info} [options.info={}]\n     * @param {array} [options.iceServers=DEFAULT_ICE_SERVERS]\n     */\n    connect(selfId, channelId, { info = {}, iceServers = DEFAULT_ICE_SERVERS } = {}) {\n        if (!IS_CLIENT_RTC_COMPATIBLE) {\n            throw new Error(\"RTCPeerConnection is not supported\");\n        }\n        this.selfId = selfId;\n        this.channelId = channelId;\n        this._iceServers = iceServers;\n        this._localInfo = Object.assign(this._localInfo, info);\n    }\n\n    removeALlPeers() {\n        for (const peer of this.peers.values()) {\n            this.removePeer(peer.id);\n        }\n        this.peers.clear();\n    }\n\n    disconnect() {\n        this.removeALlPeers();\n        this.selfId = undefined;\n        this.channelId = undefined;\n        this._isPendingNotify = false;\n        this._notificationsToSend.clear();\n        this._localInfo = Object.assign(this._localInfo, {\n            isSelfMuted: false,\n            isRaisingHand: false,\n            isDeaf: false,\n            isTalking: false,\n            isCameraOn: false,\n            isScreenSharingOn: false,\n        });\n    }\n    /**\n     * Adds a peer and starts the process of connection establishment. From this point the whole\n     * peer lifecycle is handled internally, including connection recovery attempts, until\n     * `removePeer()` or `disconnect()` is called.\n     * If a peer of that id already exists, it is returned without being re-created.\n     * This allows `addPeer` to be called to ensure that all of them are registered without fear\n     * of resetting connections (removePeer() should be called explicitly if that is the intention).\n     *\n     * @param {number} id\n     * @param {object} [options={}] options for the Peer constructor\n     * @returns {Peer} resolved when the dataChannel is open\n     */\n    async addPeer(id, options = {}) {\n        const peer = this.peers.get(id);\n        if (peer) {\n            return peer;\n        }\n        const newPeer = this._createPeer(id, options);\n        await newPeer.ready;\n        return newPeer;\n    }\n    removePeer(id) {\n        const recoverTimeoutId = this._recoverTimeouts.get(id);\n        browser.clearTimeout(recoverTimeoutId);\n        this._recoverTimeouts.delete(id);\n        const peer = this.peers.get(id);\n        if (!peer) {\n            return;\n        }\n        this.peers.delete(id);\n        peer.disconnect();\n    }\n\n    /**\n     * Broadcast a message to all peers\n     * @param message any JSON serializable\n     */\n    broadcast(message) {\n        this._dataChannelBroadcast(INTERNAL_EVENT.BROADCAST, message);\n    }\n    /**\n     * @param id\n     * @return {{\n     *     connectionState: RTCPeerConnection.connectionState\n     *     iceConnectionState: RTCPeerConnection.iceConnectionState\n     *     iceGatheringState: RTCPeerConnection.iceGatheringState\n     *     localCandidateType: RTCIceCandidatePairStats.candidateType\n     *     remoteCandidateType: RTCIceCandidatePairStats.candidateType\n     *     dataChannelState:  RTCDataChannelStats.state\n     *     dtlsState: RTCTransportStats.dtpsState,\n     *     iceState: RTCTransportStats.iceState,\n     *     packetsReceived: RTCTransportStats.packetsReceived,\n     *     packetsSent: RTCTransportStats.packetsSent,\n     * } | {}}\n     */\n    async getFormattedStats(id) {\n        const peer = this.peers.get(id);\n        const formattedStats = {};\n        if (!peer) {\n            return formattedStats;\n        }\n        formattedStats.connectionState = peer.connection.connectionState;\n        formattedStats.iceConnectionState = peer.connection.iceConnectionState;\n        formattedStats.iceGatheringState = peer.connection.iceGatheringState;\n        const stats = await peer.connection.getStats();\n        for (const value of stats?.values() || []) {\n            switch (value.type) {\n                case \"candidate-pair\":\n                    if (value.state === \"succeeded\" && value.localCandidateId) {\n                        formattedStats.localCandidateType =\n                            stats.get(value.localCandidateId)?.candidateType || \"\";\n                        formattedStats.remoteCandidateType =\n                            stats.get(value.remoteCandidateId)?.candidateType || \"\";\n                    }\n                    break;\n                case \"data-channel\":\n                    formattedStats.dataChannelState = value.state;\n                    break;\n                case \"transport\":\n                    formattedStats.dtlsState = value.dtlsState;\n                    formattedStats.iceState = value.iceState;\n                    formattedStats.packetsReceived = value.packetsReceived;\n                    formattedStats.packetsSent = value.packetsSent;\n                    break;\n            }\n        }\n        return formattedStats;\n    }\n    /**\n     * Stop or resume the consumption of tracks from the other call participants.\n     *\n     * @param {number} id\n     * @param {Object<[STREAM_TYPE[keyof STREAM_TYPE], boolean]>} states e.g: { screen: true, camera: false }\n     */\n    updateDownload(id, states) {\n        const peer = this.peers.get(id);\n        if (!peer) {\n            return;\n        }\n        for (const [streamType, accepted] of Object.entries(states)) {\n            peer.medias[streamType].accepted = accepted;\n            const transceiver = peer.getTransceiver(streamType);\n            if (!transceiver) {\n                this._recover(id, `no transceiver available when updating direction`);\n                return;\n            }\n            // changing the direction triggers a negotiation-needed\n            transceiver.direction = peer.getRecommendedTransceiverDirection(\n                streamType,\n                Boolean(this._tracks[streamType])\n            );\n        }\n    }\n\n    /**\n     * @param {STREAM_TYPE[keyof STREAM_TYPE]} streamType\n     * @param {MediaStreamTrack | null} [track] track to be sent to the other call participants\n     */\n    async updateUpload(streamType, track) {\n        this._tracks[streamType] = track || null;\n        this.updateInfo({\n            isScreenSharingOn: Boolean(this._tracks[STREAM_TYPE.SCREEN]),\n            isCameraOn: Boolean(this._tracks[STREAM_TYPE.CAMERA]),\n        });\n        const proms = [];\n        for (const peer of this.peers.values()) {\n            proms.push(peer.ready.then(() => this._updateRemote(peer, streamType)));\n        }\n        await Promise.all(proms);\n    }\n    /**\n     * @param {Info} info\n     */\n    updateInfo(info) {\n        this._localInfo = Object.assign(this._localInfo, info);\n        this._dataChannelBroadcast(INTERNAL_EVENT.INFO, this._localInfo);\n    }\n    /**\n     * @param id id of the peer sending the notification\n     * @param {string} content JSON\n     */\n    async handleNotification(id, content) {\n        /** @type {{ event: INTERNAL_EVENT[keyof INTERNAL_EVENT], channelId, payload: Object }} */\n        const { event, channelId, payload } = JSON.parse(content);\n        this._emitLog(id, `received notification: ${event}`, LOG_LEVEL.DEBUG);\n        if (channelId !== this.channelId) {\n            return;\n        }\n        let peer = this.peers.get(id);\n        if (event !== INTERNAL_EVENT.OFFER && !peer?.connection) {\n            this._emitLog(id, `received ${event} for missing peer ${id}`, LOG_LEVEL.WARN);\n            return;\n        }\n        switch (event) {\n            case INTERNAL_EVENT.ANSWER: {\n                this._emitLog(id, `received answer`, LOG_LEVEL.DEBUG);\n                if (\n                    INVALID_ICE_CONNECTION_STATES.has(peer.connection.iceConnectionState) ||\n                    peer.connection.signalingState === \"stable\" ||\n                    peer.connection.signalingState === \"have-remote-offer\"\n                ) {\n                    return;\n                }\n                const description = new window.RTCSessionDescription(payload.sdp);\n                try {\n                    await peer.connection.setRemoteDescription(description);\n                } catch {\n                    this._recover(id, \"answer handling: Failed at setting remoteDescription\");\n                    // ignored the transaction may have been resolved by another concurrent offer.\n                }\n                break;\n            }\n            case INTERNAL_EVENT.BROADCAST: {\n                this._emitUpdate({\n                    name: UPDATE_EVENT.BROADCAST,\n                    payload: { senderId: id, message: payload },\n                });\n                peer.ready.resolve(true);\n                break;\n            }\n            case INTERNAL_EVENT.DISCONNECT: {\n                this.removePeer(id);\n                this._emitUpdate({ name: UPDATE_EVENT.DISCONNECT, payload: { sessionId: id } });\n                break;\n            }\n            case INTERNAL_EVENT.ICE_CANDIDATE: {\n                if (INVALID_ICE_CONNECTION_STATES.has(peer.connection.iceConnectionState)) {\n                    return;\n                }\n                const rtcIceCandidate = new window.RTCIceCandidate(payload.candidate);\n                try {\n                    await peer.connection.addIceCandidate(rtcIceCandidate);\n                } catch {\n                    this._recover(id, \"failed at adding ice candidate\");\n                }\n                break;\n            }\n            case INTERNAL_EVENT.INFO: {\n                const { isTalking, isCameraOn, isScreenSharingOn } = payload;\n                peer.medias[STREAM_TYPE.AUDIO].active = isTalking;\n                peer.medias[STREAM_TYPE.CAMERA].active = isCameraOn;\n                peer.medias[STREAM_TYPE.SCREEN].active = isScreenSharingOn;\n                this._emitUpdate({\n                    name: UPDATE_EVENT.INFO_CHANGE,\n                    payload: { [id]: payload },\n                });\n                break;\n            }\n            case INTERNAL_EVENT.OFFER: {\n                try {\n                    const accepted = await this.acceptOffer(id, payload.sequence);\n                    if (!accepted) {\n                        this._emitLog(id, \"offer rejected\", LOG_LEVEL.INFO);\n                        return;\n                    }\n                } catch (error) {\n                    this._emitLog(id, `offer rejected: ${error}`, LOG_LEVEL.INFO);\n                }\n                if (!peer) {\n                    peer = this._createPeer(id, { sequence: payload.sequence });\n                }\n                if (\n                    !peer.connection ||\n                    INVALID_ICE_CONNECTION_STATES.has(peer.connection.iceConnectionState) ||\n                    peer.connection.signalingState === \"have-remote-offer\"\n                ) {\n                    return;\n                }\n                const isStable =\n                    peer.connection.signalingState === \"stable\" || peer.isBuildingAnswer;\n                const hasOfferCollision = !isStable || peer.isBuildingOffer;\n                if (hasOfferCollision && peer.hasPriority && this._isAntiGlareEnabled) {\n                    this._emitLog(\n                        peer.id,\n                        `rolling back due to offer collision: ${peer.connection.signalingState}`,\n                        LOG_LEVEL.WARN\n                    );\n                    try {\n                        await peer.connection.setLocalDescription({ type: \"rollback\" });\n                    } catch {\n                        this._recover(id, `failed rollback`);\n                    }\n                }\n                const description = new window.RTCSessionDescription(payload.sdp);\n                try {\n                    await peer.connection.setRemoteDescription(description);\n                } catch {\n                    this._recover(id, \"failed at setting remoteDescription\");\n                    return;\n                }\n                if (!peer.connection) {\n                    this._emitLog(\n                        id,\n                        \"the peer connection was closed during offer negotiation\",\n                        LOG_LEVEL.WARN\n                    );\n                    return;\n                }\n                if (this._isStreamingEnabled) {\n                    if (peer.connection.getTransceivers().length === 0) {\n                        for (const streamType of ORDERED_TRANSCEIVER_TYPES) {\n                            const type = streamType === STREAM_TYPE.AUDIO ? \"audio\" : \"video\";\n                            peer.connection.addTransceiver(type);\n                        }\n                    }\n                    for (const transceiverName of ORDERED_TRANSCEIVER_TYPES) {\n                        await this._updateRemote(peer, transceiverName);\n                    }\n                }\n                peer.isBuildingAnswer = true;\n                try {\n                    await peer.connection.setLocalDescription(await peer.connection.createAnswer());\n                } catch {\n                    peer.isBuildingAnswer = false;\n                    this._recover(id, \"offer handling: failed at setting answer localDescription\");\n                    return;\n                }\n                peer.isBuildingAnswer = false;\n                if (!this.isActive || !this.peers.has(id)) {\n                    return;\n                }\n                this._emitLog(id, `sending answer`, LOG_LEVEL.DEBUG);\n                await this._busNotify(INTERNAL_EVENT.ANSWER, {\n                    payload: {\n                        sdp: peer.connection.localDescription,\n                    },\n                    targets: [peer.id],\n                });\n                this._recover(peer.id, \"standard answer timeout\");\n                break;\n            }\n        }\n    }\n    /**\n     * @param {LOG_LEVEL[keyof LOG_LEVEL]} logLevel\n     */\n    setLoggingLevel(logLevel) {\n        const makeLog = (level) => (id, message) => {\n            this.dispatchEvent(new CustomEvent(\"log\", { detail: { id, level, message } }));\n        };\n        this._loggingFunctions = {\n            [LOG_LEVEL.DEBUG]: () => {},\n            [LOG_LEVEL.INFO]: () => {},\n            [LOG_LEVEL.WARN]: () => {},\n            [LOG_LEVEL.ERROR]: () => {},\n        };\n        switch (logLevel) {\n            case LOG_LEVEL.DEBUG:\n                this._loggingFunctions[LOG_LEVEL.DEBUG] = makeLog(LOG_LEVEL.DEBUG);\n            // eslint-disable-next-line no-fallthrough\n            case LOG_LEVEL.INFO:\n                this._loggingFunctions[LOG_LEVEL.INFO] = makeLog(LOG_LEVEL.INFO);\n            // eslint-disable-next-line no-fallthrough\n            case LOG_LEVEL.WARN:\n                this._loggingFunctions[LOG_LEVEL.WARN] = makeLog(LOG_LEVEL.WARN);\n            // eslint-disable-next-line no-fallthrough\n            case LOG_LEVEL.ERROR:\n                this._loggingFunctions[LOG_LEVEL.ERROR] = makeLog(LOG_LEVEL.ERROR);\n        }\n    }\n    /**\n     * @param {INTERNAL_EVENT[keyof INTERNAL_EVENT]} internalEvent\n     * @param message any JSON serializable\n     */\n    _dataChannelBroadcast(internalEvent, message) {\n        for (const peer of this.peers.values()) {\n            if (!peer?.dataChannel || peer?.dataChannel.readyState !== \"open\") {\n                continue;\n            }\n            peer.dataChannel.send(\n                JSON.stringify({\n                    event: internalEvent,\n                    channelId: this.channelId,\n                    payload: message,\n                })\n            );\n        }\n    }\n    /**\n     * @param {any} detail\n     */\n    _emitUpdate(detail) {\n        this.dispatchEvent(new CustomEvent(\"update\", { detail }));\n    }\n    /**\n     * @param id\n     * @param {string} message\n     * @param {LOG_LEVEL[keyof LOG_LEVEL]} [level=LOG_LEVEL.DEBUG]\n     */\n    _emitLog(id, message, level = LOG_LEVEL.DEBUG) {\n        this._loggingFunctions[level](id, message);\n    }\n    /**\n     * @param id\n     * @param {string} reason\n     */\n    _recover(id, reason = \"\") {\n        this._emitLog(id, `connection recovery candidate: ${reason}`, LOG_LEVEL.WARN);\n        if (this._recoverTimeouts.get(id)) {\n            return;\n        }\n        const peer = this.peers.get(id);\n        if (!peer) {\n            return;\n        }\n        // Retry connecting with an exponential backoff.\n        const delay =\n            Math.min(peer.connectRetryDelay * 1.5, MAXIMUM_RECONNECT_DELAY) + 1000 * Math.random();\n        this._recoverTimeouts.set(\n            id,\n            browser.setTimeout(async () => {\n                const peer = this.peers.get(id);\n                this._recoverTimeouts.delete(id);\n                const connectionSuccess =\n                    peer.connection.connectionState === \"connected\" ||\n                    peer.connection.connectionState === \"completed\";\n                const iceSuccess =\n                    peer.connection.iceConnectionState === \"connected\" ||\n                    peer.connection.iceConnectionState === \"completed\";\n                if (!peer?.connection || !this.channelId || (connectionSuccess && iceSuccess)) {\n                    return;\n                }\n                this._emitUpdate({ name: UPDATE_EVENT.RECOVERY, payload: { id } });\n                this._emitLog(id, `attempting to recover connection: ${reason}`, LOG_LEVEL.ERROR);\n                this._busNotify(INTERNAL_EVENT.DISCONNECT, { targets: [peer.id] });\n                this.removePeer(peer.id);\n                this.addPeer(peer.id, { connectRetryDelay: delay, sequence: peer.sequence });\n            }, delay)\n        );\n    }\n    async _sendNotifications() {\n        if (this._isPendingNotify) {\n            return;\n        }\n        this._isPendingNotify = true;\n        await new Promise((resolve) => setTimeout(resolve, this._batchDelay));\n        if (!this.isActive) {\n            this._isPendingNotify = false;\n            return;\n        }\n        const ids = [];\n        const notifications = [];\n        this._notificationsToSend.forEach((notification, id) => {\n            ids.push(id);\n            notifications.push([\n                notification.sender,\n                notification.targets,\n                JSON.stringify({\n                    event: notification.event,\n                    channelId: notification.channelId,\n                    payload: notification.payload,\n                }),\n            ]);\n        });\n        try {\n            await rpc(\n                this._notificationRoute,\n                {\n                    peer_notifications: notifications,\n                },\n                { silent: true }\n            );\n            for (const id of ids) {\n                this._notificationsToSend.delete(id);\n            }\n        } finally {\n            this._isPendingNotify = false;\n            if (this._notificationsToSend.size > 0) {\n                await this._sendNotifications();\n            }\n        }\n    }\n    /**\n     * @param {INTERNAL_EVENT[keyof INTERNAL_EVENT]} event\n     * @param {Object} [options]\n     * @param {Object} [options.payload]\n     * @param {number[]} [options.targets] list of the ids of peers to send the message to,\n     * sends to all peers if no specified target(s)\n     */\n    async _busNotify(event, { payload, targets } = {}) {\n        targets = targets || Array.from(this.peers.keys());\n        let id;\n        if (event === INTERNAL_EVENT.OFFER) {\n            // offers are always single-target, ensures that only 1 offer (the latest) per target is kept\n            id = `latestOffer_to:${targets[0]}`;\n        } else {\n            id = ++this._tmpNotificationId;\n        }\n        this._notificationsToSend.set(id, {\n            channelId: this.channelId,\n            event,\n            payload,\n            sender: this.selfId,\n            targets,\n        });\n        await this._sendNotifications();\n    }\n    /**\n     * @param {Peer} peer\n     * @param {STREAM_TYPE[keyof STREAM_TYPE]} streamType\n     */\n    async _updateRemote(peer, streamType) {\n        const track = this._tracks[streamType];\n        const transceiver = peer.getTransceiver(streamType);\n        if (!transceiver) {\n            return;\n        }\n        try {\n            await transceiver.sender.replaceTrack(track);\n            transceiver.direction = peer.getRecommendedTransceiverDirection(\n                streamType,\n                Boolean(track)\n            );\n        } catch (error) {\n            this._recover(\n                peer.id,\n                `failed to update ${streamType} transceiver for peer ${peer.id}: ${error}`\n            );\n        }\n    }\n    /**\n     * Creates a new peer.\n     * If a peer of this id already exists, it is cleared.\n     *\n     * @param {number} id\n     * @param {object} [options={}]\n     * @returns {Peer}\n     */\n    _createPeer(id, options = {}) {\n        this.removePeer(id);\n        const peerConnection = new window.RTCPeerConnection({ iceServers: this._iceServers });\n        const dataChannel = peerConnection.createDataChannel(\"notifications\", {\n            negotiated: true,\n            id: 1,\n        });\n        const peer = new Peer(id, {\n            ...options,\n            connection: peerConnection,\n            dataChannel,\n            hasPriority: id > this.selfId,\n        });\n        this._emitUpdate({\n            name: UPDATE_EVENT.CONNECTION_CHANGE,\n            payload: { id, peer, state: \"searching for network\" },\n        });\n        this.peers.set(id, peer);\n        peerConnection.addEventListener(\"icecandidate\", async (event) => {\n            if (!event.candidate) {\n                return;\n            }\n            if (!this.isActive || !this.peers.has(id)) {\n                return;\n            }\n            await this._busNotify(INTERNAL_EVENT.ICE_CANDIDATE, {\n                payload: {\n                    candidate: event.candidate,\n                },\n                targets: [id],\n            });\n        });\n        peerConnection.addEventListener(\"iceconnectionstatechange\", async () => {\n            switch (peerConnection.iceConnectionState) {\n                case \"closed\":\n                    this.removePeer(id);\n                    break;\n                case \"failed\":\n                case \"disconnected\":\n                    this._recover(peer.id, 1000, \"ice connection disconnected\");\n                    break;\n            }\n        });\n        peerConnection.addEventListener(\"icegatheringstatechange\", () => {\n            this._emitLog(\n                id,\n                `gathering state change: ${peerConnection.iceGatheringState}`,\n                LOG_LEVEL.INFO\n            );\n        });\n        peerConnection.addEventListener(\"connectionstatechange\", async () => {\n            this._emitUpdate({\n                name: UPDATE_EVENT.CONNECTION_CHANGE,\n                payload: { id, peer, state: peerConnection.connectionState },\n            });\n            switch (peerConnection.connectionState) {\n                case \"closed\":\n                    this.removePeer(id);\n                    break;\n                case \"failed\":\n                case \"disconnected\":\n                    this._recover(peer.id, 1000, \"connection disconnected\");\n                    break;\n            }\n            this._emitLog(\n                id,\n                `connection state change: ${peerConnection.connectionState}`,\n                LOG_LEVEL.INFO\n            );\n        });\n        peerConnection.addEventListener(\"icecandidateerror\", async (error) => {\n            this._recover(id, `ice candidate error: ${error.errorText}`);\n        });\n        peerConnection.addEventListener(\"negotiationneeded\", async () => {\n            peer.isBuildingOffer = true;\n            try {\n                await peerConnection.setLocalDescription(await peerConnection.createOffer());\n            } catch (error) {\n                this._recover(id, `failed to set local Description for offer: ${error}`);\n                peer.isBuildingOffer = false;\n                return;\n            }\n            peer.isBuildingOffer = false;\n            if (!this.isActive || !this.peers.has(id)) {\n                return;\n            }\n            await this._busNotify(INTERNAL_EVENT.OFFER, {\n                payload: {\n                    sdp: peerConnection.localDescription,\n                    sequence: peer.sequence,\n                },\n                targets: [id],\n            });\n        });\n        peerConnection.addEventListener(\"track\", async ({ transceiver, track }) => {\n            if (!peer?.id || !this.peers.has(peer.id)) {\n                return;\n            }\n            const streamType = peer.getTransceiverStreamType(transceiver);\n            if (!streamType) {\n                this._recover(id, \"received track for unknown transceiver\");\n                return;\n            }\n            peer.medias[streamType].track = track;\n            if (!(await peer.ready)) {\n                return;\n            }\n            this._emitUpdate({\n                name: UPDATE_EVENT.TRACK,\n                payload: {\n                    sessionId: id,\n                    type: streamType,\n                    track,\n                    active: peer.medias[streamType].active,\n                    sequence: peer.sequence,\n                },\n            });\n        });\n        dataChannel.addEventListener(\"message\", async (event) => {\n            await this.handleNotification(id, event.data);\n        });\n        dataChannel.addEventListener(\"open\", () => {\n            if (dataChannel.readyState !== \"open\") {\n                // can be closed by the time the event is emitted\n                return;\n            }\n            dataChannel.send(\n                JSON.stringify({\n                    event: INTERNAL_EVENT.INFO,\n                    channelId: this.channelId,\n                    payload: this._localInfo,\n                })\n            );\n            this.broadcast({ sequence: peer.sequence });\n        });\n        return peer;\n    }\n}\n", "import { Component, useSubEnv } from \"@odoo/owl\";\nimport { CallActionList } from \"@mail/discuss/call/common/call_action_list\";\nimport { useService } from \"@web/core/utils/hooks\";\n\nexport class PipBanner extends Component {\n    static template = \"discuss.pipBanner\";\n    static props = [\"compact?\"];\n    static components = { CallActionList };\n\n    setup() {\n        super.setup();\n        this.rtc = useService(\"discuss.rtc\");\n        useSubEnv({ isDiscussPipBanner: true });\n    }\n\n    onClickClose() {\n        this.rtc.closePip();\n    }\n}\n", "import { registry } from \"@web/core/registry\";\nimport { reactive } from \"@odoo/owl\";\nimport { Meeting } from \"./meeting\";\n\nexport const callPipService = {\n    dependencies: [\"mail.popout\"],\n\n    /**\n     * @param {import(\"@web/env\").OdooEnv} env\n     * @param {import(\"services\").ServiceFactories} services\n     */\n    start(env, services) {\n        const popoutService = services[\"mail.popout\"];\n        const popout = popoutService.createManager(Symbol(\"discuss.native.pip\"));\n        let pipWindow = null;\n        const state = reactive({\n            active: false,\n        });\n        popout.addHooks(\n            () => {},\n            () => {\n                state.active = false;\n                env.services[\"discuss.rtc\"]?.channel?.openChatWindow();\n            }\n        );\n        function closePip() {\n            state.active = false;\n            pipWindow?.close();\n        }\n        /**\n         * @param {Object} [param0] native pip options\n         * @param {Component} [param0.context]\n         */\n        async function openPip({ context }) {\n            const rtc = env.services[\"discuss.rtc\"];\n            if (!rtc?.channel) {\n                return;\n            }\n            state.active = true;\n            const isShadowRoot = context?.root?.el?.getRootNode() instanceof ShadowRoot;\n            pipWindow = await popout.pip(Meeting, {\n                props: { isPip: true },\n                options: { useAlternativeAssets: isShadowRoot },\n            });\n            pipWindow.addEventListener(\"keydown\", (ev) => {\n                rtc.onKeyDown(ev);\n            });\n            pipWindow.addEventListener(\"keyup\", (ev) => {\n                rtc.onKeyUp(ev);\n            });\n            pipWindow.document.body.style.backgroundColor = \"black\";\n            pipWindow.document.body.style.overflow = \"hidden\";\n            pipWindow.document.body.style.display = \"block\";\n        }\n        return reactive({\n            get isNativePipAvailable() {\n                return Boolean(window.documentPictureInPicture);\n            },\n            get pipWindow() {\n                return pipWindow;\n            },\n            state,\n            closePip,\n            openPip,\n        });\n    },\n};\n\nregistry.category(\"services\").add(\"discuss.pip_service\", callPipService);\n", "import { Component, useState } from \"@odoo/owl\";\nimport { browser } from \"@web/core/browser/browser\";\nimport { isMobileOS } from \"@web/core/browser/feature_detection\";\nimport { useService } from \"@web/core/utils/hooks\";\n\nexport class PttAdBanner extends Component {\n    static template = \"discuss.pttAdBanner\";\n    static props = {};\n    static LOCAL_STORAGE_KEY = \"ptt_ad_banner_discarded\";\n\n    setup() {\n        super.setup();\n        this.pttExtService = useService(\"discuss.ptt_extension\");\n        this.store = useService(\"mail.store\");\n        this.state = useState({\n            wasDiscarded: browser.localStorage.getItem(PttAdBanner.LOCAL_STORAGE_KEY),\n        });\n    }\n\n    onClickClose() {\n        browser.localStorage.setItem(PttAdBanner.LOCAL_STORAGE_KEY, true);\n        this.state.wasDiscarded = true;\n    }\n\n    get isVisible() {\n        return (\n            !this.pttExtService.isEnabled &&\n            this.store.settings.use_push_to_talk &&\n            !isMobileOS() &&\n            !this.state.wasDiscarded\n        );\n    }\n}\n", "import { markup, reactive } from \"@odoo/owl\";\n\nimport { parseVersion } from \"@mail/utils/common/misc\";\nimport { browser } from \"@web/core/browser/browser\";\nimport { registry } from \"@web/core/registry\";\nimport { _t } from \"@web/core/l10n/translation\";\n\n/** In object so it's patchable */\nexport const pttExtensionServiceInternal = {\n    onAnswerIsEnabled(pttService) {\n        pttService.isEnabled = true;\n    },\n};\n\nexport const pttExtensionHookService = {\n    start(env) {\n        const INITIAL_RELEASE_TIMEOUT = 750;\n        const COMMON_RELEASE_TIMEOUT = 200;\n        // https://chromewebstore.google.com/detail/discuss-push-to-talk/mdiacebcbkmjjlpclnbcgiepgifcnpmg\n        const EXT_ID = \"mdiacebcbkmjjlpclnbcgiepgifcnpmg\";\n        const versionPromise =\n            window.chrome?.runtime\n                ?.sendMessage(EXT_ID, { type: \"ask-version\" })\n                .catch(() => \"1.0.0.0\") ?? Promise.resolve(\"1.0.0.0\");\n        const self = reactive({\n            isEnabled: undefined,\n            voiceActivated: undefined,\n            notifyIsTalking(isTalking) {\n                sendMessage(\"is-talking\", isTalking);\n            },\n            subscribe() {\n                sendMessage(\"subscribe\");\n            },\n            unsubscribe() {\n                self.voiceActivated = false;\n                sendMessage(\"unsubscribe\");\n            },\n            downloadURL: `https://chromewebstore.google.com/detail/discuss-push-to-talk/${EXT_ID}`,\n            get downloadText() {\n                return _t(\n                    \"The Push-to-Talk feature is only accessible within tab focus. To enable the Push-to-Talk functionality outside of this tab, we recommend downloading our %(anchor_start)sextension%(anchor_end)s.\",\n                    {\n                        anchor_start: markup`<a href=\"${this.downloadURL}\" target=\"_blank\" class=\"text-reset text-decoration-underline\">`,\n                        anchor_end: markup`</a>`,\n                    }\n                );\n            },\n        });\n\n        browser.addEventListener(\"message\", ({ data, origin, source }) => {\n            const rtc = env.services[\"discuss.rtc\"];\n            if (\n                source !== window ||\n                origin !== location.origin ||\n                data.from !== \"discuss-push-to-talk\" ||\n                (!rtc && data.type !== \"answer-is-enabled\")\n            ) {\n                return;\n            }\n            switch (data.type) {\n                case \"push-to-talk-pressed\":\n                    {\n                        self.voiceActivated = false;\n                        const isFirstPress = !rtc.selfSession?.isTalking;\n                        rtc.onPushToTalk();\n                        if (rtc.selfSession?.isTalking) {\n                            // Second key press is slow to come thus, the first timeout\n                            // must be greater than the following ones.\n                            rtc.setPttReleaseTimeout(\n                                isFirstPress ? INITIAL_RELEASE_TIMEOUT : COMMON_RELEASE_TIMEOUT\n                            );\n                        }\n                    }\n                    break;\n                case \"toggle-voice\":\n                    {\n                        if (self.voiceActivated) {\n                            rtc.setPttReleaseTimeout(0);\n                        } else {\n                            rtc.onPushToTalk();\n                        }\n                        self.voiceActivated = !self.voiceActivated;\n                    }\n                    break;\n                case \"answer-is-enabled\":\n                    pttExtensionServiceInternal.onAnswerIsEnabled(self);\n                    break;\n            }\n        });\n\n        /**\n         * Send a message to the PTT extension.\n         *\n         * @param {\"ask-is-enabled\" | \"subscribe\" | \"unsubscribe\" | \"is-talking\"} type\n         * @param {*} value\n         */\n        async function sendMessage(type, value) {\n            if (!self.isEnabled && type !== \"ask-is-enabled\") {\n                return;\n            }\n            const version = parseVersion(await versionPromise);\n            if (location.origin === \"null\") {\n                return;\n            }\n            if (version.isLowerThan(\"1.0.0.2\")) {\n                window.postMessage({ from: \"discuss\", type, value }, location.origin);\n                return;\n            }\n            window.chrome?.runtime?.sendMessage(EXT_ID, { type, value });\n        }\n\n        sendMessage(\"ask-is-enabled\");\n\n        return self;\n    },\n};\n\nregistry.category(\"services\").add(\"discuss.ptt_extension\", pttExtensionHookService);\n", "import { Component } from \"@odoo/owl\";\n\nimport { CallSettingsDialog } from \"@mail/discuss/call/common/call_settings\";\nimport { DeviceSelect } from \"@mail/discuss/call/common/device_select\";\n\nimport { useService } from \"@web/core/utils/hooks\";\nimport { isBrowserSafari } from \"@web/core/browser/feature_detection\";\n\nexport class QuickVideoSettings extends Component {\n    static template = \"discuss.QuickVideoSettings\";\n    static props = [];\n    static components = { DeviceSelect };\n\n    setup() {\n        super.setup();\n        this.store = useService(\"mail.store\");\n        this.dialogService = useService(\"dialog\");\n        this.isBrowserSafari = isBrowserSafari;\n    }\n\n    onClickVideoSettings() {\n        this.dialogService.add(CallSettingsDialog, {});\n    }\n}\n", "import { Component } from \"@odoo/owl\";\n\nimport { CallSettingsDialog } from \"@mail/discuss/call/common/call_settings\";\nimport { DeviceSelect } from \"@mail/discuss/call/common/device_select\";\n\nimport { useService } from \"@web/core/utils/hooks\";\n\nexport class QuickVoiceSettings extends Component {\n    static template = \"discuss.QuickVoiceSettings\";\n    static props = [];\n    static components = { DeviceSelect };\n\n    setup() {\n        super.setup();\n        this.store = useService(\"mail.store\");\n        this.dialogService = useService(\"dialog\");\n    }\n\n    onClickVoiceSettings() {\n        this.dialogService.add(CallSettingsDialog, {});\n    }\n}\n", "import { ResPartner } from \"@mail/core/common/res_partner_model\";\nimport { fields } from \"@mail/model/misc\";\nimport { patch } from \"@web/core/utils/patch\";\n\npatch(ResPartner.prototype, {\n    setup() {\n        super.setup(...arguments);\n        this.currentRtcSession = fields.One(\"discuss.channel.rtc.session\", {\n            inverse: \"partner_id\",\n        });\n    },\n});\n", "import { fields, Record } from \"@mail/core/common/record\";\nimport { BlurManager } from \"@mail/discuss/call/common/blur_manager\";\nimport { CallPermissionDialog } from \"@mail/discuss/call/common/call_permission_dialog\";\nimport { monitorAudio } from \"@mail/utils/common/media_monitoring\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { assignDefined, closeStream, onChange } from \"@mail/utils/common/misc\";\nimport { CallInfiniteMirroringWarning } from \"@mail/discuss/call/common/call_infinite_mirroring_warning\";\n\nimport { reactive, toRaw } from \"@odoo/owl\";\n\nimport { browser } from \"@web/core/browser/browser\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { pick } from \"@web/core/utils/objects\";\nimport { debounce } from \"@web/core/utils/timing\";\nimport { loadBundle, loadJS } from \"@web/core/assets\";\nimport { memoize } from \"@web/core/utils/functions\";\nimport { url } from \"@web/core/utils/urls\";\nimport { isBrowserSafari, isMobileOS } from \"@web/core/browser/feature_detection\";\nimport { CallAction } from \"./call_actions\";\n\nlet sequence = 1;\nconst getSequence = () => sequence++;\n\n/**\n * @typedef {'audio' | 'camera' | 'screen' } streamType\n */\n\n/**\n * @return {Promise<{ SfuClient: import(\"@mail/../lib/odoo_sfu/odoo_sfu\").SfuClient, SFU_CLIENT_STATE: import(\"@mail/../lib/odoo_sfu/odoo_sfu\").SFU_CLIENT_STATE }>}\n */\nconst loadSfuAssets = memoize(async () => await loadBundle(\"mail.assets_odoo_sfu\"));\n\n/**\n *\n * @param {EventTarget} target\n * @param {string} event\n * @param {Function} f event listener callback\n * @return {Function} unsubscribe function\n */\nfunction subscribe(target, event, f) {\n    target.addEventListener(event, f);\n    return () => target.removeEventListener(event, f);\n}\n\nexport const PTT_RELEASE_DURATION = 200;\nconst SW_MESSAGE_TYPE = {\n    POST_RTC_LOGS: \"POST_RTC_LOGS\",\n};\nexport const CONNECTION_TYPES = { P2P: \"p2p\", SERVER: \"server\" };\nconst SCREEN_CONFIG = {\n    width: { max: 1920 },\n    height: { max: 1080 },\n    aspectRatio: 16 / 9,\n    frameRate: {\n        max: 24,\n    },\n};\n\nconst IS_CLIENT_RTC_COMPATIBLE = Boolean(window.RTCPeerConnection && window.MediaStream);\nfunction GET_DEFAULT_ICE_SERVERS() {\n    return [{ urls: [\"stun:stun1.l.google.com:19302\", \"stun:stun2.l.google.com:19302\"] }];\n}\nexport const CROSS_TAB_HOST_MESSAGE = {\n    PING: \"PING\", // signals that the host is still active\n    UPDATE_REMOTE: \"UPDATE_REMOTE\", // sent with updated state of the remote rtc sessions of the call\n    CLOSE: \"CLOSE\", // sent when the host ends the call\n    PIP_CHANGE: \"PIP_CHANGE\", // sent when the host changes the pip mode\n};\nexport const CROSS_TAB_CLIENT_MESSAGE = {\n    INIT: \"INIT\", // sent by a tab to signal its presence and receive a state update\n    REQUEST_ACTION: \"REQUEST_ACTION\", // request that an action be executed by the host (mute, deaf,...)\n    LEAVE: \"LEAVE\", // request the host to leave the call\n    UPDATE_VOLUME: \"UPDATE_VOLUME\", // sent by a tab to signal a volume change\n};\nconst PING_INTERVAL = 30_000;\nconst UNAVAILABLE_AS_REMOTE = _t(\"This action can only be done in the call tab.\");\nconst CALL_FULLSCREEN_ID = Symbol(\"CALL_FULLSCREEN\");\n\n/**\n * @param {Array<RTCIceServer>} iceServers\n * @returns {Boolean}\n */\nfunction hasTurn(iceServers) {\n    return iceServers.some((server) => {\n        let hasTurn = false;\n        if (server.url) {\n            hasTurn = server.url.startsWith(\"turn:\");\n        }\n        if (server.urls) {\n            if (Array.isArray(server.urls)) {\n                hasTurn = server.urls.some((url) => url.startsWith(\"turn:\")) || hasTurn;\n            } else {\n                hasTurn = server.urls.startsWith(\"turn:\") || hasTurn;\n            }\n        }\n        return hasTurn;\n    });\n}\n\n/**\n * Allows to use both peer to peer and SFU connections simultaneously, which makes it possible to\n * establish a connection with other call participants with the SFU when possible, and still handle\n * peer-to-peer for the participants who did not manage to establish a SFU connection.\n */\nexport class Network {\n    /** @type {import(\"@mail/discuss/call/common/peer_to_peer\").PeerToPeer} */\n    p2p;\n    /** @type {import(\"@mail/../lib/odoo_sfu/odoo_sfu\").SfuClient} */\n    sfu;\n    /** @type {[{ name: string, f: EventListener }]} */\n    _listeners = [];\n    /**\n     * @param {import(\"@mail/discuss/call/common/peer_to_peer\").PeerToPeer} p2p\n     * @param {import(\"@mail/../lib/odoo_sfu/odoo_sfu\").SfuClient} [sfu]\n     */\n    constructor(p2p, sfu) {\n        this.p2p = p2p;\n        this.sfu = sfu;\n    }\n\n    getSfuConsumerStats(sessionId) {\n        const consumers = this.sfu?._consumers.get(sessionId);\n        if (!consumers) {\n            return [];\n        }\n        return Object.entries(consumers).map(([type, consumer]) => {\n            let state = \"active\";\n            if (!consumer) {\n                state = \"no consumer\";\n            } else if (consumer.closed) {\n                state = \"closed\";\n            } else if (consumer.paused) {\n                state = \"paused\";\n            } else if (!consumer.track) {\n                state = \"no track\";\n            } else if (!consumer.track.enabled) {\n                state = \"track disabled\";\n            } else if (consumer.track.muted) {\n                state = \"track muted\";\n            }\n            return { type, state };\n        });\n    }\n\n    /**\n     * add a SFU to the network.\n     * @param {import(\"@mail/../lib/odoo_sfu/odoo_sfu\").SfuClient} sfu\n     */\n    addSfu(sfu) {\n        if (this.sfu) {\n            this.sfu.disconnect();\n        }\n        this.sfu = sfu;\n    }\n    removeSfu() {\n        if (!this.sfu) {\n            return;\n        }\n        for (const { name, f } of this._listeners) {\n            this.sfu.removeEventListener(name, f);\n        }\n        this.sfu.disconnect();\n    }\n    /**\n     * @param {string} name\n     * @param {function} f\n     * @override\n     */\n    addEventListener(name, f) {\n        this._listeners.push({ name, f });\n        this.p2p.addEventListener(name, f);\n        this.sfu?.addEventListener(name, f);\n    }\n    /**\n     * @param {streamType} type\n     * @param {MediaStreamTrack | null} track track to be sent to the other call participants,\n     * not setting it will remove the track from the server\n     */\n    async updateUpload(type, track) {\n        const proms = [this.p2p.updateUpload(type, track)];\n        if (this.sfu?.state === \"connected\") {\n            proms.push(this.sfu.updateUpload(type, track));\n        }\n        await Promise.all(proms);\n    }\n    /**\n     * Stop or resume the consumption of tracks from the other call participants.\n     *\n     * @param {number} sessionId\n     * @param {Object<[streamType, boolean]>} states e.g: { audio: true, camera: false }\n     */\n    updateDownload(sessionId, states) {\n        this.p2p.updateDownload(sessionId, states);\n        this.sfu?.updateDownload(sessionId, states);\n    }\n    /**\n     * Updates the server with the info of the session (isTalking, isCameraOn,...) so that it can broadcast it to the\n     * other call participants.\n     *\n     * @param {import(\"#src/models/session.js\").SessionInfo} info\n     * @param {Object} [options] see documentation of respective classes\n     */\n    updateInfo(info, options = {}) {\n        this.p2p.updateInfo(info, options);\n        this.sfu?.updateInfo(info, options);\n    }\n    disconnect() {\n        for (const { name, f } of this._listeners.splice(0)) {\n            this.p2p.removeEventListener(name, f);\n            this.sfu?.removeEventListener(name, f);\n        }\n        this.p2p.disconnect();\n        this.sfu?.disconnect();\n    }\n}\n\nexport class Rtc extends Record {\n    notifications = reactive(new Map());\n    /** @type {Map<string, number>} timeoutId by notificationId for call notifications */\n    timeouts = new Map();\n    /** @type {Map<number, number>} timeoutId by sessionId for download pausing delay */\n    downloadTimeouts = new Map();\n    /** @type {{urls: string[]}[]} */\n    iceServers = fields.Attr(undefined, {\n        compute() {\n            return this.iceServers ? this.iceServers : GET_DEFAULT_ICE_SERVERS();\n        },\n    });\n    /** @type {\"granted\" | \"denied\" | \"prompt\" | undefined} */\n    microphonePermission;\n    /** @type {\"granted\" | \"denied\" | \"prompt\" | undefined} */\n    cameraPermission;\n    /**\n     * The RtcSession of the current user for the call hosted by this tab, this is only set if\n     * the current tab is the cross-tab host (the tab that is maintaining the connections and streams).\n     *\n     * If you want a reference to the RtcSession of the call, regardless of where it is hosted,\n     * as long as it is on the same browser, use `selfSession`.\n     */\n    localSession = fields.One(\"discuss.channel.rtc.session\");\n    /**\n     * The RtcSession shared between tabs, this is set if any of the tabs of that browser is in a call.\n     *\n     * For most use cases, this is the RtcSession you want to use (to ensure cross-tab consistency),\n     * unless you need to access actual connection data (connection stats, streams,...), which can only\n     * be accessed from the tab that is hosting the call.\n     */\n    selfSession = fields.One(\"discuss.channel.rtc.session\", {\n        compute() {\n            return (\n                this.localSession ||\n                this.store[\"discuss.channel.rtc.session\"].get(this._remotelyHostedSessionId)\n            );\n        },\n    });\n    channel = fields.One(\"Thread\", {\n        compute() {\n            if (this.state.channel) {\n                return this.state.channel;\n            }\n            if (this._remotelyHostedChannelId) {\n                return this.store.Thread.insert({\n                    model: \"discuss.channel\",\n                    id: this._remotelyHostedChannelId,\n                });\n            }\n        },\n        onUpdate() {\n            if (!this.channel) {\n                return;\n            }\n            this.store.Thread.getOrFetch({\n                model: \"discuss.channel\",\n                id: this.channel.id,\n            });\n        },\n    });\n    /**\n     * Html element embedding the rtc service. Used to scope the dialog to the correct\n     * document fragment (either the actual document or the active shadow root).\n     * @type {HTMLElement|undefined}\n     */\n    rootEl;\n    serverInfo;\n    /**\n     * @type {Network}\n     */\n    network;\n    /** @type {import(\"@mail/../lib/odoo_sfu/odoo_sfu\").SfuClient} */\n    sfuClient = undefined;\n\n    /** @type {Object<string, boolean>} The keys are action names and the values are booleans indicating whether each action is active */\n    lastActions = {};\n    /** @type {Array<string>} Array of action names representing the stack of currently active actions */\n    actionsStack = [];\n    /** @type {string|undefined} String representing the last call action activated, or undefined if none are */\n    lastSelfCallAction = undefined;\n    /** callbacks to be called when cleaning the state up after a call */\n    cleanups = [];\n    /** @type {number} */\n    sfuTimeout;\n    /** @type {AudioContext} AudioContext used to mix screen and mic audio */\n    audioContext;\n    // cross tab sync\n    _broadcastChannel = new browser.BroadcastChannel(\"call_sync_state\");\n    _remotelyHostedSessionId;\n    _remotelyHostedChannelId;\n    _crossTabTimeoutId;\n    /** @type {number} count of how many times the p2p service attempted a connection recovery */\n    _p2pRecoveryCount = 0;\n    upgradeConnectionDebounce = debounce(\n        () => {\n            this._upgradeConnection();\n        },\n        15000,\n        { leading: true, trailing: false }\n    );\n\n    /**\n     * Whether this tab serves as a remote for a call hosted on another tab.\n     */\n    get isRemote() {\n        return Boolean(this._remotelyHostedChannelId);\n    }\n    /**\n     * Whether the current tab is the host of the call.\n     */\n    get isHost() {\n        return Boolean(this.localSession);\n    }\n\n    callActions = fields.Attr([], {\n        compute() {\n            const transformedActions = registry\n                .category(\"discuss.call/actions\")\n                .getEntries()\n                .map(([id, definition]) => new CallAction({ owner: this, id, definition }));\n            for (const action of transformedActions) {\n                action.setup();\n            }\n            return transformedActions;\n        },\n        onUpdate() {\n            for (const action of this.callActions) {\n                if (action.isActive === this.lastActions[action.id]) {\n                    continue;\n                }\n                if (!action.isTracked) {\n                    continue;\n                }\n                if (action.isActive) {\n                    if (!this.actionsStack.includes(action.id)) {\n                        this.actionsStack.unshift(action.id);\n                    }\n                } else {\n                    this.actionsStack.splice(this.actionsStack.indexOf(action.id), 1);\n                }\n            }\n            this.lastSelfCallAction = this.actionsStack[0];\n            this.lastActions = Object.fromEntries(\n                this.callActions.map((action) => [action.id, action.isActive])\n            );\n        },\n    });\n\n    setup() {\n        this.linkVoiceActivationDebounce = debounce(this.linkVoiceActivation, 500);\n        this.state = reactive({\n            connectionType: undefined,\n            hasPendingRequest: false,\n            channel: undefined,\n            logs: {},\n            sendCamera: false,\n            sendScreen: false,\n            updateAndBroadcastDebounce: undefined,\n            micAudioTrack: undefined,\n            screenAudioTrack: undefined,\n            audioTrack: undefined,\n            cameraTrack: undefined,\n            screenTrack: undefined,\n            /**\n             * callback to properly end the audio monitoring.\n             * If set it indicates that we are currently monitoring the local\n             * micAudioTrack for the voice activation feature.\n             */\n            disconnectAudioMonitor: undefined,\n            pttReleaseTimeout: undefined,\n            sourceCameraStream: null,\n            sourceScreenStream: null,\n            /**\n             * Whether the network fell back to p2p mode in a SFU call.\n             */\n            fallbackMode: false,\n            isPipMode: false,\n            isFullscreen: false,\n        });\n        this.blurManager = undefined;\n    }\n\n    start() {\n        const services = this.store.env.services;\n        this.notification = services.notification;\n        this.overlay = services.overlay;\n        this.dialog = services.dialog;\n        this.soundEffectsService = services[\"mail.sound_effects\"];\n        this.pttExtService = services[\"discuss.ptt_extension\"];\n        if (this._broadcastChannel) {\n            this._broadcastChannel.onmessage = this._onBroadcastChannelMessage.bind(this);\n            this._postToTabs({ type: CROSS_TAB_CLIENT_MESSAGE.INIT });\n        }\n        /**\n         * @type {import(\"@mail/discuss/call/common/peer_to_peer\").PeerToPeer}\n         */\n        this.p2pService = services[\"discuss.p2p\"];\n        onChange(this.store.settings, \"useBlur\", () => {\n            if (this.state.sendCamera) {\n                this.toggleVideo(\"camera\", { force: true });\n            }\n        });\n        onChange(this.store.settings, [\"edgeBlurAmount\", \"backgroundBlurAmount\"], () => {\n            if (this.blurManager) {\n                this.blurManager.edgeBlur = this.store.settings.edgeBlurAmount;\n                this.blurManager.backgroundBlur = this.store.settings.backgroundBlurAmount;\n            }\n        });\n        onChange(this.store.settings, [\"voiceActivationThreshold\", \"use_push_to_talk\"], () => {\n            this.linkVoiceActivationDebounce();\n        });\n        onChange(this.store.settings, \"audioInputDeviceId\", async () => {\n            if (this.localSession) {\n                await this.resetMicAudioTrack({ force: true });\n            }\n        });\n        onChange(this.store.settings, \"audioOutputDeviceId\", async () => {\n            if (this.localSession) {\n                await this.setOutputDevice(this.store.settings.audioOutputDeviceId);\n            }\n        });\n        onChange(this.store.settings, \"cameraInputDeviceId\", async () => {\n            if (this.localSession && this.state.cameraTrack) {\n                await this.toggleVideo(\"camera\", { force: true, refreshStream: true });\n            }\n        });\n        this.store.env.bus.addEventListener(\"RTC-SERVICE:PLAY_MEDIA\", () => {\n            const channel = this.state.channel;\n            if (!channel) {\n                return;\n            }\n            for (const session of channel.rtc_session_ids) {\n                session.playAudio();\n            }\n        });\n        browser.addEventListener(\"blur\", () => this.onBlur());\n        browser.addEventListener(\n            \"keydown\",\n            (ev) => {\n                this.onKeyDown(ev);\n            },\n            { capture: true }\n        );\n        browser.addEventListener(\n            \"keyup\",\n            (ev) => {\n                this.onKeyUp(ev);\n            },\n            { capture: true }\n        );\n\n        browser.addEventListener(\"pagehide\", () => {\n            if (this.state.channel) {\n                const data = JSON.stringify({\n                    params: { channel_id: this.state.channel.id, session_id: this.selfSession.id },\n                });\n                const blob = new Blob([data], { type: \"application/json\" });\n                // using sendBeacon allows sending a post request even when the\n                // browser prevents async requests from firing when the browser\n                // is closed. Alternatives like synchronous XHR are not reliable.\n                browser.navigator.sendBeacon(\"/mail/rtc/channel/leave_call\", blob);\n                this.sfuClient?.disconnect();\n            }\n        });\n        /**\n         * Call all sessions for which no peerConnection is established at\n         * a regular interval to try to recover any connection that failed\n         * to start.\n         *\n         * This is distinct from this.recover which tries to restore\n         * connections that were established but failed or timed out.\n         */\n        browser.setInterval(async () => {\n            if (!this.localSession || !this.state.channel) {\n                return;\n            }\n            this._postToTabs({\n                type: CROSS_TAB_HOST_MESSAGE.PING,\n                hostedSessionId: this.localSession.id,\n            });\n            await this.ping();\n            if (!this.localSession || !this.state.channel) {\n                return;\n            }\n            this.call();\n        }, PING_INTERVAL);\n    }\n\n    get displaySurface() {\n        return this.state.sourceScreenStream?.getVideoTracks()[0]?.getSettings().displaySurface;\n    }\n\n    isPushToTalkRelease(ev) {\n        if (\n            !this.state.channel ||\n            !this.store.settings.use_push_to_talk ||\n            (ev instanceof KeyboardEvent && !this.store.settings.isPushToTalkKey(ev)) ||\n            !this.localSession.isTalking ||\n            this.pttExtService.voiceActivated\n        ) {\n            return false;\n        }\n        return true;\n    }\n\n    onKeyDown(ev) {\n        if (!this.store.settings.isPushToTalkKey(ev)) {\n            return;\n        }\n        this.onPushToTalk();\n    }\n\n    onKeyUp(ev) {\n        if (!this.isPushToTalkRelease(ev)) {\n            return;\n        }\n        this.setPttReleaseTimeout();\n    }\n\n    onBlur() {\n        if (!this.isPushToTalkRelease()) {\n            return;\n        }\n        this.setPttReleaseTimeout();\n    }\n\n    showMirroringWarning() {\n        this.state.screenTrack.enabled = false;\n        const trackEndedFn = () => this.removeMirroringWarning?.();\n        this.removeMirroringWarning = this.overlay.add(\n            CallInfiniteMirroringWarning,\n            {\n                onClose: ({ stopScreensharing } = {}) => {\n                    this.removeMirroringWarning({ stopScreensharing });\n                },\n            },\n            {\n                onRemove: ({ stopScreensharing } = {}) => {\n                    if (stopScreensharing) {\n                        this.toggleVideo(\"screen\", false);\n                    }\n                    this.state.screenTrack?.removeEventListener(\"ended\", trackEndedFn);\n                    this.removeMirroringWarning = null;\n                },\n            }\n        );\n        this.state.screenTrack.addEventListener(\"ended\", trackEndedFn, { once: true });\n    }\n\n    setPttReleaseTimeout(duration = PTT_RELEASE_DURATION) {\n        this.state.pttReleaseTimeout = browser.setTimeout(() => {\n            this.setTalking(false);\n            if (!this.localSession?.isMute) {\n                this.soundEffectsService.play(\"ptt-release\");\n            }\n        }, Math.max(this.store.settings.voice_active_duration || 0, duration));\n    }\n\n    onPushToTalk() {\n        if (\n            !this.state.channel ||\n            this.store.settings.isRegisteringKey ||\n            !this.store.settings.use_push_to_talk\n        ) {\n            return;\n        }\n        browser.clearTimeout(this.state.pttReleaseTimeout);\n        if (!this.localSession.isTalking && !this.localSession.isMute) {\n            this.soundEffectsService.play(\"ptt-press\");\n        }\n        this.setTalking(true);\n    }\n\n    async openPip(options) {\n        if (this.isHost) {\n            this.exitFullscreen();\n            await this.pipService.openPip(options);\n            return;\n        }\n        this.notification.add(UNAVAILABLE_AS_REMOTE, {\n            type: \"warning\",\n        });\n    }\n\n    closePip() {\n        if (this.isHost) {\n            this.pipService.closePip();\n        } else {\n            this._remoteAction({ pip: false });\n        }\n    }\n\n    /**\n     * @param {Object} param0\n     * @param {any} param0.id\n     * @param {string} param0.text\n     * @param {number} [param0.delay]\n     */\n    addCallNotification({ id, text, delay = 3000 }) {\n        if (this.notifications.has(id)) {\n            return;\n        }\n        this.notifications.set(id, { id, text });\n        this.timeouts.set(\n            id,\n            browser.setTimeout(() => {\n                this.notifications.delete(id);\n                this.timeouts.delete(id);\n            }, delay)\n        );\n    }\n\n    /**\n     * @param {any} id\n     */\n    removeCallNotification(id) {\n        browser.clearTimeout(this.timeouts.get(id));\n        this.notifications.delete(id);\n        this.timeouts.delete(id);\n    }\n\n    /**\n     * Notifies the server and does the cleanup of the current call.\n     */\n    async leaveCall(channel = this.state.channel) {\n        this.store.fullscreenChannel = null;\n        this.state.hasPendingRequest = true;\n        await this.rpcLeaveCall(channel);\n        this.endCall(channel);\n        this.state.hasPendingRequest = false;\n    }\n\n    /**\n     * @param {import(\"models\").Thread} [channel]\n     */\n    endCall(channel = this.state.channel) {\n        this._endHost();\n        if (channel.self_member_id) {\n            channel.self_member_id.rtc_inviting_session_id = undefined;\n        }\n        channel.activeRtcSession = undefined;\n        if (channel.eq(this.state.channel)) {\n            this.state.logs.end = new Date().toISOString();\n            this.dumpLogs();\n            this.pttExtService.unsubscribe();\n            this.network?.disconnect();\n            this.clear();\n            this.soundEffectsService.play(\"call-leave\");\n        }\n    }\n\n    async deafen() {\n        if (this.isRemote) {\n            this._remoteAction({ is_deaf: true });\n            return;\n        }\n        await this.setDeaf(true);\n        this.soundEffectsService.play(\"earphone-off\");\n    }\n\n    /**\n     * @param {import(\"models\").RtcSession} session\n     * @param {boolean} active\n     */\n    setRemoteRaiseHand(session, active) {\n        if (Boolean(session.raisingHand) === active) {\n            return;\n        }\n        Object.assign(session, {\n            raisingHand: active ? new Date() : undefined,\n        });\n        const notificationId = \"raise_hand_\" + session.id;\n        if (session.raisingHand) {\n            this.addCallNotification({\n                id: notificationId,\n                text: _t(\"%s raised their hand\", session.name),\n            });\n        } else {\n            this.removeCallNotification(notificationId);\n        }\n    }\n\n    setVolume(session, volume) {\n        session.volume = volume;\n        this.store.settings.saveVolumeSetting({\n            guestId: session?.guest_id?.id,\n            partnerId: session?.partner_id?.id,\n            volume,\n        });\n        this._postToTabs({\n            type: CROSS_TAB_CLIENT_MESSAGE.UPDATE_VOLUME,\n            changes: { sessionId: session.id, volume },\n        });\n    }\n\n    async mute() {\n        if (this.isRemote) {\n            this._remoteAction({ is_muted: true });\n            return;\n        }\n        await this.setMute(true);\n        this.soundEffectsService.play(\"mic-off\");\n    }\n\n    /** @param {Object} props Properties to pass to the meeting component. */\n    async enterFullscreen(props) {\n        const Meeting = registry.category(\"discuss.call/components\").get(\"Meeting\");\n        this.store.fullscreenChannel = this.channel;\n        await this.fullscreen.enter(Meeting, {\n            id: CALL_FULLSCREEN_ID,\n            keepBrowserHeader: true,\n            props,\n            rootId: this.rootEl?.getRootNode()?.host?.id,\n        });\n    }\n\n    async exitFullscreen() {\n        this.store.fullscreenChannel = null;\n        await this.fullscreen.exit(CALL_FULLSCREEN_ID);\n    }\n\n    /**\n     * @param {import(\"models\").Thread} channel\n     * @param {Object} [initialState={}]\n     * @param {boolean} [initialState.audio]\n     * @param {boolean} [initialState.camera]\n     */\n    async toggleCall(channel, { audio = true, camera } = {}) {\n        if (channel.id === this._remotelyHostedChannelId) {\n            this._postToTabs({ type: CROSS_TAB_CLIENT_MESSAGE.LEAVE });\n            this.clear();\n            return;\n        }\n        await Promise.resolve(() =>\n            loadJS(url(\"/mail/static/lib/selfie_segmentation/selfie_segmentation.js\")).catch(\n                () => {}\n            )\n        );\n        if (this.state.hasPendingRequest) {\n            return;\n        }\n        const isActiveCall = channel.eq(this.state.channel);\n        if (this.state.channel) {\n            await this.leaveCall(this.state.channel);\n        }\n        if (!isActiveCall) {\n            const joinCallOpts = { audio, camera };\n            if (this.microphonePermission !== \"granted\") {\n                joinCallOpts.audio = false;\n            }\n            await this.joinCall(channel, joinCallOpts);\n        }\n    }\n\n    async toggleCameraFacingMode() {\n        this.store.settings.cameraFacingMode =\n            this.store.settings.cameraFacingMode === \"user\" ? \"environment\" : \"user\";\n        await this.toggleVideo(\"camera\", { force: true, refreshStream: true });\n    }\n\n    async toggleDeafen() {\n        if (this.selfSession.is_deaf) {\n            await this.undeafen();\n            if (this.selfSession.is_muted) {\n                await this.unmute();\n            }\n        } else {\n            await this.deafen();\n        }\n    }\n\n    async toggleMicrophone() {\n        if (this.selfSession.isMute) {\n            if (this.selfSession.is_muted) {\n                await this.unmute();\n            }\n            if (this.selfSession.is_deaf) {\n                await this.undeafen();\n            }\n        } else {\n            await this.mute();\n        }\n    }\n\n    async undeafen() {\n        if (this.isRemote) {\n            this._remoteAction({ is_deaf: false });\n            return;\n        }\n        await this.setDeaf(false);\n        this.soundEffectsService.play(\"earphone-on\");\n    }\n\n    /** @param {\"microphone\" | \"camera\"} media */\n    showMediaPermissionDialog(media) {\n        this.closeCallPermissionDialog = this.dialog.add(\n            CallPermissionDialog,\n            {\n                media,\n                useMicrophone: () => this.unmute(),\n                useCamera: () => this.toggleVideo(\"camera\", { force: true, refreshStream: true }),\n            },\n            { context: { root: { el: this.rootEl } } }\n        );\n    }\n\n    showMediaUnavailableWarning({ microphone, camera, screen }) {\n        let errorMessage;\n        if (microphone && camera) {\n            errorMessage = _t(\"Camera and microphone access blocked. Enable in browser settings.\");\n        } else if (camera) {\n            errorMessage = _t(\"Camera access blocked. Enable in browser settings.\");\n        } else if (microphone) {\n            errorMessage = _t(\"Microphone access blocked. Enable in browser settings.\");\n        } else if (screen) {\n            errorMessage = _t(\"Screen sharing access blocked. Enable in browser settings.\");\n        }\n        this.notification.add(errorMessage, { type: \"warning\" });\n    }\n\n    async askForBrowserPermission({ audio, video }) {\n        try {\n            const stream = await browser.navigator.mediaDevices.getUserMedia({\n                audio: audio ? this.store.settings.audioConstraints : false,\n                video: video ? this.store.settings.cameraConstraints : false,\n            });\n            if (isBrowserSafari() || isMobileOS()) {\n                if (audio) {\n                    this.microphonePermission = \"granted\";\n                }\n                if (video) {\n                    this.cameraPermission = \"granted\";\n                }\n            }\n            closeStream(stream);\n        } catch {\n            this.showMediaUnavailableWarning({ microphone: audio, camera: video });\n        }\n        if (audio && video) {\n            return this.microphonePermission === \"granted\" && this.cameraPermission === \"granted\";\n        }\n        return audio\n            ? this.microphonePermission === \"granted\"\n            : this.cameraPermission === \"granted\";\n    }\n\n    async unmute() {\n        if (this.isRemote) {\n            this._remoteAction({ is_muted: false });\n            return;\n        }\n        if (this.microphonePermission === \"prompt\") {\n            this.showMediaPermissionDialog(\"microphone\");\n            return;\n        }\n        if (this.state.micAudioTrack) {\n            await this.setMute(false);\n        } else {\n            await this.resetMicAudioTrack({ force: true });\n        }\n        this.soundEffectsService.play(\"mic-on\");\n    }\n\n    //----------------------------------------------------------------------\n    // Private\n    //----------------------------------------------------------------------\n\n    async _loadSfu() {\n        const load = async () => {\n            await loadSfuAssets();\n            const sfuModule = odoo.loader.modules.get(\"@mail/../lib/odoo_sfu/odoo_sfu\");\n            this.SFU_CLIENT_STATE = sfuModule.SFU_CLIENT_STATE;\n            this.sfuClient = new sfuModule.SfuClient();\n        };\n        try {\n            await load();\n        } catch {\n            // trying again with a delay in case of race condition with the asset loading.\n            await new Promise((resolve, reject) => {\n                browser.setTimeout(async () => {\n                    try {\n                        await load();\n                    } catch (error) {\n                        reject(error);\n                    }\n                    resolve();\n                }, 1000);\n            });\n        }\n    }\n\n    updateUpload() {\n        this.network?.updateUpload(\"audio\", this.state.audioTrack);\n        this.network?.updateUpload(\"camera\", this.state.cameraTrack);\n        this.network?.updateUpload(\"screen\", this.state.screenTrack);\n    }\n\n    async _initConnection() {\n        this.localSession.connectionState = \"selecting network type\";\n        this.state.connectionType = CONNECTION_TYPES.P2P;\n        this.network?.disconnect();\n        // loading p2p in any case as we may need to receive peer-to-peer connections from users who failed to connect to the SFU.\n        this.p2pService.connect(this.localSession.id, this.state.channel.id, {\n            info: this.formatInfo(),\n            iceServers: this.iceServers,\n        });\n        this.network = new Network(this.p2pService);\n        this.updateUpload();\n        if (this.serverInfo) {\n            this.log(this.localSession, \"loading sfu server\", {\n                step: \"loading sfu server\",\n                serverInfo: toRaw(this.serverInfo),\n            });\n            this.localSession.connectionState = \"loading SFU assets\";\n            try {\n                await this._loadSfu();\n                this.state.connectionType = CONNECTION_TYPES.SERVER;\n                if (this.network) {\n                    this.network.addSfu(this.sfuClient);\n                } else {\n                    return; // the call may be ended by the time the sfu is loaded\n                }\n            } catch (e) {\n                this.state.fallbackMode = true;\n                this.notification.add(\n                    _t(\"Failed to load the SFU server, falling back to peer-to-peer\"),\n                    {\n                        type: \"warning\",\n                    }\n                );\n                this.log(this.localSession, \"failed to load sfu server\", {\n                    error: e,\n                    important: true,\n                });\n            }\n            this.selfSession.connectionState = \"initializing\";\n        } else {\n            this.log(this.localSession, \"no sfu server info, using peer-to-peer\");\n        }\n        this.network.addEventListener(\"stateChange\", this._handleSfuClientStateChange);\n        this.network.addEventListener(\"update\", this._handleNetworkUpdates);\n        this.network.addEventListener(\"log\", ({ detail: { id, level, message } }) => {\n            const session = this.store[\"discuss.channel.rtc.session\"].get(id);\n            if (session) {\n                this.log(session, message, { step: \"p2p\", level, important: true });\n            }\n        });\n        if (this.state.channel) {\n            await this.call();\n            this.updateUpload();\n        }\n    }\n\n    /**\n     * Send an action to the host tab of the call\n     *\n     * @param {Object} changes\n     */\n    _remoteAction(changes) {\n        this._postToTabs({\n            type: CROSS_TAB_CLIENT_MESSAGE.REQUEST_ACTION,\n            changes,\n        });\n    }\n\n    _updateInfo() {\n        if (!this.isHost) {\n            return;\n        }\n        const info = toRaw(this.formatInfo());\n        this.network?.updateInfo(info);\n        this._updateRemoteTabs({ [this.localSession.id]: info });\n    }\n\n    _host() {\n        this._remotelyHostedChannelId = undefined;\n        this._remotelyHostedSessionId = this.localSession.id;\n        this._updateRemoteTabs({ [this.localSession.id]: toRaw(this.formatInfo()) });\n    }\n    _endHost() {\n        this._postToTabs({\n            type: CROSS_TAB_HOST_MESSAGE.CLOSE,\n            hostedSessionId: this._remotelyHostedSessionId,\n        });\n    }\n\n    _updateRemoteTabs(changes) {\n        this._postToTabs({\n            type: CROSS_TAB_HOST_MESSAGE.UPDATE_REMOTE,\n            hostedChannelId: this.state.channel.id,\n            hostedSessionId: this.localSession.id,\n            changes,\n        });\n    }\n\n    _postToTabs(message) {\n        if (!this._broadcastChannel) {\n            this.log(this.selfSession, \"broadcast channel not available\");\n            return;\n        }\n        try {\n            this._broadcastChannel.postMessage(message);\n        } catch (error) {\n            this.log(this.selfSession, \"failed to post message to broadcast channel\", { error });\n        }\n    }\n\n    _refreshCrossTabTimeout() {\n        browser.clearTimeout(this._crossTabTimeoutId);\n        this._crossTabTimeoutId = browser.setTimeout(() => {\n            this.clear();\n        }, PING_INTERVAL + 10_000);\n    }\n\n    async _onBroadcastChannelMessage({\n        data: { type, hostedChannelId, hostedSessionId, changes },\n    }) {\n        switch (type) {\n            case CROSS_TAB_HOST_MESSAGE.UPDATE_REMOTE:\n                if (this.isHost) {\n                    return;\n                }\n                this._remotelyHostedSessionId = hostedSessionId;\n                this._remotelyHostedChannelId = hostedChannelId;\n                this._refreshCrossTabTimeout();\n                this.updateSessionInfo(changes);\n                return;\n            case CROSS_TAB_HOST_MESSAGE.CLOSE: {\n                if (this._remotelyHostedSessionId !== hostedSessionId) {\n                    return;\n                }\n                this.clear();\n                return;\n            }\n            case CROSS_TAB_HOST_MESSAGE.PIP_CHANGE: {\n                if (this.isHost) {\n                    return;\n                }\n                this.state.isPipMode = changes.isPipMode;\n                return;\n            }\n            case CROSS_TAB_HOST_MESSAGE.PING: {\n                this._refreshCrossTabTimeout();\n                return;\n            }\n            case CROSS_TAB_CLIENT_MESSAGE.INIT: {\n                if (!this.isHost) {\n                    return;\n                }\n                this._updateRemoteTabs({ [this.localSession.id]: toRaw(this.formatInfo()) });\n                this._postToTabs({\n                    type: CROSS_TAB_HOST_MESSAGE.PIP_CHANGE,\n                    changes: { isPipMode: this.state.isPipMode },\n                });\n                return;\n            }\n            case CROSS_TAB_CLIENT_MESSAGE.REQUEST_ACTION: {\n                if (!this.isHost) {\n                    return;\n                }\n                await this._localAction(changes);\n                this._updateRemoteTabs({ [this.localSession.id]: toRaw(this.formatInfo()) });\n                return;\n            }\n            case CROSS_TAB_CLIENT_MESSAGE.LEAVE: {\n                if (!this.isHost) {\n                    return;\n                }\n                await this.leaveCall(this.channel);\n                return;\n            }\n            case CROSS_TAB_CLIENT_MESSAGE.UPDATE_VOLUME: {\n                const session = this.store[\"discuss.channel.rtc.session\"].get(changes.sessionId);\n                if (!session) {\n                    return;\n                }\n                session.volume = changes.volume;\n                return;\n            }\n        }\n    }\n\n    async _localAction(actions = {}) {\n        const promises = [];\n        for (const [key, value] of Object.entries(actions)) {\n            switch (key) {\n                case \"is_muted\":\n                    if (value === this.localSession.is_muted) {\n                        break;\n                    }\n                    promises.push(value ? this.mute() : this.unmute());\n                    break;\n                case \"is_deaf\":\n                    if (value === this.localSession.is_deaf) {\n                        break;\n                    }\n                    value ? promises.push(this.deafen()) : promises.push(this.undeafen());\n                    break;\n                case \"raisingHand\":\n                    if (value === Boolean(this.localSession.raisingHand)) {\n                        break;\n                    }\n                    promises.push(this.raiseHand(value));\n                    break;\n                case \"pip\":\n                    if (value === this.state.isPipMode) {\n                        break;\n                    }\n                    if (value) {\n                        promises.push(this.openPip());\n                    } else {\n                        this.closePip();\n                    }\n                    break;\n            }\n        }\n        await Promise.all(promises);\n    }\n\n    /**\n     * @param {import(\"models\").RtcSession} session\n     * @param {String} entry\n     * @param {Object} [param2]\n     * @param {Error} [param2.error]\n     * @param {String} [param2.step] current step of the flow\n     * @param {String} [param2.state] current state of the connection\n     * @param {Boolean} [param2.important] if the log is important and should be kept even if logRtc is disabled\n     */\n    log(session, entry, param2 = {}) {\n        if (!session) {\n            return;\n        }\n        const { error, step, state, important, ...data } = param2;\n        session.logStep = entry;\n        if (!this.store.settings.logRtc && !important) {\n            return;\n        }\n        console.debug(\n            `%c${new Date().toLocaleString()} - [${entry}]`,\n            \"color: #e36f17; font-weight: bold;\",\n            toRaw(session)._raw,\n            param2\n        );\n        if (!this.state.logs) {\n            return;\n        }\n        let sessionEntry = this.state.logs.entriesBySessionId[session.id];\n        if (!sessionEntry) {\n            this.state.logs.entriesBySessionId[session.id] = sessionEntry = {\n                step: \"\",\n                state: \"\",\n                logs: [],\n            };\n        }\n        if (step) {\n            sessionEntry.step = step;\n        }\n        if (state) {\n            sessionEntry.state = state;\n        }\n        sessionEntry.logs.push({\n            event: `${new Date().toISOString()}: ${entry}`,\n            error: error && {\n                name: error.name,\n                message: error.message,\n                stack: error.stack && error.stack.split(\"\\n\"),\n            },\n            ...data,\n        });\n    }\n\n    /**\n     * @param {CustomEvent} param0\n     * @param {Object} param0.detail\n     * @param {String} param0.detail.name\n     * @param {any} param0.detail.payload\n     */\n    async _handleNetworkUpdates({ detail: { name, payload } }) {\n        if (!this.state.channel) {\n            return;\n        }\n        switch (name) {\n            case \"broadcast\":\n                {\n                    const {\n                        senderId,\n                        message: { sequence },\n                    } = payload;\n                    if (!sequence) {\n                        return;\n                    }\n                    const session = await this.store[\"discuss.channel.rtc.session\"].getWhenReady(\n                        senderId\n                    );\n                    if (!session) {\n                        return;\n                    }\n                    if (!session.sequence || session.sequence < sequence) {\n                        session.sequence = sequence;\n                    }\n                }\n                return;\n            case \"connection_change\":\n                {\n                    const { id, state } = payload;\n                    const session = this.store[\"discuss.channel.rtc.session\"].get(id);\n                    if (!session) {\n                        return;\n                    }\n                    session.connectionState = state;\n                }\n                return;\n            case \"disconnect\":\n                {\n                    const { sessionId } = payload;\n                    const session = this.store[\"discuss.channel.rtc.session\"].get(sessionId);\n                    if (!session) {\n                        return;\n                    }\n                    this.disconnect(session);\n                }\n                return;\n            case \"info_change\":\n                this.updateSessionInfo(payload);\n                return;\n            case \"track\":\n                {\n                    const { sessionId, type, track, active, sequence } = payload;\n                    const session = await this.store[\"discuss.channel.rtc.session\"].getWhenReady(\n                        sessionId\n                    );\n                    if (!session || !this.state.channel) {\n                        this.log(\n                            this.selfSession,\n                            `track received for unknown session ${sessionId} (${this.state.connectionType})`\n                        );\n                        return;\n                    }\n                    if (sequence && sequence < session.sequence) {\n                        this.log(\n                            session,\n                            `track received for old sequence ${sequence} (${this.state.connectionType})`\n                        );\n                        return;\n                    }\n                    this.log(session, `${type} track received (${this.state.connectionType})`);\n                    try {\n                        await this.handleRemoteTrack({ session, track, type, active });\n                    } catch {\n                        // ignored, the session may be closing.\n                        // this can happen when you join a call from another tab in which you have another session.\n                    }\n                    // makes sure we are not downloading a video that is not displayed\n                    setTimeout(() => {\n                        this.updateVideoDownload(session);\n                    }, 2000);\n                }\n                return;\n            case \"recovery\": {\n                const { id } = payload;\n                const session = this.store[\"discuss.channel.rtc.session\"].get(id);\n                if (\n                    this.selfSession?.persona.main_user_id?.share !== false ||\n                    this.serverInfo ||\n                    this.state.fallbackMode ||\n                    !session?.channel.eq(this.state.channel)\n                ) {\n                    return;\n                }\n                this._p2pRecoveryCount++;\n                if (this._p2pRecoveryCount > 1 || !hasTurn(this.iceServers)) {\n                    this.upgradeConnectionDebounce();\n                }\n            }\n        }\n    }\n\n    async _handleSfuClientStateChange({ detail: { state, cause } }) {\n        this.log(this.localSession, `connection state change: ${state}`, { state, cause });\n        this.localSession.connectionState = state;\n        switch (state) {\n            case this.SFU_CLIENT_STATE.AUTHENTICATED:\n                // if we are hot-swapping connection type, we clear the p2p as late as possible\n                this.p2pService.removeALlPeers();\n                this.sfuClient.broadcast({ sequence: getSequence() });\n                break;\n            case this.SFU_CLIENT_STATE.CONNECTED:\n                browser.clearTimeout(this.sfuTimeout);\n                this.sfuClient.updateInfo(this.formatInfo(), {\n                    needRefresh: true, // asks the server to send the info from all the channel\n                });\n                this.sfuClient.updateUpload(\"audio\", this.state.audioTrack);\n                this.sfuClient.updateUpload(\"camera\", this.state.cameraTrack);\n                this.sfuClient.updateUpload(\"screen\", this.state.screenTrack);\n                return;\n            case this.SFU_CLIENT_STATE.CLOSED:\n                {\n                    if (!this.state.channel) {\n                        return;\n                    }\n                    let text;\n                    if (cause === \"full\") {\n                        text = _t(\"Channel full\");\n                        this.leaveCall();\n                    } else {\n                        text = _t(\n                            \"Connection to SFU server closed by the server, falling back to peer-to-peer\"\n                        );\n                        this.log(this.localSession, text, { important: true });\n                        this._downgradeConnection();\n                    }\n                    this.notification.add(text, {\n                        type: \"warning\",\n                    });\n                }\n                return;\n        }\n    }\n\n    async _upgradeConnection() {\n        const channelId = this.state.channel?.id;\n        if (this.serverInfo || this.state.fallbackMode || !channelId) {\n            return;\n        }\n        await rpc(\n            \"/mail/rtc/channel/upgrade_connection\",\n            { channel_id: channelId },\n            { silent: true }\n        );\n    }\n\n    updateSessionInfo(payload) {\n        if (!payload) {\n            return;\n        }\n        if (this.isHost) {\n            this._updateRemoteTabs(payload);\n        }\n        for (const [id, info] of Object.entries(payload)) {\n            (async () => {\n                const session = await this.store[\"discuss.channel.rtc.session\"].getWhenReady(\n                    Number(id)\n                );\n                if (!session || session.eq(this.localSession) || !this.channel) {\n                    return;\n                }\n                // `isRaisingHand` is turned into the Date `raisingHand`\n                this.setRemoteRaiseHand(session, info.isRaisingHand);\n                delete info.isRaisingHand;\n                assignDefined(session, {\n                    is_muted: info.isSelfMuted ?? info.is_muted,\n                    is_deaf: info.isDeaf ?? info.is_deaf,\n                    isTalking: info.isTalking,\n                    is_camera_on: info.isCameraOn ?? info.is_camera_on,\n                    is_screen_sharing_on: info.isScreenSharingOn ?? info.is_screen_sharing_on,\n                });\n            })();\n        }\n    }\n\n    async _downgradeConnection() {\n        this.serverInfo = undefined;\n        this.state.fallbackMode = true;\n        this.state.connectionType = CONNECTION_TYPES.P2P;\n        this.network.removeSfu();\n        await this.call();\n        this.updateUpload();\n    }\n\n    /**\n     *\n     * @param {Object} [param0={}]\n     * @param {boolean} [param0.asFallback=false] whether the call is made as a fallback to the SFU, in which case\n     * p2p connections are offered more eagerly as other participants may not offer them if their primary connection\n     * type is SFU.\n     * @return {Promise<void>}\n     */\n    async call({ asFallback = false } = {}) {\n        if (asFallback && !this.state.fallbackMode) {\n            return;\n        }\n        if (this.state.connectionType === CONNECTION_TYPES.SERVER) {\n            if (this.sfuClient.state === this.SFU_CLIENT_STATE.DISCONNECTED) {\n                browser.clearTimeout(this.sfuTimeout);\n                this.sfuTimeout = browser.setTimeout(() => {\n                    this.log(this.selfSession, \"sfu connection timeout\", { important: true });\n                    this._downgradeConnection();\n                }, 10000);\n                await this.sfuClient.connect(this.serverInfo.url, this.serverInfo.jsonWebToken, {\n                    channelUUID: this.serverInfo.channelUUID,\n                    iceServers: this.iceServers,\n                });\n            }\n            return;\n        }\n        if (this.state.channel.rtc_session_ids.length === 0) {\n            return;\n        }\n        const sequence = getSequence();\n        for (const session of this.state.channel.rtc_session_ids) {\n            if (session.eq(this.localSession)) {\n                continue;\n            }\n            this.p2pService.addPeer(session.id, { sequence });\n        }\n    }\n\n    /**\n     * @param {import(\"models\").RtcSession} session\n     * @param {MediaStreamTrack} track\n     * @param {streamType} type\n     * @param {boolean} active false if the track is muted/disabled\n     */\n    async handleRemoteTrack({ session, track, type, active = true }) {\n        session.updateStreamState(type, active);\n        await this.updateStream(session, track, {\n            mute: this.localSession.is_deaf,\n            videoType: type,\n        });\n        this.updateActiveSession(session, type, { addVideo: true });\n    }\n\n    /**\n     * @param {import(\"models\").Thread} channel\n     * @param {object} [initialState]\n     * @param {boolean} [initialState.audio] whether to request and use the user audio input (microphone) at start\n     * @param {boolean} [initialState.camera] whether to request and use the user video input (camera) at start\n     */\n    async joinCall(channel, { audio = true, camera = false } = {}) {\n        if (!IS_CLIENT_RTC_COMPATIBLE) {\n            this.notification.add(_t(\"Your browser does not support webRTC.\"), { type: \"warning\" });\n            return;\n        }\n        this.pttExtService.subscribe();\n        this.state.hasPendingRequest = true;\n        const data = await rpc(\n            \"/mail/rtc/channel/join_call\",\n            {\n                camera,\n                channel_id: channel.id,\n                check_rtc_session_ids: channel.rtc_session_ids.map((session) => session.id),\n            },\n            { silent: true }\n        );\n        this.state.hasPendingRequest = false;\n        // Initializing a new session implies closing the current session.\n        this.clear();\n        this.state.channel = channel;\n        this.store.insert(data);\n        this.newLogs();\n        this.state.updateAndBroadcastDebounce = debounce(\n            async () => {\n                if (!this.localSession) {\n                    return;\n                }\n                await rpc(\n                    \"/mail/rtc/session/update_and_broadcast\",\n                    {\n                        session_id: this.localSession.id,\n                        values: pick(\n                            this.localSession,\n                            \"is_camera_on\",\n                            \"is_deaf\",\n                            \"is_muted\",\n                            \"is_screen_sharing_on\"\n                        ),\n                    },\n                    { silent: true }\n                );\n            },\n            3000,\n            { leading: true, trailing: true }\n        );\n        if (this.state.channel.self_member_id) {\n            this.state.channel.self_member_id.rtc_inviting_session_id = undefined;\n        }\n        if (camera) {\n            await this.toggleVideo(\"camera\");\n        }\n        if (!this.selfSession) {\n            return;\n        }\n        await this._initConnection();\n        await this.resetMicAudioTrack({ force: audio });\n        if (!this.state.channel?.id) {\n            return;\n        }\n        this.soundEffectsService.play(\"call-join\");\n        this._host();\n        this.cleanups.push(\n            // only register the beforeunload event if there is a call as FireFox will not place\n            // the pages with beforeunload listeners in the bfcache.\n            subscribe(browser, \"beforeunload\", (event) => {\n                event.preventDefault();\n            })\n        );\n        this.channel?.focusAvailableVideo();\n    }\n\n    newLogs() {\n        this.state.logs = {\n            channelId: this.state.channel.id,\n            selfSessionId: this.localSession.id,\n            start: new Date().toISOString(),\n            hasTurn: hasTurn(this.iceServers),\n            entriesBySessionId: {},\n        };\n    }\n\n    /**\n     * @param {Object} [param0={}]\n     * @param  {boolean} [param0.download=false] true if we want to download the logs\n     */\n    dumpLogs({ download = false } = {}) {\n        const logs = [];\n        if (this.state.logs) {\n            logs.push({\n                type: \"timeline\",\n                entry: this.state.logs.start,\n                value: toRaw(this.state.logs),\n            });\n        }\n        if (this.state.channel) {\n            logs.push(this.buildSnapshot());\n        }\n        if (logs.length || download) {\n            browser.navigator.serviceWorker?.controller?.postMessage({\n                name: SW_MESSAGE_TYPE.POST_RTC_LOGS,\n                logs,\n                download,\n            });\n        }\n    }\n\n    buildSnapshot() {\n        const server = {};\n        if (this.state.connectionType === CONNECTION_TYPES.SERVER) {\n            server.info = toRaw(this.serverInfo);\n            server.state = this.sfuClient?.state;\n            server.errors = this.sfuClient?.errors.map((error) => error.message);\n        }\n        const sessions = this.state.channel.rtc_session_ids.map((session) => {\n            const sessionInfo = {\n                id: session.id,\n                channelMemberId: session.channel_member_id?.id,\n                state: session.connectionState,\n                audioError: session.audioError,\n                videoError: session.videoError,\n                sfuConsumers: this.network?.getSfuConsumerStats(session.id),\n            };\n            if (session.eq(this.selfSession)) {\n                sessionInfo.isSelf = true;\n            }\n            const audioEl = session.audioElement;\n            if (audioEl) {\n                sessionInfo.audio = {\n                    state: audioEl.readyState,\n                    muted: audioEl.muted,\n                    paused: audioEl.paused,\n                    networkState: audioEl.networkState,\n                };\n            }\n            const peer = this.p2pService?.peers.get(session.id);\n            if (peer) {\n                sessionInfo.peer = {\n                    id: peer.id,\n                    state: peer.connection.connectionState,\n                    iceState: peer.connection.iceConnectionState,\n                };\n            }\n            return sessionInfo;\n        });\n        return {\n            type: \"snapshot\",\n            entry: new Date().toISOString(),\n            value: {\n                server,\n                sessions,\n                connectionType: this.state.connectionType,\n                fallback: this.state.fallbackMode,\n            },\n        };\n    }\n\n    logSnapshot() {\n        if (!this.state.channel) {\n            // a snapshot out of a call would not collect any data\n            return;\n        }\n        browser.navigator.serviceWorker?.controller?.postMessage({\n            name: SW_MESSAGE_TYPE.POST_RTC_LOGS,\n            logs: [this.buildSnapshot()],\n        });\n    }\n\n    async rpcLeaveCall(channel) {\n        await rpc(\n            \"/mail/rtc/channel/leave_call\",\n            {\n                channel_id: channel.id,\n            },\n            { silent: true }\n        );\n    }\n\n    async ping() {\n        const data = await rpc(\n            \"/discuss/channel/ping\",\n            {\n                channel_id: this.state.channel.id,\n                check_rtc_session_ids: this.state.channel.rtc_session_ids.map(\n                    (session) => session.id\n                ),\n                rtc_session_id: this.localSession.id,\n            },\n            { silent: true }\n        );\n        this.store.insert(data);\n    }\n\n    disconnect(session) {\n        const downloadTimeout = this.downloadTimeouts.get(session.id);\n        if (downloadTimeout) {\n            clearTimeout(downloadTimeout);\n            this.downloadTimeouts.delete(session.id);\n        }\n        this.removeCallNotification(\"raise_hand_\" + session.id);\n        session.raisingHand = undefined;\n        session.logStep = undefined;\n        session.audioError = undefined;\n        session.videoError = undefined;\n        session.connectionState = undefined;\n        session.isTalking = false;\n        session.mainVideoStreamType = undefined;\n        this.removeAudioFromSession(session);\n        this.removeVideoFromSession(session);\n        this.p2pService?.removePeer(session.id);\n        this.log(session, \"peer removed\", { step: \"peer removed\" });\n    }\n\n    clear() {\n        if (this.state.channel) {\n            for (const session of this.state.channel.rtc_session_ids) {\n                this.removeAudioFromSession(session);\n                this.removeVideoFromSession(session);\n                session.isTalking = false;\n            }\n        }\n        this.exitFullscreen();\n        this._remotelyHostedSessionId = undefined;\n        this._remotelyHostedChannelId = undefined;\n        browser.clearTimeout(this._crossTabTimeoutId);\n        this.cleanups.splice(0).forEach((cleanup) => cleanup());\n        browser.clearTimeout(this.sfuTimeout);\n        this.sfuClient = undefined;\n        this.network = undefined;\n        this.audioContext?.close();\n        this.audioContext = undefined;\n        this._p2pRecoveryCount = 0;\n        this.closeCallPermissionDialog?.();\n        this.state.updateAndBroadcastDebounce?.cancel();\n        this.state.disconnectAudioMonitor?.();\n        this.state.micAudioTrack?.stop();\n        this.state.screenAudioTrack?.stop();\n        this.state.audioTrack?.stop();\n        this.state.cameraTrack?.stop();\n        this.state.screenTrack?.stop();\n        this.state.fallbackMode = undefined;\n        this.state.isPipMode = false;\n        closeStream(this.state.sourceCameraStream);\n        this.state.sourceCameraStream = null;\n        closeStream(this.state.sourceScreenStream);\n        this.state.sourceScreenStream = null;\n        if (this.blurManager) {\n            this.blurManager.close();\n            this.blurManager = undefined;\n        }\n        this.update({\n            localSession: undefined,\n            serverInfo: undefined,\n        });\n        Object.assign(this.state, {\n            updateAndBroadcastDebounce: undefined,\n            connectionType: undefined,\n            disconnectAudioMonitor: undefined,\n            cameraTrack: undefined,\n            screenTrack: undefined,\n            screenAudioTrack: undefined,\n            micAudioTrack: undefined,\n            audioTrack: undefined,\n            sendCamera: false,\n            sendScreen: false,\n            channel: undefined,\n            fallbackMode: false,\n        });\n        this.pipService?.closePip();\n    }\n\n    /**\n     * @param {Boolean} is_deaf\n     */\n    async setDeaf(is_deaf) {\n        this.updateAndBroadcast({ is_deaf });\n        for (const session of this.state.channel.rtc_session_ids) {\n            if (!session.audioElement) {\n                continue;\n            }\n            session.audioElement.muted = is_deaf;\n        }\n        await this.refreshMicAudioStatus();\n    }\n\n    async setOutputDevice(deviceId) {\n        const promises = [];\n        for (const session of this.state.channel.rtc_session_ids) {\n            if (!session.audioElement) {\n                continue;\n            }\n            promises.push(session.audioElement.setSinkId(deviceId));\n        }\n        await Promise.all(promises);\n    }\n\n    /**\n     * @param {Boolean} is_muted\n     */\n    async setMute(is_muted) {\n        this.updateAndBroadcast({ is_muted });\n        await this.refreshMicAudioStatus();\n    }\n\n    /**\n     * @param {Boolean} raise\n     */\n    async raiseHand(raise) {\n        if (this.isRemote) {\n            this._remoteAction({ raisingHand: raise });\n            return;\n        }\n        if (!this.localSession || !this.state.channel) {\n            return;\n        }\n        this.localSession.raisingHand = raise ? new Date() : undefined;\n        await this._updateInfo();\n    }\n\n    /**\n     * @param {boolean} isTalking\n     */\n    async setTalking(isTalking) {\n        if (!this.localSession || isTalking === this.localSession.isTalking) {\n            return;\n        }\n        this.localSession.isTalking = isTalking;\n        if (!this.localSession.isMute) {\n            this.pttExtService.notifyIsTalking(isTalking);\n            await this.refreshMicAudioStatus();\n        }\n    }\n\n    /**\n     * Applies blur effect to a video stream using BlurManager.\n     *\n     * @param {MediaStream} videoStream - input video stream.\n     * @returns {Promise<BlurManager>} - BlurManager instance.\n     */\n    async applyBlurEffect(videoStream) {\n        return new BlurManager(videoStream, {\n            backgroundBlur: this.store.settings.backgroundBlurAmount,\n            edgeBlur: this.store.settings.edgeBlurAmount,\n        });\n    }\n\n    /**\n     * @param {string} type\n     * @param {Object} [param1]\n     * @param {boolean} [param1.force]\n     * @param {boolean} [param1.env]\n     * @param {boolean} [param1.refreshStream]\n     */\n    async toggleVideo(type, options) {\n        let force;\n        let env;\n        let refreshStream;\n        if (typeof options === \"boolean\") {\n            force = options;\n        } else {\n            force = options?.force;\n            env = options?.env;\n            refreshStream = options?.refreshStream;\n        }\n        if (this.isRemote) {\n            this.notification.add(UNAVAILABLE_AS_REMOTE, {\n                type: \"warning\",\n            });\n            return;\n        }\n        if (!this.state.channel?.id) {\n            return;\n        }\n        switch (type) {\n            case \"camera\": {\n                if (this.cameraPermission === \"prompt\" && !this.state.cameraTrack) {\n                    this.showMediaPermissionDialog(\"camera\");\n                    return;\n                }\n                const track = this.state.cameraTrack;\n                const sendCamera = force ?? !this.state.sendCamera;\n                this.state.sendCamera = false;\n                await this.setVideo(track, type, { activateVideo: sendCamera, env, refreshStream });\n                break;\n            }\n            case \"screen\": {\n                const track = this.state.screenTrack;\n                const sendScreen = force ?? !this.state.sendScreen;\n                this.state.sendScreen = false;\n                await this.setVideo(track, type, { activateVideo: sendScreen, env });\n                break;\n            }\n        }\n        if (this.localSession) {\n            switch (type) {\n                case \"camera\": {\n                    this.removeVideoFromSession(this.localSession, {\n                        type: \"camera\",\n                        cleanup: false,\n                    });\n                    if (this.state.cameraTrack) {\n                        this.updateStream(this.localSession, this.state.cameraTrack);\n                    }\n                    break;\n                }\n                case \"screen\": {\n                    if (!this.state.screenTrack) {\n                        this.removeVideoFromSession(this.localSession, {\n                            type: \"screen\",\n                            cleanup: false,\n                        });\n                    } else {\n                        this.updateStream(this.localSession, this.state.screenTrack);\n                    }\n                    break;\n                }\n            }\n        }\n        const updatedTrack = type === \"camera\" ? this.state.cameraTrack : this.state.screenTrack;\n        await this.network?.updateUpload(type, updatedTrack);\n        if (!this.localSession) {\n            return;\n        }\n        switch (type) {\n            case \"camera\": {\n                this.updateAndBroadcast({\n                    is_camera_on: !!this.state.sendCamera,\n                });\n                break;\n            }\n            case \"screen\": {\n                this.updateAndBroadcast({\n                    is_screen_sharing_on: !!this.state.sendScreen,\n                });\n                break;\n            }\n        }\n    }\n\n    updateAndBroadcast(data) {\n        this._updateRemoteTabs({ [this.localSession.id]: data });\n        assignDefined(this.localSession, data);\n        this.state.updateAndBroadcastDebounce?.();\n    }\n\n    /**\n     * Sets the enabled property of the local microphone audio track based on the\n     * current session state. And notifies peers of the new audio state.\n     */\n    async refreshMicAudioStatus() {\n        if (!this.state.micAudioTrack) {\n            return;\n        }\n        this.state.micAudioTrack.enabled = !this.localSession.isMute && this.localSession.isTalking;\n        this._updateInfo();\n    }\n\n    /**\n     * @param {String} type 'camera' or 'screen'\n     * @param {Object} [param1] options\n     * @param {Boolean} [param1.activateVideo=false] options\n     * @param {Env} [param1.env]\n     * @param {Boolean} [param1.refreshStream] whether we are requesting a new stream\n     */\n    async setVideo(track, type, options) {\n        let activateVideo;\n        let env;\n        if (typeof options === \"boolean\") {\n            activateVideo = options ?? false;\n        } else {\n            activateVideo = options?.activateVideo ?? false;\n            env = options?.env;\n        }\n        const stopVideo = () => {\n            if (track) {\n                track.stop();\n            }\n            switch (type) {\n                case \"camera\": {\n                    this.state.cameraTrack = undefined;\n                    closeStream(this.state.sourceCameraStream);\n                    this.state.sourceCameraStream = null;\n                    break;\n                }\n                case \"screen\": {\n                    this.state.screenTrack = undefined;\n                    closeStream(this.state.sourceScreenStream);\n                    this.state.sourceScreenStream = null;\n                    break;\n                }\n            }\n        };\n        if (!activateVideo) {\n            if (type === \"screen\") {\n                this.soundEffectsService.play(\"screen-sharing\");\n            }\n            if (type === \"camera\" && this.blurManager) {\n                this.blurManager.close();\n                this.blurManager = undefined;\n            }\n            stopVideo();\n            return;\n        }\n        let sourceStream;\n        const sourceWindow = env?.pipWindow ?? browser;\n        try {\n            if (type === \"camera\") {\n                if (this.state.sourceCameraStream && !options?.refreshStream) {\n                    sourceStream = this.state.sourceCameraStream;\n                } else {\n                    closeStream(this.state.sourceCameraStream);\n                    sourceStream = await sourceWindow.navigator.mediaDevices.getUserMedia({\n                        video: this.store.settings.cameraConstraints,\n                    });\n                }\n            }\n            if (type === \"screen\") {\n                if (this.state.sourceScreenStream) {\n                    sourceStream = this.state.sourceScreenStream;\n                } else {\n                    sourceStream = await sourceWindow.navigator.mediaDevices.getDisplayMedia({\n                        video: SCREEN_CONFIG,\n                        audio: true,\n                    });\n                }\n                this.soundEffectsService.play(\"screen-sharing\");\n            }\n        } catch {\n            this.showMediaUnavailableWarning({\n                camera: type === \"camera\",\n                screen: type === \"screen\",\n            });\n            stopVideo();\n            return;\n        }\n        if (!this.selfSession) {\n            closeStream(sourceStream);\n            return;\n        }\n        let outputTrack = sourceStream ? sourceStream.getVideoTracks()[0] : undefined;\n        const screenAudioTrack = sourceStream ? sourceStream.getAudioTracks()[0] : undefined;\n        if (outputTrack) {\n            outputTrack.addEventListener(\"ended\", async () => {\n                await this.toggleVideo(type, { force: false });\n            });\n            if (type === \"camera\" && isMobileOS()) {\n                const settings = outputTrack.getSettings();\n                if (settings?.facingMode) {\n                    this.store.settings.cameraFacingMode = settings.facingMode;\n                } else if (!this.store.settings.cameraFacingMode) {\n                    this.store.settings.cameraFacingMode = \"user\";\n                }\n            }\n        }\n        if (this.store.settings.useBlur && type === \"camera\") {\n            this.blurManager?.close();\n            this.blurManager = undefined;\n            try {\n                this.blurManager = await this.applyBlurEffect(sourceStream);\n                const blurredStream = await this.blurManager.stream;\n                outputTrack = blurredStream.getVideoTracks()[0];\n            } catch (_e) {\n                this.notification.add(_e.message, { type: \"warning\" });\n                this.store.settings.useBlur = false;\n                outputTrack = sourceStream.getVideoTracks()[0];\n            }\n        } else if (!this.store.settings.useBlur && type === \"camera\") {\n            this.blurManager?.close();\n            this.blurManager = undefined;\n        }\n        switch (type) {\n            case \"camera\": {\n                Object.assign(this.state, {\n                    sourceCameraStream: sourceStream,\n                    cameraTrack: outputTrack,\n                    sendCamera: Boolean(outputTrack),\n                    isCameraSourceExternal: Boolean(sourceStream) && env?.pipWindow,\n                });\n                break;\n            }\n            case \"screen\": {\n                Object.assign(this.state, {\n                    sourceScreenStream: sourceStream,\n                    screenTrack: outputTrack,\n                    screenAudioTrack: screenAudioTrack,\n                    sendScreen: Boolean(outputTrack),\n                    isScreenSourceExternal: Boolean(sourceStream) && env?.pipWindow,\n                });\n                break;\n            }\n        }\n        if (this.state.screenAudioTrack) {\n            this.updateAudioTrack();\n        }\n    }\n\n    async updateAudioTrack() {\n        const { micAudioTrack, screenAudioTrack } = this.state;\n        if (!micAudioTrack && !screenAudioTrack) {\n            return;\n        }\n        if (micAudioTrack && screenAudioTrack) {\n            await this.audioContext?.close();\n            this.audioContext = undefined;\n            this.audioContext = new AudioContext();\n            const micSource = this.audioContext.createMediaStreamSource(\n                new MediaStream([micAudioTrack])\n            );\n            const screenSource = this.audioContext.createMediaStreamSource(\n                new MediaStream([screenAudioTrack])\n            );\n            const destination = this.audioContext.createMediaStreamDestination();\n            micSource.connect(destination);\n            screenSource.connect(destination);\n            this.state.audioTrack = destination.stream.getAudioTracks()[0];\n        } else {\n            this.state.audioTrack = micAudioTrack ?? screenAudioTrack;\n        }\n        await this.network?.updateUpload(\"audio\", this.state.audioTrack);\n    }\n\n    async resetMicAudioTrack({ force = false }) {\n        this.state.micAudioTrack?.stop();\n        this.state.micAudioTrack = undefined;\n        this.state.audioTrack?.stop();\n        this.state.audioTrack = undefined;\n        if (!this.state.channel) {\n            return;\n        }\n        if (this.localSession) {\n            this.setMute(true);\n        }\n        if (force) {\n            let micAudioTrack;\n            try {\n                const audioStream = await browser.navigator.mediaDevices.getUserMedia({\n                    audio: this.store.settings.audioConstraints,\n                });\n                micAudioTrack = audioStream.getAudioTracks()[0];\n                if (this.localSession) {\n                    this.setMute(false);\n                }\n            } catch {\n                this.showMediaUnavailableWarning({ microphone: true });\n                return;\n            }\n            if (!this.localSession) {\n                // The getUserMedia promise could resolve when the call is ended\n                // in which case the track is no longer relevant.\n                micAudioTrack.stop();\n                return;\n            }\n            micAudioTrack.addEventListener(\"ended\", async () => {\n                // this mostly happens when the user retracts microphone permission.\n                await this.resetMicAudioTrack({ force: false });\n                this.setMute(true);\n            });\n            micAudioTrack.enabled = !this.localSession.isMute && this.localSession.isTalking;\n            this.state.micAudioTrack = micAudioTrack;\n            this.linkVoiceActivationDebounce();\n            this.updateAudioTrack();\n        }\n    }\n\n    /**\n     * Updates the way broadcast of the local audio track is handled,\n     * attaches an audio monitor for voice activation if necessary.\n     */\n    async linkVoiceActivation() {\n        this.state.disconnectAudioMonitor?.();\n        if (!this.localSession) {\n            return;\n        }\n        if (\n            this.store.settings.use_push_to_talk ||\n            !this.state.channel ||\n            !this.state.micAudioTrack\n        ) {\n            this.localSession.isTalking = false;\n            await this.refreshMicAudioStatus();\n            return;\n        }\n        try {\n            this.state.disconnectAudioMonitor = await monitorAudio(this.state.micAudioTrack, {\n                onThreshold: async (isAboveThreshold) => {\n                    this.setTalking(isAboveThreshold);\n                },\n                volumeThreshold: this.store.settings.voiceActivationThreshold,\n            });\n        } catch {\n            /**\n             * The browser is probably missing audioContext,\n             * in that case, voice activation is not enabled\n             * and the microphone is always 'on'.\n             */\n            this.notification.add(_t(\"Your browser does not support voice activation\"), {\n                type: \"warning\",\n            });\n            this.localSession.isTalking = true;\n        }\n        await this.refreshMicAudioStatus();\n    }\n\n    /**\n     * @param {import(\"models\").id} id\n     */\n    deleteSession(id) {\n        const session = this.store[\"discuss.channel.rtc.session\"].get(id);\n        if (session) {\n            if (this.localSession && session.eq(this.localSession)) {\n                this.notifyServerDisconnect();\n                this.endCall();\n            }\n            this.disconnect(session);\n            session.delete();\n        }\n    }\n\n    notifyServerDisconnect() {\n        this.log(this.localSession, \"self session deleted by the server, ending call\", {\n            important: true,\n        });\n        this.notification.add(_t(\"Disconnected from the call by the server\"), {\n            type: \"warning\",\n        });\n    }\n\n    formatInfo() {\n        this.localSession.is_camera_on = Boolean(this.state.cameraTrack);\n        this.localSession.is_screen_sharing_on = Boolean(this.state.screenTrack);\n        return this.localSession.info;\n    }\n\n    /**\n     * @param {import(\"models\").RtcSession} session\n     * @param {MediaStreamTrack} track\n     * @param {Object} [parm1]\n     * @param {boolean} [parm1.mute]\n     * @param {\"camera\"|\"screen\"} [parm1.videoType]\n     */\n    async updateStream(session, track, { mute, videoType } = {}) {\n        const stream = new window.MediaStream();\n        stream.addTrack(track);\n        if (track.kind === \"audio\") {\n            const audioElement = session.audioElement || new window.Audio();\n            audioElement.srcObject = stream;\n            audioElement.load();\n            audioElement.muted = mute;\n            audioElement.volume = this.store.settings.getVolume(session);\n            // Using both autoplay and play() as safari may prevent play() outside of user interactions\n            // while some browsers may not support or block autoplay.\n            audioElement.autoplay = true;\n            session.audioElement = audioElement;\n            session.audioStream = stream;\n            session.is_muted = false;\n            session.isTalking = false;\n            await session.playAudio();\n        }\n        if (track.kind === \"video\") {\n            videoType = videoType\n                ? videoType\n                : track.id === this.state.cameraTrack?.id\n                ? \"camera\"\n                : \"screen\";\n            session.videoStreams.set(videoType, stream);\n            this.updateActiveSession(session, videoType, { addVideo: true });\n        }\n    }\n\n    /**\n     * @param {import(\"models\").RtcSession} session\n     * @param {Object} [param1]\n     * @param {String} [param1.type]\n     * @param {boolean} [param1.cleanup]\n     */\n    removeVideoFromSession(session, { type, cleanup = true } = {}) {\n        if (type) {\n            this.updateActiveSession(session, type);\n            if (cleanup) {\n                closeStream(session.videoStreams.get(type));\n            }\n            session.videoStreams.delete(type);\n            if (\n                this.selfSession.videoStreams.size === 0 &&\n                this.selfSession.eq(this.state.channel.activeRtcSession)\n            ) {\n                this.state.channel.activeRtcSession = undefined;\n            }\n        } else {\n            if (cleanup) {\n                for (const stream of session.videoStreams.values()) {\n                    closeStream(stream);\n                }\n            }\n            session.videoStreams.clear();\n        }\n    }\n    /**\n     * @param {import(\"models\").RtcSession} session\n     */\n    removeAudioFromSession(session) {\n        closeStream(session.audioStream);\n        if (session.audioElement) {\n            session.audioElement.pause();\n            try {\n                session.audioElement.srcObject = undefined;\n            } catch {\n                // ignore error during remove, the value will be overwritten at next usage anyway\n            }\n        }\n        session.audioStream = undefined;\n    }\n\n    /**\n     * @param {import(\"models\").RtcSession} session\n     * @param {\"screen\"|\"camera\"} [videoType]\n     * @param {Object} [parm2]\n     * @param {boolean} [parm2.addVideo]\n     */\n    updateActiveSession(session, videoType, { addVideo = false } = {}) {\n        const activeRtcSession = this.state.channel.activeRtcSession;\n        if (addVideo) {\n            if (videoType === \"screen\") {\n                this.state.channel.activeRtcSession = session;\n                session.mainVideoStreamType = videoType;\n                return;\n            }\n            if (activeRtcSession && session.hasVideo && !session.isMainVideoStreamActive) {\n                session.mainVideoStreamType = videoType;\n            }\n            return;\n        }\n        if (!activeRtcSession || activeRtcSession.notEq(session)) {\n            return;\n        }\n        if (activeRtcSession.isMainVideoStreamActive) {\n            if (videoType === session.mainVideoStreamType) {\n                if (videoType === \"screen\") {\n                    session.mainVideoStreamType = \"camera\";\n                } else if (\n                    this.actionsStack.includes(\"camera-on\") &&\n                    this.actionsStack.includes(\"share-screen\")\n                ) {\n                    session.mainVideoStreamType = \"screen\";\n                }\n            }\n        }\n    }\n\n    /**\n     * @param {import(\"models\").RtcSession} rtcSession\n     * @param {Object} [param1]\n     * @param {number} [param1.viewCountIncrement=0] negative value to decrement\n     */\n    updateVideoDownload(rtcSession, { viewCountIncrement = 0 } = {}) {\n        rtcSession.videoComponentCount += viewCountIncrement;\n        const downloadTimeout = this.downloadTimeouts.get(rtcSession.id);\n        if (downloadTimeout) {\n            this.downloadTimeouts.delete(rtcSession.id);\n            browser.clearTimeout(downloadTimeout);\n        }\n        if (rtcSession.videoComponentCount > 0) {\n            this.network?.updateDownload(rtcSession.id, {\n                camera: true,\n                screen: true,\n            });\n        } else {\n            /**\n             * We wait a bit before pausing a download to avoid flickering, if the user stops downloading and starts again\n             * soon after, it is not worth pausing the download.\n             */\n            this.downloadTimeouts.set(\n                rtcSession.id,\n                browser.setTimeout(() => {\n                    this.downloadTimeouts.delete(rtcSession.id);\n                    this.network?.updateDownload(rtcSession.id, {\n                        camera: false,\n                        screen: false,\n                    });\n                }, 1000)\n            );\n        }\n    }\n}\n\nRtc.register();\n\nexport const rtcService = {\n    dependencies: [\n        \"bus_service\",\n        \"discuss.p2p\",\n        \"discuss.pip_service\",\n        \"discuss.ptt_extension\",\n        \"mail.fullscreen\",\n        \"mail.sound_effects\",\n        \"mail.store\",\n        \"legacy_multi_tab\",\n        \"notification\",\n        \"presence\",\n    ],\n    /**\n     * @param {import(\"@web/env\").OdooEnv} env\n     * @param {import(\"services\").ServiceFactories} services\n     */\n    start(env, services) {\n        const store = env.services[\"mail.store\"];\n        const rtc = store.rtc;\n        rtc.pipService = services[\"discuss.pip_service\"];\n        onChange(rtc.pipService.state, \"active\", () => {\n            const isPipMode = rtc.pipService.state.active;\n            if (!isPipMode) {\n                rtc.channel?.openChatWindow();\n            }\n            rtc.state.isPipMode = isPipMode;\n            rtc._postToTabs({\n                type: CROSS_TAB_HOST_MESSAGE.PIP_CHANGE,\n                changes: { isPipMode },\n            });\n        });\n        rtc.fullscreen = services[\"mail.fullscreen\"];\n        onChange(rtc.fullscreen, \"id\", () => {\n            const wasFullscreen = rtc.state.isFullscreen;\n            rtc.state.isFullscreen = rtc.fullscreen.id === CALL_FULLSCREEN_ID;\n            if (\n                rtc.state.screenTrack &&\n                rtc.displaySurface !== \"browser\" &&\n                rtc.fullscreen.id === CALL_FULLSCREEN_ID\n            ) {\n                rtc.showMirroringWarning();\n            } else if (!rtc.state.isFullscreen) {\n                rtc.removeMirroringWarning?.();\n                if (wasFullscreen && rtc.state.screenTrack) {\n                    rtc.state.screenTrack.enabled = true;\n                }\n            }\n        });\n        browser.navigator.permissions?.query({ name: \"microphone\" }).then((status) => {\n            rtc.microphonePermission = status.state;\n            status.onchange = () => (rtc.microphonePermission = status.state);\n        });\n        browser.navigator.permissions?.query({ name: \"camera\" }).then((status) => {\n            rtc.cameraPermission = status.state;\n            status.onchange = () => (rtc.cameraPermission = status.state);\n        });\n        rtc.p2pService = services[\"discuss.p2p\"];\n        rtc.p2pService.acceptOffer = async (id, sequence) => {\n            const session = await store[\"discuss.channel.rtc.session\"].getWhenReady(Number(id));\n            /**\n             * We only accept offers for new connections (higher sequence),\n             * or offers that renegotiate an existing connection (same sequence).\n             */\n            return sequence >= session?.sequence;\n        };\n        services[\"bus_service\"].subscribe(\n            \"discuss.channel.rtc.session/sfu_hot_swap\",\n            async ({ serverInfo }) => {\n                if (!rtc.localSession) {\n                    return;\n                }\n                if (rtc.serverInfo?.channelUUID === serverInfo.channelUUID) {\n                    // we clear peers as inbound p2p connections may still be active\n                    rtc.p2pService.removeALlPeers();\n                    // no reason to swap if the server is the same, if at some point we want to force a swap\n                    // there should be an explicit flag in the event payload.\n                    return;\n                }\n                rtc.serverInfo = serverInfo;\n                await rtc._initConnection();\n            }\n        );\n        services[\"bus_service\"].subscribe(\"discuss.channel.rtc.session/ended\", ({ sessionId }) => {\n            if (rtc.localSession?.id === sessionId) {\n                rtc.notifyServerDisconnect();\n                rtc.endCall();\n            }\n        });\n        services[\"bus_service\"].subscribe(\"res.users.settings.volumes\", (payload) => {\n            if (payload) {\n                rtc.store.Volume.insert(payload);\n            }\n        });\n        services[\"bus_service\"].subscribe(\n            \"discuss.channel.rtc.session/update_and_broadcast\",\n            (payload) => {\n                const { data, channelId } = payload;\n                /**\n                 * If this event comes from the channel of the current call, information is shared in real time\n                 * through the peer to peer connection. So we do not use this less accurate broadcast.\n                 */\n                if (channelId !== rtc.channel?.id) {\n                    rtc.store.insert(data);\n                }\n            }\n        );\n        /**\n         * Attempts to play RTC medias when a user shows signs of presence (interaction with the page) as\n         * they cannot be played on windows that have not been interacted with.\n         */\n        services[\"presence\"].bus.addEventListener(\n            \"presence\",\n            () => {\n                env.bus.trigger(\"RTC-SERVICE:PLAY_MEDIA\");\n            },\n            { once: true }\n        );\n        return rtc;\n    },\n};\n\nregistry.category(\"services\").add(\"discuss.rtc\", rtcService);\n", "import { fields, Record } from \"@mail/core/common/record\";\nimport { Deferred } from \"@web/core/utils/concurrency\";\n\n/**\n * @typedef {object} SessionInfo\n * @property {boolean} [isSelfMuted]\n * @property {boolean} [isDeaf]\n * @property {boolean} [isTalking]\n * @property {boolean} [isRaisingHand]\n * @property {boolean} [isCameraOn]\n * @property {boolean} [isScreenSharingOn]\n */\n\nexport class RtcSession extends Record {\n    static _name = \"discuss.channel.rtc.session\";\n    static id = \"id\";\n    static awaitedRecords = new Map();\n    static _insert() {\n        /** @type {import(\"models\").RtcSession} */\n        const session = super._insert(...arguments);\n        session.channel?.rtc_session_ids.add(session);\n        return session;\n    }\n    /** @returns {Promise<import(\"models\").RtcSession>} */\n    static async getWhenReady(id) {\n        const session = this.get(id);\n        if (!session) {\n            let deferred = this.awaitedRecords.get(id);\n            if (!deferred) {\n                deferred = new Deferred();\n                this.awaitedRecords.set(id, deferred);\n                setTimeout(() => {\n                    deferred.resolve();\n                    this.awaitedRecords.delete(id);\n                }, 120_000);\n            }\n            return deferred;\n        }\n        return session;\n    }\n    /** @returns {import(\"models\").RtcSession} */\n    static new() {\n        const record = super.new(...arguments);\n        this.awaitedRecords.get(record.id)?.resolve(record);\n        this.awaitedRecords.delete(record.id);\n        return record;\n    }\n\n    // Server data\n    channel_member_id = fields.One(\"discuss.channel.member\", { inverse: \"rtcSession\" });\n    partner_id = fields.One(\"res.partner\", {\n        compute() {\n            return this.channel_member_id?.partner_id;\n        },\n    });\n    guest_id = fields.One(\"mail.guest\", {\n        compute() {\n            return this.channel_member_id?.guest_id;\n        },\n    });\n    get persona() {\n        return this.partner_id || this.guest_id;\n    }\n    /** @type {boolean} */\n    is_camera_on;\n    /** @type {boolean} */\n    is_screen_sharing_on = fields.Attr(undefined, {\n        onUpdate() {\n            if (\n                this.eq(this.channel?.activeRtcSession) &&\n                this.mainVideoStreamType === \"screen\" &&\n                !this.is_screen_sharing_on\n            ) {\n                this.channel.activeRtcSession = undefined;\n            }\n        },\n    });\n    /** @type {number} */\n    id;\n    /** @type {boolean} */\n    is_deaf;\n    /** @type {boolean} */\n    is_muted;\n    // Client data\n    /** @type {HTMLAudioElement} */\n    audioElement;\n    /** @type {MediaStream} */\n    audioStream;\n    /** @type {RTCDataChannel} */\n    dataChannel;\n    audioError;\n    videoError;\n    isTalking = fields.Attr(false, {\n        /** @this {import(\"models\").RtcSession} */\n        onUpdate() {\n            if (this.isTalking && !this.isMute) {\n                this.talkingTime = this.store.nextTalkingTime++;\n            }\n            this.channel?.updateCallFocusStack(this);\n        },\n    });\n    isActuallyTalking = fields.Attr(false, {\n        /** @this {import(\"models\").RtcSession} */\n        compute() {\n            return this.isTalking && !this.isMute;\n        },\n    });\n    isVideoStreaming = fields.Attr(false, {\n        /** @this {import(\"models\").RtcSession} */\n        compute() {\n            return this.is_screen_sharing_on || this.is_camera_on;\n        },\n        /** @this {import(\"models\").RtcSession} */\n        onUpdate() {\n            if (\n                this.isVideoStreaming &&\n                this.channel?.channel_type === \"chat\" &&\n                this.store.rtc.selfSession?.in(this.channel.rtc_session_ids)\n            ) {\n                this.channel.focusAvailableVideo();\n            }\n        },\n    });\n    shortStatus = fields.Attr(undefined, {\n        compute() {\n            if (this.is_screen_sharing_on) {\n                return \"live\";\n            }\n            if (this.is_deaf) {\n                return \"deafen\";\n            }\n            if (this.isMute) {\n                return \"mute\";\n            }\n        },\n    });\n    talkingTime = 0;\n    localVolume;\n    /** @type {RTCPeerConnection} */\n    peerConnection;\n    /** @type {Date|undefined} */\n    raisingHand;\n    videoComponentCount = 0;\n    /** @type {Map<'screen'|'camera', MediaStream>} */\n    videoStreams = new Map();\n    /** @type {string} */\n    mainVideoStreamType;\n    /**\n     * Represents the sequence of the last valid connection with that session. This can be used to\n     * compare connection attempts (if they follow the last valid connection) and to validate information\n     * (if they match the sequence).\n     *\n     *  @type {number}\n     */\n    sequence = 0;\n    // RTC stats\n    connectionState;\n    logStep;\n\n    get channel() {\n        return this.channel_member_id?.channel_id;\n    }\n\n    get isMute() {\n        return this.is_muted || this.is_deaf;\n    }\n\n    get mainVideoStream() {\n        return this.isMainVideoStreamActive && this.videoStreams.get(this.mainVideoStreamType);\n    }\n\n    get isMainVideoStreamActive() {\n        if (!this.mainVideoStreamType) {\n            return false;\n        }\n        return this.mainVideoStreamType === \"camera\"\n            ? this.is_camera_on\n            : this.is_screen_sharing_on;\n    }\n\n    get hasVideo() {\n        return this.is_screen_sharing_on || this.is_camera_on;\n    }\n\n    getStream(type) {\n        const isActive = type === \"camera\" ? this.is_camera_on : this.is_screen_sharing_on;\n        return isActive && this.videoStreams.get(type);\n    }\n\n    /**\n     * @returns {{isSelfMuted: boolean, isDeaf: boolean, isTalking: boolean, isRaisingHand: boolean}}\n     */\n    get info() {\n        return {\n            isSelfMuted: this.is_muted,\n            isRaisingHand: Boolean(this.raisingHand),\n            isDeaf: this.is_deaf,\n            isTalking: this.isTalking,\n            isCameraOn: this.is_camera_on,\n            isScreenSharingOn: this.is_screen_sharing_on,\n        };\n    }\n\n    /**\n     * @returns {string}\n     */\n    get name() {\n        return this.channel_member_id?.name;\n    }\n\n    /**\n     * @returns {number} float\n     */\n    get volume() {\n        return this.audioElement?.volume || this.localVolume;\n    }\n\n    set volume(value) {\n        if (this.audioElement) {\n            this.audioElement.volume = value;\n        }\n        this.localVolume = value;\n    }\n\n    async playAudio() {\n        if (!this.audioElement) {\n            return;\n        }\n        if (this.store.settings.audioOutputDeviceId) {\n            // skipping, it will use the default device.\n            await this.audioElement.setSinkId(this.store.settings.audioOutputDeviceId).catch();\n        }\n        try {\n            await this.audioElement.play();\n            this.audioError = undefined;\n        } catch (error) {\n            this.audioError = error.name;\n        }\n    }\n\n    /**\n     * @param {\"audio\" | \"camera\" | \"screen\"} type\n     * @param {boolean} state\n     */\n    updateStreamState(type, state) {\n        if (type === \"camera\") {\n            this.is_camera_on = state;\n        } else if (type === \"screen\") {\n            this.is_screen_sharing_on = state;\n        }\n    }\n}\n\nRtcSession.register();\n", "import { Settings } from \"@mail/core/common/settings_model\";\n\nimport { patch } from \"@web/core/utils/patch\";\n\n/** @type {import(\"models\").Settings} */\nconst SettingsPatch = {\n    setup() {\n        super.setup(...arguments);\n    },\n    /** @param {import(\"models\").RtcSession} rtcSession */\n    getVolume(rtcSession) {\n        return (\n            rtcSession.volume ??\n            this.volumes.find(\n                (volume) =>\n                    volume.partner_id?.eq(rtcSession.partner_id) ||\n                    volume.guest_id?.eq(rtcSession.guest_id)\n            )?.volume ??\n            0.5\n        );\n    },\n};\npatch(Settings.prototype, SettingsPatch);\n", "import { fields } from \"@mail/core/common/record\";\nimport { Store } from \"@mail/core/common/store_service\";\nimport { router } from \"@web/core/browser/router\";\n\nimport { patch } from \"@web/core/utils/patch\";\n\n/** @type {import(\"models\").Store} */\nconst StorePatch = {\n    setup() {\n        super.setup(...arguments);\n        this.rtc = fields.One(\"Rtc\", {\n            compute() {\n                return {};\n            },\n        });\n        this.ringingThreads = fields.Many(\"Thread\", {\n            /** @this {import(\"models\").Store} */\n            onUpdate() {\n                if (this.ringingThreads.length > 0) {\n                    this.env.services[\"mail.sound_effects\"].play(\"call-invitation\", {\n                        loop: true,\n                    });\n                } else {\n                    this.env.services[\"mail.sound_effects\"].stop(\"call-invitation\");\n                }\n            },\n        });\n        this.allActiveRtcSessions = fields.Many(\"discuss.channel.rtc.session\");\n        this.nextTalkingTime = 1;\n        this.fullscreenChannel = fields.One(\"Thread\");\n        this._hasFullscreenUrl = fields.Attr(false, {\n            compute() {\n                return this.discuss?.thread?.eq(this.fullscreenChannel);\n            },\n            onUpdate() {\n                if (!this.discuss?.hasRestoredThread) {\n                    return;\n                }\n                this._hasFullscreenUrlOnUpdate();\n            },\n            eager: true,\n        });\n        this.meetingViewOpened = false;\n    },\n    _hasFullscreenUrlOnUpdate() {\n        router.pushState({\n            fullscreen: this._hasFullscreenUrl ? true : undefined,\n        });\n    },\n    onStarted() {\n        super.onStarted(...arguments);\n        this.rtc.start();\n    },\n    sortMembers(m1, m2) {\n        const m1HasRtc = Boolean(m1.rtcSession);\n        const m2HasRtc = Boolean(m2.rtcSession);\n        if (m1HasRtc === m2HasRtc) {\n            /**\n             * If raisingHand is falsy, it gets an Infinity value so that when\n             * we sort by [oldest/lowest-value]-first, falsy values end up last.\n             */\n            const m1RaisingValue = m1.rtcSession?.raisingHand || Infinity;\n            const m2RaisingValue = m2.rtcSession?.raisingHand || Infinity;\n            if (m1HasRtc && m1RaisingValue !== m2RaisingValue) {\n                return m1RaisingValue - m2RaisingValue;\n            } else {\n                return super.sortMembers(m1, m2);\n            }\n        } else {\n            return m2HasRtc - m1HasRtc;\n        }\n    },\n};\npatch(Store.prototype, StorePatch);\n", "import { ACTION_TAGS } from \"@mail/core/common/action\";\nimport { registerThreadAction } from \"@mail/core/common/thread_actions\";\nimport { CallSettings } from \"@mail/discuss/call/common/call_settings\";\n\nimport { _t } from \"@web/core/l10n/translation\";\n\nregisterThreadAction(\"call\", {\n    condition: ({ store, thread }) => thread?.allowCalls && !thread?.eq(store.rtc.channel),\n    icon: \"fa fa-fw fa-phone\",\n    name: ({ thread }) =>\n        thread.rtc_session_ids.length > 0 ? _t(\"Join the Call\") : _t(\"Start Call\"),\n    open: ({ store, thread }) => store.rtc.toggleCall(thread),\n    sequence: 10,\n    sequenceQuick: 30,\n    tags: [ACTION_TAGS.SUCCESS, ACTION_TAGS.JOIN_LEAVE_CALL],\n});\nregisterThreadAction(\"camera-call\", {\n    condition: ({ store, thread }) => thread?.allowCalls && !thread?.eq(store.rtc.channel),\n    icon: \"fa fa-fw fa-video-camera\",\n    name: ({ thread }) =>\n        thread.rtc_session_ids.length > 0\n            ? _t(\"Join the Call with Camera\")\n            : _t(\"Start Video Call\"),\n    open: ({ store, thread }) => store.rtc.toggleCall(thread, { camera: true }),\n    sequence: 5,\n    sequenceQuick: ({ owner }) => (owner.env.inDiscussApp ? 25 : 35),\n    tags: [ACTION_TAGS.SUCCESS, ACTION_TAGS.JOIN_LEAVE_CALL],\n});\nregisterThreadAction(\"call-settings\", {\n    actionPanelComponent: CallSettings,\n    actionPanelComponentProps: () => ({ isCompact: true }),\n    condition: ({ owner, store, thread }) =>\n        thread?.allowCalls &&\n        (owner.props.chatWindow?.isOpen || store.inPublicPage) &&\n        !owner.isDiscussSidebarChannelActions,\n    icon: \"fa fa-fw fa-gear\",\n    name: _t(\"Call Settings\"),\n    sequence: 20,\n    sequenceGroup: 30,\n    toggle: true,\n});\nregisterThreadAction(\"disconnect\", {\n    condition: ({ owner, store, thread }) =>\n        store.rtc.selfSession?.in(thread?.rtc_session_ids) && owner.isDiscussSidebarChannelActions,\n    open: ({ store, thread }) => store.rtc.toggleCall(thread),\n    icon: \"fa fa-fw fa-phone\",\n    name: _t(\"Disconnect\"),\n    sequence: 30,\n    sequenceGroup: 10,\n    tags: [ACTION_TAGS.DANGER, ACTION_TAGS.JOIN_LEAVE_CALL],\n});\n", "import { fields } from \"@mail/core/common/record\";\nimport { Thread } from \"@mail/core/common/thread_model\";\n\nimport { patch } from \"@web/core/utils/patch\";\n\nexport const CALL_PROMOTE_FULLSCREEN = Object.freeze({\n    INACTIVE: \"INACTIVE\",\n    ACTIVE: \"ACTIVE\",\n    DISCARDED: \"DISCARDED\",\n});\n\n/** @type {import(\"models\").Thread} */\nconst ThreadPatch = {\n    setup() {\n        super.setup(...arguments);\n        this.activeRtcSession = fields.One(\"discuss.channel.rtc.session\", {\n            /** @this {import(\"models\").Thread} */\n            onAdd(r) {\n                this.store.allActiveRtcSessions.add(r);\n            },\n            /** @this {import(\"models\").Thread} */\n            onDelete(r) {\n                this.store.allActiveRtcSessions.delete(r);\n            },\n        });\n        /** @type {typeof CALL_PROMOTE_FULLSCREEN[keyof CALL_PROMOTE_FULLSCREEN]} */\n        this.promoteFullscreen = CALL_PROMOTE_FULLSCREEN.DISABLED;\n        this.hadSelfSession = false;\n        /** @type {Set<number>} */\n        this.lastSessionIds = new Set();\n        /** @type {number|undefined} */\n        this.cancelRtcInvitationTimeout;\n        this.rtc_session_ids = fields.Many(\"discuss.channel.rtc.session\", {\n            /** @this {import(\"models\").Thread} */\n            onDelete(r) {\n                this.store.env.services[\"discuss.rtc\"].deleteSession(r.id);\n            },\n            /** @this {import(\"models\").Thread} */\n            async onUpdate() {\n                const hadSelfSession = this.hadSelfSession;\n                const lastSessionIds = this.lastSessionIds;\n                this.hadSelfSession = Boolean(this.store.rtc.selfSession?.in(this.rtc_session_ids));\n                this.lastSessionIds = new Set(this.rtc_session_ids.map((s) => s.id));\n                const shouldPlayJoinSound = [...this.lastSessionIds].some(\n                    (id) => !lastSessionIds.has(id)\n                );\n                const shouldPlayLeaveSound = [...lastSessionIds].some(\n                    (id) => !this.lastSessionIds.has(id)\n                );\n                if (\n                    !hadSelfSession || // sound for self-join is played instead\n                    !this.hadSelfSession || // sound for self-leave is played instead\n                    !(await this.store.env.services[\"multi_tab\"].isOnMainTab()) // another tab playing sound\n                ) {\n                    return;\n                }\n                if (shouldPlayJoinSound) {\n                    this.store.env.services[\"mail.sound_effects\"].play(\"call-join\");\n                    this.store.rtc.call({ asFallback: true });\n                }\n                if (shouldPlayLeaveSound) {\n                    this.store.env.services[\"mail.sound_effects\"].play(\"member-leave\");\n                }\n            },\n        });\n        this.videoCountNotSelf = fields.Attr(0, {\n            compute() {\n                return this.rtc_session_ids.filter(\n                    (s) => s.hasVideo && s.notEq(this.store.rtc.selfSession)\n                ).length;\n            },\n            onUpdate() {\n                if (this.promoteFullscreen === CALL_PROMOTE_FULLSCREEN.DISCARDED) {\n                    return;\n                }\n                this.promoteFullscreen =\n                    this.videoCountNotSelf > 0 && this.chat_window?.isOpen\n                        ? CALL_PROMOTE_FULLSCREEN.ACTIVE\n                        : CALL_PROMOTE_FULLSCREEN.INACTIVE;\n            },\n        });\n        this.videoCount = fields.Attr(0, {\n            compute() {\n                return this.rtc_session_ids.filter((s) => s.hasVideo).length;\n            },\n        });\n        this.focusStack = fields.Many(\"discuss.channel.rtc.session\");\n        /** @type {import(\"@mail/discuss/call/common/call\").CardData[]} */\n        this.visibleCards = fields.Attr([], {\n            compute() {\n                const raisingHandCards = [];\n                const sessionCards = [];\n                const invitationCards = [];\n                const filterVideos = this.store.settings.showOnlyVideo && this.videoCount > 0;\n                for (const session of this.rtc_session_ids) {\n                    const target = session.raisingHand ? raisingHandCards : sessionCards;\n                    const cameraStream = session.is_camera_on\n                        ? session.videoStreams.get(\"camera\")\n                        : undefined;\n                    if (!filterVideos || cameraStream) {\n                        target.push({\n                            key: \"session_main_\" + session.id,\n                            session,\n                            type: \"camera\",\n                            videoStream: cameraStream,\n                        });\n                    }\n                    const screenStream = session.is_screen_sharing_on\n                        ? session.videoStreams.get(\"screen\")\n                        : undefined;\n                    if (screenStream) {\n                        target.push({\n                            key: \"session_secondary_\" + session.id,\n                            session,\n                            type: \"screen\",\n                            videoStream: screenStream,\n                        });\n                    }\n                }\n                if (!filterVideos) {\n                    for (const member of this.invited_member_ids) {\n                        invitationCards.push({ key: \"member_\" + member.id, member });\n                    }\n                }\n                raisingHandCards.sort((c1, c2) => c1.session.raisingHand - c2.session.raisingHand);\n                sessionCards.sort(\n                    (c1, c2) =>\n                        c1.session.channel_member_id?.persona?.name?.localeCompare(\n                            c2.session.channel_member_id?.persona?.name\n                        ) ?? 1\n                );\n                invitationCards.sort(\n                    (c1, c2) => c1.member.persona?.name?.localeCompare(c2.member.persona?.name) ?? 1\n                );\n                return raisingHandCards.concat(sessionCards, invitationCards);\n            },\n        });\n        this.useCameraByDefault = fields.Attr(null, {\n            /** @this {import(\"models\").Thread} */\n            compute() {\n                if (this.channel_type === \"chat\" && this.store.rtc.selfSession?.channel?.eq(this)) {\n                    return this.store.rtc.selfSession.is_camera_on;\n                }\n                return JSON.parse(\n                    localStorage.getItem(`discuss_channel_camera_default_${this.id}`)\n                );\n            },\n            /** @this {import(\"models\").Thread} */\n            onUpdate() {\n                if (this.useCameraByDefault !== null) {\n                    localStorage.setItem(\n                        `discuss_channel_camera_default_${this.id}`,\n                        JSON.stringify(this.useCameraByDefault)\n                    );\n                }\n            },\n        });\n    },\n    get isCallDisplayedInChatWindow() {\n        return this.chat_window?.isOpen && !this.store.meetingViewOpened;\n    },\n    get showCallView() {\n        return !this.store.rtc.state.isFullscreen && this.rtc_session_ids.length > 0;\n    },\n    focusAvailableVideo() {\n        if (\n            !this.store.settings.useCallAutoFocus ||\n            !(\n                this.store.env.services.ui.isSmall ||\n                this.store.rtc.state.isPipMode ||\n                this.isCallDisplayedInChatWindow\n            )\n        ) {\n            return;\n        }\n        const otherStreamingSession = this.rtc_session_ids.find(\n            (session) => session.notEq(this.store.rtc.selfSession) && session.hasVideo\n        );\n        if (!otherStreamingSession) {\n            return;\n        }\n        this.activeRtcSession = otherStreamingSession;\n        otherStreamingSession.mainVideoStreamType = otherStreamingSession.is_screen_sharing_on\n            ? \"screen\"\n            : \"camera\";\n    },\n    open(options) {\n        if (this.store.fullscreenChannel?.notEq(this)) {\n            this.store.rtc.exitFullscreen();\n        }\n        return super.open(...arguments);\n    },\n    /**\n     * @param {import(\"models\").RtcSession} session\n     */\n    updateCallFocusStack(session) {\n        if (\n            this.notEq(this.store.rtc?.channel) ||\n            session.eq(this.store.rtc.selfSession) ||\n            !this.activeRtcSession ||\n            !this.store.settings.useCallAutoFocus ||\n            this.activeRtcSession?.mainVideoStreamType === \"screen\"\n        ) {\n            return;\n        }\n        this.focusStack.delete(session);\n        if (session.isTalking && !session.isMute) {\n            this.focusStack.push(session);\n        }\n        const activeSession = this.focusStack.at(-1);\n        if (!activeSession) {\n            return;\n        }\n        this.activeRtcSession = activeSession;\n        activeSession.mainVideoStreamType = \"camera\";\n    },\n};\npatch(Thread.prototype, ThreadPatch);\n", "/* eslint-env worker */\n/* eslint-disable no-restricted-globals */\n\nlet intervalId = null;\nlet awaitingTock = false;\nlet isRunning = false;\n\nfunction reset() {\n    if (intervalId) {\n        clearInterval(intervalId);\n        intervalId = null;\n    }\n    awaitingTock = false;\n    isRunning = false;\n}\n\nfunction startProcessing(fps) {\n    if (isRunning) {\n        return;\n    }\n    isRunning = true;\n    intervalId = setInterval(() => {\n        if (awaitingTock) {\n            return;\n        }\n        awaitingTock = true;\n        self.postMessage({ command: \"tick\" });\n    }, Math.floor(1000 / (fps || 30)));\n}\n\nself.onmessage = (e) => {\n    if (!e.data?.command) {\n        return;\n    }\n    const { command, fps } = e.data;\n    switch (command) {\n        case \"start\":\n            startProcessing(fps);\n            break;\n        case \"tock\":\n            awaitingTock = false;\n            break;\n        case \"stop\":\n            reset();\n            break;\n        default:\n            break;\n    }\n};\n\nself.onmessageerror = () => {\n    awaitingTock = false;\n};\n\nself.onerror = reset;\n", "import {\n    registerComposerAction,\n    pickerOnClick,\n    pickerSetup,\n} from \"@mail/core/common/composer_actions\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { markEventHandled } from \"@web/core/utils/misc\";\nimport { useGifPicker } from \"./gif_picker\";\n\nregisterComposerAction(\"add-gif\", {\n    condition: ({ composer, owner, store }) =>\n        (store.hasGifPickerFeature || store.self.main_user_id?.is_admin) &&\n        !owner.env.inChatter &&\n        !composer.message,\n    isPicker: true,\n    pickerName: _t(\"GIF\"),\n    icon: \"oi oi-gif-picker\",\n    name: _t(\"Add GIFs\"),\n    onSelected({ owner }, ev) {\n        pickerOnClick(owner, this, ev);\n        markEventHandled(ev, \"Composer.onClickAddGif\");\n    },\n    setup({ owner }) {\n        pickerSetup(this, () =>\n            useGifPicker(\n                undefined,\n                {\n                    onSelect: (gif) => owner.sendGifMessage(gif),\n                    onClose: () => owner.setActivePicker(null),\n                },\n                { arrow: false }\n            )\n        );\n    },\n    sequence: ({ owner }) => (!owner.env.inDiscussApp ? 40 : undefined),\n    sequenceQuick: ({ owner }) => (owner.env.inDiscussApp ? 15 : undefined),\n});\n", "import { Composer } from \"@mail/core/common/composer\";\nimport { markEventHandled } from \"@web/core/utils/misc\";\n\nimport { markup, useRef } from \"@odoo/owl\";\n\nimport { useService } from \"@web/core/utils/hooks\";\nimport { patch } from \"@web/core/utils/patch\";\n\n/** @type {Composer} */\nconst composerPatch = {\n    setup() {\n        this.gifButton = useRef(\"gif-button\");\n        super.setup();\n        this.ui = useService(\"ui\");\n    },\n    get pickerSettings() {\n        const setting = super.pickerSettings;\n        if (this.hasGifPicker) {\n            setting.pickers.gif = (gif) => this.sendGifMessage(gif);\n            if (this.hasGifPickerButton) {\n                setting.buttons.push(this.gifButton);\n            }\n        }\n        return setting;\n    },\n    get hasGifPicker() {\n        return (\n            (this.store.hasGifPickerFeature || this.store.self.main_user_id?.is_admin) &&\n            !this.env.inChatter &&\n            !this.props.composer.message\n        );\n    },\n    get hasGifPickerButton() {\n        return this.hasGifPicker && !this.ui.isSmall && !this.env.inChatWindow;\n    },\n    onClickAddGif(ev) {\n        markEventHandled(ev, \"Composer.onClickAddGif\");\n    },\n    async sendGifMessage(gif) {\n        const href = encodeURI(gif.url);\n        await this._sendMessage(\n            markup`<a href=\"${href}\" target=\"_blank\" rel=\"noreferrer noopener\">${gif.url}</a>`,\n            {\n                parentId: this.props.composer.replyToMessage?.id,\n            }\n        );\n    },\n};\npatch(Composer.prototype, composerPatch);\n", "import { Gif } from \"@mail/core/common/gif\";\nimport { useOnBottomScrolled, useSequential } from \"@mail/utils/common/hooks\";\n\nimport { Component, onWillStart, useState, useEffect } from \"@odoo/owl\";\nimport { user } from \"@web/core/user\";\nimport { useService, useAutofocus } from \"@web/core/utils/hooks\";\nimport { useDebounced } from \"@web/core/utils/timing\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { PICKER_PROPS, usePicker } from \"@web/core/emoji_picker/emoji_picker\";\n\nexport function useGifPicker(...args) {\n    return usePicker(GifPicker, ...args);\n}\n\n/**\n * @typedef {Object} TenorCategory\n * @property {string} searchterm\n * @property {string} path\n * @property {string} image\n * @property {string} name\n */\n\n/**\n * @typedef {Object} TenorMediaFormat\n * @property {string} url\n * @property {number} duration\n * @property {string} preview\n * @property {number[]} dims\n * @property {number} size\n */\n\n/**\n * @typedef {Object} TenorGif\n * @property {string} id\n * @property {string} title\n * @property {number} created\n * @property {string} content_description\n * @property {string} itemurl\n * @property {string} url\n * @property {string[]} tags\n * @property {string[]} flags\n * @property {boolean} hasaudio\n * @property {{ tinygif: TenorMediaFormat }} media_formats\n */\n\n/**\n * @typedef {Object} Props\n * @property {function} onSelect Callback to use when the gif is selected\n * @property {string} [className]\n * @property {function} [close]\n * @property {Object} [state]\n * @extends {Component<Props, Env>}\n */\n\nexport class GifPicker extends Component {\n    static template = \"discuss.GifPicker\";\n    static props = PICKER_PROPS;\n    static components = { Gif };\n\n    setup() {\n        super.setup();\n        this.orm = useService(\"orm\");\n        this.store = useService(\"mail.store\");\n        this.sequential = useSequential();\n        this.inputRef = useAutofocus();\n        useOnBottomScrolled(\n            \"scroller\",\n            () => {\n                if (!this.state.showCategories) {\n                    if (!this.showFavorite) {\n                        this.search();\n                    } else {\n                        this.loadFavoritesDebounced(this.offset);\n                    }\n                }\n            },\n            300\n        );\n        this.next = \"\";\n        this.showFavorite = false;\n        this.offset = 0;\n        this.state = useState({\n            favorites: {\n                /** @type {TenorGif[]} */\n                gifs: [],\n                offset: 0,\n            },\n            searchTerm: \"\",\n            showCategories: true,\n            /** @type {TenorCategory[]} */\n            categories: [],\n            loadingGif: false,\n            loadingError: false,\n            evenGif: {\n                /** @type {Map<Number, TenorGif>} */\n                gifs: new Map(),\n                /** Size, in pixel, of the column. */\n                columnSize: 0,\n            },\n            oddGif: {\n                /** @type {Map<Number, TenorGif>} */\n                gifs: new Map(),\n                /** Size, in pixel, of the column. */\n                columnSize: 0,\n            },\n            focused: false,\n        });\n        this.loadFavoritesDebounced = useDebounced(this.loadFavorites, 200);\n        onWillStart(() => {\n            this.loadCategories();\n        });\n        if (this.store.self_partner) {\n            onWillStart(() => {\n                this.loadFavorites();\n            });\n        }\n        useEffect(\n            () => {\n                if (this.props.state?.picker !== this.props.PICKERS?.GIF) {\n                    return;\n                }\n                this.clear();\n                this.search();\n                if (this.searchTerm) {\n                    this.closeCategories();\n                } else {\n                    this.openCategories();\n                }\n            },\n            () => [this.searchTerm, this.props.state?.picker]\n        );\n    }\n\n    get style() {\n        return \"\";\n    }\n\n    get searchTerm() {\n        return this.props.state ? this.props.state.searchTerm : this.state.searchTerm;\n    }\n\n    set searchTerm(value) {\n        if (this.props.state) {\n            this.props.state.searchTerm = value;\n        } else {\n            this.state.searchTerm = value;\n        }\n    }\n\n    async loadCategories() {\n        if (!this.store.hasGifPickerFeature) {\n            return;\n        }\n        try {\n            let { language, region } = new Intl.Locale(user.lang);\n            if (!region && language === \"sr\") {\n                region = \"RS\";\n            }\n            const { tags } = await rpc(\n                \"/discuss/gif/categories\",\n                {\n                    country: region,\n                    locale: `${language}_${region}`,\n                },\n                { silent: true }\n            );\n            if (tags) {\n                this.state.categories = tags;\n            }\n        } catch {\n            this.state.loadingError = true;\n        }\n    }\n\n    openCategories() {\n        this.showFavorite = false;\n        this.state.showCategories = true;\n        this.searchTerm = \"\";\n        this.clear();\n    }\n\n    closeCategories() {\n        this.state.showCategories = false;\n    }\n\n    async search() {\n        if (!this.searchTerm) {\n            return;\n        }\n        try {\n            let { language, region } = new Intl.Locale(user.lang);\n            if (!region && language === \"sr\") {\n                region = \"RS\";\n            }\n            const params = {\n                country: region,\n                locale: `${language}_${region}`,\n                search_term: this.searchTerm,\n            };\n            if (this.next) {\n                params.position = this.next;\n            }\n            const res = await this.sequential(() => {\n                this.state.loadingGif = true;\n                const res = rpc(\"/discuss/gif/search\", params, {\n                    silent: true,\n                });\n                this.state.loadingGif = false;\n                return res;\n            });\n            if (res) {\n                const { next, results } = res;\n                this.next = next;\n                for (const gif of results) {\n                    this.pushGif(gif);\n                }\n                this.state.loadingError = false;\n            }\n        } catch {\n            this.state.loadingError = true;\n        }\n    }\n\n    /**\n     * @param {TenorGif} gif\n     */\n    pushGif(gif) {\n        if (this.state.evenGif.columnSize <= this.state.oddGif.columnSize) {\n            this.state.evenGif.gifs.set(gif.id, gif);\n            this.state.evenGif.columnSize += gif.media_formats.tinygif.dims[1];\n        } else {\n            this.state.oddGif.gifs.set(gif.id, gif);\n            this.state.oddGif.columnSize += gif.media_formats.tinygif.dims[1];\n        }\n    }\n\n    /**\n     * @param {TenorGif} gif\n     */\n    onClickGif(gif) {\n        this.props.onSelect(gif, true);\n        this.props.close?.();\n    }\n\n    clear() {\n        this.state.evenGif.gifs.clear();\n        this.state.evenGif.columnSize = 0;\n        this.state.oddGif.gifs.clear();\n        this.state.oddGif.columnSize = 0;\n    }\n\n    /**\n     * @param {TenorCategory} category\n     */\n    async onClickCategory(category) {\n        this.clear();\n        this.searchTerm = category.searchterm;\n        this.inputRef.el?.focus();\n        this.closeCategories();\n    }\n\n    /**\n     * @param {TenorGif} gif\n     */\n    async onClickFavorite(gif) {\n        if (!this.isFavorite(gif)) {\n            this.state.favorites.gifs.push(gif);\n            await this.orm.silent.create(\"discuss.gif.favorite\", [{ tenor_gif_id: gif.id }]);\n        } else {\n            const index = this.state.favorites.gifs.findIndex(({ id }) => id === gif.id);\n            if (index >= 0) {\n                this.state.favorites.gifs.splice(index, 1);\n            }\n            await rpc(\"/discuss/gif/remove_favorite\", { tenor_gif_id: gif.id }, { silent: true });\n        }\n    }\n\n    async loadFavorites() {\n        if (!this.store.hasGifPickerFeature) {\n            return;\n        }\n        this.state.loadingGif = true;\n        try {\n            const [results] = await rpc(\n                \"/discuss/gif/favorites\",\n                { offset: this.offset },\n                { silent: true }\n            );\n            this.offset += 20;\n            this.state.favorites.gifs.push(...results);\n        } catch {\n            this.state.loadingError = true;\n        }\n        this.state.loadingGif = false;\n    }\n\n    /**\n     * @param {TenorGif} gif\n     */\n    isFavorite(gif) {\n        return this.state.favorites.gifs.map((favorite) => favorite.id).includes(gif.id);\n    }\n\n    onClickFavoritesCategory() {\n        this.showFavorite = true;\n        for (const gif of this.state.favorites.gifs) {\n            this.pushGif(gif);\n        }\n        this.closeCategories();\n    }\n}\n", "import { Store } from \"@mail/core/common/store_service\";\n\nimport { patch } from \"@web/core/utils/patch\";\n\n/** @type {import(\"models\").Store} */\nconst StorePatch = {\n    setup() {\n        super.setup(...arguments);\n        this.hasGifPickerFeature = false;\n    },\n};\npatch(Store.prototype, StorePatch);\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { registerMessageAction } from \"@mail/core/common/message_actions\";\n\nregisterMessageAction(\"pin\", {\n    condition: ({ store, thread }) => store.self_partner && thread?.model === \"discuss.channel\",\n    icon: \"fa fa-thumb-tack\",\n    name: ({ message }) => (message.pinned_at ? _t(\"Unpin\") : _t(\"Pin\")),\n    onSelected: ({ message }) => message.pin(),\n    sequence: 65,\n});\n", "import { patch } from \"@web/core/utils/patch\";\nimport { Message } from \"@mail/core/common/message_model\";\nimport { fields } from \"@mail/core/common/record\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { MessageConfirmDialog } from \"@mail/core/common/message_confirm_dialog\";\nimport { Deferred } from \"@web/core/utils/concurrency\";\n\npatch(Message.prototype, {\n    setup() {\n        super.setup();\n        this.pinned_at = fields.Datetime();\n    },\n    /** @returns {Deferred<boolean>} */\n    pin() {\n        if (this.pinned_at) {\n            return this.unpin();\n        }\n        const def = new Deferred();\n        this.store.env.services.dialog.add(\n            MessageConfirmDialog,\n            {\n                confirmText: _t(\"Yeah, pin it!\"),\n                message: this,\n                prompt: _t(\n                    \"You sure want this message pinned to %(conversation)s forever and ever?\",\n                    {\n                        conversation: this.thread.prefix + this.thread.displayName,\n                    }\n                ),\n                size: \"md\",\n                title: _t(\"Pin It\"),\n                onConfirm: () => {\n                    def.resolve(true);\n                    this.store.env.services.orm.call(\n                        \"discuss.channel\",\n                        \"set_message_pin\",\n                        [this.thread.id],\n                        { message_id: this.id, pinned: true }\n                    );\n                },\n            },\n            { onClose: () => def.resolve(false) }\n        );\n        return def;\n    },\n    /** @returns {Deferred<boolean>} */\n    unpin() {\n        const def = new Deferred();\n        this.store.env.services.dialog.add(\n            MessageConfirmDialog,\n            {\n                confirmColor: \"btn-danger\",\n                confirmText: _t(\"Yes, remove it please\"),\n                message: this,\n                prompt: _t(\n                    \"Well, nothing lasts forever, but are you sure you want to unpin this message?\"\n                ),\n                size: \"md\",\n                title: _t(\"Unpin Message\"),\n                onConfirm: () => {\n                    def.resolve(true);\n                    this.store.env.services.orm.call(\n                        \"discuss.channel\",\n                        \"set_message_pin\",\n                        [this.thread.id],\n                        { message_id: this.id, pinned: false }\n                    );\n                },\n            },\n            { onClose: () => def.resolve(false) }\n        );\n        return def;\n    },\n});\n", "import { Message } from \"@mail/core/common/message\";\n\nimport { patch } from \"@web/core/utils/patch\";\n\npatch(Message, {\n    components: { ...Message.components },\n});\n\npatch(Message.prototype, {\n    get isAlignedRight() {\n        return !this.env.messageCard && super.isAlignedRight;\n    },\n    get shouldDisplayAuthorName() {\n        if (this.env.messageCard) {\n            return true;\n        }\n        return super.shouldDisplayAuthorName;\n    },\n});\n", "import { NotificationMessage } from \"@mail/core/common/notification_message\";\n\nimport { patch } from \"@web/core/utils/patch\";\n\npatch(NotificationMessage.prototype, {\n    /**\n     * @override\n     * @param {MouseEvent} ev\n     */\n    async onClickNotificationMessage(ev) {\n        const { oeType } = ev.target.dataset;\n        if (oeType === \"pin-menu\") {\n            this.env.pinMenu?.open();\n        }\n        await super.onClickNotificationMessage(...arguments);\n    },\n});\n", "import { MessageCardList } from \"@mail/core/common/message_card_list\";\nimport { ActionPanel } from \"@mail/discuss/core/common/action_panel\";\n\nimport { Component, onWillStart, onWillUpdateProps } from \"@odoo/owl\";\nimport { _t } from \"@web/core/l10n/translation\";\n\n/**\n * @typedef {Object} Props\n * @property {import(\"@mail/core/common/thread_model\").Thread} thread\n * @property {string} [className]\n * @extends {Component<Props, Env>}\n */\nexport class PinnedMessagesPanel extends Component {\n    static components = {\n        MessageCardList,\n        ActionPanel,\n    };\n    static props = [\"thread\", \"className?\"];\n    static template = \"discuss.PinnedMessagesPanel\";\n\n    setup() {\n        super.setup();\n        onWillStart(() => {\n            this.props.thread.fetchPinnedMessages();\n        });\n        onWillUpdateProps((nextProps) => {\n            if (nextProps.thread.notEq(this.props.thread)) {\n                nextProps.thread.fetchPinnedMessages();\n            }\n        });\n    }\n\n    /**\n     * Get the message to display when nothing is pinned on this thread.\n     */\n    get emptyText() {\n        if (this.props.thread.channel_type === \"channel\") {\n            return _t(\"This channel doesn't have any pinned messages.\");\n        } else {\n            return _t(\"This conversation doesn't have any pinned messages.\");\n        }\n    }\n}\n", "import { registerThreadAction } from \"@mail/core/common/thread_actions\";\nimport { PinnedMessagesPanel } from \"@mail/discuss/message_pin/common/pinned_messages_panel\";\n\nimport { useChildSubEnv } from \"@odoo/owl\";\n\nimport { _t } from \"@web/core/l10n/translation\";\n\nregisterThreadAction(\"pinned-messages\", {\n    actionPanelComponent: PinnedMessagesPanel,\n    condition: ({ owner, thread }) =>\n        thread?.model === \"discuss.channel\" &&\n        (!owner.props.chatWindow || owner.props.chatWindow.isOpen) &&\n        !owner.isDiscussSidebarChannelActions,\n    panelOuterClass: \"o-discuss-PinnedMessagesPanel bg-inherit\",\n    icon: \"fa fa-fw fa-thumb-tack\",\n    name: ({ action }) => (action.isActive ? _t(\"Hide Pinned Messages\") : _t(\"Pinned Messages\")),\n    sequence: 20,\n    sequenceGroup: 10,\n    setup() {\n        useChildSubEnv({\n            pinMenu: {\n                open: () => this.open(),\n                close: () => {\n                    if (this.isActive) {\n                        this.close();\n                    }\n                },\n            },\n        });\n    },\n    toggle: true,\n});\n", "import { patch } from \"@web/core/utils/patch\";\nimport { fields } from \"@mail/core/common/record\";\nimport { Thread } from \"@mail/core/common/thread_model\";\n\nimport { rpc } from \"@web/core/network/rpc\";\n\npatch(Thread.prototype, {\n    setup() {\n        super.setup();\n\n        /** @type {'loaded'|'loading'|'error'|undefined} */\n        this.pinnedMessagesState = undefined;\n        this.pinnedMessages = fields.Many(\"mail.message\", {\n            compute() {\n                return this.allMessages.filter((m) => m.pinned_at);\n            },\n            sort: (m1, m2) => {\n                if (m1.pinned_at === m2.pinned_at) {\n                    return m1.id - m2.id;\n                }\n                return m1.pinned_at < m2.pinned_at ? 1 : -1;\n            },\n        });\n    },\n\n    /**\n     * @param {import(\"models\").Thread} channel\n     */\n    async fetchPinnedMessages() {\n        if (\n            this.model !== \"discuss.channel\" ||\n            [\"loaded\", \"loading\"].includes(this.pinnedMessagesState)\n        ) {\n            return;\n        }\n        this.pinnedMessagesState = \"loading\";\n        let data;\n        try {\n            data = await rpc(\"/discuss/channel/pinned_messages\", {\n                channel_id: this.id,\n            });\n        } catch (e) {\n            this.pinnedMessagesState = \"error\";\n            throw e;\n        }\n        this.store.insert(data);\n        this.pinnedMessagesState = \"loaded\";\n    },\n});\n", "import { Composer } from \"@mail/core/common/composer\";\nimport { Typing } from \"@mail/discuss/typing/common/typing\";\nimport { rpc } from \"@web/core/network/rpc\";\n\nimport { onWillDestroy } from \"@odoo/owl\";\nimport { browser } from \"@web/core/browser/browser\";\nimport { registry } from \"@web/core/registry\";\nimport { patch } from \"@web/core/utils/patch\";\nimport { useDebounced } from \"@web/core/utils/timing\";\n\nconst commandRegistry = registry.category(\"discuss.channel_commands\");\n\nexport const SHORT_TYPING = 5000;\nexport const LONG_TYPING = 50000;\n\npatch(Composer, {\n    components: { ...Composer.components, Typing },\n});\n\npatch(Composer.prototype, {\n    /**\n     * @override\n     */\n    setup() {\n        super.setup();\n        this.typingNotified = false;\n        this.stopTypingDebounced = useDebounced(this.stopTyping.bind(this), SHORT_TYPING);\n        onWillDestroy(() => {\n            this.stopTyping();\n        });\n    },\n    /**\n     * Notify the server of the current typing status\n     *\n     * @param {boolean} [is_typing=true]\n     */\n    notifyIsTyping(is_typing = true) {\n        if (this.thread?.model === \"discuss.channel\" && this.thread.id > 0) {\n            rpc(\n                \"/discuss/channel/notify_typing\",\n                {\n                    channel_id: this.thread.id,\n                    is_typing,\n                },\n                { silent: true }\n            );\n        }\n    },\n    /** @override */\n    onInput(ev) {\n        super.onInput(ev);\n        this.detectTyping(ev);\n    },\n    detectTyping() {\n        const value = this.props.composer.composerText;\n        if (this.thread?.model === \"discuss.channel\" && value.startsWith(\"/\")) {\n            const [firstWord] = value.substring(1).split(/\\s/);\n            const command = commandRegistry.get(firstWord, false);\n            if (\n                value === \"/\" || // suggestions not yet started\n                this.hasSuggestions ||\n                (command &&\n                    (!command.channel_types ||\n                        command.channel_types.includes(this.thread.channel_type)))\n            ) {\n                this.stopTyping();\n                return;\n            }\n        }\n        if (!this.typingNotified && value) {\n            this.typingNotified = true;\n            this.notifyIsTyping();\n            browser.setTimeout(() => (this.typingNotified = false), LONG_TYPING);\n        }\n        this.stopTypingDebounced();\n    },\n    /**\n     * @override\n     */\n    async sendMessage() {\n        await super.sendMessage();\n        this.stopTyping();\n    },\n    stopTyping() {\n        if (this.typingNotified) {\n            this.typingNotified = false;\n            this.notifyIsTyping(false);\n        }\n    },\n    addEmoji(str) {\n        const res = super.addEmoji(str);\n        this.detectTyping();\n        return res;\n    },\n});\n", "import { ThreadIcon } from \"@mail/core/common/thread_icon\";\nimport { Typing } from \"@mail/discuss/typing/common/typing\";\n\nimport { patch } from \"@web/core/utils/patch\";\n\npatch(ThreadIcon, {\n    components: { ...ThreadIcon.components, Typing },\n});\n", "import { Component } from \"@odoo/owl\";\n\nimport { _t } from \"@web/core/l10n/translation\";\n\n/**\n * @typedef {Object} Props\n * @property {import(\"models\").Thread} channel\n * @property {string} [size]\n * @property {boolean} [displayText]\n * @extends {Component<Props, Env>}\n */\nexport class Typing extends Component {\n    static defaultProps = {\n        size: \"small\",\n        displayText: true,\n    };\n    static props = [\"channel?\", \"size?\", \"displayText?\", \"member?\"];\n    static template = \"discuss.Typing\";\n\n    /** @returns {string} */\n    get text() {\n        const typingMemberNames = this.props.member\n            ? [this.props.member.name]\n            : this.props.channel.otherTypingMembers.map(({ name }) => name);\n        if (typingMemberNames.length === 1) {\n            return _t(\"%s is typing...\", typingMemberNames[0]);\n        }\n        if (typingMemberNames.length === 2) {\n            return _t(\"%(user1)s and %(user2)s are typing...\", {\n                user1: typingMemberNames[0],\n                user2: typingMemberNames[1],\n            });\n        }\n        return _t(\"%(user1)s, %(user2)s and more are typing...\", {\n            user1: typingMemberNames[0],\n            user2: typingMemberNames[1],\n        });\n    }\n}\n", "import { AttachmentList } from \"@mail/core/common/attachment_list\";\nimport { VoicePlayer } from \"@mail/discuss/voice_message/common/voice_player\";\nimport { patch } from \"@web/core/utils/patch\";\n\npatch(AttachmentList, {\n    components: { ...AttachmentList.components, VoicePlayer },\n});\n", "import { Attachment } from \"@mail/core/common/attachment_model\";\nimport { fields } from \"@mail/core/common/record\";\nimport { patch } from \"@web/core/utils/patch\";\n\n/** @type {import(\"models\").Attachment} */\nconst attachmentPatch = {\n    setup() {\n        this.voice_ids = fields.Many(\"discuss.voice.metadata\");\n    },\n    get isViewable() {\n        return !this.voice && super.isViewable;\n    },\n    delete() {\n        if (this.voice && this.id > 0) {\n            this.store.env.services[\"discuss.voice_message\"].activePlayer = null;\n        }\n        super.delete(...arguments);\n    },\n    onClickAttachment(attachment) {\n        if (!attachment.voice) {\n            super.onClickAttachment(attachment);\n        }\n    },\n    get voice() {\n        return this.voice_ids.length > 0;\n    },\n};\npatch(Attachment.prototype, attachmentPatch);\n", "import { AttachmentUploadService } from \"@mail/core/common/attachment_upload_service\";\n\nimport { patch } from \"@web/core/utils/patch\";\n\npatch(AttachmentUploadService.prototype, {\n    _makeAttachmentData(upload, tmpId, thread, tmpUrl) {\n        const attachmentData = super._makeAttachmentData(...arguments);\n        if (upload.data.get(\"voice\")) {\n            attachmentData.voice_ids = [this.store[\"discuss.voice.metadata\"].insert({ id: -1 })];\n        }\n        return attachmentData;\n    },\n    _buildFormData(formData, tmpURL, thread, composer, tmpId, options) {\n        super._buildFormData(...arguments);\n        if (options?.voice) {\n            formData.append(\"voice\", true);\n        }\n        return formData;\n    },\n});\n", "import { registerComposerAction } from \"@mail/core/common/composer_actions\";\nimport { Component, xml } from \"@odoo/owl\";\nimport { _t } from \"@web/core/l10n/translation\";\n\nregisterComposerAction(\"voice-start\", {\n    condition: ({ composer, owner }) =>\n        composer.targetThread?.model === \"discuss.channel\" &&\n        owner.voiceRecorder &&\n        !owner.voiceRecorder?.recording &&\n        !composer.voiceAttachment,\n    icon: \"fa fa-microphone\",\n    name: _t(\"Voice Message\"),\n    onSelected: ({ owner }) => owner.voiceRecorder.onClick(),\n    sequence: 10,\n});\nregisterComposerAction(\"voice-stop\", {\n    condition: ({ composer, owner }) =>\n        composer.targetThread?.model === \"discuss.channel\" && owner.voiceRecorder?.recording,\n    icon: \"fa fa-circle text-danger o-mail-VoiceRecorder-dot\",\n    name: _t(\"Stop Recording\"),\n    onSelected: ({ owner }) => owner.voiceRecorder.onClick(),\n    sequence: 10,\n});\nregisterComposerAction(\"voice-recording\", {\n    component: class VoiceMessageRecordingButton extends Component {\n        static props = [\"composer\", \"state\"];\n        static template = xml`\n            <button class=\"o-mail-VoiceRecorder d-flex align-items-center btn border-0 o-recording rounded-start-0 rounded-end user-select-none p-0\" t-att-title=\"title\" t-att-disabled=\"props.state.isActionPending or props.composer.voiceAttachment\" t-on-click=\"props.state.onClick\">\n                <div class=\"o-mail-VoiceRecorder-elapsed o-active recording ms-2 me-1\" t-att-class=\"{ 'text-danger': props.state.limitWarning }\" style=\"font-variant-numeric: tabular-nums;\">\n                    <span class=\"d-flex text-truncate\" t-esc=\"props.state.elapsed\"/>\n                </div>\n                <span class=\"rounded-circle p-1\"><i class=\"fa fa-fw fa-circle text-danger o-mail-VoiceRecorder-dot\"/></span>\n            </button>\n        `;\n        get title() {\n            return _t(\"Stop Recording\");\n        }\n    },\n    componentProps: ({ composer, owner }) => ({ composer, state: owner.voiceRecorder }),\n    condition: ({ composer, owner }) =>\n        composer.targetThread?.model === \"discuss.channel\" && owner.voiceRecorder?.recording,\n    sequenceQuick: 10,\n});\n", "import { Composer } from \"@mail/core/common/composer_model\";\n\nimport { patch } from \"@web/core/utils/patch\";\n\npatch(Composer.prototype, {\n    /** @returns {import(\"models\").Attachment|undefined} */\n    get voiceAttachment() {\n        return this.attachments.find((attachment) => attachment.voice);\n    },\n});\n", "import { Composer } from \"@mail/core/common/composer\";\nimport { patch } from \"@web/core/utils/patch\";\nimport { useVoiceRecorder } from \"./voice_recorder\";\n\npatch(Composer, {\n    components: { ...Composer.components },\n});\n\npatch(Composer.prototype, {\n    setup() {\n        super.setup();\n        this.voiceRecorder = useVoiceRecorder();\n    },\n    get isSendButtonDisabled() {\n        return this.voiceRecording?.recording || super.isSendButtonDisabled;\n    },\n    onKeydown(ev) {\n        if (ev.key === \"Enter\" && this.voiceRecording?.recording) {\n            ev.preventDefault();\n            return;\n        }\n        return super.onKeydown(ev);\n    },\n});\n", "const MAX_SAMPLES = 1152;\n\nexport class Mp3Encoder {\n    /** @type {Object} */\n    config;\n    /** @type {boolean} */\n    encoding;\n    /** @type {lameJs.Mp3Encoder} */\n    mp3Encoder;\n    /** @type {Int16Array} */\n    samplesMono;\n\n    constructor(config = {}) {\n        this.config = {\n            sampleRate: 44100,\n            bitRate: 128,\n        };\n        Object.assign(this.config, config);\n        // eslint-disable-next-line no-undef\n        this.mp3Encoder = new lamejs.Mp3Encoder(1, this.config.sampleRate, this.config.bitRate);\n        this.samplesMono = null;\n        this.clearBuffer();\n    }\n\n    clearBuffer() {\n        this.dataBuffer = [];\n    }\n\n    appendToBuffer(buffer) {\n        this.dataBuffer.push(new Int8Array(buffer));\n    }\n\n    floatTo16BitPCM(input, output) {\n        for (let i = 0; i < input.length; i++) {\n            const s = Math.max(-1, Math.min(1, input[i]));\n            output[i] = s < 0 ? s * 0x8000 : s * 0x7fff;\n        }\n    }\n\n    convertBuffer(arrayBuffer) {\n        const data = new Float32Array(arrayBuffer);\n        const out = new Int16Array(arrayBuffer.length);\n        this.floatTo16BitPCM(data, out);\n        return out;\n    }\n\n    encode(arrayBuffer) {\n        this.encoding = true;\n        this.samplesMono = this.convertBuffer(arrayBuffer);\n        let remaining = this.samplesMono.length;\n        for (let i = 0; remaining >= 0; i += MAX_SAMPLES) {\n            const left = this.samplesMono.subarray(i, i + MAX_SAMPLES);\n            const mp3buffer = this.mp3Encoder.encodeBuffer(left);\n            this.appendToBuffer(mp3buffer);\n            remaining -= MAX_SAMPLES;\n        }\n    }\n\n    finish() {\n        if (this.encoding) {\n            this.appendToBuffer(this.mp3Encoder.flush());\n            return this.dataBuffer;\n        } else {\n            return [];\n        }\n    }\n}\n", "import { loadBundle } from \"@web/core/assets\";\nimport { registry } from \"@web/core/registry\";\nimport { memoize } from \"@web/core/utils/functions\";\n\nconst loader = {\n    loadLamejs: memoize(() => loadBundle(\"mail.assets_lamejs\")),\n};\n\nexport async function loadLamejs() {\n    try {\n        await loader.loadLamejs();\n    } catch {\n        // Could be intentional (tour ended successfully while lamejs still loading)\n    }\n}\n\nexport class VoiceMessageService {\n    constructor(env) {\n        /** @type {import(\"@mail/discuss/voice_message/common/voice_player\").VoicePlayer} */\n        this.activePlayer = null;\n    }\n}\n\nexport const voiceMessageService = {\n    start(env) {\n        return new VoiceMessageService(env);\n    },\n};\n\nregistry.category(\"services\").add(\"discuss.voice_message\", voiceMessageService);\n", "import { fields } from \"@mail/model/misc\";\nimport { Record } from \"@mail/model/record\";\n\nexport class VoiceMetadata extends Record {\n    static _name = \"discuss.voice.metadata\";\n    static id = \"id\";\n\n    attachment_id = fields.One(\"ir.attachment\", { inverse: \"voice_ids\" });\n}\n\nVoiceMetadata.register();\n", "import {\n    Component,\n    useState,\n    onMounted,\n    onWillUnmount,\n    useEffect,\n    useRef,\n    status,\n} from \"@odoo/owl\";\nimport { browser } from \"@web/core/browser/browser\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { url } from \"@web/core/utils/urls\";\n\nconst WAVE_COLOR = \"#7775\";\n\n/**\n * @typedef {Object} Props\n * @property {import(\"models\").Attachment} attachment\n * @extends {Component<Props, Env>}\n */\nexport class VoicePlayer extends Component {\n    static props = [\"attachment\"];\n    static template = \"mail.VoicePlayer\";\n\n    /** @type {number} */\n    lastPlaytime = 0;\n    /** @type {number} */\n    lastPos = 0;\n    /** @type {number} */\n    startPosition = 0;\n    /** @type {string} */\n    progressColor;\n    /** @type {GainNode} */\n    gainNode;\n    /** @type {AudioContext} */\n    audioCtx;\n    scheduledPause;\n    /** @type {AudioBuffer} */\n    buffer;\n    /** @type {AnalyserNode} */\n    analyser;\n    /** @type {AudioBufferSourceNode} */\n    source;\n    /** @type {number} */\n    width;\n    /** @type {number} */\n    height;\n    /** @type {HTMLElement} */\n    wrapper;\n    /** @type {HTMLElement} */\n    progressWave;\n    /** @type {CanvasRenderingContext2D} */\n    waveCtx;\n    /** @type {CanvasRenderingContext2D} */\n    progressCtx;\n\n    setup() {\n        super.setup();\n        this.wrapperRef = useRef(\"wrapper\");\n        this.drawerRef = useRef(\"drawer\");\n        this.waveRef = useRef(\"wave\");\n        this.progressRef = useRef(\"progress\");\n        /** @type {import(\"@mail/discuss/voice_message/common/voice_message_service\").VoiceMessageService} */\n        this.voiceMessageService = useService(\"discuss.voice_message\");\n        this.state = useState({\n            paused: true,\n            playing: false,\n            repeat: false,\n            visualTime: \"-- : --\",\n        });\n        useEffect(\n            (playing) => {\n                if (playing) {\n                    this.addOnAudioProcess();\n                }\n            },\n            () => [this.state.playing]\n        );\n        useEffect(\n            (uploading) => {\n                if (uploading) {\n                    return;\n                }\n                if (this.wasUploading && !uploading) {\n                    this.makeAudio();\n                }\n                this.wasUploading = uploading;\n            },\n            () => [this.props.attachment.uploading]\n        );\n        onMounted(() => {\n            this.initElements();\n            this.wrapper.addEventListener(\"click\", (e) => {\n                if (this.props.attachment.uploading) {\n                    return;\n                }\n                const clientX = (e.targetTouches ? e.targetTouches[0] : e).clientX;\n                const bcr = this.wrapper.getBoundingClientRect();\n                const progressPixels = clientX - bcr.left;\n                const progress = Math.min(\n                    Math.max(0, progressPixels / this.wrapper.scrollWidth),\n                    1\n                );\n                this.seekTo(progress);\n            });\n            if (!this.props.attachment.uploading) {\n                this.makeAudio();\n            }\n            this.wasUploading = this.props.attachment.uploading;\n        });\n        onWillUnmount(() => {\n            if (this.state.playing) {\n                this.pause();\n            }\n            this.destroyWebAudio();\n        });\n    }\n\n    makeAudio() {\n        this.audioCtx = new browser.AudioContext();\n        this.gainNode = this.audioCtx.createGain();\n        this.gainNode.connect(this.audioCtx.destination);\n        this.analyser = this.audioCtx.createAnalyser();\n        this.analyser.connect(this.gainNode);\n        this.fetchFile(\n            url(this.props.attachment.urlRoute, {\n                ...this.props.attachment.urlQueryParams,\n            })\n        ).then((arrayBuffer) => this.drawBuffer(arrayBuffer));\n    }\n\n    _fetch(...args) {\n        return fetch(...args);\n    }\n\n    async fetchFile(url) {\n        const response = await this._fetch(url);\n        if (!response.ok) {\n            throw new Error(\"HTTP error status: \" + response.status);\n        }\n        const arrayBuffer = await response.arrayBuffer();\n        return arrayBuffer;\n    }\n\n    getPlayedTime() {\n        return this.audioCtx.currentTime - this.lastPlaytime;\n    }\n\n    getCurrentTime() {\n        if (this.state.paused) {\n            return this.startPosition;\n        } else {\n            return this.startPosition + this.getPlayedTime();\n        }\n    }\n\n    play() {\n        if (this.voiceMessageService.activePlayer) {\n            this.voiceMessageService.activePlayer.pause();\n        }\n        this.voiceMessageService.activePlayer = this;\n        this.state.repeat = false;\n        this.createSource();\n        const { start, end } = this.seekToElapsed();\n        this.scheduledPause = end;\n        this.source.start(0, start);\n        this.state.playing = true;\n        this.state.paused = false;\n    }\n\n    pause(options) {\n        this.voiceMessageService.activePlayer = null;\n        if (options?.end) {\n            this.state.repeat = true;\n        }\n        this.scheduledPause = null;\n        this.startPosition += this.getPlayedTime();\n        if (this.source) {\n            try {\n                this.source.stop();\n            } catch (e) {\n                if (e.name === \"InvalidStateError\") {\n                    return;\n                }\n                throw e;\n            }\n        }\n        if (!options?.continue) {\n            this.state.paused = true;\n            this.state.playing = false;\n        }\n    }\n\n    getPeaks() {\n        const peaks = [];\n        const sampleSize = this.buffer.length / this.width;\n        const sampleStep = Math.floor(sampleSize / 10);\n        const chan = this.buffer.getChannelData(0);\n        let i;\n        for (i = 0; i < this.width; i++) {\n            const start = Math.floor(i * sampleSize);\n            const end = Math.floor(start + sampleSize);\n            let min = chan[start];\n            let max = min;\n            let j;\n            for (j = start; j < end; j += sampleStep) {\n                const value = chan[j];\n                if (value > max) {\n                    max = value;\n                }\n                if (value < min) {\n                    min = value;\n                }\n            }\n            peaks[i] = max;\n        }\n        return peaks;\n    }\n\n    createSource() {\n        this.source?.disconnect();\n        this.source = this.audioCtx.createBufferSource();\n        this.source.buffer = this.buffer;\n        this.source.connect(this.analyser);\n    }\n\n    /**\n     * @param {number} [start] float representing start time\n     * @param {number} [end] float representing end time\n     * @returns {Object} res\n     * @returns {number} res.start\n     * @returns {number} res.end\n     */\n    seekToElapsed(start, end) {\n        this.scheduledPause = null;\n        if (start === undefined) {\n            start = this.getCurrentTime();\n            if (start >= this.buffer.duration) {\n                start = 0;\n            }\n        }\n        if (end === undefined) {\n            end = this.buffer.duration;\n        }\n        this.startPosition = start;\n        this.lastPlaytime = this.audioCtx.currentTime;\n        return { start, end };\n    }\n\n    onProgress(progress) {\n        const position = Math.round(progress * this.width);\n        if (position < this.lastPos || position - this.lastPos >= 1) {\n            this.lastPos = position;\n            this.progressWave.style.width = position + \"px\";\n        }\n    }\n\n    seekTo(progress) {\n        if (this.state.playing) {\n            this.pause({ continue: true });\n        }\n        this.state.repeat = false;\n        const elapsedTime = progress * this.buffer.duration;\n        this.state.visualTime = this.generateTime(Math.floor(elapsedTime));\n        this.seekToElapsed(elapsedTime);\n        this.onProgress(progress);\n        if (this.state.playing) {\n            this.play();\n        }\n    }\n\n    async drawBuffer(arrayBuffer) {\n        const buffer = await this.audioCtx.decodeAudioData(arrayBuffer);\n        this.state.visualTime = this.generateTime(Math.floor(buffer.duration));\n        this.startPosition = 0;\n        this.lastPlaytime = this.audioCtx.currentTime;\n        this.buffer = buffer;\n        this.createSource();\n        this.drawWave(this.getPeaks());\n    }\n\n    async destroyWebAudio() {\n        this.source?.disconnect();\n        this.gainNode?.disconnect();\n        this.analyser?.disconnect();\n        try {\n            await this.audioCtx?.close();\n        } catch (e) {\n            if (e.name === \"InvalidStateError\") {\n                return;\n            }\n            throw e;\n        }\n    }\n\n    addOnAudioProcess() {\n        if (status(this) === \"destroyed\") {\n            return;\n        }\n        const time = this.getCurrentTime();\n        if (time >= this.scheduledPause && this.state.playing) {\n            this.pause({ end: true });\n        } else if (this.state.playing) {\n            this.state.visualTime = this.generateTime(Math.floor(time));\n            const playedPercents = this.getCurrentTime() / this.buffer.duration;\n            this.onProgress(playedPercents);\n            requestAnimationFrame(() => this.addOnAudioProcess());\n        }\n    }\n\n    generateTime(timeInSecond) {\n        const second = timeInSecond % 60;\n        const minute = Math.floor(timeInSecond / 60);\n        return (\n            (minute < 10 ? \"0\" + minute : minute) + \" : \" + (second < 10 ? \"0\" + second : second)\n        );\n    }\n\n    initElements() {\n        this.wrapper = this.wrapperRef.el;\n        this.progressWave = this.drawerRef.el;\n        this.progressColor = getComputedStyle(this.wrapper).getPropertyValue(\"--primary\");\n        this.width = this.wrapper.clientWidth;\n        this.height = this.wrapper.clientHeight;\n\n        const wave = this.waveRef.el;\n        wave.width = this.width;\n        wave.height = this.height;\n        this.waveCtx = wave.getContext(\"2d\");\n        this.waveCtx.fillStyle = WAVE_COLOR;\n\n        const progress = this.progressRef.el;\n        progress.width = this.width;\n        progress.height = this.height;\n        this.progressCtx = progress.getContext(\"2d\");\n        this.progressCtx.fillStyle = this.progressColor;\n    }\n\n    drawWave(peaks) {\n        return requestAnimationFrame(() => {\n            this.drawLines(peaks);\n            this.fillRect(0, this.height / 2, this.width, 0.5);\n        });\n    }\n\n    fillRect(x, y, width, height) {\n        const intersection = {\n            x1: x,\n            y1: y,\n            x2: x + width,\n            y2: y + height,\n        };\n        if (intersection.x1 < intersection.x2) {\n            this.fillRects(\n                intersection.x1,\n                intersection.y1,\n                intersection.x2 - intersection.x1,\n                intersection.y2 - intersection.y1\n            );\n        }\n    }\n\n    fillRects(x, y, width, height) {\n        this.waveCtx.fillRect(x, y, width, height);\n        this.progressCtx.fillRect(x, y, width, height);\n    }\n\n    drawLines(peaks) {\n        this.drawLineToContext(this.waveCtx, peaks);\n        this.drawLineToContext(this.progressCtx, peaks);\n    }\n\n    drawLineToContext(ctx, peaks) {\n        const maxPeak = Math.max(...peaks);\n        let i, peak;\n        for (i = 0; i <= peaks.length; i++) {\n            peak = peaks[i];\n            const h = (peak * this.height) / maxPeak;\n            ctx.fillRect(i, (this.height - h) / 2, 1.5, h);\n        }\n    }\n\n    onClickPlayPause() {\n        if (this.props.attachment.uploading) {\n            return;\n        }\n        if (this.state.paused) {\n            this.play();\n        } else {\n            this.pause();\n        }\n    }\n}\n", "import { useState, onWillUnmount, status, useComponent } from \"@odoo/owl\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { browser } from \"@web/core/browser/browser\";\nimport { Mp3Encoder } from \"./mp3_encoder\";\nimport { loadLamejs } from \"@mail/discuss/voice_message/common/voice_message_service\";\n\nexport const patchable = {\n    makeFile(file) {\n        return file;\n    },\n};\n\nexport function useVoiceRecorder() {\n    /** @type {MediaStream} */\n    let microphone;\n    /** @type {number} */\n    let startTimeStamp;\n    /** @type {AudioContext} */\n    let audioContext;\n    /** @type {MediaStreamAudioSourceNode} */\n    let streamSource;\n    /** @type {AudioWorkletNode} */\n    let processor;\n    /** @type {Mp3Encoder} */\n    let encoder;\n\n    const component = useComponent();\n    const state = useState({\n        limitWarning: false,\n        isActionPending: false,\n        recording: component.props.state?.recording ?? false,\n        elapsed: \"00 : 00\",\n        onClick() {\n            if (state.recording) {\n                stopRecording();\n            } else {\n                startRecording();\n            }\n        },\n    });\n    /** @type {ReturnType<typeof import(\"@web/core/notifications/notification_service\").notificationService.start>} */\n    const notification = useService(\"notification\");\n    const store = useService(\"mail.store\");\n    const config = { bitRate: 128 }; // 128 or 160 kbit/s \u2013 mid-range bitrate quality\n    onWillUnmount(() => {\n        if (state.recording) {\n            notification.add(_t(\"Voice recording stopped\"), { type: \"warning\" });\n            stopRecording();\n        } else {\n            cleanUp();\n        }\n    });\n\n    function filename() {\n        return (\n            \"Voice-\" +\n            new Date().toISOString().split(\"T\")[0] +\n            \"-\" +\n            Math.floor(Math.random() * 100000) +\n            \".mp3\"\n        );\n    }\n\n    async function startRecording() {\n        if (state.isActionPending) {\n            return;\n        }\n        state.isActionPending = true;\n        if (!microphone) {\n            try {\n                microphone = await browser.navigator.mediaDevices.getUserMedia({\n                    audio: store.settings.audioConstraints,\n                });\n                if (status(component) === \"destroyed\") {\n                    cleanUp();\n                    return;\n                }\n            } catch {\n                notification.add(\n                    _t('\"%(hostname)s\" needs to access your microphone', {\n                        hostname: window.location.host,\n                    }),\n                    { type: \"warning\" }\n                );\n                state.isActionPending = false;\n                return;\n            }\n        }\n        state.elapsed = \"00 : 00\";\n        state.recording = true;\n        audioContext = new browser.AudioContext();\n\n        await loadLamejs();\n        await audioContext.audioWorklet.addModule(\"/discuss/voice/worklet_processor\");\n        processor = new browser.AudioWorkletNode(audioContext, \"processor\");\n        processor.port.onmessage = (e) => {\n            if (state.recording && !startTimeStamp) {\n                startTimeStamp = e.timeStamp;\n            }\n            if (!startTimeStamp) {\n                return;\n            }\n            const elapsedSeconds = Math.floor((e.timeStamp - startTimeStamp) / 1000);\n            const second = elapsedSeconds % 60;\n            const minute = Math.floor(elapsedSeconds / 60);\n            state.elapsed =\n                (minute < 10 ? \"0\" + minute : minute) +\n                \" : \" +\n                (second < 10 ? \"0\" + second : second);\n            if (elapsedSeconds > 55 && elapsedSeconds < 60) {\n                state.limitWarning = true;\n            }\n            if (elapsedSeconds === 60) {\n                notification.add(_t(\"The duration of voice messages is limited to 1 minute.\"), {\n                    type: \"warning\",\n                });\n                stopRecording();\n            }\n            if (!e.data) {\n                return;\n            }\n            _encode(e.data);\n        };\n        streamSource = audioContext.createMediaStreamSource(microphone);\n\n        // Start to get microphone data\n        streamSource.connect(processor);\n        processor.connect(audioContext.destination);\n        config.sampleRate = audioContext.sampleRate;\n        encoder = new Mp3Encoder(config);\n        state.isActionPending = false;\n    }\n\n    function _encode(data) {\n        encoder.encode(data);\n    }\n\n    function _getEncoderBuffer() {\n        return encoder.finish();\n    }\n\n    function _makeFile(buffer, type) {\n        return patchable.makeFile(new File(buffer, filename(), { type }));\n    }\n\n    function stopRecording() {\n        getMp3()\n            .then((buffer) => {\n                const file = _makeFile(buffer, \"audio/mp3\");\n                if (file.size === 0) {\n                    return;\n                }\n                component.attachmentUploader.uploadFile(file, { voice: true });\n            })\n            .catch(() => {});\n        cleanUp();\n    }\n\n    function cleanUp() {\n        if (processor && streamSource) {\n            // Clean up the Web Audio API resources.\n            streamSource.disconnect();\n            processor.disconnect();\n\n            if (audioContext && audioContext.state !== \"closed\") {\n                // If all references using audioContext are destroyed, context is\n                // closed automatically. DOMException is fired when trying to close again\n                audioContext.close();\n            }\n        }\n\n        startTimeStamp = false;\n        microphone?.getTracks().forEach((track) => track.stop());\n        microphone = null;\n        state.recording = false;\n        state.limitWarning = false;\n    }\n\n    function getMp3() {\n        const finalBuffer = _getEncoderBuffer();\n        return new Promise((resolve, reject) => {\n            if (finalBuffer.length === 0) {\n                reject(new Error(\"No buffer to send\"));\n            } else {\n                resolve(finalBuffer);\n                encoder.clearBuffer();\n            }\n        });\n    }\n\n    return state;\n}\n", "import { DiscussClientAction } from \"@mail/core/public_web/discuss_client_action\";\n\nimport { browser } from \"@web/core/browser/browser\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { patch } from \"@web/core/utils/patch\";\n\npatch(DiscussClientAction.prototype, {\n    setup() {\n        super.setup(...arguments);\n        this.rtc = useService(\"discuss.rtc\");\n    },\n    /**\n     * Checks if we are in a client action and if we have a query parameter requesting to join a call,\n     * if so, the call is joined on the current discuss thread.\n     */\n    async restoreDiscussThread() {\n        const hasFullScreenUrl = new URL(browser.location.href).searchParams.has(\"fullscreen\");\n        await super.restoreDiscussThread(...arguments);\n        const action = this.props.action;\n        if (!action) {\n            return;\n        }\n        const call = action.context?.call || action.params?.call;\n        if (call === \"accept\") {\n            await this.rtc.joinCall(this.store.discuss.thread);\n            return;\n        }\n        if (\n            hasFullScreenUrl &&\n            this.store.discuss.thread?.default_display_mode === \"video_full_screen\" &&\n            this.store.discuss.thread.rtc_session_ids.length > 0\n        ) {\n            this.joinCallWithDefaultSettings();\n        }\n    },\n    async joinCallWithDefaultSettings() {\n        const mute = browser.localStorage.getItem(\"discuss_call_preview_join_mute\") === \"true\";\n        const camera = browser.localStorage.getItem(\"discuss_call_preview_join_video\") === \"true\";\n        await this.rtc.toggleCall(this.store.discuss.thread, { audio: !mute, camera });\n        await this.rtc.enterFullscreen();\n    },\n});\n", "import { Thread } from \"@mail/core/common/thread_model\";\nimport { discussSidebarChannelIndicatorsRegistry } from \"@mail/discuss/core/public_web/discuss_sidebar_categories\";\n\nimport { Component } from \"@odoo/owl\";\nimport { useService } from \"@web/core/utils/hooks\";\n\n/**\n * @typedef {Object} Props\n * @property {import(models\").Thread} thread\n * @extends {Component<Props, Env>}\n */\nexport class DiscussSidebarCallIndicator extends Component {\n    static template = \"mail.DiscussSidebarCallIndicator\";\n    static props = { thread: { type: Thread } };\n    static components = {};\n\n    setup() {\n        super.setup();\n        this.store = useService(\"mail.store\");\n        this.rtc = useService(\"discuss.rtc\");\n    }\n}\n\ndiscussSidebarChannelIndicatorsRegistry.add(\"call-indicator\", DiscussSidebarCallIndicator);\n", "import { Component, useEffect, useState } from \"@odoo/owl\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { Thread } from \"@mail/core/common/thread_model\";\nimport { CALL_ICON_DEAFEN, CALL_ICON_MUTED } from \"@mail/discuss/call/common/call_actions\";\nimport { AvatarStack } from \"@mail/discuss/core/common/avatar_stack\";\nimport { useHover } from \"@mail/utils/common/hooks\";\nimport { useDropdownState } from \"@web/core/dropdown/dropdown_hooks\";\nimport { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { _t } from \"@web/core/l10n/translation\";\n\n/**\n * @typedef {Object} Props\n * @property {import(\"models\").Thread} thread\n * @extends {Component<Props, Env>}\n */\nexport class DiscussSidebarCallParticipants extends Component {\n    static template = \"mail.DiscussSidebarCallParticipants\";\n    static props = { thread: { type: Thread }, compact: { type: Boolean, optional: true } };\n    static components = { AvatarStack, DiscussSidebarCallParticipants, Dropdown };\n\n    setup() {\n        super.setup();\n        this.store = useService(\"mail.store\");\n        this.rtc = useService(\"discuss.rtc\");\n        this.hover = useHover([\"root\", \"floating\"], {\n            onHover: () => (this.floating.isOpen = true),\n            onAway: () => (this.floating.isOpen = false),\n        });\n        this.state = useState({ expanded: false });\n        this.floating = useDropdownState();\n        this.CALL_ICON_DEAFEN = CALL_ICON_DEAFEN;\n        this.CALL_ICON_MUTED = CALL_ICON_MUTED;\n        useEffect(\n            (selfSession, compact) => {\n                if (selfSession?.in(this.sessions) && !compact) {\n                    this.state.expanded = true;\n                }\n                if (compact) {\n                    this.state.expanded = false;\n                }\n            },\n            () => [this.rtc.selfSession, this.compact]\n        );\n    }\n\n    get compact() {\n        if (typeof this.props.compact === \"boolean\") {\n            return this.props.compact;\n        }\n        return this.store.discuss.isSidebarCompact;\n    }\n\n    get lastActiveSession() {\n        const sessions = [...this.props.thread.rtc_session_ids];\n        sessions?.sort((s1, s2) => {\n            if (s1.isActuallyTalking && !s2.isActuallyTalking) {\n                return -1;\n            }\n            if (!s1.isActuallyTalking && s2.isActuallyTalking) {\n                return 1;\n            }\n            if (s1.isVideoStreaming && !s2.isVideoStreaming) {\n                return -1;\n            }\n            if (!s1.isVideoStreaming && s2.isVideoStreaming) {\n                return 1;\n            }\n            return s2.talkingTime - s1.talkingTime;\n        });\n        return sessions[0];\n    }\n\n    get attClass() {\n        return {\n            \"justify-content-center bg-inherit\": this.compact,\n        };\n    }\n\n    get sessions() {\n        const sessions = [...this.props.thread.rtc_session_ids];\n        return sessions.sort((s1, s2) => {\n            const persona1 = s1.channel_member_id?.persona;\n            const persona2 = s2.channel_member_id?.persona;\n            return (\n                persona1?.name?.localeCompare(persona2?.name) ||\n                s1.channel_member_id?.id - s2.channel_member_id?.id ||\n                s1.id - s2.id\n            );\n        });\n    }\n\n    /**\n     * @param {import(\"models\").Persona} persona\n     */\n    avatarClass(persona) {\n        return persona.currentRtcSession?.isActuallyTalking\n            ? \"o-mail-DiscussSidebarCallParticipants-avatar o-isTalking\"\n            : \"\";\n    }\n\n    onClickAvatarStack() {\n        if (this.compact) {\n            return;\n        }\n        this.state.expanded = true;\n    }\n\n    get title() {\n        return this.state.expanded ? _t(\"Collapse participants\") : _t(\"Expand participants\");\n    }\n\n    onClickParticipant(ev, session) {}\n}\n", "import { DiscussSidebarCallParticipants } from \"@mail/discuss/call/public_web/discuss_sidebar_call_participants\";\nimport { DiscussSidebarChannel } from \"@mail/discuss/core/public_web/discuss_sidebar_categories\";\nimport { patch } from \"@web/core/utils/patch\";\n\nDiscussSidebarChannel.components = Object.assign(DiscussSidebarChannel.components || {}, {\n    DiscussSidebarCallParticipants,\n});\n\npatch(DiscussSidebarChannel.prototype, {\n    get attClass() {\n        return {\n            ...super.attClass,\n            \"o-ongoingCall\": this.thread.rtc_session_ids.length > 0,\n        };\n    },\n    get attClassContainer() {\n        return {\n            ...super.attClassContainer,\n            \"o-selfInCall\": this.store.rtc.selfSession?.in(this.thread.rtc_session_ids),\n        };\n    },\n    get bordered() {\n        return super.bordered || this.thread.rtc_session_ids.length > 0;\n    },\n});\n", "import { Thread } from \"@mail/core/common/thread_model\";\nimport { patch } from \"@web/core/utils/patch\";\n\n/** @type {import(\"models\").Thread} */\nconst ThreadPatch = {\n    get isCallDisplayedInChatWindow() {\n        return (\n            super.isCallDisplayedInChatWindow &&\n            (this.store.env.services.ui.isSmall || !this.store.discuss.isActive)\n        );\n    },\n};\npatch(Thread.prototype, ThreadPatch);\n", "import { DiscussSidebarCallParticipants } from \"@mail/discuss/call/public_web/discuss_sidebar_call_participants\";\nimport { AvatarCardPopover } from \"@mail/discuss/web/avatar_card/avatar_card_popover\";\n\nimport { usePopover } from \"@web/core/popover/popover_hook\";\nimport { patch } from \"@web/core/utils/patch\";\n\npatch(DiscussSidebarCallParticipants.prototype, {\n    setup() {\n        super.setup();\n        this.avatarCard = usePopover(AvatarCardPopover, {\n            position: \"right\",\n        });\n    },\n    get attClass() {\n        return {\n            ...super.attClass,\n            \"o-active cursor-pointer rounded-4\": this.session.persona.main_user_id,\n        };\n    },\n    onClickParticipant(ev, session) {\n        if (!session.persona.main_user_id) {\n            return;\n        }\n        if (!this.avatarCard.isOpen) {\n            this.avatarCard.open(ev.currentTarget, {\n                id: session.persona.main_user_id.id,\n            });\n        }\n    },\n});\nObject.assign(DiscussSidebarCallParticipants.components, { AvatarCardPopover });\n", "import { useService } from \"@web/core/utils/hooks\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { patch } from \"@web/core/utils/patch\";\nimport { QuickVideoSettings } from \"../common/quick_video_settings\";\n\npatch(QuickVideoSettings.prototype, {\n    setup() {\n        super.setup();\n        this.actionService = useService(\"action\");\n    },\n    onClickVideoSettings() {\n        this.actionService.doAction({\n            context: {\n                dialog_size: \"medium\",\n                footer: false,\n            },\n            name: _t(\"Voice & Video Settings\"),\n            tag: \"mail.discuss_call_settings_action\",\n            target: \"new\",\n            type: \"ir.actions.client\",\n        });\n    },\n});\n", "import { useService } from \"@web/core/utils/hooks\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { QuickVoiceSettings } from \"@mail/discuss/call/common/quick_voice_settings\";\nimport { patch } from \"@web/core/utils/patch\";\n\npatch(QuickVoiceSettings.prototype, {\n    setup() {\n        super.setup();\n        this.actionService = useService(\"action\");\n    },\n    onClickVoiceSettings() {\n        this.actionService.doAction({\n            context: {\n                dialog_size: \"medium\",\n                footer: false,\n            },\n            name: _t(\"Voice & Video Settings\"),\n            tag: \"mail.discuss_call_settings_action\",\n            target: \"new\",\n            type: \"ir.actions.client\",\n        });\n    },\n});\n", "import { useService } from \"@web/core/utils/hooks\";\nimport { Component } from \"@odoo/owl\";\nimport { useOpenChat } from \"@mail/core/web/open_chat_hook\";\nimport { ImStatus } from \"@mail/core/common/im_status\";\n\nexport class AvatarCardPopover extends Component {\n    static template = \"mail.AvatarCardPopover\";\n    static components = { ImStatus };\n    static props = {\n        id: { type: Number, required: true },\n        close: { type: Function, required: true },\n        model: {\n            type: String,\n            validate: (m) => [\"res.users\", \"res.partner\"].includes(m),\n            optional: true,\n        },\n    };\n    static defaultProps = {\n        model: \"res.users\",\n    };\n\n    setup() {\n        this.actionService = useService(\"action\");\n        this.store = useService(\"mail.store\");\n        this.openChat = useOpenChat(this.props.model);\n        this.store.fetchStoreData(\"avatar_card\", {\n            id: this.props.id,\n            model: this.props.model,\n        });\n    }\n\n    get user() {\n        if (this.props.model === \"res.users\") {\n            return this.store[\"res.users\"].get(this.props.id);\n        }\n        return undefined;\n    }\n\n    get partner() {\n        if (this.props.model === \"res.partner\") {\n            return this.store[\"res.partner\"].get(this.props.id);\n        }\n        return this.user?.partner_id;\n    }\n\n    get name() {\n        return this.partner?.name;\n    }\n\n    get email() {\n        return this.partner?.email;\n    }\n\n    get phone() {\n        return this.partner?.phone;\n    }\n\n    get showViewProfileBtn() {\n        return this.partner;\n    }\n\n    get hasFooter() {\n        return false;\n    }\n\n    async getProfileAction() {\n        return {\n            res_id: this.partner.id,\n            res_model: \"res.partner\",\n            type: \"ir.actions.act_window\",\n            views: [[false, \"form\"]],\n        };\n    }\n\n    onSendClick() {\n        this.openChat(this.props.id);\n        this.props.close();\n    }\n\n    async onClickViewProfile(newWindow) {\n        const action = await this.getProfileAction();\n        if (!action) {\n            return;\n        }\n        this.actionService.doAction(action, { newWindow });\n    }\n}\n", "import { patch } from \"@web/core/utils/patch\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { BusConnectionAlert } from \"@mail/discuss/core/public_web/bus_connection_alert\";\n\npatch(BusConnectionAlert.prototype, {\n    setup() {\n        super.setup(...arguments);\n        this.store = useService(\"mail.store\");\n    },\n\n    get showBorderOnFailure() {\n        return super.showBorderOnFailure || this.store.discuss.isActive;\n    },\n});\n", "import { DiscussCoreCommon } from \"@mail/discuss/core/common/discuss_core_common_service\";\nimport { patch } from \"@web/core/utils/patch\";\n\npatch(DiscussCoreCommon.prototype, {\n    _handleNotificationChannelDelete(thread, metadata) {\n        const { notifId } = metadata;\n        const filteredStarredMessages = [];\n        let starredCounter = 0;\n        for (const msg of this.store.starred.messages) {\n            if (!msg.thread?.eq(thread)) {\n                filteredStarredMessages.push(msg);\n            } else {\n                starredCounter++;\n            }\n        }\n        this.store.starred.messages = filteredStarredMessages;\n        if (notifId > this.store.starred.counter_bus_id) {\n            this.store.starred.counter -= starredCounter;\n        }\n        this.store.inbox.messages = this.store.inbox.messages.filter(\n            (msg) => !msg.thread?.eq(thread)\n        );\n        if (notifId > this.store.inbox.counter_bus_id) {\n            this.store.inbox.counter -= thread.message_needaction_counter;\n        }\n        this.store.history.messages = this.store.history.messages.filter(\n            (msg) => !msg.thread?.eq(thread)\n        );\n        if (thread.eq(this.store.discuss.thread)) {\n            this.store.discuss.thread = undefined;\n        }\n        super._handleNotificationChannelDelete(thread, metadata);\n    },\n});\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { Component, useState } from \"@odoo/owl\";\nimport { memoize } from \"@web/core/utils/functions\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { ModelSelector } from \"@web/core/model_selector/model_selector\";\nimport { registry } from \"@web/core/registry\";\nimport { SelectCreateDialog } from \"@web/views/view_dialogs/select_create_dialog\";\nimport { standardFieldProps } from \"@web/views/fields/standard_field_props\";\n\n/** largely taken from documents' DocumentsDetailPanel, which selects arbitrary models and records\n * through two interactions:\n * 1- select the model through a list of accesible and appropriate models (getAvailableResModels)\n * 2- select one of this model's records through a tree view in a new dialog that opens\n **/\n\n// Small hack, memoize uses the first argument as cache key, but we need the orm which will not be the same.\nconst getAvailableResModels = memoize((_null, orm) =>\n    orm.call(\"mail.activity.schedule\", \"get_model_options\")\n);\n\nclass ActivityModelSelector extends Component {\n    static components = { ModelSelector };\n    static template = \"mail.ActivityModelSelector\";\n    static props = standardFieldProps;\n\n    setup() {\n        // Use a state for the model to not write on the record the model without record id\n        this.orm = useService(\"orm\");\n        this.dialog = useService(\"dialog\");\n        this.state = useState({\n            resModel: this.props.record.data.res_model,\n            resModelName: this.props.record.data.res_model_name || \"\",\n            models: [],\n        });\n        getAvailableResModels(null, this.orm).then((models) => (this.state.models = models));\n    }\n\n    async onModelSelected(value) {\n        this.state.resModel = value.technical;\n        this.state.resModelName = value.label || \"\";\n        if (this.state.resModel) {\n            this.dialog.add(\n                SelectCreateDialog,\n                {\n                    title: _t(\"Select a Record To Link\"),\n                    noCreate: true,\n                    multiSelect: false,\n                    resModel: this.state.resModel,\n                    onSelected: async (resId) => {\n                        /* Changing the model linked to the activity also changes available activity types.\n                         * This in turn triggers a recompute of all fields dependent on activity types, including\n                         * summary and notes, which may already have been edited (especially summary as the user is\n                         * likely to fill out the wizard in order).\n                         * To prevent this, current summary and notes are saved and will be recovered after the model\n                         * has been changed.\n                         */\n                        const persistDataThroughModelChange = {\n                            summary: this.props.record.data.summary,\n                            note: this.props.record.data.note,\n                        };\n\n                        await this.props.record.update(\n                            {\n                                res_model: this.state.resModel,\n                                res_ids: resId,\n                            },\n                            { save: false }\n                        );\n                        const recordInfo = await this.orm.call(\n                            this.state.resModel,\n                            \"name_search\",\n                            [],\n                            {\n                                domain: [[\"id\", \"in\", resId]],\n                            }\n                        );\n                        this.state.resModelName = recordInfo[0][1];\n\n                        // recover saved inputs\n                        this.props.record.update(persistDataThroughModelChange);\n                    },\n                },\n                {\n                    onClose: () => {\n                        if (!this.props.record.data.res_ids) {\n                            this.onRecordReset();\n                        }\n                    },\n                }\n            );\n        }\n    }\n\n    onRecordReset() {\n        // information to persist current summary and notes through model_id changes\n        const persistDataThroughModelChange = {\n            summary: this.props.record.data.summary,\n            note: this.props.record.data.note,\n        };\n        this.props.record.update({\n            res_model: false,\n            res_ids: false,\n        });\n        this.props.record.update(persistDataThroughModelChange);\n        return this.onModelSelected({ technical: false, label: false });\n    }\n}\n\nregistry.category(\"fields\").add(\"activity_model_selector\", {\n    component: ActivityModelSelector,\n});\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { getFieldDomain } from \"@web/model/relational_model/utils\";\nimport { useSpecialData } from \"@web/views/fields/relational_utils\";\nimport {\n    badgeSelectionField,\n    BadgeSelectionField,\n} from \"@web/views/fields/badge_selection/badge_selection_field\";\n\n/**\n * @typedef BadgeSelectionIconsField\n * Overrides the standard BadgeSelectionField and inserts FontAwesome icons before each option's title.\n * Only compatible with Many2one selectors. Related options should have an \"icon\" field, the name\n * of which should be specified through the iconField prop.\n *\n * Special props:\n * @param {String} iconField The name of the field on which the icon is stored on the many2one option\n * @param {String} defaultIcon If the field pointed through iconField is empty on the related record, a default fa icon can be specified.\n */\nexport class BadgeSelectionWithIconsField extends BadgeSelectionField {\n    static props = {\n        ...BadgeSelectionField.props,\n        iconField: { type: String },\n        defaultIcon: { type: String, optional: true, default: \"fa-check\" },\n    };\n    static template = \"mail.BadgeSelectionIconsField\";\n\n    /**\n     * @override\n     * many2one fields use attribute \"specialData\" to store information pertaining to many2one relations.\n     * As such, this.specialData is used by the inherited BadgeSelectionField to store the Many2one selection options for this field.\n     */\n    async setup() {\n        this.type = this.props.record.fields[this.props.name].type;\n        this.specialData = useSpecialData(async (orm, props) => {\n            const domain = getFieldDomain(props.record, props.name, props.domain);\n            const { relation } = props.record.fields[props.name];\n            const ret = await orm.call(relation, \"search_read\", [], {\n                domain: domain,\n                fields: [\"id\", \"name\", props.iconField],\n            });\n            return ret.map((opt) => {\n                const option = Object.values(opt);\n                if (!option[2]) {\n                    option[2] = props.defaultIcon;\n                }\n                return option;\n            });\n        });\n    }\n}\n\nexport const badgeSelectionWithIconsField = {\n    ...badgeSelectionField,\n    component: BadgeSelectionWithIconsField,\n    supportedTypes: [\"many2one\"],\n    displayName: _t(\"Badges with Icons\"),\n    extractProps: (fieldInfo, dynamicInfo) => ({\n        ...badgeSelectionField.extractProps(fieldInfo, dynamicInfo),\n        iconField: fieldInfo.attrs.iconField,\n        defaultIcon: fieldInfo.attrs.defaultIcon,\n    }),\n};\nregistry.category(\"fields\").add(\"selection_badge_icons\", badgeSelectionWithIconsField);\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { SelectionField, selectionField } from \"@web/views/fields/selection/selection_field\";\nimport { user } from \"@web/core/user\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { useState } from \"@odoo/owl\";\n\nexport class MailServerConfiguratorSelection extends SelectionField {\n    static template = \"mail.MailServerConfiguratorSelection\";\n\n    setup() {\n        super.setup();\n        this.action = useService(\"action\");\n        this.orm = useService(\"orm\");\n        this.notification = useService(\"notification\");\n        this.state = useState({\n            value: null,\n            connectionFailed: false,\n        });\n    }\n\n    /**\n     * Allow to change the value displayed without making the field dirty.\n     */\n    get value() {\n        return this.state.value || super.value;\n    }\n\n    get readonly() {\n        return this.props.record.resId !== user.userId;\n    }\n\n    get isServerConfigured() {\n        return !!this.props.record.data.outgoing_mail_server_id;\n    }\n\n    async onOptionChange(value) {\n        const oldValue = this.value;\n        this.state.value = value;\n        await this.props.record.model.root.save();\n        try {\n            const action = await this.orm.call(\"res.users\", \"action_setup_outgoing_mail_server\", [\n                value,\n            ]);\n            if (action) {\n                this.action.doAction(action);\n            }\n        } catch (error) {\n            this.state.value = oldValue;\n            this.notification.add(error.data.message, {\n                type: \"danger\",\n            });\n        }\n    }\n\n    async onTestConnection() {\n        await this.props.record.model.root.save();\n        try {\n            const action = await this.orm.call(\"res.users\", \"action_test_outgoing_mail_server\");\n            this.state.connectionFailed = false;\n            this.action.doAction(action);\n        } catch (error) {\n            this.notification.add(_t(\"Connection failed: %s\", error.data.message), {\n                type: \"danger\",\n            });\n            this.state.connectionFailed = true;\n        }\n    }\n}\n\nregistry.category(\"fields\").add(\"mail_server_configurator_selection\", {\n    ...selectionField,\n    component: MailServerConfiguratorSelection,\n});\n", "import { formatDuration } from \"@web/core/l10n/dates\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { statusBarField, StatusBarField } from \"@web/views/fields/statusbar/statusbar_field\";\n\nexport class StatusBarDurationField extends StatusBarField {\n    static template = \"mail.StatusBarDurationField\";\n\n    getAllItems() {\n        const items = super.getAllItems();\n        const durationTracking = this.props.record.data.duration_tracking || {};\n        if (Object.keys(durationTracking).length) {\n            for (const item of items) {\n                const duration = durationTracking[item.value];\n                if (duration > 0) {\n                    item.shortTimeInStage = formatDuration(duration, false);\n                    item.fullTimeInStage = formatDuration(duration, true);\n                } else {\n                    item.shortTimeInStage = 0;\n                }\n            }\n        }\n        return items;\n    }\n}\n\nexport const statusBarDurationField = {\n    ...statusBarField,\n    component: StatusBarDurationField,\n    displayName: _t(\"Status with time\"),\n    supportedTypes: [\"many2one\"],\n    fieldDependencies: [{ name: \"duration_tracking\", type: \"JSON\" }],\n};\n\nregistry.category(\"fields\").add(\"statusbar_duration\", statusBarDurationField);\n", "/** @odoo-module **/\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { formView } from \"@web/views/form/form_view\";\nimport { FormController } from \"@web/views/form/form_controller\";\nimport { registry } from \"@web/core/registry\";\nimport { user } from \"@web/core/user\";\nimport { useService } from \"@web/core/utils/hooks\";\n\n\n/**\n * Controller used to directly activate the multi-team option\n * via a button present in the crm team member alert.\n *\n * This alert is only displayed when a user is assigned to\n * multiple teams but the multi-team option is deactivated.\n */\nclass CrmTeamFormController extends FormController {\n\n    setup() {\n        super.setup();\n        this.orm = useService(\"orm\");\n    }\n\n    async beforeExecuteActionButton(clickParams) {\n        if (clickParams.name === \"crm_team_activate_multi_membership\") {\n            if (!user.hasGroup(\"sales_team.group_sale_manager\")) {\n                return false;\n            }\n            const alert = document.querySelector(\".alert\");\n            try {\n                await this.orm.call(\"ir.config_parameter\", \"set_param\", [\n                    \"sales_team.membership_multi\",\n                    true,\n                ]);\n                alert?.classList.add('d-none');\n            } catch {\n                if (alert) {\n                    alert.classList.replace(\"alert-info\", \"alert-danger\");\n                    alert.textContent = _t(\"An error occurred while activating the Multi-Team option.\");\n                }\n            }\n            return false;\n        }\n        return super.beforeExecuteActionButton(...arguments);\n    }\n}\nregistry.category(\"views\").add(\"crm_team_form\", {\n    ...formView,\n    Controller: CrmTeamFormController,\n});\n", "import { FormController } from \"@web/views/form/form_controller\";\nimport { useService } from \"@web/core/utils/hooks\";\n\n/**\n * Controller to use for an onboarding step dialog, not the\n * onboarding.onboarding.step form view itself.\n */\nexport default class OnboardingStepFormController extends FormController {\n    setup() {\n        super.setup();\n        this.action = useService('action');\n        this.orm = useService('orm');\n    }\n    /**\n     * If necessary, mark the step as done and reload the main view.\n     * @override\n     */\n    async save({ closable, ...otherParams }) {\n        const saved = await super.save(otherParams);\n        if (saved) {\n            const { reloadOnFirstValidation, reloadAlways } = this.stepConfig;\n            const validationResponse = await this.orm.call(\n                'onboarding.onboarding.step',\n                'action_validate_step',\n                [this.stepName],\n            );\n            if (reloadAlways || (reloadOnFirstValidation && validationResponse === \"JUST_DONE\")) {\n                this.action.restore(this.action.currentController.jsId);\n            } else if (closable) {\n                this.action.doAction({ type: \"ir.actions.act_window_close\" });\n            }\n        }\n        return saved;\n    }\n    /**\n     * Returns the name of the onboarding step to validate after the dialog\n     * record is saved\n     *\n     * @return {string}\n     */\n    get stepName() {\n        return ''\n    }\n    /**\n     *  Returns whether to reload the page (useful if the current\n     * view needs to be updated).\n     *\n     * @returns {{reloadAlways: boolean, reloadOnFirstValidation: boolean}}\n     */\n    get stepConfig() {\n        return { reloadAlways: false, reloadOnFirstValidation: false };\n    }\n}\n", "import { Component } from \"@odoo/owl\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { computeM2OProps, Many2One } from \"@web/views/fields/many2one/many2one\";\nimport {\n    buildM2OFieldDescription,\n    extractM2OFieldProps,\n    m2oSupportedOptions,\n    Many2OneField,\n} from \"@web/views/fields/many2one/many2one_field\";\nimport { getProductRelatedModel, Many2XUomTagsAutocomplete } from \"../many2x_uom_tags/many2x_uom_tags\";\n\n// @todo: this extension will be removed in the future\n// when the autocomplete source generation come from a hook.\nclass UomMany2One extends Many2One {\n    static components = {\n        ...super.components,\n        Many2XAutocomplete: Many2XUomTagsAutocomplete,\n    };\n    static props = {\n        ...super.props,\n        productModel: { type: String, optional: true },\n        productId: { type: Number, optional: true },\n        productQuantity: { type: Number, optional: true },\n    };\n\n    get many2XAutocompleteProps() {\n        return {\n            ...super.many2XAutocompleteProps,\n            productModel: this.props.productModel,\n            productId: this.props.productId,\n            productQuantity: this.props.productQuantity,\n        };\n    }\n}\n\nexport class Many2OneUomField extends Component {\n    static template = \"uom.Many2OneUomField\";\n    static components = { UomMany2One };\n    static props = {\n        ...Many2OneField.props,\n        productField: { type: String, optional: true },\n        quantityField: { type: String, optional: true },\n    };\n    static defaultProps = {\n        ...Many2OneField.defaultProps,\n        productField: \"product_id\",\n        quantityField: \"product_uom_qty\",\n    };\n\n    get m2oProps() {\n        const productModel = getProductRelatedModel.call(this);\n        let productId = this.props.record.data[this.props.productField]?.id || 0;\n        if ([\"product.template\", \"product.product\"].includes(this.props.record.resModel)) {\n            productId = this.props.record.resId || 0;\n        }\n        return {\n            ...computeM2OProps(this.props),\n            productModel,\n            productId,\n            productQuantity: this.props.record.data[this.props.quantityField],\n            // specification: {\n            //     name: {},\n            //     relative_factor: {},\n            //     relative_uom_id: {\n            //         fields: {\n            //             display_name: {},\n            //         },\n            //     },\n            // },\n        };\n    }\n\n    getLabel(record) {\n        return record.name ? record.name.split(\"\\n\")[0] : _t(\"Unnamed\");\n    }\n}\n\nregistry.category(\"fields\").add(\"many2one_uom\", {\n    ...buildM2OFieldDescription(Many2OneUomField),\n    additionalClasses: [\"o_field_many2one\"],\n    extractProps(staticInfo, dynamicInfo) {\n        return {\n            ...extractM2OFieldProps(staticInfo, dynamicInfo),\n            productField: staticInfo.options.product_field,\n            quantityField: staticInfo.options.quantity_field,\n        };\n    },\n    supportedOptions: [\n        ...m2oSupportedOptions,\n        {\n            label: _t(\"Product Field Name\"),\n            name: \"product_field\",\n            type: \"field\",\n            availableTypes: [\"many2one\"],\n        },\n        {\n            label: _t(\"Quantity Field Name\"),\n            name: \"quantity_field\",\n            type: \"field\",\n            availableTypes: [\"many2one\"],\n        },\n    ],\n});\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { Many2XAutocomplete } from \"@web/views/fields/relational_utils\";\nimport {\n    Many2ManyTagsFieldColorEditable,\n    many2ManyTagsFieldColorEditable,\n} from \"@web/views/fields/many2many_tags/many2many_tags_field\";\nimport { roundPrecision } from \"@web/core/utils/numbers\";\nimport { onWillUpdateProps } from \"@odoo/owl\";\n\nexport function getProductRelatedModel() {\n    const field = this.props.record.fields[this.props.productField];\n    // The widget is either used alongisde a product related field or either used in a product view.\n    let resModel = field?.relation || this.props.record.resModel;\n    if (![\"product.product\", \"product.template\"].includes(resModel)) {\n        throw new Error(`The widget '${this.constructor.name}' (field '${this.props.name}') needs a 'product.product' or 'product.template' field. '${this.props.productField}' is used but is related to '${field?.relation}' model.`);\n    }\n    return resModel;\n}\n\nexport class Many2XUomTagsAutocomplete extends Many2XAutocomplete {\n    static props = {\n        ...Many2XAutocomplete.props,\n        productModel: { type: String, optional: true },\n        productId: { type: Number, optional: true },\n        productQuantity: { type: Number, optional: true },\n    };\n\n    async setup() {\n        super.setup();\n        onWillUpdateProps(async (nextProps) => {\n            if (nextProps.productModel !== this.props.productModel ||\n                nextProps.productId !== this.props.productId\n            ) {\n                await this.updateReferenceUnit(nextProps);\n            }\n        });\n        await this.updateReferenceUnit();\n    }\n\n    async updateReferenceUnit(props = this.props) {\n        if (props.productModel && props.productId) {\n            const context = { \"active_test\" : false };\n            const product = await this.orm.searchRead(props.productModel, [[\"id\", \"=\", props.productId]], [\"uom_id\"], { context });\n            this.referenceUnit = (await this.orm.searchRead(\"uom.uom\", [[\"id\", \"=\", product[0].uom_id[0]]], [\"name\", \"factor\", \"parent_path\", \"rounding\"]))[0];\n        }\n    }\n\n    async search(name) {\n        let records = await this.orm.searchRead(\n            this.props.resModel,\n            [...this.props.getDomain(), [\"name\", \"ilike\", name]],\n            [\"id\", \"display_name\", \"relative_factor\", \"factor\", \"relative_uom_id\", \"parent_path\"],\n        );\n        const hasCommonReference = (uom1, uom2) => {\n            const uom1Path = uom1.parent_path.split(\"/\");\n            const uom2Path = uom2.parent_path.split(\"/\");\n            return uom1Path[0] === uom2Path[0];\n        };\n        records = records.map((record) => {\n            let relativeInfo = this.referenceUnit && this.referenceUnit.id !== record.id ? `${roundPrecision((this.props.productQuantity || 1) * record.factor / this.referenceUnit.factor, this.referenceUnit.rounding)} ${this.referenceUnit.name}` : \"\";\n            if (\n                this.referenceUnit &&\n                record.id !== this.referenceUnit.id &&\n                hasCommonReference(record, this.referenceUnit) &&\n                record.relative_uom_id\n            ) {\n                relativeInfo = `${roundPrecision((this.props.productQuantity || 1) * record.relative_factor, this.referenceUnit.rounding)} ${record.relative_uom_id[1]}`;\n            }\n            return {\n                ...record,\n                relative_info: relativeInfo,\n            };\n        });\n        if (this.referenceUnit) {\n            records.sort((a, b) => hasCommonReference(a, this.referenceUnit) ? -1 : hasCommonReference(b, this.referenceUnit) ? 1 : 0);\n        }\n        return records;\n    }\n}\n\nexport class Many2ManyUomTagsField extends Many2ManyTagsFieldColorEditable {\n    static template = \"uom.Many2ManyUomTagsField\";\n    static components = {\n        ...Many2ManyTagsFieldColorEditable.components,\n        Many2XAutocomplete: Many2XUomTagsAutocomplete,\n    };\n    static props = {\n        ...Many2ManyTagsFieldColorEditable.props,\n        productField: { type: String, optional: true },\n        quantityField: { type: String, optional: true },\n    }\n    static defaultProps = {\n        ...Many2ManyTagsFieldColorEditable.defaultProps,\n        productField: \"product_id\",\n        quantityField: \"product_uom_qty\",\n    }\n\n    async setup() {\n        super.setup();\n        this.productModel = getProductRelatedModel.call(this);\n    }\n}\n\nexport const many2ManyUomTagsField = {\n    ...many2ManyTagsFieldColorEditable,\n    component: Many2ManyUomTagsField,\n    additionalClasses: ['o_field_many2many_tags'],\n    supportedOptions: [\n        ...(many2ManyTagsFieldColorEditable.supportedOptions || []),\n        {\n            label: _t(\"Product Field Name\"),\n            name: \"product_field\",\n            type: \"field\",\n            availableTypes: [\"many2one\"]\n        },\n        {\n            label: _t(\"Quantity Field Name\"),\n            name: \"quantity_field\",\n            type: \"field\",\n            availableTypes: [\"many2one\"]\n        }\n    ],\n    extractProps({ options }) {\n        const props = many2ManyTagsFieldColorEditable.extractProps(...arguments);\n        props.productField = options.product_field;\n        props.quantityField = options.quantity_field;\n        return props;\n    },\n};\n\nregistry.category(\"fields\").add(\"many2many_uom_tags\", many2ManyUomTagsField);\n", "import { Component, markup, onRendered, onWillStart, useState } from \"@odoo/owl\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { download } from \"@web/core/network/download\";\nimport { registry } from \"@web/core/registry\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { useSetupAction } from \"@web/search/action_hook\";\nimport { Layout } from \"@web/search/layout\";\nimport { SelectCreateDialog } from \"@web/views/view_dialogs/select_create_dialog\";\nimport { standardActionServiceProps } from \"@web/webclient/actions/action_service\";\n\nfunction sendCustomNotification(type, message) {\n    return {\n        type: \"ir.actions.client\",\n        tag: \"display_notification\",\n        params: {\n            \"type\": type,\n            \"message\": message\n        },\n    }\n}\n\nexport class ProductPricelistReport extends Component {\n    static props = { ...standardActionServiceProps };\n    static components = { Layout };\n    static template = \"product.ProductPricelistReport\";\n\n    setup() {\n        this.action = useService(\"action\");\n        this.orm = useService(\"orm\");\n        this.dialog = useService(\"dialog\");\n\n        this.MAX_QTY = 5;\n        const pastState = this.props.state || {};\n\n        const active_model = pastState.activeModel || this.props.action.context.active_model;\n        this.noProducts = active_model === 'product.pricelist';\n        this.activeIds = this.noProducts ? [] : pastState.activeIds || this.props.action.context.active_ids;\n        this.activeModel = this.noProducts ? 'product.template' : active_model;\n        this.defaultPricelistId = this.noProducts ? this.props.action.context.active_id : false;\n\n        this.state = useState({\n            displayPricelistTitle: pastState.displayPricelistTitle || false,\n            html: \"\",\n            pricelists: [],\n            _quantities: pastState.quantities || [1, 5, 10],\n            selectedPricelist: {},\n        });\n\n        onWillStart(async () => {\n            this.state.pricelists = await this.getPricelists();\n            if (this.defaultPricelistId) {\n                this.state.selectedPricelist = this.pricelists.find(p => p.id === this.defaultPricelistId) || this.pricelists[0];\n            } else {\n                this.state.selectedPricelist = pastState.selectedPricelist || this.pricelists[0];\n            }\n            if(this.noProducts){\n                await this.onClickAddProducts();\n            }\n            this.renderHtml();\n        });\n\n        onRendered(() => {\n            this.env.config.setDisplayName(_t(\"Pricelist Report\"));\n        });\n\n        /*\n        When following the link of a product and coming back we need to keep the\n        precedent state:\n            - if the pricelist was being showed\n            - wich pricelist is selected at the moment\n            - which quantities\n        */\n        useSetupAction({\n            getLocalState: () => {\n                return {\n                    displayPricelistTitle: this.displayPricelistTitle,\n                    quantities: this.quantities,\n                    selectedPricelist: this.selectedPricelist,\n                    activeModel: this.activeModel,\n                    activeIds: this.activeIds,\n                };\n            },\n        });\n    }\n\n    // getters and setters\n\n    get displayPricelistTitle() {\n        return this.state.displayPricelistTitle;\n    }\n\n    get html() {\n        return this.state.html;\n    }\n\n    get pricelists() {\n        return this.state.pricelists;\n    }\n\n    get quantities() {\n        return this.state._quantities;\n    }\n\n    set quantities(value) {\n        this.state._quantities = value;\n    }\n\n    get reportParams() {\n        return {\n            active_model: this.activeModel || 'product.template',\n            active_ids: this.activeIds || [],\n            display_pricelist_title: this.displayPricelistTitle || '',\n            pricelist_id: this.selectedPricelist.id || '',\n            quantities: this.quantities || [1],\n        };\n    }\n\n    get selectedPricelist() {\n        return this.state.selectedPricelist;\n    }\n\n    // orm calls\n\n    getPricelists() {\n        return this.orm.searchRead(\"product.pricelist\", [], [\"id\", \"name\"]);\n    }\n\n    async renderHtml() {\n        if (this.noProducts) {\n            // do not make an rpc to get empty report data\n            this.state.html = \"\";\n            return\n        }\n        let html = await this.orm.call(\n            \"report.product.report_pricelist\", \"get_html\", [], {data: this.reportParams}\n        );\n        this.state.html = markup(html);\n    }\n\n    // events\n\n    async onClickAddQty(ev) {\n        ev.preventDefault(); // avoid automatic reloading of the page\n\n        if (this.quantities.length >= this.MAX_QTY) {\n            let message = _t(\n                \"At most %s quantities can be displayed simultaneously. Remove a selected quantity to add others.\",\n                this.MAX_QTY\n            );\n            await this.action.doAction(sendCustomNotification(\"warning\", message));\n            return;\n        }\n\n        const qty = parseInt(ev.target.previousSibling.value);\n        if (qty > 0) {\n            // Check qty already exist.\n            if (this.quantities.indexOf(qty) === -1) {\n                this.quantities.push(qty);\n                this.quantities = this.quantities.sort((a, b) => a - b);\n                this.renderHtml();\n            } else {\n                let message = _t(\"Quantity already present (%s).\", qty);\n                await this.action.doAction(sendCustomNotification(\"info\", message));\n            }\n        } else {\n            await this.action.doAction(\n                sendCustomNotification(\"info\", _t(\"Please enter a positive whole number.\"))\n            );\n        }\n    }\n\n    onClickLink(ev) {\n        ev.preventDefault();\n\n        const parent = ev.target.parentElement;\n\n        let classes = parent.getAttribute(\"class\", \"\");\n        let resModel = parent.getAttribute(\"data-model\", \"\");\n        let resId = parent.getAttribute(\"data-res-id\", \"\");\n\n        if (classes && classes.includes(\"o_action\") && resModel && resId) {\n            this.action.doAction({\n                type: 'ir.actions.act_window',\n                res_model: resModel,\n                res_id: parseInt(resId),\n                views: [[false, 'form']],\n                target: 'self',\n            });\n        }\n    }\n\n    async onClickPrint() {\n        if (this.noProducts) {\n            this.action.doAction(\n                sendCustomNotification(\"warning\", _t(\"Please select some products first.\"))\n            );\n            return;\n        }\n        const selectedFormat = document.getElementById('formats').value;\n        if (selectedFormat === 'pdf') {\n            this.export_pdf();\n        } else {\n            await this.export_pricelist_csv_xlsx(selectedFormat);\n        }\n    }\n\n    export_pdf() {\n        this.action.doAction({\n            type: 'ir.actions.report',\n            report_type: 'qweb-pdf',\n            report_name: 'product.report_pricelist',\n            report_file: 'product.report_pricelist',\n            data: this.reportParams,\n        });\n    }\n\n    async export_pricelist_csv_xlsx(format) {\n        try {\n            await download({\n                url: `/product/export/pricelist/`,\n                data: {\n                    report_data: JSON.stringify(this.reportParams),\n                    export_format: format,\n                }\n            });\n        } catch (error) {\n            console.error(`Error exporting ${format.toUpperCase()} file:`, error);\n            await this.action.doAction(\n                sendCustomNotification(\n                    \"danger\",\n                    _t(\"Error exporting file. Please try again.\")\n                )\n            );\n        }\n    }\n\n    async onClickAddProducts() {\n        this.dialog.add(SelectCreateDialog, {\n            resModel: this.activeModel || 'product.template',\n            title: _t(\"Add Products to pricelist report\"),\n            noCreate: true,\n            onSelected: async (resIds) => {\n                resIds.forEach((id) => {\n                    if (!this.activeIds.includes(id)) {\n                        this.activeIds.push(id);\n                    }\n                });\n                this.noProducts = false;\n                await this.renderHtml();\n            },\n        });\n    }\n\n    async onClickRemoveQty(ev) {\n        if (this.quantities.length <= 1) {\n            await this.action.doAction(\n                sendCustomNotification(\"warning\", _t(\"You must leave at least one quantity.\"))\n            );\n            return;\n        }\n\n        const qty = parseInt(ev.srcElement.parentElement.childNodes[0].data);\n        this.quantities = this.quantities.filter(q => q !== qty);\n        this.renderHtml();\n    }\n\n    onSelectPricelist(ev) {\n        this.state.selectedPricelist = this.pricelists.filter(pricelist =>\n            pricelist.id === parseInt(ev.target.value)\n        )[0];\n\n        this.renderHtml();\n    }\n\n    onToggleDisplayPricelist() {\n        this.state.displayPricelistTitle = !this.displayPricelistTitle;\n        this.renderHtml();\n    }\n}\n\nregistry.category(\"actions\").add(\"generate_pricelist_report\", ProductPricelistReport);\n", "import { _t } from '@web/core/l10n/translation';\nimport { ConfirmationDialog, deleteConfirmationMessage } from '@web/core/confirmation_dialog/confirmation_dialog';\nimport { ListRenderer } from '@web/views/list/list_renderer';\nimport { registry } from '@web/core/registry';\nimport { useService } from '@web/core/utils/hooks';\nimport { X2ManyField, x2ManyField } from '@web/views/fields/x2many/x2many_field';\n\n\nexport class PAVListRenderer extends ListRenderer {\n    setup() {\n        super.setup();\n        this.dialog = useService(\"dialog\");\n        this.orm = useService(\"orm\");\n    }\n\n    async onDeleteRecord(record) {\n        const message = await this.orm.call(\n            'product.attribute.value',\n            'check_is_used_on_products',\n            [record.resId],\n        )\n        if (message) {\n            return this.dialog.add(ConfirmationDialog, {\n                title: _t(\"Invalid Operation\"),\n                body: message,\n            });\n        }\n        if (record.isNew) {\n            return super.onDeleteRecord(...arguments);\n        }\n        return new Promise((resolve) => {\n            this.dialog.add(ConfirmationDialog, {\n                title: _t(\"Bye-bye, record!\"),\n                body: deleteConfirmationMessage,\n                confirmLabel: _t(\"Delete\"),\n                confirm: () => this.onConfirmDelete(record).then(resolve),\n                cancel: resolve,\n                cancelLabel: _t(\"No, keep it\"),\n            });\n        });\n    }\n\n    async onConfirmDelete(record) {\n        await this.orm.unlink('product.attribute.value', [record.resId])\n        const res = await super.onDeleteRecord(record);\n        await this.props.list.model.root.save();\n        return res;\n    }\n}\n\nexport class PAVOne2ManyField extends X2ManyField {\n    static components = {\n        ...X2ManyField.components,\n        ListRenderer: PAVListRenderer,\n    };\n}\n\nexport const pavOne2ManyField = {\n    ...x2ManyField,\n    component: PAVOne2ManyField,\n}\n\nregistry.category(\"fields\").add(\"pavs_one2many\", pavOne2ManyField);\n", "import { UploadButton } from '@product/js/product_document_kanban/upload_button/upload_button';\nimport { KanbanController } from '@web/views/kanban/kanban_controller';\n\nexport class ProductDocumentKanbanController extends KanbanController {\n    static components = { ...KanbanController.components, UploadButton };\n\n    setup() {\n        super.setup();\n        this.uploadRoute = '/product/document/upload';\n        this.formData = {\n            'res_model': this.props.context.default_res_model,\n            'res_id': this.props.context.default_res_id,\n        };\n    }\n}\n", "import { CANCEL_GLOBAL_CLICK, KanbanRecord } from \"@web/views/kanban/kanban_record\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { useFileViewer } from \"@web/core/file_viewer/file_viewer_hook\";\n\nexport class ProductDocumentKanbanRecord extends KanbanRecord {\n    setup() {\n        super.setup();\n        this.store = useService(\"mail.store\");\n        this.fileViewer = useFileViewer();\n    }\n    /**\n     * @override\n     *\n     * Override to open the preview upon clicking the image, if compatible.\n     */\n    onGlobalClick(ev) {\n        if (ev.target.closest(CANCEL_GLOBAL_CLICK)) {\n            return;\n        } else if (ev.target.closest(\".o_kanban_previewer\")) {\n            const attachment = this.store[\"ir.attachment\"].insert({\n                id: this.props.record.data.ir_attachment_id.id,\n                name: this.props.record.data.name,\n                mimetype: this.props.record.data.mimetype,\n            });\n            this.fileViewer.open(attachment);\n            return;\n        }\n        return super.onGlobalClick(...arguments);\n    }\n}\n", "import { useService } from \"@web/core/utils/hooks\";\nimport { KanbanRenderer } from \"@web/views/kanban/kanban_renderer\";\nimport { ProductDocumentKanbanRecord } from \"@product/js/product_document_kanban/product_document_kanban_record\";\nimport { FileUploadProgressContainer } from \"@web/core/file_upload/file_upload_progress_container\";\nimport { FileUploadProgressKanbanRecord } from \"@web/core/file_upload/file_upload_progress_record\";\n\nexport class ProductDocumentKanbanRenderer extends KanbanRenderer {\n    static components = {\n        ...KanbanRenderer.components,\n        FileUploadProgressContainer,\n        FileUploadProgressKanbanRecord,\n        KanbanRecord: ProductDocumentKanbanRecord,\n    };\n    static template = \"product.ProductDocumentKanbanRenderer\";\n    setup() {\n        super.setup();\n        this.fileUploadService = useService(\"file_upload\");\n    }\n}\n\n", "import { registry } from \"@web/core/registry\";\n\nimport { kanbanView } from \"@web/views/kanban/kanban_view\";\nimport { ProductDocumentKanbanController } from \"@product/js/product_document_kanban/product_document_kanban_controller\";\nimport { ProductDocumentKanbanRenderer } from \"@product/js/product_document_kanban/product_document_kanban_renderer\";\n\nexport const productDocumentKanbanView = {\n    ...kanbanView,\n    Controller: ProductDocumentKanbanController,\n    Renderer: ProductDocumentKanbanRenderer,\n    buttonTemplate: \"product.ProductDocumentKanbanView.Buttons\",\n};\n\nregistry.category(\"views\").add(\"product_documents_kanban\", productDocumentKanbanView);\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { Component, useRef } from \"@odoo/owl\";\nimport { useBus, useService } from \"@web/core/utils/hooks\";\n\nexport class UploadButton extends Component {\n    static template = \"product.UploadButton\";\n    static props = {\n        formData: { type: Object, optional: true},\n        // See https://www.iana.org/assignments/media-types/media-types.xhtml\n        allowedMIMETypes: { type: String, optional: true},\n        load: Function,\n        uploadRoute: String,\n    }\n    static defaultProps = {\n        formData: {},\n    }\n\n    setup() {\n        this.uploadFileInputRef = useRef(\"uploadFileInput\");\n        this.fileUploadService = useService(\"file_upload\");\n        this.notification = useService('notification');\n        useBus(\n            this.fileUploadService.bus,\n            \"FILE_UPLOAD_LOADED\",\n            async () => {\n                await this.props.load();\n            },\n        );\n    }\n\n    async onFileInputChange(ev) {\n        const files = [...ev.target.files].filter(file => this.validFileType(file));\n        if (!files.length) {\n            return;\n        }\n        await this.fileUploadService.upload(\n            this.props.uploadRoute,\n            files,\n            {\n                buildFormData: (formData) => this.buildFormData(formData)\n            },\n        );\n        // Reset the file input's value so that the same file may be uploaded twice.\n        ev.target.value = \"\";\n    }\n\n    /**\n     * The `allowedMIMETypes` prop can restrict the file types users are guided to select. However,\n     * the `accept` attribute doesn't enforce strict validation; it only suggests file types for\n     * browsers.\n     *\n     * @param {File} file\n     * @returns Whether the upload file's type is in the whitelist (`allowedMIMETypes`).\n     */\n    validFileType(file) {\n        if (this.props.allowedMIMETypes && !this.props.allowedMIMETypes.includes(file.type)) {\n            this.notification.add(\n                _t(`Oops! '%(fileName)s' didn\u2019t upload since its format isn\u2019t allowed.`, {\n                    fileName: file.name,\n                }),\n                {\n                    type: \"danger\",\n                }\n            );\n            return false;\n        }\n        return true;\n    }\n\n    buildFormData(formData) {\n        for (const [key, value] of Object.entries(this.props.formData)) {\n            formData.append(key, value);\n        }\n    }\n\n}\n", "import { KanbanController } from \"@web/views/kanban/kanban_controller\";\nimport { onWillStart } from \"@odoo/owl\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { useDebounced } from \"@web/core/utils/timing\";\nimport { _t } from \"@web/core/l10n/translation\";\n\nexport class ProductCatalogKanbanController extends KanbanController {\n    static template = \"ProductCatalogKanbanController\";\n\n    setup() {\n        super.setup();\n        this.orm = useService(\"orm\");\n        this.orderId = this.props.context.order_id;\n        this.orderResModel = this.props.context.product_catalog_order_model;\n        this.backToQuotationDebounced = useDebounced(this.backToQuotation, 500)\n\n        onWillStart(() => this.onWillStart());\n    }\n\n    async onWillStart() {\n        await this.setOrderStateInfo();\n        this._defineButtonContent();\n    }\n\n    // Force the slot for the \"Back to Quotation\" button to always be shown.\n    get canCreate() {\n        return true;\n    }\n\n    get stateFiels() {\n        return [\"state\"];\n    }\n\n    async setOrderStateInfo() {\n        const orderData = await this.orm.searchRead(\n            this.orderResModel, [[\"id\", \"=\", this.orderId]], this.stateFiels\n        );\n        this.orderStateInfo = orderData[0] || {};\n    }\n\n    _defineButtonContent() {\n        // Define the button's label depending of the order's state.\n        const orderIsQuotation = [\"draft\", \"sent\"].includes(this.orderStateInfo.state);\n        if (orderIsQuotation) {\n            this.buttonString = _t(\"Back to Quotation\");\n        } else {\n            this.buttonString = _t(\"Back to Order\");\n        }\n    }\n\n    async backToQuotation() {\n        // Restore the last form view from the breadcrumbs if breadcrumbs are available.\n        // If, for some weird reason, the user reloads the page then the breadcrumbs are\n        // lost, and we fall back to the form view ourselves.\n        if (this.env.config.breadcrumbs.length > 1) {\n            await this.actionService.restore();\n        } else {\n            await this.actionService.doAction({\n                type: \"ir.actions.act_window\",\n                res_model: this.orderResModel,\n                views: [[false, \"form\"]],\n                view_mode: \"form\",\n                res_id: this.orderId,\n            });\n        }\n    }\n}\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { Record } from \"@web/model/relational_model/record\";\nimport { RelationalModel } from \"@web/model/relational_model/relational_model\";\n\nclass ProductCatalogRecord extends Record {\n    setup(config, data, options = {}) {\n        this.productCatalogData = data.productCatalogData;\n        data = { ...data };\n        delete data.productCatalogData;\n        super.setup(config, data, options);\n    }\n}\n\nexport class ProductCatalogKanbanModel extends RelationalModel {\n    static Record = ProductCatalogRecord;\n    static withCache = false;\n\n    async _loadData(params) {\n        // if orm have isSample field and its value set to be true then we have sample data as there is no product found for selected vendor, show sample data\n        const isSample = this.orm.isSample !== undefined ? this.orm.isSample : false;\n        const result = await super._loadData(...arguments);\n        if (!params.isMonoRecord) {\n            let records;\n            if (params.groupBy?.length) {\n                // web_read_group: find all opened records from (sub)group\n                records = [];\n                const stackGroups = [...result.groups];\n                while (stackGroups.length) {\n                    const group = stackGroups.pop();\n                    if (group.groups?.length) {\n                        stackGroups.push(...group.groups);\n                    }\n                    if (group.records?.length) {\n                        records.push(...group.records);\n                    }\n                }\n            } else {\n                records = result.records;\n            }\n\n            let orderLinesInfo;\n            if (!isSample) {\n                orderLinesInfo = await rpc(\"/product/catalog/order_lines_info\", this._getOrderLinesInfoParams(params, records.map((rec) => rec.id)));\n            } else {\n                orderLinesInfo = this._getSampleOrderLineInfo();\n            }\n            for (const record of records) {\n                record.productCatalogData = orderLinesInfo[record.id];\n            }\n        }\n        return result;\n    }\n\n    _getOrderLinesInfoParams(params, productIds) {\n        return {\n            order_id: params.context.order_id,\n            product_ids: productIds,\n            res_model: params.context.product_catalog_order_model,\n            child_field: params.context.child_field,\n        }\n    }\n\n    _getSampleOrderLineInfo() {\n         // this function only returns data for sample view similar to rpc call (\"/product/catalog/order_lines_info) made in _loadData\n        const sampleOrderLineInfo = {};\n        const numRecords = 10; // Number of records to generate\n        for (let i = 1; i <= numRecords; i++) {\n            sampleOrderLineInfo[i] = {\n                isSample: true,\n                quantity: Math.floor(Math.random() * 10),\n                min_qty: 0,\n                price: Math.floor(Math.random() * 500) + 100,\n                productType: \"consu\",\n                readOnly: false,\n                uomDisplayName: _t(\"Units\"),\n            };\n        }\n        return sampleOrderLineInfo;\n    }\n}\n", "import { useSubEnv } from \"@odoo/owl\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { useDebounced } from \"@web/core/utils/timing\";\nimport { KanbanRecord } from \"@web/views/kanban/kanban_record\";\nimport { ProductCatalogOrderLine } from \"./order_line/order_line\";\n\nexport class ProductCatalogKanbanRecord extends KanbanRecord {\n    static template = \"ProductCatalogKanbanRecord\";\n    static components = {\n        ...KanbanRecord.components,\n        ProductCatalogOrderLine,\n    };\n\n    setup() {\n        super.setup();\n        this.debouncedUpdateQuantity = useDebounced(this._updateQuantity, 500, {\n            execBeforeUnmount: true,\n        });\n\n        useSubEnv({\n            currencyId: this.props.record.context.product_catalog_currency_id,\n            orderId: this.props.record.context.product_catalog_order_id,\n            orderResModel: this.props.record.context.product_catalog_order_model,\n            digits: this.props.record.context.product_catalog_digits,\n            displayUoM: this.props.record.context.display_uom,\n            precision: this.props.record.context.precision,\n            productId: this.props.record.resId,\n            addProduct: this.addProduct.bind(this),\n            removeProduct: this.removeProduct.bind(this),\n            increaseQuantity: this.increaseQuantity.bind(this),\n            setQuantity: this.setQuantity.bind(this),\n            decreaseQuantity: this.decreaseQuantity.bind(this),\n            childField: this.props.record.context.child_field,\n        });\n    }\n\n    get orderLineComponent() {\n        return ProductCatalogOrderLine;\n    }\n\n    get productCatalogData() {\n        return this.props.record.productCatalogData;\n    }\n\n    onGlobalClick(ev) {\n        // avoid a concurrent update when clicking on the buttons (that are inside the record)\n        if (ev.target.closest(\".o_product_catalog_cancel_global_click\")) {\n            return;\n        }\n        if (this.productCatalogData.quantity === 0) {\n            this.addProduct();\n        } else {\n            this.increaseQuantity();\n        }\n    }\n\n    //--------------------------------------------------------------------------\n    // Data Exchanges\n    //--------------------------------------------------------------------------\n\n    async _updateQuantity() {\n        const price = await this._updateQuantityAndGetPrice();\n        this.productCatalogData.price = parseFloat(price);\n    }\n\n    _updateQuantityAndGetPrice() {\n        return rpc(\"/product/catalog/update_order_line_info\", this._getUpdateQuantityAndGetPriceParams());\n    }\n\n    _getUpdateQuantityAndGetPriceParams() {\n        return {\n            order_id: this.env.orderId,\n            product_id: this.env.productId,\n            quantity: this.productCatalogData.quantity,\n            res_model: this.env.orderResModel,\n            child_field: this.env.childField,\n        }\n    }\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    updateQuantity(quantity) {\n        if (this.productCatalogData.readOnly) {\n            return;\n        }\n        this.productCatalogData.quantity = quantity || 0;\n        this.debouncedUpdateQuantity();\n    }\n\n    /**\n     * Add the product to the order\n     */\n    addProduct(qty=1) {\n        this.updateQuantity(qty);\n    }\n\n    /**\n     * Remove the product to the order\n     */\n    removeProduct() {\n        this.updateQuantity(0);\n    }\n\n    /**\n     * Increase the quantity of the product on the order line.\n     */\n    increaseQuantity(qty=1) {\n        this.updateQuantity(this.productCatalogData.quantity + qty);\n    }\n\n    /**\n     * Set the quantity of the product on the order line.\n     *\n     * @param {Event} event\n     */\n    setQuantity(event) {\n        this.updateQuantity(parseFloat(event.target.value));\n    }\n\n    /**\n     * Decrease the quantity of the product on the order line.\n     */\n    decreaseQuantity() {\n        this.updateQuantity(parseFloat(this.productCatalogData.quantity - 1));\n    }\n}\n", "import { KanbanRenderer } from \"@web/views/kanban/kanban_renderer\";\nimport { useService } from \"@web/core/utils/hooks\";\n\nimport { ProductCatalogKanbanRecord } from \"./kanban_record\";\n\nexport class ProductCatalogKanbanRenderer extends KanbanRenderer {\n    static template = \"ProductCatalogKanbanRenderer\";\n    static components = {\n        ...KanbanRenderer.components,\n        KanbanRecord: ProductCatalogKanbanRecord,\n    };\n\n    setup() {\n        super.setup();\n        this.action = useService(\"action\");\n    }\n\n    get createProductContext() {\n        return {};\n    }\n\n    async createProduct() {\n        await this.action.doAction(\n            {\n                type: \"ir.actions.act_window\",\n                res_model: \"product.product\",\n                target: \"new\",\n                views: [[false, \"form\"]],\n                view_mode: \"form\",\n                context: this.createProductContext,\n            },\n            {\n                onClose: () => this.props.list.model.load(),\n            }\n        );\n    }\n}\n", "import { kanbanView } from \"@web/views/kanban/kanban_view\";\nimport { registry } from \"@web/core/registry\";\n\nimport { ProductCatalogKanbanController } from \"./kanban_controller\";\nimport { ProductCatalogKanbanModel } from \"./kanban_model\";\nimport { ProductCatalogKanbanRenderer } from \"./kanban_renderer\";\n\n\nexport const productCatalogKanbanView = {\n    ...kanbanView,\n    Controller: ProductCatalogKanbanController,\n    Model: ProductCatalogKanbanModel,\n    Renderer: ProductCatalogKanbanRenderer,\n};\n\nregistry.category(\"views\").add(\"product_kanban_catalog\", productCatalogKanbanView);\n", "import { Component } from \"@odoo/owl\";\nimport { formatFloat, formatMonetary } from \"@web/views/fields/formatters\";\n\nexport class ProductCatalogOrderLine extends Component {\n    static template = \"product.ProductCatalogOrderLine\";\n    static props = {\n        isSample: { type: Boolean, optional: true},\n        productId: Number,\n        quantity: Number,\n        price: Number,\n        productType: String,\n        uomDisplayName: String,\n        code: { type: String, optional: true},\n        readOnly: { type: Boolean, optional: true },\n        warning: { type: String, optional: true},\n    };\n\n    /**\n     * Focus input text when clicked\n     * @param {Event} ev \n     */\n    _onFocus(ev) {\n        ev.target.select();\n    }\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    isInOrder() {\n        return this.props.quantity !== 0;\n    }\n\n    get disableRemove() {\n        return false;\n    }\n\n    get disabledButtonTooltip() {\n        return \"\";\n    }\n\n    get price() {\n        const { currencyId, digits } = this.env;\n        return formatMonetary(this.props.price, { currencyId, digits });\n    }\n\n    get quantity() {\n        const digits = [false, this.env.precision];\n        const options = { digits, decimalPoint: \".\", thousandsSep: \"\" };\n        return parseFloat(formatFloat(this.props.quantity, options));\n    }\n\n    get showPrice() {\n        return true;\n    }\n}\n", "import { useAutoresize } from \"@web/core/utils/autoresize\";\n\n/**\n * This overriden version of the resizeTextArea method is specificly done for the product_label_section_and_note widget\n * His necessity is found in the fact that the cell of said widget doesn't contain only the input or textarea to resize\n * but also another node containing the name of the product if said data is available. This means that the autoresize\n * method which sets the height of the parent cell should sometimes add an additional row to the parent cell so that\n * no text overflows\n *\n * @param {Ref} ref\n */\nexport function useProductAndLabelAutoresize(ref, options = {}) {\n    useAutoresize(ref, { \n        onMounted: productAndLabelResizeTextArea, \n        onResize: productAndLabelResizeTextArea,\n        ...options,\n    });\n}\n\nexport function productAndLabelResizeTextArea(textarea, options = {}) {\n    const style = window.getComputedStyle(textarea);\n    if (options.targetParentName) {\n        let target = textarea.parentElement;\n        let shouldContinue = true;\n        while (target && shouldContinue) {\n            const totalParentHeight = Array.from(target.children).reduce((total, child) => {\n                const childHeight = child.style.height || style.lineHeight;\n                return total + parseFloat(childHeight);\n            }, 0);\n            target.style.height = `${totalParentHeight}px`;\n            if (target.getAttribute(\"name\") === options.targetParentName) {\n                shouldContinue = false;\n            }\n            target = target.parentElement;\n        }\n    }\n}\n", "/** @odoo-module */\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { getActiveHotkey } from \"@web/core/hotkeys/hotkey_service\";\nimport { Component, onMounted, onPatched, onWillUnmount, useEffect, useRef, useState } from \"@odoo/owl\";\nimport { Many2OneField } from \"@web/views/fields/many2one/many2one_field\";\nimport { useProductAndLabelAutoresize } from \"./product_and_label_autoresize\";\nimport { computeM2OProps, Many2One } from \"@web/views/fields/many2one/many2one\";\nimport { useInputField } from \"@web/views/fields/input_field_hook\";\n\nexport const ProductNameAndDescriptionListRendererMixin = {\n    getCellTitle(column, record) {\n        // When using this list renderer, we don't want the product_id cell to have a tooltip with its label.\n        if (this.productColumns.includes(column.name)) {\n            return;\n        }\n        return super.getCellTitle(column, record);\n    },\n\n    getActiveColumns() {\n        let activeColumns = super.getActiveColumns();\n        const productCol = activeColumns.find((col) => this.productColumns.includes(col.name));\n        const labelCol = activeColumns.find((col) => col.name === this.descriptionColumn);\n\n        if (productCol) {\n            if (labelCol) {\n                this.props.list.records.forEach((record) => (record.columnIsProductAndLabel = true));\n            } else {\n                this.props.list.records.forEach((record) => (record.columnIsProductAndLabel = false));\n            }\n            activeColumns = activeColumns.filter((col) => col.name !== this.descriptionColumn);\n            this.titleField = productCol.name;\n        } else {\n            this.titleField = \"name\";\n        }\n\n        return activeColumns;\n    }\n};\n\nexport class ProductNameAndDescriptionField extends Component {\n    static components = { Many2One };\n    static props = { ...Many2OneField.props };\n    static template = Many2One.template;\n\n    static descriptionColumn = \"\";\n\n    setup() {\n        this.isPrintMode = useState({ value: false });\n        this.labelVisibility = useState({ value: false });\n        this.switchToLabel = false;\n        this.columnIsProductAndLabel = useState({ value: this.props.record.columnIsProductAndLabel });\n        this.labelNode = useRef(\"labelNodeRef\");\n        useProductAndLabelAutoresize(this.labelNode, { targetParentName: this.props.name });\n        this.productNode = useRef(\"productNodeRef\");\n        useProductAndLabelAutoresize(this.productNode, { targetParentName: this.props.name });\n\n        this.descriptionColumn = this.constructor.descriptionColumn;\n        useInputField({\n            ref: this.labelNode,\n            fieldName: this.descriptionColumn,\n            getValue: () => this.label,\n            parse: (v) => this.parseLabel(v),\n        });\n\n        useEffect(\n            () => {\n                this.columnIsProductAndLabel.value = this.props.record.columnIsProductAndLabel;\n            },\n            () => [this.props.record.columnIsProductAndLabel]\n        );\n\n        onPatched(() => {\n            if (this.labelNode.el && this.switchToLabel) {\n                this.switchToLabel = false;\n                this.labelNode.el.focus();\n            }\n        });\n\n        this.onBeforePrint = () => {\n            this.isPrintMode.value = true;\n        };\n\n        this.onAfterPrint = () => {\n            this.isPrintMode.value = false;\n        };\n\n        // The following hooks are used to make a div visible only in the print view. This div is necessary in the\n        // print view in order not to have scroll bars but can't be displayed in the normal view because it adds\n        // an empty line. This is done by switching an attribute to true only during the print view life cycle and\n        // including the said div in a t-if depending on that attribute.\n        onMounted(() => {\n            window.addEventListener(\"beforeprint\", this.onBeforePrint);\n            window.addEventListener(\"afterprint\", this.onAfterPrint);\n        });\n\n        onWillUnmount(() => {\n            window.removeEventListener(\"beforeprint\", this.onBeforePrint);\n            window.removeEventListener(\"afterprint\", this.onAfterPrint);\n        });\n    }\n\n    get productName() {\n        return this.props.record.data[this.props.name].display_name || \"\";\n    }\n\n    get label() {\n        let label = this.props.record.data[this.descriptionColumn];\n        if (label.includes(this.productName)) {\n            label = label.replace(this.productName, \"\");\n        }\n        return label.trim();\n    }\n\n    get m2oProps() {\n        const p = computeM2OProps(this.props);\n        let value = p.value && { ...p.value };\n        if (this.props.readonly && this.productName) {\n            value = { ...value, display_name: this.productName };\n        }\n        return {\n            ...p,\n            canOpen: !this.props.readonly || this.isProductClickable,\n            placeholder: _t(\"Search a product\"),\n            value,\n        };\n    }\n\n    get isProductClickable() {\n        return this.props.record.evalContext.parent.state !== \"draft\";\n    }\n\n    get showLabelVisibilityToggler() {\n        return !this.props.readonly && this.columnIsProductAndLabel.value && !this.label;\n    }\n\n    switchLabelVisibility() {\n        this.labelVisibility.value = !this.labelVisibility.value;\n        this.switchToLabel = true;\n    }\n\n    parseLabel(value) {\n        return value || this.productName;\n    }\n\n    /**\n     * @param {KeyboardEvent} ev\n     */\n    onM2oInputKeydown(ev) {\n        const hotkey = getActiveHotkey(ev);\n        if (hotkey === \"enter\" && this.showLabelVisibilityToggler) {\n            this.switchLabelVisibility();\n            ev.stopPropagation();\n            ev.preventDefault();\n        }\n    }\n}\n", "import { registry } from \"@web/core/registry\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { evaluateExpr } from \"@web/core/py_js/py\";\nimport { getNextTabableElement, getPreviousTabableElement } from \"@web/core/utils/ui\";\nimport { usePosition } from \"@web/core/position/position_hook\";\nimport { getActiveHotkey } from \"@web/core/hotkeys/hotkey_service\";\nimport { shallowEqual } from \"@web/core/utils/arrays\";\nimport { roundDecimals } from \"@web/core/utils/numbers\";\nimport { isMobileOS } from \"@web/core/browser/feature_detection\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { useRecordObserver } from \"@web/model/relational_model/utils\";\n\nimport { standardFieldProps } from \"@web/views/fields/standard_field_props\";\nimport { TagsList } from \"@web/core/tags_list/tags_list\";\nimport { useOpenMany2XRecord } from \"@web/views/fields/relational_utils\";\nimport { formatPercentage } from \"@web/views/fields/formatters\";\n\nimport { Record } from \"@web/model/record\";\nimport { Field } from \"@web/views/fields/field\";\nimport {\n    Component,\n    useState,\n    useRef,\n    useExternalListener,\n    onWillStart,\n    onPatched,\n} from \"@odoo/owl\";\n\nexport class AnalyticDistribution extends Component {\n    static template = \"analytic.AnalyticDistribution\";\n    static components = {\n        TagsList,\n        Record,\n        Field,\n    }\n\n    static props = {\n        ...standardFieldProps,\n        business_domain: { type: String, optional: true },\n        account_field: { type: String, optional: true },\n        product_field: { type: String, optional: true },\n        amount_field: { type: String, optional: true },\n        business_domain_compute: { type: String, optional: true },\n        force_applicability: { type: String, optional: true },\n        allow_save: { type: Boolean, optional: true },\n        multi_edit: { type: Boolean, optional: true },\n        placeholder: { type: String, optional: true },\n    }\n\n    setup(){\n        this.orm = useService(\"orm\");\n        this.batchedOrm = useService(\"batchedOrm\");\n\n        this.state = useState({\n            showDropdown: false,\n            formattedData: [],\n            update_plan: {},\n        });\n\n        this.widgetRef = useRef(\"analyticDistribution\");\n        this.dropdownRef = useRef(\"analyticDropdown\");\n        this.mainRef = useRef(\"mainElement\");\n        this.addLineButton = useRef(\"addLineButton\");\n        usePosition(\"analyticDropdown\", () => this.widgetRef.el);\n\n        this.nextId = 1;\n        this.focusSelector = false;\n\n        this.currentValue = this.props.record.data[this.props.name];\n        this.initialFormattedData = [];\n\n        onWillStart(this.willStart);\n        useRecordObserver(this.willUpdateRecord.bind(this));\n        onPatched(this.patched);\n\n        useExternalListener(window, \"click\", this.onWindowClick, true);\n        useExternalListener(window, \"resize\", this.onWindowResized);\n\n        this.openTemplate = useOpenMany2XRecord({\n            resModel: \"account.analytic.distribution.model\",\n            activeActions: {\n                create: true,\n                edit: false,\n                write: true,\n            },\n            isToMany: false,\n            onRecordSaved: async (record) => {\n                if (!this.props.record.model.multiEdit) {\n                    this.mainRef.el.focus();\n                }\n            },\n            onClose: () => {\n                if (!this.props.record.model.multiEdit) {\n                    this.mainRef.el.focus();\n                }\n            },\n            fieldString: _t(\"Analytic Distribution Model\"),\n        });\n        this.allPlans = [];\n        this.planIdToColumn = {};\n        this.lastAccount = this.props.account_field && this.props.record.data[this.props.account_field] || false;\n        this.lastProduct = this.props.product_field && this.props.record.data[this.props.product_field] || false;\n    }\n\n    // Lifecycle\n    async willStart() {\n        if (this.editingRecord) {\n            // for performance in list views, plans are not retrieved until they are required.\n            await this.fetchAllPlans(this.props);\n        }\n        await this.jsonToData(this.props.record.data[this.props.name]);\n        if (this.props.multi_edit) {\n            this.initialFormattedData = this.state.formattedData;\n            this.state.formattedData = [];\n        }\n    }\n\n    async willUpdateRecord(record) {\n        // Unless force_applicability, Plans need to be retrieved again as the product or account might have changed\n        // and thus different applicabilities apply\n        // or a model applies that contains unavailable plans\n        // This should only execute when these fields have changed, therefore we use the `_field` props.\n        const valueChanged =\n            JSON.stringify(this.currentValue) !==\n            JSON.stringify(record.data[this.props.name]);\n        const currentAccount = this.props.account_field && record.data[this.props.account_field] || false;\n        const currentProduct = this.props.product_field && record.data[this.props.product_field] || false;\n        const accountChanged = !shallowEqual(this.lastAccount, currentAccount);\n        const productChanged = !shallowEqual(this.lastProduct, currentProduct);\n        if (valueChanged || accountChanged || productChanged) {\n            if (!this.props.force_applicability) {\n                await this.fetchAllPlans({ record });\n            }\n            this.lastAccount = accountChanged && currentAccount || this.lastAccount;\n            this.lastProduct = productChanged && currentProduct || this.lastProduct;\n            await this.jsonToData(record.data[this.props.name]);\n            if (this.props.multi_edit) {\n                this.initialFormattedData = this.state.formattedData;\n                this.state.formattedData = [];\n            }\n        }\n        this.currentValue = record.data[this.props.name];\n    }\n\n    patched() {\n        this.focusToSelector();\n    }\n\n    /**\n     * Computes the totals for each account, grouped by plan (primarily used in tags)\n     * @returns {Object}\n     */\n    accountTotalsByPlan() {\n        const accountTotals = {};\n        const formattedData = this.props.multi_edit ? this.initialFormattedData : this.state.formattedData;\n        formattedData.map((line) => {\n            line.analyticAccounts.map((column) => {\n                if (column.accountId) {\n                    let {\n                        accId = column.accountId,\n                        accName = column.accountDisplayName,\n                        total = 0.0,\n                        planId = column.accountRootPlanId,\n                        planColor = column.accountColor,\n                    } = accountTotals[column.accountRootPlanId]?.[column.accountId] || {};\n\n                    total += roundDecimals(line.percentage, this.decimalPrecision.digits[1] + 2);\n\n                    accountTotals[planId] = accountTotals[planId] || {};\n                    accountTotals[planId][accId] = { accId, accName, planId, total, planColor};\n                }\n            })\n        });\n        return accountTotals;\n    }\n\n    /**\n     * Computes the totals for each plan (used in the table headers)\n     * @returns {Object}\n     */\n    planTotals() {\n        const summary = this.accountTotalsByPlan();\n        this.allPlans.map((plan) => {\n            const planTotal = (summary[plan.id] && Object.values(summary[plan.id]) || []).reduce((prev, next) => prev + next.total, 0.0);\n            const className = plan.applicability === \"mandatory\" && !this.planIsComplete(planTotal) ? 'text-danger' : plan.applicability === \"mandatory\" ? 'text-success' : '';\n            summary[plan.id] = {\n                value: planTotal,\n                formattedValue: formatPercentage(planTotal, this.decimalPrecision),\n                class: className,\n                applicability: plan.applicability,\n            }\n        });\n        return summary;\n    }\n\n    planIsComplete(total) {\n        return roundDecimals(total, this.decimalPrecision.digits[1] + 2) === 1;\n    }\n\n    /**\n     * Converts the account Totals to a list of tags\n     * PlanA  PlanB  PlanC  Percentage\n     * A1                   100\n     *        B1            80.123     => [\"A1\", \"80.12% B1\", \"C1\"]\n     *               C1     100\n     *\n     * PlanA  PlanB  PlanC  Percentage\n     * A1     B1     C1     50\n     * A2     B1     C1     50         => [\"50% A1 | 50% A2 | 50% A3\", \"150% B1\", \"C1 | 50% C2\"]\n     * A3     B1     C2     50\n     * @returns [List] of tag objects\n     */\n    planSummaryTags() {\n        const accountTotals = this.accountTotalsByPlan();\n        return Object.values(accountTotals).map((planSummary) => {\n            const accs = Object.values(planSummary);\n            return {\n                id: accs[0].planId,\n                text: accs.reduce((p, n) => p + (p.length ? \" | \" : \"\") + (this.planIsComplete(n.total) ? n.accName : `${formatPercentage(n.total)} ${n.accName}`) , \"\"),\n                colorIndex: accs[0].planColor,\n                onClick: (ev) => this.tagClicked(ev),\n            };\n        });\n    }\n\n    plansToArray() {\n        return this.allPlans.map((plan) => ({\n            planId: plan.id,\n            planName: plan.name,\n            planColor: plan.color,\n        }));\n    }\n\n    async jsonToData(jsonFieldValue) {\n        const analyticAccountIds = jsonFieldValue ? Object.keys(jsonFieldValue).filter((key) => key != '__update__' ).map((key) => key.split(',')).flat().map((id) => parseInt(id)) : [];\n        const analyticAccountDict = analyticAccountIds.length ? await this.fetchAnalyticAccounts([[\"id\", \"in\", analyticAccountIds]]) : [];\n\n        let distribution = [];\n        let accountNotFound = false;\n\n        for (const [accountIds, percentage] of Object.entries(jsonFieldValue)) {\n            if (accountIds == '__update__') continue;\n            const defaultVals = this.plansToArray(); // empty if the popup was not opened\n            const ids = accountIds.split(',');\n\n            for (const id of ids) {\n                const account = analyticAccountDict[parseInt(id)];\n                if (account) {\n                    // since tags are displayed even though plans might not be retrieved (ie defaultVals is empty)\n                    // push the accounts anyway, as order doesn't matter\n                    // once the popup is opened, plans are fetched and the analyticAccounts list will be ordered\n                    Object.assign(defaultVals.find((plan) => plan.planId == account.root_plan_id[0]) || defaultVals.push({}) && defaultVals[defaultVals.length-1],\n                    {\n                        accountId: parseInt(id),\n                        accountDisplayName: account.display_name,\n                        accountColor: account.color,\n                        accountRootPlanId: account.root_plan_id[0],\n                    });\n                } else {\n                    accountNotFound = true;\n                }\n            }\n            distribution.push({\n                analyticAccounts: defaultVals,\n                percentage: percentage / 100,\n                id: this.nextId++,\n            })\n        }\n        this.state.formattedData = distribution;\n        if (accountNotFound) {\n            // Analytic accounts in the json were not found, save the json without them\n            await this.save();\n        }\n    }\n\n    recordProps(line) {\n        const analyticAccountFields = {\n            id: { type: \"int\" },\n            display_name: { type: \"char\" },\n            color: { type: \"int\" },\n            plan_id: { type: \"many2one\" },\n            root_plan_id: { type: \"many2one\" },\n        };\n        let recordFields = {};\n        const values = {};\n        // Analytic Account fields\n        line.analyticAccounts.map((account) => {\n            const fieldName = this.planIdToColumn[account.planId];\n            const companyId = this.props.record.data.company_id && this.props.record.data.company_id[0];\n            const domain = companyId\n                ? [\n                    \"&\",\n                    [\"root_plan_id\", \"=\", account.planId],\n                    \"|\",\n                    [\"company_id\", \"parent_of\", companyId],\n                    [\"company_id\", \"=\", false],\n                  ]\n                : [[\"root_plan_id\", \"=\", account.planId]];\n            recordFields[fieldName] = {\n                string: account.planName,\n                relation: \"account.analytic.account\",\n                type: \"many2one\",\n                related: {\n                    fields: analyticAccountFields,\n                    activeFields: analyticAccountFields,\n                },\n                domain,\n            };\n            values[fieldName] = account?.accountId\n                ? [account.accountId, account.accountDisplayName]\n                : false;\n        });\n        // Percentage field\n        recordFields['percentage'] = {\n            string: _t(\"Percentage\"),\n            type: \"percentage\",\n            cellClass: \"numeric_column_width\",\n            ...this.decimalPrecision,\n        };\n        values['percentage'] = line.percentage;\n        // Value field copied from original\n        if (this.props.amount_field) {\n            const { string, name, type, currency_field } = this.props.record.fields[this.props.amount_field];\n            recordFields[name] = { string, name, type, currency_field, cellClass: \"numeric_column_width\" };\n            values[name] = this.props.record.data[name] * values['percentage'];\n            // Currency field\n            if (currency_field) {\n                const { string, name, type, relation } = this.props.record.fields[currency_field];\n                recordFields[currency_field] = { name, string, type, relation, invisible: true };\n                values[currency_field] = [this.props.record.data[currency_field].id, \"\"];\n            }\n        }\n        return {\n            fields: recordFields,\n            values: values,\n            activeFields: recordFields,\n            hooks: {\n                onRecordChanged: async (record, changes) => await this.lineChanged(record, changes, line),\n            }\n        }\n    }\n\n    accountCount(line) {\n        return line.analyticAccounts.map((acc) => acc.accountId).filter(Boolean).length;\n    }\n\n    lineIsValid(line) {\n        return this.accountCount(line) && line.percentage;\n    }\n\n    // ORM\n    fetchPlansArgs({ record }) {\n        let args = {};\n        if (this.props.business_domain_compute) {\n            args['business_domain'] = evaluateExpr(this.props.business_domain_compute, record.evalContext);\n        }\n        if (this.props.business_domain) {\n            args['business_domain'] = this.props.business_domain;\n        }\n        if (this.props.product_field && record.data[this.props.product_field]) {\n            args['product'] = record.data[this.props.product_field].id;\n        }\n        if (this.props.account_field && record.data[this.props.account_field]) {\n            args['account'] = record.data[this.props.account_field].id;\n        }\n        if (this.props.force_applicability) {\n            args['applicability'] = this.props.force_applicability;\n        }\n        const existing_account_ids = Object.keys(record.data[this.props.name]).map((k) => k.split(\",\")).flat().map((i) => parseInt(i));\n        if (existing_account_ids.length) {\n            args['existing_account_ids'] = existing_account_ids;\n        }\n        if (record.data.company_id) {\n            args['company_id'] = record.data.company_id.id;\n        }\n        return args;\n    }\n\n    async fetchAllPlans(props) {\n        const argsPlan = this.fetchPlansArgs(props);\n        this.allPlans = await this.orm.call(\"account.analytic.plan\", \"get_relevant_plans\", [], argsPlan);\n        this.planIdToColumn = Object.fromEntries(this.allPlans.map((plan) => [plan.id, plan.column_name]));\n        if (!this.props.multi_edit) {\n            this.allPlans.forEach(plan => {\n                this.state.update_plan[plan.column_name] = true;\n            });\n        }\n    }\n\n    async fetchAnalyticAccounts(domain) {\n        const args = {\n            domain: domain,\n            fields: [\"id\", \"display_name\", \"root_plan_id\", \"color\"],\n            context: [],\n        }\n        // batched call\n        const records = await this.batchedOrm.read(\"account.analytic.account\", domain[0][2], args.fields, {});\n        return Object.assign({}, ...records.map((r) => {\n            const {id, ...rest} = r;\n            return {[id]: rest};\n        }));\n    }\n\n    // Editing Distributions\n    async lineChanged(record, changes, line) {\n        // record analytic account changes to the state\n        for (const account of line.analyticAccounts) {\n            const selected = record.data[this.planIdToColumn[account.planId]];\n            account.accountId = selected.id;\n            account.accountDisplayName = selected.display_name;\n            account.accountColor = account.planColor;\n            account.accountRootPlanId = account.planId;\n        }\n        // record percentage or value changes\n        if (changes.percentage != line.percentage) {\n            roundDecimals(line.percentage = record.data.percentage, this.decimalPrecision.digits[1] + 2);\n        } else if (\n            this.valueColumnEnabled &&\n            changes[this.props.amount_field] != line[this.props.amount_field]\n        ) {\n            line.percentage = roundDecimals(\n                record.data[this.props.amount_field] / this.props.record.data[this.props.amount_field],\n                this.decimalPrecision.digits[1] + 2);\n        }\n    }\n\n    // Getters\n    get valueColumnEnabled() {\n        return Boolean(this.props.amount_field && this.props.record.data[this.props.amount_field]);\n    }\n\n    get decimalPrecision() {\n        return { digits: [12, this.props.record.data.analytic_precision || 2] };\n    }\n\n    get allowSave() {\n        return this.props.allow_save && this.state.formattedData.some((line) => this.lineIsValid(line));\n    }\n\n    get editingRecord() {\n        return !this.props.readonly;\n    }\n\n    get isDropdownOpen() {\n        return this.state.showDropdown && !!this.dropdownRef.el;\n    }\n\n    // actions\n    addLine() {\n        let maxMandatory = 0, maxOptional = 0, hasMandatory = false;\n\n        Object.values(this.planTotals()).filter((plan) => plan.value < 1).map((plan) => {\n            if (plan.applicability == \"mandatory\"){\n                maxMandatory = Math.max(plan.value, maxMandatory);\n                hasMandatory = true;\n            } else {\n                maxOptional = Math.max(plan.value, maxOptional);\n            }\n        });\n        let noPlanTotal = this.state.formattedData.filter((line) => !this.accountCount(line)).reduce((p, n) => p + n.percentage, 0);\n        const remainder = roundDecimals(1 - (hasMandatory ? maxMandatory : (maxOptional || noPlanTotal)), this.decimalPrecision.digits[1] + 2);\n        const lineToAdd = {\n            id: this.nextId++,\n            analyticAccounts: this.plansToArray(),\n            percentage: Math.max(remainder, 0) || 1,\n        }\n        this.state.formattedData.push(lineToAdd);\n        this.setFocusSelector(`[name=line_${this.state.formattedData.length - 1}] td:first-of-type`);\n    }\n\n    deleteLine(index) {\n        this.state.formattedData.splice(index, 1);\n        if (!this.state.formattedData.length) {\n            this.addLine();\n        }\n    }\n\n    dataToJson() {\n        const result = {};\n        if (this.props.multi_edit) {\n            result.__update__ = Object.entries(this.state.update_plan).filter((e) => e[1]).map((e) => e[0]);\n        }\n        this.state.formattedData = this.state.formattedData.filter((line) => this.accountCount(line));\n        this.state.formattedData.map((line) => {\n            const key = line.analyticAccounts.reduce((p, n) => p.concat(n.accountId ? n.accountId : []), []);\n            result[key] = (result[key] || 0) + line.percentage * 100;\n        });\n        return result;\n    }\n\n    async save() {\n        await this.props.record.update({ [this.props.name]: this.dataToJson() });\n        if (this.props.multi_edit) {\n            await this.jsonToData(this.props.record.data[this.props.name]);\n            this.initialFormattedData = this.state.formattedData;\n            this.state.formattedData = [];\n            this.state.update_plan = {};\n        }\n    }\n\n    onSaveNew() {\n        this.closeAnalyticEditor();\n        const { record, product_field, account_field } = this.props;\n        this.openTemplate({ resId: false, context: {\n            'default_analytic_distribution': this.dataToJson(),\n            'default_partner_id': record.data['partner_id'] ? record.data['partner_id'].id : undefined,\n            'default_product_id': product_field ? record.data[product_field].id : undefined,\n            'default_account_prefix': account_field ? record.data[account_field].display_name.substr(0, 3) : undefined,\n        }});\n    }\n\n    forceCloseEditor() {\n        // focus to the main Element but the dropdown should not open\n        this.preventOpen = true;\n        this.closeAnalyticEditor();\n        this.mainRef.el.focus();\n        this.preventOpen = false;\n    }\n\n    closeAnalyticEditor() {\n        this.save();\n        this.state.showDropdown = false;\n    }\n\n    async openAnalyticEditor() {\n        if (!this.allPlans.length) {\n            await this.fetchAllPlans(this.props);\n            await this.jsonToData(this.props.record.data[this.props.name]);\n            if (this.props.multi_edit) {\n                this.state.formattedData = [];\n            }\n        }\n        if (!this.state.formattedData.length) {\n            await this.addLine();\n        }\n        this.setFocusSelector(\"[name='line_0'] td:first-of-type\");\n        this.state.showDropdown = true;\n    }\n\n    async tagClicked(ev) {\n        if (this.editingRecord && !this.isDropdownOpen) {\n            // TODO: focus is not working when tag is clicked while on an editable line\n            await this.openAnalyticEditor();\n        }\n        if (this.isDropdownOpen) {\n            this.setFocusSelector(\"[name='line_0'] td:first-of-type\");\n            this.focusToSelector();\n            ev.stopPropagation();\n        }\n    }\n\n    // Focus\n    onMainElementFocus(ev) {\n        if (!this.isDropdownOpen && !this.preventOpen) {\n            this.openAnalyticEditor();\n        }\n    }\n\n    focusToSelector() {\n        if (this.focusSelector && this.isDropdownOpen) {\n            this.focus(this.adjacentElementToFocus(\"next\", this.dropdownRef.el.querySelector(this.focusSelector)));\n        }\n        this.focusSelector = false;\n    }\n\n    setFocusSelector(selector) {\n        this.focusSelector = selector;\n    }\n\n    adjacentElementToFocus(direction, el = null) {\n        if (!this.isDropdownOpen) {\n            return null;\n        }\n        if (!el) {\n            el = this.dropdownRef.el;\n        }\n        return direction == \"next\" ? getNextTabableElement(el) : getPreviousTabableElement(el);\n    }\n\n    focusAdjacent(direction) {\n        const elementToFocus = this.adjacentElementToFocus(direction);\n        if (elementToFocus){\n            this.focus(elementToFocus);\n            return true;\n        }\n        return false;\n    }\n\n    focus(el) {\n        if (!el) return;\n        el.focus();\n        if ([\"INPUT\", \"TEXTAREA\"].includes(el.tagName)) {\n            if (el.selectionStart) {\n                el.selectionStart = 0;\n                el.selectionEnd = el.value.length;\n            }\n            el.select();\n        }\n    }\n\n    // Keys and Clicks\n    async onWidgetKeydown(ev) {\n        if (!this.editingRecord) {\n            return;\n        }\n        const hotkey = getActiveHotkey(ev);\n        switch (hotkey) {\n            case \"enter\":\n            case \"tab\": {\n                if (this.isDropdownOpen) {\n                    const closestCell = ev.target.closest(\"td, th\");\n                    const row = closestCell.parentElement;\n                    const line = this.state.formattedData[parseInt(row.id)];\n                    if (this.adjacentElementToFocus(\"next\") == this.addLineButton.el && line && this.lineIsValid(line)) {\n                        this.addLine();\n                        break;\n                    }\n                    this.focusAdjacent(\"next\") || this.forceCloseEditor();\n                    break;\n                };\n                return;\n            }\n            case \"shift+tab\": {\n                if (this.isDropdownOpen) {\n                    this.focusAdjacent(\"previous\") || this.forceCloseEditor();\n                    break;\n                };\n                return;\n            }\n            case \"escape\": {\n                if (this.isDropdownOpen) {\n                    this.forceCloseEditor();\n                    break;\n                }\n            }\n            case \"arrowdown\": {\n                if (!this.isDropdownOpen) {\n                    this.onMainElementFocus();\n                    break;\n                }\n                return;\n            }\n            default: {\n                return;\n            }\n        }\n        ev.preventDefault();\n        ev.stopPropagation();\n    }\n\n    onWindowClick(ev) {\n        /*\n        Dropdown should be closed only if all these condition are true:\n            - dropdown is open\n            - click is outside widget element (widgetRef)\n            - there is no active modal containing a list/kanban view (search more modal)\n            - there is no popover (click is not in search modal's search bar menu)\n            - click is not targeting document dom element (drag and drop search more modal)\n        */\n\n        const selectors = [\n            \".o_popover\",\n            \".modal:not(.o_inactive_modal):not(:has(.o_act_window))\",\n        ];\n        if (this.isDropdownOpen\n            && !this.widgetRef.el.contains(ev.target)\n            && !ev.target.closest(selectors.join(\",\"))\n            && !ev.target.isSameNode(document.documentElement)\n           ) {\n            this.forceCloseEditor();\n        }\n    }\n\n    onWindowResized() {\n        // popup ui is ugly when window is resized, so close it\n        if (this.isDropdownOpen && !isMobileOS()) {\n            this.forceCloseEditor();\n        }\n    }\n}\n\nexport const analyticDistribution = {\n    component: AnalyticDistribution,\n    supportedTypes: [\"json\"],\n    fieldDependencies: [\n        { name: \"analytic_precision\", type: \"integer\" },\n    ],\n    supportedOptions: [\n        {\n            label: _t(\"Disable save\"),\n            name: \"disable_save\",\n            type: \"boolean\",\n        },\n        {\n            label: _t(\"Multi edit\"),\n            name: \"multi_edit\",\n            type: \"boolean\",\n        },\n        {\n            label: _t(\"Force applicability\"),\n            name: \"force_applicability\",\n            type: \"boolean\",\n        },\n        {\n            label: _t(\"Business domain\"),\n            name: \"business_domain\",\n            type: \"string\",\n        },\n        {\n            label: _t(\"Product field\"),\n            name: \"product_field\",\n            type: \"field\",\n            availableTypes: [\"many2one\"],\n        },\n        {\n            label: _t(\"Amount field\"),\n            name: \"amount_field\",\n            type: \"field\",\n            availableTypes: [\"monetary\"],\n        },\n        {\n            label: _t(\"Account field\"),\n            name: \"account_field\",\n            type: \"field\",\n            availableTypes: [\"many2one\"],\n        },\n        {\n            label: _t(\"Dynamic Placeholder\"),\n            name: \"placeholder_field\",\n            type: \"field\",\n            availableTypes: [\"char\"],\n        },\n    ],\n    extractProps: ({ attrs, options, placeholder }) => ({\n        business_domain: options.business_domain,\n        account_field: options.account_field,\n        product_field: options.product_field,\n        amount_field: options.amount_field,\n        business_domain_compute: attrs.business_domain_compute,\n        force_applicability: options.force_applicability,\n        allow_save: !options.disable_save,\n        multi_edit: options.multi_edit,\n        placeholder: placeholder,\n    }),\n};\n\nregistry.category(\"fields\").add(\"analytic_distribution\", analyticDistribution);\n", "import { registry } from \"@web/core/registry\";\nimport { ORM } from \"@web/core/orm_service\";\nimport { unique } from \"@web/core/utils/arrays\";\nimport { Deferred } from \"@web/core/utils/concurrency\";\n\nclass RequestBatcherORM extends ORM {\n    constructor() {\n        super();\n        this.searchReadBatches = {};\n        this.searchReadBatchId = 1;\n        this.batches = {};\n    }\n\n    /**\n     * @param {number[]} ids\n     * @param {any[]} keys\n     * @param {Function} callback\n     * @returns {Promise<any>}\n     */\n    async batch(ids, keys, callback) {\n        const key = JSON.stringify(keys);\n        let batch = this.batches[key];\n        if (!batch) {\n            batch = {\n                deferred: new Deferred(),\n                scheduled: false,\n                ids: [],\n            };\n            this.batches[key] = batch;\n        }\n        batch.ids = unique([...batch.ids, ...ids]);\n\n        if (!batch.scheduled) {\n            batch.scheduled = true;\n            Promise.resolve().then(async () => {\n                delete this.batches[key];\n                let result;\n                try {\n                    result = await callback(batch.ids);\n                } catch (e) {\n                    return batch.deferred.reject(e);\n                }\n                batch.deferred.resolve(result);\n            });\n        }\n\n        return batch.deferred;\n    }\n\n    /**\n     * Entry point to batch \"read\" calls. If the `fields` and `resModel`\n     * arguments have already been called, the given ids are added to the\n     * previous list of ids to perform a single read call. Once the server\n     * responds, records are then dispatched to the callees based on the\n     * given ids arguments (kept in the closure).\n     *\n     * @param {string} resModel\n     * @param {number[]} resIds\n     * @param {string[]} fields\n     * @returns {Promise<Object[]>}\n     */\n    async read(resModel, resIds, fields, kwargs) {\n        const records = await this.batch(resIds, [\"read\", resModel, fields, kwargs], (resIds) =>\n            super.read(resModel, resIds, fields, kwargs)\n        );\n        return records.filter((r) => resIds.includes(r.id));\n    }\n}\n\nexport const batchedOrmService = {\n    async: [\n        \"call\",\n        \"create\",\n        \"nameGet\",\n        \"read\",\n        \"formattedReadGroup\",\n        \"search\",\n        \"searchRead\",\n        \"unlink\",\n        \"webSearchRead\",\n        \"write\",\n    ],\n    start() {\n        return new RequestBatcherORM();\n    },\n};\n\nregistry.category(\"services\").add(\"batchedOrm\", batchedOrmService);\n", "import { SearchModel } from \"@web/search/search_model\";\n\nconst PLAN_REGEX = /^(?:x_)?(x_plan\\d+_id|account_id)(_\\d+)?$/;\n\nexport class AnalyticSearchModel extends SearchModel {\n    getSearchItems(predicate) {\n        let searchItems = super.getSearchItems(predicate);\n        const mapped = Map.groupBy(\n            searchItems.filter((f) => f.fieldName?.match(PLAN_REGEX)),\n            (f) => f.fieldName.match(PLAN_REGEX)[1],\n        );\n        searchItems = searchItems.filter(\n            (f) => !f.fieldName?.match(PLAN_REGEX) || mapped.has(f.fieldName)\n        );\n        searchItems.forEach((f) => {\n            if (f.fieldName && mapped.has(f.fieldName) && mapped.get(f.fieldName).length > 1) {\n                f.options = mapped.get(f.fieldName);\n            }\n        });\n        return searchItems;\n    }\n\n    toggleDateGroupBy(searchItemId, intervalId) {\n        if (typeof(intervalId) === \"number\") {\n            this.toggleSearchItem(intervalId);\n        } else {\n            super.toggleDateGroupBy(searchItemId, intervalId);\n        }\n    }\n}\n", "import { registry } from \"@web/core/registry\";\nimport { kanbanView } from \"@web/views/kanban/kanban_view\";\nimport { AnalyticSearchModel } from \"@analytic/views/analytic_search_model\";\n\nexport const analyticKanbanView = {\n    ...kanbanView,\n    SearchModel: AnalyticSearchModel,\n};\n\nregistry.category(\"views\").add(\"analytic_kanban\", analyticKanbanView);\n", "import { registry } from \"@web/core/registry\";\nimport { listView } from \"@web/views/list/list_view\";\nimport { AnalyticSearchModel } from \"@analytic/views/analytic_search_model\";\n\nexport const analyticListView = {\n    ...listView,\n    SearchModel: AnalyticSearchModel,\n};\n\nregistry.category(\"views\").add(\"analytic_list\", analyticListView);\n", "import { PortalWizardUserListController } from \"../list/portal_wizard_user_list_controller\";\nimport { X2ManyField, x2ManyField } from \"@web/views/fields/x2many/x2many_field\";\nimport { registry } from \"@web/core/registry\";\n\nexport class PortalUserX2ManyField extends X2ManyField {\n    static components = {\n        ...X2ManyField.components,\n        Controller: PortalWizardUserListController,\n    };\n}\n\nexport const portalUserX2ManyField = {\n    ...x2ManyField,\n    component: PortalUserX2ManyField,\n};\n\nregistry.category(\"fields\").add(\"portal_wizard_user_one2many\", portalUserX2ManyField);\n", "import { ListController } from \"@web/views/list/list_controller\";\n\nexport class PortalWizardUserListController extends ListController {\n    setup() {\n        super.setup();\n        this.isPortalActionOngoing = false;\n    }\n\n    /**\n     * @override\n     */\n     async beforeExecuteActionButton(clickParams) {\n        if (clickParams.name === 'action_refresh_modal' || this.isPortalActionOngoing) {\n            return false;\n        }\n        this.isPortalActionOngoing = true;\n        return super.beforeExecuteActionButton(clickParams);\n    }\n    \n    /**\n     * @override\n     */\n    async afterExecuteActionButton(clickParams) {\n        this.isPortalActionOngoing = false;\n    }\n}\n", "import { ListRenderer } from \"@web/views/list/list_renderer\";\nimport { useEffect } from \"@odoo/owl\";\n\nexport class SectionListRenderer extends ListRenderer {\n    setup() {\n        super.setup();\n\n        this.displayType = \"line_section\";\n        this.titleField = \"title\";\n\n        useEffect(\n            (table) => {\n                if (table) {\n                    table.classList.add(\"o_section_list_view\");\n                }\n            },\n            () => [this.tableRef.el]\n        );\n    }\n\n    getColumns(record) {\n        const columns = super.getColumns(record);\n        if (this.isSection(record)) {\n            return this.getSectionColumns(columns);\n        }\n        return columns;\n    }\n\n    getRowClass(record) {\n        const classNames = super.getRowClass(record).split(\" \");\n        if (this.isSection(record)) {\n            classNames.push(`o_is_${this.displayType}`, `fw-bold`);\n        }\n        return classNames.join(\" \");\n    }\n\n    getSectionColumns(columns) {\n        const sectionColumns = columns.filter((col) => col.widget === \"handle\");\n        let colspan = columns.length - sectionColumns.length;\n        if (this.activeActions.onDelete) {\n            colspan++;\n        }\n        const titleCol = columns.find(\n            (col) => col.type === \"field\" && col.name === this.titleField\n        );\n        sectionColumns.push({ ...titleCol, colspan });\n        return sectionColumns;\n    }\n\n    isSection(record) {\n        return record.data.display_type === this.displayType;\n    }\n}\nSectionListRenderer.recordRowTemplate = \"resource.SectionListRenderer.RecordRow\";\n", "import { SectionListRenderer } from \"./section_list_renderer\";\nimport { registry } from \"@web/core/registry\";\nimport { X2ManyField, x2ManyField } from \"@web/views/fields/x2many/x2many_field\";\n\nclass SectionOneToManyField extends X2ManyField {\n    static components = {\n        ...X2ManyField.components,\n        ListRenderer: SectionListRenderer,\n    };\n    static defaultProps = {\n        ...X2ManyField.defaultProps,\n        editable: \"bottom\",\n    };\n}\n\nregistry.category(\"fields\").add(\"section_one2many\", {\n    ...x2ManyField,\n    component: SectionOneToManyField,\n    additionalClasses: [...x2ManyField.additionalClasses || [], \"o_field_one2many\"],\n});\n", "import { useState } from \"@odoo/owl\";\nimport { FormController } from \"@web/views/form/form_controller\";\n\nexport class FormControllerWithHTMLExpander extends FormController {\n    static template = \"resource.FormViewWithHtmlExpander\";\n\n    setup() {\n        super.setup();\n        this.htmlExpanderState = useState({ reload: true });\n        const oldOnNotebookPageChange = this.onNotebookPageChange;\n        this.onNotebookPageChange = (notebookId, page) => {\n            oldOnNotebookPageChange(notebookId, page);\n            if (page && !this.htmlExpanderState.reload) {\n                this.htmlExpanderState.reload = true;\n            }\n        };\n    }\n\n    get modelParams() {\n        const modelParams = super.modelParams;\n        const onRootLoaded = modelParams.hooks.onRootLoaded;\n        modelParams.hooks.onRootLoaded = async () => {\n            if (onRootLoaded) {\n                onRootLoaded();\n            }\n            this.htmlExpanderState.reload = true;\n        };\n        return modelParams;\n    }\n\n    notifyHTMLFieldExpanded() {\n        this.htmlExpanderState.reload = false;\n    }\n\n    async onRecordSaved(record, changes) {\n        super.onRecordSaved(record, changes);\n        this.htmlExpanderState.reload = true;\n    }\n}\n", "import { useService } from \"@web/core/utils/hooks\";\nimport { FormRenderer } from \"@web/views/form/form_renderer\";\nimport { useRef, useEffect } from \"@odoo/owl\";\n\nexport class FormRendererWithHtmlExpander extends FormRenderer {\n    static props = {\n        ...FormRenderer.props,\n        reloadHtmlFieldHeight: { type: Boolean, optional: true },\n        notifyHtmlExpander: { type: Function, optional: true },\n    };\n    static defaultProps = {\n        ...FormRenderer.defaultProps,\n        reloadHtmlFieldHeight: true,\n        notifyHtmlExpander: () => {},\n    };\n\n    setup() {\n        super.setup();\n        if (!this.uiService) {\n            // Should be defined in FormRenderer\n            this.uiService = useService(\"ui\");\n        }\n        const ref = useRef(\"compiled_view_root\");\n        useEffect(\n            (el, size) => {\n                if (el && this._canExpandHTMLField(size)) {\n                    const descriptionField = el.querySelector(this.htmlFieldQuerySelector);\n                    if (descriptionField) {\n                        const containerEL = descriptionField.closest(\n                            this.getHTMLFieldContainerQuerySelector\n                        );\n                        const editor = descriptionField.querySelector(\".note-editable\");\n                        const elementToResize = editor || descriptionField;\n                        const { top, bottom } = elementToResize.getBoundingClientRect();\n                        const { bottom: containerBottom } = containerEL.getBoundingClientRect();\n                        const { paddingTop, paddingBottom } = window.getComputedStyle(containerEL);\n                        const nonEditableHeight =\n                            containerBottom -\n                            bottom +\n                            parseInt(paddingTop) +\n                            parseInt(paddingBottom);\n                        const minHeight =\n                            document.documentElement.clientHeight - top - nonEditableHeight;\n                        elementToResize.style.minHeight = `${minHeight}px`;\n                    }\n                }\n                this.props.notifyHtmlExpander();\n            },\n            () => [ref.el, this.uiService.size, this.props.reloadHtmlFieldHeight]\n        );\n    }\n\n    get htmlFieldQuerySelector() {\n        return \".o_field_html[name=description]\";\n    }\n\n    get getHTMLFieldContainerQuerySelector() {\n        return \".o_form_sheet\";\n    }\n\n    _canExpandHTMLField(size) {\n        return size === 6;\n    }\n}\n", "import { registry } from \"@web/core/registry\";\nimport { formView } from \"@web/views/form/form_view\";\nimport { FormRendererWithHtmlExpander } from \"./form_renderer_with_html_expander\";\nimport { FormControllerWithHTMLExpander } from \"./form_controller_with_html_expander\";\n\nexport const formViewWithHtmlExpander = {\n    ...formView,\n    Controller: FormControllerWithHTMLExpander,\n    Renderer: FormRendererWithHtmlExpander,\n};\n\nregistry.category(\"views\").add(\"form_description_expander\", formViewWithHtmlExpander);\n", "import {Component} from \"@odoo/owl\";\nimport {registry} from \"@web/core/registry\";\nimport { standardFieldProps } from \"@web/views/fields/standard_field_props\";\n\nexport class AccountBatchSendingSummary extends Component {\n    static template = \"account.BatchSendingSummary\";\n    static props = {\n        ...standardFieldProps,\n    };\n\n    setup() {\n        super.setup();\n        this.data = this.props.record.data[this.props.name];\n    }\n}\n\nexport const accountBatchSendingSummary = {\n    component: AccountBatchSendingSummary,\n}\n\nregistry.category(\"fields\").add(\"account_batch_sending_summary\", accountBatchSendingSummary);\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { DocumentFileUploader } from \"../document_file_uploader/document_file_uploader\";\n\nexport class AccountFileUploader extends DocumentFileUploader {\n    static template = \"account.AccountFileUploader\";\n    static props = {\n        ...DocumentFileUploader.props,\n        btnClass: { type: String, optional: true },\n        linkText: { type: String, optional: true },\n        togglerTemplate: { type: String, optional: true },\n    };\n\n    getExtraContext() {\n        const extraContext = super.getExtraContext();\n        const record_data = this.props.record ? this.props.record.data : false;\n        return record_data ? {\n            ...extraContext,\n            default_journal_id: record_data.id,\n            default_move_type: (\n                (record_data.type === 'sale' && 'out_invoice')\n                || (record_data.type === 'purchase' && 'in_invoice')\n                || 'entry'\n            ),\n        } : extraContext;\n\n    }\n\n    getResModel() {\n        return \"account.journal\";\n    }\n}\n\n//when file uploader is used on account.journal (with a record)\nexport const accountFileUploader = {\n    component: AccountFileUploader,\n    extractProps: ({ attrs }) => ({\n        togglerTemplate: attrs.template || \"account.JournalUploadLink\",\n        btnClass: attrs.btnClass || \"\",\n        linkText: attrs.title || _t(\"Upload\"),\n    }),\n    fieldDependencies: [\n        { name: \"id\", type: \"integer\" },\n        { name: \"type\", type: \"selection\" },\n    ],\n};\n\nregistry.category(\"view_widgets\").add(\"account_file_uploader\", accountFileUploader);\n", "import { registry } from \"@web/core/registry\";\nimport {\n    SectionAndNoteListRenderer,\n    SectionAndNoteFieldOne2Many,\n    sectionAndNoteFieldOne2Many,\n} from \"../section_and_note_fields_backend/section_and_note_fields_backend\";\n\nexport class AccountMergeWizardLinesRenderer extends SectionAndNoteListRenderer {\n    setup() {\n        super.setup();\n        this.titleField = \"info\";\n    }\n\n    getCellClass(column, record) {\n        const classNames = super.getCellClass(column, record);\n        // Even though the `is_selected` field is invisible for section lines, we should\n        // keep its column (which would be hidden by the call to super.getCellClass)\n        // in order to align the section header name with the account names.\n        if (this.isSectionOrNote(record) && column.name === \"is_selected\") {\n            return classNames.replace(\" o_hidden\", \"\");\n        }\n        return classNames;\n    }\n\n    /** @override **/\n    getSectionColumns(columns) {\n        const sectionCols = columns.filter(\n            (col) =>\n                col.type === \"field\" && (col.name === this.titleField || col.name === \"is_selected\")\n        );\n        return sectionCols.map((col) => {\n            if (col.name === this.titleField) {\n                return { ...col, colspan: columns.length - sectionCols.length + 1 };\n            } else {\n                return { ...col };\n            }\n        });\n    }\n\n    /** @override */\n    isSortable(column) {\n        // Don't allow sorting columns, as that doesn't make sense in the wizard view.\n        return false;\n    }\n}\n\nexport class AccountMergeWizardLinesOne2Many extends SectionAndNoteFieldOne2Many {\n    static components = {\n        ...SectionAndNoteFieldOne2Many.components,\n        ListRenderer: AccountMergeWizardLinesRenderer,\n    };\n}\n\nexport const accountMergeWizardLinesOne2Many = {\n    ...sectionAndNoteFieldOne2Many,\n    component: AccountMergeWizardLinesOne2Many,\n};\n\nregistry\n    .category(\"fields\")\n    .add(\"account_merge_wizard_lines_one2many\", accountMergeWizardLinesOne2Many);\n", "import { registry } from \"@web/core/registry\";\nimport { createElement, append } from \"@web/core/utils/xml\";\nimport { Notebook } from \"@web/core/notebook/notebook\";\nimport { formView } from \"@web/views/form/form_view\";\nimport { FormCompiler } from \"@web/views/form/form_compiler\";\nimport { FormRenderer } from \"@web/views/form/form_renderer\";\nimport { FormController } from '@web/views/form/form_controller';\nimport { useService } from \"@web/core/utils/hooks\";\nimport { deleteConfirmationMessage } from \"@web/core/confirmation_dialog/confirmation_dialog\";\nimport {_t} from \"@web/core/l10n/translation\";\n\n\nexport class AccountMoveFormController extends FormController {\n    setup() {\n        super.setup();\n        this.account_move_service = useService(\"account_move\");\n    }\n\n    get cogMenuProps() {\n        return {\n            ...super.cogMenuProps,\n            printDropdownTitle: _t(\"Print\"),\n            loadExtraPrintItems: this.loadExtraPrintItems.bind(this),\n        };\n    }\n\n    async loadExtraPrintItems() {\n        const items = await this.orm.call(\"account.move\", \"get_extra_print_items\", [this.model.root.resId]);\n        return items.filter((item) => item.key !== \"download_all\");\n    }\n\n\n    async deleteRecord() {\n        const deleteConfirmationDialogProps = this.deleteConfirmationDialogProps;\n        deleteConfirmationDialogProps.body = await this.account_move_service.getDeletionDialogBody(deleteConfirmationMessage, this.model.root.resId);\n        this.deleteRecordsWithConfirmation(deleteConfirmationDialogProps, [this.model.root]);\n    }\n}\n\nexport class AccountMoveFormNotebook extends Notebook {\n    static template = \"account.AccountMoveFormNotebook\";\n    static props = {\n        ...Notebook.props,\n        onBeforeTabSwitch: { type: Function, optional: true },\n    };\n\n    async changeTabTo(page_id) {\n        if (this.props.onBeforeTabSwitch) {\n            await this.props.onBeforeTabSwitch(page_id);\n        }\n        this.state.currentPage = page_id;\n    }\n}\n\nexport class AccountMoveFormRenderer extends FormRenderer {\n    static components = {\n        ...FormRenderer.components,\n        AccountMoveFormNotebook: AccountMoveFormNotebook,\n    };\n\n    async saveBeforeTabChange() {\n        if (this.props.record.isInEdition && await this.props.record.isDirty()) {\n            const contentEl = document.querySelector('.o_content');\n            const scrollPos = contentEl.scrollTop;\n            await this.props.record.save();\n            if (scrollPos) {\n                contentEl.scrollTop = scrollPos;\n            }\n        }\n    }\n}\n\nexport class AccountMoveFormCompiler extends FormCompiler {\n    compileNotebook(el, params) {\n        const originalNoteBook = super.compileNotebook(...arguments);\n        const noteBook = createElement(\"AccountMoveFormNotebook\");\n        for (const attr of originalNoteBook.attributes) {\n            noteBook.setAttribute(attr.name, attr.value);\n        }\n        noteBook.setAttribute(\"onBeforeTabSwitch\", \"() => __comp__.saveBeforeTabChange()\");\n        const slots = originalNoteBook.childNodes;\n        append(noteBook, [...slots]);\n        return noteBook;\n    }\n}\n\nexport const AccountMoveFormView = {\n    ...formView,\n    Renderer: AccountMoveFormRenderer,\n    Compiler: AccountMoveFormCompiler,\n    Controller: AccountMoveFormController,\n};\n\nregistry.category(\"views\").add(\"account_move_form\", AccountMoveFormView);\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { usePopover } from \"@web/core/popover/popover_hook\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { localization } from \"@web/core/l10n/localization\";\nimport { formatDate, deserializeDate } from \"@web/core/l10n/dates\";\n\nimport { formatMonetary } from \"@web/views/fields/formatters\";\nimport { standardFieldProps } from \"@web/views/fields/standard_field_props\";\nimport { Component } from \"@odoo/owl\";\n\nclass AccountPaymentPopOver extends Component {\n    static props = { \"*\": { optional: true } };\n    static template = \"account.AccountPaymentPopOver\";\n}\n\nexport class AccountPaymentField extends Component {\n    static props = { ...standardFieldProps };\n    static template = \"account.AccountPaymentField\";\n\n    setup() {\n        const position = localization.direction === \"rtl\" ? \"bottom\" : \"left\";\n        this.popover = usePopover(AccountPaymentPopOver, { position });\n        this.orm = useService(\"orm\");\n        this.action = useService(\"action\");\n    }\n\n    getInfo() {\n        const info = this.props.record.data[this.props.name] || {\n            content: [],\n            outstanding: false,\n            title: \"\",\n            move_id: this.props.record.resId,\n        };\n        for (const [key, value] of Object.entries(info.content)) {\n            value.index = key;\n            value.amount_formatted = formatMonetary(value.amount, {\n                currencyId: value.currency_id,\n            });\n            if (value.date) {\n                // value.date is a string, parse to date and format to the users date format\n                value.formattedDate = formatDate(deserializeDate(value.date))\n            }\n        }\n        return {\n            lines: info.content,\n            outstanding: info.outstanding,\n            title: info.title,\n            moveId: info.move_id,\n        };\n    }\n\n    onInfoClick(ev, line) {\n        this.popover.open(ev.currentTarget, {\n            title: _t(\"Journal Entry Info\"),\n            ...line,\n            _onRemoveMoveReconcile: this.removeMoveReconcile.bind(this),\n            _onOpenMove: this.openMove.bind(this),\n        });\n    }\n\n    async assignOutstandingCredit(moveId, id) {\n        await this.orm.call(this.props.record.resModel, 'js_assign_outstanding_line', [moveId, id], {});\n        await this.props.record.model.root.load();\n    }\n\n    async removeMoveReconcile(moveId, partialId) {\n        this.popover.close();\n        await this.orm.call(this.props.record.resModel, 'js_remove_outstanding_partial', [moveId, partialId], {});\n        await this.props.record.model.root.load();\n    }\n\n    async openMove(moveId) {\n        const action = await this.orm.call(this.props.record.resModel, 'action_open_business_doc', [moveId], {});\n        this.action.doAction(action);\n    }\n}\n\nexport const accountPaymentField = {\n    component: AccountPaymentField,\n    supportedTypes: [\"binary\"],\n};\n\nregistry.category(\"fields\").add(\"payment\", accountPaymentField);\n", "import { Component } from \"@odoo/owl\";\nimport { registry } from \"@web/core/registry\";\nimport { standardFieldProps } from \"@web/views/fields/standard_field_props\";\n\nclass AccountPaymentRegisterHtmlField extends Component {\n    static props = standardFieldProps;\n    static template = \"account.AccountPaymentRegisterHtmlField\";\n\n    get value() {\n        return this.props.record.data[this.props.name];\n    }\n\n    switchInstallmentsAmount(ev) {\n        if (ev.srcElement.classList.contains(\"installments_switch_button\")) {\n            const root = this.env.model.root;\n            root.update({ amount: root.data.installments_switch_amount });\n        }\n    }\n}\n\nconst accountPaymentRegisterHtmlField = { component: AccountPaymentRegisterHtmlField };\n\nregistry.category(\"fields\").add(\"account_payment_register_html\", accountPaymentRegisterHtmlField);\n", "import { registry } from \"@web/core/registry\";\n\nimport { X2ManyField, x2ManyField } from \"@web/views/fields/x2many/x2many_field\";\nimport { useAddInlineRecord } from \"@web/views/fields/relational_utils\";\n\nexport class PaymentTermLineIdsOne2Many extends X2ManyField {\n    setup() {\n        super.setup();\n        // Overloads the addInLine method to mark all new records as 'dirty' by calling update with an empty object.\n        // This prevents the records from being abandoned if the user clicks globally or on an existing record.\n        this.addInLine = useAddInlineRecord({\n            addNew: async (...args) => {\n                const newRecord = await this.list.addNewRecord(...args);\n                newRecord.update({});\n            }\n        });\n    }\n}\n\nexport const PaymentTermLineIds = {\n    ...x2ManyField,\n    component: PaymentTermLineIdsOne2Many,\n}\n\nregistry.category(\"fields\").add(\"payment_term_line_ids\", PaymentTermLineIds);\n", "import { Component } from \"@odoo/owl\";\nimport { registry } from \"@web/core/registry\";\nimport { useDateTimePicker } from \"@web/core/datetime/datetime_picker_hook\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { today } from \"@web/core/l10n/dates\";\nimport { standardWidgetProps } from \"@web/views/widgets/standard_widget_props\";\n\n\nexport class AccountPickCurrencyDate extends Component {\n    static template = \"account.AccountPickCurrencyDate\";\n    static props = {\n        ...standardWidgetProps,\n        record: { type: Object, optional: true },\n    };\n\n    setup() {\n        this.orm = useService(\"orm\");\n        this.dateTimePicker = useDateTimePicker({\n            target: 'datetime-picker-target',\n            onApply: async (date) => {\n                const record = this.props.record\n                const rate = await this.orm.call(\n                    'account.move',\n                    'get_currency_rate',\n                    [record.resId, record.data.company_id.id, record.data.currency_id.id, date.toISODate()],\n                );\n                this.props.record.update({ invoice_currency_rate: rate });\n                await this.props.record.save();\n            },\n            get pickerProps() {\n                return {\n                    type: 'date',\n                    value: today(),\n                };\n            },\n        });\n    }\n}\n\nexport const accountPickCurrencyDate = {\n    component: AccountPickCurrencyDate,\n}\n\nregistry.category(\"view_widgets\").add(\"account_pick_currency_date\",  accountPickCurrencyDate);\n", "import { registry } from \"@web/core/registry\";\nimport { Component } from \"@odoo/owl\";\nimport { standardFieldProps } from \"@web/views/fields/standard_field_props\";\n\nclass ChangeLine extends Component {\n    static template = \"account.ResequenceChangeLine\";\n    static props = [\"changeLine\", \"ordering\"];\n}\n\nclass ShowResequenceRenderer extends Component {\n    static template = \"account.ResequenceRenderer\";\n    static components = { ChangeLine };\n    static props = { ...standardFieldProps };\n    getValue() {\n        const value = this.props.record.data[this.props.name];\n        return value ? JSON.parse(value) : { changeLines: [], ordering: \"date\" };\n    }\n}\n\nregistry.category(\"fields\").add(\"account_resequence_widget\", {\n    component: ShowResequenceRenderer,\n});\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { statusBarField, StatusBarField } from \"@web/views/fields/statusbar/statusbar_field\";\n\nexport class AccountMoveStatusBarSecuredField extends StatusBarField {\n    static template = \"account.MoveStatusBarSecuredField\";\n\n    get isSecured() {\n        return this.props.record.data['secured'];\n    }\n\n    get currentItem() {\n        return this.getAllItems().find((item) => item.isSelected);\n    }\n}\n\nexport const accountMoveStatusBarSecuredField = {\n    ...statusBarField,\n    component: AccountMoveStatusBarSecuredField,\n    displayName: _t(\"Status with secured indicator for Journal Entries\"),\n    supportedTypes: [\"selection\"],\n    additionalClasses: [\"o_field_statusbar\"],\n};\n\nregistry.category(\"fields\").add(\"account_move_statusbar_secured\", accountMoveStatusBarSecuredField);\n", "import { FloatField, floatField } from \"@web/views/fields/float/float_field\";\nimport { roundPrecision } from \"@web/core/utils/numbers\";\nimport {registry} from \"@web/core/registry\";\n\nexport class AccountTaxRepartitionLineFactorPercent extends FloatField {\n    static defaultProps = {\n        ...FloatField.defaultProps,\n        digits: [16, 12],\n    };\n\n    /*\n     * @override\n     * We don't want to display all amounts with 12 digits behind so we remove the trailing 0\n     * as much as possible.\n     */\n    get formattedValue() {\n        const value = super.formattedValue;\n        const trailingNumbersMatch = value.match(/(\\d+)$/);\n        if (!trailingNumbersMatch) {\n            return value;\n        }\n        const trailingZeroMatch = trailingNumbersMatch[1].match(/(0+)$/);\n        if (!trailingZeroMatch) {\n            return value;\n        }\n        const nbTrailingZeroToRemove = Math.min(trailingZeroMatch[1].length, trailingNumbersMatch[1].length - 2);\n        return value.substring(0, value.length - nbTrailingZeroToRemove);\n    }\n\n    /*\n     * @override\n     * Prevent the users of showing a rounding at 12 digits on the screen but\n     * getting an unrounded value after typing \"= 2/3\" on the field when saving.\n     */\n    parse(value) {\n        const parsedValue = super.parse(value);\n        try {\n            Number(parsedValue);\n        } catch {\n            return parsedValue;\n        }\n        const precisionRounding = Number(`1e-${this.props.digits[1]}`);\n        return roundPrecision(parsedValue, precisionRounding);\n    }\n}\n\n\nexport const accountTaxRepartitionLineFactorPercent = {\n    ...floatField,\n    component: AccountTaxRepartitionLineFactorPercent,\n};\n\n\nregistry.category(\"fields\").add(\"account_tax_repartition_line_factor_percent\", accountTaxRepartitionLineFactorPercent);\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { SelectionField, selectionField } from \"@web/views/fields/selection/selection_field\";\n\nexport class AccountTypeSelection extends SelectionField {\n    static template = \"account.AccountTypeSelection\";\n    setup() {\n        super.setup();\n        const getChoicesForGroup = (group) => {\n            return this.choices.filter(x => x.value.startsWith(group));\n        }\n        this.sections = [\n            {\n                label: _t('Balance Sheet'),\n                name: \"balance_sheet\"\n            },\n            {\n                label: _t('Profit & Loss'),\n                name: \"profit_and_loss\"\n            },\n        ]\n        this.groups = [\n            {\n                label: _t('Assets'),\n                choices: getChoicesForGroup('asset'),\n                section: \"balance_sheet\",\n            },\n            {\n                label: _t('Liabilities'),\n                choices: getChoicesForGroup('liability'),\n                section: \"balance_sheet\",\n            },\n            {\n                label: _t('Equity'),\n                choices: getChoicesForGroup('equity'),\n                section: \"balance_sheet\",\n            },\n            {\n                label: _t('Income'),\n                choices: getChoicesForGroup('income'),\n                section: \"profit_and_loss\",\n            },\n            {\n                label: _t('Expense'),\n                choices: getChoicesForGroup('expense'),\n                section: \"profit_and_loss\",\n            },\n            {\n                label: _t('Other'),\n                choices: getChoicesForGroup('off_balance'),\n                section: \"profit_and_loss\",\n            },\n        ];\n    }\n}\n\nexport const accountTypeSelection = {\n    ...selectionField,\n    component: AccountTypeSelection,\n};\n\nregistry.category(\"fields\").add(\"account_type_selection\", accountTypeSelection);\n", "import { registry } from \"@web/core/registry\";\nimport { Component } from \"@odoo/owl\";\nimport { standardFieldProps } from \"@web/views/fields/standard_field_props\";\nimport { useService } from \"@web/core/utils/hooks\";\n\nconst WARNING_TYPE_ORDER = [\"danger\", \"warning\", \"info\"];\n\nexport class ActionableErrors extends Component {\n    static props = { errorData: {type: Object} };\n    static template = \"account.ActionableErrors\";\n\n    setup() {\n        super.setup();\n        this.actionService = useService(\"action\");\n        this.orm = useService(\"orm\");\n    }\n\n    get errorData() {\n        return this.props.errorData;\n    }\n\n    async handleOnClick(errorData){\n        if (errorData.action?.view_mode) {\n            // view_mode is not handled JS side\n            errorData.action['views'] = errorData.action.view_mode.split(',').map(mode => [false, mode]);\n            delete errorData.action['view_mode'];\n        }\n        if (errorData.action_call) {\n            const [model, method, args] = errorData.action_call;\n            await this.orm.call(model, method, [args]);\n            this.env.model.action.doAction(\"soft_reload\");\n        } else {\n            this.env.model.action.doAction(errorData.action);\n        }\n    }\n\n    get sortedActionableErrors() {\n        return this.errorData && Object.fromEntries(\n            Object.entries(this.errorData).sort(\n                (a, b) =>\n                    WARNING_TYPE_ORDER.indexOf(a[1][\"level\"] || \"warning\") -\n                    WARNING_TYPE_ORDER.indexOf(b[1][\"level\"] || \"warning\"),\n            ),\n        );\n    }\n}\n\nexport class ActionableErrorsField extends ActionableErrors {\n    static props = { ...standardFieldProps };\n\n    get errorData() {\n        return this.props.record.data[this.props.name];\n    }\n}\n\nexport const actionableErrorsField = {component: ActionableErrorsField};\nregistry.category(\"fields\").add(\"actionable_errors\", actionableErrorsField);\n", "import { registry } from \"@web/core/registry\";\nimport { X2ManyField, x2ManyField } from \"@web/views/fields/x2many/x2many_field\";\n\n\nexport class AutoSaveResPartnerField extends X2ManyField {\n     async onAdd({ context, editable } = {}) {\n        await this.props.record.model.root.save();\n        await super.onAdd({ context, editable });\n     }\n}\n\nexport const autoSaveResPartnerField = {\n    ...x2ManyField,\n    component: AutoSaveResPartnerField,\n};\n\nregistry.category(\"fields\").add(\"auto_save_res_partner\", autoSaveResPartnerField);\n", "import { registry } from \"@web/core/registry\";\nimport { useRecordObserver } from \"@web/model/relational_model/utils\";\nimport {\n    Many2ManyTaxTagsField,\n    many2ManyTaxTagsField\n} from \"@account/components/many2x_tax_tags/many2x_tax_tags\";\n\nexport class AutosaveMany2ManyTaxTagsField extends Many2ManyTaxTagsField {\n    setup() {\n        super.setup();\n\n        this.lastBalance = this.props.record.data.balance;\n        this.lastAccount = this.props.record.data.account_id;\n        this.lastPartner = this.props.record.data.partner_id;\n\n        const super_update = this.update;\n        this.update = (recordlist) => {\n            super_update(recordlist);\n            this._saveOnUpdate();\n        };\n        useRecordObserver(this.onRecordChange.bind(this));\n    }\n\n    async deleteTag(id) {\n        await super.deleteTag(id);\n        await this._saveOnUpdate();\n    }\n\n    onRecordChange(record) {\n        const line = record.data;\n        if (line.tax_ids.records.length > 0) {\n            if (line.balance !== this.lastBalance\n                || line.account_id.id !== this.lastAccount.id\n                || line.partner_id.id !== this.lastPartner.id) {\n                this.lastBalance = line.balance;\n                this.lastAccount = line.account_id;\n                this.lastPartner = line.partner_id;\n                return record.model.root.save();\n            }\n        }\n    }\n\n    async _saveOnUpdate() {\n        await this.props.record.model.root.save();\n    }\n}\n\nexport const autosaveMany2ManyTaxTagsField = {\n    ...many2ManyTaxTagsField,\n    component: AutosaveMany2ManyTaxTagsField,\n};\n\nregistry.category(\"fields\").add(\"autosave_many2many_tax_tags\", autosaveMany2ManyTaxTagsField);\n", "import { registry } from \"@web/core/registry\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { DocumentFileUploader } from \"../document_file_uploader/document_file_uploader\";\n\nimport { Component, onWillStart } from \"@odoo/owl\";\n\nexport class BillGuide extends Component {\n    static template = \"account.BillGuide\";\n    static components = {\n        DocumentFileUploader,\n    };\n    static props = [\"*\"];  // could contain view_widget props\n\n    setup() {\n        this.orm = useService(\"orm\");\n        this.action = useService(\"action\");\n        this.context = null;\n        this.alias = null;\n        onWillStart(this.onWillStart);\n    }\n\n    async onWillStart() {\n        const rec = this.props.record;\n        const ctx = this.env.searchModel.context;\n        if (rec) {\n            // prepare context from journal record\n            this.context = {\n                default_journal_id: rec.resId,\n                default_move_type: (rec.data.type === 'sale' && 'out_invoice') || (rec.data.type === 'purchase' && 'in_invoice') || 'entry',\n                active_model: rec.resModel,\n                active_ids: [rec.resId],\n            }\n            this.alias = rec.data.alias_domain_id && rec.data.alias_id[1] || false;\n        } else if (!ctx?.default_journal_id && ctx?.active_id) {\n            this.context = {\n                default_journal_id: ctx.active_id,\n            }\n        }\n    }\n\n    handleButtonClick(action, model=\"account.journal\") {\n        this.action.doActionButton({\n            resModel: model,\n            name: action,\n            context: this.context || this.env.searchModel.context,\n            type: 'object',\n        });\n    }\n}\n\n\nexport const billGuide = {\n    component: BillGuide,\n};\n\nregistry.category(\"view_widgets\").add(\"bill_upload_guide\", billGuide);\n", "import { registry } from \"@web/core/registry\";\nimport { CharField, charField } from \"@web/views/fields/char/char_field\";\n\n// Ensure that in Hoot tests, this module is loaded after `@mail/js/onchange_on_keydown`\n// (needed because that module patches `charField`).\nimport \"@mail/js/onchange_on_keydown\";\n\nexport class CharWithPlaceholderField extends CharField {\n    static template = \"account.CharWithPlaceholderField\";\n\n    /** Override **/\n    get formattedValue() {\n        return super.formattedValue || this.props.placeholder;\n    }\n}\n\nexport const charWithPlaceholderField = {\n    ...charField,\n    component: CharWithPlaceholderField,\n};\n\nregistry.category(\"fields\").add(\"char_with_placeholder_field\", charWithPlaceholderField);\n", "import { registry } from \"@web/core/registry\";\nimport {\n    charWithPlaceholderField,\n    CharWithPlaceholderField\n} from \"../char_with_placeholder_field/char_with_placeholder_field\";\n\nexport class CharWithPlaceholderFieldToCheck extends CharWithPlaceholderField {\n    static template = \"account.CharWithPlaceholderField\";\n}\n\nexport const charWithPlaceholderFieldToCheck = {\n    ...charWithPlaceholderField,\n    component: CharWithPlaceholderFieldToCheck,\n};\n\nregistry.category(\"fields\").add(\"char_with_placeholder_field_to_check\", charWithPlaceholderFieldToCheck);\n", "/** @odoo-module **/\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { ConfirmationDialog } from \"@web/core/confirmation_dialog/confirmation_dialog\";\nimport { registry } from \"@web/core/registry\";\nimport { FormController } from \"@web/views/form/form_controller\";\nimport { formView } from \"@web/views/form/form_view\";\n\nexport class CurrencyFormController extends FormController {\n\n    async onWillSaveRecord(record) {\n        if (record.data.display_rounding_warning &&\n            record._values.rounding !== undefined &&\n            record.data.rounding < record._values.rounding\n        ) {\n            return new Promise((resolve) => {\n                this.dialogService.add(ConfirmationDialog, {\n                    title: _t(\"Confirmation Warning\"),\n                    body: _t(\n                        \"You're about to permanently change the decimals for all prices in your database.\\n\" +\n                        \"This change cannot be undone without technical support.\"\n                    ),\n                    confirmLabel: _t(\"Confirm\"),\n                    cancelLabel: _t(\"Cancel\"),\n                    confirm: () => resolve(true),\n                    cancel: () => {\n                        record.discard();\n                        resolve(false);\n                    },\n                });\n            });\n        }\n\n        return true;\n    }\n}\n\nexport const currencyFormView = {\n    ...formView,\n    Controller: CurrencyFormController,\n};\n\nregistry.category(\"views\").add(\"currency_form\", currencyFormView);\n", "/** @odoo-module **/\n\nimport { registry } from \"@web/core/registry\";\nimport { standardFieldProps } from \"@web/views/fields/standard_field_props\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { Component } from \"@odoo/owl\";\n\nclass OpenDecimalPrecisionButton extends Component {\n    static template = \"account.OpenDecimalPrecisionButton\";\n    static props = { ...standardFieldProps };\n\n    setup() {\n        this.action = useService(\"action\");\n    }\n\n    async discardAndOpen() {\n        await this.props.record.discard();\n        this.action.doAction(\"base.action_decimal_precision_form\");\n    }\n}\n\nregistry.category(\"fields\").add(\"open_decimal_precision_button\", {\n    component: OpenDecimalPrecisionButton,\n});\n", "import { useService } from \"@web/core/utils/hooks\";\nimport { FileUploader } from \"@web/views/fields/file_handler\";\nimport { standardWidgetProps } from \"@web/views/widgets/standard_widget_props\";\n\nimport { Component, markup } from \"@odoo/owl\";\n\nexport class DocumentFileUploader extends Component {\n    static template = \"account.DocumentFileUploader\";\n    static components = {\n        FileUploader,\n    };\n    static props = {\n        ...standardWidgetProps,\n        record: { type: Object, optional: true },\n        slots: { type: Object, optional: true },\n        resModel: { type: String, optional: true },\n    };\n\n    setup() {\n        this.orm = useService(\"orm\");\n        this.action = useService(\"action\");\n        this.notification = useService(\"notification\");\n        this.attachmentIdsToProcess = [];\n        this.extraContext = this.getExtraContext();\n    }\n\n    // To pass extra context while creating record\n    getExtraContext() {\n        return {};\n    }\n\n    async onFileUploaded(file) {\n        const att_data = {\n            name: file.name,\n            mimetype: file.type,\n            datas: file.data,\n        };\n        // clean the context to ensure the `create` call doesn't fail from unknown `default_*` context\n        const cleanContext = Object.fromEntries(Object.entries(this.env.searchModel.context).filter(([key]) => !key.startsWith('default_')));\n        const [att_id] = await this.orm.create(\"ir.attachment\", [att_data], {context: cleanContext});\n        this.attachmentIdsToProcess.push(att_id);\n    }\n\n    // To define specific resModal from another model\n    getResModel() {\n        return this.props.resModel;\n    }\n\n    async onUploadComplete() {\n        const resModal = this.getResModel();\n        let action;\n        try {\n            action = await this.orm.call(\n                resModal,\n                \"create_document_from_attachment\",\n                [\"\", this.attachmentIdsToProcess],\n                { context: { ...this.extraContext, ...this.env.searchModel.context } }\n            );\n        } finally {\n            // ensures attachments are cleared on success as well as on error\n            this.attachmentIdsToProcess = [];\n        }\n        if (action.context && action.context.notifications) {\n            for (const [file, msg] of Object.entries(action.context.notifications)) {\n                this.notification.add(msg, {\n                    title: file,\n                    type: \"info\",\n                    sticky: true,\n                });\n            }\n            delete action.context.notifications;\n        }\n        if (action.help?.length) {\n            action.help = markup(action.help);\n        }\n        this.action.doAction(action);\n    }\n}\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { useService } from \"@web/core/utils/hooks\";\n\nimport { SelectionField, selectionField } from \"@web/views/fields/selection/selection_field\";\n\nimport { Component } from \"@odoo/owl\";\n\nexport class DocumentStatePopover extends Component {\n    static template = \"account.DocumentStatePopover\";\n    static props = {\n        close: Function,\n        onClose: Function,\n        copyText: Function,\n        message: String,\n    };\n}\n\nexport class DocumentState extends SelectionField {\n    static template = \"account.DocumentState\";\n\n    setup() {\n        super.setup();\n        this.popover = useService(\"popover\");\n        this.notification = useService(\"notification\");\n    }\n\n    get message() {\n        return this.props.record.data.message;\n    }\n\n    copyText() {\n        navigator.clipboard.writeText(this.message);\n        this.notification.add(_t(\"Text copied\"), { type: \"success\" });\n        this.popoverCloseFn();\n        this.popoverCloseFn = null;\n    }\n\n    showMessagePopover(ev) {\n        const close = () => {\n            this.popoverCloseFn();\n            this.popoverCloseFn = null;\n        };\n\n        if (this.popoverCloseFn) {\n            close();\n            return;\n        }\n\n        this.popoverCloseFn = this.popover.add(\n            ev.currentTarget,\n            DocumentStatePopover,\n            {\n                message: this.message,\n                copyText: this.copyText.bind(this),\n                onClose: close,\n            },\n            {\n                closeOnClickAway: true,\n                position: \"top\",\n            },\n        );\n    }\n}\n\nregistry.category(\"fields\").add(\"account_document_state\", {\n    ...selectionField,\n    component: DocumentState,\n});\n", "/** @odoo-module **/\n\nimport { registry } from \"@web/core/registry\";\nimport { SelectionField, selectionField } from \"@web/views/fields/selection/selection_field\";\n\nexport class DynamicSelectionField extends SelectionField {\n\n    static props = {\n        ...SelectionField.props,\n        available_field: { type: String },\n    }\n\n    get availableOptions() {\n        return this.props.record.data[this.props.available_field]?.split(\",\") || [];\n    }\n\n    /**\n     * Filter the options with the accepted available options.\n     * @override\n     */\n    get options() {\n        const availableOptions = this.availableOptions;\n        return super.options.filter(x => availableOptions.includes(x[0]));\n    }\n\n    /**\n     * In dynamic selection field, sometimes we can have no options available.\n     * This override handles that case by adding optional chaining when accessing the found options.\n     * @override\n     */\n    get string() {\n        if (this.type === \"selection\") {\n            return this.props.record.data[this.props.name] !== false\n                ? this.options.find((o) => o[0] === this.props.record.data[this.props.name])?.[1]\n                : \"\";\n        }\n        return super.string;\n    }\n\n}\n\n/*\nEXAMPLE USAGE:\n\nIn python:\nthe_available_field = fields.Char()  # string of comma separated available selection field keys\nthe_selection_field = fields.Selection([ ... ])\n\nIn the views:\n<field name=\"the_available_field\" column_invisible=\"1\"/>\n<field name=\"the_selection_field\"\n       widget=\"dynamic_selection\"\n       options=\"{'available_field': 'the_available_field'}\"/>\n */\n\nregistry.category(\"fields\").add(\"dynamic_selection\", {\n    ...selectionField,\n    component: DynamicSelectionField,\n    extractProps: (fieldInfo, dynamicInfo) => ({\n        ...selectionField.extractProps(fieldInfo, dynamicInfo),\n        available_field: fieldInfo.options.available_field,\n    }),\n})\n", "import { Component } from \"@odoo/owl\";\nimport { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\nimport { registry } from \"@web/core/registry\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { ACTIONS_GROUP_NUMBER } from \"@web/search/action_menus/action_menus\";\n\nconst cogMenuRegistry = registry.category(\"cogMenu\");\n\nexport class FetchEInvoices extends Component {\n    static template = \"account.FetchEInvoices\";\n    static props = {};\n    static components = { DropdownItem };\n\n    setup() {\n        super.setup();\n        this.action = useService(\"action\");\n    }\n\n    get buttonAction() {\n        return this.env.searchModel.globalContext.show_fetch_in_einvoices_button\n            ? \"button_fetch_in_einvoices\"\n            : \"button_refresh_out_einvoices_status\";\n    }\n\n    get buttonLabel() {\n        return this.env.searchModel.globalContext.show_fetch_in_einvoices_button\n            ? _t(\"Fetch e-Invoices\")\n            : _t(\"Refresh e-Invoices Status\");\n    }\n\n    fetchEInvoices() {\n        const journalId = this.env.searchModel.globalContext.default_journal_id;\n        if (!journalId) {\n            return;\n        }\n\n        this.action.doActionButton({\n            type: \"object\",\n            resId: journalId,\n            name: this.buttonAction,\n            resModel: \"account.journal\",\n            onClose: () => window.location.reload(),\n        });\n    }\n}\n\nexport const fetchEInvoicesActionMenu = {\n    Component: FetchEInvoices,\n    groupNumber: ACTIONS_GROUP_NUMBER,\n    isDisplayed: ({ config, searchModel }) =>\n        searchModel.resModel === \"account.move\" &&\n        (searchModel.globalContext.default_journal_id || false) &&\n        (searchModel.globalContext.show_fetch_in_einvoices_button ||\n            searchModel.globalContext.show_refresh_out_einvoices_status_button ||\n            false),\n};\n\ncogMenuRegistry.add(\"account-fetch-e-invoices\", fetchEInvoicesActionMenu, { sequence: 11 });\n", "import { registry } from \"@web/core/registry\";\nimport { Component } from \"@odoo/owl\";\nimport { standardFieldProps } from \"@web/views/fields/standard_field_props\";\n\nclass ListItem extends Component {\n    static template = \"account.GroupedItemTemplate\";\n    static props = [\"item_vals\", \"options\"];\n}\n\nclass ListGroup extends Component {\n    static template = \"account.GroupedItemsTemplate\";\n    static components = { ListItem };\n    static props = [\"group_vals\", \"options\"];\n}\n\nclass ShowGroupedList extends Component {\n    static template = \"account.GroupedListTemplate\";\n    static components = { ListGroup };\n    static props = {...standardFieldProps};\n    getValue() {\n        const value = this.props.record.data[this.props.name];\n        return value\n            ? JSON.parse(value)\n            : { groups_vals: [], options: { discarded_number: \"\", columns: [] } };\n    }\n}\n\nregistry.category(\"fields\").add(\"grouped_view_widget\", {\n    component: ShowGroupedList,\n});\n", "import { registry } from \"@web/core/registry\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { FileInput } from \"@web/core/file_input/file_input\";\nimport { Component, onWillUnmount } from \"@odoo/owl\";\nimport { standardFieldProps } from \"@web/views/fields/standard_field_props\";\n\nexport class MailAttachments extends Component {\n    static template = \"account.mail_attachments\";\n    static components = { FileInput };\n    static props = {...standardFieldProps};\n\n    setup() {\n        this.orm = useService(\"orm\");\n        this.notification = useService(\"notification\");\n        this.attachmentIdsToUnlink = new Set();\n\n        onWillUnmount(this.onWillUnmount);\n    }\n\n    get attachments() {\n        return this.props.record.data[this.props.name] || [];\n    }\n\n    get renderedAttachments() {\n        const attachments = JSON.parse(JSON.stringify(this.attachments));\n        const attachmentsNotSupported = this.props.record.data.attachments_not_supported || {};\n        for (const attachment of attachments) {\n            if (attachment.id && attachment.id in attachmentsNotSupported) {\n                attachment.tooltip = attachmentsNotSupported[attachment.id];\n            }\n        }\n        return attachments;\n    }\n\n    onFileRemove(deleteId) {\n        const newValue = [];\n\n        for (let item of this.attachments) {\n            if (item.id === deleteId) {\n                if (item.placeholder || item.protect_from_deletion) {\n                    const copyItem = Object.assign({ skip: true }, item);\n                    newValue.push(copyItem);\n                } else {\n                    this.attachmentIdsToUnlink.add(item.id);\n                }\n            } else {\n                newValue.push(item);\n            }\n        }\n\n        this.props.record.update({ [this.props.name]: newValue });\n    }\n\n    async onWillUnmount() {\n        // Unlink added attachments if the wizard is not saved.\n        if (!this.props.record.resId) {\n            this.attachments.forEach((item) => {\n                if (item.manual) {\n                    this.attachmentIdsToUnlink.add(item.id);\n                }\n            });\n        }\n\n        if (this.attachmentIdsToUnlink.size) {\n            await this.orm.unlink(\"ir.attachment\", Array.from(this.attachmentIdsToUnlink));\n        }\n    }\n}\n\nexport const mailAttachments = {\n    component: MailAttachments,\n};\n\nregistry.category(\"fields\").add(\"mail_attachments\", mailAttachments);\n", "import { registry } from \"@web/core/registry\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { FileUploader } from \"@web/views/fields/file_handler\";\nimport { Component } from \"@odoo/owl\";\nimport { standardFieldProps } from \"@web/views/fields/standard_field_props\";\nimport { useX2ManyCrud } from \"@web/views/fields/relational_utils\";\nimport { dataUrlToBlob } from \"@mail/core/common/attachment_uploader_hook\";\n\nexport class MailAttachments extends Component {\n    static template = \"mail.MailComposerAttachmentSelector\";\n    static components = { FileUploader };\n    static props = {...standardFieldProps};\n\n    setup() {\n        this.mailStore = useService(\"mail.store\");\n        this.attachmentUploadService = useService(\"mail.attachment_upload\");\n        this.operations = useX2ManyCrud(() => {\n            return this.props.record.data[\"attachment_ids\"];\n        }, true);\n    }\n\n    get attachments() {\n        return this.props.record.data[this.props.name] || [];\n    }\n\n    async onFileUploaded({ name, data, type }) {\n        const resIds = JSON.parse(this.props.record.data.res_ids);\n        const thread = await this.mailStore.Thread.insert({\n            model: this.props.record.data.model,\n            id: resIds[0],\n        });\n\n        const file = new File([dataUrlToBlob(data, type)], name, { type });\n        const attachment = await this.attachmentUploadService.upload(thread, thread.composer, file);\n\n        let fileDict = {\n            id: attachment.id,\n            name: attachment.name,\n            mimetype: attachment.mimetype,\n            placeholder: false,\n            manual: true,\n        };\n        this.props.record.update({ [this.props.name]: this.attachments.concat([fileDict]) });\n    }\n}\n\nexport const mailAttachments = {\n    component: MailAttachments,\n};\n\nregistry.category(\"fields\").add(\"mail_attachments_selector\", mailAttachments);\n", "import {\n    many2ManyTagsFieldColorEditable,\n    Many2ManyTagsFieldColorEditable,\n} from \"@web/views/fields/many2many_tags/many2many_tags_field\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { registry } from \"@web/core/registry\";\nimport { TagsList } from \"@web/core/tags_list/tags_list\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { onMounted } from \"@odoo/owl\";\n\nexport class FieldMany2ManyTagsBanksTagsList extends TagsList {\n    static template = \"FieldMany2ManyTagsBanksTagsList\";\n}\n\nexport class FieldMany2ManyTagsBanks extends Many2ManyTagsFieldColorEditable {\n    static template = \"account.FieldMany2ManyTagsBanks\";\n    static components = {\n        ...FieldMany2ManyTagsBanks.components,\n        TagsList: FieldMany2ManyTagsBanksTagsList,\n    };\n\n    setup() {\n        super.setup();\n        this.actionService = useService(\"action\");\n        onMounted(async () => {\n            // Needed when you create a partner (from a move for example), we want the partner to be saved to be able\n            // to have it as account holder\n            const isDirty = await this.props.record.model.root.isDirty();\n            if (isDirty) {\n                this.props.record.model.root.save();\n            }\n        });\n    }\n\n    getTagProps(record) {\n        return {\n            ...super.getTagProps(record),\n            allowOutPayment: record.data?.allow_out_payment,\n        };\n    }\n\n    openBanksListView() {\n        this.actionService.doAction({\n            type: \"ir.actions.act_window\",\n            name: _t(\"Banks\"),\n            res_model: this.relation,\n            views: [\n                [false, \"list\"],\n                [false, \"form\"],\n            ],\n            domain: this.getDomain(),\n            target: \"current\",\n        });\n    }\n}\n\nexport const fieldMany2ManyTagsBanks = {\n    ...many2ManyTagsFieldColorEditable,\n    component: FieldMany2ManyTagsBanks,\n    supportedOptions: [\n        ...(many2ManyTagsFieldColorEditable.supportedOptions || []),\n        {\n            label: _t(\"Allows out payments\"),\n            name: \"allow_out_payment_field\",\n            type: \"boolean\",\n        },\n    ],\n    additionalClasses: [\n        ...(many2ManyTagsFieldColorEditable.additionalClasses || []),\n        \"o_field_many2many_tags\",\n    ],\n    relatedFields: ({ options }) => {\n        return [\n            ...many2ManyTagsFieldColorEditable.relatedFields({ options }),\n            { name: options.allow_out_payment_field, type: \"boolean\", readonly: false },\n        ];\n    },\n};\n\nregistry.category(\"fields\").add(\"many2many_tags_banks\", fieldMany2ManyTagsBanks);\n", "import { registry } from \"@web/core/registry\";\nimport {\n    Many2ManyTagsField,\n    many2ManyTagsField,\n} from \"@web/views/fields/many2many_tags/many2many_tags_field\";\nimport { Many2XAutocomplete } from \"@web/views/fields/relational_utils\";\n\nexport class Many2ManyTagsJournalsMany2xAutocomplete extends Many2XAutocomplete {\n    static template = \"account.Many2ManyTagsJournalsMany2xAutocomplete\";\n    static props = {\n        ...Many2XAutocomplete.props,\n        group_company_id: { type: Number, optional: true },\n    };\n\n    get searchSpecification() {\n        return {\n            ...super.searchSpecification,\n            company_id: {\n                fields: {\n                    display_name: {},\n                },\n            },\n        };\n    }\n}\n\nexport class Many2ManyTagsJournals extends Many2ManyTagsField {\n    static template = \"account.Many2ManyTagsJournals\";\n    static components = {\n        ...Many2ManyTagsField.components,\n        Many2XAutocomplete: Many2ManyTagsJournalsMany2xAutocomplete,\n    };\n\n    getTagProps(record) {\n        const group_company_id = this.props.record.data[\"company_id\"];\n\n        const text = group_company_id\n            ? record.data.display_name\n            : `${record.data.company_id.display_name} - ${record.data.display_name}`;\n        return {\n            ...super.getTagProps(record),\n            text,\n        };\n    }\n}\n\nexport const fieldMany2ManyTagsJournals = {\n    ...many2ManyTagsField,\n    component: Many2ManyTagsJournals,\n    relatedFields: (fieldInfo) => [\n        ...many2ManyTagsField.relatedFields(fieldInfo),\n        { name: \"company_id\", type: \"many2one\", relation: \"res.company\" },\n    ],\n};\n\nregistry.category(\"fields\").add(\"many2many_tags_journals\", fieldMany2ManyTagsJournals);\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { Many2XAutocomplete } from \"@web/views/fields/relational_utils\";\nimport {\n    Many2ManyTagsField,\n    many2ManyTagsField,\n} from \"@web/views/fields/many2many_tags/many2many_tags_field\";\n\nexport class Many2XTaxTagsAutocomplete extends Many2XAutocomplete {\n    static components = {\n        ...Many2XAutocomplete.components,\n    };\n\n    async loadOptionsSource(request) {\n        // Always include Search More\n        let options = await super.loadOptionsSource(...arguments);\n        if (!options.slice(-1)[0]?.cssClass?.includes(\"o_m2o_dropdown_option_search_more\")) {\n            options.push({\n                label: this.SearchMoreButtonLabel,\n                onSelect: this.onSearchMore.bind(this, request),\n                cssClass: \"o_m2o_dropdown_option o_m2o_dropdown_option_search_more\",\n            });\n        }\n        return options;\n    }\n\n    async onSearchMore(request) {\n        const { getDomain, context, fieldString } = this.props;\n\n        const domain = getDomain();\n        let dynamicFilters = [];\n        if (request.length) {\n            dynamicFilters = [\n                {\n                    description: _t(\"Quick search: %s\", request),\n                    domain: [[\"name\", \"ilike\", request]],\n                },\n            ];\n        }\n\n        const title = _t(\"Search: %s\", fieldString);\n        this.selectCreate({\n            domain,\n            context,\n            filters: dynamicFilters,\n            title,\n        });\n    }\n\n}\n\nexport class Many2ManyTaxTagsField extends Many2ManyTagsField {\n    static components = {\n        ...Many2ManyTagsField.components,\n        Many2XAutocomplete: Many2XTaxTagsAutocomplete,\n    };\n}\n\nexport const many2ManyTaxTagsField = {\n    ...many2ManyTagsField,\n    component: Many2ManyTaxTagsField,\n    additionalClasses: ['o_field_many2many_tags']\n};\n\nregistry.category(\"fields\").add(\"many2many_tax_tags\", many2ManyTaxTagsField);\n", "import { registry } from \"@web/core/registry\";\nimport { standardWidgetProps } from \"@web/views/widgets/standard_widget_props\";\nimport { useService } from \"@web/core/utils/hooks\";\n\nimport { Component } from \"@odoo/owl\";\n\nclass AccountOnboardingWidget extends Component {\n    static template = \"account.Onboarding\";\n    static props = {\n        ...standardWidgetProps,\n    };\n    setup() {\n        this.action = useService(\"action\");\n        this.orm = useService(\"orm\");\n    }\n\n    get recordOnboardingSteps() {\n        return JSON.parse(this.props.record.data.kanban_dashboard).onboarding?.steps;\n    }\n\n    async onboardingLinkClicked(step) {\n        const action = await this.orm.call(\"onboarding.onboarding.step\", step.action, [], {\n            context: {\n                journal_id: this.props.record.resId,\n            }\n        });\n        this.action.doAction(action);\n    }\n}\n\nexport const accountOnboarding = {\n    component: AccountOnboardingWidget,\n}\n\nregistry.category(\"view_widgets\").add(\"account_onboarding\", accountOnboarding);\n", "import { Component } from \"@odoo/owl\";\nimport { registry } from \"@web/core/registry\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { computeM2OProps, Many2One } from \"@web/views/fields/many2one/many2one\";\nimport { buildM2OFieldDescription, Many2OneField } from \"@web/views/fields/many2one/many2one_field\";\n\nclass LineOpenMoveWidget extends Component {\n    static template = \"account.LineOpenMoveWidget\";\n    static components = { Many2One };\n    static props = { ...Many2OneField.props };\n\n    setup() {\n        this.action = useService(\"action\");\n    }\n\n    get m2oProps() {\n        return {\n            ...computeM2OProps(this.props),\n            openRecordAction: () => this.openAction(),\n        };\n    }\n\n    async openAction() {\n        return this.action.doActionButton({\n            type: \"object\",\n            resId: this.props.record.data[this.props.name].id,\n            name: \"action_open_business_doc\",\n            resModel: \"account.move.line\",\n        });\n    }\n}\n\nregistry.category(\"fields\").add(\"line_open_move_widget\", {\n    ...buildM2OFieldDescription(LineOpenMoveWidget),\n});\n", "import { registry } from \"@web/core/registry\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { standardFieldProps } from \"@web/views/fields/standard_field_props\";\nimport { Component } from \"@odoo/owl\";\n\nclass OpenMoveWidget extends Component {\n    static template = \"account.OpenMoveWidget\";\n    static props = { ...standardFieldProps };\n\n    setup() {\n        super.setup();\n        this.action = useService(\"action\");\n    }\n\n    async openMove(ev) {\n        this.action.doActionButton({\n            type: \"object\",\n            resId: this.props.record.resId,\n            name: \"action_open_business_doc\",\n            resModel: this.props.record.resModel,\n        });\n    }\n}\n\nregistry.category(\"fields\").add(\"open_move_widget\", {\n    component: OpenMoveWidget,\n});\n", "import { ProductCatalogOrderLine } from \"@product/product_catalog/order_line/order_line\";\n\nexport class ProductCatalogAccountMoveLine extends ProductCatalogOrderLine {\n    static props = {\n        ...ProductCatalogOrderLine.props,\n        min_qty: { type: Number, optional: true },\n    };\n}\n", "import { ProductCatalogKanbanController } from \"@product/product_catalog/kanban_controller\";\nimport { patch } from \"@web/core/utils/patch\";\nimport { _t } from \"@web/core/l10n/translation\";\n\npatch(ProductCatalogKanbanController.prototype, {\n    get stateFiels() {\n        return this.orderResModel === \"account.move\" ? [\"state\", \"move_type\"] : super.stateFiels;\n    },\n\n    _defineButtonContent() {\n        if (this.orderStateInfo.move_type === \"out_invoice\") {\n            this.buttonString = _t(\"Back to Invoice\");\n        } else if (this.orderStateInfo.move_type === \"in_invoice\") {\n            this.buttonString = _t(\"Back to Bill\");\n        } else {\n            super._defineButtonContent();\n        }\n    },\n});\n", "import { ProductCatalogKanbanModel } from \"@product/product_catalog/kanban_model\";\nimport { patch } from \"@web/core/utils/patch\";\n\npatch(ProductCatalogKanbanModel.prototype, {\n    async _loadData(params) {\n        const selectedSection = this.env.searchModel.selectedSection;\n        if (selectedSection.filtered) {\n            params = {\n                ...params,\n                domain: [...(params.domain || []), ['is_in_selected_section_of_order', '=', true]],\n                context: {\n                    ...params.context,\n                    section_id: selectedSection.sectionId,\n                },\n            };\n        }\n        return await super._loadData(params);\n    },\n\n    _getOrderLinesInfoParams(params, productIds) {\n        return {\n            ...super._getOrderLinesInfoParams(params, productIds),\n            section_id: this.env.searchModel.selectedSection.sectionId,\n        };\n    }\n})\n", "import { useSubEnv } from \"@odoo/owl\";\nimport { ProductCatalogKanbanRecord } from \"@product/product_catalog/kanban_record\";\nimport { ProductCatalogAccountMoveLine } from \"./account_move_line\";\nimport { patch } from \"@web/core/utils/patch\";\n\npatch(ProductCatalogKanbanRecord.prototype, {\n    setup() {\n        super.setup();\n\n        useSubEnv({\n            ...this.env,\n            selectedSectionId: this.env.searchModel.selectedSection?.sectionId,\n        });\n    },\n\n    get orderLineComponent() {\n        if (this.env.orderResModel === \"account.move\") {\n            return ProductCatalogAccountMoveLine;\n        }\n        return super.orderLineComponent;\n    },\n\n    _getUpdateQuantityAndGetPriceParams() {\n        return {\n            ...super._getUpdateQuantityAndGetPriceParams(),\n            section_id: this.env.searchModel.selectedSection.sectionId || false,\n        };\n    },\n\n    addProduct(qty = 1) {\n        if (this.productCatalogData.quantity === 0 && qty < this.productCatalogData.min_qty) {\n            qty = this.productCatalogData.min_qty; // Take seller's minimum if trying to add less\n        }\n        super.addProduct(qty);\n    },\n\n    updateQuantity(quantity) {\n        const lineCountChange = (quantity > 0) - (this.productCatalogData.quantity > 0);\n        if (lineCountChange !== 0) {\n            this.notifyLineCountChange(lineCountChange);\n        }\n\n        super.updateQuantity(quantity);\n    },\n\n    notifyLineCountChange(lineCountChange) {\n        this.env.searchModel.trigger('section-line-count-change', {\n            sectionId: this.env.selectedSectionId,\n            lineCountChange: lineCountChange,\n        });\n    },\n})\n", "import { productCatalogKanbanView } from \"@product/product_catalog/kanban_view\";\nimport { patch } from \"@web/core/utils/patch\";\nimport { AccountProductCatalogSearchModel } from \"./search/search_model\";\nimport { AccountProductCatalogSearchPanel} from \"./search/search_panel\";\n\npatch(productCatalogKanbanView, {\n    SearchModel: AccountProductCatalogSearchModel,\n    SearchPanel: AccountProductCatalogSearchPanel,\n});\n", "import { SearchModel } from \"@web/search/search_model\";\n\nexport class AccountProductCatalogSearchModel extends SearchModel {\n    setup() {\n        super.setup(...arguments);\n        this.selectedSection = {sectionId: null, filtered: false};\n    }\n\n    setSelectedSection(sectionId, filtered) {\n        this.selectedSection = {sectionId, filtered};\n        this._notify();\n    }\n}\n", "import { onWillStart, useState } from '@odoo/owl';\nimport { getActiveHotkey } from '@web/core/hotkeys/hotkey_service';\nimport { rpc } from '@web/core/network/rpc';\nimport { useBus } from '@web/core/utils/hooks';\nimport { SearchPanel } from '@web/search/search_panel/search_panel';\n\n\nexport class AccountProductCatalogSearchPanel extends SearchPanel {\n    static template = 'account.ProductCatalogSearchPanel';\n\n    setup() {\n        super.setup();\n\n        this.state = useState({\n            ...this.state,\n            sections: new Map(),\n            isAddingSection: '',\n            newSectionName: \"\",\n        });\n\n        useBus(this.env.searchModel, 'section-line-count-change', this.updateSectionLineCount);\n\n        onWillStart(async () => await this.loadSections());\n    }\n\n    updateActiveValues() {\n        super.updateActiveValues();\n        this.state.sidebarExpanded ||= this.showSections;\n    }\n\n    get showSections() {\n        return this.env.model.config.context.show_sections;\n    }\n\n    get selectedSection() {\n        return this.env.searchModel.selectedSection;\n    }\n\n    onDragStart(sectionId, ev) {\n        ev.dataTransfer.setData('section_id', sectionId);\n    }\n\n    onDragOver(ev) {\n        ev.preventDefault();\n    }\n\n    onDrop(targetSecId, ev) {\n        ev.preventDefault();\n        const moveSecId = parseInt(ev.dataTransfer.getData('section_id'));\n        if (moveSecId !== targetSecId) this.reorderSections(moveSecId, targetSecId);\n    }\n\n    enableSectionInput(isAddingSection) {\n        this.state.isAddingSection = isAddingSection;\n        setTimeout(() => document.querySelector('.o_section_input')?.focus(), 100);\n    }\n\n    onSectionInputKeydown(ev) {\n        const hotkey = getActiveHotkey(ev);\n        if (hotkey === 'enter') {\n            this.createSection();\n        } else if (hotkey === 'escape') {\n            Object.assign(this.state, {\n                isAddingSection: '',\n                newSectionName: \"\",\n            });\n        }\n    }\n\n    setSelectedSection(sectionId=null, filtered=false) {\n        this.env.searchModel.setSelectedSection(sectionId, filtered);\n    }\n\n    async createSection() {\n        const sectionName = this.state.newSectionName.trim();\n        if (!sectionName) return this.state.isAddingSection = '';\n\n        const position = this.state.isAddingSection;\n        const section = await rpc('/product/catalog/create_section',\n            this._getSectionInfoParams({\n                name: sectionName,\n                position: position,\n            })\n        );\n\n        if (section) {\n            const sections = this.state.sections;\n            let newLineCount = 0;\n\n            if (position === 'top') {\n                newLineCount = sections.get(false).line_count;\n                sections.delete(false);\n            }\n            sections.set(section.id, {\n                name: this.state.newSectionName,\n                sequence: section.sequence,\n                line_count: newLineCount,\n            });\n            this._sortSectionsBySequence(sections);\n            this.setSelectedSection(section.id);\n        }\n        Object.assign(this.state, {\n            isAddingSection: '',\n            newSectionName: \"\",\n        });\n    }\n\n    async loadSections() {\n        if (!this.showSections) return;\n        const sections = await rpc('/product/catalog/get_sections', this._getSectionInfoParams());\n\n        const sectionMap = new Map();\n        for (const {id, name, sequence, line_count} of sections) {\n            sectionMap.set(id, {name, sequence, line_count});\n        }\n        this.state.sections = sectionMap;\n        this.setSelectedSection(sectionMap.size > 0 ? [...sectionMap.keys()][0] : null);\n    }\n\n    async reorderSections(moveId, targetId) {\n        const sections = this.state.sections;\n        const moveSection = sections.get(moveId);\n        const targetSection = sections.get(targetId);\n\n        if (!moveSection || !targetSection) return;\n\n        const updatedSequences = await rpc('/product/catalog/resequence_sections',\n            this._getSectionInfoParams({\n                sections: [\n                    { id: moveId, sequence: moveSection.sequence },\n                    { id: targetId, sequence: targetSection.sequence },\n                ],\n            })\n        );\n        for (const [id, sequence] of Object.entries(updatedSequences)) {\n            const section = sections.get(parseInt(id));\n            section && (section.sequence = sequence);\n        }\n        const noSection = sections.get(false);\n        noSection && (noSection.sequence = 0); // Reset the sequence of the \"No Section\"\n        this._sortSectionsBySequence(sections);\n    }\n\n    updateSectionLineCount({detail: {sectionId, lineCountChange}}) {\n        const sections = this.state.sections;\n        const section = sections.get(sectionId);\n        if (!section) return;\n\n        section.line_count = Math.max(0, section.line_count + lineCountChange);\n\n        if (section.line_count === 0 && sectionId === false && sections.size > 1) {\n            sections.delete(sectionId);\n            this.setSelectedSection(sections.size > 0 ? [...sections.keys()][0] : null);\n        }\n    }\n\n    _getSectionInfoParams(extra = {}) {\n        const ctx = this.env.model.config.context;\n        return {\n            res_model: ctx.product_catalog_order_model,\n            order_id: ctx.order_id,\n            child_field: ctx.child_field,\n            ...extra,\n        };\n    }\n\n    _sortSectionsBySequence(sections) {\n        this.state.sections = new Map(\n            [...sections].sort((a, b) => a[1].sequence - b[1].sequence)\n        );\n    }\n}\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { buildM2OFieldDescription, extractM2OFieldProps, m2oSupportedOptions } from \"@web/views/fields/many2one/many2one_field\";\nimport { registry } from \"@web/core/registry\";\nimport { ProductNameAndDescriptionField } from \"@product/product_name_and_description/product_name_and_description\";\n\nexport class ProductLabelSectionAndNoteField extends ProductNameAndDescriptionField {\n    static template = \"account.ProductLabelSectionAndNoteField\";\n    static props = {\n        ...super.props,\n        show_label_warning: { type: Boolean, optional: true, default: false },\n    };\n\n    static descriptionColumn = \"name\";\n\n    get sectionAndNoteClasses() {\n        return {\n            \"fw-bolder\": this.isSection,\n            \"fw-bold\": this.isSubSection,\n            \"fst-italic\": this.isNote(),\n            \"text-warning\": this.shouldShowWarning(),\n        };\n    }\n\n    get sectionAndNoteIsReadonly() {\n        return (\n            this.props.readonly\n            && this.isProductClickable\n            && ([\"cancel\", \"posted\"].includes(this.props.record.evalContext.parent.state)\n            || this.props.record.evalContext.parent.locked)\n        )\n    }\n\n    get isSection() {\n        return this.props.record.data.display_type === \"line_section\";\n    }\n\n    get isSubSection() {\n        return this.props.record.data.display_type === \"line_subsection\";\n    }\n\n    get isSectionOrSubSection() {\n        return this.isSection || this.isSubSection;\n    }\n\n    isNote(record = null) {\n        record = record || this.props.record;\n        return record.data.display_type === \"line_note\";\n    }\n\n    parseLabel(value) {\n        return (this.productName && value && this.productName.concat(\"\\n\", value))\n            || (this.productName && !value && this.productName)\n            || (value || \"\");\n    }\n\n    shouldShowWarning() {\n        return (\n            !this.productName &&\n            this.props.show_label_warning &&\n            !this.isSectionOrSubSection &&\n            !this.isNote()\n        );\n    }\n}\n\nexport const productLabelSectionAndNoteField = {\n    ...buildM2OFieldDescription(ProductLabelSectionAndNoteField),\n    listViewWidth: [240, 400],\n    supportedOptions: [\n        ...m2oSupportedOptions,\n        {\n            label: _t(\"Show Label Warning\"),\n            name: \"show_label_warning\",\n            type: \"boolean\",\n            default: false\n        },\n    ],\n    extractProps({ options }) {\n        const props = extractM2OFieldProps(...arguments);\n        props.show_label_warning = options.show_label_warning;\n        return props;\n    },\n};\nregistry\n    .category(\"fields\")\n    .add(\"product_label_section_and_note_field\", productLabelSectionAndNoteField);\n", "import {\n    SectionAndNoteFieldOne2Many,\n    sectionAndNoteFieldOne2Many,\n    SectionAndNoteListRenderer,\n} from \"@account/components/section_and_note_fields_backend/section_and_note_fields_backend\";\nimport { ProductNameAndDescriptionListRendererMixin } from \"@product/product_name_and_description/product_name_and_description\";\nimport { registry } from \"@web/core/registry\";\nimport { patch } from \"@web/core/utils/patch\";\n\nexport class ProductLabelSectionAndNoteListRender extends SectionAndNoteListRenderer {\n    setup() {\n        super.setup();\n        this.descriptionColumn = \"name\";\n        this.productColumns = [\"product_id\", \"product_template_id\"];\n        this.conditionalColumns = [\"product_id\", \"quantity\", \"product_uom_id\"];\n    }\n\n    processAllColumn(allColumns, list) {\n        allColumns = allColumns.map((column) => {\n            if (column[\"optional\"] === \"conditional\" && this.conditionalColumns.includes(column[\"name\"])) {\n                /**\n                 * The preference should be different whether:\n                 *     - It's a Vendor Bill or an Invoice\n                 *     - Sale module is installed\n                 * Vendor Bills -> Product should be hidden by default\n                 * Invoices -> conditionalColumns should be hidden by default if Sale module is not installed\n                 */\n                const isBill = [\"in_invoice\", \"in_refund\", \"in_receipt\"].includes(this.props.list.evalContext.parent.move_type);\n                const isInvoice = [\"out_invoice\", \"out_refund\", \"out_receipt\"].includes(this.props.list.evalContext.parent.move_type);\n                const isSaleInstalled = this.props.list.evalContext.parent.is_sale_installed;\n                column[\"optional\"] = \"show\";\n                if (isBill && column[\"name\"] === \"product_id\") {\n                    column[\"optional\"] = \"hide\";\n                }\n                else if (isInvoice && !isSaleInstalled) {\n                    column[\"optional\"] =  \"hide\";\n                }\n            }\n            return column;\n        });\n        return super.processAllColumn(allColumns, list);\n    }\n\n    isCellReadonly(column, record) {\n        if (![...this.productColumns, \"name\"].includes(column.name)) {\n            return super.isCellReadonly(column, record);\n        }\n        // The isCellReadonly method from the ListRenderer is used to determine the classes to apply to the cell.\n        // We need this override to make sure some readonly classes are not applied to the cell if it is still editable.\n        let isReadonly = super.isCellReadonly(column, record);\n        return (\n            isReadonly\n            && ([\"cancel\", \"posted\"].includes(record.evalContext.parent.state)\n            || record.evalContext.parent.locked)\n        )\n    }\n}\n\npatch(ProductLabelSectionAndNoteListRender.prototype, ProductNameAndDescriptionListRendererMixin);\n\nexport class ProductLabelSectionAndNoteOne2Many extends SectionAndNoteFieldOne2Many {\n    static components = {\n        ...super.components,\n        ListRenderer: ProductLabelSectionAndNoteListRender,\n    };\n}\n\nexport const productLabelSectionAndNoteOne2Many = {\n    ...sectionAndNoteFieldOne2Many,\n    component: ProductLabelSectionAndNoteOne2Many,\n};\n\nregistry\n    .category(\"fields\")\n    .add(\"product_label_section_and_note_field_o2m\", productLabelSectionAndNoteOne2Many);\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { radioField, RadioField } from \"@web/views/fields/radio/radio_field\";\nimport { onWillStart, useState } from \"@odoo/owl\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { deepCopy } from \"@web/core/utils/objects\";\n\n\nconst labels = {\n    'in_invoice': _t(\"Bill\"),\n    'out_invoice': _t(\"Invoice\"),\n    'in_receipt': _t(\"Receipt\"),\n    'out_receipt': _t(\"Receipt\"),\n};\n\nconst in_move_types = ['in_invoice', 'in_receipt']\nconst out_move_types = ['out_invoice', 'out_receipt']\n\n\nexport class ReceiptSelector extends RadioField {\n    static template = \"account.ReceiptSelector\";\n    static props = {\n        ...RadioField.props,\n    };\n\n    setup() {\n        super.setup();\n        this.lazySession = useService(\"lazy_session\");\n        this.show_sale_receipts = useState({ value: false });\n        onWillStart(()=> {\n            this.lazySession.getValue(\"show_sale_receipts\", (show_sale_receipts) => {\n                this.show_sale_receipts.value = show_sale_receipts;\n            })\n        });\n    }\n\n    /**\n     * Remove the unwanted options and update the English labels\n     * @override\n     */\n    get items() {\n        const original_items = super.items;\n        if ( this.type !== 'selection' ) {\n            return original_items;\n        }\n\n        // Use a copy to avoid updating the original selection labels\n        let items = deepCopy(original_items)\n\n        let allowedValues = [];\n        if ( in_move_types.includes(this.value) ) {\n            allowedValues = in_move_types\n        } else if (out_move_types.includes(this.value) && this.show_sale_receipts.value ) {\n            allowedValues = out_move_types\n        }\n\n        if ( allowedValues.length > 1 ) {\n            // Filter only the wanted items\n            items = items.filter((item) => {\n                return (allowedValues.includes(item[0]));\n            });\n\n            // Update the label of the wanted items\n            items.forEach((item) => {\n                if (item[0] in labels) {\n                    item[1] = labels[item[0]];\n                }\n            });\n        }\n        return items;\n    }\n\n    get string() {\n        if ( this.type === 'selection' ) {\n            // Use the original labels and not the modified ones\n            return this.value !== false\n                ? this.props.record.fields[this.props.name].selection.find((i) => i[0] === this.value)[1]\n                : \"\";\n        }\n        return \"\";\n    }\n}\n\nexport const receiptSelector = {\n    ...radioField,\n    additionalClasses: ['o_field_radio'],\n    component: ReceiptSelector,\n    extractProps() {\n        return radioField.extractProps(...arguments);\n    },\n};\n\nregistry.category(\"fields\").add(\"receipt_selector\", receiptSelector);\n", "import { Component, useEffect } from \"@odoo/owl\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { x2ManyCommands } from \"@web/core/orm_service\";\nimport { registry } from \"@web/core/registry\";\nimport { CharField } from \"@web/views/fields/char/char_field\";\nimport { standardFieldProps } from \"@web/views/fields/standard_field_props\";\nimport { ListTextField, TextField } from \"@web/views/fields/text/text_field\";\nimport { X2ManyField, x2ManyField } from \"@web/views/fields/x2many/x2many_field\";\nimport { ListRenderer } from \"@web/views/list/list_renderer\";\n\nconst SHOW_ALL_ITEMS_TOOLTIP = _t(\"Some lines can be on the next page, display them to unlock actions on section.\");\nconst DISABLED_MOVE_DOWN_ITEM_TOOLTIP = _t(\"Some lines of the next section can be on the next page, display them to unlock the action.\");\n\nconst DISPLAY_TYPES = {\n    NOTE: \"line_note\",\n    SECTION: \"line_section\",\n    SUBSECTION: \"line_subsection\",\n};\n\nexport function getParentSectionRecord(list, record) {\n    const { sectionIndex } = getRecordsUntilSection(list, record, false, record.data.display_type !== DISPLAY_TYPES.SUBSECTION);\n    return list.records[sectionIndex];\n}\n\nfunction getPreviousSectionRecords(list, record) {\n    const { sectionRecords } = getRecordsUntilSection(list, record, false);\n    return sectionRecords;\n}\n\nexport function getSectionRecords(list, record, subSection) {\n    const { sectionRecords } = getRecordsUntilSection(list, record, true, subSection);\n    return sectionRecords;\n}\n\nfunction hasNextSection(list, record) {\n    const { sectionIndex } = getRecordsUntilSection(list, record, true);\n    return sectionIndex < list.records.length && list.records[sectionIndex].data.display_type === record.data.display_type;\n}\n\nfunction hasPreviousSection(list, record) {\n    const { sectionIndex } = getRecordsUntilSection(list, record, false);\n    return sectionIndex >= 0 && list.records[sectionIndex].data.display_type === record.data.display_type;\n}\n\nfunction getRecordsUntilSection(list, record, asc, subSection) {\n    const stopAtTypes = [DISPLAY_TYPES.SECTION];\n    if (subSection ?? record.data.display_type === DISPLAY_TYPES.SUBSECTION) {\n        stopAtTypes.push(DISPLAY_TYPES.SUBSECTION);\n    }\n\n    const sectionRecords = [];\n    let index = list.records.findIndex(listRecord => listRecord.id === record.id);\n    if (asc) {\n        sectionRecords.push(list.records[index]);\n        index++;\n        while (index < list.records.length && !stopAtTypes.includes(list.records[index].data.display_type)) {\n            sectionRecords.push(list.records[index]);\n            index++;\n        }\n    } else {\n        index--;\n        while (index >= 0 && !stopAtTypes.includes(list.records[index].data.display_type)) {\n            sectionRecords.unshift(list.records[index]);\n            index--;\n        }\n        sectionRecords.unshift(list.records[index]);\n    }\n\n    return {\n        sectionRecords,\n        sectionIndex: index,\n    };\n}\n\nexport class SectionAndNoteListRenderer extends ListRenderer {\n    static template = \"account.SectionAndNoteListRenderer\";\n    static recordRowTemplate = \"account.SectionAndNoteListRenderer.RecordRow\";\n    static props = [\n        ...super.props,\n        \"aggregatedFields\",\n        \"subsections\",\n        \"hidePrices\",\n        \"hideComposition\",\n    ];\n\n    /**\n     * The purpose of this extension is to allow sections and notes in the one2many list\n     * primarily used on Sales Orders and Invoices\n     *\n     * @override\n     */\n    setup() {\n        super.setup();\n        this.titleField = \"name\";\n        this.priceColumns = [...this.props.aggregatedFields, \"price_unit\"];\n        // invisible fields to force copy when duplicating a section\n        this.copyFields = [\"display_type\", \"collapse_composition\", \"collapse_prices\"];\n        useEffect(\n            (editedRecord) => this.focusToName(editedRecord),\n            () => [this.editedRecord]\n        );\n    }\n\n    get disabledMoveDownItemTooltip() {\n        return DISABLED_MOVE_DOWN_ITEM_TOOLTIP;\n    }\n\n    get showAllItemsTooltip() {\n        return SHOW_ALL_ITEMS_TOOLTIP;\n    }\n\n    get hidePrices() {\n        return this.record.data.collapse_prices;\n    }\n\n    get hideComposition() {\n        return this.record.data.collapse_composition;\n    }\n\n    get disablePricesButton() {\n        return this.shouldCollapse(this.record, 'collapse_prices') || this.disableCompositionButton;\n    }\n\n    get disableCompositionButton() {\n        return this.shouldCollapse(this.record, 'collapse_composition');\n    }\n\n    async toggleCollapse(record, fieldName) {\n        // We don't want to have 'collapse_prices' & 'collapse_composition' set to True at the same time\n        const reverseFieldName = fieldName === 'collapse_prices' ? 'collapse_composition' : 'collapse_prices';\n        const changes = {\n            [fieldName]: !record.data[fieldName],\n            [reverseFieldName]: false,\n        };\n        await record.update(changes);\n    }\n\n    async addRowAfterSection(record, addSubSection) {\n        const canProceed = await this.props.list.leaveEditMode({ canAbandon: false });\n        if (!canProceed) {\n            return;\n        }\n\n        const index =\n            this.props.list.records.indexOf(record) +\n            getSectionRecords(this.props.list, record).length -\n            1;\n        const context = {\n            default_display_type: addSubSection ? DISPLAY_TYPES.SUBSECTION : DISPLAY_TYPES.SECTION,\n        };\n        await this.props.list.addNewRecordAtIndex(index, { context });\n    }\n\n    async addNoteInSection(record) {\n        const canProceed = await this.props.list.leaveEditMode({ canAbandon: false });\n        if (!canProceed) {\n            return;\n        }\n\n        const index =\n            this.props.list.records.indexOf(record) +\n            getSectionRecords(this.props.list, record, true).length -\n            1;\n        const context = {\n            default_display_type: DISPLAY_TYPES.NOTE,\n        };\n        await this.props.list.addNewRecordAtIndex(index, { context });\n    }\n\n    async addRowInSection(record, addSubSection) {\n        const canProceed = await this.props.list.leaveEditMode({ canAbandon: false });\n        if (!canProceed) {\n            return;\n        }\n\n        const index =\n            this.props.list.records.indexOf(record) +\n            getSectionRecords(this.props.list, record, !addSubSection).length -\n            1;\n        const context = this.getInsertLineContext(record, addSubSection);\n        if (addSubSection) {\n            context[\"default_display_type\"] = DISPLAY_TYPES.SUBSECTION;\n        }\n        await this.props.list.addNewRecordAtIndex(index, { context });\n    }\n\n    /**\n     * Hook for other modules to conditionally specify defaults for new lines\n     */\n    getInsertLineContext(_record, _addSubSection) {\n        return {};\n    }\n\n    canUseFormatter(column, record) {\n        if (\n            this.isSection(record) &&\n            this.props.aggregatedFields.includes(column.name)\n        ) {\n            return true;\n        }\n        return super.canUseFormatter(column, record);\n    }\n\n    async deleteSection(record) {\n        if (this.editedRecord && this.editedRecord !== record) {\n            const left = await this.props.list.leaveEditMode({ canAbandon: false });\n            if (!left) {\n                return;\n            }\n        }\n        if (this.activeActions.onDelete) {\n            const method = this.activeActions.unlink ? \"unlink\" : \"delete\";\n            const commands = [];\n            const sectionRecords = getSectionRecords(this.props.list, record);\n            for (const sectionRecord of sectionRecords) {\n                commands.push(\n                    x2ManyCommands[method](sectionRecord.resId || sectionRecord._virtualId)\n                );\n            }\n            await this.props.list.applyCommands(commands);\n        }\n    }\n\n    async duplicateSection(record) {\n        const left = await this.props.list.leaveEditMode();\n        if (!left) {\n            return;\n        }\n\n        const { sectionRecords, sectionIndex } = getRecordsUntilSection(this.props.list, record, true)\n        const recordsToDuplicate = sectionRecords.filter((record) => {\n            return this.shouldDuplicateSectionItem(record);\n        });\n        await this.props.list.duplicateRecords(recordsToDuplicate, {\n            targetIndex: sectionIndex,\n            copyFields: this.copyFields,\n        });\n    }\n\n    async editNextRecord(record, group) {\n        const canProceed = await this.props.list.leaveEditMode({ validate: true });\n        if (!canProceed) {\n            return;\n        }\n\n        const iter = getRecordsUntilSection(this.props.list, record, true, true);\n        if (this.isSection(record) || iter.sectionRecords.length === 1) {\n            return this.props.list.addNewRecordAtIndex(iter.sectionIndex - 1);\n        } else {\n            return super.editNextRecord(record, group);\n        }\n    }\n\n    expandPager() {\n        return this.props.list.load({ limit: this.props.list.count });\n    }\n\n    focusToName(editRec) {\n        if (editRec && editRec.isNew && this.isSectionOrNote(editRec)) {\n            const col = this.columns.find((c) => c.name === this.titleField);\n            this.focusCell(col, null);\n        }\n    }\n\n    hasNextSection(record) {\n        return hasNextSection(this.props.list, record);\n    }\n\n    hasPreviousSection(record) {\n        return hasPreviousSection(this.props.list, record);\n    }\n\n    isNextSectionInPage(record) {\n        if (this.props.list.count <= this.props.list.offset + this.props.list.limit) {\n            // if last page\n            return true;\n        }\n        const sectionRecords = getSectionRecords(this.props.list, record);\n        const index = this.props.list.records.indexOf(record) + sectionRecords.length;\n        if (index >= this.props.list.limit) {\n            return false;\n        }\n\n        const { sectionIndex } = getRecordsUntilSection(this.props.list, this.props.list.records[index], true);\n        return sectionIndex < this.props.list.limit;\n    }\n\n    isSectionOrNote(record = null) {\n        record = record || this.record;\n        return [DISPLAY_TYPES.SECTION, DISPLAY_TYPES.SUBSECTION, DISPLAY_TYPES.NOTE].includes(\n            record.data.display_type\n        );\n    }\n\n    isSection(record = null) {\n        record = record || this.record;\n        return [DISPLAY_TYPES.SECTION, DISPLAY_TYPES.SUBSECTION].includes(record.data.display_type);\n    }\n\n    isSectionInPage(record) {\n        if (this.props.list.count <= this.props.list.offset + this.props.list.limit) {\n            // if last page\n            return true;\n        }\n        const { sectionIndex } = getRecordsUntilSection(this.props.list, record, true);\n        return sectionIndex < this.props.list.limit;\n    }\n\n    isSortable() {\n        return false;\n    }\n\n    isTopSection(record) {\n        return record.data.display_type === DISPLAY_TYPES.SECTION;\n    }\n\n    isSubSection(record) {\n        return record.data.display_type === DISPLAY_TYPES.SUBSECTION;\n    }\n\n    /**\n     * Determines whether the line should be collapsed.\n     * - If the parent is a section: use the parent\u2019s field.\n     * - If the parent is a subsection: use parent subsection OR its section.\n     * @param {object} record\n     * @param {string} fieldName\n     * @param {boolean} checkSection - if true, also evaluates the collapse state for section or\n     *  subsection records\n     * @returns {boolean}\n     */\n    shouldCollapse(record, fieldName, checkSection = false) {\n        const parentSection = getParentSectionRecord(this.props.list, record);\n\n        // --- For sections ---\n        if (this.isSection(record) && checkSection) {\n            if (this.isTopSection(record)) {\n                return record.data[fieldName];\n            }\n            if (this.isSubSection(record)) {\n                return record.data[fieldName] || parentSection?.data[fieldName];\n            }\n            return false;\n        }\n\n        // `line_section` never collapses unless explicitly checked above\n        if (this.isTopSection(record)) {\n            return false;\n        }\n\n        if (!parentSection) {\n            return false;\n        }\n\n        // --- For regular lines ---\n        if (this.isSubSection(parentSection)) {\n            const grandParent = getParentSectionRecord(this.props.list, parentSection);\n            return parentSection.data[fieldName] || grandParent?.data[fieldName];\n        }\n\n        return !!parentSection.data[fieldName];\n    }\n\n    getRowClass(record) {\n        const existingClasses = super.getRowClass(record);\n        let newClasses = `${existingClasses} o_is_${record.data.display_type}`;\n        if (this.props.hideComposition && this.shouldCollapse(record, 'collapse_composition')) {\n            newClasses += \" text-muted\";\n        }\n        return newClasses;\n    }\n\n    getCellClass(column, record) {\n        let classNames = super.getCellClass(column, record);\n        // For hiding columnns of section and note\n        if (\n            this.isSectionOrNote(record)\n            && column.widget !== \"handle\"\n            && ![column.name, ...this.props.aggregatedFields].includes(column.name)\n        ) {\n            return `${classNames} o_hidden`;\n        }\n        // For muting the price columns\n        if (\n            this.props.hidePrices\n            && this.shouldCollapse(record, 'collapse_prices')\n            && this.priceColumns.includes(column.name)\n        ) {\n            classNames += \" text-muted\";\n        }\n\n        return classNames;\n    }\n\n    getColumns(record) {\n        const columns = super.getColumns(record);\n        if (this.isSectionOrNote(record)) {\n            return this.getSectionColumns(columns, record);\n        }\n        return columns;\n    }\n\n    getFormattedValue(column, record) {\n        if (this.isSection(record) && this.props.aggregatedFields.includes(column.name)) {\n            const total = getSectionRecords(this.props.list, record)\n                .filter((record) => !this.isSection(record))\n                .reduce((total, record) => total + record.data[column.name], 0);\n            const formatter = registry.category(\"formatters\").get(column.fieldType, (val) => val);\n            return formatter(total, {\n                ...formatter.extractOptions?.(column),\n                data: record.data,\n                field: record.fields[column.name],\n            });\n        }\n        return super.getFormattedValue(column, record);\n    }\n\n    getSectionColumns(columns, record) {\n        const sectionCols = columns.filter(\n            (col) =>\n                col.widget === \"handle\"\n                || col.name === this.titleField\n                || (this.isSection(record) && this.props.aggregatedFields.includes(col.name))\n        );\n        return sectionCols.map((col) => {\n            if (col.name === this.titleField) {\n                return { ...col, colspan: columns.length - sectionCols.length + 1 };\n            } else {\n                return { ...col };\n            }\n        });\n    }\n\n    async moveSectionDown(record) {\n        const canProceed = await this.props.list.leaveEditMode({ canAbandon: false });\n        if (!canProceed) {\n            return;\n        }\n\n        const sectionRecords = getSectionRecords(this.props.list, record);\n        const index = this.props.list.records.indexOf(record) + sectionRecords.length;\n        const nextSectionRecords = getSectionRecords(this.props.list, this.props.list.records[index]);\n        return this.swapSections(sectionRecords, nextSectionRecords);\n    }\n\n    async moveSectionUp(record) {\n        const canProceed = await this.props.list.leaveEditMode({ canAbandon: false });\n        if (!canProceed) {\n            return;\n        }\n\n        const previousSectionRecords = getPreviousSectionRecords(this.props.list, record);\n        const sectionRecords = getSectionRecords(this.props.list, record);\n        return this.swapSections(previousSectionRecords, sectionRecords);\n    }\n\n    shouldDuplicateSectionItem(record) {\n        return true;\n    }\n\n    async swapSections(sectionRecords1, sectionRecords2) {\n        const commands = [];\n        let sequence = sectionRecords1[0].data[this.props.list.handleField];\n        for (const record of sectionRecords2) {\n            commands.push(x2ManyCommands.update(record.resId || record._virtualId, {\n                [this.props.list.handleField]: sequence++,\n            }));\n        }\n        for (const record of sectionRecords1) {\n            commands.push(x2ManyCommands.update(record.resId || record._virtualId, {\n                [this.props.list.handleField]: sequence++,\n            }));\n        }\n        await this.props.list.applyCommands(commands, { sort: true });\n    }\n\n    /**\n     * @override\n     * Reset the values of `collapse_` fields of the subsection if it is dragged\n     */\n    async sortDrop(dataRowId, dataGroupId, options) {\n        await super.sortDrop(dataRowId, dataGroupId, options);\n\n        const record = this.props.list.records.find(r => r.id === dataRowId);\n        const parentSection = getParentSectionRecord(this.props.list, record);\n        const commands = [];\n\n        if (this.resetOnResequence(record, parentSection)) {\n            commands.push(x2ManyCommands.update(record.resId || record._virtualId, {\n                ...this.fieldsToReset(),\n            }));\n        }\n\n        await this.props.list.applyCommands(commands);\n    }\n\n    resetOnResequence(record, parentSection) {\n        return (\n            this.isSubSection(record)\n            && parentSection?.data.collapse_composition\n            && (record.data.collapse_composition || record.data.collapse_prices)\n        );\n    }\n\n    fieldsToReset() {\n        return {\n            ...(this.props.hideComposition && { collapse_composition: false }),\n            ...(this.props.hidePrices && { collapse_prices: false }),\n        };\n    }\n}\n\nexport class SectionAndNoteFieldOne2Many extends X2ManyField {\n    static components = {\n        ...super.components,\n        ListRenderer: SectionAndNoteListRenderer,\n    };\n    static props = {\n        ...super.props,\n        aggregatedFields: Array,\n        hideComposition: Boolean,\n        hidePrices: Boolean,\n        subsections: Boolean,\n    };\n\n    get rendererProps() {\n        const rp = super.rendererProps;\n        if (this.props.viewMode === \"list\") {\n            rp.aggregatedFields = this.props.aggregatedFields;\n            rp.hideComposition = this.props.hideComposition;\n            rp.hidePrices = this.props.hidePrices;\n            rp.subsections = this.props.subsections;\n        }\n        return rp;\n    }\n}\n\nexport class SectionAndNoteText extends Component {\n    static template = \"account.SectionAndNoteText\";\n    static props = { ...standardFieldProps };\n\n    get componentToUse() {\n        return this.props.record.data.display_type === \"line_section\" ? CharField : TextField;\n    }\n}\n\nexport class ListSectionAndNoteText extends SectionAndNoteText {\n    get componentToUse() {\n        return this.props.record.data.display_type !== \"line_section\"\n            ? ListTextField\n            : super.componentToUse;\n    }\n}\n\nexport const sectionAndNoteFieldOne2Many = {\n    ...x2ManyField,\n    component: SectionAndNoteFieldOne2Many,\n    additionalClasses: [...(x2ManyField.additionalClasses || []), \"o_field_one2many\"],\n    extractProps: (staticInfo, dynamicInfo) => {\n        return {\n            ...x2ManyField.extractProps(staticInfo, dynamicInfo),\n            aggregatedFields: staticInfo.attrs.aggregated_fields\n                ? staticInfo.attrs.aggregated_fields.split(/\\s*,\\s*/)\n                : [],\n            hideComposition: staticInfo.options?.hide_composition ?? false,\n            hidePrices: staticInfo.options?.hide_prices ?? false,\n            subsections: staticInfo.options?.subsections ?? false,\n        };\n    },\n};\n\nexport const sectionAndNoteText = {\n    component: SectionAndNoteText,\n    additionalClasses: [\"o_field_text\"],\n};\n\nexport const listSectionAndNoteText = {\n    ...sectionAndNoteText,\n    component: ListSectionAndNoteText,\n};\n\nregistry.category(\"fields\").add(\"section_and_note_one2many\", sectionAndNoteFieldOne2Many);\nregistry.category(\"fields\").add(\"section_and_note_text\", sectionAndNoteText);\nregistry.category(\"fields\").add(\"list.section_and_note_text\", listSectionAndNoteText);\n", "import { formatMonetary } from \"@web/views/fields/formatters\";\nimport { formatFloat } from \"@web/core/utils/numbers\";\nimport { parseFloat } from \"@web/views/fields/parsers\";\nimport { standardFieldProps } from \"@web/views/fields/standard_field_props\";\nimport { registry } from \"@web/core/registry\";\nimport {\n    Component,\n    onPatched,\n    onWillUpdateProps,\n    onWillRender,\n    toRaw,\n    useRef,\n    useState,\n} from \"@odoo/owl\";\nimport { useNumpadDecimal } from \"@web/views/fields/numpad_decimal_hook\";\n\n/**\n A line of some TaxTotalsComponent, giving the values of a tax group.\n **/\nclass TaxGroupComponent extends Component {\n    static props = {\n        totals: { optional: true },\n        subtotal: { optional: true },\n        taxGroup: { optional: true },\n        onChangeTaxGroup: { optional: true },\n        isReadonly: Boolean,\n        invalidate: Function,\n    };\n    static template = \"account.TaxGroupComponent\";\n\n    setup() {\n        this.inputTax = useRef(\"taxValueInput\");\n        this.state = useState({ value: \"readonly\" });\n        onPatched(() => {\n            if (this.state.value === \"edit\") {\n                const { taxGroup } = this.props;\n                const newVal = formatFloat(taxGroup.tax_amount_currency, { digits: this.props.totals.currency_pd });\n                this.inputTax.el.value = newVal;\n                this.inputTax.el.focus(); // Focus the input\n            }\n        });\n        onWillUpdateProps(() => {\n            this.setState(\"readonly\");\n        });\n        useNumpadDecimal();\n    }\n\n    formatMonetary(value) {\n        return formatMonetary(value, {currencyId: this.props.totals.currency_id});\n    }\n\n    //--------------------------------------------------------------------------\n    // Main methods\n    //--------------------------------------------------------------------------\n\n    /**\n     * The purpose of this method is to change the state of the component.\n     * It can have one of the following three states:\n     *  - readonly: display in read-only mode of the field,\n     *  - edit: display with a html input field,\n     *  - disable: display with a html input field that is disabled.\n     *\n     * If a value other than one of these 3 states is passed as a parameter,\n     * the component is set to readonly by default.\n     *\n     * @param {String} value\n     */\n    setState(value) {\n        if ([\"readonly\", \"edit\", \"disable\"].includes(value)) {\n            this.state.value = value;\n        }\n        else {\n            this.state.value = \"readonly\";\n        }\n    }\n\n    /**\n     * This method handles the \"_onChangeTaxValue\" event. In this method,\n     * we get the new value for the tax group, we format it and we call\n     * the method to recalculate the tax lines. At the moment the method\n     * is called, we disable the html input field.\n     *\n     * In case the value has not changed or the tax group is equal to 0,\n     * the modification does not take place.\n     */\n    _onChangeTaxValue() {\n        this.setState(\"disable\"); // Disable the input\n        const oldValue = this.props.taxGroup.tax_amount_currency;\n        let newValue;\n        try {\n            newValue = parseFloat(this.inputTax.el.value); // Get the new value\n        } catch {\n            this.inputTax.el.value = oldValue;\n            this.setState(\"edit\");\n            return;\n        }\n        // The newValue can\"t be equals to 0\n        if (newValue === oldValue || newValue === 0) {\n            this.setState(\"readonly\");\n            return;\n        }\n        const deltaValue = newValue - oldValue;\n        this.props.taxGroup.tax_amount_currency += deltaValue;\n        this.props.subtotal.tax_amount_currency += deltaValue;\n        this.props.totals.tax_amount_currency += deltaValue;\n        this.props.totals.total_amount_currency += deltaValue;\n\n        this.props.onChangeTaxGroup({\n            oldValue,\n            newValue: newValue,\n            taxGroupId: this.props.taxGroup.id,\n        });\n    }\n}\n\n/**\n Widget used to display tax totals by tax groups for invoices, PO and SO,\n and possibly allowing editing them.\n\n Note that this widget requires the object it is used on to have a\n currency_id field.\n **/\nexport class TaxTotalsComponent extends Component {\n    static template = \"account.TaxTotalsField\";\n    static components = { TaxGroupComponent };\n    static props = {\n        ...standardFieldProps,\n    };\n\n    setup() {\n        this.totals = {};\n        this.formatData(this.props);\n        onWillRender(() => this.formatData(this.props));\n    }\n\n    get readonly() {\n        return this.props.readonly;\n    }\n\n    invalidate() {\n        return this.props.record.setInvalidField(this.props.name);\n    }\n\n    formatMonetary(value) {\n        return formatMonetary(value, {currencyId: this.totals.currency_id});\n    }\n\n    /**\n     * This method is the main function of the tax group widget.\n     * It is called by the TaxGroupComponent and receives the newer tax value.\n     *\n     * It is responsible for triggering an event to notify the ORM of a change.\n     */\n    _onChangeTaxValueByTaxGroup({ oldValue, newValue }) {\n        if (oldValue === newValue) return;\n        this.props.record.update({ [this.props.name]: this.totals });\n        delete this.totals.cash_rounding_base_amount_currency;\n    }\n\n    formatData(props) {\n        let totals = JSON.parse(JSON.stringify(toRaw(props.record.data[this.props.name])));\n        if (!totals) {\n            return;\n        }\n        this.totals = totals;\n    }\n}\n\nexport const taxTotalsComponent = {\n    component: TaxTotalsComponent,\n};\n\nregistry.category(\"fields\").add(\"account-tax-totals-field\", taxTotalsComponent);\n", "import { rpc } from \"@web/core/network/rpc\";\nimport { registry } from \"@web/core/registry\";\n\nimport { accountTaxHelpers } from \"@account/helpers/account_tax\";\n\nimport { useState, Component } from \"@odoo/owl\";\n\nexport class TestsSharedJsPython extends Component {\n    static template = \"account.TestsSharedJsPython\";\n    static props = {\n        tests: { type: Array, optional: true },\n    };\n\n    setup() {\n        super.setup();\n        this.state = useState({ done: false });\n    }\n\n    processTest(params) {\n        if (params.test === \"taxes_computation\") {\n            let filter_tax_function = null;\n            if (params.excluded_tax_ids && params.excluded_tax_ids.length) {\n                filter_tax_function = (tax) => !params.excluded_tax_ids.includes(tax.id);\n            }\n\n            const kwargs = {\n                product: params.product,\n                product_uom: params.product_uom,\n                precision_rounding: params.precision_rounding,\n                rounding_method: params.rounding_method,\n                filter_tax_function: filter_tax_function,\n            };\n            const results = {\n                results: accountTaxHelpers.get_tax_details(\n                    params.taxes,\n                    params.price_unit,\n                    params.quantity,\n                    kwargs,\n                )\n            };\n            if (params.rounding_method === \"round_globally\") {\n                results.total_excluded_results = accountTaxHelpers.get_tax_details(\n                    params.taxes,\n                    results.results.total_excluded / params.quantity,\n                    params.quantity,\n                    {...kwargs, special_mode: \"total_excluded\"}\n                );\n                results.total_included_results = accountTaxHelpers.get_tax_details(\n                    params.taxes,\n                    results.results.total_included / params.quantity,\n                    params.quantity,\n                    {...kwargs, special_mode: \"total_included\"}\n                );\n            }\n            return results;\n        }\n        if (params.test === \"adapt_price_unit_to_another_taxes\") {\n            return {\n                price_unit: accountTaxHelpers.adapt_price_unit_to_another_taxes(\n                    params.price_unit,\n                    params.product,\n                    params.original_taxes,\n                    params.new_taxes,\n                    { product_uom: params.product_uom}\n                )\n            }\n        }\n        if (params.test === \"tax_totals_summary\") {\n            const document = this.populateDocument(params.document);\n            const taxTotals = accountTaxHelpers.get_tax_totals_summary(\n                document.lines,\n                document.currency,\n                document.company,\n                {cash_rounding: document.cash_rounding}\n            );\n            return {tax_totals: taxTotals, soft_checking: params.soft_checking};\n        }\n        if (params.test === \"global_discount\") {\n            const document = this.populateDocument(params.document);\n            const baseLines = accountTaxHelpers.prepare_global_discount_lines(\n                document.lines,\n                document.company,\n                params.amount_type,\n                params.amount,\n                \"global_discount\",\n            );\n            document.lines.push(...baseLines);\n            accountTaxHelpers.add_tax_details_in_base_lines(document.lines, document.company);\n            accountTaxHelpers.round_base_lines_tax_details(document.lines, document.company);\n            const taxTotals = accountTaxHelpers.get_tax_totals_summary(\n                document.lines,\n                document.currency,\n                document.company,\n                {cash_rounding: document.cash_rounding}\n            );\n            return {tax_totals: taxTotals, soft_checking: params.soft_checking};\n        }\n        if (params.test === \"down_payment\") {\n            const document = this.populateDocument(params.document);\n            const baseLines = accountTaxHelpers.prepare_down_payment_lines(\n                document.lines,\n                document.company,\n                params.amount_type,\n                params.amount,\n                \"down_payment\",\n            );\n            document.lines = baseLines;\n            accountTaxHelpers.add_tax_details_in_base_lines(document.lines, document.company);\n            accountTaxHelpers.round_base_lines_tax_details(document.lines, document.company);\n            const taxTotals = accountTaxHelpers.get_tax_totals_summary(\n                document.lines,\n                document.currency,\n                document.company,\n                {cash_rounding: document.cash_rounding}\n            );\n            return {\n                tax_totals: taxTotals,\n                soft_checking: params.soft_checking,\n                base_lines_tax_details: this.extractBaseLinesDetails(document),\n            };\n        }\n        if (params.test === \"base_lines_tax_details\") {\n            const document = this.populateDocument(params.document);\n            return {\n                base_lines_tax_details: this.extractBaseLinesDetails(document),\n            };\n        }\n    }\n\n    async processTests() {\n        const tests = this.props.tests || [];\n        const results = tests.map(this.processTest.bind(this));\n        await rpc(\"/account/post_tests_shared_js_python\", { results: results });\n        this.state.done = true;\n    }\n\n    populateDocument(document) {\n        const base_lines = document.lines.map(line => accountTaxHelpers.prepare_base_line_for_taxes_computation(null, line));\n        accountTaxHelpers.add_tax_details_in_base_lines(base_lines, document.company);\n        accountTaxHelpers.round_base_lines_tax_details(base_lines, document.company);\n        return {\n            ...document,\n            lines: base_lines,\n        }\n    }\n\n    extractBaseLinesDetails(document) {\n        return document.lines.map(line => ({\n            total_excluded_currency: line.tax_details.total_excluded_currency,\n            total_excluded: line.tax_details.total_excluded,\n            total_included_currency: line.tax_details.total_included_currency,\n            total_included: line.tax_details.total_included,\n            delta_total_excluded_currency: line.tax_details.delta_total_excluded_currency,\n            delta_total_excluded: line.tax_details.delta_total_excluded,\n            manual_total_excluded_currency: line.manual_total_excluded_currency,\n            manual_total_excluded: line.manual_total_excluded,\n            manual_tax_amounts: line.manual_tax_amounts,\n            taxes_data: line.tax_details.taxes_data.map(tax_data => ({\n                tax_id: tax_data.tax.id,\n                tax_amount_currency: tax_data.tax_amount_currency,\n                tax_amount: tax_data.tax_amount,\n                base_amount_currency: tax_data.base_amount_currency,\n                base_amount: tax_data.base_amount,\n            })),\n        }));\n    }\n}\n\nregistry.category(\"public_components\").add(\"account.tests_shared_js_python\", TestsSharedJsPython);\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { useService } from \"@web/core/utils/hooks\";\n\nimport { Component, useState } from \"@odoo/owl\";\n\nexport class UploadDropZone extends Component {\n    static template = \"account.UploadDropZone\";\n    static props = {\n        visible: { type: Boolean, optional: true },\n        hideZone: { type: Function, optional: true },\n        dragIcon: { type: String, optional: true },\n        dragText: { type: String, optional: true },\n        dragTitle: { type: String, optional: true },\n        dragCompany: { type: String, optional: true },\n        dragShowCompany: { type: Boolean, optional: true },\n        dropZoneTitle: { type: String, optional: true },\n        dropZoneDescription: { type: String, optional: true },\n    };\n    static defaultProps = {\n        hideZone: () => {},\n    };\n\n    setup() {\n        this.notificationService = useService(\"notification\");\n        this.dashboardState = useState(this.env.dashboardState || {});\n    }\n\n    onDrop(ev) {\n        const selector = '.document_file_uploader.o_input_file';\n        // look for the closest uploader Input as it may have a context\n        let uploadInput = ev.target.closest('.o_drop_area').parentElement.querySelector(selector) || document.querySelector(selector);\n        let files = ev.dataTransfer ? ev.dataTransfer.files : false;\n        if (uploadInput && !!files) {\n            uploadInput.files = ev.dataTransfer.files;\n            uploadInput.dispatchEvent(new Event(\"change\"));\n        } else {\n            this.notificationService.add(\n                _t(\"Could not upload files\"),\n                {\n                    type: \"danger\",\n                });\n        }\n        this.props.hideZone();\n    }\n}\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { standardFieldProps } from \"@web/views/fields/standard_field_props\";\nimport { registry } from \"@web/core/registry\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { Component } from \"@odoo/owl\";\n\nclass X2ManyButtons extends Component {\n    static template = \"account.X2ManyButtons\";\n    static props = {\n        ...standardFieldProps,\n        treeLabel: { type: String },\n    };\n\n    setup() {\n        this.orm = useService(\"orm\");\n        this.action = useService(\"action\");\n    }\n\n    async openTreeAndDiscard() {\n        const ids = this.currentField.currentIds;\n        await this.props.record.discard();\n        const context = this.currentField.resModel === \"account.move\"\n            ? { list_view_ref: \"account.view_duplicated_moves_tree_js\" }\n            : {};\n        this.action.doAction({\n            name: this.props.treeLabel,\n            type: \"ir.actions.act_window\",\n            res_model: this.currentField.resModel,\n            views: [\n                [false, \"list\"],\n                [false, \"form\"],\n            ],\n            domain: [[\"id\", \"in\", ids]],\n            context: context,\n        });\n    }\n\n    async openFormAndDiscard(id) {\n        const action = await this.orm.call(this.currentField.resModel, \"action_open_business_doc\", [id], {});\n        await this.props.record.discard();\n        this.action.doAction(action);\n    }\n\n    get currentField() {\n        return this.props.record.data[this.props.name];\n    }\n}\n\nX2ManyButtons.template = \"account.X2ManyButtons\";\nregistry.category(\"fields\").add(\"x2many_buttons\", {\n    component: X2ManyButtons,\n    relatedFields: [{ name: \"display_name\", type: \"char\" }],\n    extractProps: ({ string }) => ({ treeLabel: string || _t(\"Records\") }),\n});\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { markup } from \"@odoo/owl\";\nimport { registry } from \"@web/core/registry\";\n\nexport class AccountMoveService {\n    constructor(env, services) {\n        this.setup(env, services);\n    }\n\n    setup(env, services) {\n        this.env = env;\n        this.action = services.action;\n        this.dialog = services.dialog;\n        this.orm = services.orm;\n    }\n\n    async getDeletionDialogBody(body, moveIds) {\n        const isMoveEndOfChain = await this.orm.call(\"account.move\", \"check_move_sequence_chain\", [moveIds]);\n        if (!isMoveEndOfChain) {\n            const message = _t(\"This operation will create a gap in the sequence.\");\n            return markup`<div class=\"text-danger\">${message}</div>${body}`;\n        }\n        return body;\n    }\n\n    async downloadPdf(accountMoveId, target = \"download\") {\n        const downloadAction = await this.orm.call(\n            \"account.move\",\n            \"action_invoice_download_pdf\",\n            [accountMoveId, target]\n        );\n        await this.action.doAction(downloadAction);\n    }\n}\n\nexport const accountMoveService = {\n    dependencies: [\"action\", \"dialog\", \"orm\"],\n    start(env, services) {\n        return new AccountMoveService(env, services);\n    },\n};\n\nregistry.category(\"services\").add(\"account_move\", accountMoveService);\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\n\n\nexport const accountNotificationService = {\n    dependencies: [\"bus_service\", \"notification\", \"action\"],\n\n    start(env, { bus_service, notification, action }) {\n        bus_service.subscribe(\"account_notification\", ({ message, sticky, title, type, action_button}) => {\n            const buttons = [{\n                name: action_button.name,\n                primary: false,\n                onClick: () => {\n                    action.doAction({\n                        name: _t(action_button.action_name),\n                        type: 'ir.actions.act_window',\n                        res_model: action_button.model,\n                        domain: [[\"id\", \"in\", action_button.res_ids]],\n                        views: [[false, 'list'], [false, 'form']],\n                        target: 'current',\n                    });\n                },\n            }];\n            notification.add(message, { sticky, title, type, buttons });\n        });\n    }\n};\n\nregistry.category(\"services\").add(\"accountNotification\", accountNotificationService);\n", "import { user } from \"@web/core/user\";\nimport { AccountFileUploader } from \"@account/components/account_file_uploader/account_file_uploader\";\nimport { UploadDropZone } from \"@account/components/upload_drop_zone/upload_drop_zone\";\nimport { KanbanDropdownMenuWrapper } from \"@web/views/kanban/kanban_dropdown_menu_wrapper\";\nimport { KanbanRecord } from \"@web/views/kanban/kanban_record\";\n\nimport { useState, onWillStart } from \"@odoo/owl\";\n\n// Accounting Dashboard\nexport class DashboardKanbanDropdownMenuWrapper extends KanbanDropdownMenuWrapper {\n    onClick(ev) {\n        // Keep the dropdown open as we need the fileupload to remain in the dom\n        if (!ev.target.tagName === \"INPUT\" && !ev.target.closest('.file_upload_kanban_action_a')) {\n            super.onClick(ev);\n        }\n    }\n}\n\nexport class DashboardKanbanRecord extends KanbanRecord {\n    static template = \"account.DashboardKanbanRecord\";\n    static components = {\n        ...DashboardKanbanRecord.components,\n        UploadDropZone,\n        AccountFileUploader,\n        KanbanDropdownMenuWrapper: DashboardKanbanDropdownMenuWrapper,\n    };\n\n    setup() {\n        super.setup();\n        onWillStart(async () => {\n            this.allowDrop = this.recordDropSettings.group ? await user.hasGroup(this.recordDropSettings.group) : true;\n        });\n        this.dropzoneState = useState({\n            visible: false,\n        });\n    }\n\n    get recordDropSettings() {\n        const kanbanDashboard = JSON.parse(this.props.record.data.kanban_dashboard);\n        return {\n            image: kanbanDashboard.drag_drop_settings.image,\n            text: kanbanDashboard.drag_drop_settings.text,\n            company_name: kanbanDashboard.company_name,\n            show_company: kanbanDashboard.show_company\n        };\n    }\n\n    get dropzoneProps() {\n        const recordDropSettings = this.recordDropSettings;\n        return {\n            visible: this.dropzoneState.visible,\n            dragIcon: recordDropSettings.image,\n            dragText: recordDropSettings.text,\n            dragCompany: recordDropSettings.company_name,\n            dragShowCompany: recordDropSettings.show_company,\n            dragTitle: this.props.record.data.name,\n            hideZone: () => { this.dropzoneState.visible = false; },\n        }\n    }\n}\n", "import { KanbanRenderer } from \"@web/views/kanban/kanban_renderer\";\nimport { DashboardKanbanRecord } from \"./account_dashboard_kanban_record\";\n\nimport { useSubEnv, reactive } from \"@odoo/owl\";\n\nexport class DashboardKanbanRenderer extends KanbanRenderer {\n    static template = \"account.DashboardKanbanRenderer\";\n    static components = {\n        ...KanbanRenderer.components,\n        KanbanRecord: DashboardKanbanRecord,\n    };\n\n    setup() {\n        super.setup();\n        useSubEnv({\n            dashboardState: reactive({isDragging: false}),\n            setDragging: this.setDragging.bind(this),\n        });\n    }\n\n    kanbanDragEnter(e) {\n        this.setDragging(e.dataTransfer.types.includes(\"Files\"));\n    }\n\n    kanbanDragLeave(e) {\n        const mouseX = e.clientX, mouseY = e.clientY;\n        const {x, y, width, height} = this.rootRef.el.getBoundingClientRect();\n        const mouseInsideKanbanRenderer = mouseX > x && mouseX <= x + width && mouseY > y && mouseY <= y + height;\n        if (!mouseInsideKanbanRenderer || !e.dataTransfer.types.includes(\"Files\")) {\n            // if the mouse position is outside the kanban renderer, all cards should hide their dropzones.\n            this.setDragging(false);\n        } else {\n            this.setDragging(true);\n        }\n    }\n\n    kanbanDragDrop(e) {\n        this.setDragging(false);\n        return false;\n    }\n\n    setDragging(value) {\n        this.env.dashboardState.isDragging = value;\n    }\n}\n", "import { registry } from \"@web/core/registry\";\nimport { kanbanView } from \"@web/views/kanban/kanban_view\";\nimport { DashboardKanbanRenderer } from \"./account_dashboard_kanban_renderer\";\n\nexport const accountDashboardKanbanView = {\n    ...kanbanView,\n    Renderer: DashboardKanbanRenderer,\n};\n\nregistry.category(\"views\").add(\"account_dashboard_kanban\", accountDashboardKanbanView);\n", "import { FileUploadKanbanController } from \"../file_upload_kanban/file_upload_kanban_controller\";\nimport { AccountFileUploader } from \"@account/components/account_file_uploader/account_file_uploader\";\n\nexport class AccountMoveKanbanController extends FileUploadKanbanController {\n    static components = {\n        ...FileUploadKanbanController.components,\n        AccountFileUploader,\n    };\n\n    setup() {\n        super.setup();\n        this.showUploadButton = this.props.context.default_move_type !== 'entry' || 'active_id' in this.props.context;\n    }\n}\n", "import { registry } from \"@web/core/registry\";\nimport { fileUploadKanbanView } from \"../file_upload_kanban/file_upload_kanban_view\";\nimport { AccountMoveKanbanController } from \"./account_move_kanban_controller\";\n\nexport const accountMoveUploadKanbanView = {\n    ...fileUploadKanbanView,\n    Controller: AccountMoveKanbanController,\n    buttonTemplate: \"account.AccountMoveKanbanView.Buttons\",\n};\n\nregistry.category(\"views\").add(\"account_documents_kanban\", accountMoveUploadKanbanView);\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { FileUploadListController } from \"../file_upload_list/file_upload_list_controller\";\nimport { AccountFileUploader } from \"@account/components/account_file_uploader/account_file_uploader\";\nimport { deleteConfirmationMessage } from \"@web/core/confirmation_dialog/confirmation_dialog\";\n\nimport { useService } from \"@web/core/utils/hooks\";\n\nexport class AccountMoveListController extends FileUploadListController {\n    static components = {\n        ...FileUploadListController.components,\n        AccountFileUploader,\n    };\n\n    setup() {\n        super.setup();\n        this.orm = useService(\"orm\");\n        this.account_move_service = useService(\"account_move\");\n        this.showUploadButton = this.props.context.default_move_type !== 'entry' || 'active_id' in this.props.context;\n    }\n\n    get actionMenuProps() {\n        return {\n            ...super.actionMenuProps,\n            printDropdownTitle: _t(\"Print\"),\n            loadExtraPrintItems: this.loadExtraPrintItems.bind(this),\n        };\n    }\n\n    async loadExtraPrintItems() {\n        return this.orm.call(\"account.move\", \"get_extra_print_items\", [this.actionMenuProps.getActiveIds()]);\n    }\n\n    async onDeleteSelectedRecords() {\n        const deleteConfirmationDialogProps = this.deleteConfirmationDialogProps;\n        const selectedResIds = await this.model.root.getResIds(true);\n        if (this.props.resModel === \"account.move\") {\n            let body = deleteConfirmationMessage;\n            if (this.model.root.isDomainSelected || this.model.root.selection.length > 1) {\n                body = _t(\"Are you sure you want to delete these records?\");\n            }\n            deleteConfirmationDialogProps.body = await this.account_move_service.getDeletionDialogBody(body, selectedResIds);\n        }\n        this.deleteRecordsWithConfirmation(\n            deleteConfirmationDialogProps\n        );\n    }\n}\n", "import { BillGuide } from \"@account/components/bill_guide/bill_guide\";\nimport { FileUploadListRenderer } from \"../file_upload_list/file_upload_list_renderer\";\n\nexport class AccountMoveListRenderer extends FileUploadListRenderer {\n    static template = \"account.AccountMoveListRenderer\";\n    static components = {\n        ...FileUploadListRenderer.components,\n        BillGuide,\n    };\n\n    // Add warning background color in the ref column if we detect that the move has a duplicated\n    getCellClass(column, record) {\n        const classNames = super.getCellClass(column, record);\n        if (column.name === 'ref' && record.data.duplicated_ref_ids && record.data.duplicated_ref_ids.count !== 0) {\n            return `${classNames} table-warning`;\n        }\n        return classNames;\n    }\n}\n", "import { registry } from \"@web/core/registry\";\nimport { fileUploadListView } from \"../file_upload_list/file_upload_list_view\";\nimport { AccountMoveListController } from \"./account_move_list_controller\";\nimport { AccountMoveListRenderer } from \"./account_move_list_renderer\";\n\nexport const accountMoveUploadListView = {\n    ...fileUploadListView,\n    Controller: AccountMoveListController,\n    Renderer: AccountMoveListRenderer,\n    buttonTemplate: \"account.AccountMoveListView.Buttons\",\n};\n\nregistry.category(\"views\").add(\"account_tree\", accountMoveUploadListView);\n", "import { ListController } from \"@web/views/list/list_controller\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport {registry} from \"@web/core/registry\";\nimport {listView} from \"@web/views/list/list_view\";\n\nexport class AccountX2ManyListController extends ListController {\n    setup() {\n        super.setup();\n        this.orm = useService(\"orm\");\n        this.action = useService(\"action\");\n    }\n\n    async openRecord(record) {\n        const action = await this.orm.call(record.resModel, 'action_open_business_doc', [record.resId], {});\n        return this.actionService.doAction(action);\n    }\n}\n\nregistry.category(\"views\").add(\"account_x2many_list\", {\n    ...listView,\n    Controller: AccountX2ManyListController,\n});\n", "import { KanbanController } from \"@web/views/kanban/kanban_controller\";\nimport { DocumentFileUploader } from \"@account/components/document_file_uploader/document_file_uploader\";\n\nexport class FileUploadKanbanController extends KanbanController {\n    static components = {\n        ...KanbanController.components,\n        DocumentFileUploader,\n    };\n}\n", "import { _t } from '@web/core/l10n/translation';\nimport { KanbanRenderer } from \"@web/views/kanban/kanban_renderer\";\nimport { UploadDropZone } from \"@account/components/upload_drop_zone/upload_drop_zone\";\nimport { useState } from \"@odoo/owl\";\nimport { uploadFileFromData } from \"../upload_file_from_data_hook\";\n\nexport class FileUploadKanbanRenderer extends KanbanRenderer {\n    static template = \"account.FileUploadKanbanRenderer\";\n    static components = {\n        ...KanbanRenderer.components,\n        UploadDropZone,\n    };\n\n    setup() {\n        super.setup();\n        this.dropzoneState = useState({ visible: false });\n        this.uploadFileFromData = uploadFileFromData();\n        this.dropZoneTitle = _t(\"Drop and let the AI process your bills automatically.\");\n    }\n\n    async onPaste(ev) {\n        if (!ev.clipboardData?.items) {\n            return;\n        }\n        ev.preventDefault();\n        this.uploadFileFromData(ev.clipboardData);\n    }\n\n    onDragStart(ev) {\n        if (ev.dataTransfer.types.includes(\"Files\")) {\n            this.dropzoneState.visible = true;\n        }\n    }\n}\n", "import { registry } from \"@web/core/registry\";\nimport { kanbanView } from \"@web/views/kanban/kanban_view\";\nimport { FileUploadKanbanController } from \"./file_upload_kanban_controller\";\nimport { FileUploadKanbanRenderer } from \"./file_upload_kanban_renderer\";\n\nexport const fileUploadKanbanView = {\n    ...kanbanView,\n    Controller: FileUploadKanbanController,\n    Renderer: FileUploadKanbanRenderer,\n    buttonTemplate: \"account.FileuploadKanbanView.Buttons\",\n};\n\nregistry.category(\"views\").add(\"file_upload_kanban\", fileUploadKanbanView);\n", "import { ListController } from \"@web/views/list/list_controller\";\nimport { DocumentFileUploader } from \"@account/components/document_file_uploader/document_file_uploader\";\n\nexport class FileUploadListController extends ListController {\n    static components = {\n        ...ListController.components,\n        DocumentFileUploader,\n    };\n};\n", "import { _t } from '@web/core/l10n/translation';\nimport { ListRenderer } from \"@web/views/list/list_renderer\";\nimport { UploadDropZone } from \"@account/components/upload_drop_zone/upload_drop_zone\";\nimport { useState } from \"@odoo/owl\";\nimport { uploadFileFromData } from \"../upload_file_from_data_hook\";\n\nexport class FileUploadListRenderer extends ListRenderer {\n    static template = \"account.FileUploadListRenderer\";\n    static components = {\n        ...ListRenderer.components,\n        UploadDropZone,\n    };\n\n    setup() {\n        super.setup();\n        this.dropzoneState = useState({ visible: false });\n        this.uploadFileFromData = uploadFileFromData();\n        this.dropZoneTitle = _t(\"Drop and let the AI process your bills automatically.\");\n    }\n\n    async onPaste(ev) {\n        if (!ev.clipboardData?.items) {\n            return;\n        }\n        ev.preventDefault();\n        this.uploadFileFromData(ev.clipboardData);\n    }\n\n    onDragStart(ev) {\n        if (ev.dataTransfer.types.includes(\"Files\")) {\n            this.dropzoneState.visible = true;\n        }\n    }\n}\n", "import { registry } from \"@web/core/registry\";\nimport { listView } from \"@web/views/list/list_view\";\nimport { FileUploadListController } from \"./file_upload_list_controller\";\nimport { FileUploadListRenderer } from \"./file_upload_list_renderer\";\n\nexport const fileUploadListView = {\n    ...listView,\n    Controller: FileUploadListController,\n    Renderer: FileUploadListRenderer,\n    buttonTemplate: \"account.FileuploadListView.Buttons\",\n};\n\nregistry.category(\"views\").add(\"file_upload_list\", fileUploadListView);\n", "// Supported file types we need extract on paste\nconst supportedFileTypes = [\"text/xml\", \"application/pdf\"];\n\n/**\n * Return function to extract and upload from given dataTransfer.\n *\n * @param {dataTransfer} dataTransfer containing text or files.\n */\nexport function uploadFileFromData(dataTransfer) {\n    return async (dataTransfer) => {\n\n        function uploadFiles(dataTransfer) {\n            const invalidFiles = [...dataTransfer.items].filter(\n                (item) => item.kind !== \"file\" || !supportedFileTypes.includes(item.type)\n            );\n            if (invalidFiles.length !== 0) {\n                // don't upload any files if one of them is non supported file type\n                console.warn(\"Invalid files to extract details.\");\n                return;\n            }\n            let uploadInput = document.querySelector('.document_file_uploader.o_input_file');\n            uploadInput.files = dataTransfer.files;\n            uploadInput.dispatchEvent(new Event(\"change\"));\n        }\n\n        if (dataTransfer.files.length !== 0) {\n            uploadFiles(dataTransfer);\n        } else {\n            console.warn(\"Invalid data to extract details.\");\n        }\n    }\n}\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { stepUtils } from \"@web_tour/tour_utils\";\n\nimport { markup } from \"@odoo/owl\";\n\nexport const accountTourSteps = {\n    draftInvoiceSelector:\n        \":has(.o_field_widget[name=move_type] span[raw-value=out_invoice]):has(.o_arrow_button_current[data-value=draft])\",\n    postedInvoiceSelector:\n        \":has(.o_field_widget[name=move_type] span[raw-value=out_invoice]):has(.o_arrow_button_current[data-value=posted])\",\n    goToAccountMenu(description=\"Open Invoicing Menu\") {\n        return stepUtils.goToAppSteps('account.menu_finance', description);\n    },\n    onboarding() {\n        return [];\n    },\n    newInvoice() {\n        return [\n            {\n                trigger: \"button.o_list_button_add\",\n                content: _t(\"Now, we'll create your first invoice\"),\n                run: \"click\",\n            },\n        ];\n    },\n    endSteps() {\n        return [{\n            isActive: [\"auto\"],\n            trigger: '.breadcrumb-item',\n            run: \"click\",\n        }];\n    },\n}\n\nregistry.category(\"web_tour.tours\").add('account_tour', {\n    url: \"/odoo\",\n    steps: () => [\n    ...accountTourSteps.goToAccountMenu(markup(_t('Send invoices to your customers in no time with the <b>Invoicing app</b>.'))),\n    ...accountTourSteps.onboarding(),\n    ...accountTourSteps.newInvoice(),\n    {\n        trigger: `.o_form_view_container${accountTourSteps.draftInvoiceSelector} div[name=partner_id] .o_input_dropdown`,\n        content: markup(_t(\"Write a customer name to <b>create one</b> or <b>see suggestions</b>.\")),\n        tooltipPosition: \"right\",\n        run: \"click\",\n    },\n    {\n        isActive: [\"auto\"],\n        trigger: `.o_form_view_container${accountTourSteps.draftInvoiceSelector} div[name=partner_id] input`,\n        run: \"edit Test Customer\",\n    },\n    {\n        isActive: [\"auto\"],\n        trigger: `body${accountTourSteps.draftInvoiceSelector} .o_m2o_dropdown_option a:contains('Create')`,\n        content: _t(\"Select first partner\"),\n        run: \"click\",\n    },\n    {\n        isActive: [\"auto\"],\n        trigger: `body${accountTourSteps.draftInvoiceSelector} .modal-content button.btn-primary`,\n        content: markup(_t(\"Once everything is set, you are good to continue. You will be able to edit this later in the <b>Customers</b> menu.\")),\n        run: \"click\",\n    },\n    {\n        trigger: `.o_form_view_container${accountTourSteps.draftInvoiceSelector} div[name=invoice_line_ids] .o_field_x2many_list_row_add a`,\n        content: _t(\"Add a line to your invoice\"),\n        run: \"click\",\n    },\n    {\n        trigger: `.o_form_view_container${accountTourSteps.draftInvoiceSelector} div[name=invoice_line_ids] div[name=product_id]`,\n        content: _t(\"Fill in the details of the product or see the suggestion.\"),\n        tooltipPosition: \"bottom\",\n        run: \"click\",\n    },\n    {\n        isActive: [\"auto\"],\n        trigger: `.o_form_view_container${accountTourSteps.draftInvoiceSelector} div[name=invoice_line_ids] div[name=product_id] input`,\n        run: \"edit Test Product\",\n    },\n    {\n        isActive: [\"auto\"],\n        trigger: `.o_form_view_container${accountTourSteps.draftInvoiceSelector} div[name=invoice_line_ids] div[name=product_id] .o_m2o_dropdown_option_create a:contains(create)`,\n        content: _t(\"Create the product.\"),\n        run: \"click\",\n    },\n    {\n        trigger: `.o_form_view_container${accountTourSteps.draftInvoiceSelector} div[name=invoice_line_ids] div[name=product_id] button[id=labelVisibilityButtonId]`,\n        content: _t(\"Click here to add a description to your product.\"),\n        tooltipPosition: \"bottom\",\n        run: \"click\",\n    },\n    {\n        trigger: `.o_form_view_container${accountTourSteps.draftInvoiceSelector} div[name=invoice_line_ids] div[name=product_id] textarea`,\n        content: _t(\"Add a description to your item.\"),\n        tooltipPosition: \"bottom\",\n        run: \"edit A very useful description.\",\n    },\n    {\n        isActive: [\"auto\"],\n        trigger: `.o_form_view_container${accountTourSteps.draftInvoiceSelector} div[name=invoice_line_ids] div[name=product_id] textarea`,\n        run: function () {\n            // Since the t-on-change of the input is not triggered by the run: \"edit\" action,\n            // we need to dispatch the event manually requiring a function.\n            const input = this.anchor;\n            input.dispatchEvent(new InputEvent(\"input\"));\n            input.dispatchEvent(new Event(\"change\"));\n        },\n    },\n    {\n        trigger: `.o_form_view_container${accountTourSteps.draftInvoiceSelector} div[name=invoice_line_ids] td[name=price_unit]`,\n        content: _t(\"Verify the price and update if necessary.\"),\n        tooltipPosition: \"bottom\",\n        run: \"click\",\n    },\n    {\n        isActive: [\"auto\"],\n        trigger: `.o_form_view_container${accountTourSteps.draftInvoiceSelector} div[name=invoice_line_ids] div[name=price_unit] input`,\n        content: _t(\"Set a price.\"),\n        run: \"edit 100\",\n    },\n    ...stepUtils.saveForm(),\n    {\n        trigger: `.o_form_view_container${accountTourSteps.draftInvoiceSelector} button[name=action_post]`,\n        content: _t(\"Once your invoice is ready, confirm it.\"),\n        run: \"click\",\n    },\n    {\n        trigger: `.o_form_view_container${accountTourSteps.postedInvoiceSelector} button[name=action_invoice_sent]:contains(send)`,\n        content: _t(\"Send the invoice to the customer and check what he'll receive.\"),\n        tooltipPosition: \"bottom\",\n        run: \"click\",\n    },\n    {\n        // RecipientsInputTagsListPopover will not display if the customer already has an email address\n        // unless it's possible to have optional steps, we will only use it for tests at the moment.\n        isActive: [\"auto\"],\n        trigger: `body${accountTourSteps.postedInvoiceSelector} .o-mail-RecipientsInputTagsListPopover input`,\n        content: markup(_t(\"Write here <b>your own email address</b> to test the flow.\")),\n        run: \"edit customer@example.com\",\n    },\n    {\n        isActive: [\"auto\"],\n        trigger: `body${accountTourSteps.postedInvoiceSelector} .o-mail-RecipientsInputTagsListPopover .btn-primary`,\n        content: _t(\"Validate.\"),\n        run: \"click\",\n    },\n    {\n        trigger: `body${accountTourSteps.postedInvoiceSelector} .modal button[name=action_send_and_print]`,\n        content: _t(\"Let's send the invoice.\"),\n        tooltipPosition: \"top\",\n        run: \"click\",\n    },\n    ...accountTourSteps.endSteps(),\n]});\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { patch } from \"@web/core/utils/patch\";\nimport { SearchBar } from \"@web/search/search_bar/search_bar\";\n\npatch(SearchBar.prototype, {\n    getPreposition(searchItem) {\n        let preposition = super.getPreposition(searchItem);\n        if (\n            this.fields[searchItem.fieldName].name === 'payment_date'\n            || this.fields[searchItem.fieldName].name === 'next_payment_date'\n        ) {\n            preposition = _t(\"until\");\n        }\n        return preposition\n    }\n});\n", "import { floatIsZero, roundPrecision } from \"@web/core/utils/numbers\";\nimport { _t } from \"@web/core/l10n/translation\";\n\nexport const accountTaxHelpers = {\n    // -------------------------------------------------------------------------\n    // HELPERS IN BOTH PYTHON/JAVASCRIPT (account_tax.js / account_tax.py)\n    // -------------------------------------------------------------------------\n\n    /**\n     * Helper to stringify a grouping key that could contains some records.\n     *\n     * [!] Only added javascript-side.\n     */\n    stringify_grouping_key(grouping_key) {\n        if (!grouping_key || typeof grouping_key !== \"object\") {\n            return grouping_key;\n        }\n\n        if (\"id\" in grouping_key) {\n            return grouping_key.id;\n        }\n\n        const serializable_grouping_key = { ...grouping_key };\n        for (const [key, value] of Object.entries(grouping_key)) {\n            if (value && typeof value === \"object\" && \"id\" in value) {\n                serializable_grouping_key[key] = value.id;\n            }\n        }\n        return JSON.stringify(serializable_grouping_key);\n    },\n\n    // -------------------------------------------------------------------------\n    // PREPARE TAXES COMPUTATION\n    // -------------------------------------------------------------------------\n\n    /**\n     * [!] Mirror of the same method in account_tax.py.\n     * PLZ KEEP BOTH METHODS CONSISTENT WITH EACH OTHERS.\n     */\n    flatten_taxes_and_sort_them(taxes) {\n        function sort_key(taxes) {\n            return taxes.toSorted((t1, t2) => t1.sequence - t2.sequence || t1.id - t2.id);\n        }\n\n        const group_per_tax = {};\n        const sorted_taxes = [];\n        for (const tax of sort_key(taxes)) {\n            if (tax.amount_type === \"group\") {\n                const children = sort_key(tax.children_tax_ids);\n                for (const child of children) {\n                    group_per_tax[child.id] = tax;\n                    sorted_taxes.push(child);\n                }\n            } else {\n                sorted_taxes.push(tax);\n            }\n        }\n        return { sorted_taxes, group_per_tax };\n    },\n\n    /**\n     * [!] Mirror of the same method in account_tax.py.\n     * PLZ KEEP BOTH METHODS CONSISTENT WITH EACH OTHERS.\n     */\n    batch_for_taxes_computation(taxes, { special_mode = null, filter_tax_function = null } = {}) {\n        let { sorted_taxes, group_per_tax } = this.flatten_taxes_and_sort_them(taxes);\n        if (filter_tax_function) {\n            sorted_taxes = sorted_taxes.filter(filter_tax_function);\n        }\n\n        const results = {\n            batch_per_tax: {},\n            group_per_tax: group_per_tax,\n            sorted_taxes: sorted_taxes,\n        };\n\n        // Group them per batch.\n        let batch = [];\n        let is_base_affected = false;\n        for (const tax of results.sorted_taxes.toReversed()) {\n            if (batch.length > 0) {\n                const same_batch =\n                    tax.amount_type === batch[0].amount_type &&\n                    (special_mode || tax.price_include === batch[0].price_include) &&\n                    tax.include_base_amount === batch[0].include_base_amount &&\n                    ((tax.include_base_amount && !is_base_affected) || !tax.include_base_amount);\n                if (!same_batch) {\n                    for (const batch_tax of batch) {\n                        results.batch_per_tax[batch_tax.id] = batch;\n                    }\n                    batch = [];\n                }\n            }\n\n            is_base_affected = tax.is_base_affected;\n            batch.push(tax);\n        }\n\n        if (batch.length !== 0) {\n            for (const batch_tax of batch) {\n                results.batch_per_tax[batch_tax.id] = batch;\n            }\n        }\n        return results;\n    },\n\n    /**\n     * [!] Mirror of the same method in account_tax.py.\n     * PLZ KEEP BOTH METHODS CONSISTENT WITH EACH OTHERS.\n     */\n    propagate_extra_taxes_base(taxes, tax, taxes_data, { special_mode = null } = {}) {\n        function* get_tax_before() {\n            for (const tax_before of taxes) {\n                if (taxes_data[tax.id].batch.includes(tax_before)) {\n                    break;\n                }\n                yield tax_before;\n            }\n        }\n\n        function* get_tax_after() {\n            for (const tax_after of taxes.toReversed()) {\n                if (taxes_data[tax.id].batch.includes(tax_after)) {\n                    break;\n                }\n                yield tax_after;\n            }\n        }\n\n        function add_extra_base(other_tax, sign) {\n            const tax_amount = taxes_data[tax.id].tax_amount;\n            if (!(\"tax_amount\" in taxes_data[other_tax.id])) {\n                taxes_data[other_tax.id].extra_base_for_tax += sign * tax_amount;\n            }\n            taxes_data[other_tax.id].extra_base_for_base += sign * tax_amount;\n        }\n\n        if (tax.price_include) {\n            // Case: special mode is False or 'total_included'\n            if (!special_mode || special_mode === \"total_included\") {\n                if (tax.include_base_amount) {\n                    for (const other_tax of get_tax_after()) {\n                        if (!other_tax.is_base_affected) {\n                            add_extra_base(other_tax, -1);\n                        }\n                    }\n                } else {\n                    for (const other_tax of get_tax_after()) {\n                        add_extra_base(other_tax, -1);\n                    }\n                }\n                for (const other_tax of get_tax_before()) {\n                    add_extra_base(other_tax, -1);\n                }\n\n                // Case: special_mode = 'total_excluded'\n            } else {\n                if (tax.include_base_amount) {\n                    for (const other_tax of get_tax_after()) {\n                        if (other_tax.is_base_affected) {\n                            add_extra_base(other_tax, 1);\n                        }\n                    }\n                }\n            }\n        } else if (!tax.price_include) {\n            // Case: special_mode is False or 'total_excluded'\n            if (!special_mode || special_mode === \"total_excluded\") {\n                if (tax.include_base_amount) {\n                    for (const other_tax of get_tax_after()) {\n                        if (other_tax.is_base_affected) {\n                            add_extra_base(other_tax, 1);\n                        }\n                    }\n                }\n\n                // Case: special_mode = 'total_included'\n            } else {\n                if (!tax.include_base_amount) {\n                    for (const other_tax of get_tax_after()) {\n                        add_extra_base(other_tax, -1);\n                    }\n                }\n                for (const other_tax of get_tax_before()) {\n                    add_extra_base(other_tax, -1);\n                }\n            }\n        }\n    },\n\n    /**\n     * [!] Mirror of the same method in account_tax.py.\n     * PLZ KEEP BOTH METHODS CONSISTENT WITH EACH OTHERS.\n     */\n    eval_tax_amount_fixed_amount(tax, batch, raw_base, evaluation_context) {\n        if (tax.amount_type === \"fixed\") {\n            const sign = evaluation_context.price_unit < 0.0 ? -1 : 1;\n            return sign * evaluation_context.quantity * tax.amount;\n        }\n        return null;\n    },\n\n    /**\n     * [!] Mirror of the same method in account_tax.py.\n     * PLZ KEEP BOTH METHODS CONSISTENT WITH EACH OTHERS.\n     */\n    eval_tax_amount_price_included(tax, batch, raw_base, evaluation_context) {\n        if (tax.amount_type === \"percent\") {\n            const total_percentage =\n                batch.reduce((sum, batch_tax) => sum + batch_tax.amount, 0) / 100.0;\n            const to_price_excluded_factor =\n                total_percentage !== -1 ? 1 / (1 + total_percentage) : 0.0;\n            return (raw_base * to_price_excluded_factor * tax.amount) / 100.0;\n        }\n\n        if (tax.amount_type === \"division\") {\n            return (raw_base * tax.amount) / 100.0;\n        }\n        return null;\n    },\n\n    /**\n     * [!] Mirror of the same method in account_tax.py.\n     * PLZ KEEP BOTH METHODS CONSISTENT WITH EACH OTHERS.\n     */\n    eval_tax_amount_price_excluded(tax, batch, raw_base, evaluation_context) {\n        if (tax.amount_type === \"percent\") {\n            return (raw_base * tax.amount) / 100.0;\n        }\n\n        if (tax.amount_type === \"division\") {\n            const total_percentage =\n                batch.reduce((sum, batch_tax) => sum + batch_tax.amount, 0) / 100.0;\n            const incl_base_multiplicator = total_percentage === 1.0 ? 1.0 : 1 - total_percentage;\n            return (raw_base * tax.amount) / 100.0 / incl_base_multiplicator;\n        }\n        return null;\n    },\n\n    /**\n     * [!] Mirror of the same method in account_tax.py.\n     * PLZ KEEP BOTH METHODS CONSISTENT WITH EACH OTHERS.\n     */\n    get_tax_details(\n        taxes,\n        price_unit,\n        quantity,\n        {\n            precision_rounding = null,\n            rounding_method = \"round_per_line\",\n            // When product is null, we need the product default values to make the \"formula\" taxes\n            // working. In that case, we need to deal with the product default values before calling this\n            // method because we have no way to deal with it automatically in this method since it depends of\n            // the type of involved fields and we don't have access to this information js-side.\n            product = null,\n            product_uom = null,\n            special_mode = null,\n            manual_tax_amounts = null, // TO BE REMOVED IN MASTER\n            filter_tax_function = null,\n        } = {}\n    ) {\n        const self = this;\n\n        function add_tax_amount_to_results(tax, tax_amount) {\n            taxes_data[tax.id].tax_amount = tax_amount;\n            if (rounding_method === \"round_per_line\") {\n                taxes_data[tax.id].tax_amount = roundPrecision(\n                    taxes_data[tax.id].tax_amount,\n                    precision_rounding\n                );\n            }\n            if (tax.has_negative_factor) {\n                reverse_charge_taxes_data[tax.id].tax_amount = -taxes_data[tax.id].tax_amount;\n            }\n\n            self.propagate_extra_taxes_base(sorted_taxes, tax, taxes_data, {\n                special_mode: special_mode,\n            });\n        }\n\n        function eval_tax_amount(tax_amount_function, tax) {\n            const is_already_computed = \"tax_amount\" in taxes_data[tax.id];\n            if (is_already_computed) {\n                return;\n            }\n\n            const tax_amount = tax_amount_function(\n                tax,\n                taxes_data[tax.id].batch,\n                raw_base + taxes_data[tax.id].extra_base_for_tax,\n                evaluation_context\n            );\n            if (tax_amount !== null) {\n                add_tax_amount_to_results(tax, tax_amount);\n            }\n        }\n\n        // Flatten the taxes, order them and filter them if necessary.\n\n        function prepare_tax_extra_data(tax, kwargs = {}) {\n            let price_include;\n            if (tax.has_negative_factor) {\n                price_include = false;\n            } else if (special_mode === \"total_included\") {\n                price_include = true;\n            } else if (special_mode === \"total_excluded\") {\n                price_include = false;\n            } else {\n                price_include = tax.price_include;\n            }\n            return {\n                ...kwargs,\n                tax: tax,\n                price_include: price_include,\n                extra_base_for_tax: 0.0,\n                extra_base_for_base: 0.0,\n            };\n        }\n\n        const batching_results = this.batch_for_taxes_computation(taxes, {\n            special_mode: special_mode,\n            filter_tax_function: filter_tax_function,\n        });\n        let sorted_taxes = batching_results.sorted_taxes;\n        const taxes_data = {};\n        const reverse_charge_taxes_data = {};\n        for (const tax of sorted_taxes) {\n            taxes_data[tax.id] = prepare_tax_extra_data(tax, {\n                group: batching_results.group_per_tax[tax.id],\n                batch: batching_results.batch_per_tax[tax.id],\n            });\n            if (tax.has_negative_factor) {\n                reverse_charge_taxes_data[tax.id] = {\n                    ...taxes_data[tax.id],\n                    is_reverse_charge: true,\n                };\n            }\n        }\n\n        let raw_base = quantity * price_unit;\n        if (rounding_method === \"round_per_line\") {\n            raw_base = roundPrecision(raw_base, precision_rounding);\n        }\n\n        let evaluation_context = {\n            product: product || {},\n            uom: product_uom || {},\n            price_unit: price_unit,\n            quantity: quantity,\n            raw_base: raw_base,\n            special_mode: special_mode,\n        };\n\n        // Define the order in which the taxes must be evaluated.\n        // Fixed taxes are computed directly because they could affect the base of a price included batch right after.\n        for (const tax of sorted_taxes.toReversed()) {\n            eval_tax_amount(this.eval_tax_amount_fixed_amount.bind(this), tax);\n        }\n\n        // Then, let's travel the batches in the reverse order and process the price-included taxes.\n        for (const tax of sorted_taxes.toReversed()) {\n            if (taxes_data[tax.id].price_include) {\n                eval_tax_amount(this.eval_tax_amount_price_included.bind(this), tax);\n            }\n        }\n\n        // Then, let's travel the batches in the normal order and process the price-excluded taxes.\n        for (const tax of sorted_taxes) {\n            if (!taxes_data[tax.id].price_include) {\n                eval_tax_amount(this.eval_tax_amount_price_excluded.bind(this), tax);\n            }\n        }\n\n        // Mark the base to be computed in the descending order. The order doesn't matter for no special mode or 'total_excluded' but\n        // it must be in the reverse order when special_mode is 'total_included'.\n        const subsequent_taxes = [];\n        for (const tax of sorted_taxes.toReversed()) {\n            const tax_data = taxes_data[tax.id];\n            if (!(\"tax_amount\" in tax_data)) {\n                continue;\n            }\n\n            // Base amount.\n            const total_tax_amount =\n                taxes_data[tax.id].batch.reduce(\n                    (sum, other_tax) => sum + taxes_data[other_tax.id].tax_amount,\n                    0\n                ) +\n                Object.values(taxes_data[tax.id].batch)\n                    .filter((other_tax) => other_tax.has_negative_factor)\n                    .reduce(\n                        (sum, other_tax) =>\n                            sum + reverse_charge_taxes_data[other_tax.id].tax_amount,\n                        0\n                    );\n            let base = raw_base + taxes_data[tax.id].extra_base_for_base;\n            if (tax_data.price_include && (!special_mode || special_mode === \"total_included\")) {\n                base -= total_tax_amount;\n            }\n            tax_data.base = base;\n\n            // Subsequence taxes.\n            tax_data.taxes = [];\n            if (tax.include_base_amount) {\n                tax_data.taxes.push(...subsequent_taxes);\n            }\n\n            // Reverse charge.\n            if (tax.has_negative_factor) {\n                const reverse_charge_tax_data = reverse_charge_taxes_data[tax.id];\n                reverse_charge_tax_data.base = base;\n                reverse_charge_tax_data.taxes = tax_data.taxes;\n            }\n\n            if (tax.is_base_affected) {\n                subsequent_taxes.push(tax);\n            }\n        }\n\n        const taxes_data_list = [];\n        for (const tax of sorted_taxes) {\n            const tax_data = taxes_data[tax.id];\n            if (\"tax_amount\" in tax_data) {\n                taxes_data_list.push(tax_data);\n                if (tax.has_negative_factor) {\n                    taxes_data_list.push(reverse_charge_taxes_data[tax.id]);\n                }\n            }\n        }\n\n        let total_excluded, total_included;\n        if (taxes_data_list.length > 0) {\n            total_excluded = taxes_data_list[0].base;\n            const tax_amount = taxes_data_list.reduce(\n                (sum, tax_data) => sum + tax_data.tax_amount,\n                0\n            );\n            total_included = total_excluded + tax_amount;\n        } else {\n            total_excluded = total_included = raw_base;\n        }\n\n        return {\n            total_excluded: total_excluded,\n            total_included: total_included,\n            taxes_data: taxes_data_list.map((tax_data) =>\n                Object.assign(\n                    {},\n                    {\n                        tax: tax_data.tax,\n                        taxes: tax_data.taxes,\n                        group: batching_results.group_per_tax[tax_data.tax.id],\n                        batch: batching_results.batch_per_tax[tax_data.tax.id],\n                        tax_amount: tax_data.tax_amount,\n                        price_include: tax_data.price_include,\n                        base_amount: tax_data.base,\n                        is_reverse_charge: tax_data.is_reverse_charge || false,\n                    }\n                )\n            ),\n        };\n    },\n\n    // -------------------------------------------------------------------------\n    // MAPPING PRICE_UNIT\n    // -------------------------------------------------------------------------\n\n    /**\n     * [!] Mirror of the same method in account_tax.py.\n     * PLZ KEEP BOTH METHODS CONSISTENT WITH EACH OTHERS.\n     */\n    adapt_price_unit_to_another_taxes(price_unit, product, original_taxes, new_taxes, { product_uom = null } = {}) {\n        const original_tax_ids = new Set(original_taxes.map((x) => x.id));\n        const new_tax_ids = new Set(new_taxes.map((x) => x.id));\n        if (\n            (original_tax_ids.size === new_tax_ids.size &&\n                [...original_tax_ids].every((value) => new_tax_ids.has(value))) ||\n            original_taxes.some((x) => !x.price_include)\n        ) {\n            return price_unit;\n        }\n\n        // Find the price unit without tax.\n        let taxes_computation = this.get_tax_details(original_taxes, price_unit, 1.0, {\n            rounding_method: \"round_globally\",\n            product: product,\n            product_uom: product_uom,\n        });\n        price_unit = taxes_computation.total_excluded;\n\n        // Find the new price unit after applying the price included taxes.\n        taxes_computation = this.get_tax_details(new_taxes, price_unit, 1.0, {\n            rounding_method: \"round_globally\",\n            product: product,\n            product_uom: product_uom,\n            special_mode: \"total_excluded\",\n        });\n        let delta = 0.0;\n        for (const tax_data of taxes_computation.taxes_data) {\n            if (tax_data.tax.price_include) {\n                delta += tax_data.tax_amount;\n            }\n        }\n        return price_unit + delta;\n    },\n\n    // -------------------------------------------------------------------------\n    // GENERIC REPRESENTATION OF BUSINESS OBJECTS & METHODS\n    // -------------------------------------------------------------------------\n\n    /**\n     * [!] Mirror of the same method in account_tax.py.\n     * PLZ KEEP BOTH METHODS CONSISTENT WITH EACH OTHERS.\n     */\n    export_base_line_extra_tax_data(base_line) {\n        const results = {};\n        if (base_line.computation_key) {\n            results.computation_key = base_line.computation_key;\n        }\n\n        let store_source_data = false;\n        if (base_line.manual_total_excluded_currency !== null) {\n            results.manual_total_excluded_currency = base_line.manual_total_excluded_currency;\n            store_source_data = true;\n        }\n        if (base_line.manual_total_excluded !== null) {\n            results.manual_total_excluded = base_line.manual_total_excluded;\n            store_source_data = true;\n        }\n        if (base_line.manual_tax_amounts && Object.keys(base_line.manual_tax_amounts).length > 0) {\n            results.manual_tax_amounts = base_line.manual_tax_amounts;\n            store_source_data = true;\n        }\n\n        if (store_source_data) {\n            Object.assign(results, {\n                currency_id: base_line.currency_id.id,\n                price_unit: base_line.price_unit,\n                discount: base_line.discount,\n                quantity: base_line.quantity,\n                rate: base_line.rate,\n            });\n        }\n        return results;\n    },\n\n    /**\n     * [!] Mirror of the same method in account_tax.py.\n     * PLZ KEEP BOTH METHODS CONSISTENT WITH EACH OTHERS.\n     */\n    import_base_line_extra_tax_data(base_line, extra_tax_data) {\n        const currency_dp = base_line.currency_id.decimal_places;\n\n        // compare_amounts does not exist in numbers.js\n        const are_amounts_equal = (a, b) =>\n            floatIsZero(\n                roundPrecision(a, currency_dp) - roundPrecision(b, currency_dp),\n                currency_dp\n            );\n\n        const results = {};\n\n        if (extra_tax_data && extra_tax_data.computation_key) {\n            results.computation_key = extra_tax_data.computation_key;\n        }\n\n        const manual_tax_amounts = extra_tax_data ? extra_tax_data.manual_tax_amounts || {} : null;\n        const extra_tax_data_tax_ids = new Set(Object.keys(manual_tax_amounts || {}));\n        const { sorted_taxes } = this.flatten_taxes_and_sort_them(base_line.tax_ids);\n        if (\n            extra_tax_data &&\n            extra_tax_data.currency_id &&\n            base_line.currency_id.id === extra_tax_data.currency_id &&\n            are_amounts_equal(base_line.price_unit, extra_tax_data.price_unit) &&\n            are_amounts_equal(base_line.discount, extra_tax_data.discount) &&\n            are_amounts_equal(base_line.quantity, extra_tax_data.quantity) &&\n            sorted_taxes.length === extra_tax_data_tax_ids.size &&\n            sorted_taxes\n                .map((tax) => tax.id.toString())\n                .every((tax_id_str) => extra_tax_data_tax_ids.has(tax_id_str))\n        ) {\n            results.price_unit = extra_tax_data.price_unit;\n\n            let delta_rate;\n            if (base_line.rate && extra_tax_data.rate) {\n                delta_rate = base_line.rate / extra_tax_data.rate;\n            } else {\n                delta_rate = 1.0;\n            }\n\n            if (\"manual_total_excluded_currency\" in extra_tax_data) {\n                results.manual_total_excluded_currency =\n                    extra_tax_data.manual_total_excluded_currency;\n            }\n            if (\"manual_total_excluded\" in extra_tax_data) {\n                results.manual_total_excluded = extra_tax_data.manual_total_excluded / delta_rate;\n            }\n\n            if (manual_tax_amounts) {\n                results.manual_tax_amounts = {};\n                for (const [tax_id_str, amounts] of Object.entries(\n                    extra_tax_data.manual_tax_amounts\n                )) {\n                    results.manual_tax_amounts[tax_id_str] = { ...amounts };\n                    if (\"tax_amount\" in amounts) {\n                        results.manual_tax_amounts[tax_id_str].tax_amount /= delta_rate;\n                    }\n                    if (\"base_amount\" in amounts) {\n                        results.manual_tax_amounts[tax_id_str].base_amount /= delta_rate;\n                    }\n                }\n            }\n        }\n        return results;\n    },\n\n    /**\n     * [!] Mirror of the same method in account_tax.py.\n     * PLZ KEEP BOTH METHODS CONSISTENT WITH EACH OTHERS.\n     */\n    get_base_line_field_value_from_record(record, field, extra_values, fallback) {\n        if (field in extra_values) {\n            return extra_values[field] || fallback;\n        }\n        if (record && field in record) {\n            return record[field] || fallback;\n        }\n        return fallback;\n    },\n\n    /**\n     * [!] Mirror of the same method in account_tax.py.\n     * PLZ KEEP BOTH METHODS CONSISTENT WITH EACH OTHERS.\n     */\n    prepare_base_line_for_taxes_computation(record, kwargs = {}) {\n        const load = (field, fallback) =>\n            this.get_base_line_field_value_from_record(record, field, kwargs, fallback);\n        const currency =\n            load(\"currency_id\", null) ||\n            load(\"company_currency_id\", null) ||\n            load(\"company_id\", {}).currency_id ||\n            {};\n\n        const base_line = {\n            ...kwargs,\n            record: record,\n            id: load(\"id\", 0),\n            product_id: load(\"product_id\", {}),\n            product_uom_id: load(\"product_uom_id\", {}),\n            tax_ids: load(\"tax_ids\", []),\n            price_unit: load(\"price_unit\", 0.0),\n            quantity: load(\"quantity\", 0.0),\n            discount: load(\"discount\", 0.0),\n            currency_id: currency,\n            sign: load(\"sign\", 1.0),\n            special_mode: kwargs.special_mode || null,\n            special_type: kwargs.special_type || null,\n            rate: load(\"rate\", 1.0),\n            filter_tax_function: kwargs.filter_tax_function || null,\n        };\n\n        const extra_tax_data = this.import_base_line_extra_tax_data(\n            base_line,\n            load(\"extra_tax_data\", {}) || {}\n        );\n        Object.assign(base_line, {\n            manual_total_excluded_currency:\n                kwargs.manual_total_excluded_currency ||\n                extra_tax_data.manual_total_excluded_currency ||\n                null,\n            manual_total_excluded:\n                kwargs.manual_total_excluded || extra_tax_data.manual_total_excluded || null,\n            computation_key: kwargs.computation_key || extra_tax_data.computation_key || null,\n            manual_tax_amounts:\n                kwargs.manual_tax_amounts || extra_tax_data.manual_tax_amounts || null,\n        });\n        if (\"price_unit\" in extra_tax_data) {\n            base_line.price_unit = extra_tax_data.price_unit;\n        }\n\n        // Propagate custom values.\n        if (record && typeof record === \"object\") {\n            for (const [k, v] of Object.entries(record)) {\n                if (k && typeof k === \"string\" && k.startsWith(\"_\") && !(k in base_line)) {\n                    base_line[k] = v;\n                }\n            }\n        }\n\n        return base_line;\n    },\n\n    /**\n     * [!] Mirror of the same method in account_tax.py.\n     * PLZ KEEP BOTH METHODS CONSISTENT WITH EACH OTHERS.\n     */\n    add_tax_details_in_base_line(base_line, company, { rounding_method = null } = {}) {\n        rounding_method = rounding_method || company.tax_calculation_rounding_method;\n        const price_unit_after_discount = base_line.price_unit * (1 - base_line.discount / 100.0);\n        const currency_pd = base_line.currency_id.rounding;\n        const company_currency_pd = company.currency_id.rounding;\n        const taxes_computation = this.get_tax_details(\n            base_line.tax_ids,\n            price_unit_after_discount,\n            base_line.quantity,\n            {\n                precision_rounding: currency_pd,\n                rounding_method: rounding_method,\n                product: base_line.product_id,\n                product_uom: base_line.product_uom_id,\n                special_mode: base_line.special_mode,\n                filter_tax_function: base_line.filter_tax_function,\n            }\n        );\n\n        const rate = base_line.rate;\n        const tax_details = (base_line.tax_details = {\n            raw_total_excluded_currency: taxes_computation.total_excluded,\n            raw_total_excluded: rate ? taxes_computation.total_excluded / rate : 0.0,\n            raw_total_included_currency: taxes_computation.total_included,\n            raw_total_included: rate ? taxes_computation.total_included / rate : 0.0,\n            taxes_data: [],\n        });\n\n        if (rounding_method === \"round_per_line\") {\n            tax_details.raw_total_excluded = roundPrecision(\n                tax_details.raw_total_excluded,\n                currency_pd\n            );\n            tax_details.raw_total_included = roundPrecision(\n                tax_details.raw_total_included,\n                currency_pd\n            );\n        }\n\n        for (const tax_data of taxes_computation.taxes_data) {\n            let tax_amount = rate ? tax_data.tax_amount / rate : 0.0;\n            let base_amount = rate ? tax_data.base_amount / rate : 0.0;\n\n            if (rounding_method === \"round_per_line\") {\n                tax_amount = roundPrecision(tax_amount, company_currency_pd);\n                base_amount = roundPrecision(base_amount, company_currency_pd);\n            }\n\n            tax_details.taxes_data.push({\n                ...tax_data,\n                raw_tax_amount_currency: tax_data.tax_amount,\n                raw_tax_amount: tax_amount,\n                raw_base_amount_currency: tax_data.base_amount,\n                raw_base_amount: base_amount,\n            });\n        }\n    },\n\n    /**\n     * [!] Mirror of the same method in account_tax.py.\n     * PLZ KEEP BOTH METHODS CONSISTENT WITH EACH OTHERS.\n     */\n    add_tax_details_in_base_lines(base_lines, company) {\n        for (const base_line of base_lines) {\n            this.add_tax_details_in_base_line(base_line, company);\n        }\n    },\n\n    /**\n     * [!] Mirror of the same method in account_tax.py.\n     * PLZ KEEP BOTH METHODS CONSISTENT WITH EACH OTHERS.\n     */\n    normalize_target_factors(target_factors) {\n        const factors = target_factors.map((x, i) => [i, Math.abs(x.factor)]);\n        factors.sort((a, b) => b[1] - a[1]);\n        const sum_of_factors = factors.reduce((sum, x) => sum + x[1], 0.0);\n        return factors.map((x) => [x[0], sum_of_factors ? x[1] / sum_of_factors : 0.0]);\n    },\n\n    /**\n     * [!] Mirror of the same method in account_tax.py.\n     * PLZ KEEP BOTH METHODS CONSISTENT WITH EACH OTHERS.\n     */\n    distribute_delta_amount_smoothly(precision_digits, delta_amount, target_factors) {\n        const precision_rounding = Number(`1e-${precision_digits}`);\n        const amounts_to_distribute = target_factors.map((x) => 0.0);\n        if (floatIsZero(delta_amount, precision_digits)) {\n            return amounts_to_distribute;\n        }\n\n        const sign = delta_amount < 0.0 ? -1 : 1;\n        const nb_of_errors = Math.round(Math.abs(delta_amount / precision_rounding));\n        let remaining_errors = nb_of_errors;\n\n        // Distribute using the factor first.\n        const factors = this.normalize_target_factors(target_factors);\n        for (const [i, factor] of factors) {\n            if (!remaining_errors) {\n                break;\n            }\n\n            const nb_of_amount_to_distribute = Math.min(\n                Math.round(factor * nb_of_errors),\n                remaining_errors\n            );\n            remaining_errors -= nb_of_amount_to_distribute;\n            const amount_to_distribute = sign * nb_of_amount_to_distribute * precision_rounding;\n            amounts_to_distribute[i] += amount_to_distribute;\n        }\n\n        // Distribute the remaining cents across the factors.\n        // There are sorted by the biggest first.\n        // Since the factors are normalized, the residual number of cents can't be higher than the number of factors.\n        for (let i = 0; i < remaining_errors; i++) {\n            amounts_to_distribute[factors[i][0]] += sign * precision_rounding;\n        }\n\n        return amounts_to_distribute;\n    },\n\n    /**\n     * [!] Mirror of the same method in account_tax.py.\n     * PLZ KEEP BOTH METHODS CONSISTENT WITH EACH OTHERS.\n     */\n    round_tax_details_tax_amounts(base_lines, company, { mode = \"mixed\" } = {}) {\n        function grouping_function(base_line, tax_data) {\n            if (!tax_data) {\n                return;\n            }\n            return {\n                is_refund: base_line.is_refund,\n                is_reverse_charge: tax_data.is_reverse_charge,\n                price_include: tax_data.price_include,\n                computation_key: base_line.computation_key,\n                tax: tax_data.tax,\n                currency: base_line.currency_id,\n            };\n        }\n\n        const base_lines_aggregated_values = this.aggregate_base_lines_tax_details(\n            base_lines,\n            grouping_function\n        );\n        const values_per_grouping_key = this.aggregate_base_lines_aggregated_values(\n            base_lines_aggregated_values\n        );\n        for (const values of Object.values(values_per_grouping_key)) {\n            const grouping_key = values.grouping_key;\n            if (!grouping_key) {\n                continue;\n            }\n\n            const price_include = grouping_key.price_include;\n            const currency = grouping_key.currency;\n            for (const [delta_currency_indicator, delta_currency] of [\n                [\"_currency\", currency],\n                [\"\", company.currency_id],\n            ]) {\n                // Tax amount\n                const raw_total_tax_amount = values[`target_tax_amount${delta_currency_indicator}`];\n                const rounded_raw_total_tax_amount = roundPrecision(\n                    raw_total_tax_amount,\n                    delta_currency.rounding\n                );\n                const total_tax_amount = values[`tax_amount${delta_currency_indicator}`];\n                const delta_total_tax_amount = rounded_raw_total_tax_amount - total_tax_amount;\n\n                if (raw_total_tax_amount) {\n                    const target_factors = values.base_line_x_taxes_data.flatMap(\n                        ([_, taxes_data]) =>\n                            taxes_data.map((tax_data) => ({\n                                factor: tax_data[`raw_tax_amount${delta_currency_indicator}`],\n                                tax_data: tax_data,\n                            }))\n                    );\n\n                    const amounts_to_distribute = this.distribute_delta_amount_smoothly(\n                        delta_currency.decimal_places,\n                        delta_total_tax_amount,\n                        target_factors\n                    );\n\n                    for (let i = 0; i < target_factors.length; i++) {\n                        const tax_data = target_factors[i].tax_data;\n                        const amount_to_distribute = amounts_to_distribute[i];\n                        tax_data[`tax_amount${delta_currency_indicator}`] += amount_to_distribute;\n                    }\n                }\n\n                // Base amount\n                const raw_total_base_amount =\n                    values[`target_base_amount${delta_currency_indicator}`];\n                let delta_total_base_amount = 0.0;\n\n                if ((mode === \"mixed\" && price_include) || mode === \"included\") {\n                    const raw_total_amount = raw_total_base_amount + raw_total_tax_amount;\n                    const rounded_raw_total_amount = roundPrecision(\n                        raw_total_amount,\n                        delta_currency.rounding\n                    );\n                    const total_amount =\n                        values[`base_amount${delta_currency_indicator}`] +\n                        total_tax_amount +\n                        delta_total_tax_amount;\n                    delta_total_base_amount = rounded_raw_total_amount - total_amount;\n                } else if ((mode === \"mixed\" && !price_include) || mode === \"excluded\") {\n                    const rounded_raw_total_base_amount = roundPrecision(\n                        raw_total_base_amount,\n                        delta_currency.rounding\n                    );\n                    const total_base_amount = values[`base_amount${delta_currency_indicator}`];\n                    delta_total_base_amount = rounded_raw_total_base_amount - total_base_amount;\n                }\n\n                if (raw_total_base_amount) {\n                    const target_factors = values.base_line_x_taxes_data.flatMap(\n                        ([_, taxes_data]) =>\n                            taxes_data.map((tax_data) => ({\n                                factor: tax_data[`raw_base_amount${delta_currency_indicator}`],\n                                tax_data: tax_data,\n                            }))\n                    );\n\n                    const amounts_to_distribute = this.distribute_delta_amount_smoothly(\n                        delta_currency.decimal_places,\n                        delta_total_base_amount,\n                        target_factors\n                    );\n\n                    for (let i = 0; i < target_factors.length; i++) {\n                        const tax_data = target_factors[i].tax_data;\n                        const amount_to_distribute = amounts_to_distribute[i];\n                        tax_data[`base_amount${delta_currency_indicator}`] += amount_to_distribute;\n                    }\n                }\n            }\n        }\n    },\n\n    /**\n     * [!] Mirror of the same method in account_tax.py.\n     * PLZ KEEP BOTH METHODS CONSISTENT WITH EACH OTHERS.\n     */\n    round_tax_details_base_lines(base_lines, company, { mode = \"mixed\" } = {}) {\n        function grouping_function(base_line, tax_data) {\n            return {\n                is_refund: base_line.is_refund,\n                currency: base_line.currency_id,\n                computation_key: base_line.computation_key,\n            };\n        }\n\n        const base_lines_aggregated_values = this.aggregate_base_lines_tax_details(\n            base_lines,\n            grouping_function\n        );\n        const values_per_grouping_key = this.aggregate_base_lines_aggregated_values(\n            base_lines_aggregated_values\n        );\n        for (const values of Object.values(values_per_grouping_key)) {\n            const grouping_key = values.grouping_key;\n            let current_mode = mode;\n            if (current_mode === \"mixed\") {\n                current_mode = \"included\";\n                for (const base_line_taxes_data of values.base_line_x_taxes_data) {\n                    const taxes_data = base_line_taxes_data[1];\n                    if (taxes_data.some((tax_data) => !tax_data.price_include)) {\n                        current_mode = \"excluded\";\n                        break;\n                    }\n                }\n            }\n\n            const currency = grouping_key.currency;\n            for (const [delta_currency_indicator, delta_currency] of [\n                [\"_currency\", currency],\n                [\"\", company.currency_id],\n            ]) {\n                let delta_total_excluded = 0.0;\n                let target_factors = [];\n                if (current_mode === \"excluded\") {\n                    // Price-excluded rounding.\n                    const raw_total_excluded =\n                        values[`target_total_excluded${delta_currency_indicator}`];\n                    if (!raw_total_excluded) {\n                        continue;\n                    }\n\n                    const rounded_raw_total_excluded = roundPrecision(\n                        raw_total_excluded,\n                        delta_currency.rounding\n                    );\n                    const total_excluded = values[`total_excluded${delta_currency_indicator}`];\n                    delta_total_excluded = rounded_raw_total_excluded - total_excluded;\n                    target_factors = values.base_line_x_taxes_data.map(([base_line]) => ({\n                        factor: base_line.tax_details[\n                            `raw_total_excluded${delta_currency_indicator}`\n                        ],\n                        base_line: base_line,\n                    }));\n                } else {\n                    // Price-included rounding.\n                    const raw_total_included =\n                        values[`target_total_excluded${delta_currency_indicator}`] +\n                        values[`target_tax_amount${delta_currency_indicator}`];\n                    if (!raw_total_included) {\n                        continue;\n                    }\n                    const rounded_raw_total_included = roundPrecision(\n                        raw_total_included,\n                        delta_currency.rounding\n                    );\n                    const total_included =\n                        values[`total_excluded${delta_currency_indicator}`] +\n                        values[`tax_amount${delta_currency_indicator}`];\n                    delta_total_excluded = rounded_raw_total_included - total_included;\n                    target_factors = values.base_line_x_taxes_data.map(([base_line]) => ({\n                        factor: base_line.tax_details[\n                            `raw_total_included${delta_currency_indicator}`\n                        ],\n                        base_line: base_line,\n                    }));\n                }\n\n                const amounts_to_distribute = this.distribute_delta_amount_smoothly(\n                    delta_currency.decimal_places,\n                    delta_total_excluded,\n                    target_factors\n                );\n                for (let i = 0; i < target_factors.length; i++) {\n                    const base_line = target_factors[i].base_line;\n                    const amount_to_distribute = amounts_to_distribute[i];\n                    base_line.tax_details[`delta_total_excluded${delta_currency_indicator}`] +=\n                        amount_to_distribute;\n                }\n            }\n        }\n    },\n\n    /**\n     * [!] Mirror of the same method in account_tax.py.\n     * PLZ KEEP BOTH METHODS CONSISTENT WITH EACH OTHERS.\n     */\n    round_base_lines_tax_details(base_lines, company) {\n        // Raw rounding.\n        for (const base_line of base_lines) {\n            const tax_details = base_line.tax_details;\n\n            for (const [suffix, currency] of [\n                [\"_currency\", base_line.currency_id],\n                [\"\", company.currency_id],\n            ]) {\n                const total_excluded_field = `total_excluded${suffix}`;\n                tax_details[total_excluded_field] = roundPrecision(\n                    tax_details[`raw_${total_excluded_field}`],\n                    currency.rounding\n                );\n\n                for (const tax_data of tax_details.taxes_data) {\n                    for (const prefix of [\"base\", \"tax\"]) {\n                        const field = `${prefix}_amount${suffix}`;\n                        tax_data[field] = roundPrecision(\n                            tax_data[`raw_${field}`],\n                            currency.rounding\n                        );\n                    }\n                }\n            }\n        }\n\n        // Apply 'manual_tax_amounts'.\n        for (const base_line of base_lines) {\n            const manual_tax_amounts = base_line.manual_tax_amounts;\n            const rate = base_line.rate;\n            const tax_details = base_line.tax_details;\n\n            for (const [suffix, currency] of [\n                [\"_currency\", base_line.currency_id],\n                [\"\", company.currency_id],\n            ]) {\n                const total_field = `total_excluded${suffix}`;\n                const manual_field = `manual_${total_field}`;\n                if (base_line[manual_field] !== null) {\n                    tax_details[total_field] = base_line[manual_field];\n                    if (suffix === \"_currency\" && rate) {\n                        tax_details.total_excluded = roundPrecision(\n                            tax_details[total_field] / rate,\n                            company.currency_id.rounding\n                        );\n                    }\n                }\n\n                for (const tax_data of tax_details.taxes_data) {\n                    const tax = tax_data.tax;\n                    const reverse_charge_sign = tax_data.is_reverse_charge ? -1 : 1;\n                    const current_manual_tax_amounts =\n                        (manual_tax_amounts && manual_tax_amounts[String(tax.id)]) || {};\n\n                    for (const [prefix, factor] of [\n                        [\"base\", 1],\n                        [\"tax\", reverse_charge_sign],\n                    ]) {\n                        const field = `${prefix}_amount${suffix}`;\n                        if (field in current_manual_tax_amounts) {\n                            tax_data[field] = roundPrecision(\n                                factor * current_manual_tax_amounts[field],\n                                currency.rounding\n                            );\n                            if (suffix === \"_currency\" && rate) {\n                                tax_data[`${prefix}_amount`] = roundPrecision(\n                                    tax_data[field] / rate,\n                                    company.currency_id.rounding\n                                );\n                            }\n                        }\n                    }\n                }\n            }\n        }\n\n        // Compute 'total_included' & add 'delta_total_excluded'.\n        for (const base_line of base_lines) {\n            const tax_details = base_line.tax_details;\n            for (const suffix of [\"_currency\", \"\"]) {\n                tax_details[`delta_total_excluded${suffix}`] = 0.0;\n                tax_details[`total_included${suffix}`] = tax_details[`total_excluded${suffix}`];\n                for (const tax_data of tax_details.taxes_data) {\n                    tax_details[`total_included${suffix}`] += tax_data[`tax_amount${suffix}`];\n                }\n            }\n        }\n\n        this.round_tax_details_tax_amounts(base_lines, company);\n        this.round_tax_details_base_lines(base_lines, company);\n    },\n\n    // -------------------------------------------------------------------------\n    // TAX TOTALS SUMMARY\n    // -------------------------------------------------------------------------\n\n    /**\n     * [!] Mirror of the same method in account_tax.py.\n     * PLZ KEEP BOTH METHODS CONSISTENT WITH EACH OTHERS.\n     */\n    get_tax_totals_summary(base_lines, currency, company, { cash_rounding = null } = {}) {\n        const company_pd = company.currency_id.rounding;\n        const tax_totals_summary = {\n            currency_id: currency.id,\n            currency_pd: currency.rounding,\n            company_currency_id: company.currency_id.id,\n            company_currency_pd: company.currency_id.rounding,\n            has_tax_groups: false,\n            subtotals: [],\n            base_amount_currency: 0.0,\n            base_amount: 0.0,\n            tax_amount_currency: 0.0,\n            tax_amount: 0.0,\n        };\n\n        // Global tax values.\n        const global_grouping_function = (base_line, tax_data) => tax_data !== null;\n\n        let base_lines_aggregated_values = this.aggregate_base_lines_tax_details(\n            base_lines,\n            global_grouping_function\n        );\n        let values_per_grouping_key = this.aggregate_base_lines_aggregated_values(\n            base_lines_aggregated_values\n        );\n\n        for (const values of Object.values(values_per_grouping_key)) {\n            if (values.grouping_key) {\n                tax_totals_summary.has_tax_groups = true;\n            }\n            tax_totals_summary.base_amount_currency += values.total_excluded_currency;\n            tax_totals_summary.base_amount += values.total_excluded;\n            tax_totals_summary.tax_amount_currency += values.tax_amount_currency;\n            tax_totals_summary.tax_amount += values.tax_amount;\n        }\n\n        // Tax groups.\n        const untaxed_amount_subtotal_label = _t(\"Untaxed Amount\");\n        const subtotals = {};\n\n        const tax_group_grouping_function = (base_line, tax_data) => {\n            if (!tax_data) {\n                return;\n            }\n            return tax_data.tax.tax_group_id;\n        };\n\n        base_lines_aggregated_values = this.aggregate_base_lines_tax_details(\n            base_lines,\n            tax_group_grouping_function\n        );\n        values_per_grouping_key = this.aggregate_base_lines_aggregated_values(\n            base_lines_aggregated_values\n        );\n\n        const sorted_total_per_tax_group = Object.values(values_per_grouping_key)\n            .filter((values) => values.grouping_key)\n            .sort(\n                (a, b) =>\n                    a.grouping_key.sequence - b.grouping_key.sequence ||\n                    a.grouping_key.id - b.grouping_key.id\n            );\n\n        const encountered_base_amounts = new Set();\n        const subtotals_order = {};\n\n        for (const [order, values] of sorted_total_per_tax_group.entries()) {\n            const tax_group = values.grouping_key;\n\n            // Get all involved taxes in the tax group.\n            const involved_tax_ids = new Set();\n            const involved_amount_types = new Set();\n            const involved_price_include = new Set();\n            values.base_line_x_taxes_data.forEach(([base_line, taxes_data]) => {\n                taxes_data.forEach((tax_data) => {\n                    const tax = tax_data.tax;\n                    involved_tax_ids.add(tax.id);\n                    involved_amount_types.add(tax.amount_type);\n                    involved_price_include.add(tax.price_include);\n                });\n            });\n\n            // Compute the display base amounts.\n            let display_base_amount;\n            let display_base_amount_currency;\n            if (involved_amount_types.size === 1 && involved_amount_types.has(\"fixed\")) {\n                display_base_amount = false;\n                display_base_amount_currency = false;\n            } else if (\n                involved_amount_types.size === 1 &&\n                involved_amount_types.has(\"division\") &&\n                involved_price_include.size === 1 &&\n                involved_price_include.has(true)\n            ) {\n                display_base_amount = 0.0;\n                display_base_amount_currency = 0.0;\n                values.base_line_x_taxes_data.forEach(([base_line, _taxes_data]) => {\n                    const tax_details = base_line.tax_details;\n                    display_base_amount +=\n                        tax_details.total_excluded + tax_details.delta_total_excluded;\n                    display_base_amount_currency +=\n                        tax_details.total_excluded_currency +\n                        tax_details.delta_total_excluded_currency;\n                    for (const tax_data of tax_details.taxes_data) {\n                        display_base_amount_currency += tax_data.tax_amount_currency;\n                        display_base_amount += tax_data.tax_amount;\n                    }\n                });\n            } else {\n                display_base_amount = values.base_amount;\n                display_base_amount_currency = values.base_amount_currency;\n            }\n\n            if (typeof display_base_amount_currency === \"number\") {\n                encountered_base_amounts.add(\n                    parseFloat(display_base_amount_currency.toFixed(currency.decimal_places))\n                );\n            }\n\n            // Order of the subtotals.\n            const preceding_subtotal =\n                tax_group.preceding_subtotal || untaxed_amount_subtotal_label;\n            if (!(preceding_subtotal in subtotals)) {\n                subtotals[preceding_subtotal] = {\n                    tax_groups: [],\n                    tax_amount_currency: 0.0,\n                    tax_amount: 0.0,\n                    base_amount_currency: 0.0,\n                    base_amount: 0.0,\n                };\n            }\n            if (!(preceding_subtotal in subtotals_order)) {\n                subtotals_order[preceding_subtotal] = order;\n            }\n\n            subtotals[preceding_subtotal].tax_groups.push({\n                id: tax_group.id,\n                involved_tax_ids: Array.from(involved_tax_ids),\n                tax_amount_currency: values.tax_amount_currency,\n                tax_amount: values.tax_amount,\n                base_amount_currency: values.base_amount_currency,\n                base_amount: values.base_amount,\n                display_base_amount_currency,\n                display_base_amount,\n                group_name: tax_group.name,\n                group_label: tax_group.pos_receipt_label,\n            });\n        }\n\n        // Subtotals.\n        if (!Object.keys(subtotals).length) {\n            subtotals[untaxed_amount_subtotal_label] = {\n                tax_groups: [],\n                tax_amount_currency: 0.0,\n                tax_amount: 0.0,\n                base_amount_currency: 0.0,\n                base_amount: 0.0,\n            };\n        }\n\n        const ordered_subtotals = Array.from(Object.entries(subtotals)).sort(\n            (a, b) => (subtotals_order[a[0]] || 0) - (subtotals_order[b[0]] || 0)\n        );\n        let accumulated_tax_amount_currency = 0.0;\n        let accumulated_tax_amount = 0.0;\n        for (const [subtotal_label, subtotal] of ordered_subtotals) {\n            subtotal.name = subtotal_label;\n            subtotal.base_amount_currency =\n                tax_totals_summary.base_amount_currency + accumulated_tax_amount_currency;\n            subtotal.base_amount = tax_totals_summary.base_amount + accumulated_tax_amount;\n            for (const tax_group of subtotal.tax_groups) {\n                subtotal.tax_amount_currency += tax_group.tax_amount_currency;\n                subtotal.tax_amount += tax_group.tax_amount;\n                accumulated_tax_amount_currency += tax_group.tax_amount_currency;\n                accumulated_tax_amount += tax_group.tax_amount;\n            }\n            tax_totals_summary.subtotals.push(subtotal);\n        }\n\n        // Cash rounding\n        const cash_rounding_lines = base_lines.filter(\n            (base_line) => base_line.special_type === \"cash_rounding\"\n        );\n        if (cash_rounding_lines.length) {\n            tax_totals_summary.cash_rounding_base_amount_currency = 0.0;\n            tax_totals_summary.cash_rounding_base_amount = 0.0;\n            cash_rounding_lines.forEach((base_line) => {\n                const tax_details = base_line.tax_details;\n                tax_totals_summary.cash_rounding_base_amount_currency +=\n                    tax_details.total_excluded_currency;\n                tax_totals_summary.cash_rounding_base_amount += tax_details.total_excluded;\n            });\n        } else if (cash_rounding !== null) {\n            const strategy = cash_rounding.strategy;\n            const cash_rounding_pd = cash_rounding.rounding;\n            const cash_rounding_method = cash_rounding.rounding_method;\n            const total_amount_currency =\n                tax_totals_summary.base_amount_currency + tax_totals_summary.tax_amount_currency;\n            const total_amount = tax_totals_summary.base_amount + tax_totals_summary.tax_amount;\n            const expected_total_amount_currency = roundPrecision(\n                total_amount_currency,\n                cash_rounding_pd,\n                cash_rounding_method\n            );\n            let cash_rounding_base_amount_currency =\n                expected_total_amount_currency - total_amount_currency;\n            const rate = total_amount ? Math.abs(total_amount_currency / total_amount) : 0.0;\n            let cash_rounding_base_amount = rate\n                ? roundPrecision(cash_rounding_base_amount_currency / rate, company_pd)\n                : 0.0;\n            if (!floatIsZero(cash_rounding_base_amount_currency, currency.decimal_places)) {\n                if (strategy === \"add_invoice_line\") {\n                    tax_totals_summary.cash_rounding_base_amount_currency =\n                        cash_rounding_base_amount_currency;\n                    tax_totals_summary.cash_rounding_base_amount = cash_rounding_base_amount;\n                    tax_totals_summary.base_amount_currency += cash_rounding_base_amount_currency;\n                    tax_totals_summary.base_amount += cash_rounding_base_amount;\n                    subtotals[untaxed_amount_subtotal_label].base_amount_currency +=\n                        cash_rounding_base_amount_currency;\n                    subtotals[untaxed_amount_subtotal_label].base_amount +=\n                        cash_rounding_base_amount;\n                } else if (strategy === \"biggest_tax\") {\n                    const all_subtotal_tax_group = tax_totals_summary.subtotals.flatMap(\n                        (subtotal) => subtotal.tax_groups.map((tax_group) => [subtotal, tax_group])\n                    );\n\n                    if (all_subtotal_tax_group.length) {\n                        const [max_subtotal, max_tax_group] = all_subtotal_tax_group.reduce(\n                            (a, b) => (b[1].tax_amount_currency > a[1].tax_amount_currency ? b : a)\n                        );\n\n                        max_tax_group.tax_amount_currency += cash_rounding_base_amount_currency;\n                        max_tax_group.tax_amount += cash_rounding_base_amount;\n                        max_subtotal.tax_amount_currency += cash_rounding_base_amount_currency;\n                        max_subtotal.tax_amount += cash_rounding_base_amount;\n                        tax_totals_summary.tax_amount_currency +=\n                            cash_rounding_base_amount_currency;\n                        tax_totals_summary.tax_amount += cash_rounding_base_amount;\n                    } else {\n                        // Failed to apply the cash rounding since there is no tax.\n                        cash_rounding_base_amount_currency = 0.0;\n                        cash_rounding_base_amount = 0.0;\n                    }\n                }\n            }\n        }\n\n        // Subtract the cash rounding from the untaxed amounts.\n        const cash_rounding_base_amount_currency =\n            tax_totals_summary.cash_rounding_base_amount_currency || 0.0;\n        const cash_rounding_base_amount = tax_totals_summary.cash_rounding_base_amount || 0.0;\n        tax_totals_summary.base_amount_currency -= cash_rounding_base_amount_currency;\n        tax_totals_summary.base_amount -= cash_rounding_base_amount;\n        for (const subtotal of tax_totals_summary.subtotals) {\n            subtotal.base_amount_currency -= cash_rounding_base_amount_currency;\n            subtotal.base_amount -= cash_rounding_base_amount;\n        }\n        encountered_base_amounts.add(\n            parseFloat(tax_totals_summary.base_amount_currency.toFixed(currency.decimal_places))\n        );\n        tax_totals_summary.same_tax_base = encountered_base_amounts.size === 1;\n\n        // Total amount.\n        tax_totals_summary.total_amount_currency =\n            tax_totals_summary.base_amount_currency +\n            tax_totals_summary.tax_amount_currency +\n            cash_rounding_base_amount_currency;\n        tax_totals_summary.total_amount =\n            tax_totals_summary.base_amount +\n            tax_totals_summary.tax_amount +\n            cash_rounding_base_amount;\n\n        return tax_totals_summary;\n    },\n\n    // -------------------------------------------------------------------------\n    // AGGREGATOR OF TAX DETAILS\n    // -------------------------------------------------------------------------\n\n    /**\n     * [!] Mirror of the same method in account_tax.py.\n     * PLZ KEEP BOTH METHODS CONSISTENT WITH EACH OTHERS.\n     */\n    aggregate_base_line_tax_details(base_line, grouping_function) {\n        const values_per_grouping_key = {};\n        const tax_details = base_line.tax_details;\n        const taxes_data = tax_details.taxes_data;\n        const manual_tax_amounts = base_line.manual_tax_amounts;\n\n        // If there are no taxes, we pass an empty object to the grouping function.\n        for (const tax_data of taxes_data.length !== 0 ? taxes_data : [null]) {\n            const current_manual_tax_amounts =\n                tax_data && manual_tax_amounts\n                    ? manual_tax_amounts[tax_data.tax.id.toString()] || {}\n                    : {};\n\n            let raw_grouping_key = grouping_function(base_line, tax_data);\n            let grouping_key;\n            if (\n                raw_grouping_key &&\n                typeof raw_grouping_key === \"object\" &&\n                \"raw_grouping_key\" in raw_grouping_key\n            ) {\n                // TODO: TO BE REMOVED IN MASTER (here for retro-compatibility)\n                // There is no FrozenDict in javascript.\n                // When the key is a record, it can't be jsonified so this is a trick to provide both the\n                // raw_grouping_key (to be jsonified) from the grouping_key (to be added to the values).\n                raw_grouping_key = raw_grouping_key.raw_grouping_key;\n                grouping_key = raw_grouping_key.grouping_key;\n\n                // Handle dictionary-like keys (converted to string in JS)\n                if (typeof grouping_key === \"object\") {\n                    grouping_key = JSON.stringify(grouping_key);\n                }\n            } else {\n                grouping_key = this.stringify_grouping_key(raw_grouping_key);\n            }\n\n            // Base amount.\n            if (!(grouping_key in values_per_grouping_key)) {\n                const values = {\n                    grouping_key: raw_grouping_key,\n                    taxes_data: [],\n                };\n                values_per_grouping_key[grouping_key] = values;\n\n                for (const suffix of [\"_currency\", \"\"]) {\n                    const excluded_rounded_field = `total_excluded${suffix}`;\n                    const excluded_delta_field = `delta_${excluded_rounded_field}`;\n                    const excluded_raw_field = `raw_${excluded_rounded_field}`;\n                    const excluded_target_field = `target_${excluded_rounded_field}`;\n                    const excluded_manual_field = `manual_${excluded_rounded_field}`;\n\n                    const excluded_rounded_amount =\n                        tax_details[excluded_rounded_field] + tax_details[excluded_delta_field];\n                    const excluded_raw_amount = tax_details[excluded_raw_field];\n\n                    values[excluded_rounded_field] = excluded_rounded_amount;\n                    values[excluded_raw_field] = excluded_raw_amount;\n\n                    let excluded_target_amount;\n                    if (base_line[excluded_manual_field] !== null) {\n                        excluded_target_amount = base_line[excluded_manual_field];\n                    } else if (suffix === \"\" && base_line.manual_total_excluded_currency !== null) {\n                        excluded_target_amount = excluded_rounded_amount;\n                    } else {\n                        excluded_target_amount = excluded_raw_amount;\n                    }\n                    values[excluded_target_field] = excluded_target_amount;\n\n                    const tax_base_rounded_field = `base_amount${suffix}`;\n                    const tax_base_raw_field = `raw_${tax_base_rounded_field}`;\n                    const tax_base_target_field = `target_${tax_base_rounded_field}`;\n\n                    if (tax_data) {\n                        values[tax_base_rounded_field] = tax_data[tax_base_rounded_field];\n                        values[tax_base_raw_field] = tax_data[tax_base_raw_field];\n\n                        if (tax_base_rounded_field in current_manual_tax_amounts) {\n                            values[tax_base_target_field] =\n                                current_manual_tax_amounts[tax_base_rounded_field];\n                        } else if (\n                            suffix === \"\" &&\n                            \"base_amount_currency\" in current_manual_tax_amounts\n                        ) {\n                            values[tax_base_target_field] = tax_data[tax_base_rounded_field];\n                        } else {\n                            values[tax_base_target_field] = tax_data[tax_base_raw_field];\n                        }\n                    } else {\n                        values[tax_base_rounded_field] = excluded_rounded_amount;\n                        values[tax_base_raw_field] = excluded_raw_amount;\n                        values[tax_base_target_field] = excluded_target_amount;\n                    }\n\n                    const tax_rounded_field = `tax_amount${suffix}`;\n                    const tax_raw_field = `raw_${tax_rounded_field}`;\n                    const tax_target_field = `target_${tax_rounded_field}`;\n\n                    values[tax_rounded_field] = 0.0;\n                    values[tax_raw_field] = 0.0;\n                    values[tax_target_field] = 0.0;\n                }\n            }\n\n            // Tax amount.\n            if (tax_data) {\n                const reverse_charge_sign = tax_data.is_reverse_charge ? -1 : 1;\n                const values = values_per_grouping_key[grouping_key];\n                for (const suffix of [\"_currency\", \"\"]) {\n                    const tax_rounded_field = `tax_amount${suffix}`;\n                    const tax_raw_field = `raw_${tax_rounded_field}`;\n                    const tax_target_field = `target_${tax_rounded_field}`;\n\n                    values[tax_rounded_field] += tax_data[tax_rounded_field];\n                    values[tax_raw_field] += tax_data[tax_raw_field];\n\n                    if (tax_rounded_field in current_manual_tax_amounts) {\n                        values[tax_target_field] +=\n                            reverse_charge_sign * current_manual_tax_amounts[tax_rounded_field];\n                    } else if (\n                        suffix === \"\" &&\n                        \"tax_amount_currency\" in current_manual_tax_amounts\n                    ) {\n                        values[tax_target_field] = tax_data[tax_rounded_field];\n                    } else {\n                        values[tax_target_field] += tax_data[tax_raw_field];\n                    }\n                }\n                values.taxes_data.push(tax_data);\n            }\n        }\n        return values_per_grouping_key;\n    },\n\n    /**\n     * [!] Mirror of the same method in account_tax.py.\n     * PLZ KEEP BOTH METHODS CONSISTENT WITH EACH OTHERS.\n     */\n    aggregate_base_lines_tax_details(base_lines, grouping_function) {\n        return base_lines.map((base_line) => [\n            base_line,\n            this.aggregate_base_line_tax_details(base_line, grouping_function),\n        ]);\n    },\n\n    /**\n     * [!] Mirror of the same method in account_tax.py.\n     * PLZ KEEP BOTH METHODS CONSISTENT WITH EACH OTHERS.\n     */\n    aggregate_base_lines_aggregated_values(base_lines_aggregated_values) {\n        const default_float_fields = new Set();\n        for (const prefix of [\"\", \"raw_\", \"target_\"]) {\n            for (const suffix of [\"_currency\", \"\"]) {\n                for (const field of [\"base_amount\", \"tax_amount\", \"total_excluded\"]) {\n                    default_float_fields.add(`${prefix}${field}${suffix}`);\n                }\n            }\n        }\n\n        const values_per_grouping_key = {};\n        for (const [base_line, aggregated_values] of base_lines_aggregated_values) {\n            for (const [raw_grouping_key, values] of Object.entries(aggregated_values)) {\n                const grouping_key = values.grouping_key;\n\n                if (!(raw_grouping_key in values_per_grouping_key)) {\n                    const initial_values = (values_per_grouping_key[raw_grouping_key] = {\n                        base_line_x_taxes_data: [],\n                        grouping_key: grouping_key,\n                    });\n                    default_float_fields.forEach((field) => {\n                        initial_values[field] = 0.0;\n                    });\n                }\n                const agg_values = values_per_grouping_key[raw_grouping_key];\n                default_float_fields.forEach((field) => {\n                    agg_values[field] += values[field];\n                });\n                agg_values.base_line_x_taxes_data.push([base_line, values.taxes_data]);\n            }\n        }\n        return values_per_grouping_key;\n    },\n\n    // -------------------------------------------------------------------------\n    // ADVANCED LINES MANIPULATION HELPERS\n    // -------------------------------------------------------------------------\n\n    /**\n     * [!] Mirror of the same method in account_tax.py.\n     * PLZ KEEP BOTH METHODS CONSISTENT WITH EACH OTHERS.\n     */\n    can_be_discounted(tax) {\n        return ![\"fixed\", \"code\"].includes(tax.amount_type);\n    },\n\n    /**\n     * [!] Mirror of the same method in account_tax.py.\n     * PLZ KEEP BOTH METHODS CONSISTENT WITH EACH OTHERS.\n     */\n    merge_tax_details(tax_details_1, tax_details_2) {\n        const results = {};\n        for (const prefix of [\"raw_\", \"\"]) {\n            for (const field of [\"total_excluded\", \"total_included\"]) {\n                for (const suffix of [\"_currency\", \"\"]) {\n                    const key = `${prefix}${field}${suffix}`;\n                    results[key] = tax_details_1[key] + tax_details_2[key];\n                }\n            }\n        }\n        for (const suffix of [\"_currency\", \"\"]) {\n            const field = `delta_total_excluded${suffix}`;\n            results[field] = tax_details_1[field] + tax_details_2[field];\n        }\n\n        const agg_taxes_data = {};\n        for (const tax_details of [tax_details_1, tax_details_2]) {\n            for (const tax_data of tax_details.taxes_data) {\n                const tax = tax_data.tax;\n                const tax_id_str = tax.id.toString();\n                if (tax_id_str in agg_taxes_data) {\n                    const agg_tax_data = agg_taxes_data[tax_id_str];\n                    for (const prefix of [\"raw_\", \"\"]) {\n                        for (const suffix of [\"_currency\", \"\"]) {\n                            for (const field of [\"base_amount\", \"tax_amount\"]) {\n                                const field_with_prefix = `${prefix}${field}${suffix}`;\n                                agg_tax_data[field_with_prefix] += tax_data[field_with_prefix];\n                            }\n                        }\n                    }\n                } else {\n                    agg_taxes_data[tax_id_str] = { ...tax_data };\n                }\n            }\n        }\n        results.taxes_data = Object.values(agg_taxes_data);\n\n        // In case there is some taxes that are in tax_details_1 but not on tax_details_2,\n        // we have to shift manually the base amount. It happens with fixed taxes in which the base\n        // is meaningless but still used in the computations.\n        const taxes_data_in_2 = new Set(tax_details_2.taxes_data.map((td) => td.tax.id));\n        const not_discountable_taxes_data = new Set(\n            tax_details_1.taxes_data\n                .filter((td) => !taxes_data_in_2.has(td.tax.id))\n                .map((td) => td.tax.id)\n        );\n        for (const tax_data of results.taxes_data) {\n            if (not_discountable_taxes_data.has(tax_data.tax.id)) {\n                for (const suffix of [\"_currency\", \"\"]) {\n                    for (const prefix of [\"raw_\", \"\"]) {\n                        tax_data[`${prefix}base_amount${suffix}`] +=\n                            tax_details_2[`${prefix}total_excluded${suffix}`];\n                    }\n                    tax_data[`base_amount${suffix}`] +=\n                        tax_details_2[`delta_total_excluded${suffix}`];\n                }\n            }\n        }\n\n        return results;\n    },\n\n    /**\n     * [!] Mirror of the same method in account_tax.py.\n     * PLZ KEEP BOTH METHODS CONSISTENT WITH EACH OTHERS.\n     */\n    fix_base_lines_tax_details_on_manual_tax_amounts(\n        base_lines,\n        company,\n        { filter_function = null } = {}\n    ) {\n        for (const base_line of base_lines) {\n            const tax_details = base_line.tax_details;\n            const taxes_data = tax_details.taxes_data;\n            if (!taxes_data.length) {\n                continue;\n            }\n\n            base_line.manual_total_excluded_currency =\n                tax_details.total_excluded_currency + tax_details.delta_total_excluded_currency;\n            base_line.manual_total_excluded =\n                tax_details.total_excluded + tax_details.delta_total_excluded;\n            base_line.manual_tax_amounts = {};\n            for (const tax_data of taxes_data) {\n                if (tax_data.is_reverse_charge) {\n                    continue;\n                }\n                const tax = tax_data.tax;\n                const tax_id_str = tax.id.toString();\n                base_line.manual_tax_amounts[tax_id_str] = {};\n                if (filter_function && !filter_function(base_line, tax_data)) {\n                    continue;\n                }\n\n                base_line.manual_tax_amounts[tax_id_str] = {\n                    tax_amount_currency: tax_data.tax_amount_currency,\n                    tax_amount: tax_data.tax_amount,\n                    base_amount_currency: tax_data.base_amount_currency,\n                    base_amount: tax_data.base_amount,\n                };\n            }\n        }\n    },\n\n    /**\n     * [!] Mirror of the same method in account_tax.py.\n     * PLZ KEEP BOTH METHODS CONSISTENT WITH EACH OTHERS.\n     */\n    split_tax_data(base_line, tax_data, company, target_factors) {\n        const currency = base_line.currency_id;\n\n        const factors = this.normalize_target_factors(target_factors);\n\n        const new_taxes_data = [];\n\n        // Distribution of raw amounts.\n        for (const index_factor of factors) {\n            const factor = index_factor[1];\n            new_taxes_data.push({\n                ...tax_data,\n                raw_tax_amount_currency: factor * tax_data.raw_tax_amount_currency,\n                raw_tax_amount: factor * tax_data.raw_tax_amount,\n                raw_base_amount_currency: factor * tax_data.raw_base_amount_currency,\n                raw_base_amount: factor * tax_data.raw_base_amount,\n            });\n        }\n\n        // Distribution of rounded amounts.\n        const new_target_factors = new_taxes_data.map((new_tax_data, index) => ({\n            factor: target_factors[index].factor,\n            tax_data: new_tax_data,\n        }));\n\n        for (const [delta_currency_indicator, delta_currency] of [\n            [\"_currency\", currency],\n            [\"\", company.currency_id],\n        ]) {\n            for (const prefix of [\"tax\", \"base\"]) {\n                const field = `${prefix}_amount${delta_currency_indicator}`;\n                const amounts_to_distribute = this.distribute_delta_amount_smoothly(\n                    delta_currency.decimal_places,\n                    tax_data[field],\n                    new_target_factors\n                );\n                for (let i = 0; i < new_target_factors.length; i++) {\n                    const target_factor = new_target_factors[i];\n                    const amount_to_distribute = amounts_to_distribute[i];\n                    const new_tax_data = target_factor.tax_data;\n                    new_tax_data[field] = amount_to_distribute;\n                }\n            }\n        }\n        return new_taxes_data;\n    },\n\n    /**\n     * [!] Mirror of the same method in account_tax.py.\n     * PLZ KEEP BOTH METHODS CONSISTENT WITH EACH OTHERS.\n     */\n    split_tax_details(base_line, company, target_factors) {\n        const currency = base_line.currency_id;\n        const tax_details = base_line.tax_details;\n\n        const factors = this._normalize_target_factors(target_factors);\n\n        const new_tax_details_list = [];\n\n        // Distribution of raw amounts.\n        for (const index_factor of factors) {\n            const factor = index_factor[1];\n            new_tax_details_list.push({\n                raw_total_excluded_currency: factor * tax_details.raw_total_excluded_currency,\n                raw_total_excluded: factor * tax_details.raw_total_excluded,\n                raw_total_included_currency: factor * tax_details.raw_total_included_currency,\n                raw_total_included: factor * tax_details.raw_total_included,\n                delta_total_excluded_currency: 0.0,\n                delta_total_excluded: 0.0,\n                taxes_data: [],\n            });\n        }\n\n        // Manage 'taxes_data'.\n        for (const tax_data of tax_details.taxes_data) {\n            const new_taxes_data = this.split_tax_data(\n                base_line,\n                tax_data,\n                company,\n                target_factors\n            );\n            for (let i = 0; i < new_tax_details_list.length; i++) {\n                const new_tax_details = new_tax_details_list[i];\n                const new_tax_data = new_taxes_data[i];\n                new_tax_details.taxes_data.push(new_tax_data);\n            }\n        }\n\n        // Distribution of rounded amounts.\n        for (const [delta_currency_indicator, delta_currency] of [\n            [\"_currency\", currency],\n            [\"\", company.currency_id],\n        ]) {\n            const new_target_factors = new_tax_details_list.map((new_tax_details) => ({\n                factor: new_tax_details[`raw_total_excluded${delta_currency_indicator}`],\n                tax_details: new_tax_details,\n            }));\n            const field = `total_excluded${delta_currency_indicator}`;\n            const delta_amount = tax_details[field];\n            const amounts_to_distribute = this.distribute_delta_amount_smoothly(\n                delta_currency.decimal_places,\n                delta_amount,\n                new_target_factors\n            );\n            for (let i = 0; i < new_target_factors.length; i++) {\n                const target_factor = new_target_factors[i];\n                const amount_to_distribute = amounts_to_distribute[i];\n                const new_tax_details = target_factor.tax_details;\n                new_tax_details[field] = amount_to_distribute;\n            }\n        }\n\n        // Manage 'total_included'.\n        for (const new_tax_details of new_tax_details_list) {\n            for (const delta_currency_indicator of [\"_currency\", \"\"]) {\n                new_tax_details[`total_included${delta_currency_indicator}`] =\n                    new_tax_details[`total_excluded${delta_currency_indicator}`] +\n                    new_tax_details.taxes_data.reduce(\n                        (sum, new_tax_data) =>\n                            sum + new_tax_data[`tax_amount${delta_currency_indicator}`],\n                        0\n                    );\n            }\n        }\n        return new_tax_details_list;\n    },\n\n    /**\n     * [!] Mirror of the same method in account_tax.py.\n     * PLZ KEEP BOTH METHODS CONSISTENT WITH EACH OTHERS.\n     */\n    split_base_line(base_line, company, target_factors, { populate_function = null } = {}) {\n        const factors = this.normalize_target_factors(target_factors);\n\n        // Split 'tax_details'.\n        const new_tax_details_list = this.split_tax_details(base_line, company, target_factors);\n\n        // Split 'base_line'.\n        const new_base_lines = factors.map((x) => null);\n        for (let i = 0; i < factors.length; i++) {\n            const index = factors[i][0];\n            const factor = factors[i][1];\n            const new_tax_details = new_tax_details_list[i];\n            const target_factor = target_factors[i];\n\n            const kwargs = {\n                price_unit: factor * base_line.price_unit,\n                tax_details: new_tax_details,\n            };\n\n            if (populate_function) {\n                populate_function(base_line, target_factor, kwargs);\n            }\n\n            new_base_lines[index] = this.prepare_base_line_for_taxes_computation(base_line, kwargs);\n        }\n        return new_base_lines;\n    },\n\n    /**\n     * [!] Mirror of the same method in account_tax.py.\n     * PLZ KEEP BOTH METHODS CONSISTENT WITH EACH OTHERS.\n     * DEPRECATED: TO BE REMOVED IN MASTER\n     */\n    compute_subset_base_lines_total(base_lines, company) {\n        let base_amount_currency = 0.0;\n        let tax_amount_currency = 0.0;\n        let base_amount = 0.0;\n        let tax_amount = 0.0;\n        const tax_amounts_mapping = {};\n        let raw_total_included_currency = 0.0;\n        let raw_total_included = 0.0;\n        for (const base_line of base_lines) {\n            const tax_details = base_line.tax_details;\n            base_amount_currency +=\n                tax_details.total_excluded_currency + tax_details.delta_total_excluded_currency;\n            base_amount += tax_details.total_excluded + tax_details.delta_total_excluded;\n            raw_total_included_currency += tax_details.raw_total_excluded_currency;\n            raw_total_included += tax_details.raw_total_excluded;\n            for (const tax_data of tax_details.taxes_data) {\n                const tax = tax_data.tax;\n                if (!this.can_be_discounted(tax)) {\n                    continue;\n                }\n\n                const tax_id_str = tax.id.toString();\n                if (!(tax_id_str in tax_amounts_mapping)) {\n                    tax_amounts_mapping[tax_id_str] = {\n                        tax_amount_currency: 0.0,\n                        tax_amount: 0.0,\n                    };\n                }\n\n                tax_amount_currency += tax_data.tax_amount_currency;\n                tax_amount += tax_data.tax_amount;\n                tax_amounts_mapping[tax_id_str].tax_amount_currency += tax_data.tax_amount_currency;\n                tax_amounts_mapping[tax_id_str].tax_amount += tax_data.tax_amount;\n                raw_total_included_currency += tax_data.raw_tax_amount_currency;\n                raw_total_included += tax_data.raw_tax_amount;\n            }\n        }\n        return {\n            base_amount_currency: base_amount_currency,\n            tax_amount_currency: tax_amount_currency,\n            base_amount: base_amount,\n            tax_amount: tax_amount,\n            tax_amounts_mapping: tax_amounts_mapping,\n            raw_total_included_currency: raw_total_included_currency,\n            raw_total_included: raw_total_included,\n            rate: raw_total_included ? raw_total_included_currency / raw_total_included : 0.0,\n        };\n    },\n\n    /**\n     * [!] Mirror of the same method in account_tax.py.\n     * PLZ KEEP BOTH METHODS CONSISTENT WITH EACH OTHERS.\n     */\n    reduce_base_lines_with_grouping_function(\n        base_lines,\n        { grouping_function = null, aggregate_function = null, computation_key = null } = {}\n    ) {\n        const base_line_map = {};\n        for (const base_line of base_lines) {\n            const price_unit_after_discount =\n                base_line.price_unit * (1 - base_line.discount / 100.0);\n            const new_base_line = this.prepare_base_line_for_taxes_computation(base_line, {\n                price_unit: base_line.quantity * price_unit_after_discount,\n                quantity: 1.0,\n                discount: 0.0,\n            });\n            const raw_grouping_key = {\n                tax_ids: new_base_line.tax_ids.map((tax) => tax.id),\n                computation_key: base_line.computation_key,\n            };\n            const grouping_key = {\n                tax_ids: new_base_line.tax_ids.map((tax) => tax),\n                computation_key: base_line.computation_key,\n            };\n            if (grouping_function) {\n                const generated_grouping_key = grouping_function(new_base_line);\n\n                // There is no FrozenDict in javascript.\n                // When the key is a record, it can't be jsonified so this is a trick to provide both the\n                // raw_grouping_key (to be jsonified) from the grouping_key (to be added to the values).\n                if (\"raw_grouping_key\" in generated_grouping_key) {\n                    Object.assign(raw_grouping_key, generated_grouping_key.raw_grouping_key);\n                    Object.assign(grouping_key, generated_grouping_key.grouping_key);\n                } else {\n                    Object.assign(raw_grouping_key, generated_grouping_key);\n                    Object.assign(grouping_key, generated_grouping_key);\n                }\n            }\n\n            const grouping_key_json = JSON.stringify(raw_grouping_key);\n            let target_base_line = base_line_map[grouping_key_json];\n            if (target_base_line) {\n                target_base_line.price_unit += new_base_line.price_unit;\n                target_base_line.tax_details = this.merge_tax_details(\n                    target_base_line.tax_details,\n                    base_line.tax_details\n                );\n                if (aggregate_function) {\n                    aggregate_function(target_base_line, base_line);\n                }\n            } else {\n                target_base_line = this.prepare_base_line_for_taxes_computation(new_base_line, {\n                    ...grouping_key,\n                    computation_key: computation_key,\n                    tax_details: {\n                        ...base_line.tax_details,\n                        taxes_data: base_line.tax_details.taxes_data.map((tax_data) =>\n                            Object.assign({}, tax_data)\n                        ),\n                    },\n                });\n                base_line_map[grouping_key_json] = target_base_line;\n                if (aggregate_function) {\n                    aggregate_function(target_base_line, base_line);\n                }\n            }\n        }\n\n        // Remove zero lines.\n        const reduced_base_lines = Object.values(base_line_map).filter(\n            (base_line) => !floatIsZero(base_line.price_unit, base_line.currency_id.decimal_places)\n        );\n        return reduced_base_lines;\n    },\n\n    /**\n     * [!] Mirror of the same method in account_tax.py.\n     * PLZ KEEP BOTH METHODS CONSISTENT WITH EACH OTHERS.\n     * DEPRECATED: TO BE REMOVED IN MASTER\n     */\n    apply_base_lines_manual_amounts_to_reach(\n        base_lines,\n        company,\n        target_base_amount_currency,\n        target_base_amount,\n        target_tax_amounts_mapping\n    ) {\n        const currency = base_lines[0].currency_id;\n\n        // Smooth distribution of the delta accross the base line, starting at the biggest one.\n        const sorted_base_lines = base_lines.sort((base_line_1, base_line_2) => {\n            const key_1 = [\n                Boolean(base_line_1.special_type),\n                -base_line_1.tax_details.total_excluded_currency,\n            ];\n            const key_2 = [\n                Boolean(base_line_2.special_type),\n                -base_line_2.tax_details.total_excluded_currency,\n            ];\n\n            if (key_1[0] !== key_2[0]) {\n                return key_1[0] - key_2[0];\n            }\n            return key_1[1] - key_2[1];\n        });\n        const base_lines_totals = this.compute_subset_base_lines_total(base_lines, company);\n        for (const [delta_suffix, delta_target_base_amount, delta_currency] of [\n            [\"_currency\", target_base_amount_currency, currency],\n            [\"\", target_base_amount, company.currency_id],\n        ]) {\n            const target_factors = sorted_base_lines.map((base_line) => ({\n                factor: Math.abs(\n                    (base_line.tax_details.total_excluded_currency +\n                        base_line.tax_details.delta_total_excluded_currency) /\n                        base_lines_totals.base_amount_currency\n                ),\n                base_line: base_line,\n            }));\n            const amounts_to_distribute = this.distribute_delta_amount_smoothly(\n                delta_currency.decimal_places,\n                delta_target_base_amount - base_lines_totals[`base_amount${delta_suffix}`],\n                target_factors\n            );\n            for (let i = 0; i < target_factors.length; i++) {\n                const target_factor = target_factors[i];\n                const amount_to_distribute = amounts_to_distribute[i];\n                const base_line = target_factor.base_line;\n                const tax_details = base_line.tax_details;\n                const taxes_data = tax_details.taxes_data;\n                if (delta_suffix === \"_currency\") {\n                    base_line.price_unit +=\n                        amount_to_distribute / Math.abs(base_line.quantity || 1.0);\n                }\n                if (!taxes_data.length) {\n                    continue;\n                }\n\n                const first_batch = taxes_data[0].batch;\n                for (const tax_data of taxes_data) {\n                    const tax = tax_data.tax;\n                    if (first_batch.includes(tax)) {\n                        tax_data[`base_amount${delta_suffix}`] += amount_to_distribute;\n                    } else {\n                        break;\n                    }\n                }\n            }\n        }\n\n        for (const [tax_id_str, tax_amounts] of Object.entries(target_tax_amounts_mapping)) {\n            for (const [delta_suffix, delta_target_tax_amount, delta_currency] of [\n                [\"_currency\", tax_amounts.tax_amount_currency, currency],\n                [\"\", tax_amounts.tax_amount, company.currency_id],\n            ]) {\n                const current_tax_amounts = base_lines_totals.tax_amounts_mapping[tax_id_str];\n                if (!current_tax_amounts.tax_amount_currency) {\n                    continue;\n                }\n\n                const target_factors = [];\n                for (const base_line of sorted_base_lines) {\n                    for (const tax_data of base_line.tax_details.taxes_data) {\n                        if (tax_data.tax.id.toString() === tax_id_str) {\n                            target_factors.push({\n                                factor: Math.abs(\n                                    tax_data.tax_amount_currency /\n                                        current_tax_amounts.tax_amount_currency\n                                ),\n                                tax_data: tax_data,\n                            });\n                        }\n                    }\n                }\n                const amounts_to_distribute = this.distribute_delta_amount_smoothly(\n                    delta_currency.decimal_places,\n                    delta_target_tax_amount - current_tax_amounts[`tax_amount${delta_suffix}`],\n                    target_factors\n                );\n                for (let i = 0; i < target_factors.length; i++) {\n                    const target_factor = target_factors[i];\n                    const amount_to_distribute = amounts_to_distribute[i];\n                    const tax_data = target_factor.tax_data;\n                    tax_data[`tax_amount${delta_suffix}`] += amount_to_distribute;\n                }\n            }\n        }\n\n        this.fix_base_lines_tax_details_on_manual_tax_amounts(base_lines, company);\n    },\n\n    /**\n     * [!] Mirror of the same method in account_tax.py.\n     * PLZ KEEP BOTH METHODS CONSISTENT WITH EACH OTHERS.\n     */\n    reduce_base_lines_to_target_amount(\n        base_lines,\n        company,\n        amount_type,\n        amount,\n        { computation_key = null, grouping_function = null, aggregate_function = null } = {}\n    ) {\n        if (!base_lines.length) {\n            return [];\n        }\n\n        const currency = base_lines[0].currency_id;\n        const rate = base_lines[0].rate;\n\n        // Compute the current total amount of the base lines.\n        function grouping_function_total(base_line, tax_data) {\n            return true;\n        }\n\n        let base_lines_aggregated_values = this.aggregate_base_lines_tax_details(\n            base_lines,\n            grouping_function_total\n        );\n        let values_per_grouping_key = this.aggregate_base_lines_aggregated_values(\n            base_lines_aggregated_values\n        );\n        const total_amount_currency = Object.values(values_per_grouping_key).reduce(\n            (acc, values) => acc + values.total_excluded_currency + values.tax_amount_currency,\n            0\n        );\n        const total_amount = Object.values(values_per_grouping_key).reduce(\n            (acc, values) => acc + values.total_excluded + values.tax_amount,\n            0\n        );\n\n        // Compute the current total tax amount per tax.\n        function grouping_function_tax(base_line, tax_data) {\n            return tax_data ? tax_data.tax.id.toString() : null;\n        }\n\n        base_lines_aggregated_values = this.aggregate_base_lines_tax_details(\n            base_lines,\n            grouping_function_tax\n        );\n        values_per_grouping_key = this.aggregate_base_lines_aggregated_values(\n            base_lines_aggregated_values\n        );\n        const tax_amounts_per_tax = {};\n        for (const [grouping_key, values] of Object.entries(values_per_grouping_key)) {\n            if (!grouping_key) {\n                continue;\n            }\n\n            tax_amounts_per_tax[grouping_key] = {\n                tax_amount_currency: values.tax_amount_currency,\n                tax_amount: values.tax_amount,\n                base_amount_currency: values.base_amount_currency,\n                base_amount: values.base_amount,\n            };\n        }\n\n        // Turn the 'amount_type' / 'amount' into a percentage and the total amounts to be reached\n        // from the base lines.\n        const sign = amount < 0.0 ? -1 : 1;\n        const signed_amount = sign * amount;\n        let percentage, expected_total_amount_currency, expected_total_amount;\n        if (amount_type === \"fixed\") {\n            percentage = total_amount_currency ? signed_amount / total_amount_currency : 0.0;\n            expected_total_amount_currency = roundPrecision(amount, currency.rounding);\n            expected_total_amount = rate\n                ? roundPrecision(\n                      expected_total_amount_currency / rate,\n                      company.currency_id.rounding\n                  )\n                : 0.0;\n        } else {\n            percentage = signed_amount / 100.0;\n            expected_total_amount_currency = roundPrecision(\n                total_amount_currency * sign * percentage,\n                currency.rounding\n            );\n            expected_total_amount = roundPrecision(\n                total_amount * sign * percentage,\n                company.currency_id.rounding\n            );\n        }\n\n        // Compute the expected amounts.\n        const expected_tax_amounts = {};\n        for (const [grouping_key, values] of Object.entries(tax_amounts_per_tax)) {\n            expected_tax_amounts[grouping_key] = {\n                tax_amount_currency: roundPrecision(\n                    values.tax_amount_currency * sign * percentage,\n                    currency.rounding\n                ),\n                tax_amount: roundPrecision(\n                    values.tax_amount * sign * percentage,\n                    company.currency_id.rounding\n                ),\n                base_amount_currency: roundPrecision(\n                    values.base_amount_currency * sign * percentage,\n                    currency.rounding\n                ),\n                base_amount: roundPrecision(\n                    values.base_amount * sign * percentage,\n                    company.currency_id.rounding\n                ),\n            };\n        }\n        const expected_base_amount_currency =\n            expected_total_amount_currency -\n            Object.values(expected_tax_amounts).reduce((acc, v) => acc + v.tax_amount_currency, 0);\n        const expected_base_amount =\n            expected_total_amount -\n            Object.values(expected_tax_amounts).reduce((acc, v) => acc + v.tax_amount, 0);\n\n        // Reduce the base lines to minimize the number of lines.\n        const reduced_base_lines = this.reduce_base_lines_with_grouping_function(base_lines, {\n            grouping_function: grouping_function,\n            aggregate_function: aggregate_function,\n            computation_key: computation_key,\n        });\n        if (!reduced_base_lines.length) {\n            return [];\n        }\n\n        // Reduce the unit price to approach the target amount.\n        const new_base_lines = reduced_base_lines.map((base_line) =>\n            this.prepare_base_line_for_taxes_computation(base_line, {\n                price_unit: base_line.price_unit * sign * percentage,\n                computation_key: computation_key,\n            })\n        );\n        this.add_tax_details_in_base_lines(new_base_lines, company);\n        this.round_base_lines_tax_details(new_base_lines, company);\n\n        // Smooth distribution of the delta tax/base amounts.\n        const sorted_base_lines = new_base_lines.sort((base_line_1, base_line_2) => {\n            const key_1 = [\n                Boolean(base_line_1.special_type),\n                -base_line_1.tax_details.total_excluded_currency,\n            ];\n            const key_2 = [\n                Boolean(base_line_2.special_type),\n                -base_line_2.tax_details.total_excluded_currency,\n            ];\n\n            if (key_1[0] !== key_2[0]) {\n                return key_1[0] - key_2[0];\n            }\n            return key_1[1] - key_2[1];\n        });\n        base_lines_aggregated_values = this.aggregate_base_lines_tax_details(\n            new_base_lines,\n            grouping_function_tax\n        );\n        values_per_grouping_key = this.aggregate_base_lines_aggregated_values(\n            base_lines_aggregated_values\n        );\n        const current_tax_amounts_per_tax = {};\n        for (const [grouping_key, values] of Object.entries(values_per_grouping_key)) {\n            if (!grouping_key) {\n                continue;\n            }\n            current_tax_amounts_per_tax[grouping_key] = {\n                tax_amount_currency: values.tax_amount_currency,\n                tax_amount: values.tax_amount,\n                base_amount_currency: values.base_amount_currency,\n                base_amount: values.base_amount,\n            };\n        }\n        for (const [tax_id_str, tax_amounts] of Object.entries(current_tax_amounts_per_tax)) {\n            const tax_amount_currency = tax_amounts.tax_amount_currency;\n            if (!tax_amount_currency) {\n                continue;\n            }\n\n            for (const [delta_suffix, delta_tax_amount, delta_base_amount, delta_currency] of [\n                [\n                    \"_currency\",\n                    expected_tax_amounts[tax_id_str].tax_amount_currency -\n                        tax_amounts.tax_amount_currency,\n                    expected_tax_amounts[tax_id_str].base_amount_currency -\n                        tax_amounts.base_amount_currency,\n                    currency,\n                ],\n                [\n                    \"\",\n                    expected_tax_amounts[tax_id_str].tax_amount - tax_amounts.tax_amount,\n                    expected_tax_amounts[tax_id_str].base_amount - tax_amounts.base_amount,\n                    company.currency_id,\n                ],\n            ]) {\n                // Tax amount.\n                const tax_amount_currency = tax_amounts.tax_amount_currency;\n                if (tax_amount_currency) {\n                    const target_factors = [];\n                    for (const base_line of sorted_base_lines) {\n                        for (const tax_data of base_line.tax_details.taxes_data) {\n                            if (tax_data.tax.id.toString() === tax_id_str) {\n                                target_factors.push({\n                                    factor: Math.abs(\n                                        tax_data.tax_amount_currency / tax_amount_currency\n                                    ),\n                                    base_line: base_line,\n                                    tax_data: tax_data,\n                                });\n                            }\n                        }\n                    }\n                    const amounts_to_distribute = this.distribute_delta_amount_smoothly(\n                        delta_currency.decimal_places,\n                        delta_tax_amount,\n                        target_factors\n                    );\n                    for (const [i, target_factor] of target_factors.entries()) {\n                        const amount_to_distribute = amounts_to_distribute[i];\n                        target_factor.tax_data[`tax_amount${delta_suffix}`] += amount_to_distribute;\n                    }\n                }\n\n                // Base amount.\n                const base_amount_currency = tax_amounts.base_amount_currency;\n                if (base_amount_currency) {\n                    const target_factors = [];\n                    for (const base_line of sorted_base_lines) {\n                        for (const tax_data of base_line.tax_details.taxes_data) {\n                            if (tax_data.tax.id.toString() === tax_id_str) {\n                                target_factors.push({\n                                    factor: Math.abs(\n                                        tax_data.base_amount_currency / base_amount_currency\n                                    ),\n                                    base_line: base_line,\n                                    tax_data: tax_data,\n                                });\n                            }\n                        }\n                    }\n                    const amounts_to_distribute = this.distribute_delta_amount_smoothly(\n                        delta_currency.decimal_places,\n                        delta_base_amount,\n                        target_factors\n                    );\n                    for (const [i, target_factor] of target_factors.entries()) {\n                        const amount_to_distribute = amounts_to_distribute[i];\n                        target_factor.tax_data[`base_amount${delta_suffix}`] +=\n                            amount_to_distribute;\n                    }\n                }\n            }\n        }\n\n        base_lines_aggregated_values = this.aggregate_base_lines_tax_details(\n            new_base_lines,\n            grouping_function_total\n        );\n        values_per_grouping_key = this.aggregate_base_lines_aggregated_values(\n            base_lines_aggregated_values\n        );\n        const current_base_amount_currency = Object.values(values_per_grouping_key).reduce(\n            (acc, values) => acc + values.total_excluded_currency,\n            0\n        );\n        const current_base_amount = Object.values(values_per_grouping_key).reduce(\n            (acc, values) => acc + values.total_excluded,\n            0\n        );\n        for (const [delta_suffix, delta_base_amount, delta_currency] of [\n            [\"_currency\", expected_base_amount_currency - current_base_amount_currency, currency],\n            [\"\", expected_base_amount - current_base_amount, company.currency_id],\n        ]) {\n            const target_factors = sorted_base_lines.map((base_line) => ({\n                factor: Math.abs(\n                    (base_line.tax_details.total_excluded_currency +\n                        base_line.tax_details.delta_total_excluded_currency) /\n                        current_base_amount_currency\n                ),\n                base_line: base_line,\n            }));\n            const amounts_to_distribute = this.distribute_delta_amount_smoothly(\n                delta_currency.decimal_places,\n                delta_base_amount,\n                target_factors\n            );\n            for (const [i, target_factor] of target_factors.entries()) {\n                const base_line = target_factor.base_line;\n                const amount_to_distribute = amounts_to_distribute[i];\n                const tax_details = base_line.tax_details;\n                tax_details[`delta_total_excluded${delta_suffix}`] += amount_to_distribute;\n                if (delta_suffix === \"_currency\") {\n                    base_line.price_unit += amount_to_distribute;\n                }\n            }\n        }\n        return new_base_lines;\n    },\n\n    /**\n     * [!] Mirror of the same method in account_tax.py.\n     * PLZ KEEP BOTH METHODS CONSISTENT WITH EACH OTHERS.\n     */\n    partition_base_lines_taxes(base_lines, partition_function) {\n        let has_taxes_to_exclude = false;\n        const base_lines_partition_taxes = [];\n        for (const base_line of base_lines) {\n            const tax_details = base_line.tax_details;\n            const taxes_data = tax_details.taxes_data;\n            const taxes_to_keep = [];\n            const taxes_to_exclude = [];\n            for (const tax_data of taxes_data) {\n                const tax = tax_data.tax;\n                if (partition_function(base_line, tax_data)) {\n                    taxes_to_keep.push(tax);\n                } else {\n                    taxes_to_exclude.push(tax);\n                }\n            }\n            if (taxes_to_exclude.length > 0) {\n                has_taxes_to_exclude = true;\n            }\n            base_lines_partition_taxes.push([base_line, taxes_to_keep, taxes_to_exclude]);\n        }\n        return [base_lines_partition_taxes, has_taxes_to_exclude];\n    },\n\n    /**\n     * [!] Mirror of the same method in account_tax.py.\n     * PLZ KEEP BOTH METHODS CONSISTENT WITH EACH OTHERS.\n     */\n    prepare_discountable_base_lines(base_lines, company, { exclude_function = null } = {}) {\n        function dispatch_exclude_function(base_line, tax_data) {\n            return (\n                !this.can_be_discounted(tax_data.tax) ||\n                (exclude_function && exclude_function(base_line, tax_data))\n            );\n        }\n\n        return this.dispatch_taxes_into_new_base_lines(\n            base_lines,\n            company,\n            dispatch_exclude_function.bind(this)\n        );\n    },\n\n    // -------------------------------------------------------------------------\n    // GLOBAL DISCOUNT\n    // -------------------------------------------------------------------------\n\n    /**\n     * [!] Mirror of the same method in account_tax.py.\n     * PLZ KEEP BOTH METHODS CONSISTENT WITH EACH OTHERS.\n     */\n    prepare_global_discount_lines(\n        base_lines,\n        company,\n        amount_type,\n        amount,\n        { computation_key = \"global_discount\", grouping_function = null } = {}\n    ) {\n        const discountable_base_lines = this.prepare_discountable_base_lines(base_lines, company);\n        const new_base_lines = this.reduce_base_lines_to_target_amount(\n            discountable_base_lines,\n            company,\n            amount_type,\n            -amount,\n            { computation_key: computation_key, grouping_function: grouping_function }\n        );\n        this.fix_base_lines_tax_details_on_manual_tax_amounts(new_base_lines, company);\n        return new_base_lines;\n    },\n\n    // -------------------------------------------------------------------------\n    // DOWN PAYMENT\n    // -------------------------------------------------------------------------\n\n    /**\n     * [!] Mirror of the same method in account_tax.py.\n     * PLZ KEEP BOTH METHODS CONSISTENT WITH EACH OTHERS.\n     */\n    prepare_base_lines_for_down_payment(base_lines, company, { exclude_function = null } = {}) {\n        function dispatch_exclude_function(base_line, tax_data) {\n            return (\n                !this.can_be_discounted(tax_data.tax) ||\n                (exclude_function && exclude_function(base_line, tax_data))\n            );\n        }\n\n        const new_base_lines = this.dispatch_taxes_into_new_base_lines(\n            base_lines,\n            company,\n            dispatch_exclude_function.bind(this)\n        );\n        return new_base_lines.concat(\n            this.turn_removed_taxes_into_new_base_lines(new_base_lines, company)\n        );\n    },\n\n    /**\n     * [!] Mirror of the same method in account_tax.py.\n     * PLZ KEEP BOTH METHODS CONSISTENT WITH EACH OTHERS.\n     */\n    prepare_down_payment_lines(\n        base_lines,\n        company,\n        amount_type,\n        amount,\n        { computation_key = \"down_payment\", grouping_function = null } = {}\n    ) {\n        const base_lines_for_dp = this.prepare_base_lines_for_down_payment(base_lines, company);\n        const new_base_lines = this.reduce_base_lines_to_target_amount(\n            base_lines_for_dp,\n            company,\n            amount_type,\n            amount,\n            { computation_key: computation_key, grouping_function: grouping_function }\n        );\n        this.fix_base_lines_tax_details_on_manual_tax_amounts(new_base_lines, company);\n        return new_base_lines;\n    },\n\n    // -------------------------------------------------------------------------\n    // DISPATCHING OF LINES\n    // -------------------------------------------------------------------------\n\n    /**\n     * [!] Mirror of the same method in account_tax.py.\n     * PLZ KEEP BOTH METHODS CONSISTENT WITH EACH OTHERS.\n     */\n    dispatch_taxes_into_new_base_lines(base_lines, company, exclude_function) {\n        function partition_function(base_line, tax_data) {\n            return !exclude_function(base_line, tax_data);\n        }\n\n        const base_lines_partition_taxes = this.partition_base_lines_taxes(\n            base_lines,\n            partition_function\n        )[0];\n\n        const new_base_lines_list = base_lines.map(() => []);\n        const to_process = [];\n        base_lines_partition_taxes.forEach(\n            ([base_line, taxes_to_keep, taxes_to_exclude], index) => {\n                to_process.push([index, base_line, taxes_to_exclude]);\n            }\n        );\n        while (to_process.length) {\n            const [index, base_line, taxes_to_exclude] = to_process.shift();\n\n            const tax_details = base_line.tax_details;\n            const taxes_data = tax_details.taxes_data;\n\n            // Get the index of the next 'tax_data' to exclude.\n            let next_split_index = null;\n            for (let i = 0; i < taxes_data.length; i++) {\n                if (taxes_to_exclude.includes(taxes_data[i].tax)) {\n                    next_split_index = i;\n                    break;\n                }\n            }\n\n            if (next_split_index === null) {\n                new_base_lines_list[index].push({ ...base_line });\n                continue;\n            }\n\n            const common_taxes_data = taxes_data.slice(0, next_split_index);\n            const tax_data_to_remove = taxes_data[next_split_index];\n            const remaining_taxes_data = taxes_data.slice(next_split_index + 1);\n\n            // Split 'tax_details'.\n            const first_tax_details = {\n                raw_total_excluded_currency: tax_details.raw_total_excluded_currency,\n                raw_total_excluded: tax_details.raw_total_excluded,\n                total_excluded_currency: tax_details.total_excluded_currency,\n                total_excluded: tax_details.total_excluded,\n                delta_total_excluded_currency: tax_details.delta_total_excluded_currency,\n                delta_total_excluded: tax_details.delta_total_excluded,\n                taxes_data: common_taxes_data,\n            };\n            first_tax_details.raw_total_included_currency =\n                first_tax_details.raw_total_excluded_currency +\n                common_taxes_data.reduce((sum, t) => sum + t.raw_tax_amount_currency, 0);\n            first_tax_details.total_included_currency =\n                first_tax_details.total_excluded_currency +\n                first_tax_details.delta_total_excluded_currency +\n                common_taxes_data.reduce((sum, t) => sum + t.tax_amount_currency, 0);\n            first_tax_details.raw_total_included =\n                first_tax_details.raw_total_excluded +\n                common_taxes_data.reduce((sum, t) => sum + t.raw_tax_amount, 0);\n            first_tax_details.total_included =\n                first_tax_details.total_excluded +\n                first_tax_details.delta_total_excluded +\n                common_taxes_data.reduce((sum, t) => sum + t.tax_amount, 0);\n\n            const second_tax_details = {\n                raw_total_excluded_currency: tax_data_to_remove.raw_tax_amount_currency,\n                raw_total_excluded: tax_data_to_remove.raw_tax_amount,\n                total_excluded_currency: tax_data_to_remove.tax_amount_currency,\n                total_excluded: tax_data_to_remove.tax_amount,\n                delta_total_excluded_currency: 0.0,\n                delta_total_excluded: 0.0,\n                raw_total_included_currency: tax_data_to_remove.raw_tax_amount_currency,\n                raw_total_included: tax_data_to_remove.raw_tax_amount,\n                total_included_currency: tax_data_to_remove.tax_amount_currency,\n                total_included: tax_data_to_remove.tax_amount,\n                taxes_data: [],\n            };\n\n            const target_factors = [\n                {\n                    factor: first_tax_details.raw_total_excluded_currency,\n                    tax_details: first_tax_details,\n                },\n                {\n                    factor: second_tax_details.raw_total_excluded_currency,\n                    tax_details: second_tax_details,\n                },\n            ];\n            for (const remaining_tax_data of remaining_taxes_data) {\n                let first_tax_data;\n                if (tax_data_to_remove.taxes.includes(remaining_tax_data.tax)) {\n                    const new_remaining_taxes_data = this.split_tax_data(\n                        base_line,\n                        remaining_tax_data,\n                        company,\n                        target_factors\n                    );\n\n                    first_tax_data = new_remaining_taxes_data[0];\n\n                    second_tax_details.taxes_data.push(new_remaining_taxes_data[1]);\n                    second_tax_details.raw_total_included_currency +=\n                        new_remaining_taxes_data[1].raw_tax_amount_currency;\n                    second_tax_details.raw_total_included +=\n                        new_remaining_taxes_data[1].raw_tax_amount;\n                    second_tax_details.total_included_currency +=\n                        new_remaining_taxes_data[1].tax_amount_currency;\n                    second_tax_details.total_included += new_remaining_taxes_data[1].tax_amount;\n                } else {\n                    first_tax_data = remaining_tax_data;\n                }\n\n                first_tax_details.taxes_data.push(first_tax_data);\n                first_tax_details.raw_total_included_currency +=\n                    first_tax_data.raw_tax_amount_currency;\n                first_tax_details.raw_total_included += first_tax_data.raw_tax_amount;\n                first_tax_details.total_included_currency += first_tax_data.tax_amount_currency;\n                first_tax_details.total_included += first_tax_data.tax_amount;\n            }\n\n            // Split 'base_line'.\n            const first_taxes = first_tax_details.taxes_data.map((tax_data) => tax_data.tax);\n            const first_base_line = this.prepare_base_line_for_taxes_computation(base_line, {\n                tax_ids: first_taxes,\n                tax_details: first_tax_details,\n            });\n\n            const second_taxes = second_tax_details.taxes_data.map((tax_data) => tax_data.tax);\n            const second_base_line = this.prepare_base_line_for_taxes_computation(base_line, {\n                tax_ids: second_taxes,\n                price_unit:\n                    (second_tax_details.raw_total_excluded_currency +\n                        second_tax_details.taxes_data\n                            .filter((t) => t.tax.price_include)\n                            .reduce((sum, t) => sum + t.raw_tax_amount_currency, 0)) /\n                    (base_line.quantity || 1.0),\n                tax_details: second_tax_details,\n                _removed_tax_data: tax_data_to_remove,\n            });\n\n            to_process.unshift(\n                [index, first_base_line, taxes_to_exclude],\n                [index, second_base_line, taxes_to_exclude]\n            );\n        }\n\n        const final_base_lines = [];\n        new_base_lines_list.forEach((new_base_lines) => {\n            new_base_lines[0].removed_taxes_data_base_lines = new_base_lines.slice(1);\n            final_base_lines.push(new_base_lines[0]);\n        });\n        return final_base_lines;\n    },\n\n    /**\n     * [!] Mirror of the same method in account_tax.py.\n     * PLZ KEEP BOTH METHODS CONSISTENT WITH EACH OTHERS.\n     */\n    turn_removed_taxes_into_new_base_lines(\n        base_lines,\n        company,\n        { grouping_function = null, aggregate_function = null } = {}\n    ) {\n        let extra_base_lines = [];\n        for (const base_line of base_lines) {\n            extra_base_lines = extra_base_lines.concat(\n                base_line.removed_taxes_data_base_lines || []\n            );\n        }\n        return this.reduce_base_lines_with_grouping_function(extra_base_lines, {\n            grouping_function: grouping_function,\n            aggregate_function: aggregate_function,\n        });\n    },\n};\n", "/** @odoo-module **/\n\nimport { registry } from \"@web/core/registry\";\nimport {\n    copyClipboardButtonField,\n    CopyClipboardButtonField,\n} from \"@web/views/fields/copy_clipboard/copy_clipboard_field\";\n\nimport { CopyButton } from \"@web/core/copy_button/copy_button\";\n\nclass PaymentWizardCopyButton extends CopyButton {\n    async onClick() {\n        await this.env.model.mutex.getUnlockedDef();\n        return super.onClick();\n    }\n}\n\nclass PaymentWizardCopyClipboardButtonField extends CopyClipboardButtonField {\n    static components = { CopyButton: PaymentWizardCopyButton };\n}\n\nconst paymentWizardCopyClipboardButtonField = {\n    ...copyClipboardButtonField,\n    component: PaymentWizardCopyClipboardButtonField,\n};\n\nregistry\n    .category(\"fields\")\n    .add(\"PaymentWizardCopyClipboardButtonField\", paymentWizardCopyClipboardButtonField);\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\n\nconst exampleData = {\n    applyExamplesText: _t(\"Use This For My Campaigns\"),\n    allowedGroupBys: ['stage_id'],\n    examples: [{\n        name: _t('Creative Flow'),\n        columns: [_t('Ideas'), _t('Design'), _t('Review'), _t('Send'), _t('Done')],\n        description: _t(\"Collect ideas, design creative content and publish it once reviewed.\"),\n    }, {\n        name: _t('Event-driven Flow'),\n        columns: [_t('Later'), _t('This Month'), _t('This Week'), _t('Running'), _t('Sent')],\n        description: _t(\"Track incoming events (e.g. Christmas, Black Friday, ...) and publish timely content.\"),\n    }, {\n        name: _t('Soft-Launch Flow'),\n        columns: [_t('Pre-Launch'), _t('Soft-Launch'), _t('Deploy'), _t('Report'), _t('Done')],\n        description: _t(\"Prepare your Campaign, test it with part of your audience and deploy it fully afterwards.\"),\n    }, {\n        name: _t('Audience-driven Flow'),\n        columns: [_t('Gather Data'), _t('List-Building'), _t('Copywriting'), _t('Sent')],\n        description: _t(\"Gather data, build a recipient list and write content based on your Marketing target.\"),\n    }, {\n        name: _t('Approval-based Flow'),\n        columns: [_t('To be Approved'), _t('Approved'), _t('Deployed')],\n        description: _t(\"Prepare Campaigns and get them approved before making them go live.\"),\n    }],\n};\n\nregistry.category(\"kanban_examples\").add(\"utm_campaign\", exampleData);\n", "import { Component } from \"@odoo/owl\";\nimport { formatCurrency } from \"@web/core/currency\";\n\nexport class BadgeExtraPrice extends Component {\n    static template = \"sale.BadgeExtraPrice\";\n    static props = {\n        price: Number,\n        currencyId: Number,\n    };\n\n    /**\n     * Return the price, in the format of the given currency.\n     *\n     * @return {String} - The price, in the format of the given currency.\n     */\n    getFormattedPrice() {\n        return formatCurrency( Math.abs(this.props.price), this.props.currencyId);\n    }\n}\n", "import { Component } from \"@odoo/owl\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { SaleActionHelperDialog } from \"./sale_action_helper_dialog\";\n\nexport class SaleActionHelper extends Component {\n    static template = \"sale.SaleActionHelper\";\n    static props = {\n        noContentHelp: String,\n    }\n\n    setup() {\n        this.dialogService = useService(\"dialog\");\n    }\n\n    openVideoPreview() {\n        this.dialogService.add(SaleActionHelperDialog, {\n            url: \"https://www.youtube.com/embed/N4zw-2t6spk?autoplay=1\",\n        })\n    }\n};\n", "import { Component } from \"@odoo/owl\";\nimport { Dialog } from \"@web/core/dialog/dialog\";\n\nexport class SaleActionHelperDialog extends Component {\n    static components = { Dialog };\n    static template = \"sale.SaleActionHelperDialog\";\n    static props = {\n        url: String,\n        close: Function,\n    };\n}\n", "import { Component, useState, useSubEnv } from '@odoo/owl';\nimport { formatCurrency } from '@web/core/currency';\nimport { Dialog } from '@web/core/dialog/dialog';\nimport { _t } from '@web/core/l10n/translation';\nimport { rpc } from '@web/core/network/rpc';\nimport { useService } from '@web/core/utils/hooks';\nimport { ProductCombo } from '../models/product_combo';\nimport { ProductTemplateAttributeLine } from '../models/product_template_attribute_line';\nimport { ProductCard } from '../product_card/product_card';\nimport {\n    ProductConfiguratorDialog\n} from '../product_configurator_dialog/product_configurator_dialog';\nimport { QuantityButtons } from '../quantity_buttons/quantity_buttons';\n\nexport class ComboConfiguratorDialog extends Component {\n    static template = 'sale.ComboConfiguratorDialog';\n    static components = { Dialog, ProductCard, QuantityButtons };\n    static props = {\n        product_tmpl_id: Number,\n        display_name: String,\n        quantity: Number,\n        price: Number,\n        combos: { type: Array, element: ProductCombo },\n        currency_id: Number,\n        company_id: { type: Number, optional: true },\n        pricelist_id: { type: Number, optional: true },\n        date: String,\n        price_info: { type: String, optional: true },\n        edit: { type: Boolean, optional: true },\n        options: {\n            type: Object,\n            optional: true,\n            shape: {\n                showQuantity : { type: Boolean, optional: true },\n                showPrice : { type: Boolean, optional: true },\n            },\n        },\n        save: Function,\n        discard: Function,\n        close: Function,\n    };\n\n    setup() {\n        this.dialog = useService('dialog');\n        this.env.dialogData.dismiss = !this.props.edit && this.props.discard.bind(this);\n        this.state = useState({\n            // Maps combo ids to selected combo items.\n            // Note that selected combo items can be modified (i.e. their `no_variant` PTAVs can be\n            // updated), so this map stores deep copies to avoid modifying the props.\n            selectedComboItems: new Map(),\n            quantity: this.props.quantity,\n            basePrice: this.props.price,\n            isLoading: false,\n        });\n        this._initSelectedComboItems();\n        this.getPriceUrl = '/sale/combo_configurator/get_price';\n        useSubEnv({ currency: { id: this.props.currency_id } });\n\n        this.unconfigurableCombos = this.props.combos.filter(combo => !combo.isConfigurable);\n        this.configurableCombos = this.props.combos.filter(combo => combo.isConfigurable);\n    }\n\n    /**\n     * Select the provided combo item, and open the product configurator iff the combo item's\n     * product is configurable.\n     *\n     * @param {Number} comboId The id of the combo to which the combo item belongs.\n     * @param {ProductComboItem} comboItem The combo item to select.\n     */\n    async selectComboItem(comboId, comboItem) {\n        // Use up-to-date selected PTAVs and custom values to populate the product configurator.\n        comboItem = this.getSelectedOrProvidedComboItem(comboId, comboItem);\n        let product = comboItem.product;\n        if (comboItem.is_configurable) {\n            this.dialog.add(ProductConfiguratorDialog, {\n                productTemplateId: product.product_tmpl_id,\n                ptavIds: product.selectedPtavIds,\n                customPtavs: product.selectedCustomPtavs,\n                quantity: 1,\n                companyId: this.props.company_id,\n                pricelistId: this.props.pricelist_id,\n                currencyId: this.props.currency_id,\n                soDate: this.props.date,\n                edit: true, // Hide the optional products, if any.\n                options: {\n                    canChangeVariant: false,\n                    showQuantity: false,\n                    showPrice: false,\n                    showPackaging: false,\n                },\n                size: \"md\",\n                save: async configuredProduct => {\n                    const selectedComboItem = comboItem.deepCopy();\n                    selectedComboItem.product.ptals = configuredProduct.attribute_lines.map(\n                        ProductTemplateAttributeLine.fromProductConfiguratorPtal\n                    );\n                    this.state.selectedComboItems.set(comboId, selectedComboItem);\n                },\n                discard: () => {},\n                ...this._getAdditionalDialogProps(),\n            });\n        } else {\n            this.state.selectedComboItems.set(comboId, comboItem.deepCopy());\n        }\n    }\n\n    /**\n     * Sets the quantity of this combo product.\n     *\n     * @param {Number} quantity The new quantity of this combo product.\n     */\n    async setQuantity(quantity) {\n        if (quantity <= 0) quantity = 1;\n        this.state.quantity = quantity;\n        this.state.basePrice = await rpc(this.getPriceUrl, {\n            product_tmpl_id: this.props.product_tmpl_id,\n            currency_id: this.props.currency_id,\n            quantity: quantity,\n            date: this.props.date,\n            company_id: this.props.company_id,\n            pricelist_id: this.props.pricelist_id,\n            ...this._getAdditionalRpcParams(),\n        });\n    }\n\n    /**\n     * Return the selected or provided combo item.\n     *\n     * If the provided combo item was already selected, then it may contain stale data (i.e.\n     * selected PTAVs, custom values), and we should rely on the data in `state.selectedComboItems`\n     * instead. Otherwise, the data in the provided combo item is up-to-date and can be used.\n     *\n     * @param {Number} comboId The id of the combo to which the combo item belongs.\n     * @param {ProductComboItem} comboItem The provided combo item.\n     * @return {ProductComboItem} The selected or provided combo item.\n     */\n    getSelectedOrProvidedComboItem(comboId, comboItem) {\n        const selectedComboItem = this.state.selectedComboItems.get(comboId);\n        const isComboItemAlreadySelected = selectedComboItem?.id === comboItem.id;\n        return isComboItemAlreadySelected ? selectedComboItem : comboItem;\n    }\n\n    get totalMessage() {\n        return _t(\"Total: %s\", this.formattedTotalPrice);\n    }\n\n    /**\n     * Return the total price for all units, formatted using the provided currency.\n     *\n     * @return {String} The formatted total price.\n     */\n    get formattedTotalPrice() {\n        return formatCurrency(this.state.quantity * this._comboPrice, this.props.currency_id);\n    }\n\n    /**\n     * Check whether a combo item has been selected for each combo.\n     *\n     * @return {Boolean} Whether a combo item has been selected for each combo.\n     */\n    get areAllCombosSelected() {\n        return this.state.selectedComboItems.size === this.props.combos.length;\n    }\n\n    async confirm(options) {\n        this.state.isLoading = true;\n        await this.props.save(this._comboProductData, this._selectedComboItems, options).finally(\n            () => this.state.isLoading = false\n        )\n        this.props.close();\n    }\n\n    cancel() {\n        if (!this.props.edit) {\n            this.props.discard();\n        }\n        this.props.close();\n    }\n\n    /**\n     * Initialize the selected combo item in each combo.\n     */\n    _initSelectedComboItems() {\n        for (const combo of this.props.combos) {\n            const comboItem = combo.selectedComboItem;\n            if (comboItem) {\n                this.state.selectedComboItems.set(combo.id, comboItem.deepCopy());\n            }\n        }\n    }\n\n    /**\n     * Return the total price per unit.\n     *\n     * The total price is the sum of:\n     * - The combo product's price,\n     * - The selected combo items' extra price,\n     * - The selected `no_variant` attributes' extra price.\n     *\n     * @return {Number} The total price.\n     */\n    get _comboPrice() {\n        const extraPrice = Array.from(this.state.selectedComboItems.values()).reduce(\n            (price, item) => price + item.totalExtraPrice, 0\n        );\n        return this.state.basePrice + extraPrice;\n    }\n\n    /**\n     * Return data about the combo product.\n     *\n     * @return {Object} Data about the combo product.\n     */\n    get _comboProductData() {\n        return { 'quantity': this.state.quantity };\n    }\n\n    /**\n     * Return the selected combo items, in the same order as the combos given as props.\n     *\n     * @return {ProductComboItem[]} The sorted selected combo items.\n     */\n    get _selectedComboItems() {\n        const sortedItems = new Map([...this.state.selectedComboItems.entries()].sort(\n            (entry1, entry2) =>\n                this.props.combos.findIndex(combo => combo.id === entry1[0])\n                - this.props.combos.findIndex(combo => combo.id === entry2[0])\n        ));\n        return Array.from(sortedItems.values());\n    }\n\n    /**\n     * Hook to append additional RPC params in overriding modules.\n     *\n     * @return {Object} The additional RPC params.\n     */\n    _getAdditionalRpcParams() {\n        return {};\n    }\n\n    /**\n     * Hook to append additional props in overriding modules.\n     *\n     * @return {Object} The additional props.\n     */\n    _getAdditionalDialogProps() {\n        return {};\n    }\n}\n", "import { ProductComboItem } from './product_combo_item';\n\nexport class ProductCombo {\n    /**\n     * @param {number} id\n     * @param {string} name\n     * @param {ProductComboItem[]|object[]} combo_items\n     */\n    constructor({id, name, combo_items}) {\n        this.id = id;\n        this.name = name;\n        this.combo_items = combo_items.map(item => new ProductComboItem(item));\n    }\n\n    /**\n     * Return the selected combo item, if any.\n     *\n     * @return {ProductComboItem|undefined} The selected combo item, if any.\n     */\n    get selectedComboItem() {\n        return this.combo_items.find(item => item.is_selected);\n    }\n\n    /**\n    * Return the preselected combo item, if any.\n    *\n    * @return {ProductComboItem|undefined} The preselected combo items, if any.\n    */\n    get preselectedComboItem() {\n        return this.combo_items.find(item => item.is_preselected);\n    }\n\n    /**\n     * Check whether this combo is configurable.\n     *\n     * @return {Boolean} Whether this combo is configurable.\n     */\n    get isConfigurable() {\n        return !this.combo_items.some(item => item.is_preselected);\n    }\n}\n", "import { ProductProduct } from './product_product';\n\nexport class ProductComboItem {\n    /**\n     * @param {number} id\n     * @param {number} extra_price\n     * @param {boolean} is_preselected\n     * @param {boolean} is_selected\n     * @param {boolean} is_configurable\n     * @param {ProductProduct|object} product\n     */\n    constructor({id, extra_price, is_preselected, is_selected, is_configurable, product}) {\n        this.id = id;\n        this.extra_price = extra_price;\n        this.is_preselected = is_preselected;\n        this.is_selected = is_selected;\n        this.is_configurable = is_configurable;\n        this.product = new ProductProduct(product);\n    }\n\n    /**\n     * Return the combo item's \"total\" extra price.\n     *\n     * The total extra price is the sum of:\n     * - The combo item's extra price,\n     * - The extra price of the selected `no_variant` PTAVs of the combo item's product.\n     *\n     * @return {Number} The combo item's \"total\" extra price.\n     */\n    get totalExtraPrice() {\n        return this.extra_price + this.product.selectedNoVariantPtavsPriceExtra;\n    }\n\n    /**\n     * Return a deep copy of this combo item.\n     *\n     * @return {ProductComboItem} A deep copy of this combo item.\n     */\n    deepCopy() {\n        return new ProductComboItem(JSON.parse(JSON.stringify(this)));\n    }\n}\n", "import { ProductTemplateAttributeLine } from './product_template_attribute_line';\n\nexport class ProductProduct {\n    /**\n     * The instance is initialized in `setup` to allow patching, as constructors can't be patched.\n     */\n    constructor(...args) {\n        this.setup(...args);\n    }\n\n    /**\n     * @param {number} id\n     * @param {number} product_tmpl_id\n     * @param {string} display_name\n     * @param {ProductTemplateAttributeLine[]|object[]} ptals\n     * @param {string} image_src\n     * @param {string} description\n     */\n    setup({id, product_tmpl_id, display_name, ptals, image_src, description}) {\n        this.id = id;\n        this.product_tmpl_id = product_tmpl_id;\n        this.display_name = display_name;\n        this.ptals = ptals.map(ptal => new ProductTemplateAttributeLine(ptal));\n        this.image_src = image_src;\n        this.description = description;\n    }\n\n    /**\n     * Return the `no_variant` PTALs.\n     *\n     * @return {ProductTemplateAttributeLine[]} The `no_variant` PTALs.\n     */\n    get noVariantPtals() {\n        return this.ptals.filter(ptal => ptal.create_variant === 'no_variant');\n    }\n\n    /**\n     * Return the extra price of the selected `no_variant` PTAVs.\n     *\n     * @return {Number} The extra price of the selected `no_variant` PTAVs.\n     */\n    get selectedNoVariantPtavsPriceExtra() {\n        return this.noVariantPtals.reduce((price, ptal) => price + ptal.selectedPtavsPriceExtra, 0);\n    }\n\n    /**\n     * Return the selected PTAV ids.\n     *\n     * @return {Number[]} The selected PTAV ids.\n     */\n    get selectedPtavIds() {\n        return this.ptals.flatMap(ptal => ptal.selected_ptavs).map(ptav => ptav.id);\n    }\n\n    /**\n     * Return the selected `no_variant` PTAV ids.\n     *\n     * @return {Number[]} The selected `no_variant` PTAV ids.\n     */\n    get selectedNoVariantPtavIds() {\n        return this.noVariantPtals.flatMap(ptal => ptal.selected_ptavs).map(ptav => ptav.id);\n    }\n\n    /**\n     * Return the selected custom PTAVs.\n     *\n     * @return {{id: Number, value: String}[]} The selected custom PTAVs.\n     */\n    get selectedCustomPtavs() {\n        return this.ptals.filter(ptal => ptal.hasSelectedCustomPtav).flatMap(\n            ptal => ptal.selected_ptavs\n        ).map(ptav => ({\n            'id': ptav.id,\n            'value': ptav.custom_value,\n        }));\n    }\n}\n", "import { ProductTemplateAttributeValue } from './product_template_attribute_value';\n\nexport class ProductTemplateAttributeLine {\n    /**\n     * @param {number} id\n     * @param {string} name\n     * @param {'always'|'dynamic'|'no_variant'} create_variant\n     * @param {ProductTemplateAttributeValue[]|object[]} selected_ptavs\n     */\n    constructor({id, name, create_variant, selected_ptavs}) {\n        this.id = id;\n        this.name = name;\n        this.create_variant = create_variant;\n        this.selected_ptavs = selected_ptavs.map(ptav => new ProductTemplateAttributeValue(ptav));\n    }\n\n    /**\n     * Construct a ProductTemplateAttributeLine from the provided \"product configurator\"-shaped\n     * PTAL.\n     *\n     * @param productConfiguratorPtal The \"product configurator\"-shaped PTAL.\n     * @return {ProductTemplateAttributeLine} The corresponding ProductTemplateAttributeLine.\n     */\n    static fromProductConfiguratorPtal(productConfiguratorPtal) {\n        const selectedPtavIds = new Set(productConfiguratorPtal.selected_attribute_value_ids);\n        const selectedPtavs = productConfiguratorPtal.attribute_values\n            .filter(ptav => selectedPtavIds.has(ptav.id))\n            .map(ptav => new ProductTemplateAttributeValue({\n                id: ptav.id,\n                name: ptav.name,\n                price_extra: ptav.price_extra,\n                custom_value: productConfiguratorPtal.customValue,\n            }));\n        return new ProductTemplateAttributeLine({\n            id: productConfiguratorPtal.id,\n            name: productConfiguratorPtal.attribute.name,\n            create_variant: productConfiguratorPtal.create_variant,\n            selected_ptavs: selectedPtavs,\n        });\n    }\n\n    /**\n     * Return the extra price of the selected PTAVs.\n     *\n     * @return {Number} The extra price of the selected PTAVs.\n     */\n    get selectedPtavsPriceExtra() {\n        return this.selected_ptavs.reduce((price, ptav) => price + ptav.price_extra, 0);\n    }\n\n    /**\n     * Check whether this PTAL has selected custom PTAVs.\n     *\n     * @return {Boolean} Whether this PTAL has selected custom PTAVs.\n     */\n    get hasSelectedCustomPtav() {\n        return this.selected_ptavs.some(ptav => ptav.custom_value);\n    }\n\n    /**\n     * Return the display name of this PTAL.\n     *\n     * @return {String} The display name of this PTAL.\n     */\n    get ptalDisplayName() {\n        const selectedPtavNames = this.selected_ptavs.map(ptav => ptav.name).join(', ');\n        let ptalDisplayName = `${this.name}: ${selectedPtavNames}`;\n        if (this.hasSelectedCustomPtav) {\n            ptalDisplayName += ` (${this.selected_ptavs[0].custom_value})`;\n        }\n        return ptalDisplayName;\n    }\n}\n", "export class ProductTemplateAttributeValue {\n    /**\n     * @param {number} id\n     * @param {string} name\n     * @param {number} price_extra\n     * @param {string|undefined} custom_value\n     */\n    constructor({id, name, price_extra, custom_value}) {\n        this.id = id;\n        this.name = name;\n        this.price_extra = price_extra;\n        this.custom_value = custom_value;\n    }\n}\n", "\nimport { Component } from \"@odoo/owl\";\nimport { formatCurrency } from \"@web/core/currency\";\nimport {\n    ProductTemplateAttributeLine as PTAL\n} from \"../product_template_attribute_line/product_template_attribute_line\";\nimport { QuantityButtons } from '../quantity_buttons/quantity_buttons';\nimport { getSelectedCustomPtav } from \"../sale_utils\";\nimport { _t } from \"@web/core/l10n/translation\";\n\nexport class Product extends Component {\n    static components = { PTAL, QuantityButtons };\n    static template = \"sale.Product\";\n    static props = {\n        id: { type: [Number, {value: false}], optional: true },\n        product_tmpl_id: Number,\n        display_name: String,\n        description_sale: [Boolean, String], // backend sends 'false' when there is no description\n        price: Number,\n        quantity: Number,\n        uom: { type: Object, optional: true },\n        available_uoms: { type: Object, optional: true },\n        attribute_lines: Object,\n        optional: Boolean,\n        imageURL: { type: String, optional: true },\n        archived_combinations: Array,\n        exclusions: Object,\n        parent_exclusions: Object,\n        parent_product_tmpl_id: { type: Number, optional: true },\n        price_info: { type: String, optional: true },\n        selectedComboItems: {\n            type: Array,\n            element: Object,\n            shape: {\n                name: String,\n            },\n            optional: true,\n        },\n    };\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * Return the price, in the format of the given currency.\n     *\n     * @return {String} - The price, in the format of the given currency.\n     */\n    getFormattedPrice() {\n        return formatCurrency(this.props.price, this.env.currency.id);\n    }\n\n    /**\n     * Check whether this product is the main product.\n     *\n     * @return {Boolean} - Whether this product is the main product.\n     */\n    get isMainProduct() {\n        return this.env.mainProductTmplId === this.props.product_tmpl_id;\n    }\n\n    /**\n     * Return this product's image URL.\n     *\n     * @return {String} This product's image URL.\n     */\n    get imageUrl() {\n        const modelPath = this.props.id\n            ? `product.product/${ this.props.id }`\n            : `product.template/${ this.props.product_tmpl_id }`;\n        return `/web/image/${ modelPath }/image_256`;\n    }\n\n    /**\n     * Check whether the provided PTAL should be shown.\n     *\n     * @return {Boolean} Whether the PTAL should be shown.\n     */\n    shouldShowPtal(ptal) {\n        return this.env.canChangeVariant\n            || ptal.create_variant === 'no_variant'\n            || !!getSelectedCustomPtav(ptal);\n    }\n\n\n    get UoMTitle() {\n        return _t(\"Packaging\");\n    }\n\n    async selectUoM(event) {\n        this.env.setUoM(this.props.product_tmpl_id, parseInt(event.target.value));\n    }\n\n}\n", "import { Component } from '@odoo/owl';\nimport { BadgeExtraPrice } from '../badge_extra_price/badge_extra_price';\nimport { ProductProduct } from '../models/product_product';\n\nexport class ProductCard extends Component {\n    static template = 'sale.ProductCard';\n    static components = { BadgeExtraPrice };\n    static props = {\n        product: ProductProduct,\n        extraPrice: { type: Number, optional: true },\n        onClick: Function,\n        isSelected: { type: Boolean, optional: true },\n        isConfigurable: { type: Boolean, optional: true }\n    };\n\n    /**\n     * Check whether the provided PTAL should be shown in this card.\n     *\n     * @param {ProductTemplateAttributeLine} ptal The PTAL to check.\n     * @return {Boolean} Whether to show the PTAL.\n     */\n    shouldShowPtal(ptal) {\n        return (\n            ptal.selected_ptavs.length > 0 &&\n            (ptal.hasSelectedCustomPtav || ptal.create_variant === 'no_variant')\n        );\n    }\n}\n", "import { Component, onWillStart, useState, useSubEnv } 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 { ProductList } from \"../product_list/product_list\";\nimport { formatCurrency } from '@web/core/currency';\n\nexport class ProductConfiguratorDialog extends Component {\n    static components = { Dialog, ProductList};\n    static template = 'sale.ProductConfiguratorDialog';\n    static props = {\n        productTemplateId: Number,\n        ptavIds: { type: Array, element: Number },\n        customPtavs: {\n            type: Array,\n            element: Object,\n            shape: {\n                id: Number,\n                value: String,\n            }\n        },\n        quantity: Number,\n        productUOMId: { type: Number, optional: true },\n        companyId: { type: Number, optional: true },\n        pricelistId: { type: Number, optional: true },\n        currencyId: { type: Number, optional: true },\n        selectedComboItems: {\n            type: Array,\n            element: Object,\n            shape: {\n                name: String,\n            },\n            optional: true,\n        },\n        soDate: String,\n        size: {\n            type: String,\n            optional: true,\n            validate: (s) => [\"sm\", \"md\", \"lg\", \"xl\", \"fs\", \"fullscreen\"].includes(s),\n        },\n        edit: { type: Boolean, optional: true },\n        options: {\n            type: Object,\n            optional: true,\n            shape: {\n                canChangeVariant: { type: Boolean, optional: true },\n                showQuantity : { type: Boolean, optional: true },\n                showPrice : { type: Boolean, optional: true },\n                showPackaging: { type: Boolean, optional: true },\n            },\n        },\n        save: Function,\n        discard: Function,\n        close: Function, // This is the close from the env of the Dialog Component\n    };\n    static defaultProps = {\n        edit: false,\n    }\n\n    setup() {\n        this.title = _t(\"Configure your product\");\n        this.env.dialogData.dismiss = !this.props.edit && this.props.discard.bind(this);\n        this.state = useState({\n            products: [],\n            optionalProducts: [],\n        });\n        // Nest the currency id in an object so that it stays up to date in the `env`, even if we\n        // modify it in `onWillStart` afterwards.\n        this.currency = { id: this.props.currencyId };\n        this.getValuesUrl = '/sale/product_configurator/get_values';\n        this.createProductUrl = '/sale/product_configurator/create_product';\n        this.updateCombinationUrl = '/sale/product_configurator/update_combination';\n        this.getOptionalProductsUrl = '/sale/product_configurator/get_optional_products';\n\n        useSubEnv({\n            mainProductTmplId: this.props.productTemplateId,\n            currency: this.currency,\n            canChangeVariant: this.props.options?.canChangeVariant ?? true,\n            showQuantity: this.props.options?.showQuantity ?? true,\n            showPackaging: this.props.options?.showPackaging ?? true,\n            showPrice: this.props.options?.showPrice ?? true,\n            addProduct: this._addProduct.bind(this),\n            removeProduct: this._removeProduct.bind(this),\n            setQuantity: this._setQuantity.bind(this),\n            setUoM: this._setUnitOfMeasure.bind(this),\n            updateProductTemplateSelectedPTAV: this._updateProductTemplateSelectedPTAV.bind(this),\n            updatePTAVCustomValue: this._updatePTAVCustomValue.bind(this),\n            isPossibleCombination: this._isPossibleCombination,\n        });\n\n        onWillStart(async () => {\n            const {\n                products,\n                optional_products,\n                currency_id,\n            } = await this._loadData(this.props.edit);\n\n            // If the product configurator is opened after the combo configurator (which happens if\n            // a combo product has optional products), `_loadData` will return a single product\n            // (i.e. the combo product), which should be linked to the previously selected combo\n            // items.\n            products[0].selectedComboItems = this.props.selectedComboItems || [];\n\n            this.state.products = products;\n            this.state.optionalProducts = optional_products;\n            for (const customPtav of this.props.customPtavs) {\n                this._updatePTAVCustomValue(\n                    this.env.mainProductTmplId,\n                    customPtav.id,\n                    customPtav.value\n                );\n            }\n            this._checkExclusions(this.state.products[0]);\n            // Use the currency id retrieved from the server if none was provided in the props.\n            this.currency.id ??= currency_id;\n        });\n    }\n\n    get totalMessage() {\n        return _t(\"Total: %s\", this.getFormattedTotal());\n    }\n\n    /**\n    * Return the total of the product in the list, in the currency of the `sale.order`.\n    *\n    * @return {String} - The sum of all items in the list, in the currency of the `sale.order`.\n    */\n    getFormattedTotal() {\n        const total = (this.state.products || []).reduce(\n            (sum, product) => sum + product.price * product.quantity,\n            0\n        );\n        return formatCurrency(total, this.currency.id);\n    }\n\n    //--------------------------------------------------------------------------\n    // Data Exchanges\n    //--------------------------------------------------------------------------\n\n    async _loadData(onlyMainProduct) {\n        return rpc(this.getValuesUrl, {\n            product_template_id: this.props.productTemplateId,\n            quantity: this.props.quantity,\n            currency_id: this.currency.id,\n            so_date: this.props.soDate,\n            product_uom_id: this.props.productUOMId,\n            company_id: this.props.companyId,\n            pricelist_id: this.props.pricelistId,\n            ptav_ids: this.props.ptavIds,\n            only_main_product: onlyMainProduct,\n            show_packaging: this.env.showPackaging,\n            ...this._getAdditionalRpcParams(),\n        });\n    }\n\n    async _createProduct(product) {\n        return rpc(this.createProductUrl, {\n            product_template_id: product.product_tmpl_id,\n            ptav_ids: this._getCombination(product),\n        });\n    }\n\n    async _updateCombination(product, quantity, uomId) {\n        return rpc(this.updateCombinationUrl, {\n            product_template_id: product.product_tmpl_id,\n            ptav_ids: this._getCombination(product),\n            currency_id: this.currency.id,\n            so_date: this.props.soDate,\n            quantity: quantity,\n            product_uom_id: uomId,\n            company_id: this.props.companyId,\n            pricelist_id: this.props.pricelistId,\n            ...this._getAdditionalRpcParams(),\n        });\n    }\n\n    async _getOptionalProducts(product) {\n        return rpc(this.getOptionalProductsUrl, {\n            product_template_id: product.product_tmpl_id,\n            ptav_ids: this._getCombination(product),\n            parent_ptav_ids: this._getParentsCombination(product),\n            currency_id: this.currency.id,\n            so_date: this.props.soDate,\n            company_id: this.props.companyId,\n            pricelist_id: this.props.pricelistId,\n            ...this._getAdditionalRpcParams(),\n        });\n    }\n\n    /**\n     * Hook to append additional RPC params in overriding modules.\n     *\n     * @return {Object} - The additional RPC params.\n     */\n    _getAdditionalRpcParams() {\n        return {};\n    }\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n     * Add the product to the list of products and fetch his optional products.\n     *\n     * @param {Number} productTmplId - The product template id, as a `product.template` id.\n     */\n    async _addProduct(productTmplId) {\n        const index = this.state.optionalProducts.findIndex(\n            p => p.product_tmpl_id === productTmplId\n        );\n        if (index >= 0) {\n            this.state.products.push(...this.state.optionalProducts.splice(index, 1));\n            // Fetch optional product from the server with the parent combination.\n            const product = this._findProduct(productTmplId);\n            // Filter out optional products that are already loaded in the configurator.\n            const newOptionalProducts = (await this._getOptionalProducts(product)).filter(\n                p => !this._findProduct(p.product_tmpl_id)\n            );\n            this.state.optionalProducts.push(...newOptionalProducts);\n        }\n    }\n\n    /**\n     * Remove the product and his optional products from the list of products.\n     *\n     * @param {Number} productTmplId - The product template id, as a `product.template` id.\n     */\n    _removeProduct(productTmplId) {\n        const index = this.state.products.findIndex(p => p.product_tmpl_id === productTmplId);\n        if (index >= 0) {\n            this.state.optionalProducts.push(...this.state.products.splice(index, 1));\n            for (const childProduct of this._getChildProducts(productTmplId)) {\n                this._removeProduct(childProduct.product_tmpl_id);\n                this.state.optionalProducts.splice(\n                    this.state.optionalProducts.findIndex(\n                        p => p.product_tmpl_id === childProduct.product_tmpl_id\n                    ), 1\n                );\n            }\n        }\n    }\n\n    /**\n     * Set the quantity of the product to a given value.\n     *\n     * If the value is less than or equal to zero, the product is removed from the product list\n     * instead, unless it is the main product, in which case the quantity is set to 1.\n     *\n     * @param {Number} productTmplId - The product template id, as a `product.template` id.\n     * @param {Number} quantity - The new quantity of the product.\n     * @return {Boolean} - Whether the quantity was updated.\n     */\n    async _setQuantity(productTmplId, quantity) {\n        if (quantity <= 0) {\n            if (productTmplId === this.env.mainProductTmplId) {\n                quantity = 1;\n            } else {\n                this._removeProduct(productTmplId);\n                return true;\n            }\n        }\n        const product = this._findProduct(productTmplId);\n        if (product.quantity === quantity) {\n            return false;\n        }\n        const { price } = await this._updateCombination(product, quantity, product.uom.id);\n        product.quantity = quantity;\n        product.price = parseFloat(price);\n\n        return true;\n    }\n\n    /**\n     * Set the uom of the product to a given value.\n     *\n     * @param {Number} productTmplId - The product template id, as a `product.template` id.\n     * @param {Number} uomId - The new uom of the product, as an `uom.uom` id.\n     *\n     * @return {Boolean} - Whether the uom was updated.\n     */\n    async _setUnitOfMeasure(productTmplId, uomId) {\n        const product = this._findProduct(productTmplId);\n        if (product.uom.id === uomId) {\n            return false;\n        }\n        const { price } = await this._updateCombination(product, product.quantity, uomId);\n        product.price = parseFloat(price);\n        product.uom = product.available_uoms.find((uom) => uom.id === uomId);\n\n        return true;\n    }\n\n    /**\n     * Change the value of `selected_attribute_value_ids` on the given PTAL in the product.\n     *\n     * @param {Number} productTmplId - The product template id, as a `product.template` id.\n     * @param {Number} ptalId - The PTAL id, as a `product.template.attribute.line` id.\n     * @param {Number} ptavId - The PTAV id, as a `product.template.attribute.value` id.\n     * @param {Boolean} isMulti - Whether multiple `product.template.attribute.value` can be selected.\n     */\n    async _updateProductTemplateSelectedPTAV(productTmplId, ptalId, ptavId, isMulti) {\n        const product = this._findProduct(productTmplId);\n        const ptal = product.attribute_lines.find(line => line.id === ptalId);\n        ptavId = parseInt(ptavId);\n        if (isMulti) {\n            const selectedPtavIds = new Set(ptal.selected_attribute_value_ids);\n            selectedPtavIds.has(ptavId)\n                ? selectedPtavIds.delete(ptavId)\n                : selectedPtavIds.add(ptavId);\n            ptal.selected_attribute_value_ids = Array.from(selectedPtavIds);\n        } else {\n            ptal.selected_attribute_value_ids = [ptavId];\n        }\n        this._checkExclusions(product);\n        if (this._isPossibleCombination(product)) {\n            const updatedValues = await this._updateCombination(product, product.quantity, product.uom.id);\n            Object.assign(product, updatedValues);\n            // When a combination should exist but was deleted from the database, it should not be\n            // selectable and considered as an exclusion.\n            if (!product.id && product.attribute_lines.every(ptal => ptal.create_variant === \"always\")) {\n                const combination = this._getCombination(product);\n                product.archived_combinations = product.archived_combinations.concat([combination]);\n                this._checkExclusions(product);\n            }\n        }\n    }\n\n    /**\n     * Set the custom value for a given custom PTAV.\n     *\n     * @param {Number} productTmplId - The product template id, as a `product.template` id.\n     * @param {Number} ptavId - The PTAV id, as a `product.template.attribute.value` id.\n     * @param {String} customValue - The custom value.\n     */\n    _updatePTAVCustomValue(productTmplId, ptavId, customValue) {\n        const product = this._findProduct(productTmplId);\n        product.attribute_lines.find(\n            ptal => ptal.selected_attribute_value_ids.includes(ptavId)\n        ).customValue = customValue;\n    }\n\n    /**\n     * Check the exclusions of a given product and his child.\n     *\n     * @param {Object} product - The product for which to check the exclusions.\n     */\n    _checkExclusions(product) {\n        const combination = this._getCombination(product);\n        const exclusions = product.exclusions;\n        const parentExclusions = product.parent_exclusions;\n        const archivedCombinations = product.archived_combinations;\n        const parentCombination = this._getParentsCombination(product);\n        const childProducts = this._getChildProducts(product.product_tmpl_id)\n        const ptavList = product.attribute_lines.flat().flatMap(ptal => ptal.attribute_values)\n        ptavList.map(ptav => ptav.excluded = false); // Reset all the values\n\n        if (exclusions) {\n            for(const ptavId of combination) {\n                for(const excludedPtavId of exclusions[ptavId]) {\n                    ptavList.find(ptav => ptav.id === excludedPtavId).excluded = true;\n                }\n            }\n        }\n        if (parentCombination) {\n            for(const ptavId of parentCombination) {\n                for(const excludedPtavId of (parentExclusions[ptavId]||[])) {\n                    const ptav = ptavList.find(ptav => ptav.id === excludedPtavId);\n                    if (ptav) {\n                        ptav.excluded = true; // Assign only if the element exists\n                    }\n                }\n            }\n        }\n        if (archivedCombinations) {\n            for(const excludedCombination of archivedCombinations) {\n                const ptavCommon = excludedCombination.filter((ptav) => combination.includes(ptav));\n                if (ptavCommon.length === combination.length) {\n                    for(const excludedPtavId of ptavCommon) {\n                        ptavList.find(ptav => ptav.id === excludedPtavId).excluded = true;\n                    }\n                } else if (ptavCommon.length === (combination.length - 1)) {\n                    // In this case we only need to disable the remaining ptav\n                    const disabledPtavId = excludedCombination.find(\n                        (ptav) => !combination.includes(ptav)\n                    );\n                    const excludedPtav = ptavList.find(ptav => ptav.id === disabledPtavId)\n                    if (excludedPtav) {\n                        excludedPtav.excluded = true;\n                    }\n                }\n            }\n        }\n        for(const optionalProductTmpl of childProducts) {\n            this._checkExclusions(optionalProductTmpl);\n        }\n    }\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * Return the product given his template id.\n     *\n     * @param {Number} productTmplId - The product template id, as a `product.template` id.\n     * @return {Object} - The product.\n     */\n    _findProduct(productTmplId) {\n        // The product might be in either of the two lists `products` or `optional_products`.\n        return  this.state.products.find(p => p.product_tmpl_id === productTmplId) ||\n                this.state.optionalProducts.find(p => p.product_tmpl_id === productTmplId);\n    }\n\n    /**\n     * Return the list of dependents products for a given product.\n     *\n     * @param {Number} productTmplId - The product template id for which to find his children, as a\n     *                                 `product.template` id.\n     * @return {Array} - The list of dependents products.\n     */\n    _getChildProducts(productTmplId) {\n        return [\n            ...this.state.products.filter(p => p.parent_product_tmpl_id === productTmplId),\n            ...this.state.optionalProducts.filter(p => p.parent_product_tmpl_id === productTmplId)\n        ]\n    }\n\n    /**\n     * Return the selected PTAV of the product, as a list of `product.template.attribute.value` id.\n     *\n     * @param {Object} product - The product for which to find the combination.\n     * @return {Array} - The combination of the product.\n     */\n    _getCombination(product) {\n        return product.attribute_lines.flatMap(ptal => ptal.selected_attribute_value_ids);\n    }\n\n    /**\n     * Return the selected PTAVs of the parent product, as a list of\n     * `product.template.attribute.value` ids.\n     *\n     * @param {Object} product - The product for which to find the parent combination.\n     * @return {Array} - The combination of the parent product.\n     */\n    _getParentsCombination(product) {\n        return product.parent_product_tmpl_id\n            ? this._getCombination(this._findProduct(product.parent_product_tmpl_id))\n            : [];\n    }\n\n    /**\n     * Check if a product has a valid combination.\n     *\n     * @param {Object} product - The product for which to check the combination.\n     * @return {Boolean} - Whether the combination is valid or not.\n     */\n    _isPossibleCombination(product) {\n        return product.attribute_lines.every(ptal => {\n            const selectedPtavIds = new Set(ptal.selected_attribute_value_ids);\n            return ptal.attribute_values\n                .filter(ptav => selectedPtavIds.has(ptav.id))\n                .every(ptav => !ptav.excluded);\n        });\n    }\n\n    /**\n     * Check if all the products selected have a valid combination.\n     *\n     * @return {Boolean} - Whether all the products selected have a valid combination or not.\n     */\n    isPossibleConfiguration() {\n        return [...this.state.products].every(\n            p => this._isPossibleCombination(p)\n        );\n    }\n\n    /**\n     * Confirm the current combination(s).\n     *\n     * @return {undefined}\n     */\n    async onConfirm(options) {\n        if (!this.isPossibleConfiguration()) return;\n        // Create the products with dynamic attributes\n        for (const product of this.state.products) {\n            if (\n                !product.id &&\n                product.attribute_lines.some(ptal => ptal.create_variant === \"dynamic\")\n            ) {\n                const productId = await this._createProduct(product);\n                product.id = parseInt(productId);\n            }\n        }\n        await this.props.save(\n            this.state.products.find(\n                p => p.product_tmpl_id === this.env.mainProductTmplId\n            ),\n            this.state.products.filter(\n                p => p.product_tmpl_id !== this.env.mainProductTmplId\n            ),\n            options,\n        );\n        this.props.close();\n    }\n\n    /**\n     * Discard the modal.\n     */\n    onDiscard() {\n        if (!this.props.edit) {\n            this.props.discard(); // clear the line\n        }\n        this.props.close();\n    }\n}\n", "\nimport { Component } from \"@odoo/owl\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { Product } from \"../product/product\";\n\nexport class ProductList extends Component {\n    static components = { Product };\n    static template = \"sale.ProductList\";\n    static props = {\n        products: Array,\n        areProductsOptional: { type: Boolean, optional: true },\n    };\n    static defaultProps = {\n        areProductsOptional: false,\n    };\n\n    setup() {\n        this.optionalProductsTitle = _t(\"Add optional products\");\n    }\n}\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { Component } from \"@odoo/owl\";\nimport { formatCurrency } from \"@web/core/currency\";\nimport { BadgeExtraPrice } from \"../badge_extra_price/badge_extra_price\";\nimport { getSelectedCustomPtav } from \"../sale_utils\";\n\nexport class ProductTemplateAttributeLine extends Component {\n    static components = { BadgeExtraPrice };\n    static template = \"sale.ProductTemplateAttributeLine\";\n    static props = {\n        productTmplId: Number,\n        id: Number,\n        attribute: {\n            type: Object,\n            shape: {\n                id: Number,\n                name: String,\n                display_type: {\n                    type: String,\n                    validate: type => [\"color\", \"multi\", \"pills\", \"radio\", \"select\", \"image\"].includes(type),\n                },\n            },\n        },\n        attribute_values: {\n            type: Array,\n            element: {\n                type: Object,\n                shape: {\n                    id: Number,\n                    name: String,\n                    html_color: [Boolean, String], // backend sends 'false' when there is no color\n                    image: [Boolean, String], // backend sends 'false' when there is no image set\n                    is_custom: Boolean,\n                    price_extra: Number,\n                    excluded: { type: Boolean, optional: true },\n                },\n            },\n        },\n        selected_attribute_value_ids: { type: Array, element: Number },\n        create_variant: {\n            type: String,\n            validate: type => [\"always\", \"dynamic\", \"no_variant\"].includes(type),\n        },\n        customValue: {type: [{value: false}, String], optional: true},\n    };\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n     * Update the selected PTAV in the state.\n     *\n     * @param {Event} event\n     */\n    updateSelectedPTAV(event) {\n        this.env.updateProductTemplateSelectedPTAV(\n            this.props.productTmplId, this.props.id, event.target.value, this.props.attribute.display_type == 'multi'\n        );\n    }\n\n    /**\n     * Update in the state the custom value of the selected PTAV.\n     *\n     * @param {Event} event\n     */\n    updateCustomValue(event) {\n        this.env.updatePTAVCustomValue(\n            this.props.productTmplId, this.props.selected_attribute_value_ids[0], event.target.value\n        );\n    }\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * Return template name to use by checking the display type in the props.\n     *\n     * Each attribute line can have one of this five display types:\n     *      - 'Color'  : Display each attribute as a circle filled with said color.\n     *      - 'Pills'  : Display each attribute as a rectangle-shaped element.\n     *      - 'Radio'  : Display each attribute as a radio element.\n     *      - 'Select' : Display each attribute in a selection tag.\n     *      - 'Multi'  : Display each attribute in a multi-checkbox tag.\n     *\n     * @return {String} - The template name to use.\n     */\n    getPTAVTemplate() {\n        switch(this.props.attribute.display_type) {\n            case 'select':\n                return 'sale.ptav_select';\n            case 'radio':\n                return 'sale.ptav_radio';\n            case 'pills':\n                return 'sale.ptav_pills';\n            case 'color':\n                return 'sale.ptav_color';\n            case 'multi':\n                return 'sale.ptav_multi';\n            case 'image':\n                return 'sale.ptav_image';\n        }\n    }\n\n    /**\n     * Return the name of the PTAV\n     *\n     * In the selection HTML tag, it is impossible to show the component `BadgeExtraPrice`. Append\n     * the extra price to the name to ensure that the extra price will be shown.\n     * Note: used in `sale.ptav_select`.\n     *\n     * @param {Object} ptav - The attribute, as a `product.template.attribute.value` summary dict.\n     * @return {String} - The name of the PTAV.\n     */\n    getPTAVSelectName(ptav) {\n        if (ptav.price_extra) {\n            const sign = ptav.price_extra > 0 ? '+' : '-';\n            const price = formatCurrency(Math.abs(ptav.price_extra), this.env.currency.id);\n            return ptav.name +\" (\"+ sign + \" \" + price + \")\";\n        } else {\n            return ptav.name;\n        }\n    }\n\n    /**\n     * Check if the selected ptav is custom or not.\n     *\n     * @return {Boolean} - Whether the selected ptav is custom or not.\n     */\n    isSelectedPTAVCustom() {\n        return !!getSelectedCustomPtav(this.props);\n    }\n\n    get showValuesChoice() {\n        return (this.env.canChangeVariant || this.props.create_variant === 'no_variant') && (\n            this.props.attribute_values.length > 1 || this.props.attribute.display_type === 'multi'\n        )\n    }\n\n    get customValuePlaceholder() {\n        return _t(\"Enter a customized value\");\n    }\n\n    /**\n     * Check if the line has a custom ptav or not.\n     *\n     * @return {Boolean} - Whether the line has a custom ptav or not.\n     */\n    hasPTAVCustom() {\n        return this.props.attribute_values.some(\n            ptav => ptav.is_custom\n        );\n    }\n }\n", "\nimport { Component } from '@odoo/owl';\n\nexport class QuantityButtons extends Component {\n    static template = 'sale.QuantityButtons';\n    static props = {\n        quantity: Number,\n        setQuantity: Function,\n        isMinusButtonDisabled: { type: Boolean, optional: true },\n        isPlusButtonDisabled: { type: Boolean, optional: true },\n        btnClasses: { type: String, optional: true },\n    };\n\n    /**\n     * Increase the quantity.\n     */\n    increaseQuantity() {\n        this.props.setQuantity(this.props.quantity + 1);\n    }\n\n    /**\n     * Decrease the quantity.\n     */\n    decreaseQuantity() {\n        this.props.setQuantity(this.props.quantity - 1);\n    }\n\n    /**\n     * Set the quantity to a specified value.\n     *\n     * @param {Event} event The quantity input's `on change` event, containing the new quantity.\n     */\n    async setQuantity(event) {\n        const quantity = parseFloat(event.target.value);\n        const didUpdateQuantity = await this.props.setQuantity(isNaN(quantity) ? 0 : quantity);\n        // If the quantity wasn't updated, the component won't rerender, and the input will display\n        // a stale value. As a result, we need to manually rerender the input.\n        if (!didUpdateQuantity) {\n            this.render();\n        }\n    }\n}\n", "import {\n    ProductLabelSectionAndNoteListRender,\n    productLabelSectionAndNoteOne2Many,\n    ProductLabelSectionAndNoteOne2Many,\n} from '@account/components/product_label_section_and_note_field/product_label_section_and_note_field_o2m';\nimport {\n    listSectionAndNoteText,\n    ListSectionAndNoteText,\n    sectionAndNoteFieldOne2Many,\n    sectionAndNoteText,\n    SectionAndNoteText,\n} from '@account/components/section_and_note_fields_backend/section_and_note_fields_backend';\nimport { useSubEnv } from '@odoo/owl';\nimport { registry } from '@web/core/registry';\nimport { CharField } from '@web/views/fields/char/char_field';\n\nfunction getComboRecords(listRecords, record) {\n    const comboRecords = [];\n\n    if (record.data.product_type === 'combo') {\n        // if currernt record is combo then we move forward util we find non combo line\n        comboRecords.push(record);\n        let index = listRecords.indexOf(record) + 1;\n\n        while (index < listRecords.length) {\n            const r = listRecords[index];\n            if (\n                !r.data.combo_item_id?.id\n                || (\n                    r.data.linked_line_id?.id !== record.resId\n                    && r.data.linked_virtual_id !== record.data.virtual_id\n                )\n            ) {\n                break;\n            }\n            comboRecords.push(r);\n            index++;\n        }\n\n    } else if (record.data.combo_item_id?.id) {\n        // if current record is combo item then we move backward util we find associated combo line\n        // Here we assume that the record we get is the last item of the combo \n        let index = listRecords.indexOf(record);\n        while (index >= 0) {\n            const r = listRecords[index];\n            comboRecords.unshift(r);\n\n            if (\n                r.data.product_type === 'combo'\n                && (\n                    r.resId === record.data.linked_line_id?.id\n                    || r.data.virtual_id === record.data.linked_virtual_id\n                )\n            ) {\n                break;\n            }\n            index--;\n        }\n    }\n\n    return comboRecords;\n}\n\nexport class SaleOrderLineListRenderer extends ProductLabelSectionAndNoteListRender {\n    static recordRowTemplate = 'sale.ListRenderer.RecordRow';\n\n    setup(){\n        super.setup();\n        this.priceColumns.push('discount');\n\n        useSubEnv({\n            shouldCollapse: this.shouldCollapse.bind(this),\n        });\n    }\n\n    /**\n     * Little hack to make sure we get correct title field everytime\n     * while accessing comboColumns\n     */\n    get comboColumns() {\n        return [this.titleField, ...this.props.aggregatedFields, 'product_uom_qty', 'discount'];\n    }\n\n    /**\n     * Product description widget logic\n     */\n    getCellTitle(column, record) {\n        // When using this list renderer, we don't want the product_id cell to have a tooltip with\n        // its label.\n        if (column.name === 'product_id' || column.name === 'product_template_id') {\n            return;\n        }\n        return super.getCellTitle(column, record);\n    }\n\n    getActiveColumns() {\n        let activeColumns = super.getActiveColumns();\n        let productTmplCol = activeColumns.find((col) => col.name === 'product_template_id');\n        let productCol = activeColumns.find((col) => col.name === 'product_id');\n\n        if (productCol && productTmplCol) {\n            // Hide the template column if the variant one is enabled.\n            activeColumns = activeColumns.filter((col) => col.name != 'product_template_id')\n        }\n\n        return activeColumns;\n    }\n\n    getRowClass(record) {\n        let classNames = super.getRowClass(record);\n        if (this.isCombo(record) || this.isComboItem(record)) {\n            classNames = classNames.replace('o_row_draggable', '');\n        }\n        return `${classNames} ${this.isCombo(record) ? 'o_is_line_section o_is_line_section_no_indent' : ''}`;\n    }\n\n    isCellReadonly(column, record) {\n        return super.isCellReadonly(column, record) || (\n            this.isComboItem(record)\n                && !['name', 'tax_ids', 'qty_delivered'].includes(column.name)\n        );\n    }\n\n    async onDeleteRecord(record) {\n        if (this.isCombo(record)) {\n            await record.update({ selected_combo_items: JSON.stringify([]) });\n        }\n        await super.onDeleteRecord(record);\n    }\n\n    async moveCombo(record, direction) {\n        const canProceed = await this.props.list.leaveEditMode({ canAbandon: false });\n        if (!canProceed) return;\n\n        const { movingRecords, targetRecords } = this.getComboSwapPairs(record, direction);\n        return this.swapSections(movingRecords, targetRecords);\n    }\n\n    getComboSwapPairs(record, direction) {\n        const comboRecords = getComboRecords(this.props.list.records, record);\n\n        if (direction === 'up') {\n            return {\n                movingRecords: this.getPreviousRecords(record),\n                targetRecords: comboRecords,\n            };\n        }\n        if (direction === 'down') {\n            return {\n                movingRecords: comboRecords,\n                targetRecords: this.getNextRecords(record),\n            };\n        }\n        return { movingRecords: [], targetRecords: [] };\n    }\n\n    getPreviousRecords(record) {\n        const { records } = this.props.list;\n        const previousRecord = records[records.indexOf(record) - 1];\n\n        if (previousRecord?.data.combo_item_id?.id){\n            return getComboRecords(records, previousRecord);\n        }\n        return previousRecord ? [previousRecord] : false;\n    }\n\n    getNextRecords(record) {\n        const { records } = this.props.list;\n        const comboRecords = getComboRecords(records, record);\n\n        const nextRecord = records[records.indexOf(record) + comboRecords.length];\n        if (nextRecord?.data.product_type === 'combo'){\n            return getComboRecords(records, nextRecord);\n        }\n        return nextRecord ? [nextRecord] : false;\n    }\n\n    canUseFormatter(column, record) {\n        if (\n            this.isCombo(record) &&\n            this.props.aggregatedFields.includes(column.name)\n        ) {\n            return true;\n        }\n        return super.canUseFormatter(column, record);\n    }\n\n    // For totals on combo lines\n    getFormattedValue(column, record) {\n        if (this.isCombo(record) && this.props.aggregatedFields.includes(column.name)) {\n            const total = getComboRecords(this.props.list.records, record)\n                .reduce((total, record) => total + record.data[column.name], 0);\n\n            const formatter = registry.category('formatters').get(column.fieldType, (val) => val);\n\n            return formatter(total, {\n                ...formatter.extractOptions?.(column),\n                data: record.data,\n                field: record.fields[column.name],\n            });\n        }\n        return super.getFormattedValue(column, record);\n    }\n\n    isCombo(record) {\n        return record.data.product_type === 'combo';\n    }\n\n    isComboItem(record) {\n        return !!record.data.combo_item_id;\n    }\n\n    shouldDuplicateSectionItem(record) {\n        return !this.isCombo(record) && !this.isComboItem(record);\n    }\n\n    displayDeleteIcon(record){\n        return super.displayDeleteIcon(record) && !this.isComboItem(record);\n    }\n}\n\nexport class SaleOrderLineOne2Many extends ProductLabelSectionAndNoteOne2Many {\n    static components = {\n        ...ProductLabelSectionAndNoteOne2Many.components,\n        ListRenderer: SaleOrderLineListRenderer,\n    };\n}\nexport const saleOrderLineOne2Many = {\n    ...productLabelSectionAndNoteOne2Many,\n    component: SaleOrderLineOne2Many,\n    additionalClasses: sectionAndNoteFieldOne2Many.additionalClasses,\n};\n\nregistry.category('fields').add('sol_o2m', saleOrderLineOne2Many);\n\nexport class SaleOrderLineText extends SectionAndNoteText {\n    get componentToUse() {\n        return this.props.record.data.product_type === 'combo' ? CharField : super.componentToUse;\n    }\n}\n\nexport class ListSaleOrderLineText extends ListSectionAndNoteText {\n    get componentToUse() {\n        return this.props.record.data.product_type === 'combo' ? CharField : super.componentToUse;\n    }\n}\n\nexport const saleOrderLineText = {\n    ...sectionAndNoteText,\n    component: SaleOrderLineText,\n};\n\nexport const listSaleOrderLineText = {\n    ...listSectionAndNoteText,\n    component: ListSaleOrderLineText,\n};\n\nregistry.category('fields').add('sol_text', saleOrderLineText);\nregistry.category('fields').add('list.sol_text', listSaleOrderLineText);\n", "import { useEffect } from \"@odoo/owl\";\nimport { registry } from \"@web/core/registry\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport {\n    KanbanProgressBarField,\n    kanbanProgressBarField,\n} from \"@web/views/fields/progress_bar/kanban_progress_bar_field\";\n\n/**\n * A custom Component for the view of sales teams on the kanban view in the CRM app.\n *\n * The wanted behavior is to show a progress bar when an invoicing target is defined or show\n * a link redirecting to the record's form view otherwise.\n */\nexport class SaleProgressBarField extends KanbanProgressBarField {\n    static template = \"sale.SaleProgressBarField\";\n    /**\n     * Anything used by the component is defined on the setup method.\n     */\n    setup() {\n        super.setup();\n\n        this.actionService = useService(\"action\");\n        this.orm = useService(\"orm\");\n\n        useEffect(() => {\n            this.state.isInvoicingTargetDefined = this.props.record.data[this.props.maxValueField];\n        });\n    }\n\n    /**\n     * Display the form view of the record on click.\n     */\n    async defineInvoicingTarget() {\n        const { resId, resModel } = this.props.record;\n        const action = await this.orm.call(resModel, \"get_formview_action\", [[resId]]);\n        this.actionService.doAction(action);\n    }\n}\n\nexport const saleProgressBarField = {\n    ...kanbanProgressBarField,\n    component: SaleProgressBarField,\n};\n\nregistry.category(\"fields\").add(\"sales_team_progressbar\", saleProgressBarField);\n", "import { markup } from \"@odoo/owl\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { stepUtils } from \"@web_tour/tour_utils\";\n\nregistry.category(\"web_tour.tours\").add(\"sale_tour\", {\n    url: \"/odoo\",\n    steps: () => [\n        stepUtils.showAppsMenuItem(),\n        {\n            isActive: [\"community\"],\n            trigger: \".o_app[data-menu-xmlid='sale.sale_menu_root']\",\n            content: _t(\"Let\u2019s create a beautiful quotation in a few clicks .\"),\n            tooltipPosition: \"right\",\n            run: \"click\",\n        },\n        {\n            isActive: [\"enterprise\"],\n            trigger: \".o_app[data-menu-xmlid='sale.sale_menu_root']\",\n            content: _t(\"Let\u2019s create a beautiful quotation in a few clicks .\"),\n            tooltipPosition: \"bottom\",\n            run: \"click\",\n        },\n        {\n            trigger: \".o_sale_order\",\n        },\n        {\n            trigger: \"button.o_list_button_add\",\n            content: _t(\"Build your first quotation right here!\"),\n            tooltipPosition: \"bottom\",\n            run: \"click\",\n        },\n        {\n            trigger: \".o_sale_order\",\n        },\n        {\n            trigger: \".o_field_res_partner_many2one[name='partner_id'] input\",\n            content: _t(\"Search a customer name, or create one on the fly.\"),\n            tooltipPosition: \"right\",\n            run: \"edit Agrolait\",\n        },\n        {\n            isActive: [\"auto\"],\n            trigger: \".ui-menu-item > a:contains('Agrolait')\",\n            run: \"click\",\n        },\n        {\n            trigger: \".o_field_x2many_list_row_add > a\",\n            content: _t(\"Click here to add some products or services to your quotation.\"),\n            tooltipPosition: \"bottom\",\n            run: \"click\",\n        },\n        {\n            trigger: \".o_sale_order\",\n        },\n        {\n            trigger: `\n                .o_field_widget[name='product_id'] input,\n                .o_field_widget[name='product_template_id'] input\n            `,\n            content: _t(\"Select a product, or create a new one on the fly.\"),\n            tooltipPosition: \"right\",\n            run: \"edit DESK0001\",\n        },\n        {\n            isActive: [\"auto\"],\n            trigger: \"a:contains('DESK0001')\",\n            run: \"click\",\n        },\n        {\n            trigger: \".oi-arrow-right\", // Wait for product creation\n        },\n        {\n            trigger: \".o_field_widget[name='price_unit'] input\",\n            content: _t(\"add the price of your product.\"),\n            tooltipPosition: \"right\",\n            run: \"edit 10.0 && click body\",\n        },\n        {\n            isActive: [\"auto\"],\n            trigger: \".o_field_cell[name='price_subtotal']:contains(10.00)\",\n            run: \"click\",\n        },\n        {\n            isActive: [\"auto\", \"mobile\"],\n            trigger: \".o_statusbar_buttons button[name='action_quotation_send']\",\n        },\n        ...stepUtils.statusbarButtonsSteps(\n            \"Send\",\n            markup(_t(\"<b>Send the quote</b> to yourself and check what the customer will receive.\")),\n        ),\n        {\n            isActive: [\"body:not(:has(.modal-footer button.o_mail_send))\"],\n            trigger: \".modal-footer button[name='document_layout_save']\",\n            content: _t(\"let's continue\"),\n            tooltipPosition: \"bottom\",\n            run: \"click\",\n        },\n        {\n            trigger: \".modal-footer button.o_mail_send\",\n            content: _t(\"Go ahead and send the quotation.\"),\n            tooltipPosition: \"bottom\",\n            run: \"click\",\n        },\n        {\n            isActive: [\"auto\"],\n            trigger: \"body:not(.modal-open)\",\n            run: \"click\",\n        },\n    ],\n});\n", "import { registry } from '@web/core/registry';\nimport { exprToBoolean } from \"@web/core/utils/strings\";\nimport { DocumentFileUploader } from '@account/components/document_file_uploader/document_file_uploader';\n\nconst cogMenuRegistry = registry.category('cogMenu');\n\n/**\n * 'Upload Request for Quotation' Menu\n *\n * This menu allows users to import requests for quotation.\n */\nexport class QuotationRequestUploader extends DocumentFileUploader {\n    static template = 'upload_rfq_cog_menu.QuotationRequestUploader';\n\n    getResModel() {\n        return 'sale.order';\n    }\n}\n\nexport const quotationUploaderMenuItem = {\n    Component: QuotationRequestUploader,\n    groupNumber: 0,\n    isDisplayed: ({ config, searchModel }) =>\n        searchModel.resModel === 'sale.order'\n        && ['list', 'kanban'].includes(config.viewType)\n        && exprToBoolean(config.viewArch.getAttribute('create'), true),\n};\n\ncogMenuRegistry.add('quotation-upload-menu', quotationUploaderMenuItem);\n", "import {\n    ProductLabelSectionAndNoteField,\n    productLabelSectionAndNoteField,\n} from \"@account/components/product_label_section_and_note_field/product_label_section_and_note_field\";\nimport { useEffect } from \"@odoo/owl\";\nimport { serializeDateTime } from \"@web/core/l10n/dates\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { x2ManyCommands } from \"@web/core/orm_service\";\nimport { registry } from \"@web/core/registry\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { uuid } from \"@web/core/utils/strings\";\nimport { ComboConfiguratorDialog } from \"./combo_configurator_dialog/combo_configurator_dialog\";\nimport { ProductCombo } from \"./models/product_combo\";\nimport { ProductConfiguratorDialog } from \"./product_configurator_dialog/product_configurator_dialog\";\nimport { getLinkedSaleOrderLines, serializeComboItem, getSelectedCustomPtav } from \"./sale_utils\";\n\nasync function applyProduct(record, product) {\n    // handle custom values & no variants\n    const customAttributesCommands = [\n        x2ManyCommands.set([]),  // Command.clear isn't supported in static_list/_applyCommands\n    ];\n    for (const ptal of product.attribute_lines) {\n        const selectedCustomPTAV = getSelectedCustomPtav(ptal);\n        if (selectedCustomPTAV) {\n            customAttributesCommands.push(\n                x2ManyCommands.create(undefined, {\n                    custom_product_template_attribute_value_id: [\n                        selectedCustomPTAV.id,\n                        \"we don't care\",\n                    ],\n                    custom_value: ptal.customValue,\n                })\n            );\n        }\n    }\n\n    const noVariantPTAVIds = product.attribute_lines\n        .filter((ptal) => ptal.create_variant === \"no_variant\")\n        .flatMap((ptal) => ptal.selected_attribute_value_ids);\n\n    // We use `_update` (not locked) instead of `update` (locked) so that multiple records can be\n    // updated in parallel (for performance).\n    const update_values = {\n        product_id: { id: product.id, display_name: product.display_name },\n        product_uom_qty: product.quantity,\n        product_no_variant_attribute_value_ids: [x2ManyCommands.set(noVariantPTAVIds)],\n        product_custom_attribute_value_ids: customAttributesCommands,\n    }\n    if (product.uom) {\n        // only update uom field if uom are enabled (uom_data provided), otherwise we don't have the display_name\n        // and the value isn't expected to change anyway.\n        update_values.product_uom_id = product.uom;\n    }\n    await record._update(update_values);\n}\n\nexport class SaleOrderLineProductField extends ProductLabelSectionAndNoteField {\n    static template = \"sale.SaleProductField\";\n    static props = {\n        ...super.props,\n        readonlyField: { type: Boolean, optional: true },\n    };\n\n    setup() {\n        super.setup();\n        this.dialog = useService(\"dialog\");\n        this.notification = useService(\"notification\");\n        this.orm = useService(\"orm\");\n        this.isInternalUpdate = false;\n        this.wasCombo = false;\n        let isMounted = false;\n        useEffect(value => {\n            if (!isMounted) {\n                isMounted = true;\n            } else if (value && this.isInternalUpdate) {\n                // we don't want to trigger product update when update comes from an external sources,\n                // such as an onchange, or the product configuration dialog itself\n                if (this.wasCombo) {\n                    // If the previously selected product was a combo, delete its selected combo\n                    // items before changing the product.\n                    this.props.record.update({ selected_combo_items: JSON.stringify([]) });\n                }\n                if (this.relation === \"product.template\" || this.isCombo) {\n                    this._onProductTemplateUpdate();\n                } else {\n                    this._onProductUpdate();\n                }\n            }\n            this.isInternalUpdate = false;\n        }, () => [this.value && this.value.id]);\n    }\n\n    get productName() {\n        if (this.props.name == 'product_template_id') {\n            const product_id_data = this.props.record.data.product_id;\n            if (product_id_data && product_id_data.display_name) {\n                return product_id_data.display_name.split(\"\\n\")[0];\n            }\n        }\n        return super.productName;\n    }\n    get isProductClickable() {\n        // product form should be accessible if the widget field is readonly\n        // or if the line cannot be edited (e.g. locked SO)\n        return (\n            this.props.readonlyField ||\n            (this.props.record.model.root.activeFields.order_line &&\n                this.props.record.model.root._isReadonly(\"order_line\"))\n        );\n    }\n    get hasConfigurationButton() {\n        return this.isConfigurableTemplate || this.isCombo;\n    }\n    get isConfigurableTemplate() {\n        return this.props.record.data.is_configurable_product;\n    }\n    get isCombo() {\n        return this.props.record.data.product_template_id && this.props.record.data.product_type === 'combo';\n    }\n    get isDownpayment() {\n        return this.props.record.data.is_downpayment;\n    }\n\n    get configurationButtonHelp() {\n        return _t(\"Edit Configuration\");\n    }\n\n    /**\n     * @override\n     */\n    get sectionAndNoteClasses() {\n        return {\n            ...super.sectionAndNoteClasses,\n            \"text-warning\":\n                !this.isSectionOrSubSection && !this.isNote() && !this.productName && !this.isDownpayment,\n        };\n    }\n\n    get label() {\n        let label = this.props.record.data.name;\n        if (this.translatedProductName && label.startsWith(this.translatedProductName)) {\n            // Remove the translated name as it is already shown to the salesman on the SOL.\n            label = label.slice(this.translatedProductName.length + 1);  // + \"\\n\"\n        } else {\n            label = super.label;\n        }\n        return label;\n    }\n\n    get translatedProductName() {\n        return this.props.record.data.translated_product_name;\n    }\n\n    parseLabel(value) {\n        if (!this.translatedProductName) {\n            return super.parseLabel(value);\n        }\n        return value && this.translatedProductName.concat(\"\\n\", value) || this.translatedProductName;\n    }\n\n    get m2oProps() {\n        const p = super.m2oProps;\n        const value = p.value && { ...p.value };\n        if (this.isCombo && value && value.display_name) {\n            // Show the product quantity next to the product name for combo lines.\n            value.display_name = `${value.display_name} x ${this.props.record.data.product_uom_qty}`;\n        }\n        return {\n            ...p,\n            canOpen: this.props.canOpen && (!this.props.readonly || this.isProductClickable),\n            update: (value) => {\n                this.isInternalUpdate = true;\n                this.wasCombo = this.isCombo;\n                return p.update(value);\n            },\n            value,\n        };\n    }\n\n    get relation() {\n        return this.props.record.fields[this.props.name].relation;\n    }\n\n    get value() {\n        return this.props.record.data[this.props.name];\n    }\n\n    async _onProductTemplateUpdate() {\n        const result = await this.orm.call(\n            'product.template',\n            'get_single_product_variant',\n            [this.props.record.data.product_template_id.id],\n            {\n                context: this.context,\n            }\n        );\n        if (result && result.product_id) {\n            if (this.props.record.data.product_id != result.product_id.id) {\n                if (result.is_combo) {\n                    await this.props.record.update({\n                        product_id: { id: result.product_id, display_name: result.product_name },\n                    });\n                    this._openComboConfigurator(false, result.has_optional_products);\n                } else if (result.has_optional_products) {\n                    this._openProductConfigurator();\n                } else {\n                    await this.props.record.update({\n                        product_id: { id: result.product_id, display_name: result.product_name },\n                    });\n                    this._onProductUpdate();\n                }\n            }\n        } else if (!result.mode || result.mode === 'configurator') {\n            this._openProductConfigurator();\n        } else {\n            // only triggered when sale_product_matrix is installed.\n            this._openGridConfigurator();\n        }\n    }\n\n    _openGridConfigurator(edit = false) {} // sale_product_matrix\n\n    async _onProductUpdate() {} // event_booth_sale, event_sale, sale_renting\n\n    onEditConfiguration() {\n        if (this.isCombo) {\n            this._openComboConfigurator(true);\n        } else if (this.isConfigurableTemplate) {\n            this._openProductConfigurator(true);\n        }\n    }\n\n    async _openProductConfigurator(edit = false, selectedComboItems = []) {\n        const saleOrderRecord = this.props.record.model.root;\n        const saleOrderLine = this.props.record.data;\n        const ptavIds = this._getVariantPtavIds(saleOrderLine);\n        let customPtavs = [];\n\n        if (edit) {\n            /**\n             * no_variant and custom attribute don't need to be given to the configurator for new\n             * products.\n             */\n            ptavIds.push(...this._getNoVariantPtavIds(saleOrderLine));\n            customPtavs = await this._getCustomPtavs(saleOrderLine);\n        }\n\n        this.dialog.add(ProductConfiguratorDialog, {\n            productTemplateId: saleOrderLine.product_template_id.id,\n            ptavIds: ptavIds,\n            customPtavs: customPtavs,\n            quantity: saleOrderLine.product_uom_qty,\n            productUOMId: saleOrderLine.product_uom_id.id,\n            companyId: saleOrderRecord.data.company_id.id,\n            pricelistId: saleOrderRecord.data.pricelist_id.id,\n            currencyId: saleOrderLine.currency_id.id,\n            soDate: serializeDateTime(saleOrderRecord.data.date_order),\n            selectedComboItems: selectedComboItems,\n            edit: edit,\n            save: async (mainProduct, optionalProducts) => {\n                await Promise.all([\n                    // Don't add main product if it's a combo product as it has already been added\n                    // from combo configurator\n                    ...(\n                        !selectedComboItems.length ?\n                            [applyProduct(this.props.record, mainProduct)]: []\n                    ),\n                    ...optionalProducts.map(async product => {\n                        const line = await saleOrderRecord.data.order_line.addNewRecord({\n                            position: 'bottom', mode: 'readonly'\n                        });\n                        const productData = this._prepareNewLineData(line, product);\n                        await applyProduct(line, productData);\n                    }),\n                ]);\n                this._onProductUpdate();\n                saleOrderRecord.data.order_line.leaveEditMode();\n            },\n            discard: () => {\n                if (!selectedComboItems.length) {\n                    // Don't delete the main product if it's a combo product as it has been added\n                    // from combo configurator\n                    saleOrderRecord.data.order_line.delete(this.props.record);\n                }\n            },\n            ...this._getAdditionalDialogProps(),\n        });\n    }\n\n    async _openComboConfigurator(edit = false, hasOptionalProducts = false) {\n        const saleOrder = this.props.record.model.root.data;\n        const comboLineRecord = this.props.record;\n        const comboItemLineRecords = getLinkedSaleOrderLines(comboLineRecord).filter(record => !!record.data.combo_item_id);\n        const selectedComboItems = await Promise.all(comboItemLineRecords.map(async record => ({\n            id: record.data.combo_item_id.id,\n            no_variant_ptav_ids: edit ? this._getNoVariantPtavIds(record.data) : [],\n            custom_ptavs: edit ? await this._getCustomPtavs(record.data) : [],\n        })));\n        const { combos, ...remainingData } = await rpc('/sale/combo_configurator/get_data', {\n            product_tmpl_id: comboLineRecord.data.product_template_id.id,\n            currency_id: comboLineRecord.data.currency_id.id,\n            quantity: comboLineRecord.data.product_uom_qty,\n            date: serializeDateTime(saleOrder.date_order),\n            company_id: saleOrder.company_id.id,\n            pricelist_id: saleOrder.pricelist_id.id,\n            selected_combo_items: selectedComboItems,\n            ...this._getAdditionalRpcParams(),\n        });\n\n        const comboChoices = combos.map(combo => new ProductCombo(combo));\n        const preselectedComboItems = comboChoices\n            .map(combo => combo.preselectedComboItem)\n            .filter(Boolean);\n        if (preselectedComboItems.length === comboChoices.length) {\n            return this.handleComboSave(\n                { 'quantity' : remainingData.quantity },\n                preselectedComboItems,\n                edit,\n                hasOptionalProducts\n            );\n        }\n        this.dialog.add(ComboConfiguratorDialog, {\n            combos: comboChoices,\n            ...remainingData,\n            company_id: saleOrder.company_id.id,\n            pricelist_id: saleOrder.pricelist_id.id,\n            date: serializeDateTime(saleOrder.date_order),\n            edit: edit,\n            save: async (comboProductData, selectedComboItems) => {\n                this.handleComboSave(\n                    comboProductData,\n                    selectedComboItems,\n                    edit,\n                    hasOptionalProducts\n                );\n            },\n            discard: () => saleOrder.order_line.delete(comboLineRecord),\n            ...this._getAdditionalDialogProps(),\n        });\n    }\n\n    async handleComboSave(comboProductData, selectedComboItems, edit, hasOptionalProducts) {\n        const saleOrder = this.props.record.model.root.data;\n        const comboLineRecord = this.props.record;\n        saleOrder.order_line.leaveEditMode();\n        const comboLineValues = {\n            product_uom_qty: comboProductData.quantity,\n            selected_combo_items: JSON.stringify(\n                selectedComboItems.map(serializeComboItem)\n            ),\n        };\n        if (!edit) {\n            comboLineValues.virtual_id = uuid();\n        }\n        await comboLineRecord.update(comboLineValues);\n        // Ensure that the order lines are sorted according to their sequence.\n        await saleOrder.order_line._sort();\n\n        if (hasOptionalProducts && !edit) {\n            const selectedComboProducts = selectedComboItems.map(\n                item => ({ name: item.product.display_name })\n            );\n            await this._openProductConfigurator(false, selectedComboProducts);\n        }\n    }\n\n    /**\n     * Hook to append additional RPC params in overriding modules.\n     *\n     * @return {Object} The additional RPC params.\n     */\n    _getAdditionalRpcParams() {\n        return {};\n    }\n\n    /**\n     * Hook to append additional props in overriding modules.\n     *\n     * @return {Object} The additional props.\n     */\n    _getAdditionalDialogProps() {\n        return {};\n    }\n\n    /**\n     * Hook to append extra data in newly created optional product lines.\n     */\n    _prepareNewLineData(_line, product) {\n        return product;\n    }\n\n    /**\n     * Return the PTAV ids of the provided sale order line.\n     *\n     * @param saleOrderLine The sale order line\n     * @return {Number[]} The sale order line's PTAV ids.\n     */\n    _getVariantPtavIds(saleOrderLine) {\n        return saleOrderLine.product_template_attribute_value_ids.currentIds;\n    }\n\n    /**\n     * Return the `no_variant` PTAV ids of the provided sale order line.\n     *\n     * @param saleOrderLine The sale order line\n     * @return {Number[]} The sale order line's `no_variant` PTAV ids.\n     */\n    _getNoVariantPtavIds(saleOrderLine) {\n        return saleOrderLine.product_no_variant_attribute_value_ids.currentIds;\n    }\n\n    /**\n     * Return the custom PTAVs of the provided sale order line.\n     *\n     * @param saleOrderLine The sale order line\n     * @return {Promise<CustomPtav[]>} The sale order line's custom PTAVs.\n     */\n    async _getCustomPtavs(saleOrderLine) {\n        // `product.attribute.custom.value` records are not loaded in the view because sub templates\n        // are not loaded in list views. Therefore, we fetch them from the server if the record was\n        // saved. Otherwise, we use the value stored on the line.\n        const customPtavIds = saleOrderLine.product_custom_attribute_value_ids;\n        let customPtavs = [];\n        if (customPtavIds.records[0]?.isNew) {\n            customPtavs = customPtavIds.records.map(record => record.data);\n        } else if (customPtavIds.currentIds.length) {\n            const specification = {\n                custom_product_template_attribute_value_id: {\n                    fields: { id: {} },\n                },\n                custom_value: {},\n            };\n            customPtavs = await this.orm.webRead(\n                'product.attribute.custom.value',\n                customPtavIds.currentIds,\n                { specification },\n            );\n        }\n        return customPtavs.map(customPtav => ({\n            id: customPtav.custom_product_template_attribute_value_id &&\n                customPtav.custom_product_template_attribute_value_id.id,\n            value: customPtav.custom_value,\n        }));\n    }\n}\n\nexport const saleOrderLineProductField = {\n    ...productLabelSectionAndNoteField,\n    component: SaleOrderLineProductField,\n    extractProps(fieldInfo, dynamicInfo) {\n        return {\n            ...productLabelSectionAndNoteField.extractProps(fieldInfo, dynamicInfo),\n            readonlyField: dynamicInfo.readonly,\n        };\n    },\n    fieldDependencies: [\n        { name: 'is_configurable_product', type: 'boolean' },\n        { name: 'product_type', type: 'selection' },\n        { name: 'service_tracking', type: 'selection' },\n        { name: 'product_template_attribute_value_ids', type: 'many2many' },\n        { name: 'translated_product_name', type: 'char' },\n    ],\n};\n\nregistry.category(\"fields\").add(\"sol_product_many2one\", saleOrderLineProductField);\n", "/**\n * Checks whether the 2 provided sale order lines are linked.\n *\n * @param linkingSaleOrderLine The line that is linking to the other line.\n * @param linkedSaleOrderLine The line that is linked by the other line.\n * @return {Boolean} Whether the 2 lines are linked.\n */\nexport function areSaleOrderLinesLinked(linkingSaleOrderLine, linkedSaleOrderLine) {\n    const linkingId = linkedSaleOrderLine.isNew\n        ? linkingSaleOrderLine.data.linked_virtual_id\n        : linkingSaleOrderLine.data.linked_line_id.id;\n    const linkedId = linkedSaleOrderLine.isNew\n        ? linkedSaleOrderLine.data.virtual_id\n        : linkedSaleOrderLine.resId;\n    return linkingId && linkingId === linkedId;\n}\n\n/**\n * Gets the linked lines of the provided sale order line.\n *\n * @param saleOrderLine The line whose linked lines to get.\n * @return {Object[]} The list of linked lines.\n */\nexport function getLinkedSaleOrderLines(saleOrderLine) {\n    const saleOrder = saleOrderLine.model.root;\n    // TODO(loti): this leaves out any combo items that are on another page.\n    return saleOrder.data.order_line.records.filter(\n        record => areSaleOrderLinesLinked(record, saleOrderLine)\n    );\n}\n\n/**\n * Serialize a combo item into a format understandable by the server.\n *\n * @param {ProductComboItem} comboItem The combo item to serialize.\n * @return {Object} The serialized combo item.\n */\nexport function serializeComboItem(comboItem) {\n    return {\n        combo_item_id: comboItem.id,\n        product_id: comboItem.product.id,\n        no_variant_attribute_value_ids: comboItem.product.selectedNoVariantPtavIds,\n        product_custom_attribute_values: comboItem.product.selectedCustomPtavs.map(\n            customPtav => ({\n                custom_product_template_attribute_value_id: customPtav.id,\n                custom_value: customPtav.value,\n            })\n        ),\n    }\n}\n\n/**\n * Get the selected custom PTAV in the provided PTAL, if any.\n *\n * Note: a PTAL can have at most one selected custom PTAV, by design.\n *\n * @param {ProductTemplateAttributeLine.props} ptal The PTAL in which to look for the selected\n *     custom PTAV.\n * @return {Object|undefined} The selected custom PTAV, if any.\n *\n */\nexport function getSelectedCustomPtav(ptal) {\n    const selectedPtavIds = new Set(ptal.selected_attribute_value_ids);\n    return ptal.attribute_values.find(ptav => ptav.is_custom && selectedPtavIds.has(ptav.id));\n}\n", "import { FileUploadKanbanController } from '@account/views/file_upload_kanban/file_upload_kanban_controller';\n\nexport class SaleFileUploadKanbanController extends FileUploadKanbanController {\n    setup() {\n        super.setup();\n        this.hideUploadButton = true;\n    }\n};\n", "import { _t } from '@web/core/l10n/translation';\nimport { FileUploadKanbanRenderer } from '@account/views/file_upload_kanban/file_upload_kanban_renderer';\n\nexport class SaleFileUploadKanbanRenderer extends FileUploadKanbanRenderer {\n    setup() {\n        super.setup();\n        this.dropZoneTitle = _t(\"Import a request for quotation from a customer\");\n        this.dropZoneDescription = _t(`\n            If your customer runs on Odoo 18 or higher, customer data and sales order lines\n            will be automatically created. Any other pdf containing an attached\n            UBL-RequestForQuotation file will work as well.\n        `);\n    }\n}\n", "import { registry } from '@web/core/registry';\nimport { fileUploadKanbanView } from '@account/views/file_upload_kanban/file_upload_kanban_view';\nimport { SaleFileUploadKanbanController } from './sale_file_upload_kanban_controller';\nimport { SaleFileUploadKanbanRenderer } from './sale_file_upload_kanban_renderer';\n\nexport const saleFileUploadKanbanView = {\n    ...fileUploadKanbanView,\n    Controller: SaleFileUploadKanbanController,\n    Renderer: SaleFileUploadKanbanRenderer,\n};\n\nregistry.category('views').add('sale_file_upload_kanban', saleFileUploadKanbanView);\n", "import { FileUploadListController } from '@account/views/file_upload_list/file_upload_list_controller';\n\nexport class SaleFileUploadListController extends FileUploadListController {\n    setup() {\n        super.setup();\n        this.hideUploadButton = true;\n    }\n};\n", "import { _t } from '@web/core/l10n/translation';\nimport { FileUploadListRenderer } from '@account/views/file_upload_list/file_upload_list_renderer';\n\nexport class SaleFileUploadListRenderer extends FileUploadListRenderer {\n    setup() {\n        super.setup();\n        this.dropZoneTitle = _t(\"Import a request for quotation from a customer\");\n        this.dropZoneDescription = _t(`\n            If your customer runs on Odoo 18 or higher, customer data and sales order lines\n            will be automatically created. Any other pdf containing an attached\n            UBL-RequestForQuotation file will work as well.\n        `);\n    }\n}\n", "import { registry } from '@web/core/registry';\nimport { fileUploadListView } from '@account/views/file_upload_list/file_upload_list_view';\nimport { SaleFileUploadListController } from './sale_file_upload_list_controller';\nimport { SaleFileUploadListRenderer } from './sale_file_upload_list_renderer';\n\nexport const saleFileUploadListView = {\n    ...fileUploadListView,\n    Controller: SaleFileUploadListController,\n    Renderer: SaleFileUploadListRenderer,\n};\n\nregistry.category('views').add('sale_file_upload_list', saleFileUploadListView);\n", "import { SaleFileUploadKanbanRenderer } from '../sale_file_upload_kanban/sale_file_upload_kanban_renderer';\nimport { SaleActionHelper } from \"../../js/sale_action_helper/sale_action_helper\";\n\nexport class SaleKanbanRenderer extends SaleFileUploadKanbanRenderer {\n    static template = \"sale.SaleKanbanRenderer\";\n    static components = {\n        ...SaleFileUploadKanbanRenderer.components,\n        SaleActionHelper,\n    };\n};\n", "import { registry } from \"@web/core/registry\";\nimport { saleFileUploadKanbanView } from \"../sale_file_upload_kanban/sale_file_upload_kanban_view\";\nimport { SaleKanbanRenderer } from \"./sale_onboarding_kanban_renderer\";\n\nexport const saleKanbanView = {\n    ...saleFileUploadKanbanView,\n    Renderer: SaleKanbanRenderer,\n};\n\nregistry.category(\"views\").add(\"sale_onboarding_kanban\", saleKanbanView);\n", "import { SaleFileUploadListRenderer } from '../sale_file_upload_list/sale_file_upload_list_renderer';\nimport { SaleActionHelper } from \"../../js/sale_action_helper/sale_action_helper\";\n\nexport class SaleListRenderer extends SaleFileUploadListRenderer {\n    static template = \"sale.SaleListRenderer\";\n    static components = {\n        ...SaleFileUploadListRenderer.components,\n        SaleActionHelper,\n    };\n};\n", "import { registry } from \"@web/core/registry\";\nimport { saleFileUploadListView } from '../sale_file_upload_list/sale_file_upload_list_view';\nimport { SaleListRenderer } from \"./sale_onboarding_list_renderer\";\n\nexport const SaleListView = {\n    ...saleFileUploadListView,\n    Renderer: SaleListRenderer,\n};\n\nregistry.category(\"views\").add(\"sale_onboarding_list\", SaleListView);\n", "import { getSectionRecords } from '@account/components/section_and_note_fields_backend/section_and_note_fields_backend';\nimport { SaleOrderLineListRenderer } from '@sale/js/sale_order_line_field/sale_order_line_field';\nimport { makeContext } from '@web/core/context';\nimport { x2ManyCommands } from '@web/core/orm_service';\nimport { patch } from '@web/core/utils/patch';\n\npatch(SaleOrderLineListRenderer.prototype, {\n\n    setup() {\n        super.setup();\n        this.copyFields.push('is_optional');\n    },\n\n    /**\n     * Disable \"Hide Composition\" and \"Hide Prices\" buttons for optional sections and their\n     * subsections.\n     */\n    get disableCompositionButton() {\n        return (\n            super.disableCompositionButton\n            || this.shouldCollapse(this.record, 'is_optional', true)\n        );\n    },\n\n    get disablePricesButton() {\n        return (\n            super.disablePricesButton\n            || this.shouldCollapse(this.record, 'is_optional', true)\n        );\n    },\n\n    /**\n     * Disable \"Set Optional\" button if\n     *  - Parent section is optional\n     *  - Parent section hides prices or composition\n     *  - Section itself hides prices or composition\n     */\n    get disableOptionalButton() {\n        return (\n            this.shouldCollapse(this.record, 'is_optional')\n            || this.shouldCollapse(this.record, 'collapse_prices', true)\n            || this.shouldCollapse(this.record, 'collapse_composition', true)\n        );\n    },\n\n    get isCurrentSectionOptional() {\n        if (this.props.list.records.length === 0) return false;\n\n        return this.shouldCollapse(\n            this.props.list.records[this.props.list.records.length - 1],\n            'is_optional',\n            true\n        );\n    },\n\n    /**\n     * Override to set the default `product_uom_qty` to 0 for new lines created under an optional\n     * section.\n     */\n    add(params){\n        params.context = this.getCreateContext(params);\n        super.add(params);\n    },\n\n    getCreateContext(params) {\n        const evaluatedContext = makeContext([params.context]);\n        // A falsy context indicates a product line (no `display_type` specified)\n        if(!evaluatedContext[`default_display_type`] && this.isCurrentSectionOptional) {\n            return { ...evaluatedContext, default_product_uom_qty: 0 };\n        }\n        return params.context;\n    },\n\n    /**\n     * Override to set the default `product_uom_qty` to 0 for new lines inserted by optional\n     * sections from dropdown.\n     */\n    getInsertLineContext(record, addSubSection) {\n        if (this.shouldCollapse(record, 'is_optional', true) && !addSubSection) {\n            return {\n                ...super.getInsertLineContext(record, addSubSection),\n                default_product_uom_qty: 0\n            };\n        }\n        return super.getInsertLineContext(record, addSubSection);\n    },\n\n    getRowClass(record) {\n        let rowClasses = super.getRowClass(record);\n        if (this.shouldCollapse(record, 'is_optional', true)) {\n            rowClasses += ' text-primary';\n        }\n        return rowClasses;\n    },\n\n    /**\n     * @override\n     * This override resets optional state of subsections when their parent sections is collapsed\n     */\n    async toggleCollapse(record, fieldName) {\n        await super.toggleCollapse(record, fieldName);\n\n        if (this.isTopSection(record) && record.data[fieldName]) {\n            const commands = [];\n\n            for (const sectionRecord of getSectionRecords(this.props.list, record)) {\n                if (this.isSubSection(sectionRecord)) {\n                    commands.push(\n                        x2ManyCommands.update(sectionRecord.resId || sectionRecord._virtualId, {\n                            is_optional: false,\n                        })\n                    )\n                }\n            }\n\n            if (commands.length) {\n                await this.props.list.applyCommands(commands, { sort: true });\n            }\n        }\n    },\n\n    /**\n     * Toggles optional state on a section:\n     * - Product lines \u2192 qty = 0 when set optional, reset to 1 when unset.\n     * - Subsections \u2192 force hide composition/prices to false.\n     */\n    async toggleIsOptional(record) {\n        const setOptional = !record.data.is_optional;\n\n        const commands = [(x2ManyCommands.update(record.resId || record._virtualId, {\n            is_optional: setOptional,\n        }))];\n\n        for (const sectionRecord of getSectionRecords(this.props.list, record)) {\n            let changes = {};\n\n            if (!sectionRecord.data.display_type) {\n                changes = setOptional\n                    ? { product_uom_qty: 0, price_total: 0, price_subtotal: 0 }\n                    : { product_uom_qty: sectionRecord.data.product_uom_qty || 1 };\n            } else if (this.isSubSection(sectionRecord)) {\n                changes = setOptional && {\n                    collapse_composition: false,\n                    collapse_prices: false,\n                };\n            }\n\n            if (Object.keys(changes).length) {\n                commands.push(\n                    x2ManyCommands.update(\n                        sectionRecord.resId || sectionRecord._virtualId,\n                        changes\n                    )\n                );\n            }\n        }\n\n        await this.props.list.applyCommands(commands, { sort: true });\n    },\n\n    /**\n     * @override\n     * Handles product line quantity adjustments when a record is dragged and dropped.\n     *\n     * Behavior:\n     * - If a product line is moved under an optional section, its quantity is set to `0`.\n     * - If a product line is dragged out of an optional section and had `0` quantity,\n     *   its quantity is reset to `1`.\n     * - Non-product lines (`display_type` set) are ignored.\n     */\n    async sortDrop(dataRowId, dataGroupId, { element, previous }) {\n        const record = this.props.list.records.find(r => r.id === dataRowId);\n        const recordMap = this._getRecordsToRecompute(record, previous ? previous.dataset.id : null);\n\n        await super.sortDrop(dataRowId, dataGroupId, { element, previous });\n\n        await this._handleQuantityAdjustment(recordMap);\n    },\n\n    /**\n     * Builds a map of records whose optional state needs to be recomputed\n     * after a record is moved within the list.\n     *\n     * The map\u2019s keys are record IDs, and values represent their current\n     * `is_optional` collapse state as determined by `shouldCollapse()`.\n     *\n     * @param {Object} record - The record being moved.\n     * @param {number|string} targetId - The ID of the record that serves as the new drop target.\n     * @returns {Map<number|string, boolean>} A map of record IDs to their recomputed optional states.\n     */\n    _getRecordsToRecompute(record, targetId) {\n        const optionalStateMap = new Map();\n\n        if (this.isSection(record)) { // If a section or subsection is moved\n            let currentIndex = this.props.list.records.indexOf(record);\n            let targetIndex = this.props.list.records.findIndex(r => r.id === targetId);\n            if (currentIndex > targetIndex) {\n                //When moving up, recompute:\n                // 1. All records under the moved section.\n                // 2. All records between the new and old positions.\n                for (let i = currentIndex; i > targetIndex; i--) {\n                    if (!this.props.list.records[i].data.display_type) {\n                        optionalStateMap.set(\n                            this.props.list.records[i].id,\n                            this.shouldCollapse(this.props.list.records[i], 'is_optional')\n                        );\n                    }\n                }\n                for (const sectionRecord of getSectionRecords(this.props.list, record)) {\n                    if (!sectionRecord.data.display_type) {\n                        optionalStateMap.set(sectionRecord.id, this.shouldCollapse(sectionRecord, 'is_optional'));\n                    }\n                }\n            } else {\n                //When moving down, recompute:\n                // 1. All records under sections between the old and new positions.\n                // 2. All records between the old and new positions (skipping overlaps).\n                for (let i = currentIndex; i <= targetIndex; i++) {\n                    if (this.isSection(this.props.list.records[i])) {\n                        for (const sectionRecord of getSectionRecords(this.props.list, this.props.list.records[i])) {\n                            if (\n                                !optionalStateMap.has(sectionRecord.id)\n                                && !sectionRecord.data.display_type\n                            ) {\n                                optionalStateMap.set(\n                                    sectionRecord.id,\n                                    this.shouldCollapse(sectionRecord, 'is_optional')\n                                );\n                            }\n                        }\n                    }\n\n                    // we must skip overlapping records\n                    if (\n                        !optionalStateMap.has(this.props.list.records[i].id)\n                        && !this.props.list.records[i].data.display_type\n                    ) {\n                        optionalStateMap.set(\n                            this.props.list.records[i].id,\n                            this.shouldCollapse(this.props.list.records[i], 'is_optional')\n                        );\n                    }\n                }\n            }\n        } else if (!record.data.display_type) { // If a regular record is moved compute its own optional state\n            optionalStateMap.set(record.id, this.shouldCollapse(record, 'is_optional'));\n        }\n\n        return optionalStateMap;\n    },\n\n    async _handleQuantityAdjustment(recordMap) {\n        const commands = [];\n\n        for (const [recordId, wasOptional] of recordMap.entries()) {\n            const record = this.props.list.records.find(r => r.id === recordId);\n            const isOptional = this.shouldCollapse(record, 'is_optional');\n\n            if (wasOptional && !isOptional && !record.data.product_uom_qty) {\n                commands.push(x2ManyCommands.update(\n                    record.resId || record._virtualId, { product_uom_qty: 1 }\n                ));\n            } else if (!wasOptional && isOptional) {\n                commands.push(x2ManyCommands.update(\n                    record.resId || record._virtualId, { product_uom_qty: 0 }\n                ));\n            }\n        }\n\n        await this.props.list.applyCommands(commands, { sort: true });\n    },\n\n    /**\n     * @override\n     * Reset fields when a subsection is moved under an optional section,\n     * since optional sections cannot contain hidden subsections or hidden prices.\n     */\n    resetOnResequence(record, parentSection) {\n        return (\n            super.resetOnResequence(record, parentSection)\n            || (\n                this.isSubSection(record)\n                && parentSection?.data.is_optional\n                && (\n                    record.data.collapse_composition\n                    || record.data.collapse_prices\n                    || record.data.is_optional\n                )\n            )\n        );\n    },\n\n    fieldsToReset() {\n        return { ...super.fieldsToReset(), is_optional: false };\n    },\n\n    async moveCombo(record, direction) {\n        const wasOptional = this.shouldCollapse(record, 'is_optional');\n\n        await super.moveCombo(record, direction);\n\n        const isOptional = this.shouldCollapse(record, 'is_optional');\n\n        if (wasOptional && !isOptional && !record.data.product_uom_qty) {\n            await record.update({ product_uom_qty: 1 });\n        } else if (!wasOptional && isOptional) {\n            await record.update({ product_uom_qty: 0 });\n        }\n    }\n});\n", "import {\n    SectionAndNoteFieldOne2Many,\n    sectionAndNoteFieldOne2Many,\n    SectionAndNoteListRenderer,\n    getSectionRecords,\n} from '@account/components/section_and_note_fields_backend/section_and_note_fields_backend';\nimport { makeContext } from '@web/core/context';\nimport { x2ManyCommands } from '@web/core/orm_service';\nimport { registry } from '@web/core/registry';\n\nexport class SaleOrderTemplateLineListRenderer extends SectionAndNoteListRenderer {\n    static recordRowTemplate = 'sale_management.ListRenderer.RecordRow';\n\n    setup() {\n        super.setup();\n        this.copyFields.push('is_optional');\n    }\n\n    get disableOptionalButton() {\n        return this.shouldCollapse(this.record, 'is_optional');\n    }\n\n    get isCurrentSectionOptional() {\n        if (this.props.list.records.length === 0) return false;\n\n        return this.shouldCollapse(\n            this.props.list.records[this.props.list.records.length - 1],\n            'is_optional',\n            true\n        );\n    }\n\n    /**\n     * Override to set the default `product_uom_qty` to 0 for new lines created under an optional\n     * section.\n     */\n    add(params){\n        params.context = this.getCreateContext(params);\n        super.add(params);\n    }\n\n    getCreateContext(params) {\n        const evaluatedContext = makeContext([params.context]);\n        // A falsy context indicates a product line (no `display_type` specified)\n        if(!evaluatedContext[`default_display_type`] && this.isCurrentSectionOptional) {\n            return { ...evaluatedContext, default_product_uom_qty: 0 };\n        }\n        return params.context;\n    }\n\n    /**\n     * Override to set the default `product_uom_qty` to 0 for new lines inserted by optional\n     * sections from dropdown.\n     */\n    getInsertLineContext(record, addSubSection) {\n        if (this.shouldCollapse(record, 'is_optional', true) && !addSubSection) {\n            return {\n                ...super.getInsertLineContext(record, addSubSection),\n                default_product_uom_qty: 0\n            };\n        }\n        return super.getInsertLineContext(record, addSubSection);\n    }\n\n    getRowClass(record) {\n        let rowClasses = super.getRowClass(record);\n        if (this.shouldCollapse(record, 'is_optional', true)) {\n            rowClasses += ' text-primary';\n        }\n        return rowClasses;\n    }\n\n    async toggleIsOptional(record) {\n        const setOptional = !record.data.is_optional;\n\n        const commands = [(x2ManyCommands.update(record.resId || record._virtualId, {\n            is_optional: setOptional,\n        }))];\n\n        for (const sectionRecord of getSectionRecords(this.props.list, record)) {\n            let changes = {};\n\n            if (!sectionRecord.data.display_type) {\n                changes = setOptional\n                    ? { product_uom_qty: 0 }\n                    : { product_uom_qty: sectionRecord.data.product_uom_qty || 1 };\n            }\n\n            if (Object.keys(changes).length) {\n                commands.push(\n                    x2ManyCommands.update(\n                        sectionRecord.resId || sectionRecord._virtualId,\n                        changes\n                    )\n                );\n            }\n        }\n\n        await this.props.list.applyCommands(commands, { sort: true });\n    }\n\n    /**\n     * @override\n     * Handles product line quantity adjustments when a record is dragged and dropped.\n     *\n     * Behavior:\n     * - If a product line is moved under an optional section, its quantity is set to `0`.\n     * - If a product line is dragged out of an optional section and had `0` quantity,\n     *   its quantity is reset to `1`.\n     * - Non-product lines (`display_type` set) are ignored.\n     *\n     */\n    async sortDrop(dataRowId, dataGroupId, { element, previous }) {\n        const record = this.props.list.records.find(r => r.id === dataRowId);\n        const recordMap = this._getRecordsToRecompute(record, previous ? previous.dataset.id : null);\n\n        await super.sortDrop(dataRowId, dataGroupId, { element, previous });\n\n        await this._handleQuantityAdjustment(recordMap);\n    }\n\n    /**\n     * Builds a map of records whose optional state needs to be recomputed\n     * after a record is moved within the list.\n     *\n     * The map\u2019s keys are record IDs, and values represent their current\n     * `is_optional` collapse state as determined by `shouldCollapse()`.\n     *\n     * @param {Object} record - The record being moved.\n     * @param {number|string} targetId - The ID of the record that serves as the new drop target.\n     * @returns {Map<number|string, boolean>} A map of record IDs to their recomputed optional states.\n     */\n    _getRecordsToRecompute(record, targetId) {\n        const optionalStateMap = new Map();\n\n        if (this.isSection(record)) { // If a section or subsection is moved\n            let currentIndex = this.props.list.records.indexOf(record);\n            let targetIndex = this.props.list.records.findIndex(r => r.id === targetId);\n            if (currentIndex > targetIndex) {\n                //When moving up, recompute:\n                // 1. All records under the moved section.\n                // 2. All records between the new and old positions.\n                for (let i = currentIndex; i > targetIndex; i--) {\n                    if (!this.props.list.records[i].data.display_type) {\n                        optionalStateMap.set(\n                            this.props.list.records[i].id,\n                            this.shouldCollapse(this.props.list.records[i], 'is_optional')\n                        );\n                    }\n                }\n                for (const sectionRecord of getSectionRecords(this.props.list, record)) {\n                    if (!sectionRecord.data.display_type) {\n                        optionalStateMap.set(sectionRecord.id, this.shouldCollapse(sectionRecord, 'is_optional'));\n                    }\n                }\n            } else {\n                //When moving down, recompute:\n                // 1. All records under sections between the old and new positions.\n                // 2. All records between the old and new positions (skipping overlaps).\n                for (let i = currentIndex; i <= targetIndex; i++) {\n                    if (this.isSection(this.props.list.records[i])) {\n                        for (const sectionRecord of getSectionRecords(this.props.list, this.props.list.records[i])) {\n                            if (\n                                !optionalStateMap.has(sectionRecord.id)\n                                && !sectionRecord.data.display_type\n                            ) {\n                                optionalStateMap.set(\n                                    sectionRecord.id,\n                                    this.shouldCollapse(sectionRecord, 'is_optional')\n                                );\n                            }\n                        }\n                    }\n\n                    // we must skip overlapping records\n                    if (\n                        !optionalStateMap.has(this.props.list.records[i].id)\n                        && !this.props.list.records[i].data.display_type\n                    ) {\n                        optionalStateMap.set(\n                            this.props.list.records[i].id,\n                            this.shouldCollapse(this.props.list.records[i], 'is_optional')\n                        );\n                    }\n                }\n            }\n        } else if (!record.data.display_type) { // If a regular record is moved compute its own optional state\n            optionalStateMap.set(record.id, this.shouldCollapse(record, 'is_optional'));\n        }\n\n        return optionalStateMap;\n    }\n\n    async _handleQuantityAdjustment(recordMap) {\n        const commands = [];\n\n        for (const [recordId, wasOptional] of recordMap.entries()) {\n            const record = this.props.list.records.find(r => r.id === recordId);\n            const isOptional = this.shouldCollapse(record, 'is_optional');\n\n            if (wasOptional && !isOptional && !record.data.product_uom_qty) {\n                commands.push(x2ManyCommands.update(\n                    record.resId || record._virtualId, { product_uom_qty: 1 }\n                ));\n            } else if (!wasOptional && isOptional) {\n                commands.push(x2ManyCommands.update(\n                    record.resId || record._virtualId, { product_uom_qty: 0 }\n                ));\n            }\n        }\n\n        await this.props.list.applyCommands(commands, { sort: true });\n    }\n\n}\nexport class SaleOrderTemplateLineOne2Many extends SectionAndNoteFieldOne2Many {\n    static components = {\n        ...super.components,\n        ListRenderer: SaleOrderTemplateLineListRenderer,\n    };\n}\n\nexport const saleOrderTemplateLineOne2Many = {\n    ...sectionAndNoteFieldOne2Many,\n    component: SaleOrderTemplateLineOne2Many,\n};\n\nregistry.category('fields').add('so_template_line_o2m', saleOrderTemplateLineOne2Many);\n", "import { SaleOrderLineProductField } from '@sale/js/sale_product_field';\nimport { patch } from '@web/core/utils/patch';\n\npatch(SaleOrderLineProductField.prototype, {\n    _getAdditionalDialogProps() {\n        const props = super._getAdditionalDialogProps();\n\n        const isOptionalLine = this.env.shouldCollapse(this.props.record, 'is_optional');\n        props.options = {\n            showQuantity: !isOptionalLine,\n            showPrice: !isOptionalLine,\n        };\n\n        return props;\n    },\n\n    _prepareNewLineData(line, product) {\n        const data = super._prepareNewLineData(line, product);\n        if (this.env.shouldCollapse(line, 'is_optional')) {\n            data.quantity = 0;\n        }\n        return data;\n    }\n});\n", "import { CodeEditor } from \"@web/core/code_editor/code_editor\";\nimport { ConfirmationDialog } from \"@web/core/confirmation_dialog/confirmation_dialog\";\nimport { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { CheckboxItem } from \"@web/core/dropdown/checkbox_item\";\nimport { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { SelectMenu } from \"@web/core/select_menu/select_menu\";\nimport { user } from \"@web/core/user\";\nimport { sortBy } from \"@web/core/utils/arrays\";\nimport { KeepLast } from \"@web/core/utils/concurrency\";\nimport { useService } from \"@web/core/utils/hooks\";\n\nimport { ResourceEditorWarningOverlay } from \"./resource_editor_warning\";\nimport { checkSCSS, checkXML, formatXML } from \"./utils\";\n\nimport { Component, onWillUnmount, onWillStart, reactive, useRef, useState } from \"@odoo/owl\";\n\nconst BUNDLES_RESTRICTION = [\n    \"web.assets_frontend\",\n    \"web.assets_frontend_minimal\",\n    \"web.assets_frontend_lazy\",\n];\n\nexport class ResourceEditor extends Component {\n    static components = {\n        ResourceEditorWarningOverlay,\n        CodeEditor,\n        Dropdown,\n        CheckboxItem,\n        DropdownItem,\n        SelectMenu,\n    };\n    static template = \"website.ResourceEditor\";\n    static props = {\n        close: { type: Function, optional: true },\n    };\n    static defaultProps = {\n        close: () => {},\n    };\n\n    setup() {\n        this.website = useService(\"website\");\n        this.orm = useService(\"orm\");\n        this.dialog = useService(\"dialog\");\n\n        this.keepLast = new KeepLast();\n\n        this.editorRef = useRef(\"editor\");\n\n        this.debug = this.env.debug;\n        this.viewKey =\n            this.website.pageDocument &&\n            this.website.pageDocument.documentElement.dataset.viewXmlid;\n\n        this.types = {\n            xml: \"XML (HTML)\",\n            scss: \"SCSS (CSS)\",\n            js: \"JS\",\n        };\n        this.typeToCodeEditorModeMap = {\n            xml: \"qweb\",\n            scss: \"scss\",\n            js: \"javascript\",\n        };\n\n        this.xmlFilters = {\n            views: _t(\"Only Views\"),\n            all: _t(\"Views and Assets bundles\"),\n        };\n        this.scssFilters = {\n            custom: _t(\"Only Custom SCSS Files\"),\n            restricted: _t(\"Only Page SCSS Files\"),\n            all: _t(\"All SCSS Files\"),\n        };\n        this.state = useState({\n            type: \"xml\",\n            xmlFilter: \"views\",\n            scssFilter: \"custom\",\n            currentResource: false,\n            showEditWarning: true,\n            resources: {\n                xml: {},\n                js: {},\n                scss: {},\n            },\n            sortedXML: [],\n            sortedSCSS: [],\n            sortedJS: [],\n            saving: false,\n        });\n\n        let showErrorInterval;\n        this.errors = reactive([], () => {\n            clearInterval(showErrorInterval);\n            if (this.errors.length) {\n                this.showErrorLine();\n                // The ace library updates its content asynchronously, and sometimes\n                // at unexpected moments, so we consistently re-apply the error indicators\n                // when they are errors. This is kind of a hack, but it works.\n                showErrorInterval = setInterval(() => this.showErrorLine(), 500);\n            } else {\n                this.clearErrorLine();\n            }\n        });\n        onWillUnmount(() => clearInterval(showErrorInterval));\n\n        onWillStart(async () => this.loadResources());\n    }\n\n    // -------------------------------------------------------------------------\n    // Getters\n    // -------------------------------------------------------------------------\n\n    get context() {\n        return {\n            ...user.context,\n            website_id: this.website.currentWebsite.id,\n        };\n    }\n\n    get resourceInfo() {\n        if (!this.state.currentResource) {\n            return \"\";\n        }\n        if (this.state.type === \"xml\") {\n            return _t(\"Template ID: %s\", this.state.currentResource.key);\n        } else if (this.state.type === \"scss\") {\n            return _t(\"SCSS file: %s\", this.state.currentResource.url);\n        } else {\n            return _t(\"JS file: %s\", this.state.currentResource.url);\n        }\n    }\n\n    get selectMenuProps() {\n        const props = {\n            onSelect: (value) => {\n                this.state.currentResource = this.state.resources[this.state.type][value];\n            },\n            autoSort: false,\n            required: true,\n        };\n        if (this.state.type === \"xml\") {\n            const choices = this.state.sortedXML.map((view) => ({\n                value: view.id,\n                label: view.label,\n            }));\n            const value = this.state.currentResource?.id;\n            return { ...props, choices, value };\n        } else {\n            const { type, sortedSCSS, sortedJS } = this.state;\n            const bundles = type === \"scss\" ? sortedSCSS : sortedJS;\n            const groups = bundles.map(([name, files]) => {\n                const choices = files.map((file) => ({ value: file.url, label: file.label }));\n                return { label: name, choices };\n            });\n            const value = this.state.currentResource?.url;\n            return { ...props, groups, value };\n        }\n    }\n\n    // -------------------------------------------------------------------------\n    // Methods\n    // -------------------------------------------------------------------------\n\n    /**\n     * Checks resource is customized or not.\n     *\n     * @param {string} url\n     * @returns {boolean}\n     */\n    isCustomResource(url) {\n        // TODO we should be able to detect if the XML template is customized\n        // to not show the warning in that case\n        if (this.state.type === \"scss\") {\n            return this.state.resources.scss[url].customized;\n        } else if (this.state.type === \"js\") {\n            return this.state.resources.js[url].customized;\n        }\n        return false;\n    }\n\n    async loadResources() {\n        const resources = await this.keepLast.add(\n            rpc(\"/website/get_assets_editor_resources\", {\n                key: this.viewKey,\n                bundles: this.state.xmlFilter === \"all\",\n                bundles_restriction: BUNDLES_RESTRICTION,\n                only_user_custom_files: this.state.scssFilter === \"custom\",\n            })\n        );\n        this.state.resources = { xml: {}, js: {}, scss: {} };\n        this.processResources(resources.views || [], \"xml\");\n        this.processResources(resources.scss || [], \"scss\");\n        this.processResources(resources.js || [], \"js\");\n        const type = this.state.type;\n        if (this.state.currentResource) {\n            this.state.currentResource = this.state.resources[type][this.state.currentResource.id];\n        }\n        if (!this.state.currentResource) {\n            this.setDefaultFile();\n        }\n        this.errors.length = 0;\n    }\n\n    processResources(resources, type) {\n        if (type === \"xml\") {\n            // Only keep the active views and index them by ID.\n            const indexedById = {};\n            resources\n                .filter((view) => view.active)\n                .forEach((view) => {\n                    view.type = \"xml\";\n                    indexedById[view.id] = view;\n                });\n            Object.assign(this.state.resources.xml, indexedById);\n\n            // Initialize a 0 level for each view and assign them an array containing their children.\n            const roots = [];\n            Object.values(this.state.resources.xml).forEach((view) => {\n                view.level = 0;\n                view.children = [];\n            });\n            Object.values(this.state.resources.xml).forEach((view) => {\n                const parentId = view.inherit_id[0];\n                const parent = parentId && this.state.resources.xml[parentId];\n                if (parent) {\n                    parent.children.push(view);\n                } else {\n                    roots.push(view);\n                }\n            });\n\n            // Assign the correct level based on children key and save a sorted array where\n            // each view is followed by their children.\n            const sortedXML = [];\n            const visit = (view, level) => {\n                view.level = level;\n                sortedXML.push(view);\n                view.children.forEach((child) => {\n                    visit(child, level + 1);\n                });\n            };\n            roots.forEach((root) => {\n                visit(root, 0);\n            });\n            this.state.sortedXML = sortedXML;\n\n            // Compute labels\n            Object.values(this.state.resources.xml).forEach((view) => {\n                view.label = `${\"-\".repeat(view.level)} ${view.name}`;\n                if (this.debug && view.xml_id) {\n                    view.label += ` (${view.xml_id})`;\n                }\n            });\n        } else if (type === \"scss\" || type === \"js\") {\n            // The received scss or js data is already sorted by bundle and DOM order\n            if (type === \"scss\") {\n                this.state.sortedSCSS = resources;\n            } else {\n                this.state.sortedJS = resources;\n            }\n\n            // Store the URL ungrouped by bundle and use the URL as key (resource ID)\n            resources.forEach(([bundle, files]) => {\n                const indexedByUrl = {};\n                files.forEach((file) => {\n                    // Compute labels\n                    file.label = file.url.split(\"/\").at(-1).split(\".\")[0];\n                    if (this.debug) {\n                        file.label += ` (${file.url})`;\n                    }\n\n                    file.bundle = bundle;\n                    file.id = file.url; // for consistency with xml resources\n                    file.type = type;\n                    indexedByUrl[file.url] = file;\n                });\n                if (type === \"scss\") {\n                    Object.assign(this.state.resources.scss, indexedByUrl);\n                } else {\n                    Object.assign(this.state.resources.js, indexedByUrl);\n                }\n            });\n        }\n    }\n\n    /**\n     * Forces the current scss/js file identified by its url to be reset to the way\n     * it was before the user started editing it.\n     *\n     * @todo views (xml) reset is not supported yet\n     *\n     * @returns {Promise}\n     */\n    async resetResource() {\n        if (this.state.type === \"xml\") {\n            throw new Error(_t(\"Reseting views is not supported yet\"));\n        }\n        const resource = this.state.currentResource;\n        await this.orm.call(\"website.assets\", \"reset_asset\", [resource.url, resource.bundle], {\n            context: this.context,\n        });\n        await this.loadResources();\n        this.website.contentWindow.location.reload();\n    }\n\n    async saveResources() {\n        const { js, scss, xml } = this.state.resources;\n        const toSave = {\n            js: Object.values(js).filter((r) => r.dirty),\n            scss: Object.values(scss).filter((r) => r.dirty),\n            // child views first as COW on a parent would delete them\n            xml: sortBy(\n                Object.values(xml).filter((r) => r.dirty),\n                \"id\"\n            ).reverse(),\n        };\n\n        for (const [type, resources] of Object.entries(toSave)) {\n            for (let i = 0; i < resources.length; i++) {\n                const arch = resources[i].arch;\n                const { isValid, error } = type === \"xml\" ? checkXML(arch) : checkSCSS(arch);\n                if (!isValid) {\n                    this.errors.push({ error, resource: resources[i] });\n                }\n            }\n        }\n        if (this.errors.length) {\n            // switch to the first resource in error if the current has no error\n            if (\n                !this.errors\n                    .map(({ resource }) => resource.id)\n                    .includes(this.state.currentResource.id)\n            ) {\n                this.state.currentResource = this.errors[0].resource;\n                this.state.type = this.errors[0].resource.type;\n            }\n            return;\n        }\n\n        // sequentially save all resources\n        for (const [type, resources] of Object.entries(toSave)) {\n            for (const resource of resources) {\n                if (type === \"xml\") {\n                    await this.saveXML(resource);\n                } else {\n                    await this.saveSCSSorJS(resource);\n                }\n            }\n        }\n        await this.loadResources();\n        this.website.contentWindow.location.reload();\n    }\n\n    /**\n     * Saves a unique SCSS or JS file.\n     *\n     * @private\n     * @param {Object} resource a SCSS or JS file to save\n     * @return {Promise} indicates if the save is finished or if an error occured.\n     */\n    async saveSCSSorJS(resource) {\n        const { url, arch } = resource;\n        const isJSFile = String(url).endsWith(\".js\");\n        const bundle = isJSFile\n            ? this.state.resources.js[url].bundle\n            : this.state.resources.scss[url].bundle;\n        const fileType = isJSFile ? \"js\" : \"scss\";\n        const params = [url, bundle, arch, fileType];\n        await this.orm.call(\"website.assets\", \"save_asset\", params, { context: this.context });\n        delete resource.dirty;\n    }\n\n    /**\n     * Saves a unique XML view.\n     *\n     * @param {Object} resource an xml view to save\n     * @returns {Promise} indicates if the save is finished or if an error occured.\n     */\n    async saveXML(resource) {\n        const { id, arch } = resource;\n        await rpc(\"/website/save_xml\", {\n            view_id: id,\n            arch: arch,\n        });\n        delete resource.dirty;\n    }\n\n    setDefaultFile() {\n        if (this.state.type === \"xml\") {\n            const views = Object.values(this.state.resources.xml);\n            let view = views.find((view) => [view.id, view.xml_id].includes(this.viewKey));\n            if (!view) {\n                view = views.find((view) => view.key === this.viewKey);\n            }\n            this.state.currentResource = view || this.state.sortedXML[0] || false;\n        } else if (this.state.type === \"scss\") {\n            // By default show the user_custom_rules.scss one as some people\n            // would write rules in user_custom_bootstrap_overridden.scss\n            // otherwise, not reading the comment inside explaining how that\n            // file should be used.\n            this.state.currentResource =\n                this.state.resources.scss[\"/website/static/src/scss/user_custom_rules.scss\"];\n        } else {\n            this.state.currentResource =\n                this.state.sortedJS.map(([_, files]) => files).flat()[0] || false;\n        }\n    }\n\n    showErrorLine() {\n        if (!this.editorRef.el) {\n            // Possibly destroyed.\n            return;\n        }\n        const resourceId = this.state.currentResource.id;\n        const error = this.errors.find(({ resource }) => resource.id === resourceId)?.error;\n        if (error) {\n            const { line, message } = error;\n            const gutterCell = this.editorRef.el.querySelectorAll(\".ace_gutter-cell\")[line - 1];\n            if (gutterCell && !gutterCell.classList.contains(\"o_error\")) {\n                gutterCell.classList.add(\"o_error\");\n                gutterCell.setAttribute(\"data-tooltip\", message);\n                gutterCell.setAttribute(\"data-tooltip-position\", \"left\");\n            }\n        }\n    }\n\n    clearErrorLine() {\n        if (!this.editorRef.el) {\n            // Possibly destroyed.\n            return;\n        }\n        const allGutterCells = this.editorRef.el.querySelectorAll(\".ace_gutter-cell\");\n        for (const gutterCell of allGutterCells) {\n            gutterCell.classList.remove(\"o_error\");\n            gutterCell.removeAttribute(\"data-tooltip\");\n            gutterCell.removeAttribute(\"data-tooltip-position\");\n        }\n    }\n\n    // -------------------------------------------------------------------------\n    // Handlers\n    // -------------------------------------------------------------------------\n\n    onEditorChange(value) {\n        const currentResource = this.state.currentResource;\n        currentResource.arch = value;\n        currentResource.dirty = true;\n        this.errors.length = 0;\n    }\n\n    /**\n     * @param {\"xml\"|\"scss\"|\"js\"} type\n     */\n    onFileTypeChange(type) {\n        if (type !== this.state.type) {\n            this.state.type = type;\n            this.setDefaultFile();\n        }\n    }\n\n    /**\n     * @param {\"xml\"|\"scss\"} type\n     * @param {string} filter\n     */\n    onFilterChange(type, filter) {\n        if (type === \"scss\") {\n            this.state.scssFilter = filter;\n        } else if (type === \"xml\") {\n            this.state.xmlFilter = filter;\n        }\n        this.loadResources();\n    }\n\n    onFormat() {\n        if (this.state.type === \"xml\") {\n            const { isValid, error } = checkXML(this.state.currentResource.arch);\n            if (isValid) {\n                this.state.currentResource.arch = formatXML(this.state.currentResource.arch);\n            } else {\n                this.errors.push({ error, resource: this.state.currentResource });\n            }\n        }\n    }\n\n    onReset() {\n        this.dialog.add(ConfirmationDialog, {\n            title: _t(\"Careful\"),\n            body: _t(\n                \"If you reset this file, all your customizations will be lost as it will be reverted to the default file.\"\n            ),\n            confirm: () => this.resetResource(),\n            cancel: () => {},\n        });\n    }\n\n    async onSave() {\n        this.state.saving = true;\n        try {\n            await this.saveResources();\n        } finally {\n            this.state.saving = false;\n        }\n    }\n}\n", "import { EditHeadBodyDialog } from \"../edit_head_body_dialog/edit_head_body_dialog\";\nimport { Component, useState } from \"@odoo/owl\";\nimport { browser } from \"@web/core/browser/browser\";\nimport { useService } from \"@web/core/utils/hooks\";\n\n/**\n * Represents the warning overlay that appears when the user opens the ResourceEditor\n * It provides options to hide the warning, inject code, and not show the warning again.\n */\nexport class ResourceEditorWarningOverlay extends Component {\n    static template = \"website.ResourceEditorWarningOverlay\";\n    static props = {};\n\n    /**\n     * Initializes the component by setting up the necessary services and state.\n     */\n    setup() {\n        this.website = useService(\"website\");\n        this.dialog = useService(\"dialog\");\n\n        const localStorageValue = browser.localStorage.getItem(\"website.ace.doNotShowWarning\");\n        this.state = useState({\n            visible: !localStorageValue || localStorageValue === \"false\",\n        });\n    }\n\n    /**\n     * Closes the Ace editor and updates the website context to hide it.\n     */\n    onCloseEditor() {\n        this.website.context.showResourceEditor = false;\n    }\n\n    /**\n     * Hides the warning overlay.\n     */\n    onHideWarning() {\n        this.state.visible = false;\n    }\n\n    /**\n     * Sets a flag in the local storage to prevent the warning overlay from\n     * showing again and hides the overlay.\n     */\n    onStopAsking() {\n        browser.localStorage.setItem(\"website.ace.doNotShowWarning\", \"true\");\n        this.onHideWarning();\n    }\n\n    /**\n     * Opens a dialog to edit the head and body of the website and closes the\n     * Ace editor.\n     */\n    onInjectCode() {\n        this.dialog.add(EditHeadBodyDialog);\n        this.onCloseEditor();\n    }\n}\n", "import { _t } from \"@web/core/l10n/translation\";\n\nconst MAPPING = {\n    \"{\": \"}\",\n    \"}\": \"{\",\n    \"(\": \")\",\n    \")\": \"(\",\n    \"[\": \"]\",\n    \"]\": \"[\",\n};\nconst OPENINGS = [\"{\", \"(\", \"[\"];\nconst CLOSINGS = [\"}\", \")\", \"]\"];\n\n/**\n * Checks the syntax validity of some SCSS.\n *\n * @param {string} scss\n * @returns {Object} object with keys \"isValid\" and \"error\" if not valid\n */\nexport function checkSCSS(scss) {\n    const stack = [];\n    let line = 1;\n    for (let i = 0; i < scss.length; i++) {\n        if (OPENINGS.includes(scss[i])) {\n            stack.push(scss[i]);\n        } else if (CLOSINGS.includes(scss[i])) {\n            if (stack.pop() !== MAPPING[scss[i]]) {\n                return {\n                    isValid: false,\n                    error: {\n                        line,\n                        message: _t(\"Unexpected %(char)s\", { char: scss[i] }),\n                    },\n                };\n            }\n        } else if (scss[i] === \"\\n\") {\n            line++;\n        }\n    }\n    if (stack.length > 0) {\n        return {\n            isValid: false,\n            error: {\n                line,\n                message: _t(\"Expected %(char)s\", { char: MAPPING[stack.pop()] }),\n            },\n        };\n    }\n    return { isValid: true };\n}\n\n/**\n * Checks the syntax validity of some XML.\n *\n * @param {string} xml\n * @returns {Object} object with keys \"isValid\" and \"error\" if not valid\n */\nexport function checkXML(xml) {\n    const xmlDoc = new window.DOMParser().parseFromString(xml, \"text/xml\");\n    const errorEls = xmlDoc.getElementsByTagName(\"parsererror\");\n    if (errorEls.length > 0) {\n        const errorEl = errorEls[0];\n        const sourceTextEls = errorEl.querySelectorAll(\"sourcetext\");\n        let codeEls = null;\n        if (sourceTextEls.length) {\n            codeEls = [...sourceTextEls].map((el) => {\n                const codeEl = document.createElement(\"code\");\n                codeEl.textContent = el.textContent;\n                const brEl = document.createElement(\"br\");\n                brEl.classList.add(\"o_we_source_text_origin\");\n                el.parentElement.insertBefore(brEl, el);\n                return codeEl;\n            });\n            for (const el of sourceTextEls) {\n                el.remove();\n            }\n        }\n        for (const el of [...errorEl.querySelectorAll(\":not(code):not(pre):not(br)\")]) {\n            const pEl = document.createElement(\"p\");\n            for (const cEl of [...el.childNodes]) {\n                pEl.appendChild(cEl);\n            }\n            el.parentElement.insertBefore(pEl, el);\n            el.remove();\n        }\n        errorEl.querySelectorAll(\".o_we_source_text_origin\").forEach((el, i) => {\n            el.after(codeEls[i]);\n        });\n        return {\n            isValid: false,\n            error: {\n                line: parseInt(errorEl.innerHTML.match(/[Ll]ine[^\\d]+(\\d+)/)[1], 10),\n                message: errorEl.textContent,\n            },\n        };\n    }\n    return { isValid: true };\n}\n\n/**\n * Formats some XML so that it has proper indentation and structure.\n *\n * @param {string} xml\n * @returns {string} formatted xml\n */\nexport function formatXML(xml) {\n    // do nothing if an inline script is present to avoid breaking it\n    if (/<script(?: [^>]*)?>[^<][\\s\\S]*<\\/script>/i.test(xml)) {\n        return xml;\n    }\n    return window.vkbeautify.xml(xml, 4);\n}\n", "import { useService } from \"@web/core/utils/hooks\";\nimport { Component, onWillStart, useState } from \"@odoo/owl\";\nimport { Dialog } from \"@web/core/dialog/dialog\";\nimport { CodeEditor } from \"@web/core/code_editor/code_editor\";\n\n/**\n * A dialog that let the user edit the code that will be injected in the <head>\n * and before the </body> of every page of the website. This is a stable and\n * upgrade proof alternative to directly editing the website xml.\n */\nexport class EditHeadBodyDialog extends Component {\n    static template = \"website.EditHeadBodyDialog\";\n    static components = { CodeEditor, Dialog };\n    static props = {\n        close: Function,\n    };\n\n    setup() {\n        this.orm = useService(\"orm\");\n        this.website = useService(\"website\");\n\n        this.state = useState({\n            head: \"\",\n            body: \"\",\n        });\n\n        onWillStart(async () => {\n            const websites = await this.orm.read(\n                \"website\",\n                [this.website.currentWebsite.id],\n                [\"custom_code_head\", \"custom_code_footer\"]\n            );\n            const website = websites[0];\n            this.state.head = website.custom_code_head || \"\";\n            this.state.body = website.custom_code_footer || \"\";\n        });\n    }\n\n    async onSave() {\n        await this.orm.write(\"website\", [this.website.currentWebsite.id], {\n            custom_code_head: this.state.head,\n            custom_code_footer: this.state.body,\n        });\n        this.props.close();\n    }\n}\n", "// Definitely not the right location for this file !!!\n\n/**\n * @param {HTMLElement} element\n */\nexport function onceAllImagesLoaded(element) {\n    const imgEls = element.nodeName === \"IMG\" ? [element] : [...element.querySelectorAll(\"img\")];\n    const defs = imgEls.map((imgEl) => {\n        if (imgEl.complete) {\n            return; // Already loaded\n        }\n        return new Promise((resolve, reject) => {\n            imgEl.addEventListener(\"load\", resolve, { once: true });\n            imgEl.addEventListener(\"error\", reject, { once: true });\n        });\n    });\n    return Promise.all(defs);\n}\n", "import { cookie as cookieManager } from \"@web/core/browser/cookie\";\n\nexport class EventBus extends EventTarget {\n    trigger(name, payload) {\n        this.dispatchEvent(new CustomEvent(name, { detail: payload }));\n    }\n}\n\nexport function getClosestLiEls(selector) {\n    return Array.from(document.querySelectorAll(selector), (el) => el.closest(\"li\"));\n}\n\n/**\n * Unhide elements that are hidden by default and that should be visible\n * according to the snippet visibility option.\n */\nexport function unhideConditionalElements() {\n    // Create CSS rules in a dedicated style tag according to the snippet\n    // visibility option's computed ones (saved as data attributes).\n    const styleEl = document.createElement(\"style\");\n    styleEl.id = \"conditional_visibility\";\n    document.head.appendChild(styleEl);\n    const conditionalEls = document.querySelectorAll('[data-visibility=\"conditional\"]');\n\n    const desktopMegaMenuLiEls = getClosestLiEls(\n        \"header#top nav:not(.o_header_mobile) .o_mega_menu_toggle\"\n    );\n    const mobileMegaMenuLiEls = getClosestLiEls(\n        \"header#top nav.o_header_mobile .o_mega_menu_toggle\"\n    );\n    for (const conditionalEl of conditionalEls) {\n        // For mega menu block, add conditional visibility to the navbar link\n        if (conditionalEl.parentElement.classList.contains(\"o_mega_menu\")) {\n            const desktopMegaMenuLiEl = conditionalEl.closest(\"li\");\n            const index = desktopMegaMenuLiEls.indexOf(desktopMegaMenuLiEl);\n            const mobileMegaMenuLiEl = mobileMegaMenuLiEls[index];\n\n            const visibilityId = conditionalEl.dataset.visibilityId;\n            desktopMegaMenuLiEl.dataset.visibilityId = visibilityId;\n            mobileMegaMenuLiEl.dataset.visibilityId = visibilityId;\n        }\n        const selectors = conditionalEl.dataset.visibilitySelectors;\n        styleEl.sheet.insertRule(`${selectors} { display: none !important; }`);\n    }\n\n    // Now remove the classes that makes them always invisible\n    for (const conditionalEl of conditionalEls) {\n        conditionalEl.classList.remove(\"o_conditional_hidden\");\n    }\n}\n\nexport function setUtmsHtmlDataset() {\n    const htmlEl = document.documentElement;\n    const cookieNamesToDataNames = {\n        utm_source: \"utmSource\",\n        utm_medium: \"utmMedium\",\n        utm_campaign: \"utmCampaign\",\n    };\n    for (const [name, dsName] of Object.entries(cookieNamesToDataNames)) {\n        const cookie = cookieManager.get(`odoo_${name}`);\n        if (cookie) {\n            // Remove leading and trailing \" and '\n            htmlEl.dataset[dsName] = cookie.replace(/(^[\"']|[\"']$)/g, \"\");\n        }\n    }\n}\n\n/**\n * Performs a basic check to make sure a link's protocol is http(s), mainly to\n * deny `javascript:` URLs.\n *\n * @param {string} link\n * @returns {URL|\"\"} URL if the protocol is http(s), empty string otherwise\n */\nexport function verifyHttpsUrl(link) {\n    const url = new URL(link, window.location.href);\n    if (!/https?:/.test(url.protocol)) {\n        return \"\";\n    }\n    return url;\n}\n", "import { loadJS } from \"@web/core/assets\";\nimport { hasTouch } from \"@web/core/browser/feature_detection\";\nimport { SIZES, utils as uiUtils } from \"@web/core/ui/ui_service\";\n\n/**\n * Takes care of any necessary setup for autoplaying video. In practice,\n * this method will load the youtube iframe API for mobile environments\n * because mobile environments don't support the youtube autoplay param\n * passed in the url.\n *\n * @param {string} src\n * @param {boolean} needCookiesApproval\n */\nexport function setupAutoplay(src, needCookiesApproval = false) {\n    const isYoutubeVideo = src.indexOf(\"youtube\") >= 0;\n    const isMobileEnv = uiUtils.getSize() <= SIZES.LG && hasTouch();\n\n    if (isYoutubeVideo && isMobileEnv && !window.YT && !needCookiesApproval) {\n        const oldOnYoutubeIframeAPIReady = window.onYouTubeIframeAPIReady;\n        const promise = new Promise((resolve) => {\n            window.onYouTubeIframeAPIReady = () => {\n                if (oldOnYoutubeIframeAPIReady) {\n                    oldOnYoutubeIframeAPIReady();\n                }\n                return resolve();\n            };\n        });\n        loadJS(\"https://www.youtube.com/iframe_api\");\n        return promise;\n    }\n    return Promise.resolve();\n}\n\n/**\n * @param {HTMLIframeElement} iframeEl - the iframe containing the video player\n */\nexport function triggerAutoplay(iframeEl) {\n    const isYoutubeVideo = iframeEl.src.indexOf(\"youtube\") >= 0;\n    const isMobileEnv = uiUtils.getSize() <= SIZES.LG && hasTouch();\n\n    // YouTube does not allow to auto-play video in mobile devices, so we\n    // have to play the video manually.\n    if (isYoutubeVideo && isMobileEnv && iframeEl.closest(\"[data-need-cookies-approval]\")) {\n        new window.YT.Player(iframeEl, {\n            events: {\n                onReady: (ev) => ev.target.playVideo(),\n            },\n        });\n    }\n}\n", "import { isBrowserFirefox } from \"@web/core/browser/feature_detection\";\nimport { getActiveHotkey } from \"@web/core/hotkeys/hotkey_service\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { renderToElement } from \"@web/core/utils/render\";\nimport { useAutofocus, useService } from \"@web/core/utils/hooks\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { WebsiteDialog } from \"@website/components/dialog/dialog\";\nimport { Switch } from \"@html_editor/components/switch/switch\";\nimport {\n    applyTextHighlight,\n    removeTextHighlight,\n    getObservedEls,\n} from \"@website/js/highlight_utils\";\nimport { useRef, useState, useSubEnv, Component, onWillStart, onMounted, status } from \"@odoo/owl\";\nimport { onceAllImagesLoaded } from \"@website/utils/images\";\n\nconst NO_OP = () => {};\n\nexport class AddPageConfirmDialog extends Component {\n    static template = \"website.AddPageConfirmDialog\";\n    static props = {\n        close: Function,\n        createPage: Function,\n        name: String,\n        sectionsArch: String,\n        templateId: String,\n    };\n    static components = {\n        Switch,\n        WebsiteDialog,\n    };\n\n    setup() {\n        super.setup();\n        useAutofocus();\n\n        this.state = useState({\n            addMenu: true,\n            name: this.props.name,\n            sectionsArch: this.props.sectionsArch,\n            templateId: this.props.templateId,\n        });\n    }\n\n    onChangeAddMenu(value) {\n        this.state.addMenu = value;\n    }\n\n    async addPage() {\n        await this.props.createPage(this.state.sectionsArch, this.state.name, this.state.addMenu);\n    }\n}\n\nclass AddPageTemplateBlank extends Component {\n    static template = \"website.AddPageTemplateBlank\";\n    static props = {\n        firstRow: {\n            type: Boolean,\n            optional: true,\n        },\n    };\n\n    setup() {\n        super.setup();\n        this.holderRef = useRef(\"holder\");\n\n        onMounted(async () => {\n            this.holderRef.el.classList.add(\"o_ready\");\n        });\n    }\n\n    select() {\n        this.env.addPage();\n    }\n}\n\nclass AddPageTemplatePreview extends Component {\n    static template = \"website.AddPageTemplatePreview\";\n    static props = {\n        template: Object,\n        animationDelay: Number,\n        firstRow: {\n            type: Boolean,\n            optional: true,\n        },\n        isCustom: {\n            type: Boolean,\n            optional: true,\n        },\n    };\n\n    setup() {\n        super.setup();\n        this.iframeRef = useRef(\"iframe\");\n        this.previewRef = useRef(\"preview\");\n        this.holderRef = useRef(\"holder\");\n        this.resizeObserver = new ResizeObserver((entries) => {\n            for (const entry of entries) {\n                const targetEl = entry.target.querySelector(\".o_text_highlight\") || entry.target;\n                removeTextHighlight(targetEl);\n                applyTextHighlight(targetEl);\n            }\n        });\n\n        onMounted(async () => {\n            const holderEl = this.holderRef.el;\n            holderEl.classList.add(\"o_loading\");\n            if (!this.props.template.key) {\n                return;\n            }\n            const previewEl = this.previewRef.el;\n            const iframeEl = this.iframeRef.el;\n            // Firefox replaces the built content with about:blank.\n            const isFirefox = isBrowserFirefox();\n            if (isFirefox) {\n                // Make sure empty preview iframe is loaded.\n                // This event is never triggered on Chrome.\n                await new Promise((resolve) => {\n                    iframeEl.contentDocument.body.onload = resolve;\n                });\n            }\n            // Apply styles.\n            for (const cssLinkEl of await this.env.getCssLinkEls()) {\n                const preloadLinkEl = document.createElement(\"link\");\n                preloadLinkEl.setAttribute(\"rel\", \"preload\");\n                preloadLinkEl.setAttribute(\"href\", cssLinkEl.getAttribute(\"href\"));\n                preloadLinkEl.setAttribute(\"as\", \"style\");\n                iframeEl.contentDocument.head.appendChild(preloadLinkEl);\n                iframeEl.contentDocument.head.appendChild(cssLinkEl.cloneNode(true));\n            }\n            // Adjust styles.\n            const styleEl = document.createElement(\"style\");\n            // Does not work with fit-content in Firefox.\n            const carouselHeight = isFirefox ? \"450px\" : \"fit-content\";\n            // Prevent successive resizes.\n            const fullHeight = getComputedStyle(document.querySelector(\".o_action_manager\")).height;\n            const halfHeight = `${Math.round(parseInt(fullHeight) / 2)}px`;\n            const css = `\n                html, body {\n                    /* Needed to prevent scrollbar to appear on chrome */\n                    overflow: hidden;\n                }\n                #wrapwrap {\n                    padding-right: 0px;\n                    padding-left: 0px;\n                    --snippet-preview-height: 340px;\n                }\n                section {\n                    /* Avoid the zoom's missing pixel. */\n                    transform: scale(101%);\n                }\n                section[data-snippet=\"s_carousel\"],\n                section[data-snippet=\"s_carousel_intro\"],\n                section[data-snippet=\"s_carousel_cards\"],\n                section[data-snippet=\"s_quotes_carousel_minimal\"],\n                section[data-snippet=\"s_quotes_carousel_compact\"],\n                section[data-snippet=\"s_quotes_carousel\"] {\n                    height: ${carouselHeight} !important;\n                }\n                section.o_half_screen_height {\n                    min-height: ${halfHeight} !important;\n                }\n                section.o_full_screen_height {\n                    min-height: ${fullHeight} !important;\n                }\n                section[data-snippet=\"s_three_columns\"] .figure-img[style*=\"height:50vh\"] {\n                    /* In Travel theme. */\n                    height: 170px !important;\n                }\n                .o_we_shape {\n                    /* Avoid the zoom's missing pixel. */\n                    transform: scale(101%);\n                }\n                .o_animate {\n                    visibility: visible;\n                    animation-name: none;\n                }\n            `;\n            const cssText = document.createTextNode(css);\n            styleEl.appendChild(cssText);\n            iframeEl.contentDocument.head.appendChild(styleEl);\n            // Put blocks.\n            // To preserve styles, the whole #wrapwrap > main > #wrap\n            // nesting must be reproduced.\n            const mainEl = document.createElement(\"main\");\n            const wrapwrapEl = document.createElement(\"div\");\n            wrapwrapEl.id = \"wrapwrap\";\n            wrapwrapEl.appendChild(mainEl);\n            iframeEl.contentDocument.body.appendChild(wrapwrapEl);\n            const templateDocument = new DOMParser().parseFromString(\n                this.props.template.template,\n                \"text/html\"\n            );\n            const wrapEl = templateDocument.getElementById(\"wrap\");\n            mainEl.appendChild(wrapEl);\n            // Make image loading eager.\n            const lazyLoadedImgEls = wrapEl.querySelectorAll(\"img[loading=lazy]\");\n            for (const imgEl of lazyLoadedImgEls) {\n                imgEl.setAttribute(\"loading\", \"eager\");\n            }\n            mainEl.appendChild(wrapEl);\n            await onceAllImagesLoaded(wrapEl);\n            // Restore image lazy loading.\n            for (const imgEl of lazyLoadedImgEls) {\n                imgEl.setAttribute(\"loading\", \"lazy\");\n            }\n            if (!this.previewRef.el) {\n                // Stop the process when preview is removed\n                return;\n            }\n            // Wait for fonts.\n            await iframeEl.contentDocument.fonts.ready;\n            holderEl.classList.remove(\"o_loading\");\n            const adjustHeight = () => {\n                if (!this.previewRef.el) {\n                    // Stop ajusting height when preview is removed.\n                    return;\n                }\n                const outerWidth = parseInt(window.getComputedStyle(previewEl).width);\n                const innerHeight = wrapEl.getBoundingClientRect().height;\n                const innerWidth = wrapEl.getBoundingClientRect().width;\n                const ratio = outerWidth / innerWidth;\n                iframeEl.height = Math.round(innerHeight);\n                previewEl.style.setProperty(\"height\", `${Math.round(innerHeight * ratio)}px`);\n                // Sometimes the final height is not ready yet.\n                setTimeout(adjustHeight, 50);\n                holderEl.classList.add(\"o_ready\");\n            };\n            adjustHeight();\n            if (this.props.isCustom) {\n                this.adaptCustomTemplate(wrapEl);\n            }\n            for (const textEl of iframeEl.contentDocument?.querySelectorAll(\".o_text_highlight\") ||\n                []) {\n                for (const elToObserve of getObservedEls(textEl)) {\n                    this.resizeObserver.observe(elToObserve);\n                }\n            }\n        });\n    }\n\n    adaptCustomTemplate(wrapEl) {\n        for (const sectionEl of wrapEl.querySelectorAll(\n            \"section:not(.o_snippet_desktop_invisible)\"\n        )) {\n            const style = window.getComputedStyle(sectionEl);\n            if (!style.height || style.display === \"none\") {\n                const messageEl = renderToElement(\"website.AddPageTemplatePreviewDynamicMessage\", {\n                    message: _t(\n                        \"No preview for the %s block because it is dynamically rendered.\",\n                        sectionEl.dataset.name\n                    ),\n                });\n                sectionEl.insertAdjacentElement(\"beforebegin\", messageEl);\n            }\n        }\n    }\n\n    select() {\n        if (this.holderRef.el.classList.contains(\"o_loading\")) {\n            return;\n        }\n        const wrapEl = this.iframeRef.el.contentDocument.getElementById(\"wrap\").cloneNode(true);\n        const templateId = this.props.template.key;\n        for (const previewEl of wrapEl.querySelectorAll(\n            \".o_new_page_snippet_preview, .s_dialog_preview\"\n        )) {\n            previewEl.remove();\n        }\n        this.resizeObserver.disconnect();\n        // Remove highlighted text content from the cloned page. The full\n        // highlight structure will be restored on page load.\n        for (const textHighlightEl of wrapEl.querySelectorAll(\".o_text_highlight\")) {\n            removeTextHighlight(textHighlightEl);\n        }\n        this.env.addPage(\n            wrapEl.innerHTML,\n            this.props.template.name && _t(\"Copy of %s\", this.props.template.name),\n            templateId\n        );\n    }\n}\n\nclass AddPageTemplatePreviews extends Component {\n    static template = \"website.AddPageTemplatePreviews\";\n    static props = {\n        isCustom: {\n            type: Boolean,\n            optional: true,\n        },\n        templates: {\n            type: Array,\n            element: Object,\n        },\n    };\n    static components = {\n        AddPageTemplateBlank,\n        AddPageTemplatePreview,\n    };\n\n    setup() {\n        super.setup();\n    }\n\n    get columns() {\n        const result = [[], [], []];\n        let currentColumnIndex = 0;\n        for (const template of this.props.templates) {\n            result[currentColumnIndex].push(template);\n            currentColumnIndex = (currentColumnIndex + 1) % result.length;\n        }\n        return result;\n    }\n}\n\nclass AddPageTemplates extends Component {\n    static template = \"website.AddPageTemplates\";\n    static props = {\n        onTemplatePageChanged: Function,\n    };\n    static components = {\n        AddPageTemplatePreviews,\n    };\n\n    setup() {\n        super.setup();\n        this.website = useService(\"website\");\n        this.tabsRef = useRef(\"tabs\");\n        this.panesRef = useRef(\"panes\");\n        useAutofocus();\n\n        this.state = useState({\n            pages: [\n                {\n                    Component: AddPageTemplatePreviews,\n                    title: _t(\"Loading...\"),\n                    isPreloading: true,\n                    props: {\n                        id: \"basic\",\n                        title: _t(\"Basic\"),\n                        // Blank and 5 preloading boxes.\n                        templates: [{ isBlank: true }, {}, {}, {}, {}, {}],\n                    },\n                },\n            ],\n        });\n        this.pages = undefined;\n\n        onWillStart(() => {\n            this.preparePages().then((pages) => {\n                this.state.pages = pages;\n            });\n        });\n    }\n\n    async preparePages() {\n        // Fetch templates without client-side caching to reflect recent changes\n        // to custom templates within the same session.\n        const loadTemplates = rpc(\n            \"/website/get_new_page_templates\",\n            { context: { website_id: this.website.currentWebsiteId } },\n            { silent: true }\n        );\n\n        // Forces the correct website if needed before fetching the templates.\n        // Displaying the correct images in the previews also relies on the\n        // website id having been forced.\n        await this.env.getCssLinkEls();\n        if (status(this) === \"destroyed\") {\n            return new Promise(() => {});\n        }\n\n        if (this.pages) {\n            return this.pages;\n        }\n\n        const newPageTemplates = await loadTemplates;\n        newPageTemplates[0].templates.unshift({\n            isBlank: true,\n        });\n        const pages = [];\n        for (const template of newPageTemplates) {\n            pages.push({\n                Component: AddPageTemplatePreviews,\n                title: template.title,\n                props: template,\n                id: `${template.id}`,\n            });\n        }\n        this.pages = pages;\n        return pages;\n    }\n\n    onTabListBtnClick(id) {\n        for (const page of this.state.pages) {\n            if (page.id === id) {\n                page.isAccessed = true;\n            }\n        }\n        const activeTabEl = this.tabsRef.el.querySelector(\".active\");\n        const activePaneEl = this.panesRef.el.querySelector(\".active\");\n        activeTabEl?.classList?.remove(\"active\");\n        activeTabEl?.setAttribute(\"tabIndex\", \"-1\");\n        activePaneEl?.classList?.remove(\"active\");\n        activePaneEl?.setAttribute(\"inert\", \"inert\"); // Make sure trapFocus() works.\n        const tabEl = this.tabsRef.el.querySelector(`[data-id=${id}]`);\n        const paneEl = this.panesRef.el.querySelector(`[data-id=${id}]`);\n        tabEl.classList.add(\"active\");\n        tabEl.tabIndex = 0;\n        paneEl.classList.add(\"active\");\n        paneEl.removeAttribute(\"inert\");\n        this.props.onTemplatePageChanged(tabEl.dataset.id === \"basic\" ? \"\" : tabEl.textContent);\n    }\n\n    onTabListBtnKeydown(ev) {\n        const hotkey = getActiveHotkey(ev);\n        if (![\"arrowleft\", \"arrowright\", \"arrowdown\", \"arrowup\"].includes(hotkey)) {\n            return;\n        }\n        const currentTabEl = this.tabsRef.el.querySelector(`[data-id=${ev.target.dataset.id}]`);\n        if ([\"arrowleft\", \"arrowup\"].includes(hotkey)) {\n            currentTabEl.previousElementSibling?.focus();\n        } else {\n            currentTabEl.nextElementSibling?.focus();\n        }\n    }\n}\n\nexport class AddPageDialog extends Component {\n    static template = \"website.AddPageDialog\";\n    static props = {\n        close: Function,\n        onAddPage: {\n            type: Function,\n            optional: true,\n        },\n        websiteId: {\n            type: Number,\n        },\n        forcedURL: {\n            type: String,\n            optional: true,\n        },\n        goToPage: {\n            type: Boolean,\n            optional: true,\n        },\n        pageTitle: {\n            type: String,\n            optional: true,\n        },\n    };\n    static defaultProps = {\n        onAddPage: NO_OP,\n        goToPage: true,\n    };\n    static components = {\n        WebsiteDialog,\n        AddPageTemplates,\n        AddPageTemplatePreviews,\n    };\n\n    setup() {\n        super.setup();\n        useAutofocus();\n\n        this.primaryTitle = _t(\"Create\");\n        this.switchLabel = _t(\"Add to menu\");\n        this.website = useService(\"website\");\n        this.dialogs = useService(\"dialog\");\n        this.http = useService(\"http\");\n        this.action = useService(\"action\");\n\n        this.cssLinkEls = undefined;\n        this.lastTabName = \"\";\n\n        useSubEnv({\n            addPage: (sectionsArch, name, templateId) =>\n                this.addPage(sectionsArch, name, templateId),\n            getCssLinkEls: () => this.getCssLinkEls(),\n        });\n    }\n\n    onTemplatePageChanged(name) {\n        this.lastTabName = name;\n    }\n\n    async addPage(sectionsArch, name, templateId) {\n        if (this.props.forcedURL) {\n            // We also skip the possibility to choose to add in menu in that\n            // case (e.g. in creation from 404 page button). The user can still\n            // create its menu afterwards if needed.\n            await this.createPage(sectionsArch, this.props.forcedURL, false, this.props.pageTitle);\n        } else {\n            this.dialogs.add(AddPageConfirmDialog, {\n                createPage: (...args) => this.createPage(...args),\n                name: name || this.lastTabName,\n                sectionsArch: sectionsArch || \"\",\n                templateId: templateId || \"\",\n            });\n        }\n    }\n\n    async createPage(sectionsArch, name = \"\", addMenu = false, pageTitle = \"\") {\n        // Remove any leading slash.\n        const pageName = name.replace(/^\\/*/, \"\") || _t(\"New Page\");\n        const data = await this.http.post(`/website/add/${encodeURIComponent(pageName)}`, {\n            // Needed to be passed as a (falsy) string because false would be\n            // converted to 'false' with a POST.\n            sections_arch: sectionsArch || \"\",\n            add_menu: addMenu || \"\",\n\n            website_id: this.props.websiteId,\n            csrf_token: odoo.csrf_token,\n            page_title: pageTitle,\n        });\n        if (data.view_id) {\n            this.action.doAction({\n                res_model: \"ir.ui.view\",\n                res_id: data.view_id,\n                views: [[false, \"form\"]],\n                type: \"ir.actions.act_window\",\n                view_mode: \"form\",\n            });\n        } else if (this.props.goToPage) {\n            this.website.goToWebsite({\n                path: data.url,\n                edition: true,\n                websiteId: this.props.websiteId,\n            });\n        }\n        this.props.onAddPage();\n        this.props.close();\n    }\n\n    getCssLinkEls() {\n        if (!this.cssLinkEls) {\n            this.cssLinkEls = new Promise((resolve) => {\n                const container = document.querySelector(\".o_website_preview .o_iframe_container\");\n                const iframe = container?.querySelector(\n                    'iframe:not([src=\"/website/iframefallback\"])'\n                );\n                if (iframe?.contentDocument.body.getAttribute(\"is-ready\") === \"true\") {\n                    // If there is a fully loaded website preview, use it.\n                    resolve(iframe.contentDocument.head.querySelectorAll(\"link[type='text/css']\"));\n                } else {\n                    // If there is no website preview or it was not ready yet, fetch page.\n                    this.http\n                        .get(`/website/force/${this.props.websiteId}?path=/`, \"text\")\n                        .then((html) => {\n                            const doc = new DOMParser().parseFromString(html, \"text/html\");\n                            resolve(doc.head.querySelectorAll(\"link[type='text/css']\"));\n                        });\n                }\n            });\n        }\n        return this.cssLinkEls;\n    }\n}\n", "import { Dialog } from \"@web/core/dialog/dialog\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { useChildRef } from \"@web/core/utils/hooks\";\nimport { useState, Component } from \"@odoo/owl\";\n\nconst NO_OP = () => {};\n\nexport class WebsiteDialog extends Component {\n    static template = \"website.WebsiteDialog\";\n    static components = { Dialog };\n    static props = {\n        ...Dialog.props,\n        primaryTitle: { type: String, optional: true },\n        primaryClick: { type: Function, optional: true },\n        secondaryTitle: { type: String, optional: true },\n        secondaryClick: { type: Function, optional: true },\n        showSecondaryButton: { type: Boolean, optional: true },\n        close: { type: Function, optional: true },\n        closeOnClick: { type: Boolean, optional: true },\n        body: { type: String, optional: true },\n        slots: { type: Object, optional: true },\n        showFooter: { type: Boolean, optional: true },\n    };\n    static defaultProps = {\n        ...Dialog.defaultProps,\n        title: _t(\"Confirmation\"),\n        showFooter: true,\n        primaryTitle: _t(\"Ok\"),\n        secondaryTitle: _t(\"Cancel\"),\n        showSecondaryButton: true,\n        size: \"md\",\n        closeOnClick: true,\n        close: NO_OP,\n    };\n\n    setup() {\n        this.state = useState({\n            disabled: false,\n        });\n        this.modalRef = useChildRef();\n    }\n    /**\n     * Disables the buttons of the dialog when a click is made.\n     * If a handler is provided, await for its call.\n     * If the prop closeOnClick is true, close the dialog.\n     * Otherwise, restore the button.\n     *\n     * @param handler {function|void} The handler to protect.\n     * @returns {function(): Promise} handler called when a click is made.\n     */\n    protectedClick(handler) {\n        return async () => {\n            if (this.state.disabled) {\n                return;\n            }\n            this.state.disabled = true;\n            if (handler) {\n                await handler();\n            }\n            if (this.props.closeOnClick) {\n                return this.props.close();\n            }\n            this.state.disabled = false;\n        };\n    }\n\n    get contentClasses() {\n        const websiteDialogClass = \"o_website_dialog\";\n        if (this.props.contentClass) {\n            return `${websiteDialogClass} ${this.props.contentClass}`;\n        }\n        return websiteDialogClass;\n    }\n}\n", "import { useService, useAutofocus } from \"@web/core/utils/hooks\";\nimport { useNestedSortable } from \"@web/core/utils/nested_sortable\";\nimport wUtils from \"@website/js/utils\";\nimport { WebsiteDialog } from \"./dialog\";\nimport { Component, useState, useEffect, onWillStart, useRef, onMounted } from \"@odoo/owl\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { isEmail } from \"@web/core/utils/strings\";\nimport { AddPageDialog } from \"@website/components/dialog/add_page_dialog\";\nimport { isAbsoluteURLInCurrentDomain } from \"@html_editor/utils/url\";\n\nconst isPageNotFound = (url, allPages) => {\n    let relativeUrl = false;\n\n    // Do not check if the page exists if the input is empty, an anchor, an\n    // email, or a phone number.\n    if (!url.trim() || url.startsWith(\"#\") || isEmail(url) || /^(mailto:|tel:)/.test(url)) {\n        return false;\n    }\n\n    try {\n        relativeUrl = toRelativeIfSameDomain(url);\n        if (relativeUrl === url) {\n            // External URL\n            return false;\n        }\n    } catch {\n        // Relative or invalid URL; proceed with original.\n        relativeUrl = url;\n    }\n\n    // Remove query params and hash.\n    relativeUrl = relativeUrl.split(\"?\")[0].split(\"#\")[0];\n    // Ensure the URL starts with \"/\".\n    relativeUrl = relativeUrl.startsWith(\"/\") ? relativeUrl : \"/\" + relativeUrl;\n    // Remove trailing slash if it's not the root \"/\".\n    relativeUrl =\n        relativeUrl.endsWith(\"/\") && relativeUrl !== \"/\" ? relativeUrl.slice(0, -1) : relativeUrl;\n    // Check if the page exists.\n    return !allPages.includes(relativeUrl);\n};\n\nconst toRelativeIfSameDomain = (url) => {\n    // Remove domain from url to keep only the relative path if same domain.\n    const urlObj = new URL(url);\n    const isSameDomain = isAbsoluteURLInCurrentDomain(url, this.env);\n    return isSameDomain ? url.replace(urlObj.origin, \"\") : url;\n};\n\nconst getAllPages = async () => {\n    const res = await rpc(\"/website/get_suggested_links\", {\n        needle: \"/\",\n        limit: \"no_limit\",\n    });\n    const allPages = res.matching_pages.map((page) => page.value);\n    allPages.push(...res.others.flatMap((o) => o.values?.map((v) => v.value) || []));\n    return allPages;\n};\n\nexport class MenuDialog extends Component {\n    static template = \"website.MenuDialog\";\n    static components = { WebsiteDialog };\n    static props = {\n        name: { type: String, optional: true },\n        url: { type: String, optional: true },\n        isMegaMenu: { type: Boolean, optional: true },\n        allPages: { type: Array, optional: true },\n        save: Function,\n        close: Function,\n    };\n\n    setup() {\n        this.website = useService(\"website\");\n        this.title = this.props.isMegaMenu ? _t(\"Mega menu item\") : _t(\"Menu item\");\n        useAutofocus();\n\n        this.urlInputRef = useRef(\"url-input\");\n        this.urlInputEdited = !!this.props.url;\n\n        this.state = useState({\n            pageNotFound: false,\n            url: this.props.url,\n            name: this.props.name,\n            invalidName: false,\n            invalidUrl: false,\n        });\n\n        onWillStart(async () => {\n            if (!this.props.isMegaMenu) {\n                this.allPages = this.props.allPages || (await getAllPages());\n            }\n        });\n\n        useEffect(\n            (input) => {\n                if (!input) {\n                    return;\n                }\n                const options = {\n                    body: this.website.pageDocument.body,\n                    position: \"bottom-fit\",\n                    classes: {\n                        \"ui-autocomplete\": \"o_edit_menu_autocomplete\",\n                    },\n                    urlChosen: () => {\n                        this.state.url = input.value;\n                        this.state.pageNotFound = false;\n                    },\n                };\n                const unmountAutocompleteWithPages = wUtils.autocompleteWithPages(\n                    input,\n                    options,\n                    this.env\n                );\n                return () => unmountAutocompleteWithPages();\n            },\n            () => [this.urlInputRef.el]\n        );\n\n        useEffect(\n            () => {\n                this.state.pageNotFound = isPageNotFound(this.state.url, this.allPages);\n            },\n            () => [this.state.url]\n        );\n\n        onMounted(() => {\n            if (!this.props.isMegaMenu) {\n                this.state.pageNotFound = isPageNotFound(this.urlInputRef.el.value, this.allPages);\n            }\n        });\n    }\n\n    onClickOk() {\n        this.state.invalidName = !this.state.name;\n        if (this.state.invalidName) {\n            return;\n        }\n\n        let url = this.state.url;\n        if (!this.props.isMegaMenu) {\n            try {\n                url = toRelativeIfSameDomain(url);\n            } catch {\n                // Do nothing if URL is invalid.\n            }\n        }\n        this.props.save(this.state.name, url, this.state.pageNotFound);\n        this.props.close();\n    }\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    onUrlInput(ev) {\n        this.state.invalidUrl = false;\n        this.urlInputEdited = true;\n    }\n\n    onTitleInput(ev) {\n        this.state.invalidName = false;\n        if (!this.urlInputEdited && !this.props.isMegaMenu) {\n            const title = ev.target.value;\n            this.state.url = title ? \"/\" + wUtils.slugify(title) : \"\";\n        }\n    }\n}\n\nclass MenuRow extends Component {\n    static template = \"website.MenuRow\";\n    static props = {\n        menu: Object,\n        edit: Function,\n        delete: Function,\n        createPage: Function,\n    };\n    static components = {\n        MenuRow,\n    };\n\n    edit() {\n        this.props.edit(this.props.menu.fields[\"id\"]);\n    }\n\n    delete() {\n        this.props.delete(this.props.menu.fields[\"id\"]);\n    }\n\n    createPage() {\n        this.props.createPage(this.props.menu.fields[\"id\"]);\n    }\n}\n\nexport class EditMenuDialog extends Component {\n    static template = \"website.EditMenuDialog\";\n    static components = {\n        MenuRow,\n        WebsiteDialog,\n    };\n    static props = [\"rootID?\", \"close\", \"save?\"];\n\n    setup() {\n        this.orm = useService(\"orm\");\n        this.website = useService(\"website\");\n        this.dialogs = useService(\"dialog\");\n\n        this.menuEditor = useRef(\"menu-editor\");\n\n        this.state = useState({ rootMenu: {} });\n\n        onWillStart(async () => {\n            this.allPages = await getAllPages();\n            const menu = await this.orm.call(\n                \"website.menu\",\n                \"get_tree\",\n                [this.website.currentWebsite.id, this.props.rootID],\n                { context: { lang: this.website.currentWebsite.metadata.lang } }\n            );\n            this.markPageNotFound(menu);\n            this.state.rootMenu = menu;\n            this.map = new Map();\n            this.populate(this.map, this.state.rootMenu);\n            this.toDelete = [];\n        });\n\n        useNestedSortable({\n            ref: this.menuEditor,\n            handle: \"div\",\n            nest: true,\n            maxLevels: 2,\n            onDrop: this._moveMenu.bind(this),\n            isAllowed: this._isAllowedMove.bind(this),\n            useElementSize: true,\n            /**\n             * @param {DOMElement} element - moved element\n             * @param {DOMElement} parent - parent element of where the element was moved\n             * @param {DOMElement} placeholder - hint element showing the current position\n             */\n            onMove: ({ element, placeholder, parent }) => {\n                // Adapt the dragged menu item to match the width and position\n                // of the placeholder.\n                element.style.width = getComputedStyle(placeholder).width;\n                element.style.marginLeft =\n                    parent && element.parentElement === this.menuEditor.el ? \"2rem\" : \"\";\n            },\n            preventDrag: (el) => el.querySelector(\":scope > button\"),\n        });\n    }\n\n    populate(map, menu) {\n        map.set(menu.fields[\"id\"], menu);\n        for (const submenu of menu.children) {\n            this.populate(map, submenu);\n        }\n    }\n\n    markPageNotFound(menu) {\n        for (const menuItem of menu.children) {\n            menuItem.page_not_found = isPageNotFound(menuItem.fields[\"url\"], this.allPages);\n            if (menuItem.children) {\n                this.markPageNotFound(menuItem);\n            }\n        }\n    }\n\n    _isAllowedMove(current, elementSelector) {\n        const currentIsMegaMenu = current.element.dataset.isMegaMenu === \"true\";\n        if (!currentIsMegaMenu) {\n            return (\n                current.placeHolder.parentNode.closest(\n                    `${elementSelector}[data-is-mega-menu=\"true\"]`\n                ) === null\n            );\n        }\n        const isDropOnRoot = current.placeHolder.parentNode.closest(elementSelector) === null;\n        return currentIsMegaMenu && isDropOnRoot;\n    }\n\n    _getMenuIdForElement(element) {\n        const menuIdStr = element.dataset.menuId;\n        const menuId = parseInt(menuIdStr);\n        return isNaN(menuId) ? menuIdStr : menuId;\n    }\n\n    _moveMenu({ element, parent, previous }) {\n        const menuId = this._getMenuIdForElement(element);\n        const menu = this.map.get(menuId);\n\n        // Remove element from parent's children (since we are moving it, this is the mandatory first step)\n        const parentId = menu.fields[\"parent_id\"] || this.state.rootMenu.fields[\"id\"];\n        let parentMenu = this.map.get(parentId);\n        parentMenu.children = parentMenu.children.filter((m) => m.fields[\"id\"] !== menuId);\n\n        // Determine next parent\n        const menuParentId = parent\n            ? this._getMenuIdForElement(parent.closest(\"li\"))\n            : this.state.rootMenu.fields[\"id\"];\n        parentMenu = this.map.get(menuParentId);\n        menu.fields[\"parent_id\"] = parentMenu.fields[\"id\"];\n\n        // Determine at which position we should place the element\n        if (previous) {\n            const previousMenu = this.map.get(this._getMenuIdForElement(previous));\n            const index = parentMenu.children.findIndex((menu) => menu === previousMenu);\n            parentMenu.children.splice(index + 1, 0, menu);\n        } else {\n            parentMenu.children.unshift(menu);\n        }\n    }\n\n    addMenu(isMegaMenu) {\n        this.dialogs.add(MenuDialog, {\n            isMegaMenu,\n            allPages: this.allPages,\n            url: \"\",\n            save: (name, url, pageNotFound, isNewWindow) => {\n                const newMenu = {\n                    fields: {\n                        id: `menu_${new Date().toISOString()}`,\n                        name,\n                        url: isMegaMenu || !url ? \"#\" : url,\n                        new_window: isNewWindow,\n                        is_mega_menu: isMegaMenu,\n                        sequence: 0,\n                        parent_id: false,\n                    },\n                    children: [],\n                    page_not_found: pageNotFound,\n                };\n                this.state.rootMenu.children.push(newMenu);\n                // this.state.rootMenu.children.at(-1) to forces a rerender\n                this.map.set(newMenu.fields[\"id\"], this.state.rootMenu.children.at(-1));\n            },\n        });\n    }\n\n    editMenu(id) {\n        const menuToEdit = this.map.get(id);\n        this.dialogs.add(MenuDialog, {\n            name: menuToEdit.fields[\"name\"],\n            url: menuToEdit.fields[\"url\"],\n            isMegaMenu: menuToEdit.fields[\"is_mega_menu\"],\n            allPages: this.allPages,\n            save: (name, url, pageNotFound) => {\n                menuToEdit.fields[\"name\"] = name;\n                menuToEdit.fields[\"url\"] = url || \"#\";\n                menuToEdit.page_not_found = pageNotFound;\n            },\n        });\n    }\n\n    deleteMenu(id) {\n        const menuToDelete = this.map.get(id);\n\n        // Delete children first\n        for (const child of menuToDelete.children) {\n            this.deleteMenu(child.fields.id);\n        }\n\n        const parentId = menuToDelete.fields[\"parent_id\"] || this.state.rootMenu.fields[\"id\"];\n        const parent = this.map.get(parentId);\n        parent.children = parent.children.filter((menu) => menu.fields[\"id\"] !== id);\n        this.map.delete(id);\n        if (parseInt(id)) {\n            this.toDelete.push(id);\n        }\n    }\n\n    async onClickSave(goToWebsite = true, url) {\n        const data = [];\n        this.map.forEach((menu, id) => {\n            if (this.state.rootMenu.fields[\"id\"] !== id) {\n                const menuFields = menu.fields;\n                const parentId = menuFields.parent_id || this.state.rootMenu.fields[\"id\"];\n                const parentMenu = this.map.get(parentId);\n                menuFields[\"sequence\"] = parentMenu.children.findIndex(\n                    (m) => m.fields[\"id\"] === id\n                );\n                menuFields[\"parent_id\"] = parentId;\n                data.push(menuFields);\n            }\n        });\n\n        await this.orm.call(\n            \"website.menu\",\n            \"save\",\n            [\n                this.website.currentWebsite.id,\n                {\n                    data: data,\n                    to_delete: this.toDelete,\n                },\n            ],\n            { context: { lang: this.website.currentWebsite.metadata.lang } }\n        );\n        if (this.props.save) {\n            this.props.save(url);\n        } else if (goToWebsite) {\n            this.website.goToWebsite();\n        }\n    }\n\n    async createPage(id) {\n        const menu = this.map.get(id);\n        let url = menu.fields[\"url\"];\n        url = url.startsWith(\"/\") ? url : \"/\" + url;\n        this.dialogs.add(AddPageDialog, {\n            onAddPage: () => {\n                this.onClickSave(false, url);\n            },\n            websiteId: this.website.currentWebsite.id,\n            forcedURL: url,\n            goToPage: !this.props.save,\n            pageTitle: menu.fields[\"name\"],\n        });\n    }\n}\n", "import { CheckBox } from \"@web/core/checkbox/checkbox\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { useService, useAutofocus } from \"@web/core/utils/hooks\";\nimport { sprintf } from \"@web/core/utils/strings\";\nimport { WebsiteDialog } from \"./dialog\";\nimport { standardFieldProps } from \"@web/views/fields/standard_field_props\";\nimport { FormViewDialog } from \"@web/views/view_dialogs/form_view_dialog\";\nimport { formView } from \"@web/views/form/form_view\";\nimport { renderToFragment } from \"@web/core/utils/render\";\nimport { Component, onWillDestroy, useEffect, useRef, useState, xml } from \"@odoo/owl\";\nimport { FormController } from \"@web/views/form/form_controller\";\nimport { registry } from \"@web/core/registry\";\n\nexport class PageDependencies extends Component {\n    static template = \"website.PageDependencies\";\n    static popoverTemplate = xml`\n        <div class=\"popover o_page_dependencies\" role=\"tooltip\">\n            <div class=\"arrow\"/>\n            <h3 class=\"popover-header\"/>\n            <div class=\"popover-body\"/>\n        </div>\n    `;\n    static props = {\n        resIds: Array,\n        resModel: String,\n        mode: String,\n    };\n\n    setup() {\n        super.setup();\n        this.orm = useService(\"orm\");\n\n        this.action = useRef(\"action\");\n        this.sprintf = sprintf;\n\n        useEffect(\n            () => {\n                this.fetchDependencies();\n            },\n            () => []\n        );\n        this.state = useState({\n            dependencies: {},\n        });\n\n        onWillDestroy(async () => {\n            await this.destroyDependenciesPopover();\n        });\n    }\n\n    async getResIds() {\n        return this.props.resIds;\n    }\n\n    async fetchDependencies() {\n        this.state.dependencies = await this.orm.call(\"website\", \"search_url_dependencies\", [\n            this.props.resModel,\n            await this.getResIds(),\n        ]);\n    }\n\n    showDependencies() {\n        const popover = window.Popover.getOrCreateInstance(this.action.el, {\n            title: _t(\"Dependencies\"),\n            boundary: \"viewport\",\n            placement: \"right\",\n            trigger: \"focus\",\n            content: () =>\n                renderToFragment(\"website.PageDependencies.Tooltip\", {\n                    dependencies: this.state.dependencies,\n                }),\n        });\n        popover.toggle();\n    }\n\n    async destroyDependenciesPopover() {\n        const actionEl = this.action.el;\n        const popover = window.Popover.getInstance(actionEl);\n        if (popover) {\n            // If popover is hiding (animation), wait for the animation to\n            // complete.\n            if (!popover.tip.classList.contains(\"show\")) {\n                await new Promise((resolve) => {\n                    const handler = () => {\n                        actionEl.removeEventListener(\"hidden.bs.popover\", handler);\n                        resolve();\n                    };\n                    actionEl.addEventListener(\"hidden.bs.popover\", handler);\n                });\n            }\n            popover.dispose();\n        }\n    }\n}\n\nexport class FormPageDependencies extends PageDependencies {\n    static props = {\n        ...standardFieldProps,\n        ...PageDependencies.props,\n        resIds: { type: Array, optional: true },\n    };\n\n    async getResIds() {\n        const records = await this.orm.read(\n            this.props.record.resModel,\n            [this.props.record.resId],\n            [\"target_model_id\"]\n        );\n        return records.map((record) => record.target_model_id[0]);\n    }\n}\n\nexport const formPageDependenciesWidget = {\n    component: FormPageDependencies,\n    extractProps: ({ attrs }) => {\n        const { mode, name, resModel, resIds } = attrs;\n        return {\n            mode,\n            name: name || \"\",\n            resModel,\n            resIds,\n        };\n    },\n};\nregistry.category(\"view_widgets\").add(\"form_page_dependencies\", formPageDependenciesWidget);\n\nexport class DeletePageDialog extends Component {\n    static template = \"website.DeletePageDialog\";\n    static components = {\n        PageDependencies,\n        CheckBox,\n        WebsiteDialog,\n    };\n    static props = {\n        resIds: Array,\n        resModel: String,\n        onDelete: { type: Function, optional: true },\n        close: Function,\n        hasNewPageTemplate: { type: Boolean, optional: true },\n    };\n\n    setup() {\n        this.website = useService(\"website\");\n\n        this.state = useState({\n            confirm: false,\n        });\n    }\n\n    onConfirmCheckboxChange(checked) {\n        this.state.confirm = checked;\n    }\n\n    onClickDelete() {\n        this.props.close();\n        this.props.onDelete();\n    }\n}\n\nexport class DuplicatePageDialog extends Component {\n    static components = { WebsiteDialog };\n    static template = \"website.DuplicatePageDialog\";\n    static props = {\n        onDuplicate: Function,\n        close: Function,\n        pageIds: { type: Array, element: Number },\n    };\n\n    setup() {\n        this.orm = useService(\"orm\");\n        this.website = useService(\"website\");\n        useAutofocus();\n\n        this.state = useState({\n            name: \"\",\n        });\n    }\n\n    async duplicate() {\n        const duplicates = [];\n        if (this.state.name) {\n            for (let count = 0; count < this.props.pageIds.length; count++) {\n                const name = this.state.name + (count ? ` ${count + 1}` : \"\");\n                duplicates.push(\n                    await this.orm.call(\"website.page\", \"clone_page\", [\n                        this.props.pageIds[count],\n                        name,\n                    ])\n                );\n            }\n        }\n        this.props.onDuplicate(duplicates);\n    }\n}\n\nexport class PagePropertiesFormController extends FormController {\n    static props = {\n        ...FormController.props,\n        clonePage: { type: Function, optional: true },\n        deletePage: { type: Function, optional: true },\n    };\n}\n\nregistry.category(\"views\").add(\"page_properties_dialog_form\", {\n    ...formView,\n    Controller: PagePropertiesFormController,\n});\n\nexport class PagePropertiesDialog extends FormViewDialog {\n    static props = {\n        ...FormViewDialog.props,\n        onClose: { type: Function, optional: true },\n        resModel: { type: String, optional: true },\n    };\n\n    static defaultProps = {\n        ...FormViewDialog.defaultProps,\n        title: _t(\"Page Properties\"),\n        size: \"md\",\n        onClose: () => {},\n    };\n\n    setup() {\n        super.setup();\n        this.dialog = useService(\"dialog\");\n        this.orm = useService(\"orm\");\n        this.website = useService(\"website\");\n\n        this.viewProps = {\n            ...this.viewProps,\n            resId: this.resId,\n            resModel: this.resModel,\n            context: Object.assign(\n                {\n                    form_view_ref: this.isPage\n                        ? \"website.website_page_properties_view_form\"\n                        : \"website.website_page_properties_base_view_form\",\n                },\n                this.viewProps.context\n            ),\n            ...(this.isPage\n                ? {\n                      buttonTemplate: \"website.PagePropertiesDialogButtons\",\n                      clonePage: this.clonePage.bind(this),\n                      deletePage: this.deletePage.bind(this),\n                  }\n                : {}),\n        };\n    }\n\n    get resId() {\n        return this.props.resId;\n    }\n\n    get resModel() {\n        if (this.props.resModel) {\n            return this.props.resModel;\n        }\n        return this.isPage ? \"website.page.properties\" : \"website.page.properties.base\";\n    }\n\n    get targetId() {\n        return this.website.currentWebsite?.metadata.mainObject.id;\n    }\n\n    get targetModel() {\n        return this.website.currentWebsite?.metadata.mainObject.model;\n    }\n\n    get isPage() {\n        return this.targetModel === \"website.page\";\n    }\n\n    clonePage() {\n        this.dialog.add(DuplicatePageDialog, {\n            pageIds: [this.targetId],\n            onDuplicate: (duplicates) => {\n                this.props.close();\n                this.props.onClose();\n                this.website.goToWebsite({ path: duplicates[0], edition: true });\n            },\n        });\n    }\n\n    async deletePage() {\n        const pageIds = [this.targetId];\n        const newPageTemplateFields = await this.orm.read(\"website.page\", pageIds, [\n            \"is_new_page_template\",\n        ]);\n        this.dialog.add(DeletePageDialog, {\n            resIds: pageIds,\n            resModel: \"website.page\",\n            onDelete: async () => {\n                await this.orm.unlink(\"website.page\", pageIds);\n                this.website.goToWebsite({ path: \"/\" });\n                this.props.close();\n                this.props.onClose();\n            },\n            hasNewPageTemplate: newPageTemplateFields[0].is_new_page_template,\n        });\n    }\n}\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { deduceURLfromText } from \"@html_editor/main/link/utils\";\nimport { pyToJsLocale, jsToPyLocale } from \"@web/core/l10n/utils\";\nimport { htmlToTextContentInline } from \"@mail/utils/common/format\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { escapeRegExp } from \"@web/core/utils/strings\";\nimport { useService, useAutofocus } from \"@web/core/utils/hooks\";\nimport { isVisible } from \"@web/core/utils/ui\";\nimport { CheckBox } from \"@web/core/checkbox/checkbox\";\nimport { MediaDialog } from \"@html_editor/main/media/media_dialog/media_dialog\";\nimport { WebsiteDialog } from \"./dialog\";\nimport {\n    Component,\n    onMounted,\n    onWillStart,\n    reactive,\n    useEffect,\n    useState,\n    useRef,\n} from \"@odoo/owl\";\nimport wUtils from \"@website/js/utils\";\n\n// This replaces \\b, because accents(e.g. \u00e0, \u00e9) are not seen as word boundaries.\n// Javascript \\b is not unicode aware, and words beginning or ending by accents won't match \\b\nconst WORD_SEPARATORS_REGEX =\n    \"([\\\\u2000-\\\\u206F\\\\u2E00-\\\\u2E7F'!\\\"#\\\\$%&\\\\(\\\\)\\\\*\\\\+,\\\\-\\\\.\\\\/:;<=>\\\\?\u00bf\u00a1@\\\\[\\\\]\\\\^_`\\\\{\\\\|\\\\}~\\\\s]+|^|$)\";\n\nconst seoContext = reactive({\n    description: \"\",\n    keywords: [],\n    title: \"\",\n    seoName: \"\",\n    metaImage: \"\",\n    defaultTitle: \"\",\n    updatedAlts: [],\n    brokenLinks: [],\n});\n\nconst LINK_CHECK_BASE_OPTIONS = {\n    method: \"GET\",\n    referrerPolicy: \"no-referrer\",\n    credentials: \"omit\",\n};\nconst LINK_CHECK_MANUAL_OPTIONS = { ...LINK_CHECK_BASE_OPTIONS, redirect: \"manual\" };\nconst LINK_CHECK_NO_CORS_OPTIONS = { ...LINK_CHECK_BASE_OPTIONS, mode: \"no-cors\" };\n\nconst inspectLink = async (url, options) => {\n    try {\n        const response = await fetch(url, options);\n        if (response.type === \"opaqueredirect\") {\n            return \"external_redirect\";\n        }\n        if (response.status >= 400) {\n            return \"error\";\n        }\n        return \"ok\";\n    } catch {\n        return \"failed\";\n    }\n};\n\nconst checkLinkStatus = async (url, { useNoCorsFallback = false } = {}) => {\n    let status = await inspectLink(url, LINK_CHECK_BASE_OPTIONS);\n    if (status === \"failed\") {\n        const fallbackStatus = await inspectLink(url, LINK_CHECK_MANUAL_OPTIONS);\n        status = fallbackStatus === \"external_redirect\" ? \"ok\" : fallbackStatus;\n    }\n    if (useNoCorsFallback && status === \"failed\") {\n        status = await inspectLink(url, LINK_CHECK_NO_CORS_OPTIONS);\n    }\n    return status;\n};\n\nconst getSeo = async (self, onlyKeywords = false) => {\n    const pageTextContentEl =\n        self.website.pageDocument.documentElement.querySelector(\"#wrap, main, body\");\n    const lang = self.state.language || \"en\";\n    const tagWeights = {\n        h1: 5,\n        h2: 4,\n        h3: 3,\n        a: 2,\n        p: 1,\n    };\n    const maxNGrams = 2;\n\n    const getKeywordsFromText = (text, weight, wordCounts) => {\n        const segmenter = new Intl.Segmenter(lang, { granularity: \"word\" });\n        const segmentedText = segmenter.segment(text);\n        const words = [...segmentedText]\n            .filter((s) => s.isWordLike)\n            .map((s) => s.segment.toLowerCase())\n            .filter((word) => !/[0-9]+/.test(word));\n        const singleWordsLength = words.length;\n        for (let nGram = maxNGrams; nGram > 1; nGram--) {\n            for (let i = 0; i <= singleWordsLength - nGram; i++) {\n                if (words[i].length > 4 && words[i + nGram - 1].length > 4) {\n                    words.push(words.slice(i, i + nGram).join(\" \"));\n                }\n            }\n        }\n        if (words) {\n            words\n                .filter((word) => word.length > 4)\n                .forEach((word) => {\n                    if (!wordCounts[word]) {\n                        wordCounts[word] = 0;\n                    }\n                    wordCounts[word] += weight * (word.length - 3);\n                });\n        }\n    };\n\n    const jaccardSimilarity = (str1, str2) => {\n        const set1 = new Set(str1.replace(/\\s+/g, \"\").split(\"\"));\n        const set2 = new Set(str2.replace(/\\s+/g, \"\").split(\"\"));\n\n        const intersection = new Set([...set1].filter((item) => set2.has(item)));\n        const union = new Set([...set1, ...set2]);\n\n        return intersection.size / union.size;\n    };\n\n    const extractKeywords = () => {\n        const wordCounts = {};\n\n        Object.keys(tagWeights).forEach((tag) => {\n            const elements = pageTextContentEl.getElementsByTagName(tag);\n            for (const element of elements) {\n                getKeywordsFromText(element.innerText, tagWeights[tag], wordCounts);\n            }\n        });\n\n        const keywords = Object.entries(wordCounts)\n            .sort((a, b) => b[1] - a[1])\n            .slice(0, 20);\n\n        for (let i = 0; i < keywords.length; i++) {\n            for (let j = 0; j < keywords.length; j++) {\n                if (i == j || keywords[i][1] === 0) {\n                    continue;\n                }\n                if (jaccardSimilarity(keywords[i][0], keywords[j][0]) > 0.5) {\n                    keywords[i][1] += keywords[j][1];\n                    keywords[j][1] = 0;\n                }\n            }\n        }\n        const sortedKeywords = keywords\n            .sort((a, b) => b[1] - a[1])\n            .filter((entry) => entry[1] > 0)\n            .map((entry) => entry[0])\n            .slice(0, 7);\n        return sortedKeywords;\n    };\n\n    const extractDescription = () => {\n        let subtitlesEls = pageTextContentEl.querySelectorAll(\n            \"[class*='subtitle'],[class*='lead'],[data-oe-field*='subtitle'],[data-oe-field*='description']\"\n        );\n        subtitlesEls = Array.from(subtitlesEls).filter(\n            (el) => isVisible(el) && el.innerText.trim()\n        );\n        if (subtitlesEls.length) {\n            return subtitlesEls[0].innerText.trim();\n        }\n        let headersEls = pageTextContentEl.querySelectorAll(\"h2,h3\");\n        headersEls = Array.from(headersEls).filter(\n            (el) => isVisible(el) && el.innerText.trim().replace(/[\\W\\d]/g, \"\")\n        );\n        if (headersEls.length) {\n            return headersEls.map((el) => el.innerText.trim().replace(/\\s+/g, \" \")).join(\", \");\n        }\n        return self.seoContext.title || self.seoContext.description || \"\";\n    };\n\n    const keywords = extractKeywords();\n    if (keywords.length) {\n        self.seoContext.keywords = keywords;\n    }\n    if (!onlyKeywords) {\n        self.seoContext.title = htmlToTextContentInline(self.seoContext.defaultTitle);\n        self.seoContext.description = extractDescription();\n    }\n};\n\nclass MetaImage extends Component {\n    static template = \"website.MetaImage\";\n    static props = [\"active\", \"src\", \"custom\", \"selectImage\"];\n}\n\nclass ImageSelector extends Component {\n    static template = \"website.ImageSelector\";\n    static components = {\n        MetaImage,\n    };\n    static props = {\n        previewDescription: String,\n        defaultTitle: String,\n        hasSocialDefaultImage: Boolean,\n        pageImages: Array,\n        url: String,\n    };\n\n    setup() {\n        this.website = useService(\"website\");\n        this.dialogs = useService(\"dialog\");\n\n        this.seoContext = useState(seoContext);\n\n        const firstImageId = this.props.hasSocialDefaultImage ? \"social_default_image\" : \"logo\";\n        const firstImageSrc = `/web/image/website/${encodeURIComponent(\n            this.website.currentWebsite.id\n        )}/${firstImageId}`;\n        const firstImage = {\n            src: firstImageSrc,\n            active: this.areSameImages(firstImageSrc, this.seoContext.metaImage),\n            custom: false,\n        };\n\n        this.state = useState({\n            images: [\n                firstImage,\n                ...this.props.pageImages.map((src) => ({\n                    src,\n                    active: this.areSameImages(src, this.seoContext.metaImage),\n                    custom: false,\n                })),\n            ],\n        });\n\n        if (\n            this.seoContext.metaImage &&\n            !this.state.images\n                .map(({ src }) => this.getImagePathname(src))\n                .includes(this.getImagePathname(this.seoContext.metaImage))\n        ) {\n            this.state.images.push({\n                src: this.seoContext.metaImage,\n                active: true,\n                custom: true,\n            });\n        }\n\n        if (!this.activeMetaImage) {\n            this.selectImage(this.state.images[0].src);\n        }\n    }\n\n    get activeMetaImage() {\n        const activeImage = this.state.images.find(({ active }) => active);\n        return activeImage && activeImage.src;\n    }\n\n    getImagePathname(src) {\n        return new URL(src, this.website.pageDocument.location.origin).pathname;\n    }\n\n    areSameImages(src1, src2) {\n        return this.getImagePathname(src1) === this.getImagePathname(src2);\n    }\n\n    selectImage(src) {\n        this.state.images = this.state.images.map((img) => {\n            img.active = img.src === src;\n            return img;\n        });\n        this.seoContext.metaImage = src;\n    }\n\n    openMediaDialog() {\n        this.dialogs.add(MediaDialog, {\n            onlyImages: true,\n            resModel: \"ir.ui.view\",\n            useMediaLibrary: true,\n            save: (image) => {\n                let existingImage;\n                const src = image.getAttribute(\"src\");\n                this.state.images = this.state.images.map((img) => {\n                    img.active = false;\n                    if (img.src === src) {\n                        existingImage = img;\n                        img.active = true;\n                    }\n                    return img;\n                });\n                if (!existingImage) {\n                    this.state.images.push({\n                        src: src,\n                        active: true,\n                        custom: true,\n                    });\n                }\n                this.seoContext.metaImage = src;\n            },\n        });\n    }\n}\n\nclass Keyword extends Component {\n    static template = \"website.Keyword\";\n    static props = {\n        language: String,\n        keyword: String,\n        addKeyword: Function,\n        removeKeyword: Function,\n    };\n\n    setup() {\n        this.website = useService(\"website\");\n\n        this.seoContext = useState(seoContext);\n\n        this.state = useState({\n            suggestions: [],\n        });\n\n        this.translatedStrings = {\n            usedInH1: _t('\"%(keyword)s\" is used in page first level heading', {\n                keyword: this.props.keyword,\n            }),\n            usedInH2: _t('\"%(keyword)s\" is used in page second level heading', {\n                keyword: this.props.keyword,\n            }),\n            usedInTitle: _t('\"%(keyword)s\" is used in page title', {\n                keyword: this.props.keyword,\n            }),\n            usedInDescription: _t('\"%(keyword)s\" is used in page description', {\n                keyword: this.props.keyword,\n            }),\n            usedInContent: _t('\"%(keyword)s\" is used in page content', {\n                keyword: this.props.keyword,\n            }),\n            suggestionTag: (suggestion) => _t('Add \"%(suggestion)s\"', { suggestion }),\n            googleTrendsTitle: _t(\"See Google Trends about '%(keyword)s'\", {\n                keyword: this.props.keyword,\n            }),\n            removeBtn: _t('Remove \"%(keyword)s\"', { keyword: this.props.keyword }),\n        };\n\n        onMounted(async () => {\n            const suggestions = await rpc(\"/website/seo_suggest\", {\n                lang: jsToPyLocale(this.props.language),\n                keywords: this.props.keyword,\n            });\n            const regex = new RegExp(\n                WORD_SEPARATORS_REGEX + escapeRegExp(this.props.keyword) + WORD_SEPARATORS_REGEX,\n                \"gi\"\n            );\n            this.state.suggestions = [\n                ...new Set(\n                    JSON.parse(suggestions)\n                        .map((word) => word.replace(regex, \"\").trim())\n                        .filter(Boolean)\n                ),\n            ];\n        });\n    }\n\n    isKeywordIn(string) {\n        return new RegExp(\n            WORD_SEPARATORS_REGEX + escapeRegExp(this.props.keyword) + WORD_SEPARATORS_REGEX,\n            \"gi\"\n        ).test(string);\n    }\n\n    getGoogleTrendsURL() {\n        return `https://trends.google.com/trends/explore?q=${encodeURIComponent(\n            this.props.keyword\n        )}`;\n    }\n\n    getHeaders(tag) {\n        return Array.from(\n            this.website.pageDocument.documentElement.querySelectorAll(`#wrap ${tag}`)\n        ).map((header) => header.textContent);\n    }\n\n    getBodyText() {\n        return this.website.pageDocument.body.textContent;\n    }\n\n    get usedInH1() {\n        return this.isKeywordIn(this.getHeaders(\"h1\"));\n    }\n\n    get usedInH2() {\n        return this.isKeywordIn(this.getHeaders(\"h2\"));\n    }\n\n    get usedInTitle() {\n        return this.isKeywordIn(this.seoContext.title || this.seoContext.defaultTitle);\n    }\n\n    get usedInDescription() {\n        return this.isKeywordIn(this.seoContext.description);\n    }\n\n    get usedInContent() {\n        return this.isKeywordIn(this.getBodyText());\n    }\n}\n\nclass MetaKeywords extends Component {\n    static template = \"website.MetaKeywords\";\n    static components = {\n        Keyword,\n    };\n    static props = {};\n\n    setup() {\n        this.website = useService(\"website\");\n\n        this.seoContext = useState(seoContext);\n\n        this.state = useState({\n            language: \"\",\n            keyword: \"\",\n        });\n\n        this.maxKeywords = 10;\n\n        onWillStart(async () => {\n            this.languages = await rpc(\"/website/get_languages\");\n            this.state.language = this.getLanguage();\n        });\n    }\n\n    provideKeywords() {\n        getSeo(this, true);\n    }\n\n    onKeyup(ev) {\n        // Add keyword on enter.\n        if (ev.key === \"Enter\") {\n            this.addKeyword(this.state.keyword);\n        }\n    }\n\n    getLanguage() {\n        return pyToJsLocale(\n            this.website.pageDocument.documentElement.getAttribute(\"lang\") || \"en-US\"\n        );\n    }\n\n    get isFull() {\n        return this.seoContext.keywords.length >= this.maxKeywords;\n    }\n\n    addKeyword(keyword) {\n        keyword = keyword.replaceAll(/,\\s*/gi, \" \").trim();\n        if (keyword && !this.isFull && !this.seoContext.keywords.includes(keyword)) {\n            this.seoContext.keywords.push(keyword);\n            this.state.keyword = \"\";\n        }\n    }\n\n    removeKeyword(keyword) {\n        this.seoContext.keywords = this.seoContext.keywords.filter((kw) => kw !== keyword);\n    }\n}\n\nclass SEOPreview extends Component {\n    static template = \"website.SEOPreview\";\n    static props = {\n        isIndexed: Boolean,\n        title: String,\n        description: String,\n        url: String,\n    };\n\n    setup() {\n        this.website = useService(\"website\");\n        this.seoContext = useState(seoContext);\n        this.logo = `/web/image/website/${encodeURIComponent(this.website.currentWebsite.id)}/logo`;\n    }\n\n    get urlToBreadcrumbs() {\n        const MAX_LENGTH = 45;\n        const REPLACEMENT = \"\u2026\";\n        let translatedPage = false;\n        if (this.website.currentWebsite.metadata.langName !== undefined) {\n            translatedPage = true;\n        }\n        const urlObj = new URL(this.props.url);\n        const hostname = urlObj.hostname;\n        const path = urlObj.pathname;\n\n        const segments = path.split(\"/\").filter((segment) => segment);\n        // Remove non-readable elements (numeric parts)\n        const readableSegments = segments.map((segment) => {\n            // Remove numeric suffixes (e.g., \"astronomy-2\" becomes \"astronomy\")\n            const noNumericSuffix = segment.replace(/-\\d+$/, \"\");\n            // Replace dashes with spaces and remove numbers\n            return noNumericSuffix.replace(/-/g, \" \").replace(/\\d+/g, \"\");\n        });\n        // Capitalise the first word of each segment\n        let capitalisedSegments = readableSegments.map(\n            (segment) => segment.replace(/\\b\\w/, (char) => char.toUpperCase()) // Capitalise each word\n        );\n        // Remove the localisation part if it's there\n        if (translatedPage) {\n            capitalisedSegments = capitalisedSegments.slice(1);\n        }\n        capitalisedSegments.unshift(`https://${hostname}`);\n        // Manage the truncated parts if it's too long\n        let lastIndexOfEllipsis = null;\n        while (\n            capitalisedSegments.length > 2 &&\n            capitalisedSegments.join(\"   \").length > MAX_LENGTH\n        ) {\n            let replaced = false;\n            for (let index = 1; index < capitalisedSegments.length - 1; index++) {\n                if (capitalisedSegments[index] !== REPLACEMENT) {\n                    capitalisedSegments[index] = REPLACEMENT;\n                    replaced = true;\n                    lastIndexOfEllipsis = index;\n                    break;\n                }\n            }\n            if (!replaced) {\n                break;\n            }\n        }\n        if (lastIndexOfEllipsis) {\n            capitalisedSegments = capitalisedSegments.filter(\n                (item, index) => item !== REPLACEMENT || index === lastIndexOfEllipsis\n            );\n        }\n        return capitalisedSegments.join(\" \u203a \");\n    }\n\n    get description() {\n        if (this.props.description?.length > 160) {\n            return this.props.description.substring(0, 159) + \"\u2026\";\n        } else if (!this.props.description?.length) {\n            return _t(\n                \"If you don't write one, the description will be generated by the search engines based on the content of your page.\"\n            );\n        }\n        return this.props.description || \"\";\n    }\n}\nclass TitleDescription extends Component {\n    static template = \"website.TitleDescription\";\n    static props = {\n        canEditSeo: Boolean,\n        canEditDescription: Boolean,\n        canEditUrl: Boolean,\n        canEditTitle: Boolean,\n        seoNameHelp: String,\n        seoNameDefault: { optional: true, String },\n        isIndexed: Boolean,\n        defaultTitle: String,\n        previewDescription: String,\n        url: String,\n    };\n    static components = {\n        SEOPreview,\n    };\n\n    setup() {\n        this.seoContext = useState(seoContext);\n        this.website = useService(\"website\");\n        useAutofocus();\n\n        this.state = useState({\n            language: this.getLanguage(),\n        });\n        this.previousSeoName = this.seoContext.seoName;\n\n        this.maxRecommendedDescriptionSize = 160;\n        this.minRecommendedDescriptionSize = 50;\n\n        this.titleTooltip = _t(\n            'Add your own title or leave empty to use \"%(defaultTitle)s\". Your page title should contain max 65 characters.',\n            { defaultTitle: this.props.defaultTitle }\n        );\n\n        // Update the title when its input value changes\n        useEffect(\n            () => {\n                document.title = this.title;\n            },\n            () => [this.seoContext.title]\n        );\n\n        // Restore the original title when unmounting the component\n        useEffect(\n            () => {\n                const initialTitle = document.title;\n                return () => (document.title = initialTitle);\n            },\n            () => []\n        );\n    }\n\n    //--------------------------------------------------------------------------\n    // Getters\n    //--------------------------------------------------------------------------\n\n    get seoNameUrl() {\n        return this.previousSeoName || this.props.seoNameDefault;\n    }\n\n    get seoNamePre() {\n        return this.pathname.split(this.seoNameUrl)[0];\n    }\n\n    get seoNamePost() {\n        return this.pathname.split(this.seoNameUrl).slice(-1)[0]; // at least the -id theorically\n    }\n\n    get pathname() {\n        return new URL(this.props.url).pathname;\n    }\n\n    get url() {\n        if (this.seoContext.seoName) {\n            return this.props.url.replace(this.seoNameUrl, this.seoContext.seoName);\n        }\n        return this.props.url.replace(this.seoNameUrl, this.props.seoNameDefault);\n    }\n\n    get titleOrDescriptionNotSet() {\n        return !this.seoContext.title || !this.seoContext.description;\n    }\n\n    get title() {\n        return this.seoContext.title || this.props.defaultTitle;\n    }\n\n    get description() {\n        return this.seoContext.description || \"\";\n    }\n\n    get descriptionWarning() {\n        if (!this.seoContext.description) {\n            return false;\n        }\n        if (this.seoContext.description.length < this.minRecommendedDescriptionSize) {\n            return _t(\"Too short (min 50 chars)\");\n        } else if (this.seoContext.description.length > this.maxRecommendedDescriptionSize) {\n            return _t(\"Too long (max 160 chars)\");\n        }\n        return false;\n    }\n\n    getLanguage() {\n        return pyToJsLocale(\n            this.website.pageDocument.documentElement.getAttribute(\"lang\") || \"en-US\"\n        );\n    }\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    autoFill() {\n        getSeo(this);\n    }\n\n    /**\n     * @private\n     * @param {InputEvent} ev\n     */\n    _updateInputValue(ev) {\n        this.seoContext.seoName = wUtils.slugify(ev.target.value);\n    }\n}\n\nexport class BrokenLink extends Component {\n    static template = \"website.BrokenLink\";\n\n    static props = {\n        link: Object,\n    };\n\n    setup() {\n        this.website = useService(\"website\");\n        this.urlInputRef = useRef(\"url-input\");\n        this.link = this.props.link;\n\n        this.state = useState({\n            checkingLink: false,\n        });\n\n        useEffect(\n            (input) => {\n                if (!input) {\n                    return;\n                }\n                const options = {\n                    body: this.website.pageDocument.body,\n                    position: \"bottom-fit\",\n                    urlChosen: () => {\n                        this.link.newLink = input.value;\n                    },\n                };\n                const unmountAutocompleteWithPages = wUtils.autocompleteWithPages(\n                    input,\n                    options,\n                    this.env\n                );\n                return () => unmountAutocompleteWithPages();\n            },\n            () => [this.urlInputRef.el]\n        );\n    }\n\n    async modifyLink(link) {\n        this.state.checkingLink = true;\n        let broken = false;\n        link.newLink = deduceURLfromText(link.newLink) || link.newLink;\n        let url;\n        try {\n            const base = link.newLink.startsWith(\"/\") ? window.origin : undefined;\n            url = new URL(link.newLink, base);\n        } catch {\n            url = null;\n            broken = true;\n        }\n        if (url?.protocol === \"http:\" || url?.protocol === \"https:\") {\n            const sameOrigin = url.origin === window.location.origin;\n            const targetUrl = sameOrigin ? url.pathname + url.search : url.href;\n            const status = await checkLinkStatus(targetUrl, {\n                useNoCorsFallback: !sameOrigin,\n            });\n            broken = status === \"error\" || status === \"failed\";\n        }\n        link.broken = broken;\n        link.validLink = !broken ? link.newLink : null;\n        this.state.checkingLink = false;\n    }\n\n    removeLink(link) {\n        link.newLink = \"\";\n        link.broken = false;\n        link.remove = true;\n    }\n\n    checkButtonDisabled(link) {\n        return (\n            this.state.checkingLink ||\n            !link.newLink.trim().length ||\n            link.newLink.trim() == link.oldLink ||\n            link.validLink == link.newLink.trim()\n        );\n    }\n}\n\nexport class SeoChecks extends Component {\n    static template = \"website.SeoChecks\";\n    static components = {\n        CheckBox,\n        BrokenLink,\n    };\n    static props = {};\n\n    async setup() {\n        this.website = useService(\"website\");\n        this.seoContext = useState(seoContext);\n        const {\n            metadata: { mainObject, seoObject },\n        } = this.website.currentWebsite;\n        this.object = seoObject || mainObject;\n        this.state = useState({\n            altAttributes: [],\n            checkingLinks: false,\n            checkedLinks: false,\n            counterLinks: 0,\n            totalLinks: 0,\n        });\n        this.imgUpdated = this.imgUpdated.bind(this);\n        onWillStart(async () => {\n            this.state.altAttributes = await this.getAltAttributes();\n            this.seoContext.updatedAlts = [];\n        });\n        onMounted(() => {\n            this.getBrokenLinks();\n        });\n    }\n\n    imgUpdated(img) {\n        img.updated = true;\n        this.seoContext.updatedAlts = this.state.altAttributes.filter((img) => img.updated);\n    }\n\n    async getAltAttributes() {\n        const uniqueRecords = new Set();\n\n        // Select all relevant <img> elements in the editable page.\n        const imgEls = this.website.pageDocument.documentElement.querySelectorAll(\"#wrapwrap img\");\n\n        imgEls.forEach((el) => {\n            // Find the closest ancestor element containing Odoo metadata.\n            const recordEl = el.closest(\"[data-oe-model][data-oe-field][data-oe-id]\");\n            if (!recordEl) {\n                return; // Skip images without a proper metadata wrapper.\n            }\n\n            const model = recordEl.dataset.oeModel;\n            const id = recordEl.dataset.oeId;\n            const field = recordEl.dataset.oeField;\n            const type = recordEl.dataset.oeType;\n\n            // Only include images that belong to static content definitions.\n            if ((model !== \"ir.ui.view\" || field !== \"arch\") && type !== \"html\") {\n                return;\n            }\n\n            // Build a unique signature string to avoid duplicates.\n            uniqueRecords.add(`${model}||${id}||${field}||${type}`);\n        });\n\n        // Transform the Set of unique strings back into structured objects.\n        const models = Array.from(uniqueRecords).map((entry) => {\n            const [model, id, field, type] = entry.split(\"||\");\n            return { model, id: parseInt(id), field, type };\n        });\n\n        const results = await rpc(\"/website/get_alt_images\", { models });\n\n        return JSON.parse(results);\n    }\n\n    async getBrokenLinks() {\n        this.state.checkingLinks = true;\n        this.state.counterLinks = 1;\n        const hrefEls = this.website.pageDocument.documentElement.querySelectorAll(\n            \"#wrapwrap a[href]:not(.oe_unremovable)\"\n        );\n        let links = Array.from(hrefEls)\n            .filter((a) => {\n                const href = a.href;\n                // Check if the href is not empty and belongs to the same origin as the\n                // current page\n                return (\n                    href !== \"\" &&\n                    href.startsWith(\"http\") &&\n                    new URL(href).origin === window.location.origin &&\n                    a.getAttribute(\"href\") !== \"#\"\n                );\n            })\n            .map((el, index) => {\n                const recordEl = el.closest(\n                    \"[data-res-model][data-res-id], [data-oe-model][data-oe-id]\"\n                );\n                if (\n                    !recordEl ||\n                    ((recordEl.dataset.oeModel !== \"ir.ui.view\" ||\n                        recordEl.dataset.oeField !== \"arch\") &&\n                        recordEl.dataset.oeType !== \"html\")\n                ) {\n                    return false;\n                }\n                const hashIndex = el.href.indexOf(\"#\");\n                const cleanedUrl = hashIndex !== -1 ? el.href.substring(0, hashIndex) : el.href;\n                const path = new URL(cleanedUrl);\n                let label = \"\";\n                let isImageLink = false;\n                const textContent = el.textContent.trim();\n                if (textContent) {\n                    label = textContent;\n                } else {\n                    const imgLinkEl = el.querySelector(\"img\");\n                    if (imgLinkEl?.src) {\n                        label = imgLinkEl.src.split(\"/\").pop();\n                        isImageLink = true;\n                    } else if (el.querySelector(\".fa\")) {\n                        label =\n                            el.ariaLabel || el.title || el.href.split(\"/\").filter(Boolean).pop();\n                        isImageLink = true;\n                    }\n                }\n                return {\n                    link: path.pathname + path.search,\n                    res_model: recordEl.dataset.resModel || recordEl.dataset.oeModel,\n                    res_id: parseInt(recordEl.dataset.resId || recordEl.dataset.oeId, 10),\n                    field: recordEl.dataset.oeField || null,\n                    label: label,\n                    isImageLink: isImageLink,\n                    position: index,\n                };\n            })\n            .filter(Boolean);\n        const seen = new Set();\n        links = links.filter((item) => {\n            const key = `${item.link}::${item.res_model || \"\"}::${item.res_id || \"\"}::${\n                item.field || \"\"\n            }`;\n            if (seen.has(key)) {\n                return false;\n            }\n            seen.add(key);\n            return true;\n        });\n        this.state.totalLinks = links.length;\n        const brokenLinks = [];\n        const promises = links.map(async (link) => {\n            // Let the browser follow internal redirects; most site routes land here.\n            const status = await checkLinkStatus(link.link);\n\n            if (status === \"error\" || status === \"failed\") {\n                brokenLinks.push(link);\n            }\n\n            this.state.counterLinks++;\n        });\n        await Promise.all(promises);\n        this.state.checkingLinks = false;\n        this.state.checkedLinks = true;\n        // Keep links order in the DOM.\n        brokenLinks.sort((a, b) => a.position - b.position);\n        this.seoContext.brokenLinks = brokenLinks.map((link) => ({\n            oldLink: link.link,\n            newLink: link.link,\n            broken: true,\n            remove: false,\n            res_model: link.res_model,\n            res_id: link.res_id,\n            field: link.field,\n            label: link.label,\n            isImageLink: link.isImageLink,\n            validLink: null,\n        }));\n    }\n}\n\nexport class OptimizeSEODialog extends Component {\n    static template = \"website.OptimizeSEODialog\";\n    static components = {\n        WebsiteDialog,\n        TitleDescription,\n        ImageSelector,\n        MetaKeywords,\n        SeoChecks,\n        BrokenLink,\n    };\n    static props = {\n        close: Function,\n    };\n\n    setup() {\n        this.website = useService(\"website\");\n        this.dialogs = useService(\"dialog\");\n        this.orm = useService(\"orm\");\n\n        this.title = _t(\"Search Engine Optimization\");\n        this.saveButton = _t(\"Save\");\n        this.size = \"lg\";\n        this.contentClass = \"oe_seo_configuration\";\n\n        onWillStart(async () => {\n            // Wait for the preview iframe because this dialog reads directly\n            // from the iframe DOM.\n            await this.waitForIframe();\n            const {\n                metadata: { mainObject, seoObject, path },\n            } = this.website.currentWebsite;\n            this.object = seoObject || mainObject;\n            this.data = await rpc(\"/website/get_seo_data\", {\n                res_id: this.object.id,\n                res_model: this.object.model,\n            });\n\n            this.canEditSeo = this.data.can_edit_seo;\n            this.canEditDescription = this.canEditSeo && \"website_meta_description\" in this.data;\n            this.canEditTitle = this.canEditSeo && \"website_meta_title\" in this.data;\n            this.canEditUrl = this.canEditSeo && \"seo_name\" in this.data;\n            seoContext.title = this.canEditTitle && this.data.website_meta_title;\n\n            // If website.page, hide the google preview & tell user his page is currently unindexed\n            this.isIndexed = \"website_indexed\" in this.data ? this.data.website_indexed : true;\n            this.seoNameHelp = _t(\n                \"This value will be escaped to be compliant with all major browsers and used in url. Keep it empty to use the default name of the record.\"\n            );\n            this.previousSeoName = this.canEditUrl && this.data.seo_name;\n            seoContext.seoName = this.previousSeoName;\n            this.seoNameDefault = this.canEditUrl && this.data.seo_name_default;\n\n            seoContext.description = this.getMeta({ name: \"description\" });\n            this.previewDescription = _t(\n                \"Your page description should be between 50 and 160 characters long.\"\n            );\n            this.defaultTitle = this.getMeta({ name: \"default_title\" }) || \"\";\n            seoContext.defaultTitle = this.defaultTitle;\n            this.url = path;\n\n            seoContext.metaImage =\n                this.data.website_meta_og_img || this.getMeta({ property: \"og:image\" });\n\n            this.pageImages = this.getImages();\n            this.socialPreviewDescription = _t(\n                \"The description will be generated by social media based on page content unless you specify one.\"\n            );\n            this.hasSocialDefaultImage = this.data.has_social_default_image;\n\n            this.canEditKeywords = \"website_meta_keywords\" in this.data;\n            seoContext.keywords = this.getMeta({ name: \"keywords\" });\n        });\n    }\n\n    async waitForIframe() {\n        await new Promise((resolve) => {\n            const iframeEl = document.querySelector(\n                \".o_iframe_container > iframe:not(.o_ignore_in_tour)\"\n            );\n            if (!iframeEl || iframeEl.contentDocument?.readyState === \"complete\") {\n                return resolve();\n            }\n            iframeEl.addEventListener(\"load\", resolve, { once: true });\n        });\n    }\n\n    get pageDocumentElement() {\n        return this.website.pageDocument.documentElement;\n    }\n\n    getImages() {\n        const imageEls = this.pageDocumentElement.querySelectorAll(\"#wrap img\");\n        return [\n            ...new Set(\n                Array.from(imageEls)\n                    .filter((img) => img.naturalHeight > 200 && img.naturalWidth > 200)\n                    .map((img) => img.getAttribute(\"src\"))\n            ),\n        ];\n    }\n\n    getMeta({ name, property }) {\n        let query = \"\";\n        if (name) {\n            query = `meta[name=\"${name}\"]`;\n        }\n        if (property) {\n            query = `meta[property=\"${property}\"]`;\n        }\n        const el = this.pageDocumentElement.querySelector(query);\n        if (name === \"keywords\") {\n            // Keywords might contain spaces which makes them fail the content\n            // check. Trim the strings to prevent this from happening.\n            const parsed = el && el.content.split(\",\").map((kw) => kw.trim());\n            return parsed && parsed[0] ? [...new Set(parsed)] : [];\n        }\n        return el && el.content;\n    }\n\n    async save() {\n        const data = {};\n        if (this.canEditTitle) {\n            data.website_meta_title = seoContext.title;\n        }\n        if (this.canEditDescription) {\n            data.website_meta_description = seoContext.description;\n        }\n        if (this.canEditKeywords) {\n            data.website_meta_keywords = seoContext.keywords.join(\",\");\n        }\n        if (this.canEditUrl) {\n            if (seoContext.seoName !== this.previousSeoName) {\n                data.seo_name = seoContext.seoName;\n            }\n        }\n        data.website_meta_og_img = seoContext.metaImage;\n        await this.orm.write(this.object.model, [this.object.id], data, {\n            context: {\n                lang: this.website.currentWebsite.metadata.lang,\n                website_id: this.website.currentWebsite.id,\n            },\n        });\n\n        const rpcCalls = [];\n        if (\n            seoContext.brokenLinks.some(\n                (link) => link.oldLink !== link.newLink || link.remove === true\n            )\n        ) {\n            rpcCalls.push(\n                rpc(\"/website/update_broken_links\", {\n                    links: seoContext.brokenLinks,\n                })\n            );\n        }\n        if (seoContext.updatedAlts?.length) {\n            rpcCalls.push(\n                rpc(\"/website/update_alt_images\", {\n                    imgs: seoContext.updatedAlts,\n                })\n            );\n        }\n\n        await Promise.all(rpcCalls);\n\n        this.website.goToWebsite({\n            path: this.url.replace(this.previousSeoName || this.seoNameDefault, seoContext.seoName),\n        });\n    }\n}\n", "import { NavBar } from \"@web/webclient/navbar/navbar\";\nimport { useService, useBus } from \"@web/core/utils/hooks\";\nimport { registry } from \"@web/core/registry\";\nimport { patch } from \"@web/core/utils/patch\";\nimport { UserMenu } from \"@web/webclient/user_menu/user_menu\";\nimport { useEffect } from \"@odoo/owl\";\n\nconst websiteSystrayRegistry = registry.category(\"website_systray\");\nwebsiteSystrayRegistry.add(\"UserMenu\", { Component: UserMenu }, { sequence: 14 });\n\npatch(NavBar.prototype, {\n    setup() {\n        super.setup();\n        this.websiteService = useService(\"website\");\n        this.websiteCustomMenus = useService(\"website_custom_menus\");\n\n        // The navbar is rerendered with an event, as it can not naturally be\n        // with props/state (the WebsitePreview client action and the navbar\n        // are not related).\n        useBus(websiteSystrayRegistry, \"EDIT-WEBSITE\", () => this.render(true));\n\n        if (this.env.debug && !websiteSystrayRegistry.contains(\"web.debug_mode_menu\")) {\n            websiteSystrayRegistry.add(\n                \"web.debug_mode_menu\",\n                registry.category(\"systray\").get(\"web.debug_mode_menu\"),\n                { sequence: 100 }\n            );\n        }\n        // Similar to what is done in web/navbar. When the app menu or systray\n        // is updated, we need to adapt the navbar so that the \"more\" menu\n        // can be computed.\n        let adaptCounter = 0;\n        const renderAndAdapt = () => {\n            this.render(true);\n            adaptCounter++;\n        };\n        useEffect(\n            (adaptCounter) => {\n                // We do not want to adapt on the first render\n                // as the super class already does it.\n                if (adaptCounter > 0) {\n                    this.adapt();\n                }\n            },\n            () => [adaptCounter]\n        );\n\n        useBus(websiteSystrayRegistry, \"CONTENT-UPDATED\", renderAndAdapt);\n    },\n\n    get shouldDisplayWebsiteSystray() {\n        return this.websiteService.currentWebsite && this.websiteService.isRestrictedEditor;\n    },\n\n    // Somehow a setter is needed in `patch()` to avoid an owl error.\n    set shouldDisplayWebsiteSystray(_) {},\n\n    /**\n     * @override\n     */\n    get systrayItems() {\n        if (this.websiteService.currentWebsite) {\n            const websiteItems = websiteSystrayRegistry\n                .getEntries()\n                .map(([key, value], index) => ({ key, ...value, index }))\n                .filter((item) => (\"isDisplayed\" in item ? item.isDisplayed(this.env) : true))\n                .reverse();\n            // Do not override the regular Odoo navbar if the only visible\n            // elements are the debug items.\n            if (\n                !websiteItems.every((item) =>\n                    [\"burger_menu\", \"web.debug_mode_menu\"].includes(item.key)\n                )\n            ) {\n                return websiteItems;\n            }\n        }\n        return super.systrayItems;\n    },\n\n    /**\n     * @override\n     */\n    get currentAppSections() {\n        const currentAppSections = super.currentAppSections;\n        if (this.currentApp && this.currentApp.xmlid === \"website.menu_website_configuration\") {\n            return this.websiteCustomMenus\n                .addCustomMenus(currentAppSections)\n                .filter((section) => section.childrenTree.length);\n        }\n        return currentAppSections;\n    },\n\n    /**\n     * @override\n     */\n    async onNavBarDropdownItemSelection(menu) {\n        const websiteMenu = this.websiteCustomMenus.get(menu.xmlid);\n        if (websiteMenu) {\n            return this.websiteCustomMenus.open(menu);\n        }\n        return super.onNavBarDropdownItemSelection(menu);\n    },\n});\n", "import { BurgerMenu } from \"@web/webclient/burger_menu/burger_menu\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { registry } from \"@web/core/registry\";\nimport { patch } from \"@web/core/utils/patch\";\n\nconst websiteSystrayRegistry = registry.category(\"website_systray\");\n\npatch(BurgerMenu.prototype, {\n    setup() {\n        super.setup();\n        this.websiteCustomMenus = useService(\"website_custom_menus\");\n\n        if (!websiteSystrayRegistry.contains(\"burger_menu\")) {\n            websiteSystrayRegistry.add(\n                \"burger_menu\",\n                registry.category(\"systray\").get(\"burger_menu\"),\n                { sequence: 0 }\n            );\n        }\n    },\n\n    /**\n     * @override\n     */\n    get currentAppSections() {\n        const currentAppSections = super.currentAppSections;\n        if (this.currentApp && this.currentApp.xmlid === \"website.menu_website_configuration\") {\n            return this.websiteCustomMenus\n                .addCustomMenus(currentAppSections)\n                .filter((section) => section.childrenTree.length);\n        }\n        return currentAppSections;\n    },\n\n    /**\n     * This dummy setter is only here to prevent conflicts between the\n     * Enterprise BurgerMenue extension and the Website BurgerMenu patch.\n     */\n    set currentAppSections(_) {},\n\n    /**\n     * @override\n     */\n    async _onMenuClicked(menu) {\n        const websiteMenu = this.websiteCustomMenus.get(menu.xmlid);\n        if (websiteMenu) {\n            await this.websiteCustomMenus.open(menu);\n            this._closeBurger();\n        } else {\n            super._onMenuClicked(menu);\n        }\n    },\n});\n", "import { formView } from \"@web/views/form/form_view\";\nimport { registry } from \"@web/core/registry\";\n\nexport class NewContentFormController extends formView.Controller {\n    /**\n     * @override\n     */\n    async save() {\n        return super.save({ computePath: () => this.computePath(), ...arguments });\n    }\n\n    /**\n     * Returns the URL to redirect to once the website content (blog, etc)\n     * record is created.\n     * Override this method to get the correct path for records without\n     * 'website_url' field.\n     *\n     * @returns {String}\n     */\n    computePath() {\n        return this.model.root.data.website_url;\n    }\n}\n\nexport const NewContentFormView = {\n    ...formView,\n    display: { controlPanel: false },\n    Controller: NewContentFormController,\n};\n\nregistry.category(\"views\").add(\"website_new_content_form\", NewContentFormView);\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { EditMenuDialog } from \"@website/components/dialog/edit_menu\";\nimport { OptimizeSEODialog } from \"@website/components/dialog/seo\";\nimport { PagePropertiesDialog } from \"@website/components/dialog/page_properties\";\n\n/**\n * This service displays contextual menus, depending of the state of the\n * website. These menus are defined in xml with the \"website_preview\" action,\n * which is overriden here for displaying dialogs, or regular components that\n * are not client actions.\n */\nexport const websiteCustomMenus = {\n    dependencies: [\"website\", \"orm\", \"dialog\", \"ui\"],\n    start(env, { website, orm, dialog, ui }) {\n        const services = { website, orm, dialog, ui };\n        return {\n            get(xmlId) {\n                return registry.category(\"website_custom_menus\").get(xmlId, null);\n            },\n            async open(customMenu) {\n                const menuConfig = this.get(customMenu.xmlid);\n                if (menuConfig.openWidget) {\n                    return menuConfig.openWidget(services);\n                }\n                const menuProps = {\n                    ...(menuConfig.getProps && (await menuConfig.getProps(services))),\n                    // Values on 'dynamicProps' are retrieved after the content is loaded (e.g. id of\n                    // the content menu to be edited).\n                    ...customMenu.dynamicProps,\n                };\n                return dialog.add(menuConfig.Component, menuProps);\n            },\n            addCustomMenus(sections) {\n                const filteredSections = [];\n                for (const section of sections) {\n                    const isWebsiteCustomMenu = !!this.get(section.xmlid);\n                    const displayWebsiteCustomMenu =\n                        isWebsiteCustomMenu &&\n                        website.isRestrictedEditor &&\n                        this.get(section.xmlid).isDisplayed(env);\n                    if (!isWebsiteCustomMenu || displayWebsiteCustomMenu) {\n                        let subSections = [];\n                        if (section.childrenTree.length) {\n                            subSections = this.addCustomMenus(section.childrenTree);\n                        }\n                        if (section.xmlid === \"website.custom_menu_edit_menu\") {\n                            // Hack: this code will simulate an XML pre-configured navbar menuitem to edit each\n                            // content menu found on the current page by duplicating one menuitem with\n                            // different data (name, dialog props...). this will prevent breaking the current\n                            // 'navbar menus' display system.\n                            filteredSections.push(\n                                ...website.currentWebsite.metadata.contentMenus.map(\n                                    (menu, index) => ({\n                                        ...section,\n                                        name: _t(\"Edit %s\", menu[0]),\n                                        dynamicProps: { rootID: parseInt(menu[1], 10) },\n                                        // Prevent a 't-foreach' duplicate key on menus template.\n                                        id: `${section.id}-${index}`,\n                                    })\n                                )\n                            );\n                        } else {\n                            filteredSections.push(\n                                Object.assign({}, section, { childrenTree: subSections })\n                            );\n                        }\n                    }\n                }\n                for (const section of filteredSections) {\n                    section.childrenTree = section.childrenTree.filter(\n                        // Exclude non-leaf node having no visible sub-element.\n                        (tree) => !(tree.children.length && !tree.childrenTree.length)\n                    );\n                }\n                return filteredSections;\n            },\n        };\n    },\n};\nregistry.category(\"services\").add(\"website_custom_menus\", websiteCustomMenus);\n\nregistry.category(\"website_custom_menus\").add(\"website.menu_edit_menu\", {\n    Component: EditMenuDialog,\n    isDisplayed: (env) =>\n        !!env.services.website.currentWebsite &&\n        env.services.website.isDesigner &&\n        !env.services.website.currentWebsite.metadata.translatable,\n});\nregistry.category(\"website_custom_menus\").add(\"website.menu_optimize_seo\", {\n    Component: OptimizeSEODialog,\n    isDisplayed: (env) =>\n        env.services.website.currentWebsite &&\n        env.services.website.isRestrictedEditor &&\n        !!env.services.website.currentWebsite.metadata.canOptimizeSeo,\n});\nregistry.category(\"website_custom_menus\").add(\"website.menu_ace_editor\", {\n    openWidget: (services) => (services.website.context.showResourceEditor = true),\n    isDisplayed: (env) =>\n        env.services.website.currentWebsite &&\n        env.services.website.currentWebsite.metadata.viewXmlid &&\n        !env.services.ui.isSmall,\n});\nregistry.category(\"website_custom_menus\").add(\"website.menu_page_properties\", {\n    Component: PagePropertiesDialog,\n    isDisplayed: (env) =>\n        env.services.website.currentWebsite &&\n        env.services.website.isDesigner &&\n        !!env.services.website.currentWebsite.metadata.mainObject,\n    getProps: async ({ orm, website }) => {\n        const mainObject = website.currentWebsite.metadata.mainObject;\n        const isPage = mainObject.model === \"website.page\";\n        const model = isPage ? \"website.page.properties\" : \"website.page.properties.base\";\n        const websiteId = website.currentWebsite.id;\n        // Do not rely on window.location.pathname: while the editor boots it can\n        // still be \"/web\" or even empty, which would give the dialog a wrong URL.\n        // website.currentLocation already tracks the actual page (without the\n        // language prefix), so we reuse it and fall back to the metadata path,\n        // forcing a leading '/' for extra safety. This keeps the wizard working\n        // even if the user opens it as soon as the builder loads.\n        const getNormalizedPath = () => {\n            let path = website.currentLocation;\n            if (!path) {\n                try {\n                    path = new URL(website.currentWebsite.metadata.path).pathname;\n                } catch {\n                    path = website.currentWebsite.metadata.path || \"/\";\n                }\n            }\n            if (path && !path.startsWith(\"/\")) {\n                path = `/${path}`;\n            }\n            return path || \"/\";\n        };\n        const recordValues = isPage\n            ? {\n                  target_model_id: mainObject.id,\n                  website_id: websiteId,\n              }\n            : {\n                  target_model_id: `${mainObject.model},${mainObject.id}`,\n                  url: getNormalizedPath(),\n                  website_id: websiteId,\n              };\n        return {\n            resId: await orm.call(model, \"create\", [recordValues]),\n            resModel: model,\n            onRecordSaved: async (record) => {\n                const page = isPage\n                    ? (await orm.read(\"website.page\", [mainObject.id], [\"website_id\", \"url\"]))[0]\n                    : undefined;\n                return website.goToWebsite({\n                    websiteId: page?.website_id?.[0] ?? website.currentWebsite.id,\n                    path: page?.url ?? website.currentWebsite.metadata.path,\n                });\n            },\n        };\n    },\n});\nregistry.category(\"website_custom_menus\").add(\"website.custom_menu_edit_menu\", {\n    Component: EditMenuDialog,\n    // 'isDisplayed' === true => at least 1 content menu was found on the page. This\n    // menuitem will be cloned (in 'addCustomMenus()') to edit every content menu using\n    // the 'EditMenuDialog' component.\n    isDisplayed: (env) =>\n        env.services.website.currentWebsite &&\n        env.services.website.currentWebsite.metadata.contentMenus &&\n        env.services.website.currentWebsite.metadata.contentMenus.length,\n});\n", "import {\n    changeBackgroundColor,\n    clickOnSnippet,\n    clickOnText,\n    insertSnippet,\n    goBackToBlocks,\n    registerThemeHomepageTour,\n} from \"@website/js/tours/tour_utils\";\n\nconst snippets = [\n    {\n        id: \"s_banner\",\n        name: \"Banner\",\n        groupName: \"Intro\",\n    },\n    {\n        id: \"s_three_columns\",\n        name: \"Columns\",\n        groupName: \"Columns\",\n    },\n    {\n        id: \"s_text_image\",\n        name: \"Image - Text\",\n        groupName: \"Content\",\n    },\n    {\n        id: \"s_masonry_block_default_template\",\n        name: \"Masonry\",\n        groupName: \"Images\",\n    },\n    {\n        id: \"s_title\",\n        name: \"Title\",\n        groupName: \"Text\",\n    },\n    {\n        id: \"s_showcase\",\n        name: \"Showcase\",\n        groupName: \"Content\",\n    },\n    {\n        id: \"s_call_to_action\",\n        name: \"Call to Action\",\n        groupName: \"Content\",\n    },\n    {\n        id: \"s_quotes_carousel\",\n        name: \"Quotes\",\n        groupName: \"People\",\n    },\n];\n\nregisterThemeHomepageTour(\"homepage\", () => [\n    ...insertSnippet(snippets[0], { position: \"top\" }),\n    ...clickOnText(snippets[0], \"h1\"),\n    goBackToBlocks(),\n    ...insertSnippet(snippets[1]),\n    ...insertSnippet(snippets[2]),\n    ...clickOnSnippet(snippets[2], \"top\"),\n    changeBackgroundColor(),\n    goBackToBlocks(),\n    ...insertSnippet(snippets[3]),\n    ...insertSnippet(snippets[4], { position: \"top\" }),\n    ...insertSnippet(snippets[5]),\n    ...insertSnippet(snippets[6]),\n    ...insertSnippet(snippets[7]),\n]);\n", "import { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\nimport { Component, useRef, useState } from \"@odoo/owl\";\n\nexport class HierarchyNavbar extends Component {\n    static template = \"website.hierarchy_navbar\";\n    static components = {\n        Dropdown,\n        DropdownItem,\n    };\n    static props = {\n        toggleInactive: Function,\n        websites: Object,\n        selectWebsite: Function,\n        searchView: Function,\n    };\n\n    setup() {\n        this.searchInput = useRef(\"search\");\n        this.websiteNamesState = useState(Array.from(this.props.websites.names));\n    }\n\n    get websiteNames() {\n        return this.websiteNamesState.map((websiteName) => ({\n            label: websiteName,\n            onSelected: () => this.props.selectWebsite(websiteName),\n        }));\n    }\n\n    /**\n     * @param {Event} event\n     */\n    onInputKeydown(event) {\n        if (event.key === \"Enter\" || event.key === \"Tab\") {\n            event.preventDefault();\n            this.props.searchView(event.target.value, !event.shiftKey);\n        }\n    }\n\n    /**\n     * @param {Event} event\n     */\n    onInputClick(event) {\n        this.props.searchView(this.searchInput.el.value, !event.shiftKey);\n    }\n}\n", "import { HierarchyNavbar } from \"./hierarchy_navbar\";\nimport { Layout } from \"@web/search/layout\";\nimport { registry } from \"@web/core/registry\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { Component, onWillStart, useEffect, useState } from \"@odoo/owl\";\nimport { router } from \"@web/core/browser/router\";\nimport { standardActionServiceProps } from \"@web/webclient/actions/action_service\";\n\nexport class ViewHierarchy extends Component {\n    static components = { Layout, HierarchyNavbar };\n    static template = \"website.view_hierarchy\";\n    static props = { ...standardActionServiceProps };\n    setup() {\n        this.action = useService(\"action\");\n        this.orm = useService(\"orm\");\n        this.state = useState({ showInactive: false, searchedView: {}, viewTree: {} });\n        this.websites = useState({ names: new Set([\"All Websites\"]), selected: \"All Websites\" });\n        this.viewId = this.props.action.context.active_id || router.current.active_id;\n        this.hideGenericViewByWebsite = {};\n\n        onWillStart(async () => {\n            ({ sibling_views: this.siblingViews, hierarchy: this.state.viewTree } =\n                await this.orm.call(\"ir.ui.view\", \"get_view_hierarchy\", [this.viewId], {}));\n\n            this.setupWebsiteNames();\n            this.setupHideGenericViewByWebsite();\n            this.linkViewsToParent();\n        });\n\n        useEffect(\n            (searchFoundElem) => {\n                if (searchFoundElem) {\n                    searchFoundElem.scrollIntoView({ behavior: \"smooth\", block: \"center\" });\n                }\n            },\n            () => [document.querySelector(\".o_search_found\")]\n        );\n    }\n\n    /**\n     * Filter the treeView by website\n     * @param {String} websiteName\n     */\n    selectWebsite(websiteName) {\n        this.websites.selected = websiteName;\n    }\n\n    /**\n     * Show/hide inactive views\n     * @param {Boolean} checked\n     */\n    toggleInactive(checked) {\n        this.state.showInactive = checked;\n    }\n\n    /**\n     * @param {String} keyword\n     * @returns {Array} a list of visible views that match the keyword\n     * insensitive case.\n     * The comparison is done on the name, the key and the id of each views.\n     * Priority is given to the exact matches and then to the order\n     */\n    getSearchResults(keyword) {\n        const exactMatches = [];\n        const matches = [];\n        const lowercaseKeyword = keyword.toLowerCase();\n        this.viewTraversal(\n            this.state.viewTree,\n            (currentView) => {\n                if (\n                    this.isViewDisplayed(currentView) &&\n                    (currentView.name.toLowerCase() === lowercaseKeyword ||\n                        currentView.key.toLowerCase() === lowercaseKeyword ||\n                        currentView.id === parseInt(lowercaseKeyword))\n                ) {\n                    exactMatches.push(currentView);\n                } else if (\n                    this.isViewDisplayed(currentView) &&\n                    (currentView.name.toLowerCase().includes(lowercaseKeyword) ||\n                        currentView.key.toLowerCase().includes(lowercaseKeyword))\n                ) {\n                    matches.push(currentView);\n                }\n            },\n            (currentView) => this.isViewDisplayed(currentView)\n        );\n        return exactMatches.concat(matches);\n    }\n\n    /**\n     * Search the next visibile view that matches the keyword to the name, the\n     * key and the id of the view\n     * @param {String} keyword\n     * @param {Boolean} forward\n     */\n    searchView(keyword, forward = true) {\n        const matches = this.getSearchResults(keyword);\n        let index = 0;\n        if (this.state.searchedView.keyword === keyword) {\n            index = matches.findIndex((view) => this.state.searchedView.id === view.id);\n            index = forward ? index + 1 : index - 1;\n            index = index - matches.length * Math.floor(index / matches.length);\n        }\n\n        const view = matches[index];\n        if (view) {\n            this.state.searchedView = {\n                id: view.id,\n                keyword: keyword,\n                total: matches.length,\n                index,\n            };\n        }\n    }\n\n    /**\n     * Makes an inorder traversal of the view tree and apply a function at each\n     * node\n     * @param {Object} currentView represent the current view tree\n     * @param {Function} fn function applied at each node with currentView as\n     * parameter\n     * @param {Function} continueRec take the view as argument and decide if\n     * the recursion continue\n     */\n    viewTraversal(currentView, fn, continueRec = (view) => true) {\n        fn(currentView);\n        if (continueRec(currentView)) {\n            currentView.inherit_children.forEach((childView) => {\n                this.viewTraversal(childView, fn, continueRec);\n            });\n        }\n    }\n\n    /**\n     * Setup website names from the viewTree into this.websites.names\n     */\n    setupWebsiteNames() {\n        this.viewTraversal(this.state.viewTree, (currentView) => {\n            if (currentView.website_name) {\n                this.websites.names.add(currentView.website_name);\n            }\n        });\n    }\n\n    /**\n     * States for each website filter if a generic view should be hided or not\n     */\n    setupHideGenericViewByWebsite() {\n        this.viewTraversal(this.state.viewTree, (currentView) => {\n            if (currentView.website_name) {\n                if (!this.hideGenericViewByWebsite[currentView.website_name]) {\n                    this.hideGenericViewByWebsite[currentView.website_name] = {};\n                }\n                this.hideGenericViewByWebsite[currentView.website_name][currentView.name] = true;\n            }\n        });\n    }\n\n    /**\n     * Link views in the viewTree to their parent\n     */\n    linkViewsToParent() {\n        this.viewTraversal(this.state.viewTree, (currentView) => {\n            currentView.inherit_children.forEach((child) => (child.parent = currentView));\n        });\n    }\n\n    /**\n     * Collapse the view to show/hide the children\n     * @param {Object} view\n     */\n    onCollapseClick(view) {\n        view.collapsed = !view.collapsed;\n        if (view.collapsed) {\n            // When folding a parent, children should also fold\n            this.viewTraversal(view, (child) => {\n                child.collapsed = view.collapsed;\n            });\n        }\n    }\n\n    /**\n     * @param {Object} view\n     * @param {Boolean} isCollapsedDisplayed\n     * @returns true if the view is displayed in the view tree, false otherwise\n     */\n    isViewDisplayed(view, isCollapsedDisplayed = false) {\n        let isCollapsed = view.parent ? view.parent.collapsed : false;\n        if (isCollapsedDisplayed) {\n            isCollapsed = false;\n        }\n        const isActive = this.state.showInactive || view.active;\n        const isWebsiteDisplayed =\n            this.websites.selected === \"All Websites\" ||\n            view.website_name === this.websites.selected ||\n            (!view.website_name &&\n                !this.hideGenericViewByWebsite[this.websites.selected][view.name]);\n        return !isCollapsed && isActive && isWebsiteDisplayed;\n    }\n\n    /**\n     * @param {Object} view\n     * @returns true if view has a child to unfold, false otherwise\n     */\n    hasChildToUnfold(view) {\n        return view.inherit_children.some((child) => this.isViewDisplayed(child, true));\n    }\n\n    /**\n     * @param {Number} viewId\n     */\n    onShowDiffClick(viewId) {\n        this.action.doAction(\"base.reset_view_arch_wizard_action\", {\n            additionalContext: {\n                active_model: \"ir.ui.view\",\n                active_ids: [viewId],\n            },\n        });\n    }\n\n    /**\n     * @param {Number} viewId\n     */\n    openFormView(viewId) {\n        this.action.doAction({\n            type: \"ir.actions.act_window\",\n            res_model: \"ir.ui.view\",\n            res_id: viewId,\n            views: [[false, \"form\"]],\n        });\n    }\n\n    /**\n     * @param {Number} viewId\n     */\n    onShowHierarchy(viewId) {\n        this.action.doAction({\n            type: \"ir.actions.client\",\n            tag: \"website_view_hierarchy\",\n            name: \"View Hierarchy\",\n            context: {\n                active_id: viewId,\n            },\n        });\n    }\n}\n\nregistry.category(\"actions\").add(\"website_view_hierarchy\", ViewHierarchy);\n", "import { patch } from \"@web/core/utils/patch\";\nimport {\n    NewContentSystrayItem,\n    MODULE_STATUS,\n} from \"@website/client_actions/website_preview/new_content_systray_item\";\n\npatch(NewContentSystrayItem.prototype, {\n    setup() {\n        super.setup();\n\n        const newProductElement = this.state.newContentElements.find(element => element.moduleXmlId === 'base.module_website_sale');\n        newProductElement.createNewContent = () => this.onAddContent(\n            'website_sale.product_product_action_add',\n            true,\n            {default_is_published: true});\n        newProductElement.status = MODULE_STATUS.INSTALLED;\n        newProductElement.model = 'product.product';\n    },\n});\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { patch } from \"@web/core/utils/patch\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { useState } from \"@odoo/owl\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { AddPageConfirmDialog } from \"@website/components/dialog/add_page_dialog\";\n\npatch(AddPageConfirmDialog.prototype, {\n    setup() {\n        super.setup();\n        this.notification = useService(\"notification\");\n        this.state = useState({\n            ...this.state,\n            instructions: \"\",\n            tone: \"\",\n            generateText: false,\n            loading: false,\n        });\n\n        this.tones = {\n            concise: {\n                title: _t(\"Concise\"),\n                description: \"Keep it short and to the point\",\n            },\n            professional: {\n                title: _t(\"Professional\"),\n                description: \"Use a formal, professional tone\",\n            },\n            friendly: {\n                title: _t(\"Friendly\"),\n                description: \"Keep it relaxed and easygoing\",\n            },\n            persuasive: {\n                title: _t(\"Persuasive\"),\n                description: \"Make it more action-oriented\",\n            },\n            informative: {\n                title: _t(\"Informative\"),\n                description: \"Make it clear and explanatory\",\n            },\n        };\n    },\n\n    onChangeGenerateText(value) {\n        this.state.generateText = value;\n    },\n\n    onToneSelect(ev) {\n        const tone = ev.target.dataset.tone;\n        if (tone === this.state.tone) {\n            this.state.tone = \"\";\n            return;\n        }\n        this.state.tone = tone;\n    },\n\n    get buttonTitle() {\n        if (this.state.generateText) {\n            return _t(\"Create with AI\");\n        }\n        return _t(\"Create\");\n    },\n\n    async processSectionsArch() {\n        if (this.state.sectionsArch) {\n            const aiGeneratedContent = await rpc('/ai_website/generate_page', {\n                instructions: this.state.instructions || \"\",\n                name: this.state.name,\n                sectionsArch: this.state.sectionsArch,\n                tone: this.state.tone ? this.tones[this.state.tone] : \"\",\n                templateId: this.state.templateId || \"\",\n            });\n            if (aiGeneratedContent && aiGeneratedContent.html) {\n                if (aiGeneratedContent.error) {\n                    this.notification.add(aiGeneratedContent.error, {\n                        type: \"danger\",\n                        sticky: true,\n                    });\n                    return false;\n                }\n                this.state.sectionsArch = aiGeneratedContent.html;\n            }\n        }\n    },\n\n    async addPage() {\n        if (this.state.generateText) {\n            this.state.loading = true;\n            await this.processSectionsArch();\n        }\n        await super.addPage();\n        this.state.loading = false;\n    },\n});\n", "(function() {\n\n    const dns_menu_deps = [\n        \"@saas_website/owl/dns_menu/dns_menu\",\n        \"@web_enterprise/webclient/navbar/navbar\",\n    ];\n\n    odoo.define(\"saas_website.dns_menu\", dns_menu_deps, function (require) {\n        \"use strict\";\n\n        const { EnterpriseNavBar } = require('@web_enterprise/webclient/navbar/navbar');\n        const { DnsMenu: OwlDnsMenu } = require('@saas_website/owl/dns_menu/dns_menu');\n        EnterpriseNavBar.components = Object.assign(EnterpriseNavBar.components || {}, { DnsMenu: OwlDnsMenu });\n    });\n\n})();\n", "(function () {\n    /* >= 16.0\n    NavBar is now a proper owl component and\n    the website builder is run from an iframe, so we just need to add\n    the default DbExpirationTag to the proper systray.\n    */\n    const db_activation_website_deps = [\n        '@saas_trial/owl/db_expiration_tag/db_expiration_tag',\n        '@web/core/registry',\n    ];\n    odoo.define('saas_website.db_activation_website', db_activation_website_deps, function (require) {\n        'use strict';\n\n        var { registry } = require('@web/core/registry');\n        var { DbExpirationTag } = require('@saas_trial/owl/db_expiration_tag/db_expiration_tag');\n\n        registry.category(\"website_systray\").add(\n            'saas_trial.db_expiration_tag',\n            { Component: DbExpirationTag },\n            { sequence: 200 }\n        );\n\n    });\n\n})();\n", "import {\n    NewContentSystrayItem,\n    MODULE_STATUS,\n} from \"@website/client_actions/website_preview/new_content_systray_item\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { patch } from \"@web/core/utils/patch\";\n\npatch(NewContentSystrayItem.prototype, {\n    setup() {\n        super.setup();\n\n        this.state.newContentElements.push({\n            moduleName: \"website_appointment\",\n            moduleXmlId: \"base.module_website_appointment\",\n            status: MODULE_STATUS.NOT_INSTALLED,\n            icon: \"/appointment/static/description/icon.png\",\n            title: _t(\"Appointment Form\"),\n            description: _t(\"Let visitors book meetings online\"),\n        });\n    },\n});\n", "import { browser } from \"@web/core/browser/browser\";\nimport { patch } from \"@web/core/utils/patch\";\nimport { colorSchemeService } from \"@web_enterprise/webclient/color_scheme/color_scheme_service\";\n\npatch(colorSchemeService, {\n    reload() {\n        if (document.querySelector(\"header.o_navbar + .o_action_manager > .o_website_preview\")) {\n            browser.location.pathname = \"/@\" + browser.location.pathname;\n        } else {\n            super.reload();\n        }\n    },\n});\n", "import {\n    NewContentSystrayItem,\n    MODULE_STATUS,\n} from \"@website/client_actions/website_preview/new_content_systray_item\";\nimport { patch } from \"@web/core/utils/patch\";\n\npatch(NewContentSystrayItem.prototype, {\n    setup() {\n        super.setup();\n\n        const newBlogElement = this.state.newContentElements.find(\n            (element) => element.moduleXmlId === \"base.module_website_blog\"\n        );\n        newBlogElement.createNewContent = () =>\n            this.onAddContent(\n                \"website_blog.blog_post_action_add\",\n                true,\n                this.getCurrentBlogContext()\n            );\n        newBlogElement.status = MODULE_STATUS.INSTALLED;\n        newBlogElement.model = \"blog.post\";\n    },\n\n    getCurrentBlogContext() {\n        // Using iframe to access mainObject to check if we are on blog page\n        const iframeEl = document.querySelector(\"iframe\").contentDocument;\n        const isBlogPage = iframeEl.documentElement.dataset.mainObject?.startsWith(\"blog\");\n\n        if (isBlogPage) {\n            const blogEl = iframeEl.querySelector(\"#wrap.website_blog [data-oe-model='blog.blog']\");\n            const blogId = parseInt(blogEl?.dataset.oeId);\n\n            if (blogId) {\n                return { default_blog_id: blogId };\n            }\n        }\n        return null;\n    },\n});\n", "import {NewContentFormController, NewContentFormView} from '@website/js/new_content_form';\nimport {registry} from \"@web/core/registry\";\n\nexport class AddForumFormController extends NewContentFormController {\n    /**\n     * @override\n     */\n    computePath() {\n        return `/forum/${encodeURIComponent(this.model.root.resId)}`;\n    }\n}\n\nexport const AddForumFormView = {\n    ...NewContentFormView,\n    Controller: AddForumFormController,\n};\n\nregistry.category(\"views\").add(\"website_forum_add_form\", AddForumFormView);\n", "import {\n    NewContentSystrayItem,\n    MODULE_STATUS,\n} from \"@website/client_actions/website_preview/new_content_systray_item\";\nimport { patch } from \"@web/core/utils/patch\";\n\npatch(NewContentSystrayItem.prototype, {\n    setup() {\n        super.setup();\n\n        const newForumElement = this.state.newContentElements.find(element => element.moduleXmlId === 'base.module_website_forum');\n        newForumElement.createNewContent = () => this.onAddContent('website_forum.forum_forum_action_add');\n        newForumElement.status = MODULE_STATUS.INSTALLED;\n        newForumElement.model = 'forum.forum';\n    },\n});\n", "/** @odoo-module */\n\nimport * as wTourUtils from '@website/js/tours/tour_utils';\n\nconst snippets = [\n    {\n        id: 's_discovery',\n        name: 'Discovery',\n        groupName: \"Intro\",\n    },\n    {\n        id: 's_parallax',\n        name: 'Parallax',\n        groupName: \"Images\",\n    },\n    {\n        id: 's_text_block',\n        name: 'Text',\n        groupName: \"Text\",\n    },\n    {\n        id: 's_key_images',\n        name: 'Key Images',\n        groupName: \"Columns\",\n    },\n    {\n        id: 's_image_text_overlap',\n        name: 'Image - Text Overlap',\n        groupName: \"Content\",\n    },\n    {\n        id: 's_company_team',\n        name: 'Team',\n        groupName: \"People\",\n    },\n    {\n        id: 's_references',\n        name: 'References',\n        groupName: \"People\",\n    },\n    {\n        id: 's_numbers',\n        name: 'Numbers',\n        groupName: \"Content\",\n    },\n    {\n        id: 's_cta_box',\n        name: 'Box Call to Action',\n        groupName: \"Content\",\n    },\n];\n\nwTourUtils.registerThemeHomepageTour(\"nano_tour\", () => [\n    wTourUtils.assertCssVariable('--color-palettes-name', '\"nano-1\"'),\n    ...wTourUtils.insertSnippet(snippets[0]),\n    ...wTourUtils.clickOnText(snippets[0], 'h1', 'top'),\n    wTourUtils.goBackToBlocks(),\n    ...wTourUtils.insertSnippet(snippets[1]),\n    ...wTourUtils.insertSnippet(snippets[2]),\n    ...wTourUtils.clickOnSnippet(snippets[2], 'top'),\n    wTourUtils.changeBackgroundColor(),\n    wTourUtils.selectColorPalette(),\n    wTourUtils.goBackToBlocks(),\n    ...wTourUtils.insertSnippet(snippets[3]),\n    ...wTourUtils.insertSnippet(snippets[4]),\n    ...wTourUtils.insertSnippet(snippets[5]),\n]);\n", "import { LinkPopover } from \"@html_editor/main/link/link_popover\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { AutoComplete } from \"@web/core/autocomplete/autocomplete\";\nimport { patch } from \"@web/core/utils/patch\";\nimport { useChildRef } from \"@web/core/utils/hooks\";\nimport wUtils from \"@website/js/utils\";\nimport { useEffect } from \"@odoo/owl\";\nimport { browser } from \"@web/core/browser/browser\";\nimport { session } from \"@web/session\";\n\n/**\n * The goal of this patch is to handle the URL autocomplete in the LinkPopover\n * component. The URL autocomplete is used to suggest internal links, anchors.\n * Before, the autocomplete was implemented as another OWL app. Now with this\n * patch, URL autocomplete is implemented as a child component.\n */\n\n/**\n * this class is used to create a new autocomplete component that will be used\n * in the LinkPopover component. Similar with AutoCompleteWithPages but it has\n * two new props:\n * - inputClass: to change the style of the input element in autocomplete\n * - updateValue: to update the URL of the link element\n */\nexport class AutoCompleteInLinkPopover extends AutoComplete {\n    static props = {\n        ...AutoComplete.props,\n        inputClass: { type: String, optional: true },\n        updateValue: { type: Function, optional: true },\n    };\n    static template = \"website.AutoCompleteInLinkPopover\";\n\n    // overwrite the div class to avoid breaking the popover style\n    get autoCompleteRootClass() {\n        return `${super.autoCompleteRootClass} col`;\n    }\n\n    // apply classes on the input element in autocomplete\n    get inputClass() {\n        return this.props.inputClass || \"o_input pe-3\";\n    }\n\n    /**\n     * @override\n     */\n    onInput() {\n        super.onInput();\n        this.props.updateValue(this.targetDropdown.value);\n    }\n}\n\npatch(LinkPopover, {\n    components: { ...LinkPopover.components, AutoCompleteInLinkPopover },\n});\n\n/* patch the LinkPopover component to maintain the option source for the\n * AutoCompleteInLinkPopover component. Also we make sure state.url is updated\n * when the user enters text in the autocomplete and selects an option from the\n * autocomplete.\n */\npatch(LinkPopover.prototype, {\n    setup() {\n        super.setup();\n        this.urlRef = useChildRef();\n        useEffect(\n            (el) => {\n                if (el && (this.state.isImage || (!this.state.url && this.state.label))) {\n                    el.focus();\n                }\n            },\n            () => [this.urlRef.el]\n        );\n    },\n\n    get sources() {\n        return [this.optionsSource];\n    },\n\n    get optionsSource() {\n        return {\n            placeholder: _t(\"Loading...\"),\n            options: this.loadOptionsSource.bind(this),\n            optionSlot: \"urlOption\",\n        };\n    },\n\n    async loadOptionsSource(term) {\n        const makeItem = (item) => ({\n            cssClass: \"ui-autocomplete-item\",\n            label: item.label,\n            onSelect: this.onSelect.bind(this, item.value),\n            data: { icon: item.icon || false, isCategory: false },\n        });\n\n        if (term[0] === \"#\") {\n            const anchors = await wUtils.loadAnchors(\n                term,\n                this.props.linkElement.ownerDocument.body\n            );\n            return anchors.map((anchor) => makeItem({ label: anchor, value: anchor }), this);\n        } else if (term.startsWith(\"http\") || term.length === 0) {\n            // avoid useless call to /website/get_suggested_links\n            return [];\n        }\n\n        const res = await rpc(\"/website/get_suggested_links\", {\n            needle: term,\n            limit: 15,\n        });\n        const choices = [];\n        for (const page of res.matching_pages) {\n            choices.push(makeItem(page));\n        }\n        for (const other of res.others) {\n            if (other.values.length) {\n                choices.push({\n                    cssClass: \"ui-autocomplete-category\",\n                    label: other.title,\n                    data: { icon: false, isCategory: true },\n                });\n                for (const page of other.values) {\n                    choices.push(makeItem(page));\n                }\n            }\n        }\n        return choices;\n    },\n\n    onSelect(value) {\n        this.state.url = value;\n        if (!this.state.isImage) {\n            this.onChange();\n        }\n    },\n\n    updateValue(val) {\n        this.state.url = val;\n        if (!this.state.isImage) {\n            this.onChange();\n        }\n    },\n    isFrontendUrl(url) {\n        const parsedUrl = new URL(url);\n        return (\n            (browser.location.hostname === parsedUrl.hostname ||\n                // Also check if the odoo-hosted domain is the current domain of the url\n                new RegExp(`^https?://${session.db}\\\\.odoo\\\\.com(/.*)?$`).test(parsedUrl.origin)) &&\n            !parsedUrl.pathname.startsWith(\"/odoo\") &&\n            !parsedUrl.pathname.startsWith(\"/web\") &&\n            !parsedUrl.pathname.startsWith(\"/@/\")\n        );\n    },\n    onClickForcePreviewMode(ev) {\n        if (this.props.linkElement.href) {\n            const currentUrl = new URL(this.props.linkElement.href);\n            // only when we are on a frontend page (in website builder) and the link is also a frontend link\n            if (\n                this.isFrontendUrl(browser.location.href) &&\n                this.isFrontendUrl(this.props.linkElement.href)\n            ) {\n                ev.preventDefault();\n                currentUrl.pathname = `/@${currentUrl.pathname}`;\n                browser.open(currentUrl);\n            }\n        }\n    },\n});\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { cookie } from \"@web/core/browser/cookie\";\n\nimport { markup } from \"@odoo/owl\";\nimport { omit } from \"@web/core/utils/objects\";\nimport { stepUtils } from \"@web_tour/tour_utils\";\n\nexport function addMedia(position = \"right\") {\n    return {\n        trigger: `.modal-content footer .btn-primary`,\n        content: markup(_t(\"<b>Add</b> the selected image.\")),\n        tooltipPosition: position,\n        run: \"click\",\n    };\n}\nexport function assertCssVariable(variableName, variableValue, trigger = \":iframe body\") {\n    return {\n        isActive: [\"auto\"],\n        content: `Check CSS variable ${variableName}=${variableValue}`,\n        trigger: trigger,\n        run() {\n            const styleValue = getComputedStyle(this.anchor).getPropertyValue(variableName);\n            if (\n                (styleValue && styleValue.trim().replace(/[\"']/g, \"\")) !==\n                variableValue.trim().replace(/[\"']/g, \"\")\n            ) {\n                throw new Error(\n                    `Failed precondition: ${variableName}=${styleValue} (should be ${variableValue})`\n                );\n            }\n        },\n    };\n}\nexport function assertPathName(pathname, trigger) {\n    return {\n        content: `Check if we have been redirected to ${pathname}`,\n        trigger: trigger,\n        async run() {\n            await new Promise((resolve) => {\n                let elapsedTime = 0;\n                const intervalTime = 100;\n                const interval = setInterval(() => {\n                    if (window.location.pathname.startsWith(pathname)) {\n                        clearInterval(interval);\n                        resolve();\n                    }\n                    elapsedTime += intervalTime;\n                    if (elapsedTime >= 5000) {\n                        clearInterval(interval);\n                        console.error(`The pathname ${pathname} has not been found`);\n                    }\n                }, intervalTime);\n            });\n        },\n    };\n}\n\nexport function changeBackground(snippet, position = \"bottom\") {\n    return [\n        {\n            trigger: `.o_customize_tab button[data-action-id=\"replaceBgImage\"]`,\n            content: markup(\n                _t(\n                    \"<b>Customize</b> any block through this menu. Try to change the background image of this block.\"\n                )\n            ),\n            tooltipPosition: position,\n            run: \"click\",\n        },\n    ];\n}\n\nexport function changeBackgroundColor(position = \"bottom\") {\n    return {\n        trigger: \".o_customize_tab .o_we_color_preview\",\n        content: markup(\n            _t(\n                \"<b>Customize</b> any block through this menu. Try to change the background color of this block.\"\n            )\n        ),\n        tooltipPosition: position,\n        run: \"click\",\n    };\n}\n\n// TODO: RAHG: This function's trigger is same as above. need to be changed\n// to avoid duplication\nexport function selectColorPalette(position = \"left\") {\n    return {\n        trigger: \".o_customize_tab .o_we_color_preview\",\n        content: markup(_t(`<b>Select</b> a Color Palette.`)),\n        tooltipPosition: position,\n        run: \"click\",\n    };\n}\n\nexport function changeColumnSize(position = \"right\") {\n    return {\n        trigger: `.oe_overlay.oe_active .o_handles .o_handle:not(.readonly)`,\n        content: markup(_t(\"<b>Slide</b> this button to change the column size.\")),\n        tooltipPosition: position,\n        run: \"click\",\n    };\n}\n\nexport function changeImage(snippet, position = \"bottom\") {\n    return [\n        {\n            trigger: \".o_builder_sidebar_open\",\n        },\n        {\n            trigger: snippet.id ? `#wrapwrap .${snippet.id} img` : snippet,\n            content: markup(\n                _t(\"<b>Double click on an image</b> to change it with one of your choice.\")\n            ),\n            tooltipPosition: position,\n            run: \"dblclick\",\n        },\n    ];\n}\n\n/**\n    wTourUtils.changeOption('HeaderTemplate', '[data-name=\"header_alignment_opt\"]', _t('alignment')),\n    By default, prevents the step from being active if a palette is opened.\n    Set allowPalette to true to select options within a palette.\n*/\nexport function changeOption(\n    blockName,\n    actionId = \"\",\n    optionTooltipLabel = \"\",\n    position = \"bottom\",\n    allowPalette = false\n) {\n    const noPalette = allowPalette\n        ? \"\"\n        : !document.querySelector(\".o_popover .o_font_color_selector\") && \".o_customize_tab\";\n    const option_block = `${noPalette} [data-container-title='${blockName}']`;\n    return {\n        trigger: `${option_block} ${actionId}, ${option_block} [data-action-id=\"${actionId}\"]`,\n        content: markup(\n            _t(\"<b>Click</b> on this option to change the %s of the block.\", optionTooltipLabel)\n        ),\n        tooltipPosition: position,\n        run: \"click\",\n    };\n}\n\n/*\n * This function is used when the desired UI control is embedded inside popover\n * (e.g., a dropdown that appears only after clicking a toggle).\n *\n * It constructs two steps:\n *   1. Clicks the dropdown toggle or control to open the popover.\n *   2. Clicks the target element (option) inside the popover.\n *\n * Note: This function assumes that the popover content is available and render\n *       immediately after the first click.\n *\n * @param {string} blockName - The name of the block (e.g., \"Text - Image\").\n * @param {string} optionName - The name of the option (e.g., \"Visibility\").\n * @param {string} elementName - The name of the element to be clicked inside\n *                               the popover (e.g., \"Conditionally\").\n * @param {Boolean} searchNeeded - If the widget is a m2o widget and a search is needed.\n *\n * Example:\n *      ...changeOptionInPopover(\"Text - Image\", \"Visibility\", \"Conditionally\")\n */\nexport function changeOptionInPopover(blockName, optionName, elementName, searchNeeded = false) {\n    const steps = [changeOption(blockName, `[data-label='${optionName}'] .dropdown-toggle`)];\n\n    if (searchNeeded) {\n        steps.push({\n            content: `Inputing ${elementName} in toogle option search`,\n            trigger: `.o_popover input`,\n            run: `edit ${elementName}`,\n        });\n    }\n\n    steps.push(\n        clickOnElement(\n            `${elementName} in the ${optionName} option`,\n            [\n                `.o_popover div.o-dropdown-item:contains(\"${elementName}\")`,\n                `.o_popover span.o-dropdown-item:contains(\"${elementName}\")`,\n                `.o_popover div.o-dropdown-item[title=\"${elementName}\"]`,\n                `.o_popover span.o-dropdown-item[title=\"${elementName}\"]`,\n                `.o_popover ${elementName}`,\n            ].join(\", \")\n        )\n    );\n    return steps;\n}\n\nexport function selectNested(\n    trigger,\n    optionName,\n    altTrigger = null,\n    optionTooltipLabel = \"\",\n    position = \"top\",\n    allowPalette = false\n) {\n    const noPalette = allowPalette\n        ? \"\"\n        : \".o_we_customize_panel:not(:has(.o_we_so_color_palette.o_we_widget_opened))\";\n    const option_block = `${noPalette} we-customizeblock-option[class='snippet-option-${optionName}']`;\n    return {\n        trigger: trigger + (altTrigger ? `, ${option_block} ${altTrigger}` : \"\"),\n        content: markup(_t(\"<b>Select</b> a %s.\", optionTooltipLabel)),\n        tooltipPosition: position,\n        run: \"click\",\n    };\n}\n\nexport function changePaddingSize(direction) {\n    let paddingDirection = \"n\";\n    let position = \"top\";\n    if (direction === \"bottom\") {\n        paddingDirection = \"s\";\n        position = \"bottom\";\n    }\n    return {\n        trigger: `.oe_overlay.oe_active .o_handle.${paddingDirection}`,\n        content: markup(_t(\"<b>Slide</b> this button to change the %s padding\", direction)),\n        tooltipPosition: position,\n        run: \"click\",\n    };\n}\n\n/**\n * Checks if an element is visible on the screen, i.e., not masked by another\n * element.\n *\n * @param {String} elementSelector The selector of the element to be checked.\n * @returns {Object} The steps required to check if the element is visible.\n */\nexport function checkIfVisibleOnScreen(elementSelector) {\n    return {\n        content: \"Check if the element is visible on screen\",\n        trigger: `${elementSelector}`,\n        run() {\n            const boundingRect = this.anchor.getBoundingClientRect();\n            const centerX = boundingRect.left + boundingRect.width / 2;\n            const centerY = boundingRect.top + boundingRect.height / 2;\n            const iframeDocument = document.querySelector(\n                \".o_website_preview iframe\"\n            ).contentDocument;\n            const el = iframeDocument.elementFromPoint(centerX, centerY);\n            if (!this.anchor.contains(el)) {\n                console.error(\"The element is not visible on screen\");\n            }\n        },\n    };\n}\n\n/**\n * Simple click on an element in the page.\n * @param {*} elementName\n * @param {*} selector\n */\nexport function clickOnElement(elementName, selector) {\n    return {\n        content: `Clicking on the ${elementName}`,\n        trigger: selector,\n        run: \"click\",\n    };\n}\n\n/**\n * Click on the top right edit button and wait for the edit mode\n *\n * @param {string} position Where the purple arrow will show up\n */\nexport function clickOnEditAndWaitEditMode(position = \"bottom\") {\n    return [\n        {\n            content: markup(_t(\"<b>Click Edit</b> to start designing your homepage.\")),\n            trigger: \"body .o_menu_systray .o_menu_systray_item.o_edit_website_container button\",\n            tooltipPosition: position,\n            run: \"click\",\n        },\n        {\n            content: \"Check that we are in edit mode\",\n            trigger: \".o_builder_sidebar_open\",\n        },\n    ];\n}\n\n/**\n * Click on the top right edit dropdown, then click on the edit dropdown item\n * and wait for the edit mode\n *\n * @param {string} position Where the purple arrow will show up\n */\nexport function clickOnEditAndWaitEditModeInTranslatedPage(position = \"bottom\") {\n    return [\n        {\n            content: markup(_t(\"<b>Click Edit</b> dropdown\")),\n            trigger: \"body .o_menu_systray button:contains('Edit')\",\n            tooltipPosition: position,\n            run: \"click\",\n        },\n        {\n            content: markup(_t(\"<b>Click Edit</b> to start designing your homepage.\")),\n            trigger: \".o_edit_website_dropdown_item\",\n            tooltipPosition: position,\n            run: \"click\",\n        },\n        {\n            content: \"Check that we are in edit mode\",\n            trigger: \".o_builder_sidebar_open\",\n        },\n    ];\n}\n\n/**\n * Simple click on a snippet in the edition area\n * @param {*} snippet\n * @param {*} position\n */\nexport function clickOnSnippet(snippet, position = \"bottom\") {\n    const trigger = snippet.id ? `#wrapwrap .${snippet.id}` : snippet;\n    return [\n        {\n            trigger: \".o-website-builder_sidebar\",\n            noPrepend: true,\n        },\n        {\n            trigger: `:iframe ${trigger}`,\n            content: markup(_t(\"<b>Click on a snippet</b> to access its options menu.\")),\n            tooltipPosition: position,\n            run: \"click\",\n        },\n    ];\n}\n\nexport function clickOnSave(position = \"bottom\", timeout = 50000, withContains = true) {\n    return [\n        {\n            trigger: \".o-snippets-menu:not(:has(.o_we_ongoing_insertion))\",\n        },\n        {\n            trigger: \"body:not(:has(.o_dialog))\",\n            noPrepend: true,\n        },\n        {\n            trigger: withContains\n                ? \"button[data-action=save]:enabled:contains(save)\"\n                : \"button[data-action=save]:enabled\",\n            content: markup(_t(\"Good job! It's time to <b>Save</b> your work.\")),\n            tooltipPosition: position,\n            run: \"click\",\n            timeout,\n        },\n        {\n            trigger: \"body:not(.o_builder_open)\",\n            noPrepend: true,\n            timeout,\n        },\n        stepUtils.waitIframeIsReady(),\n    ];\n}\n\n/**\n * Click on a snippet's text to modify its content\n * @param {*} snippet\n * @param {*} element Target the element which should be rewrite\n * @param {*} position\n */\nexport function clickOnText(snippet, element, position = \"bottom\") {\n    return [\n        {\n            trigger: \":iframe body .odoo-editor-editable\",\n        },\n        {\n            trigger: snippet.id ? `:iframe #wrapwrap .${snippet.id} ${element}` : snippet,\n            content: markup(_t(\"<b>Click on a text</b> to start editing it.\")),\n            tooltipPosition: position,\n            run: \"click\",\n        },\n        {\n            trigger: \"#customize-tab.active\",\n        },\n    ];\n}\n\n/**\n * Selects a category or an inner snippet from the snippets menu and insert it\n * in the page.\n * @param {*} snippet contain the id and the name of the targeted snippet. If it\n * contains a group it means that the snippet is shown in the \"add snippets\"\n * dialog.\n * @param {*} position Where the purple arrow will show up\n */\nexport function insertSnippet(snippet, { position = \"bottom\", ignoreLoading = false } = {}) {\n    const blockEl = snippet.groupName || snippet.name;\n    const insertSnippetSteps = [\n        {\n            trigger: \".o_builder_sidebar_open\",\n            noPrepend: true,\n        },\n    ];\n    const snippetIDSelector = snippet.id\n        ? `[data-snippet-id=\"${snippet.id}\"]`\n        : `[data-snippet-id^=\"${snippet.customID}_\"]`;\n    if (snippet.groupName) {\n        insertSnippetSteps.push(\n            {\n                content: markup(_t(\"Click on the <b>%s</b> category.\", blockEl)),\n                trigger: `.o_block_tab:not(.o_we_ongoing_insertion) #snippet_groups .o_snippet[name=\"${blockEl}\"].o_draggable .o_snippet_thumbnail_area`,\n                tooltipPosition: position,\n                run: \"click\",\n            },\n            {\n                content: markup(_t(\"Click on the <b>%s</b> building block.\", snippet.name)),\n                // FIXME `:not(.d-none)` should obviously not be needed but it seems\n                // currently needed when using a tour in user/interactive mode.\n                trigger: `.modal .show:iframe .o_snippet_preview_wrap${snippetIDSelector}:not(.d-none)`,\n                noPrepend: true,\n                tooltipPosition: \"top\",\n                run: \"click\",\n            }\n        );\n    } else {\n        insertSnippetSteps.push({\n            content: markup(\n                _t(\"Drag the <b>%s</b> block and drop it at the bottom of the page.\", blockEl)\n            ),\n            trigger: `.o_block_tab:not(.o_we_ongoing_insertion) #snippet_content .o_snippet[name=\"${blockEl}\"].o_draggable .o_snippet_thumbnail`,\n            tooltipPosition: position,\n            run: \"drag_and_drop :iframe #wrapwrap > footer\",\n        });\n    }\n\n    if (!ignoreLoading) {\n        insertSnippetSteps.push({\n            trigger: \":iframe:not(:has(.o_loading_screen))\",\n        });\n    }\n\n    return insertSnippetSteps;\n}\n\nexport function goBackToBlocks(position = \"bottom\") {\n    return {\n        trigger: \"button[data-name='blocks']\",\n        content: _t(\"Click here to go back to block tab.\"),\n        tooltipPosition: position,\n        run: \"click\",\n    };\n}\n\nexport function goToTheme(position = \"bottom\") {\n    return [\n        {\n            trigger: \".o-website-builder_sidebar\",\n        },\n        {\n            trigger: \"button[data-name='theme']\",\n            content: _t(\"Go to the Theme tab\"),\n            tooltipPosition: position,\n            run: \"click\",\n        },\n        {\n            content: \"Check that the theme tab is active\",\n            trigger: \".o-tab-content .options-container [data-action-id='switchTheme']\",\n        },\n    ];\n}\n\nexport function selectHeader(position = \"bottom\") {\n    return {\n        trigger: `:iframe header#top`,\n        content: markup(_t(`<b>Click</b> on this header to configure it.`)),\n        tooltipPosition: position,\n        run: \"click\",\n    };\n}\n\nexport function selectSnippetColumn(snippet, index = 0, position = \"bottom\") {\n    return {\n        trigger: `:iframe #wrapwrap .${snippet.id} .row div[class*=\"col-lg-\"]:eq(${index})`,\n        content: markup(_t(\"<b>Click</b> on this column to access its options.\")),\n        tooltipPosition: position,\n        run: \"click\",\n    };\n}\n\nexport function prepend_trigger(steps, prepend_text = \"\") {\n    for (const step of steps) {\n        if (!step.noPrepend && prepend_text) {\n            step.trigger = prepend_text + step.trigger;\n        }\n    }\n    return steps;\n}\n\nexport function getClientActionUrl(path, edition) {\n    let url = `/odoo/action-website.website_preview`;\n    if (path) {\n        url += `?path=${encodeURIComponent(path)}`;\n    }\n    if (edition) {\n        url += `${path ? \"&\" : \"?\"}enable_editor=1`;\n    }\n    return url;\n}\n\nexport function clickOnExtraMenuItem(stepOptions, backend = false) {\n    return Object.assign(\n        {\n            content: \"Click on the extra menu dropdown toggle if it is there\",\n            trigger: `${backend ? \":iframe\" : \"\"} .top_menu`,\n            async run(actions) {\n                // Note: the button might not exist (it only appear if there is\n                // many menu items).\n                const extraMenuButton = this.anchor.querySelector(\".o_extra_menu_items a.nav-link\");\n                // Don't click on the extra menu button if it's already visible.\n                if (extraMenuButton && !extraMenuButton.classList.contains(\"show\")) {\n                    await actions.click(extraMenuButton);\n                }\n            },\n        },\n        stepOptions\n    );\n}\n\n/**\n * Registers a tour that will go in the website client action.\n *\n * @param {string} name The tour's name\n * @param {object} options The tour options\n * @param {string} options.url The page to edit\n * @param {boolean} [options.edition] If the tour starts in edit mode\n * @param {() => TourStep[]} steps The steps of the tour. Has to be a function to avoid direct interpolation of steps.\n */\nexport function registerWebsitePreviewTour(name, options, steps) {\n    if (typeof steps !== \"function\") {\n        throw new Error(`tour.steps has to be a function that returns TourStep[]`);\n    }\n    registry.category(\"web_tour.tours\").remove(name);\n    return registry.category(\"web_tour.tours\").add(name, {\n        ...omit(options, \"edition\"),\n        url: getClientActionUrl(options.url, !!options.edition),\n        steps: () => {\n            const tourSteps = [...steps()];\n            // Note: for both non edit mode and edit mode, we set a high timeout for the\n            // first step. Indeed loading both the backend and the frontend (in the\n            // iframe) and potentially starting the edit mode can take a long time in\n            // automatic tests. We'll try and decrease the need for this high timeout\n            // of course.\n            if (options.edition) {\n                tourSteps.unshift({\n                    content: \"Wait for the edit mode to be started\",\n                    trigger: \".o_builder_sidebar_open\",\n                    timeout: 30000,\n                });\n            } else {\n                tourSteps[0].timeout = 20000;\n            }\n            return tourSteps.map((step) => {\n                delete step.noPrepend;\n                return step;\n            });\n        },\n    });\n}\n\nexport function registerThemeHomepageTour(name, steps) {\n    if (typeof steps !== \"function\") {\n        throw new Error(`tour.steps has to be a function that returns TourStep[]`);\n    }\n    return registerWebsitePreviewTour(\n        \"homepage\", // it overrides the community tour with the associated theme tour\n        {\n            url: \"/\",\n        },\n        () => [\n            ...clickOnEditAndWaitEditMode(),\n            // FIXME(?) this should probably reuse the prepend_trigger function\n            // so that we do check that we are really on the homepage.\n            ...steps(),\n            ...goToTheme(),\n            ...clickOnSave(),\n        ]\n    );\n}\n\nexport function registerBackendAndFrontendTour(name, options, steps) {\n    if (typeof steps !== \"function\") {\n        throw new Error(`tour.steps has to be a function that returns TourStep[]`);\n    }\n    if (window.location.pathname === \"/odoo\") {\n        return registerWebsitePreviewTour(name, options, () => {\n            const newSteps = [];\n            for (const step of steps()) {\n                const newStep = Object.assign({}, step);\n                newStep.trigger = `:iframe ${step.trigger}`;\n                newSteps.push(newStep);\n            }\n            return newSteps;\n        });\n    }\n\n    return registry.category(\"web_tour.tours\").add(name, {\n        url: options.url,\n        steps: () => steps(),\n    });\n}\n\n/**\n * Switches to a different website by clicking on the website switcher.\n *\n * @param {number} websiteId - The ID of the website to switch to.\n * @param {string} websiteName - The name of the website to switch to.\n * @returns {Array} - The steps required to perform the website switch.\n */\nexport function switchWebsite(websiteId, websiteName) {\n    return [\n        {\n            content: `Click on the website switch to switch to website '${websiteName}'`,\n            trigger: \".o_website_switcher_container button\",\n            run: \"click\",\n        },\n        {\n            trigger: `:iframe html:not([data-website-id=\"${websiteId}\"])`,\n        },\n        {\n            content: `Switch to website '${websiteName}'`,\n            trigger: `.o-dropdown--menu .dropdown-item[data-website-id=\"${websiteId}\"]:contains(\"${websiteName}\")`,\n            run: \"click\",\n        },\n        {\n            content: \"Wait for the iframe to be loaded\",\n            // The page reload generates assets for the new website, it may take\n            // some time\n            timeout: 20000,\n            trigger: `:iframe html[data-website-id=\"${websiteId}\"]`,\n        },\n    ];\n}\n\n/**\n * Switches to a different website by clicking on the website switcher.\n * This function can only be used during test tours as it requires\n * specific cookies to properly function.\n *\n * @param {string} websiteName - The name of the website to switch to.\n * @returns {Array} - The steps required to perform the website switch.\n */\nexport function testSwitchWebsite(websiteName) {\n    const websiteIdMapping = JSON.parse(cookie.get(\"websiteIdMapping\") || \"{}\");\n    const websiteId = websiteIdMapping[websiteName];\n    return switchWebsite(websiteId, websiteName);\n}\n\n/**\n * Toggles the mobile preview on or off.\n *\n * @param {Boolean} toggleOn true to toggle the mobile preview on, false to\n *     toggle it off.\n * @returns {Array}\n */\nexport function toggleMobilePreview(toggleOn) {\n    const onOrOff = toggleOn ? \"on\" : \"off\";\n    const mobileOnSelector = \".o_is_mobile\";\n    const mobileOffSelector = \":not(.o_is_mobile)\";\n    return [\n        {\n            trigger: `div.o_website_preview${toggleOn ? mobileOffSelector : mobileOnSelector}`,\n        },\n        {\n            content: `Toggle the mobile preview ${onOrOff}`,\n            trigger: \".o-snippets-top-actions [data-action='mobile']\",\n            run: \"click\",\n        },\n        {\n            content: `Check that the mobile preview is ${onOrOff}`,\n            trigger: `div.o_website_preview${toggleOn ? mobileOnSelector : mobileOffSelector}`,\n        },\n    ];\n}\n\n/**\n * Opens the link popup for the specified link element.\n *\n * @param {string} triggerSelector - Selector for the link element.\n * @param {string} [linkName=\"\"] - Name of the link.\n * @param {number} [focusNodeIndex=0] - Index of the child node to focus inside\n *                                      the link element.\n * @returns {TourStep[]} The tour steps that opens the link popup.\n */\nexport function openLinkPopup(\n    triggerSelector,\n    linkName = \"\",\n    focusNodeIndex = 0,\n    triggerClick = false\n) {\n    return [\n        {\n            content: `Open '${linkName}' link popup`,\n            trigger: triggerSelector,\n            async run(actions) {\n                if (triggerClick) {\n                    actions.click();\n                }\n                const el = this.anchor;\n                const sel = el.ownerDocument.getSelection();\n                sel.collapse(el.childNodes[focusNodeIndex], 1);\n                el.focus();\n            },\n        },\n        {\n            content: \"Check if the link popover opened\",\n            trigger: \".o-we-linkpopover\",\n        },\n    ];\n}\n\n/**\n * Selects all the text of an element.\n * @param {*} elementName\n * @param {*} selector\n */\nexport function selectFullText(elementName, selector) {\n    return {\n        content: `Select all the text of the ${elementName}`,\n        trigger: `:iframe ${selector}`,\n        async run(actions) {\n            await actions.click();\n            const range = document.createRange();\n            const selection = this.anchor.ownerDocument.getSelection();\n            range.selectNodeContents(this.anchor);\n            selection.removeAllRanges();\n            selection.addRange(range);\n        },\n    };\n}\n\n/**\n * Click button from the toolbar, if expand is true, it will\n * first expand the toolbar.\n * @param {string} elementName\n * @param {string} selector\n * @param {string} button\n * @param {boolean} expand - Whether to expand the toolbar for more buttons.\n * @returns {Array} The steps to click the toolbar button.\n */\nexport function clickToolbarButton(elementName, selector, button, expand = false) {\n    const steps = [\n        selectFullText(`${elementName}`, selector),\n        {\n            content: `Click on the ${button} from toolbar`,\n            trigger: `.o-we-toolbar button[title=\"${button}\"], .o-we-toolbar button[name=\"${button}\"]`,\n            run: \"click\",\n        },\n    ];\n    if (expand) {\n        steps.splice(1, 0, {\n            content: \"Expand the toolbar for more buttons\",\n            trigger: \".o-we-toolbar button[name='expand_toolbar']\",\n            run: \"click\",\n        });\n    }\n    return steps;\n}\n", "import { isVisible } from \"@web/core/utils/ui\";\n\n//TODO: Delete higlight function (duplicate whith highlight_utils) when deleting snippets options\n// SVG generator: contains all information needed to draw highlight SVGs\n// according to text dimensions, highlight style,...\nconst _textHighlightFactory = {\n    underline: (targetEl) => drawPath(targetEl, { mode: \"line\" }),\n    freehand_1: (targetEl) => {\n        const template = (w, h) => [\n            `M 0,${h * 1.1} C ${w / 8},${h * 1.05} ${w / 4},${h} ${w},${h}`,\n        ];\n        return drawPath(targetEl, { mode: \"free\", template });\n    },\n    freehand_2: (targetEl) => {\n        const template = (w, h) => [\n            `M181.27 13.873c-.451-1.976-.993-3.421-1.072-4.9-.125-2.214-.61-4.856.384-6.539.756-1.287 3.636-2.055 5.443-1.852 3.455.395 7.001 1.231` +\n                ` 10.14 2.676 1.728.802 3.174 3.06 3.817 4.98.237.712-1.953 2.824-3.399 3.4-2.766 1.095-5.748 1.75-8.706 2.179-2.394.339-4.879.068-6.584.068l-.023-.012ZM8.416 3.90` +\n                `2c3.862.26 7.78.249 11.574.926 1.65.294 3.027 2.033 4.54 3.117-1.095 1.186-1.987 2.982-3.343 3.456a67.118 67.118 0 0 1-11.19 2.823c-3.253.53-6.494-.339-8.617-2.98` +\n                `1C.364 9.978-.302 7.686.138 6.263c.361-1.152 2.54-2 4.077-2.44 1.287-.372 2.789.046 4.2.102v-.023Zm154.267 9.983c-4.291-.305-8.153-1.58-9.915-5.623-.745-1.694-.39` +\n                `5-4.382.474-6.121 1.073-2.168 3.512-1.965 5.613-1.005 2.541 1.174 5.251 2.157 7.509 3.76 1.502 1.073 3.557 3.445 3.207 4.574-.519 1.694-2.857 2.913-4.562 4.133-.5` +\n                `76.406-1.592.203-2.326.282ZM72.58 17.42c-2.733-1.807-5.307-3.004-7.137-4.913-.892-.925-.892-3.376-.361-4.776.407-1.05 2.304-2.112 3.546-2.135 3.602-.056 7.238.215` +\n                ` 10.818.723 3.828.542 5.15 4.1 2.213 6.539-2.439 2.021-5.77 2.958-9.079 4.562Zm30.795-.802c-2.507-1.536-5.228-2.823-7.397-4.743-.925-.813-1.377-3.297-.813-4.359.6` +\n                `78-1.265 2.677-2.507 4.11-2.518 3.016-.023 6.155.418 9.001 1.389 1.412.485 3.173 2.552 3.185 3.907 0 1.57-1.423 3.557-2.801 4.619-1.152.892-3.139.711-4.743 1.005-` +\n                `.181.226-.35.463-.531.689l-.011.01Zm-59.704-1.457c-2.066-1.163-4.788-2.224-6.82-4.054-.915-.824-1.04-3.478-.407-4.765.486-.983 2.722-1.559 4.156-1.502 2.676.101 5` +\n                `.398.542 7.95 1.332 1.457.452 3.523 1.75 3.681 2.891.18 1.31-1.13 3.309-2.383 4.201-1.411 1.005-3.466 1.118-6.188 1.886l.011.011Zm88.489-1.863c-2.643-1.48-5.567-2` +\n                `.62-7.803-4.574-1.005-.88-1.31-3.692-.667-5.002.509-1.04 2.982-1.615 4.529-1.513 2.032.135 4.054 1.027 6.007 1.772 2.485.95 5.026 2.236 4.382 5.455-.644 3.15-3.49` +\n                ` 2.947-5.963 3.004-.169.293-.327.575-.496.87l.011-.012Z`,\n        ];\n        return drawPath(targetEl, {\n            mode: \"fill\",\n            template,\n            SVGWidth: 200,\n            SVGHeight: 18,\n            position: \"bottom\",\n        });\n    },\n    freehand_3: (targetEl) => {\n        const template = (w, h) => [\n            `M189.705 18.285c-3.99.994-7.968 2.015-11.958 2.972-1.415.344-2.926 1.008-4.278.727-6.305-1.327-12.568-3.036-18.874-4.376-1.995-.42-4.2` +\n                `46-.701-6.133-.038-5.867 2.067-11.54 2.386-17.374-.242-1.491-.676-3.56-.421-5.125.217-5.523 2.22-10.789 3.597-16.494.127-1.64-.995-4.675-.038-6.584 1.148-6.102 3.` +\n                `789-12.01 4.414-18.198.434-.998-.638-2.681-.638-3.754-.115-6.852 3.355-13.404 2.858-20.043-1.008-1.5-.867-4.02-.6-5.608.307-7.528 4.35-14.842 5.702-22.07-.638-2.1` +\n                `44-1.875-3.71-.37-5.394 1.046-4.622 3.89-9.565 6.327-15.367 4.286C6.338 20.989.505 13.067.022 5.949-.085 4.38.194 1.753.955 1.332 2.253.617 4.537.553 5.588 1.51 7` +\n                `.55 3.27 9.18 5.77 10.52 8.296c2.82 5.269 4.15 5.766 8.504 2.156 1.555-1.288 2.992-2.768 4.396-4.286 4.022-4.311 7.143-4.465 11.26-.472 7.068 6.837 8.226 7.067 15` +\n                `.979 1.314 3.721-2.755 7.206-2.653 10.627.128 4.987 4.056 9.791 4.49 14.853.191 2.702-2.296 5.78-2.296 8.45.115 4.29 3.89 8.45 3.33 12.719.166.847-.638 1.705-1.26` +\n                `3 2.552-1.914 3.035-2.309 6.048-2.5 9.019.166 3.453 3.087 7.12 3.15 10.616.472 4.107-3.138 7.85-3.342 12.16-.306 3.668 2.59 7.83 1.964 11.594-.255 3.935-2.322 7.6` +\n                `67-2.488 11.409.408.365.28.794.612 1.213.65 6.799.549 13.522 3.394 20.428.779 1.887-.715 3.914-1.034 5.899-1.148 3.313-.192 6.659-.358 9.941 0 1.993.23 4.354.905 ` +\n                `5.737 2.436 1.308 1.429 2.113 4.235 2.123 6.442.022 3.023-2.424 3.431-4.472 3.597-1.887.153-3.796.038-5.695.038-.053-.216-.106-.446-.16-.663l.032-.025Z`,\n        ];\n        return drawPath(targetEl, {\n            mode: \"fill\",\n            template,\n            SVGWidth: 200,\n            SVGHeight: 24,\n            position: \"bottom\",\n        });\n    },\n    double: (targetEl) => {\n        const template = (w, h) => [`M 0,${h * 0.9} h ${w}`, `M 0,${h * 1.1} h ${w}`];\n        return drawPath(targetEl, { mode: \"free\", template });\n    },\n    wavy: (targetEl) => {\n        const template = (w, h) => [\n            `c ${w / 4},0 ${w / 4},-${h / 2} ${w / 2},-${h / 2}` +\n                `c ${w / 4},0 ${w / 4},${h / 2} ${w / 2},${h / 2}`,\n        ];\n        return drawPath(targetEl, { mode: \"pattern\", template });\n    },\n    circle_1: (targetEl) => {\n        const template = (w, h) => [\n            `M ${w / 2.88},${h / 1.1} C ${w / 1.1},${h / 1.05} ${w * 1.05},${h / 1.1} ${\n                w * 1.023\n            },${h / 2.32}` +\n                `C ${w}, ${h / 14.6} ${w / 1.411},0 ${w / 2},0 S -2,${h / 14.6} -2,${h / 2.2}` +\n                `S ${w / 4.24},${h} ${w / 1.36},${h * 1.04}`,\n        ];\n        return drawPath(targetEl, { mode: \"free\", template });\n    },\n    circle_2: (targetEl) => {\n        const template = (w, h) => [\n            `M112.58 21.164h18.516c-.478-.176-1.722-.64-2.967-1.105.101-.401.214-.803.315-1.192 12.255 2.912 24.561 5.573 36.716 8.823 5.896 1.582 ` +\n                `11.628 3.967 17.171 6.527 10.433 4.832 14.418 14.22 16.479 24.739.377 1.92.566 3.878.83 5.823 2.212 15.94-5.858 23.986-21.595 33.813-.993.615-2.288.79-3.181 1.494` +\n                `-14.229 11.308-31.412 14.32-48.608 17.107-29.01 4.694-57.431 2.209-84.91-8.372-8.145-3.138-16.164-6.853-23.706-11.22C6.176 90.986 1.16 80.053.193 67.25c-1.798-23.` +\n                `809 9.025-42.485 30.356-53.304C44.678 6.793 59.8 3.367 75.45 2.375 90.583 1.42 105.793.379 120.927.78c16.089.427 32.041 3.05 46.911 9.84 2.074.941 3.67 2.912 4.91` +\n                `5 5.083-9.73-1.443-19.433-2.987-29.175-4.305-4.89-.665-9.842-1.067-14.77-1.33-23.82-1.28-47.376.514-70.391 7.003a133.771 133.771 0 0 0-22.639 8.648c-17.9 8.786-27` +\n                `.616 26.935-25.567 46.364.666 6.263 3.507 11.133 9.05 14.308 26.862 15.401 55.748 21.965 86.645 19.819 15.561-1.08 31.01-2.787 45.767-8.284 11.099-4.142 21.658-9.` +\n                `25 30.595-17.195 9.779-8.698 11.715-18.55 5.669-30.249-1.131-2.196-3.256-4.079-5.33-5.56-7.981-5.736-17.773-7.48-26.459-11.534-13.249-6.175-27.541-6.916-41.343-10` +\n                `.167-.817-.188-1.571-.64-2.35-.966.037-.364.088-.728.125-1.092Z`,\n        ];\n        return drawPath(targetEl, { mode: \"fill\", template, SVGWidth: 200, SVGHeight: 120 });\n    },\n    circle_3: (targetEl) => {\n        const template = (w, h) => [\n            `M78.653 89.204c-14.815 0-29.403-1.096-43.354-4.698-5.227-1.346-10.407-3.069-14.997-5.199-22.996-10.649-27.04-28.502-9.135-43.035 12.18` +\n                `-9.866 26.813-18.04 43.355-24.242C88.515-.718 124.19-3.725 161.228 4.889c13.224 3.07 24.449 8.268 31.902 16.662 8.862 9.992 9.453 20.422 0 30.068-5.817 5.889-13.2` +\n                `24 11.37-21.359 15.786-27.176 14.752-58.579 21.518-93.072 21.8h-.046Zm3.5-4.228c4.408-.282 11.725-.47 18.86-1.253 30.357-3.351 57.579-11.432 79.211-26.842 5.362-3` +\n                `.82 10.134-8.832 12.27-13.875 2.545-5.982 5.817-13.311-6.226-17.352-.454-.156-.727-.563-1.045-.845-10.771-9.146-25.086-14.157-41.719-15.348-39.674-2.85-76.62 3.19` +\n                `5-109.66 18.762-8.18 3.883-15.497 9.177-21.359 14.752-9.725 9.27-8.044 19.889 3.727 28.032 4.862 3.383 10.997 6.233 17.269 8.237 14.406 4.605 30.04 5.544 48.58 5.` +\n                `763l.092-.03ZM130.37 3.573c-24.813-1.88-48.263 1.378-70.44 9.146 22.814-5.481 46.172-9.02 70.44-9.146Z`,\n        ];\n        return drawPath(targetEl, { mode: \"fill\", template, SVGWidth: 200, SVGHeight: 90 });\n    },\n    over_underline: (targetEl) => {\n        const template = (w, h) => [`M 0,0 h ${w}`, `M 0,${h} h ${w}`];\n        return drawPath(targetEl, { mode: \"free\", template });\n    },\n    scribble_1: (targetEl) => {\n        const template = (w, h) => [\n            `M ${w / 2},${h * 0.9} c ${w / 16},0 ${w},1 ${w / 5},1 c 2,0 -${w / 10},-2 -${\n                w / 2\n            },-1` +\n                `c -${w / 20},0 -${w / 5},2 -${w / 5},4 c -2,0 ${w / 10},-1 ${w / 2},${h / 16}` +\n                `c ${w / 25},0 ${w / 10},0 ${w / 5},1 c 0,0 -${w / 10},1 -${w / 8},1` +\n                `c -${w / 40},0 -${w / 16},0 -${w / 4},${h / 22}`,\n        ];\n        return drawPath(targetEl, { mode: \"free\", template });\n    },\n    scribble_2: (targetEl) => {\n        const template = (w, h) => [\n            `M200 3.985c-.228-.332-3.773.541-.01-.006-.811-.037-6.705-1.442-9.978-1.706-1.473.194-2.907.534-4.351.818-1.398.27-2.937.985-4.144.756-` +\n                `9.56-1.782-19.3-1.089-28.955-1.31C118.932 1.767 85.301.942 51.671.45c-13.732-.201-27.492.333-41.233.665C6.561 1.212 3.026 2.363.84 4.838.09 5.684-.262 7.126.223 7` +\n                `.993c.313.554 2.518.79 3.839.728 2.47-.118 4.922-.548 8.096-.936-.96 1.227-1.568 1.865-1.986 2.558-1.368 2.302.029 4 3.203 4.083 24.716.666 49.424 1.4 74.15 2.01 ` +\n                `21.087.52 42.145.34 63.146-1.414 4.495-.374 8.999-.644 14.425-1.026-3.117-1.629-4.723-3.521-8.39-3.535-17.999-.077-36.016-.07-54.005-.534-22.246-.576-44.464-1.58-` +\n                `66.7-2.406-.276-.007-.551-.097-.817-.471 1.016 0 2.033-.021 3.04 0 21.961.506 43.913.998 65.864 1.539 25.249.624 50.47.367 75.642-1.144 5.892-.354 11.765-.93 17.6` +\n                `19-1.54.788-.082 1.416-.99 2.651-1.92Z`,\n        ];\n        return drawPath(targetEl, {\n            mode: \"fill\",\n            template,\n            SVGWidth: 200,\n            SVGHeight: 17,\n            position: \"bottom\",\n        });\n    },\n    scribble_3: (targetEl) => {\n        const template = (w, h) => [\n            `M133.953 15.961c7.87.502 15.751.975 23.611 1.522 2.027.141 4.055.44 5.999.79 4.118.727 7.202 4.977 2.53 6.707.606.293 1.181.564 1.902.` +\n                `908-8.477 2.069-17.267 2.65-26.203 2.818-19.023.361-38.056.603-57.068 1.088-13.807.355-27.572 1.06-41.369 1.545-3.23.113-6.532.096-9.73-.147-1.548-.118-3.492-.721` +\n                `-4.234-1.42-.93-.88-1.484-2.199-.93-3.1.397-.655 2.812-1.263 4.41-1.33 6.397-.277 12.825-.333 19.243-.474 26.976-.592 53.942-1.156 80.919-1.804 3.742-.09 7.452-.5` +\n                `92 11.173-.908 0-.174-.01-.35-.021-.524-2.717-.197-5.435-.53-8.163-.575-21.865-.383-43.741-1.009-65.607-.936-11.34.04-22.65 1.432-34 2.047-6.898.377-13.88.732-20.` +\n                `779.569-7.044-.17-9.406-3.568-5.34-6.742 3.428-2.677 7.567-4.391 13.984-4.757 16.441-.93 32.798-2.26 49.219-3.27 14.162-.868 28.366-1.516 42.549-2.266.586-.034 1.` +\n                `15-.147 1.641-.45-5.006 0-10.023-.012-15.029.01-1.077 0-2.154.186-3.24.192-18.793.18-37.596.355-56.389.507-10.672.085-21.343.13-32.014.153a65.89 65.89 0 0 1-6.167` +\n                `-.277C1.787 5.555-.02 4.247 0 2.59 0 1.384.89.72 3.293.742c5.874.056 11.748.124 17.622.09C41.045.708 61.186.409 81.317.42c28.408.012 56.827.158 85.225.417 8.686.0` +\n                `8 17.35.7 26.015 1.122 3.23.158 5.832.902 7.024 2.678 1.055 1.572.125 2.21-2.875 1.95a30.51 30.51 0 0 0-2.268-.107c-.397 0-.805.073-1.557.146.721.451 1.306.767 1.` +\n                `777 1.128 2.926 2.238 1.641 4.013-3.272 4.369-13.483.958-26.966 1.91-40.459 2.767-3.334.214-6.752 0-10.118.085-2.31.062-4.609.299-6.909.462l.042.519.011.005Z`,\n        ];\n        return drawPath(targetEl, {\n            mode: \"fill\",\n            template,\n            SVGWidth: 200,\n            SVGHeight: 32,\n            position: \"bottom\",\n        });\n    },\n    scribble_4: (targetEl) => {\n        const template = (w, h) => [\n            `M96.414 17.157c1.34-2.173 2.462-4.075 3.649-5.944 2.117-3.335 5.528-4.302 9.372-2.694 3.962 1.651 4.89 3.575 3.908 8.073-.205.967-.388` +\n                ` 1.934-.022 3.118 1.513-3.075 3.013-6.15 4.557-9.203 1.306-2.586 4.297-3.433 7.859-2.195 2.765.968 4.395 2.706 3.564 5.922-.529 2.054-1.005 4.118-.918 6.487.463-.` +\n                `859 1.015-1.685 1.371-2.586 1.447-3.673 3.002-7.324 4.2-11.083.896-2.792 2.192-3.955 5.323-3.564 4.772.598 7.049 3.412 5.84 7.986-.626 2.38-1.22 4.77-1.144 7.486.` +\n                `745-1.358 1.544-2.683 2.213-4.074a138.72 138.72 0 0 0 2.926-6.487c2.376-5.66 3.12-4.704 8.724-3.618 3.552.685 5.063 4.031 4.34 7.997-.616 3.423-1.166 6.856-1.749 ` +\n                `10.29l.95.358c.993-2.151 2.062-4.27 2.958-6.454.594-1.456.886-3.042 1.403-4.53 2.43-6.911 2.43-6.813 9.566-5.542.928.163 2.656-.967 3.078-1.923.992-2.26 2.332-2.7` +\n                `16 4.523-2.097 4.297 1.206 8.659 2.184 12.945 3.444 2.796.826 4.319 2.988 4.135 5.889-.173 2.684-.961 5.324-1.274 8.008-.734 6.4-1.361 12.799-2.019 19.21-.065.673` +\n                `.043 1.38-.097 2.031-.551 2.477-.41 5.465-3.476 6.421-2.311.717-6.489-2.194-7.644-5.03-.206-.5-.357-1.01-.918-2.63-1.22 3.27-2.073 5.629-2.991 7.965-2.095 5.345-3` +\n                `.66 5.954-8.874 3.705-.853-.37-2.354-.783-2.786-.359-3.163 3.075-5.971 1.217-8.853-.358-.378-.207-.81-.316-1.188-.457-5.851 7.65-12.502 4.596-15.061-3.944-1.543 3` +\n                `.042-2.883 5.726-4.265 8.399-3.357 6.53-7.783 6.975-12.47 1.25-.485-.587-.992-1.152-1.511-1.75-5.647 6.715-12.848 2.293-15.19-6.063-1.253 2.25-2.257 3.88-3.099 5.` +\n                `596-1.285 2.64-2.883 4.65-6.23 3.868-3.498-.826-6.532-4.085-6.65-7.225-.054-1.424 0-2.847-.475-4.433-1.393 2.879-2.71 5.802-4.19 8.637-3.228 6.204-6.067 6.824-11.` +\n                `67 2.912-.962-.673-2.57-.988-3.704-.728-3.681.837-6.272-.619-8.626-3.248-.691-.783-2.084-1.771-2.807-1.543-4.243 1.347-6.91-.641-9.166-3.836-.378-.543-.8-1.053-1.` +\n                `555-2.031-1.08 2.194-2.008 4.041-2.915 5.9-2.397 4.943-5.528 5.932-10.02 2.835-2.008-1.38-3.713-2.118-6.37-1.738-5.117.728-8.54-3.444-7.762-8.649.227-1.521.378-3.` +\n                `064-.086-4.9-.853 1.369-1.793 2.684-2.548 4.107-2.775 5.259-5.301 5.856-10.074 2.206-.971-.75-1.803-1.674-2.86-2.673-.67.271-1.598 1.043-2.257.858-2.71-.771-5.625` +\n                `-1.423-7.838-3.01-.842-.608-.378-3.683.108-5.465 2.008-7.41 4.232-14.755 6.413-22.11.572-1.945 1.166-3.901 1.943-5.77 1.89-4.52 5.02-5.454 9.145-2.89 1.144.706 2.` +\n                `408 1.217 3.552 1.923 2.364 1.456 4.696 2.988 7.439 4.737C32.423 7.14 37.444 6.64 42.82 10.41c2.602-2.107 1.803-7.17 6.748-6.323 3.369.587 6.478 1.217 7.439 4.878` +\n                ` 2.289-2.281 4.221-5.693 6.877-6.42 2.624-.718 5.992 1.26 9.599 2.216-.044.054.636-.565.96-1.348 1.048-2.499 2.883-3.4 5.42-2.825 2.775.62 5.474 1.304 6.284 4.76.` +\n                `216.89 1.285 2.042 2.159 2.248 7.58 1.793 7.6 1.739 8.108 9.55v.012Z`,\n        ];\n        return drawPath(targetEl, { mode: \"fill\", template, SVGWidth: 200, SVGHeight: 61 });\n    },\n    jagged: (targetEl) => {\n        const template = (w, h) => [\n            `q ${(4 * w) / 3} -${(2 * w) / 3} ${(2 * w) / 3} 0` +\n                `c -${w / 3} ${w / 3} -${w / 3} ${w / 3} ${w / 3} 0`,\n        ];\n        return drawPath(targetEl, { mode: \"pattern\", template });\n    },\n    cross: (targetEl) => {\n        const template = (w, h) => [`M 0,0 L ${w},${h}`, `M 0,${h} L ${w},0`];\n        return drawPath(targetEl, { mode: \"free\", template });\n    },\n    diagonal: (targetEl) => {\n        const template = (w, h) => [`M 0,${h} L${w},0`];\n        return drawPath(targetEl, { mode: \"free\", template });\n    },\n    strikethrough: (targetEl) => drawPath(targetEl, { mode: \"line\", position: \"center\" }),\n    bold: (targetEl) => {\n        const template = (w, h) => [\n            `M136.604 41.568c5.373.513 10.746 1.047 16.12 1.479 14.437 1.13 29.327 4.047 42.858-4.294 4.92-3.04 2.346-13.56-2.687-13.395-.825.02-1.` +\n                `635.062-2.46.082.858-3.677-.34-8.3-3.545-9.41 2.655.062 5.309.104 7.963.165 6.863.185 6.863-14.176 0-14.36A1958.994 1958.994 0 0 0 5.263 5.778C-.4 6.169-2.392 18.` +\n                `455 3.84 19.893c9.727 2.24 19.454 4.335 29.214 6.307-1.085 1.09-1.764 2.671-2.023 4.356-.615.061-1.214.102-1.83.164-6.748.74-6.959 14.587 0 14.361l107.42-3.513h-.` +\n                `016Z`,\n        ];\n        return drawPath(targetEl, { mode: \"fill\", template, SVGWidth: 200, SVGHeight: 46 });\n    },\n    bold_1: (targetEl) => {\n        const template = (w, h) => [\n            `M190.276 34.01c5.618-.25 7.136-6.526 4.444-9.755.037-.25.055-.5.072-.749 7.046-.949 7.01-11.752-.523-11.553-.796.017-1.59.017-2.403.05` +\n                `C196.78 9.573 195.931.8 189.264.983L13.784 5.678c-7.226.2-7.497 9.422-1.499 11.32-2.186 0-4.354 0-6.54-.017-7.696-.05-7.624 11.286 0 11.635 8.22.383 16.423.733 24` +\n                `.643 1.016l-7.823.35c-7.624.349-7.678 11.985 0 11.635 55.915-2.53 111.813-5.077 167.729-7.607h-.018Z`,\n        ];\n        return drawPath(targetEl, { mode: \"fill\", template, SVGWidth: 200, SVGHeight: 42 });\n    },\n    bold_2: (targetEl) => {\n        const template = (w, h) => [\n            `M193.221 20.193c.555 1.245.863 2.005 1.22 2.734 1.399 2.84 2.758 5.757 1.607 9.509-1.21 3.95-3.651 4.208-6.072 4.314-5.059.212-10.129.` +\n                `152-15.178.592-15.873 1.367-31.737 3.585-47.619 4.238-19.921.82-39.862.638-59.802.486-13.938-.106-27.887-.88-41.825-1.428-4.018-.151-8.046-.47-12.064-.896-2.758-.` +\n                `304-4.772-2.46-6.21-6.182-.645-1.656-1.756-2.993-2.798-4.177-2.768-3.13-5.06-6.38-3.899-12.502C.9 15.226.393 13.16.165 11.307c-.715-5.818.903-9.524 4.722-9.646 10` +\n                `.218-.35 20.437-.38 30.655-.577C51.236.78 66.94-.04 82.635.264c14.652.273 29.296 1.655 43.948 2.643 19.822 1.336 39.643 2.02 59.455-.426.923-.121 1.835-.5 2.758-.` +\n                `622 1.329-.183 2.688-.456 4.008-.274 3.829.501 7.073 5.666 7.192 11.21.09 4.466-1.418 6.213-6.775 7.428v-.03Z`,\n        ];\n        return drawPath(targetEl, { mode: \"fill\", template, SVGWidth: 200, SVGHeight: 43 });\n    },\n};\n// Returns the width of the DOMRect object.\nexport const getDOMRectWidth = (el) => el.getBoundingClientRect().width;\n\n/**\n * Draws one or many SVG paths using templates of path shape commands.\n *\n * @param {HTMLElement} textEl\n * @param {String} options.mode Specifies how to draw the path:\n * - \"pattern\": repeat the template along the horizontal axis.\n * - \"line\": draw a simple line (we specify the width & position).\n * - \"free\": draw the path shape using the template only.\n * - \"fill\": used for irregular shapes that do not follow the \"stroke\" design.\n * @param {Function} options.template Returns a list of SVG path\n * commands adapted to the container's size.\n * @returns {String[]}\n */\nfunction drawPath(textEl, options) {\n    // Note: cannot use getBoundingClientRect as we want to be able to draw\n    // text highlights in snippets/add page dialogs where iframe is scaled.\n    const width = textEl.offsetWidth;\n    const height = textEl.offsetHeight;\n    options = { ...options, width, height };\n    const yStart = options.position === \"center\" ? height / 2 : height;\n\n    switch (options.mode) {\n        case \"pattern\": {\n            let i = 0;\n            const d = [];\n            const nbrChars = textEl.textContent.length;\n            const w = width / nbrChars,\n                h = height * 0.2;\n            while (i < nbrChars) {\n                d.push(options.template(w, h));\n                i++;\n            }\n            return buildPath([`M 0,${yStart} ${d.join(\" \")}`], options);\n        }\n        case \"line\": {\n            return buildPath([`M 0,${yStart} h ${width}`], options);\n        }\n    }\n    return buildPath(options.template(width, height), options);\n}\n\n/**\n * Used to build the SVG <path/>, it should mainly adapt it to take into\n * consideration some cases where the shape is a \"filled path\" instead\n * of a single line stroke.\n *\n * @param {String[]} templates\n * @param {Object} options\n * @returns {Element[]}\n */\nfunction buildPath(templates, options) {\n    return templates.map((d) => {\n        const path = document.createElementNS(\"http://www.w3.org/2000/svg\", \"path\");\n        path.setAttribute(\"stroke-width\", \"var(--text-highlight-width)\");\n        path.setAttribute(\"stroke\", \"var(--text-highlight-color)\");\n        path.setAttribute(\"stroke-linecap\", \"round\");\n        if (options.mode === \"fill\") {\n            const wScale = options.width / options.SVGWidth;\n            let hScale = options.height / options.SVGHeight;\n            const transforms = [];\n            if (options.position === \"bottom\") {\n                hScale *= 0.3;\n                transforms.push(`translate(0 ${options.height * 0.8})`);\n            }\n            transforms.push(`scale(${wScale}, ${hScale})`);\n            path.setAttribute(\"fill\", \"var(--text-highlight-color)\");\n            path.setAttribute(\"transform\", transforms.join(\" \"));\n        }\n        path.setAttribute(\"d\", d);\n        return path;\n    });\n}\n\n/**\n * Returns a new highlight SVG adapted to the text container.\n *\n * @param {HTMLElement} textEl\n * @param {String} highlightID\n */\nexport function drawTextHighlightSVG(textEl, highlightID) {\n    const svg = document.createElementNS(\"http://www.w3.org/2000/svg\", \"svg\");\n    svg.setAttribute(\"fill\", \"none\");\n    svg.classList.add(\n        \"o_text_highlight_svg\",\n        // Identifies DOM content that should not be merged by the editor, even\n        // on identical parents.\n        \"o_content_no_merge\",\n        \"position-absolute\",\n        \"overflow-visible\",\n        \"top-0\",\n        \"start-0\",\n        \"w-100\",\n        \"h-100\",\n        \"pe-none\"\n    );\n    _textHighlightFactory[highlightID](textEl).forEach((pathEl) => {\n        pathEl.classList.add(`o_text_highlight_path_${highlightID}`);\n        svg.appendChild(pathEl);\n    });\n    return svg;\n}\n\n/**\n * Divides the content of a text container into multiple\n * `.o_text_highlight_item` units, and applies the highlight\n * on each unit.\n *\n * @param {HTMLElement} topTextEl\n * @param {String} highlightID\n */\nexport function applyTextHighlight(topTextEl, highlightID) {\n    const endHighlightUpdate = () =>\n        topTextEl.dispatchEvent(new Event(\"text_highlight_added\", { bubbles: true }));\n    // Don't reapply the effects to a highlighted text.\n    // If the target is invisible, we still need to notify the public widget\n    // that a highlight was detected (It's needed anyway, so the public widget\n    // can link the element to its observer, which tracks size changes and\n    // adapts the highlights accordingly).\n    if (topTextEl.querySelector(\".o_text_highlight_item\") || !isVisible(topTextEl)) {\n        return endHighlightUpdate();\n    }\n    const style = window.getComputedStyle(topTextEl);\n    if (!style.getPropertyValue(\"--text-highlight-width\")) {\n        // The default value for `--text-highlight-width` is 0.1em.\n        topTextEl.style.setProperty(\n            \"--text-highlight-width\",\n            `${Math.round(parseFloat(style.fontSize) * 0.1)}px`\n        );\n    }\n    const lines = [];\n    let lineIndex = 0;\n    const nodeIsBR = (node) => node.nodeName === \"BR\";\n    const isRTL = (el) => window.getComputedStyle(el).direction === \"rtl\";\n\n    [...topTextEl.childNodes].forEach((child) => {\n        // We consider `<br/>` tags as full text lines to ease\n        // excluding them when the highlight is applied on the DOM.\n        if (nodeIsBR(child)) {\n            lines[++lineIndex] = [child];\n            return lineIndex++;\n        }\n        const textLines = splitNodeLines(child);\n\n        // Special case: The text lines detection code in `splitNodeLines()`\n        // (based on `getClientRects()`) can't handle a situation when a line\n        // exactly ends with the current child node. We need to handle this\n        // manually by checking if the current child node is the last one in\n        // the line (taking into account the RTL direction).\n        // TODO: Improve this.\n        let lastNodeInLine = false;\n        if (child.textContent && child.nextSibling?.textContent) {\n            const range = document.createRange();\n            const lastCurrentText = selectAllTextNodes(child).at(-1);\n            range.setStart(lastCurrentText, lastCurrentText.length - 1);\n            range.setEnd(lastCurrentText, lastCurrentText.length);\n            // Get the \"END\" position of the last text node in current child.\n            const currentEnd = range.getBoundingClientRect()[isRTL(topTextEl) ? \"left\" : \"right\"];\n            const firstnextText = selectAllTextNodes(child.nextSibling)[0];\n            range.setStart(firstnextText, 0);\n            range.setEnd(firstnextText, 1);\n            // Get the \"START\" position of the first text node in the next\n            // sibling.\n            const nextStart = range.getBoundingClientRect()[isRTL(topTextEl) ? \"right\" : \"left\"];\n            // The next sibling starts before the end of the current node\n            // => Line break detected.\n            lastNodeInLine = nextStart + 1 < currentEnd;\n        }\n\n        // for each text line detected, we add the content as new\n        // line and adjust the line index accordingly.\n        textLines.map((node, i, { length }) => {\n            if (!lines[lineIndex]) {\n                lines[lineIndex] = [];\n            }\n            lines[lineIndex].push(node);\n            if (i !== length - 1 || lastNodeInLine) {\n                lineIndex++;\n            }\n        });\n    });\n    topTextEl.replaceChildren(\n        ...lines.map((textLine) =>\n            // First we add text content to be able to build svg paths\n            // correctly (`<br/>` tags are excluded).\n            nodeIsBR(textLine[0]) ? textLine[0] : createHighlightContainer(textLine)\n        )\n    );\n    // Build and set highlight SVGs.\n    [...topTextEl.querySelectorAll(\".o_text_highlight_item\")].forEach((container) => {\n        container.append(\n            drawTextHighlightSVG(container, highlightID || getCurrentTextHighlight(topTextEl))\n        );\n    });\n    endHighlightUpdate();\n}\n\n/**\n * Used to rollback the @see applyTextHighlight behaviour.\n *\n * @param {HTMLElement} topTextEl\n */\nexport function removeTextHighlight(topTextEl) {\n    topTextEl.dispatchEvent(new Event(\"text_highlight_remove\", { bubbles: true }));\n    // Simply replace every `<span class=\"o_text_highlight_item\">\n    // textNode1 [textNode2,...]<svg .../></span>` by `textNode1\n    // [textNode2,...]`.\n    [...topTextEl.querySelectorAll(\".o_text_highlight_item\")].forEach((unit) => {\n        unit.after(...[...unit.childNodes].filter((node) => node.tagName !== \"svg\"));\n        unit.remove();\n    });\n    // Prevents incorrect text lines detection on the next updates.\n    let child = topTextEl.firstElementChild;\n    while (child) {\n        const next = child.nextElementSibling;\n        // Merge identical elements.\n        if (next && next === child.nextSibling && child.cloneNode().isEqualNode(next.cloneNode())) {\n            child.replaceChildren(...child.childNodes, ...next.childNodes);\n            next.remove();\n        } else {\n            child = next;\n        }\n    }\n    topTextEl.normalize();\n}\n\n/**\n * Used to wrap text nodes in a single \"text highlight\" unit.\n *\n * @param {Node[]} nodes\n * @returns {HTMLElement} The one line text element that should contain\n * the highlight SVG.\n */\nfunction createHighlightContainer(nodes) {\n    const highlightContainer = document.createElement(\"span\");\n    highlightContainer.className = \"o_text_highlight_item\";\n    highlightContainer.append(...nodes);\n    return highlightContainer;\n}\n\n/**\n * Used to get the current text highlight id from the top `.o_text_highlight`\n * container class.\n *\n * @param {HTMLElement} el\n * @returns {String}\n */\nexport function getCurrentTextHighlight(el) {\n    const topTextEl = el.closest(\".o_text_highlight\");\n    const match = topTextEl?.className.match(/o_text_highlight_(?<value>[\\w]+)/);\n    let highlight = \"\";\n    if (match) {\n        highlight = match.groups.value;\n    }\n    return highlight;\n}\n\n/**\n * Returns a list of detected lines in the content of a text node.\n *\n * @param {Node} node\n */\nfunction splitNodeLines(node) {\n    const isTextContainer =\n        node.childNodes.length === 1 && node.firstChild.nodeType === Node.TEXT_NODE;\n    if (node.nodeType !== Node.TEXT_NODE && !isTextContainer) {\n        return [node];\n    }\n    const text = node.textContent;\n    const textNode = isTextContainer ? node.firstChild : node;\n    const lines = [];\n    const range = document.createRange();\n    let i = -1;\n    while (++i < text.length) {\n        range.setStart(textNode, 0);\n        range.setEnd(textNode, i + 1);\n        const clientRects = range.getClientRects().length || 1;\n        const lineIndex = clientRects - 1;\n        const currentText = lines[lineIndex];\n        lines[lineIndex] = (currentText || \"\") + text.charAt(i);\n    }\n    // Return the original node when no lines were detected.\n    if (lines.length === 1) {\n        return [node];\n    }\n    return lines.map((line) => {\n        if (isTextContainer) {\n            const wrapper = node.cloneNode();\n            wrapper.appendChild(document.createTextNode(line));\n            return wrapper;\n        }\n        return document.createTextNode(line);\n    });\n}\n\n/**\n * Get all text nodes inside a parent DOM element.\n *\n * @param {Node} topNode\n * @returns {Node[]} List of text \"childNodes\" or the element itself\n * (if it's a text node).\n */\nexport function selectAllTextNodes(topNode) {\n    const textNodes = [];\n    const selectTextNodes = (node) => {\n        if (node.nodeType === Node.TEXT_NODE) {\n            textNodes.push(node);\n        } else {\n            [...node.childNodes].forEach((child) => selectTextNodes(child));\n        }\n    };\n    selectTextNodes(topNode);\n    return textNodes;\n}\n\n/**\n * Used to get the node of a text element in which a selection starts/ends.\n *\n * @param {HTMLElement} textEl The parent text element.\n * @param {Number} offset The selection offset in parent element.\n * @returns {[Node, Number]} The node found in the cursor position\n * and the new offset compared to that node.\n */\nexport function getOffsetNode(textEl, offset) {\n    let index = 0,\n        offsetNode;\n    for (const node of selectAllTextNodes(textEl)) {\n        const stepLength = node.textContent.length;\n        if (index + stepLength < offset - 1) {\n            index += stepLength;\n        } else {\n            offsetNode = node;\n            break;\n        }\n    }\n    return [offsetNode, offset - index];\n}\n", "import { descendants } from \"@html_editor/utils/dom_traversal\";\nimport { memoize } from \"@web/core/utils/functions\";\n\nexport const textHighlightFactory = {\n    underline: (params) => drawPath({ ...params, mode: \"line\" }),\n    freehand_1: (params) => {\n        const template = (w, h) => [\n            `M 0,${h * 1.1} C ${w / 8},${h * 1.05} ${w / 4},${h} ${w},${h}`,\n        ];\n        return drawPath({ ...params, mode: \"free\", template });\n    },\n    freehand_2: (params) => {\n        const template = (w, h) => [\n            `M181.27 13.873c-.451-1.976-.993-3.421-1.072-4.9-.125-2.214-.61-4.856.384-6.539.756-1.287 3.636-2.055 5.443-1.852 3.455.395 7.001 1.231` +\n                ` 10.14 2.676 1.728.802 3.174 3.06 3.817 4.98.237.712-1.953 2.824-3.399 3.4-2.766 1.095-5.748 1.75-8.706 2.179-2.394.339-4.879.068-6.584.068l-.023-.012ZM8.416 3.90` +\n                `2c3.862.26 7.78.249 11.574.926 1.65.294 3.027 2.033 4.54 3.117-1.095 1.186-1.987 2.982-3.343 3.456a67.118 67.118 0 0 1-11.19 2.823c-3.253.53-6.494-.339-8.617-2.98` +\n                `1C.364 9.978-.302 7.686.138 6.263c.361-1.152 2.54-2 4.077-2.44 1.287-.372 2.789.046 4.2.102v-.023Zm154.267 9.983c-4.291-.305-8.153-1.58-9.915-5.623-.745-1.694-.39` +\n                `5-4.382.474-6.121 1.073-2.168 3.512-1.965 5.613-1.005 2.541 1.174 5.251 2.157 7.509 3.76 1.502 1.073 3.557 3.445 3.207 4.574-.519 1.694-2.857 2.913-4.562 4.133-.5` +\n                `76.406-1.592.203-2.326.282ZM72.58 17.42c-2.733-1.807-5.307-3.004-7.137-4.913-.892-.925-.892-3.376-.361-4.776.407-1.05 2.304-2.112 3.546-2.135 3.602-.056 7.238.215` +\n                ` 10.818.723 3.828.542 5.15 4.1 2.213 6.539-2.439 2.021-5.77 2.958-9.079 4.562Zm30.795-.802c-2.507-1.536-5.228-2.823-7.397-4.743-.925-.813-1.377-3.297-.813-4.359.6` +\n                `78-1.265 2.677-2.507 4.11-2.518 3.016-.023 6.155.418 9.001 1.389 1.412.485 3.173 2.552 3.185 3.907 0 1.57-1.423 3.557-2.801 4.619-1.152.892-3.139.711-4.743 1.005-` +\n                `.181.226-.35.463-.531.689l-.011.01Zm-59.704-1.457c-2.066-1.163-4.788-2.224-6.82-4.054-.915-.824-1.04-3.478-.407-4.765.486-.983 2.722-1.559 4.156-1.502 2.676.101 5` +\n                `.398.542 7.95 1.332 1.457.452 3.523 1.75 3.681 2.891.18 1.31-1.13 3.309-2.383 4.201-1.411 1.005-3.466 1.118-6.188 1.886l.011.011Zm88.489-1.863c-2.643-1.48-5.567-2` +\n                `.62-7.803-4.574-1.005-.88-1.31-3.692-.667-5.002.509-1.04 2.982-1.615 4.529-1.513 2.032.135 4.054 1.027 6.007 1.772 2.485.95 5.026 2.236 4.382 5.455-.644 3.15-3.49` +\n                ` 2.947-5.963 3.004-.169.293-.327.575-.496.87l.011-.012Z`,\n        ];\n        return drawPath({\n            ...params,\n            mode: \"fill\",\n            template,\n            SVGWidth: 200,\n            SVGHeight: 18,\n            position: \"bottom\",\n        });\n    },\n    freehand_3: (params) => {\n        const template = (w, h) => [\n            `M189.705 18.285c-3.99.994-7.968 2.015-11.958 2.972-1.415.344-2.926 1.008-4.278.727-6.305-1.327-12.568-3.036-18.874-4.376-1.995-.42-4.2` +\n                `46-.701-6.133-.038-5.867 2.067-11.54 2.386-17.374-.242-1.491-.676-3.56-.421-5.125.217-5.523 2.22-10.789 3.597-16.494.127-1.64-.995-4.675-.038-6.584 1.148-6.102 3.` +\n                `789-12.01 4.414-18.198.434-.998-.638-2.681-.638-3.754-.115-6.852 3.355-13.404 2.858-20.043-1.008-1.5-.867-4.02-.6-5.608.307-7.528 4.35-14.842 5.702-22.07-.638-2.1` +\n                `44-1.875-3.71-.37-5.394 1.046-4.622 3.89-9.565 6.327-15.367 4.286C6.338 20.989.505 13.067.022 5.949-.085 4.38.194 1.753.955 1.332 2.253.617 4.537.553 5.588 1.51 7` +\n                `.55 3.27 9.18 5.77 10.52 8.296c2.82 5.269 4.15 5.766 8.504 2.156 1.555-1.288 2.992-2.768 4.396-4.286 4.022-4.311 7.143-4.465 11.26-.472 7.068 6.837 8.226 7.067 15` +\n                `.979 1.314 3.721-2.755 7.206-2.653 10.627.128 4.987 4.056 9.791 4.49 14.853.191 2.702-2.296 5.78-2.296 8.45.115 4.29 3.89 8.45 3.33 12.719.166.847-.638 1.705-1.26` +\n                `3 2.552-1.914 3.035-2.309 6.048-2.5 9.019.166 3.453 3.087 7.12 3.15 10.616.472 4.107-3.138 7.85-3.342 12.16-.306 3.668 2.59 7.83 1.964 11.594-.255 3.935-2.322 7.6` +\n                `67-2.488 11.409.408.365.28.794.612 1.213.65 6.799.549 13.522 3.394 20.428.779 1.887-.715 3.914-1.034 5.899-1.148 3.313-.192 6.659-.358 9.941 0 1.993.23 4.354.905 ` +\n                `5.737 2.436 1.308 1.429 2.113 4.235 2.123 6.442.022 3.023-2.424 3.431-4.472 3.597-1.887.153-3.796.038-5.695.038-.053-.216-.106-.446-.16-.663l.032-.025Z`,\n        ];\n        return drawPath({\n            ...params,\n            mode: \"fill\",\n            template,\n            SVGWidth: 200,\n            SVGHeight: 24,\n            position: \"bottom\",\n        });\n    },\n    double: (params) => {\n        const template = (w, h) => [`M 0,${h * 0.9} h ${w}`, `M 0,${h * 1.1} h ${w}`];\n        return drawPath({ ...params, mode: \"free\", template });\n    },\n    wavy: (params) => {\n        const template = (w, h) => [\n            `c ${w / 4},0 ${w / 4},-${h / 2} ${w / 2},-${h / 2}` +\n                `c ${w / 4},0 ${w / 4},${h / 2} ${w / 2},${h / 2}`,\n        ];\n        return drawPath({ ...params, mode: \"pattern\", template });\n    },\n    circle_1: (params) => {\n        const template = (w, h) => [\n            `M ${w / 2.88},${h / 1.1} C ${w / 1.1},${h / 1.05} ${w * 1.05},${h / 1.1} ${\n                w * 1.023\n            },${h / 2.32}` +\n                `C ${w}, ${h / 14.6} ${w / 1.411},0 ${w / 2},0 S -2,${h / 14.6} -2,${h / 2.2}` +\n                `S ${w / 4.24},${h} ${w / 1.36},${h * 1.04}`,\n        ];\n        return drawPath({ ...params, mode: \"free\", template });\n    },\n    circle_2: (params) => {\n        const template = (w, h) => [\n            `M112.58 21.164h18.516c-.478-.176-1.722-.64-2.967-1.105.101-.401.214-.803.315-1.192 12.255 2.912 24.561 5.573 36.716 8.823 5.896 1.582 ` +\n                `11.628 3.967 17.171 6.527 10.433 4.832 14.418 14.22 16.479 24.739.377 1.92.566 3.878.83 5.823 2.212 15.94-5.858 23.986-21.595 33.813-.993.615-2.288.79-3.181 1.494` +\n                `-14.229 11.308-31.412 14.32-48.608 17.107-29.01 4.694-57.431 2.209-84.91-8.372-8.145-3.138-16.164-6.853-23.706-11.22C6.176 90.986 1.16 80.053.193 67.25c-1.798-23.` +\n                `809 9.025-42.485 30.356-53.304C44.678 6.793 59.8 3.367 75.45 2.375 90.583 1.42 105.793.379 120.927.78c16.089.427 32.041 3.05 46.911 9.84 2.074.941 3.67 2.912 4.91` +\n                `5 5.083-9.73-1.443-19.433-2.987-29.175-4.305-4.89-.665-9.842-1.067-14.77-1.33-23.82-1.28-47.376.514-70.391 7.003a133.771 133.771 0 0 0-22.639 8.648c-17.9 8.786-27` +\n                `.616 26.935-25.567 46.364.666 6.263 3.507 11.133 9.05 14.308 26.862 15.401 55.748 21.965 86.645 19.819 15.561-1.08 31.01-2.787 45.767-8.284 11.099-4.142 21.658-9.` +\n                `25 30.595-17.195 9.779-8.698 11.715-18.55 5.669-30.249-1.131-2.196-3.256-4.079-5.33-5.56-7.981-5.736-17.773-7.48-26.459-11.534-13.249-6.175-27.541-6.916-41.343-10` +\n                `.167-.817-.188-1.571-.64-2.35-.966.037-.364.088-.728.125-1.092Z`,\n        ];\n        return drawPath({ ...params, mode: \"fill\", template, SVGWidth: 200, SVGHeight: 120 });\n    },\n    circle_3: (params) => {\n        const template = (w, h) => [\n            `M78.653 89.204c-14.815 0-29.403-1.096-43.354-4.698-5.227-1.346-10.407-3.069-14.997-5.199-22.996-10.649-27.04-28.502-9.135-43.035 12.18` +\n                `-9.866 26.813-18.04 43.355-24.242C88.515-.718 124.19-3.725 161.228 4.889c13.224 3.07 24.449 8.268 31.902 16.662 8.862 9.992 9.453 20.422 0 30.068-5.817 5.889-13.2` +\n                `24 11.37-21.359 15.786-27.176 14.752-58.579 21.518-93.072 21.8h-.046Zm3.5-4.228c4.408-.282 11.725-.47 18.86-1.253 30.357-3.351 57.579-11.432 79.211-26.842 5.362-3` +\n                `.82 10.134-8.832 12.27-13.875 2.545-5.982 5.817-13.311-6.226-17.352-.454-.156-.727-.563-1.045-.845-10.771-9.146-25.086-14.157-41.719-15.348-39.674-2.85-76.62 3.19` +\n                `5-109.66 18.762-8.18 3.883-15.497 9.177-21.359 14.752-9.725 9.27-8.044 19.889 3.727 28.032 4.862 3.383 10.997 6.233 17.269 8.237 14.406 4.605 30.04 5.544 48.58 5.` +\n                `763l.092-.03ZM130.37 3.573c-24.813-1.88-48.263 1.378-70.44 9.146 22.814-5.481 46.172-9.02 70.44-9.146Z`,\n        ];\n        return drawPath({ ...params, mode: \"fill\", template, SVGWidth: 200, SVGHeight: 90 });\n    },\n    over_underline: (params) => {\n        const template = (w, h) => [`M 0,0 h ${w}`, `M 0,${h} h ${w}`];\n        return drawPath({ ...params, mode: \"free\", template });\n    },\n    scribble_1: (params) => {\n        const template = (w, h) => [\n            `M ${w / 2},${h * 0.9} c ${w / 16},0 ${w},1 ${w / 5},1 c 2,0 -${w / 10},-2 -${\n                w / 2\n            },-1` +\n                `c -${w / 20},0 -${w / 5},2 -${w / 5},4 c -2,0 ${w / 10},-1 ${w / 2},${h / 16}` +\n                `c ${w / 25},0 ${w / 10},0 ${w / 5},1 c 0,0 -${w / 10},1 -${w / 8},1` +\n                `c -${w / 40},0 -${w / 16},0 -${w / 4},${h / 22}`,\n        ];\n        return drawPath({ ...params, mode: \"free\", template });\n    },\n    scribble_2: (params) => {\n        const template = (w, h) => [\n            `M200 3.985c-.228-.332-3.773.541-.01-.006-.811-.037-6.705-1.442-9.978-1.706-1.473.194-2.907.534-4.351.818-1.398.27-2.937.985-4.144.756-` +\n                `9.56-1.782-19.3-1.089-28.955-1.31C118.932 1.767 85.301.942 51.671.45c-13.732-.201-27.492.333-41.233.665C6.561 1.212 3.026 2.363.84 4.838.09 5.684-.262 7.126.223 7` +\n                `.993c.313.554 2.518.79 3.839.728 2.47-.118 4.922-.548 8.096-.936-.96 1.227-1.568 1.865-1.986 2.558-1.368 2.302.029 4 3.203 4.083 24.716.666 49.424 1.4 74.15 2.01 ` +\n                `21.087.52 42.145.34 63.146-1.414 4.495-.374 8.999-.644 14.425-1.026-3.117-1.629-4.723-3.521-8.39-3.535-17.999-.077-36.016-.07-54.005-.534-22.246-.576-44.464-1.58-` +\n                `66.7-2.406-.276-.007-.551-.097-.817-.471 1.016 0 2.033-.021 3.04 0 21.961.506 43.913.998 65.864 1.539 25.249.624 50.47.367 75.642-1.144 5.892-.354 11.765-.93 17.6` +\n                `19-1.54.788-.082 1.416-.99 2.651-1.92Z`,\n        ];\n        return drawPath({\n            ...params,\n            mode: \"fill\",\n            template,\n            SVGWidth: 200,\n            SVGHeight: 17,\n            position: \"bottom\",\n        });\n    },\n    scribble_3: (params) => {\n        const template = (w, h) => [\n            `M133.953 15.961c7.87.502 15.751.975 23.611 1.522 2.027.141 4.055.44 5.999.79 4.118.727 7.202 4.977 2.53 6.707.606.293 1.181.564 1.902.` +\n                `908-8.477 2.069-17.267 2.65-26.203 2.818-19.023.361-38.056.603-57.068 1.088-13.807.355-27.572 1.06-41.369 1.545-3.23.113-6.532.096-9.73-.147-1.548-.118-3.492-.721` +\n                `-4.234-1.42-.93-.88-1.484-2.199-.93-3.1.397-.655 2.812-1.263 4.41-1.33 6.397-.277 12.825-.333 19.243-.474 26.976-.592 53.942-1.156 80.919-1.804 3.742-.09 7.452-.5` +\n                `92 11.173-.908 0-.174-.01-.35-.021-.524-2.717-.197-5.435-.53-8.163-.575-21.865-.383-43.741-1.009-65.607-.936-11.34.04-22.65 1.432-34 2.047-6.898.377-13.88.732-20.` +\n                `779.569-7.044-.17-9.406-3.568-5.34-6.742 3.428-2.677 7.567-4.391 13.984-4.757 16.441-.93 32.798-2.26 49.219-3.27 14.162-.868 28.366-1.516 42.549-2.266.586-.034 1.` +\n                `15-.147 1.641-.45-5.006 0-10.023-.012-15.029.01-1.077 0-2.154.186-3.24.192-18.793.18-37.596.355-56.389.507-10.672.085-21.343.13-32.014.153a65.89 65.89 0 0 1-6.167` +\n                `-.277C1.787 5.555-.02 4.247 0 2.59 0 1.384.89.72 3.293.742c5.874.056 11.748.124 17.622.09C41.045.708 61.186.409 81.317.42c28.408.012 56.827.158 85.225.417 8.686.0` +\n                `8 17.35.7 26.015 1.122 3.23.158 5.832.902 7.024 2.678 1.055 1.572.125 2.21-2.875 1.95a30.51 30.51 0 0 0-2.268-.107c-.397 0-.805.073-1.557.146.721.451 1.306.767 1.` +\n                `777 1.128 2.926 2.238 1.641 4.013-3.272 4.369-13.483.958-26.966 1.91-40.459 2.767-3.334.214-6.752 0-10.118.085-2.31.062-4.609.299-6.909.462l.042.519.011.005Z`,\n        ];\n        return drawPath({\n            ...params,\n            mode: \"fill\",\n            template,\n            SVGWidth: 200,\n            SVGHeight: 32,\n            position: \"bottom\",\n        });\n    },\n    scribble_4: (params) => {\n        const template = (w, h) => [\n            `M96.414 17.157c1.34-2.173 2.462-4.075 3.649-5.944 2.117-3.335 5.528-4.302 9.372-2.694 3.962 1.651 4.89 3.575 3.908 8.073-.205.967-.388` +\n                ` 1.934-.022 3.118 1.513-3.075 3.013-6.15 4.557-9.203 1.306-2.586 4.297-3.433 7.859-2.195 2.765.968 4.395 2.706 3.564 5.922-.529 2.054-1.005 4.118-.918 6.487.463-.` +\n                `859 1.015-1.685 1.371-2.586 1.447-3.673 3.002-7.324 4.2-11.083.896-2.792 2.192-3.955 5.323-3.564 4.772.598 7.049 3.412 5.84 7.986-.626 2.38-1.22 4.77-1.144 7.486.` +\n                `745-1.358 1.544-2.683 2.213-4.074a138.72 138.72 0 0 0 2.926-6.487c2.376-5.66 3.12-4.704 8.724-3.618 3.552.685 5.063 4.031 4.34 7.997-.616 3.423-1.166 6.856-1.749 ` +\n                `10.29l.95.358c.993-2.151 2.062-4.27 2.958-6.454.594-1.456.886-3.042 1.403-4.53 2.43-6.911 2.43-6.813 9.566-5.542.928.163 2.656-.967 3.078-1.923.992-2.26 2.332-2.7` +\n                `16 4.523-2.097 4.297 1.206 8.659 2.184 12.945 3.444 2.796.826 4.319 2.988 4.135 5.889-.173 2.684-.961 5.324-1.274 8.008-.734 6.4-1.361 12.799-2.019 19.21-.065.673` +\n                `.043 1.38-.097 2.031-.551 2.477-.41 5.465-3.476 6.421-2.311.717-6.489-2.194-7.644-5.03-.206-.5-.357-1.01-.918-2.63-1.22 3.27-2.073 5.629-2.991 7.965-2.095 5.345-3` +\n                `.66 5.954-8.874 3.705-.853-.37-2.354-.783-2.786-.359-3.163 3.075-5.971 1.217-8.853-.358-.378-.207-.81-.316-1.188-.457-5.851 7.65-12.502 4.596-15.061-3.944-1.543 3` +\n                `.042-2.883 5.726-4.265 8.399-3.357 6.53-7.783 6.975-12.47 1.25-.485-.587-.992-1.152-1.511-1.75-5.647 6.715-12.848 2.293-15.19-6.063-1.253 2.25-2.257 3.88-3.099 5.` +\n                `596-1.285 2.64-2.883 4.65-6.23 3.868-3.498-.826-6.532-4.085-6.65-7.225-.054-1.424 0-2.847-.475-4.433-1.393 2.879-2.71 5.802-4.19 8.637-3.228 6.204-6.067 6.824-11.` +\n                `67 2.912-.962-.673-2.57-.988-3.704-.728-3.681.837-6.272-.619-8.626-3.248-.691-.783-2.084-1.771-2.807-1.543-4.243 1.347-6.91-.641-9.166-3.836-.378-.543-.8-1.053-1.` +\n                `555-2.031-1.08 2.194-2.008 4.041-2.915 5.9-2.397 4.943-5.528 5.932-10.02 2.835-2.008-1.38-3.713-2.118-6.37-1.738-5.117.728-8.54-3.444-7.762-8.649.227-1.521.378-3.` +\n                `064-.086-4.9-.853 1.369-1.793 2.684-2.548 4.107-2.775 5.259-5.301 5.856-10.074 2.206-.971-.75-1.803-1.674-2.86-2.673-.67.271-1.598 1.043-2.257.858-2.71-.771-5.625` +\n                `-1.423-7.838-3.01-.842-.608-.378-3.683.108-5.465 2.008-7.41 4.232-14.755 6.413-22.11.572-1.945 1.166-3.901 1.943-5.77 1.89-4.52 5.02-5.454 9.145-2.89 1.144.706 2.` +\n                `408 1.217 3.552 1.923 2.364 1.456 4.696 2.988 7.439 4.737C32.423 7.14 37.444 6.64 42.82 10.41c2.602-2.107 1.803-7.17 6.748-6.323 3.369.587 6.478 1.217 7.439 4.878` +\n                ` 2.289-2.281 4.221-5.693 6.877-6.42 2.624-.718 5.992 1.26 9.599 2.216-.044.054.636-.565.96-1.348 1.048-2.499 2.883-3.4 5.42-2.825 2.775.62 5.474 1.304 6.284 4.76.` +\n                `216.89 1.285 2.042 2.159 2.248 7.58 1.793 7.6 1.739 8.108 9.55v.012Z`,\n        ];\n        return drawPath({ ...params, mode: \"fill\", template, SVGWidth: 200, SVGHeight: 61 });\n    },\n    jagged: (params) => {\n        const template = (w, h) => [\n            `q ${(4 * w) / 3} -${(2 * w) / 3} ${(2 * w) / 3} 0` +\n                `c -${w / 3} ${w / 3} -${w / 3} ${w / 3} ${w / 3} 0`,\n        ];\n        return drawPath({ ...params, mode: \"pattern\", template });\n    },\n    cross: (params) => {\n        const template = (w, h) => [`M 0,0 L ${w},${h}`, `M 0,${h} L ${w},0`];\n        return drawPath({ ...params, mode: \"free\", template });\n    },\n    diagonal: (params) => {\n        const template = (w, h) => [`M 0,${h} L${w},0`];\n        return drawPath({ ...params, mode: \"free\", template });\n    },\n    strikethrough: (params) => drawPath({ ...params, mode: \"line\", position: \"center\" }),\n    bold: (params) => {\n        const template = (w, h) => [\n            `M136.604 41.568c5.373.513 10.746 1.047 16.12 1.479 14.437 1.13 29.327 4.047 42.858-4.294 4.92-3.04 2.346-13.56-2.687-13.395-.825.02-1.` +\n                `635.062-2.46.082.858-3.677-.34-8.3-3.545-9.41 2.655.062 5.309.104 7.963.165 6.863.185 6.863-14.176 0-14.36A1958.994 1958.994 0 0 0 5.263 5.778C-.4 6.169-2.392 18.` +\n                `455 3.84 19.893c9.727 2.24 19.454 4.335 29.214 6.307-1.085 1.09-1.764 2.671-2.023 4.356-.615.061-1.214.102-1.83.164-6.748.74-6.959 14.587 0 14.361l107.42-3.513h-.` +\n                `016Z`,\n        ];\n        return drawPath({ ...params, mode: \"fill\", template, SVGWidth: 200, SVGHeight: 46 });\n    },\n    bold_1: (params) => {\n        const template = (w, h) => [\n            `M190.276 34.01c5.618-.25 7.136-6.526 4.444-9.755.037-.25.055-.5.072-.749 7.046-.949 7.01-11.752-.523-11.553-.796.017-1.59.017-2.403.05` +\n                `C196.78 9.573 195.931.8 189.264.983L13.784 5.678c-7.226.2-7.497 9.422-1.499 11.32-2.186 0-4.354 0-6.54-.017-7.696-.05-7.624 11.286 0 11.635 8.22.383 16.423.733 24` +\n                `.643 1.016l-7.823.35c-7.624.349-7.678 11.985 0 11.635 55.915-2.53 111.813-5.077 167.729-7.607h-.018Z`,\n        ];\n        return drawPath({ ...params, mode: \"fill\", template, SVGWidth: 200, SVGHeight: 42 });\n    },\n    bold_2: (params) => {\n        const template = (w, h) => [\n            `M193.221 20.193c.555 1.245.863 2.005 1.22 2.734 1.399 2.84 2.758 5.757 1.607 9.509-1.21 3.95-3.651 4.208-6.072 4.314-5.059.212-10.129.` +\n                `152-15.178.592-15.873 1.367-31.737 3.585-47.619 4.238-19.921.82-39.862.638-59.802.486-13.938-.106-27.887-.88-41.825-1.428-4.018-.151-8.046-.47-12.064-.896-2.758-.` +\n                `304-4.772-2.46-6.21-6.182-.645-1.656-1.756-2.993-2.798-4.177-2.768-3.13-5.06-6.38-3.899-12.502C.9 15.226.393 13.16.165 11.307c-.715-5.818.903-9.524 4.722-9.646 10` +\n                `.218-.35 20.437-.38 30.655-.577C51.236.78 66.94-.04 82.635.264c14.652.273 29.296 1.655 43.948 2.643 19.822 1.336 39.643 2.02 59.455-.426.923-.121 1.835-.5 2.758-.` +\n                `622 1.329-.183 2.688-.456 4.008-.274 3.829.501 7.073 5.666 7.192 11.21.09 4.466-1.418 6.213-6.775 7.428v-.03Z`,\n        ];\n        return drawPath({ ...params, mode: \"fill\", template, SVGWidth: 200, SVGHeight: 43 });\n    },\n};\n\n/**\n * Divides the content of a text container into multiple\n * `.o_text_highlight_item` units, and applies the highlight\n * on each unit.\n *\n * @param {HTMLElement} highlightEl\n * @param {String} highlightID\n */\nexport function makeHighlightSvgs(highlightEl, highlightID) {\n    const style = window.getComputedStyle(highlightEl);\n    if (!style.getPropertyValue(\"--text-highlight-width\")) {\n        // The default value for `--text-highlight-width` is 0.1em.\n        highlightEl.style.setProperty(\n            \"--text-highlight-width\",\n            `${Math.round(parseFloat(style.fontSize) * 0.1)}px`\n        );\n    }\n\n    const textNodes = descendants(highlightEl).filter((el) => el.nodeType === Node.TEXT_NODE);\n    const rects = textNodes.map((node) => getTextnodeRects(node, 0, node.length)).flat();\n    const finalRects = rectToBatch(rects).map((rects) => getBiggestBoxFromBoxes(rects));\n\n    const sizePerChar = memoize(() => {\n        const numberOfChar = textNodes.reduce(\n            (acc, node) => acc + node.textContent.replaceAll(/\\s/g, \"\").length,\n            0\n        );\n        return finalRects.reduce((acc, rect) => acc + rect.width, 0) / numberOfChar;\n    });\n    const numberOfCharPerWidth = memoize((width) => Math.round(width / sizePerChar()));\n\n    const containerRect = highlightEl.getBoundingClientRect();\n    // Note: We cannot use `getClientRects()` as we want to be able to draw\n    // text highlights in the snippet/page dialogs where iframe is scaled.\n    const inPreviewIframe =\n        highlightEl.ownerDocument.documentElement.classList.contains(\"o_add_snippets_preview\");\n    const scale = inPreviewIframe ? highlightEl.offsetWidth / containerRect.width : 1;\n    const firstRect = highlightEl.getClientRects()[0];\n    const svgs = [];\n    for (const rects of finalRects) {\n        const svg = makeHighlightSvg(highlightID || getCurrentTextHighlight(highlightEl), {\n            width: rects.width * scale,\n            height: rects.height * scale,\n            numberOfCharPerWidth,\n        });\n        svgs.push(svg);\n        const spanOffsetX = firstRect.x - containerRect.x;\n        const spanOffsetY = firstRect.y - containerRect.y;\n        svg.style.left = `${(rects.x - containerRect.x - spanOffsetX) * scale}px`;\n        svg.style.top = `${(rects.y - containerRect.y - spanOffsetY) * scale}px`;\n        svg.style.bottom = `0px`;\n        svg.style.right = `0px`;\n    }\n    return svgs;\n}\nexport function applyTextHighlight(highlightEl, highlightID) {\n    const svgs = makeHighlightSvgs(highlightEl, highlightID);\n    for (const svg of svgs) {\n        highlightEl.appendChild(svg);\n    }\n}\n\n/**\n * Deactivates a text highlight effect by removing its SVGs.\n *\n * @param {HTMLElement} highlightEl\n */\nexport function removeTextHighlight(highlightEl) {\n    for (const svg of highlightEl.querySelectorAll(\":scope svg\")) {\n        svg.remove();\n    }\n}\n\n/**\n * Returns a new highlight SVG adapted to the text container.\n *\n * @param {HTMLElement} textEl\n * @param {String} highlightID\n */\nexport function makeHighlightSvg(highlightID, params) {\n    const svg = document.createElementNS(\"http://www.w3.org/2000/svg\", \"svg\");\n    svg.setAttribute(\"fill\", \"none\");\n    svg.classList.add(\n        \"o_text_highlight_svg\",\n        // Identifies DOM content that should not be merged by the editor, even\n        // on identical parents.\n        \"o_content_no_merge\",\n        \"position-absolute\",\n        \"overflow-visible\",\n        \"pe-none\"\n    );\n    textHighlightFactory[highlightID](params).forEach((pathEl) => {\n        // pathEl.classList.add(`o_text_highlight_path_${highlightID}`);\n        svg.appendChild(pathEl);\n    });\n    return svg;\n}\n\n/**\n * Draws one or many SVG paths using templates of path shape commands.\n *\n * @param {HTMLElement} textEl\n * @param {String} options.mode Specifies how to draw the path:\n * - \"pattern\": repeat the template along the horizontal axis.\n * - \"line\": draw a simple line (we specify the width & position).\n * - \"free\": draw the path shape using the template only.\n * - \"fill\": used for irregular shapes that do not follow the \"stroke\" design.\n * @param {Function} options.template Returns a list of SVG path\n * commands adapted to the container's size.\n * @returns {String[]}\n */\nfunction drawPath(options) {\n    const { width, height, numberOfCharPerWidth } = options;\n    const yStart = options.position === \"center\" ? height / 2 : height;\n\n    switch (options.mode) {\n        case \"pattern\": {\n            let i = 0;\n            const arr = [];\n            const w = width / numberOfCharPerWidth(width),\n                h = height * 0.2;\n            while (i < numberOfCharPerWidth(width)) {\n                arr.push(options.template(w, h));\n                i++;\n            }\n            return buildPath([`M 0,${yStart} ${arr.join(\" \")}`], options);\n        }\n        case \"line\": {\n            return buildPath([`M 0,${yStart} h ${width}`], options);\n        }\n    }\n    return buildPath(options.template(width, height), options);\n}\n/**\n * Used to build the SVG <path/>, it should mainly adapt it to take into\n * consideration some cases where the shape is a \"filled path\" instead\n * of a single line stroke.\n *\n * @param {String[]} templates\n * @param {Object} options\n * @returns {Element[]}\n */\nfunction buildPath(templates, options) {\n    return templates.map((d) => {\n        const path = document.createElementNS(\"http://www.w3.org/2000/svg\", \"path\");\n        path.setAttribute(\"stroke-width\", \"var(--text-highlight-width)\");\n        path.setAttribute(\"stroke\", \"var(--text-highlight-color)\");\n        path.setAttribute(\"stroke-linecap\", \"round\");\n        if (options.mode === \"fill\") {\n            const wScale = options.width / options.SVGWidth;\n            let hScale = options.height / options.SVGHeight;\n            const transforms = [];\n            if (options.position === \"bottom\") {\n                hScale *= 0.3;\n                transforms.push(`translate(0 ${options.height * 0.8})`);\n            }\n            transforms.push(`scale(${wScale}, ${hScale})`);\n            path.setAttribute(\"fill\", \"var(--text-highlight-color)\");\n            path.setAttribute(\"transform\", transforms.join(\" \"));\n        }\n        path.setAttribute(\"d\", d);\n        return path;\n    });\n}\n/**\n * Used to get the current text highlight id from the top `.o_text_highlight`\n * container class.\n *\n * @param {HTMLElement} el\n * @returns {String}\n */\nexport function getCurrentTextHighlight(el) {\n    const highlightEl = el.closest(\".o_text_highlight\");\n    if (!highlightEl) {\n        return;\n    }\n    return Array.from(highlightEl.classList)\n        .find((cls) => cls.startsWith(\"o_text_highlight_\"))\n        ?.replace(\"o_text_highlight_\", \"\");\n}\nfunction rectToBatch(rects) {\n    if (!rects.length) {\n        return [];\n    }\n    const rectBatches = [];\n    let lastX = rects[0].x - 1;\n    let lineIndex2 = 0;\n    for (const rect of rects) {\n        if (rect.x <= lastX) {\n            lineIndex2++;\n        }\n        lastX = rect.x;\n        rectBatches[lineIndex2] = rectBatches[lineIndex2] || [];\n        rectBatches[lineIndex2].push(rect);\n    }\n    return rectBatches;\n}\nfunction getBiggestBoxFromBoxes(includedBoxes) {\n    const firstBox = includedBoxes[0];\n    const combinedRect = new DOMRect(firstBox.x, firstBox.y, firstBox.width, firstBox.height);\n    for (const box of includedBoxes) {\n        if (box.x < combinedRect.x) {\n            combinedRect.x = box.x;\n        }\n        if (box.y < combinedRect.y) {\n            combinedRect.y = box.y;\n        }\n        if (box.bottom > combinedRect.bottom) {\n            combinedRect.height = box.bottom - combinedRect.y;\n        }\n        if (box.right > combinedRect.right) {\n            combinedRect.width = box.right - combinedRect.x;\n        }\n    }\n    return combinedRect;\n}\nfunction getTextnodeRects(el) {\n    const range = new Range();\n    range.setStart(el, 0);\n    range.setEnd(el, el.textContent.length);\n    return [...range.getClientRects()];\n}\n// todo: handle RTL\n// function isRTL(el) {\n//     return window.getComputedStyle(el).direction === \"rtl\";\n// }\n\n/**\n * Returns the closest ancestor element that should be observed for adapting\n * highlight effects.\n *\n * @param {HTMLElement} el\n * @param {HTMLElement} topEl The upper boundary element to observe (defaults\n * to document body).\n * @returns {HTMLElement}\n */\nexport function closestToObserve(el, topEl = el.ownerDocument.body) {\n    el = el.nodeType === Node.ELEMENT_NODE ? el : el.parentElement;\n    if (!el || el === topEl) {\n        return null;\n    }\n    if (window.getComputedStyle(el).display !== \"inline\") {\n        return el;\n    }\n    return closestToObserve(el.parentElement, topEl);\n}\n\n/**\n * @param {HTMLElement} el\n */\nexport function getObservedEls(el) {\n    const closestToObserveEl = closestToObserve(el);\n    return closestToObserveEl ? [closestToObserveEl, el] : [el];\n}\n", "import { browser } from \"@web/core/browser/browser\";\nconst sessionStorage = browser.sessionStorage;\nimport { AutoComplete } from \"@web/core/autocomplete/autocomplete\";\nimport { getActiveHotkey } from \"@web/core/hotkeys/hotkey_service\";\nimport { delay } from \"@web/core/utils/concurrency\";\nimport { getDataURLFromFile, redirect } from \"@web/core/utils/urls\";\nimport { getCSSVariableValue } from \"@html_editor/utils/formatting\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { svgToPNG, webpToPNG } from \"@website/js/utils\";\nimport { escapeRegExp } from \"@web/core/utils/strings\";\nimport { useAutofocus, useService } from \"@web/core/utils/hooks\";\nimport { registry } from \"@web/core/registry\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { mixCssColors } from \"@web/core/utils/colors\";\nimport { router } from \"@web/core/browser/router\";\nimport {\n    Component,\n    onMounted,\n    reactive,\n    useEffect,\n    useEnv,\n    useRef,\n    useState,\n    useSubEnv,\n    onWillStart,\n    useExternalListener,\n} from \"@odoo/owl\";\nimport { standardActionServiceProps } from \"@web/webclient/actions/action_service\";\nimport { fuzzyLevenshteinLookup } from \"@web/core/utils/search\";\nimport { isBrowserSafari } from \"@web/core/browser/feature_detection\";\n\nexport const ROUTES = {\n    descriptionScreen: 2,\n    paletteSelectionScreen: 3,\n    featuresSelectionScreen: 4,\n    themeSelectionScreen: 5,\n};\n\nexport const WEBSITE_TYPES = {\n    1: { id: 1, label: _t(\"a website\"), name: \"business\" },\n    2: { id: 2, label: _t(\"an online store\"), name: \"online_store\" },\n    3: { id: 3, label: _t(\"a blog\"), name: \"blog\" },\n    4: { id: 4, label: _t(\"an event website\"), name: \"event\" },\n    5: { id: 5, label: _t(\"an elearning platform\"), name: \"elearning\" },\n};\n\nexport const WEBSITE_PURPOSES = {\n    1: { id: 1, label: _t(\"get leads\"), name: \"get_leads\" },\n    2: { id: 2, label: _t(\"develop the brand\"), name: \"develop_brand\" },\n    3: { id: 3, label: _t(\"sell more\"), name: \"sell_more\" },\n    4: { id: 4, label: _t(\"inform customers\"), name: \"inform_customers\" },\n    5: { id: 5, label: _t(\"schedule appointments\"), name: \"schedule_appointments\" },\n};\n\nexport const PALETTE_NAMES = [\n    \"default-light-1\",\n    \"default-light-2\",\n    \"default-light-4\",\n    \"default-light-3\",\n    \"default-light-5\",\n    \"default-24\",\n    \"default-light-7\",\n    \"default-light-6\",\n    \"default-light-11\",\n    \"default-light-14\",\n    \"default-light-8\",\n    \"default-6\",\n    \"default-7\",\n    \"default-8\",\n    \"default-9\",\n    \"default-23\",\n    \"default-25\",\n    \"default-12\",\n    \"default-14\",\n    \"default-22\",\n    \"default-15\",\n    \"default-16\",\n    \"default-17\",\n    \"default-light-10\",\n    \"default-19\",\n    \"default-20\",\n    \"default-5\",\n    \"default-4\",\n    \"default-light-9\",\n    \"default-2\",\n    \"default-light-13\",\n    \"default-27\",\n    \"default-light-12\",\n    \"default-1\",\n    \"default-28\",\n    \"default-21\",\n];\n\n// Attributes for which background color should be retrieved\n// from CSS and added in each palette.\nexport const CUSTOM_BG_COLOR_ATTRS = [\"menu\", \"footer\"];\n\nconst MAX_NBR_DISPLAY_MAIN_THEMES = 3;\n\n/**\n * Returns a list of maximum \"resultNbrMax\" themes that depends on the wanted\n * industry and the color palette.\n *\n * @param {Object} orm - The orm used for the server call.\n * @param {Object} state - The state that contains the wanted industry and color\n * palette.\n * @param {Number} resultNbrMax - The number of different wanted themes.\n * @returns {Promise<Array>} A list of objects that contains the different\n * theme names and their related text svgs (as result of a Promise). The length\n * of the list is at most 'resultNbrMax'.\n */\nasync function getRecommendedThemes(orm, state, resultNbrMax = MAX_NBR_DISPLAY_MAIN_THEMES) {\n    return orm.call(\"website\", \"configurator_recommended_themes\", [], {\n        industry_id: state.selectedIndustry.id,\n        palette: state.selectedPalette,\n        result_nbr_max: resultNbrMax,\n    });\n}\n\n//------------------------------------------------------------------------------\n// Components\n//------------------------------------------------------------------------------\n\nexport class SkipButton extends Component {\n    static template = \"website.Configurator.SkipButton\";\n    static props = {\n        skip: Function,\n    };\n}\n\nexport class WelcomeScreen extends Component {\n    static template = \"website.Configurator.WelcomeScreen\";\n    static components = { SkipButton };\n    static props = {\n        skip: Function,\n        navigate: Function,\n    };\n    setup() {\n        this.state = useStore();\n    }\n\n    goToDescription() {\n        this.props.navigate(ROUTES.descriptionScreen);\n    }\n}\n\nexport class DescriptionScreen extends Component {\n    static template = \"website.Configurator.DescriptionScreen\";\n    static components = { SkipButton, AutoComplete };\n    static props = {\n        navigate: Function,\n        skip: Function,\n    };\n    setup() {\n        this.industrySelection = useRef(\"industrySelection\");\n        this.purposeSelectionRef = useRef(\"purposeSelection\");\n        this.state = useStore();\n        this.orm = useService(\"orm\");\n        useAutofocus();\n\n        this.splitRegex = /[|\\s,]+/;\n\n        // Get all words from the industry names and synonyms\n        this.dictionarySet = new Set();\n        for (const industry of this.state.industries) {\n            let industryWords = this._splitToSet(industry.label);\n            if (industry.synonyms) {\n                industryWords = industryWords.union(this._splitToSet(industry.synonyms));\n            }\n            this.dictionarySet = this.dictionarySet.union(industryWords);\n        }\n\n        onMounted(() => this.onMounted());\n\n        // Autofocus the next field once the current one is confirmed.\n        useEffect(\n            (selectedType, selectedIndustry) => {\n                if (selectedType && !selectedIndustry) {\n                    this.industrySelection.el.querySelector(\"input\").focus();\n                }\n                if (selectedIndustry) {\n                    this.purposeSelectionRef.el.focus();\n                }\n            },\n            () => [this.state.selectedType, this.state.selectedIndustry]\n        );\n\n        this.safariHackFocusedOutDropdown = null;\n    }\n\n    onMounted() {\n        this.selectWebsitePurpose();\n    }\n    /**\n     * Set the input's parent label value to automatically adapt input size\n     * and update the selected industry.\n     *\n     * @private\n     * @param {string} label\n     * @param {number} id\n     */\n    _setSelectedIndustry(label, id) {\n        this.state.selectIndustry(label, id);\n        this.checkDescriptionCompletion();\n    }\n\n    _splitToSet(string) {\n        return new Set(string.toLowerCase().split(this.splitRegex));\n    }\n\n    get sources() {\n        return [\n            {\n                options: (request) => (request.length < 1 ? [] : this._autocompleteSearch(request)),\n            },\n        ];\n    }\n    /**\n     * Called each time the autocomplete input's value changes. Only industries\n     * having a label or a synonym containing all terms of the input value are\n     * kept.\n     * The order received from IAP is kept (expected to be on descending hit\n     * count) unless there are 7 or less matches in which case the results are\n     * sorted alphabetically.\n     * The result size is limited to 30.\n     *\n     * @param {String} term input current value\n     */\n    _autocompleteSearch(term) {\n        this.state.selectedIndustry = undefined;\n        const termsSet = this._splitToSet(term);\n\n        //-------words correction--------\n        // Check and correct all the terms\n        const correctedSet = new Set();\n        for (const term of termsSet) {\n            if (this.dictionarySet.has(term)) {\n                correctedSet.add(term);\n                continue;\n            }\n            const res = fuzzyLevenshteinLookup(term, this.dictionarySet);\n            correctedSet.add(res[0] || term);\n        }\n        let terms = Array.from(correctedSet);\n        const limit = 30;\n        // `this.state.industries` is already sorted by hit count (from IAP).\n        // That order should be kept after manipulating the recordset.\n        let matches = this.state.industries.filter((val, index) =>\n            // To match, every term should be contained in the label\n            terms.every((term) => val.label.toLowerCase().includes(term))\n        );\n\n        matches = matches.sort((x, y) => x.hitCountOrder - y.hitCountOrder);\n        if (matches.length > limit) {\n            // Keep matches with the least number of words so that e.g.\n            // \"restaurant\" remains available even if there are 30 specific\n            // sub-types that have a higher hit count.\n            matches = matches\n                .sort((x, y) => x.wordCount - y.wordCount)\n                .slice(0, limit)\n                .sort((x, y) => x.hitCountOrder - y.hitCountOrder);\n        } else {\n            let synonymMatches = this.state.industries.filter((val, index) => {\n                // To match, every term should be contained in the synonym\n                for (const candidate of [...(val.synonyms || \"\").split(this.splitRegex)]) {\n                    // Check if industry label has already matched\n                    if (\n                        terms.every((term) => candidate.toLowerCase().includes(term)) &&\n                        !matches.includes(val)\n                    ) {\n                        return true;\n                    }\n                }\n                return false;\n            });\n            synonymMatches = synonymMatches.sort((x, y) => x.hitCountOrder - y.hitCountOrder);\n            matches = matches.concat(synonymMatches);\n            if (matches.length > limit) {\n                matches = matches.slice(0, limit);\n            }\n        }\n        if (matches.length === 0) {\n            matches = [{ label: term, id: -1 }];\n            terms = [term];\n        }\n        return matches.map((match) => ({\n            label: match.label,\n            labelTermOrder: this._getMatchTermOrder(match.label, terms),\n            onSelect: () => this._setSelectedIndustry(match.label, match.id),\n        }));\n    }\n\n    /**\n     * Splits the string parameter 'label' into bits based on the location\n     * of the 'terms' typed by the user.\n     *\n     * @param {string} label\n     * @param {string[]} terms\n     * @returns {object}\n     * The return object 'matchTermOrder' contains two lists:\n     * - 'labelBits' store all the segments of the split 'label'\n     * - 'searchTermIndexes' keeps the indexes of the bits that matches with the 'terms'\n     */\n    _getMatchTermOrder(label, terms) {\n        const sortedTerms = terms.sort((a, b) => b.length - a.length);\n        const matchTermOrder = {\n            labelBits: [],\n            searchTermIndexes: [],\n        };\n        if (!label) {\n            return matchTermOrder;\n        }\n\n        matchTermOrder.labelBits.push(label);\n        for (const term of sortedTerms) {\n            let bitIndex = 0;\n            while (bitIndex < matchTermOrder.labelBits.length) {\n                const currentBit = matchTermOrder.labelBits[bitIndex];\n                const splitBits = currentBit.split(new RegExp(`(${escapeRegExp(term)})`));\n                matchTermOrder.labelBits.splice(bitIndex, 1, ...splitBits);\n                bitIndex += splitBits.length;\n            }\n        }\n        // Saves the indexes of the segments matching the terms\n        const labelBits = [];\n        for (const i in matchTermOrder.labelBits) {\n            labelBits.push({\n                bit: matchTermOrder.labelBits[i],\n                id: i,\n            });\n            if (sortedTerms.includes(matchTermOrder.labelBits[i].toLowerCase())) {\n                matchTermOrder.searchTermIndexes.push(i);\n            }\n        }\n        matchTermOrder.labelBits = labelBits;\n        return matchTermOrder;\n    }\n\n    selectWebsiteType(id) {\n        this.state.selectWebsiteType(id);\n        this.checkDescriptionCompletion();\n    }\n\n    selectWebsitePurpose(id) {\n        this.state.selectWebsitePurpose(id);\n        this.checkDescriptionCompletion();\n    }\n\n    checkDescriptionCompletion() {\n        const { selectedType, selectedPurpose, selectedIndustry } = this.state;\n        if (selectedType && selectedPurpose && selectedIndustry) {\n            // If the industry name is not known by the server, send it to the\n            // IAP server.\n            if (selectedIndustry.id === -1) {\n                this.orm.call(\"website\", \"configurator_missing_industry\", [], {\n                    unknown_industry: selectedIndustry.label,\n                });\n            }\n            this.props.navigate(ROUTES.paletteSelectionScreen);\n        }\n    }\n    onConfiguratorScreenFocusin(ev) {\n        // On safari, hide the previously focused out dropdown if focusin is\n        // outside of it\n        if (isBrowserSafari() && this.safariHackFocusedOutDropdown) {\n            if (ev.target.closest(\".dropdown\") !== this.safariHackFocusedOutDropdown) {\n                window.Dropdown.getOrCreateInstance(this.safariHackFocusedOutDropdown).hide();\n            }\n            this.safariHackFocusedOutDropdown = null;\n        }\n    }\n    /**\n     * Hide the dropdown once the focus isn't contained within it anymore.\n     *\n     * @param {FocusEvent} ev\n     */\n    onDropdownFocusout(ev) {\n        // On safari, we are missing relatedTarget because we can't focus on a\n        // button, so we delay dropdown hiding to focusin of next element\n        if (isBrowserSafari()) {\n            this.safariHackFocusedOutDropdown = ev.currentTarget;\n            return;\n        }\n        if (ev.relatedTarget?.closest(\".dropdown\") !== ev.currentTarget) {\n            window.Dropdown.getOrCreateInstance(ev.currentTarget).hide();\n        }\n    }\n\n    onAutocompleteInput({ inputValue }) {\n        if (!inputValue) {\n            this.state.selectIndustry(); // reset\n        }\n    }\n}\n\nexport class PaletteSelectionScreen extends Component {\n    static components = { SkipButton };\n    static template = \"website.Configurator.PaletteSelectionScreen\";\n    static props = {\n        navigate: Function,\n        skip: Function,\n    };\n    setup() {\n        this.state = useStore();\n        this.logoInputRef = useRef(\"logoSelectionInput\");\n        this.notification = useService(\"notification\");\n        this.orm = useService(\"orm\");\n\n        onMounted(() => {\n            if (this.state.logo) {\n                this.updatePalettes();\n            }\n        });\n    }\n\n    uploadLogo() {\n        this.logoInputRef.el.click();\n    }\n\n    /**\n     * Removes the previously uploaded logo.\n     *\n     * @param {Event} ev\n     */\n    async removeLogo(ev) {\n        ev.stopPropagation();\n        // Permit to trigger onChange even with the same file.\n        this.logoInputRef.el.value = \"\";\n        if (this.state.logoAttachmentId) {\n            await this._removeAttachments([this.state.logoAttachmentId]);\n        }\n        this.state.changeLogo();\n        // Remove recommended palette.\n        this.state.setRecommendedPalette();\n    }\n\n    async changeLogo() {\n        const logoSelectInput = this.logoInputRef.el;\n        if (logoSelectInput.files.length === 1) {\n            const previousLogoAttachmentId = this.state.logoAttachmentId;\n            const file = logoSelectInput.files[0];\n            if (file.size > 2500000) {\n                this.notification.add(\n                    _t(\"The logo is too large. Please upload a logo smaller than 2.5 MB.\"),\n                    {\n                        title: file.name,\n                        type: \"warning\",\n                    }\n                );\n                return;\n            }\n            const data = await getDataURLFromFile(file);\n            const attachment = await rpc(\"/web_editor/attachment/add_data\", {\n                name: \"logo\",\n                data: data.split(\",\")[1],\n                is_image: true,\n            });\n            if (!attachment.error) {\n                if (previousLogoAttachmentId) {\n                    await this._removeAttachments([previousLogoAttachmentId]);\n                }\n                this.state.changeLogo(data, attachment.id);\n                this.updatePalettes();\n            } else {\n                this.notification.add(attachment.error, {\n                    title: file.name,\n                });\n            }\n        }\n    }\n\n    async updatePalettes() {\n        let img = this.state.logo;\n        if (img.startsWith(\"data:image/svg+xml\")) {\n            img = await svgToPNG(img);\n        }\n        if (img.startsWith(\"data:image/webp\")) {\n            img = await webpToPNG(img);\n        }\n        img = img.split(\",\")[1];\n        const [color1, color2] = await this.orm.call(\n            \"base.document.layout\",\n            \"extract_image_primary_secondary_colors\",\n            [img],\n            { mitigate: 255 }\n        );\n        this.state.setRecommendedPalette(color1, color2);\n    }\n\n    selectPalette(paletteName) {\n        this.state.selectPalette(paletteName);\n        this.props.navigate(ROUTES.featuresSelectionScreen);\n    }\n\n    /**\n     * Removes the attachments from the DB.\n     *\n     * @private\n     * @param {Array<number>} ids the attachment ids to remove\n     */\n    async _removeAttachments(ids) {\n        rpc(\"/html_editor/attachment/remove\", { ids: ids });\n    }\n}\n\nexport class ApplyConfiguratorScreen extends Component {\n    static template = \"\";\n    static props = [\"*\"];\n    setup() {\n        this.websiteService = useService(\"website\");\n    }\n\n    async applyConfigurator(themeName) {\n        if (!this.state.selectedIndustry) {\n            return this.props.navigate(ROUTES.descriptionScreen);\n        }\n        if (!this.state.selectedPalette) {\n            return this.props.navigate(ROUTES.paletteSelectionScreen);\n        }\n\n        const attemptConfiguratorApply = async (data, retryCount = 0) => {\n            try {\n                return await this.orm.silent.call(\"website\", \"configurator_apply\", [], data);\n            } catch (error) {\n                // Wait a bit before retrying or allowing manual retry.\n                await delay(5000);\n                if (retryCount < 3) {\n                    return attemptConfiguratorApply(data, retryCount + 1);\n                }\n                document.querySelector(\".o_website_loader_container\").remove();\n                throw error;\n            }\n        };\n\n        if (themeName !== undefined) {\n            const selectedFeatures = Object.values(this.state.features)\n                .filter((feature) => feature.selected)\n                .map((feature) => feature.id);\n            this.websiteService.showLoader({\n                showTips: true,\n                selectedFeatures: selectedFeatures,\n                showWaitingMessages: true,\n            });\n            let selectedPalette = this.state.selectedPalette.name;\n            if (!selectedPalette) {\n                selectedPalette = [\n                    this.state.selectedPalette.color1,\n                    this.state.selectedPalette.color2,\n                    this.state.selectedPalette.color3,\n                    this.state.selectedPalette.color4,\n                    this.state.selectedPalette.color5,\n                ];\n            }\n            const resp = await attemptConfiguratorApply(\n                this.getConfigurationData(selectedFeatures, selectedPalette, themeName)\n            );\n\n            this.props.clearStorage();\n\n            this.websiteService.prepareOutLoader();\n            // Here the website service goToWebsite method is not used because\n            // the web client needs to be reloaded after the new modules have\n            // been installed.\n            redirect(\n                `/odoo/action-website.website_preview?website_id=${encodeURIComponent(\n                    resp.website_id\n                )}`\n            );\n        }\n    }\n\n    getConfigurationData(selectedFeatures, selectedPalette, themeName) {\n        return {\n            selected_features: selectedFeatures,\n            industry_id: this.state.selectedIndustry.id,\n            industry_name: this.state.selectedIndustry.label.toLowerCase(),\n            selected_palette: selectedPalette,\n            theme_name: themeName,\n            website_purpose:\n                WEBSITE_PURPOSES[this.state.selectedPurpose || this.state.formerSelectedPurpose]\n                    .name,\n            website_type: WEBSITE_TYPES[this.state.selectedType].name,\n            logo_attachment_id: this.state.logoAttachmentId,\n        };\n    }\n}\n\nexport class FeaturesSelectionScreen extends Component {\n    static components = { SkipButton };\n    static template = \"website.Configurator.FeatureSelection\";\n    static props = {\n        navigate: Function,\n        skip: Function,\n    };\n    setup() {\n        super.setup();\n        this.state = useStore();\n    }\n\n    /**\n     * Return the theme selection screen as the next step, unless overridden.\n     *\n     * @return {int} Next step route.\n     */\n    static nextStep() {\n        return ROUTES.themeSelectionScreen;\n    }\n\n    async buildWebsite() {\n        const industryId = this.state.selectedIndustry && this.state.selectedIndustry.id;\n        if (!industryId) {\n            return this.props.navigate(ROUTES.descriptionScreen);\n        }\n\n        this.props.navigate(FeaturesSelectionScreen.nextStep());\n    }\n\n    onKeydown(ev) {\n        const hotkey = getActiveHotkey(ev);\n        if ([\"enter\", \"space\"].includes(hotkey)) {\n            ev.target.click();\n        }\n    }\n}\n\nexport class ThemeSelectionScreen extends ApplyConfiguratorScreen {\n    static template = \"website.Configurator.ThemeSelectionScreen\";\n    setup() {\n        super.setup();\n\n        this.uiService = useService(\"ui\");\n        this.orm = useService(\"orm\");\n        this.maxNbrDisplayExtraThemes = 100;\n        const env = useEnv();\n        env.store[\"extraThemesLoaded\"] = false;\n        env.store[\"extraThemes\"] = [];\n        this.state = useState(env.store);\n        this.themeSVGPreviews = [\n            useRef(\"ThemePreview1\"),\n            useRef(\"ThemePreview2\"),\n            useRef(\"ThemePreview3\"),\n        ];\n        this.extraThemesButtonRef = useRef(\"extraThemesButton\");\n        this.extraThemeSVGPreviews = [];\n        for (let i = 0; i < this.maxNbrDisplayExtraThemes; i++) {\n            this.extraThemeSVGPreviews.push(useRef(`ExtraThemePreview${i}`));\n        }\n        onWillStart(async () => {\n            const themes = await getRecommendedThemes(this.orm, this.state);\n            if (!themes.length) {\n                await this.applyConfigurator(\"theme_default\");\n            } else {\n                this.state.updateRecommendedThemes(themes);\n            }\n        });\n\n        onMounted(() => {\n            this.blockUiDuringImageLoading(this.state.themes, this.themeSVGPreviews);\n        });\n\n        useEffect(\n            () =>\n                this.blockUiDuringImageLoading(this.state.extraThemes, this.extraThemeSVGPreviews),\n            () => [this.state.extraThemes]\n        );\n    }\n\n    /**\n     * The button should be shown if we never tried to load the extra themes and\n     * if they are enough main themes already displayed. If this last condition\n     * is not fulfilled, there is no need to display the button as no more will\n     * be displayed.\n     */\n    get showViewMoreThemesButton() {\n        return (\n            !this.state.extraThemesLoaded &&\n            this.state.themes.length === MAX_NBR_DISPLAY_MAIN_THEMES\n        );\n    }\n\n    /**\n     * Transforms text svgs into svg elements and adds a loading effect that\n     * blocks the UI during the loading of the images inside those svg elements.\n     *\n     * @param {Array<Object>} themes - The text svgs.\n     * @param {Array} themeSVGPreviews - A reference to the svg elements.\n     */\n    blockUiDuringImageLoading(themes, themeSVGPreviews) {\n        if (!themes.length) {\n            // There is no svg to transform\n            return;\n        }\n        const proms = [];\n        this.uiService.block({ delay: 700 });\n        themes.forEach((theme, idx) => {\n            const svgEl = new DOMParser().parseFromString(\n                theme.svg,\n                \"image/svg+xml\"\n            ).documentElement;\n            for (const imgEl of svgEl.querySelectorAll(\"image\")) {\n                proms.push(\n                    new Promise((resolve, reject) => {\n                        imgEl.addEventListener(\n                            \"load\",\n                            () => {\n                                resolve(imgEl);\n                            },\n                            { once: true }\n                        );\n                        imgEl.addEventListener(\n                            \"error\",\n                            () => {\n                                reject(imgEl);\n                            },\n                            { once: true }\n                        );\n                    })\n                );\n            }\n            themeSVGPreviews[idx].el.appendChild(svgEl);\n        });\n        // When all the images inside the svgs are loaded then remove the\n        // loading effect.\n        Promise.allSettled(proms).then(() => {\n            this.uiService.unblock();\n        });\n    }\n\n    async chooseTheme(themeName) {\n        await this.applyConfigurator(themeName);\n    }\n\n    async getMoreThemes() {\n        this.uiService.block();\n        const themes = await getRecommendedThemes(\n            this.orm,\n            this.state,\n            this.maxNbrDisplayExtraThemes\n        );\n        // Filter the extra themes to not propose a theme that is already\n        // present in the main themes.\n        const mainThemeNames = this.state.themes.map((theme) => theme.name);\n        this.state.extraThemes = themes.filter(\n            (extraTheme) => !mainThemeNames.includes(extraTheme.name)\n        );\n        this.state.extraThemesLoaded = true;\n        this.uiService.unblock();\n    }\n\n    getExtraThemeName(idx) {\n        return this.state.extraThemes.length > idx && this.state.extraThemes[idx].name;\n    }\n}\n\n//------------------------------------------------------------------------------\n// Store\n//------------------------------------------------------------------------------\n\nexport class Store {\n    async start(getInitialState) {\n        Object.assign(this, await getInitialState());\n    }\n\n    //-------------------------------------------------------------------------\n    // Getters\n    //-------------------------------------------------------------------------\n\n    getWebsiteTypes() {\n        return Object.values(WEBSITE_TYPES);\n    }\n\n    getSelectedType(id) {\n        return id && WEBSITE_TYPES[id];\n    }\n\n    getWebsitePurpose() {\n        return Object.values(WEBSITE_PURPOSES);\n    }\n\n    getSelectedPurpose(id) {\n        return id && WEBSITE_PURPOSES[id];\n    }\n\n    getFeatures() {\n        return Object.values(this.features);\n    }\n\n    getPalettes() {\n        return Object.values(this.palettes);\n    }\n\n    getThemeName(idx) {\n        return this.themes.length > idx && this.themes[idx].name;\n    }\n\n    /**\n     * @returns {string | false}\n     */\n    getSelectedPaletteName() {\n        const palette = this.selectedPalette;\n        return palette ? palette.name || \"recommendedPalette\" : false;\n    }\n\n    //-------------------------------------------------------------------------\n    // Actions\n    //-------------------------------------------------------------------------\n\n    selectWebsiteType(id) {\n        Object.values(this.features)\n            .filter((feature) => feature.module_state !== \"installed\")\n            .forEach((feature) => {\n                feature.selected = feature.website_config_preselection.includes(\n                    WEBSITE_TYPES[id].name\n                );\n            });\n        this.selectedType = id;\n    }\n\n    selectWebsitePurpose(id) {\n        // Keep track or the former selection in order to be able to keep\n        // the auto-advance navigation scheme while being able to use the\n        // browser's back and forward buttons.\n        if (!id && this.selectedPurpose) {\n            this.formerSelectedPurpose = this.selectedPurpose;\n        }\n        Object.values(this.features)\n            .filter((feature) => feature.module_state !== \"installed\")\n            .forEach((feature) => {\n                // need to check id, since we set to undefined in mount() to avoid the auto next screen on back button\n                feature.selected |=\n                    id && feature.website_config_preselection.includes(WEBSITE_PURPOSES[id].name);\n            });\n        this.selectedPurpose = id;\n    }\n\n    selectIndustry(label, id) {\n        if (!label || !id) {\n            this.selectedIndustry = undefined;\n        } else {\n            this.selectedIndustry = { id, label };\n        }\n    }\n\n    changeLogo(data, attachmentId) {\n        this.logo = data;\n        this.logoAttachmentId = attachmentId;\n    }\n\n    selectPalette(paletteName) {\n        if (paletteName === \"recommendedPalette\") {\n            this.selectedPalette = this.recommendedPalette;\n        } else {\n            this.selectedPalette = this.palettes[paletteName];\n        }\n    }\n\n    toggleFeature(featureId) {\n        const feature = this.features[featureId];\n        const isModuleInstalled = feature.module_state === \"installed\";\n        feature.selected = !feature.selected || isModuleInstalled;\n    }\n\n    setRecommendedPalette(color1, color2) {\n        if (color1 && color2) {\n            if (color1 === color2) {\n                color2 = mixCssColors(\"#FFFFFF\", color1, 0.2);\n            }\n            const recommendedPalette = {\n                color1: color1,\n                color2: color2,\n                color3: mixCssColors(\"#FFFFFF\", color2, 0.9),\n                color4: \"#FFFFFF\",\n                color5: mixCssColors(color1, \"#000000\", 0.125),\n            };\n            CUSTOM_BG_COLOR_ATTRS.forEach((attr) => {\n                recommendedPalette[attr] = recommendedPalette[this.defaultColors[attr]];\n            });\n            this.recommendedPalette = recommendedPalette;\n        } else {\n            this.recommendedPalette = undefined;\n        }\n        this.selectedPalette = this.recommendedPalette;\n    }\n\n    updateRecommendedThemes(themes) {\n        this.themes = themes.slice(0, MAX_NBR_DISPLAY_MAIN_THEMES);\n    }\n}\n\nexport function useStore() {\n    const env = useEnv();\n    return useState(env.store);\n}\n\nexport class Configurator extends Component {\n    static components = {\n        WelcomeScreen,\n        DescriptionScreen,\n        PaletteSelectionScreen,\n        FeaturesSelectionScreen,\n        ThemeSelectionScreen,\n    };\n    static template = \"website.Configurator.Configurator\";\n    static props = { ...standardActionServiceProps };\n\n    setup() {\n        this.orm = useService(\"orm\");\n        this.action = useService(\"action\");\n        this.website = useService(\"website\");\n\n        // Using the back button must update the router state.\n        useExternalListener(window, \"popstate\", (ev) => {\n            // FIXME: this doesn't work unless this component is already mounted so navigating through\n            // history from a different client action will not work.\n            if (ev.state && \"configuratorStep\" in ev.state) {\n                // Do not use navigate because URL is already updated.\n                this.state.currentStep = ev.state.configuratorStep;\n            }\n        });\n\n        const initialStep = router.current.step;\n        const store = reactive(new Store(), () => this.updateStorage(store));\n\n        this.state = useState({\n            currentStep: initialStep,\n        });\n\n        useSubEnv({ store });\n\n        onWillStart(async () => {\n            this.websiteId = (await this.orm.call(\"website\", \"get_current_website\"))[0];\n\n            await store.start(() => this.getInitialState());\n            this.updateStorage(store);\n            if (!store.industries || store.configurator_done) {\n                await this.skipConfigurator();\n            }\n        });\n\n        // This is a hack to overwrite the history state, modified by the\n        // router service after executing an action. Ideally, the router\n        // service would let us push a state with a new pathname.\n        onMounted(() => {\n            setTimeout(() => {\n                router.cancelPushes();\n                this.updateBrowserUrl();\n            });\n        });\n    }\n\n    get pathname() {\n        return `/website/configurator${\n            this.state.currentStep ? `/${encodeURIComponent(this.state.currentStep)}` : \"\"\n        }`;\n    }\n\n    get storageItemName() {\n        return `websiteConfigurator${this.websiteId}`;\n    }\n\n    updateBrowserUrl() {\n        history.pushState(\n            { skipRouteChange: true, configuratorStep: this.state.currentStep },\n            \"\",\n            this.pathname\n        );\n    }\n\n    get currentComponent() {\n        if (this.state.currentStep === ROUTES.descriptionScreen) {\n            return DescriptionScreen;\n        } else if (this.state.currentStep === ROUTES.paletteSelectionScreen) {\n            return PaletteSelectionScreen;\n        } else if (this.state.currentStep === ROUTES.featuresSelectionScreen) {\n            return FeaturesSelectionScreen;\n        } else if (this.state.currentStep === ROUTES.themeSelectionScreen) {\n            return ThemeSelectionScreen;\n        }\n        return WelcomeScreen;\n    }\n\n    get componentProps() {\n        const props = {\n            skip: this.skipConfigurator.bind(this),\n            navigate: this.navigate.bind(this),\n        };\n        if (this.state.currentStep === ROUTES.themeSelectionScreen) {\n            props.clearStorage = this.clearStorage.bind(this);\n        }\n        return props;\n    }\n\n    navigate(step, reload = false) {\n        this.state.currentStep = step;\n        if (reload) {\n            redirect(this.pathname);\n        } else {\n            this.updateBrowserUrl();\n        }\n    }\n\n    clearStorage() {\n        sessionStorage.removeItem(this.storageItemName);\n    }\n\n    async getInitialState() {\n        // Load values from python and iap\n        var results = await this.orm.call(\"website\", \"configurator_init\");\n        const r = {\n            industries: results.industries,\n            logo: results.logo ? \"data:image/png;base64,\" + results.logo : false,\n            configurator_done: results.configurator_done,\n        };\n        r.industries = r.industries.map((industry, index) => ({\n            ...industry,\n            wordCount: industry.label.split(\" \").length,\n            hitCountOrder: index,\n        }));\n\n        // Load palettes from the current CSS\n        const palettes = {};\n        const style = window.getComputedStyle(document.documentElement);\n\n        PALETTE_NAMES.forEach((paletteName) => {\n            const palette = {\n                name: paletteName,\n            };\n            for (let j = 1; j <= 5; j += 1) {\n                palette[`color${j}`] = getCSSVariableValue(\n                    `o-palette-${paletteName}-o-color-${j}`,\n                    style\n                );\n            }\n            CUSTOM_BG_COLOR_ATTRS.forEach((attr) => {\n                palette[attr] = getCSSVariableValue(`o-palette-${paletteName}-${attr}-bg`, style);\n            });\n            palettes[paletteName] = palette;\n        });\n\n        const localState = JSON.parse(sessionStorage.getItem(this.storageItemName));\n        if (localState) {\n            let themes = [];\n            if (localState.selectedIndustry && localState.selectedPalette) {\n                themes = await getRecommendedThemes(this.orm, localState);\n            }\n            return Object.assign(r, { ...localState, palettes, themes });\n        }\n\n        const features = {};\n        results.features.forEach((feature) => {\n            features[feature.id] = Object.assign({}, feature, {\n                selected: feature.module_state === \"installed\",\n            });\n            const wtp = features[feature.id][\"website_config_preselection\"];\n            features[feature.id][\"website_config_preselection\"] = wtp ? wtp.split(\",\") : [];\n        });\n\n        // Palette color used by default as background color for menu and footer.\n        // Needed to build the recommended palette.\n        const defaultColors = {};\n        CUSTOM_BG_COLOR_ATTRS.forEach((attr) => {\n            const color = getCSSVariableValue(`o-default-${attr}-bg`, style);\n            const match = color.match(/o-color-(?<idx>[1-5])/);\n            const colorIdx = parseInt(match.groups[\"idx\"]);\n            defaultColors[attr] = `color${colorIdx}`;\n        });\n\n        return Object.assign(r, {\n            selectedType: undefined,\n            selectedPurpose: undefined,\n            formerSelectedPurpose: undefined,\n            selectedIndustry: undefined,\n            selectedPalette: undefined,\n            recommendedPalette: undefined,\n            defaultColors: defaultColors,\n            palettes: palettes,\n            features: features,\n            themes: [],\n            logoAttachmentId: undefined,\n        });\n    }\n\n    updateStorage(state) {\n        const newState = JSON.stringify({\n            defaultColors: state.defaultColors,\n            features: state.features,\n            logo: state.logo,\n            logoAttachmentId: state.logoAttachmentId,\n            selectedIndustry: state.selectedIndustry,\n            selectedPalette: state.selectedPalette,\n            selectedPurpose: state.selectedPurpose,\n            formerSelectedPurpose: state.formerSelectedPurpose,\n            selectedType: state.selectedType,\n            recommendedPalette: state.recommendedPalette,\n        });\n        sessionStorage.setItem(this.storageItemName, newState);\n    }\n\n    async skipConfigurator() {\n        this.website.showLoader({ showTips: true });\n        const redirectUrl = await this.orm.call(\"website\", \"configurator_skip\");\n        this.clearStorage();\n        // Here the website service goToWebsite method is not used because\n        // the web client needs to be reloaded after the new modules have\n        // been installed.\n        await this.action.doAction(redirectUrl);\n    }\n}\n\nregistry.category(\"actions\").add(\"website_configurator\", Configurator);\n", "import { registry } from \"@web/core/registry\";\n\nexport async function openCustomMenu(env, action) {\n    const websiteCustomMenus = env.services[\"website_custom_menus\"];\n    const websiteMenu = websiteCustomMenus.get(action.context.xmlid);\n    if (websiteMenu) {\n        websiteCustomMenus.open({ xmlid: action.context.xmlid });\n    }\n}\n\n// TODO we should probably have a more standard system for this\n// \"website_custom_menus\" feature.\nregistry.category(\"actions\").add(\"open_website_custom_menu\", openCustomMenu);\n", "import { rpc } from \"@web/core/network/rpc\";\nimport { registry } from \"@web/core/registry\";\nimport { Layout } from \"@web/search/layout\";\nimport { Component, useEffect, useState } from \"@odoo/owl\";\nimport { KeepLast } from \"@web/core/utils/concurrency\";\nimport { DocumentationLink } from \"@web/views/widgets/documentation_link/documentation_link\";\n\nclass WebsiteDashboard extends Component {\n    static template = \"website.WebsiteDashboardMain\";\n    static components = { Layout, DocumentationLink };\n    static props = [\"*\"];\n    setup() {\n        super.setup();\n        this.keepLast = new KeepLast();\n\n        this.state = useState({\n            website: false,\n            groups: {},\n            websites: [],\n            dashboards: {},\n        });\n\n        useEffect(\n            () => {\n                this.fetchData();\n            },\n            () => [this.state.website]\n        );\n    }\n\n    get display() {\n        return {\n            controlPanel: {},\n        };\n    }\n\n    async fetchData() {\n        const dashboardData = await this.keepLast.add(\n            rpc(\"/website/fetch_dashboard_data\", {\n                website_id: this.state.website,\n            })\n        );\n        Object.assign(this.state, dashboardData);\n    }\n}\n\nregistry.category(\"actions\").add(\"backend_dashboard\", WebsiteDashboard);\n", "import { Component } from \"@odoo/owl\";\n\nexport class CreatePageMessage extends Component {\n    static template = \"website.CreatePageMessage\";\n    static props = {\n        createPage: { type: Function },\n    };\n}\n", "import { registry } from \"@web/core/registry\";\nimport { useService, useBus } from \"@web/core/utils/hooks\";\nimport { Component, onWillStart, useState } from \"@odoo/owl\";\n\nconst websiteSystrayRegistry = registry.category(\"website_systray\");\n\nexport class EditInBackendSystrayItem extends Component {\n    static template = \"website.EditInBackendSystrayItem\";\n    static props = {};\n    setup() {\n        this.websiteService = useService(\"website\");\n        this.actionService = useService(\"action\");\n        this.state = useState({ mainObjectName: \"\" });\n\n        onWillStart(this._updateMainObjectName);\n        useBus(websiteSystrayRegistry, \"CONTENT-UPDATED\", this._updateMainObjectName);\n    }\n\n    editInBackend() {\n        const {\n            metadata: { mainObject },\n        } = this.websiteService.currentWebsite;\n        this.actionService.doAction({\n            res_model: mainObject.model,\n            res_id: mainObject.id,\n            views: [[false, \"form\"]],\n            type: \"ir.actions.act_window\",\n            view_mode: \"form\",\n        });\n    }\n\n    async _updateMainObjectName() {\n        this.state.mainObjectName = await this.websiteService.getUserModelName();\n    }\n}\n", "import { Component, useState } from \"@odoo/owl\";\nimport { useBus, useService } from \"@web/core/utils/hooks\";\nimport { registry } from \"@web/core/registry\";\nimport { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { _t } from \"@web/core/l10n/translation\";\n\nconst websiteSystrayRegistry = registry.category(\"website_systray\");\n\nexport class EditWebsiteSystrayItem extends Component {\n    static template = \"website.EditWebsiteSystrayItem\";\n    static props = {\n        onNewPage: { type: Function },\n        onEditPage: { type: Function },\n        iframeEl: { type: HTMLElement },\n    };\n    static components = {\n        Dropdown,\n        DropdownItem,\n    };\n\n    setup() {\n        this.websiteService = useService(\"website\");\n        this.notification = useService(\"notification\");\n        this.websiteContext = useState(this.websiteService.context);\n        // TODO: website service should share a reactive\n        useBus(websiteSystrayRegistry, \"CONTENT-UPDATED\", () => this.checkPendingTranslations());\n        this.isEnteringTranslateMode = false;\n    }\n\n    onClickEditPage() {\n        this.websiteContext.edition = true;\n        this.props.onEditPage();\n    }\n\n    get currentWebsiteInfo() {\n        return this.websiteService.currentWebsite?.metadata;\n    }\n\n    get translatable() {\n        return this.websiteService.currentWebsite?.metadata.translatable;\n    }\n\n    async attemptStartTranslate() {\n        // TODO: move on the website part (not html_builder) and add a test tour\n        if (this.websiteService.isRestrictedEditor && !this.websiteService.isDesigner) {\n            const pageModelAndId = this.websiteService.currentWebsite.metadata.mainObject;\n            const recordsOnPage = {\n                [pageModelAndId.model]: pageModelAndId.id,\n            };\n            const otherRecordEls = this.props.iframeEl.querySelectorAll(\n                \"[data-res-model][data-res-id]:not([data-res-model='ir.ui.view']), [data-oe-model][data-oe-id]:not([data-oe-model='ir.ui.view'])\"\n            );\n            for (const el of otherRecordEls) {\n                const model = el.dataset.resModel || el.dataset.oeModel;\n                if (!recordsOnPage[model]) {\n                    // Keep one record of each type.\n                    recordsOnPage[model] = parseInt(el.dataset.resId || el.dataset.oeId);\n                }\n            }\n            await rpc(\"/website/check_can_modify_any\", {\n                records: Object.entries(recordsOnPage).map(([res_model, res_id]) => ({\n                    res_model,\n                    res_id,\n                })),\n            });\n        }\n        this.startTranslate();\n    }\n\n    getLocation() {\n        return this.websiteService.contentWindow.location;\n    }\n\n    editFromTranslate() {\n        // We are in translate mode, the pathname starts with '/<url_code>'. By\n        // adding a trailing slash we can simply search for the first slash\n        // after the language code to remove the language part.\n        const { pathname, search, hash } = this.getLocation();\n        const languagePrefix = `${pathname}/`.indexOf(\"/\", 1);\n        const defaultLanguagePathname = pathname.substring(languagePrefix);\n        this.websiteService.goToWebsite({\n            path: defaultLanguagePathname + search + hash,\n            lang: \"default\",\n            edition: true,\n            htmlBuilder: true,\n        });\n    }\n\n    startTranslate() {\n        this.isEnteringTranslateMode = true;\n        const { pathname, search, hash } = this.getLocation();\n        const searchParams = new URLSearchParams(search);\n        searchParams.set(\"edit_translations\", \"1\");\n        this.websiteService.goToWebsite({\n            path: pathname + `?${searchParams.toString() + hash}`,\n            translation: true,\n            htmlBuilder: true,\n        });\n    }\n\n    async checkPendingTranslations() {\n        if (this.translatable && !this.isEnteringTranslateMode) {\n            const { pathname, search, hash } = this.getLocation();\n            const searchParams = new URLSearchParams(search);\n            searchParams.set(\"edit_translations\", \"1\");\n            const path = pathname + `?${searchParams.toString() + hash}`;\n            const response = await fetch(path);\n            const html = await response.text();\n            const parser = new DOMParser();\n            const doc = parser.parseFromString(html, \"text/html\");\n            if (doc.querySelector(\"#wrap .o_delay_translation\")) {\n                this.notification.add(\n                    _t('Click on \"Edit/Translate\" to apply changes made on default language.'),\n                    { type: \"info\" }\n                );\n            }\n        }\n        this.isEnteringTranslateMode = false;\n    }\n}\n", "import { Component } from \"@odoo/owl\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { WebsiteDialog } from \"@website/components/dialog/dialog\";\n\nexport class InstallModuleDialog extends Component {\n    static components = { WebsiteDialog };\n    static template = \"website.InstallModuleDialog\";\n    static props = {\n        title: String,\n        installationText: String,\n        installModule: Function,\n        close: Function,\n    };\n\n    setup() {\n        this.installButtonTitle = _t(\"Install\");\n    }\n\n    onClickInstall() {\n        this.props.close();\n        this.props.installModule();\n    }\n}\n", "import { useService } from \"@web/core/utils/hooks\";\nimport { Component, useState } from \"@odoo/owl\";\n\nexport class MobilePreviewSystrayItem extends Component {\n    static template = \"website.MobilePreviewSystrayItem\";\n    static props = {};\n    setup() {\n        this.websiteService = useService(\"website\");\n        this.state = useState(this.websiteService.context);\n    }\n\n    onClick() {\n        this.websiteService.context.isMobile = !this.websiteService.context.isMobile;\n    }\n}\n", "import { Component, useState, xml } from \"@odoo/owl\";\nimport { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\nimport { useDropdownState } from \"@web/core/dropdown/dropdown_hooks\";\nimport { useHotkey } from \"@web/core/hotkeys/hotkey_hook\";\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 { sprintf } from \"@web/core/utils/strings\";\nimport { redirect } from \"@web/core/utils/urls\";\nimport { InstallModuleDialog } from \"./install_module_dialog\";\n\nexport const MODULE_STATUS = {\n    NOT_INSTALLED: \"NOT_INSTALLED\",\n    INSTALLING: \"INSTALLING\",\n    FAILED_TO_INSTALL: \"FAILED_TO_INSTALL\",\n    INSTALLED: \"INSTALLED\",\n};\n\nexport class NewContentSystrayItem extends Component {\n    static template = \"website.NewContentSystrayItem\";\n    static components = { Dropdown, DropdownItem };\n    static props = {\n        onNewPage: Function,\n    };\n\n    setup() {\n        this.orm = useService(\"orm\");\n        this.dialogs = useService(\"dialog\");\n        this.website = useService(\"website\");\n        this.action = useService(\"action\");\n\n        this.isDesigner = this.website.isDesigner;\n        this.dropdown = useDropdownState();\n\n        this.newContentText = {\n            failed: _t('Failed to install \"%s\"'),\n            installInProgress: _t(\"The installation of an App is already in progress.\"),\n            installNeeded: _t('Do you want to install the \"%s\" App?'),\n            installPleaseWait: _t('Installing \"%s\"'),\n        };\n\n        this.state = useState({\n            newContentElements: [\n                {\n                    moduleName: \"website_blog\",\n                    moduleXmlId: \"base.module_website_blog\",\n                    status: MODULE_STATUS.NOT_INSTALLED,\n                    icon: \"/website_blog/static/description/icon.png\",\n                    title: _t(\"Blog Post\"),\n                    description: _t(\"Write a new article\"),\n                },\n                {\n                    moduleName: \"website_event\",\n                    moduleXmlId: \"base.module_website_event\",\n                    status: MODULE_STATUS.NOT_INSTALLED,\n                    icon: \"/website_event/static/description/icon.png\",\n                    title: _t(\"Event\"),\n                    description: _t(\"Launch an event, start registrations\"),\n                },\n                {\n                    moduleName: \"website_forum\",\n                    moduleXmlId: \"base.module_website_forum\",\n                    status: MODULE_STATUS.NOT_INSTALLED,\n                    icon: \"/website_forum/static/description/icon.png\",\n                    redirectUrl: \"/forum\",\n                    title: _t(\"Forum\"),\n                    description: _t(\"Set up a new forum\"),\n                },\n                {\n                    moduleName: \"website_hr_recruitment\",\n                    moduleXmlId: \"base.module_website_hr_recruitment\",\n                    status: MODULE_STATUS.NOT_INSTALLED,\n                    icon: \"/website_hr_recruitment/static/description/icon.png\",\n                    title: _t(\"Job Position\"),\n                    description: _t(\"Post a new job offer\"),\n                },\n                {\n                    moduleName: \"website_sale\",\n                    moduleXmlId: \"base.module_website_sale\",\n                    status: MODULE_STATUS.NOT_INSTALLED,\n                    icon: \"/website_sale/static/description/icon.png\",\n                    title: _t(\"Product\"),\n                    description: _t(\"Sell online\"),\n                },\n                {\n                    moduleName: \"website_slides\",\n                    moduleXmlId: \"base.module_website_slides\",\n                    status: MODULE_STATUS.NOT_INSTALLED,\n                    icon: \"/website_slides/static/description/icon.png\",\n                    title: _t(\"Course\"),\n                    description: _t(\"Teach with videos, slides and PDF\"),\n                },\n                {\n                    moduleName: \"website_livechat\",\n                    moduleXmlId: \"base.module_website_livechat\",\n                    status: MODULE_STATUS.NOT_INSTALLED,\n                    icon: \"/website_livechat/static/description/icon.png\",\n                    title: _t(\"Livechat Widget\"),\n                    description: _t(\"Add a livechat widget\"),\n                },\n            ],\n        });\n\n        useHotkey(\"escape\", () => this.dropdown.close(), {\n            isAvailable: () => this.dropdown.isOpen,\n        });\n    }\n\n    get newPageAttrs() {\n        return {\n            \"aria-label\": _t(\"New Page\"),\n            style: \"width: 300px\",\n        };\n    }\n\n    swapDescription(element) {\n        if (element.status !== MODULE_STATUS.NOT_INSTALLED) {\n            return;\n        }\n        if (!element.description2) {\n            element.description2 = sprintf(\n                _t('Install \"%s\"'),\n                this.modulesInfo[element.moduleName].name\n            );\n        }\n        const tmp = element.description;\n        element.description = element.description2;\n        element.description2 = tmp;\n    }\n\n    async toggleDropdown() {\n        if (this.dropdownWasAlreadyOpened) {\n            this.dropdown.isOpen = !this.dropdown.isOpen;\n            return;\n        }\n        this.dropdownWasAlreadyOpened = true;\n\n        const proms = [];\n\n        proms.push(\n            (async () => {\n                this.canInstall = user.isAdmin;\n                if (this.canInstall) {\n                    const moduleNames = this.state.newContentElements\n                        .filter(({ status }) => status === MODULE_STATUS.NOT_INSTALLED)\n                        .map(({ moduleName }) => moduleName);\n                    this.modulesInfo = {};\n                    for (const record of await this.orm\n                        .cache()\n                        .searchRead(\n                            \"ir.module.module\",\n                            [[\"name\", \"in\", moduleNames]],\n                            [\"id\", \"name\", \"shortdesc\"]\n                        )) {\n                        this.modulesInfo[record.name] = { id: record.id, name: record.shortdesc };\n                    }\n                }\n            })()\n        );\n\n        proms.push(\n            (async () => {\n                const modelsToCheck = [];\n                const elementsToUpdate = {};\n                for (const element of this.state.newContentElements) {\n                    if (element.model) {\n                        modelsToCheck.push(element.model);\n                        elementsToUpdate[element.model] = element;\n                    }\n                }\n                const accesses = await rpc(\n                    \"/website/check_new_content_access_rights\",\n                    {\n                        models: modelsToCheck,\n                    },\n                    { cache: true }\n                );\n                for (const [model, access] of Object.entries(accesses)) {\n                    elementsToUpdate[model].isDisplayed = access;\n                }\n            })()\n        );\n\n        await Promise.all(proms);\n        this.dropdown.open();\n\n        // Preload the new page templates so they are ready as soon as possible\n        rpc(\n            \"/website/get_new_page_templates\",\n            { context: { website_id: this.website.currentWebsiteId } },\n            { cache: true, silent: true }\n        );\n    }\n\n    get sortedNewContentElements() {\n        return this.state.newContentElements\n            .filter(({ status }) => status !== MODULE_STATUS.NOT_INSTALLED)\n            .concat(\n                this.state.newContentElements.filter(\n                    ({ status }) => status === MODULE_STATUS.NOT_INSTALLED\n                )\n            )\n            .filter((el) => (\"isDisplayed\" in el ? el.isDisplayed : user.isSystem));\n    }\n\n    async installModule(id, redirectUrl) {\n        await this.orm.silent.call(\"ir.module.module\", \"button_immediate_install\", [id]);\n        if (redirectUrl) {\n            this.website.prepareOutLoader();\n            redirect(redirectUrl);\n        } else {\n            const {\n                id,\n                metadata: { path, viewXmlid },\n            } = this.website.currentWebsite;\n            const url = new URL(path);\n            if (viewXmlid === \"website.page_404\") {\n                url.pathname = \"\";\n            }\n            // A reload is needed after installing a new module, to instantiate\n            // the feature with patches from the installed module.\n            this.website.prepareOutLoader();\n            const encodedPath = encodeURIComponent(url.toString());\n            redirect(`/odoo/action-website.website_preview?website_id=${id}&path=${encodedPath}`);\n        }\n    }\n\n    onClickNewContent(element) {\n        if (element.createNewContent) {\n            return element.createNewContent();\n        }\n\n        const { id, name } = this.modulesInfo[element.moduleName];\n        const dialogProps = {\n            title: element.title,\n            installationText: sprintf(this.newContentText.installNeeded, name),\n            installModule: async () => {\n                // Update the NewContentElement with installing icon and text.\n                this.state.newContentElements = this.state.newContentElements.map((el) => {\n                    if (el.moduleXmlId === element.moduleXmlId) {\n                        el.status = MODULE_STATUS.INSTALLING;\n                        el.icon = xml`<i class=\"fa fa-spin fa-circle-o-notch\"/>`;\n                        el.title = sprintf(this.newContentText.installPleaseWait, name);\n                    }\n                    return el;\n                });\n                this.website.showLoader({ title: _t(\"Building your %s\", name) });\n                try {\n                    await this.installModule(id, element.redirectUrl);\n                } catch (error) {\n                    this.website.hideLoader();\n                    // Update the NewContentElement with failure icon and text.\n                    this.state.newContentElements = this.state.newContentElements.map((el) => {\n                        if (el.moduleXmlId === element.moduleXmlId) {\n                            el.status = MODULE_STATUS.FAILED_TO_INSTALL;\n                            el.icon = xml`<i class=\"fa fa-exclamation-triangle\"/>`;\n                            el.title = sprintf(this.newContentText.failed, name);\n                        }\n                        return el;\n                    });\n                    console.error(error);\n                }\n            },\n        };\n        this.dialogs.add(InstallModuleDialog, dialogProps);\n    }\n\n    /**\n     * This method registers the action to perform when a new content is\n     * saved. The path must be computed once the record is saved, to\n     * perform the 'ir.act_window_close' action, which will be used when\n     * the dialog is closed to go to the correct website page.\n     */\n    async onAddContent(action, edition = false, context = null) {\n        this.action.doAction(action, {\n            additionalContext: context ? context : {},\n            onClose: (infos) => {\n                if (infos && !infos.dismiss) {\n                    this.website.goToWebsite({ path: infos.path, edition: edition });\n                    this.dropdown.close();\n                }\n            },\n            props: {\n                onSave: (record, params) => {\n                    if (record.resId) {\n                        const path = params.computePath();\n                        this.action.doAction({\n                            type: \"ir.actions.act_window_close\",\n                            infos: { path },\n                        });\n                    }\n                },\n            },\n        });\n    }\n}\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { registry } from \"@web/core/registry\";\nimport { CheckBox } from \"@web/core/checkbox/checkbox\";\nimport { useService, useBus } from \"@web/core/utils/hooks\";\nimport { Component, xml, useState } from \"@odoo/owl\";\nimport { OptimizeSEODialog } from \"@website/components/dialog/seo\";\nimport { checkAndNotifySEO } from \"@website/js/utils\";\n\nconst websiteSystrayRegistry = registry.category(\"website_systray\");\n\nexport class PublishSystrayItem extends Component {\n    static template = xml`\n        <div t-on-click=\"publishContent\" class=\"o_menu_systray_item o_website_publish_container d-flex ms-auto\" t-att-data-processing=\"state.processing and 1\">\n            <a href=\"#\" class=\"d-flex align-items-center mx-1 px-2 px-md-0\" data-hotkey=\"p\">\n                <span class=\"o_nav_entry d-none d-md-block mx-0 pe-1\" t-esc=\"this.label\"/>\n                <CheckBox value=\"state.published\" className=\"'form-switch d-flex justify-content-center m-0 pe-none'\"/>\n            </a>\n        </div>`;\n    static components = {\n        CheckBox,\n    };\n    static props = {};\n\n    setup() {\n        this.website = useService(\"website\");\n        this.orm = useService(\"orm\");\n        this.dialogService = useService(\"dialog\");\n        this.notificationService = useService(\"notification\");\n\n        this.state = useState({\n            published: this.website.currentWebsite.metadata.isPublished,\n            processing: false,\n        });\n\n        // TODO: website service should share a reactive\n        useBus(\n            websiteSystrayRegistry,\n            \"CONTENT-UPDATED\",\n            () => (this.state.published = this.website.currentWebsite.metadata.isPublished)\n        );\n    }\n\n    get label() {\n        return this.state.published ? _t(\"Published\") : _t(\"Unpublished\");\n    }\n\n    async publishContent() {\n        if (this.state.processing) {\n            return;\n        }\n        this.state.processing = true;\n        this.state.published = !this.state.published;\n        const {\n            metadata: { mainObject },\n        } = this.website.currentWebsite;\n        return this.orm.call(mainObject.model, \"website_publish_button\", [[mainObject.id]]).then(\n            async (published) => {\n                this.state.published = published;\n                if (published && this.website.currentWebsite.metadata.canOptimizeSeo) {\n                    const seo_data = await rpc(\"/website/get_seo_data\", {\n                        res_id: mainObject.id,\n                        res_model: mainObject.model,\n                    });\n                    checkAndNotifySEO(seo_data, OptimizeSEODialog, {\n                        notification: this.notificationService,\n                        dialog: this.dialogService,\n                    });\n                }\n                this.state.processing = false;\n                return published;\n            },\n            (err) => {\n                this.state.published = !this.state.published;\n                this.state.processing = false;\n                throw err;\n            }\n        );\n    }\n}\n", "/**\n * Checks if the 2 given URLs are the same, to prevent redirecting uselessly\n * from one to another.\n * It will consider naked URL and `www` URL as the same URL.\n * It will consider `https` URL `http` URL as the same URL.\n *\n * @param {string} url1\n * @param {string} url2\n * @returns {Boolean}\n */\nexport function isHTTPSorNakedDomainRedirection(url1, url2) {\n    try {\n        url1 = new URL(url1).host;\n        url2 = new URL(url2).host;\n    } catch {\n        // Incorrect URL, `false` URL..\n        return false;\n    }\n    return url1 === url2 || url1.replace(/^www\\./, \"\") === url2.replace(/^www\\./, \"\");\n}\n", "import { LocalOverlayContainer } from \"@html_editor/local_overlay_container\";\nimport {\n    Component,\n    onMounted,\n    onWillDestroy,\n    onWillStart,\n    onWillUnmount,\n    status,\n    useComponent,\n    useEffect,\n    useRef,\n    useState,\n    useSubEnv,\n} from \"@odoo/owl\";\nimport { LazyComponent, loadBundle } from \"@web/core/assets\";\nimport { browser } from \"@web/core/browser/browser\";\nimport { getActiveHotkey } from \"@web/core/hotkeys/hotkey_service\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { ResizablePanel } from \"@web/core/resizable_panel/resizable_panel\";\nimport { RPCError } from \"@web/core/network/rpc\";\nimport { Deferred } from \"@web/core/utils/concurrency\";\nimport { uniqueId } from \"@web/core/utils/functions\";\nimport { useChildRef, useService, useBus } from \"@web/core/utils/hooks\";\nimport { effect } from \"@web/core/utils/reactive\";\nimport { redirect } from \"@web/core/utils/urls\";\nimport { standardActionServiceProps } from \"@web/webclient/actions/action_service\";\nimport { AddPageDialog } from \"@website/components/dialog/add_page_dialog\";\nimport { ResourceEditor } from \"@website/components/resource_editor/resource_editor\";\nimport { isHTTPSorNakedDomainRedirection } from \"./utils\";\nimport { WebsiteSystrayItem } from \"./website_systray_item\";\nimport { renderToElement } from \"@web/core/utils/render\";\nimport { isBrowserChrome, isBrowserMicrosoftEdge } from \"@web/core/browser/feature_detection\";\nimport { router } from \"@web/core/browser/router\";\nimport { getScrollingElement } from \"@web/core/utils/scrolling\";\nimport { CreatePageMessage } from \"./create_page_message\";\n\nconst websiteSystrayRegistry = registry.category(\"website_systray\");\n\nexport class WebsiteBuilderClientAction extends Component {\n    static template = \"website.WebsiteBuilderClientAction\";\n    static components = {\n        LazyComponent,\n        LocalOverlayContainer,\n        ResizablePanel,\n        ResourceEditor,\n        CreatePageMessage,\n    };\n    static props = {\n        ...standardActionServiceProps,\n        editTranslations: { type: Boolean, optional: true },\n        enableEditor: { type: Boolean, optional: true },\n        path: { type: String, optional: true },\n        websiteId: { type: [Number, { value: false }], optional: true },\n    };\n\n    static extractProps(action) {\n        return {\n            editTranslations: action.params?.edit_translations || false,\n            enableEditor: action.params?.enable_editor || false,\n            path: action.params?.path,\n            websiteId: action.params?.website_id || false,\n        };\n    }\n\n    setup() {\n        this.target = null;\n        this.orm = useService(\"orm\");\n        this.notification = useService(\"notification\");\n        this.dialog = useService(\"dialog\");\n        this.websiteService = useService(\"website\");\n        this.ui = useService(\"ui\");\n        this.title = useService(\"title\");\n        this.hotkeyService = useService(\"hotkey\");\n        this.websiteService.websiteRootInstance = undefined;\n        this.iframeFallbackUrl = \"/website/iframefallback\";\n        this.iframefallback = useRef(\"iframefallback\");\n\n        this.websiteContent = useRef(\"iframe\");\n        this.cleanups = [];\n\n        this.snippetsTemplate = \"website.snippets\";\n        // Track iframe navigation state\n        this.isNavigatingToAnotherPage = null;\n\n        useSubEnv({\n            builderRef: useRef(\"container\"),\n        });\n        this.state = useState({ isEditing: false, showSidebar: true, key: 1, is404: false });\n        this.websiteContext = useState(this.websiteService.context);\n        this.component = useComponent();\n\n        useBus(\n            websiteSystrayRegistry,\n            \"CONTENT-UPDATED\",\n            () => (this.state.is404 = this.websiteService.is404)\n        );\n\n        onMounted(() => {\n            // You can't wait for rendering because the Builder depends on the\n            // page style synchronously.\n            effect(\n                (websiteContext) => {\n                    if (status(this.component) === \"destroyed\") {\n                        return;\n                    }\n                    this.toggleIsMobile(websiteContext.isMobile);\n                },\n                [this.websiteContext]\n            );\n        });\n\n        this.overlayRef = useChildRef();\n        useSubEnv({\n            localOverlayContainerKey: uniqueId(\"website\"),\n        });\n        this.websitePreviewRef = useRef(\"website_preview\");\n\n        onWillStart(async () => {\n            const updateWebsiteId = (websiteId) => {\n                const encodedPath = encodeURIComponent(this.path);\n                this.initialUrl = `/website/force/${encodeURIComponent(\n                    websiteId\n                )}?path=${encodedPath}`;\n                this.websiteService.currentWebsiteId = websiteId;\n            };\n            const proms = [\n                this.websiteService.fetchWebsites(),\n                this.websiteService.fetchUserGroups(),\n            ];\n            if (this.websiteId) {\n                updateWebsiteId(this.websiteId);\n                await Promise.all(proms);\n            } else {\n                const [backendWebsiteRepr] = await Promise.all([\n                    this.orm.call(\"website\", \"get_current_website\"),\n                    ...proms,\n                ]);\n                updateWebsiteId(backendWebsiteRepr[0]);\n            }\n        });\n        onMounted(() => {\n            this.addListeners(document);\n            this.addSystrayItems();\n            const edition = !!(this.enableEditor || this.editTranslations);\n            if (edition) {\n                this.onEditPage();\n            }\n            if (!this.ui.isSmall) {\n                // preload builder and snippets so clicking on \"edit\" is faster\n                loadBundle(\"website.website_builder_assets\").then(() => {\n                    this.env.services[\"html_builder.snippets\"]\n                        .getSnippetModel(this.snippetsTemplate)\n                        .reload({\n                            lang: this.websiteService.currentWebsite?.default_lang_id.code,\n                            website_id: this.websiteService.currentWebsite?.id,\n                        });\n                });\n            }\n        });\n        onWillUnmount(() => {\n            for (const fn of this.cleanups) {\n                fn();\n            }\n        });\n        this.publicRootReady = new Deferred();\n        this.setIframeLoaded();\n        this.addSystrayItems();\n        onWillDestroy(() => {\n            websiteSystrayRegistry.remove(\"website.WebsiteSystrayItem\");\n            this.websiteService.currentWebsiteId = null;\n            websiteSystrayRegistry.trigger(\"EDIT-WEBSITE\");\n        });\n\n        effect(\n            (state) => {\n                this.websiteContext.edition = state.isEditing;\n                if (!state.isEditing) {\n                    this.addSystrayItems();\n                }\n            },\n            [this.state]\n        );\n        useEffect(\n            (isEditing) => {\n                document.querySelector(\"body\").classList.toggle(\"o_builder_open\", isEditing);\n                if (isEditing) {\n                    // When entering edit mode, the navbar animates upwards.\n                    // To avoid an abrupt disappearance, we delay adding the\n                    // 'd-none' class\n                    this.navBarTimeout = setTimeout(() => {\n                        websiteSystrayRegistry.remove(\"website.WebsiteSystrayItem\");\n                        websiteSystrayRegistry.trigger(\"EDIT-WEBSITE\");\n                        document\n                            .querySelector(\".o_builder_open .o_main_navbar\")\n                            .classList.add(\"d-none\");\n                    }, 200);\n                } else {\n                    document.querySelector(\".o_main_navbar\")?.classList.remove(\"d-none\");\n                }\n                return () => clearTimeout(this.navBarTimeout);\n            },\n            () => [this.state.isEditing]\n        );\n    }\n\n    get testMode() {\n        return false;\n    }\n\n    get websiteBuilderProps() {\n        const iframeLoaded = this.iframeLoaded.then((el) =>\n            this.waitForIframeReady().then(() => el)\n        );\n        const builderProps = {\n            closeEditor: this.reloadIframeAndCloseEditor.bind(this),\n            editableSelector: \"#wrapwrap\",\n            reloadEditor: this.reloadEditor.bind(this),\n            snippetsName: this.snippetsTemplate,\n            toggleMobile: this.toggleMobile.bind(this),\n            installSnippetModule: this.installSnippetModule.bind(this),\n            overlayRef: this.overlayRef,\n            iframeLoaded: iframeLoaded,\n            isMobile: this.websiteContext.isMobile,\n            config: {\n                initialTarget: this.target,\n                initialTab: this.initialTab || this.translation ? \"customize\" : \"blocks\",\n                builderSidebar: {\n                    withHiddenSidebar: async (cb) => {\n                        try {\n                            this.state.showSidebar = false;\n                            return await cb();\n                        } finally {\n                            this.state.showSidebar = true;\n                        }\n                    },\n                    // TODO: remove `toggle` in master\n                    toggle: (show) => {\n                        this.state.showSidebar = show ?? !this.state.showSidebar;\n                    },\n                },\n                isTranslationMode: this.translation,\n            },\n        };\n        return { translation: this.translation, builderProps };\n    }\n\n    get systrayProps() {\n        return {\n            onNewPage: this.onNewPage.bind(this),\n            onEditPage: this.onEditPage.bind(this),\n            iframeLoaded: this.iframeLoaded,\n        };\n    }\n\n    addSystrayItems() {\n        if (!websiteSystrayRegistry.contains(\"website.WebsiteSystrayItem\")) {\n            websiteSystrayRegistry.add(\n                \"website.WebsiteSystrayItem\",\n                {\n                    Component: WebsiteSystrayItem,\n                    props: this.systrayProps,\n                    isDisplayed: () => true,\n                },\n                { sequence: -100 }\n            );\n            websiteSystrayRegistry.trigger(\"EDIT-WEBSITE\");\n        }\n    }\n\n    onNewPage(keepUrl = false) {\n        const params = {\n            websiteId: this.websiteService.currentWebsite.id,\n        };\n        if (keepUrl) {\n            params.forcedURL = this.websiteService.currentLocation;\n        }\n        this.dialog.add(AddPageDialog, params);\n    }\n\n    async onEditPage() {\n        if (!this.websiteContext) {\n            await this.iframeLoaded;\n        }\n        this.websiteContext.showResourceEditor = false;\n        this.blockIframe();\n\n        // Wait for navigation to complete if currently navigating\n        if (this.isNavigatingToAnotherPage) {\n            await this.isNavigatingToAnotherPage;\n        }\n\n        await this.loadIframeAndBundles(true);\n        window.document.dispatchEvent(\n            new CustomEvent(\"edit_page\", {\n                detail: {\n                    iframeDocument: this.websiteContent.el.contentDocument.document,\n                },\n            })\n        );\n        this.unblockIframe();\n        this.state.isEditing = true;\n    }\n    /**\n     * @param {Boolean} isEditing\n     */\n    async loadIframeAndBundles(isEditing) {\n        await this.iframeLoaded;\n        if (isEditing) {\n            await this.publicRootReady;\n            await this.loadAssetsEditBundle();\n        }\n    }\n\n    async loadAssetsEditBundle() {\n        // at this point, the iframe should be loaded. In a normal user flow, the\n        // iframe had the time to load and start the js, and has an environment\n        // with services. But it could happen (most likely in tours or tests) that\n        // the js is not loaded yet. If we load the assets_inside_builder_iframe bundle\n        // now, and if it comes first, we'll get a crash. So we make sure that we\n        // properly wait for the iframe to be completely ready.\n        await this.waitForIframeReady();\n        await Promise.all([\n            loadBundle(\"website.assets_inside_builder_iframe\", {\n                targetDoc: this.websiteContent.el.contentDocument,\n            }),\n        ]);\n    }\n\n    /**\n     * This replaces the browser url (/odoo/website...) with\n     * the iframe's url (it is clearer for the user).\n     */\n    replaceBrowserUrl() {\n        const iframe = this.websiteContent.el;\n        if (!iframe || !iframe.contentWindow) {\n            return;\n        }\n\n        if (\n            !isHTTPSorNakedDomainRedirection(\n                iframe.contentWindow.location.origin,\n                window.location.origin\n            )\n        ) {\n            // If another domain ends up loading in the iframe (for example,\n            // if the iframe is being redirected and has no initial URL, so it\n            // loads \"about:blank\"), do not push that into the history\n            // state as that could prevent the user from going back and could\n            // trigger a traceback.\n            history.replaceState(history.state, document.title, \"/odoo\");\n            return;\n        }\n        const currentTitle = iframe.contentDocument.title;\n        history.replaceState(history.state, currentTitle, iframe.contentDocument.location.href);\n        this.title.setParts({ action: currentTitle });\n        const frontendIconEl = iframe.contentDocument.querySelector(\"link[rel~='icon']\");\n        if (frontendIconEl) {\n            document.querySelector(\"link[rel~='icon']\").href = frontendIconEl.href;\n        }\n    }\n\n    onIframeLoad(ev) {\n        // FIX Chrome-only. If you have the backend in a language A but the\n        // website in English only, you can 1) modify a record's (event,\n        // product...) name in language A (say \"New Name\").\n        // 2) visit the page `/new-name-11` => the server will redirect you to\n        // the English page `/origin-11`, which is the only one existing.\n        // Chrome caches the redirection.\n        // 3) give the same name in English as in language A, try to visit\n        // => the server now wants to access `/new-name-11`\n        // => Chrome uses the cache to redirect `/new-name-11` to `/origin-11`,\n        // => the server tries to redirect to `/new-name-11` => loop.\n        // Chrome injects a \"Too many redirects\" layout in the iframe, which in\n        // turn raises a CORS error when the app tries to update the iframe.\n        // If we detect that behavior, we reload the iframe with a new query\n        // parameter, so that it's not cached for Chrome.\n        const iframe = this.websiteContent.el;\n        iframe.contentDocument.body.setAttribute(\"is-ready\", \"false\");\n        if (isBrowserChrome() && !iframe.src.includes(\"iframe_reload\")) {\n            try {\n                /* eslint-disable no-unused-expressions */\n                iframe.contentWindow.location.href;\n            } catch (err) {\n                if (err.name === \"SecurityError\") {\n                    ev.stopImmediatePropagation();\n                    // Note that iframe's `src` is the URL used to start the\n                    // website preview, it's not sync'd with iframe navigation.\n                    const srcUrl = new URL(iframe.src);\n                    const pathUrl = new URL(srcUrl.searchParams.get(\"path\"), srcUrl.origin);\n                    pathUrl.searchParams.set(\"iframe_reload\", \"1\");\n                    srcUrl.searchParams.set(\"path\", `${pathUrl.pathname}${pathUrl.search}`);\n                    // We could inject `pathUrl` directly but keep the same\n                    // expected URL format `/website/force/1?path=..`\n                    iframe.src = srcUrl.toString();\n                    return;\n                } else {\n                    throw err;\n                }\n            }\n        }\n        if (this.lastPageURL !== iframe.contentWindow.location.href) {\n            // Hide Ace Editor when moving to another page.\n            this.websiteService.context.showResourceEditor = false;\n        }\n        this.websiteService.pageDocument = this.websiteContent.el.contentDocument;\n        const url = new URL(this.websiteService.contentWindow.location.href);\n        if (url.searchParams.has(\"edit_translations\")) {\n            deleteQueryParam(\"edit_translations\", this.websiteService.contentWindow, true);\n        }\n\n        this.toggleIsMobile(this.websiteContext.isMobile);\n        this.preparePublicRootReady();\n        this.setupClickListener();\n        this.replaceBrowserUrl();\n        this.resolveIframeLoaded();\n        this.addWelcomeMessage();\n        this.websiteService.hideLoader();\n        this.lastPageURL = iframe.contentWindow.location.href;\n\n        if (this.isNavigatingToAnotherPage) {\n            this.isNavigatingToAnotherPage.resolve();\n            this.isNavigatingToAnotherPage = null;\n        }\n    }\n\n    blockIframe() {\n        this.websiteContent.el.setAttribute(\"inert\", \"\");\n    }\n    unblockIframe() {\n        this.websiteContent.el.removeAttribute(\"inert\");\n    }\n\n    setupClickListener() {\n        // The clicks on the iframe are listened, so that links with external\n        // redirections can be opened in the top window.\n        this.websiteContent.el.contentDocument.addEventListener(\"click\", (ev) => {\n            if (!this.state.isEditing) {\n                // Forward clicks to close backend client action's navbar\n                // dropdowns.\n                this.websiteContent.el.dispatchEvent(new MouseEvent(\"click\", ev));\n            } else {\n                // When in edit mode, prevent the default behaviours of clicks\n                // as to avoid DOM changes not handled by the editor.\n                // (Such as clicking on a link that triggers navigating to\n                // another page.)\n                ev.preventDefault();\n            }\n            const linkEl = ev.target.closest(\"[href]\");\n            if (!linkEl) {\n                return;\n            }\n\n            const { href, target } = linkEl;\n            if (href && target !== \"_blank\" && !this.state.isEditing) {\n                if (isTopWindowURL(linkEl)) {\n                    ev.preventDefault();\n                    try {\n                        browser.location.assign(href);\n                    } catch {\n                        this.notification.add(_t(\"%s is not a valid URL.\", href), {\n                            title: _t(\"Invalid URL\"),\n                            type: \"danger\",\n                        });\n                    }\n                } else if (\n                    this.websiteContent.el.contentWindow.location.pathname !==\n                    new URL(href).pathname\n                ) {\n                    // This scenario triggers a navigation inside the iframe.\n                    this.websiteService.websiteRootInstance = undefined;\n\n                    this.isNavigatingToAnotherPage = new Deferred();\n                }\n            }\n        });\n    }\n\n    get editTranslations() {\n        return this.props.editTranslations || !!router.current.edit_translations;\n    }\n\n    get enableEditor() {\n        return this.props.enableEditor || !!router.current.enable_editor;\n    }\n\n    get path() {\n        let path = this.props.path || router.current.path;\n        if (path) {\n            const url = new URL(path, window.location.origin);\n            if (isTopWindowURL(url)) {\n                // If the client action is initialized with a path that should\n                // not be opened inside the iframe (= something we would want to\n                // open on the top window), we consider that this is not a valid\n                // flow. Instead of trying to open it on the top window, we\n                // initialize the iframe with the website homepage...\n                path = \"/\";\n            } else {\n                // ... otherwise, the path still needs to be normalized (as it\n                // would be if the given path was used as an href of a  <a/>\n                // element).\n                path = url.pathname + url.search;\n            }\n        } else {\n            path = \"/\";\n        }\n        return path;\n    }\n\n    get websiteId() {\n        return this.props.websiteId || router.current.website_id || false;\n    }\n\n    waitForIframeReady() {\n        return new Promise((resolve) => {\n            const doc = this.websiteContent.el.contentDocument;\n            if (doc.body.hasAttribute(\"is-ready\")) {\n                resolve();\n            } else {\n                const observer = new MutationObserver(() => {\n                    if (doc.body.getAttribute(\"is-ready\") === \"true\") {\n                        observer.disconnect();\n                        resolve();\n                    }\n                });\n                observer.observe(doc.body, { attributes: true, attributeFilter: [\"is-ready\"] });\n            }\n        });\n    }\n\n    async reloadEditor(param = {}) {\n        this.initialTab = param.initialTab;\n        this.target = param.target || null;\n        await this.reloadIframe(this.state.isEditing, param.url);\n        // trigger an new instance of the builder menu\n        this.state.key++;\n    }\n\n    async reloadIframeAndCloseEditor() {\n        this.initialTab = null;\n        this.target = null;\n        const isEditing = false;\n        this.state.isEditing = isEditing;\n        this.addSystrayItems();\n        await this.reloadIframe(isEditing);\n    }\n\n    async reloadIframe(isEditing = true, url) {\n        this.ui.block();\n        this.preparePublicRootReady();\n        this.setIframeLoaded();\n        this.websiteService.websiteRootInstance = undefined;\n        if (url) {\n            const urlObj = new URL(url, this.websiteContent.el.contentWindow.location);\n            const pathSegments = urlObj.pathname.split(\"/\").map(encodeURIComponent);\n            const encodedPath = pathSegments.join(\"/\");\n            this.websiteContent.el.contentWindow.location.href = new URL(\n                encodedPath,\n                this.websiteContent.el.contentWindow.location\n            );\n        } else {\n            this.websiteContent.el.contentWindow.location.reload();\n        }\n        await this.loadIframeAndBundles(isEditing);\n        this.ui.unblock();\n    }\n\n    reloadWebClient() {\n        const currentPath = encodeURIComponent(window.location.pathname);\n        const websiteId = this.websiteService.currentWebsite.id;\n        redirect(\n            `/odoo/action-website.website_preview?website_id=${encodeURIComponent(\n                websiteId\n            )}&path=${currentPath}&enable_editor=1`\n        );\n    }\n\n    async installSnippetModule(snippet, beforeInstall) {\n        this.dialog.closeAll();\n        try {\n            this.ui.block();\n            await beforeInstall();\n            await this.orm.call(\"ir.module.module\", \"button_immediate_install\", [\n                [parseInt(snippet.moduleId)],\n            ]);\n            this.reloadWebClient();\n        } catch (e) {\n            if (e instanceof RPCError) {\n                const message = _t(\"Could not install module %s\", snippet.moduleDisplayName);\n                this.notification.add(message, {\n                    type: \"danger\",\n                    sticky: true,\n                });\n                return;\n            }\n            throw e;\n        } finally {\n            this.ui.unblock();\n        }\n    }\n\n    preparePublicRootReady() {\n        const deferred = new Deferred();\n        this.publicRootReady = deferred;\n        this.websiteContent.el.contentWindow.addEventListener(\n            \"PUBLIC-ROOT-READY\",\n            (event) => {\n                this.websiteService.websiteRootInstance = event.detail.rootInstance;\n                deferred.resolve();\n            },\n            { once: true }\n        );\n    }\n\n    async addWelcomeMessage() {\n        if (this.websiteService.isRestrictedEditor && !this.state.isEditing) {\n            const wrapEl = this.websiteContent.el.contentDocument.querySelector(\n                \"#wrapwrap.homepage #wrap\"\n            );\n            if (wrapEl && !wrapEl.innerHTML.trim()) {\n                this.welcomeMessageEl = renderToElement(\"website.homepage_editor_welcome_message\");\n                wrapEl.replaceChildren(this.welcomeMessageEl);\n            }\n        }\n    }\n\n    setIframeLoaded() {\n        this.iframeLoaded = new Promise((resolve) => {\n            this.resolveIframeLoaded = () => {\n                this.hotkeyService.registerIframe(this.websiteContent.el);\n                this.websiteContent.el.contentWindow.addEventListener(\n                    \"beforeunload\",\n                    this.onPageUnload.bind(this)\n                );\n\n                this.addListeners(this.websiteContent.el.contentDocument);\n                this.iframefallback.el?.contentDocument.documentElement.replaceChildren();\n                resolve(this.websiteContent.el);\n            };\n        });\n    }\n\n    onPageUnload() {\n        // If the iframe is currently displaying an XML file, the body does not\n        // exist, so we do not replace the iframefallback content.\n        const websiteDoc = this.websiteContent.el?.contentDocument;\n        const fallBackDoc = this.iframefallback.el?.contentDocument;\n        if (!this.state.isEditing && websiteDoc && fallBackDoc) {\n            fallBackDoc.documentElement.replaceWith(websiteDoc.documentElement.cloneNode(true));\n            const currentScrollEl = getScrollingElement(websiteDoc);\n            const scrollElement = getScrollingElement(fallBackDoc);\n            scrollElement.scrollTop = currentScrollEl.scrollTop;\n            this.cleanIframeFallback();\n        }\n    }\n\n    cleanIframeFallback() {\n        // Remove autoplay in all iframes urls so videos are not\n        const iframesEl = this.iframefallback.el.contentDocument.querySelectorAll(\n            'iframe[src]:not([src=\"\"])'\n        );\n        for (const iframeEl of iframesEl) {\n            const url = new URL(iframeEl.src);\n            url.searchParams.delete(\"autoplay\");\n            iframeEl.src = url.toString();\n        }\n    }\n\n    toggleMobile() {\n        // Adding the mobile class directly, to not wait for the component\n        // re-rendering.\n        this.websiteService.context.isMobile = !this.websiteService.context.isMobile;\n    }\n\n    toggleIsMobile(isMobile) {\n        this.websitePreviewRef.el.classList.toggle(\"o_is_mobile\", isMobile);\n        this.websiteContent.el?.contentDocument.documentElement.classList.toggle(\n            \"o_is_mobile\",\n            isMobile\n        );\n    }\n\n    get aceEditorWidth() {\n        const storedWidth = browser.localStorage.getItem(\"ace_editor_width\");\n        return storedWidth ? parseInt(storedWidth) : 720;\n    }\n\n    onResourceEditorResize(width) {\n        browser.localStorage.setItem(\"ace_editor_width\", width);\n    }\n\n    get translation() {\n        return this.websiteService.currentWebsite.metadata.translatable;\n    }\n\n    /**\n     * Handles refreshing while the website preview is active.\n     * Makes it possible to stay in the backend after an F5 or CTRL-R keypress.\n     * Cannot be done through the hotkey service due to F5.\n     *\n     * @param {KeyboardEvent} ev\n     */\n    onKeydownRefresh(ev) {\n        const hotkey = getActiveHotkey(ev);\n        if (hotkey !== \"control+r\" && hotkey !== \"f5\") {\n            return;\n        }\n        // The iframe isn't loaded yet: fallback to default refresh.\n        if (this.websiteService.contentWindow === undefined) {\n            return;\n        }\n        ev.preventDefault();\n        const path = this.websiteService.contentWindow.location;\n        const debugMode = this.env.debug ? `&debug=${this.env.debug}` : \"\";\n        redirect(\n            `/odoo/action-website.website_preview?path=${encodeURIComponent(path)}${debugMode}`\n        );\n    }\n\n    /**\n     * Registers listeners on both the main document and the iframe document.\n     * It can mostly be done through the hotkey service, but not all keys are\n     * whitelisted, specifically F5 which we want to override.\n     *\n     * @param {HTMLElement} target - document or iframe document\n     */\n    addListeners(target) {\n        const listener = (ev) => this.onKeydownRefresh(ev);\n        target.addEventListener(\"keydown\", listener);\n        this.cleanups.push(() => {\n            target.removeEventListener(\"keydown\", listener);\n        });\n    }\n\n    get isMicrosoftEdge() {\n        return isBrowserMicrosoftEdge();\n    }\n}\n\nfunction deleteQueryParam(param, target = window, adaptBrowserUrl = false) {\n    const url = new URL(target.location.href);\n    url.searchParams.delete(param);\n    // TODO: maybe to use in the action service\n    target.history.replaceState(target.history.state, null, url);\n    if (adaptBrowserUrl) {\n        deleteQueryParam(param);\n    }\n}\n\n/**\n * Returns true if the url should be opened in the top\n * window.\n *\n * @param host {string} host of the route.\n * @param pathname {string} path of the route.\n */\nfunction isTopWindowURL({ host, pathname }) {\n    for (const fn of registry.category(\"isTopWindowURL\").getAll()) {\n        if (fn({ host, pathname })) {\n            return true;\n        }\n    }\n    return false;\n}\n\nregistry\n    .category(\"isTopWindowURL\")\n    .add(\"html_builder.website_builder_action\", ({ host, pathname }) => {\n        const backendRoutes = [\"/web\", \"/web/session/logout\", \"/odoo\"];\n        return (\n            host !== window.location.host ||\n            (pathname &&\n                (backendRoutes.includes(pathname) ||\n                    pathname.startsWith(\"/@/\") ||\n                    pathname.startsWith(\"/odoo/\") ||\n                    pathname.startsWith(\"/web/content/\") ||\n                    pathname.startsWith(\"/document/share/\")))\n        );\n    });\n\nregistry.category(\"actions\").add(\"website_preview\", WebsiteBuilderClientAction);\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { browser } from \"@web/core/browser/browser\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\nimport { session } from \"@web/session\";\nimport { Component } from \"@odoo/owl\";\nimport { isHTTPSorNakedDomainRedirection } from \"./utils\";\n\nexport class WebsiteSwitcherSystrayItem extends Component {\n    static template = \"website.WebsiteSwitcherSystrayItem\";\n    static components = {\n        Dropdown,\n        DropdownItem,\n    };\n    static props = {};\n    setup() {\n        this.websiteService = useService(\"website\");\n        this.notificationService = useService(\"notification\");\n        this.actionService = useService(\"action\");\n    }\n\n    getElements() {\n        return this.websiteService.websites.map((website) => ({\n            name: website.name,\n            id: website.id,\n            domain: website.domain,\n            dataset: Object.assign(\n                {\n                    \"data-website-id\": website.id,\n                },\n                website.domain\n                    ? {}\n                    : {\n                          \"data-tooltip\": _t(\"This website does not have a domain configured.\"),\n                          \"data-tooltip-position\": \"left\",\n                      }\n            ),\n            callback: () => {\n                if (\n                    !session.website_bypass_domain_redirect && // Used by the Odoo support (bugs to be expected)\n                    website.domain &&\n                    !isHTTPSorNakedDomainRedirection(website.domain, window.location.origin)\n                ) {\n                    const {\n                        location: { pathname, search, hash },\n                    } = this.websiteService.contentWindow;\n                    const path = pathname + search + hash;\n                    // Automatically converts Unicode domains (e.g. d\u00fcsseldorf.com) to\n                    // punycode (ASCII-safe) using the native URL API\n                    const url = new URL(\"/web\", website.domain);\n                    url.hash = new URLSearchParams({\n                        action: \"website.website_preview\",\n                        path: path,\n                        website_id: website.id,\n                    });\n                    window.location.href = url;\n                } else {\n                    this.websiteService.goToWebsite({\n                        websiteId: website.id,\n                        path: \"\",\n                        lang: \"default\",\n                    });\n                    if (!website.domain) {\n                        const closeFn = this.notificationService.add(\n                            _t(\"Add a domain to your website.\"),\n                            {\n                                type: \"warning\",\n                                sticky: true,\n                                buttons: [\n                                    {\n                                        onClick: () => {\n                                            this.actionService.doAction(\n                                                \"website.action_website_configuration\"\n                                            );\n                                            closeFn();\n                                        },\n                                        primary: true,\n                                        name: \"Settings\",\n                                    },\n                                ],\n                            }\n                        );\n                        browser.setTimeout(closeFn, 7000);\n                    }\n                }\n            },\n            class:\n                website.id === this.websiteService.currentWebsite.id\n                    ? \"text-truncate active\"\n                    : \"text-truncate\",\n        }));\n    }\n}\n", "import { Component, onWillStart } from \"@odoo/owl\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { EditInBackendSystrayItem } from \"./edit_in_backend\";\nimport { EditWebsiteSystrayItem } from \"./edit_website_systray_item\";\nimport { MobilePreviewSystrayItem } from \"./mobile_preview_systray\";\nimport { NewContentSystrayItem } from \"./new_content_systray_item\";\nimport { PublishSystrayItem } from \"./publish_website_systray_item\";\nimport { WebsiteSwitcherSystrayItem } from \"./website_switcher_systray_item\";\n\nexport class WebsiteSystrayItem extends Component {\n    static template = \"website.WebsiteSystrayItem\";\n    static props = {\n        onNewPage: { type: Function },\n        onEditPage: { type: Function },\n        iframeLoaded: { type: Object },\n    };\n    static components = {\n        MobilePreviewSystrayItem,\n        WebsiteSwitcherSystrayItem,\n        EditInBackendSystrayItem,\n        NewContentSystrayItem,\n        EditWebsiteSystrayItem,\n        PublishSystrayItem,\n    };\n\n    setup() {\n        onWillStart(async () => {\n            this.iframeEl = await this.props.iframeLoaded;\n        });\n        this.website = useService(\"website\");\n    }\n\n    get hasMultiWebsites() {\n        return this.website.websites.length > 1;\n    }\n\n    get canPublish() {\n        return this.website.currentWebsite && this.website.currentWebsite.metadata.canPublish;\n    }\n\n    get isRestrictedEditor() {\n        return this.website.isRestrictedEditor;\n    }\n\n    get hasEditableRecordInBackend() {\n        return (\n            this.website.currentWebsite &&\n            this.website.currentWebsite.metadata.editableInBackend &&\n            // TODO the functional desire is to have read access on all\n            // \"website\" models for all internal users, but there are many\n            // fields preventing that... to review in master (should views just\n            // be smarter? should they be more basic in the website app?). This\n            // disables the form view access feature for some models that are\n            // known to lead to access rights lock. At least, list views are\n            // accessible at the moment.\n            // See WEBSITE_RECORDS_VIEWS_ACCESS_RIGHTS.\n            (!this.website.currentWebsite.metadata.mainObject ||\n                ![\"event.event\", \"hr.job\"].includes(\n                    this.website.currentWebsite.metadata.mainObject.model\n                ) ||\n                this.website.currentWebsite.metadata.canPublish)\n        );\n    }\n\n    get canEdit() {\n        return (\n            this.website.currentWebsite &&\n            (this.website.currentWebsite.metadata.editable ||\n                this.website.currentWebsite.metadata.translatable)\n        );\n    }\n\n    get editWebsiteSystrayItemProps() {\n        return {\n            onNewPage: this.props.onNewPage,\n            onEditPage: this.props.onEditPage,\n            iframeEl: this.iframeEl,\n        };\n    }\n}\n", "import { PageDependencies } from \"@website/components/dialog/page_properties\";\nimport { standardFieldProps } from \"@web/views/fields/standard_field_props\";\nimport { UrlField, urlField } from \"@web/views/fields/url/url_field\";\nimport { registry } from \"@web/core/registry\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { debounce } from \"@web/core/utils/timing\";\nimport { Component, useEffect, useRef } from \"@odoo/owl\";\n\n/**\n * Displays website page dependencies and URL redirect options when the page URL\n * is updated.\n */\nclass PageUrlField extends UrlField {\n    static components = { PageDependencies };\n    static template = \"website.PageUrlField\";\n    static defaultProps = {\n        ...UrlField.defaultProps,\n        websitePath: true,\n    };\n\n    setup() {\n        super.setup();\n        this.serverUrl = `${window.location.origin}/`;\n        this.inputRef = useRef(\"input\");\n\n        // Trigger onchange api on input event to display redirection\n        // parameters as soon as the user types.\n        // TODO should find a way to do this more automatically (and option in\n        // the framework? or at least a t-on-input?)\n        useEffect(\n            (inputEl) => {\n                if (inputEl) {\n                    const originalValue = inputEl.value;\n                    let previousValueChanged = false;\n                    const fireChangeEvent = debounce(() => {\n                        const currentValue = inputEl.value;\n                        const valueChanged = currentValue !== originalValue;\n                        if (valueChanged !== previousValueChanged) {\n                            if (currentValue[0] !== \"/\") {\n                                inputEl.value = `/${currentValue}`;\n                            }\n                            inputEl.dispatchEvent(new Event(\"change\"));\n                            inputEl.value = currentValue;\n                            previousValueChanged = valueChanged;\n                        }\n                    }, 100);\n\n                    inputEl.addEventListener(\"input\", fireChangeEvent);\n                    return () => {\n                        inputEl.removeEventListener(\"input\", fireChangeEvent);\n                    };\n                }\n            },\n            () => [this.inputRef.el]\n        );\n    }\n\n    get value() {\n        let value = super.value;\n        // Strip leading slash\n        if (value[0] === \"/\") {\n            value = value.substring(1);\n        }\n        // Re-add the leading slash for saving, because url field is required\n        // and thus doesn't accept an empty string.\n        this.props.record.data[this.props.name] = `/${value.trim()}`;\n        return value;\n    }\n}\n\nconst pageUrlField = {\n    ...urlField,\n    component: PageUrlField,\n};\n\nregistry.category(\"fields\").add(\"page_url\", pageUrlField);\n\n/**\n * Displays 'Selection' field's values as images to select.\n * Image src for each value can be added using the option 'images' on field XML.\n */\nexport class ImageRadioField extends Component {\n    static template = \"website.FieldImageRadio\";\n    static props = {\n        ...standardFieldProps,\n        images: { type: Array, element: String },\n    };\n\n    setup() {\n        const selection = this.props.record.fields[this.props.name].selection;\n        // Check if value / label exists for each selection item and add the\n        // corresponding image from field options.\n        this.values = selection\n            .filter((item) => item[0] || item[1])\n            .map((value, index) => [\n                ...value,\n                (this.props.images && this.props.images[index]) || \"\",\n            ]);\n    }\n\n    /**\n     * @param {String} value\n     */\n    onSelectValue(value) {\n        this.props.record.update({ [this.props.name]: value });\n    }\n}\n\nexport const imageRadioField = {\n    component: ImageRadioField,\n    supportedOptions: [\n        {\n            label: _t(\"Images\"),\n            name: \"images\",\n            type: \"string\",\n            help: _t(\"Use an array to list the images to use in the radio selection.\"),\n        },\n    ],\n    supportedTypes: [\"selection\"],\n    extractProps: ({ options }) => ({\n        images: options.images,\n    }),\n};\n\nregistry.category(\"fields\").add(\"image_radio\", imageRadioField);\n", "import { registry } from \"@web/core/registry\";\nimport { Component } from \"@odoo/owl\";\nimport { standardFieldProps } from \"@web/views/fields/standard_field_props\";\n\nclass PublishField extends Component {\n    static template = \"website.PublishField\";\n    static props = { ...standardFieldProps };\n}\n\nregistry.category(\"fields\").add(\"website_publish_button\", {\n    component: PublishField,\n});\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { pick } from \"@web/core/utils/objects\";\nimport { Component } from \"@odoo/owl\";\nimport { standardFieldProps } from \"@web/views/fields/standard_field_props\";\n\nclass RedirectField extends Component {\n    static template = \"website.RedirectField\";\n    static props = { ...standardFieldProps };\n    get info() {\n        return this.props.record.data[this.props.name] ? _t(\"Published\") : _t(\"Unpublished\");\n    }\n\n    onClick() {\n        this.env.onClickViewButton({\n            clickParams: {\n                type: \"object\",\n                name: \"open_website_url\",\n            },\n            getResParams: () =>\n                pick(this.props.record, \"context\", \"evalContext\", \"resModel\", \"resId\", \"resIds\"),\n        });\n    }\n}\n\nregistry.category(\"fields\").add(\"website_redirect_button\", {\n    component: RedirectField,\n});\n", "import { registry } from \"@web/core/registry\";\nimport { useBus } from \"@web/core/utils/hooks\";\nimport { Component, useState } from \"@odoo/owl\";\nimport { standardFieldProps } from \"@web/views/fields/standard_field_props\";\n\nclass FieldIframePreview extends Component {\n    static template = \"website.iframeWidget\";\n    static props = { ...standardFieldProps };\n    setup() {\n        this.state = useState({ isMobile: false });\n\n        useBus(this.env.bus, \"THEME_PREVIEW:SWITCH_MODE\", (ev) => {\n            this.state.isMobile = ev.detail.mode === \"mobile\";\n        });\n    }\n}\n\nexport const fieldIframePreview = {\n    component: FieldIframePreview,\n};\n\nregistry.category(\"fields\").add(\"iframe\", fieldIframePreview);\n", "import { useBus } from \"@web/core/utils/hooks\";\nimport { EventBus, Component, useState, markup } from \"@odoo/owl\";\nimport { _t } from \"@web/core/l10n/translation\";\n\nexport class FullscreenIndication extends Component {\n    static props = {\n        bus: EventBus,\n    };\n    static template = \"website.FullscreenIndication\";\n\n    setup() {\n        this.state = useState({ isVisible: false });\n        useBus(this.props.bus, \"FULLSCREEN-INDICATION-SHOW\", this.show.bind(this));\n        useBus(this.props.bus, \"FULLSCREEN-INDICATION-HIDE\", this.hide.bind(this));\n    }\n\n    show() {\n        setTimeout(() => (this.state.isVisible = true));\n        this.autofade = setTimeout(() => (this.state.isVisible = false), 2000);\n    }\n\n    hide() {\n        if (this.state.isVisible) {\n            this.state.isVisible = false;\n            clearTimeout(this.autofade);\n        }\n    }\n\n    get fullScreenIndicationText() {\n        return _t(\"Press %(key)s to exit full screen\", { key: markup`<span>esc</span>` });\n    }\n}\n", "import { rpc } from \"@web/core/network/rpc\";\nimport { useBus, useService } from \"@web/core/utils/hooks\";\nimport { sprintf } from \"@web/core/utils/strings\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { EventBus, Component, markup, useEffect, useState } from \"@odoo/owl\";\n\nexport class WebsiteLoader extends Component {\n    static props = {\n        bus: EventBus,\n    };\n    static template = \"website.website_loader\";\n\n    setup() {\n        this.website = useService(\"website\");\n\n        const initialState = {\n            isVisible: false,\n            title: \"\",\n            flag: false,\n            showTips: false,\n            selectedFeatures: [],\n            showWaitingMessages: false,\n            progressPercentage: 0,\n            bottomMessageTemplate: undefined,\n            showLoader: true,\n            showCloseButton: false,\n        };\n\n        const defaultMessages = [\n            {\n                title: _t(\"Building your website.\"),\n                description: _t(\"Applying your colors and design...\"),\n                flag: \"colors\",\n            },\n            {\n                title: _t(\"Building your website.\"),\n                description: _t(\"Searching your images....\"),\n                flag: \"images\",\n            },\n            {\n                title: _t(\"Building your website.\"),\n                description: _t(\"Generating inspiring text...\"),\n                flag: \"text\",\n            },\n        ];\n\n        let messagesInterval;\n\n        this.state = useState({\n            ...initialState,\n        });\n        this.waitingMessages = useState(defaultMessages);\n        this.currentWaitingMessage = useState({ ...defaultMessages[0] });\n        this.featuresInstallInfo = { nbInstalled: 0, total: undefined };\n\n        useEffect(\n            (selectedFeatures) => {\n                if (this.state.showWaitingMessages) {\n                    const messagesToDisplay = [...defaultMessages]; // Start with defaultMessages\n                    if (selectedFeatures.length > 0) {\n                        // Merge defaultMessages with the relevant waitingMessages\n                        messagesToDisplay.push(...this.getWaitingMessages(selectedFeatures));\n                    }\n\n                    this.waitingMessages.splice(\n                        0,\n                        this.waitingMessages.length,\n                        ...messagesToDisplay\n                    );\n\n                    // Request the number of modules/dependencies to install\n                    // and already installed\n                    this.trackModules(selectedFeatures).catch(console.error);\n\n                    return () => {\n                        clearTimeout(this.trackModulesTimeout);\n                        clearInterval(this.updateProgressInterval);\n                    };\n                }\n            },\n            () => [this.state.selectedFeatures]\n        );\n\n        // Cycle through the waitingMessages every 6s\n        useEffect(\n            () => {\n                if (this.state.showWaitingMessages) {\n                    let msgIndex = 0;\n                    messagesInterval = setInterval(() => {\n                        msgIndex++;\n                        const nextMessage = this.waitingMessages[msgIndex];\n                        Object.assign(this.currentWaitingMessage, nextMessage);\n                        if (this.waitingMessages.length - 1 === msgIndex) {\n                            clearInterval(messagesInterval);\n                        }\n                    }, 6000);\n\n                    return () => clearInterval(messagesInterval);\n                }\n            },\n            () => [this.waitingMessages.length]\n        );\n\n        // Prevent user from closing/refreshing the window\n        useEffect(\n            (isVisible) => {\n                if (isVisible) {\n                    window.addEventListener(\"beforeunload\", this.showRefreshConfirmation);\n                    if (!this.state.selectedFeatures || this.state.selectedFeatures.length === 0) {\n                        // If there is no feature selected, we fake the progress\n                        // for theme installation and configurator_apply. If\n                        // there is at least 1 feature selected, the progress\n                        // bar will be initialized in trackModules().\n                        this.initProgressBar();\n                    }\n                } else {\n                    window.removeEventListener(\"beforeunload\", this.showRefreshConfirmation);\n                }\n\n                return () => {\n                    window.removeEventListener(\"beforeunload\", this.showRefreshConfirmation);\n                    clearInterval(this.updateProgressInterval);\n                };\n            },\n            () => [this.state.isVisible]\n        );\n\n        useBus(this.props.bus, \"SHOW-WEBSITE-LOADER\", (ev) => {\n            const props = ev.detail;\n            this.state.isVisible = true;\n            for (const prop of [\n                \"title\",\n                // FIXME: website user/interactive tours are not properly\n                // working at the moment. This disables the \"follow the tips\"\n                // message in the website loader while waiting for a fix.\n                // \"showTips\",\n                \"selectedFeatures\",\n                \"showWaitingMessages\",\n                \"bottomMessageTemplate\",\n                \"showCloseButton\",\n                \"flag\",\n            ]) {\n                this.state[prop] = props && props[prop];\n            }\n            this.state.showLoader = props && props.showLoader !== false;\n        });\n        useBus(this.props.bus, \"HIDE-WEBSITE-LOADER\", () => {\n            if (!this.state.isVisible) {\n                return;\n            }\n            for (const key of Object.keys(initialState)) {\n                this.state[key] = initialState[key];\n            }\n            clearInterval(messagesInterval);\n            clearTimeout(this.trackModulesTimeout);\n            clearInterval(this.updateProgressInterval);\n        });\n        // Action needed if the app automatically refreshes or redirects the\n        // page without hiding/removing the WebsiteLoader. This should be\n        // called prior to any refresh/redirect if the loader is still visible.\n        useBus(this.props.bus, \"PREPARE-OUT-WEBSITE-LOADER\", () => {\n            window.removeEventListener(\"beforeunload\", this.showRefreshConfirmation);\n        });\n    }\n\n    /**\n     * Initializes the progress bar.\n     */\n    initProgressBar() {\n        // The progress speed decreases as it approaches its limit. This way,\n        // users have the feeling that the website creation progressing is fast\n        // and we prevent them from leaving the page too early (because they\n        // already did XX% of the process).\n        // If there is no module to install, we fake the progress from 0 to 100.\n        // If there is at least 1 module to install, we take 70% of the progress\n        // bar that we divide by the number of modules to install. We fake the\n        // progress of each module individually and when all modules are\n        // installed, we fake the progress of the remaining 30%.\n        const nbModulesToInstall = this.featuresInstallInfo.total || 0;\n        const isSomethingToInstall = nbModulesToInstall > 0;\n        let currentProgress = 0;\n        // This controls the speed of the progress bar.\n        const progressStep = isSomethingToInstall ? 0.04 : 0.02;\n        const progressForAfterModules = isSomethingToInstall ? 30 : 100;\n        const progressForAllModules = 100 - progressForAfterModules;\n        let lastTotalInstalled = 0;\n        const progressPerModule = isSomethingToInstall\n            ? progressForAllModules / nbModulesToInstall\n            : 0;\n\n        this.updateProgressInterval = setInterval(() => {\n            if (this.featuresInstallInfo.nbInstalled !== lastTotalInstalled) {\n                // A module just finished its install.\n                currentProgress = 0;\n                lastTotalInstalled = this.featuresInstallInfo.nbInstalled;\n            }\n            currentProgress += progressStep;\n            const limit =\n                this.featuresInstallInfo.nbInstalled === nbModulesToInstall\n                    ? progressForAfterModules\n                    : progressPerModule;\n            this.state.progressPercentage =\n                lastTotalInstalled * progressPerModule +\n                (Math.atan(currentProgress) / (Math.PI / 2)) * limit;\n        }, 100);\n    }\n    /**\n     * Makes a RPC call to track the features and dependencies being installed\n     * and, as long as the number of features installed is different from the\n     * total expected, recursively calls itself again after 1s.\n     *\n     * @param {integer[]} selectedFeatures\n     */\n    async trackModules(selectedFeatures) {\n        const installInfo = await rpc(\n            \"/website/track_installing_modules\",\n            {\n                selected_features: selectedFeatures,\n                total_features: this.featuresInstallInfo.total,\n            },\n            { silent: true }\n        );\n        if (\n            !this.featuresInstallInfo.total ||\n            this.featuresInstallInfo.nbInstalled !== installInfo.nbInstalled\n        ) {\n            this.featuresInstallInfo = installInfo;\n        }\n        this.initProgressBar();\n        if (this.featuresInstallInfo.nbInstalled !== this.featuresInstallInfo.total) {\n            this.trackModulesTimeout = setTimeout(() => this.trackModules(selectedFeatures), 1000);\n        }\n    }\n\n    /**\n     * Depending on the features selected, returns the right waiting messages.\n     *\n     * @param {integer[]} selectedFeatures\n     * @returns {Object[]} - the messages filtered by the selected features\n     */\n    getWaitingMessages(selectedFeatures) {\n        const websiteFeaturesMessages = [\n            {\n                id: 5,\n                title: _t(\"Adding features.\"),\n                name: _t(\"blog\"),\n                description: _t(\"Enabling your %s.\"),\n                flag: \"generic\",\n            },\n            {\n                id: 7,\n                title: _t(\"Adding features.\"),\n                name: _t(\"recruitment platform\"),\n                description: _t(\"Integrating your %s.\"),\n                flag: \"generic\",\n            },\n            {\n                id: 8,\n                title: _t(\"Adding features.\"),\n                name: _t(\"online store\"),\n                description: _t(\"Activating your %s.\"),\n                flag: \"generic\",\n            },\n            {\n                id: 9,\n                title: _t(\"Adding features.\"),\n                name: _t(\"online appointment system\"),\n                description: _t(\"Configuring your %s.\"),\n                flag: \"generic\",\n            },\n            {\n                id: 10,\n                title: _t(\"Adding features.\"),\n                name: _t(\"forum\"),\n                description: _t(\"Setting up your %s.\"),\n                flag: \"generic\",\n            },\n            {\n                id: 12,\n                title: _t(\"Adding features.\"),\n                name: _t(\"e-learning platform\"),\n                description: _t(\"Installing your %s.\"),\n                flag: \"generic\",\n            },\n            {\n                // Always the last message if there is at least 1 feature selected.\n                id: \"last\",\n                title: _t(\"Finalizing.\"),\n                description: _t(\"Activating the last features.\"),\n                flag: \"generic\",\n            },\n        ];\n\n        const filteredIds = [...selectedFeatures, \"last\"];\n        const messagesList = websiteFeaturesMessages.filter((msg) => {\n            if (filteredIds.includes(msg.id)) {\n                if (msg.name) {\n                    const highlight = markup`<span class=\"o_website_loader_text_highlight\">${msg.name}</span>`;\n                    msg.description = markup(sprintf(msg.description, highlight));\n                }\n                return true;\n            }\n        });\n        return messagesList;\n    }\n\n    /**\n     * Prevents refreshing/leaving the page if the loader is displayed (and\n     * thus some work is being done in the backend) by opening a prompt dialog.\n     *\n     * @param {Event} ev\n     * @returns empty returnValue for Chrome & Safari\n     * cf. https://developer.mozilla.org/en-US/docs/Web/API/Window/beforeunload_event#compatibility_notes\n     */\n    showRefreshConfirmation = (ev) => {\n        if (this.state.isVisible) {\n            ev.preventDefault(); // Firefox\n            ev.returnValue = \"\";\n            return ev.returnValue;\n        }\n    };\n\n    /**\n     * Hide the loader.\n     */\n    close() {\n        this.website.hideLoader();\n    }\n}\n", "import { usePageManager } from \"./page_manager_hook\";\nimport { PageSearchModel } from \"./page_search_model\";\nimport { registry } from \"@web/core/registry\";\nimport { kanbanView } from \"@web/views/kanban/kanban_view\";\n\nexport class PageKanbanController extends kanbanView.Controller {\n    static components = {\n        ...kanbanView.Controller.components,\n    };\n\n    setup() {\n        super.setup();\n        this.pageManager = usePageManager({\n            resModel: this.props.resModel,\n            createAction: this.props.context.create_action,\n        });\n    }\n    /**\n     * @override\n     */\n    async createRecord() {\n        return this.pageManager.createWebsiteContent();\n    }\n}\n\nexport const PageKanbanView = {\n    ...kanbanView,\n    Controller: PageKanbanController,\n    SearchModel: PageSearchModel,\n};\n\nregistry.category(\"views\").add(\"website_pages_kanban\", PageKanbanView);\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { usePageManager } from \"./page_manager_hook\";\nimport { PageSearchModel } from \"./page_search_model\";\nimport { registry } from \"@web/core/registry\";\nimport { listView } from \"@web/views/list/list_view\";\nimport { ConfirmationDialog } from \"@web/core/confirmation_dialog/confirmation_dialog\";\nimport { DeletePageDialog, DuplicatePageDialog } from \"@website/components/dialog/page_properties\";\nimport { useService } from \"@web/core/utils/hooks\";\n\nexport class PageListController extends listView.Controller {\n    static components = {\n        ...listView.Controller.components,\n    };\n\n    /**\n     * @override\n     */\n    setup() {\n        super.setup();\n        this.orm = useService(\"orm\");\n        this.dialog = useService(\"dialog\");\n        this.pageManager = usePageManager({\n            resModel: this.props.resModel,\n            createAction: this.props.context.create_action,\n        });\n        if (this.props.resModel === \"website.page\") {\n            this.archiveEnabled = false;\n        }\n    }\n\n    /**\n     * @override\n     */\n    onClickCreate() {\n        return this.pageManager.createWebsiteContent();\n    }\n\n    /**\n     * Adds a \"Publish/Unpublish\" button to the 'action' menu of the list view.\n     *\n     * @override\n     */\n    getStaticActionMenuItems() {\n        const menuItems = super.getStaticActionMenuItems();\n        if (Object.prototype.hasOwnProperty.call(this.props.fields, \"is_published\")) {\n            menuItems.publish = {\n                sequence: 15,\n                icon: \"fa fa-globe\",\n                description: _t(\"Publish\"),\n                callback: async () => {\n                    this.dialogService.add(ConfirmationDialog, {\n                        title: _t(\"Publish Website Content\"),\n                        body: _t(\n                            \"%s record(s) selected, are you sure you want to publish them all?\",\n                            this.model.root.selection.length\n                        ),\n                        confirm: () => this.togglePublished(true),\n                    });\n                },\n            };\n            menuItems.unpublish = {\n                sequence: 16,\n                icon: \"fa fa-chain-broken\",\n                description: _t(\"Unpublish\"),\n                callback: async () => this.togglePublished(false),\n            };\n        }\n        if (this.props.resModel === \"website.page\") {\n            menuItems.duplicate.callback = async (records = []) => {\n                const resIds = this.model.root.selection.map((record) => record.resId);\n                this.dialog.add(DuplicatePageDialog, {\n                    pageIds: resIds,\n                    onDuplicate: () => {\n                        this.model.load();\n                    },\n                });\n            };\n        }\n        return menuItems;\n    }\n\n    async onDeleteSelectedRecords() {\n        const pageIds = this.model.root.selection.map((record) => record.resId);\n        const newPageTemplateRecords = await this.orm.read(\"website.page\", pageIds, [\n            \"is_new_page_template\",\n        ]);\n        this.dialogService.add(DeletePageDialog, {\n            resIds: pageIds,\n            resModel: this.props.resModel,\n            onDelete: () => {\n                this.model.root.deleteRecords();\n            },\n            hasNewPageTemplate: newPageTemplateRecords.some(\n                (record) => record.is_new_page_template\n            ),\n        });\n    }\n\n    async togglePublished(publish) {\n        const resIds = this.model.root.selection.map((record) => record.resId);\n        await this.orm.write(this.props.resModel, resIds, { is_published: publish });\n        this.actionService.switchView(\"list\");\n    }\n}\n\nexport class PageListRenderer extends listView.Renderer {\n    static recordRowTemplate = \"website.PageListRenderer.RecordRow\";\n}\n\nexport const PageListView = {\n    ...listView,\n    Renderer: PageListRenderer,\n    Controller: PageListController,\n    SearchModel: PageSearchModel,\n};\n\nregistry.category(\"views\").add(\"website_pages_list\", PageListView);\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { AddPageDialog } from \"@website/components/dialog/add_page_dialog\";\nimport { onWillStart, useEnv, useState } from \"@odoo/owl\";\n\n/**\n * Used to share code and keep the same behaviour on different types of 'website\n * content' views:\n * - Trigger the 'new content' dialogs when 'CREATE' button is clicked.\n * - Add a website selector on ControlPanel (that will be used by the renderer\n * to filter content).\n */\nexport function usePageManager({ resModel, createAction }) {\n    const env = useEnv();\n    const website = useService(\"website\");\n    const dialog = useService(\"dialog\");\n    const actionService = useService(\"action\");\n    const websiteSelection = odoo.debug ? [{ id: 0, name: _t(\"All Websites\") }] : [];\n    const state = useState({\n        activeWebsite: undefined,\n    });\n\n    onWillStart(async () => {\n        // `fetchWebsites()` already done by parent PageSearchModel\n        websiteSelection.push(...website.websites);\n        state.activeWebsite = await env.searchModel.getCurrentWebsite();\n    });\n\n    async function createWebsiteContent() {\n        if (resModel === \"website.page\") {\n            return dialog.add(AddPageDialog, {\n                websiteId: state.activeWebsite.id,\n            });\n        }\n        if (createAction) {\n            if (/^\\//.test(createAction)) {\n                const url = await rpc(createAction);\n                website.goToWebsite({ path: url, edition: true });\n                return;\n            }\n            actionService.doAction(createAction, {\n                onClose: (infos) => {\n                    if (infos) {\n                        website.goToWebsite({ path: infos.path });\n                    }\n                },\n                props: {\n                    onSave: (record, params) => {\n                        if (record.resId && params.computePath) {\n                            const path = params.computePath();\n                            actionService.doAction({\n                                type: \"ir.actions.act_window_close\",\n                                infos: { path },\n                            });\n                        }\n                    },\n                },\n            });\n        }\n    }\n\n    function selectWebsite(website) {\n        state.activeWebsite = website;\n        env.searchModel.notifyWebsiteChange(website.id);\n    }\n    return {\n        get websites() {\n            const activeId = state.activeWebsite.id;\n            return websiteSelection.map((website) => {\n                const isActive = website.id === activeId;\n                return { ...website, isActive };\n            });\n        },\n        createWebsiteContent,\n        selectWebsite,\n    };\n}\n", "import { useService } from \"@web/core/utils/hooks\";\nimport { SearchModel } from \"@web/search/search_model\";\n\nexport class PageSearchModel extends SearchModel {\n    /**\n     * @override\n     */\n    setup() {\n        super.setup(...arguments);\n        this.website = useService(\"website\");\n    }\n\n    /**\n     * @override\n     */\n    async load() {\n        await super.load(...arguments);\n\n        // Call `fetchWebsites` to populate `this.website.websites`.\n        await this.website.fetchWebsites();\n\n        if (this.searchViewFields.website_id) {\n            await this.createFilterForAllWebsites();\n            await this.selectCurrentWebsiteFilter();\n        }\n    }\n\n    /**\n     * Creates filter for all available websites.\n     */\n    async createFilterForAllWebsites() {\n        const existingWebsiteFilters = this.getSearchItems(\n            (searchItem) => searchItem.type === \"filter\" && searchItem.name.startsWith(\"website_\")\n        );\n\n        // Check if filters are already created\n        if (existingWebsiteFilters.length === this.website.websites.length) {\n            return;\n        }\n\n        let websitePageIds = {};\n        if (this.resModel === \"website.page\") {\n            const websiteIds = this.website.websites.map((website) => website.id);\n            websitePageIds = await this.orm.call(\"website\", \"get_website_page_ids\", [websiteIds]);\n        }\n\n        const websiteFilters = this.website.websites.map((website) => {\n            const websiteDomain =\n                this.resModel === \"website.page\"\n                    ? [[\"id\", \"in\", websitePageIds[website.id] || []]]\n                    : [[\"website_id\", \"in\", [false, website.id]]];\n\n            return {\n                name: `website_${website.id}`,\n                description: website.name,\n                domain: websiteDomain,\n                type: \"filter\",\n            };\n        });\n\n        this._createGroupOfSearchItems(websiteFilters);\n    }\n\n    /**\n     * Selects the current website filter if no other website filter is active.\n     */\n    async selectCurrentWebsiteFilter() {\n        const currentlySelectedWebsiteFilters = this.getSearchItems(\n            (searchItem) =>\n                searchItem.type === \"filter\" &&\n                searchItem.name.startsWith(\"website_\") &&\n                searchItem.isActive\n        );\n        if (currentlySelectedWebsiteFilters.length) {\n            return;\n        }\n\n        const currentWebsite = await this.getCurrentWebsite();\n        const [currentWebsiteFilter] = this.getSearchItems(\n            (searchItem) =>\n                searchItem.type === \"filter\" && searchItem.name === `website_${currentWebsite.id}`\n        );\n        if (currentWebsiteFilter) {\n            this.toggleSearchItem(currentWebsiteFilter.id);\n        }\n    }\n\n    /**\n     * Retrieves the current website.\n     *\n     * @returns {Object} The current website.\n     */\n    async getCurrentWebsite() {\n        const currentWebsite = await this.orm.call(\"website\", \"get_current_website\");\n        if (currentWebsite) {\n            return this.website.websites.find((w) => w.id === currentWebsite[0]);\n        }\n        return this.website.websites[0];\n    }\n}\n", "import { ControlPanel } from \"@web/search/control_panel/control_panel\";\nimport { FormController } from \"@web/views/form/form_controller\";\nimport { formView } from \"@web/views/form/form_view\";\nimport { registry } from \"@web/core/registry\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { ViewButton } from \"@web/views/view_button/view_button\";\nimport { useSubEnv, onMounted, useEnv } from \"@odoo/owl\";\n\n/*\n * Common code for theme installation/update handler.\n * It overrides the onClickViewButton function that's present in the env.\n * That way, we display our own Loader and make a silent call to the ORM.\n */\nexport function useLoaderOnClick() {\n    const website = useService(\"website\");\n    const orm = useService(\"orm\");\n    const action = useService(\"action\");\n    const env = useEnv();\n    const previousOnClickViewButton = env.onClickViewButton;\n    useSubEnv({\n        async onClickViewButton(params) {\n            const name = params.clickParams.name;\n            if ([\"button_refresh_theme\", \"button_choose_theme\"].includes(name)) {\n                website.invalidateSnippetCache = true;\n                website.showLoader({ showTips: name !== \"button_refresh_theme\" });\n                try {\n                    const resParams = params.getResParams();\n                    const callback = await orm.silent.call(resParams.resModel, name, [\n                        [resParams.resId],\n                    ]);\n                    let keepLoader = false;\n                    if (callback) {\n                        callback.target = \"main\";\n                        await action.doAction(callback);\n                        if (callback.tag === \"website_preview\") {\n                            keepLoader = true;\n                        }\n                    }\n                    if (!keepLoader) {\n                        website.hideLoader();\n                    }\n                } catch (error) {\n                    website.hideLoader();\n                    throw error;\n                }\n            } else {\n                return previousOnClickViewButton(...arguments);\n            }\n        },\n    });\n}\n\nclass ThemePreviewFormController extends FormController {\n    static components = { ...FormController.components, ViewButton };\n    static template = \"website.ThemePreviewFormController\";\n    /**\n     * @override\n     */\n    setup() {\n        super.setup();\n        useLoaderOnClick();\n\n        // TODO adapt theme previews then remove this\n        // ... or remove the feature entirely ? See task-3454790.\n        onMounted(() => {\n            setTimeout(() => {\n                document.querySelector('button[name=\"button_choose_theme\"]')?.click();\n            }, 0);\n        });\n    }\n    /**\n     * @override\n     */\n    get className() {\n        return { ...super.className, o_view_form_theme_preview_controller: true };\n    }\n    /**\n     * Handler called when user click on 'Choose another theme' button.\n     */\n    back() {\n        this.env.config.historyBack();\n    }\n}\n\nclass ThemePreviewFormControlPanel extends ControlPanel {\n    static template = \"website.ThemePreviewForm.ControlPanel\";\n    /**\n     * Triggers an event on the main bus.\n     * @see {FieldIframePreview} for the event handler.\n     */\n    onMobileClick() {\n        this.env.bus.trigger(\"THEME_PREVIEW:SWITCH_MODE\", { mode: \"mobile\" });\n    }\n    /**\n     * @see {onMobileClick}\n     */\n    onDesktopClick() {\n        this.env.bus.trigger(\"THEME_PREVIEW:SWITCH_MODE\", { mode: \"desktop\" });\n    }\n    /**\n     * Handler called when user click on Go Back button.\n     */\n    back() {\n        this.env.config.historyBack();\n    }\n}\n\nconst ThemePreviewFormView = {\n    ...formView,\n    Controller: ThemePreviewFormController,\n    ControlPanel: ThemePreviewFormControlPanel,\n};\n\nregistry.category(\"views\").add(\"theme_preview_form\", ThemePreviewFormView);\n", "import { ControlPanel } from \"@web/search/control_panel/control_panel\";\nimport { kanbanView } from \"@web/views/kanban/kanban_view\";\nimport { KanbanController } from \"@web/views/kanban/kanban_controller\";\nimport { registry } from \"@web/core/registry\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { useLoaderOnClick } from \"./theme_preview_form\";\nimport { KanbanRecord } from \"@web/views/kanban/kanban_record\";\nimport { KanbanRenderer } from \"@web/views/kanban/kanban_renderer\";\n\nclass ThemePreviewKanbanController extends KanbanController {\n    /**\n     * @override\n     */\n    setup() {\n        super.setup();\n        useLoaderOnClick();\n    }\n}\n\nclass ThemePreviewControlPanel extends ControlPanel {\n    static template = \"website.ThemePreviewKanban.ControlPanel\";\n    setup() {\n        super.setup();\n        this.website = useService(\"website\");\n    }\n    close() {\n        this.website.goToWebsite();\n    }\n}\nclass ThemePreviewKanbanrecord extends KanbanRecord {\n    /** @override **/\n    getRecordClasses() {\n        return super.getRecordClasses() + \" p-0 border-0 bg-transparent\";\n    }\n}\n\nexport class ThemePreviewKanbanRenderer extends KanbanRenderer {\n    static components = {\n        ...KanbanRenderer.components,\n        KanbanRecord: ThemePreviewKanbanrecord,\n    };\n}\n\nconst ThemePreviewKanbanView = {\n    ...kanbanView,\n    Controller: ThemePreviewKanbanController,\n    ControlPanel: ThemePreviewControlPanel,\n    Renderer: ThemePreviewKanbanRenderer,\n};\n\nregistry.category(\"views\").add(\"theme_preview_kanban\", ThemePreviewKanbanView);\n", "import { jsToPyLocale } from \"@web/core/l10n/utils\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { user } from \"@web/core/user\";\nimport { isVisible } from \"@web/core/utils/ui\";\n\nimport { FullscreenIndication } from \"../components/fullscreen_indication/fullscreen_indication\";\nimport { WebsiteLoader } from \"../components/website_loader/website_loader\";\nimport { reactive, EventBus } from \"@odoo/owl\";\n\nconst websiteSystrayRegistry = registry.category(\"website_systray\");\n\n// TODO this is duplicated in website_root at least, it should be a shared util\nexport const unslugHtmlDataObject = (repr) => {\n    const match = repr && repr.match(/(.+)\\((-?\\d+),(.*)\\)/);\n    if (!match) {\n        return null;\n    }\n    return {\n        model: match[1],\n        id: match[2] | 0,\n    };\n};\n\nconst ANONYMOUS_PROCESS_ID = \"ANONYMOUS_PROCESS_ID\";\n\nexport const websiteService = {\n    dependencies: [\"orm\", \"action\", \"hotkey\"],\n    start(env, { orm, action, hotkey }) {\n        let websites = [];\n        let currentWebsiteId;\n        const currentWebsiteIdList = [];\n        let currentMetadata = {};\n        let fullscreen;\n        let pageDocument;\n        let contentWindow;\n        let lastUrl;\n        let websiteRootInstance;\n        let isRestrictedEditor;\n        let isDesigner;\n        let hasMultiWebsites;\n        let actionJsId;\n        const blockingProcesses = [];\n        let modelNamesProm = null;\n        const modelNames = {};\n        let invalidateSnippetCache = false;\n        let lastWebsiteId = null;\n\n        const context = reactive({\n            showResourceEditor: false,\n            edition: false,\n            isPublicRootReady: false,\n            snippetsLoaded: false,\n            isMobile: false,\n        });\n        const bus = new EventBus();\n\n        hotkey.add(\n            \"escape\",\n            () => {\n                // Toggle fullscreen mode when pressing escape.\n                if (\n                    (!currentWebsiteId && !fullscreen) ||\n                    (pageDocument && isVisible(pageDocument.querySelector(\".modal\")))\n                ) {\n                    // Only allow to use this feature while on the website app, or\n                    // while it is already fullscreen (in case you left the website\n                    // app in fullscreen mode, thanks to CTRL-K), or if a modal\n                    // is open within the preview and could be closed with escape.\n                    return;\n                }\n                fullscreen = !fullscreen;\n                document.body.classList.toggle(\"o_website_fullscreen\", fullscreen);\n                bus.trigger(\n                    fullscreen ? \"FULLSCREEN-INDICATION-SHOW\" : \"FULLSCREEN-INDICATION-HIDE\"\n                );\n            },\n            { global: true }\n        );\n        registry.category(\"main_components\").add(\"FullscreenIndication\", {\n            Component: FullscreenIndication,\n            props: { bus },\n        });\n        registry.category(\"main_components\").add(\"WebsiteLoader\", {\n            Component: WebsiteLoader,\n            props: { bus },\n        });\n\n        function addWebsiteId(id) {\n            if (!currentWebsiteIdList.length) {\n                currentWebsiteId = id;\n            }\n            currentWebsiteIdList.push(id);\n        }\n\n        function removeWebsiteId() {\n            currentWebsiteIdList.shift();\n            if (currentWebsiteIdList.length) {\n                currentWebsiteId = currentWebsiteIdList[0];\n            } else {\n                currentWebsiteId = null;\n            }\n        }\n\n        return {\n            set currentWebsiteId(id) {\n                if (id === null) {\n                    removeWebsiteId();\n                    return;\n                }\n                if (id && id !== lastWebsiteId) {\n                    invalidateSnippetCache = true;\n                    lastWebsiteId = id;\n                }\n                addWebsiteId(id);\n                websiteSystrayRegistry.trigger(\"EDIT-WEBSITE\");\n            },\n            /**\n             * This represents the current website being edited in the\n             * WebsitePreview client action. Multiple components based their\n             * visibility on this value, which is falsy if the client action is\n             * not displayed.\n             */\n            get currentWebsite() {\n                const currentWebsite = websites.find((w) => w.id === currentWebsiteId);\n                if (currentWebsite) {\n                    currentWebsite.metadata = currentMetadata;\n                }\n                return currentWebsite;\n            },\n            get currentWebsiteId() {\n                return currentWebsiteId;\n            },\n            get websites() {\n                return websites;\n            },\n            get context() {\n                return context;\n            },\n            get bus() {\n                return bus;\n            },\n            set pageDocument(document) {\n                pageDocument = document;\n                if (!document) {\n                    currentMetadata = {};\n                    contentWindow = null;\n                    return;\n                }\n                const { dataset } = document.documentElement;\n                // XML files have no dataset on Firefox, and an empty one on\n                // Chrome.\n                const isWebsitePage = dataset && dataset.websiteId;\n                if (!isWebsitePage) {\n                    currentMetadata = {};\n                } else {\n                    const {\n                        mainObject,\n                        seoObject,\n                        isPublished,\n                        canOptimizeSeo,\n                        canPublish,\n                        editableInBackend,\n                        translatable,\n                        viewXmlid,\n                        defaultLangName,\n                        langName,\n                    } = dataset;\n                    // We ignore multiple menus with the same `content_menu_id`\n                    // in the DOM, since it's possible to have different\n                    // templates for the same content menu (E.g. used for a\n                    // different desktop / mobile UI).\n                    const contentMenus = [\n                        ...new Map(\n                            [...document.querySelectorAll(\"[data-content_menu_id]\")].map(\n                                (menuEl) => [\n                                    menuEl.dataset.content_menu_id,\n                                    [menuEl.dataset.menu_name, menuEl.dataset.content_menu_id],\n                                ]\n                            )\n                        ).values(),\n                    ];\n                    currentMetadata = {\n                        path: document.location.href,\n                        mainObject: unslugHtmlDataObject(mainObject),\n                        seoObject: unslugHtmlDataObject(seoObject),\n                        isPublished: isPublished === \"True\",\n                        canOptimizeSeo: canOptimizeSeo === \"True\",\n                        canPublish: canPublish === \"True\",\n                        editableInBackend: editableInBackend === \"True\",\n                        title: document.title,\n                        translatable: !!translatable,\n                        contentMenus,\n                        // TODO: Find a better way to figure out if\n                        // a page is editable or not. For now, we use\n                        // the editable selector because it's the common\n                        // denominator of editable pages.\n                        editable: !!document.getElementById(\"wrapwrap\"),\n                        viewXmlid: viewXmlid,\n                        lang: jsToPyLocale(document.documentElement.getAttribute(\"lang\")),\n                        defaultLangName: defaultLangName,\n                        langName: langName,\n                        direction: document.documentElement.querySelector(\"#wrapwrap.o_rtl\")\n                            ? \"rtl\"\n                            : \"ltr\",\n                    };\n                }\n                contentWindow = document.defaultView;\n                websiteSystrayRegistry.trigger(\"CONTENT-UPDATED\");\n            },\n            get pageDocument() {\n                return pageDocument;\n            },\n            get contentWindow() {\n                return contentWindow;\n            },\n            get websiteRootInstance() {\n                return websiteRootInstance;\n            },\n            set websiteRootInstance(rootInstance) {\n                websiteRootInstance = rootInstance;\n                context.isPublicRootReady = !!rootInstance;\n            },\n            set lastUrl(url) {\n                lastUrl = url;\n            },\n            get lastUrl() {\n                return lastUrl;\n            },\n            get isRestrictedEditor() {\n                return isRestrictedEditor === true;\n            },\n            get isDesigner() {\n                return isDesigner === true;\n            },\n            get is404() {\n                return currentMetadata.viewXmlid === \"website.page_404\";\n            },\n            get currentLocation() {\n                const path = decodeURIComponent(this.contentWindow.location.pathname);\n                if (!this.currentWebsite.metadata.translatable) {\n                    return path;\n                }\n                // If the website is translatable, remove the /lang in the\n                // location pathname, e.g. /fr/hello-page -> /hello-page\n                const lang = path.split(\"/\")[1];\n                return path.slice(lang.length + 1);\n            },\n            get hasMultiWebsites() {\n                return hasMultiWebsites === true;\n            },\n            get actionJsId() {\n                return actionJsId;\n            },\n            set actionJsId(jsId) {\n                actionJsId = jsId;\n            },\n            get invalidateSnippetCache() {\n                return invalidateSnippetCache;\n            },\n            set invalidateSnippetCache(value) {\n                invalidateSnippetCache = value;\n            },\n\n            goToWebsite({ websiteId, path, edition, translation, lang } = {}) {\n                this.websiteRootInstance = undefined;\n                if (lang) {\n                    invalidateSnippetCache = true;\n                    path = `/website/lang/${encodeURIComponent(lang)}?r=${encodeURIComponent(\n                        path\n                    )}`;\n                }\n                action.doAction(\"website.website_preview\", {\n                    clearBreadcrumbs: true,\n                    props: {\n                        websiteId: websiteId || currentWebsiteId || false,\n                        path: path || (contentWindow && contentWindow.location.href) || \"/\",\n                        enableEditor: edition,\n                        editTranslations: translation,\n                    },\n                });\n            },\n            async fetchUserGroups() {\n                // Fetch user groups, before fetching the websites.\n                [isRestrictedEditor, isDesigner, hasMultiWebsites] = await Promise.all([\n                    user.hasGroup(\"website.group_website_restricted_editor\"),\n                    user.hasGroup(\"website.group_website_designer\"),\n                    user.hasGroup(\"website.group_multi_website\"),\n                ]);\n            },\n            async fetchWebsites() {\n                websites = (\n                    await orm.webSearchRead(\"website\", [], {\n                        specification: {\n                            domain: {},\n                            id: {},\n                            name: {},\n                            language_ids: {},\n                            default_lang_id: { fields: { code: {} } },\n                            cookies_bar: {},\n                        },\n                    })\n                ).records;\n            },\n            blockPreview(showLoader, processId) {\n                if (!blockingProcesses.length) {\n                    bus.trigger(\"BLOCK\", { showLoader });\n                }\n                blockingProcesses.push(processId || ANONYMOUS_PROCESS_ID);\n            },\n            unblockPreview(processId) {\n                const processIndex = blockingProcesses.indexOf(processId || ANONYMOUS_PROCESS_ID);\n                if (processIndex > -1) {\n                    blockingProcesses.splice(processIndex, 1);\n                    if (blockingProcesses.length === 0) {\n                        bus.trigger(\"UNBLOCK\");\n                    }\n                }\n            },\n            showLoader(props) {\n                bus.trigger(\"SHOW-WEBSITE-LOADER\", props);\n            },\n            hideLoader() {\n                bus.trigger(\"HIDE-WEBSITE-LOADER\");\n            },\n            prepareOutLoader() {\n                bus.trigger(\"PREPARE-OUT-WEBSITE-LOADER\");\n            },\n            /**\n             * Returns the (translated) \"functional\" name of a model\n             * (_description) given its \"technical\" name (_name).\n             *\n             * @param {string} [model]\n             * @returns {string}\n             */\n            async getUserModelName(model = this.currentWebsite.metadata.mainObject.model) {\n                if (!modelNamesProm) {\n                    // FIXME the `get_available_models` is to be removed/changed\n                    // in a near future. This code is to be adapted, probably\n                    // with another helper to map a model functional name from\n                    // its technical map without the need of the right access\n                    // rights (which is why I cannot use search_read here).\n                    modelNamesProm = orm\n                        .call(\"ir.model\", \"get_available_models\")\n                        .then((modelsData) => {\n                            for (const modelData of modelsData) {\n                                modelNames[modelData[\"model\"]] = modelData[\"display_name\"];\n                            }\n                        })\n                        // Precaution in case the util is simply removed without\n                        // adapting this method: not critical, we can restore\n                        // later and use the fallback until the fix is made.\n                        .catch(() => {});\n                }\n                await modelNamesProm;\n                return modelNames[model] || _t(\"Data\");\n            },\n        };\n    },\n};\n\nregistry.category(\"services\").add(\"website\", websiteService);\n", "import { intersection } from \"@web/core/utils/arrays\";\nimport { _t, appTranslateFn } from \"@web/core/l10n/translation\";\nimport { renderToElement } from \"@web/core/utils/render\";\nimport { App, Component } from \"@odoo/owl\";\nimport { getTemplate } from \"@web/core/templates\";\nimport { UrlAutoComplete } from \"@website/components/autocomplete_with_pages/url_autocomplete\";\nimport * as urlUtils from \"@html_editor/utils/url\";\nimport { patch } from \"@web/core/utils/patch\";\n\n/**\n * Allows to load anchors from a page.\n *\n * @param {string} url\n * @param {Node} body the editable for which to recover anchors\n * @returns {Deferred<string[]>}\n */\nfunction loadAnchors(url, body) {\n    return new Promise(function (resolve, reject) {\n        if (url === window.location.pathname || url[0] === \"#\") {\n            resolve(body ? body.outerHTML : document.body.outerHTML);\n        } else if (url.length && !url.startsWith(\"http\")) {\n            // TODO: Might be broken with ReplaceMedia (NBY) and LinkTools\n            fetch(window.location.origin + url)\n                .then((response) => response.text())\n                .then((text) => {\n                    const parser = new DOMParser();\n                    const doc = parser.parseFromString(text, \"text/html\");\n                    return doc.body;\n                })\n                .then(resolve, reject);\n        } else {\n            // avoid useless query\n            resolve();\n        }\n    })\n        .then(function (response) {\n            const fragment = new DOMParser().parseFromString(response, \"text/html\");\n            const anchorEls = fragment.querySelectorAll(\n                `[id][data-anchor=\"true\"], .modal[id][data-display=\"onClick\"]`\n            );\n            const anchors = Array.from(anchorEls).map((el) => \"#\" + el.id);\n\n            // Always suggest the top and the bottom of the page as internal link\n            // anchor even if the header and the footer are not in the DOM. Indeed,\n            // the \"scrollTo\" function handles the scroll towards those elements\n            // even when they are not in the DOM.\n            if (!anchors.includes(\"#top\")) {\n                anchors.unshift(\"#top\");\n            }\n            if (!anchors.includes(\"#bottom\")) {\n                anchors.push(\"#bottom\");\n            }\n            return anchors;\n        })\n        .catch((error) => {\n            console.debug(error);\n            return [];\n        });\n}\n\n/**\n * Allows the given input to propose existing website URLs.\n *\n * @param {HTMLInputElement} input\n */\nfunction autocompleteWithPages(input, options = {}, env = undefined) {\n    const owlApp = new App(UrlAutoComplete, {\n        env: env || Component.env,\n        dev: env ? env.debug : Component.env.debug,\n        getTemplate,\n        props: {\n            options,\n            loadAnchors,\n            targetDropdown: input,\n        },\n        translatableAttributes: [\"data-tooltip\"],\n        translateFn: appTranslateFn,\n    });\n\n    const container = document.createElement(\"div\");\n    container.classList.add(\"ui-widget\", \"ui-autocomplete\", \"ui-widget-content\", \"border-0\");\n    document.body.appendChild(container);\n    owlApp.mount(container);\n    return () => {\n        owlApp.destroy();\n        container.remove();\n    };\n}\n\n/**\n * @param {jQuery} $element\n * @param {jQuery} [$excluded]\n */\nfunction onceAllImagesLoaded($element, $excluded) {\n    var defs = Array.from($element.find(\"img\").addBack(\"img\")).map((img) => {\n        if (img.complete || ($excluded && ($excluded.is(img) || $excluded.has(img).length))) {\n            return; // Already loaded\n        }\n        var def = new Promise(function (resolve, reject) {\n            $(img).one(\"load\", function () {\n                resolve();\n            });\n        });\n        return def;\n    });\n    return Promise.all(defs);\n}\n\n/**\n * @deprecated\n * @todo create Dialog.prompt instead of this\n */\nfunction prompt(options, _qweb) {\n    /**\n     * A bootstrapped version of prompt() albeit asynchronous\n     * This was built to quickly prompt the user with a single field.\n     * For anything more complex, please use editor.Dialog class\n     *\n     * Usage Ex:\n     *\n     * website.prompt(\"What... is your quest?\").then(function (answer) {\n     *     arthur.reply(answer || \"To seek the Holy Grail.\");\n     * });\n     *\n     * website.prompt({\n     *     select: \"Please choose your destiny\",\n     *     init: function () {\n     *         return [ [0, \"Sub-Zero\"], [1, \"Robo-Ky\"] ];\n     *     }\n     * }).then(function (answer) {\n     *     mame_station.loadCharacter(answer);\n     * });\n     *\n     * @param {Object|String} options A set of options used to configure the prompt or the text field name if string\n     * @param {String} [options.window_title=''] title of the prompt modal\n     * @param {String} [options.input] tell the modal to use an input text field, the given value will be the field title\n     * @param {String} [options.textarea] tell the modal to use a textarea field, the given value will be the field title\n     * @param {String} [options.select] tell the modal to use a select box, the given value will be the field title\n     * @param {Object} [options.default=''] default value of the field\n     * @param {Function} [options.init] optional function that takes the `field` (enhanced with a fillWith() method) and the `dialog` as parameters [can return a promise]\n     */\n    if (typeof options === \"string\") {\n        options = {\n            text: options,\n        };\n    }\n    if (typeof _qweb === \"undefined\") {\n        _qweb = \"website.prompt\";\n    }\n    options = Object.assign(\n        {\n            window_title: \"\",\n            field_name: \"\",\n            default: \"\", // dict notation for IE<9\n            init: function () {},\n            btn_primary_title: _t(\"Create\"),\n            btn_secondary_title: _t(\"Cancel\"),\n        },\n        options || {}\n    );\n\n    var type = intersection(Object.keys(options), [\"input\", \"textarea\", \"select\"]);\n    type = type.length ? type[0] : \"input\";\n    options.field_type = type;\n    options.field_name = options.field_name || options[type];\n\n    var def = new Promise(function (resolve, reject) {\n        var dialog = $(renderToElement(_qweb, options)).appendTo(\"body\");\n        options.$dialog = dialog;\n        var field = dialog.find(options.field_type).first();\n        field.val(options[\"default\"]); // dict notation for IE<9\n        field.fillWith = function (data) {\n            if (field.is(\"select\")) {\n                var select = field[0];\n                data.forEach(function (item) {\n                    select.options[select.options.length] = new window.Option(item[1], item[0]);\n                });\n            } else {\n                field.val(data);\n            }\n        };\n        var init = options.init(field, dialog);\n        Promise.resolve(init).then(function (fill) {\n            if (fill) {\n                field.fillWith(fill);\n            }\n            dialog.modal(\"show\");\n            field.focus();\n            dialog.on(\"click\", \".btn-primary\", function () {\n                var backdrop = $(\".modal-backdrop\");\n                resolve({ val: field.val(), field: field, dialog: dialog });\n                dialog.modal(\"hide\").remove();\n                backdrop.remove();\n            });\n        });\n        dialog.on(\"hidden.bs.modal\", function () {\n            var backdrop = $(\".modal-backdrop\");\n            reject();\n            dialog.remove();\n            backdrop.remove();\n        });\n        if (field.is('input[type=\"text\"], select')) {\n            field.keypress(function (e) {\n                if (e.key === \"Enter\") {\n                    e.preventDefault();\n                    dialog.find(\".btn-primary\").trigger(\"click\");\n                }\n            });\n        }\n    });\n\n    return def;\n}\n\nfunction websiteDomain(self) {\n    var websiteID;\n    self.trigger_up(\"context_get\", {\n        callback: function (ctx) {\n            websiteID = ctx[\"website_id\"];\n        },\n    });\n    return [\"|\", [\"website_id\", \"=\", false], [\"website_id\", \"=\", websiteID]];\n}\n\n/**\n * Checks if the 2 given URLs are the same, to prevent redirecting uselessly\n * from one to another.\n * It will consider naked URL and `www` URL as the same URL.\n * It will consider `https` URL `http` URL as the same URL.\n *\n * @param {string} url1\n * @param {string} url2\n * @returns {Boolean}\n */\nfunction isHTTPSorNakedDomainRedirection(url1, url2) {\n    try {\n        url1 = new URL(url1).host;\n        url2 = new URL(url2).host;\n    } catch {\n        // Incorrect URL, `false` URL..\n        return false;\n    }\n    return url1 === url2 || url1.replace(/^www\\./, \"\") === url2.replace(/^www\\./, \"\");\n}\n\nexport function sendRequest(route, params) {\n    function _addInput(form, name, value) {\n        const param = document.createElement(\"input\");\n        param.setAttribute(\"type\", \"hidden\");\n        param.setAttribute(\"name\", name);\n        param.setAttribute(\"value\", value);\n        form.appendChild(param);\n    }\n\n    const form = document.createElement(\"form\");\n    form.setAttribute(\"action\", route);\n    form.setAttribute(\"method\", params.method || \"POST\");\n    // This is an exception for the 404 page create page button, in backend we\n    // want to open the response in the top window not in the iframe.\n    if (params.forceTopWindow) {\n        form.setAttribute(\"target\", \"_top\");\n    }\n\n    if (odoo.csrf_token) {\n        _addInput(form, \"csrf_token\", odoo.csrf_token);\n    }\n\n    for (const key in params) {\n        const value = params[key];\n        if (Array.isArray(value) && value.length) {\n            for (const val of value) {\n                _addInput(form, key, val);\n            }\n        } else {\n            _addInput(form, key, value);\n        }\n    }\n\n    document.body.appendChild(form);\n    form.submit();\n}\n\n/**\n * Converts a base64 SVG into a base64 PNG.\n *\n * @param {string|HTMLImageElement} src - an URL to a SVG or a *loaded* image\n *      with such an URL. This allows the call to potentially be a bit more\n *      efficient in that second case.\n * @returns {Promise<string>} a base64 PNG (as result of a Promise)\n */\nexport async function svgToPNG(src) {\n    return _exportToPNG(src, \"svg+xml\");\n}\n\n/**\n * Converts a base64 WEBP into a base64 PNG.\n *\n * @param {string|HTMLImageElement} src - an URL to a WEBP or a *loaded* image\n *     with such an URL. This allows the call to potentially be a bit more\n *     efficient in that second case.\n * @returns {Promise<string>} a base64 PNG (as result of a Promise)\n */\nexport async function webpToPNG(src) {\n    return _exportToPNG(src, \"webp\");\n}\n\n/**\n * Converts a formatted base64 image into a base64 PNG.\n *\n * @private\n * @param {string|HTMLImageElement} src - an URL to a image or a *loaded* image\n *     with such an URL. This allows the call to potentially be a bit more\n *     efficient in that second case.\n * @param {string} format - the format of the image\n * @returns {Promise<string>} a base64 PNG (as result of a Promise)\n */\nasync function _exportToPNG(src, format) {\n    function checkImg(imgEl) {\n        // Firefox does not support drawing SVG to canvas unless it has width\n        // and height attributes set on the root <svg>.\n        return imgEl.naturalHeight !== 0;\n    }\n    function toPNGViaCanvas(imgEl) {\n        const canvas = document.createElement(\"canvas\");\n        canvas.width = imgEl.width;\n        canvas.height = imgEl.height;\n        canvas.getContext(\"2d\").drawImage(imgEl, 0, 0);\n        return canvas.toDataURL(\"image/png\");\n    }\n\n    // In case we receive a loaded image and that this image is not problematic,\n    // we can convert it to PNG directly.\n    if (src instanceof HTMLImageElement) {\n        const loadedImgEl = src;\n        if (checkImg(loadedImgEl)) {\n            return toPNGViaCanvas(loadedImgEl);\n        }\n        src = loadedImgEl.src;\n    }\n\n    // At this point, we either did not receive a loaded image or the received\n    // loaded image is problematic => we have to do some asynchronous code.\n    return new Promise((resolve) => {\n        const imgEl = new Image();\n        imgEl.onload = () => {\n            if (format !== \"svg+xml\" || checkImg(imgEl)) {\n                resolve(imgEl);\n                return;\n            }\n\n            // Set arbitrary height on image and attach it to the DOM to force\n            // width computation.\n            imgEl.height = 1000;\n            imgEl.style.opacity = 0;\n            document.body.appendChild(imgEl);\n\n            const request = new XMLHttpRequest();\n            request.open(\"GET\", imgEl.src, true);\n            request.onload = () => {\n                // Convert the data URI to a SVG element\n                const parser = new DOMParser();\n                const result = parser.parseFromString(request.responseText, \"text/xml\");\n                const svgEl = result.getElementsByTagName(\"svg\")[0];\n\n                // Add the attributes Firefox needs and remove the image from\n                // the DOM.\n                svgEl.setAttribute(\"width\", imgEl.width);\n                svgEl.setAttribute(\"height\", imgEl.height);\n                imgEl.remove();\n\n                // Convert the SVG element to a data URI\n                const svg64 = btoa(new XMLSerializer().serializeToString(svgEl));\n                const finalImg = new Image();\n                finalImg.onload = () => {\n                    resolve(finalImg);\n                };\n                finalImg.src = `data:image/svg+xml;base64,${svg64}`;\n            };\n            request.send();\n        };\n        imgEl.src = src;\n    }).then((loadedImgEl) => toPNGViaCanvas(loadedImgEl));\n}\n\n/**\n * Bootstraps an \"empty\" Google Maps iframe.\n *\n * @returns {HTMLIframeElement}\n */\nexport function generateGMapIframe() {\n    const iframeEl = document.createElement(\"iframe\");\n    iframeEl.classList.add(\"s_map_embedded\", \"o_not_editable\");\n    iframeEl.setAttribute(\"width\", \"100%\");\n    iframeEl.setAttribute(\"height\", \"100%\");\n    iframeEl.setAttribute(\"frameborder\", \"0\");\n    iframeEl.setAttribute(\"scrolling\", \"no\");\n    iframeEl.setAttribute(\"marginheight\", \"0\");\n    iframeEl.setAttribute(\"marginwidth\", \"0\");\n    iframeEl.setAttribute(\"src\", \"about:blank\");\n    iframeEl.setAttribute(\"aria-label\", _t(\"Map\"));\n    return iframeEl;\n}\n\n/**\n * Generates a Google Maps URL based on the given parameter.\n *\n * @param {DOMStringMap} dataset\n * @returns {string} a Google Maps URL\n */\nexport function generateGMapLink(dataset) {\n    return (\n        \"https://maps.google.com/maps?q=\" +\n        encodeURIComponent(dataset.mapAddress) +\n        \"&t=\" +\n        encodeURIComponent(dataset.mapType) +\n        \"&z=\" +\n        encodeURIComponent(dataset.mapZoom) +\n        \"&ie=UTF8&iwloc=&output=embed\"\n    );\n}\n\n/**\n * Checks if the edited content is currently previewed as in a mobile device.\n *\n * @param {Object} self - context object (\"this\")\n * @returns {boolean}\n */\nfunction isMobile(self) {\n    let isMobile;\n    self.trigger_up(\"service_context_get\", {\n        callback: (ctx) => {\n            isMobile = ctx[\"isMobile\"];\n        },\n    });\n\n    return isMobile;\n}\n\n/**\n * Returns the parsed data coming from the data-for element for the given form.\n *\n * @param {string} formId\n * @param {HTMLElement} parentEl\n * @returns {Object|undefined} the parsed data\n */\nfunction getParsedDataFor(formId, parentEl) {\n    const dataForEl = parentEl.querySelector(`[data-for='${formId}']`);\n    if (!dataForEl) {\n        return;\n    }\n    return JSON.parse(\n        dataForEl.dataset.values\n            // replaces `True` by `true` if they are after `,` or `:` or `[`\n            .replace(/([,:[]\\s*)True/g, \"$1true\")\n            // replaces `False` and `None` by `\"\"` if they are after `,` or `:` or `[`\n            .replace(/([,:[]\\s*)(False|None)/g, '$1\"\"')\n            // replaces the `'` by `\"` if they are before `,` or `:` or `]` or `}`\n            .replace(/'(\\s*[,:\\]}])/g, '\"$1')\n            // replaces the `'` by `\"` if they are after `{` or `[` or `,` or `:`\n            .replace(/([{[:,]\\s*)'/g, '$1\"')\n    );\n}\n\n/**\n * Deep clones children or parses a string into elements, with or without\n * <script> elements.\n *\n * @param {DocumentFragment|HTMLElement|String} content\n * @param {Boolean} [keepScripts=false] - whether to keep script tags or not.\n * @returns {DocumentFragment}\n */\nexport function cloneContentEls(content, keepScripts = false) {\n    let copyFragment;\n    if (typeof content === \"string\") {\n        copyFragment = new Range().createContextualFragment(content);\n    } else {\n        copyFragment = new DocumentFragment();\n        const els = [...content.children].map((el) => el.cloneNode(true));\n        copyFragment.append(...els);\n    }\n    if (!keepScripts) {\n        copyFragment.querySelectorAll(\"script\").forEach((scriptEl) => scriptEl.remove());\n    }\n    return copyFragment;\n}\n\n/**\n * Checks SEO data and notifies if either the page title or description is not\n * set.\n *\n * @param {Object} seo_data - The SEO data to check.\n * @param {Component} OptimizeSEODialog - Dialog to be displayed\n * @param {Object} services - Services object which will be used to display\n * notifications and dialog.\n */\nexport function checkAndNotifySEO(seo_data, OptimizeSEODialog, services) {\n    if (seo_data) {\n        let message;\n        if (!seo_data.website_meta_title) {\n            message = _t(\"Page title not set.\");\n        } else if (!seo_data.website_meta_description) {\n            message = _t(\"Page description not set.\");\n        }\n        if (message) {\n            const closeNotification = services.notification.add(message, {\n                type: \"warning\",\n                sticky: false,\n                buttons: [\n                    {\n                        name: _t(\"Optimize SEO\"),\n                        onClick: () => {\n                            services.dialog.add(OptimizeSEODialog);\n                            closeNotification();\n                        },\n                    },\n                ],\n            });\n        }\n    }\n}\n\n/**\n * Converts a string into a URL-friendly slug.\n *\n * @param {string} value - The string to slugify.\n * @returns {string} The slugified string.\n */\nexport function slugify(value) {\n    // `NFKD` as in `http_routing` python `slugify()`\n    return !value\n        ? \"\"\n        : value\n              .trim()\n              .normalize(\"NFKD\")\n              .toLowerCase()\n              .replace(/['\u2019]/g, \"-\") // Replace apostrophes with hyphens\n              .replace(/\\s+/g, \"-\") // Replace spaces with -\n              .replace(/[^\\w-]+/g, \"\") // Remove all non-word chars\n              .replace(/--+/g, \"-\"); // Replace multiple - with single -\n}\n\npatch(urlUtils, {\n    isAbsoluteURLInCurrentDomain(url, env = null) {\n        const res = super.isAbsoluteURLInCurrentDomain(url, env);\n        if (res) {\n            return true;\n        }\n\n        const w = env?.services.website.currentWebsite;\n        if (!w) {\n            return false;\n        }\n\n        // Make sure that while being on abc.odoo.com, if you edit a link and\n        // enter an absolute URL using your real domain, it is still considered\n        // to be added as relative, preferably.\n        // In the past, you could not edit your website from abc.odoo.com if you\n        // properly configured your real domain already.\n        let origin;\n        try {\n            // Needed: \"http:\" would crash\n            origin = new URL(url, window.location.origin).origin;\n        } catch {\n            return false;\n        }\n        return `${origin}/`.startsWith(w.domain);\n    },\n});\n\nexport default {\n    loadAnchors: loadAnchors,\n    autocompleteWithPages: autocompleteWithPages,\n    onceAllImagesLoaded: onceAllImagesLoaded,\n    prompt: prompt,\n    sendRequest: sendRequest,\n    websiteDomain: websiteDomain,\n    isHTTPSorNakedDomainRedirection: isHTTPSorNakedDomainRedirection,\n    svgToPNG: svgToPNG,\n    webpToPNG: webpToPNG,\n    generateGMapIframe: generateGMapIframe,\n    generateGMapLink: generateGMapLink,\n    isMobile: isMobile,\n    getParsedDataFor: getParsedDataFor,\n    cloneContentEls: cloneContentEls,\n    checkAndNotifySEO: checkAndNotifySEO,\n    slugify: slugify,\n};\n", "import { AutoComplete } from \"@web/core/autocomplete/autocomplete\";\nimport { useEffect } from \"@odoo/owl\";\n\nexport class AutoCompleteWithPages extends AutoComplete {\n    static props = {\n        ...AutoComplete.props,\n        targetDropdown: { type: HTMLElement },\n    };\n    static template = \"website.AutoCompleteWithPages\";\n\n    setup() {\n        super.setup();\n        useEffect(\n            (input, inputRef) => {\n                if (inputRef) {\n                    inputRef.value = input.value;\n                }\n                const targetBlur = this.onInputBlur.bind(this);\n                const targetClick = this._syncInputClick.bind(this);\n                const targetChange = this.onInputChange.bind(this);\n                const targetInput = this._syncInputValue.bind(this);\n                const targetKeydown = this.onInputKeydown.bind(this);\n                const targetFocus = this.onInputFocus.bind(this);\n                input.addEventListener(\"blur\", targetBlur);\n                input.addEventListener(\"click\", targetClick);\n                input.addEventListener(\"change\", targetChange);\n                input.addEventListener(\"input\", targetInput);\n                input.addEventListener(\"keydown\", targetKeydown);\n                input.addEventListener(\"focus\", targetFocus);\n                return () => {\n                    input.removeEventListener(\"blur\", targetBlur);\n                    input.removeEventListener(\"click\", targetClick);\n                    input.removeEventListener(\"change\", targetChange);\n                    input.removeEventListener(\"input\", targetInput);\n                    input.removeEventListener(\"keydown\", targetKeydown);\n                    input.removeEventListener(\"focus\", targetFocus);\n                };\n            },\n            () => [this.targetDropdown, this.inputRef.el]\n        );\n    }\n\n    get targetDropdown() {\n        return this.props.targetDropdown;\n    }\n\n    _syncInputClick(ev) {\n        ev.stopPropagation();\n        this.onInputClick(ev);\n    }\n\n    async _syncInputValue() {\n        if (this.inputRef.el) {\n            this.inputRef.el.value = this.targetDropdown.value;\n            this.onInput();\n        }\n    }\n\n    /**\n     * @override\n     */\n    onInputFocus(ev) {\n        this.targetDropdown.setSelectionRange(0, this.targetDropdown.value.length);\n        this.props.onFocus(ev);\n    }\n}\n", "import { Component } from \"@odoo/owl\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { useChildRef } from \"@web/core/utils/hooks\";\nimport { AutoCompleteWithPages } from \"@website/components/autocomplete_with_pages/autocomplete_with_pages\";\n\n// TODO: we probably don't need it anymore after merging html_builder\n// see: https://github.com/odoo/odoo/pull/187091\nexport class UrlAutoComplete extends Component {\n    static props = {\n        options: { type: Object },\n        loadAnchors: { type: Function },\n        targetDropdown: { type: HTMLElement },\n    };\n    static template = \"website.UrlAutoComplete\";\n    static components = { AutoCompleteWithPages };\n\n    setup() {\n        this.inputRef = useChildRef();\n    }\n\n    get dropdownClass() {\n        const classList = [];\n        for (const key in this.props.options?.classes) {\n            classList.push(key, this.props.options.classes[key]);\n        }\n        return classList.join(\" \");\n    }\n\n    get dropdownOptions() {\n        const options = {};\n        if (this.props.options?.position) {\n            options.position = this.props.options?.position;\n        }\n        return options;\n    }\n\n    get sources() {\n        return [\n            {\n                optionSlot: \"option\",\n                options: async (term) => {\n                    const makeItem = (item) => ({\n                        cssClass: \"ui-autocomplete-item\",\n                        label: item.label,\n                        onSelect: this.onSelect.bind(this, item.value),\n                    });\n\n                    if (term[0] === \"#\") {\n                        const anchors = await this.props.loadAnchors(\n                            term,\n                            this.props.options && this.props.options.body\n                        );\n                        return anchors.map((anchor) => makeItem({ label: anchor, value: anchor }));\n                    } else if (term.startsWith(\"http\") || term.length === 0) {\n                        // avoid useless call to /website/get_suggested_links\n                        return [];\n                    }\n                    if (this.props.options.isDestroyed?.()) {\n                        return [];\n                    }\n                    const res = await rpc(\"/website/get_suggested_links\", {\n                        needle: term,\n                        limit: 15,\n                    });\n                    const choices = [];\n                    for (const page of res.matching_pages) {\n                        choices.push(makeItem(page));\n                    }\n                    for (const other of res.others) {\n                        if (other.values.length) {\n                            choices.push({\n                                cssClass: \"ui-autocomplete-category\",\n                                data: { separator: true },\n                                label: other.title,\n                            });\n                            for (const page of other.values) {\n                                choices.push(makeItem(page));\n                            }\n                        }\n                    }\n                    return choices;\n                },\n            },\n        ];\n    }\n\n    onSelect(value) {\n        this.inputRef.value = value;\n        this.props.targetDropdown.value = value;\n        this.props.options.urlChosen?.();\n    }\n\n    onInput({ inputValue }) {\n        this.props.targetDropdown.value = inputValue;\n    }\n}\n", "import { Record } from \"@mail/core/common/record\";\n\nexport class Website extends Record {\n    static id = \"id\";\n    static _name = \"website\";\n\n    /** @type {number} */\n    id;\n    /** @type {string} */\n    name;\n}\n\nWebsite.register();\n", "import { fields, Record } from \"@mail/core/common/record\";\n\nexport class WebsiteVisitor extends Record {\n    static _name = \"website.visitor\";\n    static id = \"id\";\n\n    country = fields.One(\"res.country\", {\n        /** @this {import(\"models\").WebsiteVisitor} */\n        compute() {\n            return this.partner_id?.country_id || this.country_id;\n        },\n    });\n    country_id = fields.One(\"res.country\");\n    discuss_channel_ids = fields.Many(\"Thread\");\n    /** @type {string} */\n    display_name;\n    lang_id = fields.One(\"res.lang\");\n    partner_id = fields.One(\"res.partner\");\n    website_id = fields.One(\"website\");\n}\n\nWebsiteVisitor.register();\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\n\nregistry.category(\"website.form_editor_actions\").add(\"send_mail\", {\n    formFields: [\n        {\n            type: \"char\",\n            custom: true,\n            required: true,\n            fillWith: \"name\",\n            name: \"name\",\n            string: _t(\"Your Name\"),\n        },\n        {\n            type: \"tel\",\n            custom: true,\n            fillWith: \"phone\",\n            name: \"phone\",\n            string: _t(\"Phone Number\"),\n        },\n        {\n            type: \"email\",\n            modelRequired: true,\n            fillWith: \"email\",\n            name: \"email_from\",\n            string: _t(\"Your Email\"),\n        },\n        {\n            type: \"char\",\n            custom: true,\n            fillWith: \"commercial_company_name\",\n            name: \"company\",\n            string: _t(\"Your Company\"),\n        },\n        {\n            type: \"char\",\n            modelRequired: true,\n            name: \"subject\",\n            string: _t(\"Subject\"),\n        },\n        {\n            type: \"text\",\n            custom: true,\n            required: true,\n            name: \"description\",\n            string: _t(\"Your Question\"),\n        },\n    ],\n    fields: [\n        {\n            name: \"email_to\",\n            type: \"char\",\n            required: true,\n            string: _t(\"Recipient Email\"),\n            defaultValue: \"info@yourcompany.example.com\",\n        },\n    ],\n});\n", "import { registry } from \"@web/core/registry\";\nimport { standardFieldProps } from \"@web/views/fields/standard_field_props\";\nimport { useBus, useService } from \"@web/core/utils/hooks\";\nimport { Component, xml } from \"@odoo/owl\";\n\nexport class BarcodeHandlerField extends Component {\n    static template = xml``;\n    static props = { ...standardFieldProps };\n    setup() {\n        const barcode = useService(\"barcode\");\n        useBus(barcode.bus, \"barcode_scanned\", this.onBarcodeScanned);\n    }\n    onBarcodeScanned(event) {\n        const { barcode } = event.detail;\n        this.props.record.update({ [this.props.name]: barcode });\n    }\n}\n\nexport const barcodeHandlerField = {\n    component: BarcodeHandlerField,\n};\n\nregistry.category(\"fields\").add(\"barcode_handler\", barcodeHandlerField);\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { getVisibleElements } from \"@web/core/utils/ui\";\nimport { Macro } from \"@web/core/macro\";\n\nconst ACTION_HELPERS = {\n    click(el) {\n        el.dispatchEvent(new MouseEvent(\"mouseover\"));\n        el.dispatchEvent(new MouseEvent(\"mouseenter\"));\n        el.dispatchEvent(new MouseEvent(\"mousedown\"));\n        el.dispatchEvent(new MouseEvent(\"mouseup\"));\n        el.click();\n        el.dispatchEvent(new MouseEvent(\"mouseout\"));\n        el.dispatchEvent(new MouseEvent(\"mouseleave\"));\n    },\n    text(el, value) {\n        this.click(el);\n        el.value = value;\n        el.dispatchEvent(new InputEvent(\"input\", { bubbles: true }));\n        el.dispatchEvent(new InputEvent(\"change\", { bubbles: true }));\n    },\n};\n\nfunction clickOnButton(selector) {\n    const button = document.body.querySelector(selector);\n    if (button) {\n        button.click();\n    }\n}\nfunction updatePager(position) {\n    const pager = document.body.querySelector(\"nav.o_pager\");\n    if (!pager || pager.innerText.includes(\"-\")) {\n        // we don't change pages if we are in a multi record view\n        return;\n    }\n    let next;\n    if (position === \"first\") {\n        next = 1;\n    } else {\n        next = parseInt(pager.querySelector(\".o_pager_limit\").textContent, 10);\n    }\n    let current = parseInt(pager.innerText.split('/')[0], 10);\n    if (current === next) {\n        return;\n    }\n    new Macro({\n        name: \"updating pager\",\n        timeout: 1000,\n        steps: [\n            {\n                trigger: \"span.o_pager_value\",\n                async action(trigger) {\n                    ACTION_HELPERS.click(trigger);\n                },\n            },\n            {\n                trigger: \"input.o_pager_value\",\n                async action(trigger) {\n                    ACTION_HELPERS.text(trigger, next);\n                },\n            },\n        ],\n    }).start();\n}\n\nexport const COMMANDS = {\n    \"OCDEDIT\": () => clickOnButton(\".o_form_button_edit\"),\n    \"OCDDISC\": () => clickOnButton(\".o_form_button_cancel\"),\n    \"OCDSAVE\": () => clickOnButton(\".o_form_button_save\"),\n    \"OCDPREV\": () => clickOnButton(\".o_pager_previous\"),\n    \"OCDNEXT\": () => clickOnButton(\".o_pager_next\"),\n    \"OCDPAGERFIRST\": () => updatePager(\"first\"),\n    \"OCDPAGERLAST\": () => updatePager(\"last\"),\n};\n\nexport const barcodeGenericHandlers = {\n    dependencies: [\"ui\", \"barcode\", \"notification\"],\n    start(env, { ui, barcode, notification }) {\n\n        barcode.bus.addEventListener(\"barcode_scanned\", (ev) => {\n            const barcode = ev.detail.barcode;\n            if (barcode.startsWith(\"OBT\")) {\n                let targets = [];\n                try {\n                    // the scanned barcode could be anything, and could crash the queryselectorall\n                    // function\n                    targets = getVisibleElements(ui.activeElement, `[barcode_trigger=${barcode.slice(3)}]`);\n                } catch {\n                    console.warn(`Barcode '${barcode}' is not valid`);\n                }\n                for (let elem of targets) {\n                    elem.click();\n                }\n            }\n            if (barcode.startsWith(\"OCD\")) {\n                const fn = COMMANDS[barcode];\n                if (fn) {\n                    fn();\n                } else {\n                    notification.add(_t(\"Barcode: %(barcode)s\", { barcode }), {\n                        title: _t(\"Unknown barcode command\"),\n                        type: \"danger\"\n                    });\n                }\n            }\n        });\n    }\n};\n\nregistry.category(\"services\").add(\"barcode_handlers\", barcodeGenericHandlers);\n", "import { isBrowserChrome, isMobileOS } from \"@web/core/browser/feature_detection\";\nimport { registry } from \"@web/core/registry\";\nimport { session } from \"@web/session\";\nimport { EventBus, whenReady } from \"@odoo/owl\";\n\nfunction isEditable(element) {\n    return element.matches('input,textarea,[contenteditable=\"true\"]');\n}\n\nfunction makeBarcodeInput() {\n    const inputEl = document.createElement('input');\n    inputEl.setAttribute(\"style\", \"position:fixed;top:50%;transform:translateY(-50%);z-index:-1;opacity:0\");\n    inputEl.setAttribute(\"autocomplete\", \"off\");\n    inputEl.setAttribute(\"inputmode\", \"none\"); // magic! prevent native keyboard from popping\n    inputEl.classList.add(\"o-barcode-input\");\n    inputEl.setAttribute('name', 'barcode');\n    return inputEl;\n}\n\nexport const barcodeService = {\n    // Keys from a barcode scanner are usually processed as quick as possible,\n    // but some scanners can use an intercharacter delay (we support <= 50 ms)\n    maxTimeBetweenKeysInMs: session.max_time_between_keys_in_ms || 150,\n\n    // this is done here to make it easily mockable in mobile tests\n    isMobileChrome: isMobileOS() && isBrowserChrome(),\n\n    cleanBarcode: function(barcode) {\n        return barcode.replace(/Alt|Shift|Control/g, '');\n    },\n\n    start() {\n        const bus = new EventBus();\n        let timeout = null;\n\n        let bufferedBarcode = \"\";\n        let currentTarget = null;\n        let barcodeInput = null;\n\n        function handleBarcode(barcode, target) {\n            bus.trigger('barcode_scanned', {barcode,target});\n            if (target.getAttribute('barcode_events') === \"true\") {\n                const barcodeScannedEvent = new CustomEvent(\"barcode_scanned\", { detail: { barcode, target } });\n                target.dispatchEvent(barcodeScannedEvent);\n            }\n        }\n\n        /**\n         * check if we have a barcode, and trigger appropriate events\n         */\n        function checkBarcode(ev) {\n            let str = barcodeInput ? barcodeInput.value : bufferedBarcode;\n            str = barcodeService.cleanBarcode(str);\n            if (str.length >= 3) {\n                if (ev) {\n                    ev.preventDefault();\n                }\n                handleBarcode(str, currentTarget);\n            }\n            if (barcodeInput) {\n                barcodeInput.value = \"\";\n            }\n            bufferedBarcode = \"\";\n            currentTarget = null;\n        }\n\n        function keydownHandler(ev) {\n            if (!ev.key) {\n                // Chrome may trigger incomplete keydown events under certain circumstances.\n                // E.g. when using browser built-in autocomplete on an input.\n                // See https://stackoverflow.com/questions/59534586/google-chrome-fires-keydown-event-when-form-autocomplete\n                return;\n            }\n            // Ignore 'Shift', 'Escape', 'Backspace', 'Insert', 'Delete', 'Home', 'End', Arrow*, F*, Page*, ...\n            // meta is often used for UX purpose (like shortcuts)\n            // Notes:\n            // - shiftKey is not ignored because it can be used by some barcode scanner for digits.\n            // - altKey/ctrlKey are not ignored because it can be used in some barcodes (e.g. GS1 separator)\n            const isSpecialKey = !['Control', 'Alt'].includes(ev.key) && (ev.key.length > 1 || ev.metaKey);\n            const isEndCharacter = ev.key.match(/(Enter|Tab)/);\n\n            // Don't catch non-printable keys except 'enter' and 'tab'\n            if (isSpecialKey && !isEndCharacter) {\n                return;\n            }\n\n            currentTarget = ev.target;\n            // Don't catch events targeting elements that are editable because we\n            // have no way of redispatching 'genuine' key events. Resent events\n            // don't trigger native event handlers of elements. So this means that\n            // our fake events will not appear in eg. an <input> element.\n            if (currentTarget !== barcodeInput && isEditable(currentTarget) &&\n                !currentTarget.dataset.enableBarcode &&\n                currentTarget.getAttribute(\"barcode_events\") !== \"true\") {\n                return;\n            }\n\n            clearTimeout(timeout);\n            if (isEndCharacter) {\n                checkBarcode(ev);\n            } else {\n                bufferedBarcode += ev.key;\n                timeout = setTimeout(checkBarcode, barcodeService.maxTimeBetweenKeysInMs);\n            }\n        }\n\n        function mobileChromeHandler(ev) {\n            if (ev.key === \"Unidentified\") {\n                return;\n            }\n            if (document.activeElement && !document.activeElement.matches('input:not([type]), input[type=\"text\"], textarea, [contenteditable], ' +\n                '[type=\"email\"], [type=\"number\"], [type=\"password\"], [type=\"tel\"], [type=\"search\"]')) {\n                barcodeInput.focus();\n            }\n            keydownHandler(ev);\n        }\n\n        whenReady(() => {\n            const isMobileChrome = barcodeService.isMobileChrome;\n            if (isMobileChrome) {\n                barcodeInput = makeBarcodeInput();\n                document.body.appendChild(barcodeInput);\n            }\n            const handler = isMobileChrome ? mobileChromeHandler : keydownHandler;\n            document.body.addEventListener('keydown', handler);\n        });\n\n        return {\n            bus,\n        };\n    },\n};\n\nregistry.category(\"services\").add(\"barcode\", barcodeService);\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { scanBarcode } from \"@web/core/barcode/barcode_dialog\";\nimport { isBarcodeScannerSupported } from \"@web/core/barcode/barcode_video_scanner\";\nimport { Component } from \"@odoo/owl\";\nimport { useService } from \"@web/core/utils/hooks\";\n\nexport class BarcodeScanner extends Component {\n    static template = \"barcodes.BarcodeScanner\";\n    static props = {\n        onBarcodeScanned: { type: Function },\n    };\n\n    setup() {\n        this.notification = useService(\"notification\");\n        this.isBarcodeScannerSupported = isBarcodeScannerSupported();\n        this.scanBarcode = () => scanBarcode(this.env, this.facingMode);\n    }\n\n    get facingMode() {\n        return \"environment\";\n    }\n\n    async openMobileScanner() {\n        let error = null;\n        let barcode = null;\n        try {\n            barcode = await this.scanBarcode();\n        } catch (err) {\n            error = err.message;\n        }\n\n        if (barcode) {\n            this.props.onBarcodeScanned(barcode);\n            if (\"vibrate\" in window.navigator) {\n                window.navigator.vibrate(100);\n            }\n        } else {\n            this.notification.add(error || _t(\"Please, Scan again!\"), {\n                type: \"warning\",\n            });\n        }\n    }\n}\n", "import { BarcodeDialog } from \"@web/core/barcode/barcode_dialog\";\nimport { Component, onMounted, useRef, useState } from \"@odoo/owl\";\nimport { getActiveHotkey } from \"@web/core/hotkeys/hotkey_service\";\nimport { _t } from \"@web/core/l10n/translation\";\n\nexport class BarcodeInput extends Component {\n    static template = \"barcodes.BarcodeInput\";\n    static props = {\n        onSubmit: Function,\n        placeholder: { type: String, optional: true },\n    };\n    static defaultProps = {\n        placeholder: _t(\"Enter a barcode...\"),\n    };\n    setup() {\n        this.state = useState({\n            barcode: false,\n        });\n        this.barcodeManual = useRef(\"manualBarcode\");\n        // Autofocus processing was blocked because a document already has a focused element.\n        onMounted(() => {\n            this.barcodeManual.el.focus();\n        });\n    }\n\n    /**\n     * Called when press Enter after filling barcode input manually.\n     *\n     * @private\n     * @param {KeyboardEvent} ev\n     */\n    _onKeydown(ev) {\n        const hotkey = getActiveHotkey(ev);\n        if (hotkey === \"enter\" && this.state.barcode) {\n            this.props.onSubmit(this.state.barcode);\n        }\n    }\n}\n\nexport class ManualBarcodeScanner extends BarcodeDialog {\n    static template = \"barcodes.ManualBarcodeScanner\";\n    static components = {\n        ...BarcodeDialog.components,\n        BarcodeInput,\n    };\n    static props = [...BarcodeDialog.props, \"placeholder?\"];\n}\n", "import { registry } from \"@web/core/registry\";\nimport { FloatField, floatField } from \"@web/views/fields/float/float_field\";\n\nexport class FloatScannableField extends FloatField {\n    static template = \"barcodes.FloatScannableField\";\n    onBarcodeScanned() {\n        this.inputRef.el.dispatchEvent(new InputEvent(\"input\"));\n    }\n}\n\nexport const floatScannableField = {\n    ...floatField,\n    component: FloatScannableField,\n};\n\nregistry.category(\"fields\").add(\"field_float_scannable\", floatScannableField);\n", "export class BarcodeParser {\n    static barcodeNomenclatureFields = [\"name\", \"rule_ids\", \"upc_ean_conv\"];\n    static barcodeRuleFields = [\"name\", \"sequence\", \"type\", \"encoding\", \"pattern\", \"alias\"];\n    static async fetchNomenclature(orm, id) {\n        const [nomenclature] = await orm.read(\n            \"barcode.nomenclature\",\n            [id],\n            this.barcodeNomenclatureFields\n        );\n        let rules = await orm.searchRead(\n            \"barcode.rule\",\n            [[\"barcode_nomenclature_id\", \"=\", id]],\n            this.barcodeRuleFields\n        );\n        rules = rules.sort((a, b) => {\n            return a.sequence - b.sequence;\n        });\n        nomenclature.rules = rules;\n        return nomenclature;\n    }\n\n    constructor() {\n        this.setup(...arguments);\n    }\n\n    setup({ nomenclature }) {\n        this.nomenclature = nomenclature;\n    }\n\n    /**\n     * This algorithm is identical for all fixed length numeric GS1 data structures.\n     *\n     * It is also valid for EAN-8, EAN-12 (UPC-A), EAN-13 check digit after sanitizing.\n     * https://www.gs1.org/sites/default/files/docs/barcodes/GS1_General_Specifications.pdf\n     *\n     * @param {String} numericBarcode Need to have a length of 18\n     * @returns {number} Check Digit\n     */\n    get_barcode_check_digit(numericBarcode) {\n        let oddsum = 0, evensum = 0, total = 0;\n        // Reverses the barcode to be sure each digit will be in the right place\n        // regardless the barcode length.\n        const code = numericBarcode.split('').reverse();\n        // Removes the last barcode digit (should not be took in account for its own computing).\n        code.shift();\n\n        // Multiply value of each position by\n        // N1  N2  N3  N4  N5  N6  N7  N8  N9  N10 N11 N12 N13 N14 N15 N16 N17 N18\n        // x3  X1  x3  x1  x3  x1  x3  x1  x3  x1  x3  x1  x3  x1  x3  x1  x3  CHECK_DIGIT\n        for (let i = 0; i < code.length; i++) {\n            if (i % 2 === 0) {\n                evensum += parseInt(code[i]);\n            } else {\n                oddsum += parseInt(code[i]);\n            }\n        }\n        total = evensum * 3 + oddsum;\n        return (10 - total % 10) % 10;\n    }\n\n    /**\n     * Checks if the barcode string is encoded with the provided encoding.\n     *\n     * @param {String} barcode\n     * @param {String} encoding could be 'any' (no encoding rules), 'ean8', 'upca' or 'ean13'\n     * @returns {boolean}\n     */\n    check_encoding(barcode, encoding) {\n        if (encoding === 'any') {\n            return true;\n        }\n        const barcodeSizes = {\n            ean8: 8,\n            ean13: 13,\n            upca: 12,\n        };\n        return barcode.length === barcodeSizes[encoding] && /^\\d+$/.test(barcode) &&\n            this.get_barcode_check_digit(barcode) === parseInt(barcode[barcode.length - 1]);\n    }\n\n    /**\n     * Sanitizes a EAN-13 prefix by padding it with chars zero.\n     *\n     * @param {String} ean\n     * @returns {String}\n     */\n    sanitize_ean(ean) {\n        ean = ean.substr(0, 13);\n        ean = \"0\".repeat(13 - ean.length) + ean;\n        return ean.substr(0, 12) + this.get_barcode_check_digit(ean);\n    }\n\n    /**\n     * Sanitizes a UPC-A prefix by padding it with chars zero.\n     *\n     * @param {String} upc\n     * @returns {String}\n     */\n    sanitize_upc(upc) {\n        return this.sanitize_ean(upc).substr(1, 12);\n    }\n\n    // Checks if barcode matches the pattern\n    // Additionnaly retrieves the optional numerical content in barcode\n    // Returns an object containing:\n    // - value: the numerical value encoded in the barcode (0 if no value encoded)\n    // - base_code: the barcode in which numerical content is replaced by 0's\n    // - match: boolean\n    match_pattern(barcode, pattern, encoding) {\n        var match = {\n            value: 0,\n            base_code: barcode,\n            match: false,\n        };\n        barcode = barcode.replace(\"\\\\\", \"\\\\\\\\\").replace(\"{\", '\\{').replace(\"}\", \"\\}\").replace(\".\", \"\\.\");\n\n        var numerical_content = pattern.match(/[{][N]*[D]*[}]/); // look for numerical content in pattern\n        var base_pattern = pattern;\n        if(numerical_content){ // the pattern encodes a numerical content\n            var num_start = numerical_content.index; // start index of numerical content\n            var num_length = numerical_content[0].length; // length of numerical content\n            var value_string = barcode.substr(num_start, num_length-2); // numerical content in barcode\n            var whole_part_match = numerical_content[0].match(\"[{][N]*[D}]\"); // looks for whole part of numerical content\n            var decimal_part_match = numerical_content[0].match(\"[{N][D]*[}]\"); // looks for decimal part\n            var whole_part = value_string.substr(0, whole_part_match.index+whole_part_match[0].length-2); // retrieve whole part of numerical content in barcode\n            var decimal_part = \"0.\" + value_string.substr(decimal_part_match.index, decimal_part_match[0].length-1); // retrieve decimal part\n            if (whole_part === ''){\n                whole_part = '0';\n            }\n            match.value = parseInt(whole_part) + parseFloat(decimal_part);\n\n            // replace numerical content by 0's in barcode and pattern\n            match.base_code = barcode.substr(0, num_start);\n            base_pattern = pattern.substr(0, num_start);\n            for(var i=0;i<(num_length-2);i++) {\n                match.base_code += \"0\";\n                base_pattern += \"0\";\n            }\n            match.base_code += barcode.substr(num_start + num_length - 2, barcode.length - 1);\n            base_pattern += pattern.substr(num_start + num_length, pattern.length - 1);\n\n            match.base_code = match.base_code\n                .replace(\"\\\\\\\\\", \"\\\\\")\n                .replace(\"\\{\", \"{\")\n                .replace(\"\\}\",\"}\")\n                .replace(\"\\.\",\".\");\n\n            var base_code = match.base_code.split('');\n            if (encoding === 'ean13') {\n                base_code[12] = '' + this.get_barcode_check_digit(match.base_code);\n            } else if (encoding === 'ean8') {\n                base_code[7]  = '' + this.get_barcode_check_digit(match.base_code);\n            } else if (encoding === 'upca') {\n                base_code[11] = '' + this.get_barcode_check_digit(match.base_code);\n            }\n            match.base_code = base_code.join('');\n        }\n\n        base_pattern = base_pattern.split('|')\n            .map(part => part.startsWith('^') ? part : '^' + part)\n            .join('|');\n        match.match = match.base_code.match(base_pattern);\n\n        return match;\n    }\n\n    /**\n     * Attempts to interpret a barcode (string encoding a barcode Code-128)\n     *\n     * @param {string} barcode\n     * @returns {Object} the returned object containing informations about the barcode:\n     *      - code: the barcode\n     *      - type: the type of the barcode (e.g. alias, unit product, weighted product...)\n     *      - value: if the barcode encodes a numerical value, it will be put there\n     *      - base_code: the barcode with all the encoding parts set to zero; the one put on the product in the backend\n     */\n    parse_barcode(barcode) {\n        if (barcode.match(/^urn:/)) {\n            return this.parseURI(barcode);\n        }\n        return this.parseBarcodeNomenclature(barcode);\n    }\n\n    parseBarcodeNomenclature(barcode) {\n        const parsed_result = {\n            encoding: '',\n            type:'error',\n            code:barcode,\n            base_code: barcode,\n            value: 0,\n        };\n\n        if (!this.nomenclature) {\n            return parsed_result;\n        }\n\n        var rules = this.nomenclature.rules;\n        for (var i = 0; i < rules.length; i++) {\n            var rule = rules[i];\n            var cur_barcode = barcode;\n\n            if (    rule.encoding === 'ean13' &&\n                    this.check_encoding(barcode,'upca') &&\n                    this.nomenclature.upc_ean_conv in {'upc2ean':'','always':''} ){\n                cur_barcode = '0' + cur_barcode;\n            } else if (rule.encoding === 'upca' &&\n                    this.check_encoding(barcode,'ean13') &&\n                    barcode[0] === '0' &&\n                    this.nomenclature.upc_ean_conv in {'ean2upc':'','always':''} ){\n                cur_barcode = cur_barcode.substr(1,12);\n            }\n\n            if (!this.check_encoding(cur_barcode,rule.encoding)) {\n                continue;\n            }\n\n            var match = this.match_pattern(cur_barcode, rules[i].pattern, rule.encoding);\n            if (match.match) {\n                if(rules[i].type === 'alias') {\n                    barcode = rules[i].alias;\n                    parsed_result.code = barcode;\n                    parsed_result.type = 'alias';\n                }\n                else {\n                    parsed_result.encoding  = rules[i].encoding;\n                    parsed_result.type      = rules[i].type;\n                    parsed_result.value     = match.value;\n                    parsed_result.code      = cur_barcode;\n                    if (rules[i].encoding === \"ean13\"){\n                        parsed_result.base_code = this.sanitize_ean(match.base_code);\n                    }\n                    else{\n                        parsed_result.base_code = match.base_code;\n                    }\n                    return parsed_result;\n                }\n            }\n        }\n        return parsed_result;\n    }\n\n    // URI methods\n    /**\n     * Parse an URI into an object with either the product and its lot/serial\n     * number, either the package.\n     * @param {String} barcode\n     * @returns {Object}\n     */\n    parseURI(barcode) {\n        const uriParts = barcode.split(\":\").map(v => v.trim());\n        // URI should be formatted like that (number is the index once split):\n        // 0: urn, 1: epc, 2: id/tag, 3: identifier, 4: data\n        const identifier = uriParts[3];\n        const data = uriParts[4].split(\".\");\n        if (identifier === \"lgtin\" || identifier === \"sgtin\") {\n            return this.convertURIGTINDataIntoProductAndTrackingNumber(barcode, data);\n        } else if (identifier === \"sgtin-96\" || identifier === \"sgtin-198\") {\n            // Same compute then SGTIN but we have to remove the filter.\n            return this.convertURIGTINDataIntoProductAndTrackingNumber(barcode, data.slice(1));\n        } else if (identifier === \"sscc\") {\n            return this.convertURISSCCDataIntoPackage(barcode, data);\n        } else if (identifier === \"sscc-96\") {\n            // Same compute then SSCC but we have to remove the filter.\n            return this.convertURISSCCDataIntoPackage(barcode, data.slice(1));\n        }\n        return barcode;\n    }\n\n    convertURIGTINDataIntoProductAndTrackingNumber(base_code, data) {\n        const [gs1CompanyPrefix, itemRefAndIndicator, trackingNumber] = data;\n        const indicator = itemRefAndIndicator[0];\n        const itemRef = itemRefAndIndicator.slice(1);\n        let productBarcode = indicator + gs1CompanyPrefix + itemRef;\n        productBarcode += this.get_barcode_check_digit(productBarcode + \"0\");\n        return [\n            {\n                base_code,\n                code: productBarcode,\n                string_value: productBarcode,\n                type: \"product\",\n                value: productBarcode,\n            }, {\n                base_code,\n                code: trackingNumber,\n                string_value: trackingNumber,\n                type: \"lot\",\n                value: trackingNumber,\n            }\n        ];\n    }\n\n    convertURISSCCDataIntoPackage(base_code, data) {\n        const [gs1CompanyPrefix, serialReference] = data;\n        const extension = serialReference[0];\n        const serialRef = serialReference.slice(1);\n        let sscc = extension + gs1CompanyPrefix + serialRef;\n        sscc += this.get_barcode_check_digit(sscc + \"0\");\n        return [{\n            base_code,\n            code: sscc,\n            string_value: sscc,\n            type: \"package\",\n            value: sscc,\n        }];\n    }\n}\n", "import { patch } from \"@web/core/utils/patch\";\nimport { BarcodeParser } from \"@barcodes/js/barcode_parser\";\nimport { _t } from \"@web/core/l10n/translation\";\nexport class GS1BarcodeError extends Error {};\n\nexport const FNC1_CHAR = String.fromCharCode(29);\n\npatch(BarcodeParser, {\n    barcodeNomenclatureFields: [\n        ...BarcodeParser.barcodeNomenclatureFields,\n        \"is_gs1_nomenclature\",\n        \"gs1_separator_fnc1\",\n    ],\n    barcodeRuleFields: [\n        ...BarcodeParser.barcodeRuleFields,\n        \"gs1_content_type\",\n        \"gs1_decimal_usage\",\n        \"associated_uom_id\",\n    ],\n});\n\npatch(BarcodeParser.prototype, {\n    setup(attributes) {\n        super.setup(...arguments);\n        // Use the nomenclature's separaor regex, else use an impossible one.\n        const nomenclatureSeparator = this.nomenclature && this.nomenclature.gs1_separator_fnc1;\n        this.gs1SeparatorRegex = new RegExp(nomenclatureSeparator || '.^', 'g');\n    },\n\n    /**\n     * Convert YYMMDD GS1 date into a Date object\n     *\n     * @param {string} gs1Date YYMMDD string date, length must be 6\n     * @returns {Date}\n     */\n    gs1_date_to_date(gs1Date) {\n        // See 7.12 Determination of century in dates:\n        // https://www.gs1.org/sites/default/files/docs/barcodes/GS1_General_Specifications.pdfDetermination of century\n        const now = new Date();\n        const substractYear = parseInt(gs1Date.slice(0, 2)) - (now.getFullYear() % 100);\n        let century = Math.floor(now.getFullYear() / 100);\n        if (51 <= substractYear && substractYear <= 99) {\n            century--;\n        } else if (-99 <= substractYear && substractYear <= -50) {\n            century++;\n        }\n        const year = century * 100 + parseInt(gs1Date.slice(0, 2));\n        const date = new Date(year, parseInt(gs1Date.slice(2, 4) - 1));\n\n        if (gs1Date.slice(-2) === '00'){\n            // Day is not mandatory, when not set -> last day of the month\n            date.setDate(new Date(year, parseInt(gs1Date.slice(2, 4)), 0).getDate());\n        } else {\n            date.setDate(parseInt(gs1Date.slice(-2)));\n        }\n        return date;\n    },\n\n    /**\n     * Perform interpretation of the barcode value depending of the rule.gs1_content_type\n     *\n     * @param {Array} match Result of a regex match with atmost 2 groups (ia and value)\n     * @param {Object} rule Matched Barcode Rule\n     * @returns {Object|null}\n     */\n    parse_gs1_rule_pattern(match, rule) {\n        const result = {\n            rule: Object.assign({}, rule),\n            ai: match[1],\n            string_value: match[2],\n            code: match[2],\n            base_code: match[2],\n            type: rule.type\n        };\n        if (rule.gs1_content_type === 'measure'){\n            let decimalPosition = 0; // Decimal position begin at the end, 0 means no decimal\n            if (rule.gs1_decimal_usage){\n                decimalPosition = parseInt(match[1][match[1].length - 1]);\n            }\n            if (decimalPosition > 0) {\n                const integral = match[2].slice(0, match[2].length - decimalPosition);\n                const decimal = match[2].slice(match[2].length - decimalPosition);\n                result.value = parseFloat( integral + \".\" + decimal);\n            } else {\n                result.value = parseInt(match[2]);\n            }\n        } else if (rule.gs1_content_type === 'identifier'){\n            if (parseInt(match[2][match[2].length - 1]) !== this.get_barcode_check_digit(\"0\".repeat(18 - match[2].length) + match[2])){\n                throw new Error(_t(\"Invalid barcode: the check digit is incorrect\"));\n                // return {error: _t(\"Invalid barcode: the check digit is incorrect\")};\n            }\n            result.value = match[2];\n        } else if (rule.gs1_content_type === 'date'){\n            if (match[2].length !== 6){\n                throw new Error(_t(\"Invalid barcode: can't be formated as date\"));\n                // return {error: _t(\"Invalid barcode: can't be formated as date\")};\n            }\n            result.value = this.gs1_date_to_date(match[2]);\n        } else {\n            result.value = match[2];\n        }\n        return result;\n    },\n\n    /**\n     * Try to decompose the gs1 extanded barcode into several unit of information using gs1 rules.\n     *\n     * @param {string} barcode\n     * @returns {Array} Array of object\n     */\n    gs1_decompose_extended(barcode) {\n        const results = [];\n        const rules = this.nomenclature.rules.filter(rule => rule.encoding === 'gs1-128');\n        const separatorReg = `(?:${FNC1_CHAR}+)?`;\n        barcode = this._convertGS1Separators(barcode);\n        barcode = this.cleanBarcode(barcode);\n\n        while (barcode.length > 0) {\n            const barcodeLength = barcode.length;\n            for (const rule of rules) {\n                const match = barcode.match(\"^\" + rule.pattern + separatorReg);\n                if (match && match.length >= 3) {\n                    const res = this.parse_gs1_rule_pattern(match, rule);\n                    if (res) {\n                        barcode = barcode.slice(match.index + match[0].length);\n                        results.push(res);\n                        if (barcode.length === 0) {\n                            return results; // Barcode completly parsed, no need to keep looping.\n                        }\n                    } else {\n                        throw new GS1BarcodeError(_t(\"This barcode can't be parsed by any barcode rules.\"));\n                    }\n                }\n            }\n            if (barcodeLength === barcode.length) {\n                throw new GS1BarcodeError(_t(\"This barcode can't be partially or fully parsed.\"));\n            }\n        }\n\n        return results;\n    },\n\n    /**\n     * @override\n     * @returns {Object|Array|null} If nomenclature is GS1, returns an array or null\n     */\n    parseBarcodeNomenclature(barcode) {\n        if (this.nomenclature && this.nomenclature.is_gs1_nomenclature) {\n            return this.gs1_decompose_extended(barcode);\n        }\n        return super.parseBarcodeNomenclature(...arguments);\n    },\n\n    /**\n     * Makes all needed operations to clean and prepare the barcode.\n     * @param {string} barcode\n     * @returns {string}\n     */\n    cleanBarcode(barcode) {\n        if (barcode[0] === FNC1_CHAR) {\n            // If first character is the separator, remove it to be able to parse the barcode.\n            barcode = barcode.slice(1);\n        }\n        return barcode;\n    },\n\n    /**\n     * The FNC1 is the default GS1 separator character, but through the field `gs1_separator_fnc1`,\n     * the user has the possibility to define one or multiple characters to use as separator as\n     * a regex. This method replaces all of the matches in the given barcode by the FNC1.\n     *\n     * @param {string} barcode\n     * @returns {string}\n     */\n    _convertGS1Separators: function (barcode) {\n        barcode = barcode.replace(this.gs1SeparatorRegex, FNC1_CHAR);\n        return barcode;\n    },\n});\n", "import { session } from \"@web/session\";\nimport { patch } from \"@web/core/utils/patch\";\nimport { barcodeService } from '@barcodes/barcode_service';\n\nimport { FNC1_CHAR } from \"@barcodes_gs1_nomenclature/js/barcode_parser\";\n\n\npatch(barcodeService, {\n    // Use the regex given by the session, else use an impossible one\n    gs1SeparatorRegex: new RegExp(session.gs1_group_separator_encodings || '.^', 'g'),\n\n    cleanBarcode(barcode) {\n        barcode = barcode.replace(barcodeService.gs1SeparatorRegex, FNC1_CHAR);\n        return super.cleanBarcode(barcode);\n    },\n});\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\n\nasync function doMultiPrint(env, action) {\n    for (const report of action.params.reports) {\n        if (report.type != \"ir.actions.report\") {\n            env.services.notification.add(_t(\"Incorrect type of action submitted as a report, skipping action\"), {\n                title: _t(\"Report Printing Error\"),\n            });\n            continue\n        } else if (report.report_type === \"qweb-html\") {\n            env.services.notification.add(\n                _t(\"HTML reports cannot be auto-printed, skipping report: %s\", report.name),\n                { title: _t(\"Report Printing Error\") }\n            );\n            continue\n        }\n        // WARNING: potential issue if pdf generation fails, then action_service defaults\n        // to HTML and rest of the action chain will break w/potentially never resolving promise\n        await env.services.action.doAction({ type: \"ir.actions.report\", ...report });\n    }\n    if (action.params.anotherAction) {\n        return env.services.action.doAction(action.params.anotherAction);\n    } else if (action.params.onClose) {\n        // handle special cases such as barcode\n        action.params.onClose()\n    } else {\n        return env.services.action.doAction(\"reload_context\");\n    }\n}\n\nregistry.category(\"actions\").add(\"do_multi_print\", doMultiPrint);\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { Component, onWillStart, useState } from \"@odoo/owl\";\nimport { download } from \"@web/core/network/download\";\nimport { registry } from \"@web/core/registry\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { useSetupAction } from \"@web/search/action_hook\";\nimport { Layout } from \"@web/search/layout\";\nimport { standardActionServiceProps } from \"@web/webclient/actions/action_service\";\n\nfunction processLine(line) {\n    return { ...line, lines: [], isFolded: true };\n}\n\nfunction extractPrintData(lines) {\n    const data = [];\n    for (const line of lines) {\n        const { id, model_id, model, unfoldable, level } = line;\n        data.push({\n            id: id,\n            model_id: model_id,\n            model_name: model,\n            unfoldable,\n            level: level || 1,\n        });\n        if (!line.isFolded) {\n            data.push(...extractPrintData(line.lines));\n        }\n    }\n    return data;\n}\n\nexport class TraceabilityReport extends Component {\n    static template = \"stock.TraceabilityReport\";\n    static components = { Layout };\n    static props = { ...standardActionServiceProps };\n\n    setup() {\n        this.actionService = useService(\"action\");\n        this.orm = useService(\"orm\");\n\n        onWillStart(this.onWillStart);\n        useSetupAction({\n            getLocalState: () => ({\n                lines: [...this.state.lines],\n            }),\n        });\n\n        this.state = useState({\n            lines: this.props.state?.lines || [],\n        });\n\n        const { active_id, active_model, auto_unfold, context, lot_name, ttype, url, lang } =\n            this.props.action.context;\n        this.controllerUrl = url;\n\n        this.context = context || {};\n        Object.assign(this.context, {\n            active_id: active_id || this.props.action.params.active_id,\n            auto_unfold: auto_unfold || false,\n            model: active_model || this.props.action.context.params?.active_model || false,\n            lot_name: lot_name || false,\n            ttype: ttype || false,\n            lang: lang || false,\n        });\n\n        if (this.context.model) {\n            this.props.updateActionState({ active_model: this.context.model });\n        }\n\n        this.display = {\n            controlPanel: {},\n            searchPanel: false,\n        };\n    }\n\n    async onWillStart() {\n        if (!this.state.lines.length) {\n            const mainLines = await this.orm.call(\"stock.traceability.report\", \"get_main_lines\", [\n                this.context,\n            ]);\n            this.state.lines = mainLines.map(processLine);\n        }\n    }\n\n    onClickBoundLink(line) {\n        this.actionService.doAction({\n            type: \"ir.actions.act_window\",\n            res_model: line.res_model,\n            res_id: line.res_id,\n            views: [[false, \"form\"]],\n            target: \"current\",\n        });\n    }\n\n    onClickPartner(line) {\n        this.actionService.doAction({\n            type: \"ir.actions.act_window\",\n            res_model: \"res.partner\",\n            res_id: line.partner_id,\n            views: [[false, \"form\"]],\n            target: \"current\",\n        });\n    }\n\n    onClickOpenLot(line) {\n        this.actionService.doAction({\n            type: 'ir.actions.act_window',\n            res_model: 'stock.lot',\n            res_id: line.lot_id,\n            views: [[false, 'form']],\n            target: 'current',\n        });\n    }\n\n    onClickUpDownStream(line) {\n        this.actionService.doAction({\n            type: \"ir.actions.client\",\n            tag: \"stock_report_generic\",\n            name: _t(\"Traceability Report\"),\n            context: {\n                active_id: line.model_id,\n                active_model: line.model,\n                auto_unfold: true,\n                lot_name: line.lot_name !== undefined && line.lot_name,\n                url: \"/stock/output_format/stock/active_id\",\n            },\n        });\n    }\n\n    onClickPrint() {\n        const data = JSON.stringify(extractPrintData(this.state.lines));\n        const url = this.controllerUrl\n            .replace(\":active_id\", this.context.active_id)\n            .replace(\":active_model\", this.context.model)\n            .replace(\"output_format\", \"pdf\");\n\n        download({\n            data: { data },\n            url,\n        });\n    }\n\n    async toggleLine(line) {\n        line.isFolded = !line.isFolded;\n        if (!line.lines.length) {\n            line.lines = (\n                await this.orm.call(\"stock.traceability.report\", \"get_lines\", [line.id], {\n                    model_id: line.model_id,\n                    model_name: line.model,\n                    level: line.level + 30 || 1,\n                })\n            ).map(processLine);\n        }\n    }\n}\n\nregistry.category(\"actions\").add(\"stock_report_generic\", TraceabilityReport);\n", "import { useService } from \"@web/core/utils/hooks\";\nimport { formatFloat } from \"@web/views/fields/formatters\";\nimport { Component } from \"@odoo/owl\";\n\nexport class ReceptionReportLine extends Component {\n    static template = \"stock.ReceptionReportLine\";\n    static props = {\n        data: Object,\n        labelReport: Object,\n        parentIndex: String,\n        showUom: Boolean,\n        precision: Number,\n    };\n\n    setup() {\n        this.ormService = useService(\"orm\");\n        this.actionService = useService(\"action\");\n        this.formatFloat = (val) => formatFloat(val, { digits: [false, this.props.precision] });\n    }\n\n    //---- Handlers ----\n\n    async onClickForecast() {\n        const action = await this.ormService.call(\n            \"stock.move\",\n            \"action_product_forecast_report\",\n            [[this.data.move_out_id]],\n        );\n\n        return this.actionService.doAction(action);\n    }\n\n    async onClickPrint() {\n        if (!this.data.move_out_id) {\n            return;\n        }\n        const modelIds = [this.data.move_out_id];\n        const productQtys = [Math.ceil(this.data.quantity) || '1'];\n\n        return this.actionService.doAction({\n            ...this.props.labelReport,\n            context: { active_ids: modelIds },\n            data: { docids: modelIds, quantity: productQtys.join(\",\") },\n        });\n    }\n\n    async onClickAssign() {\n        await this.ormService.call(\n            \"report.stock.report_reception\",\n            \"action_assign\",\n            [false, [this.data.move_out_id], [this.data.quantity], [this.data.move_ins]],\n        );\n        this.env.bus.trigger(\"update-assign-state\", { isAssigned: true, tableIndex: this.props.parentIndex, lineIndex: this.data.index });\n    }\n\n    async onClickUnassign() {\n        const done = await this.ormService.call(\n            \"report.stock.report_reception\",\n            \"action_unassign\",\n            [false, this.data.move_out_id, this.data.quantity, this.data.move_ins]\n        )\n        if (done) {\n            this.env.bus.trigger(\"update-assign-state\", { isAssigned: false, tableIndex: this.props.parentIndex, lineIndex: this.data.index });\n        }\n    }\n\n    //---- Getters ----\n\n    get data() {\n        return this.props.data;\n    }\n}\n", "import { registry } from \"@web/core/registry\";\nimport { useBus, useService } from \"@web/core/utils/hooks\";\nimport { ControlPanel } from \"@web/search/control_panel/control_panel\";\nimport { ReceptionReportTable } from \"../reception_report_table/stock_reception_report_table\";\nimport { Component, onWillStart, useState } from \"@odoo/owl\";\nimport { standardActionServiceProps } from \"@web/webclient/actions/action_service\";\n\nexport class ReceptionReportMain extends Component {\n    static template = \"stock.ReceptionReportMain\";\n    static components = {\n        ControlPanel,\n        ReceptionReportTable,\n    };\n    static props = { ...standardActionServiceProps };\n\n    setup() {\n        this.controlPanelDisplay = {};\n        this.ormService = useService(\"orm\");\n        this.actionService = useService(\"action\");\n        this.reportName = \"stock.report_reception\";\n        this.labelReportName = \"stock.report_reception_report_label\";\n        this.state = useState({\n            sourcesToLines: {},\n        });\n        useBus(this.env.bus, \"update-assign-state\", (ev) => this._changeAssignedState(ev.detail));\n\n        onWillStart(async () => {\n            // Check the URL if report was alreadu loaded.\n            let defaultDocIds;\n            const { rfield, rids } = this.props.action.context.params || {};\n            if (rfield && rids) {\n                const parsedIds = JSON.parse(rids);\n                defaultDocIds = [rfield, parsedIds instanceof Array ? parsedIds : [parsedIds]];\n            } else {\n                defaultDocIds = Object.entries(this.context).find(([k,v]) => k.startsWith(\"default_\"));\n                if (!defaultDocIds) {\n                    // If nothing could be found, just ask for empty data.\n                    defaultDocIds = [false, [0]];\n                }\n            }\n            this.contextDefaultDoc = { field: defaultDocIds[0], ids: defaultDocIds[1] };\n\n            if (this.contextDefaultDoc.field) {\n                // Add the fields/ids to the URL, so we can properly reload them after a page refresh.\n                this.props.updateActionState({ rfield: this.contextDefaultDoc.field, rids: JSON.stringify(this.contextDefaultDoc.ids) });\n            }\n            this.data = await this.getReportData();\n            this.state.sourcesToLines = this.data.sources_to_lines;\n\n            const matchingReports = await this.ormService.searchRead(\"ir.actions.report\", [\n                [\"report_name\", \"in\", [this.reportName, this.labelReportName]],\n            ]);\n            this.receptionReportAction = matchingReports.find(\n                (report) => report.report_name === this.reportName\n            );\n            this.receptionReportLabelAction = matchingReports.find(\n                (report) => report.report_name === this.labelReportName\n            );\n        });\n    }\n\n    async getReportData() {\n        const context = { ...this.context, [this.contextDefaultDoc.field]: this.contextDefaultDoc.ids };\n        const args = [\n            this.contextDefaultDoc.ids,\n            { context, report_type: \"html\" },\n        ];\n        return this.ormService.call(\n            \"report.stock.report_reception\",\n            \"get_report_data\",\n            args,\n            { context },\n        );\n    }\n\n    //---- Handlers ----\n\n    async onClickAssignAll() {\n        const moveIds = [];\n        const quantities = [];\n        const inIds = [];\n\n        for (const lines of Object.values(this.state.sourcesToLines)) {\n            for (const line of lines) {\n                if (line.is_assigned) continue;\n                moveIds.push(line.move_out_id);\n                quantities.push(line.quantity);\n                inIds.push(line.move_ins);\n            }\n        }\n\n        await this.ormService.call(\n            \"report.stock.report_reception\",\n            \"action_assign\",\n            [false, moveIds, quantities, inIds],\n        );\n        this._changeAssignedState({ isAssigned: true });\n    }\n\n    async onClickTitle(docId) {\n        return this.actionService.doAction({\n            type: \"ir.actions.act_window\",\n            res_model: this.data.doc_model,\n            res_id: docId,\n            views: [[false, \"form\"]],\n            target: \"current\",\n        });\n    }\n\n    onClickPrint() {\n        return this.actionService.doAction({\n            ...this.receptionReportAction,\n            context: { [this.contextDefaultDoc.field]: this.contextDefaultDoc.ids },\n        });\n    }\n\n    onClickPrintLabels() {\n        const modelIds = [];\n        const quantities = [];\n\n        for (const lines of Object.values(this.state.sourcesToLines)) {\n            for (const line of lines) {\n                if (!line.is_assigned) continue;\n                modelIds.push(line.move_out_id);\n                quantities.push(Math.ceil(line.quantity) || 1);\n            }\n        }\n        if (!modelIds.length) {\n            return;\n        }\n\n        return this.actionService.doAction({\n            ...this.receptionReportLabelAction,\n            context: { active_ids: modelIds },\n            data: { docids: modelIds, quantity: quantities.join(\",\") },\n        });\n    }\n\n    //---- Utils ----\n\n    _changeAssignedState(options) {\n        const { isAssigned, tableIndex, lineIndex } = options;\n\n        for (const [tabIndex, lines] of Object.entries(this.state.sourcesToLines)) {\n            if (tableIndex && tableIndex != tabIndex) continue;\n            lines.forEach(line => {\n                if (isNaN(lineIndex) || lineIndex == line.index) {\n                    line.is_assigned = isAssigned;\n                }\n            });\n        }\n    }\n\n    //---- Getters ----\n\n    get context() {\n        return this.props.action.context;\n    }\n\n    get hasContent() {\n        return this.data.sources_to_lines && Object.keys(this.data.sources_to_lines).length > 0;\n    }\n\n    get isAssignAllDisabled() {\n        return Object.values(this.state.sourcesToLines).every(lines => lines.every(line => line.is_assigned || !line.is_qty_assignable));\n    }\n\n    get isPrintLabelDisabled() {\n        return Object.values(this.state.sourcesToLines).every(lines => lines.every(line => !line.is_assigned));\n    }\n}\n\nregistry.category(\"actions\").add(\"reception_report\", ReceptionReportMain);\n", "import { useService } from \"@web/core/utils/hooks\";\nimport { ReceptionReportLine } from \"../reception_report_line/stock_reception_report_line\";\nimport { Component } from \"@odoo/owl\";\n\nexport class ReceptionReportTable extends Component {\n    static template = \"stock.ReceptionReportTable\";\n    static components = {\n        ReceptionReportLine,\n    };\n    static props = {\n        index: String,\n        scheduledDate: { type: String, optional: true },\n        lines: Array,\n        source: Array,\n        labelReport: Object,\n        showUom: Boolean,\n        precision: Number,\n    };\n\n    setup() {\n        this.actionService = useService(\"action\");\n        this.ormService = useService(\"orm\");\n    }\n\n    //---- Handlers ----\n\n    async onClickAssignAll() {\n        const moveIds = [];\n        const quantities = [];\n        const inIds = [];\n        for (const line of this.props.lines) {\n            if (line.is_assigned) continue;\n            moveIds.push(line.move_out_id);\n            quantities.push(line.quantity);\n            inIds.push(line.move_ins);\n        }\n\n        await this.ormService.call(\n            \"report.stock.report_reception\",\n            \"action_assign\",\n            [false, moveIds, quantities, inIds],\n        );\n        this.env.bus.trigger(\"update-assign-state\", { isAssigned: true, tableIndex: this.props.index });\n    }\n\n    async onClickLink(resModel, resId, viewType) {\n        return this.actionService.doAction({\n            type: \"ir.actions.act_window\",\n            res_model: resModel,\n            res_id: resId,\n            views: [[false, viewType]],\n            target: \"current\",\n        });\n    }\n\n    async onClickPrintLabels() {\n        const modelIds = [];\n        const quantities = [];\n        for (const line of this.props.lines) {\n            if (!line.is_assigned) continue;\n            modelIds.push(line.move_out_id);\n            quantities.push(Math.ceil(line.quantity) || 1);\n        }\n        if (!modelIds.length) {\n            return;\n        }\n\n        return this.actionService.doAction({\n            ...this.props.labelReport,\n            context: { active_ids: modelIds },\n            data: { docids: modelIds, quantity: quantities.join(\",\") },\n        });\n    }\n\n    //---- Getters ----\n\n    get hasMovesIn() {\n        return this.props.lines.some(line => line.move_ins && line.move_ins.length > 0);\n    }\n\n    get hasAssignAllButton() {\n        return this.props.lines.some(line => line.is_qty_assignable);\n    }\n\n    get isAssignAllDisabled() {\n        return this.props.lines.every(line => line.is_assigned);\n    }\n\n    get isPrintLabelDisabled() {\n        return this.props.lines.every(line => !line.is_assigned);\n    }\n}\n", "import { registry } from \"@web/core/registry\";\nimport { kanbanView } from \"@web/views/kanban/kanban_view\";\nimport { KanbanRenderer } from \"@web/views/kanban/kanban_renderer\";\n\nimport { DynamicRecordList } from \"@web/model/relational_model/dynamic_record_list\";\nimport { DynamicGroupList } from \"@web/model/relational_model/dynamic_group_list\";\n\nexport class StockKanbanRenderer extends KanbanRenderer {\n    setup() {\n        super.setup();\n    }\n\n    // If all Inventory Overview graphs are empty, we use random sample data\n    getGroupsOrRecords() {\n        const { list } = this.props;\n        let records = [];\n        if (list instanceof DynamicRecordList) {\n            records.push(...list.records);\n        } else if (list instanceof DynamicGroupList) {\n            list.groups.forEach(g => {\n                records.push(...g.list.records);\n            });\n        }\n        // Data type \"sample\" is assigned in Python to empty graph data\n        let allEmpty = records.every(r => {\n            return r.data.kanban_dashboard_graph.includes('\"type\": \"sample\"');\n        });\n        if (allEmpty) {\n            records.forEach(r => {\n                let parsedDashboardData = JSON.parse(r.data.kanban_dashboard_graph);\n                parsedDashboardData[0].values.forEach(d => {\n                    d.value = Math.floor(Math.random() * 9 + 1);\n                });\n                r.data.kanban_dashboard_graph = JSON.stringify(parsedDashboardData);\n            });\n        }\n        return super.getGroupsOrRecords();\n    }\n}\n\nexport const StockKanbanView = {\n    ...kanbanView,\n    Renderer: StockKanbanRenderer,\n};\n\nregistry.category(\"views\").add(\"stock_dashboard_kanban\", StockKanbanView);\n", "import { Component } from \"@odoo/owl\";\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { evaluateExpr } from \"@web/core/py_js/py\";\nimport { floatField, FloatField } from \"@web/views/fields/float/float_field\";\nimport { monetaryField, MonetaryField } from \"@web/views/fields/monetary/monetary_field\";\nimport { registry } from \"@web/core/registry\";\nimport { useService } from \"@web/core/utils/hooks\";\n\nconst fieldRegistry = registry.category(\"fields\");\n\nclass StockActionField extends Component {\n    static props = {\n        ...FloatField.props,\n        ...MonetaryField.props,\n        actionName: { type: String, optional: false },\n        actionContext: { type: String, optional: true },\n    };\n    static components = {\n        FloatField,\n        MonetaryField,\n    }\n    static template = \"stock.actionField\";\n\n    setup() {\n        super.setup();\n        this.actionService = useService(\"action\");\n        this.orm = useService(\"orm\");\n        this.fieldType = this.props.record.fields[this.props.name].type;\n    }\n    \n    extractProps () {\n        const keysToRemove = [\"actionName\", \"actionContext\"];\n        return Object.fromEntries(\n         Object.entries(this.props).filter(([prop]) => !keysToRemove.includes(prop))\n       );\n    }\n\n    _onClick(ev) {\n        ev.stopPropagation();\n        ev.preventDefault();\n\n        // Get the action name from props.options\n        const actionName = this.props.actionName;\n        const actionContext = evaluateExpr(this.props.actionContext, this.props.record.evalContext);\n\n        // const action = this.orm.call(this.props.record.resModel, actionName, this.props.record.resId);\n        // Use the action service to perform the action\n        this.actionService.doAction(actionName, {\n            additionalContext: { ...actionContext, ...this.props.record.context },\n        });\n    }\n}\n\nconst stockActionField = {\n    ...floatField,\n    ...monetaryField,\n    component: StockActionField,\n    supportedOptions: [\n        Object.values(\n            Object.fromEntries(\n                [...floatField.supportedOptions, ...monetaryField.supportedOptions].map(\n                    (option) => [option.name, option]\n                )\n            )\n        ),\n        {\n            label: _t(\"Action Name\"),\n            name: \"action_name\",\n            type: \"string\",\n        },\n    ],\n    extractProps: (...args) => {\n        const [{ context, fieldType, options }] = args;\n        const action_props = {\n            actionName: options.action_name,\n            actionContext: context,\n        }\n        let props = {...action_props}\n        if (fieldType === \"monetary\") {\n            props = { ...action_props, ...monetaryField.extractProps(...args) };\n        } else if (fieldType === \"float\") {\n            props = { ...action_props, ...floatField.extractProps(...args) };\n        };\n        return props;\n    },\n};\n\nfieldRegistry.add(\"stock_action_field\", stockActionField);\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { X2ManyField, x2ManyField } from \"@web/views/fields/x2many/x2many_field\";\nimport { useSelectCreate, useOpenMany2XRecord} from \"@web/views/fields/relational_utils\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { Domain } from \"@web/core/domain\";\n\nexport class SMLX2ManyField extends X2ManyField {\n    setup() {\n        super.setup();\n        this.orm = useService(\"orm\");\n        this.dirtyQuantsData = new Map();\n        const selectCreate = useSelectCreate({\n            resModel: \"stock.quant\",\n            activeActions: this.activeActions,\n            onSelected: (resIds) => this.selectRecord(resIds),\n            onCreateEdit: () => this.createOpenRecord(),\n        });\n\n        this.selectCreate = (params) => {\n            return selectCreate(params);\n        };\n        this.openQuantRecord = useOpenMany2XRecord({\n            resModel: \"stock.quant\",\n            activeActions: this.activeActions,\n            onRecordSaved: (record) => this.selectRecord([record.resId]),\n            fieldString: this.props.string,\n            is2Many: true,\n        });\n    }\n\n    get quantListViewShowOnHandOnly(){\n        return true; // To override in mrp_subcontracting\n    }\n\n    async onAdd({ context, editable } = {}) {\n        if (!this.props.record.data.show_quant) {\n            return super.onAdd(...arguments);\n        }\n        // Compute the quant offset from move lines quantity changes that were not saved yet.\n        // Hence, did not yet affect quant's quantity in DB.\n        await this.updateDirtyQuantsData();\n        context = {\n            ...context,\n            single_product: true,\n            list_view_ref: \"stock.view_stock_quant_tree_simple\",\n        };\n        const productName = this.props.record.data.product_id.display_name;\n        const title = _t(\"Add line: %s\", productName);\n        let domain = [\n            [\"product_id\", \"=\", this.props.record.data.product_id.id],\n            [\"location_id\", \"child_of\", this.props.context.default_location_id],\n            [\"quantity\", \">\", 0.0],\n        ];\n        if (this.quantListViewShowOnHandOnly) {\n            domain.push([\"on_hand\", \"=\", true]);\n        }\n        if (this.dirtyQuantsData.size) {\n            const notFullyUsed = [];\n            const fullyUsed = [];\n            for (const [quantId, quantData] of this.dirtyQuantsData.entries()) {\n                if (quantData.available_quantity > 0) {\n                    notFullyUsed.push(quantId);\n                } else {\n                    fullyUsed.push(quantId);\n                }\n            }\n            if (fullyUsed.length) {\n                domain = Domain.and([domain, [[\"id\", \"not in\", fullyUsed]]]).toList();\n            }\n            if (notFullyUsed.length) {\n                domain = Domain.or([domain, [[\"id\", \"in\", notFullyUsed]]]).toList();\n            }\n        }\n        return this.selectCreate({ domain, context, title });\n    }\n\n    async updateDirtyQuantsData() {\n        // Since changes of move line quantities will not affect the available quantity of the quant before\n        // the record has been saved, it is necessary to determine the offset of the DB quant data.\n        this.dirtyQuantsData.clear();\n        const dirtyQuantityMoveLines = this._move_line_ids.filter(\n            (ml) => !ml.data.quant_id && ml._values.quantity - ml._changes.quantity\n        );\n        const dirtyQuantMoveLines = this._move_line_ids.filter(\n            (ml) => ml.data.quant_id.id\n        );\n        const dirtyMoveLines = [...dirtyQuantityMoveLines, ...dirtyQuantMoveLines];\n        if (!dirtyMoveLines.length) {\n            return;\n        }\n        const match = await this.orm.call(\n            \"stock.move.line\",\n            \"get_move_line_quant_match\",\n            [\n                this._move_line_ids\n                    .filter((rec) => rec.resId)\n                    .map((rec) => rec.resId),\n                this.props.record.resId,\n                dirtyMoveLines.filter((rec) => rec.resId).map((rec) => rec.resId),\n                dirtyQuantMoveLines.map((ml) => ml.data.quant_id.id),\n            ],\n            {}\n        );\n        const quants = match[0];\n        if (!quants.length) {\n            return;\n        }\n        const dbMoveLinesData = new Map();\n        for (const data of match[1]) {\n            dbMoveLinesData.set(data[0], { quantity: data[1].quantity, quantId: data[1].quant_id });\n        }\n        const offsetByQuant = new Map();\n        for (const ml of dirtyQuantMoveLines) {\n            const quantId = ml.data.quant_id.id;\n            offsetByQuant.set(quantId, (offsetByQuant.get(quantId) || 0) - ml.data.quantity);\n            const dbQuantId = dbMoveLinesData.get(ml.resId)?.quantId;\n            if (dbQuantId && quantId != dbQuantId) {\n                offsetByQuant.set(\n                    dbQuantId,\n                    (offsetByQuant.get(dbQuantId) || 0) + dbMoveLinesData.get(ml.resId).quantity\n                );\n            }\n        }\n        const offsetByQuantity = new Map();\n        for (const ml of dirtyQuantityMoveLines) {\n            offsetByQuantity.set(ml.resId, ml._values.quantity - ml._changes.quantity);\n        }\n        for (const quant of quants) {\n            const quantityOffest = quant[1].move_line_ids\n                .map((ml) => offsetByQuantity.get(ml) || 0)\n                .reduce((val, sum) => val + sum, 0);\n            const quantOffest = offsetByQuant.get(quant[0]) || 0;\n            this.dirtyQuantsData.set(quant[0], {\n                available_quantity: quant[1].available_quantity + quantityOffest + quantOffest,\n            });\n        }\n    }\n\n    async selectRecord(res_ids) {\n        const demand =\n            this.props.record.data.product_uom_qty -\n            this._move_line_ids\n                .map((ml) => ml.data.quantity)\n                .reduce((val, sum) => val + sum, 0);\n        const params = {\n            context: { default_quant_id: res_ids[0] },\n        };\n        if (demand <= 0) {\n            params.context.default_quantity = 0;\n        } else if (this.dirtyQuantsData.has(res_ids[0])) {\n            params.context.default_quantity = Math.min(\n                this.dirtyQuantsData.get(res_ids[0]).available_quantity,\n                demand\n            );\n        }\n        this.list.addNewRecord(params).then((record) => {\n            // Make it dirty to force the save of the record. addNewRecord make\n            // the new record dirty === False by default to remove them at unfocus event\n            record.dirty = true;\n        });\n    }\n\n    createOpenRecord() {\n        const activeElement = document.activeElement;\n        this.openQuantRecord({\n            context: {\n                ...this.props.context,\n                form_view_ref: \"stock.view_stock_quant_form\",\n            },\n            immediate: true,\n            onClose: () => {\n                if (activeElement) {\n                    activeElement.focus();\n                }\n            },\n        });\n    }\n\n    get _move_line_ids() {\n        return this.props.record.data.move_line_ids.records;\n    }\n}\n\nexport const smlX2ManyField = {\n    ...x2ManyField,\n    component: SMLX2ManyField,\n};\n\nregistry.category(\"fields\").add(\"sml_x2_many\", smlX2ManyField);\n", "import { cookie } from \"@web/core/browser/cookie\";\nimport { getColor, getCustomColor } from \"@web/core/colors/colors\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { registry } from \"@web/core/registry\";\nimport { JournalDashboardGraphField } from \"@web/views/fields/journal_dashboard_graph/journal_dashboard_graph_field\";\n\nexport class PickingTypeDashboardGraphField extends JournalDashboardGraphField {\n    setup() {\n        super.setup();\n        this.actionService = useService(\"action\");\n    }\n    getBarChartConfig() {\n        // Only bar chart is available for picking types\n        const data = [];\n        const labels = [];\n        const backgroundColor = [];\n\n        const colorPast = getColor(8, cookie.get(\"color_scheme\"));\n        const colorPresent = getColor(16, cookie.get(\"color_scheme\"));\n        const colorFuture = getColor(12, cookie.get(\"color_scheme\"));\n        this.data[0].values.forEach((pt) => {\n            data.push(pt.value);\n            labels.push(pt.label);\n            if (pt.type === \"past\") {\n                backgroundColor.push(colorPast);\n            } else if (pt.type === \"present\") {\n                backgroundColor.push(colorPresent);\n            } else if (pt.type === \"future\") {\n                backgroundColor.push(colorFuture);\n            } else {\n                backgroundColor.push(getCustomColor(cookie.get(\"color_scheme\"), \"#ebebeb\", \"#3C3E4B\"));\n            }\n        });\n        return {\n            type: \"bar\",\n            data: {\n                labels,\n                datasets: [\n                    {\n                        backgroundColor,\n                        data,\n                        fill: \"start\",\n                        label: this.data[0].key,\n                    },\n                ],\n            },\n            options: {\n                onClick: (e) => {\n                    const pickingTypeId = e.chart.config._config.options.pickingTypeId;\n                    // If no picking type ID was provided, than this is sample data\n                    if (!pickingTypeId) {\n                        return;\n                    }\n                    const columnIndex = e.chart.tooltip.dataPoints[0].parsed.x;\n                    const dateCategories = {\n                        0: \"before\",\n                        1: \"yesterday\",\n                        2: \"today\",\n                        3: \"day_1\",\n                        4: \"day_2\",\n                        5: \"after\",\n                    };\n                    const dateCategory = dateCategories[columnIndex];\n                    const additionalContext = {\n                        picking_type_id: pickingTypeId,\n                        search_default_picking_type_id: [pickingTypeId],\n                    };\n                    // Add a filter for the given date category\n                    additionalContext[\"search_default_\".concat(dateCategory)] = true;\n                    this.actionService.doAction(\"stock.click_dashboard_graph\", {\n                        additionalContext: additionalContext\n                    });\n                },\n                plugins: {\n                    legend: { display: false },\n                    tooltip: {\n                        intersect: false,\n                        position: \"nearest\",\n                        caretSize: 0,\n                    },\n                },\n                scales: {\n                    y: {\n                        display: false,\n                    },\n                    x: {\n                        display: false,\n                    },\n                },\n                pickingTypeId: this.data[0].picking_type_id,\n                maintainAspectRatio: false,\n                elements: {\n                    line: {\n                        tension: 0.000001,\n                    },\n                },\n            },\n        };\n    }\n}\n\nexport const pickingTypeDashboardGraphField = {\n    component: PickingTypeDashboardGraphField,\n    supportedTypes: [\"text\"],\n    extractProps: ({ attrs }) => ({\n        graphType: attrs.graph_type,\n    }),\n};\n\nregistry.category(\"fields\").add(\"picking_type_dashboard_graph\", pickingTypeDashboardGraphField);\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { Component, markup } from \"@odoo/owl\";\n\nexport class ForecastedButtons extends Component {\n    static template = \"stock.ForecastedButtons\";\n    static props = {\n        action: Object,\n        resModel: { type: String, optional: true },\n        reloadReport: Function,\n    };\n\n    setup() {\n        this.actionService = useService(\"action\");\n        this.orm = useService(\"orm\");\n        this.context = this.props.action.context;\n        this.productId = this.context.active_id;\n        this.resModel = this.props.resModel || this.context.active_model || this.context.params?.active_model || 'product.template';\n    }\n\n    /**\n     * Called when an action open a wizard. If the wizard is discarded, this\n     * method does nothing, otherwise it reloads the report.\n     * @param {Object | undefined} res\n     */\n    _onClose(res) {\n        return res?.special || !res?.noReload || this.props.reloadReport();\n    }\n\n    async _onClickReplenish() {\n        const context = { ...this.context };\n        if (this.resModel === 'product.product') {\n            context.default_product_id = this.productId;\n        } else if (this.resModel === 'product.template') {\n            context.default_product_tmpl_id = this.productId;\n        }\n        context.default_warehouse_id = this.context.warehouse_id;\n\n        const action = {\n            res_model: 'product.replenish',\n            name: _t('Product Replenish'),\n            type: 'ir.actions.act_window',\n            views: [[false, 'form']],\n            target: 'new',\n            context: context,\n        };\n        return this.actionService.doAction(action, { onClose: this._onClose.bind(this) });\n    }\n\n    async _onClickUpdateQuantity() {\n        const action = await this.orm.call(this.resModel, \"action_open_quants\", [[this.productId]]);\n        if (action.res_model === \"stock.quant\") { // Quant view in inventory mode.\n            action.views = [[false, \"list\"]];\n        }\n        if (action.help) {\n            action.help = markup(action.help);\n        }\n        return this.actionService.doAction(action, { onClose: this._onClose.bind(this) });\n    }\n}\n", "import { formatFloat } from \"@web/views/fields/formatters\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { Component } from \"@odoo/owl\";\nimport { _t } from \"@web/core/l10n/translation\";\n\nexport class ForecastedDetails extends Component {\n    static template = \"stock.ForecastedDetails\";\n    static props = { docs: Object, openView: Function, reloadReport: Function };\n\n    setup() {\n        this.orm = useService(\"orm\");\n        this._groupLines();\n        this._prepareLines();\n        this._prepareData();\n        this._mergeLines();\n\n        this._formatFloat = (num) => {\n            return formatFloat(num, { digits: this.props.docs.precision });\n        };\n    }\n\n    async _reserve(move_id){\n        await this.orm.call(\n            'stock.forecasted_product_product',\n            'action_reserve_linked_picks',\n            [move_id],\n        );\n        this.props.reloadReport();\n    }\n\n    async _unreserve(move_id){\n        await this.orm.call(\n            'stock.forecasted_product_product',\n            'action_unreserve_linked_picks',\n            [move_id],\n        );\n        this.props.reloadReport();\n    }\n\n    async _onClickChangePriority(modelName, record) {\n        const value = record.priority == \"0\" ? \"1\" : \"0\";\n\n        await this.orm.call(modelName, \"write\", [[record.id], { priority: value }]);\n        this.props.reloadReport();\n    }\n\n    _onHandCondition(line){\n        return !line.document_in && !line.in_transit && line.replenishment_filled && line.document_out;\n    }\n\n    _reconciledCondition(line){\n        return line.document_in && !line.in_transit && line.replenishment_filled && line.document_out;\n    }\n\n    _freeStockCondition(line){\n        return !line.document_in && !line.in_transit && line.replenishment_filled && !line.document_out;\n    }\n\n    _notAvailableCondition(line){\n        return !line.document_in && !line.in_transit && !line.replenishment_filled && line.document_out;\n    }\n\n    //Extend this to add new lines grouping\n    _groupLines(){\n        this._groupLinesByProduct();\n        this._groupOnHandLinesByProduct();\n        this._groupReconciledLinesByProduct();\n        this._groupFreeStockLinesByProduct();\n        this._groupNotAvailableLinesByProduct();\n    }\n\n    _groupLinesByProduct() {\n        this.LinesPerProduct = {};\n        for (const line of this.props.docs.lines) {\n            const key = line.product.id;\n            (this.LinesPerProduct[key] ??= []).push(line);\n        }\n    }\n\n    _groupOnHandLinesByProduct() {\n        this.OnHandLinesPerProduct = {};\n        for (const line of this.props.docs.lines) {\n            if (this._onHandCondition(line)) {\n                const key = line.product.id;\n                (this.OnHandLinesPerProduct[key] ??= []).push(line);\n            }\n        }\n    }\n\n    _groupReconciledLinesByProduct() {\n        this.ReconciledLinesPerProduct = {};\n        for (const line of this.props.docs.lines) {\n            if (this._onHandCondition(line)) {\n                const key = line.product.id;\n                (this.ReconciledLinesPerProduct[key] ??= []).push(line);\n            }\n        }\n    }\n\n    _groupNotAvailableLinesByProduct() {\n        this.NotAvailableLinesPerProduct = {};\n        for (const line of this.props.docs.lines) {\n            if (this._notAvailableCondition(line)) {\n                const key = line.product.id;\n                (this.NotAvailableLinesPerProduct[key] ??= []).push(line);\n            }\n        }\n    }\n\n    _groupFreeStockLinesByProduct() {\n        this.FreeStockLinesPerProduct = {};\n        for (const line of this.props.docs.lines) {\n            if (this._freeStockCondition(line) && line?.removal_date !== -1) {\n                const key = line.product.id;\n                (this.FreeStockLinesPerProduct[key] ??= []).push(line);\n            }\n        }\n    }\n\n    _prepareLines(){\n        if (this.multipleProducts) {\n            this.props.docs.lines.sort((a, b) => (a.product.id || 0) - (b.product.id || 0));\n        }\n    }\n\n    _prepareData(){\n        this.OnHandTotalQty = Object.fromEntries(\n            Object.entries(this.OnHandLinesPerProduct).map(([id, lines]) => [\n                id,\n                lines.reduce((sum, line) => sum + line.quantity, 0),\n            ])\n        );\n        this.AvailableOnHandTotalQty = Object.fromEntries(\n            Object.entries(this.OnHandLinesPerProduct).map(([id, lines]) => [\n                id,\n                lines.reduce((sum, line) => sum + (line.reservation ? 0 : line.quantity), 0),\n            ])\n        );\n        for (const productId of this.productIds){\n            if (!(productId in this.FreeStockLinesPerProduct) || !(productId in this.LinesPerProduct)){\n                continue;\n            }\n            const lines = this.FreeStockLinesPerProduct[productId]\n            if (this.LinesPerProduct[productId].length > 1 && lines.length == 1 && lines[0]?.quantity === 0 ){\n                const removeIndex = this.lines.indexOf(lines[0]);\n                this.lines.splice(removeIndex,1);\n            }\n        }\n    }\n\n    _mergeLines(){\n        let lines = this.lines;\n        this.mergesLinesData = {};\n        let lastIndex = 0;\n        for(let i = 0; i < lines.length-1; i++){\n            const line = lines[i];\n            const nextLine = lines[i + 1];\n            if (line.product.id != nextLine.product.id || !this._sameLineRule(line, nextLine)) {\n                lastIndex = i+1;\n                continue;\n            }\n            if (!this.mergesLinesData[lastIndex]){\n                this.mergesLinesData[lastIndex] = {\n                    rowcount: 1,\n                    tot_qty: line.quantity,\n                };\n            }\n            this.mergesLinesData[lastIndex].rowcount += 1;\n            this.mergesLinesData[lastIndex].tot_qty += nextLine.quantity;\n        }\n    }\n\n    _sameLineRule(line, nextLine){\n        const OnHand = this.OnHandLinesPerProduct[line.product.id] || [];\n        const NotAvailable = this.NotAvailableLinesPerProduct[line.product.id] || [];\n        return  this.sameDocumentIn(line, nextLine) || (OnHand.includes(line) && OnHand.includes(nextLine)) || (NotAvailable.includes(line) && NotAvailable.includes(nextLine));\n    }\n\n    displayReserve(line){\n        let splittedLine = true;\n        if(this.line_index - 1 >= 0){\n            const previousLine = this.lines[this.line_index - 1];\n            const sameProduct = this.line.product.id == previousLine.product.id;\n            const isOnHandSplittedLine = this.OnHandLinesPerProduct[line.product.id] && this.OnHandLinesPerProduct[line.product.id].some(l => this.sameDocumentOut(l, line))\n            const isReconciledSplittedLine = this.ReconciledLinesPerProduct[line.product.id] && !this.isReconciled(line) && this.ReconciledLinesPerProduct[line.product.id].some(l => this.sameDocumentOut(l, line))\n            splittedLine = sameProduct && (this.sameDocumentOut(line, previousLine) || isOnHandSplittedLine || isReconciledSplittedLine);\n        }\n        const hasFreeStock = this.props.docs.product[line.product.id].free_qty > 0;\n        return this.props.docs.user_can_edit_pickings && !line.in_transit && this.canReserveOperation(line) &&\n            (this.isOnHand(line) || (hasFreeStock && !splittedLine));\n    }\n\n    canReserveOperation(line){\n        return line.move_out?.picking_id;\n    }\n\n    futureVirtualAvailable(line) {\n        const product = this.props.docs.product[line.product.id]\n        return product.virtual_available + product.qty.in - product.qty.out;\n    }\n\n    sameDocumentIn(line1, line2){\n        return this._sameDocument(line1, line2, 'document_in');\n    }\n\n    sameDocumentOut(line1, line2){\n        return this._sameDocument(line1, line2, 'document_out');\n    }\n\n    _sameDocument(line1, line2, docField) {\n        return (\n            line1[docField] && line2[docField] &&\n            line1[docField].id === line2[docField].id &&\n            line1[docField]._name === line2[docField]._name &&\n            line1[docField].name === line2[docField].name\n        );\n    }\n\n    isOnHand(line){\n        return this.OnHandLinesPerProduct[line.product.id] && this.OnHandLinesPerProduct[line.product.id].includes(this.lines[this.line_index]);\n    }\n\n    isReconciled(line){\n        return this.ReconciledLinesPerProduct[line.product.id] && this.ReconciledLinesPerProduct[line.product.id].includes(this.lines[this.line_index]);\n    }\n\n    get freeStockLabel() {\n        return _t('Free Stock');\n    }\n\n    get lines() {\n        return this.props.docs.lines;\n    }\n\n    get multipleProducts() {\n        return this.props.docs.multiple_product;\n    }\n\n    get productIds(){\n        return Object.keys(this.props.docs.product).map(Number);\n    }\n}\n", "import { useService } from \"@web/core/utils/hooks\";\nimport { formatFloat } from \"@web/views/fields/formatters\";\nimport { Component, markup } from \"@odoo/owl\";\n\nexport class ForecastedHeader extends Component {\n    static template = \"stock.ForecastedHeader\";\n    static props = { docs: Object, openView: Function };\n\n    setup(){\n        this.orm = useService(\"orm\");\n        this.action = useService(\"action\");\n        this.tooltip = useService(\"tooltip\");\n\n        this._formatFloat = (num) => formatFloat(num, { digits: this.props.docs.precision });\n    }\n\n    async _onClickInventory(){\n        const productIds = this.props.docs.product_variants_ids;\n        const action = await this.orm.call('product.product', 'action_open_quants', [productIds]);\n        if (action.help) {\n            action.help = markup(action.help);\n        }\n        return this.action.doAction(action);\n    }\n\n    get products() {\n        return this.props.docs.product;\n    }\n\n    get leadTime() {\n        if (!this.products || this.products.length === 0) {\n            return null;\n        }\n        const productsArray = Object.values(this.products || {});\n        const product = productsArray.reduce((minProduct, p) => {\n            if (\n            !minProduct ||\n            (p.leadtime && p.leadtime.total_delay < (minProduct.leadtime?.total_delay ?? Infinity))\n            ) {\n            return p;\n            }\n            return minProduct;\n        }, null);\n        const today = new Date(Date.now());\n        product.leadtime[\"today\"] = today.toLocaleDateString();\n        product.leadtime[\"earliestPossibleArrival\"] = this.addDays(today, product.leadtime.total_delay);\n        return product.leadtime;\n    }\n\n    get leadTimeShort() {\n        let short = \" \" + (this.leadTime.total_delay) + \" day(s)\";\n        if (this.leadTime.total_delay != 0) {\n            short += \" (\" + this.leadTime.earliestPossibleArrival + \")\";\n        }\n        return short;\n    }\n\n    get quantityOnHand() {\n        return Object.values(this.products).reduce((sum, product) => sum + product.quantity_on_hand, 0);\n    }\n\n    get incomingQty() {\n        return Object.values(this.products).reduce((sum, product) => sum + product.incoming_qty, 0);\n    }\n\n    get outgoingQty() {\n        return Object.values(this.products).reduce((sum, product) => sum + product.outgoing_qty, 0);\n    }\n\n    get virtualAvailable() {\n        return Object.values(this.products).reduce((sum, product) => sum + product.virtual_available, 0);\n    }\n\n    get uom() {\n        return Object.values(this.products)[0].uom;\n    }\n\n    addDays(date, days) {\n        const result = new Date(date);\n        result.setDate(result.getDate() + days);\n        return result.toLocaleDateString();\n    }\n\n    toJsonString(obj) {\n        return JSON.stringify(obj);\n    }\n}\n", "import { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { Component, onWillStart } from \"@odoo/owl\";\n\nexport class ForecastedWarehouseFilter extends Component {\n    static template = \"stock.ForecastedWarehouseFilter\";\n    static components = { Dropdown, DropdownItem };\n    static props = { action: Object, setWarehouseInContext: Function, warehouses: Array };\n\n    setup() {\n        this.orm = useService(\"orm\");\n        this.context = this.props.action.context;\n        this.warehouses = this.props.warehouses;\n        onWillStart(this.onWillStart)\n    }\n\n    async onWillStart() {\n        this.displayWarehouseFilter = (this.warehouses.length > 1);\n    }\n\n    _onSelected(id){\n        this.props.setWarehouseInContext(Number(id));\n    }\n\n    get activeWarehouse(){\n        return this.context.warehouse_id ? this.warehouses.find((w) => w.id == this.context.warehouse_id) : this.warehouses[0];\n    }\n\n    get warehousesItems() {\n        return this.warehouses.map(warehouse => ({\n            id: warehouse.id,\n            label: warehouse.name,\n            onSelected: () => this._onSelected(warehouse.id),\n        }));\n    }\n}\n", "import { useService } from \"@web/core/utils/hooks\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { View } from \"@web/views/view\";\nimport { ControlPanel } from \"@web/search/control_panel/control_panel\";\n\nimport { ForecastedButtons } from \"./forecasted_buttons\";\nimport { ForecastedDetails } from \"./forecasted_details\";\nimport { ForecastedHeader } from \"./forecasted_header\";\nimport { ForecastedWarehouseFilter } from \"./forecasted_warehouse_filter\";\nimport { Component, onWillStart, useState } from \"@odoo/owl\";\nimport { standardActionServiceProps } from \"@web/webclient/actions/action_service\";\n\nexport class StockForecasted extends Component {\n    static template = \"stock.Forecasted\";\n    static components = {\n        ControlPanel,\n        ForecastedButtons,\n        ForecastedWarehouseFilter,\n        ForecastedHeader,\n        View,\n        ForecastedDetails,\n    };\n    static props = { ...standardActionServiceProps };\n    setup() {\n        this.orm = useService(\"orm\");\n        this.action = useService(\"action\");\n\n        this.context = useState(this.props.action.context);\n        this.productId = this.context.active_id;\n        this.resModel = this.context.active_model;\n        this.title = this.props.action.name || _t(\"Forecasted Report\");\n        if(!this.context.active_id){\n            this.context.active_id = this.props.action.params.active_id;\n            this.reloadReport();\n        }\n        this.warehouses = useState([]);\n\n        onWillStart(this._getReportValues);\n    }\n\n    async _getReportValues() {\n        await this._getResModel();\n        const isTemplate = !this.resModel || this.resModel === 'product.template';\n        this.reportModelName = `stock.forecasted_product_${isTemplate ? \"template\" : \"product\"}`;\n        this.warehouses.splice(0, this.warehouses.length);\n        this.warehouses.push(...await this.orm.searchRead('stock.warehouse', [],['id', 'name', 'code']));\n        if (!this.context.warehouse_id) {\n            this.updateWarehouse(this.warehouses[0].id);\n        }\n        const reportValues = await this.orm.call(this.reportModelName, \"get_report_values\", [], {\n            context: this.context,\n            docids: [this.productId],\n        });\n        this.docs = {\n            ...reportValues.docs,\n            ...reportValues.precision,\n            lead_horizon_date: this.context.lead_horizon_date,\n            qty_to_order: this.context.qty_to_order,\n        };\n    }\n\n    async _getResModel(){\n        this.resModel = this.context.active_model || this.context.params?.active_model;\n        //Following is used as a fallback when the forecast is not called by an action but through browser's history\n        if (!this.resModel) {\n            let resModel = this.props.action.res_model;\n            if (resModel) {\n                if (/^\\d+$/.test(resModel)) {\n                    // legacy action definition where res_model is the model id instead of name\n                    const actionModel = await this.orm.read('ir.model', [Number(resModel)], ['model']);\n                    resModel = actionModel[0]?.model;\n                }\n                this.resModel = resModel;\n            } else if (this.props.action._originalAction) {\n                const originalContextAction = JSON.parse(this.props.action._originalAction).context;\n                if (typeof originalContextAction === \"string\") {\n                    this.resModel = JSON.parse(originalContextAction.replace(/'/g, '\"')).active_model;\n                } else if (originalContextAction) {\n                    this.resModel = originalContextAction.active_model;\n                }\n            }\n            this.context.active_model = this.resModel;\n        }\n    }\n\n    async updateWarehouse(id) {\n        const hasPreviousValue = this.context.warehouse_id !== undefined;\n        this.context.warehouse_id = id;\n        if (hasPreviousValue) {\n            await this.reloadReport();\n        }\n    }\n\n    async reloadReport() {\n        const actionRequest = {\n            id: this.props.action.id,\n            type: \"ir.actions.client\",\n            tag: \"stock_forecasted\",\n            context: this.context,\n            name: this.title,\n        };\n        const options = { stackPosition: \"replaceCurrentAction\" };\n        return this.action.doAction(actionRequest, options);\n    }\n\n    get graphDomain() {\n        const domain = [\n            [\"state\", \"=\", \"forecast\"],\n            [\"warehouse_id\", \"=\", this.context.warehouse_id],\n        ];\n        if (this.resModel === \"product.template\") {\n            domain.push([\"product_tmpl_id\", \"=\", this.productId]);\n        } else if (this.resModel === \"product.product\") {\n            domain.push([\"product_id\", \"=\", this.productId]);\n        }\n        return domain;\n    }\n\n    get graphInfo() {\n        return { noContentHelp: _t(\"Try to add some incoming or outgoing transfers.\") };\n    }\n\n    async openView(resModel, view, resId=false, domain = false) {\n        const action = {\n            type: \"ir.actions.act_window\",\n            res_model: resModel,\n            views: [[false, view]],\n            view_mode: view,\n            res_id:  resId,\n            domain: domain,\n        };\n        return this.action.doAction(action);\n    }\n}\n\nregistry.category(\"actions\").add(\"stock_forecasted\", StockForecasted);\n", "import { registry } from \"@web/core/registry\";\nimport { browser } from \"@web/core/browser/browser\";\nimport { UPDATE_METHODS } from \"@web/core/orm_service\";\nimport { rpcBus } from \"@web/core/network/rpc\";\n\nregistry.category(\"services\").add(\"stock_warehouse\", {\n    dependencies: [\"action\"],\n    start(env, { action }) {\n        rpcBus.addEventListener(\"RPC:RESPONSE\", (ev) => {\n            const { data, error } = ev.detail;\n            const { model, method } = data.params;\n            if (!error && model === \"stock.warehouse\") {\n                if (UPDATE_METHODS.includes(method) && !browser.localStorage.getItem(\"running_tour\")) {\n                    action.doAction(\"reload_context\");\n                }\n            }\n        });\n    },\n});\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { DynamicRecordList } from \"@web/model/relational_model/dynamic_record_list\";\nimport { RelationalModel } from \"@web/model/relational_model/relational_model\";\n\nexport class InventoryReportListModel extends RelationalModel {\n    /**\n     * Override\n     */\n    setup(params, { action, dialog, notification, rpc, user, view, company }) {\n        // model has not created any record yet\n        this._lastCreatedRecordId;\n        return super.setup(...arguments);\n    }\n\n    /**\n     * Function called when a record has been _load (after saved).\n     * We need to detect when the user added to the list a quant which already exists\n     * (see stock.quant.create), either already loaded or not, to warn the user\n     * the quant was updated.\n     * This is done by checking :\n     * - the record id against the '_lastCreatedRecordId' on model\n     * - the create_date against the write_date (both are equal for newly created records).\n     *\n     */\n    async _updateSimilarRecords(reloadedRecord, serverValues) {\n        if (this.config.isMonoRecord) {\n            return;\n        }\n\n        const justCreated = reloadedRecord.id == this._lastCreatedRecordId;\n        if (justCreated && serverValues.create_date !== serverValues.write_date) {\n            this.notification.add(\n                _t(\n                    \"You tried to create a record that already exists. The existing record was modified instead.\"\n                ),\n                { title: _t(\"This record already exists\") }\n            );\n            const duplicateRecords = this.root.records.filter(\n                (record) => record.resId === reloadedRecord.resId && record.id !== reloadedRecord.id\n            );\n            if (duplicateRecords.length > 0) {\n                /* more than 1 'resId' record loaded in view (user added an already loaded record) :\n                 * - both have been updated\n                 * - remove the current record (the added one)\n                 */\n                await this.root._removeRecords([reloadedRecord.id]);\n                for (const record of duplicateRecords) {\n                    record._applyValues(serverValues);\n                }\n            }\n        } else {\n            super._updateSimilarRecords(...arguments)\n        }\n    }\n}\n\nexport class InventoryReportListDynamicRecordList extends DynamicRecordList {\n    /**\n     * Override\n     */\n    async addNewRecord() {\n        const record = await super.addNewRecord(...arguments);\n        // keep created record id on model\n        record.model._lastCreatedRecordId = record.id;\n        return record;\n    }\n}\n\nInventoryReportListModel.DynamicRecordList = InventoryReportListDynamicRecordList;\n", "import { listView } from \"@web/views/list/list_view\";\nimport { InventoryReportListModel } from \"./inventory_report_list_model\";\nimport { registry } from \"@web/core/registry\";\n\nexport const InventoryReportListView = {\n    ...listView,\n    Model: InventoryReportListModel,\n};\n\nregistry.category(\"views\").add('inventory_report_list', InventoryReportListView);\n", "import { registry } from \"@web/core/registry\";\nimport { listView } from \"@web/views/list/list_view\";\nimport { ListRenderer } from \"@web/views/list/list_renderer\";\nimport { useOwnedDialogs, useService } from \"@web/core/utils/hooks\";\nimport { SelectCreateDialog } from \"@web/views/view_dialogs/select_create_dialog\";\nimport { _t } from \"@web/core/l10n/translation\";\n\nexport class AddPackageListRenderer extends ListRenderer {\n    setup() {\n        super.setup();\n        this.orm = useService(\"orm\");\n        this.actionService = useService(\"action\");\n        this.addDialog = useOwnedDialogs();\n        this.pickingId = this.props.list.context.picking_ids?.length\n            ? this.props.list.context.picking_ids[0]\n            : 0;\n        this.locationId = this.props.list.context.location_id || 0;\n        this.canAddEntirePacks = this.props.list.context?.can_add_entire_packs;\n    }\n\n    get displayRowCreates() {\n        return this.canAddEntirePacks;\n    }\n\n    async add(params) {\n        await this.onClickAdd();\n    }\n\n    async onClickAdd() {\n        const domain = [];\n        if (this.locationId) {\n            domain.push([\"location_id\", \"child_of\", this.locationId]);\n        }\n        this.addDialog(SelectCreateDialog, {\n            title: _t(\"Select Packages to Move\"),\n            noCreate: true,\n            multiSelect: true,\n            resModel: \"stock.package\",\n            domain,\n            context: {\n                list_view_ref: \"stock.stock_package_view_add_list\",\n            },\n            onSelected: async (resIds) => {\n                if (resIds.length) {\n                    const done = await this.orm.call(\"stock.picking\", \"action_add_entire_packs\", [\n                        [this.pickingId],\n                        resIds,\n                    ]);\n                    if (done) {\n                        await this.actionService.doAction({\n                            type: \"ir.actions.client\",\n                            tag: \"soft_reload\",\n                        });\n                    }\n                }\n            },\n        });\n    }\n}\n\nregistry.category(\"views\").add(\"stock_add_package_list_view\", {\n    ...listView,\n    Renderer: AddPackageListRenderer,\n});\n", "import { listView } from '@web/views/list/list_view';\nimport { registry } from \"@web/core/registry\";\nimport { StockReportSearchModel } from \"../search/stock_report_search_model\";\nimport { StockReportSearchPanel } from '../search/stock_report_search_panel';\n\n\nexport const StockReportListView = {\n    ...listView,\n    SearchModel: StockReportSearchModel,\n    SearchPanel: StockReportSearchPanel,\n};\n\nregistry.category(\"views\").add(\"stock_report_list_view\", StockReportListView);\n", "import { registry } from \"@web/core/registry\";\nimport { ListRenderer } from \"@web/views/list/list_renderer\";\nimport { X2ManyField, x2ManyField } from \"@web/views/fields/x2many/x2many_field\";\nimport { ProductNameAndDescriptionListRendererMixin } from \"@product/product_name_and_description/product_name_and_description\";\nimport { user } from \"@web/core/user\";\nimport { patch } from \"@web/core/utils/patch\";\nimport { useOwnedDialogs, useService } from \"@web/core/utils/hooks\";\nimport { onWillStart } from \"@odoo/owl\";\nimport { SelectCreateDialog } from \"@web/views/view_dialogs/select_create_dialog\";\nimport { _t } from \"@web/core/l10n/translation\";\n\nexport class MovesListRenderer extends ListRenderer {\n    static rowsTemplate = \"stock.AddPackageListRendererRows\";\n\n    setup() {\n        super.setup();\n        this.addDialog = useOwnedDialogs();\n        this.orm = useService(\"orm\");\n        this.actionService = useService(\"action\");\n        this.descriptionColumn = \"description_picking\";\n        this.productColumns = [\"product_id\", \"product_template_id\"];\n\n        onWillStart(async () => {\n            this.hasPackageActive = await user.hasGroup(\"stock.group_tracking_lot\");\n        });\n    }\n\n    async onClickMovePackage() {\n        // If picking doesn't exist yet or location is outdated, it will lead to incorrect results\n        const canOpenDialog = await this.forceSave();\n        if (!canOpenDialog) {\n            return;\n        }\n        const domain = [];\n        if (this.locationId) {\n            domain.push([\"location_id\", \"child_of\", this.locationId]);\n        }\n        this.addDialog(SelectCreateDialog, {\n            title: _t(\"Select Packages to Move\"),\n            noCreate: true,\n            multiSelect: true,\n            resModel: \"stock.package\",\n            domain,\n            context: {\n                list_view_ref: \"stock.stock_package_view_add_list\",\n            },\n            onSelected: async (resIds) => {\n                if (resIds.length) {\n                    const done = await this.orm.call(\"stock.picking\", \"action_add_entire_packs\", [\n                        [this.pickingId],\n                        resIds,\n                    ]);\n                    if (done) {\n                        await this.actionService.doAction({\n                            type: \"ir.actions.client\",\n                            tag: \"soft_reload\",\n                        });\n                    }\n                }\n            },\n        });\n    }\n\n    get canAddPackage() {\n        return (\n            this.hasPackageActive &&\n            ![\"done\", \"cancel\"].includes(this.props.list.context.picking_state) &&\n            this.props.list.context.picking_type_code !== \"incoming\"\n        );\n    }\n\n    async forceSave() {\n        // This means the record hasn't been saved once, but we need the picking id to know for which picking we create the move lines.\n        const record = this.env.model.root;\n        const result = await record.save();\n        this.pickingId = record.data.id;\n        this.locationId = record.data.location_id?.id;\n        return result;\n    }\n}\n\npatch(MovesListRenderer.prototype, ProductNameAndDescriptionListRendererMixin);\n\nexport class StockMoveX2ManyField extends X2ManyField {\n    static components = { ...X2ManyField.components, ListRenderer: MovesListRenderer };\n}\n\nexport const stockMoveX2ManyField = {\n    ...x2ManyField,\n    component: StockMoveX2ManyField,\n};\n\nregistry.category(\"fields\").add(\"stock_move_one2many\", stockMoveX2ManyField);\n", "import { registry } from \"@web/core/registry\";\nimport { ProductNameAndDescriptionField } from \"@product/product_name_and_description/product_name_and_description\";\nimport { many2OneField } from \"@web/views/fields/many2one/many2one_field\";\n\nexport class MoveProductLabelField extends ProductNameAndDescriptionField {\n    static template = \"stock.MoveProductLabelField\";\n    static descriptionColumn = \"description_picking\";\n\n    get label() {\n        const record = this.props.record.data;\n        let label = record[this.descriptionColumn];\n        const productName = record.product_id.display_name;\n        if (label === productName) {\n            label = \"\";\n        }\n        return label.trim();\n    }\n    parseLabel(value) {\n        return value;\n    }\n}\n\nexport const moveProductLabelField = {\n    ...many2OneField,\n    component: MoveProductLabelField,\n};\nregistry.category(\"fields\").add(\"move_product_label_field\", moveProductLabelField);\n", "import { useService } from \"@web/core/utils/hooks\";\nimport { SearchModel } from \"@web/search/search_model\";\nimport { debounce } from \"@web/core/utils/timing\";\n\n\nexport class StockOrderpointSearchModel extends SearchModel {\n    static DEBOUNCE_DELAY = 500;\n\n    setup(services) {\n        super.setup(services);\n        this.ui = useService(\"ui\");\n        this.applyGlobalHorizonDays = debounce(\n            this.applyGlobalHorizonDays.bind(this),\n            StockOrderpointSearchModel.DEBOUNCE_DELAY\n        );\n    }\n\n    async applyGlobalHorizonDays(globalHorizonDays) {\n        this.ui.block();\n        this.globalContext = {\n            ...this.globalContext,\n            global_horizon_days: globalHorizonDays,\n        };\n        this._context = false; // Force rebuild of this.context to take into account the updated this.globalContext\n        await this.orm.call(\"stock.warehouse.orderpoint\", \"action_open_orderpoints\", [], {\n            context: {\n                ...this.context,\n                force_orderpoint_recompute: true,\n            }\n        });\n        await this._fetchSections(this.categories, this.filters);\n        this._notify();\n        this.ui.unblock();\n    }\n}\n", "import { useService } from \"@web/core/utils/hooks\";\nimport { onWillStart, useState } from '@odoo/owl';\nimport { SearchPanel } from \"@web/search/search_panel/search_panel\";\n\n\nexport class StockOrderpointSearchPanel extends SearchPanel {\n    static template = \"stock.StockOrderpointSearchPanel\";\n\n    setup() {\n        this.orm = useService(\"orm\");\n        super.setup(...arguments);\n        this.globalHorizonDays = useState({value: 0});\n        onWillStart(this.getHorizonParameter);\n    }\n\n    async getHorizonParameter() {\n        let res = await this.orm.call(\"stock.warehouse.orderpoint\", \"get_horizon_days\", [0]);\n        this.globalHorizonDays.value = Math.abs(parseInt(res)) || 0;\n    }\n\n    async applyGlobalHorizonDays(ev) {\n        this.globalHorizonDays.value = Math.max(parseInt(ev.target.value || 0), 0);\n        await this.env.searchModel.applyGlobalHorizonDays(this.globalHorizonDays.value);\n    }\n}\n", "import { SearchModel } from \"@web/search/search_model\";\n\nexport class StockReportSearchModel extends SearchModel {\n\n    async load() {\n        await super.load(...arguments);\n        await this._loadWarehouses();\n      }\n\n\n    //---------------------------------------------------------------------\n    // Actions / Getters\n    //---------------------------------------------------------------------\n\n    getWarehouses() {\n        return this.warehouses;\n    }\n\n    async _loadWarehouses() {\n        this.warehouses = await this.orm.call(\n            'stock.warehouse',\n            'get_current_warehouses',\n            [[]],\n            { context: this.context },\n        );\n    }\n\n    /**\n     * Clears the context of a warehouse so values calculate based on all possible\n     * warehouses\n     */\n    clearWarehouseContext() {\n        delete this.globalContext.warehouse_id;\n        this._notify();\n    }\n\n    /**\n     * @param {number} warehouse_id\n     * Sets the context to the selected warehouse so values that take this into account\n     * will recalculate based on this without filtering out any records\n     */\n    applyWarehouseContext(warehouse_id) {\n        this.globalContext['warehouse_id'] = warehouse_id;\n        this._notify();\n    }\n}\n", "import { SearchPanel } from \"@web/search/search_panel/search_panel\";\n\nexport class StockReportSearchPanel extends SearchPanel {\n    static template = \"stock.StockReportSearchPanel\";\n    setup() {\n        super.setup(...arguments);\n        this.selectedWarehouse = false;\n    }\n\n    //---------------------------------------------------------------------\n    // Actions / Getters\n    //---------------------------------------------------------------------\n\n    get warehouses() {\n        return this.env.searchModel.getWarehouses();\n    }\n\n    clearWarehouseContext() {\n        this.env.searchModel.clearWarehouseContext();\n        this.selectedWarehouse = null;\n    }\n\n    applyWarehouseContext(warehouse_id) {\n        this.env.searchModel.applyWarehouseContext(warehouse_id);\n        this.selectedWarehouse = warehouse_id;\n    }\n}\n", "import { registry } from \"@web/core/registry\";\nimport { listView } from \"@web/views/list/list_view\";\nimport { ListRenderer } from \"@web/views/list/list_renderer\";\nimport { Component } from \"@odoo/owl\";\nimport { useActionLinks } from \"@web/views/view_hook\";\n\nexport class StockActionHelper extends Component {\n    static template = \"stock.StockActionHelper\";\n    static props = [\"noContentHelp\"];\n    setup() {\n        const resModel = \"searchModel\" in this.env ? this.env.searchModel.resModel : undefined;\n        this.handler = useActionLinks(resModel);\n    }\n}\n\nexport class StockListRenderer extends ListRenderer {\n    static template = \"stock.StockListRenderer\";\n    static components = {\n        ...StockListRenderer.components,\n        StockActionHelper,\n    };\n}\n\nexport const StockListView = {\n    ...listView,\n    Renderer: StockListRenderer,\n};\n\nregistry.category(\"views\").add(\"stock_list_view\", StockListView);\n", "import { ListController } from '@web/views/list/list_controller';\nimport { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\n\nexport class StockOrderpointListController extends ListController {\n    static template = \"stock.StockOrderpoint.listView\";\n\n    static components = {\n        ...super.components,\n        Dropdown,\n        DropdownItem,\n    }\n\n    get nbSelected() {\n        return this.model.root.selection.length;\n    }\n\n    async onClickOrder(force_to_max) {\n        const resIds = await this.model.root.getResIds(true);\n        const action = await this.model.orm.call(this.props.resModel, 'action_replenish', [resIds], {\n            context: this.props.context,\n            force_to_max: force_to_max,\n        });\n        if (action) {\n            await this.actionService.doAction(action);\n        }\n        return this.actionService.doAction({type: 'ir.actions.client', tag: 'reload'});\n    }\n\n    async onClickSnooze() {\n        const resIds = await this.model.root.getResIds(true);\n        return this.actionService.doAction('stock.action_orderpoint_snooze', {\n            additionalContext: { default_orderpoint_ids: resIds },\n            onClose: () => { this.actionService.doAction({type: 'ir.actions.client', tag: 'reload'}); },\n        });\n    }\n}\n", "import { listView } from '@web/views/list/list_view';\nimport { registry } from \"@web/core/registry\";\nimport { StockOrderpointListController as Controller } from './stock_orderpoint_list_controller';\nimport { StockOrderpointSearchPanel } from './search/stock_orderpoint_search_panel';\nimport { StockOrderpointSearchModel } from './search/stock_orderpoint_search_model';\n\nexport const StockOrderpointListView = {\n    ...listView,\n    Controller,\n    SearchPanel: StockOrderpointSearchPanel,\n    SearchModel: StockOrderpointSearchModel,\n};\n\nregistry.category(\"views\").add(\"stock_orderpoint_list\", StockOrderpointListView);\n", "import { FloatField, floatField } from \"@web/views/fields/float/float_field\";\nimport { registry } from \"@web/core/registry\";\nimport { getActiveHotkey } from \"@web/core/hotkeys/hotkey_service\";\nimport { useEffect, useRef } from \"@odoo/owl\";\n\nexport class CountedQuantityWidgetField extends FloatField {\n    setup() {\n        // Need to adapt useInputField to overide onInput and onChange\n        super.setup();\n\n        const inputRef = useRef(\"numpadDecimal\");\n\n        useEffect(\n            (inputEl) => {\n                if (inputEl) {\n                    const boundOnInput = this.onInput.bind(this);\n                    const boundOnKeydown = this.onKeydown.bind(this);\n                    const boundOnBlur = this.onBlur.bind(this);\n                    inputEl.addEventListener(\"input\", boundOnInput);\n                    inputEl.addEventListener(\"keydown\", boundOnKeydown);\n                    inputEl.addEventListener(\"blur\", boundOnBlur);\n                    return () => {\n                        inputEl.removeEventListener(\"input\", boundOnInput);\n                        inputEl.removeEventListener(\"keydown\", boundOnKeydown);\n                        inputEl.removeEventListener(\"blur\", boundOnBlur);\n                    };\n                }\n            },\n            () => [inputRef.el]\n        );\n    }\n\n    onInput(ev) {\n        return this.props.record.update({ inventory_quantity_set: true });\n    }\n\n    updateValue(ev){\n        try {\n           const val = this.parse(ev.target.value);\n            this.props.record.update({ [this.props.name]: val });\n        } catch {} // ignore since it will be handled later\n    }\n\n    onBlur(ev) {\n         if (!this.props.record.data.inventory_quantity_set) {\n           return;\n        }\n        this.updateValue(ev);\n    }\n\n    onKeydown(ev) {\n        const hotkey = getActiveHotkey(ev);\n        if ([\"enter\", \"tab\", \"shift+tab\"].includes(hotkey)) {\n            this.updateValue(ev);\n            this.onInput(ev);\n        }\n    }\n\n    get formattedValue() {\n        if (\n            this.props.readonly &&\n            !this.props.record.data[this.props.name] & !this.props.record.data.inventory_quantity_set\n        ) {\n            return \"\";\n        }\n        return super.formattedValue;\n    }\n}\n\nexport const countedQuantityWidgetField = {\n    ...floatField,\n    component: CountedQuantityWidgetField,\n};\n\nregistry.category(\"fields\").add(\"counted_quantity_widget\", countedQuantityWidgetField);\n", "import { Component } from \"@odoo/owl\";\nimport { registry } from \"@web/core/registry\";\nimport { computeM2OProps, Many2One } from \"@web/views/fields/many2one/many2one\";\nimport { buildM2OFieldDescription, Many2OneField } from \"@web/views/fields/many2one/many2one_field\";\n\nexport class ForcedPlaceholder extends Many2One {\n    static template = \"stock.ForcedPlaceholder\";\n    static components = { ...Many2One.components };\n    static props = { ...Many2One.props };\n}\n\nexport class ForcedPlaceholderField extends Component {\n    static template = \"stock.ForcedPlaceholderField\";\n    static components = { ForcedPlaceholder };\n    static props = { ...Many2OneField.props };\n\n    get m2oProps() {\n        const props = computeM2OProps(this.props);\n        return {\n            ...props,\n            canOpen: !props.readonly && props.canOpen, // to remove the wrong link and the hand cursor on hover\n        }\n    }\n}\n\nregistry.category(\"fields\").add(\"stock.forced_placeholder\", {\n    ...buildM2OFieldDescription(ForcedPlaceholderField),\n});\n", "import { FloatField, floatField } from \"@web/views/fields/float/float_field\";\nimport { formatDate } from \"@web/core/l10n/dates\";\nimport { formatFloat } from \"@web/views/fields/formatters\";\nimport { registry } from \"@web/core/registry\";\nimport { useService } from \"@web/core/utils/hooks\";\n\nexport class ForecastWidgetField extends FloatField {\n    static template = \"stock.ForecastWidget\";\n    setup() {\n        const { data, fields, resId } = this.props.record;\n        this.actionService = useService(\"action\");\n        this.orm = useService(\"orm\");\n        this.resId = resId;\n\n        this.forecastExpectedDate = formatDate(\n            data.forecast_expected_date,\n            fields.forecast_expected_date\n        );\n        if (data.forecast_expected_date && data.date_deadline) {\n            this.forecastIsLate = data.forecast_expected_date > data.date_deadline;\n        }\n        const digits = fields.forecast_availability.digits;\n        const options = { digits, thousandsSep: \"\", decimalPoint: \".\" };\n        const forecast_availability = parseFloat(formatFloat(data.forecast_availability, options));\n        const product_qty = parseFloat(formatFloat(data.product_qty, options));\n        this.willBeFulfilled = forecast_availability >= product_qty;\n        this.state = data.state;\n    }\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n     * Opens the Forecast Report for the `stock.move` product.\n     */\n    async _openReport(ev) {\n        ev.preventDefault();\n        ev.stopPropagation();\n        if (!this.resId || !this.props.record.data.is_storable) {\n            return;\n        }\n        const action = await this.orm.call(\"stock.move\", \"action_product_forecast_report\", [\n            this.resId,\n        ]);\n        this.actionService.doAction(action);\n    }\n\n    get decoration() {\n        if (!this.forecastExpectedDate && this.willBeFulfilled){\n            return \"text-bg-success\"\n        } else if (this.forecastExpectedDate && this.willBeFulfilled){\n            return this.forecastIsLate ? 'text-bg-danger' : 'text-bg-warning'\n        } else {\n            return 'text-bg-danger'\n        }\n\n    }\n}\n\nexport const forecastWidgetField = {\n    ...floatField,\n    component: ForecastWidgetField,\n};\n\nregistry.category(\"fields\").add(\"forecast_widget\", forecastWidgetField);\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { x2ManyCommands } from \"@web/core/orm_service\";\nimport { Dialog } from '@web/core/dialog/dialog';\nimport { useService } from \"@web/core/utils/hooks\";\nimport { registry } from \"@web/core/registry\";\nimport { parseInteger  } from \"@web/views/fields/parsers\";\nimport { getId } from \"@web/model/relational_model/utils\";\nimport { Component, useRef, onMounted, onWillStart } from \"@odoo/owl\";\nimport { standardWidgetProps } from \"@web/views/widgets/standard_widget_props\";\nimport { user } from \"@web/core/user\";\n\nexport class GenerateDialog extends Component {\n    static template = \"stock.generate_serial_dialog\";\n    static components = { Dialog };\n    static props = {\n        mode: { type: String },\n        move: { type: Object },\n        close: { type: Function },\n    };\n    setup() {\n        this.size = 'md';\n        if (this.props.mode === 'generate') {\n            this.title = this.props.move.data.has_tracking === 'lot'\n            ? _t(\"Generate Lot numbers\")\n            : _t(\"Generate Serial numbers\");\n        } else {\n            this.title = this.props.move.data.has_tracking === 'lot' ? _t(\"Import Lots\") : _t(\"Import Serials\");\n        }\n\n        this.nextSerial = useRef('nextSerial');\n        this.nextSerialCount = useRef('nextSerialCount');\n        this.totalReceived = useRef('totalReceived');\n        this.keepLines = useRef('keepLines');\n        this.lots = useRef('lots');\n        this.orm = useService(\"orm\");\n        onWillStart(async () => {\n            this.displayUOM = await user.hasGroup(\"uom.group_uom\");\n        });\n        onMounted(() => {\n            if (this.props.mode === 'generate') {\n                this.nextSerialCount.el.value = this.props.move.data.product_uom_qty || 2;\n                if (this.props.move.data.has_tracking === 'lot') {\n                    this.totalReceived.el.value = this.props.move.data.quantity;\n                }\n            }\n        });\n    }\n    async _onGenerateCustomSerial() {\n        const product = (await this.orm.searchRead(\"product.product\", [[\"id\", \"=\", this.props.move.data.product_id.id]], [\"lot_sequence_id\"]))[0];\n        this.sequence = product.lot_sequence_id;\n        if (product.lot_sequence_id) {\n            this.sequence = (await this.orm.searchRead(\"ir.sequence\", [[\"id\", \"=\", this.sequence[0]]], [\"number_next_actual\"]))[0];\n            this.nextCustomSerialNumber = await this.orm.call(\"ir.sequence\", \"next_by_id\", [this.sequence.id]);\n            this.nextSerial.el.value = this.nextCustomSerialNumber;\n        }\n    }\n    async _onGenerate() {\n        let count;\n        let qtyToProcess;\n        if (this.props.move.data.has_tracking === 'lot'){\n            count = parseFloat(this.nextSerialCount.el?.value || '0');\n            qtyToProcess = parseFloat(this.totalReceived.el?.value || this.props.move.data.product_qty);\n        } else {\n            count = parseInteger(this.nextSerialCount.el?.value || '0');\n            qtyToProcess = this.props.move.data.product_qty;\n        }\n        const move_line_vals = await this.orm.call(\"stock.move\", \"action_generate_lot_line_vals\", [{\n                ...this.props.move.context,\n                default_product_id: this.props.move.data.product_id.id,\n                default_location_dest_id: this.props.move.data.location_dest_id.id,\n                default_location_id: this.props.move.data.location_id.id,\n                default_tracking: this.props.move.data.has_tracking,\n                default_quantity: qtyToProcess,\n            },\n            this.props.mode,\n            this.nextSerial.el?.value,\n            count,\n            this.lots.el?.value,\n        ]);\n        const newlines = [];\n        let lines = []\n        lines = this.props.move.data.move_line_ids;\n\n        // create records directly from values to bypass onchanges\n        for (const values of move_line_vals) {\n            newlines.push(\n                lines._createRecordDatapoint(values, {\n                    mode: 'readonly',\n                    virtualId: getId(\"virtual\"),\n                    manuallyAdded: false,\n                })\n            );\n        }\n        if (!this.keepLines.el.checked) {\n            await lines._applyCommands(lines._currentIds.map((currentId) => [\n                x2ManyCommands.DELETE,\n                currentId,\n            ]));\n        }\n        lines.records.push(...newlines);\n        lines._commands.push(...newlines.map((record) => [\n            x2ManyCommands.CREATE,\n            record._virtualId,\n        ]));\n        lines._currentIds.push(...newlines.map((record) => record._virtualId));\n        await lines._onUpdate();\n        if (this.sequence && this.nextSerial.el.value === this.nextCustomSerialNumber) {\n            await this.orm.write(\"ir.sequence\", [this.sequence.id], {number_next_actual: this.sequence.number_next_actual + newlines.length});\n        }\n        this.props.close();\n    }\n}\n\nclass GenerateSerials extends Component {\n    static template = \"stock.GenerateSerials\";\n    static props = {...standardWidgetProps};\n\n    setup(){\n        this.dialog = useService(\"dialog\");\n    }\n\n    openDialog(ev){\n        this.dialog.add(GenerateDialog, {\n            move: this.props.record,\n            mode: 'generate',\n        });\n    }\n}\n\nclass ImportLots extends Component {\n    static template = \"stock.ImportLots\";\n    static props = {...standardWidgetProps};\n    setup(){\n        this.dialog = useService(\"dialog\");\n    }\n\n    openDialog(ev){\n        this.dialog.add(GenerateDialog, {\n            move: this.props.record,\n            mode: 'import',\n        });\n    }\n}\nregistry.category(\"view_widgets\").add(\"import_lots\", {component: ImportLots});\nregistry.category(\"view_widgets\").add(\"generate_serials\", {component: GenerateSerials});\n", "import { loadBundle } from \"@web/core/assets\";\nimport { cookie } from \"@web/core/browser/cookie\";\nimport { getColor } from \"@web/core/colors/colors\";\nimport { registry } from \"@web/core/registry\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { user } from \"@web/core/user\";\nimport { Component, onWillStart, useEffect, useRef } from \"@odoo/owl\";\nimport { standardFieldProps } from \"@web/views/fields/standard_field_props\";\n\nexport class JsonPopOver extends Component {\n    static template = \"\";\n    static props = {...standardFieldProps};\n    get jsonValue() {\n        return JSON.parse(this.props.record.data[this.props.name]);\n    }\n}\n\nexport const jsonPopOver = {\n    component: JsonPopOver,\n    displayName: _t(\"Json Popup\"),\n    supportedTypes: [\"char\"],\n};\n\n// --------------------------------------------------------------------------\n// Lead Days\n// --------------------------------------------------------------------------\n\nexport class PopOverLeadDays extends JsonPopOver {\n    static template = \"stock.leadDays\";\n}\n\nexport const popOverLeadDays = {\n    ...jsonPopOver,\n    component: PopOverLeadDays,\n};\nregistry.category(\"fields\").add(\"lead_days_widget\", popOverLeadDays);\n\n// --------------------------------------------------------------------------\n// Forecast Graph\n// --------------------------------------------------------------------------\n\nexport class ReplenishmentGraphWidget extends JsonPopOver {\n    static template = \"stock.replenishmentGraph\";\n    setup() {\n        super.setup();\n        this.chart = null;\n        this.canvasRef = useRef(\"canvas\");\n        onWillStart(async () => {\n            this.displayUOM = await user.hasGroup(\"uom.group_uom\");\n            await loadBundle(\"web.chartjs_lib\");\n        });\n\n        useEffect(() => {\n            this.renderChart();\n            return () => {\n                if (this.chart) {\n                    this.chart.destroy();\n                }\n            };\n        });\n    }\n    get productUomName(){\n        return this.jsonValue[\"product_uom_name\"];\n    }\n    get qtyOnHand(){\n        return this.jsonValue[\"qty_on_hand\"];\n    }\n    get productMaxQty() {\n        return this.jsonValue[\"product_max_qty\"];\n    }\n    get productMinQty() {\n        return this.jsonValue[\"product_min_qty\"];\n    }\n    get dailyDemand() {\n        return this.jsonValue[\"daily_demand\"];\n    }\n    get averageStock() {\n        return this.jsonValue[\"average_stock\"];\n    }\n    get orderingPeriod() {\n        return this.jsonValue[\"ordering_period\"];\n    }\n    get qtiesAreTheSame() {\n        return this.productMinQty === this.productMaxQty;\n    }\n    get leadTime() {\n        return this.jsonValue[\"lead_time\"];\n    }\n\n    renderChart() {\n        if (this.chart) {\n            this.chart.destroy();\n        }\n        const config = this.getScatterGraphConfig();\n        this.chart = new Chart(this.canvasRef.el, config);\n    }\n\n    getScatterGraphConfig() {\n        const dashLine = (ctx, value) => ctx.p1.raw.x === this.jsonValue['x_axis_vals'].slice(-1)[0] ? value : undefined;\n        const pushYLabels = (ticks) => ticks.push({value: this.productMinQty}, {value: this.productMaxQty});\n        const showYLabel = (tick) => tick === this.productMinQty || tick === this.productMaxQty ? tick : '';\n        const labels = this.jsonValue['x_axis_vals'];\n        const maxLineColor = getColor(1, cookie.get(\"color_scheme\"), \"odoo\");\n        const minLineColor = getColor(2, cookie.get(\"color_scheme\"), \"odoo\");\n        const curveLineColor = getColor(3, cookie.get(\"color_scheme\"), \"odoo\");\n        return {\n            type: \"scatter\",\n            data: {\n                labels,\n                datasets: [{\n                    type: \"line\",\n                    data: this.jsonValue[\"max_line_vals\"],\n                    fill: false,\n                    pointStyle: false,\n                    borderColor: maxLineColor,\n                }, {\n                    type: \"line\",\n                    data: this.jsonValue[\"min_line_vals\"],\n                    fill: false,\n                    pointStyle: false,\n                    borderColor: minLineColor,\n                }, {\n                    type: \"line\",\n                    data: this.jsonValue[\"curve_line_vals\"],\n                    fill: false,\n                    pointStyle: false,\n                    borderColor: curveLineColor,\n                    segment: {\n                        borderDash: ctx => dashLine(ctx, [6, 6]),\n                    }\n                }],\n            },\n            options: {\n                maintainAspectRatio: false,\n                showLine: true,\n                plugins: {\n                    legend: { display: false },\n                    tooltip: { enabled: false },\n                },\n                scales: {\n                    y: {\n                        grid: {display: false},\n                        beforeTickToLabelConversion: data => pushYLabels(data.ticks),\n                        ticks: {\n                            autoSkip: false,\n                            callback: tick => showYLabel(tick),\n                        },\n                        suggestedMax: this.productMaxQty * 1.1,\n                        suggestedMin: this.productMinQty * 0.9,\n                    },\n                    x: {\n                        type: 'category',\n                        grid: {display: false},\n                    },\n                },\n            },\n        }\n    }\n}\n\nexport const replenishmentGraphWidget = {\n    ...jsonPopOver,\n    component: ReplenishmentGraphWidget,\n};\n\nregistry.category(\"fields\").add(\"replenishment_graph_widget\", replenishmentGraphWidget);\n", "import { Many2XAutocomplete } from \"@web/views/fields/relational_utils\";\nimport {\n    Many2ManyTagsField,\n    many2ManyTagsField,\n} from \"@web/views/fields/many2many_tags/many2many_tags_field\";\nimport { registry } from \"@web/core/registry\";\n\nexport class Many2XBarcodeTagsAutocomplete extends Many2XAutocomplete {\n    onQuickCreateError(error, request) {\n        if (error.data?.debug && error.data.debug.includes(\"psycopg2.errors.UniqueViolation\")) {\n            throw error;\n        }\n        super.onQuickCreateError(error, request);\n    }\n}\n\nexport class Many2ManyBarcodeTagsField extends Many2ManyTagsField {\n    static components = {\n        ...Many2ManyTagsField.components,\n        Many2XAutocomplete: Many2XBarcodeTagsAutocomplete,\n    };\n}\n\nexport const many2ManyBarcodeTagsField = {\n    ...many2ManyTagsField,\n    component: Many2ManyBarcodeTagsField,\n    additionalClasses: ['o_field_many2many_tags'],\n}\n\nregistry.category(\"fields\").add(\"many2many_barcode_tags\", many2ManyBarcodeTagsField);\n", "import { registry } from \"@web/core/registry\";\nimport { usePopover } from \"@web/core/popover/popover_hook\";\nimport { Component } from \"@odoo/owl\";\nimport { standardFieldProps } from \"@web/views/fields/standard_field_props\";\n\n/**\n * Extend this to add functionality to Popover (custom methods etc.)\n * need to extend PopoverWidgetField as well and set its Popover Component to new extension\n */\nexport class PopoverComponent extends Component {\n    static template = \"stock.popoverContent\";\n    static props = [\"record\", \"*\"];\n}\n\n/**\n * Widget Popover for JSON field (char), renders a popover above an icon button on click\n * {\n *  'msg': '<CONTENT OF THE POPOVER>' required if not 'popoverTemplate' is given,\n *  'icon': '<FONT AWESOME CLASS>' default='fa-info-circle',\n *  'color': '<COLOR CLASS OF ICON>' default='text-primary',\n *  'position': <POSITION OF THE POPOVER> default='top',\n *  'popoverTemplate': '<TEMPLATE OF THE POPOVER>' default='stock.popoverContent'\n *   pass a template for popover to use, other data passed in JSON field will be passed\n *   to popover template inside props (ex. props.someValue), must be owl template\n * }\n */\n\nexport class PopoverWidgetField extends Component {\n    static template = \"stock.popoverButton\";\n    static components = { Popover: PopoverComponent };\n    static props = {...standardFieldProps};\n    setup(){\n        let fieldValue = this.props.record.data[this.props.name];\n        this.jsonValue = JSON.parse(fieldValue || \"{}\");\n        const position = this.jsonValue.position || \"top\";\n        this.popover = usePopover(this.constructor.components.Popover, { position });\n        this.color = this.jsonValue.color || 'text-primary';\n        this.icon = this.jsonValue.icon || 'fa-info-circle';\n    }\n\n    showPopup(ev){\n        this.popover.open(ev.currentTarget, { ...this.jsonValue, record: this.props.record });\n    }\n}\n\nexport const popoverWidgetField = {\n    component: PopoverWidgetField,\n    supportedTypes: ['char'],\n};\n\nregistry.category(\"fields\").add(\"popover_widget\", popoverWidgetField);\n", "import {\n    Many2ManyTagsField,\n    many2ManyTagsField,\n} from \"@web/views/fields/many2many_tags/many2many_tags_field\";\nimport { registry } from \"@web/core/registry\";\nimport { _t } from \"@web/core/l10n/translation\";\n\n\nexport class Many2ManyPackageTagsField extends Many2ManyTagsField {\n    setup() {\n        this.hasNoneTag = this.props.record.data?.has_lines_without_result_package || false;\n    }\n\n    get tags() {\n        const tags = super.tags;\n        if (this.hasNoneTag) {\n            tags.push({\n                ...this.getTagProps(this.props.record.data[this.props.name].records.at(-1)),\n                id: \"datapoint_None\",\n                text: _t(\"No Package\"),\n            });\n        }\n        return tags;\n    }\n\n    getTagProps(record) {\n        return {\n            ...super.getTagProps(record),\n            text: record.data.name,\n        };\n    }\n}\n\nexport const many2ManyPackageTagsField = {\n    ...many2ManyTagsField,\n    component: Many2ManyPackageTagsField,\n    additionalClasses: ['o_field_many2many_tags'],\n    relatedFields: () => [\n        { name: \"name\", type: \"char\" },\n    ],\n}\n\nregistry.category(\"fields\").add(\"package_m2m\", many2ManyPackageTagsField);\n", "import { Component } from \"@odoo/owl\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { registry } from \"@web/core/registry\";\nimport { computeM2OProps, Many2One } from \"@web/views/fields/many2one/many2one\";\nimport {\n    buildM2OFieldDescription,\n    extractM2OFieldProps,\n    Many2OneField,\n} from \"@web/views/fields/many2one/many2one_field\";\nimport { Many2XAutocomplete } from \"@web/views/fields/relational_utils\";\nimport { FormViewDialog } from \"@web/views/view_dialogs/form_view_dialog\";\n\nclass PackageFormDialog extends FormViewDialog {}\n\nclass Many2XStockPackageAutocomplete extends Many2XAutocomplete {\n    get createDialog() {\n        const packageFormDialog = PackageFormDialog;\n        packageFormDialog.defaultProps = {\n            ...packageFormDialog.defaultProps,\n            onRecordSave: async (record) => {\n                // We need to reload to get the name computed from the backend.\n                const saved = await record.save({ reload: true });\n                if (saved && this.props.update) {\n                    // Without this, the package is named 'Unnamed' in the UI until the record is saved.\n                    this.props.update([{ ...record.data, id: record.resId }]);\n                }\n                return saved;\n            },\n        };\n        return packageFormDialog;\n    }\n}\n\nclass StockPackageMany2OneReplacer extends Many2One {\n    static components = {\n        ...Many2One.components,\n        Many2XAutocomplete: Many2XStockPackageAutocomplete,\n    };\n}\n\nexport class StockPackageMany2One extends Component {\n    static template = \"stock.StockPackageMany2One\";\n    static components = { Many2One: StockPackageMany2OneReplacer };\n    static props = {\n        ...Many2OneField.props,\n        displaySource: { type: Boolean },\n        displayDestination: { type: Boolean },\n    };\n\n    setup() {\n        this.orm = useService(\"orm\");\n        this.isDone = [\"done\", \"cancel\"].includes(this.props.record?.data?.state);\n    }\n\n    get m2oProps() {\n        const props = computeM2OProps(this.props);\n        return {\n            ...props,\n            context: {\n                ...props.context,\n                ...this.displayNameContext,\n            },\n            value: this.displayValue,\n        };\n    }\n\n    get isEditing() {\n        return this.props.record.isInEdition;\n    }\n\n    get displayValue() {\n        const displayVal = this.props.record.data[this.props.name];\n        if (this.isDone && displayVal?.display_name) {\n            displayVal[\"display_name\"] = displayVal[\"display_name\"].split(\" > \").pop();\n        }\n        return displayVal;\n    }\n\n    get displayNameContext() {\n        return {\n            show_src_package: this.props.displaySource,\n            show_dest_package: this.props.displayDestination,\n            is_done: this.isDone,\n        };\n    }\n}\n\nregistry.category(\"fields\").add(\"package_m2o\", {\n    ...buildM2OFieldDescription(StockPackageMany2One),\n    extractProps(staticInfo, dynamicInfo) {\n        const context = dynamicInfo.context;\n        return {\n            ...extractM2OFieldProps(staticInfo, dynamicInfo),\n            displaySource: !!context?.show_src_package,\n            displayDestination: !!context?.show_dest_package,\n        };\n    },\n});\n", "import { Component } from \"@odoo/owl\";\nimport { registry } from \"@web/core/registry\";\nimport { computeM2OProps, Many2One } from \"@web/views/fields/many2one/many2one\";\nimport { buildM2OFieldDescription, Many2OneField } from \"@web/views/fields/many2one/many2one_field\";\n\nexport class StockPickFrom extends Component {\n    static template = \"stock.StockPickFrom\";\n    static components = { Many2One };\n    static props = { ...Many2OneField.props };\n\n    get m2oProps() {\n        const props = computeM2OProps(this.props);\n        return {\n            ...props,\n            value: props.value || { id: 0, display_name: this._quant_display_name() },\n        };\n    }\n\n    _quant_display_name() {\n        let name_parts = [];\n        // if location group is activated\n        const data = this.props.record.data;\n        name_parts.push(data.location_id?.display_name)\n        if (data.lot_id) {\n            name_parts.push(data.lot_id?.display_name || data.lot_name)\n        }\n        if (data.package_id) {\n            let packageName = data.package_id?.display_name;\n            if (packageName && [\"done\", \"cancel\"].includes(data.state)) {\n                packageName = packageName.split(\" > \").pop();\n            }\n            name_parts.push(packageName);\n        }\n        if (data.owner) {\n            name_parts.push(data.owner?.display_name)\n        }\n        const result = name_parts.join(\" - \");\n        if (result) return result;\n        return \"\";\n    }\n}\n\nregistry.category(\"fields\").add(\"pick_from\", {\n    ...buildM2OFieldDescription(StockPickFrom),\n    fieldDependencies: [\n        // dependencies to build the quant display name\n        { name: \"location_id\", type: \"relation\" },\n        { name: \"location_dest_id\", type: \"relation\" },\n        { name: \"package_id\", type: \"relation\" },\n        { name: \"owner_id\", type: \"relation\" },\n        { name: \"state\", type: \"char\" },\n    ],\n});\n", "import { useService } from \"@web/core/utils/hooks\";\nimport { registry } from \"@web/core/registry\";\nimport {\n    PopoverComponent,\n    PopoverWidgetField,\n    popoverWidgetField,\n} from \"@stock/widgets/popover_widget\";\n\nexport class StockRescheculingPopoverComponent extends PopoverComponent {\n    setup(){\n        this.action = useService(\"action\");\n    }\n\n    openElement(ev){\n        this.action.doAction({\n            res_model: ev.currentTarget.getAttribute('element-model'),\n            res_id: parseInt(ev.currentTarget.getAttribute('element-id')),\n            views: [[false, \"form\"]],\n            type: \"ir.actions.act_window\",\n            view_mode: \"form\",\n        });\n    }\n}\n\nexport class StockRescheculingPopover extends PopoverWidgetField {\n    static components = {\n        Popover: StockRescheculingPopoverComponent\n    };\n    setup(){\n        super.setup();\n        this.color = this.jsonValue.color || 'text-danger';\n        this.icon = this.jsonValue.icon || 'fa-exclamation-triangle';\n    }\n\n    showPopup(ev){\n        if (!this.jsonValue.late_elements){\n            return;\n        }\n        super.showPopup(ev);\n    }\n}\n\nregistry.category(\"fields\").add(\"stock_rescheduling_popover\", {\n    ...popoverWidgetField,\n    component: StockRescheculingPopover,\n});\n", "import { Message } from \"@mail/core/common/message_model\";\nimport { fields } from \"@mail/core/common/record\";\n\nimport { patch } from \"@web/core/utils/patch\";\n\npatch(Message.prototype, {\n    setup() {\n        super.setup(...arguments);\n        this.rating_id = fields.One(\"rating.rating\");\n    },\n\n    computeIsEmpty() {\n        return super.computeIsEmpty() && !this.rating_id && !this.rating_value;\n    },\n\n    get removeParams() {\n        return { ...super.removeParams, rating_value: false };\n    },\n});\n", "import { Record } from \"@mail/core/common/record\";\n\nexport class Rating extends Record {\n    static _name = \"rating.rating\";\n    static id = \"id\";\n\n    /** @type {number} */\n    id;\n    /** @type {number} */\n    rating;\n    /** @type {string} */\n    rating_image_url;\n    /** @type {string} */\n    rating_text;\n}\nRating.register();\n", "import { NotificationItem } from \"@mail/core/public_web/notification_item\";\n\nNotificationItem.props = [...NotificationItem.props, \"rating?\"];\n", "import { patch } from '@web/core/utils/patch';\n\nimport {\n    ApplyConfiguratorScreen,\n    Configurator,\n    FeaturesSelectionScreen,\n    ROUTES,\n} from '@website/client_actions/configurator/configurator';\nimport {\n    ProductPageSelectionScreen,\n} from '@website_sale/js/client_actions/configurator/productPageSelectionScreen';\nimport {\n    ShopPageSelectionScreen,\n} from '@website_sale/js/client_actions/configurator/shopPageSelectionScreen';\n\nROUTES.shopPageSelectionScreen = 50;\nROUTES.productPageSelectionScreen = 55;\n\npatch(ApplyConfiguratorScreen.prototype, {\n\n    /**\n     * @override to include eCommerce pages style configuration.\n     */\n    getConfigurationData() {\n        const data = super.getConfigurationData(...arguments);\n        return Object.assign(data, {\n            'shop_page_style_option': this.state.selectedShopPageStyleOption,\n            'product_page_style_option': this.state.selectedProductPageStyleOption,\n        });\n    },\n\n})\n\npatch(FeaturesSelectionScreen, {\n\n    /**\n     * @override to redirect to the shop page selection screen.\n     */\n    nextStep() {\n        return ROUTES.shopPageSelectionScreen;\n    },\n\n});\n\npatch(Configurator, {\n\n    components: {\n        ...Configurator.components,\n        ShopPageSelectionScreen,\n        ProductPageSelectionScreen,\n    },\n\n})\n\npatch(Configurator.prototype, {\n\n    /**\n     * @override to include eCommerce's selection screen components.\n     */\n    get currentComponent() {\n        if (this.state.currentStep === ROUTES.shopPageSelectionScreen) {\n            return ShopPageSelectionScreen;\n        }\n        if (this.state.currentStep === ROUTES.productPageSelectionScreen) {\n            return ProductPageSelectionScreen;\n        }\n        return super.currentComponent;\n    },\n\n    /**\n     * @override to include eCommerce's initial page style values.\n     */\n    async getInitialState() {\n        const initState = await super.getInitialState(...arguments);\n        initState.selectedShopPageStyleOption = undefined;\n        initState.selectedProductPageStyleOption = undefined;\n        return initState;\n    },\n\n})\n", "import { Component, onWillStart } from '@odoo/owl';\nimport { useService } from '@web/core/utils/hooks';\n\nimport { ROUTES, useStore } from '@website/client_actions/configurator/configurator';\n\nexport class ProductPageSelectionScreen extends Component {\n    static template = 'website_sale.Configurator.ProductPageSelectionScreen';\n    static props = {\n        navigate: Function,\n        skip: Function,\n    };\n\n    setup() {\n        super.setup();\n        this.orm = useService('orm');\n        this.state = useStore();\n        onWillStart(async () => {\n            this.productPageStyles = await this.orm.call(\n                'website', 'get_configurator_product_page_styles', [], {}\n            );\n        });\n    }\n\n    selectStyle(option) {\n        this.state.selectedProductPageStyleOption = option;\n        this.props.navigate(ROUTES.themeSelectionScreen);\n    }\n}\n", "import { Component, onWillStart } from '@odoo/owl';\nimport { useService } from '@web/core/utils/hooks';\n\nimport { ROUTES, useStore } from '@website/client_actions/configurator/configurator';\n\nexport class ShopPageSelectionScreen extends Component {\n    static template = 'website_sale.Configurator.ShopPageSelectionScreen';\n    static props = {\n        navigate: Function,\n        skip: Function,\n    };\n\n    setup() {\n        super.setup();\n        this.orm = useService('orm');\n        this.state = useStore();\n        onWillStart(async () => {\n            this.shopPageStyles = await this.orm.call(\n                'website', 'get_configurator_shop_page_styles', [], {}\n            );\n        });\n    }\n\n    selectStyle(option) {\n        this.state.selectedShopPageStyleOption = option;\n        this.props.navigate(ROUTES.productPageSelectionScreen);\n    }\n}\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { clickOnElement } from '@website/js/tours/tour_utils';\n\nexport function addToCart({\n    productName,\n    search = true,\n    productHasVariants = false,\n    expectUnloadPage = false,\n} = {}) {\n    const steps = [];\n    if (search) {\n        steps.push(...searchProduct(productName));\n    }\n    steps.push({\n        content: productName,\n        trigger: `a:contains(${productName})`,\n        run: \"click\",\n        expectUnloadPage,\n    });\n    steps.push({\n        content: \"Add to cart\",\n        trigger: \"#add_to_cart\",\n        run: \"click\",\n    });\n    if (productHasVariants) {\n        steps.push(clickOnElement('Continue Shopping', 'button:contains(\"Continue Shopping\")'));\n    }\n    return steps;\n}\n\nexport function assertCartAmounts({taxes = false, untaxed = false, total = false, delivery = false}) {\n    let steps = [];\n    if (taxes) {\n        steps.push({\n            content: 'Check if the tax is correct',\n            trigger: `tr[name=\"o_order_total_taxes\"] .oe_currency_value:text(${taxes})`,\n        });\n    }\n    if (untaxed) {\n        steps.push({\n            content: 'Check if the subtotal is correct',\n            trigger: `tr[name=\"o_order_total_untaxed\"] .oe_currency_value:text(${untaxed})`,\n        });\n    }\n    if (total) {\n        steps.push({\n            content: 'Check if the total is correct',\n            trigger: `tr[name=\"o_order_total\"] .oe_currency_value:text(${total})`,\n        });\n    }\n    if (delivery) {\n        steps.push({\n            content: 'Check if the delivery is correct',\n            trigger: `tr[name='o_order_delivery'] .oe_currency_value:text(${delivery})`,\n        });\n    }\n    return steps\n}\n\nexport function assertCartContains({productName, backend, notContains = false, combinationName = false} = {}) {\n    let trigger = `h6:contains(${productName})`;\n\n    if (notContains) {\n        trigger = `:not(${trigger})`;\n    }\n    let steps = [{\n        content: `Checking if ${productName} is in the cart`,\n        trigger: `${backend ? \":iframe\" : \"\"} ${trigger}`,\n    }];\n\n    if (combinationName) {\n        const combination_trigger = `span[class*=h6]:contains(${combinationName})`;\n        steps.push({\n            content: `Checking if ${combinationName} is the chosen combination in the cart`,\n            trigger: `${backend ? \":iframe\" : \"\"} ${combination_trigger}`,\n        })\n    }\n\n    return steps;\n}\n\n/**\n * Used to assert if the price attribute of a given product is correct on the /shop view\n */\nexport function assertProductPrice(attribute, value, productName) {\n    return {\n        content: `The ${attribute} of the ${productName} is ${value}`,\n        trigger: `div:contains(\"${productName}\") [data-oe-expression=\"template_price_vals['${attribute}']\"] .oe_currency_value:contains(\"${value}\")`,\n    };\n}\n\nexport function fillAdressForm(\n    adressParams = {\n        name: \"John Doe\",\n        phone: \"123456789\",\n        email: \"johndoe@gmail.com\",\n        street: \"1 rue de la paix\",\n        city: \"Paris\",\n        zip: \"75000\",\n    },\n    expectUnloadPage = false\n) {\n    const steps = [];\n    steps.push({\n        trigger: \"#o_country_id\",\n        run: \"selectByLabel Belgium\",\n    });\n    for (const arg of [\"name\", \"phone\", \"email\", \"street\", \"city\", \"zip\"]) {\n        steps.push({\n            content: `Address filling ${arg}`,\n            trigger: `form.address_autoformat input[name=${arg}]`,\n            run: `edit ${adressParams[arg]}`,\n        });\n    }\n    steps.push({\n        content: \"Continue checkout\",\n        trigger: \"a[name='website_sale_main_button']\",\n        run: \"click\",\n        expectUnloadPage,\n    });\n    return steps;\n}\n\nexport function goToCart({\n    quantity = 1,\n    position = \"bottom\",\n    backend = false,\n    expectUnloadPage = true,\n} = {}) {\n    return {\n        content: _t(\"Go to cart\"),\n        trigger: `${backend ? \":iframe\" : \"\"} a sup.my_cart_quantity:text(${quantity})`,\n        tooltipPosition: position,\n        run: \"click\",\n        expectUnloadPage,\n    };\n}\n\nexport function goToCheckout() {\n    return {\n        content: 'Checkout your order',\n        trigger: 'a[href^=\"/shop/checkout\"]',\n        run: 'click',\n        expectUnloadPage: true,\n    };\n}\n\nexport function confirmOrder() {\n    return {\n        content: 'Confirm',\n        trigger: 'a[href^=\"/shop/payment\"]',\n        run: 'click',\n        expectUnloadPage: true,\n    };\n}\n\nexport function pay({ expectUnloadPage = false, waitFinalizeYourPayment = false } = {}) {\n    const steps = [\n        {\n            content: 'Pay',\n            //Either there are multiple payment methods, and one is checked, either there is only one, and therefore there are no radio inputs\n            trigger: 'button[name=\"o_payment_submit_button\"]',\n            run: \"click\",\n            expectUnloadPage,\n        },\n    ];\n    if (waitFinalizeYourPayment) {\n        steps.push({\n            trigger: \"h1:contains(finalize your payment)\",\n            expectUnloadPage: true,\n        });\n    }\n    return steps;\n}\n\nexport function payWithDemo() {\n    return [{\n        content: 'eCommerce: add card number',\n        trigger: 'input[name=\"customer_input\"]',\n        run: \"edit 4242424242424242\",\n    },\n    ...pay({expectUnloadPage: true}),\n    {\n        content: 'eCommerce: check that the payment is successful',\n        trigger: '[name=\"order_confirmation\"]:contains(\"Your payment has been processed.\")',\n    }]\n}\n\nexport function payWithTransfer({\n    redirect = false,\n    expectUnloadPage = false,\n    waitFinalizeYourPayment = false,\n} = {}) {\n    const first_step = {\n        content: \"Select `Wire Transfer` payment method\",\n        trigger: 'input[name=\"o_payment_radio\"][data-payment-method-code=\"wire_transfer\"]',\n        run: \"click\",\n    }\n    if (!redirect) {\n        return [\n            first_step,\n            ...pay({ expectUnloadPage, waitFinalizeYourPayment }),\n            {\n                content: \"Last step\",\n                trigger:\n                    '[name=\"order_confirmation\"]:contains(\"Please use the following transfer details\")',\n                timeout: 30000,\n            },\n        ];\n    } else {\n        return [\n            first_step,\n            ...pay({ expectUnloadPage, waitFinalizeYourPayment }),\n            {\n                content: \"Last step\",\n                trigger:\n                    '[name=\"order_confirmation\"]:contains(\"Please use the following transfer details\")',\n                timeout: 30000,\n                run() {\n                    window.location.href = '/contactus'; // Redirect in JS to avoid the RPC loop (20x1sec)\n                },\n                expectUnloadPage: true,\n            },\n            {\n                content: \"wait page loaded\",\n                trigger: 'h1:contains(\"Contact us\")',\n            },\n        ];\n    }\n}\n\nexport function searchProduct(productName, { select = false } = {}) {\n    const steps = [\n        {\n            content: \"Search for the product\",\n            trigger: 'form input[name=\"search\"]',\n            run: `edit ${productName}`,\n        },\n        {\n            content: `Search ${productName}`,\n            trigger: `form:has(input[name=\"search\"]) .oe_search_button`,\n            run: \"click\",\n            expectUnloadPage: true,\n        },\n    ];\n    if (select) {\n        steps.push({\n            content: `Select ${productName}`,\n            trigger: `.oe_product_cart:first a:text(${productName})`,\n            run: \"click\",\n            expectUnloadPage: true,\n        });\n    }\n    return steps;\n}\n\n/**\n * Used to select a pricelist on the /shop view\n */\nexport function selectPriceList(pricelist) {\n    return [\n        {\n            content: \"Click on pricelist dropdown\",\n            trigger: \"div.o_pricelist_dropdown a[data-bs-toggle=dropdown]\",\n            run: \"click\",\n        },\n        {\n            content: \"Click on pricelist\",\n            trigger: `span:contains(${pricelist})`,\n            run: \"click\",\n            expectUnloadPage: true,\n        },\n    ];\n}\n\n/**\n * Used for resolving indeterministic behavior of tours\n */\nexport function waitForInteractionToLoad() {\n    return {\n        content: \"Wait for interaction to be ready\",\n        trigger: `body[is-ready=true]`,\n    };\n}\n", "import { Component } from \"@odoo/owl\";\nimport { registry } from \"@web/core/registry\";\nimport { standardFieldProps } from \"@web/views/fields/standard_field_props\";\n\nexport class FieldVideoPreview extends Component {\n    static template = \"website_sale.FieldVideoPreview\";\n    static props = {...standardFieldProps};\n}\n\nexport const fieldVideoPreview = {\n    component: FieldVideoPreview,\n};\n\nregistry.category(\"fields\").add(\"video_preview\", fieldVideoPreview);\n", "import { _t } from \"@web/core/l10n/translation\";\nimport {\n    goBackToBlocks,\n    insertSnippet,\n    registerWebsitePreviewTour,\n} from '@website/js/tours/tour_utils';\n\nimport { markup } from \"@odoo/owl\";\n\nregisterWebsitePreviewTour(\"test_01_admin_shop_tour\", {\n    url: '/shop',\n},\n() => [\n{\n    trigger: \":iframe .js_sale\",\n},\n{\n    trigger: \".o_menu_systray .o_new_content_container > button\",\n    content: _t(\"Let's create your first product.\"),\n    tooltipPosition: \"bottom\",\n    run: \"click\",\n}, {\n    trigger: \"button[data-module-xml-id='base.module_website_sale']\",\n    content: markup(_t(\"Select <b>New Product</b> to create it and manage its properties to boost your sales.\")),\n    tooltipPosition: \"bottom\",\n    run: \"click\",\n}, {\n    trigger: \".modal-dialog input[type=text]\",\n    content: _t(\"Enter a name for your new product\"),\n    tooltipPosition: \"left\",\n    run: \"edit Test\",\n}, {\n    trigger: \".modal-footer button.btn-primary\",\n    content: markup(_t(\"Click on <em>Save</em> to create the product.\")),\n    tooltipPosition: \"right\",\n    run: \"click\",\n},\n{\n    trigger: \".o_builder_sidebar_open\",\n},\n{\n    trigger: \":iframe .product_price .oe_currency_value:visible\",\n    content: _t(\"Edit the price of this product by clicking on the amount.\"),\n    tooltipPosition: \"bottom\",\n    run: \"editor 1.99\",\n    timeout: 30000,\n},\n{\n    trigger: \":iframe .product_price .o_dirty .oe_currency_value:not(:text(1.00))\",\n},\n{\n    trigger: \":iframe #wrap img.product_detail_img\",\n    content: _t(\"Double click here to set an image describing your product.\"),\n    tooltipPosition: \"top\",\n    run: \"dblclick\",\n}, {\n    isActive: [\"auto\"],\n    trigger: \".o_select_media_dialog .o_upload_media_button\",\n    content: _t(\"Upload a file from your local library.\"),\n    tooltipPosition: \"bottom\",\n    run: \"click .modal-footer .btn-secondary\",\n},\ngoBackToBlocks(),\n{\n    trigger: \"body:not(.modal-open)\",\n},\n...insertSnippet({\n    id: \"s_text_image\",\n    name: \"Text - Image\",\n    groupName: \"Content\",\n}), {\n    // Wait until the drag and drop is resolved (causing a history step)\n    // before clicking save.\n    trigger: \".o-snippets-top-actions button.fa-undo:not([disabled])\",\n}, {\n    trigger: \"button[data-action=save]\",\n    content: markup(_t(\"Once you click on <b>Save</b>, your product is updated.\")),\n    tooltipPosition: \"bottom\",\n    run: \"click\",\n},\n{\n    trigger: \":iframe body:not(.editor_enable)\",\n},\n{\n    trigger: \".o_menu_systray_item.o_website_publish_container a\",\n    content: _t(\"Click on this button so your customers can see it.\"),\n    tooltipPosition: \"bottom\",\n    run: \"click\",\n}, {\n    trigger: \"button[data-menu-xmlid='website.menu_reporting']\",\n    content: _t(\"Click here to open the reporting menu\"),\n    tooltipPosition: \"bottom\",\n    run: \"click\",\n}, {\n    trigger: \"a[data-menu-xmlid='website.menu_website_dashboard'], a[data-menu-xmlid='website.menu_website_analytics']\",\n    content: _t(\"Let's now take a look at your eCommerce dashboard to get your eCommerce website ready in no time.\"),\n    tooltipPosition: \"bottom\",\n    // Just check during test mode. Otherwise, clicking it will result to random error on loading the Chart.js script.\n}]);\n", "import { ArticleTemplatePickerDialog } from \"@knowledge/components/article_template_picker_dialog/article_template_picker_dialog\";\nimport { ArticleAnnexePickerNoContentHelper } from \"@knowledge/components/article_annexe_picker_dialog/article_annexe_picker_no_content_helper\";\n\nexport class ArticleAnnexePickerDialog extends ArticleTemplatePickerDialog {\n    static components = {\n        ...ArticleTemplatePickerDialog.components,\n        NoContentHelper: ArticleAnnexePickerNoContentHelper,\n    };\n\n    /**\n     * @param {Object} template\n     * @returns {boolean}\n     */\n    canDeleteTemplate(template) {\n        return false;\n    }\n}\n", "import { Component } from \"@odoo/owl\";\n\nexport class ArticleAnnexePickerNoContentHelper extends Component {\n    static template = \"knowledge.ArticleAnnexePickerNoContentHelper\";\n    static props = {};\n}\n", "import { Dialog } from \"@web/core/dialog/dialog\";\nimport { useHotkey } from \"@web/core/hotkeys/hotkey_hook\";\nimport { closestScrollableY } from \"@web/core/utils/scrolling\";\nimport { highlightText } from \"@web/core/utils/html\";\nimport { debounce } from \"@web/core/utils/timing\";\nimport { Component, markup, onWillStart, useExternalListener, useRef, useState } from \"@odoo/owl\";\n\nexport class ArticleSearchDialog extends Component {\n    static template = \"knowledge.ArticleSearchDialog\";\n    static components = { Dialog };\n    static props = {\n        close: Function,\n        select: Function,\n        search: Function,\n        create: { type: Function, optional: true },\n        searchEmptyQuery: { type: Boolean, optional: true },\n    };\n\n    setup() {\n        this.state = useState({\n            articles: [],\n            searchValue: \"\",\n            selectedIdx: 0,\n            displayEmptySearch: false,\n        });\n        this.root = useRef(\"root\");\n        this.searchInput = useRef(\"searchInput\");\n        this.debouncedSearch = debounce(this.search, 500);\n        useExternalListener(window, \"pointerup\", this.onWindowPointerUp);\n        useHotkey(\"ArrowDown\", () => this.onArrowDown(), {\n            allowRepeat: true,\n            bypassEditableProtection: true,\n        });\n        useHotkey(\"ArrowUp\", () => this.onArrowUp(), {\n            allowRepeat: true,\n            bypassEditableProtection: true,\n        });\n        useHotkey(\"Enter\", () => this.openSelectedArticle(), { bypassEditableProtection: true });\n        useHotkey(\"escape\", () => this.props.close());\n        onWillStart(async () => {\n            if (this.props.searchEmptyQuery) {\n                await this.search(\"\");\n            }\n        });\n    }\n\n    onArrowUp() {\n        this.state.selectedIdx =\n            this.state.selectedIdx > 0\n                ? this.state.selectedIdx - 1\n                : this.state.articles.length - 1;\n    }\n\n    onArrowDown() {\n        this.state.selectedIdx =\n            this.state.selectedIdx < this.state.articles.length - 1\n                ? this.state.selectedIdx + 1\n                : 0;\n    }\n\n    onArticleClick(articleIdx) {\n        this.props.select(this.state.articles[articleIdx]);\n        this.props.close();\n    }\n\n    onArticleMouseEnter(articleIdx) {\n        this.state.selectedIdx = articleIdx;\n    }\n\n    onCreateClick() {\n        this.props.create(this.searchInput.el.value);\n        this.props.close();\n    }\n\n    async onSearchInput(ev) {\n        await this.debouncedSearch(ev.target.value);\n        this.state.displayEmptySearch = true;\n    }\n\n    onWindowPointerUp(ev) {\n        const container = closestScrollableY(this.root.el) ?? this.root.el;\n        if (!container.contains(ev.target)) {\n            this.props.close();\n        }\n    }\n\n    openSelectedArticle() {\n        if (this.state.articles.length > this.state.selectedIdx) {\n            this.props.select(this.state.articles[this.state.selectedIdx]);\n        }\n        this.props.close();\n    }\n\n    async search(searchValue) {\n        if (!searchValue.length && !this.props.searchEmptyQuery) {\n            this.state.articles = [];\n        } else {\n            this.state.articles = (await this.props.search(searchValue)).map((article) => ({\n                displayName: `${article.icon || \"\ud83d\udcc4\"} ${article.name}`.trim(),\n                headline: article.headline ? markup(article.headline) : \"\",\n                icon: article.icon || \"\ud83d\udcc4\",\n                id: article.id,\n                isFavorite: article.is_user_favorite,\n                text:\n                    article.name &&\n                    highlightText(searchValue, article.name, \"fw-bolder text-primary\"),\n                subjectText:\n                    article.root_article_id?.[0] &&\n                    article.root_article_id[0] != article.id &&\n                    highlightText(\n                        searchValue,\n                        article.root_article_id[1],\n                        \"fw-bolder text-primary\"\n                    ),\n            }));\n        }\n        this.state.selectedIdx = 0;\n    }\n}\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { Component, onMounted, useEffect, useRef, useState } from \"@odoo/owl\";\nimport { ConfirmationDialog } from \"@web/core/confirmation_dialog/confirmation_dialog\";\nimport { Dialog } from \"@web/core/dialog/dialog\";\nimport { groupBy } from \"@web/core/utils/arrays\";\nimport { Record } from \"@web/model/record\";\nimport { user } from \"@web/core/user\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { KnowledgeHtmlViewer } from \"@knowledge/components/knowledge_html_viewer/knowledge_html_viewer\";\nimport { ArticleTemplatePickerNoContentHelper } from \"@knowledge/components/article_template_picker_dialog/article_template_picker_no_content_helper\";\nimport { WithSubEnv } from \"@knowledge/components/with_sub_env/with_sub_env\";\nimport { READONLY_MAIN_EMBEDDINGS } from \"@html_editor/others/embedded_components/embedding_sets\";\nimport { KNOWLEDGE_READONLY_EMBEDDINGS } from \"@knowledge/editor/embedded_components/embedding_sets\";\n\n/**\n * This component will display an article template picker. The user will be able\n * to preview the article templates and select the one they want.\n */\nexport class ArticleTemplatePickerDialog extends Component {\n    static template = \"knowledge.ArticleTemplatePickerDialog\";\n    static components = {\n        Dialog,\n        Record,\n        KnowledgeHtmlViewer,\n        NoContentHelper: ArticleTemplatePickerNoContentHelper,\n        WithSubEnv\n    };\n    static props = {\n        articles: { type: Object },\n        templates: { type: Object },\n        templateRenderingContext: { type: Object, optional: true },\n        onLoadArticle: { type: Function },\n        onLoadTemplate: { type: Function },\n        onDeleteArticle: { type: Function },\n        onDeleteTemplate: { type: Function },\n        close: { type: Function },\n    };\n    /**\n     * @override\n     */\n    setup() {\n        super.setup();\n        this.size = \"fs\";\n        this.user = user;\n        this.orm = useService(\"orm\");\n        this.dialogService = useService(\"dialog\");\n        this.scrollView = useRef(\"scroll-view\");\n\n        const articlesListedInTemplateGallery = this.props.articles;\n        const templates = this.props.templates;\n\n        const templatesGroupedByCategory = Object.values(\n            groupBy(templates, template => template[\"template_category_id\"][0])\n        ).sort((a, b) => {\n            return a[0][\"template_category_sequence\"] - b[0][\"template_category_sequence\"];\n        }).map(group => group.sort((a, b) => {\n            return a[\"template_sequence\"] - b[\"template_sequence\"];\n        }));\n\n        this.state = useState({\n            articlesListedInTemplateGallery,\n            templatesGroupedByCategory,\n        });\n        this.selectFirstEntryFromSidebar();\n\n        onMounted(() => {\n            const { el } = this.scrollView;\n            if (el) {\n                el.scrollTop = 0;\n            }\n        });\n\n        useEffect(() => {\n            const { el } = this.scrollView;\n            if (el) {\n                el.style.visibility = \"visible\";\n            }\n        }, () => [this.state.resId]);\n    }\n\n    /**\n     * @param {integer} resId\n     * @param {function} onLoadRecord\n     */\n    loadRecord(resId, onLoadRecord) {\n        const { el } = this.scrollView;\n        if (el) {\n            el.scrollTop = 0;\n        }\n        if (resId !== this.state.resId) {\n            if (el) {\n                el.style.visibility = \"hidden\";\n            }\n            this.state.resId = resId;\n            onLoadRecord?.();\n        }\n    }\n\n    /**\n     * Selects the first entry from the templates gallery sidebar.\n     */\n    selectFirstEntryFromSidebar() {\n        if (this.state.articlesListedInTemplateGallery.length > 0) {\n            const articleId = this.state.articlesListedInTemplateGallery[0].id;\n            this.onSelectArticle(articleId);\n        } else if (this.state.templatesGroupedByCategory.length > 0) {\n            const templateId = this.state.templatesGroupedByCategory[0][0].id;\n            this.onSelectTemplate(templateId);\n        }\n    }\n\n    /**\n     * @param {Object} template\n     * @returns {boolean}\n     */\n    canDeleteTemplate(template) {\n        return user.isAdmin;\n    }\n\n    /**\n     * @param {integer} articleId\n     */\n    async onDeleteArticle(articleId) {\n        this.dialogService.add(ConfirmationDialog, {\n            title: _t(\"Remove Template from List\"),\n            body: _t(\"Are you sure you want to remove this template from the list?\\nIf needed, it can be added later from the Article.\"),\n            confirmLabel: _t(\"Remove Template\"),\n            confirm: async () => {\n                await this.props.onDeleteArticle(articleId);\n                this.state.articlesListedInTemplateGallery = this.state.articlesListedInTemplateGallery.filter(article => {\n                    return article.id !== articleId;\n                });\n                if (this.state.resId === articleId) {\n                    this.selectFirstEntryFromSidebar();\n                }\n            },\n            cancel: () => {},\n        });\n    }\n\n    /**\n     * @param {integer} templateId\n     */\n    async onDeleteTemplate(templateId) {\n        this.dialogService.add(ConfirmationDialog, {\n            title: _t(\"Delete Template\"),\n            body: _t(\"Are you sure you want to delete this template from the database?\"),\n            confirmLabel: _t(\"Remove Template\"),\n            confirm: async () => {\n                await this.props.onDeleteTemplate(templateId);\n                const groups = [];\n                for (const group of this.state.templatesGroupedByCategory) {\n                    const templates = group.filter(template => {\n                        return template.id !== templateId;\n                    });\n                    if (templates.length > 0) {\n                        groups.push(templates);\n                    }\n                }\n                this.state.templatesGroupedByCategory = groups;\n                if (this.state.resId === templateId) {\n                    this.selectFirstEntryFromSidebar();\n                }\n            },\n            cancel: () => {},\n        });\n    }\n\n    /**\n     * @param {integer} articleId\n     */\n    onSelectArticle(articleId) {\n        this.loadRecord(articleId, () => {\n            this.state.isRenderingTemplate = false;\n        });\n    }\n\n    /**\n     * @param {integer} templateId\n     */\n    onSelectTemplate(templateId) {\n        this.loadRecord(templateId, () => {\n            this.state.isRenderingTemplate = true;\n        });\n    }\n\n    async onLoadArticle() {\n        await this.props.onLoadArticle(this.state.resId);\n        this.props.close();\n    }\n\n    async onLoadTemplate() {\n        await this.props.onLoadTemplate(this.state.resId);\n        this.props.close();\n    }\n\n    /**\n     * @param {Record} record\n     * @returns {Object}\n     */\n    getHtmlViewerConfig(record) {\n        return {\n            config: {\n                value: record.data.template_preview || record.data.body,\n                embeddedComponents: [...READONLY_MAIN_EMBEDDINGS, ...KNOWLEDGE_READONLY_EMBEDDINGS],\n            },\n        };\n    }\n\n    /**\n     * Returns the fields required for the preview of the templates.\n     * @returns {Array[String]}\n     */\n    get templateFieldNames() {\n        return [\n            \"cover_image_url\",\n            \"icon\",\n            \"id\",\n            \"parent_id\",\n            \"template_name\",\n            \"template_preview\",\n            \"template_description\",\n        ];\n    }\n\n    /**\n     * Returns the fields required for the preview of the articles that are\n     * listed in the templates gallery.\n     * @returns {Array[String]}\n     */\n    get articleFieldNames() {\n        return [\n            \"body\",\n            \"cover_image_url\",\n            \"icon\",\n            \"id\",\n            \"name\",\n            \"parent_id\",\n        ];\n    }\n}\n", "import { Component } from \"@odoo/owl\";\n\nexport class ArticleTemplatePickerNoContentHelper extends Component {\n    static template = \"knowledge.ArticleTemplatePickerNoContentHelper\";\n    static props = {};\n}\n", "import { registry } from \"@web/core/registry\";\nimport { standardWidgetProps } from \"@web/views/widgets/standard_widget_props\";\n\nimport { useService } from \"@web/core/utils/hooks\";\nimport { Chatter } from \"@mail/chatter/web_portal/chatter\";\nimport { SIZES } from \"@web/core/ui/ui_service\";\n\nimport { Component, useState } from \"@odoo/owl\";\n\nexport class KnowledgeArticleChatter extends Component {\n    static template = \"knowledge.KnowledgeArticleChatter\";\n    static components = { Chatter };\n    static props = { ...standardWidgetProps };\n\n    setup() {\n        this.state = useState(this.env.chatterPanelState);\n        this.ui = useService(\"ui\");\n    }\n\n    get isChatterAside() {\n        return this.ui.size >= SIZES.LG;\n    }\n}\n\nexport const knowledgeChatterPanel = {\n    component: KnowledgeArticleChatter,\n    additionalClasses: [\"border-top\", \"col-12\", \"col-lg-4\", \"position-relative\", \"p-0\"],\n};\n\nregistry.category(\"view_widgets\").add(\"knowledge_chatter_panel\", knowledgeChatterPanel);\n", "import { CustomFavoriteItem } from \"@web/search/custom_favorite_item/custom_favorite_item\";\nimport { patch } from \"@web/core/utils/patch\";\n\npatch(CustomFavoriteItem.prototype, {\n    isKnowledgeEmbeddedView() {\n        return (\n            this.env.searchModel &&\n            this.env.searchModel.context &&\n            this.env.searchModel.context.knowledgeEmbeddedViewId\n        );\n    },\n\n    async saveFavorite(ev) {\n        return super.saveFavorite(ev, this.isKnowledgeEmbeddedView());\n    },\n});\n", "import { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\nimport { registry } from \"@web/core/registry\";\nimport { Component } from \"@odoo/owl\";\n\nconst cogMenuRegistry = registry.category(\"cogMenu\");\n\nexport class EmbeddedViewActionsMenu extends Component {\n    static props = {};\n    static template = \"knowledge.EmbeddedViewActionsMenu\";\n    static components = { Dropdown, DropdownItem };\n\n    _onOpenEmbeddedView () {\n        this.env.bus.trigger(`KNOWLEDGE_EMBEDDED_${this.env.searchModel.context.knowledgeEmbeddedViewId}:OPEN`);\n    }\n    _onEditEmbeddedView () {\n        this.env.bus.trigger(`KNOWLEDGE_EMBEDDED_${this.env.searchModel.context.knowledgeEmbeddedViewId}:EDIT`);\n    }\n}\n\ncogMenuRegistry.add(\n    'embedded-view-actions-menu',\n    {\n        Component: EmbeddedViewActionsMenu,\n        groupNumber: 10,\n        isDisplayed: (env) => {\n            /**\n             * Those buttons should only be displayed when inside the main Knowledge view.\n             * This means that the context should contain an embedded ID and a context key called\n             * `isOpenedEmbeddedView`. (Which is added when clicking on the open button)\n             */\n            return env.searchModel.context.knowledgeEmbeddedViewId &&\n                    !env.searchModel.context.isOpenedEmbeddedView;\n        },\n    },\n);\n", "import { useKnowledgeArticleSelector } from \"@knowledge/hooks/knowledge_article_selector\";\nimport { Component } from \"@odoo/owl\";\nimport { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\nimport { registry } from \"@web/core/registry\";\nimport { user } from \"@web/core/user\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { omit } from \"@web/core/utils/objects\";\nimport { renderToElement } from \"@web/core/utils/render\";\n\nconst cogMenuRegistry = registry.category(\"cogMenu\");\n\nconst supportedEmbeddedViews = new Set([\n    \"calendar\",\n    \"graph\",\n    \"hierarchy\",\n    \"kanban\",\n    \"list\",\n    \"pivot\",\n    \"cohort\",\n    \"gantt\",\n    \"map\",\n]);\n\nclass InsertEmbeddedViewMenu extends Component {\n    static props = {};\n    static template = \"knowledge.InsertEmbeddedViewMenu\";\n    static components = { Dropdown, DropdownItem };\n\n    setup() {\n        this.orm = useService(\"orm\");\n        this.actionService = useService(\"action\");\n        this.knowledgeCommandsService = useService(\"knowledgeCommandsService\");\n        this.openArticleSelector = useKnowledgeArticleSelector();\n    }\n\n    /**\n     * @returns {Object|null} Template props necessary to render an embedded\n     *                        view in Knowledge, or null if it is not possible\n     *                        to store this view as an embedded view.\n     */\n    extractCurrentViewEmbedTemplateProps() {\n        /**\n         * Note: If you change the embedded view props (i.e: context key, etc.),\n         * do not forget to update the related python tests.\n         */\n        const viewProps = {\n            context: this.getViewContext(),\n            displayName: this.env.config.getDisplayName(),\n            viewType: this.env.config.viewType,\n        };\n        const xmlId = this.actionService.currentController?.action?.xml_id;\n        if (xmlId) {\n            viewProps.actionXmlId = xmlId;\n            return { embeddedProps: { viewProps } };\n        }\n        /**\n         * Recover the original action (before the service pre-processing). The\n         * raw action is needed because it will be pre-processed again as a\n         * \"different\" action, after being stripped of its id, in Knowledge.\n         * If there is no original action, it means that the action is not\n         * serializable, therefore it cannot be stored in the body of an\n         * article.\n         */\n        const originalAction = this.actionService.currentController?.action?._originalAction;\n        if (originalAction) {\n            const action = JSON.parse(originalAction);\n            // remove action help as it won't be used\n            delete action.help;\n            viewProps.actWindow = action;\n            return { embeddedProps: { viewProps } };\n        }\n        return null;\n    }\n\n    /**\n     * Returns the full context that will be passed to the embedded view.\n     * @returns {Object}\n     */\n    getViewContext() {\n        const context = {};\n        if (this.env.searchModel) {\n            // Store the context of the search model:\n            Object.assign(\n                context,\n                omit(this.env.searchModel.context, ...Object.keys(user.context))\n            );\n            // Store the state of the search model:\n            Object.assign(context, {\n                knowledge_search_model_state: JSON.stringify(this.env.searchModel.exportState()),\n            });\n        }\n        // Store the \"local context\" of the view:\n        const fns = this.env.__getContext__.callbacks;\n        const localContext = Object.assign({}, ...fns.map((fn) => fn()));\n        const extraContext = {};\n        this.env.searchModel.trigger(\"insert-embedded-view\", extraContext);\n        Object.assign(context, localContext, extraContext);\n        return context;\n    }\n\n    /**\n     * Prepare a Embedded Component rendered in backend to be inserted in an\n     * article by the KnowledgeCommandsService.\n     * Allow to choose an article in a modal, redirect to that article and\n     * append the rendered template \"blueprint\" needed for the desired Embedded\n     * Component\n     * @param {string} template template name of the embedded blueprint to\n     *                 render.\n     */\n    insertCurrentViewInKnowledge(template) {\n        const config = this.env.config;\n        const templateProps = this.extractCurrentViewEmbedTemplateProps();\n        if (config.actionType !== \"ir.actions.act_window\" || !templateProps) {\n            throw new Error(\n                'This view can not be embedded in an article: the action is not an \"ir.actions.act_window\" or is not serializable.'\n            );\n        }\n        this.openArticleSelector(async (id) => {\n            this.knowledgeCommandsService.setPendingEmbeddedBlueprint({\n                embeddedBlueprint: renderToElement(template, {\n                    embeddedProps: JSON.stringify(templateProps.embeddedProps),\n                }),\n                model: \"knowledge.article\",\n                field: \"body\",\n                resId: id,\n            });\n            this.actionService.doAction(\"knowledge.ir_actions_server_knowledge_home_page\", {\n                additionalContext: {\n                    res_id: id,\n                },\n            });\n        });\n    }\n\n    onInsertEmbeddedViewInArticle() {\n        this.insertCurrentViewInKnowledge(\"knowledge.EmbeddedViewBlueprint\");\n    }\n\n    onInsertViewLinkInArticle() {\n        this.insertCurrentViewInKnowledge(\"knowledge.EmbeddedViewLinkBlueprint\");\n    }\n}\n\ncogMenuRegistry.add(\"insert-embedded-view-menu\", {\n    Component: InsertEmbeddedViewMenu,\n    groupNumber: 10,\n    isDisplayed: (env) => {\n        // only support act_window with an id for now, but act_window\n        // object could potentially be used too (rework backend API to insert\n        // views in articles)\n        return (\n            env.config.actionId &&\n            !env.searchModel.context.knowledgeEmbeddedViewId &&\n            supportedEmbeddedViews.has(env.config.viewType)\n        );\n    },\n});\n", "import { useBus } from \"@web/core/utils/hooks\";\nimport { patch } from \"@web/core/utils/patch\";\nimport { ListRenderer } from \"@web/views/list/list_renderer\";\n\npatch(ListRenderer.prototype, {\n    setup() {\n        super.setup();\n        if (this.env.searchModel) {\n            useBus(this.env.searchModel, \"insert-embedded-view\", (ev) => {\n                Object.assign(ev.detail, {\n                    orderBy: JSON.stringify(this.props.list.orderBy),\n                    keyOptionalFields: this.keyOptionalFields,\n                });\n            });\n        }\n    },\n\n    /**\n     * When the user hides/shows some columns from the list view, the system will\n     * add a new cache entry in the local storage of the user and will list all\n     * visible columns for the current view. To make the configuration specific to\n     * a view, the system generates a unique key for the cache entry by using all\n     * available information about the view.\n     *\n     * When loading the view, the system regenerates a key from the current view\n     * and check if there is any entry in the cache for that key. If there is a\n     * match, the system will load the configuration specified in the cache entry.\n     *\n     * For the embedded views of Knowledge, we want the configuration of the view\n     * to be unique for each embedded view. To achieve that, we will overwrite the\n     * function generating the key for the cache entry and include the unique id\n     * of the embedded view.\n     *\n     * @override\n     * @returns {string}\n     */\n    createViewKey() {\n        if (this.env.searchModel?.context.knowledgeEmbeddedViewId) {\n            return `${super.createViewKey()},${this.env.searchModel.context.knowledgeEmbeddedViewId}`;\n        }\n        return super.createViewKey();\n    },\n});\n", "import { FormStatusIndicator } from \"@web/views/form/form_status_indicator/form_status_indicator\";\n\n/**\n * This extension of the FormStatusIndicator is used to add a new indicator to the ones that already\n * exists. This new icon is used in the same way as the icon in Google Docs => indicate that all changes\n * have been committed to the DB.\n */\nexport class KnowledgeFormStatusIndicator extends FormStatusIndicator {\n    static template = 'knowledge.FormStatusIndicator';\n}\n", "import { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\nimport { KnowledgeFormStatusIndicator } from \"@knowledge/components/form_status_indicator/form_status_indicator\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { useRecordObserver } from \"@web/model/relational_model/utils\";\n\nimport KnowledgeIcon from \"@knowledge/components/knowledge_icon/knowledge_icon\";\n\nimport { Component, useState } from \"@odoo/owl\";\n\nexport default class KnowledgeHierarchy extends Component {\n    static components = {\n        Dropdown,\n        DropdownItem,\n        KnowledgeIcon,\n        KnowledgeFormStatusIndicator,\n    };\n    static props = { record: Object };\n    static template = \"knowledge.KnowledgeHierarchy\";\n\n    setup() {\n        super.setup();\n        this.orm = useService(\"orm\");\n        this.state = useState({\n            articleName: this.props.record.data.name,\n            isLoadingArticleHierarchy: false,\n        });\n        useRecordObserver((record) => {\n            if (this.state.articleName !== record.data.name) {\n                this.state.articleName = record.data.name;\n            }\n        });\n    }\n\n    /**\n     * Whether to display the dropdown toggle used to get the articles that are between the root\n     * and the parent article. It is only shown if there are any articles to show (parent_path is\n     * of the form \"1/2/3/4/\", hence length > 4 as condition)\n     */\n    get displayDropdownToggle() {\n        return this.props.record.data.parent_path.split(\"/\").length > 4;\n    }\n\n    get isReadonly() {\n        return this.props.record.data.is_locked || !this.props.record.data.user_can_write;\n    }\n\n    get parentId() {\n        return this.props.record.data.parent_id?.id;\n    }\n\n    get parentName() {\n        return this.props.record.data.parent_id?.display_name;\n    }\n\n    get rootId() {\n        return this.props.record.data.root_article_id.id;\n    }\n\n    get rootName() {\n        return this.props.record.data.root_article_id.display_name;\n    }\n\n    /**\n     * Load the articles that should be shown in the dropdown\n     */\n    async loadHierarchy() {\n        this.articleHierarchy = await this.orm.call(\n            \"knowledge.article\",\n            \"get_article_hierarchy\",\n            [this.props.record.resId],\n            { exclude_article_ids: [this.rootId, this.parentId, this.props.record.resId] },\n        );\n        this.state.isLoadingArticleHierarchy = false;\n    }\n\n    /**\n     * If needed, will show the loading indicator in the dropdown and start the loading\n     * of the articles to show in it\n     */\n    async onBeforeOpen() {\n        this.state.isLoadingArticleHierarchy = true;\n        this.loadHierarchy();\n    }\n}\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { Dialog } from \"@web/core/dialog/dialog\";\nimport { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\nimport { SCALE_LABELS } from \"@web/views/calendar/calendar_controller\";\nimport { SelectMenu } from \"@web/core/select_menu/select_menu\";\nimport { useAutofocus, useService } from \"@web/core/utils/hooks\";\nimport { uuid } from \"@web/core/utils/strings\";\n\nimport { Component, onWillStart, useExternalListener, useState } from \"@odoo/owl\";\n\n/**\n * Dialog allowing to dynamically edit the item calendar view configuration\n * (the \"itemCalendarProps\"). Used when clicking on the \"edit\" button of the\n * embedded view manager.\n */\nexport class ItemCalendarPropsDialog extends Component {\n    static template = \"knowledge.ItemCalendarPropsDialog\";\n    static components = {\n        DropdownItem,\n        Dialog,\n        SelectMenu,\n    };\n    static props = {\n        knowledgeArticleId: { type: Number, optional: true },\n        close: { type: Function, optional: true },\n        colorPropertyId: { type: String, optional: true },\n        dateStartPropertyId: { type: String, optional: true },\n        dateStopPropertyId: { type: String, optional: true },\n        dateType: { type: String, optional: true },\n        isNew: { type: Boolean, optional: true },\n        name: { type: String, optional: true },\n        saveItemCalendarProps: { type: Function },\n        scale: { type: String, optional: true },\n        showWeekEnds: { type: Boolean, optional: true },\n        slotMaxTime: { type: String, optional: true },\n        slotMinTime: { type: String, optional: true },\n    };\n\n    static defaultProps = {\n        showWeekEnds: true,\n        slotMinTime: \"00:00\",\n        slotMaxTime: \"24:00\",\n    };\n\n    setup() {\n        super.setup();\n        useAutofocus();\n        this.orm = useService(\"orm\");\n        this.notification = useService(\"notification\");\n        this.state = useState({\n            name: this.props.name,\n            scale: this.props.scale || \"week\",\n            showWeekEnds: this.props.showWeekEnds,\n            slotMaxTime: this.props.slotMaxTime,\n            slotMinTime: this.props.slotMinTime,\n        });\n        this.colorChoices = [];\n        this.dateChoices = [];\n        this.dateTimeChoices = [];\n        this.propertyFieldEntries = {};\n        this.scalesChoices = Object.entries(SCALE_LABELS).map(([value, label]) => ({ value, label }));\n\n        onWillStart(async () => {\n            // Fetch the properties definitions and create choices to use in\n            // the SelectMenu components\n            const propertiesDefinitions = await this.orm.read(\n                \"knowledge.article\",\n                [this.props.knowledgeArticleId],\n                [\"article_properties_definition\"]\n            );\n            this.propertiesDefinitions = propertiesDefinitions[0].article_properties_definition;\n            for (const definition of this.propertiesDefinitions) {\n                this.propertyFieldEntries[definition.name] = {\n                    label: definition.string,\n                    type: definition.type,\n                };\n                // Add choice in corresponding dropdown\n                const choice = {\n                    value: definition.name,\n                    label: definition.string,\n                };\n                if (definition.type === \"datetime\") {\n                    this.dateTimeChoices.push(choice);\n                } else if (definition.type === \"date\") {\n                    this.dateChoices.push(choice);\n                } else if ([\"boolean\", \"many2one\", \"selection\"].includes(definition.type)) {\n                    this.colorChoices.push(choice);\n                }\n            }\n\n            if (this.props.isNew) {\n                // If no date(time) properties exists, create default ones\n                if (!this.dateTimeChoices.length && !this.dateChoices.length) {\n                    this.autoCreateDateProperties = true;\n                    this.createProperty(_t(\"Start Date Time\"), \"datetime\", \"dateStart\");\n                    this.createProperty(_t(\"End Date Time\"), \"datetime\", \"dateStop\");\n                } else {\n                    // If some exist, select them by default (prefer to use 2\n                    // of the same type if possible, and prefer datetimes over\n                    // dates)\n                    if (this.dateTimeChoices.length > 1) {\n                        this.selectDateStart(this.dateTimeChoices[0].value);\n                        this.selectDateStop(this.dateTimeChoices[1].value);\n                    } else if (this.dateChoices.length > 1) {\n                        this.selectDateStart(this.dateChoices[0].value);\n                        this.selectDateStop(this.dateChoices[1].value);\n                    } else if (this.dateTimeChoices.length !== 0) {\n                        this.selectDateStart(this.dateTimeChoices[0].value);\n                    } else {\n                        this.selectDateStart(this.dateChoices[0].value);\n                    }\n\n                }\n            } else {\n                // Use props only if the related property still exists\n                for (const propName of [\"dateStartPropertyId\", \"dateStopPropertyId\", \"colorPropertyId\"]) {\n                    if (this.propertyFieldEntries[this.props[propName]]) {\n                        this.state[propName] = this.props[propName];\n                    }\n                }\n            }\n        });\n\n        // Save when pressing on enter\n        useExternalListener(window, \"keydown\", (event) => {\n            if (event.key === \"Enter\") {\n                this.save();\n            }\n        });\n    }\n\n    /**\n     * Return the available start properties formatted as groups (date and\n     * datetime) for the SelectMenu component\n     */\n    get availableDateStartProperties() {\n        return [{\n            label: _t(\"Date and Time Properties\"),\n            choices: this.dateTimeChoices\n        }, {\n            label: _t(\"Date Properties\"),\n            choices: this.dateChoices,\n        }];\n    }\n\n    /**\n     * Return the available stop properties (that are of the same type as the\n     * current start date) formatted as a group for the SelectMenu component\n     */\n    get availableDateStopProperties() {\n        // Don't show current start date nor dates with other type\n        if (this.dateStartProperty?.type === \"datetime\") {\n            return [{\n                label: _t(\"Date and Time Properties\"),\n                choices: this.dateTimeChoices.filter(choice => choice.value !== this.state.dateStartPropertyId),\n            }];\n        }\n        return [{\n            label: _t(\"Date Properties\"),\n            choices: this.dateChoices.filter(choice => choice.value !== this.state.dateStartPropertyId),\n        }];\n    }\n\n    get colorProperty() {\n        return this.propertyFieldEntries[this.state.colorPropertyId];\n    }\n\n    get dateStartProperty() {\n        return this.propertyFieldEntries[this.state.dateStartPropertyId];\n    }\n\n    get dateStopProperty() {\n        return this.propertyFieldEntries[this.state.dateStopPropertyId];\n    }\n\n    /**\n     * Create a new date or datetime property and use it as start or stop\n     * choice.\n     * Note: only the new properties that are selected when saving will be\n     * stored.\n     * @param {string} label: label of the property\n     * @param {string} type: type of the property (date or datetime)\n     * @param {string} calendarProp: for which calendar prop the property has\n     * been created\n     */\n    createProperty(label, type, calendarProp) {\n        const newPropertyId = uuid();\n        // Add to list of properties\n        this.propertyFieldEntries[newPropertyId] = {\n            isNew: true,\n            label: label,\n            type: type,\n        };\n        // Add new choice in dropdowns\n        const newChoice = {\n            value: newPropertyId,\n            label,\n        };\n        if (type === \"date\") {\n            this.dateChoices.push(newChoice);\n        } else if (type === \"datetime\") {\n            this.dateTimeChoices.push(newChoice);\n        } else {\n            this.colorChoices.push(newChoice);\n        }\n        // Select the new choice\n        if (calendarProp === \"dateStart\") {\n            this.selectDateStart(newPropertyId);\n        } else if (calendarProp === \"dateStop\") {\n            this.selectDateStop(newPropertyId);\n        } else if (calendarProp === \"color\") {\n            this.selectColor(newPropertyId);\n        }\n    }\n\n    /**\n     * Save the item calendar props and close the dialog. If there is a new\n     * choice selected as start and/or stop date, create the associated\n     * property(ies) first.\n     */\n    async save() {\n        if (!this.state.dateStartPropertyId) {\n            this.notification.add(_t(\"The start date property is required.\"), {type: \"danger\"});\n            return;\n        }\n        // Create new property if needed\n        if (this.dateStartProperty.isNew || this.dateStopProperty?.isNew || this.colorProperty?.isNew) {\n            // Keep existing properties to not lose them.\n            const propertiesDefinitions = [...this.propertiesDefinitions];\n            if (this.dateStartProperty.isNew) {\n                propertiesDefinitions.push({\n                    name: this.state.dateStartPropertyId,\n                    string: this.dateStartProperty.label,\n                    type: this.dateStartProperty.type,\n                });\n            }\n            if (this.dateStopProperty?.isNew) {\n                propertiesDefinitions.push({\n                    name: this.state.dateStopPropertyId,\n                    string: this.dateStopProperty.label,\n                    type: this.dateStopProperty.type,\n                });\n            }\n            if (this.colorProperty?.isNew) {\n                propertiesDefinitions.push({\n                    name: this.state.colorPropertyId,\n                    string: this.colorProperty.label,\n                    type: this.colorProperty.type,\n                });\n            }\n            try {\n                await this.orm.write(\"knowledge.article\", [this.props.knowledgeArticleId], {article_properties_definition: propertiesDefinitions});\n            } catch (e) {\n                this.notification.add(_t(\"New property could not be created.\"), {type: \"danger\"});\n                console.error(e);\n                return;\n            }\n        }\n        this.props.saveItemCalendarProps(this.state.name, {\n            dateStartPropertyId: this.state.dateStartPropertyId,\n            dateStopPropertyId: this.state.dateStopPropertyId || undefined,\n            colorPropertyId: this.state.colorPropertyId || undefined,\n            scale: this.state.scale,\n            dateType: this.dateStartProperty.type,\n            showWeekEnds: this.state.showWeekEnds,\n            slotMinTime: this.state.slotMinTime || \"00:00\",\n            slotMaxTime: this.state.slotMaxTime || \"24:00\",\n        });\n        this.props.close();\n    }\n\n    selectColor(value) {\n        this.state.colorPropertyId = value;\n    }\n\n    selectDateStop(value) {\n        this.state.dateStopPropertyId = value;\n    }\n\n    selectScale(value) {\n        this.state.scale = value;\n    }\n\n    selectDateStart(value) {\n        this.state.dateStartPropertyId = value;\n        // DateStop must be of the same type as start, but must not be the same property\n        if (this.state.dateStopPropertyId && (this.dateStartProperty.type !== this.dateStopProperty.type || this.state.dateStopPropertyId === this.state.dateStartPropertyId)) {\n            this.state.dateStopPropertyId = false;\n        }\n    }\n}\n", "import { registry } from \"@web/core/registry\";\nimport { standardWidgetProps } from \"@web/views/widgets/standard_widget_props\";\nimport { user } from \"@web/core/user\";\nimport { throttleForAnimation } from \"@web/core/utils/timing\";\nimport { useRecordObserver } from \"@web/model/relational_model/utils\";\nimport { Component, onWillStart, useRef, useState } from \"@odoo/owl\";\n\nclass KnowledgeCover extends Component {\n    static props = standardWidgetProps;\n    static template = \"knowledge.KnowledgeCover\";\n\n    setup() {\n        super.setup();\n        this.root = useRef(\"root\");\n        this.image = useRef(\"image\");\n        this.state = useState({\n            repositioning: false,\n            grabbing: false,\n            // cover_image_position is false if the cover has never been repositioned before\n            verticalPosition: this.props.record.data.cover_image_position || 50,\n        });\n\n        onWillStart(async () => {\n            this.isInternalUser = await user.hasGroup('base.group_user');\n        });\n\n        // Update the state when we open an article (because since we open an\n        // article by loading the related record, the state of this component\n        // is shared between the articles and is not recomputed).\n        let previousArticleId = this.props.record.resId;\n        useRecordObserver((record) => {\n            if (!previousArticleId || previousArticleId !== record.resId) {\n                previousArticleId = record.resId;\n                this.state.repositioning = false;\n                this.state.grabbing = false;\n                this.state.verticalPosition = record.data.cover_image_position || 50;\n            }\n        });\n    }\n\n    /**\n     * @returns {boolean} - True if the article is editable\n     */\n    get isEditable() {\n        const recordData = this.props.record.data;\n        return !recordData.is_locked\n            && recordData.user_can_write\n            && recordData.active\n            && this.isInternalUser;\n    }\n\n    changeCover() {\n        this.env.ensureArticleName();\n        this.env.openCoverSelector();\n    }\n\n    removeCover() {\n        this.props.record.update({cover_image_id: false});\n    }\n\n    /**\n     * Make the cover draggable so that the user can reposition it.\n     */\n    repositionCover() {\n        this.state.repositioning = true;\n        const cover = this.image.el;\n        let prevPos;\n\n        const moveCover = throttleForAnimation(ev => {\n            if (prevPos !== ev.y) {\n                // Add offset proportional to the distance traveled by the cursor.\n                // (dividing by 5 to not move the cover too fast).\n                const verticalPosition = this.state.verticalPosition + (prevPos - ev.y) / 5;\n                // Make sure we are between 0.01 and 100% (not 0 since it's the\n                // default value when the cover has never been repositioned).\n                this.state.verticalPosition = Math.max(0.01, Math.min(100, verticalPosition));\n                prevPos = ev.y;\n            }\n        });\n\n        const onPointerUp = () => {\n            this.state.grabbing = false;\n            document.removeEventListener('pointermove', moveCover);\n        };\n\n        const onPointerDown = (ev) => {\n            ev.preventDefault();\n            ev.stopPropagation();\n            prevPos = ev.y;\n            this.state.grabbing = true;\n            document.addEventListener('pointermove', moveCover);\n            document.addEventListener('pointerup', onPointerUp, {once: true});\n        };\n\n        cover.addEventListener('pointerdown', onPointerDown);\n\n        // Click outside the cover (or on save button) leaves the repositioning mode\n        document.addEventListener('pointerdown', (ev) => {\n            cover.removeEventListener('pointerdown', onPointerDown);\n            this.state.repositioning = false;\n            if (ev.target.classList.contains('o_knowledge_undo_cover_move')) {\n                // Undo move\n                this.state.verticalPosition = this.props.record.data.cover_image_position || 50;\n            } else {\n                // Save new position\n                this.props.record.update({cover_image_position: this.state.verticalPosition});\n            }\n        }, {once: true});\n    }\n\n}\n\nexport const knowledgeCover = {\n    component: KnowledgeCover,\n};\nregistry.category(\"view_widgets\").add(\"knowledge_cover\", knowledgeCover);\n", "import { _t } from \"@web/core/l10n/translation\";\nimport {\n    AutoResizeImage,\n    ImageSelector,\n} from \"@html_editor/main/media/media_dialog/image_selector\";\nimport { ConfirmationDialog } from '@web/core/confirmation_dialog/confirmation_dialog';\nimport { Dialog } from '@web/core/dialog/dialog';\nimport { UnsplashError } from '@web_unsplash/unsplash_error/unsplash_error';\nimport { useService, useChildRef } from \"@web/core/utils/hooks\";\nimport { Component } from \"@odoo/owl\";\n\nexport class AutoResizeCover extends AutoResizeImage {\n    setup() {\n        super.setup();\n        this.orm = useService(\"orm\");\n    }\n\n    /**\n     * @override\n     * Open Dialog to delete the cover record.\n     */\n    remove() {\n        this.dialogs.add(ConfirmationDialog, {\n            body: _t(\"Are you sure you want to delete this cover? It will be removed from every article it is used in.\"),\n            confirm: async () => {\n                const res = await this.orm.unlink(this.props.model,\n                    [this.props.resId],\n                );\n                if (res) {\n                    this.props.onRemoved(this.props.id, this.props.resId);\n                }\n            },\n        });\n    }\n}\n\nexport class KnowledgeCoverSelector extends ImageSelector {\n    static defaultProps = {\n        resModel: \"knowledge.cover\",\n        orientation: \"landscape\",\n    };\n    static attachmentsListTemplate = \"knowledge.CoversListTemplate\";\n    static components = {\n        ...ImageSelector.components,\n        AutoResizeCover,\n        UnsplashError,\n    };\n    setup() {\n        super.setup();\n        // Search for images matching the article name when opening the dialog.\n        this.unsplashService = useService(\"unsplash\");\n        this.state.needle = this.props.searchTerm;\n        this.searchUnsplash(this.state.needle);\n    }\n\n    /**\n     * @override\n     * Domain used to fetch the attachments to display in the coverSelector.\n     */\n    get attachmentsDomain() {\n        return ['&', ['res_model', '=', this.props.resModel], ['name', 'ilike', this.state.needle]];\n    }\n\n    /**\n     * @override\n     * Update the article's cover using the id of the cover associated to the\n     * attachment clicked.\n     */\n    onClickAttachment(attachment) {\n        this.props.save(attachment.res_id);\n    }\n\n    /**\n     * @override\n     * Upload the unsplash image clicked.\n     */\n    onClickRecord(unsplashRecord) {\n        this.unsplashService.uploadUnsplashRecords(\n            [unsplashRecord],\n            { resModel: this.props.resModel },\n            async (attachments) => this.onUploaded(attachments[0])\n        );\n    }\n\n    /**\n     * @override\n     * Remove cover from the opened CoverSelector, and update article's cover\n     * if the cover was used in the current article.\n     */\n    onRemoved(attachmentId, coverId) {\n        super.onRemoved(attachmentId);\n        if (coverId === this.props.articleCoverId) {\n            this.props.save(false);\n        }\n    }\n\n    /**\n     * @override\n     * Associate the created attachment to a new cover record, update the\n     * attachment's resId, and update the article's cover.\n     */\n    async onUploaded(attachment) {\n        const [coverId] = await this.orm.create(this.props.resModel, [{attachment_id: attachment.id}]);\n        this.props.save(coverId);\n    }\n\n    /**\n     * @override\n     * Overridden to not throw an error.\n     * Designed to be used with several tabs in the dialog (not the case here)\n     * and used for rendering purposes to show selected images when multiImage\n     * is allowed (not the case either).\n     */\n    get selectedAttachmentIds() {\n        return [];\n    }\n\n    /**\n     * @override\n     * Overridden to not throw an error.\n     * Designed to be used with several tabs in the dialog (not the case here)\n     * and used for rendering purposes to show selected images when multiImage\n     * is allowed (not the case either).\n     */\n    get selectedRecordIds() {\n        return [];\n    }\n}\n\nexport class KnowledgeCoverDialog extends Component {\n    static template = 'knowledge.KnowledgeCoverDialog';\n    static components = {\n        KnowledgeCoverSelector,\n        Dialog\n    };\n    static props = {\n        articleCoverId: {type: Number, optional: true},\n        articleName: String,\n        save: Function,\n        close: Function,\n    };\n\n    setup() {\n        this.size = 'xl';\n        this.contentClass = 'o_select_media_dialog h-100';\n        this.modalRef = useChildRef();\n    }\n\n    /**\n     * Update the article's cover.\n     */\n    save(coverId) {\n        this.props.save(coverId);\n        this.props.close();\n    }\n}\n", "import { patch } from \"@web/core/utils/patch\";\nimport { Dropdown } from \"@web/core/dropdown/dropdown\";\n\npatch(Dropdown.prototype, {\n    /**\n     * Embedded views in knowledge can become the active element,\n     * but dropdowns should be able to be closed by clicking.\n     * This override is necessary because dropdowns mounted inside an\n     * embedded view register the document as their active element instead\n     * of the embedded view itself.\n     *\n     * @override\n     */\n    popoverCloseOnClickAway(target, activeEl) {\n        const currentActiveEl = this.uiService.getActiveElementOf(target);\n        return (\n            super.popoverCloseOnClickAway(target, activeEl) ||\n            (currentActiveEl !== activeEl &&\n                activeEl &&\n                activeEl.contains(currentActiveEl) &&\n                currentActiveEl.dataset.embedded)\n        );\n    },\n});\n", "import { HtmlField, htmlField } from \"@html_editor/fields/html_field\";\nimport { withSequence } from \"@html_editor/utils/resource\";\nimport {\n    KNOWLEDGE_EMBEDDINGS,\n    KNOWLEDGE_READONLY_EMBEDDINGS,\n} from \"@knowledge/editor/embedded_components/embedding_sets\";\nimport {\n    KNOWLEDGE_EMBEDDED_COMPONENT_PLUGINS,\n    KNOWLEDGE_PLUGINS,\n} from \"@knowledge/editor/plugin_sets\";\nimport { registry } from \"@web/core/registry\";\nimport { useState, useSubEnv } from \"@odoo/owl\";\nimport { KnowledgeHtmlViewer } from \"../knowledge_html_viewer/knowledge_html_viewer\";\nimport { KnowledgeWysiwyg } from \"../knowledge_wysiwyg/knowledge_wysiwyg\";\nimport { CallbackRecorder } from \"@web/search/action_hook\";\nimport { useRecordObserver } from \"@web/model/relational_model/utils\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { _t } from \"@web/core/l10n/translation\";\n\nexport class KnowledgeHtmlField extends HtmlField {\n    static components = {\n        ...HtmlField.components,\n        Wysiwyg: KnowledgeWysiwyg,\n        HtmlViewer: KnowledgeHtmlViewer,\n    };\n    setup() {\n        super.setup();\n        useSubEnv({\n            __onLayoutGeometryChange__: new CallbackRecorder(),\n        });\n        this.commentsService = useService(\"knowledge.comments\");\n        this.commentsState = useState(this.commentsService.getCommentsState());\n        useRecordObserver((record) => {\n            if (record.resId !== this.commentsState.articleId) {\n                this.commentsService.setArticleId(record.resId);\n                this.commentsService.loadRecords(record.resId, {\n                    ignoreBatch: true,\n                    includeLoaded: true,\n                });\n            }\n        });\n    }\n\n    getConfig() {\n        const config = super.getConfig();\n        // TODO @engagement: fill this array with knowledge components\n        if (this.props.embeddedComponents) {\n            config.resources.embedded_components = [\n                ...(config.resources.embedded_components || []),\n                ...KNOWLEDGE_EMBEDDINGS,\n            ];\n            config.Plugins.push(...KNOWLEDGE_EMBEDDED_COMPONENT_PLUGINS);\n        }\n        config.resources.hints = [\n            ...(config.resources.hints || []),\n            withSequence(20, {\n                selector: \".odoo-editor-editable > h1:only-child\",\n                text: _t(\"Start with a title...\"),\n            }),\n        ];\n        config.Plugins.push(...KNOWLEDGE_PLUGINS);\n        config.onLayoutGeometryChange = () => this.onLayoutGeometryChange();\n        return config;\n    }\n\n    getReadonlyConfig() {\n        const config = super.getReadonlyConfig();\n        if (this.props.embeddedComponents) {\n            config.embeddedComponents = [\n                ...(config.embeddedComponents || []),\n                ...KNOWLEDGE_READONLY_EMBEDDINGS,\n            ];\n        }\n        config.onLayoutGeometryChange = () => this.onLayoutGeometryChange();\n        return config;\n    }\n\n    onLayoutGeometryChange() {\n        for (const cb of this.env.__onLayoutGeometryChange__.callbacks) {\n            cb();\n        }\n    }\n}\n\nexport const knowledgeHtmlField = {\n    ...htmlField,\n    component: KnowledgeHtmlField,\n};\n\nregistry.category(\"fields\").add(\"knowledge_html\", knowledgeHtmlField);\n", "import { HtmlViewer } from \"@html_editor/components/html_viewer/html_viewer\";\nimport { LocalOverlayContainer } from \"@html_editor/local_overlay_container\";\nimport { usePositionHook } from \"@html_editor/position_hook\";\nimport { onWillDestroy, useEffect, useExternalListener, useState, useSubEnv } from \"@odoo/owl\";\nimport { registry } from \"@web/core/registry\";\nimport { KnowledgeCommentsHandler } from \"@knowledge/comments/comments_handler/comments_handler\";\nimport { CommentBeaconManager } from \"@knowledge/comments/comment_beacon_manager\";\nimport { useChildRef, useService } from \"@web/core/utils/hooks\";\nimport { uniqueId } from \"@web/core/utils/functions\";\nimport { effect } from \"@web/core/utils/reactive\";\nimport { batched } from \"@web/core/utils/timing\";\n\nexport class KnowledgeHtmlViewer extends HtmlViewer {\n    static template = \"knowledge.KnowledgeHtmlViewer\";\n    static components = {\n        ...HtmlViewer.components,\n        LocalOverlayContainer,\n    };\n\n    setup() {\n        super.setup();\n        this.commentsService = useService(\"knowledge.comments\");\n        this.commentsState = useState(this.commentsService.getCommentsState());\n        let editorThreadsKeys;\n        this.alive = true;\n        useExternalListener(window, \"click\", this.onWindowClick);\n        effect(\n            batched((state) => {\n                if (!this.alive) {\n                    return;\n                }\n                const editorThreads = state.editorThreads;\n                const keys = Object.keys(editorThreads).toString();\n                if (keys !== editorThreadsKeys) {\n                    this.commentBeaconManager?.sortThreads();\n                    this.commentBeaconManager?.drawThreadOverlays();\n                    editorThreadsKeys = keys;\n                }\n            }),\n            [this.commentsState]\n        );\n        onWillDestroy(() => {\n            this.alive = false;\n        });\n        this.overlayRef = useChildRef();\n        useSubEnv({\n            localOverlayContainerKey: uniqueId(\"html_viewer\"),\n        });\n        usePositionHook(this.readonlyElementRef, document, () => {\n            this.commentBeaconManager?.drawThreadOverlays();\n            this.props.config.onLayoutGeometryChange?.();\n        });\n        this.overlayComponentsKey = uniqueId(\"KnowledgeCommentsHandler\");\n        useEffect(\n            // TODO ABD: investigate if we need useRecordObserver if user_permission changes ?\n            // maybe patch occurs too late when user_permission change ?\n            // is it even necessary to check for permission = \"none\" ?\n            () => {\n                let overlayContainer;\n                if (\n                    this.readonlyElementRef.el &&\n                    this.overlayRef.el &&\n                    this.env.model.root.data.user_permission &&\n                    this.env.model.root.data.user_permission !== \"none\"\n                ) {\n                    overlayContainer = this.makeLocalOverlay();\n                    this.overlayRef.el.append(overlayContainer);\n                    // reactive on this object if necessary\n                    this.commentBeaconManager = new CommentBeaconManager({\n                        document,\n                        source: this.readonlyElementRef.el,\n                        overlayContainer: overlayContainer,\n                        commentsState: this.commentsState,\n                        readonly: true,\n                    });\n                    this.commentBeaconManager.sortThreads();\n                    this.commentBeaconManager.drawThreadOverlays();\n                    registry\n                        .category(this.env.localOverlayContainerKey)\n                        .add(this.overlayComponentsKey, {\n                            Component: KnowledgeCommentsHandler,\n                            props: {\n                                commentBeaconManager: this.commentBeaconManager,\n                                contentRef: this.readonlyElementRef,\n                            },\n                        });\n                }\n                return () => {\n                    overlayContainer?.remove();\n                    this.commentBeaconManager?.destroy();\n                    this.commentBeaconManager = undefined;\n                    registry\n                        .category(this.env.localOverlayContainerKey)\n                        .remove(this.overlayComponentsKey);\n                    this.overlayComponentsKey = uniqueId(\"KnowledgeCommentsHandler\");\n                };\n            },\n            () => [this.readonlyElementRef.el, this.overlayRef.el, this.state.value]\n        );\n    }\n\n    onWindowClick(ev) {\n        const selector = \".oe-local-overlay, .o_knowledge_comment_box\";\n        const closestElement = ev.target.closest(selector);\n        if (!closestElement) {\n            this.commentsState.activeThreadId = undefined;\n        }\n    }\n\n    makeLocalOverlay() {\n        const overlayContainer = document.createElement(\"div\");\n        overlayContainer.className = `oe-local-overlay`;\n        overlayContainer.setAttribute(\"data-oe-local-overlay-id\", \"KnowledgeThreadBeacons\");\n        return overlayContainer;\n    }\n}\n", "import { useEmojiPicker } from \"@web/core/emoji_picker/emoji_picker\";\nimport { registry } from \"@web/core/registry\";\nimport { exprToBoolean } from \"@web/core/utils/strings\";\nimport { standardFieldProps } from \"@web/views/fields/standard_field_props\";\n\nimport { getRandomIcon } from \"@knowledge/js/knowledge_utils\";\n\nimport { Component, useRef } from \"@odoo/owl\";\n\nexport default class KnowledgeIcon extends Component {\n    static template = \"knowledge.KnowledgeIcon\";\n    static props = {\n        record: Object,\n        readonly: Boolean,\n        iconClasses: {type: String, optional: true},\n        allowRandomIconSelection: {type: Boolean, optional: true},\n        autoSave: {type: Boolean, optional: true},\n        fallbackDefaultIcon: { type: Boolean, optional: true },\n    };\n\n    setup() {\n        super.setup();\n        this.iconRef = useRef(\"icon\");\n        this.emojiPicker = useEmojiPicker(this.iconRef, { hasRemoveFeature: true, onSelect: this.updateIcon.bind(this) });\n    }\n\n    get icon() {\n        return this.props.record.data.icon || (this.props.fallbackDefaultIcon && \"\ud83d\udcc4\");\n    }\n\n    async selectRandomIcon() {\n        this.updateIcon(await getRandomIcon());\n    }\n\n    updateIcon(icon) {\n        this.props.record.update({icon});\n        if (this.props.autoSave) {\n            this.props.record.save();\n        }\n    }\n}\n\nclass KnowledgeIconField extends KnowledgeIcon {\n    static props = {\n        ...standardFieldProps,\n        allowRandomIconSelection: Boolean,\n        autoSave: Boolean,\n    };\n}\n\nregistry.category(\"fields\").add(\"knowledge_icon\", {\n    component: KnowledgeIconField,\n    extractProps({ attrs, viewType }, dynamicInfo) {\n        return {\n            autoSave: viewType === \"kanban\",\n            readonly: dynamicInfo.readonly,\n            allowRandomIconSelection: exprToBoolean(attrs.allow_random_icon_selection),\n        };\n    },\n});\n", "import { Wysiwyg } from \"@html_editor/wysiwyg\";\nimport { isEmptyBlock } from \"@html_editor/utils/dom_info\";\nimport { WysiwygArticleHelper } from \"@knowledge/components/wysiwyg_article_helper/wysiwyg_article_helper\";\nimport { useState } from \"@odoo/owl\";\n\nexport class KnowledgeWysiwyg extends Wysiwyg {\n    static template = \"knowledge.KnowledgeWysiwyg\";\n    static components = {\n        ...Wysiwyg.components,\n        WysiwygArticleHelper,\n    };\n\n    setup() {\n        super.setup();\n        this.articleHelperState = useState({\n            isVisible: false,\n        });\n    }\n\n    /** @override */\n    getEditorConfig() {\n        const config = super.getEditorConfig();\n        return {\n            ...config,\n            onChange: () => {\n                this.articleHelperState.isVisible = isEmptyBlock(this.editor.editable);\n                config.onChange?.();\n            },\n            onEditorReady: () => {\n                this.articleHelperState.isVisible = isEmptyBlock(this.editor.editable);\n            },\n        };\n    }\n}\n", "import { Dialog } from '@web/core/dialog/dialog';\nimport { user } from \"@web/core/user\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { SelectMenu } from '@web/core/select_menu/select_menu';\n\nimport { Component, onWillStart, useEffect, useRef, useState } from \"@odoo/owl\";\nimport { _t } from \"@web/core/l10n/translation\";\n\nclass MoveArticleDialog extends Component {\n\n    static template = \"knowledge.MoveArticleDialog\";\n    static components = { Dialog, SelectMenu };\n    static props = {\n        close: Function,\n        knowledgeArticleRecord: Object\n    };\n\n    setup() {\n        this.size = 'md';\n        this.orm = useService(\"orm\");\n        this.state = useState({selectedParentArticle: false, selectionDisplayGroups: []});\n        this.placeholderLabel = _t('Choose an Article...');\n        this.toggler = useRef(\"togglerRef\");\n\n        onWillStart(this.fetchValues);\n\n        //autofocus\n        useEffect((toggler) => {\n            toggler.click();\n        }, () => [this.toggler.el]);\n\n    }\n\n    /**\n     * For this dialog we needed to get valid articles and the sections since the user can move an\n     * article either under a specific article, that becomes its parent, or inside a section, to become a\n     * root article in either the workspace or the private section.\n     *\n     * @param {String} searchValue Term inputted by the user in the SelectMenu's input\n     */\n    async fetchValues(searchValue) {\n        const knowledgeArticles = await this.orm.call(\n            'knowledge.article',\n            'get_valid_parent_options',\n            [this.props.knowledgeArticleRecord.resId],\n            {search_term: searchValue}\n        );\n        const formattedKnowledgeArticles = knowledgeArticles.map(({id, display_name, root_article_id}) => {\n            return {\n                value: {\n                    parentArticleId: id,\n                    rootArticleName:  root_article_id[0] !== id ? root_article_id[1] : ''\n                },\n                label: display_name\n            };\n        });\n        const selectionGroups = [\n            {\n                label: _t('Categories'),\n                choices: [\n                    {\n                        value: {parentArticleId: 'private'},\n                        label: _t('Private'),\n                    },\n                    {\n                        value: {parentArticleId: 'workspace'},\n                        label: _t('Workspace')\n                    }\n                ],\n            },\n            {\n                label: _t('Articles'),\n                choices: formattedKnowledgeArticles,\n            }\n        ];\n        if (this.props.knowledgeArticleRecord.data.category !== 'private') {\n            selectionGroups[0].choices.push({\n                value: {parentArticleId: 'shared'},\n                label: _t('Shared'),\n            });\n        }\n        this.state.selectionDisplayGroups = selectionGroups;\n        this.state.choices = [\n            ...selectionGroups[0].choices,\n            ...formattedKnowledgeArticles\n        ];\n    }\n\n    selectArticle(value) {\n        this.state.selectedParentArticle = this.state.choices.find(\n            (knowledgeArticle) => knowledgeArticle.value.parentArticleId === value.parentArticleId\n        );\n    }\n\n    async confirmArticleMove() {\n        if (!this.state.selectedParentArticle){\n            // return if no data selectedParentArticle in the SelectMenu\n            return;\n        }\n        const selectedParentArticle = this.state.selectedParentArticle.value.parentArticleId;\n        const params = {};\n        if (typeof selectedParentArticle === 'number') {\n            params.parent_id = selectedParentArticle;\n        } else {\n            params.category = selectedParentArticle;\n        }\n        await this.orm.call(\n            'knowledge.article',\n            'move_to',\n            [this.props.knowledgeArticleRecord.resId],\n            params\n        );\n        // Reload the current article to apply changes\n        await this.props.knowledgeArticleRecord.model.load();\n        this.props.close();\n    }\n\n    get loggedUserPicture() {\n        return `/web/image?model=res.users&field=avatar_128&id=${user.userId}`;\n    }\n\n    get moveArticleLabel() {\n        return _t('Move \"%s\" under:', this.props.knowledgeArticleRecord.data.display_name);\n    }\n}\n\nexport default MoveArticleDialog;\n", "import { HistoryDialog } from \"@html_editor/components/history_dialog/history_dialog\";\nimport { READONLY_MAIN_EMBEDDINGS } from \"@html_editor/others/embedded_components/embedding_sets\";\nimport { useOpenChat } from \"@mail/core/web/open_chat_hook\";\nimport MoveArticleDialog from \"@knowledge/components/move_article_dialog/move_article_dialog\";\nimport { KNOWLEDGE_READONLY_EMBEDDINGS } from \"@knowledge/editor/embedded_components/embedding_sets\";\nimport { getRandomIcon } from \"@knowledge/js/knowledge_utils\";\nimport { browser } from \"@web/core/browser/browser\";\nimport { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\nimport { formatDateTime } from \"@web/core/l10n/dates\";\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\";\n\nimport { Component, onWillStart, useState } from \"@odoo/owl\";\n\nexport class OptionsDropdown extends Component {\n    static components = { Dropdown, DropdownItem };\n    static template = \"knowledge.OptionsDropdown\";\n    static props = {\n        record: { type: Object },\n    };\n\n    setup() {\n        this.dialog = useService(\"dialog\");\n        this.notification = useService(\"notification\");\n        this.orm = useService(\"orm\");\n        this.openChat = useOpenChat(\"res.users\");\n        this.formatDateTime = formatDateTime;\n        this.commentsState = useState(this.env.commentsState);\n        this.chatterPanelState = useState(this.env.chatterPanelState);\n        this.propertiesPanelState = useState(this.env.propertiesPanelState);\n        onWillStart(async () => {\n            this.isInternalUser = await user.hasGroup(\"base.group_user\");\n        });\n    }\n\n    get articleId() {\n        return this.props.record.resId;\n    }\n\n    get creationDate() {\n        return this.data.create_date;\n    }\n\n    get creator() {\n        return this.data.create_uid.display_name;\n    }\n\n    get data() {\n        return this.props.record.data;\n    }\n\n    get lastEditionDate() {\n        return this.data.last_edition_date;\n    }\n\n    get lastEditor() {\n        return this.data.last_edition_uid.display_name;\n    }\n\n    get lastEditorAvatarUrl() {\n        return `/web/image/knowledge.article/${this.articleId}/last_edition_user_avatar`;\n    }\n\n    async addCover() {\n        let res;\n        try {\n            res = await rpc(`/knowledge/article/${this.articleId}/add_random_cover`, {\n                query: this.props.record.data.name,\n                orientation: \"landscape\",\n            });\n        } catch (e) {\n            console.error(e);\n        }\n        if (res?.cover_id) {\n            await this.props.record.update({ cover_image_id: { id: res.cover_id } });\n        } else {\n            this.env.openCoverSelector();\n        }\n    }\n\n    async addIcon() {\n        await this.props.record.update({ icon: await getRandomIcon() });\n    }\n\n    async beforeOpen() {\n        await this.props.record.save();\n    }\n\n    async copy() {\n        await this.props.record.save();\n        const [articleId] = await this.orm.call(\"knowledge.article\", \"action_make_copy\", [\n            this.articleId,\n        ]);\n        this.env.openArticle(articleId);\n    }\n\n    export() {\n        // Use the browser print as wkhtmltopdf sadly does not handle emojis / embed views / ...\n        // (Investigation shows that it would be complicated to add that support).\n        window.print();\n    }\n\n    moveArticle() {\n        this.dialog.add(MoveArticleDialog, { knowledgeArticleRecord: this.props.record });\n    }\n\n    onCreatorClick() {\n        this.openChat(this.data.create_uid.id);\n    }\n\n    onLastEditorClick() {\n        this.openChat(this.data.last_edition_uid.id);\n    }\n\n    async openHistory() {\n        const [{ html_field_history_metadata: metaData }] = await this.orm.read(\n            \"knowledge.article\",\n            [this.articleId],\n            [\"html_field_history_metadata\"]\n        );\n        if (!metaData.body) {\n            return;\n        }\n        this.dialog.add(HistoryDialog, {\n            recordId: this.articleId,\n            recordModel: \"knowledge.article\",\n            versionedFieldName: \"body\",\n            historyMetadata: metaData.body,\n            restoreRequested: (html, close) => {\n                this.props.record.update({ body: html });\n                close();\n            },\n            embeddedComponents: [...READONLY_MAIN_EMBEDDINGS, ...KNOWLEDGE_READONLY_EMBEDDINGS],\n        });\n    }\n\n    removeIcon() {\n        this.props.record.update({ icon: false });\n    }\n\n    removeCover() {\n        this.props.record.update({ cover_image_id: false });\n    }\n\n    sendToTrash() {\n        this.env.sendArticleToTrash();\n    }\n\n    toggleFullWidth() {\n        this.props.record.update({ full_width: !this.props.record.data.full_width });\n        // For calendar view to resize.\n        browser.dispatchEvent(new Event(\"resize\"));\n    }\n\n    async toggleIsListedInTemplatesGallery() {\n        const newVal = !this.data.is_listed_in_templates_gallery;\n        await this.orm.write(\"knowledge.article\", [this.props.record.resId], {\n            is_listed_in_templates_gallery: newVal,\n        });\n        this.notification.add(\n            newVal\n                ? _t(\"Article added to the list of Templates\")\n                : _t(\"Article removed from the list of Templates\"),\n            { type: \"success\" }\n        );\n    }\n\n    toggleItem() {\n        this.props.record.update(\n            { is_article_item: !this.props.record.data.is_article_item },\n            { save: true }\n        );\n    }\n\n    async toggleLock() {\n        await this.props.record.save();\n        await this.orm.call(\"knowledge.article\", \"action_set_lock\", [this.articleId], {\n            lock: !this.props.record.data.is_locked,\n        });\n        await this.props.record.load();\n    }\n\n    async unarchive() {\n        await this.orm.call(\"knowledge.article\", \"action_unarchive\", [this.articleId]);\n        await this.props.record.load();\n    }\n}\n", "import { ConfirmationDialog } from \"@web/core/confirmation_dialog/confirmation_dialog\";\nimport { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { user } from \"@web/core/user\";\nimport { useService } from \"@web/core/utils/hooks\";\n\nimport { Component, onWillStart, useState } from \"@odoo/owl\";\n\nclass Member {\n    constructor(data) {\n        Object.assign(this, data);\n    }\n\n    get avatarUrl() {\n        return `/web/image/knowledge.article.member/${this.member_id}/article_member_avatar`;\n    }\n\n    get basedOnName() {\n        return `${this.based_on_icon || \"\ud83d\udcc4\"} ${this.based_on_name || _t(\"Untitled\")}`;\n    }\n\n    get canWrite() {\n        return this.permission === \"write\";\n    }\n\n    get id() {\n        return this.member_id;\n    }\n\n    get isCurrentUser() {\n        return this.partner_id === user.partnerId;\n    }\n\n    get isRemovable() {\n        return !(this.based_on && this.permission === \"none\");\n    }\n}\n\nexport class PermissionPanel extends Component {\n    static template = \"knowledge.PermissionPanel\";\n    static props = {\n        reactiveRecordWrapper: Object,\n        openArticle: Function,\n        sendArticleToTrash: Function,\n        close: Function,\n    };\n    static components = { Dropdown, DropdownItem };\n\n    setup() {\n        this.orm = useService(\"orm\");\n        this.state = useState({ members: {}, isArticleLoaded: true });\n        this.actionService = useService(\"action\");\n        this.dialog = useService(\"dialog\");\n        this.mailStore = useService(\"mail.store\");\n        this.userIsAdmin = user.isAdmin;\n        onWillStart(async () => (this.userIsInternal = await user.hasGroup(\"base.group_user\")));\n        onWillStart(async () => await this.loadMembers());\n    }\n\n    get record() {\n        return this.props.reactiveRecordWrapper.record;\n    }\n\n    get hasUniqueWriter() {\n        return (\n            this.data.inherited_permission !== \"write\" &&\n            this.members.filter((member) => member.permission === \"write\").length === 1\n        );\n    }\n\n    get inheritedPermissionParent() {\n        return {\n            id: this.data.inherited_permission_parent_id.id,\n            name: this.data.inherited_permission_parent_id.display_name || _t(\"Untitled\"),\n        };\n    }\n\n    get isRootArticle() {\n        return !this.data.parent_id;\n    }\n\n    get parentArticle() {\n        return {\n            id: this.data.parent_id.id,\n            name: this.data.parent_id.display_name,\n        };\n    }\n\n    get visibility() {\n        return this.data.is_article_visible_by_everyone ? \"everyone\" : \"members\";\n    }\n\n    get data() {\n        return this.props.reactiveRecordWrapper.record.data;\n    }\n\n    get members() {\n        return this.state.members;\n    }\n\n    get permissionLevel() {\n        return { none: 0, read: 1, write: 2 };\n    }\n\n    get permissions() {\n        return { none: _t(\"No Access\"), read: _t(\"Can Read\"), write: _t(\"Can Edit\") };\n    }\n\n    get internalPermissions() {\n        return { none: _t(\"Members only\"), read: _t(\"Can Read\"), write: _t(\"Can Edit\") };\n    }\n\n    get userCanEdit() {\n        return this.data.user_can_write;\n    }\n\n    get userIsInternalEditor() {\n        return this.userIsAdmin || (this.userIsInternal && this.userCanEdit);\n    }\n\n    get visibilities() {\n        return { everyone: _t(\"Everyone\"), members: _t(\"Members only\") };\n    }\n\n    /**\n     * Loads the members displayed in the permissions panel.\n     * If the user is included in the member list, they will be shown first,\n     * followed by the other members in alphabetical order.\n     * @param {integer} articleId\n     */\n    async loadMembers(articleId = this.record.resId) {\n        const members = await this.orm.call(\n            \"knowledge.article\",\n            \"get_permission_panel_members\",\n            [articleId]\n        );\n        this.state.members = members.map((memberVals) => {\n            return new Member(memberVals);\n        }).sort((m1, m2) => {\n            if (m1.isCurrentUser || !m2.name) {\n                return -1;\n            }\n            if (m2.isCurrentUser || !m1.name) {\n                return 1;\n            }\n            return m1.name.localeCompare(m2.name);\n        });\n    }\n\n    async load() {\n        await Promise.all([this.loadMembers(), this.record.load()]);\n    }\n\n    /**\n     * @param {String} permission\n     */\n    async setInternalPermission(permission) {\n        await this.orm.call(\"knowledge.article\", \"set_internal_permission\", [\n            this.record.resId,\n            permission,\n        ]);\n        await this.load();\n    }\n\n    async removeArticleMember(member) {\n        await this.orm.call(\"knowledge.article\", \"remove_member\", [this.record.resId, member.id]);\n        //edge case: if the current user is removed from the article, we need to redirect to the home page\n        if (\n            member.isCurrentUser &&\n            this.userIsInternal &&\n            this.data.inherited_permission === \"none\" &&\n            !this.userIsAdmin\n        ) {\n            this.actionService.doAction(\n                await this.orm.call(\"knowledge.article\", \"action_home_page\", [false]),\n                { stackPosition: \"replaceCurrentAction\" }\n            );\n            return;\n        }\n        await this.load();\n    }\n\n    async restore() {\n        await this.orm.call(\"knowledge.article\", \"restore_article_access\", [this.record.resId]);\n        await this.load();\n    }\n\n    /**\n     * @param {String} visibility\n     */\n    async setInternalVisibility(visibility) {\n        await this.orm.call(\"knowledge.article\", \"set_is_article_visible_by_everyone\", [\n            this.record.resId,\n            visibility === \"everyone\",\n        ]);\n        await this.load();\n    }\n\n    async setMemberPermission(member, permission) {\n        await this.orm.call(\"knowledge.article\", \"set_member_permission\", [\n            this.record.resId,\n            member.id,\n            permission,\n        ]);\n        await this.load();\n    }\n\n    openInviteDialog() {\n        this.actionService.doAction(\"knowledge.knowledge_invite_action_from_article\", {\n            additionalContext: { active_id: this.record.resId },\n            onClose: async () => await this.load(),\n        });\n    }\n\n    /**\n     * @param {Member} member\n     */\n    onMemberClick(member) {\n        this.mailStore.openChat({ partnerId: member.partner_id });\n    }\n\n    /**\n     * @param {number} resId\n     */\n    async openArticle(resId) {\n        // Permission panel needs to stay open while switching articles.\n        this.state.isArticleLoaded = false;\n        await this.props.openArticle(resId);\n        await this.loadMembers(resId);\n        this.state.isArticleLoaded = true;\n    }\n\n    async restoreArticle() {\n        this.dialog.add(ConfirmationDialog, {\n            body: _t(\n                \"Are you sure you want to restore access? This means this article will now inherit any access set on its parent articles.\"\n            ),\n            cancel: () => {},\n            cancelLabel: _t(\"Discard\"),\n            confirm: async () => {\n                await this.restore();\n            },\n            confirmLabel: _t(\"Restore Access\"),\n            title: _t(\"Restore Access\"),\n        });\n    }\n\n    /**\n     * @param {Member} member\n     * @param {String} permission\n     */\n    async selectMemberPermission(member, permission) {\n        if (permission === member.permission) {\n            return;\n        }\n        if (member.isCurrentUser && !this.userIsAdmin) {\n            if (permission === \"none\") {\n                // setting own permission to none, user will leave the article\n                this.dialog.add(ConfirmationDialog, {\n                    cancel: () => {},\n                    cancelLabel: _t(\"Discard\"),\n                    confirm: async () => {\n                        await this.setMemberPermission(member, permission);\n                        await this.actionService.doAction(\n                            \"knowledge.ir_actions_server_knowledge_home_page\"\n                        );\n                    },\n                    confirmLabel: _t(\"Lose Access\"),\n                    title: _t(\"Leave Article\"),\n                    body: _t(\n                        'Are you sure you want to set your permission to \"No Access\"? If you do, you will no longer have access to the article.'\n                    ),\n                });\n            } else {\n                // downgrading own permission from write to read\n                this.dialog.add(ConfirmationDialog, {\n                    cancel: () => {},\n                    cancelLabel: _t(\"Discard\"),\n                    confirm: async () => {\n                        await this.setMemberPermission(member, permission);\n                    },\n                    confirmLabel: _t(\"Restrict own access\"),\n                    title: _t(\"Change Permission\"),\n                    body: _t('Are you sure you want to remove your own \"Write\" access?'),\n                });\n            }\n        } else if (member.based_on) {\n            // we are desyncing the article from its parent.\n            this.dialog.add(ConfirmationDialog, {\n                cancel: () => {},\n                cancelLabel: _t(\"Discard\"),\n                confirm: async () => {\n                    await this.setMemberPermission(member, permission);\n                },\n                confirmLabel: _t(\"Restrict Access\"),\n                title: _t(\"Change Permission\"),\n                body: _t(\n                    \"Are you sure you want to change the permission? This means it will no longer inherit access rights from its parent articles.\"\n                ),\n            });\n        } else {\n            // changing permission of non-inherited member or admin upgrading own permission\n            await this.setMemberPermission(member, permission);\n        }\n    }\n\n    async removeMember(member) {\n        if (member.isCurrentUser && member.permission !== \"none\") {\n            if (this.data.category === \"private\") {\n                // leaving private article, article will be sent to trash\n                this.dialog.add(ConfirmationDialog, {\n                    title: _t(\"Leave Private Article\"),\n                    body: _t(\n                        \"Are you sure you want to leave your private Article? As you are its last member, it will be moved to the Trash.\"\n                    ),\n                    cancel: () => {},\n                    cancelLabel: _t(\"Discard\"),\n                    confirm: () => this.props.sendArticleToTrash(),\n                    confirmLabel: _t(\"Move to Trash\"),\n                });\n            } else {\n                this.dialog.add(ConfirmationDialog, {\n                    title: _t(\"Leave Article\"),\n                    body: _t(\n                        \"Are you sure you want to leave this article? That means losing your personal access to it.\"\n                    ),\n                    cancel: () => {},\n                    cancelLabel: _t(\"Discard\"),\n                    confirm: async () => {\n                        await this.removeArticleMember(member);\n                    },\n                    confirmLabel: _t(\"Leave Article\"),\n                });\n            }\n        } else if (member.based_on) {\n            // removing inherited member permission, this will desync the article from its parent\n            this.dialog.add(ConfirmationDialog, {\n                title: _t(\"Restrict Access\"),\n                body: _t(\n                    \"Are you sure you want to restrict access to this article? This means that it will no longer inherit access rights from its parents.\"\n                ),\n                confirm: async () => {\n                    await this.removeArticleMember(member);\n                },\n                confirmLabel: _t(\"Restrict Access\"),\n            });\n        } else {\n            await this.removeArticleMember(member);\n        }\n    }\n\n    async selectInternalPermission(permission) {\n        if (permission === this.data.inherited_permission) {\n            return;\n        }\n        if (\n            this.data.inherited_permission_parent_id &&\n            this.permissionLevel[permission] < this.permissionLevel[this.data.inherited_permission]\n        ) {\n            this.dialog.add(ConfirmationDialog, {\n                cancel: () => {},\n                cancelLabel: _t(\"Discard\"),\n                confirm: async () => {\n                    await this.setInternalPermission(permission);\n                },\n                title: _t(\"Restrict Access\"),\n                confirmLabel: _t(\"Restrict Access\"),\n                body: _t(\n                    \"Are you sure you want to restrict access to this article? This means it will no longer inherit access rights from its parents.\"\n                ),\n            });\n        } else {\n            await this.setInternalPermission(permission);\n        }\n    }\n\n    async selectInternalVisibility(visibility) {\n        if (visibility === this.visibility) {\n            return;\n        }\n        await this.setInternalVisibility(visibility);\n    }\n}\n", "import { PermissionPanel } from \"@knowledge/components/permission_panel/permission_panel\";\nimport { Dialog } from \"@web/core/dialog/dialog\";\nimport { Component } from \"@odoo/owl\";\n\n/**\n * This component is used exclusively on mobile devices to render the permissions\n * panel within a fullscreen dialog. The fullscreen mode improves usability by\n * providing a more accessible interface for managing article permissions and members.\n */\nexport class PermissionPanelDialog extends Component {\n    static template = \"knowledge.PermissionPanelDialog\";\n    static components = { Dialog, PermissionPanel };\n    static props = {\n        reactiveRecordWrapper: Object,\n        openArticle: Function,\n        sendArticleToTrash: Function,\n        close: Function\n    };\n}\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { Dialog } from \"@web/core/dialog/dialog\";\nimport {\n    Component,\n    onMounted,\n    useRef } from \"@odoo/owl\";\n\nexport class PromptEmbeddedViewNameDialog extends Component {\n    static template = \"knowledge.PromptEmbeddedViewNameDialog\";\n    static components = { Dialog };\n    static props = {\n        defaultName: { type: String, optional: true },\n        isNew: { type: Boolean, optional: true },\n        viewType: { type: String },\n        save: { type: Function },\n        close: { type: Function, optional: true }\n    };\n\n    /**\n     * @override\n     */\n    setup () {\n        super.setup();\n        this.input = useRef('input');\n        onMounted(() => {\n            window.setTimeout(() => {\n                this.input.el?.focus(); // auto-focus\n            }, 0);\n        });\n    }\n    async save () {\n        await this.props.save(this.input.el.value);\n        this.props.close();\n    }\n    /**\n     * @returns {String}\n     */\n    get placeholder () {\n        if (this.props.viewType === 'kanban') {\n            return _t('e.g. Buildings');\n        }\n        if (this.props.viewType === 'list') {\n            return _t('e.g. Todos');\n        }\n    }\n    /**\n     * @returns {String}\n     */\n    get title () {\n        if (this.props.viewType === 'list') {\n            return _t('Insert a List View');\n        }\n        if (this.props.viewType === 'kanban') {\n            return _t('Insert a Kanban View');\n        }\n        return _t('Embed a View');\n    }\n    /**\n     * @param {Event} event\n     */\n    onInputKeydown (event) {\n        if (event.key === 'Enter') {\n            this.save();\n        }\n    }\n}\n", "import { registry } from \"@web/core/registry\";\nimport { standardWidgetProps } from \"@web/views/widgets/standard_widget_props\";\nimport { PropertiesField } from \"@web/views/fields/properties/properties_field\";\nimport { user } from \"@web/core/user\";\nimport { useService } from \"@web/core/utils/hooks\";\n\nimport { Component, onWillStart, useEffect, useState } from \"@odoo/owl\";\n\nexport class KnowledgeArticleProperties extends Component {\n    static template = \"knowledge.KnowledgeArticleProperties\";\n    static props = { ...standardWidgetProps };\n    static components = { PropertiesField };\n\n    setup() {\n        this.state = useState(this.env.propertiesPanelState);\n        this.ui = useService(\"ui\");\n        // open/close the panel based on the existence of properties\n        useEffect(\n            () => {\n                if (!this.ui.isSmall && this.hasProperties && !this.state.isDisplayed) {\n                    this.state.isDisplayed = true;\n                } else if (this.state.isDisplayed && !this.hasProperties) {\n                    this.state.isDisplayed = false;\n                }\n            },\n            () => [this.props.record.resId, this.hasProperties]\n        );\n        onWillStart(async () => (this.userIsInternal = await user.hasGroup(\"base.group_user\")));\n    }\n\n    get hasProperties() {\n        return this.props.record.data.article_properties.some((prop) => !prop.definition_deleted);\n    }\n}\n\nexport const knowledgePropertiesPanel = {\n    component: KnowledgeArticleProperties,\n    additionalClasses: [\n        \"col-12\",\n        \"col-lg-2\",\n        \"p-0\",\n        \"position-relative\",\n        \"border-top\",\n    ],\n    fieldDependencies: [{ name: \"article_properties\", type: \"jsonb\" }],\n};\n\nregistry.category(\"view_widgets\").add(\"knowledge_properties_panel\", knowledgePropertiesPanel);\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { ArticleSearchDialog } from \"@knowledge/components/article_search_dialog/article_search_dialog\";\nimport { ArticleTemplatePickerDialog } from \"@knowledge/components/article_template_picker_dialog/article_template_picker_dialog\";\nimport { browser } from \"@web/core/browser/browser\";\nimport { ConfirmationDialog } from \"@web/core/confirmation_dialog/confirmation_dialog\";\nimport {\n    KnowledgeSidebarFavoriteSection,\n    KnowledgeSidebarPrivateSection,\n    KnowledgeSidebarSharedSection,\n    KnowledgeSidebarWorkspaceSection\n} from \"./sidebar_section\";\nimport { localization } from \"@web/core/l10n/localization\";\nimport { user } from \"@web/core/user\";\nimport { throttleForAnimation } from \"@web/core/utils/timing\";\nimport { useNestedSortable } from \"@web/core/utils/nested_sortable\";\nimport { useBus, useService } from \"@web/core/utils/hooks\";\nimport { useRecordObserver } from \"@web/model/relational_model/utils\";\n\nimport {\n    Component,\n    onWillStart,\n    reactive,\n    useRef,\n    useState,\n    useChildSubEnv,\n    useExternalListener,\n} from \"@odoo/owl\";\n\nexport const SORTABLE_TOLERANCE = 10;\n\n/**\n * Main Sidebar component. Its role is mainly to fetch and manage the articles\n * to show and allow to reorder them. It updates the info of the current\n * article each time the props are updated.\n * The articles are stored in the state and have the following shape:\n * - {string} category,\n * - {array} child_ids,\n * - {string} icon,\n * - {boolean} is_article_item,\n * - {boolean} is_locked,\n * - {boolean} is_user_favorite,\n * - {string} name,\n * - {number} parent_id,\n * - {boolean} user_can_write,\n * - {boolean} has_article_children,\n */\nexport class KnowledgeSidebar extends Component {\n    static props = {\n        record: Object,\n    };\n    static template = \"knowledge.Sidebar\";\n    static components = {\n        KnowledgeSidebarFavoriteSection,\n        KnowledgeSidebarPrivateSection,\n        KnowledgeSidebarSharedSection,\n        KnowledgeSidebarWorkspaceSection,\n    };\n\n    setup() {\n        super.setup();\n        // In case a portal user loads a hidden website_published article,\n        // subsequent searches should include articles in that hidden tree.\n\n        this.actionService = useService(\"action\");\n        this.dialog = useService(\"dialog\");\n        this.hotkey = useService(\"hotkey\");\n        this.orm = useService(\"orm\");\n\n        this.favoriteTree = useRef(\"favoriteTree\");\n        this.mainTree = useRef(\"mainTree\");\n\n        this.currentData = {};\n\n        this.storageKeys = {\n            size: \"knowledge.sidebarSize\",\n            unfoldedArticles: \"knowledge.unfolded.ids\",\n            unfoldedFavorites: \"knowledge.unfolded.favorite.ids\",\n        };\n\n        // Get set of unfolded ids and sync it with the local storage (any\n        // change will trigger a write in the local storage)\n        this.unfoldedArticlesIds = reactive(\n            new Set(localStorage.getItem(this.storageKeys.unfoldedArticles)?.split(\";\").map(Number)),\n            () => localStorage.setItem(this.storageKeys.unfoldedArticles, Array.from(this.unfoldedArticlesIds).join(\";\"))\n        );\n        this.unfoldedFavoritesIds = reactive(\n            new Set(localStorage.getItem(this.storageKeys.unfoldedFavorites)?.split(\";\").map(Number)),\n            () => localStorage.setItem(this.storageKeys.unfoldedFavorites, Array.from(this.unfoldedFavoritesIds).join(\";\"))\n        );\n\n        useChildSubEnv({\n            fold: this.fold.bind(this),\n            getArticle: this.getArticle.bind(this),\n            model: this.props.record.model,\n            unfold: this.unfold.bind(this),\n        });\n\n        this.state = useState({\n            dragging: false,\n            sidebarSize: localStorage.getItem(this.storageKeys.size) || 300,\n            resizing: false,\n        });\n\n        this.loadArticles();\n\n        useBus(this.env.bus, \"KNOWLEDGE:RELOAD_SIDEBAR\", async (event) => {\n            await this.loadArticles();\n        });\n\n        // Reassign the Control+k hotkey for portal users from the CommandPalette\n        // to the article search feature.\n        useExternalListener(\n            browser,\n            \"keydown\",\n            (ev) => {\n                if (this.isPortalUser && ev.key === \"k\" && (ev.ctrlKey || ev.metaKey)) {\n                    ev.preventDefault();\n                    ev.stopImmediatePropagation();\n                    this.onSearchBarClick();\n                }\n            },\n            { capture: true }\n        );\n\n        // Resequencing of the favorite articles\n        useNestedSortable({\n            ref: this.favoriteTree,\n            edgeScrolling: {\n                speed: 10,\n                threshold: 15,\n            },\n            preventDrag: (el) => {\n                // Prevent the reordering of child articles that are readonly within the favorite\n                // section.\n                return (\n                    !el.parentElement.classList.contains(\"o_tree\") &&\n                    el.classList.contains(\"readonly\")\n                );\n            },\n            tolerance: SORTABLE_TOLERANCE,\n            onDrop: ({ element, next, parent }) => {\n                if (!parent) {\n                    // Favorite resequence\n                    const articleId = parseInt(element.dataset.articleId);\n                    const beforeId = next ? parseInt(next.dataset.articleId) : false;\n                    this.resequenceFavorites(articleId, beforeId);\n                } else {\n                    // Child of favorite resequence\n                    const article = this.getArticle(parseInt(element.dataset.articleId));\n                    const parentId = parseInt(parent.dataset.articleId);\n                    const currentPosition = {\n                        category: article.category,\n                        parentId: article.parent_id,\n                        beforeArticleId:\n                            parseInt(element.nextElementSibling?.dataset.articleId) || false,\n                    };\n                    const newPosition = {\n                        category: article.category,\n                        parentId: parentId,\n                        beforeArticleId: parseInt(next?.dataset.articleId) || false,\n                    };\n                    this.moveArticle(article, currentPosition, newPosition);\n                }\n            },\n        });\n\n        // Resequencing and rehierarchisation of articles\n        useNestedSortable({\n            ref: this.mainTree,\n            elements: \"li.o_article\",\n            groups: () => this.isInternalUser ? \".o_section\" : \".o_section[data-section='private']\",\n            connectGroups: () => this.isInternalUser,\n            nest: true,\n            preventDrag: (el) => el.classList.contains(\"readonly\"),\n            tolerance: SORTABLE_TOLERANCE,\n            onDragStart: () => this.state.dragging = true,\n            onDragEnd: () => this.state.dragging = false,\n            /**\n             * @param {DOMElement} element - dropped element\n             * @param {DOMElement} next - element before which the element was dropped\n             * @param {DOMElement} group - initial (=current) group of the dropped element\n             * @param {DOMElement} newGroup - group in which the element was dropped\n             * @param {DOMElement} parent - parent element of where the element was dropped\n             * @param {DOMElement} placeholder - hint element showing the current position\n             */\n            onDrop: async ({element, next, group, newGroup, parent, placeholder}) => {\n                const article = this.getArticle(parseInt(element.dataset.articleId));\n                // Dropped on trash, move the article to the trash\n                if (newGroup.classList.contains(\"o_knowledge_sidebar_trash\")) {\n                    this.moveToTrash(article);\n                    return;\n                }\n                const parentId = parent ? parseInt(parent.dataset.articleId) : false;\n                // Dropped on restricted position (child of readonly or shared root)\n                if (placeholder.classList.contains('bg-danger')) {\n                    this.rejectDrop(article, parentId);\n                    return;\n                }\n                const currentPosition = {\n                    category: article.category,\n                    parentId: article.parent_id,\n                    beforeArticleId: parseInt(element.nextElementSibling?.dataset.articleId) || false,\n                };\n                const newPosition = {\n                    category: newGroup.dataset.section,\n                    parentId: parentId,\n                    beforeArticleId: parseInt(next?.dataset.articleId) || false,\n                };\n                this.moveArticle(article, currentPosition, newPosition);\n            },\n            /**\n             * @param {DOMElement} element - moved element\n             * @param {DOMElement} parent - parent element of where the element was moved\n             * @param {DOMElement} newGroup - group in which the element was moved\n             * @param {DOMElement} placeholder - hint element showing the current position\n             */\n            onMove: ({element, parent, newGroup, placeholder}) => {\n                if (parent) {\n                    // Cannot add child to readonly articles, unless it is the\n                    // current parent.\n                    const currentParentId = element.parentElement.parentElement.dataset.articleId;\n                    const targetParentId = parent.dataset.articleId;\n                    if (currentParentId !== targetParentId && parent.classList.contains('readonly')) {\n                        placeholder.classList.add('bg-danger');\n                        return;\n                    }\n                } else if (newGroup.dataset.section === \"shared\") {\n                    // Private articles cannot be dropped in the shared section\n                    const article = this.getArticle(parseInt(element.dataset.articleId));\n                    if (article.category === \"private\") {\n                        placeholder.classList.add('bg-danger');\n                        return;\n                    }\n                }\n                placeholder.classList.remove('bg-danger');\n            },\n        });\n\n        onWillStart(async () => {\n            this.isInternalUser = await user.hasGroup('base.group_user');\n            this.isPortalUser = await user.hasGroup('base.group_portal');\n        });\n\n        useRecordObserver(async (record) => {\n            const nextDataParentId = record.data.parent_id ? record.data.parent_id.id : false;\n            // During the first load, `loadArticles` is still pending and the component is in its\n            // loading state. However, because of OWL reactive implementation (uses a Proxy),\n            // record data still has to be read in order to subscribe to later changes, even if\n            // nothing is done with that data on the first call. This subscription is what allows\n            // this callback to be called i.e. when the name of the article is changed, reflecting\n            // the change in the sidebar. See useRecordObserver, effect, reactive implementations\n            // for further details.\n            const article = this.getArticle(record.resId) || {\n                id: record.resId,\n                name: record.data.name,\n                icon: record.data.icon,\n                category: record.data.category,\n                parent_id: nextDataParentId,\n                is_locked: record.data.is_locked,\n                user_can_write: record.data.user_can_write,\n                is_article_item: record.data.is_article_item,\n                is_user_favorite: record.data.is_user_favorite,\n                child_ids: [],\n            };\n            if (this.state.articles[record.resId]) {\n                if (record.data.is_article_item !== article.is_article_item) {\n                    if (record.data.is_article_item) {\n                        // Article became item, remove it from the sidebar\n                        this.removeArticle(article);\n                    } else {\n                        // Item became article, add it in the sidebar\n                        this.insertArticle(article, {\n                            parentId: article.parent_id\n                        });\n                        this.showArticle(article);\n                    }\n                }\n                if (record.data.is_user_favorite !== article.is_user_favorite) {\n                    if (record.data.is_user_favorite) {\n                        // Add the article to the favorites tree\n                        this.state.favoriteIds.push(article.id);\n                    } else {\n                        // Remove the article from the favorites tree\n                        this.state.favoriteIds.splice(this.state.favoriteIds.indexOf(article.id), 1);\n                    }\n                }\n                if ((nextDataParentId !== article.parent_id || record.data.category !== article.category) &&\n                    (record.data.parent_id !== this.currentData.parent_id || record.data.category !== this.currentData.category)) {\n                    // Article changed position (\"Moved to\")\n                    if (!this.getArticle(nextDataParentId)) {\n                        // Parent is not loaded, reload the tree to show moved\n                        // article in the sidebar\n                        await this.loadArticles();\n                        this.showArticle(this.getArticle(record.resId));\n                    } else {\n                        this.repositionArticle(article, {\n                            parentId: nextDataParentId,\n                            category: record.data.category,\n                        });\n                    }\n                }\n                // Update values used to display the current article in the sidebar\n                Object.assign(article, {\n                    name: record.data.name,\n                    icon: record.data.icon,\n                    is_locked: record.data.is_locked,\n                    user_can_write: record.data.user_can_write,\n                    is_article_item: record.data.is_article_item,\n                    is_user_favorite: record.data.is_user_favorite,\n                });\n            } else if (!this.state.loading) {  // New article, add it in the state and sidebar\n                if (record.data.is_user_favorite) {\n                    // Favoriting an article that is not shown in the main\n                    // tree (hidden child, item, or child of restricted)\n                    this.state.favoriteIds.push(record.resId);\n                }\n                this.state.articles[article.id] = article;\n                // Don't add new items in the sidebar\n                if (!record.data.is_article_item) {\n                    await this.insertArticle(article, {\n                        category: article.category,\n                        parentId: article.parent_id,\n                    });\n                    // Make sure the article is visible\n                    this.showArticle(article);\n                    if (nextDataParentId && this.getArticle(nextDataParentId)?.is_user_favorite) {\n                        this.unfold(nextDataParentId, true);\n                    }\n                }\n            }\n\n            this.currentData = {\n                parent_id: record.data.parent_id,\n                category: record.data.category,\n            };\n        });\n        this.env.bus.addEventListener(\"knowledge.sidebar.insertNewArticle\", async ({ detail }) => {\n            if (this.getArticle(detail.articleId)) {\n                // Article already in the sidebar\n                return;\n            }\n            const parent = detail.parentId ? this.getArticle(detail.parentId) : false;\n            const newArticle = {\n                id: detail.articleId,\n                name: detail.name,\n                icon: detail.icon,\n                parent_id: parent ? parent.id : false,\n                category: parent ? parent.category : false,\n                is_locked: false,\n                user_can_write: true,\n                is_article_item: false,\n                is_user_favorite: false,\n                child_ids: [],\n            };\n            this.state.articles[newArticle.id] = newArticle;\n            await this.insertArticle(newArticle, {\n                parentId: newArticle.parent_id,\n                category: parent ? parent.category : false\n            });\n        });\n    }\n\n    /**\n     * Open the templates dialog\n     */\n    async browseTemplates() {\n        if (await this.props.record.isDirty()) {\n            await this.props.record.save();\n        }\n        const { articles, templates } = await this.orm.call(\n            \"knowledge.article\",\n            \"get_available_templates\"\n        );\n        this.dialog.add(ArticleTemplatePickerDialog, {\n            articles: articles,\n            templates: templates,\n            /** @param {integer} articleId */\n            onLoadArticle: async articleId => {\n                const newArticleIds = await this.orm.call(\n                    \"knowledge.article\",\n                    \"action_make_private_copy\", [articleId], { preserve_name: true }\n                );\n                await this.actionService.doAction(\"knowledge.ir_actions_server_knowledge_home_page\", {\n                    stackPosition: \"replaceCurrentAction\",\n                    additionalContext: {\n                        res_id: newArticleIds[0],\n                    }\n                });\n            },\n            /** @param {integer} templateId */\n            onLoadTemplate: async templateId => {\n                const [newArticleId] = await this.orm.call(\n                    \"knowledge.article\",\n                    \"create_article_from_template\", [templateId]\n                );\n                await this.actionService.doAction(\"knowledge.ir_actions_server_knowledge_home_page\", {\n                    stackPosition: \"replaceCurrentAction\",\n                    additionalContext: {\n                        res_id: newArticleId,\n                    }\n                });\n            },\n            /** @param {integer} articleId */\n            onDeleteArticle: async (articleId) => {\n                if (this.props.record.resId === articleId) {\n                    await this.props.record.save();\n                }\n                await this.orm.write(\"knowledge.article\", [articleId], {\n                    is_listed_in_templates_gallery: false,\n                });\n                if (this.props.record.resId === articleId) {\n                    this.props.record.load();\n                }\n            },\n            /** @param {integer} templateId */\n            onDeleteTemplate: async (templateId) => {\n                await this.orm.unlink(\"knowledge.article\", [templateId]);\n            },\n        });\n    }\n\n    /**\n     * Change the category of an article, and of all its descendants.\n     * @param {Object} article\n     * @param {String} category\n     */\n    async changeCategory(article, category) {\n        article.category = category;\n        if (article.id === this.props.record.id) {\n            // Reload current record to propagate changes\n            if (await this.props.record.isDirty()) {\n                await this.props.record.save();\n            } else {\n                await this.props.record.model.load();\n            }\n        }\n        for (const childId of article.child_ids) {\n            this.changeCategory(this.getArticle(childId), category);\n        }\n    }\n\n    /**\n     * Create a new article (and open it).\n     * @param {String} - category\n     * @param {integer} - targetParentId\n     */\n    createArticle(category, targetParentId) {\n        try {\n            this.env.createArticle(category, targetParentId);\n        } catch {\n            // Could not create article, reload tree in case some permission changed\n            this.loadArticles();\n        }\n    }\n\n    /**\n     * Fold an article.\n     * @param {integer} articleId: id of article\n     * @param {boolean} isFavorite: whether to fold in favorite tree\n     */\n    fold(articleId, isFavorite) {\n        if (isFavorite) {\n            this.unfoldedFavoritesIds.delete(articleId);\n        } else {\n            this.unfoldedArticlesIds.delete(articleId);\n        }\n    }\n\n    /**\n     * Get the article stored in the state given its id.\n     * @param {integer} articleId - Id of the article\n     * @returns {Object} article\n     */\n    getArticle(articleId) {\n        return this.state.articles[articleId];\n    }\n\n    /**\n     * Get the array of article ids stored in the state from the given category\n     * (eg. this.state.workspaceIds for workspace).\n     * @param {String} category\n     * @returns {Array} array of articles ids\n     */\n    getCategoryIds(category) {\n        return this.state[`${category}Ids`];\n    }\n\n    /**\n     * Insert the given article at the given position in the sidebar.\n     * @param {Object} article - article stored in the state\n     * @param {Object} position\n     * @param {integer} position.beforeArticleId\n     * @param {String} position.category\n     * @param {integer} position.parentId\n     *\n     */\n    async insertArticle(article, position) {\n        if (position.parentId) {\n            const parent = this.getArticle(position.parentId);\n            if (parent) {\n                // Make sure the existing children are loaded if parent has any\n                if (!this.unfoldedArticlesIds.has(parent.id)) {\n                    await this.unfold(parent.id, false);\n                    if (parent.child_ids.includes(article.id)) {\n                        return;\n                    }\n                }\n                // Position it at the right position w.r. to the other children\n                // if the parent is not yet aware of the child article (eg when\n                // moving, the article is moved in frontend, then if the user\n                // confirms, the article is moved in backend)\n                if (position.beforeArticleId) {\n                    parent.child_ids.splice(parent.child_ids.indexOf(position.beforeArticleId), 0, article.id);\n                } else {\n                    parent.child_ids.push(article.id);\n                }\n                parent.has_article_children = true;\n            }\n        } else {\n            // Add article in the list of articles of the new category, at the right position\n            const categoryIds = this.getCategoryIds(position.category);\n            if (categoryIds) {\n                if (position.beforeArticleId) {\n                    categoryIds.splice(categoryIds.indexOf(position.beforeArticleId), 0, article.id);\n                } else {\n                    categoryIds.push(article.id);\n                }\n            }\n        }\n    }\n\n    /**\n     * Check if the given article is an ancestor of the active one.\n     * @param {integer} articleId\n     * @returns {Boolean}\n     */\n    isAncestor(articleId) {\n        let article = this.getArticle(this.props.record.resId);\n        while (article) {\n            if (article.id === articleId) {\n                return true;\n            }\n            article = this.getArticle(article.parent_id);\n        }\n        return false;\n    }\n\n    /**\n     * Load the articles to show in the sidebar and store them in the state.\n     * One loops through the articles fetched to create a mapping id:article\n     * that allows easy access of the articles, add the articles in their correct categories\n     * and add their children. One uses the parent_id field to fill the\n     * child_ids arrays because a simple read of the child_ids field would\n     * return items (which should not be included in the sidebar), and the\n     * articles would not be sorted correctly.\n     */\n    async loadArticles() {\n        this.state.loading = true;\n        // Remove already loaded articles\n        Object.assign(this.state, {\n            articles: {},\n            favoriteIds: [],\n            workspaceIds: [],\n            sharedIds: [],\n            privateIds: [],\n        });\n        const res = await this.orm.call(\n            this.props.record.resModel,\n            \"get_sidebar_articles\",\n            [this.props.record.resId],\n            { unfolded_ids: [...this.unfoldedArticlesIds, ...this.unfoldedFavoritesIds] }\n        );\n        const children = {};\n        for (const article of res.articles) {\n            this.state.articles[article.id] = {\n                ...article,\n                child_ids: children[article.id] ? children[article.id] : [],\n            };\n            // Items could be shown in the favorite tree as root articles, but\n            // they should not be shown as children of other articles\n            if (!article.is_article_item) {\n                if (article.parent_id) {\n                    const parent = this.getArticle(article.parent_id);\n                    if (parent) {\n                        parent.child_ids.push(article.id);\n                    } else {\n                        // Store children temporarily to add them to the parent\n                        // when the parent will be added to the state in this loop.\n                        if (children[article.parent_id]) {\n                            children[article.parent_id].push(article.id);\n                        } else {\n                            children[article.parent_id] = [article.id];\n                        }\n                    }\n                } else {\n                    this.getCategoryIds(article.category).push(article.id);\n                }\n            }\n        }\n        const ancestorRootArticleId = res.active_article_accessible_root_id;\n        if (ancestorRootArticleId) {\n            this.getCategoryIds(this.getArticle(ancestorRootArticleId).category).push(\n                ancestorRootArticleId\n            );\n        }\n        this.state.favoriteIds = res.favorite_ids;\n        this.showArticle(this.getArticle(this.props.record.resId));\n        this.state.loading = false;\n        this.resetUnfoldedArticles();\n    }\n\n    /**\n     * Load the children of a given article\n     * @param {object} article\n     */\n    async loadChildren(article) {\n        const children = await this.orm.searchRead(\n            this.props.record.resModel,\n            [['parent_id', '=', article.id], ['is_article_item', '=', false]],\n            ['name', 'icon', 'is_locked', 'user_can_write', 'has_article_children'],\n            {\n                'load': 'None',\n                'order': 'sequence, id',\n            }\n        );\n        for (const child of children) {\n            article.child_ids.push(child.id);\n            if (this.getArticle(child.id)) {\n                // Article was already loaded (if it is in the favorites)\n                continue;\n            }\n            this.state.articles[child.id] = {\n                ...child,\n                parent_id: article.id,\n                child_ids: [],\n                category: article.category,\n                is_article_item: false,\n                is_user_favorite: false,\n            };\n        }\n    }\n\n    /**\n     * Try to move the given article to the given position (change its parent/\n     * category/sequence) and update its position in the sidebar.\n     * If the move will change the permissions of the article, show a\n     * confirmation dialog.\n     * @param {Object} article\n     * @param {Object} currentPosition\n     * @param {integer} position.beforeArticleId\n     * @param {String} position.category\n     * @param {integer} position.parentId\n     * @param {Object} newPosition\n     * @param {integer} newPosition.beforeArticleId\n     * @param {String} newPosition.category\n     * @param {integer} newPosition.parentId\n     */\n    async moveArticle(article, currentPosition, newPosition) {\n        const confirmMove = async (article, position) => {\n            if (await this.props.record.isDirty()) {\n                await this.props.record.save();\n            }\n            try {\n                await this.orm.call(\n                    this.props.record.resModel,\n                    'move_to',\n                    [article.id],\n                    {\n                        category: position.category,\n                        parent_id: position.parentId,\n                        before_article_id: position.beforeArticleId,\n                    }\n                );\n            } catch (error) {\n                // Reload the article tree to show potential modifications\n                // done by another user that could cause the failure.\n                this.loadArticles();\n                throw error;\n            }\n            // Reload the current article as the move will impact its data\n            await this.props.record.load();\n        };\n\n        // Move the article in the sidebar\n        this.repositionArticle(article, newPosition);\n        // Permissions won't change, no need to ask for confirmation\n        if (currentPosition.category === newPosition.category) {\n            confirmMove(article, newPosition);\n        } else {\n            // Show confirmation dialog, and move article back to its original\n            // position if the user cancels the move\n            const emoji = article.icon || '';\n            const name = article.name;\n            let message;\n            let confirmLabel;\n            if (newPosition.category === 'workspace') {\n                message = _t(\n                    'Are you sure you want to move \"%(icon)s%(title)s\" to the Workspace? It will be shared with all internal users.',\n                    { icon: emoji, title: name || _t(\"Untitled\") }\n                );\n                confirmLabel = _t(\"Move to Workspace\");\n            } else if (newPosition.category === 'private') {\n                message = _t(\n                    'Are you sure you want to move \"%(icon)s%(title)s\" to private? Only you will be able to access it.',\n                    { icon: emoji, title: name || _t(\"Untitled\") }\n                );\n                confirmLabel = _t(\"Move to Private\");\n            } else if (newPosition.category === 'shared') {\n                if (newPosition.parentId) {\n                    const parent = this.getArticle(newPosition.parentId);\n                    message = _t(\n                        'Are you sure you want to move \"%(icon)s%(title)s\" under \"%(parentIcon)s%(parentTitle)s\"? It will be shared with the same persons.',\n                        {\n                            icon: emoji,\n                            title: name || _t(\"Untitled\"),\n                            parentIcon: parent.icon || \"\",\n                            parentTitle: parent.name || _t(\"Untitled\"),\n                        }\n                    );\n                } else {\n                    message = _t(\n                        'Are you sure you want to move \"%(icon)s%(title)s\" to the Shared section? It will be shared with all listed members.',\n                        { icon: emoji, title: name || _t(\"Untitled\") }\n                    );\n                }\n                confirmLabel = _t('Move to Shared')\n            }\n            this.dialog.add(ConfirmationDialog, {\n                body: message,\n                confirmLabel: confirmLabel,\n                confirm: () => confirmMove(article, newPosition),\n                cancel: () => {\n                    // Move article back to its position\n                    this.repositionArticle(article, currentPosition);\n                },\n            });\n        }\n    }\n\n    /**\n     * Move an article to the trash, and remove it from the sidebar.\n     * @param {Object} article\n     */\n    async moveToTrash(article) {\n        try {\n            await this.orm.call(\n                \"knowledge.article\",\n                \"action_send_to_trash\",\n                [article.id],\n            );\n        } catch {\n            await this.loadArticles();\n            return;\n        }\n        // If the article moved to the trash is an ancestor of the active\n        // article, redirect to first accessible article.\n        if (this.isAncestor(article.id)) {\n            this.actionService.doAction(\n                await this.orm.call('knowledge.article', 'action_home_page', [false]),\n                {stackPosition: 'replaceCurrentAction'}\n            );\n        } else {\n            this.removeArticle(article);\n            this.removeFavorite(article);\n        }\n    }\n\n    /**\n     * Open the command palette if the user is an internal user, and open the\n     * article selection dialog if the user is a portal user\n     */\n    onSearchBarClick() {\n        // Ensure that if the command palette is already open, it is forcibly\n        // closed to be open again with the search configuration.\n        this.dialog.closeAll();\n        if (this.isInternalUser) {\n            this.env.services.command.openMainPalette({ searchValue: \"?\" });\n        } else {\n            this.dialog.add(ArticleSearchDialog, {\n                search: (searchValue) => {\n                    const params = { search_query: searchValue };\n                    let searchFunction = \"get_user_sorted_articles\";\n                    if (searchValue) {\n                        searchFunction = \"get_sorted_articles\";\n                        params.domain = this.searchDomain;\n                    }\n                    return this.orm.call(\"knowledge.article\", searchFunction, [[]], params);\n                },\n                select: (article) => this.env.openArticle(article.id),\n            });\n        }\n    }\n\n    get searchDomain() {\n        return [\"|\", [\"is_article_visible\", \"=\", true], [\"is_user_favorite\", \"=\", true]];\n    }\n\n    /**\n     * Show a dialog explaining why the given article cannot be moved to the\n     * target position.\n     * @param {article}\n     * @param {parentId}\n     */\n    rejectDrop(article, parentId) {\n        let message;\n        if (parentId) {\n            const parent = this.getArticle(parentId);\n            message = _t(\n                'Could not move \"%(icon)s%(title)s\" under \"%(parentIcon)s%(parentTitle)s\", because you do not have write permission on the latter.',\n                {\n                    icon: article.icon || \"\",\n                    title: article.name,\n                    parentIcon: parent.icon || \"\",\n                    parentTitle: parent.name,\n                }\n            );\n        } else {\n            message = _t('You need at least 2 members for the Article to be shared.');\n        }\n        this.dialog.add(ConfirmationDialog, {\n            confirmLabel: _t(\"Close\"),\n            title: _t(\"Move cancelled\"),\n            body: message,\n        });\n    }\n\n    /**\n     * Remove the given article from the sidebar.\n     * @param {Object} article\n     */\n    removeArticle(article) {\n        if (article.parent_id) {\n            // Remove article from array of children of its parent\n            const parent = this.getArticle(article.parent_id);\n            if (parent) {\n                const articleIdx = parent.child_ids.indexOf(article.id);\n                if (articleIdx !== -1) {\n                    parent.child_ids.splice(parent.child_ids.indexOf(article.id), 1);\n                    // Removed last child of the parent article\n                    if (!parent.child_ids.length) {\n                        this.fold(parent.id);\n                        this.fold(parent.id, true);\n                        parent.has_article_children = false;\n                    }\n                }\n            }\n        } else {\n            // Remove article from list of articles category\n            const categoryIds = this.getCategoryIds(article.category);\n            const articleIdx = categoryIds.indexOf(article.id);\n            if (articleIdx !== -1) {\n                categoryIds.splice(articleIdx, 1);\n            }\n        }\n    }\n\n    /**\n     * Remove the given article from the list of favorites.\n     * @param {Object} article\n     */\n    removeFavorite(article) {\n        const favoriteIdx = this.state.favoriteIds.indexOf(article.id);\n        if (favoriteIdx !== -1) {\n            this.state.favoriteIds.splice(favoriteIdx, 1);\n        }\n     }\n\n    /**\n     * Change the position of an article in the sidebar.\n     * @param {Object} article\n     * @param {Object} position\n     * @param {integer} position.beforeArticleId\n     * @param {String} position.category\n     * @param {integer} position.parentId\n     */\n    async repositionArticle(article, position) {\n        this.removeArticle(article);\n        await this.insertArticle(article, position);\n        // Change the parent of the article\n        if (article.parent_id !== position.parentId) {\n            article.parent_id = position.parentId;\n        }\n        // Change category of article and every descendant\n        if (article.category !== position.category) {\n            this.changeCategory(article, position.category);\n        }\n        // Make sure the article is visible\n        this.showArticle(article);\n    }\n\n    /**\n     * Updates the sequence of favorite articles for the current user.\n     * @param {integer} articleId - Id of the moved favorite article\n     * @param {integer} beforeId - Id of the favorite article after\n     *      which the article is moved\n     */\n    resequenceFavorites(articleId, beforeId) {\n        this.state.favoriteIds.splice(this.state.favoriteIds.indexOf(articleId), 1);\n        if (beforeId) {\n            this.state.favoriteIds.splice(this.state.favoriteIds.indexOf(beforeId), 0, articleId);\n        } else {\n            this.state.favoriteIds.push(articleId);\n        }\n        this.orm.call(\"knowledge.article.favorite\", \"resequence_favorites\", [false], {\n            article_ids: this.state.favoriteIds,\n        });\n    }\n\n    /**\n     * User could have unfolded ids in its local storage of articles that are\n     * not shown in its sidebar anymore (trashed, converted to items, hidden,\n     * permission change). This method will reset the list of ids in the local\n     * storage using only the articles that are shown to the user, so that we\n     * do not load the articles using a list containing a lot of useless ids.\n     */\n    resetUnfoldedArticles() {\n        this.unfoldedArticlesIds.forEach(id => {\n            if (!this.getArticle(id)) {\n                this.unfoldedArticlesIds.delete(id);\n            }\n        });\n        this.unfoldedFavoritesIds.forEach(id => {\n            if (!this.getArticle(id)) {\n                this.unfoldedFavoritesIds.delete(id);\n            }\n        });\n    }\n\n    /**\n     * Resize the sidebar horizontally.\n     */\n    resize() {\n        const isRtl = localization.direction === \"rtl\";\n        const onPointerMove = throttleForAnimation(event => {\n            event.preventDefault();\n            this.state.sidebarSize = isRtl ? document.documentElement.clientWidth - event.pageX : event.pageX;\n            browser.dispatchEvent(new Event(\"resize\"));\n        });\n        const onPointerUp = () => {\n            document.removeEventListener('pointermove', onPointerMove);\n            this.state.resizing = false;\n            document.body.style.cursor = \"auto\";\n            document.body.style.userSelect = \"auto\";\n            localStorage.setItem(this.storageKeys.size, this.state.sidebarSize);\n            browser.dispatchEvent(new Event(\"resize\"));\n        };\n        // Add style to root element because resizing has a transition delay,\n        // meaning that the cursor is not always on top of the resizer.\n        document.body.style.cursor = \"col-resize\";\n        document.body.style.userSelect = \"none\";\n        this.state.resizing = true;\n        document.addEventListener('pointermove', onPointerMove);\n        document.addEventListener('pointerup', onPointerUp, {once: true});\n    }\n\n    /**\n     * Make sure the given article is shown in the sidebar by unfolding every\n     * article until a root article is met.\n     * @param {object} article - article to show in the sidebar\n     */\n    showArticle(article) {\n        while (article && article.parent_id && article.parent_id in this.state.articles) {\n            // Unfold in the main tree, without loading the children\n            this.unfold(article.parent_id, false);\n            article = this.getArticle(article.parent_id);\n        }\n    }\n\n    /** Unfold an article.\n     * @param {integer} articleId: id of article\n     * @param {boolean} isFavorite: whether to unfold in favorite tree\n     */\n    async unfold(articleId, isFavorite) {\n        const article = this.getArticle(articleId);\n        // Load the children of the article if it has not been unfolded yet\n        if (article.has_article_children && !article.child_ids.length) {\n            await this.loadChildren(article);\n        }\n        if (isFavorite) {\n            this.unfoldedFavoritesIds.add(articleId);\n        } else {\n            this.unfoldedArticlesIds.add(articleId);\n        }\n    }\n}\n", "import { useService } from \"@web/core/utils/hooks\";\n\nimport { Component, onWillUpdateProps, useState } from \"@odoo/owl\";\n\n/**\n * The SidebarRow component is responsible of displaying an article (and its\n * children recursively) in a section of the sidebar, and modifying the record\n * of the article.\n */\nexport class KnowledgeSidebarRow extends Component {\n    static props = {\n        article: Object,\n        unfolded: Boolean,\n        unfoldedIds: Set,\n        record: Object,\n    };\n    static template = \"knowledge.SidebarRow\";\n    static components = {\n        KnowledgeSidebarRow\n    };\n\n    setup() {\n        super.setup();\n        this.orm = useService(\"orm\");\n\n        this.state = useState({\n            unfolded: false,\n        });\n\n        onWillUpdateProps(nextProps => {\n            // Remove the loading spinner when the article is rendered as\n            // being unfolded\n            if (this.state.loading && nextProps.unfolded === true) {\n                this.state.loading = false;\n            }\n        });\n    }\n\n    get hasChildren() {\n        return this.props.article.has_article_children;\n    }\n\n    get isActive() {\n        return this.props.record.resId === this.props.article.id;\n    }\n\n    get isLocked() {\n        return this.props.article.is_locked;\n    }\n\n    get isReadonly() {\n        return !this.props.article.user_can_write;\n    }\n\n    /**\n     * Create a new child article for the row's article.\n     */\n    createChild() {\n        this.env.createArticle(this.props.article.category, this.props.article.id);\n    }\n\n    /**\n     * (Un)fold the row\n     */\n    onCaretClick() {\n        if (this.props.unfolded) {\n            this.env.fold(this.props.article.id);\n        } else if (!this.state.loading) {\n            this.state.loading = true;\n            // If there are a lot of articles, make sure the rendering caused\n            // by the state change and the one cause by the prop update are not\n            // done at once, because otherwise the loader will not be shown.\n            // If there are not too much articles, the renderings can be done\n            // at once so that there is no flickering.\n            if (this.props.article.child_ids.length > 500) {\n                setTimeout(() => this.env.unfold(this.props.article.id), 0);\n            } else {\n                this.env.unfold(this.props.article.id);\n            }\n        }\n    }\n\n    /**\n     * Open the row's article\n     */\n    onNameClick() {\n        this.env.openArticle(this.props.article.id);\n    }\n}\n", "import { KnowledgeSidebarRow } from \"./sidebar_row\";\nimport { browser } from \"@web/core/browser/browser\";\nimport { user } from \"@web/core/user\";\nimport { useRecordObserver } from \"@web/model/relational_model/utils\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { Component, onWillStart, useChildSubEnv, useState } from \"@odoo/owl\";\n\n/**\n * This file defines the different sections used in the sidebar.\n * Each section is responsible of displaying an array of root articles and\n * their children.\n */\n\nexport class KnowledgeSidebarSection extends Component {\n    static template = \"\";\n    static props = {\n        rootIds: Array,\n        unfoldedIds: Set,\n        record: Object,\n    };\n    static components = {\n        KnowledgeSidebarRow,\n    };\n\n    setup() {\n        super.setup();\n        const foldedSections = JSON.parse(browser.localStorage.getItem(\"knowledge.folded.sections\") ?? \"{}\");\n        this.state = useState({\n            isFolded: foldedSections[this.getSectionIdentifier()] || this.props.rootIds.length === 0,\n        });\n\n        // Unfold the section of the current article:\n        useRecordObserver((record) => {\n            if (record.data.category === this.getSectionIdentifier()) {\n                this.state.isFolded = false;\n            }\n        });\n\n        onWillStart(async () => {\n            this.isInternalUser = await user.hasGroup('base.group_user');\n        });\n    }\n\n    toggleSidebarSection() {\n        this.state.isFolded = !this.state.isFolded;\n        // Persist the new folding state in local storage:\n        const foldedSections = JSON.parse(browser.localStorage.getItem(\"knowledge.folded.sections\") ?? \"{}\");\n        foldedSections[this.getSectionIdentifier()] = this.state.isFolded;\n        browser.localStorage.setItem(\"knowledge.folded.sections\", JSON.stringify(foldedSections));\n    }\n}\n\nexport class KnowledgeSidebarFavoriteSection extends KnowledgeSidebarSection {\n    static template = \"knowledge.SidebarFavoriteSection\";\n\n    setup() {\n        super.setup();\n\n        // (Un)fold in the favorite tree by default.\n        useChildSubEnv({\n            fold: id => this.env.fold(id, true),\n            unfold: id => this.env.unfold(id, true),\n        });\n    }\n\n    getSectionIdentifier() {\n        return \"favorites\";\n    }\n}\n\nexport class KnowledgeSidebarWorkspaceSection extends KnowledgeSidebarSection {\n    static template = \"knowledge.SidebarWorkspaceSection\";\n\n    setup() {\n        super.setup();\n        this.command = useService(\"command\");\n    }\n\n    createRoot() {\n        this.env.createArticle(\"workspace\");\n    }\n\n    searchHiddenArticle() {\n        this.command.openMainPalette({searchValue: \"$\"});\n    }\n\n    getSectionIdentifier() {\n        return \"workspace\";\n    }\n}\n\nexport class KnowledgeSidebarSharedSection extends KnowledgeSidebarSection {\n    static template = \"knowledge.SidebarSharedSection\";\n\n    getSectionIdentifier() {\n        return \"shared\";\n    }\n}\n\nexport class KnowledgeSidebarPrivateSection extends KnowledgeSidebarSection {\n    static template = \"knowledge.SidebarPrivateSection\";\n\n    createRoot() {\n        this.env.createArticle(\"private\");\n    }\n\n    getSectionIdentifier() {\n        return \"private\";\n    }\n}\n", "import KnowledgeHierarchy from \"@knowledge/components/hierarchy/hierarchy\";\nimport { OptionsDropdown } from \"@knowledge/components/options_dropdown/options_dropdown\";\nimport { PermissionPanel } from \"@knowledge/components/permission_panel/permission_panel\";\nimport { PermissionPanelDialog } from \"../permission_panel_dialog/permission_panel_dialog\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { usePopover } from \"@web/core/popover/popover_hook\";\nimport { registry } from \"@web/core/registry\";\nimport { user } from \"@web/core/user\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { useRecordObserver } from \"@web/model/relational_model/utils\";\nimport { standardWidgetProps } from \"@web/views/widgets/standard_widget_props\";\n\nimport { Component, onWillStart, reactive, useState } from \"@odoo/owl\";\n\nclass KnowledgeTopbar extends Component {\n    static template = \"knowledge.KnowledgeTopbar\";\n    static props = standardWidgetProps;\n    static components = {\n        KnowledgeHierarchy,\n        OptionsDropdown,\n    };\n\n    setup() {\n        super.setup();\n\n        this.dialog = useService(\"dialog\");\n        this.permissionPopover = usePopover(PermissionPanel, {\n            closeOnClickAway: true,\n            arrow: false,\n            onClose: () => (this.state.shareBtnIsActive = false),\n            position: \"bottom-end\",\n        });\n        this.state = useState({\n            shareBtnIsActive: false,\n        });\n        this.commentsState = useState(this.env.commentsState);\n        this.chatterPanelState = useState(this.env.chatterPanelState);\n        this.propertiesPanelState = useState(this.env.propertiesPanelState);\n\n        onWillStart(async () => {\n            this.isInternalUser = await user.hasGroup(\"base.group_user\");\n        });\n\n        this.reactiveRecordWrapper = reactive({ record: this.props.record });\n        useRecordObserver((record) => {\n            this.reactiveRecordWrapper.record = record;\n        });\n    }\n\n    get chatterButtonTitle() {\n        return this.chatterPanelState.isDisplayed\n            ? _t(\"Close chatter panel\")\n            : _t(\"Open chatter panel\");\n    }\n\n    get commentButtonTitle() {\n        return this.commentsState.isDisplayed\n            ? _t(\"Close comments panel\")\n            : _t(\"Open comments panel\");\n    }\n\n    get favoriteButtonTitle() {\n        return this.props.record.data.is_user_favorite\n            ? _t(\"Remove from favorites\")\n            : _t(\"Add to favorites\");\n    }\n\n    togglePermissionPanel(event) {\n        if (this.env.isSmall && !this.state.shareBtnIsActive) {\n            this.dialog.add(PermissionPanelDialog, {\n                reactiveRecordWrapper: this.reactiveRecordWrapper,\n                openArticle: this.env.openArticle,\n                sendArticleToTrash: this.env.sendArticleToTrash\n            });\n        } else if (this.permissionPopover.isOpen) {\n            this.permissionPopover.close();\n        } else {\n            if (this.props.record.dirty) {\n                this.props.record.save();\n            }\n            this.permissionPopover.open(event.currentTarget, {\n                reactiveRecordWrapper: this.reactiveRecordWrapper,\n                openArticle: this.env.openArticle,\n                sendArticleToTrash: this.env.sendArticleToTrash\n            });\n            this.state.shareBtnIsActive = true;\n        }\n    }\n}\n\nexport const knowledgeTopbar = {\n    component: KnowledgeTopbar,\n    fieldDependencies: [\n        { name: \"create_uid\", type: \"many2one\", relation: \"res.users\" },\n        { name: \"display_name\", type: \"char\" },\n        { name: \"last_edition_uid\", type: \"many2one\", relation: \"res.users\" },\n        { name: \"active\", type: \"boolean\" },\n        { name: \"article_properties\", type: \"jsonb\" },\n        { name: \"cover_image_id\", type: \"many2one\", relation: \"knowledge.cover\" },\n        { name: \"full_width\", type: \"boolean\" },\n        { name: \"icon\", type: \"char\" },\n        { name: \"inherited_permission\", type: \"char\"},\n        { name: \"inherited_permission_parent_id\", type: \"many2one\", relation: \"knowledge.article\"},\n        { name: \"is_article_item\", type: \"boolean\" },\n        { name: \"is_locked\", type: \"boolean\" },\n        { name: \"is_desynchronized\", type: \"boolean\"},\n        { name: \"is_user_favorite\", type: \"boolean\" },\n        { name: \"name\", type: \"char\" },\n        { name: \"parent_id\", type: \"char\" },\n        { name: \"parent_path\", type: \"char\" },\n        { name: \"root_article_id\", type: \"many2one\", relation: \"knowledge.article\" },\n        { name: \"has_item_parent\", type: \"boolean\" },\n        { name: \"is_listed_in_templates_gallery\", type: \"boolean\" },\n        { name: \"to_delete\", type: \"boolean\" },\n        { name: \"user_can_write\", type: \"boolean\" },\n    ],\n};\n\nregistry.category(\"view_widgets\").add(\"knowledge_topbar\", knowledgeTopbar);\n", "import { Component, onMounted, onWillUnmount, useRef, useState } from \"@odoo/owl\";\nimport { setIntersectionObserver } from \"@knowledge/js/knowledge_utils\";\n\nexport class WithLazyLoading extends Component {\n    static template = \"knowledge.WithLazyLoading\";\n    static props = {\n        slots: { type: Object },\n    };\n\n    setup() {\n        this.loader = useRef(\"loader\");\n        this.state = useState({ isLoaded: false });\n\n        onMounted(() => {\n            const { el } = this.loader;\n            this.observer = setIntersectionObserver(el, () => {\n                this.state.isLoaded = true;\n            });\n        });\n\n        onWillUnmount(() => {\n            this.observer.disconnect();\n        });\n    }\n}\n", "import { Component, useSubEnv, xml } from \"@odoo/owl\";\n\n/**\n * Loads the given props to the environment of the nested components.\n *\n * Usage:\n * <WithSubEnv prop1=\"value1\" prop2=\"value2\">\n *    <MyComponent/>\n * </WithSubEnv>\n *\n * MyComponent will then be able to access `prop1` and `prop2` from its environment.\n * (i.e: with `this.env.prop1` and `this.env.prop2`)\n */\nexport class WithSubEnv extends Component {\n    static template = xml`<t t-slot=\"default\"/>`;\n    static props = [\"*\"];\n    setup() {\n        useSubEnv(this.props);\n    }\n}\n", "import { Component, onWillStart } from \"@odoo/owl\";\nimport { parseHTML } from \"@html_editor/utils/html\";\nimport { ArticleTemplatePickerDialog } from \"@knowledge/components/article_template_picker_dialog/article_template_picker_dialog\";\nimport { ItemCalendarPropsDialog } from \"@knowledge/components/item_calendar_props_dialog/item_calendar_props_dialog\";\nimport { PromptEmbeddedViewNameDialog } from \"@knowledge/components/prompt_embedded_view_name_dialog/prompt_embedded_view_name_dialog\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { user } from \"@web/core/user\";\nimport { renderToFragment } from \"@web/core/utils/render\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { HtmlUpgradeManager } from \"@html_editor/html_migrations/html_upgrade_manager\";\n\nexport class WysiwygArticleHelper extends Component {\n    static template = \"knowledge.WysiwygArticleHelper\";\n    static props = {\n        editor: { type: Object },\n        isVisible: { type: Boolean },\n        record: { type: Object },\n    };\n\n    setup() {\n        this.actionService = useService(\"action\");\n        this.dialogService = useService(\"dialog\");\n        this.orm = useService(\"orm\");\n        onWillStart(async () => {\n            this.isPortalUser = await user.hasGroup(\"base.group_portal\");\n        });\n    }\n\n    /** @param {string} body */\n    replaceCurrentArticleBodyWith(body) {\n        let newBody = new HtmlUpgradeManager().processForUpgrade(body);\n        newBody = parseHTML(this.props.editor.document, body);\n        newBody = this.props.editor.shared.sanitize.sanitize(newBody);\n        this.props.editor.editable.replaceChildren(newBody);\n        this.props.editor.shared.selection.setCursorEnd(this.props.editor.editable);\n        this.props.editor.shared.history.addStep();\n    }\n\n    async onLoadTemplateBtnClick() {\n        const { articles, templates } = await this.orm.call(\n            \"knowledge.article\",\n            \"get_available_templates\"\n        );\n        this.dialogService.add(ArticleTemplatePickerDialog, {\n            articles: articles,\n            templates: templates,\n            /** @param {integer} articleId */\n            onLoadArticle: async (articleId) => {\n                const body = await this.orm.call(\n                    \"knowledge.article\",\n                    \"apply_article_as_template\",\n                    [this.props.record.resId],\n                    {\n                        article_id: articleId\n                    }\n                );\n                this.replaceCurrentArticleBodyWith(body);\n                await this.actionService.doAction(\n                    \"knowledge.ir_actions_server_knowledge_home_page\",\n                    {\n                        stackPosition: \"replaceCurrentAction\",\n                        additionalContext: {\n                            res_id: this.props.record.resId,\n                        },\n                    }\n                );\n            },\n            /** @param {integer} templateId */\n            onLoadTemplate: async (templateId) => {\n                const body = await this.orm.call(\n                    \"knowledge.article\",\n                    \"apply_template\",\n                    [this.props.record.resId],\n                    {\n                        template_id: templateId,\n                        skip_body_update: true,\n                    }\n                );\n                this.replaceCurrentArticleBodyWith(body);\n                // TODO: apply_template could return all modified values on the current\n                // article and record.update would reload related components\n                await this.actionService.doAction(\n                    \"knowledge.ir_actions_server_knowledge_home_page\",\n                    {\n                        stackPosition: \"replaceCurrentAction\",\n                        additionalContext: {\n                            res_id: this.props.record.resId,\n                        },\n                    }\n                );\n            },\n            /** @param {integer} articleId */\n            onDeleteArticle: async (articleId) => {\n                if (this.props.record.resId === articleId) {\n                    await this.props.record.save();\n                }\n                await this.orm.write(\"knowledge.article\", [articleId], {\n                    is_listed_in_templates_gallery: false,\n                });\n                if (this.props.record.resId === articleId) {\n                    this.props.record.load();\n                }\n            },\n            /** @param {integer} templateId */\n            onDeleteTemplate: async (templateId) => {\n                await this.orm.unlink(\"knowledge.article\", [templateId]);\n            },\n        });\n    }\n\n    onBuildItemCalendarBtnClick() {\n        this.dialogService.add(ItemCalendarPropsDialog, {\n            isNew: true,\n            knowledgeArticleId: this.props.record.resId,\n            saveItemCalendarProps: async (name, itemCalendarProps) => {\n                const title = name || _t(\"Article Items\");\n                const displayName = name\n                    ? _t(\"Calendar of %s\", name)\n                    : _t(\"Calendar of Article Items\");\n                const embeddedProps = {\n                    viewProps: {\n                        additionalViewProps: { itemCalendarProps },\n                        actionXmlId: \"knowledge.knowledge_article_action_item_calendar\",\n                        displayName: displayName,\n                        viewType: \"calendar\",\n                        context: {\n                            active_id: this.props.record.resId,\n                            default_parent_id: this.props.record.resId,\n                            default_is_article_item: true,\n                        },\n                    },\n                };\n                const fragment = renderToFragment(\"knowledge.ArticleItemTemplate\", {\n                    embeddedProps: JSON.stringify(embeddedProps),\n                });\n                this.props.editor.editable.replaceChildren(...fragment.children);\n                this.props.editor.shared.selection.setCursorEnd(this.props.editor.editable);\n                this.props.editor.shared.history.addStep();\n                this.props.record.update({ name: title });\n            },\n        });\n    }\n\n    onBuildItemKanbanBtnClick() {\n        this.dialogService.add(PromptEmbeddedViewNameDialog, {\n            isNew: true,\n            viewType: \"kanban\",\n            /**\n             * @param {string} name\n             */\n            save: async (name) => {\n                const embeddedProps = {\n                    viewProps: {\n                        actionXmlId: \"knowledge.knowledge_article_item_action_stages\",\n                        displayName: name\n                            ? _t(\"Kanban of %s\", name)\n                            : _t(\"Kanban of Article Items\"),\n                        viewType: \"kanban\",\n                        context: {\n                            active_id: this.props.record.resId,\n                            default_parent_id: this.props.record.resId,\n                            default_is_article_item: true,\n                        },\n                    },\n                };\n                const title = name || _t(\"Article Items\");\n                await this.orm.call(\"knowledge.article\", \"create_default_item_stages\", [\n                    this.props.record.resId,\n                ]);\n                const fragment = renderToFragment(\"knowledge.ArticleItemTemplate\", {\n                    embeddedProps: JSON.stringify(embeddedProps),\n                });\n                this.props.editor.editable.replaceChildren(...fragment.children);\n                this.props.editor.shared.selection.setCursorEnd(this.props.editor.editable);\n                this.props.editor.shared.history.addStep();\n                this.props.record.update({ name: title });\n            },\n        });\n    }\n\n    onBuildItemListBtnClick() {\n        this.dialogService.add(PromptEmbeddedViewNameDialog, {\n            isNew: true,\n            viewType: \"list\",\n            /**\n             * @param {string} name\n             */\n            save: async (name) => {\n                const embeddedProps = {\n                    viewProps: {\n                        actionXmlId: \"knowledge.knowledge_article_item_action\",\n                        displayName: name ? _t(\"List of %s\", name) : _t(\"List of Article Items\"),\n                        viewType: \"list\",\n                        context: {\n                            active_id: this.props.record.resId,\n                            default_parent_id: this.props.record.resId,\n                            default_is_article_item: true,\n                        },\n                    },\n                };\n                const title = name || _t(\"Article Items\");\n\n                const fragment = renderToFragment(\"knowledge.ArticleItemTemplate\", {\n                    embeddedProps: JSON.stringify(embeddedProps),\n                });\n                this.props.editor.editable.replaceChildren(...fragment.children);\n                this.props.editor.shared.selection.setCursorEnd(this.props.editor.editable);\n                this.props.editor.shared.history.addStep();\n                this.props.record.update({ name: title });\n            },\n        });\n    }\n}\n", "import { onWillStart, useState, useSubEnv, Component } from \"@odoo/owl\";\nimport {\n    getEmbeddedProps,\n    useEmbeddedState,\n    StateChangeManager,\n} from \"@html_editor/others/embedded_component_utils\";\nimport {\n    EmbeddedComponentToolbar,\n    EmbeddedComponentToolbarButton,\n} from \"@html_editor/others/embedded_components/core/embedded_component_toolbar/embedded_component_toolbar\";\nimport { ArticleIndexList } from \"@knowledge/editor/embedded_components/backend/article_index/article_index_list\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { KeepLast } from \"@web/core/utils/concurrency\";\n\nexport class EmbeddedArticleIndexComponent extends Component {\n    static template = \"knowledge.EmbeddedArticleIndex\";\n    static components = {\n        ArticleIndexList,\n        EmbeddedComponentToolbar,\n        EmbeddedComponentToolbarButton,\n    };\n    static props = {\n        host: { type: Object },\n        articles: { type: Object, optional: true },\n        showAllChildren: { type: Boolean, optional: true },\n    };\n\n    setup() {\n        this.orm = useService(\"orm\");\n        this.embeddedState = useEmbeddedState(this.props.host);\n        this.keepLastFetch = new KeepLast();\n        this.state = useState({\n            loading: false,\n            key: 0,\n        });\n\n        useSubEnv({\n            reloadArticleIndex: () => this.loadArticleIndex(),\n        });\n\n        onWillStart(async () => {\n            if (this.embeddedState.articles === undefined) {\n                this.loadArticleIndex({ firstLoad: true });\n            }\n        });\n    }\n\n    /**\n     * @param {integer} resId\n     * @param {Boolean} showAllChildren\n     * @returns {Array[Object]}\n     */\n    async fetchAllArticles(resId, showAllChildren) {\n        const domain = [\n            [\"parent_id\", !showAllChildren ? \"=\" : \"child_of\", resId],\n            [\"is_article_item\", \"=\", false],\n        ];\n        const { records } = await this.orm.webSearchRead(\"knowledge.article\", domain, {\n            specification: {\n                display_name: {},\n                parent_id: {},\n            },\n            order: \"sequence\",\n        });\n        return records;\n    }\n\n    async loadArticleIndex({ showAllChildren = undefined, firstLoad = false } = {}) {\n        this.state.loading = true;\n        if (showAllChildren === undefined) {\n            showAllChildren = this.embeddedState.showAllChildren;\n        }\n        const resId = this.env.model.root.resId;\n        const promise = this.fetchAllArticles(resId, showAllChildren);\n        const articles = await this.keepLastFetch.add(promise);\n        if (firstLoad && this.embeddedState.articles !== undefined) {\n            // Articles were provided by a collaborator before\n            // the first load was finished, discard loaded articles.\n            this.state.loading = false;\n            return;\n        }\n        /**\n         * @param {integer} parentId\n         * @returns {Object}\n         */\n        const buildIndex = (parentId) => {\n            return articles\n                .filter((article) => {\n                    return article.parent_id && article.parent_id === parentId;\n                })\n                .map((article) => {\n                    return {\n                        id: article.id,\n                        name: article.display_name,\n                        childIds: buildIndex(article.id),\n                    };\n                });\n        };\n        this.state.loading = false;\n        this.embeddedState.showAllChildren = showAllChildren;\n        this.embeddedState.articles = buildIndex(resId);\n        this.env.bus.trigger(\"KNOWLEDGE:RELOAD_SIDEBAR\", {});\n    }\n\n    async onSwitchModeBtnClick() {\n        await this.loadArticleIndex({\n            showAllChildren: !this.embeddedState.showAllChildren,\n        });\n        this.state.key++; // restart the `ArticleIndexList` component to update\n                          // the `nest` static option.\n    }\n\n    async openTemplatePicker() {\n        const record = this.env.model.root;\n        const templates = await this.orm.call(\"knowledge.article\", \"get_suggested_templates\", [\n            [record.resId],\n        ]);\n        this.env.bus.trigger(\"KNOWLEDGE:OPEN_ANNEXE_TEMPLATE_PICKER\", {\n            articles: [],\n            templates: templates,\n            onLoadArticle: () => {},\n            /** @param {integer} templateId */\n            onLoadTemplate: async (templateId) => {\n                const articleIds = await this.orm.call(\n                    \"knowledge.article\",\n                    \"load_suggested_template\",\n                    [record.resId, templateId]\n                );\n                if (articleIds?.length) {\n                    await this.loadArticleIndex();\n                    await this.env.openArticle(articleIds[0]);\n                }\n            },\n            onDeleteArticle: () => {},\n            /** @param {integer} templateId */\n            onDeleteTemplate: async (templateId) => {\n                await this.orm.unlink(\"knowledge.article\", [templateId]);\n            },\n        });\n    }\n\n    async addChildToArticle() {\n        const [articleId] = await this.orm.create(\"knowledge.article\", [\n            {\n                parent_id: this.env.model.root.resId,\n            },\n        ]);\n        await this.env.openArticle(articleId);\n    }\n}\n\nexport const articleIndexEmbedding = {\n    name: \"articleIndex\",\n    Component: EmbeddedArticleIndexComponent,\n    getStateChangeManager: (config) => {\n        return new StateChangeManager(config);\n    },\n    getProps: (host) => {\n        return {\n            host,\n            ...getEmbeddedProps(host),\n        };\n    },\n};\n", "import { Component, useRef } from \"@odoo/owl\";\nimport { useNestedSortable } from \"@web/core/utils/nested_sortable\";\nimport { useService } from \"@web/core/utils/hooks\";\n\nexport class ArticleIndexList extends Component {\n    static template = \"knowledge.ArticleIndexList\";\n    static props = {\n        articles: { type: Object },\n        isNested: { type: Boolean, optional: true },\n    };\n\n    setup() {\n        this.orm = useService(\"orm\");\n        this.root = useRef(\"root\");\n\n        useNestedSortable({\n            ref: this.root,\n            elements: \"li\",\n            nest: this.props.isNested,\n            onDrop: async ({ element, next, parent }) => {\n                const options = {\n                    parent_id: parent\n                        ? parseInt(parent.getAttribute(\"data-article-id\"))\n                        : this.env.model.root.resId\n                };\n                if (next) {\n                    options.before_article_id = parseInt(next.getAttribute(\"data-article-id\"));\n                }\n                const id = parseInt(element.getAttribute(\"data-article-id\"));\n                await this.orm.call(\"knowledge.article\", \"move_to\", [id], options);\n                await this.env.reloadArticleIndex();\n            },\n        });\n    }\n\n    /** @param {integer} articleId */\n    openArticle(articleId) {\n        if (this.env.openArticle) {\n            this.env.openArticle(articleId);\n        }\n    }\n\n    /** @param {integer} articleId */\n    async deleteArticle(articleId) {\n        try {\n            await this.orm.call(\"knowledge.article\", \"action_send_to_trash\", [articleId]);\n        } finally {\n            await this.env.reloadArticleIndex();\n        }\n    }\n}\n", "import {\n    getEditableDescendants,\n    getEmbeddedProps,\n} from \"@html_editor/others/embedded_component_utils\";\nimport { isEmptyBlock } from \"@html_editor/utils/dom_info\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { EmbeddedClipboardComponent } from \"@knowledge/editor/embedded_components/core/clipboard/embedded_clipboard\";\nimport { user } from \"@web/core/user\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { renderToElement } from \"@web/core/utils/render\";\nimport { SendAsMessageMacro, UseAsDescriptionMacro } from \"@knowledge/macros/clipboard_macros\";\nimport { markup } from \"@odoo/owl\";\n\nexport class MacrosEmbeddedClipboardComponent extends EmbeddedClipboardComponent {\n    static template = \"knowledge.MacrosEmbeddedClipboard\";\n\n    setup() {\n        super.setup();\n        this.actionService = useService(\"action\");\n        this.dialogService = useService(\"dialog\");\n        this.knowledgeCommandsService = useService(\"knowledgeCommandsService\");\n        this.orm = useService(\"orm\");\n        this.uiService = useService(\"ui\");\n        this.macrosServices = {\n            action: this.actionService,\n            dialog: this.dialogService,\n            ui: this.uiService,\n        };\n        this.targetRecordInfo = this.knowledgeCommandsService.getCommandsRecordInfo();\n        this.htmlFieldTargetMessage = _t(\n            \"Use as %s\",\n            this.targetRecordInfo?.fieldInfo?.string || \"Description\"\n        );\n    }\n\n    //--------------------------------------------------------------------------\n    // TECHNICAL\n    //--------------------------------------------------------------------------\n\n    /**\n     * Create a dataTransfer object with the editable content of the template\n     * block, to be used for a paste event in the editor\n     */\n    createHtmlDataTransfer() {\n        const dataTransfer = new DataTransfer();\n        const content = this.descendants.clipboardContent;\n        const value = `<p></p>${content.innerHTML}<p></p>`;\n        dataTransfer.setData(\"application/vnd.odoo.odoo-editor\", value);\n        return dataTransfer;\n    }\n\n    //--------------------------------------------------------------------------\n    // HANDLERS\n    //--------------------------------------------------------------------------\n\n    /**\n     * Callback function called when the user clicks on the \"Send as Message\" button.\n     * The function executes a macro that opens the latest form view, composes a\n     * new message and attaches the associated file to it.\n     * @param {Event} ev\n     */\n    async onClickSendAsMessage(ev) {\n        const rows = await this.orm.read(\"res.users\", [user.userId], [\"signature\"]);\n        const signature = renderToElement(\"html_editor.Signature\", {\n            signature: markup(rows[0]?.signature || \"\"),\n            signatureClass: \"o-signature-container\",\n        });\n\n        const dataTransfer = new DataTransfer();\n        dataTransfer.setData(\"application/vnd.odoo.odoo-editor\",\n            this.descendants.clipboardContent.innerHTML + (\n                isEmptyBlock(signature) ? \"\" : signature.outerHTML));\n\n        const macro = new SendAsMessageMacro({\n            targetXmlDoc: this.targetRecordInfo.xmlDoc,\n            breadcrumbs: this.targetRecordInfo.breadcrumbs,\n            data: {\n                dataTransfer: dataTransfer,\n            },\n            services: this.macrosServices,\n        });\n        macro.start();\n    }\n\n    /**\n     * Callback function called when the user clicks on the \"Use As ...\" button.\n     * The function executes a macro that opens the latest form view containing\n     * a valid target field (see `KNOWLEDGE_RECORDED_FIELD_NAMES`) and copy/past\n     * the content of the template to it.\n     * @param {Event} ev\n     */\n    onClickUseAsDescription(ev) {\n        const dataTransfer = this.createHtmlDataTransfer();\n        const macro = new UseAsDescriptionMacro({\n            targetXmlDoc: this.targetRecordInfo.xmlDoc,\n            breadcrumbs: this.targetRecordInfo.breadcrumbs,\n            data: {\n                fieldName: this.targetRecordInfo.fieldInfo.name,\n                pageName: this.targetRecordInfo.fieldInfo.pageName,\n                dataTransfer: dataTransfer,\n            },\n            services: this.macrosServices,\n        });\n        macro.start();\n    }\n}\n\nexport const macrosClipboardEmbedding = {\n    name: \"clipboard\",\n    Component: MacrosEmbeddedClipboardComponent,\n    getProps: (host) => {\n        return { host, ...getEmbeddedProps(host) };\n    },\n    getEditableDescendants: getEditableDescendants,\n};\n", "import {\n    getEmbeddedProps,\n    useEmbeddedState,\n    StateChangeManager,\n} from \"@html_editor/others/embedded_component_utils\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { usePopover } from \"@web/core/popover/popover_hook\";\nimport { EmbeddedViewLinkPopover } from \"@knowledge/editor/embedded_components/backend/embedded_view_link/embedded_view_link_popover\";\nimport { EmbeddedViewLinkEditDialog } from \"@knowledge/editor/embedded_components/backend/embedded_view_link/embedded_view_link_edit_dialog\";\nimport { ReadonlyViewLinkComponent } from \"@knowledge/editor/embedded_components/backend/embedded_view_link/readonly_embedded_view_link\";\n\nexport class EmbeddedViewLinkComponent extends ReadonlyViewLinkComponent {\n    static template = \"knowledge.EmbeddedViewLink\";\n    static props = {\n        ...ReadonlyViewLinkComponent.props,\n        host: { type: Object },\n        copyViewLink: { type: Function },\n        removeViewLink: { type: Function },\n    };\n\n    setup() {\n        this.actionService = useService(\"action\");\n        this.dialogService = useService(\"dialog\");\n        this.notificationService = useService(\"notification\");\n        this.popover = usePopover(EmbeddedViewLinkPopover, {\n            popoverClass: \"o_edit_menu_popover\",\n        });\n        this.embeddedState = useEmbeddedState(this.props.host);\n    }\n\n    set displayName(value) {\n        this.embeddedState.displayName = value;\n    }\n\n    get displayName() {\n        return this.embeddedState.displayName;\n    }\n\n    set linkStyle(value) {\n        this.embeddedState.linkStyle = value;\n    }\n\n    get linkStyle() {\n        return this.embeddedState.linkStyle;\n    }\n\n    onCopyViewLinkClick() {\n        this.props.copyViewLink();\n        this.popover.close();\n    }\n\n    onEditViewLinkClick() {\n        this.dialogService.add(EmbeddedViewLinkEditDialog, {\n            name: this.displayName || \"\",\n            style: this.linkStyle,\n            onSave: (name, style) => {\n                this.displayName = name;\n                this.linkStyle = style;\n            },\n        });\n        this.popover.close();\n    }\n\n    onRemoveViewLinkClick() {\n        this.props.removeViewLink(this.displayName || \"\");\n        this.popover.close();\n    }\n\n    openEmbeddedViewLinkPopover() {\n        this.popover.open(this.props.host, {\n            name: this.displayName,\n            openViewLink: this.openViewLink.bind(this),\n            onCopyViewLinkClick: this.onCopyViewLinkClick.bind(this),\n            onEditViewLinkClick: this.onEditViewLinkClick.bind(this),\n            onRemoveViewLinkClick: this.onRemoveViewLinkClick.bind(this),\n        });\n    }\n}\n\nexport const viewLinkEmbedding = {\n    name: \"viewLink\",\n    Component: EmbeddedViewLinkComponent,\n    getProps: (host) => {\n        return { host, ...getEmbeddedProps(host) };\n    },\n    getStateChangeManager: (config) => {\n        return new StateChangeManager(\n            Object.assign(config, {\n                getEmbeddedState: (host) => {\n                    const props = getEmbeddedProps(host);\n                    return {\n                        displayName: props.viewProps.displayName,\n                        linkStyle: props.linkStyle || \"link\",\n                    };\n                },\n                stateToEmbeddedProps: (host, state) => {\n                    const props = getEmbeddedProps(host);\n                    props.viewProps.displayName = state.displayName;\n                    props.linkStyle = state.linkStyle;\n                    return props;\n                },\n            })\n        );\n    },\n};\n", "import { Component, useState, useRef } from \"@odoo/owl\";\nimport { Dialog } from \"@web/core/dialog/dialog\";\nimport { EMBEDDED_VIEW_LINK_STYLES } from \"@knowledge/editor/embedded_components/core/embedded_view_link/embedded_view_link_style\";\n\nexport class EmbeddedViewLinkEditDialog extends Component {\n    static template = \"knowledge.EmbeddedViewLinkEditDialog\";\n    static components = { Dialog };\n    static props = {\n        style: { type: String },\n        close: { type: Function },\n        name: { type: String },\n        onSave: { type: Function },\n    };\n\n    setup() {\n        this.state = useState({\n            name: this.props.name,\n            style: this.props.style,\n        });\n        this.input = useRef(\"input\");\n    }\n\n    //--------------------------------------------------------------------------\n    // GETTERS/SETTERS\n    //--------------------------------------------------------------------------\n\n    get name() {\n        return this.state.name.trim();\n    }\n\n    get styles() {\n        return EMBEDDED_VIEW_LINK_STYLES;\n    }\n\n    //--------------------------------------------------------------------------\n    // HANDLERS\n    //--------------------------------------------------------------------------\n\n    onConfirm() {\n        if (!this.name) {\n            return this.input.el.focus();\n        }\n        this.props.onSave(this.name, this.state.style);\n        this.props.close();\n    }\n\n    updateStyle(style) {\n        this.state.style = style;\n    }\n}\n", "import { Component } from \"@odoo/owl\";\n\nexport class EmbeddedViewLinkPopover extends Component {\n    static template = \"knowledge.EmbeddedViewLinkPopover\";\n    static props = {\n        close: { type: Function },\n        name: { type: String },\n        onCopyViewLinkClick: { type: Function },\n        onEditViewLinkClick: { type: Function },\n        onRemoveViewLinkClick: { type: Function },\n        openViewLink: { type: Function },\n    };\n}\n", "import { Component } from \"@odoo/owl\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { getEmbeddedProps } from \"@html_editor/others/embedded_component_utils\";\nimport { EMBEDDED_VIEW_LINK_STYLES } from \"@knowledge/editor/embedded_components/core/embedded_view_link/embedded_view_link_style\";\nimport { makeContext } from \"@web/core/context\";\n\nexport class ReadonlyViewLinkComponent extends Component {\n    static template = \"knowledge.ReadonlyEmbeddedViewLink\";\n    static props = {\n        viewProps: { type: Object },\n        linkStyle: { type: String, optional: true },\n    };\n    static defaultProps = {\n        linkStyle: \"link\",\n    };\n\n    get displayName() {\n        return this.props.viewProps.displayName;\n    }\n\n    get linkStyle() {\n        return this.props.linkStyle;\n    }\n\n    setup() {\n        this.actionService = useService(\"action\");\n    }\n\n    getLinkClass() {\n        return EMBEDDED_VIEW_LINK_STYLES[this.linkStyle].class;\n    }\n\n    async openViewLink() {\n        const context = makeContext([this.props.viewProps.context || {}]);\n        const action = await this.actionService.loadAction(\n            this.props.viewProps.actWindow || this.props.viewProps.actionXmlId,\n            context\n        );\n        if (action.type !== \"ir.actions.act_window\") {\n            throw new Error(\n                `Invalid action type \"${action.type}\". Expected \"ir.actions.act_window\"`\n            );\n        }\n        if (this.displayName) {\n            action.name = this.displayName;\n            action.display_name = this.displayName;\n        }\n        action.globalState = {\n            searchModel: this.props.viewProps.context.knowledge_search_model_state,\n        };\n        const props = {};\n        if (action.context.orderBy) {\n            try {\n                props.orderBy = JSON.parse(this.action.context.orderBy);\n            } catch {\n                console.error(\"Parsing orderBy failed\");\n            }\n        }\n        this.actionService.doAction(action, {\n            viewType: this.props.viewProps.viewType,\n            props,\n        });\n    }\n}\n\nexport const readonlyViewLinkEmbedding = {\n    name: \"viewLink\",\n    Component: ReadonlyViewLinkComponent,\n    getProps: (host) => {\n        return { ...getEmbeddedProps(host) };\n    },\n};\n", "import {\n    fileEmbedding,\n    EmbeddedFileComponent,\n} from \"@html_editor/others/embedded_components/backend/file/file\";\nimport { MacrosFileMixin } from \"@knowledge/editor/embedded_components/backend/file/macros_file_mixin\";\n\nexport const MacrosEmbeddedFileComponent = MacrosFileMixin(\n    EmbeddedFileComponent,\n    \"knowledge.EmbeddedFile\"\n);\n\nexport const macrosFileEmbedding = {\n    ...fileEmbedding,\n    Component: MacrosEmbeddedFileComponent,\n};\n", "import { rpc } from \"@web/core/network/rpc\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { getDataURLFromFile } from \"@web/core/utils/urls\";\nimport { AttachToMessageMacro, UseAsAttachmentMacro } from \"@knowledge/macros/file_macros\";\n\nexport const MacrosFileMixin = (T, template) => {\n    return class extends T {\n        static template = template;\n\n        setup() {\n            super.setup();\n            this.actionService = useService(\"action\");\n            this.uiService = useService(\"ui\");\n            this.knowledgeCommandsService = useService(\"knowledgeCommandsService\");\n            this.macrosServices = {\n                action: this.actionService,\n                dialog: this.dialogService,\n                ui: this.uiService,\n            };\n            this.targetRecordInfo = this.knowledgeCommandsService.getCommandsRecordInfo();\n        }\n\n        /**\n         * Callback function called when the user clicks on the \"Send as Message\" button.\n         * The function will execute a macro that will open the last opened form view,\n         * compose a new message and attach the associated file to it.\n         * @param {Event} ev\n         */\n        async onClickAttachToMessage(ev) {\n            const dataTransfer = new DataTransfer();\n            try {\n                const response = await window.fetch(this.fileModel.urlRoute);\n                const blob = await response.blob();\n                const file = new File([blob], this.fileModel.name, {\n                    type: blob.type,\n                });\n                /**\n                 * dataTransfer will be used to mimic a drag and drop of\n                 * the file in the target record chatter.\n                 * @see KnowledgeMacro\n                 */\n                dataTransfer.items.add(file);\n            } catch {\n                return;\n            }\n            const macro = new AttachToMessageMacro({\n                targetXmlDoc: this.targetRecordInfo.xmlDoc,\n                breadcrumbs: this.targetRecordInfo.breadcrumbs,\n                data: {\n                    dataTransfer: dataTransfer,\n                },\n                services: this.macrosServices,\n            });\n            macro.start();\n        }\n\n        /**\n         * Callback function called when the user clicks on the \"Use As Attachment\" button.\n         * The function will execute a macro that will open the last opened form view\n         * and add the associated file to the attachments of the chatter.\n         * @param {Event} ev\n         */\n        async onClickUseAsAttachment(ev) {\n            let attachment;\n            try {\n                const response = await window.fetch(this.fileModel.urlRoute);\n                const blob = await response.blob();\n                const dataURL = await getDataURLFromFile(blob);\n                attachment = await rpc(\"/html_editor/attachment/add_data\", {\n                    name: this.fileModel.name,\n                    data: dataURL.split(\",\")[1],\n                    is_image: false,\n                    res_id: this.targetRecordInfo.resId,\n                    res_model: this.targetRecordInfo.resModel,\n                });\n            } catch {\n                return;\n            }\n            if (!attachment) {\n                return;\n            }\n            const macro = new UseAsAttachmentMacro({\n                targetXmlDoc: this.targetRecordInfo.xmlDoc,\n                breadcrumbs: this.targetRecordInfo.breadcrumbs,\n                data: null,\n                services: this.macrosServices,\n            });\n            macro.start();\n        }\n    };\n};\n", "import {\n    readonlyFileEmbedding,\n    ReadonlyEmbeddedFileComponent,\n} from \"@html_editor/others/embedded_components/core/file/readonly_file\";\nimport { MacrosFileMixin } from \"@knowledge/editor/embedded_components/backend/file/macros_file_mixin\";\n\nexport const ReadonlyMacrosEmbeddedFileComponent = MacrosFileMixin(\n    ReadonlyEmbeddedFileComponent,\n    \"knowledge.ReadonlyEmbeddedFile\"\n);\n\nexport const readonlyMacrosFileEmbedding = {\n    ...readonlyFileEmbedding,\n    Component: ReadonlyMacrosEmbeddedFileComponent,\n};\n", "import {\n    getEditableDescendants,\n    StateChangeManager,\n    useEmbeddedState,\n} from \"@html_editor/others/embedded_component_utils\";\nimport { ReadonlyFoldableSection } from \"@knowledge/editor/embedded_components/core/readonly_foldable_section/readonly_foldable_section\";\n\nexport class FoldableSection extends ReadonlyFoldableSection {\n    static props = {\n        host: Object,\n    };\n    setup() {\n        super.setup();\n        this.state = useEmbeddedState(this.props.host);\n    }\n    onInputChange(ev) {\n        this.env.editorShared.selection.setCursorEnd(\n            this.editableDescendants.title.firstElementChild\n        );\n        super.onInputChange(ev);\n    }\n}\n\nexport const foldableSectionEmbedding = {\n    name: \"foldableSection\",\n    Component: FoldableSection,\n    getProps: (host) => ({ host }),\n    getEditableDescendants: getEditableDescendants,\n    getStateChangeManager: (config) => {\n        const commitStateChanges = config.commitStateChanges;\n        const stateChangeManager = new StateChangeManager(\n            Object.assign(config, {\n                commitStateChanges: () => {\n                    const state = stateChangeManager.getEmbeddedState();\n                    config.host.classList.toggle(\"d-print-none\", !state.showContent);\n                    commitStateChanges();\n                },\n            })\n        );\n        return stateChangeManager;\n    },\n};\n", "import {\n    ReadonlyEmbeddedViewComponent,\n    readonlyViewEmbedding,\n} from \"@knowledge/editor/embedded_components/backend/view/readonly_embedded_view\";\nimport {\n    applyObjectPropertyDifference,\n    getEmbeddedProps,\n    StateChangeManager,\n    useEmbeddedState,\n} from \"@html_editor/others/embedded_component_utils\";\nimport { ItemCalendarPropsDialog } from \"@knowledge/components/item_calendar_props_dialog/item_calendar_props_dialog\";\nimport { PromptEmbeddedViewNameDialog } from \"@knowledge/components/prompt_embedded_view_name_dialog/prompt_embedded_view_name_dialog\";\nimport { useBus, useService } from \"@web/core/utils/hooks\";\nimport { uuid } from \"@web/core/utils/strings\";\nimport { useSubEnv } from \"@odoo/owl\";\n\nexport class EmbeddedViewComponent extends ReadonlyEmbeddedViewComponent {\n    setup() {\n        this.embeddedState = useEmbeddedState(this.props.host);\n        if (!this.id) {\n            this.initNewView();\n        }\n        super.setup();\n        useSubEnv({\n            isEmbeddedReadonly: false,\n        });\n        this.dialogService = useService(\"dialog\");\n        useBus(this.env.bus, `KNOWLEDGE_EMBEDDED_${this.id}:EDIT`, () => {\n            if (this.additionalViewProps && Object.keys(this.additionalViewProps).length) {\n                this.editView();\n            } else {\n                this.renameView();\n            }\n        });\n    }\n\n    set additionalViewProps(value) {\n        this.embeddedState.additionalViewProps = value;\n    }\n\n    get additionalViewProps() {\n        return this.embeddedState.additionalViewProps;\n    }\n\n    set displayName(value) {\n        this.embeddedState.displayName = value;\n    }\n\n    get displayName() {\n        return this.embeddedState.displayName;\n    }\n\n    get favoriteFilters() {\n        return this.embeddedState.favoriteFilters;\n    }\n\n    set id(value) {\n        this.embeddedState.id = value;\n    }\n\n    get id() {\n        return this.embeddedState.id;\n    }\n\n    deleteFavoriteFilter(searchItem) {\n        delete this.favoriteFilters[searchItem.description];\n    }\n\n    editView() {\n        if (this.action.res_model === \"knowledge.article\" && this.action.view_mode === \"calendar\") {\n            this.dialogService.add(ItemCalendarPropsDialog, {\n                isNew: false,\n                name: this.displayName,\n                saveItemCalendarProps: (name, itemCalendarProps) => {\n                    this.displayName = name;\n                    this.additionalViewProps.itemCalendarProps = itemCalendarProps;\n                },\n                knowledgeArticleId: this.env.model.root.resId,\n                ...this.additionalViewProps.itemCalendarProps,\n            });\n        } else {\n            throw new Error(\"Cannot edit the view: the dialog is not implemented\");\n        }\n    }\n\n    initNewView() {\n        if (!this.env.model.root.data.full_width) {\n            this.env.model.root.update({ full_width: true });\n        }\n        this.id = uuid();\n        // duplicate optional fields with new unique key\n        const keyOptionalFields = this.props.viewProps.context.keyOptionalFields;\n        if (keyOptionalFields) {\n            const optionalFields = localStorage.getItem(keyOptionalFields);\n            if (optionalFields) {\n                localStorage.setItem(`${keyOptionalFields},${this.id}`, optionalFields);\n            }\n        }\n    }\n\n    renameView() {\n        this.dialogService.add(PromptEmbeddedViewNameDialog, {\n            isNew: false,\n            defaultName: this.displayName,\n            viewType: this.props.viewProps.viewType,\n            save: (name) => {\n                // TODO ABD: make collaborative setDisplayName reactive. It should be possible since\n                // the embeddedState of collaborators receive the new name.\n                this.displayName = name;\n            },\n        });\n    }\n\n    saveFavoriteFilter(filter) {\n        // favorite filters are saved in an object to allow collaborative writes: multiple users\n        // can create a new filter at the same time and all of them should be kept.\n        this.favoriteFilters[filter.name] = filter;\n    }\n}\n\nexport const viewEmbedding = {\n    ...readonlyViewEmbedding,\n    Component: EmbeddedViewComponent,\n    getStateChangeManager: (config) => {\n        return new StateChangeManager(\n            Object.assign(config, {\n                getEmbeddedState: (host) => {\n                    const props = getEmbeddedProps(host);\n                    return {\n                        additionalViewProps: props.viewProps.additionalViewProps || {},\n                        displayName: props.viewProps.displayName,\n                        favoriteFilters: props.viewProps.favoriteFilters || {},\n                        id: props.viewProps.id,\n                    };\n                },\n                propertyUpdater: {\n                    favoriteFilters: (state, previous, next) => {\n                        applyObjectPropertyDifference(\n                            state,\n                            \"favoriteFilters\",\n                            previous.favoriteFilters,\n                            next.favoriteFilters\n                        );\n                    },\n                },\n                stateToEmbeddedProps: (host, state) => {\n                    const props = getEmbeddedProps(host);\n                    props.viewProps.additionalViewProps = state.additionalViewProps;\n                    props.viewProps.displayName = state.displayName;\n                    props.viewProps.favoriteFilters = state.favoriteFilters;\n                    props.viewProps.id = state.id;\n                    return props;\n                },\n            })\n        );\n    },\n};\n", "import { getEmbeddedProps } from \"@html_editor/others/embedded_component_utils\";\nimport { EmbeddedView } from \"@knowledge/views/embedded_view\";\nimport { WithLazyLoading } from \"@knowledge/components/with_lazy_loading/with_lazy_loading\";\nimport { makeContext } from \"@web/core/context\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { useOwnDebugContext } from \"@web/core/debug/debug_context\";\nimport { useBus, useService } from \"@web/core/utils/hooks\";\nimport { CallbackRecorder } from \"@web/search/action_hook\";\nimport { getDefaultConfig } from \"@web/views/view\";\n\nimport {\n    Component,\n    onError,\n    onWillStart,\n    useExternalListener,\n    useState,\n    useSubEnv,\n} from \"@odoo/owl\";\n\nconst VIEW_RECORDS_LIMITS = {\n    kanban: 20,\n    list: 40,\n};\n\nexport class ReadonlyEmbeddedViewComponent extends Component {\n    static components = {\n        EmbeddedView,\n        WithLazyLoading\n    };\n    static props = {\n        host: { type: Object },\n        viewProps: { type: Object },\n    };\n    static template = \"knowledge.EmbeddedView\";\n\n    setup() {\n        super.setup();\n        this.actionService = useService(\"action\");\n        this.notificationService = useService(\"notification\");\n        this.uiService = useService(\"ui\");\n        this.viewFilters = useService(\"knowledgeEmbedViewsFilters\");\n        useOwnDebugContext(); // define a debug context when the developer mode is enable\n        this.state = useState({\n            error: false,\n            isLoaded: false,\n        });\n        const services = this.env.services;\n        const extendedServices = Object.create(services);\n        extendedServices.action = Object.create(services.action);\n        extendedServices.action.switchView = (viewType, props = {}) => {\n            if (props.resId) {\n                this.action.res_id = props.resId;\n            }\n            this.action.globalState = this.getEmbeddedViewGlobalState();\n            this.actionService.doAction(this.action, { viewType });\n        };\n\n        useSubEnv({\n            config: { ...getDefaultConfig(), disableSearchBarAutofocus: true },\n            isEmbeddedView: true,\n            isEmbeddedReadonly: true,\n            services: extendedServices,\n        });\n\n        useBus(this.env.bus, `KNOWLEDGE_EMBEDDED_${this.id}:OPEN`, () => {\n            this.openView();\n        });\n\n        /**\n         * Normally, the editable is the only element in the editor which has the focus.\n         * In this case, it can be useful to delegate the focus at a lower level\n         * (i.e. to use hotkeys with the uiService).\n         *\n         * `activateElement` is used to set the host as an active element in the ui service, this enables\n         * us to contain the events inside the embedded view when it has the focus.\n         *\n         * `deactivateElement` removes the host as an active element, leaving only the document as active\n         * and we come back to the default behavior of the document handling all the events.\n         *\n         * As a reminder, tabindex=\"-1\" must be set in the template for an element to capture\n         * focusin and focusout events.\n         */\n        useExternalListener(this.props.host, \"focusin\", () => {\n            if (!this.props.host.contains(this.uiService.activeElement)) {\n                this.uiService.activateElement(this.props.host);\n            }\n        });\n        useExternalListener(this.props.host, \"focusout\", (event) => {\n            if (!this.props.host.contains(event.relatedTarget)) {\n                this.uiService.deactivateElement(this.props.host);\n            }\n        });\n\n        onWillStart(async () => {\n            await this.loadEmbeddedView();\n        });\n\n        // TODO ABD: better error handling (discard filters)\n        onError((error) => {\n            console.error(error);\n            this.state.error = true;\n        });\n    }\n\n    get embeddedViewProps() {\n        // forces reactivity for additionalViewProps, displayName and\n        // favoriteFilters\n        return {\n            ...this.staticEmbeddedViewProps,\n            additionalViewProps: { ...this.additionalViewProps },\n            displayName: this.displayName,\n            irFilters: this.irFilters,\n        };\n    }\n\n    get additionalViewProps() {\n        return this.props.viewProps.additionalViewProps;\n    }\n\n    get displayName() {\n        return this.props.viewProps.displayName;\n    }\n\n    get favoriteFilters() {\n        return this.props.viewProps.favoriteFilters;\n    }\n\n    get id() {\n        return this.props.viewProps.id;\n    }\n\n    get irFilters() {\n        // TODO ABD: make collaborative filters update reactive since the embedded state is updated,\n        // it should be possible to update the searchModel too.\n        return (Object.values(this.favoriteFilters || {}) || []).map((filter) => ({\n            ...filter,\n            context: JSON.stringify(filter.context),\n        }));\n    }\n\n    async createRecord() {\n        this.saveFilters();\n        // can we use res_id: false and thus make one method for select and create?\n        this.actionService.doAction({\n            res_model: this.action.res_model,\n            type: 'ir.actions.act_window',\n            views: [this.action.views.find(([_, type]) => type === \"form\") || [false, \"form\"]],\n        });\n    }\n\n    deleteFavoriteFilter(searchItem) {\n        this.notificationService.add(\n            _t(\"You are not allowed to delete a favorite filter in this article.\"),\n            {\n                type: \"danger\",\n            }\n        );\n    }\n\n    /**\n     * Extract the SearchModel state of the embedded view\n     *\n     * @returns {Object} globalState\n     */\n    getEmbeddedViewGlobalState() {\n        const callbacks = this.__getGlobalState__.callbacks;\n        let globalState;\n        if (callbacks.length) {\n            globalState = callbacks.reduce((res, callback) => {\n                return { ...res, ...callback() };\n            }, {});\n        }\n        return { searchModel: globalState && globalState.searchModel };\n    }\n\n    async loadEmbeddedView() {\n        // allow access to the SearchModel exported state which contain facets\n        this.__getGlobalState__ = new CallbackRecorder();\n        const context = this.makeEmbeddedContext();\n        this.action = await this.loadAction(context);\n        this.env.config.views = this.action.views;\n        this.env.config.setDisplayName(this.action.name);\n        this.staticEmbeddedViewProps = this.prepareStaticEmbeddedViewProps(context);\n        this.viewFilters.applyFilter(\n            this.actionService.currentController,\n            this.id,\n            this.staticEmbeddedViewProps\n        );\n        this.state.isLoaded = true;\n    }\n\n    async loadAction(context) {\n        // never use an action help with actWindow (could have been added by a malicious user)\n        if (this.props.actWindow?.help) {\n            delete this.props.actWindow.help;\n        }\n        const action = await this.actionService.loadAction(\n            this.props.viewProps.actWindow || this.props.viewProps.actionXmlId,\n            context\n        );\n        if (action.type !== \"ir.actions.act_window\") {\n            throw new Error(\n                `Invalid action type \"${action.type}\". Expected \"ir.actions.act_window\"`\n            );\n        }\n        if (this.displayName) {\n            action.name = this.displayName;\n            action.display_name = this.displayName;\n        }\n        return action;\n    }\n\n    /**\n     * Create the context used by the embedded view.\n     */\n    makeEmbeddedContext() {\n        const context = makeContext([\n            this.props.viewProps.context || {},\n            {\n                knowledgeEmbeddedViewId: this.id,\n            },\n        ]);\n        return context;\n    }\n\n    /**\n     * Open the view in \"full screen\"\n     */\n    openView() {\n        this.saveFilters();\n        const props = { ...(this.additionalViewProps || {}) };\n        if (this.action.context.orderBy) {\n            try {\n                props.orderBy = JSON.parse(this.action.context.orderBy);\n            } catch {\n                console.error(\"Parsing orderBy failed\");\n            }\n        }\n        // make sure name is up to date (could have been updated by collaborative)\n        if (this.displayName !== this.action.display_name) {\n            this.action.display_name = this.displayName;\n            this.action.name = this.displayName;\n        }\n        this.action.globalState = this.getEmbeddedViewGlobalState();\n        this.actionService.doAction(this.action, {\n            viewType: this.props.viewProps.viewType,\n            props,\n            additionalContext: {\n                knowledgeEmbeddedViewId: this.id,\n                isOpenedEmbeddedView: true,\n            },\n        });\n    }\n\n    prepareStaticEmbeddedViewProps(context) {\n        return {\n            context,\n            createRecord: this.createRecord.bind(this),\n            deleteEmbeddedViewFavoriteFilter: this.deleteFavoriteFilter.bind(this),\n            domain: this.action.domain || [],\n            __getGlobalState__: this.__getGlobalState__,\n            globalState: { searchModel: context.knowledge_search_model_state },\n            irFilters: this.irFilters,\n            limit: VIEW_RECORDS_LIMITS[this.props.viewProps.type],\n            loadActionMenus: true,\n            loadIrFilters: true,\n            noContentHelp: this.action.help,\n            resModel: this.action.res_model,\n            saveEmbeddedViewFavoriteFilter: this.saveFavoriteFilter.bind(this),\n            searchViewId: this.action.searchViewId?.[0],\n            selectRecord: this.selectRecord.bind(this),\n            type: this.props.viewProps.viewType,\n        };\n    }\n\n    saveFilters() {\n        this.viewFilters.saveFilters(\n            this.actionService.currentController,\n            this.id,\n            this.getEmbeddedViewGlobalState().searchModel\n        );\n    }\n\n    saveFavoriteFilter(filter) {\n        this.notificationService.add(\n            _t(\"You are not allowed to save a favorite filter in this article.\"),\n            {\n                type: \"danger\",\n            }\n        );\n    }\n\n    selectRecord(resId) {\n        this.saveFilters();\n        if (this.action.res_model === \"knowledge.article\") {\n            this.env.openArticle(resId);\n        } else {\n            this.actionService.doAction({\n                res_id: resId,\n                res_model: this.action.res_model,\n                type: \"ir.actions.act_window\",\n                views: [this.action.views.find((_, type) => type === \"form\") || [false, \"form\"]],\n            });\n        }\n    }\n}\n\nexport const readonlyViewEmbedding = {\n    name: \"view\",\n    Component: ReadonlyEmbeddedViewComponent,\n    getProps: (host) => {\n        return { host, ...getEmbeddedProps(host) };\n    },\n};\n", "import { Component } from \"@odoo/owl\";\nimport { getEmbeddedProps } from \"@html_editor/others/embedded_component_utils\";\n\nexport class ReadonlyEmbeddedArticleIndexComponent extends Component {\n    static props = {\n        articles: { type: Object, optional: true },\n        showAllChildren: { type: Boolean, optional: true },\n    };\n    static defaultProps = {\n        articles: {},\n        showAllChildren: true,\n    };\n    static template = \"knowledge.ReadonlyEmbeddedArticleIndex\";\n\n    /** @param {integer} articleId */\n    openArticle(articleId) {\n        if (this.env.openArticle) {\n            this.env.openArticle(articleId);\n        }\n    }\n}\n\nexport const readonlyArticleIndexEmbedding = {\n    name: \"articleIndex\",\n    Component: ReadonlyEmbeddedArticleIndexComponent,\n    getProps: (host) => {\n        return {\n            ...getEmbeddedProps(host),\n        };\n    },\n};\n", "import {\n    getEditableDescendants,\n    useEditableDescendants,\n} from \"@html_editor/others/embedded_component_utils\";\nimport {\n    EmbeddedComponentToolbar,\n    EmbeddedComponentToolbarButton,\n} from \"@html_editor/others/embedded_components/core/embedded_component_toolbar/embedded_component_toolbar\";\nimport { browser } from \"@web/core/browser/browser\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { Tooltip } from \"@web/core/tooltip/tooltip\";\nimport { usePopover } from \"@web/core/popover/popover_hook\";\nimport { useChildRef } from \"@web/core/utils/hooks\";\nimport { Component } from \"@odoo/owl\";\n\nexport class EmbeddedClipboardComponent extends Component {\n    static components = {\n        EmbeddedComponentToolbar,\n        EmbeddedComponentToolbarButton,\n    };\n    static props = {\n        host: { type: Object },\n    };\n    static template = \"knowledge.EmbeddedClipboard\";\n\n    setup() {\n        this.popover = usePopover(Tooltip);\n        this.descendants = useEditableDescendants(this.props.host);\n        this.copyToClipboardButtonRef = useChildRef();\n    }\n\n    //--------------------------------------------------------------------------\n    // HANDLERS\n    //--------------------------------------------------------------------------\n\n    async onClickCopyToClipboard() {\n        const selection = document.getSelection();\n        selection.removeAllRanges();\n        const range = new Range();\n        range.selectNodeContents(this.descendants.clipboardContent);\n        selection.addRange(range);\n        if (document.execCommand(\"copy\")) {\n            // Nor the original `clipboard.write` function nor the polyfill\n            // written in `clipboard.js` does trigger the `clipboard_plugin`\n            // `copy` handler, therefore `execCommand` should be called here so\n            // that html content is properly handled within the editor.\n            this.popover.open(this.copyToClipboardButtonRef.el, {\n                tooltip: _t(\"Content copied to clipboard.\"),\n            });\n            browser.setTimeout(this.popover.close, 800);\n        }\n        selection.removeAllRanges();\n    }\n}\n\nexport const clipboardEmbedding = {\n    name: \"clipboard\",\n    Component: EmbeddedClipboardComponent,\n    getEditableDescendants: getEditableDescendants,\n    getProps: (host) => {\n        return { host };\n    },\n};\n", "import { _t } from \"@web/core/l10n/translation\";\n\nexport const EMBEDDED_VIEW_LINK_STYLES = {\n    link: { display: _t(\"Link\"), class: \"btn btn-link\" },\n    primary: { display: _t(\"Primary\"), class: \"btn btn-primary\" },\n    secondary: { display: _t(\"Secondary\"), class: \"btn btn-secondary\" },\n};\n", "import {\n    getEditableDescendants,\n    getEmbeddedProps,\n    useEditableDescendants,\n} from \"@html_editor/others/embedded_component_utils\";\nimport { Component, useState } from \"@odoo/owl\";\n\nexport class ReadonlyFoldableSection extends Component {\n    static template = \"knowledge.ReadonlyEmbeddedFoldableSection\";\n    static props = {\n        host: { type: Object },\n        showContent: { type: Boolean, optional: true },\n    };\n    setup() {\n        this.editableDescendants = useEditableDescendants(this.props.host);\n        this.state = useState({\n            showContent: this.props.showContent,\n        });\n    }\n    onInputChange(ev) {\n        this.state.showContent = ev.target.checked;\n    }\n}\n\nexport const readonlyFoldableSectionEmbedding = {\n    name: \"foldableSection\",\n    Component: ReadonlyFoldableSection,\n    getProps: (host) => ({ host, ...getEmbeddedProps(host) }),\n    getEditableDescendants: getEditableDescendants,\n};\n", "import { macrosClipboardEmbedding } from \"@knowledge/editor/embedded_components/backend/clipboard/macros_embedded_clipboard\";\nimport { macrosFileEmbedding } from \"@knowledge/editor/embedded_components/backend/file/macros_file\";\nimport { readonlyMacrosFileEmbedding } from \"@knowledge/editor/embedded_components/backend/file/readonly_macros_file\";\nimport { articleIndexEmbedding } from \"@knowledge/editor/embedded_components/backend/article_index/article_index\";\nimport { readonlyArticleIndexEmbedding } from \"@knowledge/editor/embedded_components/core/article_index/readonly_article_index\";\nimport { readonlyViewEmbedding } from \"@knowledge/editor/embedded_components/backend/view/readonly_embedded_view\";\nimport { viewEmbedding } from \"@knowledge/editor/embedded_components/backend/view/embedded_view\";\nimport { readonlyViewLinkEmbedding } from \"@knowledge/editor/embedded_components/backend/embedded_view_link/readonly_embedded_view_link\";\nimport { viewLinkEmbedding } from \"@knowledge/editor/embedded_components/backend/embedded_view_link/embedded_view_link\";\nimport { foldableSectionEmbedding } from \"@knowledge/editor/embedded_components/backend/foldable_section/foldable_section\";\nimport { readonlyFoldableSectionEmbedding } from \"@knowledge/editor/embedded_components/core/readonly_foldable_section/readonly_foldable_section\";\n\nexport const KNOWLEDGE_EMBEDDINGS = [\n    articleIndexEmbedding,\n    foldableSectionEmbedding,\n    macrosClipboardEmbedding,\n    macrosFileEmbedding,\n    viewEmbedding,\n    viewLinkEmbedding,\n];\n\nexport const KNOWLEDGE_READONLY_EMBEDDINGS = [\n    macrosClipboardEmbedding,\n    readonlyArticleIndexEmbedding,\n    readonlyFoldableSectionEmbedding,\n    readonlyMacrosFileEmbedding,\n    readonlyViewEmbedding,\n    readonlyViewLinkEmbedding,\n];\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { withSequence } from \"@html_editor/utils/resource\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { renderToElement } from \"@web/core/utils/render\";\nimport { isHtmlContentSupported } from \"@html_editor/core/selection_plugin\";\n\nexport class ArticleIndexPlugin extends Plugin {\n    static id = \"articleIndex\";\n    static dependencies = [\"history\", \"dom\"];\n    resources = {\n        user_commands: [\n            {\n                id: \"insertArticleIndex\",\n                title: _t(\"Index\"),\n                description: _t(\"Show nested articles\"),\n                icon: \"fa-list\",\n                run: this.insertArticleIndex.bind(this),\n                isAvailable: isHtmlContentSupported,\n            },\n        ],\n        powerbox_categories: [\n            withSequence(20, {\n                id: \"knowledge\",\n                name: _t(\"Knowledge\"),\n            }),\n        ],\n        powerbox_items: [\n            {\n                categoryId: \"knowledge\",\n                commandId: \"insertArticleIndex\",\n            },\n        ],\n    };\n\n    insertArticleIndex() {\n        const articleIndexBlueprint = renderToElement(\"knowledge.ArticleIndexBlueprint\");\n        this.dependencies.dom.insert(articleIndexBlueprint);\n        this.dependencies.history.addStep();\n    }\n}\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { getBaseContainerSelector } from \"@html_editor/utils/base_container\";\nimport { closestElement } from \"@html_editor/utils/dom_traversal\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { renderToElement } from \"@web/core/utils/render\";\nimport { isHtmlContentSupported } from \"@html_editor/core/selection_plugin\";\n\nexport class EmbeddedClipboardPlugin extends Plugin {\n    static id = \"embeddedClipboard\";\n    static dependencies = [\"baseContainer\", \"history\", \"dom\", \"selection\"];\n    resources = {\n        user_commands: [\n            {\n                id: \"insertClipboard\",\n                title: _t(\"Clipboard\"),\n                description: _t(\"Add a clipboard section\"),\n                icon: \"fa-pencil-square\",\n                run: this.insertClipboard.bind(this),\n                isAvailable: isHtmlContentSupported,\n            },\n        ],\n        powerbox_items: [\n            {\n                categoryId: \"media\",\n                commandId: \"insertClipboard\",\n                isAvailable: (selection) =>\n                    !closestElement(selection.anchorNode, \"[data-embedded='clipboard']\"),\n            },\n        ],\n    };\n\n    insertClipboard() {\n        const baseContainer = this.dependencies.baseContainer.createBaseContainer();\n        const baseContainerNodeName = baseContainer.nodeName;\n        const baseContainerClass = baseContainer.className;\n        const baseContainerSelector = getBaseContainerSelector(baseContainerNodeName);\n        const clipboardBlock = renderToElement(\"knowledge.EmbeddedClipboardBlueprint\", {\n            baseContainerNodeName,\n            baseContainerAttributes: {\n                class: baseContainerClass,\n            },\n        });\n        this.dependencies.dom.insert(clipboardBlock);\n        this.dependencies.selection.setCursorStart(\n            clipboardBlock.querySelector(baseContainerSelector)\n        );\n        this.dependencies.history.addStep();\n    }\n}\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { boundariesOut } from \"@html_editor/utils/position\";\nimport { EMBEDDED_COMPONENT_PLUGINS } from \"@html_editor/plugin_sets\";\nimport { _t } from \"@web/core/l10n/translation\";\n\nexport class EmbeddedViewLinkPlugin extends Plugin {\n    static id = \"embeddedViewLink\";\n    static dependencies = [\"history\", \"dom\", \"selection\"];\n    resources = {\n        mount_component_handlers: this.setupNewComponent.bind(this),\n    };\n\n    setupNewComponent({ name, props }) {\n        if (name === \"viewLink\") {\n            Object.assign(props, {\n                removeViewLink: (text) => {\n                    this.replaceElementWith(props.host, text);\n                    this.dependencies.history.addStep();\n                },\n                copyViewLink: () => {\n                    const cursors = this.dependencies.selection.preserveSelection();\n                    this.copyElementToClipboard(props.host);\n                    cursors?.restore();\n                },\n            });\n        }\n    }\n\n    replaceElementWith(target, element) {\n        const [anchorNode, anchorOffset, focusNode, focusOffset] = boundariesOut(target);\n        this.dependencies.selection.setSelection({\n            anchorNode,\n            anchorOffset,\n            focusNode,\n            focusOffset,\n        });\n        this.dependencies.dom.insert(element);\n    }\n\n    copyElementToClipboard(element) {\n        const range = document.createRange();\n        range.selectNode(element);\n        const selection = window.getSelection();\n        selection.removeAllRanges();\n        selection.addRange(range);\n        const copySucceeded = document.execCommand(\"copy\");\n        selection.removeAllRanges();\n        if (copySucceeded) {\n            this.services.notification.add(_t(\"Link copied to clipboard.\"), {\n                type: \"success\",\n            });\n        }\n    }\n}\n\nEMBEDDED_COMPONENT_PLUGINS.push(EmbeddedViewLinkPlugin);\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { closestElement } from \"@html_editor/utils/dom_traversal\";\nimport { ItemCalendarPropsDialog } from \"@knowledge/components/item_calendar_props_dialog/item_calendar_props_dialog\";\nimport { PromptEmbeddedViewNameDialog } from \"@knowledge/components/prompt_embedded_view_name_dialog/prompt_embedded_view_name_dialog\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { renderToElement } from \"@web/core/utils/render\";\nimport { isHtmlContentSupported } from \"@html_editor/core/selection_plugin\";\n\nfunction isAvailable(selection) {\n    return !closestElement(selection.anchorNode, \".o_editor_banner, table, [data-embedded]\");\n}\n\nexport class EmbeddedViewPlugin extends Plugin {\n    static id = \"embeddedView\";\n    static dependencies = [\"history\", \"dom\", \"selection\"];\n    resources = {\n        user_commands: [\n            {\n                id: \"insertEmbeddedViewKanban\",\n                title: _t(\"Item Kanban\"),\n                description: _t(\"Insert a Kanban view of article items\"),\n                icon: \"fa-th-large\",\n                run: () => {\n                    this.promptInsertEmbeddedView(\"kanban\", true);\n                },\n                isAvailable: isHtmlContentSupported,\n            },\n            {\n                id: \"insertEmbeddedViewCards\",\n                title: _t(\"Item Cards\"),\n                description: _t(\"Insert a Card view of article items\"),\n                icon: \"fa-address-card\",\n                run: () => {\n                    this.promptInsertEmbeddedView(\"kanban\");\n                },\n                isAvailable: isHtmlContentSupported,\n            },\n            {\n                id: \"insertEmbeddedViewList\",\n                title: _t(\"Item List\"),\n                description: _t(\"Insert a List view of article items\"),\n                icon: \"fa-th-list\",\n                run: () => {\n                    this.promptInsertEmbeddedView(\"list\");\n                },\n                isAvailable: isHtmlContentSupported,\n            },\n            {\n                id: \"insertEmbeddedViewCalendar\",\n                title: _t(\"Item Calendar\"),\n                description: _t(\"Insert a Calendar view of article items\"),\n                icon: \"fa-calendar-plus-o\",\n                run: () => {\n                    this.promptInsertEmbeddedCalendarView();\n                },\n                isAvailable: isHtmlContentSupported,\n            },\n        ],\n        powerbox_items: [\n            {\n                categoryId: \"knowledge\",\n                commandId: \"insertEmbeddedViewKanban\",\n                isAvailable,\n            },\n            {\n                categoryId: \"knowledge\",\n                commandId: \"insertEmbeddedViewCards\",\n                isAvailable,\n            },\n            {\n                categoryId: \"knowledge\",\n                commandId: \"insertEmbeddedViewList\",\n                isAvailable,\n            },\n            {\n                categoryId: \"knowledge\",\n                commandId: \"insertEmbeddedViewCalendar\",\n                isAvailable,\n            },\n        ],\n    };\n\n    insertEmbeddedView(actionXmlId, name, viewType, additionalViewProps = {}) {\n        const resId = this.config.getRecordInfo().resId;\n        const embeddedViewBlueprint = renderToElement(\"knowledge.EmbeddedViewBlueprint\", {\n            embeddedProps: JSON.stringify({\n                viewProps: {\n                    actionXmlId,\n                    additionalViewProps,\n                    context: {\n                        active_id: resId,\n                        default_parent_id: resId,\n                        default_is_article_item: true,\n                    },\n                    displayName: name,\n                    viewType,\n                },\n            }),\n        });\n        this.dependencies.dom.insert(embeddedViewBlueprint);\n        this.dependencies.history.addStep();\n    }\n\n    promptInsertEmbeddedCalendarView() {\n        let cursor = this.dependencies.selection.preserveSelection();\n        const resId = this.config.getRecordInfo().resId;\n        this.services.dialog.add(\n            ItemCalendarPropsDialog,\n            {\n                isNew: true,\n                knowledgeArticleId: resId,\n                saveItemCalendarProps: (name, itemCalendarProps) => {\n                    cursor = null;\n                    this.insertEmbeddedView(\n                        \"knowledge.knowledge_article_action_item_calendar\",\n                        name,\n                        \"calendar\",\n                        { itemCalendarProps }\n                    );\n                },\n            },\n            {\n                onClose: () => {\n                    cursor?.restore();\n                },\n            }\n        );\n    }\n\n    promptInsertEmbeddedView(viewType, withStages) {\n        let cursor = this.dependencies.selection.preserveSelection();\n        const resId = this.config.getRecordInfo().resId;\n        this.services.dialog.add(\n            PromptEmbeddedViewNameDialog,\n            {\n                isNew: true,\n                save: async (name) => {\n                    cursor = null;\n                    if (withStages) {\n                        await this.services.orm.call(\n                            \"knowledge.article\",\n                            \"create_default_item_stages\",\n                            [resId]\n                        );\n                    }\n                    this.insertEmbeddedView(\n                        `knowledge.knowledge_article_item_action${withStages ? \"_stages\" : \"\"}`,\n                        name,\n                        viewType\n                    );\n                },\n                viewType,\n            },\n            {\n                onClose: () => {\n                    cursor?.restore();\n                },\n            }\n        );\n    }\n}\n", "import { isHtmlContentSupported } from \"@html_editor/core/selection_plugin\";\nimport { Plugin } from \"@html_editor/plugin\";\nimport {\n    baseContainerGlobalSelector,\n    getBaseContainerSelector,\n} from \"@html_editor/utils/base_container\";\nimport { closestElement } from \"@html_editor/utils/dom_traversal\";\nimport { withSequence } from \"@html_editor/utils/resource\";\nimport { renderToElement } from \"@web/core/utils/render\";\nimport { _t } from \"@web/core/l10n/translation\";\n\nconst hostSelector = \"[data-embedded='foldableSection']\";\nconst titleSelector = \"[data-embedded-editable='title']\";\nconst contentSelector = \"[data-embedded-editable='content']\";\n\nexport class FoldableSectionPlugin extends Plugin {\n    static id = \"foldableSection\";\n    static dependencies = [\"baseContainer\", \"history\", \"selection\", \"dom\"];\n    resources = {\n        hints: [\n            withSequence(10, {\n                selector: `${hostSelector} ${titleSelector} > ${baseContainerGlobalSelector}`,\n                text: _t(\"Add a title to your section\"),\n            }),\n            withSequence(10, {\n                selector: `${hostSelector} ${contentSelector}:not(:focus) > ${baseContainerGlobalSelector}:only-child`,\n                text: _t(\"Type something inside this section\"),\n            }),\n        ],\n        move_node_blacklist_selectors: `${hostSelector} ${titleSelector} > ${baseContainerGlobalSelector}`,\n        user_commands: [\n            {\n                id: \"addFoldableSection\",\n                title: _t(\"Foldable Section\"),\n                description: _t(\"Add a foldable section.\"),\n                icon: \"fa-bookmark\",\n                run: () => this.insertFoldableSection(),\n                isAvailable: isHtmlContentSupported,\n            },\n        ],\n        powerbox_items: [\n            {\n                categoryId: \"structure\",\n                commandId: \"addFoldableSection\",\n                isAvailable: (selection) => !closestElement(selection.anchorNode, hostSelector),\n            },\n        ],\n        hint_targets_providers: (selectionData, editable) => [\n            ...editable.querySelectorAll(\n                `${hostSelector} ${contentSelector} > ${baseContainerGlobalSelector}:only-child`\n            ),\n        ],\n        power_buttons_visibility_predicates: this.showPowerButtons.bind(this),\n        split_element_block_overrides: withSequence(1, this.handleSplitElementBlock.bind(this)),\n        powerbox_blacklist_selectors: `${hostSelector} ${titleSelector} > ${baseContainerGlobalSelector}`,\n    };\n\n    handleSplitElementBlock({ targetNode }) {\n        if (closestElement(targetNode, `${hostSelector} ${titleSelector}`)) {\n            return true;\n        }\n    }\n\n    insertFoldableSection() {\n        const baseContainer = this.dependencies.baseContainer.createBaseContainer();\n        const baseContainerNodeName = baseContainer.nodeName;\n        const baseContainerClass = baseContainer.className;\n        const baseContainerSelector = getBaseContainerSelector(baseContainerNodeName);\n        const foldableSection = renderToElement(\"knowledge.FoldableSectionBlueprint\", {\n            baseContainerAttributes: {\n                class: baseContainerClass,\n            },\n            baseContainerNodeName,\n            embeddedProps: JSON.stringify({ showContent: true }),\n        });\n        this.dependencies.dom.insert(foldableSection);\n        this.dependencies.selection.setCursorStart(\n            foldableSection.querySelector(\n                `${hostSelector} ${titleSelector} ${baseContainerSelector}`\n            )\n        );\n        this.dependencies.history.addStep();\n    }\n    showPowerButtons(selection) {\n        return (\n            selection.isCollapsed &&\n            !closestElement(selection.anchorNode, `${hostSelector} ${titleSelector}`)\n        );\n    }\n}\n", "import { registry } from \"@web/core/registry\";\n\n// See `HtmlUpgradeManager` docstring for usage details.\nconst html_upgrade = registry.category(\"html_editor_upgrade\");\n\n// Handle the conversion of `o_knowledge_behavior_anchor` elements to their\n// `data-embedded` counterpart, when loading the value of a html_field.\nhtml_upgrade.category(\"1.0\").add(\"knowledge\", \"@knowledge/editor/html_migrations/migration-1.0\");\n\n// embeddedViews favorite irFilters should have a `user_ids` property\nhtml_upgrade.category(\"2.0\").add(\"knowledge\", \"@knowledge/editor/html_migrations/migration-2.0\");\n", "import { decodeDataBehaviorProps, getPropNameNode } from \"@knowledge/editor/html_migrations/utils\";\nimport { getOrigin } from \"@web/core/utils/urls\";\nimport { _t } from \"@web/core/l10n/translation\";\n\nexport function migrate(container, env) {\n    for (const [key, selector] of Object.entries(selectors)) {\n        const elements = container.querySelectorAll(selector);\n        if (elements.length) {\n            migrations[key](elements, env);\n        }\n    }\n}\n\nfunction migrateSearchModelState(searchModelState) {\n    searchModelState = JSON.parse(searchModelState);\n    const dfOptMap = {\n        this_year: \"year\",\n        last_year: \"year-1\",\n        antepenultimate_year: \"year-2\",\n        this_month: \"month\",\n        last_month: \"month-1\",\n        antepenultimate_month: \"month-2\",\n    };\n    for (const searchItem of Object.values(searchModelState.searchItems)) {\n        if (searchItem.type === \"dateFilter\") {\n            const newDefaults = new Set();\n            for (const generatorId of searchItem.defaultGeneratorIds) {\n                if (generatorId in dfOptMap) {\n                    newDefaults.add(dfOptMap[generatorId]);\n                }\n            }\n            if (newDefaults.size) {\n                searchItem.defaultGeneratorIds = Array.from(newDefaults);\n            }\n            if (!searchItem.optionsParams) {\n                searchItem.optionsParams = {\n                    startYear: -2,\n                    endYear: 0,\n                    startMonth: -2,\n                    endMonth: 0,\n                    customOptions: [],\n                };\n            }\n            for (const queryItem of searchModelState.query) {\n                if (\n                    queryItem.searchItemId === searchItem.id &&\n                    queryItem.generatorId &&\n                    queryItem.generatorId in dfOptMap\n                ) {\n                    queryItem.generatorId = dfOptMap[queryItem.generatorId];\n                }\n            }\n        }\n    }\n    return JSON.stringify(searchModelState);\n}\n\nconst selectors = {\n    articleBehavior: \".o_knowledge_behavior_type_article\",\n    articlesStructureBehavior: \".o_knowledge_behavior_type_articles_structure\",\n    comments: \".knowledge-thread-comment\",\n    drawBehavior: \".o_knowledge_behavior_type_draw\",\n    embeddedViewBehavior: \".o_knowledge_behavior_type_embedded_view\",\n    fileBehavior: \".o_knowledge_behavior_type_file\",\n    tableOfContentBehavior: \".o_knowledge_behavior_type_toc\",\n    templateBehavior: \".o_knowledge_behavior_type_template\",\n    videoBehavior: \".o_knowledge_behavior_type_video\",\n    viewLinkBehavior: \".o_knowledge_behavior_type_view_link\",\n};\n\nconst migrations = {\n    articleBehavior: (elements) => {\n        for (const el of elements) {\n            const oldProps = decodeDataBehaviorProps(el.dataset.behaviorProps);\n            if (!oldProps?.article_id || !oldProps?.display_name) {\n                // Abort the conversion if data can not be recovered.\n                // Element will still exist in the DOM as raw data.\n                continue;\n            }\n            delete el.dataset.behaviorProps;\n            el.dataset.res_id = oldProps?.article_id;\n            el.removeAttribute(\"tabindex\");\n            el.classList.remove(\"o_knowledge_behavior_anchor\", \"o_knowledge_behavior_type_article\");\n            el.classList.add(\"o_knowledge_article_link\");\n            el.replaceChildren(document.createTextNode(oldProps.display_name));\n        }\n    },\n    articlesStructureBehavior: (elements) => {\n        function buildIndex(el, props) {\n            const index = [];\n            while (el) {\n                const anchor = el.querySelector(\"a\");\n                const id = parseInt(\n                    anchor\n                        .getAttribute(\"href\")\n                        .match(/(\\d+)$/)\n                        .at(1)\n                );\n                const name = anchor.textContent;\n                const article = { id, name, childIds: [] };\n                el = el.nextElementSibling;\n                const child = el?.querySelector(\":scope > ol > li\");\n                if (child) {\n                    article.childIds = buildIndex(child, props);\n                    props.showAllChildren = true;\n                    el = el.nextElementSibling;\n                }\n                index.push(article);\n            }\n            return index;\n        }\n        for (const el of elements) {\n            const props = {};\n            try {\n                const content = getPropNameNode(\"content\", el);\n                const articles = buildIndex(content.querySelector(\"li\"), props);\n                if (articles.length) {\n                    props.articles = articles;\n                    el.dataset.embeddedProps = JSON.stringify(props);\n                }\n            } catch {\n                // ignore the existing article index if the parsing fails, it will\n                // have to be refreshed manually.\n            }\n            el.removeAttribute(\"class\");\n            el.removeAttribute(\"tabindex\");\n            el.dataset.embedded = \"articleIndex\";\n            el.replaceChildren();\n        }\n    },\n    comments: (elements, env) => {\n        function createBeacon({ type, threadId, resId, resModel, disabled }) {\n            const anchor = document.createElement(\"A\");\n            anchor.classList.add(\"oe_unremovable\", \"oe_thread_beacon\");\n            anchor.dataset.id = threadId;\n            anchor.dataset.res_id = resId;\n            anchor.dataset.resModel = resModel;\n            anchor.dataset.oeType = type;\n            if (disabled) {\n                anchor.classList.add(\"oe_disabled_thread_beacon\");\n            }\n            return anchor;\n        }\n        function createBeacons({ anchors, threadId, resId, resModel }) {\n            const start = anchors.at(0);\n            const end = anchors.at(-1);\n            if (!start || !end) {\n                return;\n            }\n            const disabled = !start.classList.contains(\"knowledge-thread-highlighted-comment\");\n            const beaconStart = createBeacon({\n                type: \"threadBeaconStart\",\n                threadId,\n                resId,\n                resModel,\n                disabled,\n            });\n            const beaconEnd = createBeacon({\n                type: \"threadBeaconEnd\",\n                threadId,\n                resId,\n                resModel,\n                disabled,\n            });\n            start.before(beaconStart);\n            end.after(beaconEnd);\n        }\n        function groupComments(anchors) {\n            const comments = {};\n            for (const anchor of anchors) {\n                const threadId = anchor.dataset.id;\n                if (!threadId) {\n                    continue;\n                }\n                comments[threadId] ||= [];\n                comments[threadId].push(anchor);\n            }\n            return comments;\n        }\n        const resId = env.model?.root?.resId;\n        const resModel = env.model?.root?.resModel;\n        if (resId && resModel) {\n            // Only create new comment beacons if the env has a record.\n            const comments = groupComments(elements);\n            for (const threadId in comments) {\n                createBeacons({ anchors: comments[threadId], threadId, resId, resModel });\n            }\n        }\n        // Remove old comments anchors (not a big deal if there is no replacement for some)\n        for (const el of [...elements]) {\n            if (el.nodeName === \"SPAN\") {\n                const childNodes = [];\n                while (el.firstChild) {\n                    childNodes.push(el.firstChild);\n                    el.firstChild.remove();\n                }\n                el.replaceWith(...childNodes);\n                continue;\n            }\n            el.classList.remove(\n                \"focused-comment\",\n                \"knowledge-thread-highlighted-comment\",\n                \"knowledge-thread-comment\"\n            );\n            delete el.dataset.id;\n            el.removeAttribute(\"tabindex\");\n        }\n    },\n    drawBehavior: (elements) => {\n        for (const el of elements) {\n            const oldProps = decodeDataBehaviorProps(el.dataset.behaviorProps);\n            if (!oldProps || !oldProps.source) {\n                // Abort the conversion if data can not be recovered.\n                // Element will still exist in the DOM as raw data.\n                continue;\n            }\n            const props = {\n                height: oldProps.height,\n                source: oldProps.source,\n                width: oldProps.width,\n            };\n            el.dataset.embedded = \"draw\";\n            el.dataset.embeddedProps = JSON.stringify(props);\n            delete el.dataset.behaviorProps;\n            delete el.dataset.oeTransientContent;\n            el.removeAttribute(\"class\");\n            el.removeAttribute(\"tabindex\");\n            el.replaceChildren();\n        }\n    },\n    embeddedViewBehavior: (elements) => {\n        for (const el of elements) {\n            const oldProps = decodeDataBehaviorProps(el.dataset.behaviorProps);\n            const viewProps = {\n                context: oldProps.context,\n                displayName: oldProps.display_name,\n                favoriteFilters: {},\n                id: oldProps.embedded_view_id,\n                viewType: oldProps.view_type,\n            };\n            if (oldProps.act_window) {\n                viewProps.actWindow = oldProps.act_window;\n            } else {\n                viewProps.actionXmlId = oldProps.action_xml_id;\n            }\n            if (oldProps.additionalViewProps) {\n                viewProps.additionalViewProps = oldProps.additionalViewProps;\n            }\n            if (oldProps.favorites) {\n                // favorites was an array, is now an object\n                for (const filter of oldProps.favorites) {\n                    viewProps.favoriteFilters[filter.name] = filter;\n                }\n            }\n            if (viewProps.context.knowledge_search_model_state) {\n                viewProps.context.knowledge_search_model_state = migrateSearchModelState(\n                    viewProps.context.knowledge_search_model_state\n                );\n            }\n            delete el.dataset.behaviorProps;\n            el.removeAttribute(\"class\");\n            el.removeAttribute(\"tabindex\");\n            el.dataset.embedded = \"view\";\n            el.dataset.embeddedProps = JSON.stringify({ viewProps });\n            el.replaceChildren();\n        }\n    },\n    fileBehavior: (elements) => {\n        for (const el of elements) {\n            const oldProps = decodeDataBehaviorProps(el.dataset.behaviorProps);\n            const htmlFileName = getPropNameNode(\"fileName\", el)?.textContent;\n            const htmlFileExtension = getPropNameNode(\"fileExtension\", el)?.textContent;\n            const htmlFileImageLink = getPropNameNode(\"fileImage\", el)?.querySelector(\"a\");\n            const href = htmlFileImageLink?.getAttribute(\"href\");\n            const mimetype = htmlFileImageLink?.dataset.mimetype;\n            let accessToken, checksum, id, type, url;\n            if (href?.startsWith(getOrigin())) {\n                id = parseInt((href.match(/\\/web\\/(?:content|image)\\/(\\d+)/) || [])[1]);\n                checksum = (href.match(/unique=([^&]+)/) || [])[1];\n                accessToken = (href.match(/access_token=([^&]+)/) || [])[1];\n            }\n            if (!id) {\n                type = \"url\";\n                url = href?.replace(/\\?.*$/, \"\");\n            } else {\n                type = \"binary\";\n            }\n            const fileName = htmlFileName || oldProps?.fileName || _t(\"Untitled\");\n            const extension = htmlFileExtension || oldProps?.fileExtension;\n            let fileData = oldProps?.fileData;\n            // accessToken has been renamed in file_model\n            if (fileData?.accessToken) {\n                fileData.access_token = fileData.accessToken;\n                delete fileData.accessToken;\n            }\n            if (!id && !url && !fileData) {\n                // Abort the conversion if data can not be recovered.\n                // Element will still exist in the DOM as raw data.\n                continue;\n            }\n            if (!fileData) {\n                fileData = {\n                    access_token: accessToken,\n                    checksum,\n                    extension,\n                    filename: fileName,\n                    id,\n                    mimetype,\n                    name: fileName,\n                    type,\n                    url,\n                };\n            }\n            const props = {\n                fileData,\n            };\n            el.removeAttribute(\"class\");\n            el.removeAttribute(\"tabindex\");\n            delete el.dataset.behaviorProps;\n            el.dataset.embedded = \"file\";\n            el.dataset.embeddedProps = JSON.stringify(props);\n            el.replaceChildren();\n        }\n    },\n    tableOfContentBehavior: (elements) => {\n        for (const el of elements) {\n            el.removeAttribute(\"class\");\n            el.removeAttribute(\"tabindex\");\n            el.dataset.embedded = \"tableOfContent\";\n            el.replaceChildren();\n        }\n    },\n    templateBehavior: (elements) => {\n        for (const el of elements) {\n            let content = getPropNameNode(\"content\", el);\n            if (!content) {\n                content = document.createElement(\"DIV\");\n                const p = document.createElement(\"P\");\n                const br = document.createElement(\"BR\");\n                p.append(br);\n                content.append(p);\n            }\n            content.removeAttribute(\"class\");\n            delete content.dataset.propName;\n            content.dataset.embeddedEditable = \"clipboardContent\";\n            el.removeAttribute(\"class\");\n            el.removeAttribute(\"tabindex\");\n            el.dataset.embedded = \"clipboard\";\n            el.replaceChildren(content);\n        }\n    },\n    videoBehavior: (elements) => {\n        for (const el of elements) {\n            const oldProps = decodeDataBehaviorProps(el.dataset.behaviorProps);\n            if (!oldProps || !oldProps.platform || !oldProps.videoId) {\n                // Abort the conversion if data can not be recovered.\n                // Element will still exist in the DOM as raw data.\n                continue;\n            }\n            delete el.dataset.behaviorProps;\n            const props = {\n                platform: oldProps.platform,\n                videoId: oldProps.videoId,\n                params: oldProps.params,\n            };\n            el.removeAttribute(\"class\");\n            el.removeAttribute(\"tabindex\");\n            el.dataset.embedded = \"video\";\n            el.dataset.embeddedProps = JSON.stringify(props);\n            el.replaceChildren();\n        }\n    },\n    viewLinkBehavior: (elements) => {\n        for (const el of elements) {\n            const oldProps = decodeDataBehaviorProps(el.dataset.behaviorProps);\n            const viewProps = {\n                context: oldProps.context,\n                displayName: oldProps.name,\n                viewType: oldProps.view_type,\n            };\n            const props = {};\n            if (oldProps.style) {\n                props.linkStyle = oldProps.style;\n            }\n            if (oldProps.act_window) {\n                viewProps.actWindow = oldProps.act_window;\n            } else {\n                viewProps.actionXmlId = oldProps.action_xml_id;\n            }\n            if (viewProps.context.knowledge_search_model_state) {\n                viewProps.context.knowledge_search_model_state = migrateSearchModelState(\n                    viewProps.context.knowledge_search_model_state\n                );\n            }\n            delete el.dataset.behaviorProps;\n            el.removeAttribute(\"class\");\n            el.removeAttribute(\"tabindex\");\n            el.dataset.embedded = \"viewLink\";\n            el.dataset.embeddedProps = JSON.stringify({ viewProps, ...props });\n            el.replaceChildren();\n        }\n    },\n};\n", "const SELECTOR = `[data-embedded=\"view\"],[data-embedded=\"viewLink\"]`;\n\n/**\n * This migration handles the `user_id` field being replaced by `user_ids`.\n *\n * In the new implementation, to have a favorite filter in the\n * `FAVORITE_SHARED_GROUP` of the SearchModel (as opposed to the\n * `FAVORITE_PRIVATE_GROUP`), `user_ids` must either be empty or have more than\n * one value. In Knowledge, all favorites of an embedded view are visible by\n * everyone, so it makes more sense to use `FAVORITE_SHARED_GROUP`, and use\n * an empty list as `user_ids` (as a reminder, embedded view favorites are\n * not real `ir.filters` records, they are stored as html meta data in an\n * article body, so they are not directly related to any user anymore).\n *\n * @param {HTMLElement} container\n */\nexport function migrate(container) {\n    for (const host of container.querySelectorAll(SELECTOR)) {\n        migrateEmbeddedViewIrFilters(host);\n    }\n}\n\nfunction migrateEmbeddedViewIrFilters(host) {\n    if (!host.dataset.embeddedProps) {\n        return;\n    }\n    const embeddedProps = JSON.parse(host.dataset.embeddedProps);\n    if (embeddedProps.viewProps?.context?.knowledge_search_model_state) {\n        migrateSearchModelState(embeddedProps);\n    }\n    if (embeddedProps.viewProps?.favoriteFilters) {\n        migrateFavoriteFilters(embeddedProps);\n    }\n    host.dataset.embeddedProps = JSON.stringify(embeddedProps);\n}\n\nfunction migrateSearchModelState(embeddedProps) {\n    const state = JSON.parse(embeddedProps.viewProps.context.knowledge_search_model_state);\n    if (!state.searchItems) {\n        return;\n    }\n    for (const searchItem of Object.values(state.searchItems)) {\n        if (searchItem.type !== \"favorite\") {\n            continue;\n        }\n        delete searchItem.userId;\n        searchItem.userIds = [];\n    }\n    embeddedProps.viewProps.context.knowledge_search_model_state = JSON.stringify(state);\n}\n\nfunction migrateFavoriteFilters(embeddedProps) {\n    const favoriteFilters = embeddedProps.viewProps.favoriteFilters;\n    for (const favorite of Object.values(favoriteFilters)) {\n        delete favorite.user_id;\n        favorite.user_ids = [];\n    }\n}\n", "/**\n * Convert the string from a data-behavior-props attribute to an usable object.\n *\n * @param {String} dataBehaviorPropsAttribute utf-8 encoded JSON string\n * @returns {Object} object containing props for a Behavior to store in the\n *                   html_field value of a field\n */\nexport function decodeDataBehaviorProps(dataBehaviorPropsAttribute) {\n    if (!dataBehaviorPropsAttribute) {\n        return undefined;\n    }\n    return JSON.parse(decodeURIComponent(dataBehaviorPropsAttribute));\n}\n\n/**\n * Return any existing propName node owned by the Behavior related to `anchor`.\n * Filter out propName nodes owned by children Behavior.\n *\n * @param {string} propName name of the htmlProp\n * @param {Element} anchor node to search for propName children\n * @returns {Element} last matching node (there should be only one, but it's\n *           always the last one that is taken as the effective prop)\n */\nexport function getPropNameNode(propName, anchor) {\n    const propNodes = anchor.querySelectorAll(`[data-prop-name=\"${propName}\"]`);\n    for (let i = propNodes.length - 1; i >= 0; i--) {\n        const closest = propNodes[i].closest(\".o_knowledge_behavior_anchor\");\n        if (closest === anchor) {\n            return propNodes[i];\n        }\n    }\n}\n", "import { MAIN_PLUGINS } from \"@html_editor/plugin_sets\";\nimport { AutofocusPlugin } from \"@knowledge/editor/plugins/autofocus_plugin/autofocus_plugin\";\nimport { KnowledgeArticlePlugin } from \"@knowledge/editor/plugins/article_plugin/article_plugin\";\nimport { KnowledgeCommentsPlugin } from \"@knowledge/editor/plugins/comments_plugin/comments_plugin\";\nimport { KnowledgeDeleteFirstLinePlugin } from \"@knowledge/editor/plugins/delete_first_line_plugin/delete_first_line_plugin\";\nimport { ArticleIndexPlugin } from \"@knowledge/editor/embedded_components/plugins/article_index_plugin/article_index_plugin\";\nimport { EmbeddedClipboardPlugin } from \"@knowledge/editor/embedded_components/plugins/embedded_clipboard_plugin/embedded_clipboard_plugin\";\nimport { EmbeddedViewPlugin } from \"@knowledge/editor/embedded_components/plugins/embedded_view_plugin/embedded_view_plugin\";\nimport { FoldableSectionPlugin } from \"@knowledge/editor/embedded_components/plugins/foldable_section_plugin/foldable_section_plugin\";\nimport { InsertPendingElementPlugin } from \"@knowledge/editor/plugins/insert_pending_element_plugin/insert_pending_element_plugin\";\nimport { HeadingLinkPlugin } from \"@knowledge/editor/plugins/heading_link_plugin/heading_link_plugin\";\n\nMAIN_PLUGINS.push(KnowledgeArticlePlugin);\n\nexport const KNOWLEDGE_PLUGINS = [\n    AutofocusPlugin,\n    InsertPendingElementPlugin,\n    KnowledgeCommentsPlugin,\n    KnowledgeDeleteFirstLinePlugin,\n    HeadingLinkPlugin,\n];\n\nexport const KNOWLEDGE_EMBEDDED_COMPONENT_PLUGINS = [\n    ArticleIndexPlugin,\n    EmbeddedClipboardPlugin,\n    EmbeddedViewPlugin,\n    FoldableSectionPlugin,\n];\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { rightPos } from \"@html_editor/utils/position\";\nimport { ArticleSearchDialog } from \"@knowledge/components/article_search_dialog/article_search_dialog\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { renderToElement } from \"@web/core/utils/render\";\nimport { isHtmlContentSupported } from \"@html_editor/core/selection_plugin\";\n\nconst ARTICLE_LINKS_SELECTOR = \".o_knowledge_article_link\";\nexport class KnowledgeArticlePlugin extends Plugin {\n    static id = \"article\";\n    static dependencies = [\"history\", \"dom\", \"selection\", \"dialog\"];\n    resources = {\n        user_commands: [\n            {\n                id: \"insertArticle\",\n                title: _t(\"Article\"),\n                description: _t(\"Insert an Article shortcut\"),\n                icon: \"fa-newspaper-o\",\n                run: this.addArticle.bind(this),\n                isAvailable: isHtmlContentSupported,\n            },\n        ],\n        powerbox_items: [\n            {\n                categoryId: \"navigation\",\n                commandId: \"insertArticle\",\n            },\n        ],\n        clean_for_save_handlers: this.cleanForSave.bind(this),\n        normalize_handlers: this.normalize.bind(this),\n    };\n\n    setup() {\n        super.setup();\n        this.boundOpenArticle = this.openArticle.bind(this);\n    }\n\n    addArticle() {\n        const recordInfo = this.config.getRecordInfo();\n        let parentArticleId;\n        if (recordInfo.resModel === \"knowledge.article\" && recordInfo.resId) {\n            parentArticleId = recordInfo.resId;\n        }\n        const cursors = this.dependencies.selection.preserveSelection();\n        const renderArticleLink = (id, displayName) => {\n            const articleLinkBlock = renderToElement(\"knowledge.ArticleBlueprint\", {\n                href: `/knowledge/article/${id}`,\n                articleId: id,\n                displayName,\n            });\n            cursors.restore();\n            this.dependencies.dom.insert(articleLinkBlock);\n            this.dependencies.history.addStep();\n            const [anchorNode, anchorOffset] = rightPos(articleLinkBlock);\n            this.dependencies.selection.setSelection({ anchorNode, anchorOffset });\n        };\n        this.services.dialog.add(ArticleSearchDialog, {\n            create: async (label) => {\n                const articleIds = await this.services.orm.call(\n                    \"knowledge.article\",\n                    \"article_create\",\n                    [],\n                    {\n                        title: label,\n                        parent_id: parentArticleId,\n                    }\n                );\n                const articleId = articleIds[0];\n                renderArticleLink(articleId, `\ud83d\udcc4 ${label}`);\n                if (parentArticleId) {\n                    this.config.embeddedComponentInfo.env.bus.trigger(\n                        \"knowledge.sidebar.insertNewArticle\",\n                        {\n                            articleId,\n                            name: label,\n                            icon: \"\ud83d\udcc4\",\n                            parentId: parentArticleId,\n                        }\n                    );\n                }\n            },\n            search: (searchValue) => {\n                const params = { search_query: searchValue };\n                let searchFunction = \"get_user_sorted_articles\";\n                if (searchValue) {\n                    searchFunction = \"get_sorted_articles\";\n                    params.domain = [\n                        \"|\",\n                        [\"is_article_visible\", \"=\", true],\n                        [\"is_user_favorite\", \"=\", true],\n                    ];\n                }\n                return this.services.orm.call(\"knowledge.article\", searchFunction, [[]], params);\n            },\n            searchEmptyQuery: true,\n            select: (article) => renderArticleLink(article.id, article.displayName),\n        }, { onClose: () => { this.dependencies.selection.focusEditable(); }});\n    }\n\n    scanForArticleLinks(element) {\n        const articleLinks = [...element.querySelectorAll(ARTICLE_LINKS_SELECTOR)];\n        if (element.matches(ARTICLE_LINKS_SELECTOR)) {\n            articleLinks.unshift(element);\n        }\n        return articleLinks;\n    }\n\n    async openArticle(ev) {\n        if (this.config.embeddedComponentInfo?.env?.openArticle) {\n            const articleId = parseInt(ev.target.dataset.res_id);\n            if (articleId) {\n                ev.preventDefault();\n                await this.config.embeddedComponentInfo.env.openArticle(articleId);\n            }\n        }\n    }\n\n    normalize(element) {\n        const articleLinks = this.scanForArticleLinks(element);\n        for (const articleLink of articleLinks) {\n            articleLink.setAttribute(\"target\", \"_blank\");\n            articleLink.setAttribute(\"contenteditable\", \"false\");\n            articleLink.addEventListener(\"click\", this.boundOpenArticle);\n        }\n    }\n\n    cleanForSave({ root }) {\n        const articleLinks = this.scanForArticleLinks(root);\n        for (const articleLink of articleLinks) {\n            articleLink.removeAttribute(\"contenteditable\");\n        }\n    }\n\n    destroy() {\n        super.destroy();\n        const articleLinks = this.scanForArticleLinks(this.editable);\n        for (const articleLink of articleLinks) {\n            articleLink.removeEventListener(\"click\", this.boundOpenArticle);\n        }\n    }\n}\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { paragraphRelatedElementsSelector } from \"@html_editor/utils/dom_info\";\n\nexport class AutofocusPlugin extends Plugin {\n    static id = \"autofocus\";\n    static dependencies = [\"selection\"];\n    resources = {\n        start_edition_handlers: this.focusFirstElement.bind(this),\n    };\n\n    focusFirstElement() {\n        for (const paragraph of this.editable.querySelectorAll(paragraphRelatedElementsSelector)) {\n            if (paragraph.isContentEditable) {\n                const { anchorNode, anchorOffset, focusNode, focusOffset } =\n                    this.dependencies.selection.setSelection({\n                        anchorNode: paragraph,\n                        anchorOffset: 0,\n                    });\n                const selectionData = this.dependencies.selection.getSelectionData();\n                if (!selectionData.documentSelectionIsInEditable) {\n                    const selection = this.document.getSelection();\n                    selection.setBaseAndExtent(anchorNode, anchorOffset, focusNode, focusOffset);\n                }\n                break;\n            }\n        }\n    }\n}\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { leftPos, rightPos, nodeSize } from \"@html_editor/utils/position\";\nimport { renderToElement } from \"@web/core/utils/render\";\nimport { CommentBeaconManager } from \"@knowledge/comments/comment_beacon_manager\";\nimport { registry } from \"@web/core/registry\";\nimport { KnowledgeCommentsHandler } from \"@knowledge/comments/comments_handler/comments_handler\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { closestElement, childNodes } from \"@html_editor/utils/dom_traversal\";\nimport { callbacksForCursorUpdate } from \"@html_editor/utils/selection\";\nimport { fillEmpty } from \"@html_editor/utils/dom\";\nimport {\n    isParagraphRelatedElement,\n    isPhrasingContent,\n    isContentEditable,\n    isZwnbsp,\n    isListItemElement,\n} from \"@html_editor/utils/dom_info\";\nimport { uniqueId } from \"@web/core/utils/functions\";\nimport { effect } from \"@web/core/utils/reactive\";\nimport { batched } from \"@web/core/utils/timing\";\nimport { withSequence } from \"@html_editor/utils/resource\";\nimport { isHtmlContentSupported } from \"@html_editor/core/selection_plugin\";\n\nexport class KnowledgeCommentsPlugin extends Plugin {\n    static id = \"knowledgeComments\";\n    static dependencies = [\n        \"baseContainer\",\n        \"history\",\n        \"dom\",\n        \"feff\",\n        \"protectedNode\",\n        \"selection\",\n        \"position\",\n        \"localOverlay\",\n        \"format\",\n        \"delete\",\n    ];\n    resources = {\n        user_commands: [\n            {\n                id: \"addComments\",\n                icon: \"fa-commenting-o\",\n                run: this.addCommentToSelection.bind(this),\n                isAvailable: isHtmlContentSupported,\n            },\n        ],\n        toolbar_groups: [\n            withSequence(60, {\n                id: \"knowledge\",\n                namespaces: [\"compact\", \"expanded\"],\n            }),\n            withSequence(60, {\n                id: \"knowledge_image\",\n                namespaces: [\"image\"],\n            }),\n        ],\n        toolbar_items: [\n            {\n                id: \"comments\",\n                groupId: \"knowledge\",\n                commandId: \"addComments\",\n                description: _t(\"Add a comment to selection\"),\n                text: _t(\"Comment\"),\n                namespaces: [\"expanded\"],\n                isDisabled: () => !this.canAddCommentToSelection(),\n            },\n            {\n                id: \"comments_small\",\n                groupId: \"knowledge\",\n                commandId: \"addComments\",\n                description: _t(\"Add a comment to selection\"),\n                namespaces: [\"compact\"],\n                isDisabled: () => !this.canAddCommentToSelection(),\n            },\n            {\n                id: \"comments_image\",\n                groupId: \"knowledge_image\",\n                commandId: \"addComments\",\n                description: _t(\"Add a comment to an image\"),\n                text: _t(\"Comment\"),\n                isDisabled: () => !this.canAddCommentToSelection(),\n            },\n        ],\n\n        /** Handlers */\n        layout_geometry_change_handlers: () => {\n            this.commentBeaconManager?.drawThreadOverlays();\n            this.config.onLayoutGeometryChange();\n        },\n        selectionchange_handlers: (selectionData) => {\n            if (\n                !selectionData.documentSelectionIsInEditable ||\n                !selectionData.editableSelection ||\n                selectionData.documentSelectionIsProtected ||\n                selectionData.documentSelectionIsProtecting\n            ) {\n                return;\n            }\n            const editableSelection = selectionData.editableSelection;\n            const target =\n                editableSelection.anchorNode.nodeType === Node.TEXT_NODE\n                    ? editableSelection.anchorNode\n                    : childNodes(editableSelection.anchorNode).at(editableSelection.anchorOffset);\n            if (!target || !target.isConnected) {\n                return;\n            }\n            this.commentBeaconManager.activateRelatedThread(target);\n        },\n        restore_savepoint_handlers: this.updateBeacons.bind(this),\n        history_reset_handlers: () => this.updateBeacons(),\n        history_reset_from_steps_handlers: this.updateBeacons.bind(this),\n        step_added_handlers: () => this.commentBeaconManager.drawThreadOverlays(),\n        external_step_added_handlers: this.updateBeacons.bind(this),\n        clean_for_save_handlers: this.cleanForSave.bind(this),\n        normalize_handlers: this.normalize.bind(this),\n\n        /** Overrides */\n        delete_forward_overrides: withSequence(1, this.handleDeleteForward.bind(this)),\n        delete_backward_overrides: withSequence(1, this.handleDeleteBackward.bind(this)),\n\n        feff_providers: this.addFeffToBeacon.bind(this),\n        selectors_for_feff_providers: () => \".oe_thread_beacon\",\n\n        intangible_char_for_keyboard_navigation_predicates: this.arrowShouldSkip.bind(this),\n    };\n\n    setup() {\n        // ensure that this plugin is not dependent on the collaboration plugin\n        this.peerId = this.config.collaboration?.peerId ?? \"\";\n        this.commentsService = this.services[\"knowledge.comments\"];\n        this.commentsState = this.commentsService.getCommentsState();\n        // reset pending beacons\n        let previousActiveThreadId;\n        this.alive = true;\n        effect(\n            batched((state) => {\n                if (!this.alive) {\n                    return;\n                }\n                if (previousActiveThreadId === state.activeThreadId) {\n                    return;\n                }\n                if (\n                    previousActiveThreadId !== \"undefined\" &&\n                    state.activeThreadId === \"undefined\"\n                ) {\n                    this.commentBeaconManager.pendingBeacons = new Set();\n                } else if (previousActiveThreadId === \"undefined\") {\n                    this.commentBeaconManager.sortThreads();\n                    if (this.commentBeaconManager.bogusBeacons.size) {\n                        this.commentBeaconManager.removeBogusBeacons();\n                        this.dependencies.history.addStep();\n                    }\n                    this.commentBeaconManager.drawThreadOverlays();\n                }\n                previousActiveThreadId = state.activeThreadId;\n            }),\n            [this.commentsState]\n        );\n        this.localOverlay =\n            this.dependencies.localOverlay.makeLocalOverlay(\"KnowledgeThreadBeacons\");\n        this.commentBeaconManager = new CommentBeaconManager({\n            document: this.document,\n            source: this.editable,\n            overlayContainer: this.localOverlay,\n            commentsState: this.commentsState,\n            peerId: this.peerId,\n            removeBeacon: this.removeBeacon.bind(this),\n            setSelection: this.dependencies.selection.setSelection,\n            onStep: () => {\n                this.dependencies.history.addStep();\n            },\n        });\n        this.addDomListener(window, \"click\", this.onWindowClick.bind(this));\n        this.overlayComponentsKey = uniqueId(\"KnowledgeCommentsHandler\");\n        registry.category(this.config.localOverlayContainers.key).add(\n            this.overlayComponentsKey,\n            {\n                Component: KnowledgeCommentsHandler,\n                props: {\n                    commentBeaconManager: this.commentBeaconManager,\n                    contentRef: {\n                        el: this.editable,\n                    },\n                },\n            },\n            { force: true }\n        );\n    }\n\n    addFeffToBeacon(root) {\n        const feffs = [];\n        for (const beacon of root.querySelectorAll(\".oe_thread_beacon\")) {\n            if (nodeSize(beacon) !== 1 || !isZwnbsp(beacon.firstChild)) {\n                beacon.replaceChildren();\n                this.dependencies.feff.addFeff(beacon, \"prepend\");\n            }\n            feffs.push(beacon.firstChild);\n        }\n        return feffs;\n    }\n\n    arrowShouldSkip(ev, char, lastSkipped) {\n        if (char !== undefined || lastSkipped !== \"\\uFEFF\") {\n            return;\n        }\n        const selection = this.document.getSelection();\n        if (!selection) {\n            return;\n        }\n        const { anchorNode, focusNode } = selection;\n        if (\n            !this.editable.contains(anchorNode) ||\n            (focusNode !== anchorNode && !this.editable.contains(focusNode))\n        ) {\n            return;\n        }\n        const screenDirection = ev.key === \"ArrowLeft\" ? \"left\" : \"right\";\n        const isRtl = closestElement(focusNode, \"[dir]\")?.dir === \"rtl\";\n        const domDirection = (screenDirection === \"left\") ^ isRtl ? \"previous\" : \"next\";\n        let targetNode;\n        let targetOffset;\n        const range = selection.getRangeAt(0);\n        if (ev.shiftKey) {\n            targetNode = selection.focusNode;\n            targetOffset = selection.focusOffset;\n        } else {\n            if (domDirection === \"previous\") {\n                targetNode = range.startContainer;\n                targetOffset = range.startOffset;\n            } else {\n                targetNode = range.endContainer;\n                targetOffset = range.endOffset;\n            }\n        }\n        if (domDirection === \"previous\") {\n            const beacon = this.identifyPreviousBeacon({\n                endContainer: targetNode,\n                endOffset: targetOffset,\n            });\n            return beacon && this.commentBeaconManager.isDisabled(beacon);\n        } else {\n            const beacon = this.identifyNextBeacon({\n                startContainer: targetNode,\n                startOffset: targetOffset,\n            });\n            return beacon && this.commentBeaconManager.isDisabled(beacon);\n        }\n    }\n\n    identifyNextBeacon(range) {\n        const { startContainer, startOffset } = range;\n        if (startContainer.nodeType !== Node.TEXT_NODE) {\n            return;\n        }\n        let container;\n        if (isZwnbsp(startContainer)) {\n            container = startContainer;\n        } else if (\n            startOffset === nodeSize(startContainer) &&\n            startContainer.nextSibling?.nodeType === Node.TEXT_NODE &&\n            isZwnbsp(startContainer.nextSibling)\n        ) {\n            container = startContainer.nextSibling;\n        }\n        if (container) {\n            const [anchorNode, anchorOffset] = rightPos(container);\n            const target = childNodes(anchorNode).at(anchorOffset);\n            if (target?.matches?.(\".oe_thread_beacon\")) {\n                return target;\n            }\n        }\n    }\n\n    identifyPreviousBeacon(range) {\n        const { endContainer, endOffset } = range;\n        if (endContainer.nodeType !== Node.TEXT_NODE) {\n            return;\n        }\n        let container;\n        if (isZwnbsp(endContainer)) {\n            container = endContainer;\n        } else if (\n            endOffset === 0 &&\n            endContainer.previousSibling?.nodeType === Node.TEXT_NODE &&\n            isZwnbsp(endContainer.previousSibling)\n        ) {\n            // This part of the condition may not be necessary since\n            // it seems that endOffset is never set at 0\n            container = endContainer.previousSibling;\n        }\n        if (container) {\n            const [anchorNode, anchorOffset] = leftPos(container);\n            if (anchorOffset === 0) {\n                return;\n            }\n            const target = childNodes(anchorNode).at(anchorOffset - 1);\n            if (target?.matches?.(\".oe_thread_beacon\")) {\n                return target;\n            }\n        }\n    }\n\n    isAllowedBeaconPosition(node) {\n        return (\n            closestElement(node).nodeName !== \"PRE\" &&\n            (isPhrasingContent(node) ||\n                isParagraphRelatedElement(node) ||\n                isListItemElement(node) ||\n                this.dependencies.baseContainer.isCandidateForBaseContainer(node))\n        );\n    }\n\n    // TODO ABD: -> CTRL + DELETE needs some custo too (currently deletes too much)\n    handleDeleteForward(range) {\n        // allow deleteForward to go past a beacon instead of being blocked.\n        // TODO ABD: add tests for both cases\n        const target = this.identifyNextBeacon(range);\n        if (target) {\n            const [anchorNode, anchorOffset] = rightPos(target);\n            this.dependencies.selection.setSelection({\n                anchorNode,\n                anchorOffset,\n            });\n            this.dependencies.delete.delete(\"forward\", \"character\");\n            return true;\n        }\n    }\n\n    handleDeleteBackward(range) {\n        // allow deleteBackward to go past a beacon instead of being blocked.\n        const target = this.identifyPreviousBeacon(range);\n        if (target) {\n            const [anchorNode, anchorOffset] = leftPos(target);\n            this.dependencies.selection.setSelection({\n                anchorNode,\n                anchorOffset,\n            });\n            this.dependencies.delete.delete(\"backward\", \"character\");\n            return true;\n        }\n    }\n\n    addCommentToSelection() {\n        const { startContainer, startOffset, endContainer, endOffset } =\n            this.dependencies.selection.getEditableSelection({ deep: true });\n        if (!this.canAddCommentToSelection()) {\n            return;\n        }\n        const previousUndefinedBeacons = [\n            ...this.editable.querySelectorAll(\".oe_thread_beacon[data-id='undefined']\"),\n        ];\n        this.commentBeaconManager.pendingBeacons = new Set();\n        const endBeacon = renderToElement(\"knowledge.threadBeacon\", {\n            threadId: \"undefined\",\n            type: \"threadBeaconEnd\",\n            recordId: this.commentsState.articleId,\n            recordModel: \"knowledge.article\",\n            peerId: this.peerId,\n        });\n        this.dependencies.selection.setSelection({\n            anchorNode: endContainer,\n            anchorOffset: endOffset,\n        });\n        this.commentBeaconManager.pendingBeacons.add(endBeacon);\n        this.dependencies.dom.insert(endBeacon);\n        const startBeacon = renderToElement(\"knowledge.threadBeacon\", {\n            threadId: \"undefined\",\n            type: \"threadBeaconStart\",\n            recordId: this.commentsState.articleId,\n            recordModel: \"knowledge.article\",\n            peerId: this.peerId,\n        });\n        this.dependencies.selection.setSelection({\n            anchorNode: startContainer,\n            anchorOffset: startOffset,\n        });\n        this.commentBeaconManager.pendingBeacons.add(startBeacon);\n        this.dependencies.dom.insert(startBeacon);\n        this.commentBeaconManager.cleanupThread(\"undefined\");\n        for (const beacon of previousUndefinedBeacons) {\n            this.removeBeacon(beacon);\n        }\n        const [anchorNode, anchorOffset] = rightPos(startBeacon);\n        this.dependencies.selection.setSelection({\n            anchorNode,\n            anchorOffset,\n        });\n        this.commentsState.displayMode = \"handler\";\n        this.commentsService.createVirtualThread();\n        this.commentsState.activeThreadId = \"undefined\";\n        this.dependencies.history.addStep();\n    }\n\n    canAddCommentToSelection() {\n        const { startContainer, endContainer, isCollapsed } =\n            this.dependencies.selection.getEditableSelection({ deep: true });\n        return (\n            !isCollapsed &&\n            this.isAllowedBeaconPosition(startContainer) &&\n            this.isAllowedBeaconPosition(endContainer) &&\n            isContentEditable(startContainer) &&\n            isContentEditable(endContainer)\n        );\n    }\n\n    onWindowClick(ev) {\n        const selector = `.oe-local-overlay, .o_knowledge_comment_box, .o-we-toolbar, .o-overlay-container`;\n        const closestElement = ev.target.closest(selector);\n        if (!closestElement && !this.editable.contains(ev.target)) {\n            this.commentsState.activeThreadId = undefined;\n        }\n    }\n\n    normalize(elem) {\n        this.commentBeaconManager.sortThreads();\n        // TODO ABD: think about the fact that a beacon can be normalized in different steps\n        // for different users, is this an issue ?\n        this.commentBeaconManager.removeBogusBeacons();\n        for (const beacon of elem.querySelectorAll(\".oe_thread_beacon\")) {\n            if (beacon.isConnected && !this.isAllowedBeaconPosition(beacon.parentElement)) {\n                this.commentBeaconManager.cleanupBeaconPair(beacon.dataset.id);\n                this.removeBeacon(beacon);\n                continue;\n            }\n            this.dependencies.protectedNode.setProtectingNode(beacon, true);\n        }\n    }\n\n    cleanZwnbsp(beacon) {\n        if (!beacon.isConnected) {\n            return;\n        }\n        const cursors = this.dependencies.selection.preserveSelection();\n        if (\n            isZwnbsp(beacon.previousSibling) &&\n            beacon.previousSibling.previousSibling?.nodeName !== \"A\"\n        ) {\n            cursors.update(callbacksForCursorUpdate.remove(beacon.previousSibling));\n            beacon.previousSibling.remove();\n        }\n        if (isZwnbsp(beacon.nextSibling) && beacon.nextSibling.nextSibling?.nodeName !== \"A\") {\n            cursors.update(callbacksForCursorUpdate.remove(beacon.nextSibling));\n            beacon.nextSibling.remove();\n        }\n        cursors.restore();\n    }\n\n    removeBeacon(beacon) {\n        if (!beacon.isConnected) {\n            return;\n        }\n        this.cleanZwnbsp(beacon);\n        const cursors = this.dependencies.selection.preserveSelection();\n        const parent = beacon.parentElement;\n        cursors.update(callbacksForCursorUpdate.remove(beacon));\n        beacon.remove();\n        cursors.restore();\n        fillEmpty(parent);\n        this.dependencies.format.mergeAdjacentInlines(parent);\n    }\n\n    updateBeacons() {\n        this.commentBeaconManager.sortThreads();\n        this.commentBeaconManager.drawThreadOverlays();\n    }\n\n    destroy() {\n        super.destroy();\n        this.alive = false;\n        registry.category(this.config.localOverlayContainers.key).remove(this.overlayComponentsKey);\n        this.commentBeaconManager.destroy();\n        this.localOverlay.remove();\n    }\n\n    cleanForSave({ root }) {\n        const bogusIds = new Set([\"undefined\"]);\n        for (const beacon of this.commentBeaconManager.bogusBeacons) {\n            bogusIds.add(beacon.dataset.id);\n        }\n        for (const beacon of root.querySelectorAll(\".oe_thread_beacon\")) {\n            if (bogusIds.has(beacon.dataset.id)) {\n                beacon.remove();\n            } else {\n                // remove zwnbsp\n                beacon.replaceChildren();\n                delete beacon.dataset.peerId;\n            }\n        }\n    }\n}\n", "import { Plugin } from \"@html_editor/plugin\";\nimport {\n    isEmptyBlock,\n    isParagraphRelatedElement,\n    paragraphRelatedElementsSelector,\n} from \"@html_editor/utils/dom_info\";\nimport { closestBlock } from \"@html_editor/utils/blocks\";\n\nexport class KnowledgeDeleteFirstLinePlugin extends Plugin {\n    static id = \"deleteFirstLinePlugin\";\n    static dependencies = [\"selection\", \"history\"];\n    resources = {\n        delete_backward_overrides: this.handleDeleteBackward.bind(this),\n    };\n\n    /**\n     * Handles the deletion of the 1st line.\n     * @param {Range} range\n     * @returns\n     */\n    handleDeleteBackward(range) {\n        const { endContainer } = range;\n        const endContainerBlock = closestBlock(endContainer);\n        if (\n            !(isParagraphRelatedElement(endContainerBlock) && isEmptyBlock(endContainerBlock)) ||\n            !endContainerBlock.matches(\".odoo-editor-editable > *:first-child\") ||\n            endContainerBlock.matches(\":only-child\")\n        ) {\n            return;\n        }\n        let current = endContainerBlock.nextElementSibling;\n        while (current && !current.matches(paragraphRelatedElementsSelector)) {\n            current = current.nextElementSibling;\n        }\n        if (!current) {\n            const newParagraph = this.dependencies.baseContainer.createBaseContainer();\n            newParagraph.appendChild(this.document.createElement(\"br\"));\n            this.editable.append(newParagraph);\n            current = newParagraph;\n        }\n        endContainerBlock.remove();\n        this.dependencies.selection.setCursorStart(current);\n        return true;\n    }\n}\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { browser } from \"@web/core/browser/browser\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { ancestors, descendants } from \"@html_editor/utils/dom_traversal\";\nimport { xml } from \"@odoo/owl\";\nimport { renderToElement } from \"@web/core/utils/render\";\n\nexport class HeadingLinkPlugin extends Plugin {\n    static id = \"headingLink\";\n    static dependencies = [\"localOverlay\", \"history\"];\n    resources = {\n        /** Handlers */\n        start_edition_handlers: this.onStartEdition.bind(this),\n        normalize_handlers: (root) => this.updateHeadingIds(root),\n        clean_for_save_handlers: ({ root }) => this.cleanForSave(root),\n        after_split_element_handlers: ({ secondPart }) => this.onAfterSplitElement(secondPart),\n\n        system_classes: [\"o-highlight-heading\"],\n    };\n\n    setup() {\n        // Create an overlay with the anchor.\n        this.headingLinkOverlay = this.dependencies.localOverlay.makeLocalOverlay(\"o-heading-link-overlay\");\n        this.headingLinkContainer = renderToElement(xml`\n            <div class=\"d-flex\">\n                <a href=\"#\" title=\"${_t(\"Copy a link to this heading to the clipboard\")}\" class=\"o-heading-link fa fa-link\"/>\n            </div>\n        `);\n        this.headingLink = this.headingLinkContainer.firstElementChild;\n        this.headingLinkOverlay.append(this.headingLinkContainer);\n        this.headingLinkOverlay.style.visibility = \"hidden\";\n        this.addDomListener(this.headingLink, \"click\", () => {\n            const headingId = this.currentHeading?.getAttribute(\"data-heading-link-id\");\n            // Navigate to the heading and add the url to the clipboard.\n            browser.location.hash = headingId;\n            browser.navigator.clipboard.writeText(browser.location.href);\n            // Add a step to the history and make sure the ID is saved.\n            this.dependencies.history.addStep();\n            // Highlight the heading.\n            this.highlightHeading(headingId);\n        });\n        this.addDomListener(this.headingLink, \"dragstart\", ev => ev.preventDefault());\n\n        this.addDomListener(this.editable, \"mousemove\", this.onMousemove, true);\n        this.updateHeadingIds();\n    }\n\n    onStartEdition() {\n        if (browser.location.hash) {\n            const headingId = browser.location.hash.replace(/^#/, \"\");\n            if (headingId) {\n                // Wait until the browser has rendered the editor before\n                // scrolling. The timeout value of 500 is a little arbitrary,\n                // but it should be enough to prevent an irritating case where\n                // a Youtube video is in the document and loads while the\n                // autoscroll is happening, and stops it.\n                setTimeout(() => {\n                    this.highlightHeading(headingId);\n                }, 500);\n            }\n        }\n    }\n\n    onMousemove(ev) {\n        const heading = ev.target?.closest?.(\"h1, h2, h3, h4, h5, h6\");\n        if (heading?.textContent) {\n            this.currentHeading = heading;\n            // Resetting the position of the overlay.\n            this.headingLinkOverlay.style.top = \"0px\";\n            this.headingLinkOverlay.style.left = \"0px\";\n            const containerRect = this.headingLinkContainer.getBoundingClientRect();\n            // Get the range rectangle to position the overlay after it.\n            const range = this.document.createRange();\n            range.selectNodeContents(this.currentHeading);\n            const rangeRect = range.getBoundingClientRect();\n            // Position the overlay.\n            this.headingLinkOverlay.style.top = `${rangeRect.top - containerRect.top + ((rangeRect.height - containerRect.height) / 2) + 2}px`;\n            this.headingLinkOverlay.style.left = `${rangeRect.right - containerRect.left + 5}px`;\n            this.headingLinkOverlay.style.visibility = \"visible\";\n        } else {\n            this.headingLinkOverlay.style.visibility = \"hidden\";\n        }\n    }\n\n    cleanForSave(root) {\n        for (const el of root.querySelectorAll(\".o-highlight-heading\")) {\n            el.classList.remove(\"o-highlight-heading\");\n        }\n    }\n\n    onAfterSplitElement(secondPart) {\n        // Ensure the ID doesn't get cloned.\n        secondPart?.removeAttribute(\"data-heading-link-id\");\n    }\n\n    /**\n     * @param {Element} [root]\n     */\n    updateHeadingIds(root = this.editable) {\n        const headings = [root, ...ancestors(root, this.editable), ...descendants(root)]\n            .filter(node => node && /^H\\d$/.test(node.nodeName));\n        for (const heading of [...new Set(headings)]) {\n            const headingId = heading.getAttribute(\"data-heading-link-id\");\n            if (!headingId) {\n                heading.setAttribute(\"data-heading-link-id\", \"\" + Math.floor(Math.random() * Date.now()));\n            }\n        }\n    }\n\n    highlightHeading(headingId) {\n        const heading = this.editable.querySelector(`[data-heading-link-id=\"${headingId}\"]`);\n        if (heading) {\n            heading.scrollIntoView({ behavior: \"smooth\" });\n            heading.classList.add(\"o-highlight-heading\");\n            setTimeout(() => {\n                heading.classList.remove(\"o-highlight-heading\");\n            }, 2000);\n        }\n    }\n}\n", "import { Plugin } from \"@html_editor/plugin\";\nimport {\n    isEmptyBlock,\n    isParagraphRelatedElement,\n    isPhrasingContent,\n} from \"@html_editor/utils/dom_info\";\nimport { children } from \"@html_editor/utils/dom_traversal\";\n\nexport class InsertPendingElementPlugin extends Plugin {\n    static id = \"insertPendingElement\";\n    static dependencies = [\"baseContainer\", \"history\", \"dom\", \"selection\"];\n    resources = {\n        start_edition_handlers: this.insertEmbeddedBluePrint.bind(this),\n    };\n\n    insertEmbeddedBluePrint() {\n        const { resModel, resId } = this.config.getRecordInfo();\n        const embeddedBlueprint =\n            this.services.knowledgeCommandsService.popPendingEmbeddedBlueprint({\n                field: \"body\",\n                resId,\n                model: resModel,\n            });\n        if (embeddedBlueprint) {\n            let insert;\n            if (isPhrasingContent(embeddedBlueprint)) {\n                insert = () => {\n                    // insert phrasing content\n                    const paragraph = this.dependencies.baseContainer.createBaseContainer();\n                    paragraph.appendChild(embeddedBlueprint);\n                    this.dependencies.selection.setCursorEnd(this.editable);\n                    this.dependencies.dom.insert(paragraph);\n                    this.dependencies.history.addStep();\n                };\n            } else {\n                insert = () => {\n                    // insert block content\n                    const childElements = children(this.editable);\n                    let cursorTarget = null;\n                    if (\n                        childElements.every((child) => {\n                            if (isParagraphRelatedElement(child) && isEmptyBlock(child)) {\n                                cursorTarget ??= child;\n                                return true;\n                            }\n                        })\n                    ) {\n                        this.dependencies.selection.setCursorStart(this.editable);\n                        this.dependencies.dom.insert(embeddedBlueprint);\n                        this.dependencies.selection.setCursorStart(cursorTarget);\n                    } else {\n                        this.dependencies.selection.setCursorEnd(this.editable);\n                        this.dependencies.dom.insert(embeddedBlueprint);\n                        const paragraph = this.dependencies.baseContainer.createBaseContainer();\n                        paragraph.appendChild(this.document.createElement(\"br\"));\n                        this.dependencies.selection.setCursorEnd(this.editable);\n                        this.dependencies.dom.insert(paragraph);\n                    }\n                    this.dependencies.history.addStep();\n                };\n            }\n            insert();\n            this.editable.addEventListener(\"onHistoryResetFromPeer\", insert, { once: true });\n        }\n    }\n}\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { useOwnedDialogs, useService } from \"@web/core/utils/hooks\";\nimport { SelectCreateDialog } from \"@web/views/view_dialogs/select_create_dialog\";\n\n/**\n * @returns {function}\n */\nexport const useKnowledgeArticleSelector = () => {\n    const openDialog = useOwnedDialogs();\n    const orm = useService(\"orm\");\n    /** @param {function} onSelectCallback */\n    return (onSelectCallback) => {\n        openDialog(SelectCreateDialog, {\n            title: _t(\"Select an article\"),\n            noCreate: false,\n            multiSelect: false,\n            resModel: \"knowledge.article\",\n            context: {},\n            domain: [\n                [\"user_has_write_access\", \"=\", true],\n                [\"is_template\", \"=\", false],\n            ],\n            onSelected: (resIds) => {\n                onSelectCallback(resIds[0]);\n            },\n            onCreateEdit: async () => {\n                const articleIds = await orm.call(\"knowledge.article\", \"article_create\", [], {\n                    is_private: true,\n                });\n                onSelectCallback(articleIds[0]);\n            },\n        });\n    }\n}\n", "import { user } from \"@web/core/user\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { usePopover } from \"@web/core/popover/popover_hook\";\nimport { Composer } from \"@mail/core/common/composer\";\nimport { KnowledgeThread } from \"../../mail/thread/knowledge_thread\";\nimport { scrollTo } from \"@web/core/utils/scrolling\";\nimport { isBrowserChrome } from \"@web/core/browser/feature_detection\";\nimport { KnowledgeCommentsPopover } from \"../comments_popover/comments_popover\";\nimport { KnowledgeCommentCreatorComposer } from \"../../mail/composer/composer\";\nimport { CommentAnchorText } from \"./comment_anchor_text\";\nimport { imageUrl } from \"@web/core/utils/urls\";\nimport {\n    Component,\n    onWillStart,\n    useSubEnv,\n    useRef,\n    useEffect,\n    useState,\n    onWillDestroy,\n} from \"@odoo/owl\";\nimport { effect } from \"@web/core/utils/reactive\";\nimport { batched } from \"@web/core/utils/timing\";\n\nconst DEFAULT_ANCHOR_TEXT_SIZE = 50;\nexport const MIN_THREAD_WIDTH = 300;\n\nexport class KnowledgeCommentsThread extends Component {\n    static components = {\n        CommentAnchorText,\n        Composer,\n        KnowledgeThread,\n        KnowledgeCommentCreatorComposer,\n    };\n    static props = {\n        threadId: { type: String },\n        horizontalDimensions: { type: Object, optional: true },\n        top: { type: Number, optional: true },\n        readonly: { type: Boolean, optional: true },\n    };\n    static template = \"knowledge.KnowledgeCommentsThread\";\n\n    setup() {\n        this.commentsService = useService(\"knowledge.comments\");\n        this.threadScrollableRef = useRef(\"threadScrollableRef\");\n        this.targetRef = useRef(\"targetRef\");\n        this.composerRef = useRef(\"composerRef\");\n        this.commentsState = useState(this.commentsService.getCommentsState());\n        let previousThreadId;\n        this.alive = true;\n        effect(\n            batched((state) => {\n                if (!this.alive) {\n                    return;\n                }\n                if (previousThreadId !== state.activeThreadId) {\n                    if (previousThreadId === this.props.threadId && this.editorThread) {\n                        this.editorThread.onActivate(new CustomEvent(\"knowledge.deactivateThread\"));\n                    }\n                    previousThreadId = state.activeThreadId;\n                }\n            }),\n            [this.commentsState]\n        );\n        onWillDestroy(() => {\n            this.alive = false;\n        });\n        this.state = useState({\n            hasFullAnchorText: false,\n        });\n        useSubEnv({\n            // We need to unset the chatter inside the env of the child Components\n            // because this Object contains values and methods that are linked to the form view's\n            // main chatter. By doing this we distinguish the main chatter from the comments.\n            inChatter: false,\n            chatter: false,\n            closeThread: this.updateResolveState.bind(this, true),\n            inKnowledge: true,\n            isResolved: this.isResolved.bind(this),\n            openThread: this.updateResolveState.bind(this, false),\n        });\n        this.popover = usePopover(KnowledgeCommentsPopover, {\n            closeOnClickAway: true,\n            onClose: () => {\n                this.onClosePopover();\n            },\n            env: this.env,\n            position: \"left-start\",\n            popoverClass: \"o_knowledge_comments_popover\",\n        });\n        const onActivate = (ev) => {\n            switch (ev.type) {\n                case \"click\":\n                    this.activateThread();\n                    break;\n                case \"knowledge.deactivateThread\":\n                    this.focusOut();\n                    break;\n            }\n        };\n        const onFocus = (ev) => {\n            switch (ev.type) {\n                case \"mouseenter\":\n                    this.focusIn();\n                    break;\n                case \"mouseleave\":\n                    this.focusOut();\n                    break;\n            }\n        };\n        useEffect(\n            () => {\n                const editorThread = this.editorThread;\n                if (editorThread) {\n                    editorThread.onActivateMap.set(\"main\", onActivate);\n                    editorThread.onFocusMap.set(\"main\", onFocus);\n                }\n                return () => {\n                    if (editorThread) {\n                        editorThread.onActivateMap.delete(onActivate);\n                        editorThread.onFocusMap.delete(onFocus);\n                    }\n                };\n            },\n            () => [this.editorThread]\n        );\n        let wasActive;\n        useEffect(\n            () => {\n                if (this.isActive && !wasActive && !this.smallUI) {\n                    const scrollable = this.threadScrollableRef.el;\n                    const composer = this.composerRef.el;\n                    if (scrollable && composer) {\n                        const composerRect = composer.getBoundingClientRect();\n                        scrollable.scrollBy({\n                            top: composerRect.height,\n                        });\n                    }\n                }\n                if (this.isActive !== wasActive) {\n                    wasActive = this.isActive;\n                }\n            },\n            () => [this.isActive, this.smallUI]\n        );\n        if (this.commentsState.displayMode === \"handler\") {\n            const setTargetHeight = (target) => {\n                const targetRect = target.getBoundingClientRect();\n                if (!(this.props.threadId in this.env.threadHeights)) {\n                    this.env.threadHeights[this.props.threadId] = {\n                        height: undefined,\n                    };\n                }\n                this.env.threadHeights[this.props.threadId].height = targetRect.height;\n            };\n            useEffect(\n                () => {\n                    if (this.targetRef.el) {\n                        const observer = new ResizeObserver((entries) => {\n                            for (const entry of entries) {\n                                if (entry.target) {\n                                    setTargetHeight(entry.target);\n                                }\n                            }\n                        });\n                        observer.observe(this.targetRef.el);\n                        setTargetHeight(this.targetRef.el);\n                        return () => observer.disconnect();\n                    }\n                },\n                () => [this.targetRef.el]\n            );\n            useEffect(\n                () => {\n                    if (\n                        this.editorThread &&\n                        (this.editorThread.threadId === \"undefined\" ||\n                            this.commentsState.shouldOpenActiveThread) &&\n                        this.targetRef.el &&\n                        this.isActive\n                    ) {\n                        this.commentsState.shouldOpenActiveThread = false;\n                        if (this.smallUI) {\n                            this.openPopover();\n                        }\n                    }\n                },\n                () => [this.targetRef.el, this.isActive, this.smallUI, this.editorThread]\n            );\n            useEffect(\n                () => {\n                    if (!this.smallUI && this.popover.isOpen) {\n                        this.popover.close();\n                    }\n                },\n                () => [this.smallUI]\n            );\n        }\n        if (this.props.threadId === \"undefined\") {\n            onWillStart(() => {\n                this.commentsService.loadThreads([this.props.threadId]);\n            });\n        } else {\n            useEffect(\n                () => {\n                    if (!(this.props.threadId in this.commentsState.threadRecords)) {\n                        this.commentsService.loadRecords(this.env.model.root.resId, {\n                            threadId: this.props.threadId,\n                        });\n                    }\n                },\n                () => [this.commentsState.articleId, this.commentsState.displayMode]\n            );\n            useEffect(\n                () => {\n                    if (\n                        this.props.threadId in this.commentsState.threadRecords &&\n                        !(this.props.threadId in this.commentsState.threads)\n                    ) {\n                        this.commentsService.loadThreads([this.props.threadId]);\n                        this.thread.knowledgePreLoading = true;\n                        this.thread.fetchNewMessages().then(() => {\n                            if (this.editorThread?.isProtected()) {\n                                return;\n                            }\n                            let isEmpty = true;\n                            for (const message of this.thread.messages) {\n                                if (message.message_type === \"comment\" && !message.isEmpty) {\n                                    isEmpty = false;\n                                    break;\n                                }\n                            }\n                            if (isEmpty) {\n                                this.commentsService.deleteThread(this.props.threadId);\n                            }\n                        });\n                        this.thread.knowledgePreLoading = false;\n                    }\n                },\n                () => [this.threadRecord]\n            );\n        }\n        onWillDestroy(() => {\n            if (this.isActive) {\n                this.commentsState.activeThreadId = undefined;\n            }\n            this.commentsState.focusedThreads.delete(this.props.threadId);\n        });\n    }\n\n    get hasLoaded() {\n        return (\n            this.props.threadId === \"undefined\" ||\n            (this.props.threadId in this.commentsState.threadRecords &&\n                this.props.threadId in this.commentsState.threads)\n        );\n    }\n\n    get hasAllDimensions() {\n        return (\n            this.props.top !== undefined &&\n            this.props.horizontalDimensions !== undefined &&\n            this.props.horizontalDimensions.left !== undefined &&\n            this.props.horizontalDimensions.width !== undefined\n        );\n    }\n\n    get style() {\n        if (this.commentsState.displayMode === \"panel\") {\n            return \"\";\n        }\n        return `\n            position: absolute;\n            top: ${this.props.top}px;\n            left: ${this.props.horizontalDimensions.left}px;\n            width: ${this.props.horizontalDimensions.width}px;\n            transition: top 0.3s, left 0.2s, filter 0.2s;\n            z-index: ${this.isActive ? 1 : \"auto\"};\n            filter: ${\n                !this.smallUI ||\n                this.hasFocus ||\n                (!this.commentsState.activeThreadId && !this.commentsState.focusedThreads.size)\n                    ? \"none\"\n                    : \"grayscale(50%) contrast(50%)\"\n            };\n        `;\n    }\n\n    /**@see EditorThreadInfo */\n    get editorThread() {\n        return this.commentsState.editorThreads[this.props.threadId];\n    }\n\n    get authorUrl() {\n        if (this.thread?.messages?.length) {\n            return this.thread.messages.at(-1).author.avatarUrl;\n        }\n        return imageUrl(\"res.users\", user.userId, \"avatar_128\");\n    }\n\n    get anchorText() {\n        let text = this.fullAnchorText;\n        const brIndex = text.indexOf(\"<br>\");\n        const excludeIndex = brIndex === -1 ? DEFAULT_ANCHOR_TEXT_SIZE : brIndex;\n        if (text.length > excludeIndex) {\n            text = text.substring(0, excludeIndex) + \"...\";\n        }\n        return text;\n    }\n\n    get fullAnchorText() {\n        let text;\n        if (!this.editorThread) {\n            text = this.threadRecord?.article_anchor_text || \"\";\n        } else {\n            text = this.editorThread.anchorText;\n        }\n        return text.replaceAll(\"\\n\", \"<br>\");\n    }\n\n    get hasFocus() {\n        return this.commentsState.hasFocus(this.props.threadId);\n    }\n\n    get isActive() {\n        return this.commentsState.activeThreadId === this.props.threadId;\n    }\n\n    get showReadMore() {\n        const anchorText = this.anchorText;\n        const fullAnchorText = this.fullAnchorText;\n        return fullAnchorText.length > 0 && anchorText.length !== fullAnchorText.length;\n    }\n\n    /**@see Thread */\n    get thread() {\n        return this.commentsState.threads[this.props.threadId];\n    }\n\n    get threadRecord() {\n        return this.commentsState.threadRecords[this.props.threadId];\n    }\n\n    get smallUI() {\n        return (\n            this.commentsState.displayMode === \"handler\" &&\n            this.props.horizontalDimensions.width < MIN_THREAD_WIDTH\n        );\n    }\n\n    activateThread() {\n        this.commentsState.activeThreadId = this.props.threadId;\n        if (this.smallUI) {\n            this.openPopover();\n        }\n    }\n\n    /**\n     * Used for the message actions\n     */\n    isResolved() {\n        if (this.props.threadId === \"undefined\") {\n            return false;\n        }\n        return this.threadRecord.is_resolved;\n    }\n\n    onClick(ev) {\n        if (this.editorThread) {\n            this.editorThread.onActivate(ev);\n        } else {\n            this.activateThread();\n        }\n    }\n\n    focusIn() {\n        this.commentsState.focusedThreads.add(this.props.threadId);\n    }\n\n    focusOut() {\n        this.commentsState.focusedThreads.delete(this.props.threadId);\n    }\n\n    onMouseEnter(ev) {\n        if (this.editorThread) {\n            this.editorThread.onFocus(ev);\n        } else {\n            this.focusIn();\n        }\n    }\n\n    onMouseLeave(ev) {\n        if (this.editorThread) {\n            this.editorThread.onFocus(ev);\n        } else {\n            this.focusOut();\n        }\n    }\n\n    openPopover() {\n        if (!this.popover.isOpen) {\n            const popoverProps = {\n                threadId: this.props.threadId,\n            };\n            if (this.targetRef.el) {\n                this.popover.open(this.targetRef.el, popoverProps);\n            }\n        }\n    }\n\n    onClosePopover() {\n        this.focusOut();\n    }\n\n    showEditorAnchor() {\n        if (!this.editorThread) {\n            return;\n        }\n        if (isBrowserChrome()) {\n            scrollTo(this.editorThread.beaconPair.start, {\n                behavior: \"smooth\",\n            });\n        } else {\n            this.editorThread.beaconPair.start.scrollIntoView({\n                behavior: \"smooth\",\n                block: \"center\",\n            });\n        }\n    }\n\n    async updateResolveState(value) {\n        const changed = await this.commentsService.updateResolveState(this.props.threadId, value);\n        if (changed && this.commentsState.displayMode === \"panel\") {\n            await this.thread.fetchNewMessages();\n        }\n    }\n\n    onCreateThreadCallback(thread) {\n        if (thread) {\n            this.commentsState.editorThreads[thread.id]?.select();\n        }\n    }\n}\n", "import { Component, onWillUpdateProps } from \"@odoo/owl\";\n\nexport class CommentAnchorText extends Component {\n    static template = \"knowledge.CommentAnchorText\";\n    static props = {\n        anchorText: { String },\n    };\n\n    setup() {\n        this.anchorTextArray = this.props.anchorText.split(\"<br>\");\n        onWillUpdateProps((newProps) => {\n            this.anchorTextArray = newProps.anchorText.split(\"<br>\");\n        });\n    }\n}\n", "import { leftPos, rightPos } from \"@html_editor/utils/position\";\nimport { EditorThreadInfo } from \"./editor_thread_info\";\nimport { throttleForAnimation } from \"@web/core/utils/timing\";\nimport { childNodes } from \"@html_editor/utils/dom_traversal\";\n\nfunction binarySearch(comparator, needle, array) {\n    let first = 0;\n    let last = array.length - 1;\n    while (first <= last) {\n        const mid = (first + last) >> 1;\n        const c = comparator(needle, array[mid]);\n        if (c > 0) {\n            first = mid + 1;\n        } else if (c < 0) {\n            last = mid - 1;\n        } else {\n            return mid;\n        }\n    }\n    return first;\n}\n\nexport function compareDOMPosition(a, b) {\n    const compare = a.compareDocumentPosition(b);\n    if (compare & 2) {\n        // b before a\n        return 1;\n    } else if (compare & 4) {\n        // a before b\n        return -1;\n    } else {\n        return 0;\n    }\n}\n\nexport function compareBeaconsThreadIds(a, b) {\n    const id = (beacon) => parseInt(beacon.dataset.id) || -1;\n    const bound = (beacon) => (beacon.dataset.oeType === \"threadBeaconStart\" ? 0 : 1);\n    const idA = id(a);\n    const boundA = bound(a);\n    const idB = id(b);\n    const boundB = bound(b);\n    if (idA > idB) {\n        return 1;\n    } else if (idB > idA) {\n        return -1;\n    } else if (boundA > boundB) {\n        return 1;\n    } else if (boundB > boundA) {\n        return -1;\n    } else {\n        return 0;\n    }\n}\n\nexport class CommentBeaconManager {\n    constructor({\n        commentsState,\n        document,\n        overlayContainer,\n        peerId,\n        readonly = false,\n        source,\n        onStep = () => {},\n        removeBeacon = () => {},\n        setSelection = () => {},\n    } = {}) {\n        this.document = document;\n        this.source = source;\n        this.overlayContainer = overlayContainer;\n        this.beacons = [];\n        this.searchBeaconIndex = binarySearch.bind(undefined, compareDOMPosition);\n        this.beaconsByThreadId = [];\n        this.searchBeaconIndexByThreadId = binarySearch.bind(undefined, compareBeaconsThreadIds);\n        this.bogusBeacons = new Set();\n        this.beaconPairs = {};\n        this.sortedThreadIds = [];\n        this.cleanups = {};\n        this.peerId = peerId;\n        this.readonly = readonly;\n        this.commentsState = commentsState;\n        this.onStep = onStep;\n        this.removeBeacon = removeBeacon;\n        this.setSelection = setSelection;\n        this.drawThreadOverlays = throttleForAnimation(this.drawThreadOverlays.bind(this));\n        this.pendingBeacons = new Set();\n    }\n\n    addBeacons(beacons) {\n        for (const beacon of beacons) {\n            const index = this.searchBeaconIndex(beacon, this.beacons);\n            this.beacons.splice(index, 0, beacon);\n        }\n    }\n\n    deleteBeacons(beacons) {\n        for (const beacon of beacons) {\n            const index = this.searchBeaconIndex(beacon, this.beacons);\n            this.beacons.splice(index, 1);\n        }\n    }\n\n    sortThreads() {\n        this.beacons = [...this.source.querySelectorAll(\".oe_thread_beacon\")];\n        this.beaconsByThreadId = this.beacons.toSorted(compareBeaconsThreadIds);\n        const starts = {};\n        const beaconPairs = {};\n        this.bogusBeacons = new Set();\n        // loop validates that start is before end for beaconPairs\n        for (const beacon of [...this.beacons]) {\n            if (beacon.dataset.oeType === \"threadBeaconStart\") {\n                // Don't consider other peers \"undefined\" beacons\n                if (beacon.dataset.id !== \"undefined\" || beacon.dataset.peerId === this.peerId) {\n                    starts[beacon.dataset.id] = {\n                        start: beacon,\n                    };\n                }\n                this.bogusBeacons.add(beacon);\n            } else if (beacon.dataset.id in starts) {\n                const beaconPair = starts[beacon.dataset.id];\n                delete starts[beacon.dataset.id];\n                beaconPair.end = beacon;\n                if (!this.validate(beaconPair)) {\n                    this.bogusBeacons.add(beaconPair.end);\n                } else {\n                    this.preserveExistingBeaconPair(beaconPair);\n                    this.removeDuplicate(beaconPair.start);\n                    this.removeDuplicate(beaconPair.end);\n                    this.bogusBeacons.delete(beaconPair.start);\n                    beaconPairs[beacon.dataset.id] = beaconPair;\n                }\n            } else {\n                this.bogusBeacons.add(beacon);\n            }\n        }\n        for (const beacon of [...this.bogusBeacons]) {\n            // preserve peer \"undefined\" beacons\n            if (beacon.dataset.id === \"undefined\" && beacon.dataset.peerId !== this.peerId) {\n                this.bogusBeacons.delete(beacon);\n            }\n        }\n        for (const beacon of this.pendingBeacons) {\n            // preserve pending beacons\n            this.bogusBeacons.delete(beacon);\n        }\n        const threadIds = new Set(Object.keys(beaconPairs));\n        for (const threadId of Object.keys(this.beaconPairs)) {\n            if (!threadIds.has(threadId)) {\n                this.cleanupThread(threadId);\n            }\n        }\n        const threadIdsToSort = [];\n        for (const threadId of threadIds) {\n            if (!(threadId in this.beaconPairs)) {\n                this.beaconPairs[threadId] = beaconPairs[threadId];\n            } else {\n                if (this.beaconPairs[threadId].start !== beaconPairs[threadId].start) {\n                    this.beaconPairs[threadId].start = beaconPairs[threadId].start;\n                }\n                if (this.beaconPairs[threadId].end !== beaconPairs[threadId].end) {\n                    this.beaconPairs[threadId].end = beaconPairs[threadId].end;\n                }\n            }\n            let editorThread =\n                this.commentsState.editorThreads[threadId] ||\n                this.commentsState.disabledEditorThreads[threadId];\n            if (!editorThread) {\n                editorThread = new EditorThreadInfo({\n                    beaconPair: this.beaconPairs[threadId],\n                    threadId,\n                    computeTextMap: this.computeTextMap.bind(this),\n                    removeBeaconPair: (beaconPair) => {\n                        this.cleanupThread(beaconPair.start.dataset.id);\n                        this.removeBeacon(beaconPair.start);\n                        this.removeBeacon(beaconPair.end);\n                        this.onStep();\n                    },\n                    setSelectionInBeaconPair: (beaconPair) => {\n                        if (this.validate(beaconPair)) {\n                            const [anchorNode, anchorOffset] = rightPos(beaconPair.start);\n                            this.setSelection({\n                                anchorNode,\n                                anchorOffset,\n                            });\n                        }\n                    },\n                    setBeaconPairId: (beaconPair, id) => {\n                        beaconPair.start.dataset.id = id;\n                        beaconPair.end.dataset.id = id;\n                        this.onStep();\n                    },\n                    enableBeaconPair: (beaconPair) => {\n                        if (this.isDisabled(beaconPair.start)) {\n                            this.enable(beaconPair.start);\n                        }\n                        if (this.isDisabled(beaconPair.end)) {\n                            this.enable(beaconPair.end);\n                        }\n                        const threadId = beaconPair.start.dataset.id;\n                        const editorThread = this.commentsState.disabledEditorThreads[threadId];\n                        if (editorThread) {\n                            this.commentsState.editorThreads[threadId] = editorThread;\n                            delete this.commentsState.disabledEditorThreads[threadId];\n                        }\n                        this.onStep();\n                    },\n                    disableBeaconPair: (beaconPair) => {\n                        if (!this.isDisabled(beaconPair.start)) {\n                            this.disable(beaconPair.start);\n                        }\n                        if (!this.isDisabled(beaconPair.end)) {\n                            this.disable(beaconPair.end);\n                        }\n                        const threadId = beaconPair.start.dataset.id;\n                        const editorThread = this.commentsState.editorThreads[threadId];\n                        if (editorThread) {\n                            this.commentsState.disabledEditorThreads[threadId] = editorThread;\n                            delete this.commentsState.editorThreads[threadId];\n                        }\n                        this.onStep();\n                    },\n                    isOwned: (beaconPair) => {\n                        if (!this.peerId) {\n                            return true;\n                        } else {\n                            const peerId = beaconPair.start.dataset.peerId;\n                            return !peerId || this.peerId === peerId;\n                        }\n                    },\n                });\n            } else {\n                editorThread.beaconPair = this.beaconPairs[threadId];\n            }\n            if (\n                this.isDisabled(this.beaconPairs[threadId].start) ||\n                this.isDisabled(this.beaconPairs[threadId].end)\n            ) {\n                if (!(threadId in this.commentsState.disabledEditorThreads)) {\n                    this.commentsState.disabledEditorThreads[threadId] = editorThread;\n                }\n                if (threadId in this.commentsState.editorThreads) {\n                    delete this.commentsState.editorThreads[threadId];\n                }\n                this.cleanupBeaconPair(threadId);\n            } else {\n                if (!(threadId in this.commentsState.editorThreads)) {\n                    this.commentsState.editorThreads[threadId] = editorThread;\n                }\n                if (threadId in this.commentsState.disabledEditorThreads) {\n                    delete this.commentsState.disabledEditorThreads[threadId];\n                }\n                threadIdsToSort.push(threadId);\n            }\n        }\n        this.sortedThreadIds = threadIdsToSort.sort((a, b) => {\n            return compareDOMPosition(this.beaconPairs[a].start, this.beaconPairs[b].start);\n        });\n    }\n\n    drawThreadOverlays() {\n        const overlayRect = this.overlayContainer.getBoundingClientRect();\n        for (const threadId of this.sortedThreadIds) {\n            this.cleanupBeaconPair(threadId);\n            const beaconPair = this.beaconPairs[threadId];\n            if (!beaconPair.start.isConnected || !beaconPair.end.isConnected) {\n                continue;\n            }\n            const range = new Range();\n            range.setStart(...rightPos(beaconPair.start));\n            range.setEnd(...leftPos(beaconPair.end));\n            const clientRects = Array.from(range.getClientRects());\n            if (!clientRects.length) {\n                continue;\n            }\n            this.commentsState.editorThreads[threadId].top = clientRects[0].y - overlayRect.y;\n            clientRects.reverse();\n            const identifyRect = (big, small) => {\n                if (big.width === 0 || big.height === 0) {\n                    return;\n                }\n                if (\n                    small &&\n                    Math.floor(big.x) <= small.x &&\n                    Math.floor(big.y) <= small.y &&\n                    Math.ceil(big.x + big.width) >= small.x + small.width &&\n                    Math.ceil(big.y + big.height) >= small.y + small.height\n                ) {\n                    return;\n                }\n                // Faster than elementsFromPoint, but some rects will be omitted\n                // if they are under another element like the editor toolbar.\n                const target = this.document.elementFromPoint(\n                    big.x + big.width / 2,\n                    big.y + big.height / 2\n                );\n                if (!target) {\n                    return;\n                }\n                let valid = true;\n                let closestEditable = target.closest(\"[data-embedded-editable]\");\n                if (!this.source.contains(closestEditable)) {\n                    closestEditable = undefined;\n                }\n                let embedded = target.closest(\"[data-embedded]\");\n                if (!this.source.contains(embedded)) {\n                    embedded = undefined;\n                }\n                if (embedded && (!closestEditable || !embedded.contains(closestEditable))) {\n                    valid = false;\n                }\n                if (!valid || (target.textContent === \"\" && target.nodeName !== \"IMG\")) {\n                    return;\n                }\n                return target;\n            };\n            let previousRect;\n            const indicators = [];\n            for (const rect of clientRects) {\n                const identity = identifyRect(rect, previousRect);\n                if (rect.width && rect.height) {\n                    previousRect = rect;\n                }\n                if (!identity || !this.source.contains(identity)) {\n                    continue;\n                }\n                let rectElement;\n                let onFocus;\n                let onActivate;\n                switch (identity.nodeName) {\n                    case \"IMG\":\n                        rectElement = this.createImgOverlay(\n                            rect,\n                            overlayRect,\n                            threadId,\n                            getComputedStyle(identity)\n                        );\n                        onFocus = () => {\n                            const style = getComputedStyle(identity);\n                            rectElement.style.setProperty(\"border-radius\", style.borderRadius);\n                            rectElement.style.setProperty(\n                                \"box-shadow\",\n                                `0 0 0 8px ${this.getThreadOverlayColor(\n                                    this.commentsState.hasFocus(threadId)\n                                )}`\n                            );\n                        };\n                        onActivate = onFocus;\n                        break;\n                    default:\n                        rectElement = this.createTextOverlay(rect, overlayRect, threadId);\n                        onFocus = () => {\n                            rectElement.style.setProperty(\n                                \"background-color\",\n                                this.getThreadOverlayColor(this.commentsState.hasFocus(threadId))\n                            );\n                        };\n                        onActivate = onFocus;\n                        break;\n                }\n                if (rectElement) {\n                    this.setupOverlayEvents({\n                        rectElement,\n                        threadId,\n                        onFocus,\n                        onActivate,\n                    });\n                    indicators.push(rectElement);\n                }\n            }\n            this.overlayContainer.append(...indicators);\n        }\n    }\n\n    createTextOverlay(rect, overlayRect, threadId) {\n        const { x, y, width, height } = rect;\n        const rectElement = this.document.createElement(\"div\");\n        rectElement.style = `\n            position: absolute;\n            top: ${y - overlayRect.y}px;\n            left: ${x - overlayRect.x}px;\n            width: ${width}px;\n            height: ${height}px;\n            pointer-events: ${this.readonly ? \"auto\" : \"none\"};\n            cursor: ${this.readonly ? \"grab\" : \"auto\"};\n            background-color: ${this.getThreadOverlayColor(this.commentsState.hasFocus(threadId))};\n            opacity: 0.5;\n        `;\n        rectElement.dataset.threadId = threadId;\n        return rectElement;\n    }\n\n    createImgOverlay(rect, overlayRect, threadId, style) {\n        const { x, y, width, height } = rect;\n        const rectElement = this.document.createElement(\"div\");\n        rectElement.style = `\n            position: absolute;\n            top: ${y - overlayRect.y}px;\n            left: ${x - overlayRect.x}px;\n            width: ${width}px;\n            height: ${height}px;\n            pointer-events: ${this.readonly ? \"auto\" : \"none\"};\n            cursor: ${this.readonly ? \"grab\" : \"auto\"};\n            box-shadow: 0 0 0 8px ${this.getThreadOverlayColor(\n                this.commentsState.hasFocus(threadId)\n            )};\n            border-radius: ${style.borderRadius};\n            opacity: 0.5;\n        `;\n        rectElement.dataset.threadId = threadId;\n        return rectElement;\n    }\n\n    setupOverlayEvents({ rectElement, threadId, onFocus, onActivate }) {\n        this.cleanups[threadId] ||= new Set();\n        const thread = this.commentsState.editorThreads[threadId];\n        if (onActivate) {\n            thread.onActivateMap.set(rectElement, onActivate);\n        }\n        if (onFocus) {\n            thread.onFocusMap.set(rectElement, onFocus);\n        }\n        const onReadonlyActivate = (ev) => {\n            thread.onActivate(ev);\n        };\n        const onReadonlyFocus = (ev) => {\n            thread.onFocus(ev);\n        };\n        if (this.readonly) {\n            rectElement.addEventListener(\"click\", onReadonlyActivate);\n            rectElement.addEventListener(\"mouseenter\", onReadonlyFocus);\n            rectElement.addEventListener(\"mouseleave\", onReadonlyFocus);\n        }\n        this.cleanups[threadId].add(() => {\n            if (this.readonly) {\n                rectElement.removeEventListener(\"click\", onReadonlyActivate);\n                rectElement.removeEventListener(\"mouseenter\", onReadonlyFocus);\n                rectElement.removeEventListener(\"mouseleave\", onReadonlyFocus);\n            }\n            thread.onActivateMap.delete(rectElement);\n            thread.onFocusMap.delete(rectElement);\n            rectElement.remove();\n        });\n    }\n\n    getThreadOverlayColor(focus) {\n        return `rgba(27, 161, 228, ${focus ? \"0.75\" : \"0.25\"})`;\n    }\n\n    computeTextMap(beaconPair) {\n        const range = new Range();\n        range.setStart(...rightPos(beaconPair.start));\n        range.setEnd(...leftPos(beaconPair.end));\n        const fragment = range.cloneContents();\n        const embeds = [...fragment.querySelectorAll(\"[data-embedded]\")].reverse();\n        for (const embed of embeds) {\n            embed.replaceWith(...embed.querySelectorAll(\"[data-embedded-editable]\"));\n        }\n        return childNodes(fragment).map((node) => {\n            if (node.nodeName === \"IMG\") {\n                return node.src;\n            }\n            return node.textContent.trim();\n        });\n    }\n\n    validate(beaconPair) {\n        // is in DOM\n        if (!beaconPair.start.isConnected || !beaconPair.end.isConnected) {\n            return false;\n        }\n        // start is before end\n        if (compareDOMPosition(beaconPair.start, beaconPair.end) !== -1) {\n            return false;\n        }\n        // is related to the correct article\n        if (\n            parseInt(beaconPair.start.dataset.res_id) !== this.commentsState.articleId ||\n            parseInt(beaconPair.end.dataset.res_id) !== this.commentsState.articleId\n        ) {\n            return false;\n        }\n        // is deleted\n        if (this.commentsState.deletedThreadIds.has(beaconPair.start.dataset.id)) {\n            return false;\n        }\n        if (\n            this.commentsState.activeThreadId !== \"undefined\" &&\n            beaconPair.start.dataset.id === \"undefined\" &&\n            (!this.pendingBeacons.has(beaconPair.start) || !this.pendingBeacons.has(beaconPair.end))\n        ) {\n            return false;\n        }\n        // contains no visible text\n        if (this.computeTextMap(beaconPair).join(\"\").trim() === \"\") {\n            return false;\n        }\n        return true;\n    }\n\n    removeDuplicate(beacon) {\n        // while element at position search compared\n        let index = this.searchBeaconIndexByThreadId(beacon, this.beaconsByThreadId);\n        let currentBeacon = this.beaconsByThreadId.at(index);\n        while (currentBeacon && !compareBeaconsThreadIds(beacon, currentBeacon)) {\n            this.beaconsByThreadId.splice(index, 1);\n            if (beacon !== currentBeacon) {\n                this.bogusBeacons.add(currentBeacon);\n                this.deleteBeacons([currentBeacon]);\n            }\n            index = this.searchBeaconIndexByThreadId(beacon, this.beaconsByThreadId);\n            currentBeacon = this.beaconsByThreadId.at(index);\n        }\n    }\n\n    preserveExistingBeaconPair(beaconPair) {\n        // issue: should remove duplicates even if there is no concurrentBeaconPair\n        // already exists and is valid elsewhere => keep existing\n        const concurrentBeaconPair = this.beaconPairs[beaconPair.start.dataset.id];\n        if (\n            concurrentBeaconPair &&\n            beaconPair !== concurrentBeaconPair &&\n            this.validate(concurrentBeaconPair) &&\n            compareDOMPosition(concurrentBeaconPair.start, concurrentBeaconPair.end) === -1\n        ) {\n            if (beaconPair.start !== concurrentBeaconPair.start) {\n                beaconPair.start = concurrentBeaconPair.start;\n            }\n            if (beaconPair.end !== concurrentBeaconPair.end) {\n                beaconPair.end = concurrentBeaconPair.end;\n            }\n        }\n    }\n\n    activateRelatedThread(target) {\n        this.sortThreads();\n        const index = this.searchBeaconIndex(target, this.beacons);\n        const ends = {};\n        let threadId;\n        for (let i = index - 1; i >= 0; i--) {\n            const beacon = this.beacons[i];\n            if (beacon.dataset.oeType === \"threadBeaconEnd\") {\n                ends[beacon.dataset.id] = {\n                    end: beacon,\n                };\n            } else if (\n                beacon.dataset.id in ends ||\n                !(beacon.dataset.id in this.beaconPairs) ||\n                !this.beaconPairs[beacon.dataset.id].end.isConnected\n            ) {\n                continue;\n            } else if (beacon.dataset.id in this.commentsState.editorThreads) {\n                threadId = beacon.dataset.id;\n                break;\n            }\n        }\n        this.commentsState.activeThreadId = threadId;\n        this.drawThreadOverlays();\n    }\n\n    isDisabled(beacon) {\n        return beacon.classList.contains(\"oe_disabled_thread_beacon\");\n    }\n\n    enable(beacon) {\n        beacon.classList.remove(\"oe_disabled_thread_beacon\");\n    }\n\n    disable(beacon) {\n        beacon.classList.add(\"oe_disabled_thread_beacon\");\n    }\n\n    removeBogusBeacons() {\n        for (const beacon of this.bogusBeacons) {\n            // TODO ABD: evaluate cleanupThread ?\n            this.cleanupBeaconPair(beacon.dataset.id);\n            this.removeBeacon(beacon);\n        }\n        this.bogusBeacons = new Set();\n    }\n\n    cleanupBeaconPair(threadId) {\n        for (const cleanup of this.cleanups[threadId] || []) {\n            cleanup();\n        }\n        delete this.cleanups[threadId];\n    }\n\n    destroy() {\n        for (const threadId of Object.keys(this.cleanups)) {\n            this.cleanupThread(threadId);\n        }\n        for (const threadId of Object.keys(this.commentsState.editorThreads)) {\n            delete this.commentsState.editorThreads[threadId];\n        }\n        for (const threadId of Object.keys(this.commentsState.disabledEditorThreads)) {\n            delete this.commentsState.disabledEditorThreads[threadId];\n        }\n    }\n\n    cleanupThread(threadId) {\n        this.cleanupBeaconPair(threadId);\n        delete this.beaconPairs[threadId];\n        delete this.commentsState.editorThreads[threadId];\n        delete this.commentsState.disabledEditorThreads[threadId];\n    }\n}\n", "import { MIN_THREAD_WIDTH, KnowledgeCommentsThread } from \"../comment/comment\";\nimport { useCallbackRecorder } from \"@web/search/action_hook\";\nimport { batched, Component, reactive, useEffect, useState, useSubEnv } from \"@odoo/owl\";\nimport { CommentBeaconManager } from \"../../comments/comment_beacon_manager\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { debounce } from \"@web/core/utils/timing\";\nimport { localization } from \"@web/core/l10n/localization\";\n\nconst MAX_THREAD_WIDTH = 400;\nconst SMALL_THREAD_WIDTH = 40; // o_knowledge_small_ui\n\nexport class KnowledgeCommentsHandler extends Component {\n    static template = \"knowledge.KnowledgeCommentsHandler\";\n    static components = { KnowledgeCommentsThread };\n    static props = {\n        commentBeaconManager: { type: CommentBeaconManager },\n        contentRef: { type: Object },\n    };\n\n    setup() {\n        this.commentsService = useService(\"knowledge.comments\");\n        this.commentsState = useState(this.commentsService.getCommentsState());\n        const debouncedThreadDimensions = debounce(() => {\n            this.computeThreadDimensions();\n        }, 300);\n        const batchedComputeVerticalDimensions = batched(this.computeVerticalDimensions.bind(this));\n        useSubEnv({\n            threadHeights: reactive({}, batchedComputeVerticalDimensions),\n        });\n        this.lastActiveThreadId;\n        this.state = useState({\n            threadDimensions: {\n                horizontal: {},\n                threadTops: {},\n            },\n        });\n        useCallbackRecorder(this.env.__onLayoutGeometryChange__, debouncedThreadDimensions);\n        let activeThreadId;\n        useEffect(\n            () => {\n                if (this.commentsState.activeThreadId !== activeThreadId) {\n                    activeThreadId = this.commentsState.activeThreadId;\n                    batchedComputeVerticalDimensions();\n                }\n            },\n            () => [this.commentsState.activeThreadId]\n        );\n        useEffect(\n            () => {\n                const editorThreads = Object.keys(this.commentsState.editorThreads);\n                if (\n                    editorThreads.some((threadId) => {\n                        return this.props.commentBeaconManager.sortedThreadIds.includes(threadId);\n                    })\n                ) {\n                    debouncedThreadDimensions();\n                }\n            },\n            () => [\n                this.commentsState.editorThreads,\n                Object.keys(this.commentsState.editorThreads).toString(),\n            ]\n        );\n    }\n\n    get mainPositionThreadId() {\n        if (\n            this.commentsState.activeThreadId &&\n            this.commentsState.activeThreadId !== this.lastActiveThreadId\n        ) {\n            this.lastActiveThreadId = this.commentsState.activeThreadId;\n        } else if (\n            !this.props.commentBeaconManager.sortedThreadIds.includes(this.lastActiveThreadId)\n        ) {\n            this.lastActiveThreadId = undefined;\n        }\n        return this.lastActiveThreadId || this.props.commentBeaconManager.sortedThreadIds.at(0);\n    }\n\n    computeHorizontalDimensions() {\n        if (!this.props.contentRef.el) {\n            return;\n        }\n        const rtl = localization.direction === \"rtl\";\n        const keys = {\n            paddingRight: \"paddingRight\",\n            left: \"left\",\n            right: \"right\",\n        };\n        if (rtl) {\n            Object.assign(keys, {\n                paddingRight: \"paddingLeft\",\n                left: \"right\",\n                right: \"left\",\n            });\n        }\n        const contentStyle = getComputedStyle(this.props.contentRef.el);\n        const paddingRight = parseInt(contentStyle[keys.paddingRight]) || 0;\n\n        // Manually calculate margin to circumvent zero-margin bug in chromium-based browsers\n        const contentRect = this.props.contentRef.el.getBoundingClientRect();\n        const parentRect = this.props.contentRef.el.parentElement.getBoundingClientRect();\n        const marginRight = Math.abs(parentRect[keys.right] - contentRect[keys.right]);\n        const marginLeft = Math.abs(parentRect[keys.left] - contentRect[keys.left]);\n\n        const availableWidth = Math.max(0, Math.floor(marginRight + paddingRight));\n        let width = Math.min(MAX_THREAD_WIDTH, Math.max(0, availableWidth - 20));\n        if (!width) {\n            return;\n        }\n        if (width < MIN_THREAD_WIDTH) {\n            width = SMALL_THREAD_WIDTH;\n        }\n        const left =\n            (rtl ? -1 : 1) *\n            Math.ceil(\n                marginLeft +\n                    contentRect.width -\n                    paddingRight +\n                    (availableWidth + (rtl ? 1 : -1) * width) / 2\n            );\n        this.state.threadDimensions.horizontal = { left, width };\n    }\n\n    computeVerticalDimensions() {\n        const activeId = this.mainPositionThreadId;\n        if (!activeId || this.commentsState.editorThreads[activeId]?.top === undefined) {\n            return;\n        }\n        const threadIds = this.props.commentBeaconManager.sortedThreadIds.filter(\n            (threadId) =>\n                threadId in this.commentsState.editorThreads &&\n                this.commentsState.editorThreads[threadId].top !== undefined\n        );\n        const index = threadIds.indexOf(activeId);\n        this.setThreadTop(activeId, this.commentsState.editorThreads[activeId].top);\n        let masterTop = this.getThreadTop(activeId);\n        for (let i = index - 1; i >= 0; i--) {\n            const threadId = threadIds[i];\n            const expectedTop = this.commentsState.editorThreads[threadId].top;\n            const height = this.env.threadHeights[threadId]?.height || 0;\n            if (expectedTop + height < masterTop) {\n                masterTop = expectedTop;\n            } else {\n                masterTop -= height;\n            }\n            this.setThreadTop(threadId, masterTop);\n        }\n        masterTop = this.getThreadTop(activeId) + (this.env.threadHeights[activeId]?.height || 0);\n        for (let i = index + 1; i < threadIds.length; i++) {\n            const threadId = threadIds[i];\n            const expectedTop = this.commentsState.editorThreads[threadId].top;\n            masterTop = Math.max(masterTop, expectedTop);\n            this.setThreadTop(threadId, masterTop);\n            masterTop += this.env.threadHeights[threadId]?.height || 0;\n        }\n    }\n\n    computeThreadDimensions() {\n        this.computeHorizontalDimensions();\n        this.computeVerticalDimensions();\n    }\n\n    setThreadTop(threadId, top) {\n        if (!(threadId in this.state.threadDimensions.threadTops)) {\n            this.state.threadDimensions.threadTops[threadId] = {\n                top: undefined,\n            };\n        }\n        this.state.threadDimensions.threadTops[threadId].top = top;\n    }\n\n    getThreadTop(threadId) {\n        return this.state.threadDimensions.threadTops[threadId]?.top;\n    }\n}\n", "import { registry } from \"@web/core/registry\";\nimport { standardWidgetProps } from \"@web/views/widgets/standard_widget_props\";\nimport { user } from \"@web/core/user\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { KnowledgeCommentsThread } from \"../comment/comment\";\nimport { Component, onWillDestroy, onWillStart, useEffect, useState } from \"@odoo/owl\";\nimport { batched, debounce } from \"@web/core/utils/timing\";\nimport { effect } from \"@web/core/utils/reactive\";\nimport { LOAD_THREADS_LIMIT } from \"../../comments/comments_service\";\n\nexport class KnowledgeCommentsPanel extends Component {\n    static template = \"knowledge.KnowledgeCommentsPanel\";\n    static components = { KnowledgeCommentsThread };\n    static props = { ...standardWidgetProps };\n\n    setup() {\n        this.commentsService = useService(\"knowledge.comments\");\n        this.commentsState = useState(this.commentsService.getCommentsState());\n        let threadRecordsKeys;\n        this.alive = true;\n        effect(\n            batched((state) => {\n                if (!this.alive || state.displayMode !== \"panel\") {\n                    return;\n                }\n                const threadRecords = state.threadRecords;\n                const threadIds = Object.keys(threadRecords);\n                const keys = threadIds.toString();\n                if (keys !== threadRecordsKeys) {\n                    this.computeThreadIds();\n                    threadRecordsKeys = keys;\n                }\n            }),\n            [this.commentsState]\n        );\n        onWillDestroy(() => {\n            this.alive = false;\n        });\n        this.state = useState({\n            mode: \"unresolved\", // \"resolved\" / \"all\"\n            loadMoreResolved: false,\n            loadMoreOpen: false,\n            loading: false,\n            threadIds: [],\n        });\n        let firstLoad = true;\n        useEffect(\n            () => {\n                if (this.commentsState.displayMode !== \"panel\") {\n                    return;\n                }\n                this.computeThreadIds();\n                this.commentsService\n                    .loadRecords(this.env.model.root.resId, {\n                        ignoreBatch: true,\n                        includeLoaded: true,\n                        domain: this.domain,\n                    })\n                    .then((count) => {\n                        if (firstLoad) {\n                            firstLoad = false;\n                            if (count !== undefined && count < LOAD_THREADS_LIMIT) {\n                                this.sealLoadMoreState();\n                            } else {\n                                this.state.loadMoreOpen = true;\n                                this.state.loadMoreResolved = true;\n                            }\n                        }\n                    });\n            },\n            () => [this.state.mode, this.commentsState.articleId, this.commentsState.displayMode]\n        );\n        onWillStart(async () => {\n            // TODO ABD: test this use case\n            if (\n                this.env.services.action.currentController?.action?.context?.show_resolved_threads\n            ) {\n                this.commentsState.displayMode = \"panel\";\n                this.mode = \"resolved\";\n                this.env.services.action.currentController.action.context.show_resolved_threads = false;\n            }\n            this.isPortalUser = await user.hasGroup(\"base.group_portal\");\n            this.isInternalUser = await user.hasGroup(\"base.group_user\");\n        });\n        const loadMore = debounce(this.loadMore.bind(this), 500);\n        this.loadMore = () => {\n            this.state.loading = true;\n            loadMore();\n        };\n    }\n\n    canDisplayRecord(threadId) {\n        if (!this.commentsState.threadRecords[threadId]) {\n            return true;\n        }\n        if (this.state.mode === \"unresolved\") {\n            return !this.commentsState.threadRecords[threadId].is_resolved;\n        } else if (this.state.mode === \"resolved\") {\n            return this.commentsState.threadRecords[threadId].is_resolved;\n        }\n        return true;\n    }\n\n    get couldLoadMore() {\n        if (this.state.mode === \"unresolved\") {\n            return this.state.loadMoreOpen;\n        } else if (this.state.mode === \"resolved\") {\n            return this.state.loadMoreResolved;\n        } else {\n            return this.state.loadMoreOpen || this.state.loadMoreResolved;\n        }\n    }\n\n    get domain() {\n        let domain = undefined;\n        if (this.state.mode === \"unresolved\") {\n            domain = [[\"is_resolved\", \"=\", false]];\n        } else if (this.state.mode === \"resolved\") {\n            domain = [[\"is_resolved\", \"=\", true]];\n        }\n        return domain;\n    }\n\n    computeThreadIds() {\n        const threadIds = [];\n        for (const [threadId, record] of Object.entries(this.commentsState.threadRecords)) {\n            if (\n                this.state.mode === \"all\" ||\n                (this.state.mode === \"resolved\" && record.is_resolved) ||\n                (this.state.mode === \"unresolved\" && !record.is_resolved)\n            ) {\n                threadIds.push(threadId);\n            }\n        }\n        this.state.threadIds = threadIds.sort((threadIdA, threadIdB) => {\n            const dateA = this.commentsState.threadRecords[threadIdA].write_date;\n            const dateB = this.commentsState.threadRecords[threadIdB].write_date;\n            if (dateA < dateB) {\n                return 1;\n            } else if (dateA > dateB) {\n                return -1;\n            } else {\n                return 0;\n            }\n        });\n    }\n\n    onChangeMode(ev) {\n        this.state.mode = ev.target.value;\n    }\n\n    async loadMore() {\n        const count = await this.commentsService.loadRecords(this.env.model.root.resId, {\n            ignoreBatch: true,\n            domain: this.domain,\n        });\n        if (count !== undefined && count < LOAD_THREADS_LIMIT) {\n            this.sealLoadMoreState();\n        }\n        this.state.loading = false;\n    }\n\n    sealLoadMoreState() {\n        if (this.state.mode === \"unresolved\") {\n            this.state.loadMoreOpen = false;\n        } else if (this.state.mode === \"resolved\") {\n            this.state.loadMoreResolved = false;\n        } else {\n            this.state.loadMoreResolved = false;\n            this.state.loadMoreOpen = false;\n        }\n    }\n}\n\nexport const knowledgeCommentsPanel = {\n    component: KnowledgeCommentsPanel,\n    additionalClasses: [\"col-12\", \"col-lg-4\", \"border-top\", \"d-print-none\"],\n};\n\nregistry.category(\"view_widgets\").add(\"knowledge_comments_panel\", knowledgeCommentsPanel);\n", "import { Composer } from \"@mail/core/common/composer\";\nimport { KnowledgeCommentCreatorComposer } from \"../../mail/composer/composer\";\nimport { KnowledgeThread } from \"@knowledge/mail/thread/knowledge_thread\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { Component, useRef, useState } from \"@odoo/owl\";\n\nexport class KnowledgeCommentsPopover extends Component {\n    static template = \"knowledge.KnowledgeCommentsPopover\";\n    static props = {\n        threadId: { type: String },\n        close: { type: Function },\n    };\n    static components = { Composer, KnowledgeThread, KnowledgeCommentCreatorComposer };\n\n    setup() {\n        this.commentsService = useService(\"knowledge.comments\");\n        this.commentsState = useState(this.commentsService.getCommentsState());\n        this.rootRef = useRef(\"rootRef\");\n    }\n\n    onPostCallback() {\n        this.props.close();\n    }\n\n    onCreateThreadCallback(thread) {\n        if (thread) {\n            this.commentsState.editorThreads[thread.id]?.select();\n        }\n    }\n\n    get thread() {\n        return this.commentsState.threads[this.props.threadId];\n    }\n}\n", "import { registry } from \"@web/core/registry\";\nimport { batched, reactive } from \"@odoo/owl\";\nimport { Deferred } from \"@web/core/utils/concurrency\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { browser } from \"@web/core/browser/browser\";\nimport { uuid } from \"@web/core/utils/strings\";\nimport { effect } from \"@web/core/utils/reactive\";\n\nconst ARTICLE_THREAD_FIELDS = [\n    \"id\",\n    \"article_id\",\n    \"article_anchor_text\",\n    \"is_resolved\",\n    \"write_date\",\n];\n\nexport const LOAD_THREADS_LIMIT = 30;\n\nfunction clearThreadComposer(composer) {\n    if (!composer) {\n        return;\n    }\n    composer.clear();\n    browser.localStorage.removeItem(composer.localId);\n}\n\nexport const knowledgeCommentsService = {\n    dependencies: [\"orm\", \"mail.store\"],\n    start(env, services) {\n        this.services = services;\n        this.commentsState = reactive({\n            articleId: undefined,\n            activeThreadId: undefined,\n            shouldOpenActiveThread: false,\n            /** @returns {boolean} */\n            get isDisplayed () {\n                return this.displayMode === \"panel\";\n            },\n            /** @returns {boolean} */\n            get hasComments () {\n                return Object.keys(this.threadRecords).length > 0;\n            },\n            // database records\n            threadRecords: {},\n            // mail.store instances\n            threads: {},\n            disabledEditorThreads: {},\n            // editor metadata\n            editorThreads: {},\n            displayMode: \"handler\", // 'handler' 'panel'\n            focusedThreads: new Set(),\n            deletedThreadIds: new Set(),\n            hasFocus(threadId) {\n                return this.focusedThreads.has(threadId) || this.activeThreadId === threadId;\n            },\n        });\n        let previousArticleId;\n        effect(\n            (state) => {\n                if (previousArticleId !== state.articleId) {\n                    this.resetForArticleId();\n                    previousArticleId = state.articleId;\n                }\n            },\n            [this.commentsState]\n        );\n        return {\n            createThread: this.createThread.bind(this),\n            createThreadAndPost: this.createThreadAndPost.bind(this),\n            createVirtualThread: this.createVirtualThread.bind(this),\n            deleteThread: this.deleteThread.bind(this),\n            fetchMessages: this.fetchMessages.bind(this),\n            getCommentsState: this.getCommentsState.bind(this),\n            loadRecords: this.loadRecords.bind(this),\n            loadThreads: this.loadThreads.bind(this),\n            setArticleId: this.setArticleId.bind(this),\n            updateResolveState: this.updateResolveState.bind(this),\n        };\n    },\n    async createThread() {\n        if (!(\"undefined\" in this.commentsState.editorThreads)) {\n            return;\n        }\n        const loadingId = this.loadingId;\n        const record = await rpc(\"/knowledge/thread/create\", {\n            article_id: this.commentsState.articleId,\n            article_anchor_text: this.commentsState.editorThreads[\"undefined\"].anchorText,\n            fields: ARTICLE_THREAD_FIELDS,\n        });\n        if (loadingId !== this.loadingId) {\n            return;\n        }\n        this.commentsState.threadRecords[record.id] = record;\n        this.commentsState.editorThreads[record.id] = this.commentsState.editorThreads[\"undefined\"];\n        delete this.commentsState.editorThreads[\"undefined\"];\n        this.commentsState.editorThreads[record.id].setThreadId(record.id);\n        const thread = this.services[\"mail.store\"].Thread.insert({\n            id: record.id,\n            model: \"knowledge.article.thread\",\n            articleId: this.commentsState.articleId,\n        });\n        this.commentsState.threads[record.id] = thread;\n        const { Composer } = this.commentsState.threads[\"undefined\"].composer.toData();\n        Composer[0].thread = thread;\n        thread.composer = Composer[0];\n        clearThreadComposer(this.commentsState.threads[\"undefined\"].composer);\n        return thread;\n    },\n    async createThreadAndPost(value, postData) {\n        const thread = await this.createThread();\n        if (!thread) {\n            return;\n        }\n        thread.post(value, postData);\n        clearThreadComposer(thread.composer);\n        return thread;\n    },\n    createVirtualThread() {\n        this.commentsState.threads[\"undefined\"] = this.services[\"mail.store\"].Thread.insert({\n            id: undefined,\n            model: \"knowledge.article.thread\",\n            articleId: this.commentsState.articleId,\n        });\n        clearThreadComposer(this.commentsState.threads[\"undefined\"].composer);\n        return this.commentsState.threads[\"undefined\"];\n    },\n    async deleteThread(resId) {\n        if (resId === \"undefined\") {\n            return;\n        }\n        this.batchedDeleteThread.threadIds.add(resId);\n        this.batchedDeleteThread();\n    },\n    // threadId is a number\n    async fetchMessages(threadId) {\n        const deferred = new Deferred();\n        this.batchedFetchMessages.deferredPromises[threadId] ||= [];\n        this.batchedFetchMessages.deferredPromises[threadId].push(deferred);\n        this.batchedFetchMessages.threadIds.add(threadId);\n        this.batchedFetchMessages();\n        return await deferred;\n    },\n    getCommentsState() {\n        return this.commentsState;\n    },\n    loadRecords(articleId, { domain, ignoreBatch, includeLoaded, limit, threadId } = {}) {\n        if (this.commentsState.articleId !== articleId) {\n            return;\n        }\n        if (ignoreBatch) {\n            return this._loadRecords({\n                domain,\n                limit,\n                includeLoaded,\n            });\n        }\n        if (threadId) {\n            if (threadId === \"undefined\") {\n                return;\n            }\n            this.batchedLoadRecords.threadIds.add(threadId);\n            this.batchedLoadRecords();\n        }\n    },\n    loadThreads(resIds) {\n        for (const resId of resIds) {\n            this.commentsState.threads[resId] = this.services[\"mail.store\"].Thread.insert({\n                id: resId === \"undefined\" ? undefined : resId,\n                model: \"knowledge.article.thread\",\n                articleId: this.commentsState.articleId,\n            });\n        }\n    },\n    makeBatchedDeleteThread() {\n        const deleteThread = async () => {\n            if (this.loadingId !== batch.loadingId) {\n                return;\n            }\n            const resIds = [];\n            for (const resId of batch.threadIds) {\n                resIds.push(parseInt(resId));\n            }\n            batch.threadIds = new Set();\n            try {\n                await this.services[\"orm\"].unlink(\"knowledge.article.thread\", resIds);\n            } catch {\n                // deleted threads may already be deleted, or the current user\n                // may not have the right to delete them.\n            }\n            if (this.loadingId !== batch.loadingId) {\n                return;\n            }\n            for (const resId of resIds) {\n                delete this.commentsState.threadRecords[resId];\n                delete this.commentsState.threads[resId];\n                this.commentsState.deletedThreadIds.add(resId.toString());\n                this.commentsState.editorThreads[resId]?.removeBeacons();\n            }\n        };\n        const batch = batched(deleteThread);\n        batch.threadIds = new Set();\n        batch.loadingId = this.loadingId;\n        return batch;\n    },\n    makeBatchedFetchMessages() {\n        const fetchMessages = async () => {\n            if (this.loadingId !== batch.loadingId) {\n                return;\n            }\n            const deferredPromises = batch.deferredPromises;\n            const thread_ids = Array.from(batch.threadIds);\n            batch.threadIds = new Set();\n            batch.deferredPromises = {};\n            let error;\n            let result;\n            try {\n                result = await rpc(\"/knowledge/threads/messages\", {\n                    thread_model: \"knowledge.article.thread\",\n                    thread_ids,\n                });\n            } catch (e) {\n                error = e;\n            }\n            // thread_id is a number, not a string (used for backend)\n            for (const thread_id in deferredPromises) {\n                for (const deferred of deferredPromises[thread_id]) {\n                    if (error) {\n                        deferred.reject(error);\n                    } else {\n                        deferred.resolve(result[thread_id]);\n                    }\n                }\n            }\n        };\n        const batch = batched(fetchMessages);\n        batch.deferredPromises = {};\n        batch.threadIds = new Set();\n        batch.loadingId = this.loadingId;\n        return batch;\n    },\n    makeBatchedLoadRecords() {\n        const loadRecords = async () => {\n            if (this.loadingId !== batch.loadingId) {\n                return;\n            }\n            const excludedSet = new Set(Object.keys(this.commentsState.threadRecords));\n            const targetedSet = batch.threadIds;\n            batch.threadIds = new Set();\n            for (const threadId of [...targetedSet]) {\n                if (excludedSet.has(threadId)) {\n                    targetedSet.delete(threadId);\n                }\n            }\n            let threadRecords = [];\n            if (targetedSet.size) {\n                const queryDomain = [\n                    [\"article_id\", \"=\", this.commentsState.articleId],\n                    [\"id\", \"in\", [...targetedSet]],\n                ];\n                threadRecords = await this.services[\"orm\"].searchRead(\n                    \"knowledge.article.thread\",\n                    queryDomain,\n                    ARTICLE_THREAD_FIELDS\n                );\n                if (this.loadingId !== batch.loadingId) {\n                    return;\n                }\n                for (const threadRecord of threadRecords) {\n                    const threadId = threadRecord.id.toString();\n                    this.commentsState.threadRecords[threadId] = threadRecord;\n                    if (threadRecord.is_resolved) {\n                        this.commentsState.editorThreads[threadId]?.disableBeacons();\n                    } else {\n                        this.commentsState.disabledEditorThreads[threadId]?.enableBeacons();\n                    }\n                }\n            }\n            if (targetedSet.size) {\n                // cleanup targetedThreadIds which do not exist anymore\n                for (const threadId of targetedSet) {\n                    if (\n                        !(threadId in this.commentsState.threadRecords) &&\n                        !this.commentsState.editorThreads[threadId]?.isProtected()\n                    ) {\n                        this.batchedDeleteThread.threadIds.add(threadId);\n                    }\n                }\n                if (this.batchedDeleteThread.threadIds.size) {\n                    this.batchedDeleteThread();\n                }\n            }\n        };\n        const batch = batched(loadRecords);\n        batch.loadingId = this.loadingId;\n        batch.threadIds = new Set();\n        return batch;\n    },\n    makeLoadRecords() {\n        const loadRecords = async ({ domain, limit, includeLoaded } = {}) => {\n            if (!limit) {\n                limit = LOAD_THREADS_LIMIT;\n            }\n            const options = {\n                limit,\n            };\n            const queryDomain = [[\"article_id\", \"=\", this.commentsState.articleId]];\n            if (domain) {\n                queryDomain.push(...domain);\n            }\n            if (!includeLoaded) {\n                const excludedThreadIds = Object.keys(this.commentsState.threadRecords);\n                queryDomain.push([\"id\", \"not in\", excludedThreadIds]);\n            }\n            const threadRecords = await this.services[\"orm\"].searchRead(\n                \"knowledge.article.thread\",\n                queryDomain,\n                ARTICLE_THREAD_FIELDS,\n                options\n            );\n            if (this.loadingId !== loadRecords.loadingId) {\n                return;\n            }\n            for (const threadRecord of threadRecords) {\n                const threadId = threadRecord.id.toString();\n                this.commentsState.threadRecords[threadId] = threadRecord;\n                if (threadRecord.is_resolved) {\n                    this.commentsState.editorThreads[threadId]?.disableBeacons();\n                } else {\n                    this.commentsState.disabledEditorThreads[threadId]?.enableBeacons();\n                }\n            }\n            return threadRecords.length;\n        };\n        loadRecords.loadingId = this.loadingId;\n        return loadRecords;\n    },\n    resetForArticleId() {\n        this.loadingId = uuid();\n        this.commentsState.activeThreadId = undefined;\n        this.commentsState.focusedThreads = new Set();\n        this.commentsState.shouldOpenActiveThread = false;\n        this.commentsState.threadRecords = {};\n        this.commentsState.threads = {};\n        this.commentsState.disabledEditorThreads = {};\n        this.commentsState.editorThreads = {};\n        this.batchedFetchMessages = this.makeBatchedFetchMessages();\n        this.batchedDeleteThread = this.makeBatchedDeleteThread();\n        this.batchedLoadRecords = this.makeBatchedLoadRecords();\n        this._loadRecords = this.makeLoadRecords();\n    },\n    setArticleId(articleId) {\n        if (articleId !== this.commentsState.articleId) {\n            this.commentsState.articleId = articleId;\n            this.resetForArticleId();\n        }\n    },\n    async updateResolveState(resId, resolvedState) {\n        const loadingId = this.loadingId;\n        try {\n            await this.services[\"orm\"].write(\"knowledge.article.thread\", [parseInt(resId)], {\n                is_resolved: resolvedState,\n            });\n        } catch {\n            return false;\n        }\n        if (this.loadingId !== loadingId) {\n            return false;\n        }\n        this.commentsState.threadRecords[resId].is_resolved = resolvedState;\n        if (resolvedState) {\n            this.commentsState.editorThreads[resId]?.disableBeacons();\n        } else {\n            this.commentsState.disabledEditorThreads[resId]?.enableBeacons();\n        }\n        return true;\n    },\n};\n\nregistry.category(\"services\").add(\"knowledge.comments\", knowledgeCommentsService);\n", "export class EditorThreadInfo {\n    constructor({\n        beaconPair,\n        threadId,\n        computeTextMap = () => [],\n        enableBeaconPair = () => {},\n        disableBeaconPair = () => {},\n        isOwned = () => {},\n        removeBeaconPair = () => {},\n        setSelectionInBeaconPair = () => {},\n        setBeaconPairId = () => {},\n    } = {}) {\n        this.beaconPair = beaconPair;\n        this.threadId = threadId;\n        this.computeTextMap = computeTextMap;\n        this.isOwned = isOwned;\n        this.removeBeaconPair = removeBeaconPair;\n        this.setSelectionInBeaconPair = setSelectionInBeaconPair;\n        this.setBeaconPairId = setBeaconPairId;\n        this.enableBeaconPair = enableBeaconPair;\n        this.disableBeaconPair = disableBeaconPair;\n        this.onActivateMap = new Map();\n        this.onFocusMap = new Map();\n        this.hover = false;\n        this.top = 0;\n        this.anchorText = this.computeCurrentAnchorText();\n    }\n\n    computeCurrentAnchorText() {\n        return this.computeTextMap(this.beaconPair).join(\"\\n\").trim();\n    }\n\n    setThreadId(id) {\n        this.threadId = id;\n        this.setBeaconPairId(this.beaconPair, id);\n    }\n\n    enableBeacons() {\n        this.enableBeaconPair(this.beaconPair);\n    }\n\n    disableBeacons() {\n        this.disableBeaconPair(this.beaconPair);\n    }\n\n    removeBeacons() {\n        this.removeBeaconPair(this.beaconPair);\n    }\n\n    select() {\n        this.setSelectionInBeaconPair(this.beaconPair);\n    }\n\n    handleEventMapEntries(ev, map) {\n        map.get(\"main\")?.(ev);\n        for (const [key, handler] of map.entries()) {\n            if (key.isConnected) {\n                handler(ev);\n            } else if (key !== \"main\") {\n                map.delete(key);\n            }\n        }\n    }\n\n    isProtected() {\n        return !this.isOwned(this.beaconPair);\n    }\n\n    onActivate(ev) {\n        this.handleEventMapEntries(ev, this.onActivateMap);\n    }\n\n    onFocus(ev) {\n        this.handleEventMapEntries(ev, this.onFocusMap);\n    }\n}\n", "import { AttachmentUploader } from \"@mail/core/common/attachment_uploader_hook\";\nimport { patch } from \"@web/core/utils/patch\";\n\npatch(AttachmentUploader.prototype, {\n    /**\n     * @override\n     */\n    async uploadFile() {\n        if (!this.thread.id && this.thread.model === \"knowledge.article.thread\") {\n            this.thread = await this.thread.store.env.services[\"knowledge.comments\"].createThread();\n            this.composer = this.thread.composer;\n            const commentsState =\n                this.thread.store.env.services[\"knowledge.comments\"].getCommentsState();\n            commentsState.shouldOpenActiveThread = true;\n            commentsState.activeThreadId = this.thread.id.toString();\n        }\n        return super.uploadFile(...arguments);\n    },\n});\n", "import { Composer } from \"@mail/core/common/composer\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { onWillDestroy } from \"@odoo/owl\";\n\n/**\n * This Component is an extension of the classic Composer used inside the chatter. It is called when a\n * user is just creating a new comment => when we set the id of the Thread to undefined.\n * This enables us to limit the creation of void comments inside the DB and lessen journal entries in\n * it.\n * After comment creation, this component is destroyed and is replaced with the regular Composer.\n */\nexport class KnowledgeCommentCreatorComposer extends Composer {\n    static props = [...Composer.props, \"onCreateThreadCallback?\"];\n\n    setup() {\n        super.setup();\n        this.commentsService = useService(\"knowledge.comments\");\n        this.newThread = undefined;\n        onWillDestroy(() => {\n            this.props.onCreateThreadCallback?.(this.newThread);\n        });\n    }\n\n    /**\n     * @override\n     */\n    async _sendMessage(value, postData) {\n        this.newThread = await this.commentsService.createThreadAndPost(value, postData);\n        this.clear();\n    }\n}\n", "import { ComposerAction } from \"@mail/core/common/composer_actions\";\nimport { patch } from \"@web/core/utils/patch\";\n\npatch(ComposerAction.prototype, {\n    _condition({ owner }) {\n        // Always disable the send-message action for knowledge comment composer.\n        // This is to avoid having two buttons for sending messages.\n        if (this.id === \"send-message\" && owner.env.inKnowledge) {\n            return false;\n        }\n        return super._condition(...arguments);\n    },\n});\n", "import { Composer } from \"@mail/core/common/composer\";\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { patch } from \"@web/core/utils/patch\";\n\n// TODO ABD: could be replaced by a KnowledgeComposer ?\n/** @type {Composer} */\nconst composerPatch = {\n    get hasGifPicker() {\n        // Done to remove the gif picker when in Knowledge as per the specs\n        return super.hasGifPicker && !this.env.inKnowledge;\n    },\n    /**\n     * Change the label on the button that posts comments on an article.\n     * @override\n     **/\n    get SEND_TEXT() {\n        return this.props.composer?.thread?.model === \"knowledge.article.thread\"\n            ? _t(\"Post\")\n            : super.SEND_TEXT;\n    },\n};\npatch(Composer.prototype, composerPatch);\n", "import { EmojiPicker } from \"@web/core/emoji_picker/emoji_picker\";\n\nimport { patch } from \"@web/core/utils/patch\";\n\nEmojiPicker.props.push(\"hasRemoveFeature?\");\n\npatch(EmojiPicker.prototype, {\n    removeEmoji() {\n        this.props.onSelect(false);\n        this.gridRef.el.scrollTop = 0;\n        this.props.close?.();\n        this.props.onClose?.();\n    },\n});\n", "import { Message } from \"@mail/core/common/message\";\nimport { user } from \"@web/core/user\";\nimport { onWillStart } from \"@odoo/owl\";\n\nexport class KnowledgeMessage extends Message {\n    setup() {\n        super.setup(...arguments);\n        onWillStart(async () => {\n            this.isPortalUser = await user.hasGroup(\"base.group_portal\");\n            this.isInternalUser = await user.hasGroup(\"base.group_user\");\n        });\n    }\n\n    get quickActionCount() {\n        return 3;\n    }\n\n    get canToggleStar() {\n        return super.canToggleStar && this.isInternalUser;\n    }\n    get showUnfollow() {\n        return super.showUnfollow && this.isInternalUser;\n    }\n}\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { registerMessageAction } from \"@mail/core/common/message_actions\";\n\nregisterMessageAction(\"closeThread\", {\n    condition: ({ owner }) => owner.env.closeThread && !owner.env.isResolved(),\n    icon: \"fa fa-check\",\n    name: _t(\"Mark the discussion as resolved\"),\n    onSelected: ({ owner }) => owner.env.closeThread(),\n    sequence: 0,\n});\n\nregisterMessageAction(\"openThread\", {\n    condition: ({ owner }) => owner.env.openThread && owner.env.isResolved(),\n    icon: \"fa fa-retweet\",\n    name: _t(\"Re-open the discussion\"),\n    onSelected: ({ owner }) => owner.env.openThread(),\n    sequence: 0,\n});\n", "import { Message } from \"@mail/core/common/message_model\";\nimport { patch } from \"@web/core/utils/patch\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { browser } from \"@web/core/browser/browser\";\nimport { url } from \"@web/core/utils/urls\";\n\n/** @type {import(\"models\").Message} */\nconst messagePatch = {\n    async copyLink() {\n        if (this.thread?.model === \"knowledge.article.thread\") {\n            let notification = _t(\"Message Link Copied!\");\n            let type = \"info\";\n            try {\n                await browser.navigator.clipboard.writeText(\n                    url(`/knowledge/article/${this.thread.articleId}`)\n                );\n            } catch {\n                notification = _t(\"Message Link Copy Failed (Permission denied?)!\");\n                type = \"danger\";\n            }\n            this.store.env.services.notification.add(notification, { type });\n        } else {\n            return super.copyLink(...arguments);\n        }\n    },\n    canForward(thread) {\n        return thread?.model != \"knowledge.article.thread\" && super.canForward(thread);\n    },\n};\npatch(Message.prototype, messagePatch);\n", "import { Message } from \"@mail/core/common/message\";\nimport { patch } from \"@web/core/utils/patch\";\n\npatch(Message.prototype, {\n    /**\n     * This function overrides the original method so that when the user tries to open a the record\n     * from a starred discussion linked to a knowledge thread, it can be redirected to the corresponding\n     * article. This is needed to avoid the user being redirected to the technical view of the\n     * knowledge.article.thread model.\n     * @override\n     */\n    async openRecord() {\n        if (this.message.model === \"knowledge.article.thread\") {\n            const [articleThread] = await this.env.services.orm.searchRead(\n                \"knowledge.article.thread\",\n                [[\"id\", \"=\", this.message.thread.id]],\n                [\"article_id\"]\n            );\n            this.action.doAction({\n                type: \"ir.actions.act_window\",\n                res_id: articleThread.article_id[0],\n                res_model: \"knowledge.article\",\n                views: [[false, \"form\"]],\n            });\n            return;\n        }\n        super.openRecord();\n    },\n});\n", "import { KnowledgeMessage } from \"@knowledge/mail/message/knowledge_message\";\nimport { Thread } from \"@mail/core/common/thread\";\nimport { onMounted } from \"@odoo/owl\";\n\nexport class KnowledgeThread extends Thread {\n    static components = { ...Thread.components, Message: KnowledgeMessage };\n\n    setup() {\n        super.setup();\n        this.props.thread.knowledgePreLoading = true;\n        onMounted(() => {\n            this.props.thread.knowledgePreLoading = false;\n        });\n    }\n}\n", "import { Thread } from \"@mail/core/common/thread_model\";\nimport { patch } from \"@web/core/utils/patch\";\n\npatch(Thread.prototype, {\n    /** @type {boolean|undefined} */\n    knowledgePreLoading: undefined,\n    /** @type {number|undefined} */\n    articleId: undefined,\n\n    /**\n     * @override\n     */\n    async fetchMessagesData({ after, around, before }) {\n        if (!this.knowledgePreLoading) {\n            return await super.fetchMessagesData(...arguments);\n        } else {\n            return await this.store.env.services[\"knowledge.comments\"].fetchMessages(this.id);\n        }\n    },\n    /** @override */\n    open() {\n        if (this.model !== \"knowledge.article.thread\") {\n            return super.open(...arguments);\n        }\n        this.store.env.services.orm\n            .read(\"knowledge.article.thread\", [this.id], [\"article_id\"], { load: false })\n            .then(([articleThreadData]) => {\n                this.store.env.services.action.doAction(\n                    \"knowledge.ir_actions_server_knowledge_home_page\",\n                    {\n                        stackPosition: \"replaceCurrentAction\",\n                        additionalContext: {\n                            res_id: articleThreadData[\"article_id\"],\n                        },\n                    }\n                );\n            });\n        return true;\n    },\n});\n", "import { SearchModel } from \"@web/search/search_model\";\n\nexport const KnowledgeSearchModelMixin = (T) => class extends T {\n    setup(services, args) {\n        this.saveEmbeddedViewFavoriteFilter = args.saveEmbeddedViewFavoriteFilter;\n        this.deleteEmbeddedViewFavoriteFilter = args.deleteEmbeddedViewFavoriteFilter;\n        super.setup(services, args);\n    }\n\n    /**\n     * Favorites for embedded views\n     * @override\n     */\n    async load(config) {\n        await super.load(config);\n        if (config.state && !this.isStateCompleteForEmbeddedView) {\n            // If the config contains an imported state that is not directly\n            // coming from a view that was embedded in Knowledge, the favorite\n            // filters have to be loaded, since they come from the\n            // `data-embedded-props` attribute of the anchor for the\n            // EmbeddedViewComponent. Otherwise, those are already specified in\n            // the state and they should not be duplicated.\n            let defaultFavoriteId = null;\n            const activateFavorite = \"activateFavorite\" in config ? config.activateFavorite : true;\n            if (activateFavorite) {\n                defaultFavoriteId = this._createGroupOfFavorites(this.irFilters || []);\n                if (defaultFavoriteId) {\n                    // activate default search items (populate this.query)\n                    this._activateDefaultSearchItems(defaultFavoriteId);\n                }\n            }\n        }\n    }\n\n    /**\n     * Save in embedded view arch instead of creating a record\n     * @override\n     */\n    async _createIrFilters(irFilter) {\n        this.saveEmbeddedViewFavoriteFilter(irFilter);\n        return null;\n    }\n\n    /**\n     * The super method handles real ir.filters records from the database. In an\n     * Embedded View, favorites are only stored as html metadata, they do not\n     * relate to a database record, so there is nothing to reconciliate.\n     * @override\n     */\n    _reconciliateFavorites() {}\n\n    deleteFavorite(favoriteId) {\n        const searchItem = this.searchItems[favoriteId];\n        if (searchItem.type !== \"favorite\") {\n            return;\n        }\n        this._deleteIrFilters(searchItem);\n        const index = this.query.findIndex((queryElem) => queryElem.searchItemId === favoriteId);\n        delete this.searchItems[favoriteId];\n        if (index >= 0) {\n            this.query.splice(index, 1);\n        }\n        this._notify();\n    }\n\n    /**\n     * Delete from the embedded view embedded state\n     */\n    _deleteIrFilters(searchItem) {\n        this.deleteEmbeddedViewFavoriteFilter(searchItem);\n    }\n\n    /**\n     * @override\n     * @returns {Object}\n     */\n    exportState() {\n        const state = super.exportState();\n        state.isStateCompleteForEmbeddedView = true;\n        return state;\n    }\n\n    /**\n     * @override\n     */\n    _importState(state) {\n        super._importState(state);\n        this.isStateCompleteForEmbeddedView = state.isStateCompleteForEmbeddedView;\n    }\n};\n\nexport class KnowledgeSearchModel extends KnowledgeSearchModelMixin(SearchModel) {}\n", "import { useService } from \"@web/core/utils/hooks\";\nimport { Deferred, KeepLast } from \"@web/core/utils/concurrency\";\nimport { patch } from \"@web/core/utils/patch\";\nimport { Chatter } from \"@mail/chatter/web_portal/chatter\";\nimport { useCallbackRecorder } from \"@web/search/action_hook\";\nimport {\n    onWillUnmount,\n    useEffect,\n} from \"@odoo/owl\";\n\n/**\n * Knowledge articles can interact with some records with the help of the\n * @see KnowledgeCommandsService .\n * If any record in a form view has a chatter with the ability to send message\n * and/or attach files, they are a potential target for Knowledge macros.\n */\nconst ChatterPatch = {\n    setup() {\n        super.setup(...arguments);\n        if (this.env.__knowledgeUpdateCommandsRecordInfo__) {\n            this.knowledgeCommandsService = useService(\"knowledgeCommandsService\");\n            // Only keep the last request to register a recordInfo active.\n            const keepLastRecordInfoRequest = new KeepLast();\n            // Keep track of the fact that the chatter thread is ready and has\n            // loaded its access rights.\n            let chatterThreadReady = new Deferred();\n            let previousThreadId = this.props.threadId;\n            useEffect(\n                // Access rights of the current thread of the chatter are\n                // populated asynchronously after the chatter is mounted, this\n                // method keeps track of those changes in order to determine\n                // when a recordInfo request can be evaluated.\n                (threadId, canPostOnReadonly, hasReadAccess, hasWriteAccess) => {\n                    if (previousThreadId !== threadId) {\n                        // If the chatter changes threadId, resolve the current\n                        // promise keeping track of the thread state to false,\n                        // so that an ongoing request to evaluate a recordInfo\n                        // (related to the previous thread) will be discarded.\n                        chatterThreadReady.resolve(false);\n                        chatterThreadReady = new Deferred();\n                    }\n                    if (\n                        canPostOnReadonly !== undefined &&\n                        hasReadAccess !== undefined &&\n                        hasWriteAccess !== undefined\n                    ) {\n                        // When the access rights are all loaded, the thread\n                        // is ready and a recordInfo request can be evaluated.\n                        chatterThreadReady.resolve(true);\n                    }\n                    previousThreadId = threadId;\n                },\n                () => [\n                    this.props.threadId,\n                    this.state.thread?.canPostOnReadonly,\n                    this.state.thread?.hasReadAccess,\n                    this.state.thread?.hasWriteAccess,\n                ]\n            );\n            onWillUnmount(() => {\n                // If there is an ongoing request to evaluate a recordInfo,\n                // discard it.\n                chatterThreadReady.resolve(false);\n            });\n            useCallbackRecorder(\n                this.env.__knowledgeUpdateCommandsRecordInfo__,\n                // Callback used to record the values related to the ability to\n                // post messages or attach files on the current record.\n                async (recordInfo) => {\n                    // At each new recording request, all previous ongoing\n                    // requests are discarded through the keepLast.\n                    const chatterThreadReadyForRecordInfo = keepLastRecordInfoRequest.add(chatterThreadReady);\n                    if (!await chatterThreadReadyForRecordInfo) {\n                        // If the chatterThreadReadyForRecordInfo promise\n                        // resolves to false, the recording request should be\n                        // discarded.\n                        return;\n                    }\n                    if (\n                        !this.env.model.root?.resId ||\n                        recordInfo.resId !== this.env.model.root.resId ||\n                        recordInfo.resModel !== this.env.model.root.resModel\n                    ) {\n                        // Ensure that the current record matches the recordInfo\n                        // candidate.\n                        return;\n                    }\n                    // The conditions for the ability to post or attach should\n                    // be the same as the ones in the Chatter template.\n                    Object.assign(recordInfo, {\n                        canPostMessages:\n                            this.props.threadId &&\n                            (this.state.thread?.hasWriteAccess ||\n                                (this.state.thread?.hasReadAccess &&\n                                    this.state.thread?.canPostOnReadonly)),\n                        canAttachFiles: this.props.threadId && this.state.thread?.hasWriteAccess,\n                    });\n                    if (this.knowledgeCommandsService.isRecordCompatibleWithMacro(recordInfo)) {\n                        this.knowledgeCommandsService.setCommandsRecordInfo(recordInfo);\n                    }\n                }\n            );\n        }\n    }\n};\n\npatch(Chatter.prototype, ChatterPatch);\n", "import { AlertDialog } from \"@web/core/confirmation_dialog/confirmation_dialog\";\nimport { getActiveHotkey } from \"@web/core/hotkeys/hotkey_service\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { FormController } from '@web/views/form/form_controller';\nimport { KnowledgeCoverDialog } from '@knowledge/components/knowledge_cover/knowledge_cover_dialog';\nimport { KnowledgeSidebar } from '@knowledge/components/sidebar/sidebar';\nimport { useBus, useService } from \"@web/core/utils/hooks\";\n\nimport {\n    onWillStart,\n    reactive,\n    useSubEnv,\n    useEffect,\n    useExternalListener,\n    useRef,\n} from \"@odoo/owl\";\nimport { ArticleAnnexePickerDialog } from \"@knowledge/components/article_annexe_picker_dialog/article_annexe_picker_dialog\";\n\nexport class KnowledgeArticleFormController extends FormController {\n    static template = \"knowledge.ArticleFormView\";\n    static components = {\n        ...FormController.components,\n        KnowledgeSidebar,\n    };\n\n    setup() {\n        super.setup();\n        this.root = useRef('root');\n        this.orm = useService('orm');\n        this.actionService = useService('action');\n        this.dialogService = useService(\"dialog\");\n        this.commentsService = useService(\"knowledge.comments\");\n\n        useSubEnv({\n            createArticle: this.createArticle.bind(this),\n            ensureArticleName: this.ensureArticleName.bind(this),\n            openArticle: this.openArticle.bind(this),\n            openCoverSelector: this.openCoverSelector.bind(this),\n            renameArticle: this.renameArticle.bind(this),\n            sendArticleToTrash: this.sendArticleToTrash.bind(this),\n            toggleAsideMobile: this.toggleAsideMobile.bind(this),\n            toggleChatter: this.toggleChatter.bind(this),\n            toggleComments: this.toggleComments.bind(this),\n            toggleFavorite: this.toggleFavorite.bind(this),\n            toggleProperties: this.toggleProperties.bind(this),\n            save: this.save.bind(this),\n            discard: this.discard.bind(this),\n            // Internal states:\n            propertiesPanelState: reactive({ isDisplayed: false }),\n            chatterPanelState: reactive({ isDisplayed: false }),\n            commentsState: this.commentsService.getCommentsState(),\n        });\n\n        useBus(this.env.bus, 'KNOWLEDGE:OPEN_ARTICLE', (event) => {\n            this.openArticle(event.detail.id);\n        });\n\n        useBus(this.env.bus, \"KNOWLEDGE:OPEN_ANNEXE_TEMPLATE_PICKER\", async (event) => {\n            const props = event.detail;\n            this.dialogService.add(ArticleAnnexePickerDialog, props);\n        });\n\n        // Unregister the current candidate recordInfo for Knowledge macros in\n        // case of breadcrumbs mismatch.\n        onWillStart(() => {\n            if (\n                !this.env.inDialog &&\n                this.env.config.breadcrumbs &&\n                this.env.config.breadcrumbs.length\n            ) {\n                // Unregister the current candidate recordInfo in case of\n                // breadcrumbs mismatch.\n                this.knowledgeCommandsService.unregisterCommandsRecordInfo(this.env.config.breadcrumbs);\n            }\n        });\n\n        useExternalListener(document.documentElement, 'mouseleave', async () => {\n            if (await this.model.root.isDirty()) {\n                await this.model.root.save();\n            }\n        });\n\n        useEffect(\n            () => {\n                const scrollView = this.root.el?.querySelector(\".o_scroll_view_lg\");\n                if (scrollView) {\n                    scrollView.scrollTop = 0;\n                }\n                const mobileScrollView = this.root.el?.querySelector(\".o_knowledge_main_view\");\n                if (mobileScrollView) {\n                    mobileScrollView.scrollTop = 0;\n                }\n            },\n            () => [this.model.root.resId]\n        );\n\n        useExternalListener(window, \"keydown\", async event => {\n            const hotkey = getActiveHotkey(event);\n            if (hotkey === \"control+s\") {\n                event.preventDefault();\n                if (this.model.root.dirty) {\n                    await this.save({ reload: false });\n                }\n            }\n        });\n    }\n\n    /**\n     * Ensure that the title is set @see beforeUnload\n     * Dirty check is sometimes necessary in cases where the user leaves\n     * the article from inside an article (i.e. embedded views/links) very\n     * shortly after a mutation (i.e. in tours). At that point, the\n     * html_field may not have notified the model from the change.\n     * @override\n     */\n    async beforeLeave() {\n        if (this.model.root.resId) {\n            await this.ensureArticleName();\n        }\n        await this.model.root.isDirty();\n        return super.beforeLeave();\n    }\n\n    /**\n     * Check that the title is set or not before closing the tab and\n     * save the whole article, if the current article exists (it does\n     * not exist if there are no articles to show, in which case the no\n     * content helper is displayed).\n     * @override\n     */\n    async beforeUnload(ev) {\n        if (this.model.root.resId) {\n            await this.ensureArticleName();\n            if (await this.model.root.isDirty()) {\n                await super.beforeUnload(ev); // triggers an urgent save\n            }\n        }\n    }\n\n    /**\n     * If the article has no name set, tries to rename it.\n     */\n    ensureArticleName() {\n        const recordData = this.model.root.data;\n        if (\n            !recordData.name &&\n            !(recordData.is_locked || !recordData.user_can_write || !recordData.active)\n        ) {\n            return this.renameArticle();\n        }\n    }\n\n    get resId() {\n        return this.model.root.resId;\n    }\n\n    /**\n     * Create a new article and open it.\n     * @param {String} category - Category of the new article\n     * @param {integer} targetParentId - Id of the parent of the new article (optional)\n     */\n    async createArticle(category, targetParentId) {\n        const articleIds = await this.orm.call(\n            \"knowledge.article\",\n            \"article_create\",\n            [],\n            {\n                is_private: category === 'private',\n                parent_id: targetParentId ? targetParentId : false\n            }\n        );\n        this.openArticle(articleIds[0]);\n    }\n\n    getHtmlTitle() {\n        const titleEl = this.root.el.querySelector(\".note-editable.odoo-editor-editable h1\");\n        if (titleEl) {\n            const title = titleEl.textContent.trim();\n            if (title) {\n                return title;\n            }\n        }\n    }\n\n    displayName() {\n        return this.model.root.data.name || _t(\"New\");\n    }\n\n    /**\n     * Callback executed before the record save (if the record is valid).\n     * When an article has no name set, use the title (first h1 in the\n     * body) to try to save the article with a name.\n     * @overwrite\n     */\n    async onWillSaveRecord(record, changes) {\n        if (!record.data.name) {\n            const title = this.getHtmlTitle();\n            if (title) {\n                changes.name = title;\n            }\n         }\n    }\n\n    /**\n     * @param {integer} - resId: id of the article to open\n     */\n    async openArticle(resId) {\n        if (!resId || resId === this.resId) {\n            return;\n        }\n\n        // blur to remove focus on the active element\n        document.activeElement.blur();\n\n        // load the new record\n        try {\n            if (this.model.root.isNew) {\n                await this.model.load({ resId });\n            } else {\n                await this.ensureArticleName();\n                if (await this.model.root.isDirty()) {\n                    await this.model.root.save({\n                        onError: (error, options) => this.onSaveError(error, options, true),\n                        nextId: resId,\n                    });\n                } else {\n                    await this.model.load({ resId });\n                }\n            }\n        } catch {\n            this.dialogService.add(AlertDialog, {\n                title: _t(\"Access Denied\"),\n                body: _t(\n                    \"The article you are trying to open has either been removed or is inaccessible.\",\n                ),\n                confirmLabel: _t(\"Close\"),\n            });\n            return false;\n        }\n        this.toggleAsideMobile(false);\n        return true;\n    }\n\n    openCoverSelector() {\n        this.dialogService.add(KnowledgeCoverDialog, {\n            articleCoverId: this.model.root.data.cover_image_id.id,\n            articleName: this.model.root.data.name || \"\",\n            save: (id) => this.model.root.update({\n                cover_image_id: { id }\n            })\n        });\n    }\n\n    /**\n     * Rename the article using the given name, or using the article title if\n     * no name is given (first h1 in the body). If no title is found, the\n     * article is kept untitled.\n     * @param {string} name - new name of the article\n     */\n    renameArticle(name) {\n        if (!name) {\n            const title = this.getHtmlTitle();\n            if (!title) {\n                return;\n            }\n            name = title;\n        }\n        return this.model.root.update({ name });\n    }\n\n    async sendArticleToTrash() {\n        await this.orm.call(\"knowledge.article\", \"action_send_to_trash\", [this.resId]);\n        await this.actionService.doAction(\n            await this.orm.call(\"knowledge.article\", \"action_redirect_to_parent\", [this.resId]),\n            { stackPosition: \"replaceCurrentAction\" },\n        );\n    }\n\n    /**\n     * Toggle the aside menu on mobile devices (< 576px).\n     * @param {boolean} force\n     */\n    toggleAsideMobile(force) {\n        const container = this.root.el.querySelector('.o_knowledge_form_view');\n        container.classList.toggle('o_toggle_aside', force);\n    }\n\n    toggleChatter() {\n        this.env.chatterPanelState.isDisplayed = !this.env.chatterPanelState.isDisplayed;\n    }\n\n    toggleComments() {\n        if (this.env.commentsState.displayMode === \"handler\") {\n            this.env.commentsState.displayMode = \"panel\";\n        } else {\n            this.env.commentsState.displayMode = \"handler\";\n        }\n    }\n\n    /**\n     * Add/Remove article from favorites and reload the favorite tree.\n     * One does not use \"record.update\" since the article could be in readonly.\n     * @param {event} Event\n     */\n    async toggleFavorite(event) {\n        // Save in case name has been edited, so that this new name is used\n        // when adding the article in the favorite section.\n        if (await this.model.root.isDirty()) {\n            await this.model.root.save();\n        }\n        await this.orm.call(this.model.root.resModel, \"action_toggle_favorite\", [[this.resId]]);\n        // Load to have the correct value for 'is_user_favorite'.\n        await this.model.root.load();\n    }\n\n    /**\n     * Toggle the properties panel\n     * @param {boolean} [force] - Flag determining the desired visibility of the properties panel.\n     */\n    toggleProperties(force) {\n        this.env.propertiesPanelState.isDisplayed = (\n            typeof force === \"boolean\" ? force : !this.env.propertiesPanelState.isDisplayed\n        );\n    }\n}\n", "import { loadEmoji } from \"@web/core/emoji_picker/emoji_picker\";\n\n// List of icons that should be avoided when adding a random icon\nconst iconsBlocklist = [\"\ud83d\udca9\", \"\ud83d\udc80\", \"\u2620\ufe0f\", \"\ud83e\udd2e\", \"\ud83d\udd95\", \"\ud83e\udd22\", \"\ud83d\ude12\"];\n\n/**\n * Get a random icon (that is not in the icons blocklist)\n * @returns {String} emoji\n */\nexport async function getRandomIcon() {\n    const { emojis } = await loadEmoji();\n    const randomEmojis = emojis.filter((emoji) => !iconsBlocklist.includes(emoji.codepoints));\n    return randomEmojis[Math.floor(Math.random() * randomEmojis.length)].codepoints;\n}\n\n/**\n * Set an intersection observer on the given element. This function will ensure\n * that the given callback function will be called at most once when the given\n * element becomes visible on screen. This function can be used to load\n * components lazily (see: 'EmbeddedViewComponent').\n * @param {HTMLElement} element\n * @param {Function} callback\n * @returns {IntersectionObserver}\n */\nexport function setIntersectionObserver (element, callback) {\n    const options = {\n        root: null,\n        rootMargin: '0px'\n    };\n    const observer = new window.IntersectionObserver(entries => {\n        const entry = entries[0];\n        if (entry.isIntersecting) {\n            observer.unobserve(entry.target);\n            callback();\n        }\n    }, options);\n    observer.observe(element);\n    return observer;\n}\n", "import { FormRenderer } from \"@web/views/form/form_renderer\";\nimport { useExternalListener } from \"@odoo/owl\";\n\nexport class KnowledgeArticleFormRenderer extends FormRenderer {\n\n    //--------------------------------------------------------------------------\n    // Component\n    //--------------------------------------------------------------------------\n    setup() {\n        super.setup();\n        useExternalListener(document, \"click\", event => {\n            if (event.target.classList.contains(\"o_nocontent_create_btn\")) {\n                this.env.createArticle(\"private\");\n            }\n        });\n    }\n}\n", "import { formView } from '@web/views/form/form_view';\nimport { registry } from \"@web/core/registry\";\nimport { KnowledgeArticleFormController } from './knowledge_controller.js';\nimport { KnowledgeArticleFormRenderer } from './knowledge_renderers.js';\n\nclass KnowledgeModel extends formView.Model {\n    static withCache = false;\n}\n\nexport const knowledgeArticleFormView = {\n    ...formView,\n    Controller: KnowledgeArticleFormController,\n    Model: KnowledgeModel,\n    Renderer: KnowledgeArticleFormRenderer,\n    display: {controlPanel: false}\n};\n\nregistry.category('views').add('knowledge_article_view_form', knowledgeArticleFormView);\n", "import { registry } from \"@web/core/registry\";\n\nconst commandCategoryRegistry = registry.category(\"command_categories\");\n// displays the articles on input \"?\"\ncommandCategoryRegistry.add(\"knowledge_articles\", { namespace: \"?\" }, { sequence: 10 });\n// displays the advanced search command on input \"?\"\ncommandCategoryRegistry.add(\"knowledge_extra\", { namespace: \"?\" }, { sequence: 20 });\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { HotkeyCommandItem } from \"@web/core/commands/default_providers\";\nimport { DefaultCommandItem } from \"@web/core/commands/command_palette\";\nimport { markup } from \"@odoo/owl\";\nimport { user } from \"@web/core/user\";\nimport { highlightText } from \"@web/core/utils/html\";\n\n// Articles command\nclass KnowledgeCommand extends DefaultCommandItem {\n    static template = \"knowledge.KnowledgeCommandTemplate\";\n    static props = {\n        ...DefaultCommandItem.props,\n        headline: String,\n        icon_string: String,\n        isFavorite: Boolean,\n        subjectText: String,\n        subjectName: [String, Boolean],\n    };\n}\n\n// \"Not found, create one\" command\nclass Knowledge404Command extends DefaultCommandItem {\n    static template = \"knowledge.Knowledge404CommandTemplate\";\n    static props = {\n        ...DefaultCommandItem.props,\n        articleName: String,\n    };\n}\n\n// Advanced search command\nclass KnowledgeExtraCommand extends HotkeyCommandItem {\n    static template = \"knowledge.KnowledgeExtraCommandTemplate\";\n}\n\nconst commandSetupRegistry = registry.category(\"command_setup\");\ncommandSetupRegistry.add(\"?\", {\n    debounceDelay: 500,\n    emptyMessage: _t(\"No article found.\"),\n    name: _t(\"articles\"),\n    placeholder: _t(\"Search for an article or a keyword...\"),\n});\n\nconst commandProviderRegistry = registry.category(\"command_provider\");\n\nconst fn = (hidden) => {\n    // Check if the user has enough rights to create a new article\n    const canCreate = () => user.checkAccessRight(\"knowledge.article\", \"create\");\n    let articlesData;\n    return async function provide(env, options) {\n        articlesData = await env.services.orm.call(\n            \"knowledge.article\",\n            \"get_user_sorted_articles\",\n            [[]],\n            {\n                search_query: options.searchValue,\n                hidden_mode: hidden,\n            }\n        );\n        if (!hidden){\n            if (articlesData.length === 0) {\n                // Only display the \"create article\" command when the user can\n                // create an article and when the user inputs at least 3 characters\n                if (options.searchValue.length > 2 && await canCreate()) {\n                    return [{\n                        Component: Knowledge404Command,\n                        async action() {\n                            const articleIds = await env.services.orm.call(\n                                'knowledge.article',\n                                'article_create',\n                                [options.searchValue],\n                                {\n                                    is_private: true\n                                },\n                            );\n\n                            env.services.action.doAction('knowledge.ir_actions_server_knowledge_home_page', {\n                                additionalContext: {\n                                    res_id: articleIds[0],\n                                }\n                            });\n                        },\n                        name: _t('No Article found. Create \"%s\"', options.searchValue),\n                        props: {\n                            articleName: options.searchValue,\n                        },\n                    }];\n                }\n                else {\n                    return [];\n                }\n            }\n        }\n        // display the articles\n        const result = articlesData.map(article => ({\n            Component: KnowledgeCommand,\n            action() {\n                env.services.action.doAction('knowledge.ir_actions_server_knowledge_home_page', {\n                    additionalContext: {\n                        res_id: article.id,\n                    }\n                });\n\n            },\n            category: \"knowledge_articles\",\n            href: `/odoo/knowledge.article/${article.id}`,\n            name: article.name || _t(\"Untitled\"),\n            props: {\n                isFavorite: article.is_user_favorite,\n                headline: article.headline ? markup(article.headline) : \"\",\n                subjectName:\n                    article.root_article_id[0] != article.id ? article.root_article_id[1] : false,\n                subjectText: highlightText(\n                    options.searchValue,\n                    article.root_article_id[1],\n                    \"fw-bolder text-primary\"\n                ),\n                icon_string: article.icon || \"\ud83d\udcc4\",\n            },\n        }));\n        if (!hidden && !(await user.hasGroup(\"base.group_portal\"))) {\n            // add the \"advanced search\" command\n            result.push({\n                Component: KnowledgeExtraCommand,\n                async action() {\n                    const articleIds = articlesData.map(article => article.id);\n                    const action = await env.services.action.loadAction('knowledge.knowledge_article_action');\n                    delete action.context.search_default_filter_not_is_article_item;\n                    env.services.action.doAction(action, {\n                        additionalContext: {\n                            search_default_filter_search_article_ids: 1,\n                            search_article_ids: articleIds,\n                        },\n                    });\n                },\n                category: \"knowledge_extra\",\n                name: _t(\"Advanced Search\"),\n                props: {\n                    hotkey: \"alt+B\",\n                },\n            });\n        }\n        return result;\n    };\n};\n\ncommandProviderRegistry.add(\"knowledge\", {\n    debounceDelay: 500,\n    namespace: \"?\",\n    provide: fn(false),\n});\n\ncommandSetupRegistry.add(\"$\", {\n    debounceDelay: 500,\n    emptyMessage: _t(\"Oops, there's nothing here. Try another search.\"),\n    placeholder: _t(\"Search hidden Articles...\"),\n});\ncommandProviderRegistry.add(\"knowledge_members_only_articles\", {\n    debounceDelay: 500,\n    namespace: \"$\",\n    provide: fn(true),\n});\n", "import { useService } from '@web/core/utils/hooks';\n\nexport const EmbeddedControllersPatch = (T) => class EmbeddedControllersPatch extends T {\n    /**\n     * @override\n     */\n    setup() {\n        super.setup(...arguments);\n        this.orm = useService('orm');\n    }\n    /**\n     * Action when clicking on the Create button for list and kanban embedded\n     * views (for knowledge.article).\n     * Create an article item and redirect to its form view\n     * Note: If Quick Create is enabled on the view, should use quick create\n     * instead of custom create.\n     *\n     * @override\n     */\n    async createRecord() {\n        const { onCreate } = this.props.archInfo;\n        if (this.canQuickCreate && onCreate === \"quick_create\") {\n            return super.createRecord(...arguments);\n        }\n\n        const articleIds = await this.orm.call('knowledge.article', 'article_create', [], {\n            is_article_item: true,\n            is_private: false,\n            parent_id: this.props.context.active_id || false\n        });\n\n        this.props.selectRecord(articleIds[0], {});\n    }\n};\n", "import { EmbeddedControllersPatch } from \"@knowledge/views/embedded_controllers_patch\";\nimport { KanbanController } from \"@web/views/kanban/kanban_controller\";\nimport { kanbanView } from \"@web/views/kanban/kanban_view\";\nimport { registry } from \"@web/core/registry\";\n\nexport class KnowledgeArticleItemsKanbanController extends EmbeddedControllersPatch(\n    KanbanController\n) {\n    /**\n     * @override\n     * Some actions require write access on the parent article. Disable those actions if the user\n     * does not have it.\n     */\n    setup() {\n        super.setup();\n        if (!(\"isEmbeddedReadonly\" in this.env) || this.env.isEmbeddedReadonly) {\n            [\"create\", \"createGroup\", \"deleteGroup\", \"editGroup\"].forEach(\n                (action) => (this.props.archInfo.activeActions[action] = false)\n            );\n            this.props.archInfo.groupsDraggable = false;\n            this.props.archInfo.activeActions.quickCreate = false;\n        }\n    }\n}\n\nregistry.category(\"views\").add(\"knowledge_article_view_kanban_embedded\", {\n    ...kanbanView,\n    Controller: KnowledgeArticleItemsKanbanController,\n});\n", "import { EmbeddedControllersPatch } from \"@knowledge/views/embedded_controllers_patch\";\nimport { ListController } from \"@web/views/list/list_controller\";\nimport { listView } from \"@web/views/list/list_view\";\nimport { registry } from \"@web/core/registry\";\n\nexport class KnowledgeArticleItemsListController extends EmbeddedControllersPatch(ListController) {\n    /**\n     * @override\n     * Item creation is not allowed if the user can not edit the parent article.\n     */\n    setup() {\n        super.setup();\n        if (!(\"isEmbeddedReadonly\" in this.env) || this.env.isEmbeddedReadonly) {\n            this.activeActions.create = false;\n        }\n    }\n}\n\nregistry.category(\"views\").add(\"knowledge_article_view_tree_embedded\", {\n    ...listView,\n    Controller: KnowledgeArticleItemsListController,\n});\n", "import { KnowledgeSearchModelMixin } from \"@knowledge/search_model/search_model\";\nimport { SearchModel } from \"@web/search/search_model\";\nimport { View } from \"@web/views/view\";\n\nexport class EmbeddedView extends View {\n    static props = {\n        ...View.props,\n        saveEmbeddedViewFavoriteFilter: Function,\n        deleteEmbeddedViewFavoriteFilter: Function,\n    };\n\n    async loadView(props) {\n        const {\n            additionalViewProps,\n            saveEmbeddedViewFavoriteFilter,\n            deleteEmbeddedViewFavoriteFilter,\n            ...viewProps\n        } = props;\n        delete viewProps.displayName;\n        Object.assign(viewProps, additionalViewProps);\n        await super.loadView(viewProps);\n        this.withSearchProps.SearchModel = KnowledgeSearchModelMixin(\n            this.withSearchProps.SearchModel || SearchModel\n        );\n        this.withSearchProps.searchModelArgs = {\n            saveEmbeddedViewFavoriteFilter,\n            deleteEmbeddedViewFavoriteFilter,\n        };\n    }\n\n    /**\n     * The super method makes the assumption that the props can only vary in\n     * the search keys, but we cannot make this assumption with embedded views:\n     * the user can edit the additionalViewProps from the embeddedViewComponent\n     */\n    async onWillUpdateProps(nextProps) {\n        super.onWillUpdateProps(nextProps);\n        this.env.config.setDisplayName(nextProps.displayName);\n        if (\n            JSON.stringify(this.props.additionalViewProps) !==\n            JSON.stringify(nextProps.additionalViewProps)\n        ) {\n            Object.assign(this.componentProps, nextProps.additionalViewProps);\n        }\n    }\n}\n", "import { CalendarModel } from \"@web/views/calendar/calendar_model\";\nimport {\n    serializeDate,\n    serializeDateTime,\n    deserializeDate,\n    deserializeDateTime,\n} from \"@web/core/l10n/dates\";\n\n/**\n * CalendarModel allowing the usage of properties as calendar options.\n * Note: currently, as it is only used in knowledge, the properties\n * field name has been hardcoded for clarity, but if needed it can be\n * easily retrieved from the archInfo.\n */\nexport class ItemCalendarModel extends CalendarModel {\n\n    /**\n     * @override\n     * Prevent creation of record if the user does not have write access on the\n     * parent record (readonly knowledge articles for example) and if the model\n     * props are invalid\n     */\n    get canCreate() {\n        if (this.meta.invalid) {\n            return false;\n        }\n        return this.meta.canCreate;\n    }\n    /**\n     * @override\n     * Usually, records cannot be edited if the start field is readonly (which\n     * is a property shared by every record of the current calendar). In this\n     * case however, it depends on the record (eg. users cannot edit readonly\n     * knowledge articles). Therefore, we always allow users to edit the record\n     * but we show an access error if the user tried to edit a readonly record.\n     */\n    get canEdit() {\n        return true;\n    }\n\n    /**\n     * @override\n     * Show the \"all day\" slot in \"day\" and \"week\" scales even if the date is a\n     * datetime, as there is no \"allDay\" option in this model.\n     */\n    get hasAllDaySlot() {\n        return true;\n    }\n\n    /**\n     * @override\n     * Build a raw record from the given partial record and the properties. We\n     * need every property of the record when editing a record otherwise we \n     * will lose the values of these properties when writing on the record\n     * (since the properties are stored as json objects).\n     */\n    buildRawRecord(partialRecord, options = {}) {\n        let start = partialRecord.start;\n        let end = partialRecord.end;\n\n        if (!end || !end.isValid) {\n            // Set end date if not existing\n            if (this.meta.propertiesDateType === \"date\") {\n                end = start;\n            } else {\n                end = start.plus({ hours: 1 });\n            }\n        }\n\n        if (this.meta.propertiesDateType === \"datetime\" && partialRecord.isAllDay) {\n            if (partialRecord.id) {\n                // Keep time of the record when moving or resizing a datetime\n                // record in an allDay scale (eg. in month scale, there is no\n                // time scale so the partialRecord is created as an allDay\n                // record even if the record moved it is a datetime)\n                start = start.set({ hours: this.data.records[partialRecord.id].start.hour});\n                end = end.set({ hours: this.data.records[partialRecord.id].end.hour });  \n            } else {\n                // When creating a datetime that spans multiple days, set\n                // arbitrary start and end hours (to not have midnight by\n                // default)\n                start = start.set({hours: 7});\n                end = end.set({hours: 19});\n            }\n        }\n\n        start = this.meta.propertiesDateType === \"date\" ? serializeDate(start) : serializeDateTime(start);\n        end = this.meta.propertiesDateType === \"date\" ? serializeDate(end) : serializeDateTime(end);\n\n        let properties = {};\n\n        if (partialRecord.id) {\n            // Copy the properties of the existing rawRecord but update the\n            // start and stop date properties\n            properties = this.data.records[partialRecord.id].rawRecord.article_properties;\n            properties.find(property => property.name === this.meta.fieldMapping.date_start).value = start;\n\n            if (this.meta.fieldMapping.date_stop) {\n                properties.find(property => property.name === this.meta.fieldMapping.date_stop).value = end;\n            }\n        } else {\n            // Create the start and stop date properties for the new record\n            properties[this.meta.fieldMapping.date_start] = start;\n            if (this.meta.fieldMapping.date_stop) {\n                properties[this.meta.fieldMapping.date_stop] = end;\n            }\n        }\n        return {article_properties: properties};\n    }\n\n    /**\n     * If the model is invalid (missing props needed to fetch the correct\n     * items after a reload for example), no record should be loaded.\n     */\n    loadRecords(data) {\n        if (this.meta.invalid) {\n            return {};\n        }\n        return super.loadRecords(data);\n    }\n\n    /**\n     * @override\n     * Normalize the given raw record with the properties\n     */\n    normalizeRecord(rawRecord) {\n        const { fieldMapping, propertiesDateType, scale } = this.meta;\n        const isDate = propertiesDateType === \"date\";\n\n        const startValue = rawRecord.article_properties.find(property => property.name === fieldMapping.date_start)?.value;\n        const stopValue = rawRecord.article_properties.find(property => property.name === fieldMapping.date_stop)?.value;\n        const start = isDate\n            ? deserializeDate(startValue)\n            : deserializeDateTime(startValue);\n\n        // If the stop property is not set, use the same date as the start\n        // to not show an invalid date message\n        const end = stopValue ?\n            isDate\n                ? deserializeDate(stopValue)\n                : deserializeDateTime(stopValue)\n            : start;\n\n        // The time of the record is shown in the calendar if it is a datetime\n        // and if the selected scale is month (because there is no time scale\n        // in this case)\n        const showTime = scale === \"month\" && !isDate;\n            \n        const colorValue = rawRecord.article_properties.find(property => property.name === fieldMapping.color)?.value;\n        const colorIndex = Array.isArray(colorValue) ? colorValue[0] : colorValue;\n\n        return {\n            id: rawRecord.id,\n            title: rawRecord.display_name,\n            isAllDay: isDate,\n            start,\n            startType: propertiesDateType,\n            end,\n            endType: propertiesDateType,\n            colorIndex,\n            isTimeHidden: !showTime,\n            rawRecord,\n        };\n    }\n\n    /**\n     * @override\n     * Compute the domain used to fetch the items using the properties used as\n     * start and end date. The properties definition is used in the domain to\n     * make sure to not match records that still use the \"previous\" dateStart\n     * property when this property has been changed (if the type of a property\n     * changed, the property will be replaced by a new one but the change will\n     * be propagated to the records using that property when writing on them)\n     */\n    computeRangeDomain(data) {\n        const { fieldMapping } = this.meta;\n        const formattedEnd = serializeDateTime(data.range.end);\n        const formattedStart = serializeDateTime(data.range.start);\n\n        const domain = [\n            [`article_properties.${fieldMapping.date_start}`, \"<=\", formattedEnd],\n            ['parent_id.article_properties_definition', 'ilike', `\"${fieldMapping.date_start}\"`]\n        ];\n        if (fieldMapping.date_stop) {\n            domain.push(\n                [`article_properties.${fieldMapping.date_stop}`, \">=\", formattedStart],\n                ['parent_id.article_properties_definition', 'ilike', `\"${fieldMapping.date_stop}\"`]\n            );\n        }\n        return domain;\n    }\n}\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { CalendarCommonPopover } from \"@web/views/calendar/calendar_common/calendar_common_popover\";\nimport { CalendarCommonRenderer } from \"@web/views/calendar/calendar_common/calendar_common_renderer\";\nimport { CalendarController } from \"@web/views/calendar/calendar_controller\";\nimport { CalendarRenderer } from \"@web/views/calendar/calendar_renderer\";\nimport { calendarView } from \"@web/views/calendar/calendar_view\";\nimport { ConfirmationDialog } from \"@web/core/confirmation_dialog/confirmation_dialog\";\nimport { ItemCalendarModel } from \"@knowledge/views/item_calendar/item_calendar_model\";\nimport { registry } from \"@web/core/registry\";\nimport { onMounted, onWillUpdateProps, useEffect } from \"@odoo/owl\";\n\nexport class KnowledgeArticleItemsCalendarController extends CalendarController {\n    static template = \"knowledge.ArticleItemsCalendarController\";\n\n    setup() {\n        super.setup();\n        // Item creation is not allowed if the user can not edit the parent article.\n        if (!(\"isEmbeddedReadonly\" in this.env) || this.env.isEmbeddedReadonly) {\n            this.model.meta.canCreate = false;\n        }\n        // Set model meta variables to make the model work with the properties\n        if (this.props.itemCalendarProps) {\n            this.updateModel(this.props.itemCalendarProps);\n            this.state.isWeekendVisible =\n                this.props.itemCalendarProps.showWeekEnds ?? this.state.isWeekendVisible;\n        } else {\n            this.state.missingConfiguration = true;\n            this.model.meta.invalid = true;\n        }\n\n        onMounted(async () => {\n            // Show error message if the start date property is invalid (if it\n            // has been deleted or its type changed)\n            if (!this.model.meta.invalid && Object.keys(this.model.data.records).length === 0) {\n                const propertiesDefinition = await this.orm.read(\n                    this.props.resModel,\n                    [this.props.context.active_id],\n                    [\"article_properties_definition\"]\n                );\n                this.state.missingConfiguration =\n                    !propertiesDefinition[0].article_properties_definition.some(\n                        (property) =>\n                            property.name === this.props.itemCalendarProps.dateStartPropertyId\n                    );\n            }\n        });\n        onWillUpdateProps((nextProps) => {\n            // Update the model if the itemCalendarProps were updated\n            if (\n                JSON.stringify(this.props.itemCalendarProps) !==\n                JSON.stringify(nextProps.itemCalendarProps)\n            ) {\n                this.updateModel(nextProps.itemCalendarProps);\n                this.state.isWeekendVisible =\n                    nextProps.itemCalendarProps.showWeekEnds ?? this.state.isWeekendVisible;\n                this.state.missingConfiguration = false;\n            }\n        });\n    }\n\n    get showSideBar() {\n        // Hide sideabar when the view is embedded\n        return !this.env.isEmbeddedView && super.showSideBar;\n    }\n\n    /**\n     * Create a new article item and open it. If no record is given, creates it\n     * without any start/stop date properties.\n     * @param {Object} record: calendar record used to create the item with\n     * properties.\n     */\n    async createRecord(record) {\n        if (this.model.canCreate) {\n            const createValues = {\n                is_article_item: true,\n                parent_id: this.props.context.active_id || false,\n            };\n            if (record) {\n                const rawRecord = this.model.buildRawRecord(record);\n                Object.assign(createValues, rawRecord);\n            }\n            const articleIds = await this.orm.call(\n                \"knowledge.article\",\n                \"article_create\",\n                [],\n                createValues\n            );\n            this.selectRecord(articleIds[0]);\n        }\n    }\n\n    /**\n     * Send the item to the trash\n     */\n    deleteRecord(record) {\n        this.displayDialog(ConfirmationDialog, {\n            title: _t(\"Confirmation\"),\n            body: _t(\"Are you sure you want to send this article to the trash?\"),\n            confirm: async () => {\n                await this.orm.call(\"knowledge.article\", \"action_send_to_trash\", [record.id]);\n                this.model.load();\n            },\n            confirmLabel: _t(\"Send to trash\"),\n            cancel: () => {\n                // `ConfirmationDialog` needs this prop to display the cancel\n                // button but we do nothing on cancel.\n            },\n        });\n    }\n\n    /**\n     * @override\n     * Open an item to allow editing it. This override allows to load the article from the\n     * KnowledgeController if the view is embedded, allowing to preserve the navigation history\n     */\n    editRecord(record) {\n        this.selectRecord(record.id);\n    }\n\n    /**\n     * Open the given article.\n     * If the view is embedded, the article is loaded from the KnowledgeController to preserve\n     * navigation history.\n     * If the view is opened in full screen, the home page action with the given article is loaded\n     * because the knowledge_article_action_item_calendar action does not have a form view (hence,\n     * props.selectRecord does not work).\n     */\n    selectRecord(articleId) {\n        if (this.env.isEmbeddedView) {\n            this.props.selectRecord(articleId);\n        } else {\n            this.action.doAction(\n                this.orm.call(\"knowledge.article\", \"action_home_page\", [articleId]),\n                {}\n            );\n        }\n    }\n\n    /**\n     * Update the model field mappings and other meta variables using the given\n     * modelProps.\n     */\n    updateModel(modelProps) {\n        this.model.meta.fieldMapping.date_start = modelProps.dateStartPropertyId;\n        this.model.meta.fieldMapping.date_stop = modelProps.dateStopPropertyId;\n        this.model.meta.fieldMapping.color = modelProps.colorPropertyId;\n        this.model.meta.propertiesDateType = modelProps.dateType;\n        if (modelProps.scale) {\n            this.model.meta.scale = modelProps.scale;\n        }\n    }\n\n    get rendererProps() {\n        return {\n            ...super.rendererProps,\n            slotMaxTime: this.props.itemCalendarProps?.slotMaxTime || \"24:00\",\n            slotMinTime: this.props.itemCalendarProps?.slotMinTime || \"00:00\",\n        };\n    }\n}\n\nclass KnowledgeArticleItemsCommonPopover extends CalendarCommonPopover {\n    static subTemplates = {\n        ...CalendarCommonPopover.subTemplates,\n        body: \"knowledge.ArticleItemsCalendarCommonPopover.body\",\n        footer: \"knowledge.ArticleItemsCalendarCommonPopover.footer\",\n    };\n\n    /**\n     * Delete permission should not be based on the view's delete parameter only, but on the\n     * user's write permission on the item as well.\n     */\n    get isEventDeletable() {\n        return super.isEventDeletable && this.props.record.rawRecord.user_can_write;\n    }\n}\n\nclass KnowledgeArticleItemsCommonRenderer extends CalendarCommonRenderer {\n    static props = {\n        ...CalendarCommonRenderer.props,\n        slotMinTime: { type: String },\n        slotMaxTime: { type: String },\n    };\n    static components = {\n        ...CalendarCommonRenderer.components,\n        Popover: KnowledgeArticleItemsCommonPopover,\n    };\n\n    get options() {\n        return {\n            ...super.options,\n            slotMinTime: this.props.slotMinTime,\n            slotMaxTime: this.props.slotMaxTime,\n        };\n    }\n\n    setup() {\n        super.setup();\n        useEffect(\n            (slotMinTime, slotMaxTime) => {\n                this.fc.api.setOption(\"slotMinTime\", slotMinTime);\n                this.fc.api.setOption(\"slotMaxTime\", slotMaxTime);\n            },\n            () => [this.options.slotMinTime, this.options.slotMaxTime]\n        );\n    }\n}\n\nclass KnowledgeArticleItemsCalendarRenderer extends CalendarRenderer {\n    static props = {\n        ...CalendarRenderer.props,\n        slotMinTime: { type: String },\n        slotMaxTime: { type: String },\n    };\n    static components = {\n        ...CalendarRenderer.components,\n        day: KnowledgeArticleItemsCommonRenderer,\n        week: KnowledgeArticleItemsCommonRenderer,\n        month: KnowledgeArticleItemsCommonRenderer,\n    };\n}\n\nregistry.category(\"views\").add(\"knowledge_article_view_calendar_embedded\", {\n    ...calendarView,\n    Controller: KnowledgeArticleItemsCalendarController,\n    Model: ItemCalendarModel,\n    Renderer: KnowledgeArticleItemsCalendarRenderer,\n});\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { registry } from '@web/core/registry';\nimport { user } from \"@web/core/user\";\nimport { listView } from '@web/views/list/list_view';\nimport { ListController } from '@web/views/list/list_controller';\n\n\nexport class KnowledgeArticleController extends ListController {\n    setup() {\n        super.setup();\n        // Hide create button (creation cannot be deactivated to allow imports)\n        this.activeActions.create = false;\n    }\n\n    /**\n     * Add the \"duplicate\" action in the additional actions of the list view.\n     * Place it as the second additional action for admin users.\n     * @override\n     */\n    getStaticActionMenuItems() {\n        const menuItems = super.getStaticActionMenuItems();\n        menuItems.duplicate = {\n            isAvailable: () => this.hasSelectedRecords && user.isAdmin,\n            sequence: 15,\n            icon: \"fa fa-clone\",\n            description: _t(\"Duplicate\"),\n            callback: async () => {\n                const selectedResIds = await this.model.root.getResIds(true);\n                if (selectedResIds.length === 1) {\n                    await this.model.orm.call(this.props.resModel, \"copy\", [selectedResIds]);\n                } else {\n                    await this.model.orm.call(this.props.resModel, \"copy_batch\", [selectedResIds]);\n                }\n                this.model.load();\n            },\n        };\n        return menuItems;\n    }\n}\n\nregistry.category(\"views\").add('knowledge_article_view_tree', {\n    ...listView,\n    Controller: KnowledgeArticleController,\n});\n", "import { SearchBarMenu } from \"@web/search/search_bar_menu/search_bar_menu\";\nimport { ConfirmationDialog } from \"@web/core/confirmation_dialog/confirmation_dialog\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { patch } from \"@web/core/utils/patch\";\n\npatch(SearchBarMenu.prototype, {\n    setup() {\n        super.setup();\n        this.dialogService = useService(\"dialog\");\n    },\n    onDeleteKnowledgeFavoriteItem(itemId) {\n        const dialogProps = {\n            title: _t(\"Warning\"),\n            body: _t(\"This filter is global and will be removed for everyone.\"),\n            confirmLabel: _t(\"Delete Filter\"),\n            confirm: () => this.env.searchModel.deleteFavorite?.(itemId),\n            cancel: () => {},\n        };\n        this.dialogService.add(ConfirmationDialog, dialogProps);\n    },\n});\n", "import { registry } from \"@web/core/registry\";\n\n/**\n * This service store data from non-knowledge form view records that can be used\n * by a Knowledge form view.\n *\n * A typical usage could be the following:\n * - A form view is loaded and one field of the current record is a match for\n *   Knowledge @see FormControllerPatch\n *   - Information about this record and how to access its form view is stored\n *     in this @see KnowledgeCommandsService .\n * - A knowledge Article is opened and it contains a @see TemplateEmbedded .\n *   - When the embedded is injected (@see HtmlFieldPatch ) in the view, it\n *     asks this @see KnowledgeCommandsService if the record can be interacted\n *     with.\n *   - if there is one such record, the related buttons are displayed in the\n *     toolbar of the embedded.\n * - When one such button is used, the form view of the record is reloaded\n *   and the button action is executed through a @see Macro .\n *   - an example of macro action would be copying the template contents as the\n *     value of a field_html of the record, such as \"description\"\n *\n * Scope of the service:\n *\n * Knowledge macros:\n *\n * 1) @see FormControllerPatch :\n *        It will only be called if the viewed record can be used within the\n *        Knowledge module. Such a record should at least have one html field\n *        that is visible and editable by the current user.\n * 2) @see ChatterPatch :\n *        It will be called if the currently viewed record (in a Form view) has\n *        a chatter which the user can use to attach files or send messages.\n * 3) @see TemplateEmbedded or @see FileEmbedded :\n *        It will be called by a embedded to check whether it has a record that\n *        can be interacted with in the context of the toolbar, detected through\n *        case 1) and/or 2): canPostMessages, canAttachFiles, withHtmlField.\n *\n * Knowledge external embedded views insertion:\n *\n * 1) @see EmbeddedViewRendererPatch :\n *        Store information related to an Odoo view in the service, in order\n *        to insert it in a Knowledge article body.\n * 2) @see HtmlFieldPatch :\n *        Recover the previously stored information to perform the insertion.\n */\nexport const knowledgeCommandsService = {\n    start(env) {\n        //----------------------------------------------------------------------\n        // Knowledge macros features\n        //----------------------------------------------------------------------\n\n        // Potential candidate for Knowledge macros.\n        let commandsRecordInfo = null;\n\n        /**\n         * Register data related to a potential record candidate for Knowledge\n         * macros.\n         *\n         * @param {Object|null} recordInfo if null is given, the\n         *        commandsRecordInfo values are reset.\n         * @param {number} [recordInfo.resId] id of the target record\n         * @param {string} [recordInfo.resModel] model name of the target record\n         * @param {Array} [recordInfo.breadcrumbs] array of breadcrumbs objects\n         *                {jsId, name} leading to the target record\n         * @param {boolean} [recordInfo.canPostMessages] target record\n         *                  has a chatter and user can post messages\n         * @param {boolean} [recordInfo.canAttachFiles] target record\n         *                  has a chatter and user can attach files\n         * @param {boolean} [recordInfo.withHtmlField] target record has a\n         *                  targeted html field @see FormControllerPatch\n         * @param {Object} [recordInfo.fieldInfo] info object for the html field\n         *                 {string, name, pageName}\n         * @param {XMLDocument} [recordInfo.xmlDoc] xml document (arch of the\n         *                      form view of the target record)\n         */\n        function setCommandsRecordInfo(recordInfo) {\n            commandsRecordInfo = recordInfo;\n        }\n\n        /**\n         * Get the current record candidate that would be used in Knowledge.\n         */\n        function getCommandsRecordInfo() {\n            return commandsRecordInfo;\n        }\n\n        /**\n         * Copy some actionService breadcrumbs properties used as an identifier\n         * for a recordInfo object.\n         *\n         * @param {Breadcrumbs} breadcrumbs from the @see actionService\n         * @returns {Array[Object]} breadcrumbs identifier containing the jsId\n         *                          and name.\n         */\n        function getBreadcrumbsIdentifier(breadcrumbs) {\n            return breadcrumbs.map(breadcrumb => {\n                return {\n                    jsId: breadcrumb.jsId,\n                    name: breadcrumb.name,\n                };\n            });\n        }\n\n        /**\n         * Evaluate if a record candidate is usable for at least one macro in\n         * Knowledge.\n         *\n         * @param {Object} recordInfo refer to @see setCommandsRecordInfo\n         */\n        function isRecordCompatibleWithMacro(recordInfo) {\n            return recordInfo.canAttachFiles ||\n                recordInfo.canPostMessages ||\n                recordInfo.withHtmlField;\n        }\n\n        /**\n         * Compare the provided breadcrumbs identifier with a previously\n         * registered recordInfo's and unregister the recordInfo if the\n         * registered breadcrumbs are not included at the start of the provided\n         * breadcrumbs.\n         *\n         * @param {Breadcrumbs} breadcrumbs from the @see actionService\n         */\n        function unregisterCommandsRecordInfo(breadcrumbs) {\n            const commandsRecordInfo = getCommandsRecordInfo();\n            if (!commandsRecordInfo) {\n                return;\n            }\n            // Extract identifier props from the breadcrumbs.\n            breadcrumbs = getBreadcrumbsIdentifier(breadcrumbs);\n            if (\n                breadcrumbs.length <= commandsRecordInfo.breadcrumbs.length ||\n                breadcrumbs[commandsRecordInfo.breadcrumbs.length - 1].jsId !== commandsRecordInfo.breadcrumbs.at(-1).jsId\n            ) {\n                // Unregister the recordInfo if the target controller does not\n                // match what was recorded.\n                setCommandsRecordInfo(null);\n            }\n        }\n\n        //----------------------------------------------------------------------\n        // External embedded views features\n        //----------------------------------------------------------------------\n\n        let pendingEmbeddedBlueprints = {};\n\n        /**\n         * @param {Object}\n         * @param {HTMLElement} embeddedBlueprint element to be inserted in a\n         *                      html field\n         * @param {string} model model name of the target record\n         * @param {string} field field name of the target record\n         * @param {integer} resId id of the target record\n         */\n        function setPendingEmbeddedBlueprint({embeddedBlueprint, model, field, resId}) {\n            if (!(model in pendingEmbeddedBlueprints)) {\n                pendingEmbeddedBlueprints[model] = {};\n            }\n            if (!(field in pendingEmbeddedBlueprints[model])) {\n                pendingEmbeddedBlueprints[model][field] = {};\n            }\n            pendingEmbeddedBlueprints[model][field][resId] = embeddedBlueprint;\n        }\n\n        function popPendingEmbeddedBlueprint({model, field, resId}) {\n            if (model in pendingEmbeddedBlueprints && field in pendingEmbeddedBlueprints[model]) {\n                const pendingEmbeddedBlueprint = pendingEmbeddedBlueprints[model][field][resId];\n                delete pendingEmbeddedBlueprints[model][field][resId];\n                return pendingEmbeddedBlueprint;\n            }\n        }\n\n        const knowledgeCommandsService = {\n            setCommandsRecordInfo,\n            getCommandsRecordInfo,\n            getBreadcrumbsIdentifier,\n            isRecordCompatibleWithMacro,\n            unregisterCommandsRecordInfo,\n            setPendingEmbeddedBlueprint,\n            popPendingEmbeddedBlueprint,\n        };\n        return knowledgeCommandsService;\n    }\n};\n\nregistry.category(\"services\").add(\"knowledgeCommandsService\", knowledgeCommandsService);\n", "import { registry } from \"@web/core/registry\";\n\n/**\n * This service is comprised of 2 commands that are interacting with embeddedFilters which is\n * the object that will store the different filters of the embedded views.\n *\n * @function saveFilters This function is used to save the filters for a specific embedId.\n * @function applyFilter This function adds the correct filter to the view's props.\n */\nexport const knowledgeEmbedViewsFilters = {\n    start(env) {\n        const embeddedFilters = {};\n        const commands = {\n            /**\n             * @param {*} currentController The current controller returned by the action service\n             * @param {String} filterKey The ID of the impacted embedded view\n             * @param {Object} searchModel The searchModel for the view's props that will be saved\n             * inside the embeddedFilters Object.\n             */\n            saveFilters: (currentController, filterKey, searchModel) => {\n                if (!embeddedFilters[filterKey]) {\n                    embeddedFilters[filterKey] = {};\n                }\n                embeddedFilters[filterKey][currentController.jsId] = searchModel;\n            },\n            /**\n             * This function applies the previously saved filters to the view's props.\n             * Each time that we apply filters to a view we remove the filter that we apply in order to\n             * avoid collisions and to avoid applying the filters when we change articles in Knowledge\n             * (which should not add filters to the embedded view).\n             *\n             * When we come back to the root of the breadcrumbs, we remove all the filters for the specific\n             * embed view in order to not store useless filters and to avoid collisions with filters that\n             * weren't applied to the view.\n             * This way we ensure that filters are either applied once or never, if the corresponding breadcrumb has\n             * not been opened.\n             * @param {*} currentController The current controller given by the action service\n             * @param {String} filterKey The key used to get the filters of a specific embedded view\n             * @param {*} ViewProps The embedded view's props that will be updated with the filters\n             */\n            applyFilter: (currentController, filterKey, ViewProps) => {\n                const currentJsId = currentController.jsId;\n                const embedSearchFilters = embeddedFilters[filterKey];\n\n                if (embedSearchFilters && embedSearchFilters[currentJsId]) {\n                    ViewProps.globalState.searchModel = embeddedFilters[filterKey][currentJsId];\n                    delete embeddedFilters[filterKey][currentJsId];\n                }\n\n                if (currentController.config.breadcrumbs.length === 1) {\n                    delete embeddedFilters[filterKey];\n                }\n            }\n        };\n        return commands;\n    }\n};\n\nregistry.category(\"services\").add(\"knowledgeEmbedViewsFilters\", knowledgeEmbedViewsFilters);\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { AlertDialog } from \"@web/core/confirmation_dialog/confirmation_dialog\";\nimport { isVisible } from \"@web/core/utils/ui\";\nimport { Macro } from \"@web/core/macro\";\n\nclass KnowledgeMacroError extends Error {}\n\n/**\n * Abstract class for Knowledge macros, that will be used to interact like a\n * tour with a Form view chatter and/or html field.\n */\nexport class AbstractMacro extends Macro {\n    /**\n     * @param {Object} options\n     * @param {HTMLElement} options.targetXmlDoc\n     * @param {Array[Object]} options.breadcrumbs\n     * @param {Any} options.data\n     * @param {Object} options.services required: action, dialog, ui\n     */\n    constructor({ targetXmlDoc, breadcrumbs, data, services }) {\n        super({\n            name: \"restore_record\",\n            steps: [],\n        });\n        this.targetXmlDoc = targetXmlDoc;\n        this.targetBreadcrumbs = breadcrumbs;\n        this.data = data;\n        this.services = services;\n        this.steps = this.buildSteps();\n    }\n\n    blockUI() {\n        if (!this.services.ui.isBlocked) {\n            this.services.ui.block();\n        }\n    }\n\n    buildSteps() {\n        // Build the desired macro action\n        const steps = this.getSteps();\n        if (!steps.length) {\n            return [];\n        }\n        /**\n         * Preliminary breadcrumbs macro. It will use the @see breadcrumbsIndex\n         * to switch back to the view related to the stored record\n         * (@see KnowledgeCommandsService ). Once and if the view of the target\n         * record is correctly loaded, run the specific macroAction.\n         */\n        return [\n            {\n                action: () => this.blockUI(),\n            },\n            {\n                // Restore the previous view:\n                action: async () => {\n                    try {\n                        // Try to restore the target controller.\n                        await this.services.action.restore(this.targetBreadcrumbs.at(-1).jsId);\n                    } catch {\n                        // If the controller is unreachable, abort the macro.\n                        throw new KnowledgeMacroError(\n                            _t('The record that this macro is targeting could not be found.')\n                        );\n                    }\n                },\n            },\n            {\n                // Start the requested macro when the current breadcrumbs\n                // match the target Form view.\n                trigger: () => {\n                    const controllerBreadcrumbs = this.services.action.currentController.config.breadcrumbs;\n                    if (this.targetBreadcrumbs.at(-1).jsId === controllerBreadcrumbs.at(-1)?.jsId) {\n                        return this.getFirstVisibleElement(\".o_breadcrumb .o_last_breadcrumb_item\");\n                    }\n                    return null;\n                },\n            },\n            ...steps,\n        ];\n    }\n\n    /**\n     * @param {Error} error\n     * @param {Object} step\n     * @param {integer} index\n     */\n    onError(error, step, index) {\n        this.blockUI();\n        if (error instanceof KnowledgeMacroError) {\n            this.services.dialog.add(AlertDialog, {\n                body: error.message,\n                title: _t(\"Error\"),\n                confirmLabel: _t(\"Close\"),\n            });\n        } else {\n            console.error(error);\n        }\n    }\n\n    onTimeout() {\n        throw new KnowledgeMacroError(_t(\"The operation could not be completed.\"));\n    }\n\n    /**\n     * Searches for the first element in the dom matching the selector. The\n     * results are filtered with `filter` and the returned element is either\n     * the first or the last depending on `reverse`.\n     *\n     * @param {String|HTMLElement} selector\n     * @param {Function} filter\n     * @param {boolean} reverse\n     * @returns {HTMLElement}\n     */\n    getFirstVisibleElement(selector, filter=false, reverse=false) {\n        const elementsArray = typeof(selector) === 'string' ? Array.from(document.querySelectorAll(selector)) : [selector];\n        const sel = filter ? elementsArray.filter(filter) : elementsArray;\n        for (let i = 0; i < sel.length; i++) {\n            i = reverse ? sel.length - 1 - i : i;\n            if (isVisible(sel[i])) {\n                return sel[i];\n            }\n        }\n        return null;\n    }\n\n    /**\n     * To be overridden by an actual Macro implementation. It should contain\n     * the steps to be executed on the target Form view.\n     *\n     * @returns {Array[Object]}\n     */\n    getSteps() {\n        return [];\n    }\n\n    unblockUI() {\n        if (this.services.ui.isBlocked) {\n            this.services.ui.unblock();\n        }\n    }\n\n    /**\n     * Validate that the macro is still on the correct Form view by checking\n     * that the target breadcrumbs are the same as the current ones. To be used\n     * at each step inside the target Form view. Throwing an error will\n     * terminate the macro.\n     */\n    validatePage() {\n        const controllerBreadcrumbs = this.services.action.currentController.config.breadcrumbs;\n        if (this.targetBreadcrumbs.at(-1).jsId !== controllerBreadcrumbs.at(-1)?.jsId) {\n            throw new KnowledgeMacroError(\n                _t('The record that this macro is targeting could not be found.')\n            );\n        }\n    }\n    /**\n     * Handle the case where an item is hidden in a tab of the form view\n     * notebook. Only pages with the \"name\" attribute set can be navigated to.\n     * Other pages are ignored (and the fields they contain are too).\n     * @see FormControllerPatch\n     *\n     * @param {String} targetSelector selector (will be used in the target\n     * xml document) for the element (likely an html field) that could be\n     * hidden inside a non-active tab of the notebook.\n     */\n    searchInXmlDocNotebookTab(targetSelector) {\n        const searchElement = this.targetXmlDoc.querySelector(targetSelector);\n        const page = searchElement ? searchElement.closest('page') : undefined;\n        const pageName = page ? page.getAttribute('name') : undefined;\n        if (!pageName) {\n            return;\n        }\n        const pageEl = this.getFirstVisibleElement(`.o_notebook .nav-link[name=${pageName}]:not(.active)`);\n        if (pageEl) {\n            pageEl.click();\n        }\n    }\n}\n", "import { AbstractMacro } from \"@knowledge/macros/abstract_macro\";\nimport { pasteElements, replaceHtmlFieldContentWith } from \"@knowledge/macros/utils\";\n\n/**\n * Macro that will open the Full Composer Form view dialog in the Form view\n * context of a target record, and paste text content inside it. Does not\n * actually send the message.\n */\nexport class SendAsMessageMacro extends AbstractMacro {\n    /**\n     * @override\n     * @returns {Array[Object]}\n     */\n    getSteps() {\n        let sendMessageLastClickedEl = null;\n        return [\n            {\n                // Search for the chatter button to send a message and make sure\n                // that the composer is visible. Search in notebook tabs too.\n                trigger: () => {\n                    this.validatePage();\n                    const el = this.getFirstVisibleElement(\n                        \".o-mail-Chatter-sendMessage:not([disabled])\"\n                    );\n                    if (el) {\n                        if (el.classList.contains(\"active\")) {\n                            return el;\n                        } else if (el !== sendMessageLastClickedEl) {\n                            el.click();\n                            sendMessageLastClickedEl = el;\n                        }\n                    } else {\n                        this.searchInXmlDocNotebookTab(\"chatter\");\n                    }\n                    return null;\n                },\n            },\n            {\n                // Open the full composer Form view Dialog.\n                trigger: () => {\n                    this.validatePage();\n                    return this.getFirstVisibleElement(\n                        '.o-mail-Composer button[name=\"open-full-composer\"]:not([disabled])'\n                    );\n                },\n                async action(trigger) {\n                    trigger.click();\n                },\n            },\n            {\n                // Paste the html data inside the message body.\n                trigger: () => {\n                    this.validatePage();\n                    const dialog = this.getFirstVisibleElement(\".o_dialog .o_mail_composer_form\");\n                    if (dialog) {\n                        return this.getFirstVisibleElement(\n                            dialog.querySelector('.o_field_html[name=\"body\"] .odoo-editor-editable')\n                        );\n                    }\n                    return null;\n                },\n                action: (el) => {\n                    replaceHtmlFieldContentWith(this.data.dataTransfer, el);\n                },\n            },\n            {\n                action: () => this.unblockUI(),\n            },\n        ];\n    }\n}\n\n/**\n * Macro that will append content in the target record's html field in its Form\n * view. Does not trigger the save (field will be dirty).\n */\nexport class UseAsDescriptionMacro extends AbstractMacro {\n    /**\n     * @override\n     * @returns {Array[Object]}\n     */\n    getSteps() {\n        return [\n            {\n                // Ensure that the Form view is editable\n                trigger: () => this.getFirstVisibleElement(\".o_form_editable\"),\n            },\n            {\n                // Search for the target html field and ensure that it is editable.\n                // Search in notebook tabs too.\n                trigger: () => {\n                    this.validatePage();\n                    const el = this.getFirstVisibleElement(\n                        `.o_field_html[name=\"${this.data.fieldName}\"]`,\n                        (element) => element.querySelector(\".odoo-editor-editable\")\n                    );\n                    if (el) {\n                        return el;\n                    }\n                    if (this.data.pageName) {\n                        this.searchInXmlDocNotebookTab(`page[name=\"${this.data.pageName}\"]`);\n                    }\n                    return null;\n                },\n                async action(trigger) {\n                    trigger.click();\n                },\n            },\n            {\n                // Search for the editable element. Paste the html data inside the\n                // field.\n                trigger: () => {\n                    this.validatePage();\n                    return this.getFirstVisibleElement(\n                        `.o_field_html[name=\"${this.data.fieldName}\"] .odoo-editor-editable`\n                    );\n                },\n                action: (el) => {\n                    el.addEventListener(\n                        \"onHistoryResetFromPeer\",\n                        (ev) => pasteElements(this.data.dataTransfer, el),\n                        { once: true }\n                    );\n                    pasteElements(this.data.dataTransfer, el);\n                },\n            },\n            {\n                action: () => this.unblockUI(),\n            },\n        ];\n    }\n}\n", "import { AbstractMacro } from \"@knowledge/macros/abstract_macro\";\nimport { dragAndDrop } from \"@knowledge/macros/utils\";\n\n/**\n * Macro that will add a file as an attachment of a record and display it\n * in its Form view.\n */\nexport class UseAsAttachmentMacro extends AbstractMacro {\n    /**\n     * @override\n     * @returns {Array[Object]}\n     */\n    getSteps() {\n        let attachFilesLastClickedEl = null;\n        return [\n            {\n                // Search for the chatter button to attach a file and open\n                // the AttachmentList zone. Search in notebook tabs too.\n                trigger: () => {\n                    this.validatePage();\n                    const el = this.getFirstVisibleElement(\n                        \".o-mail-Chatter-attachFiles:not([disabled])\",\n                        (matchEl) => {\n                            // Wait for the attachments to be loaded by the chatter.\n                            const attachmentsCountEl = matchEl.querySelector(\"sup\");\n                            return (\n                                attachmentsCountEl &&\n                                Number.parseInt(attachmentsCountEl.textContent) > 0\n                            );\n                        }\n                    );\n                    if (el) {\n                        const attachmentBoxEl = this.getFirstVisibleElement(\n                            \".o-mail-AttachmentBox .o-mail-AttachmentList\"\n                        );\n                        if (attachmentBoxEl) {\n                            return attachmentBoxEl;\n                        } else if (el !== attachFilesLastClickedEl) {\n                            el.click();\n                            attachFilesLastClickedEl = el;\n                        }\n                    } else {\n                        this.searchInXmlDocNotebookTab(\"chatter\");\n                    }\n                    return null;\n                },\n                action: (el) => el.scrollIntoView(),\n            },\n            {\n                action: () => this.unblockUI(),\n            },\n        ];\n    }\n}\n\n/**\n * Macro that will add a file to a message (without sending it) in the context\n * of a record in its Form view.\n */\nexport class AttachToMessageMacro extends AbstractMacro {\n    /**\n     * @override\n     * @returns {Array[Object]}\n     */\n    getSteps() {\n        let sendMessageLastClickedEl = null;\n        return [\n            {\n                // Search for the chatter button to send a message and make sure\n                // that the composer is visible. Search in notebook tabs too.\n                trigger: () => {\n                    this.validatePage();\n                    const el = this.getFirstVisibleElement(\n                        \".o-mail-Chatter-sendMessage:not([disabled])\"\n                    );\n                    if (el) {\n                        if (el.classList.contains(\"active\")) {\n                            return el;\n                        } else if (el !== sendMessageLastClickedEl) {\n                            el.click();\n                            sendMessageLastClickedEl = el;\n                        }\n                    } else {\n                        this.searchInXmlDocNotebookTab(\"chatter\");\n                    }\n                    return null;\n                },\n                action: (el) => {\n                    el.scrollIntoView();\n                },\n            },\n            {\n                // Search for the composer button to attach files and start dragging\n                // the data file over it.\n                trigger: () => {\n                    this.validatePage();\n                    return this.getFirstVisibleElement(\n                        `.o-mail-Composer button[name=\"upload-files\"]:not([disabled])`\n                    );\n                },\n                action: dragAndDrop.bind(this, \"dragenter\", this.data.dataTransfer),\n            },\n            {\n                // Search for the composer drop zone for attachments and drop the\n                // data file into it.\n                trigger: () => {\n                    this.validatePage();\n                    return this.getFirstVisibleElement(\".o-mail-Composer-dropzone\");\n                },\n                action: dragAndDrop.bind(this, \"drop\", this.data.dataTransfer),\n            },\n            {\n                action: () => this.unblockUI(),\n            },\n        ];\n    }\n}\n", "/**\n * @param {String} string - Name of the drag event\n * @param {DataTransfer} dataTransfer - Object used to store data\n * @param {HTMLElement} target - Target element\n */\nexport function dragAndDrop(type, dataTransfer, target) {\n    const fakeDragAndDrop = new Event(type, {\n        bubbles: true,\n        cancelable: true,\n        composed: true,\n    });\n    fakeDragAndDrop.dataTransfer = dataTransfer;\n    target.dispatchEvent(fakeDragAndDrop);\n}\n\n/**\n * @param {DataTransfer} dataTransfer - Object used to store data\n * @param {HTMLElement} target - Target element\n */\nexport function pasteElements(dataTransfer, target) {\n    const fakePaste = new Event('paste', {\n        bubbles: true,\n        cancelable: true,\n        composed: true,\n    });\n    fakePaste.clipboardData = dataTransfer;\n\n    const sel = document.getSelection();\n    sel.removeAllRanges();\n    const range = document.createRange();\n    const lastChild = target.lastChild;\n    if (!lastChild) {\n        range.setStart(target, 0);\n        range.setEnd(target, 0);\n    } else {\n        const subLastChild = lastChild.lastChild;\n        if (subLastChild) {\n            if (subLastChild.nodeType === Node.ELEMENT_NODE && subLastChild.tagName === 'BR') {\n                range.setStartBefore(subLastChild);\n                range.setEndBefore(subLastChild);\n            } else {\n                range.setStartAfter(subLastChild);\n                range.setEndAfter(subLastChild);\n            }\n        } else {\n            range.setStartAfter(lastChild);\n            range.setEndAfter(lastChild);\n        }\n    }\n    const lastElementChild = target.lastElementChild;\n    if (lastElementChild) {\n        lastElementChild.scrollIntoView();\n    } else {\n        target.scrollIntoView();\n    }\n    sel.addRange(range);\n    target.dispatchEvent(fakePaste);\n}\n\n/**\n * @param {DataTransfer} dataTransfer\n * @param {HTMLElement} editable\n */\nexport function replaceHtmlFieldContentWith(dataTransfer, editable) {\n    editable.replaceChildren(); // Hack to avoid having a paragraph after the user's signature\n    const event = new Event(\"paste\", {\n        bubbles: true,\n        cancelable: true,\n        composed: true,\n    });\n    event.clipboardData = dataTransfer;\n    editable.dispatchEvent(event);\n}\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { registry } from '@web/core/registry';\nimport { useService } from \"@web/core/utils/hooks\";\n\nimport { CheckBox } from \"@web/core/checkbox/checkbox\";\nimport { ConfirmationDialog } from \"@web/core/confirmation_dialog/confirmation_dialog\";\nimport {\n    BooleanToggleField,\n    booleanToggleField,\n} from \"@web/views/fields/boolean_toggle/boolean_toggle_field\";\n\nexport class ConfirmCheckBox extends CheckBox {\n    onClick(ev) {\n        ev.preventDefault();\n\n        if (ev.target.tagName !== \"INPUT\") {\n            return;\n        }\n        this.props.onChange(ev.target.checked);\n    }\n}\n\nexport class BooleanToggleConfirm extends BooleanToggleField {\n    static template = \"stock_account.BooleanToggleConfirm\";\n    static components = { ConfirmCheckBox };\n\n    setup() {\n        super.setup();\n        this.dialogService = useService('dialog');\n    }\n\n    onChange(value) {\n        const record = this.props.record.data;\n        const updateAndSave = () => {\n            this.props.record.update({ [this.props.name]: value }, { save: true });\n        };\n\n        if (record.lot_valuated && !value) {\n            this.dialogService.add(ConfirmationDialog, {\n                body: _t(\"This operation might lead in a loss of data. Valuation will be identical for all lots/SN. Do you want to proceed ? \"),\n                confirm: updateAndSave,\n                cancel: () => {},\n            });\n\n        }\n        else {\n            updateAndSave();\n        }\n    }\n}\n\nexport const booleanToggleConfirm = {\n    ...booleanToggleField,\n    component: BooleanToggleConfirm,\n};\n\nregistry.category(\"fields\").add(\"confirm_boolean\", booleanToggleConfirm);\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { patch } from \"@web/core/utils/patch\";\n\nimport { ForecastedHeader as Parent } from \"@stock/stock_forecasted/forecasted_header\";\n\nexport class StockAccountForecastedHeader extends Parent {\n    static template = \"stock_account.ForecastedHeader\";\n}\n\npatch(Parent.prototype, {\n    async _onClickValuation() {\n        const context = this._getActionContext();\n        return this.action.doAction({\n            name: _t('Stock Valuation'),\n            res_model: 'stock.move',\n            type: 'ir.actions.act_window',\n            view_mode: 'list,form',\n            views: [[false, 'list'], [false, 'form']],\n            target: 'current',\n            context: context,\n        });\n    },\n\n    _getActionContext() {\n        const context = { ...this.context };\n        const templates = this.props.docs.product_templates_ids;\n        if (templates) {\n            context.search_default_product_tmpl_id = templates;\n        } else {\n            context.search_default_product_id = this.props.docs.product_variants_ids;\n        }\n        return context;\n    }\n});\n", "import { Component } from \"@odoo/owl\";\n\nexport class StockValuationReportButtonsBar extends Component {\n    static template = \"stock_account.StockValuationReportButtonsBar\";\n    static props = {};\n\n    onClickGenerateEntry() {\n        return this.env.controller.actionGenerateEntry();\n    }\n\n    onClickPDF() {\n        return this.env.controller.actionPrintReport(\"pdf\");\n    }\n\n    onClickXLSX() {\n        return this.env.controller.actionPrintReport(\"xlsx\");\n    }\n}\n", "import { reactive } from \"@odoo/owl\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { serializeDate, serializeDateTime } from \"@web/core/l10n/dates\";\nconst { DateTime } = luxon;\n\n\nexport class StockValuationReportController {\n    constructor(action) {\n        this.action = action;\n        this.actionService = useService(\"action\");\n        this.dialog = useService(\"dialog\");\n        this.orm = useService(\"orm\");\n        this.state = reactive({\n            date: DateTime.now(),\n        });\n    }\n\n    async load() {\n        await this.loadReportData();\n        this.currencyId = this.data.currency_id;\n        this.companyId = this.data.company_id;\n    }\n\n    async loadReportData() {\n        const kwargs = {\n            date: this.state.date.toISODate() || false,\n        };\n        const res = await this.orm.call(\n            \"stock_account.stock.valuation.report\",\n            \"get_report_values\",\n            [],\n            kwargs\n        );\n        this.data = res.data;\n        // Prepare the \"Inventory Loss\" lines.\n        if (this.data.inventory_loss) {\n            for (const line of this.data.inventory_loss.lines) {\n                line.account = this.data.accounts_by_id[line.account_id];\n            }\n        }\n        // Prepare \"Stock Variation\" lines.\n        for (const line of this.data.stock_variation.lines) {\n            line.account = this.data.accounts_by_id[line.account_id];\n        }\n        // Prepare the \"Initial Balance\" lines.\n        this.data.initial_balance.lines = [];\n        this.data.initial_balance.accounts = [];\n        for (let [accountId, data] of Object.entries(this.data.initial_balance.lines_by_account_id)) {\n            const account = this.data.accounts_by_id[accountId];\n            this.data.initial_balance.lines.push({\n                label: account.display_name,\n                value: data.value,\n                account_id: accountId,\n            });\n            this.data.initial_balance.accounts.push(...data.accounts);\n        }\n        // Prepare the \"Ending Stock\" lines.\n        this.data.ending_stock.lines = [];\n        this.data.ending_stock.accounts = [];\n        for (let [accountId, data] of Object.entries(this.data.ending_stock.lines_by_account_id)) {\n            const account = this.data.accounts_by_id[accountId];\n            this.data.ending_stock.lines.push({\n                label: account?.display_name,\n                value: data.value,\n                account_id: accountId,\n            });\n            this.data.ending_stock.accounts.push(...data.accounts);\n        }\n    }\n\n    async setDate(date) {\n        this.state.date = date;\n        this.dateAsString = serializeDateTime(date);\n        await this.loadReportData();\n    }\n\n    // Actions -----------------------------------------------------------------\n    async actionGenerateEntry() {\n        const args = [[this.companyId]];\n        const date = serializeDate(this.state.date);\n        if (date != serializeDate(DateTime.now())) {\n            args.push(date);\n        }\n        const action = await this.orm.call(\"res.company\", \"action_close_stock_valuation\", args);\n        if (action) {\n            this.actionService.doAction(action);\n        }\n    }\n\n    actionPrintReport(format=\"pdf\") {\n        if (format === \"pdf\") {\n            return this.orm.call(\"stock_account.stock.valuation.report\", \"action_print_as_pdf\");\n        } else if (format === \"xlsx\") {\n            return this.orm.call(\"stock_account.stock.valuation.report\", \"action_print_as_xlsx\");\n        }\n    }\n}\n", "import { Component, useRef } from \"@odoo/owl\";\nimport { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { useDateTimePicker } from \"@web/core/datetime/datetime_picker_hook\";\n\nexport class StockValuationReportFilters extends Component {\n    static template = \"stock_account.StockValuationReport.Filters\";\n    static components = {\n        Dropdown,\n    };\n    static props = {};\n\n    setup() {\n        this.dateFilterRef = useRef(\"filterDate\");\n        const getPickerProps = () => {\n            const pickerProps = {\n                value: this.env.controller.state.date,\n                type: \"date\",\n            };\n            return pickerProps;\n        };\n        this.dateTimePicker = useDateTimePicker({\n            target: \"filterDate\",\n            get pickerProps() {\n                return getPickerProps();\n            },\n            onApply: (newDate) => {\n                if (newDate) {\n                    this.env.controller.setDate(newDate);\n                    this.render();\n                }\n            },\n        });\n    }\n\n    onDateClick() {\n        this.dateTimePicker.open();\n    }\n\n    get date() {\n        return this.env.controller.state.date.toLocaleString();\n    }\n}\n", "import { Component, useState } from \"@odoo/owl\";\n\n\nexport class StockValuationReportLine extends Component {\n    static template = \"stock_account.StockValuationReport.InventoryValuationLine\";\n    static props = {\n        class: { type: String, optional: true },\n        displayDebitCredit: { type: Boolean, optional: true },\n        label: { type: String, optional: true },\n        level: { type: Number, optional: true },\n        line: { type: Object, optional: true },\n        sublines: { type: Array, optional: true },\n        onClickMethod: { type: Function, optional: true },\n        value: { type: Number, optional: true },\n    };\n    static defaultProps = {\n        level: 0,\n    };\n\n    setup() {\n        this.hasSublines = Boolean(this.props.sublines?.length);\n        this.state = useState({ displaySublines: this.hasSublines });\n    }\n\n    // Getters -----------------------------------------------------------------\n    get credit() {\n        if (this.props.line?.credit) {\n            return this.env.formatMonetary(this.props.line.credit);\n        }\n        return false;\n    }\n\n    get cssClass() {\n        let cssClass = this.props.class || \"\";\n        cssClass += ` line_level_${this.props.level}`;\n        return cssClass;\n    }\n\n    get debit() {\n        if (this.props.line?.debit) {\n            return this.env.formatMonetary(this.props.line.debit);\n        }\n        return false;\n    }\n\n    get displayTotalOnSeparateLine() {\n        return Boolean(this.props.value && this.state.displaySublines);\n    }\n\n    get formattedValue() {\n        if (this.props.value !== undefined) {\n            return this.env.formatMonetary(this.props.value);\n        }\n        return false;\n    }\n\n    get label() {\n        return this.props.label || this.props.line.account?.display_name;\n    }\n\n    get totalProps() {\n        const props = {\n            class: \"total\",\n            label: this.env._t(\"Total\"),\n            level: this.props.level,\n            value: this.props.value,\n        };\n        if (this.props.onClickMethod) {\n            props.onClickMethod = this.props.onClickMethod;\n        }\n        return props;\n    }\n\n    // On Click Methods --------------------------------------------------------\n    onClick() {\n        this.props.onClickMethod && this.props.onClickMethod(this.props.line);\n    }\n\n    onClickToggle() {\n        if (this.props.sublines && this.props.sublines.length) {\n            this.state.displaySublines = !this.state.displaySublines;\n        }\n    }\n}\n\nStockValuationReportLine.components = { StockValuationReportLine };\n", "import { StockValuationReportLine } from \"./line\";\n\n\nexport class StockValuationReportToggleLine extends StockValuationReportLine {\n    static template = \"stock_account.StockValuationReport.InventoryValuationToggleLine\";\n}\n\nStockValuationReportToggleLine.components.StockValuationReportToggleLine = StockValuationReportToggleLine;", "import { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { ControlPanel } from \"@web/search/control_panel/control_panel\";\nimport { formatMonetary } from \"@web/views/fields/formatters\";\nimport { standardActionServiceProps } from \"@web/webclient/actions/action_service\";\n\nimport { Component, onWillStart, useChildSubEnv, useState } from \"@odoo/owl\";\n\nimport { StockValuationReportButtonsBar } from \"../stock_valuation/buttons_bar/buttons_bar\"\nimport { StockValuationReportController } from \"../stock_valuation/controller\"\nimport { StockValuationReportFilters } from \"../stock_valuation/filters/filters\"\nimport { StockValuationReportLine } from \"../stock_valuation/line/line\"\nimport { StockValuationReportToggleLine } from \"../stock_valuation/line/toggle_line\"\n\n\nexport class StockValuationReport extends Component {\n    static template = \"stock_account.StockValuationReport\";\n    static props = { ...standardActionServiceProps };\n    static components = {\n        ControlPanel,\n        StockValuationReportButtonsBar,\n        StockValuationReportFilters,\n        StockValuationReportLine,\n        StockValuationReportToggleLine,\n    };\n\n    setup() {\n        this.controller = useState(new StockValuationReportController(this.props.action));\n        this.state = useState({\n            displayInventoryValuationLine: false,\n        })\n        this.orm = useService(\"orm\");\n        this.actionService = useService(\"action\");\n        this._t = _t;\n\n        onWillStart(async () => {\n            await this.controller.load(this.data);\n        })\n\n        useChildSubEnv({\n            _t,\n            controller: this.controller,\n            formatMonetary: this.formatMonetary.bind(this),\n        });\n    }\n\n    formatMonetary(value) {\n        return formatMonetary(value, {\n            currencyId: this.data.currency_id,\n        });\n    }\n\n    get accrual() {\n        return { label: _t(\"Accrual\"), lines: [], value: 0 };\n    }\n\n    // Getters -----------------------------------------------------------------\n    get data() {\n        return this.controller.data || {};\n    }\n\n    get accountingStockValuation() {\n        return this.formatMonetary(this.data.accounting_stock_valuation);\n    }\n\n    get inventoryValuation() {\n        return formatMonetary(this.data.inventory_valuation.value, {\n            currencyId: this.data.currency_id,\n        });\n    }\n\n    get stockInitial() {\n        return this.formatMonetary(this.data.stock_initial);\n    }\n\n    get stockVariation() {\n        return this.formatMonetary(this.data.stock_variation);\n    }\n\n    // On Click Methods --------------------------------------------------------\n    openAccountMoves(accountMoves=false) {\n        const additionalContext = {};\n        const domain = [];\n        if (accountMoves) {\n            const ids = accountMoves.map((am) => am.id);\n            const names = accountMoves.map((am) => am.name);\n            additionalContext.search_default_name = names;\n            additionalContext.search_default_ids = ids;\n            domain.push([\"id\", \"in\", ids])\n        }\n        return this.actionService.doAction(\n            \"account.action_move_journal_line\",\n            { additionalContext, domain }\n        );\n    }\n\n    openStockMoveView(title, usage) {\n        const domain = [\n            \"|\",\n            [\"location_id.usage\", \"=\", usage],\n            [\"location_dest_id.usage\", \"=\", usage],\n        ];\n        if (this.controller.dateAsString) {\n            domain.unshift(\"&\");\n            domain.push([\"date\", \"<=\", this.controller.dateAsString]);\n        }\n        return this.actionService.doAction({\n            name: title,\n            type: \"ir.actions.act_window\",\n            res_model: \"stock.move\",\n            domain,\n            views: [[false, 'list'], [false, 'form']],\n            target: 'current',\n        });\n    }\n\n    openInventoryLoss() {\n        return this.openStockMoveView(_t(\"Inventory Loss\"), \"inventory\");\n    }\n\n    openStockReport() {\n        const additionalContext = {};\n        if (this.controller.dateAsString) {\n            additionalContext.to_date = this.controller.dateAsString;\n        }\n        return this.actionService.doAction(\n            \"stock.action_product_stock_view\",\n            { additionalContext }\n        );\n    }\n\n    toggleInventoryValuationFold() {\n        this.state.displayInventoryValuationLine = !this.state.displayInventoryValuationLine;\n    }\n}\n\nregistry.category(\"actions\").add(\"stock_valuation_report\", StockValuationReport);\n", "import { ProductCatalogKanbanRecord } from \"@product/product_catalog/kanban_record\";\nimport { ProductCatalogSaleOrderLine } from \"./sale_order_line/sale_order_line\";\nimport { patch } from \"@web/core/utils/patch\";\n\npatch(ProductCatalogKanbanRecord.prototype, {\n    updateQuantity(quantity) {\n        if (this.env.orderResModel !== \"sale.order\" || this.productCatalogData.productType == \"service\") {\n            super.updateQuantity(...arguments);\n        } else if (\n            this.productCatalogData.quantity === this.productCatalogData.deliveredQty &&\n            quantity < this.productCatalogData.quantity\n        ) {\n            // This condition is only triggered when the product was already at the minimum quantity\n            // possible, as stated in the sale_stock module, then the user inputs a quantity lower\n            // than this limit, in this case we need the record to forcefully update the record.\n            this.props.record.load();\n            this.props.record.model.notify();\n        } else {\n            super.updateQuantity(Math.max(quantity, this.productCatalogData.deliveredQty));\n        }\n    },\n\n    get orderLineComponent() {\n        if (this.env.orderResModel === \"sale.order\") {\n            return ProductCatalogSaleOrderLine;\n        }\n        return super.orderLineComponent;\n    },\n});\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { ProductCatalogOrderLine } from \"@product/product_catalog/order_line/order_line\";\n\nexport class ProductCatalogSaleOrderLine extends ProductCatalogOrderLine {\n    static props = {\n        ...ProductCatalogOrderLine.props,\n        deliveredQty: Number,\n    }\n\n    get disableRemove() {\n        return this.props.quantity === this.props.deliveredQty;\n    }\n\n    get disabledButtonTooltip() {\n        if (this.disableRemove) {\n            return _t(\"The ordered quantity cannot be decreased below the amount already delivered. Instead, create a return in your inventory.\");\n        }\n        return super.disabledButtonTooltip;\n    }\n}\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { patch } from \"@web/core/utils/patch\";\n\nimport { StockValuationReport } from \"@stock_account/stock_valuation/stock_valuation_report\";\n\n\npatch(StockValuationReport.prototype, {\n    get accrual() {\n        const accrual = super.accrual;\n        const notInvoicedDeliveredGoods = this.data.not_invoiced_delivered_goods;\n        notInvoicedDeliveredGoods.display_name = _t(\"Goods Delivered Not Invoiced\");\n        notInvoicedDeliveredGoods.method = this.openSaleOrder.bind(this);\n        notInvoicedDeliveredGoods.index = accrual.lines.length;\n        accrual.value += notInvoicedDeliveredGoods.value;\n        accrual.lines.push(notInvoicedDeliveredGoods);\n        this.saleOrderIds = notInvoicedDeliveredGoods.lines.map((line) => line.id);\n        return accrual;\n    },\n\n    // On Click Methods --------------------------------------------------------\n    openSaleOrder(line=false) {\n        const action = {\n            type: \"ir.actions.act_window\",\n            name: _t(\"Sale Orders\"),\n            res_model: \"sale.order\",\n            views: [[false, \"list\"], [false, \"form\"]],\n            target: \"current\",\n        }\n        if (line?.id) {\n            action.views = [[false, \"form\"]],\n            action.res_id = line.id;\n        } else {\n            action.domain = [[\"id\", \"in\", this.saleOrderIds]];\n        }\n        return this.actionService.doAction(action);\n    },\n});\n", "import { formatDateTime } from \"@web/core/l10n/dates\";\nimport { localization } from \"@web/core/l10n/localization\";\nimport { registry } from \"@web/core/registry\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { usePopover } from \"@web/core/popover/popover_hook\";\nimport { Component, onWillRender } from \"@odoo/owl\";\nimport { standardWidgetProps } from \"@web/views/widgets/standard_widget_props\";\nimport { roundPrecision } from \"@web/core/utils/numbers\";\nimport { _t } from \"@web/core/l10n/translation\";\n\nexport class QtyAtDatePopover extends Component {\n    static template = \"sale_stock.QtyAtDatePopover\";\n    static props = {\n        record: Object,\n        calcData: Object,\n        close: Function,\n    };\n    setup() {\n        this.actionService = useService(\"action\");\n    }\n\n    openForecast() {\n        this.actionService.doAction(\"stock.stock_forecasted_product_product_action\", {\n            additionalContext: {\n                active_model: 'product.product',\n                active_id: this.props.record.data.product_id.id,\n                warehouse_id: this.props.record.data.warehouse_id && this.props.record.data.warehouse_id.id,\n                move_to_match_ids: this.props.record.data.move_ids.currentIds,\n                sale_line_to_match_id: this.props.record.resId,\n            },\n        });\n    }\n\n    get forecastedLabel() {\n        return _t('Forecasted Stock')\n    }\n\n    get availableLabel() {\n        return _t('Available')\n    }\n}\n\nexport class QtyAtDateWidget extends Component {\n    static components = { Popover: QtyAtDatePopover };\n    static template = \"sale_stock.QtyAtDate\";\n    static props = {...standardWidgetProps};\n    setup() {\n        this.popover = usePopover(this.constructor.components.Popover, { position: \"top\" });\n        this.orm = useService(\"orm\");\n        this.calcData = {};\n        onWillRender(() => {\n            this.initCalcData();\n        });\n    }\n\n    initCalcData() {\n        // calculate data not in record\n        const { data } = this.props.record;\n        if (data.scheduled_date) {\n            // TODO: might need some round_decimals to avoid errors\n            if (data.state === 'sale') {\n                this.calcData.will_be_fulfilled = data.free_qty_today >= data.qty_to_deliver;\n            } else {\n                this.calcData.will_be_fulfilled = data.virtual_available_at_date >= data.qty_to_deliver;\n            }\n            this.calcData.will_be_late = data.forecast_expected_date && data.forecast_expected_date > data.scheduled_date;\n            if (['draft', 'sent'].includes(data.state)) {\n                // Moves aren't created yet, then the forecasted is only based on virtual_available of quant\n                this.calcData.forecasted_issue = !this.calcData.will_be_fulfilled && !data.is_mto;\n            } else {\n                // Moves are created, using the forecasted data of related moves\n                this.calcData.forecasted_issue = !this.calcData.will_be_fulfilled || this.calcData.will_be_late;\n            }\n        }\n    }\n\n    async calcDataForDisplay() {\n        const { data } = this.props.record;\n        let lineUom;\n        if (data.product_uom_id?.[0]) {\n            lineUom = (await this.orm.read(\"uom.uom\", [data.product_uom_id[0]], [\"factor\", \"rounding\"]))[0];\n        }\n        let lineProduct;\n        if (data.product_id?.[0]) {\n            lineProduct = await this.orm.searchRead(\"product.product\", [[\"id\", \"=\", data.product_id[0]]], [\"uom_id\"]);\n        }\n        let productUom;\n        if (lineProduct?.[0]?.uom_id?.[0]) {\n            productUom = (await this.orm.searchRead(\"uom.uom\", [[\"id\", \"=\", lineProduct[0].uom_id[0]]], [\"factor\", \"name\"]))[0];\n        }\n        if (lineUom && productUom) {\n            this.calcData.product_uom_virtual_available_at_date = roundPrecision(data.virtual_available_at_date * lineUom.factor / productUom.factor, 1);\n            this.calcData.product_uom_free_qty_today = roundPrecision(data.free_qty_today * lineUom.factor / productUom.factor, 1);\n            this.calcData.product_uom_name = productUom.name;\n        }\n    }\n\n    updateCalcData() {\n        // popup specific data\n        const { data } = this.props.record;\n        if (!data.scheduled_date) {\n            return;\n        }\n        this.calcData.delivery_date = formatDateTime(data.scheduled_date, { format: localization.dateFormat });\n        if (data.forecast_expected_date) {\n            this.calcData.forecast_expected_date_str = formatDateTime(data.forecast_expected_date, { format: localization.dateFormat });\n        }\n    }\n\n    async showPopup(ev) {\n        const target = ev.currentTarget;\n        await this.calcDataForDisplay();\n        this.updateCalcData();\n        this.popover.open(target, {\n            record: this.props.record,\n            calcData: this.calcData,\n        });\n    }\n}\n\nexport const qtyAtDateWidget = {\n    component: QtyAtDateWidget,\n    fieldDependencies: [\n        { name: 'display_qty_widget', type: 'boolean'},\n        { name: 'free_qty_today', type: 'float'},\n        { name: 'forecast_expected_date', type: 'datetime'},\n        { name: 'is_mto', type: 'boolean'},\n        { name: 'move_ids', type: 'one2many'},\n        { name: 'qty_available_today', type: 'float'},\n        { name: 'qty_to_deliver', type: 'float'},\n        { name: 'scheduled_date', type: 'datetime'},\n        { name: 'virtual_available_at_date', type: 'float'},\n        { name: 'warehouse_id', type: 'many2one'},\n    ],\n};\nregistry.category(\"view_widgets\").add(\"qty_at_date_widget\", qtyAtDateWidget);\n", "import { ProductCatalogKanbanController } from \"@product/product_catalog/kanban_controller\";\nimport { patch } from \"@web/core/utils/patch\";\nimport { _t } from \"@web/core/l10n/translation\";\n\npatch(ProductCatalogKanbanController.prototype, {\n    _defineButtonContent() {\n        if (this.orderResModel === \"repair.order\") {\n            this.buttonString = _t(\"Back to Repair\");\n        } else {\n            super._defineButtonContent();\n        }\n    },\n});\n", "import { ProductCatalogOrderLine } from \"@product/product_catalog/order_line/order_line\";\nimport { patch } from \"@web/core/utils/patch\";\n\npatch(ProductCatalogOrderLine.prototype, {\n    get showPrice() {\n        return super.showPrice && this.env.orderResModel !== \"repair.order\";\n    }\n});\n", "import { BurgerMenu } from \"@web/webclient/burger_menu/burger_menu\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { registry } from \"@web/core/registry\";\n\nexport class EnterpriseBurgerMenu extends BurgerMenu {\n    setup() {\n        super.setup();\n        this.hm = useService(\"home_menu\");\n    }\n\n    get currentApp() {\n        return !this.hm.hasHomeMenu && super.currentApp;\n    }\n}\n\nconst systrayItem = {\n    Component: EnterpriseBurgerMenu,\n};\n\nregistry.category(\"systray\").add(\"burger_menu\", systrayItem, { sequence: 0, force: true });\n", "import { registry } from \"@web/core/registry\";\nimport { browser } from \"@web/core/browser/browser\";\nimport { cookie } from \"@web/core/browser/cookie\";\nimport { user } from \"@web/core/user\";\n\nconst serviceRegistry = registry.category(\"services\");\n\nexport function systemColorScheme() {\n    return browser.matchMedia(\"(prefers-color-scheme:dark)\").matches ? \"dark\" : \"light\";\n}\n\nexport function currentColorScheme() {\n    return cookie.get(\"color_scheme\");\n}\n\nexport const blockingWebClient = Promise.withResolvers();\n\nexport const colorSchemeService = {\n    async start() {\n        let newColorScheme = systemColorScheme();\n        if ([\"light\", \"dark\"].includes(user.settings.color_scheme)) {\n            newColorScheme = user.settings.color_scheme;\n        }\n        const current = currentColorScheme();\n        if (newColorScheme !== current) {\n            cookie.set(\"color_scheme\", newColorScheme);\n            if (current || (!current && newColorScheme === \"dark\")) {\n                this.reload();\n                await blockingWebClient.promise; // block WebClient rendering to avoid flickering\n            }\n        }\n        return {\n            get systemColorScheme() {\n                return systemColorScheme();\n            },\n            get currentColorScheme() {\n                return currentColorScheme();\n            },\n            get userColorScheme() {\n                return user.settings.color_scheme;\n            },\n        };\n    },\n    reload() {\n        browser.location.reload();\n    },\n};\nserviceRegistry.add(\"color_scheme\", colorSchemeService);\n", "import { registry } from \"@web/core/registry\";\nimport { session } from \"@web/session\";\nimport { browser } from \"@web/core/browser/browser\";\nimport { deserializeDateTime, serializeDate, formatDate } from \"@web/core/l10n/dates\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { ExpirationPanel } from \"./expiration_panel\";\nimport { cookie } from \"@web/core/browser/cookie\";\nimport { rpc } from \"@web/core/network/rpc\";\n\nconst { DateTime } = luxon;\nimport { Component, reactive, xml } from \"@odoo/owl\";\n\nfunction daysUntil(datetime) {\n    const duration = datetime.diff(DateTime.utc(), \"days\");\n    return Math.round(duration.values.days);\n}\n\nexport class SubscriptionManager {\n    constructor(env, { orm, notification }) {\n        this.env = env;\n        this.orm = orm;\n        this.notification = notification;\n        if (session.expiration_date) {\n            this.expirationDate = deserializeDateTime(session.expiration_date);\n        } else {\n            // If no date found, assume 1 month and hope for the best\n            this.expirationDate = DateTime.utc().plus({ days: 30 });\n        }\n        this.expirationReason = session.expiration_reason;\n        // Hack: we need to know if there is at least one app installed (except from App and\n        // Settings). We use mail to do that, as it is a dependency of almost every addon. To\n        // determine whether mail is installed or not, we check for the presence of the key\n        // \"storeData\" in session_info, as it is added in mail.\n        this.hasInstalledApps = \"storeData\" in session;\n        // \"user\" or \"admin\"\n        this.warningType = session.warning;\n        this.lastRequestStatus = null;\n        this.isWarningHidden = cookie.get(\"oe_instance_hide_panel\");\n    }\n\n    get formattedExpirationDate() {\n        return formatDate(this.expirationDate, { format: \"DDD\" });\n    }\n\n    get daysLeft() {\n        return daysUntil(this.expirationDate);\n    }\n\n    get unregistered() {\n        return [\"trial\", \"demo\", false].includes(this.expirationReason);\n    }\n\n    hideWarning() {\n        // Hide warning for 24 hours.\n        cookie.set(\"oe_instance_hide_panel\", true, 24 * 60 * 60);\n        this.isWarningHidden = true;\n    }\n\n    async buy() {\n        const limitDate = serializeDate(DateTime.utc().minus({ days: 15 }));\n        const args = [\n            [\n                [\"share\", \"=\", false],\n                [\"login_date\", \">=\", limitDate],\n            ],\n        ];\n        const nbUsers = await this.orm.call(\"res.users\", \"search_count\", args);\n        browser.location = `https://www.odoo.com/odoo-enterprise/upgrade?num_users=${nbUsers}`;\n    }\n    /**\n     * Save the registration code then triggers a ping to submit it.\n     */\n    async submitCode(enterpriseCode) {\n        const [oldDate] = await Promise.all([\n            this.orm.call(\"ir.config_parameter\", \"get_param\", [\"database.expiration_date\"]),\n            this.orm.call(\"ir.config_parameter\", \"set_param\", [\n                \"database.enterprise_code\",\n                enterpriseCode,\n            ]),\n        ]);\n\n        await this.orm.call(\"publisher_warranty.contract\", \"update_notification\", [[]]);\n\n        const [linkedSubscriptionUrl, linkedEmail, expirationDate] = await Promise.all([\n            this.orm.call(\"ir.config_parameter\", \"get_param\", [\n                \"database.already_linked_subscription_url\",\n            ]),\n            this.orm.call(\"ir.config_parameter\", \"get_param\", [\"database.already_linked_email\"]),\n            this.orm.call(\"ir.config_parameter\", \"get_param\", [\"database.expiration_date\"]),\n        ]);\n\n        if (linkedSubscriptionUrl) {\n            this.lastRequestStatus = \"link\";\n            this.linkedSubscriptionUrl = linkedSubscriptionUrl;\n            this.mailDeliveryStatus = null;\n            this.linkedEmail = linkedEmail;\n        } else if (expirationDate !== oldDate) {\n            this.lastRequestStatus = \"success\";\n            this.expirationDate = deserializeDateTime(expirationDate);\n            if (this.daysLeft > 30) {\n                this.notification.add(\n                    _t(\n                        \"Thank you, your registration was successful! Your database is valid until %s.\",\n                        this.formattedExpirationDate\n                    ),\n                    { type: \"success\" }\n                );\n            }\n        } else {\n            this.lastRequestStatus = \"error\";\n        }\n    }\n\n    async checkStatus() {\n        await this.orm.call(\"publisher_warranty.contract\", \"update_notification\", [[]]);\n\n        const expirationDateStr = await this.orm.call(\"ir.config_parameter\", \"get_param\", [\n            \"database.expiration_date\",\n        ]);\n        this.lastRequestStatus = \"update\";\n        this.expirationDate = deserializeDateTime(expirationDateStr);\n    }\n\n    async sendUnlinkEmail() {\n        const sendUnlinkInstructionsUrl = await this.orm.call(\"ir.config_parameter\", \"get_param\", [\n            \"database.already_linked_send_mail_url\",\n        ]);\n        this.mailDeliveryStatus = \"ongoing\";\n        const { result, reason } = await rpc(sendUnlinkInstructionsUrl);\n        if (result) {\n            this.mailDeliveryStatus = \"success\";\n        } else {\n            this.mailDeliveryStatus = \"fail\";\n            this.mailDeliveryStatusError = reason;\n        }\n    }\n\n    async renew() {\n        const enterpriseCode = await this.orm.call(\"ir.config_parameter\", \"get_param\", [\n            \"database.enterprise_code\",\n        ]);\n\n        const url = \"https://www.odoo.com/odoo-enterprise/renew\";\n        const contractQueryString = enterpriseCode ? `?contract=${enterpriseCode}` : \"\";\n        browser.location = `${url}${contractQueryString}`;\n    }\n\n    async upsell() {\n        const limitDate = serializeDate(DateTime.utc().minus({ days: 15 }));\n        const [enterpriseCode, nbUsers] = await Promise.all([\n            this.orm.call(\"ir.config_parameter\", \"get_param\", [\"database.enterprise_code\"]),\n            this.orm.call(\"res.users\", \"search_count\", [\n                [\n                    [\"share\", \"=\", false],\n                    [\"login_date\", \">=\", limitDate],\n                ],\n            ]),\n        ]);\n        const url = \"https://www.odoo.com/odoo-enterprise/upsell\";\n        const contractQueryString = enterpriseCode ? `&contract=${enterpriseCode}` : \"\";\n        browser.location = `${url}?num_users=${nbUsers}${contractQueryString}`;\n    }\n}\n\nclass ExpiredSubscriptionBlockUI extends Component {\n    static props = {};\n    // TODO the \"o_blockUI\" div in there seems useless (it has 0 height and thus displays and does nothing)\n    static template = xml`\n        <t t-if=\"subscription.daysLeft &lt;= 0\">\n            <div class=\"o_blockUI\"/>\n            <div style=\"position: absolute; top: 0; left: 0; right: 0; bottom: 0; z-index: 1100\" class=\"d-flex align-items-center justify-content-center\">\n                <ExpirationPanel/>\n            </div>\n        </t>`;\n    static components = { ExpirationPanel };\n    setup() {\n        this.subscription = useService(\"enterprise_subscription\");\n    }\n}\n\nexport const enterpriseSubscriptionService = {\n    name: \"enterprise_subscription\",\n    dependencies: [\"orm\", \"notification\"],\n    start(env, { orm, notification }) {\n        registry\n            .category(\"main_components\")\n            .add(\"expired_subscription_block_ui\", { Component: ExpiredSubscriptionBlockUI });\n        return reactive(new SubscriptionManager(env, { orm, notification }));\n    },\n};\n\nregistry.category(\"services\").add(\"enterprise_subscription\", enterpriseSubscriptionService);\n", "import { useService } from \"@web/core/utils/hooks\";\nimport { Transition } from \"@web/core/transition\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { Component, useState, useRef } from \"@odoo/owl\";\n\nconst { DateTime } = luxon;\n\n/**\n * Expiration panel\n *\n * Component representing the banner located on top of the home menu. Its purpose\n * is to display the expiration state of the current database and to help the\n * user to buy/renew its subscription.\n * @extends Component\n */\nexport class ExpirationPanel extends Component {\n    static template = \"DatabaseExpirationPanel\";\n    static props = {};\n    static components = { Transition };\n\n    setup() {\n        this.subscription = useService(\"enterprise_subscription\");\n\n        this.state = useState({\n            displayRegisterForm: false,\n        });\n\n        this.inputRef = useRef(\"input\");\n    }\n\n    get buttonText() {\n        return this.subscription.lastRequestStatus === \"error\" ? _t(\"Retry\") : _t(\"Register\");\n    }\n\n    get alertType() {\n        if (this.subscription.lastRequestStatus === \"success\") {\n            return \"success\";\n        }\n        const { daysLeft } = this.subscription;\n        if (daysLeft <= 6) {\n            return \"danger\";\n        } else if (daysLeft <= 16) {\n            return \"warning\";\n        }\n        return \"info\";\n    }\n\n    get expirationMessage() {\n        const { daysLeft } = this.subscription;\n        if (daysLeft <= 0) {\n            return _t(\"This database has expired. \");\n        }\n        const delay = daysLeft === 30 ? _t(\"1 month\") : _t(\"%s days\", daysLeft);\n        if (this.subscription.expirationReason === \"demo\") {\n            return _t(\"This demo database will expire in %s. \", delay);\n        }\n\n        const expirationDate = this.subscription.expirationDate;\n        const today = DateTime.now();\n        const diff = expirationDate.diff(today);\n\n        if (this.subscription.expirationReason !== 'renewal') {\n            return _t(\"This database will expire in %s. \", delay);\n        } else {\n            if (daysLeft > 15) {\n                return _t(\n                    \"Your subscription expires in %s days. \",\n                    daysLeft - 15\n                );\n            } else {\n                return _t(\n                    \"Your subscription expired %s days ago. This database will be blocked soon. \",\n                    (diff.as(\"days\") | 0)\n                );\n            }\n        }\n    }\n\n    showRegistrationForm() {\n        this.state.displayRegisterForm = !this.state.displayRegisterForm;\n    }\n\n    async onCodeSubmit() {\n        const enterpriseCode = this.inputRef.el.value;\n        if (!enterpriseCode) {\n            return;\n        }\n        await this.subscription.submitCode(enterpriseCode);\n        if (this.subscription.lastRequestStatus === \"success\") {\n            this.state.displayRegisterForm = false;\n        } else {\n            this.state.buttonText = _t(\"Retry\");\n        }\n    }\n}\n", "import { hasTouch, isIosApp, isMacOS } from \"@web/core/browser/feature_detection\";\nimport { useHotkey } from \"@web/core/hotkeys/hotkey_hook\";\nimport { user } from \"@web/core/user\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { ExpirationPanel } from \"./expiration_panel\";\nimport { useSortable } from \"@web/core/utils/sortable_owl\";\n\nimport {\n    Component,\n    useExternalListener,\n    onMounted,\n    onPatched,\n    onWillUpdateProps,\n    useState,\n    useRef,\n} from \"@odoo/owl\";\n\nclass FooterComponent extends Component {\n    static template = \"web_enterprise.HomeMenu.CommandPalette.Footer\";\n    static props = {\n        //prop added by the command palette\n        switchNamespace: { type: Function, optional: true },\n    };\n\n    setup() {\n        this.controlKey = isMacOS() ? \"COMMAND\" : \"CONTROL\";\n    }\n}\n/**\n * Home menu\n *\n * This component handles the display and navigation between the different\n * available applications and menus.\n * @extends Component\n */\nexport class HomeMenu extends Component {\n    static template = \"web_enterprise.HomeMenu\";\n    static components = { ExpirationPanel };\n    static props = {\n        apps: {\n            type: Array,\n            element: {\n                type: Object,\n                shape: {\n                    actionID: Number,\n                    href: String,\n                    appID: Number,\n                    id: Number,\n                    label: String,\n                    parents: String,\n                    webIcon: {\n                        type: [\n                            Boolean,\n                            String,\n                            {\n                                type: Object,\n                                optional: 1,\n                                shape: {\n                                    iconClass: String,\n                                    color: String,\n                                    backgroundColor: String,\n                                },\n                            },\n                        ],\n                        optional: true,\n                    },\n                    webIconData: { type: String, optional: 1 },\n                    xmlid: String,\n                },\n            },\n        },\n        reorderApps: { type: Function },\n    };\n\n    /**\n     * @param {Object} props\n     * @param {Object[]} props.apps application icons\n     * @param {number} props.apps[].actionID\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     * @param {function} props.reorderApps\n     */\n    setup() {\n        this.command = useService(\"command\");\n        this.menus = useService(\"menu\");\n        this.homeMenuService = useService(\"home_menu\");\n        this.subscription = useService(\"enterprise_subscription\");\n        this.ui = useService(\"ui\");\n        this.state = useState({\n            focusedIndex: null,\n            isIosApp: isIosApp(),\n        });\n        this.inputRef = useRef(\"input\");\n        this.rootRef = useRef(\"root\");\n        this.pressTimer;\n\n        if (!this.env.isSmall) {\n            this._registerHotkeys();\n        }\n\n        useSortable({\n            enable: this._enableAppsSorting,\n            // Params\n            ref: this.rootRef,\n            elements: \".o_draggable\",\n            cursor: \"move\",\n            delay: 500,\n            tolerance: 10,\n            // Hooks\n            onWillStartDrag: (params) => this._sortStart(params),\n            onDrop: (params) => this._sortAppDrop(params),\n        });\n\n        onWillUpdateProps(() => {\n            // State is reset on each remount\n            this.state.focusedIndex = null;\n        });\n\n        onMounted(() => {\n            if (!hasTouch()) {\n                this._focusInput();\n            }\n        });\n\n        onPatched(() => {\n            if (this.state.focusedIndex !== null && !this.env.isSmall) {\n                const selectedItem = document.querySelector(\".o_home_menu .o_menuitem.o_focused\");\n                // When TAB is managed externally the class o_focused disappears.\n                if (selectedItem) {\n                    // Center window on the focused item\n                    selectedItem.scrollIntoView({ block: \"center\" });\n                }\n            }\n        });\n    }\n\n    //--------------------------------------------------------------------------\n    // Getters\n    //--------------------------------------------------------------------------\n\n    /**\n     * @returns {Object[]}\n     */\n    get displayedApps() {\n        return this.props.apps;\n    }\n\n    /**\n     * @returns {number}\n     */\n    get maxIconNumber() {\n        const w = window.innerWidth;\n        if (w < 576) {\n            return 3;\n        } else if (w < 768) {\n            return 4;\n        } else {\n            return 6;\n        }\n    }\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * @private\n     * @param {Object} menu\n     * @returns {Promise}\n     */\n    _openMenu(menu) {\n        return this.menus.selectMenu(menu);\n    }\n\n    /**\n     * Update this.state.focusedIndex if not null.\n     * @private\n     * @param {string} cmd\n     */\n    _updateFocusedIndex(cmd) {\n        const nbrApps = this.displayedApps.length;\n        const lastIndex = nbrApps - 1;\n        const focusedIndex = this.state.focusedIndex;\n        if (lastIndex < 0) {\n            return;\n        }\n        if (focusedIndex === null) {\n            this.state.focusedIndex = 0;\n            return;\n        }\n        const lineNumber = Math.ceil(nbrApps / this.maxIconNumber);\n        const currentLine = Math.ceil((focusedIndex + 1) / this.maxIconNumber);\n        let newIndex;\n        switch (cmd) {\n            case \"previousElem\":\n                newIndex = focusedIndex - 1;\n                break;\n            case \"nextElem\":\n                newIndex = focusedIndex + 1;\n                break;\n            case \"previousColumn\":\n                if (focusedIndex % this.maxIconNumber) {\n                    // app is not the first one on its line\n                    newIndex = focusedIndex - 1;\n                } else {\n                    newIndex =\n                        focusedIndex + Math.min(lastIndex - focusedIndex, this.maxIconNumber - 1);\n                }\n                break;\n            case \"nextColumn\":\n                if (focusedIndex === lastIndex || (focusedIndex + 1) % this.maxIconNumber === 0) {\n                    // app is the last one on its line\n                    newIndex = (currentLine - 1) * this.maxIconNumber;\n                } else {\n                    newIndex = focusedIndex + 1;\n                }\n                break;\n            case \"previousLine\":\n                if (currentLine === 1) {\n                    newIndex = focusedIndex + (lineNumber - 1) * this.maxIconNumber;\n                    if (newIndex > lastIndex) {\n                        newIndex = lastIndex;\n                    }\n                } else {\n                    // we go to the previous line on same column\n                    newIndex = focusedIndex - this.maxIconNumber;\n                }\n                break;\n            case \"nextLine\":\n                if (currentLine === lineNumber) {\n                    newIndex = focusedIndex % this.maxIconNumber;\n                } else {\n                    // we go to the next line on the closest column\n                    newIndex =\n                        focusedIndex + Math.min(this.maxIconNumber, lastIndex - focusedIndex);\n                }\n                break;\n        }\n        // if newIndex is out of bounds -> normalize it\n        if (newIndex < 0) {\n            newIndex = lastIndex;\n        } else if (newIndex > lastIndex) {\n            newIndex = 0;\n        }\n        this.state.focusedIndex = newIndex;\n    }\n\n    _focusInput() {\n        if (!this.env.isSmall && this.inputRef.el) {\n            this.inputRef.el.focus({ preventScroll: true });\n        }\n    }\n\n    _enableAppsSorting() {\n        return true;\n    }\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n     * @param {Object} params\n     * @param {HTMLElement} params.element\n     * @param {HTMLElement} params.previous\n     */\n    _sortAppDrop({ element, previous }) {\n        const order = this.props.apps.map((app) => app.xmlid);\n        const elementId = element.children[0].dataset.menuXmlid;\n        const elementIndex = order.indexOf(elementId);\n        // first remove dragged element\n        order.splice(elementIndex, 1);\n        if (previous) {\n            const prevIndex = order.indexOf(previous.children[0].dataset.menuXmlid);\n            // insert dragged element after previous element\n            order.splice(prevIndex + 1, 0, elementId);\n        } else {\n            // insert dragged element at beginning if no previous element\n            order.splice(0, 0, elementId);\n        }\n        // apply new order\n        this.props.reorderApps(order);\n        user.setUserSettings(\"homemenu_config\", JSON.stringify(order));\n    }\n\n    /**\n     * @param {Object} params\n     * @param {HTMLElement} params.element\n     */\n    _sortStart({ element, addClass }) {\n        addClass(element.children[0], \"o_dragged_app\");\n    }\n\n    /**\n     * @private\n     * @param {Object} app\n     */\n    _onAppClick(app) {\n        this._openMenu(app);\n    }\n\n    /**\n     * @private\n     */\n    _registerHotkeys() {\n        const hotkeys = [\n            [\"ArrowDown\", () => this._updateFocusedIndex(\"nextLine\")],\n            [\"ArrowRight\", () => this._updateFocusedIndex(\"nextColumn\")],\n            [\"ArrowUp\", () => this._updateFocusedIndex(\"previousLine\")],\n            [\"ArrowLeft\", () => this._updateFocusedIndex(\"previousColumn\")],\n            [\"Tab\", () => this._updateFocusedIndex(\"nextElem\")],\n            [\"shift+Tab\", () => this._updateFocusedIndex(\"previousElem\")],\n            [\n                \"Enter\",\n                () => {\n                    const menu = this.displayedApps[this.state.focusedIndex];\n                    if (menu) {\n                        this._openMenu(menu);\n                    }\n                },\n            ],\n            [\"Escape\", () => this.homeMenuService.toggle(false)],\n        ];\n        hotkeys.forEach((hotkey) => {\n            useHotkey(...hotkey, {\n                allowRepeat: true,\n            });\n        });\n        useExternalListener(window, \"keydown\", this._onKeydownFocusInput);\n    }\n\n    _onKeydownFocusInput() {\n        if (\n            document.activeElement !== this.inputRef.el &&\n            this.ui.activeElement === document &&\n            ![\"TEXTAREA\", \"INPUT\"].includes(document.activeElement.tagName)\n        ) {\n            this._focusInput();\n        }\n    }\n\n    _onInputSearch() {\n        const onClose = () => {\n            this._focusInput();\n            if (this.inputRef.el) {\n                this.inputRef.el.value = \"\";\n            }\n        };\n        const searchValue = this.compositionStart ? \"/\" : `/${this.inputRef.el.value.trim()}`;\n        this.compositionStart = false;\n        this.command.openMainPalette({ searchValue, FooterComponent }, onClose);\n    }\n\n    _onInputBlur() {\n        if (hasTouch()) {\n            return;\n        }\n        // if we blur search input to focus on body (eg. click on any\n        // non-interactive element) restore focus to avoid IME input issue\n        setTimeout(() => {\n            if (document.activeElement === document.body && this.ui.activeElement === document) {\n                this._focusInput();\n            }\n        }, 0);\n    }\n\n    _onCompositionStart() {\n        this.compositionStart = true;\n    }\n}\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { user } from \"@web/core/user\";\nimport { Mutex } from \"@web/core/utils/concurrency\";\nimport { useBus, useService } from \"@web/core/utils/hooks\";\nimport { computeAppsAndMenuItems, reorderApps } from \"@web/webclient/menus/menu_helpers\";\nimport {\n    ControllerNotFoundError,\n    standardActionServiceProps,\n} from \"@web/webclient/actions/action_service\";\nimport { HomeMenu } from \"./home_menu\";\n\nimport { Component, onMounted, onWillUnmount, reactive, xml } from \"@odoo/owl\";\n\nexport const homeMenuService = {\n    dependencies: [\"action\"],\n    start(env) {\n        const state = reactive({\n            hasHomeMenu: false, // true iff the HomeMenu is currently displayed\n            hasBackgroundAction: false, // true iff there is an action behind the HomeMenu\n            toggle,\n        });\n        const mutex = new Mutex(); // used to protect against concurrent toggling requests\n        class HomeMenuAction extends Component {\n            static components = { HomeMenu };\n            static target = \"current\";\n            static props = { ...standardActionServiceProps };\n            static template = xml`<HomeMenu t-props=\"homeMenuProps\"/>`;\n            static displayName = _t(\"Home\");\n\n            setup() {\n                this.menus = useService(\"menu\");\n                onMounted(() => this.onMounted());\n                onWillUnmount(this.onWillUnmount);\n                useBus(this.env.bus, \"MENUS:APP-CHANGED\", () => this.render());\n            }\n            get homeMenuProps() {\n                const homemenuConfig = JSON.parse(user.settings?.homemenu_config || \"null\");\n                const apps = reactive(\n                    computeAppsAndMenuItems(this.menus.getMenuAsTree(\"root\")).apps\n                );\n                if (homemenuConfig) {\n                    reorderApps(apps, homemenuConfig);\n                }\n                return {\n                    apps,\n                    reorderApps: (order) => reorderApps(apps, order),\n                };\n            }\n            async onMounted() {\n                const { breadcrumbs } = this.env.config;\n                state.hasHomeMenu = true;\n                state.hasBackgroundAction = breadcrumbs.length > 0;\n                this.env.bus.trigger(\"HOME-MENU:TOGGLED\");\n            }\n            onWillUnmount() {\n                state.hasHomeMenu = false;\n                state.hasBackgroundAction = false;\n                this.env.bus.trigger(\"HOME-MENU:TOGGLED\");\n            }\n        }\n\n        registry.category(\"actions\").add(\"menu\", HomeMenuAction);\n\n        env.bus.addEventListener(\"HOME-MENU:TOGGLED\", () => {\n            document.body.classList.toggle(\"o_home_menu_background\", state.hasHomeMenu);\n        });\n\n        async function toggle(show) {\n            return mutex.exec(async () => {\n                show = show === undefined ? !state.hasHomeMenu : Boolean(show);\n                if (show !== state.hasHomeMenu) {\n                    if (show) {\n                        await env.services.action.doAction(\"menu\");\n                    } else {\n                        try {\n                            await env.services.action.restore();\n                        } catch (err) {\n                            if (!(err instanceof ControllerNotFoundError)) {\n                                throw err;\n                            }\n                        }\n                    }\n                }\n                // hack: wait for a tick to ensure that the url has been updated before\n                // switching again\n                return new Promise((r) => setTimeout(r));\n            });\n        }\n\n        return state;\n    },\n};\n\nregistry.category(\"services\").add(\"home_menu\", homeMenuService);\n", "import { NavBar } from \"@web/webclient/navbar/navbar\";\nimport { useService, useBus } from \"@web/core/utils/hooks\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { useEffect, useRef } from \"@odoo/owl\";\n\nexport class EnterpriseNavBar extends NavBar {\n    static template = \"web_enterprise.EnterpriseNavBar\";\n    setup() {\n        super.setup();\n        this.hm = useService(\"home_menu\");\n        this.pwa = useService(\"pwa\");\n        this.menuAppsRef = useRef(\"menuApps\");\n        this.navRef = useRef(\"nav\");\n        this._busToggledCallback = () => this._updateMenuAppsIcon();\n        useBus(this.env.bus, \"HOME-MENU:TOGGLED\", this._busToggledCallback);\n        useEffect(() => this._updateMenuAppsIcon());\n    }\n    get hasBackgroundAction() {\n        return this.hm.hasBackgroundAction;\n    }\n    get isInApp() {\n        return !this.hm.hasHomeMenu;\n    }\n\n    _openAppMenuSidebar() {\n        if (this.hm.hasHomeMenu) {\n            this.hm.toggle(false);\n        } else {\n            this.state.isAppMenuSidebarOpened = true;\n        }\n    }\n    _updateMenuAppsIcon() {\n        const menuAppsEl = this.menuAppsRef.el;\n        menuAppsEl.classList.toggle(\"o_hidden\", !this.isInApp && !this.hasBackgroundAction);\n        menuAppsEl.classList.toggle(\n            \"o_menu_toggle_back\",\n            !this.isInApp && this.hasBackgroundAction\n        );\n        if (!this.isScopedApp) {\n            const title =\n                !this.isInApp && this.hasBackgroundAction ? _t(\"Previous view\") : _t(\"Home menu\");\n            menuAppsEl.title = title;\n            menuAppsEl.ariaLabel = title;\n        }\n\n        const menuBrand = this.navRef.el.querySelector(\".o_menu_brand\");\n        if (menuBrand) {\n            menuBrand.classList.toggle(\"o_hidden\", !this.isInApp);\n        }\n\n        const menuBrandIcon = this.navRef.el.querySelector(\".o_menu_brand_icon\");\n        if (menuBrandIcon) {\n            menuBrandIcon.classList.toggle(\"o_hidden\", !this.isInApp);\n        }\n\n        const appSubMenus = this.appSubMenus.el;\n        if (appSubMenus) {\n            appSubMenus.classList.toggle(\"o_hidden\", !this.isInApp);\n        }\n\n        const breadcrumb = this.navRef.el.querySelector(\".o_breadcrumb\");\n        if (breadcrumb) {\n            breadcrumb.classList.toggle(\"o_hidden\", !this.isInApp);\n        }\n    }\n\n    /**\n     * @override\n     */\n    onAllAppsBtnClick() {\n        super.onAllAppsBtnClick();\n        this.hm.toggle(true);\n        this._closeAppMenuSidebar();\n    }\n}\n", "import { browser } from \"@web/core/browser/browser\";\nimport { Dialog } from \"@web/core/dialog/dialog\";\nimport { useChildRef, useService } from \"@web/core/utils/hooks\";\n\nimport { Component, useExternalListener } from \"@odoo/owl\";\n\nexport class PromoteStudioDialog extends Component {\n    static template = \"web_enterprise.PromoteStudioDialog\";\n    static components = { Dialog };\n    static props = {\n        title: String,\n        close: Function,\n    };\n\n    setup() {\n        this.ormService = useService(\"orm\");\n        this.uiService = useService(\"ui\");\n\n        this.modalRef = useChildRef();\n\n        useExternalListener(window, \"mousedown\", this.onWindowMouseDown);\n    }\n\n    async onClickInstallStudio() {\n        this.disableClick = true;\n        this.uiService.block();\n        const modules = await this.ormService.searchRead(\n            \"ir.module.module\",\n            [[\"name\", \"=\", \"web_studio\"]],\n            [\"id\"]\n        );\n        await this.ormService.call(\"ir.module.module\", \"button_immediate_install\", [\n            [modules[0].id],\n        ]);\n        // on rpc call return, the framework unblocks the page\n        // make sure to keep the page blocked until the reload ends.\n        this.uiService.unblock();\n        browser.localStorage.setItem(\"openStudioOnReload\", \"main\");\n        browser.location.reload();\n    }\n\n    /**\n     * Close the dialog on outside click.\n     */\n    onWindowMouseDown(ev) {\n        const dialogContent = this.modalRef.el.querySelector(\".modal-content\");\n        if (!this.disableClick && !dialogContent.contains(ev.target)) {\n            this.props.close();\n        }\n    }\n}\n\nexport class PromoteStudioAutomationDialog extends PromoteStudioDialog {\n    static template = \"web_enterprise.PromoteStudioAutomationDialog\";\n}\n", "import { registry } from \"@web/core/registry\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { user } from \"@web/core/user\";\nimport { PromoteStudioDialog } from \"@web_enterprise/webclient/promote_studio/promote_studio_dialog\";\nimport { _t } from \"@web/core/l10n/translation\";\n\nimport { Component } from \"@odoo/owl\";\n\nexport class PromoteStudioSystrayItem extends Component {\n    static template = \"web_enterprise.SystrayItem\";\n    static props = {};\n\n    setup() {\n        this.dialog = useService(\"dialog\");\n    }\n\n    _onClick() {\n        this.dialog.add(PromoteStudioDialog, {\n            title: _t(\"Odoo Studio - Add new fields to any view\"),\n        });\n    }\n}\n\nexport const promoteStudioSystrayItem = {\n    Component: PromoteStudioSystrayItem,\n    isDisplayed: () => user.isSystem,\n};\n\nregistry\n    .category(\"systray\")\n    .add(\"PromoteStudioSystrayItem\", promoteStudioSystrayItem, { sequence: 1 });\n", "import { isDisplayStandalone } from \"@web/core/browser/feature_detection\";\nimport { patch } from \"@web/core/utils/patch\";\nimport { BurgerMenu } from \"@web/webclient/burger_menu/burger_menu\";\nimport { shareUrl } from \"./share_url\";\n\nif (navigator.share && isDisplayStandalone()) {\n    patch(BurgerMenu.prototype, {\n        shareUrl,\n    });\n\n    patch(BurgerMenu, {\n        template: \"web_enterprise.BurgerMenu\",\n    });\n}\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { markup } from \"@odoo/owl\";\nimport { registry } from \"@web/core/registry\";\nimport { browser } from \"@web/core/browser/browser\";\nimport { isDisplayStandalone } from \"@web/core/browser/feature_detection\";\n\nexport async function shareUrl() {\n    await navigator\n        .share({\n            url: browser.location.href,\n            title: document.title,\n        })\n        .catch((e) => {\n            if (!(e instanceof DOMException && e.name === \"AbortError\")) {\n                throw e;\n            }\n        });\n}\n\nexport function shareUrlMenuItem(env) {\n    return {\n        type: \"item\",\n        hide: env.isSmall || !isDisplayStandalone(),\n        id: \"share_url\",\n        description: markup`\n            <div class=\"d-flex align-items-center justify-content-between\">\n                <span>${_t(\"Share\")}</span>\n                <span class=\"fa fa-share-alt\"></span>\n            </div>`,\n        callback: shareUrl,\n        sequence: 25,\n    };\n}\n\nif (navigator.share) {\n    registry.category(\"user_menuitems\").add(\"share_url\", shareUrlMenuItem);\n}\n", "import { WebClient } from \"@web/webclient/webclient\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { EnterpriseNavBar } from \"./navbar/navbar\";\n\nexport class WebClientEnterprise extends WebClient {\n    static components = {\n        ...WebClient.components,\n        NavBar: EnterpriseNavBar,\n    };\n    setup() {\n        super.setup();\n        this.hm = useService(\"home_menu\");\n    }\n    _loadDefaultApp() {\n        return this.hm.toggle(true);\n    }\n}\n", "import { isMobileOS } from \"@web/core/browser/feature_detection\";\nimport { user } from \"@web/core/user\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { patch } from \"@web/core/utils/patch\";\nimport { ListRenderer } from \"@web/views/list/list_renderer\";\nimport { PromoteStudioDialog } from \"@web_enterprise/webclient/promote_studio/promote_studio_dialog\";\nimport { _t } from \"@web/core/l10n/translation\";\n\nexport const patchListRendererDesktop = () => ({\n    setup() {\n        super.setup(...arguments);\n        this.actionService = useService(\"action\");\n        const list = this.props.list;\n\n        const { actionId, actionType, actionXmlId } = this.env.config || {};\n        const resModel = this.props.list.resModel;\n\n        // Start by determining if the current ListRenderer is in a context that would\n        // allow the edition of the arch by studio.\n        // It needs to be a full list view, in an action\n        // (not a X2Many list, and not an \"embedded\" list in another component)\n        // Also, there is not enough information when an action is in target new,\n        // and this use case is fairly outside of the feature's scope\n        const isPotentiallyEditable =\n            !isMobileOS() &&\n            !this.env.inDialog &&\n            user.isSystem &&\n            list === list.model.root &&\n            actionId &&\n            actionType === \"ir.actions.act_window\";\n\n        const computeStudioEditable = () => {\n            // Finalize the computation when the actionService is ready.\n            // The following code is copied from studioService.\n            if (!actionXmlId) {\n                return false;\n            }\n            if (\n                resModel.indexOf(\"settings\") > -1 &&\n                resModel.indexOf(\"x_\") !== 0\n            ) {\n                return false; // settings views aren't editable; but x_settings is\n            }\n            if (resModel === \"board.board\") {\n                return false; // dashboard isn't editable\n            }\n            if (resModel === \"knowledge.article\") {\n                // The knowledge form view is very specific and custom, it doesn't make sense\n                // to edit it. Editing the list and kanban is more debatable, but for simplicity's sake\n                // we set them to not editable too.\n                return false;\n            }\n            if (resModel === \"account.bank.statement.line\") {\n                return false; // bank reconciliation isn't editable\n            }\n            return Boolean(resModel);\n        };\n\n        this.studioEditable = isPotentiallyEditable && computeStudioEditable();\n    },\n\n    isStudioEditable() {\n        return this.studioEditable;\n    },\n\n    get displayOptionalFields() {\n        return this.isStudioEditable() || super.displayOptionalFields;\n    },\n\n    /**\n     * This function opens promote studio dialog\n     *\n     * @private\n     */\n    onSelectedAddCustomField() {\n        this.env.services.dialog.add(PromoteStudioDialog, {\n            title: _t(\"Odoo Studio - Add new fields to any view\"),\n        });\n    },\n});\n\nexport const unpatchListRendererDesktop = patch(ListRenderer.prototype, patchListRendererDesktop());\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { user } from \"@web/core/user\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { patch } from \"@web/core/utils/patch\";\nimport { GroupConfigMenu } from \"@web/views/view_components/group_config_menu\";\nimport { PromoteStudioAutomationDialog } from \"@web_enterprise/webclient/promote_studio/promote_studio_dialog\";\n\npatch(GroupConfigMenu.prototype, {\n    setup() {\n        super.setup();\n        this.orm = useService(\"orm\");\n    },\n    /**\n     * @override\n     */\n    get permissions() {\n        const permissions = super.permissions;\n        Object.defineProperty(permissions, \"canEditAutomations\", {\n            get: () => user.isAdmin,\n            configurable: true,\n        });\n        return permissions;\n    },\n\n    async openAutomations() {\n        if (typeof this._openAutomations === \"function\") {\n            // this is the case if base_automation is installed\n            return this._openAutomations();\n        } else {\n            this.env.services.dialog.add(PromoteStudioAutomationDialog, {\n                title: _t(\"Odoo Studio - Customize workflows in minutes\"),\n            });\n        }\n    },\n});\n\nregistry.category(\"group_config_items\").add(\n    \"open_automations\",\n    {\n        label: _t(\"Automations\"),\n        method: \"openAutomations\",\n        isVisible: ({ permissions }) => permissions.canEditAutomations,\n        class: \"o_column_automations\",\n        icon: \"fa-magic\",\n    },\n    { sequence: 25, force: true }\n);\n", "import ServiceCore from \"@web_mobile/js/services/core\";\n\nimport { onMounted, onPatched, onWillUnmount, useComponent } from \"@odoo/owl\";\n\n/**\n * This hook provides support for executing code when the back button is pressed\n * on the mobile application of Odoo. This actually replaces the default back\n * button behavior so this feature should only be enabled when it is actually\n * useful.\n *\n * The feature is either enabled on mount or, using the `shouldEnable` function\n * argument as condition, when the component is patched. In both cases,\n * the feature is automatically disabled on unmount.\n *\n * @param {function} func the function to execute when the back button is\n *  pressed. The function is called with the custom event as param.\n * @param {function} [shouldEnable] the function to execute when the DOM is\n *  patched to check if the backbutton should be enabled or disabled ;\n *  if undefined will be enabled on mount and disabled on unmount.\n */\nexport function useBackButton(func, shouldEnable) {\n    const component = useComponent();\n    let isEnabled = false;\n\n    /**\n     * Enables the func listener, overriding default back button behavior.\n     */\n    function enable() {\n        ServiceCore.backButtonManager.addListener(component, func);\n        isEnabled = true;\n    }\n\n    /**\n     * Disables the func listener, restoring the default back button behavior if\n     * no other listeners are present.\n     */\n    function disable() {\n        ServiceCore.backButtonManager.removeListener(component);\n        isEnabled = false;\n    }\n\n    onMounted(() => {\n        if (shouldEnable && !shouldEnable()) {\n            return;\n        }\n        enable();\n    });\n\n    onPatched(() => {\n        if (!shouldEnable) {\n            return;\n        }\n        const shouldBeEnabled = shouldEnable();\n        if (shouldBeEnabled && !isEnabled) {\n            enable();\n        } else if (!shouldBeEnabled && isEnabled) {\n            disable();\n        }\n    });\n\n    onWillUnmount(() => {\n        if (isEnabled) {\n            disable();\n        }\n    });\n}\n", "import { user } from \"@web/core/user\";\nimport mobile from \"@web_mobile/js/services/core\";\nimport { isIosApp } from \"@web/core/browser/feature_detection\";\nimport { url } from \"@web/core/utils/urls\";\n\nconst DEFAULT_AVATAR_SIZE = 128;\n\nexport const accountMethodsForMobile = {\n    url,\n    /**\n     * Update the user's account details on the mobile app\n     *\n     * @returns {Promise}\n     */\n    async updateAccount() {\n        if (!mobile.methods.updateAccount) {\n            return;\n        }\n        const base64Avatar = await accountMethodsForMobile.fetchAvatar();\n        return mobile.methods.updateAccount({\n            avatar: base64Avatar.substring(base64Avatar.indexOf(',') + 1),\n            name: user.name,\n            username: user.login,\n        });\n    },\n    /**\n     * Fetch current user's avatar as PNG image\n     *\n     * @returns {Promise} resolved with the dataURL, or rejected if the file is\n     *  empty or if an error occurs.\n     */\n    fetchAvatar() {\n        const avatarUrl = accountMethodsForMobile.url('/web/image', {\n            model: 'res.users',\n            field: 'image_medium',\n            id: user.userId,\n        });\n        return new Promise((resolve, reject) => {\n            const canvas = document.createElement('canvas');\n            canvas.width = DEFAULT_AVATAR_SIZE;\n            canvas.height = DEFAULT_AVATAR_SIZE;\n            const context = canvas.getContext('2d');\n            const image = new Image();\n            image.addEventListener('load', () => {\n                context.drawImage(image, 0, 0, DEFAULT_AVATAR_SIZE, DEFAULT_AVATAR_SIZE);\n                resolve(canvas.toDataURL('image/png'));\n            });\n            image.addEventListener('error', reject);\n            image.src = avatarUrl;\n        });\n    },\n};\n\n/**\n * Mixin to hook into the controller record's saving method and\n * trigger the update of the user's account details on the mobile app.\n *\n * @mixin\n * @name UpdateDeviceAccountControllerMixin\n *\n */\nconst UpdateDeviceAccountControllerMixin = {\n    /**\n     * @override\n     */\n    async save() {\n        const isSaved = await this._super(...arguments);\n        if (!isSaved) {\n            return false;\n        }\n        const updated = accountMethodsForMobile.updateAccount();\n        // Crapy workaround for unupdatable Odoo Mobile App iOS (Thanks Apple :@)\n        if (!isIosApp()){\n            await updated;\n        }\n        return true;\n    },\n};\n\nexport async function updateAccountOnMobileDevice() {\n    const updated = accountMethodsForMobile.updateAccount();\n    // Crapy workaround for unupdatable Odoo Mobile App iOS (Thanks Apple :@)\n    if (!isIosApp()){\n        await updated;\n    }\n}\n\n/**\n * Trigger the update of the user's account details on the mobile app.\n */\naccountMethodsForMobile.updateAccount();\n\nexport default {\n    UpdateDeviceAccountControllerMixin,\n    updateAccountOnMobileDevice,\n};\n", "import mobile from \"@web_mobile/js/services/core\";\nimport { download } from \"@web/core/network/download\";\n\nconst _download = download._download;\n\ndownload._download = async function (options) {\n    if (mobile.methods.downloadFile) {\n        if (odoo.csrf_token) {\n            options.csrf_token = odoo.csrf_token;\n        }\n        mobile.methods.downloadFile(options);\n        // There is no need to wait downloadFile because we delegate this to\n        // Download Manager Service where error handling will be handled correclty.\n        // On our side, we do not want to block the UI and consider the request\n        // as success.\n        return Promise.resolve();\n    } else {\n        return _download.apply(this, arguments);\n    }\n};\n", "import { Popover } from \"@web/core/popover/popover\";\nimport { useBackButton } from \"@web_mobile/js/core/hooks\";\nimport { patch } from \"@web/core/utils/patch\";\n\npatch(Popover.prototype, {\n    setup() {\n        super.setup(...arguments);\n        useBackButton(this.onBackButton.bind(this), () => this.props.target.isConnected);\n    },\n\n    //---------------------------------------------------------------------\n    // Handlers\n    //---------------------------------------------------------------------\n\n    /**\n     * Close popover on back-button\n     * @private\n     */\n    onBackButton() {\n        this.props.close();\n    },\n});\n", "import { registry } from \"@web/core/registry\";\nimport mobile from \"@web_mobile/js/services/core\";\n\nfunction mobileErrorHandler(env, error, originalError) {\n    if (mobile.methods.crashManager) {\n        error.originalError = originalError;\n        mobile.methods.crashManager(error);\n    }\n}\nregistry\n    .category(\"error_handlers\")\n    .add(\"web_mobile.errorHandler\", mobileErrorHandler, { sequence: 3 });\n", "import { EventBus } from \"@odoo/owl\";\n\nexport class HookEventBus extends EventBus {\n    /**\n        @param {{}} hooks\n        @param {Function} [hooks.onAddListener]\n        @param {Function} [hooks.onRemoveListener]\n     */\n    constructor(hooks = {}) {\n        super();\n        this.hooks = hooks;\n    }\n\n    addEventListener(eventName, listener) {\n        super.addEventListener(eventName, listener);\n        this.hooks.onAddListener?.(eventName, listener);\n    }\n\n    removeEventListener(eventName, listener) {\n        super.removeEventListener(eventName, listener);\n        this.hooks.onRemoveListener?.(eventName, listener);\n    }\n}\n", "import { HookEventBus } from \"@web_mobile/js/hook_event_bus\";\nimport { registry } from \"@web/core/registry\";\nimport { session } from \"@web/session\";\n\nimport mobile from \"@web_mobile/js/services/core\";\nimport { shortcutItem, switchAccountItem } from \"./user_menu_items\";\n\nconst serviceRegistry = registry.category(\"services\");\nconst userMenuRegistry = registry.category(\"user_menuitems\");\n\nexport const mobileService = {\n    timeBetweenReadsInMs: session.time_between_reads_in_ms || 100,\n    idInterval: null,\n    start() {\n        let listenerCount = 0;\n        this.bus = new HookEventBus({\n            onAddListener: (eventName, listener) => {\n                listenerCount++;\n                if (listenerCount === 1) {\n                    api.enableReader();\n                }\n            },\n            onRemoveListener: (eventName, listener) => {\n                listenerCount--;\n                if (listenerCount === 0) {\n                    api.stopReader();\n                }\n            },\n        });\n\n        const api =  {\n            bus: this.bus,\n            enableReader: this.enableReader,\n            stopReader: this.stopReader,\n        };\n\n        if (mobile.methods.addHomeShortcut) {\n            userMenuRegistry.add(\"web_mobile.shortcut\", shortcutItem);\n        }\n\n        if (mobile.methods.switchAccount) {\n            // remove \"Log Out\" and \"My Odoo.com Account\"\n            userMenuRegistry.remove('log_out');\n            userMenuRegistry.remove('odoo_account');\n\n            userMenuRegistry.add(\"web_mobile.switch\", switchAccountItem);\n        }\n\n        return api;\n    },\n\n    enableReader() {\n        if (mobile.methods.enableReader && mobile.methods.getReaderData) {\n            mobile.methods.enableReader().catch(e => console.error(e));\n            if (!this.idInterval) {\n                this.idInterval = setInterval(async () => {\n                    try {\n                        const value = await mobile.methods.getReaderData();\n                        if (value.success) {\n                            const data = value.data;\n                            if (data.length > 0) {\n                                this.bus.trigger(\"mobile_reader_scanned\", {data});\n                            }\n                        }\n                    } catch (e) {\n                        console.error(e);\n                    }\n                }, this.timeBetweenReadsInMs);\n            }\n        }\n    },\n\n    stopReader() {\n        if (this.idInterval) {\n            clearInterval(this.idInterval);\n            this.idInterval = null;\n        }\n    },\n};\nserviceRegistry.add(\"mobile\", mobileService);\n", "/* global OdooDeviceUtility */\n\nimport { uniqueId } from \"@web/core/utils/functions\";\nimport { browser } from \"@web/core/browser/browser\";\nimport { parseSearchQuery } from \"@web/core/browser/router\";\n\nvar available = typeof OdooDeviceUtility !== 'undefined';\nvar DeviceUtility;\nvar deferreds = {};\nexport var methods = {};\n\nif (available){\n    DeviceUtility = OdooDeviceUtility;\n    delete window.OdooDeviceUtility;\n}\n\n/**\n * Responsible for invoking native methods which called from JavaScript\n *\n * @param {String} name name of action want to perform in mobile\n * @param {Object} args extra arguments for mobile\n *\n * @returns Promise Object\n */\nfunction native_invoke(name, args) {\n    if (args === undefined) {\n        args = {};\n    }\n    var id = uniqueId();\n    args = JSON.stringify(args);\n    DeviceUtility.execute(name, args, id);\n    return new Promise(function (resolve, reject) {\n        deferreds[id] = {\n            successCallback: resolve,\n            errorCallback: reject\n        };\n    });\n}\n\n/**\n * Manages deferred callback from initiate from native mobile\n *\n * @param {String} id callback id\n * @param {Object} result\n */\nwindow.odoo.native_notify = function (id, result) {\n    if (deferreds.hasOwnProperty(id)) {\n        if (result.success) {\n            deferreds[id].successCallback(result);\n        } else {\n            deferreds[id].errorCallback(result);\n        }\n    }\n};\n\nvar plugins = available ? JSON.parse(DeviceUtility.list_plugins()) : [];\nplugins.forEach((plugin) => {\n    methods[plugin.name] = function (args) {\n        return native_invoke(plugin.action, args);\n    };\n});\n\n/**\n * Use to notify an uri hash change on native devices (ios / android)\n */\nif (methods.hashChange) {\n    var currentSearch;\n    browser.addEventListener(\"popstate\", function () {\n        const search = parseSearchQuery(browser.location.search);\n        if (JSON.stringify(currentSearch) !== JSON.stringify(search)) {\n            methods.hashChange(search);\n        }\n        currentSearch = search;\n    });\n}\n\n/**\n * Error related to the registration of a listener to the backbutton event\n */\nclass BackButtonListenerError extends Error {}\n\n/**\n * By using the back button feature the default back button behavior from the\n * app is actually overridden so it is important to keep count to restore the\n * default when no custom listener are remaining.\n */\nclass BackButtonManager {\n\n    constructor() {\n        this._listeners = new Map();\n        this._onGlobalBackButton = this._onGlobalBackButton.bind(this);\n    }\n\n    //--------------------------------------------------------------------------\n    // Public\n    //--------------------------------------------------------------------------\n\n    /**\n     * Enables the func listener, overriding default back button behavior.\n     *\n     * @param {Component} listener\n     * @param {function} func\n     * @throws {BackButtonListenerError} if the listener has already been registered\n     */\n    addListener(listener, func) {\n        if (!methods.overrideBackButton) {\n            return;\n        }\n        if (this._listeners.has(listener)) {\n            throw new BackButtonListenerError(\"This listener was already registered.\");\n        }\n        this._listeners.set(listener, func);\n        if (this._listeners.size === 1) {\n            document.addEventListener('backbutton', this._onGlobalBackButton);\n            methods.overrideBackButton({ enabled: true });\n        }\n    }\n    /**\n     * Disables the func listener, restoring the default back button behavior if\n     * no other listeners are present.\n     *\n     * @param {Component} listener\n     * @throws {BackButtonListenerError} if the listener has already been unregistered\n     */\n    removeListener(listener) {\n        if (!methods.overrideBackButton) {\n            return;\n        }\n        if (!this._listeners.has(listener)) {\n            throw new BackButtonListenerError(\"This listener has already been unregistered.\");\n        }\n        this._listeners.delete(listener);\n        if (this._listeners.size === 0) {\n            document.removeEventListener('backbutton', this._onGlobalBackButton);\n            methods.overrideBackButton({ enabled: false });\n        }\n    }\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    _onGlobalBackButton() {\n        const [listener, func] = [...this._listeners].pop();\n        if (listener) {\n            func.apply(listener, arguments);\n        }\n    }\n}\n\nconst backButtonManager = new BackButtonManager();\n\nexport default {\n    BackButtonManager,\n    BackButtonListenerError,\n    backButtonManager,\n    methods,\n};\n", "import { _t } from \"@web/core/l10n/translation\";\nimport mobile from \"@web_mobile/js/services/core\";\n\nexport function shortcutItem(env) {\n    return {\n        type: \"item\",\n        id: \"web_mobile.shortcut\",\n        description: _t(\"Add to Home Screen\"),\n        callback: () => {\n            const menu = env.services.menu.getCurrentApp();\n            if (menu) {\n                const base64Icon = menu.webIconData;\n                mobile.methods.addHomeShortcut({\n                    title: document.title,\n                    shortcut_url: document.URL,\n                    web_icon: base64Icon.substring(base64Icon.indexOf(\",\") + 1),\n                });\n            } else {\n                env.services.notification.add(_t(\"No shortcut for Home Menu\"));\n            }\n        },\n        sequence: 100,\n    };\n}\n\nexport function switchAccountItem(env) {\n    return {\n        type: \"item\",\n        id: \"web_mobile.switch\",\n        description: _t(\"Switch/Add Account\"),\n        callback: () => {\n            mobile.methods.switchAccount();\n        },\n        sequence: 100,\n    };\n}\n", "import { registry } from \"@web/core/registry\";\nimport { formView } from \"@web/views/form/form_view\";\nimport { updateAccountOnMobileDevice } from \"@web_mobile/js/core/mixins\";\n\nclass ResUsersPreferenceController extends formView.Controller {\n    onRecordSaved(record) {\n        return updateAccountOnMobileDevice();\n    }\n}\n\nregistry.category(\"views\").add(\"res_users_preferences_form\", {\n    ...formView,\n    Controller: ResUsersPreferenceController,\n});\n", "export class BarcodeObject {\n    constructor(rawValue) {\n        this.rawValue = rawValue; // Untouched barcode.\n\n        this.nomenclature = undefined; // Nomenclature this barcode uses.\n        this.parsedBarcode = undefined; // Barcode's value(s) once it has been parsed.\n        this.parsedData = {}; // Fetched data thanks to the barcode parsed value(s).\n        this.isParsed = false; // Flag to know if barcode was already parsed.\n        this.missingRecords = []; // Keep track of missing records (try to fetch them later.)\n        this.isURN = Boolean(this.rawValue.match(/^urn:.*$/));\n\n        if (this.parser) {\n            try {\n                this.parsedBarcode = this.parser.parse_barcode(this.rawValue);\n            } catch (err) {\n                // The barcode can't be parsed but the error is caught to fallback\n                // on the classic way to handle barcodes.\n                console.log(`%cWarning: error about ${this.rawValue}`, \"text-weight: bold;\");\n                console.log(err.message);\n            }\n            if (this.parsedBarcode && !Array.isArray(this.parsedBarcode)) {\n                // Depending of the nomenclature, the parsed data is either an object,\n                // either an array of objects. Convert it into an array in all case.\n                this.parsedBarcode = [this.parsedBarcode];\n            }\n            this.isParsed = Boolean(this.parsedBarcode?.length);\n        } else {\n            console.warn(\"No parser set !\");\n        }\n\n        // Adds this instance into the raw barcode/barcode object mapping.\n        BarcodeObject.mappingRawBarcodeToObject[rawValue] = this;\n    }\n\n    /**\n     * Attach to the barcode record(s) already in the cache.\n     * For missing record(s), they need to be fetched afterward.\n     */\n    async setRecords(options = false) {\n        if (!this.isParsed) {\n            return;\n        }\n        options = options || {\n            fetchLater: true,\n            onlyInCache: true,\n        };\n        this.missingRecords = [];\n        for (const barcodeData of this.parsedBarcode) {\n            const { type, code } = barcodeData;\n            if (type === \"product\") {\n                await this.fetchProduct(code, options);\n            } else if (type === \"lot\") {\n                await this.fetchTrackingNumber(code, options);\n            }\n        }\n    }\n\n    // Getters\n    get cache() {\n        return BarcodeObject.__cache;\n    }\n\n    get hasMissingRecords() {\n        return this.isParsed && Boolean(this.missingRecords.length);\n    }\n\n    get parser() {\n        return BarcodeObject.__parser;\n    }\n\n    // Fetching methods\n    async fetchTrackingNumber(lotBarcode, options) {\n        const lot = await this.cache.getRecordByBarcode(lotBarcode, \"stock.lot\", options);\n        if (lot) {\n            this.parsedData.lot = lot;\n        } else {\n            this.missingRecords.push({ type: \"lot\", lotBarcode });\n        }\n    }\n\n    async fetchProduct(productBarcode, options) {\n        let product = await this.cache.getRecordByBarcode(\n            productBarcode,\n            \"product.product\",\n            options\n        );\n        if (!product) {\n            const packaging = await this.cache.getRecordByBarcode(productBarcode, \"product.uom\", {\n                onlyInCache: true,\n            });\n            if (packaging) {\n                product = this.cache.getRecord(\"product.product\", packaging.product_id, false);\n                this.parsedData.packaging = packaging;\n                this.parsedData.quantity = packaging.uom_id.factor;\n            }\n        }\n        if (product) {\n            this.parsedData.product = product;\n        } else {\n            this.missingRecords.push({ type: \"product\", productBarcode });\n        }\n    }\n}\n\n// Static properties and methods.\nBarcodeObject.mappingRawBarcodeToObject = {};\n\nBarcodeObject.setEnv = (cache, parser) => {\n    BarcodeObject.__cache = cache;\n    BarcodeObject.__parser = parser;\n};\n\nBarcodeObject.forBarcode = (barcode) => {\n    if (BarcodeObject.mappingRawBarcodeToObject[barcode]) {\n        return BarcodeObject.mappingRawBarcodeToObject[barcode];\n    }\n    return new BarcodeObject(barcode);\n};\n", "import { Dialog } from \"@web/core/dialog/dialog\";\nimport { Component } from \"@odoo/owl\";\n\nexport class ApplyQuantDialog extends Component {\n    static components = { Dialog };\n    static props = {\n        onApply: Function,\n        onApplyAll: Function,\n        close: Function,\n    };\n    static template = \"stock_barcode.ApplyQuantDialog\";\n\n    onApply() {\n        this.props.onApply();\n        this.props.close();\n    }\n\n    onApplyAll() {\n        this.props.onApplyAll();\n        this.props.close();\n    }\n}\n", "import { Dialog } from \"@web/core/dialog/dialog\";\nimport { Component } from \"@odoo/owl\";\n\nexport class BackorderDialog extends Component {\n    static components = { Dialog };\n    static props = {\n        displayUoM: Boolean,\n        uncompletedLines: Array,\n        onApply: Function,\n        close: Function,\n    };\n    static template = \"stock_barcode.BackorderDialog\";\n\n    async _onApply() {\n        await this.props.onApply();\n        this.props.close();\n    }\n}\n", "import { Dialog } from \"@web/core/dialog/dialog\";\nimport { Component, useState } from \"@odoo/owl\";\n\nexport class ConfirmQuantDialog extends Component {\n    static components = { Dialog };\n    static template = \"stock_barcode.ConfirmQuantDialog\";\n    static props = {\n        close: Function,\n        onConfirm: Function,\n        onWaitReview: Function,\n    };\n\n    setup() {\n        this.inventoryReason = useState({ value: \"Physical Inventory\" });\n    }\n\n    onConfirm() {\n        this.props.onConfirm({\n            inventory_name: this.prop,\n        });\n        this.props.close();\n    }\n\n    onWaitReview() {\n        this.props.onWaitReview();\n        this.props.close();\n    }\n\n    onInventoryReasonChange(ev) {\n        this.inventoryReason.value = ev.target.value;\n    }\n}\n", "import { Component, onWillUnmount, onWillUpdateProps, useState } from \"@odoo/owl\";\nimport { session } from \"@web/session\";\n\nexport class CountScreenRFID extends Component {\n    static props = {\n        close: Function,\n        receivedRFIDs: { type: Array, default: [] },\n        totalRFIDs: { type: Array, default: [] },\n    };\n    static template = \"stock_barcode.CountScreenRFID\";\n\n    setup() {\n        this.state = useState({\n            duration: \"00:00\",\n            readRate: 0,\n        });\n        this.delayBeforeStopRefreshRate = (session.time_between_reads_in_ms || 100) * 2;\n        this.totalSeconds = 0;\n        this.activeScanningTotalTime = 0;\n        this.setActiveScanning();\n        this.initialTimestamp = Date.now();\n        this.timeInterval = setInterval(() => {\n            const currentTimestamp = Date.now();\n            const milliseconds = currentTimestamp - this.initialTimestamp;\n            this.totalSeconds = Math.floor(milliseconds / 1000);\n            const seconds = this.totalSeconds % 60;\n            const minutes = Math.floor(this.totalSeconds / 60);\n            const strSeconds = String(seconds).padStart(2, \"0\");\n            const strMinutes = String(minutes).padStart(2, \"0\");\n            this.state.duration = `${strMinutes}:${strSeconds}`;\n        }, 1000);\n        this.updateReadsRateInterval = setInterval(() => {\n            if (this.activeScanning) {\n                this.activeScanningTotalTime += 50;\n            }\n            const seconds = this.activeScanningTotalTime / 1000;\n            const divider = Math.max(seconds, 1);\n            this.state.readRate = Math.floor(this.props.receivedRFIDs.length / divider);\n        }, 50);\n\n        onWillUpdateProps(() => {\n            clearTimeout(this.activeScanningTimeout);\n            this.setActiveScanning();\n        });\n\n        onWillUnmount(() => {\n            clearInterval(this.updateReadsRateInterval);\n            clearTimeout(this.activeScanningTimeout);\n        });\n    }\n\n    setActiveScanning() {\n        if (this.activeScanningTimeout) {\n            clearTimeout(this.activeScanningTimeout);\n        }\n        this.activeScanning = true;\n        this.activeScanningTimeout = setTimeout(() => {\n            this.activeScanning = false;\n        }, this.delayBeforeStopRefreshRate);\n    }\n\n    get totalRead() {\n        return this.props.totalRFIDs.length;\n    }\n\n    get uniqueTags() {\n        return new Set(this.props.totalRFIDs).size;\n    }\n}\n", "import LineComponent from \"@stock_barcode/components/line\";\n\nexport default class GroupedLineComponent extends LineComponent {\n    static components = { LineComponent };\n    static template = \"stock_barcode.GroupedLineComponent\";\n\n    get isComplete() {\n        if (\n            this.linesToDisplay.length > 1 &&\n            this.isTracked &&\n            this.qtyDemand &&\n            this.qtyDone === this.qtyDemand\n        ) {\n            // In case the line is tracked and has multiple sublines, we consider the line complete\n            // if it has enough quantity and all sublines with quantity has a lot.\n            for (const subline of this.linesToDisplay) {\n                const lotName = subline.lot_id?.name || subline.lot_name;\n                if (this.env.model.getQtyDone(subline) && !lotName) {\n                    return false;\n                }\n            }\n            return true;\n        }\n        return super.isComplete;\n    }\n\n    get isSelected() {\n        return this.line.virtual_ids.indexOf(this.env.model.selectedLineVirtualId) !== -1;\n    }\n\n    get opened() {\n        return this.env.model.groupKey(this.line) === this.env.model.unfoldLineKey;\n    }\n\n    get sublineProps() {\n        return {\n            displayUOM: this.props.displayUOM,\n            editLine: this.props.editLine,\n            line: this.subline,\n            subline: true,\n        };\n    }\n\n    get linesToDisplay() {\n        if (!this.env.model.showReservedSns) {\n            return this.props.line.lines.filter((line) => {\n                return (\n                    this.env.model.getQtyDone(line) > 0 ||\n                    line.product_id.tracking == \"none\" ||\n                    this.env.model.getQtyDemand(line) == 0\n                );\n            });\n        }\n        return this.props.line.lines;\n    }\n\n    get lotName() {\n        if (!this.env.model.showReservedSns) {\n            // In case we don't display unscanned reserved lots, display it only\n            // if only one subline with a lot has some quantity done.\n            if (this.linesToDisplay.length === 1) {\n                for (const line of this.linesToDisplay) {\n                    const lotName = line.lot_id?.name || line.lot_name;\n                    if (lotName && this.env.model.getQtyDone(this.line)) {\n                        return lotName;\n                    }\n                }\n            } else {\n                return \"\";\n            }\n        }\n        return super.lotName;\n    }\n\n    get displayToggleBtn() {\n        return this.linesToDisplay.length > 1;\n    }\n\n    toggleSublines(ev) {\n        ev.stopPropagation();\n        this.env.model.toggleSublines(this.line);\n    }\n}\n", "import { Component } from \"@odoo/owl\";\nimport { ProductImageDialog } from \"@stock_barcode/components/product_image_dialog\";\n\nexport default class LineComponent extends Component {\n    static props = [\"displayUOM\", \"line\", \"subline?\", \"editLine\"];\n    static template = \"stock_barcode.LineComponent\";\n\n    setup() {\n        this.imageSource = this.props.line.product_id.has_image\n            ? `/web/image/product.product/${this.props.line.product_id.id}/image_128`\n            : null;\n    }\n\n    get destinationLocationPath() {\n        return this._getLocationPath(\n            this.env.model._defaultDestLocation(),\n            this.line.location_dest_id\n        );\n    }\n\n    get displayDeleteButton() {\n        return this.env.model.lineCanBeDeleted(this.line);\n    }\n\n    get displayDestinationLocation() {\n        return !this.props.subline && this.env.model.displayDestinationLocation;\n    }\n\n    get displayFulfillbutton() {\n        return (\n            this.incrementQty && this.env.model.getDisplayIncrementBtn(this.line) && !this.isPackage\n        );\n    }\n\n    get displayCompletePackageButton() {\n        return this.isSelected && this.env.model.getDisplayCompletePackageBtn(this.line);\n    }\n\n    get displayIncrementButton() {\n        if (this.isSelected && !this.isPackage && this.incrementQty !== 1) {\n            return this.isTracked && this.line.product_id.tracking === \"serial\"\n                ? this.env.model.getDisplayIncrementBtnForSerial(this.line)\n                : this.env.model.getDisplayIncrementBtn(this.line);\n        }\n        return false;\n    }\n\n    get incrementQty() {\n        return this.env.model.getIncrementQuantity(this.line);\n    }\n\n    get displayResultPackage() {\n        return this.env.model.displayResultPackage;\n    }\n\n    get isComplete() {\n        if (!this.qtyDemand || this.qtyDemand != this.qtyDone) {\n            return false;\n        } else if (this.isTracked && !this.lotName) {\n            return false;\n        }\n        return true;\n    }\n\n    get isPackage() {\n        return this.line.isPackageLine;\n    }\n\n    get isSelected() {\n        return (\n            this.env.model.lineIsSelected(this.line) ||\n            (this.line.package_id &&\n                this.line.package_id.id === this.env.model.lastScanned.packageId)\n        );\n    }\n\n    get isTracked() {\n        return this.env.model.lineIsTracked(this.line);\n    }\n\n    get lotName() {\n        if (this.env.model.showReservedSns || this.env.model.getQtyDone(this.line)) {\n            return (this.line.lot_id && this.line.lot_id.name) || this.line.lot_name || \"\";\n        }\n        return \"\";\n    }\n\n    get nextExpected() {\n        if (this.isSelected) {\n            if (this.isTracked && !this.lotName) {\n                return \"lot\";\n            } else if (this.qtyDemand && this.qtyDone < this.qtyDemand) {\n                return \"quantity\";\n            }\n        }\n        return false;\n    }\n\n    get qtyDemand() {\n        return this.env.model.getQtyDemand(this.line);\n    }\n\n    get qtyDone() {\n        return this.env.model.getQtyDone(this.line);\n    }\n\n    get quantityIsSet() {\n        return this.line.inventory_quantity_set;\n    }\n\n    get packageLabel() {\n        return this.line.package_id.complete_name;\n    }\n\n    get resultPackageLabel() {\n        return this.line.result_package_id.dest_complete_name;\n    }\n\n    get line() {\n        return this.props.line;\n    }\n\n    get componentClasses() {\n        return [\n            this.isComplete ? \"o_line_completed\" : \"o_line_not_completed\",\n            this.env.model.lineIsFaulty(this.line) ? \"o_faulty\" : \"\",\n            this.isSelected ? \"o_selected o_highlight\" : \"\",\n        ].join(\" \");\n    }\n\n    get showDescription() {\n        // In Barcode we don't get the product's `display_name` with the code, however the description uses it.\n        const possibleNames = [\n            this.line.product_id.display_name,\n            `[${this.line.product_id.default_code}] ${this.line.product_id.display_name}`,\n            `[${this.line.product_id.code}] ${this.line.product_id.display_name}`,\n        ];\n        return (\n            this.line.description_picking && !possibleNames.includes(this.line.description_picking)\n        );\n    }\n\n    _getLocationPath(rootLocation, currentLocation) {\n        let locationName = currentLocation.display_name;\n        if (\n            this.env.model.shouldShortenLocationName &&\n            this.env.model._isSublocation &&\n            this.env.model._isSublocation(currentLocation, rootLocation) &&\n            rootLocation &&\n            rootLocation.id != currentLocation.id\n        ) {\n            locationName = locationName.replace(rootLocation.display_name, \"...\");\n        }\n        return locationName.replace(new RegExp(currentLocation.name + \"$\"), \"\");\n    }\n\n    addQuantity(quantity) {\n        let lineVirtualId = this.line.virtual_id;\n        if (this.line.lines?.length > 1 && this.lotName) {\n            lineVirtualId = this.line.lines.find((subline) => {\n                const sublineLotName = subline.lot_id ? subline.lot_id.name : subline.lot_name;\n                return sublineLotName === this.lotName;\n            }).virtual_id;\n        }\n        this.env.model.updateLineQty(lineVirtualId, quantity);\n    }\n\n    completePackage() {\n        this.env.model.completePackage(this.line.virtual_id);\n    }\n\n    select(ev) {\n        ev.stopPropagation();\n        this.env.model.selectLine(this.line);\n        this.env.model.trigger(\"update\");\n    }\n\n    toggleAsCounted(ev) {\n        this.env.model.toggleAsCounted(this.line);\n    }\n\n    onClickImage() {\n        this.env.dialog.add(ProductImageDialog, { record: this.line.product_id });\n    }\n}\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { Chatter } from \"@mail/chatter/web_portal/chatter\";\nimport { COMMANDS } from \"@barcodes/barcode_handlers\";\nimport BarcodePickingModel from \"@stock_barcode/models/barcode_picking_model\";\nimport BarcodeQuantModel from \"@stock_barcode/models/barcode_quant_model\";\nimport GroupedLineComponent from \"@stock_barcode/components/grouped_line\";\nimport LineComponent from \"@stock_barcode/components/line\";\nimport PackageLineComponent from \"@stock_barcode/components/package_line\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { registry } from \"@web/core/registry\";\nimport { useService, useBus } from \"@web/core/utils/hooks\";\nimport { Mutex } from \"@web/core/utils/concurrency\";\nimport { ConfirmationDialog } from \"@web/core/confirmation_dialog/confirmation_dialog\";\nimport { View } from \"@web/views/view\";\nimport {\n    BarcodeVideoScanner,\n    isBarcodeScannerSupported,\n} from \"@web/core/barcode/barcode_video_scanner\";\nimport { url } from \"@web/core/utils/urls\";\nimport { utils as uiUtils } from \"@web/core/ui/ui_service\";\nimport {\n    Component,\n    EventBus,\n    onPatched,\n    onWillStart,\n    onWillUnmount,\n    useState,\n    useSubEnv,\n} from \"@odoo/owl\";\nimport { standardWidgetProps } from \"@web/views/widgets/standard_widget_props\";\nimport { standardActionServiceProps } from \"@web/webclient/actions/action_service\";\nimport { BarcodeInput } from \"@barcodes/components/manual_barcode\";\nimport { CountScreenRFID } from \"./count_screen_rfid\";\n\n// Lets `barcodeGenericHandlers` knows those commands exist so it doesn't warn when scanned.\nCOMMANDS[\"OCDMENU\"] = () => {};\nCOMMANDS[\"OCDCANC\"] = () => {};\n\nconst bus = new EventBus();\n\nclass StockBarcodeUnlinkButton extends Component {\n    static template = \"stock_barcode.UnlinkButton\";\n    static props = { ...standardWidgetProps };\n    setup() {\n        this.orm = useService(\"orm\");\n    }\n    async onClick() {\n        const { resModel, resId, context } = this.props.record;\n        await this.orm.unlink(resModel, [resId], { context });\n        bus.trigger(\"refresh\");\n    }\n}\nregistry.category(\"view_widgets\").add(\"stock_barcode_unlink_button\", {\n    component: StockBarcodeUnlinkButton,\n});\n\n/**\n * TODO: stock_barcode should not depend on base_import.\n */\nexport class ImportBlockUI extends Component {\n    static props = {\n        message: { type: String, optional: true },\n        blockComponent: { type: Object, optional: true },\n    };\n    static template = \"stock_barcode.BlockUI\";\n}\n\n/**\n * Main Component\n * Gather the line information.\n * Manage the scan and save process.\n */\n\nclass MainComponent extends Component {\n    static props = { ...standardActionServiceProps };\n    static template = \"stock_barcode.MainComponent\";\n    static components = {\n        BarcodeInput,\n        BarcodeVideoScanner,\n        Chatter,\n        CountScreenRFID,\n        GroupedLineComponent,\n        ImportBlockUI,\n        LineComponent,\n        PackageLineComponent,\n        View,\n    };\n\n    //--------------------------------------------------------------------------\n    // Lifecycle\n    //--------------------------------------------------------------------------\n\n    setup() {\n        this.orm = useService(\"orm\");\n        this.notification = useService(\"notification\");\n        this.dialog = useService(\"dialog\");\n        this.action = useService(\"action\");\n        this.actionMutex = new Mutex();\n        this.resModel = this.props.action.res_model;\n        this.resId = this.props.action.context.active_id || false;\n        const model = this._getModel();\n        model.newScrapProduct = this.newScrapProduct.bind(this);\n        useSubEnv({\n            model,\n            dialog: this.dialog,\n        });\n        this._scrollBehavior = \"smooth\";\n        this.isMobile = uiUtils.isSmall();\n        this.state = useState({\n            cameraScannedEnabled: false,\n            view: \"barcodeLines\", // Could be also 'printMenu' or 'editFormView'.\n            displayNote: false,\n            displayCountRFID: false,\n            uiBlocked: false,\n            barcodesProcessed: 0,\n            barcodesToProcess: 0,\n            readyToToggleCamera: true,\n        });\n        this.bufferedBarcodes = [];\n        this.receivedRFIDs = [];\n        this.totalRFIDs = [];\n        this.bufferingTimeout = null;\n        this.barcodeService = useService(\"barcode\");\n        useBus(this.barcodeService.bus, \"barcode_scanned\", (ev) =>\n            this.onBarcodeScanned(ev.detail.barcode)\n        );\n        this.mobileService = useService(\"mobile\");\n        useBus(this.mobileService.bus, \"mobile_reader_scanned\", (ev) =>\n            this.onMobileReaderScanned(ev.detail.data)\n        );\n\n        useBus(this.env.model, \"flash\", this.flashScreen.bind(this));\n        useBus(this.env.model, \"playSound\", this.playSound.bind(this));\n        useBus(this.env.model, \"blockUI\", this.blockUI.bind(this));\n        useBus(this.env.model, \"unblockUI\", this.unblockUI.bind(this));\n        useBus(this.env.model, \"addBarcodesCountToProcess\", (ev) =>\n            this.addBarcodesCountToProcess(ev.detail)\n        );\n        useBus(\n            this.env.model,\n            \"updateBarcodesCountProcessed\",\n            this.updateBarcodesCountProcessed.bind(this)\n        );\n        useBus(\n            this.env.model,\n            \"clearBarcodesCountProcessed\",\n            this.clearBarcodesCountProcessed.bind(this)\n        );\n        useBus(bus, \"refresh\", (ev) => this._onRefreshState(ev.detail));\n\n        onWillStart(() => this.onWillStart());\n\n        onWillUnmount(() => {\n            clearTimeout(this.bufferingTimeout);\n        });\n\n        onPatched(() => {\n            this._scrollToSelectedLine();\n        });\n\n        onWillUnmount(() => {\n            this.env.model._onExit();\n        });\n    }\n\n    // UI Methods --------------------------------------------------------------\n    addBarcodesCountToProcess(count) {\n        this.state.barcodesToProcess += count;\n        if (this.state.barcodesToProcess > this.state.barcodesProcessed) {\n            this.updateBarcodesCountMessage();\n            this.blockUI();\n        }\n    }\n\n    updateBarcodesCountProcessed() {\n        this.state.barcodesProcessed++;\n        this.updateBarcodesCountMessage();\n        if (this.state.barcodesProcessed >= this.state.barcodesToProcess) {\n            this.clearBarcodesCountProcessed();\n        }\n    }\n\n    clearBarcodesCountProcessed() {\n        this.state.barcodesProcessed = 0;\n        this.state.barcodesToProcess = 0;\n        this.unblockUI();\n    }\n\n    updateBarcodesCountMessage() {\n        this.blockUIMessage = _t(\"Processing %(processed)s/%(toProcess)s barcodes\", {\n            processed: this.state.barcodesProcessed,\n            toProcess: this.state.barcodesToProcess,\n        });\n    }\n\n    blockUI(ev) {\n        this.state.uiBlocked = true;\n    }\n\n    unblockUI() {\n        this.state.uiBlocked = false;\n        this.render(true);\n    }\n\n    playSound(ev) {\n        if (!this.config.play_sound || this.state.uiBlocked) {\n            return;\n        }\n        const type = ev.detail || \"notify\";\n        this.sounds[type].currentTime = 0;\n        this.sounds[type].play().catch((error) => {\n            // `play` returns a promise. In case this promise is rejected (permission\n            // issue for example), catch it to avoid Odoo's `UncaughtPromiseError`.\n            this.config.play_sound = false;\n            console.warn(error);\n        });\n    }\n\n    //--------------------------------------------------------------------------\n    // Public\n    //--------------------------------------------------------------------------\n\n    get highlightValidateButton() {\n        return this.env.model.highlightValidateButton;\n    }\n\n    async onWillStart() {\n        const barcodeData = await rpc(\"/stock_barcode/get_barcode_data\", {\n            model: this.resModel,\n            res_id: this.resId,\n        });\n        barcodeData.actionId = this.props.actionId;\n        this.config = { play_sound: true, ...barcodeData.data.config };\n        if (this.config.play_sound) {\n            const fileExtension = new Audio().canPlayType(\"audio/ogg; codecs=vorbis\")\n                ? \"ogg\"\n                : \"mp3\";\n            this.sounds = {\n                error: new Audio(url(`/barcodes/static/src/audio/error.${fileExtension}`)),\n                notify: new Audio(url(`/mail/static/src/audio/ting.${fileExtension}`)),\n                success: new Audio(url(`/stock_barcode/static/src/audio/success.${fileExtension}`)),\n            };\n            this.sounds.error.load();\n            this.sounds.notify.load();\n            this.sounds.success.load();\n        }\n        this.setupCameraScanner();\n        this.groups = barcodeData.groups;\n        this.env.model.setData(barcodeData);\n        this.state.displayNote = Boolean(this.env.model.record.note);\n        this.env.model.addEventListener(\"process-action\", this._onDoAction.bind(this));\n        this.env.model.addEventListener(\"refresh\", (ev) => this._onRefreshState(ev.detail));\n        this.env.model.addEventListener(\"update\", () => {\n            if (!this.state.uiBlocked) {\n                this.render(true);\n            }\n        });\n        this.env.model.addEventListener(\"history-back\", () => this._exit());\n    }\n\n    get isTransfer() {\n        return this.currentSourceLocation && this.currentDestinationLocation;\n    }\n\n    get lineFormViewProps() {\n        return {\n            resId: this._editedLineParams && this._editedLineParams.currentId,\n            resModel: this.env.model.lineModel,\n            context: this.env.model._getNewLineDefaultContext(),\n            viewId: this.env.model.lineFormViewId,\n            display: { controlPanel: false },\n            type: \"form\",\n            onSave: (record) => this.saveFormView(record),\n            onDiscard: () => this.toggleBarcodeLines(),\n        };\n    }\n\n    get lines() {\n        return this.env.model.groupedLines;\n    }\n\n    get packageLines() {\n        return this.env.model.packageLines;\n    }\n\n    get addLineBtnName() {\n        return _t(\"Add Product\");\n    }\n\n    get displayActionButtons() {\n        return this.state.view === \"barcodeLines\" && this.env.model.canBeProcessed;\n    }\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    _getBarcodeModel() {\n        if (this.resModel === \"stock.picking\") {\n            return BarcodePickingModel;\n        } else if (this.resModel === \"stock.quant\") {\n            return BarcodeQuantModel;\n        }\n        throw new Error(\"No JS model define\");\n    }\n\n    _getModel() {\n        const services = {\n            rpc: rpc,\n            orm: this.orm,\n            notification: this.notification,\n            action: this.action,\n        };\n        const BarcodeModel = this._getBarcodeModel();\n        return new BarcodeModel(this.resModel, this.resId, services);\n    }\n\n    //--------------------------------------------------------------------------\n    // Camera scanner\n    //--------------------------------------------------------------------------\n\n    toggleCameraScanner() {\n        if (!this.state.cameraScannedEnabled) {\n            this.state.cameraScannedEnabled = true;\n            this.state.readyToToggleCamera = false;\n        } else if (this.state.readyToToggleCamera) {\n            this.state.cameraScannedEnabled = false;\n        }\n    }\n\n    setupCameraScanner() {\n        this.cameraScannerSupported = isBarcodeScannerSupported();\n        this.barcodeVideoScannerProps = {\n            delayBetweenScan: this.config.delay_between_scan || 2000,\n            facingMode: \"environment\",\n            onResult: (barcode) => this.onBarcodeScanned(barcode),\n            onError: (error) => {\n                this.state.cameraScannedEnabled = false;\n                const message = error.message;\n                this.notification.add(message, { type: \"warning\" });\n            },\n            onReady: () => {\n                this.state.readyToToggleCamera = true;\n            },\n            cssClass: \"o_stock_barcode_camera_video\",\n        };\n    }\n\n    get cameraScannerClassState() {\n        if (!this.state.readyToToggleCamera) {\n            return \"bg-secondary\";\n        }\n        return this.state.cameraScannedEnabled ? \"bg-success text-white\" : \"text-primary\";\n    }\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    async cancel() {\n        await this.env.model.save();\n        const action = await this.orm.call(this.resModel, \"action_cancel_from_barcode\", [\n            [this.resId],\n        ]);\n        const onClose = (res) => {\n            if (res && res.cancelled) {\n                this.env.model._cancelNotification();\n                this._exit();\n            }\n        };\n        this.action.doAction(action, {\n            onClose: onClose.bind(this),\n        });\n    }\n\n    onBarcodeScanned(barcode) {\n        if (this.state.view !== \"barcodeLines\") {\n            return;\n        }\n        if (barcode) {\n            this.actionMutex.exec(async () => this.env.model.processBarcode(barcode));\n            if (\"vibrate\" in window.navigator) {\n                window.navigator.vibrate(100);\n            }\n        } else {\n            const message = _t(\"Please, Scan again!\");\n            this.env.services.notification.add(message, { type: \"warning\" });\n        }\n    }\n\n    onMobileReaderScanned(data) {\n        this.receivedRFIDs.push(...data);\n        this.totalRFIDs.push(...data);\n        this.state.displayCountRFID = true;\n        if (this.RFIDCountTimeout) {\n            clearTimeout(this.RFIDCountTimeout);\n        }\n        this.RFIDCountTimeout = setTimeout(() => this.closeRFIDCount(), 5000);\n        if (!this.bufferingTimeout) {\n            this.bufferingTimeout = setTimeout(\n                this._onMobileReaderScanned.bind(this),\n                this.config.barcode_rfid_batch_time\n            );\n        }\n        this.bufferedBarcodes = this.bufferedBarcodes.concat(data);\n    }\n\n    async _onMobileReaderScanned(ev) {\n        await this.env.model.processBarcode(this.bufferedBarcodes.join(\",\"), { readingRFID: true });\n        this.bufferedBarcodes = [];\n        clearTimeout(this.bufferingTimeout);\n        this.bufferingTimeout = null;\n    }\n\n    closeRFIDCount() {\n        if (this.RFIDCountTimeout) {\n            clearTimeout(this.RFIDCountTimeout);\n        }\n        this.state.displayCountRFID = false;\n        this.receivedRFIDs = [];\n    }\n\n    onBarcodeSubmitted(barcode) {\n        this.changeView(\"barcodeLines\");\n        barcode = this.env.model.cleanBarcode(barcode);\n        this.onBarcodeScanned(barcode);\n    }\n\n    async exit(ev) {\n        this.state.cameraScannedEnabled = false;\n        if (this.state.view === \"barcodeLines\") {\n            await this.env.model.beforeQuit();\n            this._exit();\n        } else {\n            this.toggleBarcodeLines();\n        }\n    }\n\n    _exit() {\n        const { breadcrumbs } = this.env.config;\n        if (breadcrumbs.length === 1) {\n            // Bring back to the Barcode App home menu when there is no breadcrumb.\n            this.action.doAction(\"stock_barcode.stock_barcode_action_main_menu\");\n        } else {\n            const previousPath = breadcrumbs[breadcrumbs.length - 2].url.split(\"/\");\n\n            if (isNaN(previousPath[previousPath.length - 1])) {\n                this.env.config.historyBack();\n            } else {\n                // If previous controller path's last part is a number, it will\n                // open the current record form view (happens after a refresh of\n                // the web browser.) Avoid that by calling browser's history back.\n                history.back();\n            }\n        }\n    }\n\n    flashScreen() {\n        if (this.state.uiBlocked) {\n            return;\n        }\n        const clientAction = document.querySelector(\".o_barcode_client_action\");\n        // Resets the animation (in case it still going).\n        clientAction.style.animation = \"none\";\n        clientAction.offsetHeight; // Trigger reflow.\n        clientAction.style.animation = null;\n        // Adds the CSS class linked to the keyframes animation `white-flash`.\n        clientAction.classList.add(\"o_white_flash\");\n    }\n\n    putInPack(ev) {\n        ev.stopPropagation();\n        this.env.model._putInPack();\n    }\n\n    returnProducts(ev) {\n        ev.stopPropagation();\n        this.env.model._returnProducts();\n    }\n\n    saveFormView(lineRecord) {\n        const lineId =\n            (lineRecord && lineRecord.resId) ||\n            (this._editedLineParams && this._editedLineParams.currentId);\n        const recordId = lineRecord.resModel === this.resModel ? lineId : undefined;\n        this._onRefreshState({ recordId, lineId });\n    }\n\n    changeView(view) {\n        this.state.cameraScannedEnabled = false;\n        this.state.view = view;\n    }\n\n    async toggleBarcodeLines(lineId) {\n        await this.env.model.displayBarcodeLines(lineId);\n        this._editedLineParams = undefined;\n        this.changeView(\"barcodeLines\");\n    }\n\n    async toggleInformation() {\n        if (this.env.model.formViewId) {\n            if (this.state.view === \"infoFormView\") {\n                this.changeView(\"barcodeLines\");\n            } else {\n                await this.env.model.save();\n                this.changeView(\"infoFormView\");\n            }\n        }\n    }\n\n    /**\n     * Calls `validate` on the model and then triggers up the action because OWL\n     * components don't seem able to manage wizard without doing custom things.\n     *\n     * @param {OdooEvent} ev\n     */\n    async validate(ev) {\n        ev.stopPropagation();\n        await this.env.model.validate();\n    }\n\n    _getHeaderHeight() {\n        const header = document.querySelector(\".o_barcode_header\");\n        const navbar = document.querySelector(\".o_main_navbar\");\n        // Computes the real header's height (the navbar is present if the page was refreshed).\n        return navbar ? navbar.offsetHeight + header.offsetHeight : header.offsetHeight;\n    }\n\n    _scrollToSelectedLine() {\n        if (!this.state.view === \"barcodeLines\" && this.env.model.canBeProcessed) {\n            this._scrollBehavior = \"auto\";\n            return;\n        }\n        // Tries to scroll to selected subline.\n        let targetElement = false;\n        let selectedLine = document.querySelector(\".o_sublines .o_barcode_line.o_highlight\");\n        const isSubline = Boolean(selectedLine);\n        // If no selected subline, tries to scroll to selected line.\n        if (!selectedLine) {\n            selectedLine = document.querySelector(\".o_barcode_line.o_highlight\");\n        }\n\n        let locationLine = false;\n        if (this.env.model.lastScanned.sourceLocation) {\n            const locId = this.env.model.lastScanned.sourceLocation.id;\n            locationLine = document.querySelector(\n                `.o_barcode_location_line[data-location-id=\"${locId}\"]`\n            );\n        } else if (selectedLine) {\n            locationLine = selectedLine\n                .closest(\".o_barcode_location_group\")\n                .querySelector(\".o_barcode_location_line\");\n        }\n        // Scrolls either to the selected line, either to the location line.\n        targetElement = selectedLine || (locationLine && locationLine.parentElement);\n\n        if (targetElement) {\n            // If a line is selected, checks if this line is on the top of the\n            // page, and if it's not, scrolls until the line is on top.\n            const elRect = targetElement.getBoundingClientRect();\n            const page = document.querySelector(\".o_barcode_lines\");\n            const headerHeight = this._getHeaderHeight();\n            if (elRect.top < headerHeight || elRect.bottom > headerHeight + elRect.height) {\n                let top = elRect.top - headerHeight + page.scrollTop;\n                if (isSubline) {\n                    const parentLine = targetElement\n                        .closest(\".o_sublines\")\n                        .closest(\".o_barcode_line\");\n                    const parentSummary = parentLine.querySelector(\".o_barcode_line_summary\");\n                    top -= parentSummary.getBoundingClientRect().height;\n                } else if (selectedLine && locationLine) {\n                    top -= locationLine.getBoundingClientRect().height;\n                }\n                page.scroll({ left: 0, top, behavior: this._scrollBehavior });\n                this._scrollBehavior = \"smooth\";\n            }\n        }\n    }\n\n    async _onDoAction(ev) {\n        this.action.doAction(ev.detail, {\n            onClose: this._onRefreshState.bind(this),\n        });\n    }\n\n    onOpenPackage(packageIds) {\n        this._inspectedPackageIds = packageIds;\n        this.changeView(\"packagePage\");\n    }\n\n    async newScrapProduct() {\n        await this.env.model.save();\n        this.changeView(\"scrapProductPage\");\n    }\n\n    get displayOperationButtons() {\n        const { model } = this.env;\n        return (\n            model.canScrap ||\n            model.displayCancelButton ||\n            model.displaySignatureButton ||\n            model.displayReturnButton\n        );\n    }\n\n    get scrapViewProps() {\n        const context = this.env.model.scrapContext;\n        return {\n            resModel: \"stock.scrap\",\n            context: context,\n            viewId: this.env.model.scrapViewId,\n            display: { controlPanel: false },\n            type: \"form\",\n            onSave: () => this.toggleBarcodeLines(),\n            onDiscard: () => this.toggleBarcodeLines(),\n        };\n    }\n\n    async onOpenProductPage(line) {\n        await this.env.model.save();\n        if (line) {\n            const virtualId = line.virtual_id;\n            // Updates the line id if it's missing, in order to open the line form view.\n            if (!line.id && virtualId) {\n                line = this.env.model.pageLines.find((l) => l.dummy_id === virtualId);\n            }\n            this._editedLineParams = this.env.model.getEditedLineParams(line);\n        }\n        this.changeView(\"productPage\");\n    }\n\n    async _onRefreshState(paramsRefresh) {\n        const { recordId, lineId } = paramsRefresh || {};\n        const { route, params } = this.env.model.getActionRefresh(recordId);\n        const result = await rpc(route, params);\n        await this.env.model.refreshCache(result.data.records);\n        await this.toggleBarcodeLines(lineId);\n        this.render();\n    }\n\n    /**\n     * Handles triggered warnings. It can happen from an onchange for example.\n     *\n     * @param {CustomEvent} ev\n     */\n    _onWarning(ev) {\n        const { title, message } = ev.detail;\n        this.env.services.dialog.add(ConfirmationDialog, { title, body: message });\n    }\n}\n\nregistry.category(\"actions\").add(\"stock_barcode_client_action\", MainComponent);\n\nexport default MainComponent;\n", "import LineComponent from \"./line\";\n\nexport default class PackageLineComponent extends LineComponent {\n    static props = [\"displayUOM\", \"line\", \"openPackage\"];\n    static template = \"stock_barcode.PackageLineComponent\";\n\n    get isComplete() {\n        return this.qtyDone == this.qtyDemand;\n    }\n\n    get isSelected() {\n        return this.line.package_id.id === this.env.model.lastScanned.packageId;\n    }\n\n    get qtyDemand() {\n        return this.props.line.reserved_uom_qty ? 1 : 0;\n    }\n\n    get qtyDone() {\n        const reservedQuantity = this.line.lines.reduce((r, l) => r + l.reserved_uom_qty, 0);\n        const doneQuantity = this.line.lines.reduce((r, l) => r + l.qty_done, 0);\n        if (reservedQuantity > 0) {\n            return doneQuantity / reservedQuantity;\n        }\n        return doneQuantity > 0 ? 1 : 0;\n    }\n\n    get packageLabel() {\n        if (this.line.isPackageLine && this.line.package_id.parent_package_id) {\n            // Need to recompute the result package \"complete_name\" since it is not recomputed when unpack.\n            const currentResultFullPackageName = `${this.line.package_id.parent_package_id.name} > ${this.line.result_package_id.name}`;\n            if (\n                this.line.package_id.complete_name === currentResultFullPackageName &&\n                this.line.outermost_result_package_id\n            ) {\n                return this.line.package_id.parent_package_id.name;\n            }\n        }\n        return super.packageLabel;\n    }\n\n    get resultPackageLabel() {\n        if (this.line.isPackageLine) {\n            if (this.line.outermost_result_package_id) {\n                return this.line.outermost_result_package_id.name;\n            }\n            return this.line.result_package_id.name;\n        }\n        return super.resultPackageLabel;\n    }\n\n    select(ev) {\n        ev.stopPropagation();\n        this.env.model.selectPackageLine(this.line);\n        this.env.model.trigger(\"update\");\n    }\n\n    openPackage() {\n        let packageIds = [this.line.package_id.id];\n        if (this.line.lines) {\n            packageIds = Array.from(new Set(this.line.lines.map((line) => line.package_id.id)));\n        }\n        return this.props.openPackage(packageIds);\n    }\n\n    unpack() {\n        this.env.model.unpack(this.line.lines);\n        this.env.model.trigger(\"update\");\n    }\n}\n", "import { Component } from \"@odoo/owl\";\nimport { Dialog } from \"@web/core/dialog/dialog\";\n\nexport class ProductImageDialog extends Component {\n    static components = { Dialog };\n    static props = {\n        record: Object,\n        close: Function,\n    };\n    static template = \"stock_barcode.ProductImageDialog\";\n\n    setup() {\n        this.source = `/web/image/product.product/${this.props.record.id}/image_1024`;\n        this.title = this.props.record.display_name;\n    }\n}\n", "import { ConfirmationDialog } from \"@web/core/confirmation_dialog/confirmation_dialog\";\nimport { registry } from \"@web/core/registry\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { user } from \"@web/core/user\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { formView } from \"@web/views/form/form_view\";\nimport { FormController } from \"@web/views/form/form_controller\";\nimport { onWillStart } from \"@odoo/owl\";\n\nexport class StockBarcodeSmlFormController extends FormController {\n    setup() {\n        super.setup();\n        this.dialogService = useService(\"dialog\");\n        this.orm = useService(\"orm\");\n        onWillStart(async () => {\n            this.locationsEnabled = await user.hasGroup(\"stock.group_stock_multi_locations\");\n        });\n    }\n\n    mustCheckQuantityAvailableInLocation(data) {\n        return (\n            data.product_id &&\n            this.locationsEnabled &&\n            data.qty_done > 0 &&\n            data.picking_code &&\n            data.picking_code !== \"incoming\"\n        );\n    }\n\n    /**\n     * @override\n     */\n    async beforeExecuteActionButton(clickParams) {\n        let proceed = true;\n        if (clickParams.special && clickParams.special === \"save\") {\n            const { data } = this.model.root;\n            if (this.mustCheckQuantityAvailableInLocation(data)) {\n                const context = {\n                    location: data.location_id.id,\n                    lot_id: data.lot_id ? data.lot_id.id : false,\n                    package_id: data.package_id ? data.package_id.id : false,\n                    owner_id: data.owner_id ? data.owner_id.id : false,\n                    strict: true,\n                };\n                const [{ qty_available }] = await this.orm.searchRead(\n                    \"product.product\",\n                    [[\"id\", \"=\", data.product_id.id]],\n                    [\"qty_available\"],\n                    { context, limit: 1 }\n                );\n                if (!qty_available) {\n                    proceed = await new Promise((resolve) => {\n                        this.dialogService.add(ConfirmationDialog, {\n                            body: _t(\n                                \"Oops! It seems that this product is not located in %(location)s.\\nDo you confirm you picked from there?\",\n                                { location: data.location_id.display_name }\n                            ),\n                            confirmLabel: _t(\"Confirm\"),\n                            confirm: () => resolve(true),\n                            cancelLabel: _t(\"Discard\"),\n                            cancel: () => resolve(false),\n                        });\n                    });\n                }\n            }\n        }\n        return proceed && super.beforeExecuteActionButton(...arguments);\n    }\n}\n\nexport const stockBarcodeSmlFormView = {\n    ...formView,\n    Controller: StockBarcodeSmlFormController,\n};\n\nregistry.category(\"views\").add(\"stock_barcode_sml_form\", stockBarcodeSmlFormView);\n", "import { KanbanController } from \"@web/views/kanban/kanban_controller\";\nimport { useBus, useService } from \"@web/core/utils/hooks\";\nimport { markup, onMounted } from \"@odoo/owl\";\n\nexport class StockBarcodeKanbanController extends KanbanController {\n    setup() {\n        super.setup(...arguments);\n        this.barcodeService = useService(\"barcode\");\n        useBus(this.barcodeService.bus, \"barcode_scanned\", (ev) =>\n            this._onBarcodeScannedHandler(ev.detail.barcode)\n        );\n        onMounted(() => {\n            document.activeElement.blur();\n        });\n    }\n\n    openRecord(record) {\n        this.actionService.doAction(\"stock_barcode.stock_barcode_picking_client_action\", {\n            additionalContext: { active_id: record.resId },\n        });\n    }\n\n    async createRecord() {\n        const action = await this.model.orm.call(\"stock.picking\", \"action_open_new_picking\", [], {\n            context: this.props.context,\n        });\n        if (action) {\n            return this.actionService.doAction(action);\n        }\n        return super.createRecord(...arguments);\n    }\n\n    // --------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n     * Called when the user scans a barcode.\n     *\n     * @param {String} barcode\n     */\n    async _onBarcodeScannedHandler(barcode) {\n        const kwargs = { barcode, context: this.props.context };\n        const res = await this.model.orm.call(this.props.resModel, \"filter_on_barcode\", [], kwargs);\n        const action = res.action;\n        if (action?.help) {\n            action.help = markup(action.help);\n            this.actionService.doAction(action);\n        } else if (res.warning) {\n            const params = { title: res.warning.title, type: \"danger\" };\n            this.model.notification.add(res.warning.message, params);\n        }\n    }\n}\n", "import { KanbanRenderer } from \"@web/views/kanban/kanban_renderer\";\nimport { ManualBarcodeScanner } from \"@barcodes/components/manual_barcode\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { user } from \"@web/core/user\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { markup, onWillStart } from \"@odoo/owl\";\n\nexport class StockBarcodeKanbanRenderer extends KanbanRenderer {\n    static template = \"stock_barcode.KanbanRenderer\";\n    setup() {\n        super.setup(...arguments);\n        this.barcodeService = useService(\"barcode\");\n        this.dialogService = useService(\"dialog\");\n        this.resModel = this.props.list.model.config.resModel;\n        this.displayTransferProtip = this.resModel === \"stock.picking\";\n        onWillStart(this.onWillStart);\n    }\n\n    openManualBarcodeDialog() {\n        this.dialogService.add(ManualBarcodeScanner, {\n            facingMode: \"environment\",\n            onResult: (barcode) => {\n                this.barcodeService.bus.trigger(\"barcode_scanned\", { barcode });\n            },\n            onError: () => {},\n        });\n    }\n\n    async onWillStart() {\n        const groups = [\"stock.group_tracking_lot\", \"stock.group_production_lot\", \"uom.group_uom\"];\n        const hasGroups = await Promise.all(groups.map((g) => user.hasGroup(g)));\n        this.packageEnabled = hasGroups[0];\n        this.trackingEnabled = hasGroups[1];\n        this.uomEnabled = hasGroups[2];\n    }\n\n    get transferTip() {\n        const tags = { bold_s: markup`<b>`, bold_e: markup`</b>` };\n\n        if (this.trackingEnabled) {\n            if (this.packageEnabled) {\n                if (this.uomEnabled) {\n                    return _t(\n                        \"Scan a %(bold_s)s transfer%(bold_e)s, a %(bold_s)s product%(bold_e)s, a %(bold_s)s lot%(bold_e)s, a %(bold_s)s packaging%(bold_e)s, or a %(bold_s)s package%(bold_e)s to filter your records\",\n                        tags\n                    );\n                }\n                return _t(\n                    \"Scan a %(bold_s)s transfer%(bold_e)s, a %(bold_s)s product%(bold_e)s, a %(bold_s)s lot%(bold_e)s, or a %(bold_s)s package%(bold_e)s to filter your records\",\n                    tags\n                );\n            } else if (this.uomEnabled) {\n                return _t(\n                    \"Scan a %(bold_s)s transfer%(bold_e)s, a %(bold_s)s product%(bold_e)s, a %(bold_s)s lot%(bold_e)s, or a %(bold_s)s packaging%(bold_e)s to filter your records\",\n                    tags\n                );\n            }\n            return _t(\n                \"Scan a %(bold_s)s transfer%(bold_e)s, a %(bold_s)s product%(bold_e)s, or a %(bold_s)s lot%(bold_e)s to filter your records\",\n                tags\n            );\n        } else if (this.packageEnabled) {\n            if (this.uomEnabled) {\n                return _t(\n                    \"Scan a %(bold_s)s transfer%(bold_e)s, a %(bold_s)s product%(bold_e)s, a %(bold_s)s packaging%(bold_e)s, or a %(bold_s)s package%(bold_e)s to filter your records\",\n                    tags\n                );\n            }\n            return _t(\n                \"Scan a %(bold_s)s transfer%(bold_e)s, a %(bold_s)s product%(bold_e)s, or a %(bold_s)s package%(bold_e)s to filter your records\",\n                tags\n            );\n        } else if (this.uomEnabled) {\n            return _t(\n                \"Scan a %(bold_s)s transfer%(bold_e)s, a %(bold_s)s product%(bold_e)s, or a %(bold_s)s packaging%(bold_e)s to filter your records\",\n                tags\n            );\n        }\n        return _t(\n            \"Scan a %(bold_s)s transfer%(bold_e)s or a %(bold_s)s product%(bold_e)s to filter your records\",\n            tags\n        );\n    }\n}\n", "import { kanbanView } from \"@web/views/kanban/kanban_view\";\nimport { registry } from \"@web/core/registry\";\nimport { StockBarcodeKanbanController } from \"./stock_barcode_kanban_controller\";\nimport { StockBarcodeKanbanRenderer } from \"./stock_barcode_kanban_renderer\";\n\nexport const stockBarcodeKanbanView = Object.assign({}, kanbanView, {\n    Controller: StockBarcodeKanbanController,\n    Renderer: StockBarcodeKanbanRenderer,\n});\nregistry.category(\"views\").add(\"stock_barcode_list_kanban\", stockBarcodeKanbanView);\n", "import { rpc } from \"@web/core/network/rpc\";\n\nexport default class LazyBarcodeCache {\n    constructor(cacheData) {\n        this.dbIdCache = {}; // Cache by model + id\n        this.dbBarcodeCache = {}; // Cache by model + barcode\n        this.dbQuantCache = {}; // Cache by model + quant_id\n        this.missingBarcodesCache = new Set();\n        this.missingBarcodeKeyCache = new Set(); // Used as a cache by `_getMissingRecord`\n        this.barcodeFieldByModel = {\n            \"stock.location\": \"barcode\",\n            \"product.product\": \"barcode\",\n            \"product.uom\": \"barcode\",\n            \"stock.package.type\": \"barcode\",\n            \"stock.picking\": \"name\",\n            \"stock.package\": \"name\",\n            \"stock.lot\": \"name\", // Also ref, should take in account multiple fields ?\n        };\n        this.gs1LengthsByModel = {\n            \"product.product\": 14,\n            \"product.uom\": 14,\n            \"stock.location\": 13,\n            \"stock.package\": 18,\n        };\n        // If there is only one active barcode nomenclature, set the cache to be compliant with it.\n        if (cacheData[\"barcode.nomenclature\"].length === 1) {\n            this.nomenclature = cacheData[\"barcode.nomenclature\"][0];\n        }\n        this.setCache(cacheData);\n        this.waitingFetch = [];\n    }\n\n    /**\n     * Adds records to the barcode application's cache.\n     *\n     * @param {Object} cacheData each key is a model's name and contains an array of records.\n     */\n    setCache(cacheData) {\n        for (const model in cacheData) {\n            const records = cacheData[model];\n            // Adds the model's key in the cache's DB.\n            if (this.dbIdCache[model] === undefined) {\n                this.dbIdCache[model] = {};\n            }\n            if (this.dbBarcodeCache[model] === undefined) {\n                this.dbBarcodeCache[model] = {};\n            }\n            // Adds the record in the cache.\n            const barcodeField = this._getBarcodeField(model);\n            for (const record of records) {\n                this.dbIdCache[model][record.id] = record;\n                if (model === \"stock.quant\") {\n                    const { product_id, location_id } = record;\n                    if (!this.dbQuantCache[product_id]) {\n                        this.dbQuantCache[product_id] = {};\n                    }\n                    if (!this.dbQuantCache[product_id][location_id]) {\n                        this.dbQuantCache[product_id][location_id] = [];\n                    }\n                    const matchIndex = this.dbQuantCache[product_id][location_id].findIndex(\n                        (rec) => rec.id === record.id\n                    );\n                    if (matchIndex !== -1) {\n                        this.dbQuantCache[product_id][location_id][matchIndex] = record;\n                    } else {\n                        this.dbQuantCache[product_id][location_id].push(record);\n                    }\n                } else if (model === \"product.product\" && cacheData[\"stock.quant\"]) {\n                    if (!this.dbQuantCache[record.id]) {\n                        this.dbQuantCache[record.id] = {};\n                    }\n                }\n                if (barcodeField) {\n                    const barcode = record[barcodeField];\n                    if (!this.dbBarcodeCache[model][barcode]) {\n                        this.dbBarcodeCache[model][barcode] = [];\n                    }\n                    if (!this.dbBarcodeCache[model][barcode].includes(record.id)) {\n                        this.dbBarcodeCache[model][barcode].push(record.id);\n                        if (\n                            this.nomenclature &&\n                            this.nomenclature.is_gs1_nomenclature &&\n                            this.gs1LengthsByModel[model]\n                        ) {\n                            this._setBarcodeInCacheForGS1(barcode, model, record);\n                        }\n                    }\n                }\n            }\n        }\n    }\n\n    /**\n     * Get record from the cache, throw a error if we don't find in the cache\n     * (the server should have return this information).\n     *\n     * @param {int} id id of the record\n     * @param {string} model model_name of the record\n     * @param {boolean} [copy=true] if true, returns a deep copy (to avoid to write the cache)\n     * @returns copy of the record send by the server (fields limited to _get_fields_stock_barcode)\n     */\n    getRecord(model, id, raiseErrorIfMissing = true) {\n        if (this.dbIdCache[model] === undefined) {\n            if (raiseErrorIfMissing) {\n                throw new Error(`Model ${model} doesn't exist in the cache`);\n            }\n            return null;\n        }\n        if (this.dbIdCache[model][id] === undefined) {\n            if (raiseErrorIfMissing) {\n                throw new Error(\n                    `Record ${model} with id=${id} doesn't exist in the cache, it should return by the server`\n                );\n            }\n            return null;\n        }\n        const record = this.dbIdCache[model][id];\n        return JSON.parse(JSON.stringify(record));\n    }\n\n    async getQuants(product, location_id, params = {}) {\n        const lot_id = params.lot_id?.id || params.lot_id || false;\n        const package_id = params.package_id?.id || params.package_id || false;\n        const { lot_name, owner_id = false } = params;\n        let quantsByProduct = this.dbQuantCache[product.id];\n\n        if (!quantsByProduct) {\n            const domain = [\n                [\"product_id\", \"=\", product.id],\n                [\"location_id.usage\", \"=\", \"internal\"],\n            ];\n            const result = await rpc(\"/stock_barcode/get_quants\", { domain });\n            if (result) {\n                this.setCache(result.records);\n                quantsByProduct = this.dbQuantCache[product.id];\n                if (!quantsByProduct) {\n                    // No quant found after fetch.\n                    this.dbQuantCache[product.id] = [];\n                    return [];\n                }\n            }\n        }\n        let quants = [];\n        if (location_id) {\n            const quantsByLocation = quantsByProduct[location_id];\n            if (quantsByLocation) {\n                quants.push(...quantsByLocation);\n            } else {\n                this.dbQuantCache[product.id][location_id] = [];\n            }\n        } else {\n            for (const quantsByLocation of Object.values(quantsByProduct)) {\n                quants.push(...quantsByLocation);\n            }\n        }\n        if (!lot_id && !lot_name && !package_id && !owner_id) {\n            // Return all product's quants in the given location.\n            return quants;\n        }\n        // Return only the quant with the right lot, package, and/or owner, or no quant at all.\n        if (lot_id) {\n            quants = quants.filter((quant) => quant.lot_id === lot_id);\n        } else if (lot_name) {\n            const filters = { \"stock.lot\": { product_id: product.id } };\n            const lot = await this.getRecordByBarcode(lot_name, \"stock.lot\", filters);\n            if (!lot) {\n                // If there is no existing lot, there is no existing quant.\n                return [];\n            }\n            quants = quants.filter((quant) => quant.lot_id === lot.id);\n        }\n        if (owner_id) {\n            quants = quants.filter((quant) => quant.owner_id === owner_id);\n        }\n        if (package_id) {\n            quants = quants.filter((quant) => quant.package_id === package_id);\n        }\n        return quants;\n    }\n\n    /**\n     * @param {string} barcode barcode to match with a record\n     * @param {string} [model] model name of the record to match (if empty search on all models)\n     * @param {boolean} [onlyInCache] search only in the cache\n     * @param {Object} [filters]\n     * @returns copy of the record send by the server (fields limited to _get_fields_stock_barcode)\n     */\n    async getRecordByBarcode(barcode, model = false, options = {}) {\n        const onlyInCache = Boolean(options.onlyInCache);\n        const filters = options.filters || {};\n        const fetchLater = Boolean(options.fetchLater);\n        if (model) {\n            if (this.dbBarcodeCache[model] === undefined) {\n                if (fetchLater) {\n                    this.waitingFetch.push({ barcode, model, options });\n                    if (model === \"product.product\") {\n                        // If the model is a product, we can also try to fetch it as a packaging.\n                        this.waitingFetch.push({ barcode, model: \"product.uom\", options });\n                    }\n                    return null;\n                }\n                if (onlyInCache) {\n                    return null;\n                }\n                throw new Error(`Model ${model} doesn't exist in the cache`);\n            }\n            if (this.dbBarcodeCache[model][barcode] === undefined) {\n                if (fetchLater) {\n                    this.waitingFetch.push({ barcode, model, options });\n                    if (model === \"product.product\") {\n                        // If the model is a product, we can also try to fetch it as a packaging.\n                        this.waitingFetch.push({ barcode, model: \"product.uom\", options });\n                    }\n                    return null;\n                }\n                if (onlyInCache) {\n                    return null;\n                }\n                await this._getMissingRecord(barcode, model, filters);\n                return await this.getRecordByBarcode(barcode, model, {\n                    onlyInCache: true,\n                    filters,\n                });\n            }\n            const ids = this.dbBarcodeCache[model][barcode];\n            for (const id of ids) {\n                const record = this.getRecord(model, id);\n                let pass = true;\n                if (filters[model]) {\n                    const fields = Object.keys(filters[model]);\n                    for (const field of fields) {\n                        if (record[field] != filters[model][field]) {\n                            pass = false;\n                            break;\n                        }\n                    }\n                }\n                if (pass) {\n                    return record;\n                }\n            }\n        } else {\n            const result = new Map();\n            // Returns object {model: record} of possible record.\n            const models = Object.keys(this.dbBarcodeCache);\n            for (const model of models) {\n                if (this.dbBarcodeCache[model][barcode]) {\n                    const ids = this.dbBarcodeCache[model][barcode];\n                    for (const id of ids) {\n                        const record = this.dbIdCache[model][id];\n                        let pass = true;\n                        if (filters[model]) {\n                            const fields = Object.keys(filters[model]);\n                            for (const field of fields) {\n                                if (record[field] != filters[model][field]) {\n                                    pass = false;\n                                    break;\n                                }\n                            }\n                        }\n                        if (pass) {\n                            result.set(model, JSON.parse(JSON.stringify(record)));\n                            break;\n                        }\n                    }\n                }\n            }\n            if (result.size < 1) {\n                if (onlyInCache) {\n                    return result;\n                }\n                await this._getMissingRecord(barcode, model, filters);\n                return await this.getRecordByBarcode(barcode, model, {\n                    onlyInCache: true,\n                    filters,\n                });\n            }\n            return result;\n        }\n    }\n\n    _getBarcodeField(model) {\n        if (this.barcodeFieldByModel[model] === undefined) {\n            return null;\n        }\n        return this.barcodeFieldByModel[model];\n    }\n\n    async _getMissingRecord(barcode, model, filters = {}) {\n        const keyCache = JSON.stringify([...arguments]);\n        const missCache = this.missingBarcodeKeyCache;\n        const keyCacheWithoutModel = JSON.stringify([barcode, false, {}]);\n        if (filters) {\n            // If we already tried to find the same model's record for the given barcode but\n            // without the filters, there is no need to try again with the filter.\n            const keyCacheWithoutFilters = JSON.stringify([barcode, model, {}]);\n            if (missCache.has(keyCacheWithoutFilters)) {\n                return false;\n            }\n        }\n        // Check if we already try to fetch this missing record.\n        if (missCache.has(keyCache) || missCache.has(keyCacheWithoutModel)) {\n            return false;\n        }\n        const params = {};\n        if (model) {\n            params.barcodes_by_model = { [model]: [barcode] };\n        } else {\n            params.barcode = barcode;\n        }\n        // Creates and passes a domain if some filters are provided.\n        const domainsByModel = {};\n        for (const filter of Object.entries(filters)) {\n            const modelName = filter[0];\n            const filtersByField = filter[1];\n            domainsByModel[modelName] = [];\n            for (const filterByField of Object.entries(filtersByField)) {\n                if (filterByField[1] instanceof Array) {\n                    domainsByModel[modelName].push([filterByField[0], \"in\", filterByField[1]]);\n                } else {\n                    domainsByModel[modelName].push([filterByField[0], \"=\", filterByField[1]]);\n                }\n            }\n        }\n        params.domains_by_model = domainsByModel;\n        const result = await rpc(\"/stock_barcode/get_specific_barcode_data\", params);\n        this.setCache(result);\n        missCache.add(keyCache);\n    }\n\n    async getMissingRecords(params = {}) {\n        if (!this.waitingFetch.length) {\n            return; // Nothing to fetch.\n        }\n        params.barcodes_by_model = {};\n        for (const data of this.waitingFetch) {\n            const { barcode, model } = data;\n            const keyCache = JSON.stringify([barcode, model, {}]);\n            if (this.missingBarcodeKeyCache.has(keyCache)) {\n                continue; // Avoid already fetched records.\n            }\n            this.missingBarcodeKeyCache.add(keyCache);\n            if (!params.barcodes_by_model[model]) {\n                params.barcodes_by_model[model] = [];\n            }\n            params.barcodes_by_model[model].push(barcode);\n        }\n        if (Object.keys(params.barcodes_by_model).length) {\n            const result = await rpc(\"/stock_barcode/get_specific_barcode_data\", params);\n            this.setCache(result);\n            if (params.forceUnrestrictedSearch) {\n                // Create a list of every found records' barcode.\n                const foundBarcodes = [];\n                const missingBarcodes = new Set();\n                for (const model of Object.keys(result)) {\n                    for (const record of result[model]) {\n                        foundBarcodes.push(record[this.barcodeFieldByModel[model]]);\n                    }\n                }\n                // Put every searched barcodes with no matching records into a set.\n                for (const model of Object.keys(params.barcodes_by_model)) {\n                    for (const barcode of params.barcodes_by_model[model]) {\n                        if (\n                            !foundBarcodes.includes(barcode) &&\n                            !this.missingBarcodesCache.has(barcode)\n                        ) {\n                            missingBarcodes.add(barcode);\n                            this.missingBarcodesCache.add(barcode);\n                        }\n                    }\n                }\n                // If there are barcodes with no match, make a second RPC but this\n                // time, search for those barcodes with no assigned model.\n                if (missingBarcodes.size) {\n                    const barcodes = [...missingBarcodes];\n                    // Keep in cache the fact those barcodes were search so they won't be in the future.\n                    for (const bc of barcodes) {\n                        this.missingBarcodeKeyCache.add(JSON.stringify([bc, false, {}]));\n                    }\n                    const updatedParams = { ...params, barcodes };\n                    delete updatedParams.barcodes_by_model;\n                    const notRestrictedByModelResult = await rpc(\n                        \"/stock_barcode/get_specific_barcode_data\",\n                        updatedParams\n                    );\n                    this.setCache(notRestrictedByModelResult);\n                }\n            }\n        }\n        this.waitingFetch = [];\n    }\n\n    /**\n     * Sets in the cache an entry for the given record with its formatted barcode as key.\n     * The barcode will be formatted (if needed) at the length corresponding to its data part in a\n     * GS1 barcode (e.g.: 14 digits for a product's barcode) by padding with 0 the original barcode.\n     * That makes it easier to find when a GS1 barcode is scanned.\n     * If the formatted barcode is similar to an another barcode for the same model, it will show a\n     * warning in the console (as a clue to find where issue could come from, not to alert the user)\n     *\n     * @param {string} barcode\n     * @param {string} model\n     * @param {Object} record\n     */\n    _setBarcodeInCacheForGS1(barcode, model, record) {\n        const length = this.gs1LengthsByModel[model];\n        if (!barcode || barcode.length >= length || isNaN(Number(barcode))) {\n            // Barcode already has the good length, or is too long or isn't\n            // fully numerical (and so, it doesn't make sense to adapt it).\n            return;\n        }\n        const paddedBarcode = barcode.padStart(length, \"0\");\n        // Avoids to override or mix records if there is already a key for this\n        // barcode (which means there is a conflict somewhere).\n        if (!this.dbBarcodeCache[model][paddedBarcode]) {\n            this.dbBarcodeCache[model][paddedBarcode] = [record.id];\n        } else if (!this.dbBarcodeCache[model][paddedBarcode].includes(record.id)) {\n            const previousRecordId = this.dbBarcodeCache[model][paddedBarcode][0];\n            const previousRecord = this.getRecord(model, previousRecordId);\n            console.log(\n                `Conflict for barcode %c${paddedBarcode}%c:`,\n                \"font-weight: bold\",\n                \"\",\n                `it could refer for both ${record.display_name} and ${previousRecord.display_name}.`,\n                `\\nThe last one will be used but consider to edit those products barcode to avoid error due to ambiguities.`\n            );\n        }\n    }\n}\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { ConfirmationDialog } from \"@web/core/confirmation_dialog/confirmation_dialog\";\nimport { registry } from \"@web/core/registry\";\nimport { useBus, useService } from \"@web/core/utils/hooks\";\nimport { Component, onWillStart, useState, markup } from \"@odoo/owl\";\nimport { ManualBarcodeScanner } from \"@barcodes/components/manual_barcode\";\nimport { standardActionServiceProps } from \"@web/webclient/actions/action_service\";\nimport { url } from \"@web/core/utils/urls\";\n\nexport class MainMenu extends Component {\n    static props = { ...standardActionServiceProps };\n    static components = {};\n    static template = \"stock_barcode.MainMenu\";\n\n    setup() {\n        const displayDemoMessage = this.props.action.params.message_demo_barcodes;\n        this.actionService = useService(\"action\");\n        this.dialogService = useService(\"dialog\");\n        this.pwaService = useService(\"pwa\");\n        this.home = useService(\"home_menu\");\n        this.notificationService = useService(\"notification\");\n        this.state = useState({ displayDemoMessage });\n        this.barcodeService = useService(\"barcode\");\n        useBus(this.barcodeService.bus, \"barcode_scanned\", (ev) =>\n            this._onBarcodeScanned(ev.detail.barcode)\n        );\n\n        onWillStart(async () => {\n            const data = await rpc(\"/stock_barcode/get_main_menu_data\");\n            this.locationsEnabled = data.groups.locations;\n            this.packagesEnabled = data.groups.package;\n            this.trackingEnabled = data.groups.tracking;\n            this.quantCount = data.quant_count;\n            this.soundEnable = data.play_sound;\n            if (this.soundEnable) {\n                const fileExtension = new Audio().canPlayType(\"audio/ogg; codecs=vorbis\")\n                    ? \"ogg\"\n                    : \"mp3\";\n                this.sounds = {\n                    success: new Audio(\n                        url(`/stock_barcode/static/src/audio/success.${fileExtension}`)\n                    ),\n                };\n                this.sounds.success.load();\n            }\n        });\n    }\n\n    logout() {\n        const path = `/web/session/logout${\n            this.pwaService.isScopedApp ? \"?redirect=scoped_app/barcode\" : \"\"\n        }`;\n        window.open(path, \"_self\");\n    }\n\n    openManualBarcodeDialog() {\n        let res;\n        let rej;\n        const promise = new Promise((resolve, reject) => {\n            res = resolve;\n            rej = reject;\n        });\n        this.dialogService.add(ManualBarcodeScanner, {\n            facingMode: \"environment\",\n            onResult: (barcode) => {\n                this._onBarcodeScanned(barcode);\n                res(barcode);\n            },\n            onError: (error) => rej(error),\n        });\n        promise.catch((error) => console.log(error));\n        return promise;\n    }\n\n    removeDemoMessage() {\n        const params = {\n            title: _t(\"Don't show this message again\"),\n            body: _t(\n                \"Do you want to permanently remove this message ? \" +\n                    \"It won't appear anymore, so make sure you don't need the barcodes sheet or you have a copy.\"\n            ),\n            confirm: () => {\n                rpc(\"/stock_barcode/rid_of_message_demo_barcodes\"); // Sets action message param false on server\n                this.state.displayDemoMessage = false; // Remove message from current view\n                this.props.action.params.message_demo_barcodes = false; // Remove message if using breadcrumbs\n            },\n            cancel: () => {},\n            confirmLabel: _t(\"Remove it\"),\n            cancelLabel: _t(\"Leave it\"),\n        };\n        this.dialogService.add(ConfirmationDialog, params);\n    }\n\n    playSound(soundName) {\n        if (this.soundEnable) {\n            this.sounds[soundName].currentTime = 0;\n            this.sounds[soundName].play().catch((error) => {\n                // `play` returns a promise. In case this promise is rejected (permission\n                // issue for example), catch it to avoid Odoo's `UncaughtPromiseError`.\n                this.soundEnable = false;\n                console.warn(error);\n            });\n        }\n    }\n\n    async _onBarcodeScanned(barcode) {\n        const res = await rpc(\"/stock_barcode/scan_from_main_menu\", { barcode });\n        if (res.action) {\n            this.playSound(\"success\");\n            return this.actionService.doAction(res.action);\n        }\n        this.notificationService.add(res.warning, { type: \"danger\" });\n    }\n\n    /** Builds the barcode landing page bullet points, decribing features available depending on settings */\n    get barcodeHomeHelper() {\n        const tags = {\n            bold_s: markup`<b>`,\n            bold_e: markup`</b>`,\n        };\n\n        const bullets = [\n            _t(\n                \"Scan a %(bold_s)sproduct%(bold_e)s or its %(bold_s)spackaging%(bold_e)s to locate it\",\n                tags\n            ),\n        ];\n        // 2nd bullet point depends on which setting is activated\n        if (this.packageEnabled && this.trackingEnabled) {\n            bullets.push(\n                _t(\n                    \"Scan a %(bold_s)stracking number%(bold_e)s or a %(bold_s)spackage%(bold_e)s to find a transfer\",\n                    tags\n                )\n            );\n        } else if (this.packageEnabled) {\n            bullets.push(_t(\"Scan a %(bold_s)spackage%(bold_e)s to find a transfer\", tags));\n        } else if (this.trackingEnabled) {\n            bullets.push(_t(\"Scan a %(bold_s)stracking number%(bold_e)s to find a transfer\", tags));\n        }\n        bullets.push(_t(\"Scan a %(bold_s)spicking%(bold_e)s to open it\", tags));\n        if (this.locationsEnabled) {\n            bullets.push(_t(\"Scan a %(bold_s)slocation%(bold_e)s to initiate a transfer\", tags));\n        }\n        bullets.push(_t(\"Scan an %(bold_s)soperation type%(bold_e)s to start it\", tags));\n        return bullets;\n    }\n\n    get demoMessage() {\n        const demo_link = _t(\"Download demo data sheet\");\n        const barcode_link = _t(\"Download operation barcodes\");\n\n        const sheet_s = markup`<a href=\"/stock_barcode/static/img/barcodes_demo.pdf\" target=\"_blank\" aria-label=\"${demo_link}\" title=\"${demo_link}\">`;\n        const ops_s = markup`<a href=\"/stock_barcode/print_inventory_commands?barcode_type=barcode_commands_and_operation_types\" target=\"_blank\" aria-label=\"${barcode_link}\" title=\"${barcode_link}\">`;\n        const sheet_e = markup`</a>`;\n        const ops_e = markup`</a>`;\n\n        return _t(\n            \"Print the %(sheet_s)sdemo data sheet%(sheet_e)s to test, or %(ops_s)sbarcodes%(ops_e)s for operations.\",\n            { sheet_s, sheet_e, ops_s, ops_e }\n        );\n    }\n}\n\nregistry.category(\"actions\").add(\"stock_barcode_main_menu\", MainMenu);\n", "import { BarcodeParser } from \"@barcodes/js/barcode_parser\";\nimport { Mutex } from \"@web/core/utils/concurrency\";\nimport { formatFloat } from \"@web/core/utils/numbers\";\nimport LazyBarcodeCache from \"@stock_barcode/lazy_barcode_cache\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { FNC1_CHAR } from \"@barcodes_gs1_nomenclature/js/barcode_parser\";\nimport { EventBus } from \"@odoo/owl\";\nimport { BarcodeObject } from \"../barcode_object\";\n\nexport default class BarcodeModel extends EventBus {\n    constructor(resModel, resId, services) {\n        super();\n        this.dialogService = useService(\"dialog\");\n        this.orm = services.orm;\n        this.notificationService = services.notification;\n        this.action = services.action;\n        this.resId = resId;\n        this.resModel = resModel;\n        this.unfoldLineKey = false;\n        this.currentSortIndex = 0;\n        this.validateContext = {};\n        // Keeps the history of all barcodes scanned (start with the most recent.)\n        this.scanHistory = [];\n        // Keeps track of list scanned record(s) by type.\n        this.lastScanned = { packageId: false, product: false, sourceLocation: false };\n        this._currentLocation = false; // Reminds the current source when the scanned one is forgotten.\n        this.needSourceConfirmation = false;\n        this.useTrackingNumber = true;\n        this.uriCache = new Set(); // Avoid to scan multiple times the same URI.\n        this.notificationCache = new Set(); // Avoid to display same notification\n    }\n\n    setData(data) {\n        this.actionId = data.actionId;\n        this.cache = new LazyBarcodeCache(data.data.records);\n        const nomenclature = this.cache.getRecord(\n            \"barcode.nomenclature\",\n            data.data.nomenclature_id\n        );\n        nomenclature.rules = [];\n        for (const ruleId of nomenclature.rule_ids) {\n            nomenclature.rules.push(this.cache.getRecord(\"barcode.rule\", ruleId));\n        }\n        this.parser = new BarcodeParser({ nomenclature });\n        BarcodeObject.setEnv(this.cache, this.parser);\n        this.scannedLinesVirtualId = [];\n\n        this.actionMutex = new Mutex();\n        this.config = data.data.config || {};\n        this.groups = data.groups;\n        this.groupingLinesEnabled = this.groups.group_production_lot;\n\n        this.packageTypes = [];\n        if (this.groups.group_tracking_lot) {\n            // Get the package types by barcode.\n            const packageTypes = this.cache.dbBarcodeCache[\"stock.package.type\"] || {};\n            for (const [barcode, ids] of Object.entries(packageTypes)) {\n                this.packageTypes.push([barcode, ids[0]]);\n            }\n        }\n\n        this._createState();\n        this.linesToSave = [];\n        this.selectedLineVirtualId = false;\n\n        // UI stuff.\n        this.name = this._getName();\n        // Barcode's commands are returned by a method for override purpose.\n        this.commands = this._getCommands();\n    }\n\n    // GETTER\n\n    getQtyDone(line) {\n        throw new Error(\"Not Implemented\");\n    }\n\n    getQtyDemand(line) {\n        throw new Error(\"Not Implemented\");\n    }\n\n    getDisplayCompletePackageBtn(line) {\n        return false;\n    }\n\n    getDisplayIncrementBtn(line) {\n        return true;\n    }\n\n    getDisplayIncrementBtnForSerial(line) {\n        return !(line.lot_id || line.lot_name) || this.getQtyDone(line) === 0;\n    }\n\n    getActionRefresh(newId) {\n        return {\n            route: \"/stock_barcode/get_barcode_data\",\n            params: { model: this.resModel, res_id: this.resId || false },\n        };\n    }\n\n    getIncrementQuantity(line) {\n        const remainingQty = this.getLineRemainingQuantity(line);\n        const params = { digits: [false, this.precision], thousandsSep: \"\", decimalPoint: \".\" };\n        return parseFloat(formatFloat(remainingQty, params));\n    }\n\n    getLineRemainingQuantity(line) {\n        return this.getQtyDemand(line) ? this.getQtyDemand(line) - this.getQtyDone(line) : 0;\n    }\n\n    getlotName(line) {\n        return (line.lot_id && line.lot_id.name) || line.lot_name || false;\n    }\n\n    getEditedLineParams(line) {\n        return { currentId: line.id };\n    }\n\n    async apply() {\n        throw new Error(\"Not Implemented\");\n    }\n\n    get barcodeInfo() {\n        throw new Error(\"Not Implemented\");\n    }\n\n    get canCreateNewLot() {\n        return true;\n    }\n\n    get canBeProcessed() {\n        return true;\n    }\n\n    /**\n     * The operation can be validated if there is at least one line.\n     * @returns {boolean}\n     */\n    get canBeValidate() {\n        return this.pageLines.length + this.packageLines.length;\n    }\n\n    get cancelLabel() {\n        return _t(\"Cancel\");\n    }\n\n    get canSelectLocation() {\n        return true;\n    }\n\n    get displayAddProductButton() {\n        return true;\n    }\n\n    get displayApplyButton() {\n        return false;\n    }\n\n    get displayCancelButton() {\n        return false;\n    }\n\n    get displaySignatureButton() {\n        return false;\n    }\n\n    get displayDestinationLocation() {\n        return false;\n    }\n\n    get displayReturnButton() {\n        return false;\n    }\n\n    get useScanDestinationLocation() {\n        return this.displayDestinationLocation;\n    }\n\n    get showReservedSns() {\n        return true;\n    }\n\n    get displayResultPackage() {\n        return false;\n    }\n\n    get displaySourceLocation() {\n        return this.groups.group_stock_multi_locations;\n    }\n\n    get useScanSourceLocation() {\n        return this.displaySourceLocation;\n    }\n\n    displayLineQtyDemand(line) {\n        return this.getQtyDemand(line);\n    }\n\n    groupKey(line) {\n        return `${line.product_id.id}_${line.location_id.id}`;\n    }\n\n    lineCannotBeGrouped(line) {\n        // Don't try to group a line who is not tracked or is already grouped.\n        return line.product_id.tracking === \"none\" || line.lines;\n    }\n\n    /**\n     * Returns the page's lines but with tracked products grouped by product id.\n     *\n     * @returns\n     */\n    get groupedLines() {\n        this.groupLines();\n        // Before to return the line, we sort them to have new lines always on\n        // top and complete lines always on the bottom.\n        return this._sortLine(this._groupedLines);\n    }\n\n    groupLines() {\n        this._groupedLines = [...this.pageLines];\n        if (this.groupingLinesEnabled) {\n            this._groupedLines = this._groupLines(this._groupedLines, \"parentLine\", this.groupKey);\n        }\n        return this._groupedLines;\n    }\n\n    _groupLines(lines, parentKeyString, groupKeyMethod, conditionalGrouping = true) {\n        const groupedLinesByKey = {};\n        for (let index = lines.length - 1; index >= 0; index--) {\n            const line = lines[index];\n            if (line[parentKeyString]) {\n                // Remove previous parent line's link.\n                delete line[parentKeyString];\n            }\n            if (conditionalGrouping && this.lineCannotBeGrouped(line)) {\n                continue;\n            }\n            const key = groupKeyMethod.call(this, line);\n            if (!groupedLinesByKey[key]) {\n                groupedLinesByKey[key] = [];\n            }\n            groupedLinesByKey[key].push(...lines.splice(index, 1));\n        }\n        for (const sublines of Object.values(groupedLinesByKey)) {\n            if (sublines.length === 1) {\n                lines.push(...sublines);\n                continue;\n            }\n            const groupedLine = this._groupSublines(sublines, parentKeyString);\n            lines.push(groupedLine);\n        }\n        return lines;\n    }\n\n    getLineLocation(line) {\n        // Determine whether to group lines by source or destination location.\n        return line.location_id;\n    }\n\n    get groupedLinesByLocation() {\n        const lines = [].concat(this.groupedLines, this.packageLines);\n        const linesByLocations = [];\n        const linesByLocation = {};\n        for (const line of lines) {\n            const lineLoc = this.getLineLocation(line);\n            if (!linesByLocation[lineLoc.id]) {\n                linesByLocation[lineLoc.id] = {\n                    location: lineLoc,\n                    lines: [],\n                };\n            }\n            if (!linesByLocations.includes(linesByLocation[lineLoc.id])) {\n                linesByLocations.push(linesByLocation[lineLoc.id]);\n            }\n            linesByLocation[lineLoc.id].lines.push(line);\n        }\n        // Sorts groups to ensure that locations will always follow the alphabetical order.\n        linesByLocations.sort((lblA, lblB) => {\n            const [locNameA, locNameB] = [lblA.location.display_name, lblB.location.display_name];\n            return locNameA < locNameB ? -1 : locNameA > locNameB ? 1 : 0;\n        });\n        return linesByLocations;\n    }\n\n    get highlightValidateButton() {\n        return false;\n    }\n\n    get isDone() {\n        return false;\n    }\n\n    get isCancelled() {\n        return false;\n    }\n\n    displaySetButton(_line) {\n        return false;\n    }\n\n    /**\n     * Say if the line quantity is not set. Only useful for the inventory adjustment.\n     *\n     * @param {Object} line\n     * @returns {boolean}\n     */\n    IsNotSet(line) {\n        return false;\n    }\n\n    get lastScannedLine() {\n        if (this.scannedLinesVirtualId.length) {\n            const virtualId = this.scannedLinesVirtualId[this.scannedLinesVirtualId.length - 1];\n            return this.currentState.lines.find((l) => l.virtual_id === virtualId);\n        }\n        return false;\n    }\n\n    lineCanBeDeleted(line) {\n        return !this.getQtyDemand(line);\n    }\n\n    lineIsFaulty(line) {\n        throw new Error(\"Not Implemented\");\n    }\n\n    lineIsTracked(line) {\n        return this.useTrackingNumber && line.product_id.tracking !== \"none\";\n    }\n\n    get location() {\n        if (this.lastScanned.sourceLocation) {\n            // Get last scanned location.\n            return this.cache.getRecord(\"stock.location\", this.lastScanned.sourceLocation.id);\n        }\n        // Get last defined source location (if applicable) or the default location.\n        return this._currentLocation || this._defaultLocation();\n    }\n    set location(location) {\n        this._currentLocation = location;\n        this.lastScanned.sourceLocation = location;\n    }\n\n    get pageLines() {\n        return this.currentState.lines;\n    }\n\n    get packageLines() {\n        return [];\n    }\n\n    get previousScannedLines() {\n        const lines = [];\n        const alreadyDone = [];\n        for (const virtualId of this.scannedLinesVirtualId) {\n            if (alreadyDone.includes(virtualId)) {\n                continue;\n            }\n            alreadyDone.push(virtualId);\n            const foundLine = this.currentState.lines.find((l) => l.virtual_id === virtualId);\n            if (foundLine) {\n                lines.push(foundLine);\n            }\n        }\n        if (this.groups.group_stock_packaging) {\n            lines.push(...this.previousScannedLinesByPackage);\n        }\n        return lines;\n    }\n\n    get previousScannedLinesByPackage() {\n        if (this.lastScanned.packageId) {\n            return this.currentState.lines.filter(\n                (l) => l.package_id && l.package_id.id === this.lastScanned.packageId\n            );\n        }\n        return [];\n    }\n\n    get printButtons() {\n        throw new Error(\"Not Implemented\");\n    }\n\n    get recordIds() {\n        return [this.resId];\n    }\n\n    get selectedLine() {\n        return (\n            this.selectedLineVirtualId &&\n            this.currentState.lines.find(\n                (l) => (l.dummy_id || l.virtual_id) === this.selectedLineVirtualId\n            )\n        );\n    }\n\n    get useExistingLots() {\n        return true;\n    }\n\n    get validateButtonLabel() {\n        return _t(\"Validate\");\n    }\n\n    // ACTIONS\n\n    /**\n     * @param {integer} [lineId] if provided it checks if the line still exist (selects it or removes it from the lines' list)\n     */\n    async displayBarcodeLines(lineId) {\n        if (lineId) {\n            // If we pass a record id checks if the record still exist.\n            const res = await this.orm.search(this.lineModel, [[\"id\", \"=\", lineId]]);\n            if (!res.length) {\n                // The record was deleted, we remove the corresponding line.\n                const lineIndex = this.currentState.lines.findIndex((l) => l.id == lineId);\n                this.currentState.lines.splice(lineIndex, 1);\n            } else {\n                // If it still exist, selects the record's line.\n                const line = this.currentState.lines.find((line) => line.id === lineId);\n                this.selectLine(line);\n            }\n        }\n    }\n\n    completePackage(line) {\n        throw new Error(\"Not Implemented\");\n    }\n\n    /**\n     * Searches for a line in the current source location. Will favor a line with no quantity\n     * (or less than expected) as we assume this kind of line still need to be processed.\n     * @returns {Object | Boolean} Returns a matching line or false.\n     */\n    findLineForCurrentLocation() {\n        if (!this.lastScanned.sourceLocation) {\n            return false; // Can't find anything if no location was scanned.\n        }\n        let foundLine = false;\n        for (const line of this.pageLines) {\n            if (line.location_id.id != this.lastScanned.sourceLocation.id) {\n                continue; // Not the same location.\n            }\n            const [qtyDone, qtyDemand] = [this.getQtyDone(line), this.getQtyDemand(line)];\n            if (qtyDone == 0 || (qtyDemand && qtyDone < qtyDemand)) {\n                return line.lot_id ? this._getParentLine(line) : line; // If the line still need to be processed, returns it immediately.\n            }\n            foundLine = !foundLine || qtyDone < this.getQtyDone(foundLine) ? line : foundLine;\n        }\n        return foundLine.lot_id ? this._getParentLine(foundLine) : foundLine;\n    }\n\n    /**\n     * Calls the notification service and plays a sound if the notification's type is \"warning\".\n     * @param {String} message\n     * @param {Object} options\n     */\n    notification(message, options = {}) {\n        if (this.notificationCache.has(message)) {\n            return; // Don't display the notification if it's already displayed.\n        }\n        this.notificationCache.add(message);\n        if (options.type === \"danger\") {\n            this.trigger(\"playSound\", \"error\");\n        }\n        return this.notificationService.add(message, options);\n    }\n\n    async refreshCache(records) {\n        this.cache.setCache(records);\n        this._createState();\n    }\n\n    beforeQuit() {\n        return this.save();\n    }\n\n    async save() {\n        const { route, params } = this._getSaveCommand();\n        if (route) {\n            const res = await rpc(route, params);\n            await this.refreshCache(res.records);\n        }\n        this.linesToSave = [];\n    }\n\n    selectLine(line) {\n        if (\n            this.lineCanBeSelected(line) &&\n            (!line.virtual_ids || !line.virtual_ids.includes(this.selectedLineVirtualId))\n        ) {\n            this._selectLine(line);\n        }\n    }\n\n    selectPackageLine(packageLine) {\n        if (this.lineCanBeSelected(packageLine)) {\n            this.lastScanned.packageId = packageLine.package_id.id;\n        }\n    }\n\n    toggleSublines(line) {\n        const lineKey = this.groupKey(line);\n        this.unfoldLineKey = this.unfoldLineKey === lineKey ? false : lineKey;\n        if (\n            this.unfoldLineKey === lineKey &&\n            (!this.selectedLine || this.unfoldLineKey != this.groupKey(this.selectedLine))\n        ) {\n            this.selectLine(line);\n        }\n        this.trigger(\"update\");\n    }\n\n    createSingleLinesForPackaging(barcodeData) {\n        return (\n            barcodeData.product.tracking === \"serial\" &&\n            barcodeData.packaging &&\n            (this.useExistingLots || this.canCreateNewLot)\n        );\n    }\n\n    async updateLine(line, args) {\n        let { location_id, lot_id, owner_id, package_id } = args;\n        if (!line) {\n            throw new Error(\"No line found\");\n        }\n        if (!line.product_id && args.product_id) {\n            line.product_id = args.product_id;\n            line.product_uom_id = this.cache.getRecord(\n                \"uom.uom\",\n                args.uom?.id || args.product_id.uom_id\n            );\n        }\n        if (location_id) {\n            if (typeof location_id === \"number\") {\n                location_id = this.cache.getRecord(\"stock.location\", args.location_id);\n            }\n            line.location_id = location_id;\n        }\n        if (lot_id) {\n            if (typeof lot_id === \"number\") {\n                lot_id = this.cache.getRecord(\"stock.lot\", args.lot_id);\n            }\n            line.lot_id = lot_id;\n        }\n        if (owner_id) {\n            if (typeof owner_id === \"number\") {\n                owner_id = this.cache.getRecord(\"res.partner\", args.owner_id);\n            }\n            line.owner_id = owner_id;\n        }\n        if (package_id) {\n            if (typeof package_id === \"number\") {\n                package_id = this.cache.getRecord(\"stock.package\", args.package_id);\n            }\n            const parentPackId = package_id.parent_package_id;\n            if (parentPackId && typeof parentPackId === \"number\") {\n                package_id.parent_package_id = this.cache.getRecord(\"stock.package\", parentPackId);\n            }\n            line.package_id = package_id;\n        }\n        if (args.lot_name && line.product_id.tracking !== \"none\") {\n            await this.updateLotName(line, args.lot_name);\n        }\n        this._updateLineQty(line, args);\n        this._markLineAsDirty(line);\n    }\n\n    /**\n     * Can be called by the user from the application. As the quantity field hasn't\n     * the same name for all models, this method must be overridden by each model.\n     *\n     * @param {number} virtualId\n     * @param {number} qty Quantity to increment (1 by default)\n     */\n    updateLineQty(virtualId, qty = 1) {\n        throw new Error(\"Not Implemented\");\n    }\n\n    async updateLotName(line, lotName) {\n        // Checks if the tracking number isn't already used.\n        for (const l of this.pageLines) {\n            if (\n                line.virtual_id === l.virtual_id ||\n                line.product_id.tracking !== \"serial\" ||\n                line.product_id.id !== l.product_id.id\n            ) {\n                continue;\n            }\n            if (lotName === l.lot_name || (l.lot_id && lotName === l.lot_id.name)) {\n                this.notification(_t(\"This serial number is already used.\"), { type: \"warning\" });\n                return;\n            }\n        }\n        await this._updateLotName(line, lotName);\n    }\n\n    async validate() {\n        await this.save();\n        const context = this.validateContext;\n        context[\"barcode_trigger\"] = true;\n        const action = await this.orm.call(this.resModel, this.validateMethod, [this.recordIds], {\n            context,\n        });\n        const options = {\n            onClose: (ev) => this._closeValidate(ev),\n        };\n        if (action && (action.res_model || action.type == \"ir.actions.client\")) {\n            if (action.type == \"ir.actions.client\") {\n                action.params = Object.assign(action.params || {}, options);\n            }\n            this.trigger(\"playSound\");\n            return this.action.doAction(action, options);\n        }\n        return options.onClose();\n    }\n\n    /**\n     * Check if the URI was already scanned.\n     * @param {String} uri\n     * @returns {Boolean}\n     */\n    uriInCache(uri) {\n        return this.uriCache.has(uri);\n    }\n\n    /**\n     * Sometimes, the model receives multiple barcodes as one single string.\n     * This method decomposes it into a list of barcodes.\n     * @param {String} barcode\n     * @returns {Array<String>}\n     */\n    splitBarcode(barcode) {\n        // If the barcode has multiple URI, separate them.\n        const matchedURI = [...barcode.matchAll(/urn:(?:[a-z0-9 -]+:){3} ?[0-9.]+/g)];\n        if (matchedURI.length > 1) {\n            return matchedURI.map((uri) => uri[0]);\n        }\n        // If the barcode contains the separator, split it.\n        const sepRegex = RegExp(this.config.barcode_separator_regex);\n        const splitBarcodes = barcode.split(sepRegex).filter((bc) => bc);\n        if (splitBarcodes.length > 1) {\n            return [...splitBarcodes];\n        }\n        return [barcode];\n    }\n\n    async processBarcode(barcode, options = {}) {\n        if (!barcode) {\n            return; // Do nothing if no barcode given.\n        }\n        const { readingRFID } = options;\n        const barcodes = this.splitBarcode(barcode);\n        if (barcodes.length > 1 && barcode === this._currentBarcode) {\n            // Scanning multiple barcodes at once can take some time and the user may be\n            // tempted to scan again, thinking that the barcodes weren't scanned.\n            // To avoid processing the same group of barcodes multiple times, we keep the\n            // last scanned group of barcodes in memory and nothing will be done if the barcode\n            // is scanned again while previous one is still in process.\n            return;\n        }\n        this._currentBarcode = barcode;\n\n        // Filters out already scanned URI.\n        const filteredBarcodes = [];\n        for (const bc of barcodes) {\n            const matchedURI = bc.match(/^urn:.*$/);\n            if (matchedURI && this.uriInCache(matchedURI[0])) {\n                continue;\n            }\n            filteredBarcodes.push(bc);\n        }\n\n        if (barcodes.length > 1 && !readingRFID) {\n            this.trigger(\"addBarcodesCountToProcess\", filteredBarcodes.length);\n        }\n        // Parse all barcodes.\n        const parsedBarcodes = [];\n        for (const bc of filteredBarcodes) {\n            const barcodeObject = BarcodeObject.forBarcode(bc);\n            await barcodeObject.setRecords();\n            parsedBarcodes.push(barcodeObject);\n        }\n        // Fetch all needed missing data and add them to the cache.\n        await this._getMissingRecords();\n\n        // Link parsed barcodes with missing information to the corresponding record(s).\n        const validBarcodes = [];\n        for (const barcodeObject of parsedBarcodes) {\n            if (barcodeObject.hasMissingRecords) {\n                await barcodeObject.setRecords();\n                if (\n                    barcodeObject.isURN &&\n                    barcodeObject.hasMissingRecords &&\n                    barcodeObject.missingRecords.find((mr) => mr.type === \"product\")\n                ) {\n                    // This barcode is linked to a product we don't have => We ignore it.\n                    // TODO: what to do with those barcodes ? Missing product => Barcode Lookup ?\n                    // TODO: already scanned SN should be managed here too ?\n                    this.trigger(\"updateBarcodesCountProcessed\");\n                    continue;\n                }\n            }\n            validBarcodes.push(barcodeObject);\n        }\n\n        this.actionMutex.exec(async () => {\n            for (const barcodeObject of validBarcodes) {\n                // TODO: use already parsed barcode in `_processBarcode` instead of parse it again.\n                await this._processBarcode(barcodeObject.rawValue);\n                this.trigger(\"updateBarcodesCountProcessed\");\n            }\n        });\n        this.postProcessBarcode();\n    }\n\n    /**\n     * Called after scanned barcodes were processed to do some cleanings.\n     */\n    postProcessBarcode() {\n        this.trigger(\"clearBarcodesCountProcessed\");\n        this.notificationCache.clear();\n        delete this._currentBarcode;\n    }\n\n    _getMissingRecordsParams() {\n        return {\n            context: { allowed_company_ids: [this._getCompanyId()] },\n            forceUnrestrictedSearch: !this.parser.nomenclature.is_gs1_nomenclature,\n        };\n    }\n\n    async _getMissingRecords() {\n        const params = this._getMissingRecordsParams();\n        await this.cache.getMissingRecords(params);\n    }\n\n    async getGs1Filters(gs1RulesData) {\n        const gs1Filters = {};\n        const productRule = gs1RulesData.find((bc) => bc.type === \"product\");\n        if (productRule) {\n            let product = await this.cache.getRecordByBarcode(productRule.value, \"product.product\");\n            if (!product) {\n                const packaging = await this.cache.getRecordByBarcode(\n                    productRule.value,\n                    \"product.uom\"\n                );\n                if (packaging) {\n                    product = this.cache.getRecord(\"product.product\", packaging.product_id);\n                }\n            }\n            if (product) {\n                gs1Filters[\"stock.lot\"] = { product_id: product.id };\n            }\n        }\n        return gs1Filters;\n    }\n\n    // --------------------------------------------------------------------------\n    // Private\n    // --------------------------------------------------------------------------\n\n    _canOverrideTrackingNumber(line, newLotName) {\n        const lineLotName = line.lot_name || line.lot_id?.name;\n        return !newLotName || !lineLotName || newLotName === lineLotName;\n    }\n\n    _checkBarcode(barcodeData) {\n        return true;\n    }\n\n    async _closeValidate(ev) {\n        if (ev === undefined) {\n            // If all is OK, displays a notification and goes back to the previous page.\n            this.notification(this.validateMessage, { type: \"success\" });\n            this.trigger(\"history-back\");\n        }\n    }\n\n    _convertDataToFieldsParams(args) {\n        throw new Error(\"Not Implemented\");\n    }\n\n    createNewLine(params) {\n        return this._createNewLine(params);\n    }\n\n    /**\n     * Creates a new line with passed parameters, adds it to the barcode app and\n     * to the list of lines to save, then refresh the page.\n     *\n     * @param {Object} params\n     * @param {Object} params.copyOf line to copy fields' value from\n     * @param {Object} params.fieldsParams fields' value to override\n     * @returns {Object} the newly created line\n     */\n    async _createNewLine(params) {\n        const newLine = Object.assign(\n            {},\n            params.copyOf,\n            this._getNewLineDefaultValues(params.fieldsParams)\n        );\n        const previousIndex = (params.copyOf || this.selectedLine || {}).sortIndex;\n        if (previousIndex !== undefined) {\n            // In case we copy an existing line, we update sort index of following\n            // lines to interpose new line just behind the copied line.\n            const newIndex = previousIndex + 1;\n            for (const line of this.currentState.lines) {\n                if (line.sortIndex >= newIndex) {\n                    line.sortIndex += 1;\n                }\n            }\n            this.currentSortIndex += 1;\n            newLine.sortIndex = newIndex;\n        } else {\n            newLine.sortIndex = this._getLineIndex();\n        }\n        await this.updateLine(newLine, params.fieldsParams);\n        this.currentState.lines.push(newLine);\n        return newLine;\n    }\n\n    async deleteLine(line) {\n        if (!line.id) {\n            // The line doesn't exist in the DB yet => Delete it only in the frontend.\n            const index = this.currentState.lines.findIndex(\n                (l) => l.virtual_id === line.virtual_id\n            );\n            this.currentState.lines.splice(index, 1);\n            this.linesToSave = this.linesToSave.filter((vId) => vId !== line.virtual_id);\n        } else {\n            await this.save();\n            await this.orm.call(this.lineModel, this.deleteLineMethod, [line.id]);\n            this.trigger(\"refresh\");\n        }\n    }\n\n    _shouldCreateLineOnExceed(line) {\n        return true;\n    }\n\n    _shouldBeExpressedInPackagingUom() {\n        return true;\n    }\n\n    _defaultLocation() {\n        const lastScannedLocation = this.lastScanned.sourceLocation;\n        return lastScannedLocation || Object.values(this.cache.dbIdCache[\"stock.location\"])[0];\n    }\n\n    _defaultDestLocation() {\n        return undefined;\n    }\n\n    _getCommands() {\n        const commands = { OCDMENU: this._goToMainMenu.bind(this) };\n        if (!this.isDone) {\n            commands[\"OBTVALI\"] = () => {\n                if (this.canBeValidate) {\n                    this.validate();\n                } else {\n                    this.trigger(\"playSound\", \"error\");\n                }\n            };\n        }\n        return commands;\n    }\n\n    _getLineIndex() {\n        const sortIndex = this.currentSortIndex;\n        this.currentSortIndex++;\n        return sortIndex;\n    }\n\n    _getModelRecord() {\n        return false;\n    }\n\n    _getNewLineDefaultValues(fieldsParams) {\n        return {\n            id: (fieldsParams && fieldsParams.id) || false,\n            virtual_id: this._uniqueVirtualId,\n            location_id: this._defaultLocation(),\n            package_id: false,\n        };\n    }\n\n    _getNewLineDefaultContext() {\n        throw new Error(\"Not Implemented\");\n    }\n\n    _getParentLine(line) {\n        return line && line.parentLine;\n    }\n\n    _getFieldToWrite() {\n        throw new Error(\"Not Implemented\");\n    }\n\n    _fieldToValue(fieldValue) {\n        return typeof fieldValue === \"object\" ? fieldValue.id : fieldValue;\n    }\n\n    _getSaveLineCommand() {\n        const commands = [];\n        const fields = this._getFieldToWrite();\n        for (const virtualId of this.linesToSave) {\n            const line = this.currentState.lines.find((l) => l.virtual_id === virtualId);\n            if (line.id) {\n                // Update an existing line.\n                const initialLine = this.initialState.lines.find(\n                    (l) => l.virtual_id === line.virtual_id\n                );\n                const changedValues = {};\n                let somethingToSave = false;\n                for (const field of fields) {\n                    const fieldValue = line[field];\n                    const initialValue = initialLine[field];\n                    if (\n                        fieldValue !== undefined &&\n                        (([\"boolean\", \"number\", \"string\"].includes(typeof fieldValue) &&\n                            fieldValue !== initialValue) ||\n                            (typeof fieldValue === \"object\" && fieldValue.id !== initialValue.id))\n                    ) {\n                        changedValues[field] = this._fieldToValue(fieldValue);\n                        somethingToSave = true;\n                    }\n                }\n                if (somethingToSave) {\n                    commands.push([1, line.id, changedValues]);\n                }\n            } else {\n                // Create a new line.\n                commands.push([0, 0, this._createCommandVals(line)]);\n            }\n        }\n        return commands;\n    }\n\n    _getSaveCommand() {\n        throw new Error(\"Not Implemented\");\n    }\n\n    _groupSublines(sublines, parentKey = \"parentLine\") {\n        const [ids, virtual_ids] = [[], []];\n        let [totalQtyDemand, totalQtyDone] = [0, 0];\n        const sortedSublines = this._sortLine(sublines);\n        // Use the line with lowest ID as the reference (info shown on summary\n        // line and also the move line opened for the form view.)\n        const referenceLine = sortedSublines.reduce((result, line) =>\n            line.id && (!result.id || result.id > line.id) ? line : result\n        );\n        for (const subline of sublines) {\n            ids.push(subline.id);\n            virtual_ids.push(subline.virtual_id);\n            totalQtyDemand += this.getQtyDemand(subline);\n            totalQtyDone += this.getQtyDone(subline);\n        }\n        const groupedLine = Object.assign({}, referenceLine, {\n            ids,\n            lines: sortedSublines,\n            opened: false,\n            virtual_ids,\n            totalQtyDemand,\n            totalQtyDone,\n        });\n        for (const subline of sublines) {\n            subline[parentKey] = groupedLine;\n        }\n        return groupedLine;\n    }\n\n    async _goToMainMenu() {\n        await this.save();\n        this.action.doAction(\"stock_barcode.stock_barcode_action_main_menu\", {\n            clearBreadcrumbs: true,\n        });\n    }\n\n    _createLinesState() {\n        /* Basic lines structure */\n        throw new Error(\"Not Implemented\");\n    }\n\n    /**\n     * Says if a tracked line can be incremented even if there is no tracking number on it.\n     *\n     * @returns {boolean}\n     */\n    _incrementTrackedLine() {\n        return false;\n    }\n\n    _lineIsNotComplete(line) {\n        throw new Error(\"Not Implemented\");\n    }\n\n    /**\n     * Keeps the track of a modified lines to save them later.\n     *\n     * @param {Object} line\n     */\n    _markLineAsDirty(line) {\n        this.scannedLinesVirtualId.push(line.virtual_id);\n        if (!this.linesToSave.includes(line.virtual_id)) {\n            this.linesToSave.push(line.virtual_id);\n        }\n    }\n\n    _moveEntirePackage() {\n        return false;\n    }\n\n    /**\n     * Will parse the given barcode according to the used nomenclature and return\n     * the retrieved data as an object.\n     *\n     * @param {string} barcode\n     * @param {Object} filters For some models, different records can have the same barcode\n     *      (`stock.lot` for example). In this case, these filters can help to get only\n     *      the wanted record by filtering by record's field's value.\n     * @returns {Object} Containing following data:\n     *      - {string} barcode: the scanned barcode\n     *      - {boolean} match: true if the barcode match an existing record\n     *      - {Object} data type: an object for each type of data/record corresponding to the\n     *                 barcode. It could be 'action', 'location', 'product', ...\n     */\n    async _parseBarcode(barcode, filters) {\n        const result = {\n            barcode,\n            match: false,\n        };\n        if (this.commands[barcode]) {\n            result.action = this.commands[barcode];\n            result.match = true;\n            return result;\n        }\n        let parsedBarcode;\n        try {\n            parsedBarcode = this.parser.parse_barcode(barcode);\n        } catch (err) {\n            // The barcode can't be parsed but the error is caught to fallback\n            // on the classic way to handle barcodes.\n            console.log(`%cWarning: error about ${barcode}`, \"text-weight: bold;\");\n            console.log(err.message);\n        }\n        if (parsedBarcode) {\n            if (parsedBarcode.length) {\n                // With the GS1 nomenclature, the parsed result is a list.\n                const gs1Filters = await this.getGs1Filters(parsedBarcode);\n                for (const data of parsedBarcode) {\n                    if (data.type === \"lot\" && result.product?.tracking === \"none\") {\n                        continue; // For product not tracked, we don't care about the lot.\n                    }\n                    const parsedData = await this._processGs1Data(data, gs1Filters);\n                    Object.assign(result, parsedData);\n                }\n                if (result.match) {\n                    return result;\n                }\n            } else if (parsedBarcode.type === \"weight\") {\n                result.weight = parsedBarcode;\n                result.match = true;\n                barcode = parsedBarcode.base_code;\n            } else if (parsedBarcode.type === \"product\" && parsedBarcode.code !== barcode) {\n                // The scanned barcode should match a product but was either an\n                // alias, either converted from UPC-A to EAN-13 (or vice versa.)\n                barcode = parsedBarcode.code;\n                if (this.commands[barcode]) {\n                    result.action = this.commands[barcode];\n                    result.match = true;\n                    return result; // Simple barcode, no more information to retrieve.\n                }\n            }\n        }\n        const fetchedRecord = await this._fetchRecordFromTheCache(barcode, filters, result);\n        return Object.assign(result, fetchedRecord);\n    }\n\n    async _fetchRecordFromTheCache(barcode, filters, data) {\n        const result = data || { barcode, match: false };\n        const recordByData = await this.cache.getRecordByBarcode(barcode, false, { filters });\n        if (recordByData.size > 1) {\n            const message = _t(\n                \"Barcode scan is ambiguous with several model: %s. Use the most likely.\",\n                Array.from(recordByData.keys())\n            );\n            this.notification(message, { type: \"warning\" });\n        }\n\n        if (this.groups.group_stock_multi_locations) {\n            const location = recordByData.get(\"stock.location\");\n            if (location) {\n                this._setLocationFromBarcode(result, location);\n                result.match = true;\n            }\n        }\n\n        if (this.groups.group_tracking_lot) {\n            const packageType = recordByData.get(\"stock.package.type\");\n            const stockPackage = recordByData.get(\"stock.package\");\n            if (stockPackage) {\n                // TODO: should take packages only in current (sub)location.\n                result.package = stockPackage;\n                result.match = true;\n            }\n            if (packageType) {\n                result.packageType = packageType;\n                result.match = true;\n            }\n        }\n\n        const product = recordByData.get(\"product.product\");\n        if (product) {\n            result.product = product;\n            result.match = true;\n        }\n        if (this.groups.group_uom) {\n            const packaging = recordByData.get(\"product.uom\");\n            if (packaging) {\n                result.match = true;\n                result.packaging = packaging;\n            }\n        }\n        if (this.useExistingLots) {\n            const lot = recordByData.get(\"stock.lot\");\n            if (lot) {\n                result.lot = lot;\n                result.match = true;\n            }\n        }\n\n        if (!result.match && this.packageTypes.length) {\n            // If no match, check if the barcode begins with a package type's barcode.\n            for (const [packageTypeBarcode, packageTypeId] of this.packageTypes) {\n                if (barcode.indexOf(packageTypeBarcode) === 0) {\n                    result.packageType = await this.cache.getRecord(\n                        \"stock.package.type\",\n                        packageTypeId\n                    );\n                    result.packageName = barcode;\n                    result.match = true;\n                    break;\n                }\n            }\n        }\n        return result;\n    }\n\n    async print(action, method) {\n        await this.save();\n        const options = this._getPrintOptions();\n        if (options.warning) {\n            return this.notification(options.warning, { type: \"warning\" });\n        }\n        if (!action && method) {\n            action = await this.orm.call(this.resModel, method, [[this.resId]]);\n        }\n        this.action.doAction(action, options);\n    }\n\n    async _processGs1Data(data, filters) {\n        const result = {};\n        const { rule, type, value } = data;\n        if ([\"location\", \"location_dest\"].includes(type)) {\n            const location = await this.cache.getRecordByBarcode(value, \"stock.location\");\n            if (!location) {\n                return;\n            } else {\n                result.location = location;\n                result.match = true;\n            }\n        } else if (type === \"lot\") {\n            if (this.useExistingLots) {\n                result.lot = await this.cache.getRecordByBarcode(value, \"stock.lot\", { filters });\n            }\n            if (!result.lot) {\n                // No existing lot found, set a lot name.\n                result.lotName = value;\n            }\n            if (result.lot || result.lotName) {\n                result.match = true;\n            }\n        } else if (type === \"package\") {\n            const stockPackage = await this.cache.getRecordByBarcode(value, \"stock.package\");\n            if (stockPackage) {\n                result.package = stockPackage;\n            } else {\n                // Will be used to force package's name when put in pack.\n                result.packageName = value;\n            }\n            result.match = true;\n        } else if (type === \"package_type\") {\n            const packageType = await this.cache.getRecordByBarcode(value, \"stock.package.type\");\n            if (packageType) {\n                result.packageType = packageType;\n                result.match = true;\n            } else {\n                const message = _t(\n                    \"An unexisting package type was scanned. This part of the barcode can't be processed.\"\n                );\n                this.notification(message, { type: \"warning\" });\n            }\n        } else if (type === \"product\") {\n            const product = await this.cache.getRecordByBarcode(value, \"product.product\");\n            if (product) {\n                result.product = product;\n                result.match = true;\n            } else if (this.groups.group_uom) {\n                const packaging = await this.cache.getRecordByBarcode(value, \"product.uom\");\n                if (packaging) {\n                    result.packaging = packaging;\n                    result.match = true;\n                }\n            }\n        } else if (type === \"quantity\") {\n            result.quantity = value;\n            // The quantity is usually associated to an UoM, but we\n            // ignore this info if the UoM setting is disabled.\n            if (this.groups.group_uom && rule?.associated_uom_id) {\n                result.uom = await this.cache.getRecord(\"uom.uom\", rule.associated_uom_id);\n            }\n            result.match = result.quantity ? true : false;\n        }\n        return result;\n    }\n\n    /**\n     * Starts by parse the barcode and then process each type of barcode data.\n     *\n     * @param {string} barcode\n     * @returns {Promise}\n     */\n    async _processBarcode(barcode) {\n        let barcodeData = {};\n        let currentLine = false;\n        // Creates a filter if needed, which can help to get the right record\n        // when multiple records have the same model and barcode.\n        const filters = {};\n        if (this.selectedLine && this.selectedLine.product_id.tracking !== \"none\") {\n            filters[\"stock.lot\"] = {\n                product_id: this.selectedLine.product_id.id,\n            };\n        }\n        // Constrain DB reads to records which belong to the company defined on the open operation\n        filters[\"all\"] = {\n            company_id: [false].concat(this._getCompanyId() || []),\n        };\n        try {\n            barcodeData = await this._parseBarcode(barcode, filters);\n            if (this._shouldSearchForAnotherLot(barcodeData, filters)) {\n                // Retry to parse the barcode without filters in case it matches an existing\n                // record that can't be found because of the filters\n                const lot = await this.cache.getRecordByBarcode(barcode, \"stock.lot\");\n                if (lot) {\n                    Object.assign(barcodeData, { lot, match: true });\n                }\n            }\n        } catch (parseErrorMessage) {\n            barcodeData.error = parseErrorMessage;\n        }\n\n        // Keep in memory every scans.\n        this.scanHistory.unshift(barcodeData);\n\n        if (barcodeData.match) {\n            // Makes flash the screen if the scanned barcode was recognized.\n            this.trigger(\"flash\");\n        }\n\n        // Process each data in order, starting with non-ambiguous data type.\n        if (barcodeData.action) {\n            // As action is always a single data, call it and do nothing else.\n            return await barcodeData.action();\n        }\n\n        if (barcodeData.packaging) {\n            Object.assign(barcodeData, this._retrievePackagingData(barcodeData));\n        }\n\n        // Depending of the configuration, the user can be forced to scan a specific barcode type.\n        const check = this._checkBarcode(barcodeData);\n        if (check.error) {\n            return this.notification(check.message, { title: check.title, type: \"danger\" });\n        }\n\n        if (barcodeData.product) {\n            // Remembers the product if a (packaging) product was scanned.\n            this.lastScanned.product = barcodeData.product;\n        }\n\n        if (barcodeData.lot && !barcodeData.product) {\n            Object.assign(barcodeData, this._retrieveTrackingNumberInfo(barcodeData.lot));\n        }\n\n        await this._processLocation(barcodeData);\n        await this._processPackage(barcodeData);\n        if (barcodeData.stopped) {\n            // TODO: Sometime we want to stop here instead of keeping doing thing,\n            // but it's a little hacky, it could be better to don't have to do that.\n            return;\n        }\n\n        if (barcodeData.weight) {\n            // Convert the weight into quantity.\n            barcodeData.quantity = barcodeData.weight.value;\n        }\n\n        // If no product found, take the one from last scanned line if possible.\n        if (!barcodeData.product) {\n            if (barcodeData.quantity) {\n                currentLine = this.selectedLine || this.lastScannedLine;\n            } else if (this.selectedLine && this.selectedLine.product_id.tracking !== \"none\") {\n                currentLine = this.selectedLine;\n            } else if (\n                this.lastScannedLine &&\n                this.lastScannedLine.product_id.tracking !== \"none\"\n            ) {\n                currentLine = this.lastScannedLine;\n            }\n            if (currentLine) {\n                // If we can, get the product from the previous line.\n                const previousProduct = currentLine.product_id;\n                // If the current product is tracked and the barcode doesn't fit\n                // anything else, we assume it's a new lot/serial number.\n                if (\n                    previousProduct.tracking !== \"none\" &&\n                    !barcodeData.match &&\n                    this.canCreateNewLot\n                ) {\n                    this.trigger(\"flash\");\n                    barcodeData.lotName = barcode;\n                    barcodeData.product = previousProduct;\n                }\n                if (barcodeData.lot || barcodeData.lotName || barcodeData.quantity) {\n                    barcodeData.product = previousProduct;\n                }\n            }\n        }\n        let { product } = barcodeData;\n        if (!product && barcodeData.match && this.parser.nomenclature.is_gs1_nomenclature) {\n            // Special case where something was found using the GS1 nomenclature but no product is\n            // used (eg.: a product's barcode can be read as a lot is starting with 21).\n            // In such case, tries to find a record with the barcode by by-passing the parser.\n            barcodeData = await this._fetchRecordFromTheCache(barcode, filters);\n            if (barcodeData.packaging) {\n                Object.assign(barcodeData, this._retrievePackagingData(barcodeData));\n            } else if (barcodeData.lot) {\n                Object.assign(barcodeData, this._retrieveTrackingNumberInfo(barcodeData.lot));\n            }\n            if (barcodeData.product) {\n                product = barcodeData.product;\n            } else if (barcodeData.match) {\n                await this._processPackage(barcodeData);\n                if (barcodeData.stopped) {\n                    return;\n                }\n            }\n        }\n        if (!product) {\n            // Product is mandatory, if no product, raises a warning.\n            return this.noProductToast(barcodeData);\n        } else if (barcodeData.lot && barcodeData.lot.product_id !== product.id) {\n            delete barcodeData.lot; // The product was scanned alongside another product's lot.\n        }\n        if (barcodeData.weight) {\n            // the encoded weight is based on the product's UoM\n            barcodeData.uom = this.cache.getRecord(\"uom.uom\", product.uom_id);\n        }\n\n        // Searches and selects a line if needed.\n        if (!currentLine || this._shouldSearchForAnotherLine(currentLine, barcodeData)) {\n            currentLine = this._findLine(barcodeData);\n        }\n\n        // Default quantity set to 1 by default if the product is untracked or\n        // if there is a scanned tracking number.\n        if (\n            product.tracking === \"none\" ||\n            barcodeData.lot ||\n            barcodeData.lotName ||\n            this._incrementTrackedLine()\n        ) {\n            const hasUnassignedQty =\n                currentLine && currentLine.qty_done && !currentLine.lot_id && !currentLine.lot_name;\n            const isTrackingNumber = barcodeData.lot || barcodeData.lotName;\n            const defaultQuantity = isTrackingNumber && hasUnassignedQty ? 0 : 1;\n            barcodeData.quantity = barcodeData.quantity || defaultQuantity;\n            if (\n                product.tracking === \"serial\" &&\n                barcodeData.quantity > 1 &&\n                (barcodeData.lot || barcodeData.lotName)\n            ) {\n                barcodeData.quantity = 1;\n                this.notification(\n                    _t(\n                        `A product tracked by serial numbers can't have multiple quantities for the same serial number.`\n                    ),\n                    { type: \"danger\" }\n                );\n            }\n        }\n\n        if ((barcodeData.lotName || barcodeData.lot) && product) {\n            const lotName = barcodeData.lotName || barcodeData.lot.name;\n            for (const line of this.currentState.lines) {\n                if (line.product_id.id !== product.id) {\n                    continue; // The same SN can be scanned for different product.\n                }\n                if (\n                    line.product_id.tracking === \"serial\" &&\n                    this.getQtyDone(line) !== 0 &&\n                    this.getlotName(line) === lotName\n                ) {\n                    return this.notification(\n                        _t(\"The scanned serial number %s is already used.\", lotName),\n                        { type: \"danger\" }\n                    );\n                }\n            }\n            // Prefills `owner_id` and `package_id` if possible.\n            const prefilledOwner =\n                (!currentLine || (currentLine && !currentLine.owner_id)) &&\n                this.groups.group_tracking_owner &&\n                !barcodeData.owner;\n            const prefilledPackage =\n                (!currentLine || (currentLine && !currentLine.package_id)) &&\n                this.groups.group_tracking_lot &&\n                !barcodeData.package;\n            if (this.useExistingLots && (prefilledOwner || prefilledPackage)) {\n                const lotId =\n                    (barcodeData.lot && barcodeData.lot.id) ||\n                    (currentLine && currentLine.lot_id && currentLine.lot_id.id) ||\n                    false;\n                const locationId =\n                    (currentLine && currentLine.location_id && currentLine.location_id.id) || false;\n                const params = {\n                    lot_id: lotId,\n                    lot_name: (!lotId && barcodeData.lotName) || false,\n                };\n                let quants = await this.cache.getQuants(product, locationId, params);\n                if (quants.length && quants.length > 1 && (prefilledPackage || prefilledOwner)) {\n                    // If we have multiple matching quants and we use package and/or consigment,\n                    // give priority to the quants with a package or an owner.\n                    const filteredQuants = quants.filter(\n                        (quant) => quant.package_id || quant.owner_id\n                    );\n                    quants = filteredQuants.length ? filteredQuants : quants;\n                }\n                if (quants && quants.length === 1) {\n                    const quant = quants[0];\n                    if (prefilledPackage && quant.package_id) {\n                        barcodeData.package = this.cache.getRecord(\n                            \"stock.package\",\n                            quant.package_id\n                        );\n                    }\n                    if (prefilledOwner && quant.owner_id) {\n                        barcodeData.owner = this.cache.getRecord(\"res.partner\", quant.owner_id);\n                    }\n                }\n            }\n        }\n\n        // If line found is expressed in an different unit than its packaging adapt the\n        // barcodeData to update the line properly or force the creation of a new line\n        const barcodeDataUom = this.cache.getRecord(\n            \"uom.uom\",\n            barcodeData.uom ? barcodeData.uom.id : barcodeData.product?.uom_id\n        );\n        const expressedInPackagingUom =\n            currentLine && barcodeDataUom && barcodeDataUom.id !== currentLine.product_uom_id.id;\n        if (expressedInPackagingUom) {\n            if (!this._lineIsNotComplete(currentLine)) {\n                currentLine = false;\n            } else {\n                barcodeData.quantity =\n                    (barcodeData.quantity * barcodeDataUom.factor) /\n                    currentLine.product_uom_id.factor;\n                barcodeData.uom = currentLine.product_uom_id;\n            }\n        }\n\n        // Updates or creates a line based on barcode data.\n        if (currentLine) {\n            // If line found, can it be incremented ?\n            let exceedingQuantity = 0;\n            // Checks the quantity doesn't exceed the line's remaining quantity.\n            if (currentLine.reserved_uom_qty && product.tracking === \"none\") {\n                const remainingQty = currentLine.reserved_uom_qty - currentLine.qty_done;\n                if (\n                    barcodeData.quantity > remainingQty &&\n                    this._shouldCreateLineOnExceed(currentLine)\n                ) {\n                    // In this case, lowers the increment quantity and keeps\n                    // the excess quantity to create a new line.\n                    exceedingQuantity = parseFloat(\n                        formatFloat(barcodeData.quantity - remainingQty, {\n                            digits: [false, this.precision],\n                        })\n                    );\n                    barcodeData.quantity = remainingQty;\n                }\n            }\n            if (barcodeData.quantity > 0 || barcodeData.lot || barcodeData.lotName) {\n                const fieldsParams = this._convertDataToFieldsParams(barcodeData);\n                if (barcodeData.uom) {\n                    fieldsParams.uom = barcodeData.uom;\n                }\n                await this.updateLine(currentLine, fieldsParams);\n                this.trigger(\"playSound\", \"success\");\n            }\n            if (exceedingQuantity) {\n                // Creates a new line for the excess quantity.\n                barcodeData.quantity = exceedingQuantity;\n                const fieldsParams = this._convertDataToFieldsParams(barcodeData);\n                if (barcodeData.uom) {\n                    fieldsParams.uom = barcodeData.uom;\n                }\n                currentLine = await this._createNewLine({\n                    copyOf: currentLine,\n                    fieldsParams,\n                });\n                if (expressedInPackagingUom) {\n                    currentLine.packaging_uom_id = undefined;\n                    currentLine.packaging_uom_qty = 0;\n                }\n            }\n        } else {\n            // No line found, so creates a new one.\n            const fieldsParams = this._convertDataToFieldsParams(barcodeData);\n            if (barcodeData.uom) {\n                fieldsParams.uom = barcodeData.uom;\n            }\n            if (this.createSingleLinesForPackaging(barcodeData)) {\n                const productUoM = await this.cache.getRecord(\n                    \"uom.uom\",\n                    barcodeData.product.uom_id\n                );\n                const qtyUoM = barcodeData.uom;\n                let { quantity } = barcodeData;\n                if (productUoM.factor !== qtyUoM.factor) {\n                    quantity *= qtyUoM.factor / productUoM.factor;\n                    fieldsParams.uom = productUoM;\n                }\n                for (let lineCount = 0; lineCount < quantity; lineCount++) {\n                    currentLine = await this.createNewLine({ fieldsParams });\n                }\n            } else {\n                currentLine = await this.createNewLine({ fieldsParams });\n            }\n            if (currentLine) {\n                this.trigger(\"playSound\", \"success\");\n            }\n        }\n\n        // And finally, if the scanned barcode modified a line, selects this line.\n        if (currentLine) {\n            this._selectLine(currentLine);\n        }\n\n        const matchedURI = barcode.match(/^urn:.*$/);\n        if (matchedURI) {\n            // If the process goes right and the scanned barcode is an URI, add\n            // it to the cache to avoid scanning it a second time.\n            this.uriCache.add(barcode);\n        }\n        this.trigger(\"update\");\n    }\n\n    noProductToast(barcodeData) {\n        if (!barcodeData.error) {\n            if (this.groups.group_tracking_lot) {\n                barcodeData.error = _t(\n                    \"You are expected to scan one or more products or a package available at the picking location\"\n                );\n            } else {\n                barcodeData.error = _t(\"This product doesn't exist.\");\n            }\n        }\n        return this.notification(barcodeData.error, { type: \"danger\" });\n    }\n\n    async _processLocation(barcodeData) {\n        if (barcodeData.location) {\n            await this._processLocationSource(barcodeData);\n            this.trigger(\"playSound\", \"success\");\n            this.trigger(\"update\");\n        }\n    }\n\n    async _processLocationSource(barcodeData) {\n        this.location = barcodeData.location;\n        barcodeData.stopped = true;\n        // Unselects the line.\n        this.selectedLineVirtualId = false;\n        this.lastScanned.packageId = false;\n    }\n\n    async _processPackage(barcodeData) {\n        throw new Error(\"Not Implemented\");\n    }\n\n    /**\n     * This method cleans the barcode in case the parser use the GS1 nomenclature, removing the\n     * parentheses and the extra spaces (helping for human readability but not valid).\n     * E.g.: (01) 00001234567895 (10) lot-abc -> 0100001234567895\\x1D10lot-abc\n     *\n     * @param {string} barcode\n     * @returns {string} barcode\n     */\n    cleanBarcode(barcode) {\n        if (this.parser.nomenclature.is_gs1_nomenclature) {\n            barcode = barcode.replace(/[( ]([0-9]+)[)]/g, `${FNC1_CHAR}$1`);\n            if (barcode[0] === FNC1_CHAR) {\n                barcode = barcode.slice(1, barcode.length);\n            }\n        }\n        return barcode;\n    }\n\n    lineCanBeSelected() {\n        return true;\n    }\n\n    lineCanBeEdited() {\n        return true;\n    }\n\n    /**\n     * Check if a given line can be taken depending of the current location (if no current location,\n     * it will always be true).\n     * @param {Object} line\n     * @returns {Boolean}\n     */\n    lineCanBeTakenFromTheCurrentLocation(line) {\n        return this.lineIsInTheCurrentLocation(line);\n    }\n\n    lineIsInTheCurrentLocation(line) {\n        return Boolean(\n            !this.groups.group_stock_multi_locations ||\n                !this.lastScanned.sourceLocation || // No current location so we don't care.\n                this.lastScanned.sourceLocation.id == line.location_id.id // Line at the right location.\n        );\n    }\n\n    _retrievePackagingData(barcodeData) {\n        const { packaging } = barcodeData;\n        const product = this.cache.getRecord(\"product.product\", packaging.product_id);\n        const packagingUom = this.cache.getRecord(\"uom.uom\", packaging.uom_id);\n        const uom = this._shouldBeExpressedInPackagingUom()\n            ? packagingUom\n            : this.cache.getRecord(\"uom.uom\", product.uom_id);\n        let quantity = \"quantity\" in barcodeData ? barcodeData.quantity : 1;\n        if (!this._shouldBeExpressedInPackagingUom() && packagingUom !== uom) {\n            const factor = packagingUom.factor / uom.factor;\n            quantity *= factor;\n        }\n        return { product, quantity, uom };\n    }\n\n    _retrieveTrackingNumberInfo(lot) {\n        return { product: this.cache.getRecord(\"product.product\", lot.product_id) };\n    }\n\n    _selectLine(line) {\n        const virtualId = line.virtual_id;\n        if (this.selectedLineVirtualId === virtualId) {\n            return; // Don't select the line if it's already selected.\n        }\n        this.selectedLineVirtualId = virtualId;\n        this.lastScanned.destLocation = false;\n    }\n\n    _setLocationFromBarcode(result, location) {\n        result.location = location;\n        return result;\n    }\n\n    _sortingMethod(l1, l2) {\n        // Sort by source location.\n        const sourceLocation1 = l1.location_id.display_name;\n        const sourceLocation2 = l2.location_id.display_name;\n        if (sourceLocation1 < sourceLocation2) {\n            return -1;\n        } else if (sourceLocation1 > sourceLocation2) {\n            return 1;\n        }\n        // Sort by (source) package.\n        const package1 = l1.package_id.name;\n        const package2 = l2.package_id.name;\n        if (package1 < package2) {\n            return -1;\n        } else if (package1 > package2) {\n            return 1;\n        }\n        // Sort by destination location.\n        if (l1.location_dest_id && l2.location_dest_id) {\n            const destinationLocation1 = l1.location_dest_id.display_name;\n            const destinationLocation2 = l2.location_dest_id.display_name;\n            if (destinationLocation1 < destinationLocation2) {\n                return -1;\n            } else if (destinationLocation1 > destinationLocation2) {\n                return 1;\n            }\n        }\n        // Sort by result package.\n        if (l1.result_package_id && l2.result_package_id) {\n            const resultPackage1 = l1.result_package_id.name;\n            const resultPackage2 = l2.result_package_id.name;\n            if (resultPackage1 < resultPackage2) {\n                return -1;\n            } else if (resultPackage1 > resultPackage2) {\n                return 1;\n            }\n        }\n        // Sort by product's category.\n        const categ1 = l1.product_category_name;\n        const categ2 = l2.product_category_name;\n        if (categ1 < categ2) {\n            return -1;\n        } else if (categ1 > categ2) {\n            return 1;\n        }\n        // Sort by product's display name.\n        const product1 = l1.product_id.display_name;\n        const product2 = l2.product_id.display_name;\n        if (product1 < product2) {\n            return -1;\n        } else if (product1 > product2) {\n            return 1;\n        }\n        return 0;\n    }\n\n    /**\n     * Sorts the lines to have new lines always on top and complete lines always on the bottom.\n     *\n     * @param {Array<Object>} lines\n     * @returns {Array<Object>}\n     */\n    _sortLine(lines) {\n        return lines.sort((l1, l2) => (l1.sortIndex > l2.sortIndex ? 1 : -1));\n    }\n\n    _isPackageInPackage(pack, containerPack) {\n        const parentNames = pack.dest_complete_name.split(\" > \");\n        return parentNames.some((name) => name === containerPack.name);\n    }\n\n    _findLine(barcodeData) {\n        let foundLine = false;\n        const { lot, lotName, product } = barcodeData;\n        const quantPackage = barcodeData.quantPackage || barcodeData.package;\n        const uomId = barcodeData.uom ? barcodeData.uom.id : barcodeData.product?.uom_id;\n        const dataLotName = lotName || (lot && lot.name) || false;\n        const pageLines = [...this.pageLines];\n        // If a line is selected, unshift it to the first position to start the search by it\n        if (this.selectedLineVirtualId) {\n            const selectedLineIndex = pageLines.findIndex(\n                (line) => line.virtual_id == this.selectedLineVirtualId\n            );\n            if (selectedLineIndex > -1) {\n                pageLines.splice(selectedLineIndex, 1);\n                pageLines.unshift(this.pageLines[selectedLineIndex]);\n            }\n        }\n        for (const line of pageLines) {\n            const lineLotName = this.getlotName(line);\n            if (line.product_id.id !== product.id) {\n                continue; // Not the same product.\n            }\n            if (\n                line.packaging_uom_id &&\n                line.packaging_uom_id.id !== line.product_uom_id.id &&\n                line.packaging_uom_id.id !== uomId &&\n                line.product_uom_id.id !== uomId\n            ) {\n                // If the packaging UoM is different from the UoM of the move we should\n                // only find the line if the barcode Uom is either of these\n                continue; // Not the same UoM.\n            }\n            if (quantPackage && (!line.package_id || line.package_id.id !== quantPackage.id)) {\n                continue; // Not the expected package.\n            }\n            if (\n                line.product_id.tracking !== \"none\" &&\n                !this._canOverrideTrackingNumber(line, dataLotName)\n            ) {\n                continue; // Not the same lot.\n            }\n            if (line.product_id.tracking === \"serial\") {\n                if (this.getQtyDone(line) >= 1 && lineLotName) {\n                    continue; // Line tracked by serial numbers with quantity & SN.\n                } else if (dataLotName && this.getQtyDone(line) > 1) {\n                    continue; // Can't add a SN on a line where multiple qty. was previously added.\n                }\n            }\n            if (\n                (!dataLotName || !lineLotName || dataLotName !== lineLotName) &&\n                line.qty_done &&\n                line.qty_done >= line.reserved_uom_qty &&\n                (line.product_id.tracking === \"none\" || lineLotName) &&\n                line.id &&\n                (!this.selectedLine || line.virtual_id != this.selectedLine.virtual_id)\n            ) {\n                // Has enough quantity (and another lot is set if the line's product is tracked)\n                // and the line wasn't explicitly selected.\n                continue;\n            }\n            if (this._lineCannotBeTaken(line)) {\n                continue;\n            }\n            if (this._lineIsNotComplete(line)) {\n                if (this.lineCanBeTakenFromTheCurrentLocation(line)) {\n                    // Found a uncompleted compatible line, stop searching if it has the same location\n                    // than the scanned one (or if no location was scanned).\n                    foundLine = line;\n                    if (\n                        this.lineIsInTheCurrentLocation(line) &&\n                        (line.product_id.tracking === \"none\" ||\n                            !dataLotName ||\n                            dataLotName === lineLotName) &&\n                        line.product_uom_id.id === uomId\n                    ) {\n                        // In case of tracked product, stop searching only if no\n                        // LN/SN was scanned or if it's the same.\n                        break;\n                    }\n                } else if (\n                    this.needSourceConfirmation &&\n                    foundLine &&\n                    !this._lineIsNotComplete(foundLine)\n                ) {\n                    // Found a empty line in another location, we should take it but depending of\n                    // the config, maybe we can't (location should be confirmed first).\n                    // That said, we already found another line but if it's completed, forget we\n                    // found it to avoid to create a new line in the current location because it's\n                    // basicaly the same than increment the other line found in another location.\n                    foundLine = false;\n                    continue;\n                }\n            }\n            // If all the previous checks were passed, the line can be considered\n            // as the found line. That said, if another line was already found,\n            // it can be tricky to know which one we want to prioritize.\n            if (!foundLine) {\n                // The line matches but there could be a better candidate, so keep searching.\n                // If multiple lines can match, prioritises the one at the right location (if a\n                // location source was previously selected) or the selected one if relevant.\n                const currentLocationId =\n                    this.lastScanned.sourceLocation && this.lastScanned.sourceLocation.id;\n                if (\n                    this.selectedLine &&\n                    this.selectedLine.virtual_id === line.virtual_id &&\n                    (!currentLocationId ||\n                        !foundLine ||\n                        foundLine.location_id.id != currentLocationId)\n                ) {\n                    foundLine = this.lineCanBeTakenFromTheCurrentLocation(line) ? line : foundLine;\n                } else if (\n                    !foundLine ||\n                    (currentLocationId &&\n                        foundLine.location_id.id != currentLocationId &&\n                        line.location_id.id == currentLocationId)\n                ) {\n                    foundLine = this.lineCanBeTakenFromTheCurrentLocation(line) ? line : foundLine;\n                }\n            } else if (this._lineIsNotComplete(foundLine)) {\n                // If previous line is not completed, no reason to prioritize the current one.\n                continue;\n            } else if (this._lineIsNotComplete(line)) {\n                // If previous line is completed and current one is not, prioritize the current one.\n                foundLine = line;\n            } else if (foundLine.product_uom_id.id !== uomId && line.product_uom_id.id === uomId) {\n                // If previous line does not have the perfect uom and the current does, prioritize the current one.\n                foundLine = line;\n            } else if (\n                this.lineIsSelected(line) ||\n                (!this.lineIsSelected(foundLine) && this.lineBelongsToSelectedLine(line))\n            ) {\n                // If both previous found line and current line are completed, prioritize the\n                // current one only if it's the selected line (or on of its sublines.)\n                foundLine = line;\n            }\n        }\n        return foundLine;\n    }\n\n    lineBelongsToSelectedLine(line) {\n        if (!this.selectedLine) {\n            return false;\n        }\n        const selectedGroupedLine = this._getParentLine(this.selectedLine);\n        return selectedGroupedLine && selectedGroupedLine.virtual_ids.includes(line.virtual_id);\n    }\n\n    /**\n     * Intended to be used only by `_findLine`.\n     * Depending of the model, they can have additional conditions to know if a\n     * line can be took when a barcode is scanned. This method is meant to be overriden.\n     * @param {Object} _line\n     * @returns {Boolean}\n     */\n    _lineCannotBeTaken(line) {\n        return !this.lineCanBeTakenFromTheCurrentLocation(line);\n    }\n\n    lineIsSelected(line) {\n        return (line.dummy_id || line.virtual_id) === this.selectedLineVirtualId;\n    }\n\n    _shouldSearchForAnotherLot(barcodeData, filters) {\n        return (\n            !barcodeData.match &&\n            filters[\"stock.lot\"] &&\n            !this.canCreateNewLot &&\n            this.useExistingLots\n        );\n    }\n\n    _shouldSearchForAnotherLine(line, barcodeData) {\n        if (line.product_id.id !== barcodeData.product.id) {\n            return true;\n        }\n        if (barcodeData.product.tracking === \"serial\" && this.getQtyDone(line) > 0) {\n            return true;\n        }\n        const { lot, lotName } = barcodeData;\n        const dataLotName = lotName || (lot && lot.name) || false;\n        const lineLotName = this.getlotName(line);\n        if (dataLotName && lineLotName && dataLotName !== lineLotName) {\n            return true;\n        }\n        const parentLine = this._getParentLine(line);\n        // If the line is a part of a group, we check if the group is fulfilled.\n        const currentLine = parentLine || line;\n        return this.getQtyDone(currentLine) >= this.getQtyDemand(currentLine);\n    }\n\n    get _uniqueVirtualId() {\n        this._lastVirtualId = this._lastVirtualId || 0;\n        return ++this._lastVirtualId;\n    }\n\n    _updateLineQty(line, qty) {\n        throw new Error(\"Not Implemented\");\n    }\n\n    _updateLotName(line, lotName) {\n        throw new Error(\"Not Implemented\");\n    }\n\n    _getName() {\n        return this.cache.getRecord(this.resModel, this.resId).name;\n    }\n\n    // Response -> UI State\n    _createState() {\n        this.record = this._getModelRecord();\n        const lines = this._createLinesState();\n        // Sorts the lines following some criterea and then assign an index for the sort (so they keep the same place).\n        lines.sort(this._sortingMethod.bind(this));\n        for (const line of lines) {\n            line.sortIndex = this._getLineIndex();\n        }\n        this.initialState = { lines };\n        this.currentState = JSON.parse(JSON.stringify(this.initialState)); // Deep copy\n        this.groupLines();\n    }\n\n    _getPrintOptions() {\n        return {};\n    }\n\n    zeroQtyClass(_line) {\n        return \"text-muted\";\n    }\n\n    _getCompanyId() {\n        throw new Error(\"Not Implemented\");\n    }\n\n    _onExit() {\n        return;\n    }\n}\n", "import BarcodeModel from \"@stock_barcode/models/barcode_model\";\nimport { BackorderDialog } from \"../components/backorder_dialog\";\nimport { ConfirmationDialog } from \"@web/core/confirmation_dialog/confirmation_dialog\";\nimport { Deferred } from \"@web/core/utils/concurrency\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { user } from \"@web/core/user\";\nimport { markup } from \"@odoo/owl\";\nimport { SignatureDialog } from \"@web/core/signature/signature_dialog\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { formatFloat } from \"@web/core/utils/numbers\";\n\nexport default class BarcodePickingModel extends BarcodeModel {\n    constructor(resModel, resId, services) {\n        super(resModel, resId, services);\n        this.lineModel = \"stock.move.line\";\n        this.showBackOrderDialog = true;\n        this.validateMessage = _t(\"The transfer has been validated\");\n        this.validateMethod = \"button_validate\";\n        this.deleteLineMethod = \"unlink\";\n        this.validateContext = {\n            display_detailed_backorder: true,\n            skip_backorder: true,\n        };\n        this.lastScanned.destLocation = false;\n        this.shouldShortenLocationName = true;\n        this.actionName = \"stock_barcode.stock_barcode_picking_client_action\";\n        this.backorderModel = \"stock.picking\";\n        this.needSourceConfirmation = {};\n        this.ui = useService(\"ui\");\n    }\n\n    setData(data) {\n        // Picking type's scan restrictions and other barcode's configuration.\n        this.config = data.data.config || {};\n\n        super.setData(...arguments);\n        this._useReservation = this.initialState.lines.some((line) => !line.picked);\n        const { use_create_lots, use_existing_lots } = this.record.picking_type_id || {};\n        this.useTrackingNumber = use_create_lots || use_existing_lots;\n        if (!this.useScanDestinationLocation) {\n            this.config.restrict_scan_dest_location = \"no\";\n        }\n        this.lineFormViewId = data.data.line_view_id;\n        this.formViewId = data.data.form_view_id;\n        this.scrapViewId = data.data.scrap_view_id;\n        this.packageKanbanViewId = data.data.package_view_id;\n        this.precision = data.data.precision;\n    }\n\n    askBeforeNewLinesCreation(product) {\n        return (\n            this._useReservation &&\n            product &&\n            !this.currentState.lines.some((line) => line.product_id.id === product.id)\n        );\n    }\n\n    createNewLine(params) {\n        const product = params.fieldsParams.product_id;\n        if (\n            this.needSourceConfirmation &&\n            this.needSourceConfirmation[this.location.id]?.[product.id]\n        ) {\n            const message = _t(\n                \"You are about to take the product %(productName)s from the \" +\n                    \"location %(locationName)s but this product isn't reserved in this location.\\n\" +\n                    \"Scan the current location to confirm that.\",\n                { productName: product.display_name, locationName: this.location.display_name }\n            );\n            this.needSourceConfirmation[this.location.id][product.id] = false;\n            this.notification(message, { type: \"danger\" });\n            return false;\n        } else if (this.askBeforeNewLinesCreation(product)) {\n            const productName = (product.code ? `[${product.code}] ` : \"\") + product.display_name;\n            if (!this.config.barcode_allow_extra_product) {\n                // No unreserved product can't be added, display a warning.\n                const message = _t(\n                    \"The product %s should not be picked in this operation.\",\n                    productName\n                );\n                this.notification(message, { type: \"danger\" });\n                return false;\n            }\n            // Unreserved product can be added but a confirmation is needed.\n            const body = _t(\n                \"Scanned product %s is not reserved for this transfer. Are you sure you want to add it?\",\n                productName\n            );\n            const confirmationPromise = new Promise((resolve) => {\n                this.trigger(\"playSound\");\n                this.dialogService.add(ConfirmationDialog, {\n                    title: _t(\"Add extra product?\"),\n                    body,\n                    cancel: () => resolve(false),\n                    confirm: async () => {\n                        const newLine = await this._createNewLine(params);\n                        resolve(newLine);\n                    },\n                    close: () => resolve(false),\n                });\n            });\n            return confirmationPromise;\n        }\n        return super.createNewLine(...arguments);\n    }\n\n    getDisplayCompletePackageBtn(line) {\n        return line.isPackageLine;\n    }\n\n    getDisplayIncrementBtn(line) {\n        line = (line.product_id.tracking === \"lot\" && this._getParentLine(line)) || line;\n        return !this.getQtyDemand(line) || this.getQtyDone(line) < this.getQtyDemand(line);\n    }\n\n    getDisplayIncrementBtnForSerial(line) {\n        const lineTrackingNumber = line.lot_id || line.lot_name;\n        return (\n            !this.useTrackingNumber ||\n            (!this.config.restrict_scan_tracking_number &&\n                lineTrackingNumber &&\n                this.getQtyDone(line) === 0)\n        );\n    }\n\n    getLineRemainingQuantity(line) {\n        const remainingQty = super.getLineRemainingQuantity(...arguments);\n        const parentLine =\n            (line.product_id.tracking === \"lot\" && this._getParentLine(line)) || line;\n        if (parentLine && this.getQtyDemand(parentLine)) {\n            const parentRemainingQty = this.getQtyDemand(parentLine) - this.getQtyDone(parentLine);\n            if (parentRemainingQty) {\n                return Math.min(Math.max(1, remainingQty), parentRemainingQty);\n            }\n        }\n        return remainingQty;\n    }\n\n    getQtyDone(line) {\n        return line.qty_done;\n    }\n\n    getQtyDemand(line) {\n        return line.reserved_uom_qty || 0;\n    }\n\n    getEditedLineParams(line) {\n        this._setUser();\n        return super.getEditedLineParams(...arguments);\n    }\n\n    completePackage(virtualId) {\n        this.actionMutex.exec(() => {\n            const packageLine = this.packageLines.find((l) => l.virtual_id === virtualId);\n            const factor = packageLine.qty_done ? -1 : 1;\n            for (const line of packageLine.lines) {\n                this.selectedLineVirtualId = line.virtual_id;\n                const lineQty = line.reserved_uom_qty || line.qty_done || line.packedQuantity;\n                this._updateLineQty(line, { qty_done: lineQty * factor });\n                this._markLineAsDirty(line);\n            }\n            this.trigger(\"update\");\n        });\n    }\n\n    displayLineQtyDemand(line) {\n        if (line.isPackageLine && line.reservedPackage) {\n            return true;\n        } else if (!this.showReservedSns) {\n            return (\n                this.getQtyDemand(line) &&\n                !(this.lineIsTracked(line) && !line.lines && this._getParentLine(line))\n            );\n        }\n        return super.displayLineQtyDemand(line);\n    }\n\n    groupKey(line) {\n        return super.groupKey(...arguments) + `_${line.location_dest_id.id}`;\n    }\n\n    lineCanBeSelected(line) {\n        if (this.selectedLine && this.selectedLine.virtual_id === line.virtual_id) {\n            return true; // We consider an already selected line can always be re-selected.\n        }\n        if (\n            this.config.restrict_scan_source_location &&\n            !this.lastScanned.sourceLocation &&\n            !line.qty_done\n        ) {\n            return false; // Can't select a line if source is mandatory and wasn't scanned yet.\n        }\n        if (line.isPackageLine) {\n            // The next conditions concern product, skips them in case of package line.\n            return super.lineCanBeSelected(...arguments);\n        }\n        const product = line.product_id;\n        if (\n            this.config.restrict_put_in_pack === \"mandatory\" &&\n            this.selectedLine &&\n            this.selectedLine.qty_done &&\n            !this.selectedLine.result_package_id &&\n            this.selectedLine.product_id.id != product.id\n        ) {\n            return false; // Can't select another product if a package must be scanned first.\n        }\n        if (this.config.restrict_scan_product && product.barcode) {\n            // If the product scan is mandatory, a line can't be selected if its product isn't\n            // scanned first (as we can't keep track of each line's product scanned state, we\n            // consider a product was scanned if the line has a qty. greater than zero).\n            if (product.tracking === \"none\" || !this.config.restrict_scan_tracking_number) {\n                return (\n                    !this.getQtyDemand(line) ||\n                    this.getQtyDone(line) ||\n                    (this.lastScanned.product && this.lastScanned.product.id === line.product_id.id)\n                );\n            } else if (product.tracking != \"none\") {\n                return line.lot_name || (line.lot_id && line.qty_done);\n            }\n        }\n        return super.lineCanBeSelected(...arguments);\n    }\n\n    lineCanBeEdited(line) {\n        if (\n            this.config.restrict_scan_product &&\n            line.product_id.barcode &&\n            !this.getQtyDone(line) &&\n            (!this.lastScanned.product || this.lastScanned.product.id != line.product_id.id)\n        ) {\n            return false;\n        }\n        if (\n            line.product_id.tracking !== \"none\" &&\n            this.config.restrict_scan_tracking_number &&\n            !((line.lot_id && line.qty_done) || line.lot_name)\n        ) {\n            return false;\n        }\n        return this.lineCanBeSelected(line);\n    }\n\n    lineCanBeTakenFromTheCurrentLocation(line) {\n        // A line with no qty. done can be taken regardless its location (it will be overridden).\n        const res =\n            !this.getQtyDone(line) || super.lineCanBeTakenFromTheCurrentLocation(...arguments);\n        // If source location's scan is mandatory, the source should be confirmed (scanned once\n        // again) to confirm we want to take this product from the current location.\n        if (\n            res &&\n            this.config.restrict_scan_source_location &&\n            line.location_id.id !== this.location.id &&\n            this.lineIsReserved(line)\n        ) {\n            if (this.needSourceConfirmation[this.location.id] === undefined) {\n                this.needSourceConfirmation[this.location.id] = {};\n            }\n            if (\n                !this.scanHistory[1].location ||\n                this.scanHistory[1].location.id !== this.location.id\n            ) {\n                this.needSourceConfirmation[this.location.id][line.product_id.id] = true;\n                return false;\n            }\n            // The source was scanned just before, no need to confirm it for this product anymore.\n            this.needSourceConfirmation[this.location.id][line.product_id.id] = false;\n        }\n        return res;\n    }\n\n    lineIsReserved(line) {\n        return !line.picked && line.quantity;\n    }\n\n    async updateLine(line, args) {\n        await super.updateLine(...arguments);\n        let { location_id, location_dest_id, is_entire_pack } = args;\n        if (\"result_package_id\" in args) {\n            let resultPackage = args.result_package_id;\n            if (typeof resultPackage === \"number\") {\n                resultPackage = this.cache.getRecord(\"stock.package\", resultPackage);\n            }\n            if (\n                resultPackage.package_type_id &&\n                typeof resultPackage.package_type_id === \"number\"\n            ) {\n                resultPackage.package_type_id = this.cache.getRecord(\n                    \"stock.package.type\",\n                    resultPackage.package_type_id\n                );\n            }\n            line.result_package_id = resultPackage;\n        }\n        if (\"outermost_result_package_id\" in args) {\n            let outermostResultPackage = args.outermost_result_package_id;\n            if (typeof outermostResultPackage === \"number\") {\n                outermostResultPackage = this.cache.getRecord(\n                    \"stock.package\",\n                    outermostResultPackage\n                );\n            }\n            line.outermost_result_package_id = outermostResultPackage;\n        }\n        if (!args.dontUpdateSourceLocation && !location_id && this.lastScanned.sourceLocation) {\n            line.location_id = this.lastScanned.sourceLocation;\n            if (line.package_id && line.package_id.location_id != line.location_id.id) {\n                line.package_id = false;\n            }\n        }\n        if (location_dest_id) {\n            if (typeof location_dest_id === \"number\") {\n                location_dest_id = this.cache.getRecord(\"stock.location\", args.location_dest_id);\n            }\n            line.location_dest_id = location_dest_id;\n        }\n        if (is_entire_pack) {\n            line.is_entire_pack = is_entire_pack;\n        }\n    }\n\n    updateLineQty(virtualId, qty = 1) {\n        this.actionMutex.exec(() => {\n            const line = this.pageLines.find((l) => l.virtual_id === virtualId);\n            this.updateLine(line, { qty_done: qty });\n            this.trigger(\"update\");\n        });\n    }\n\n    get backordersDomain() {\n        return [[\"backorder_id\", \"=\", this.resId]];\n    }\n\n    get barcodeInfo() {\n        if (this.isCancelled || this.isDone) {\n            return {\n                class: this.isDone ? \"picking_already_done\" : \"picking_already_cancelled\",\n                message: this.isDone\n                    ? _t(\"This picking is already done\")\n                    : _t(\"This picking is cancelled\"),\n                icon: \"exclamation-triangle\",\n                warning: true,\n            };\n        }\n        // Takes the parent line if the current line is part of a group.\n        const parentLine = this._getParentLine(this.selectedLine);\n        const line = parentLine && this.getQtyDemand(parentLine) ? parentLine : this.selectedLine;\n        // Defines some messages who can appear in multiple cases.\n        const infos = {\n            scanScrLoc: {\n                message:\n                    this.considerPackageLines && !this.config.restrict_scan_source_location\n                        ? _t(\"Scan the source location or a package\")\n                        : _t(\"Scan the source location\"),\n                class: \"scan_src\",\n                icon: \"sign-out\",\n            },\n            scanDestLoc: {\n                message: _t(\"Scan the destination location\"),\n                class: \"scan_dest\",\n                icon: \"sign-in\",\n            },\n            scanProductOrDestLoc: {\n                message: this.considerPackageLines\n                    ? _t(\"Scan a product, a package or the destination location.\")\n                    : _t(\"Scan a product or the destination location.\"),\n                class: \"scan_product_or_dest\",\n            },\n            scanPackage: {\n                message: this._getScanPackageMessage(line),\n                class: \"scan_package\",\n                icon: \"archive\",\n            },\n            scanLot: {\n                message: _t(\"Scan a lot number\"),\n                class: \"scan_lot\",\n                icon: \"barcode\",\n            },\n            scanSerial: {\n                message: _t(\"Scan a serial number\"),\n                class: \"scan_serial\",\n                icon: \"barcode\",\n            },\n            pressValidateBtn: {\n                message: _t(\"Press Validate or scan another product\"),\n                class: \"scan_validate\",\n                icon: \"check-square\",\n            },\n        };\n        let barcodeInfo = {\n            message: _t(\"Scan a product\"),\n            class: \"scan_product\",\n            icon: \"tags\",\n        };\n        if ((line || this.lastScanned.packageId) && this.groups.group_stock_multi_locations) {\n            if (this.record.picking_type_code === \"outgoing\" && this.useScanSourceLocation) {\n                barcodeInfo = {\n                    message: _t(\"Scan more products, or scan a new source location\"),\n                    class: \"scan_product_or_src\",\n                };\n            } else if (this.config.restrict_scan_dest_location != \"no\") {\n                barcodeInfo = infos.scanProductOrDestLoc;\n            }\n        }\n\n        if (!line && this._moveEntirePackage()) {\n            // About package lines.\n            const packageLine = this.selectedPackageLine;\n            if (packageLine) {\n                if (this._lineIsComplete(packageLine)) {\n                    if (\n                        this.config.restrict_scan_source_location &&\n                        !this.lastScanned.sourceLocation\n                    ) {\n                        return infos.scanScrLoc;\n                    } else if (\n                        this.config.restrict_scan_dest_location != \"no\" &&\n                        !this.lastScanned.destLocation\n                    ) {\n                        return this.config.restrict_scan_dest_location == \"mandatory\"\n                            ? infos.scanDestLoc\n                            : infos.scanProductOrDestLoc;\n                    } else if (this.pageIsDone) {\n                        return infos.pressValidateBtn;\n                    } else {\n                        barcodeInfo.message = _t(\"Scan a product or another package\");\n                        barcodeInfo.class = \"scan_product_or_package\";\n                    }\n                } else {\n                    barcodeInfo.message = _t(\n                        \"Scan the package %s\",\n                        packageLine.result_package_id.name\n                    );\n                    barcodeInfo.icon = \"archive\";\n                }\n                return barcodeInfo;\n            } else if (this.considerPackageLines && barcodeInfo.class == \"scan_product\") {\n                barcodeInfo.message = _t(\"Scan a product or a package\");\n                barcodeInfo.class = \"scan_product_or_package\";\n            }\n        }\n        if (\n            barcodeInfo.class === \"scan_product\" &&\n            !(line || this.lastScanned.packageId) &&\n            this.config.restrict_scan_source_location &&\n            this.lastScanned.sourceLocation\n        ) {\n            barcodeInfo.message = _t(\n                \"Scan a product from %s\",\n                this.lastScanned.sourceLocation.name\n            );\n        }\n\n        // About source location.\n        if (this.useScanSourceLocation) {\n            if (!this.lastScanned.sourceLocation && !this.pageIsDone) {\n                return infos.scanScrLoc;\n            } else if (\n                this.lastScanned.sourceLocation &&\n                this.lastScanned.destLocation == \"no\" &&\n                line &&\n                this._lineIsComplete(line)\n            ) {\n                if (this.config.restrict_put_in_pack === \"mandatory\" && !line.result_package_id) {\n                    return {\n                        message: _t(\"Scan a package\"),\n                        class: \"scan_package\",\n                        icon: \"archive\",\n                    };\n                }\n                return infos.scanScrLoc;\n            }\n        }\n\n        if (!line) {\n            if (this.pageIsDone) {\n                // All is done, says to validate the transfer.\n                return infos.pressValidateBtn;\n            } else if (this.config.lines_need_to_be_packed) {\n                const lines = new Array(...this.pageLines, ...this.packageLines);\n                if (\n                    lines.every((line) => !this._lineIsNotComplete(line)) &&\n                    lines.some((line) => this._lineNeedsToBePacked(line))\n                ) {\n                    return infos.scanPackage;\n                }\n            }\n            return barcodeInfo;\n        }\n        const product = line.product_id;\n\n        // About tracking numbers.\n        if (\n            product.tracking !== \"none\" &&\n            (this.record.picking_type_id.use_create_lots ||\n                this.record.picking_type_id.use_existing_lots)\n        ) {\n            const isLot = product.tracking === \"lot\";\n            if (this.getQtyDemand(line) && (line.lot_id || line.lot_name)) {\n                // Reserved.\n                if (this.getQtyDone(line) === 0) {\n                    // Lot/SN not scanned yet.\n                    return isLot ? infos.scanLot : infos.scanSerial;\n                } else if (this.getQtyDone(line) < this.getQtyDemand(line)) {\n                    // Lot/SN scanned but not enough.\n                    barcodeInfo = isLot ? infos.scanLot : infos.scanSerial;\n                    barcodeInfo.message = isLot\n                        ? _t(\"Scan more lot numbers\")\n                        : _t(\"Scan another serial number\");\n                    return barcodeInfo;\n                }\n            } else if (!(line.lot_id || line.lot_name)) {\n                // Not reserved.\n                return isLot ? infos.scanLot : infos.scanSerial;\n            }\n        }\n\n        // About package.\n        if (this._lineNeedsToBePacked(line)) {\n            if (this._lineIsComplete(line)) {\n                return infos.scanPackage;\n            }\n            if (product.tracking == \"serial\") {\n                barcodeInfo.message = _t(\"Scan a serial number or a package\");\n            } else if (product.tracking == \"lot\") {\n                barcodeInfo.message =\n                    line.qty_done == 0\n                        ? _t(\"Scan a lot number\")\n                        : _t(\"Scan more lot numbers or a package\");\n                barcodeInfo.class = \"scan_lot\";\n            } else {\n                barcodeInfo.message = _t(\"Scan more products or a package\");\n            }\n            return barcodeInfo;\n        }\n\n        if (this.pageIsDone) {\n            barcodeInfo = infos.pressValidateBtn;\n        }\n\n        // About destination location.\n        const lineWaitingPackage =\n            this.groups.group_tracking_lot &&\n            this.config.restrict_put_in_pack != \"no\" &&\n            !line.result_package_id;\n        if (this.config.restrict_scan_dest_location != \"no\" && line.qty_done) {\n            if (this.pageIsDone) {\n                if (this.lastScanned.destLocation) {\n                    return infos.pressValidateBtn;\n                } else {\n                    return this.config.restrict_scan_dest_location == \"mandatory\" &&\n                        this._lineIsComplete(line)\n                        ? infos.scanDestLoc\n                        : infos.scanProductOrDestLoc;\n                }\n            } else if (this._lineIsComplete(line)) {\n                if (lineWaitingPackage) {\n                    barcodeInfo.message =\n                        this.config.restrict_scan_dest_location == \"mandatory\"\n                            ? _t(\"Scan a package or the destination location\")\n                            : _t(\"Scan a package, the destination location or another product\");\n                } else {\n                    return this.config.restrict_scan_dest_location == \"mandatory\"\n                        ? infos.scanDestLoc\n                        : infos.scanProductOrDestLoc;\n                }\n            } else {\n                barcodeInfo = infos.scanProductOrDestLoc;\n                if (product.tracking == \"serial\") {\n                    barcodeInfo.message = lineWaitingPackage\n                        ? _t(\"Scan a serial number or a package then the destination location\")\n                        : _t(\"Scan a serial number then the destination location\");\n                } else if (product.tracking == \"lot\") {\n                    barcodeInfo.message = lineWaitingPackage\n                        ? _t(\"Scan a lot number or a packages then the destination location\")\n                        : _t(\"Scan a lot number then the destination location\");\n                } else {\n                    barcodeInfo.message = lineWaitingPackage\n                        ? _t(\"Scan a product, a package or the destination location\")\n                        : _t(\"Scan a product then the destination location\");\n                }\n            }\n        }\n\n        return barcodeInfo;\n    }\n\n    get canBeProcessed() {\n        return ![\"cancel\", \"done\"].includes(this.record.state);\n    }\n\n    get displaySignatureButton() {\n        return (\n            this.record.picking_type_code === \"outgoing\" &&\n            !this.record.signature &&\n            this.groups.group_stock_sign_delivery\n        );\n    }\n\n    /**\n     * Depending of the config, a transfer can be fully validate even if nothing was scanned (like\n     * with an immediate transfer) or if at least one product was scanned.\n     * @returns {boolean}\n     */\n    get canBeValidate() {\n        if (!this._useReservation) {\n            return super.canBeValidate; // For immediate transfers, doesn't care about any special condition.\n        } else if (\n            !this.config.barcode_validation_full &&\n            !this.currentState.lines.some((line) => line.qty_done)\n        ) {\n            return false; // Can't be validate because \"full validation\" is forbidden and nothing was processed yet.\n        }\n        return super.canBeValidate;\n    }\n\n    get cancelLabel() {\n        return _t(\"Cancel Transfer\");\n    }\n\n    get canCreateNewLot() {\n        return this.record.use_create_lots;\n    }\n\n    get showReservedSns() {\n        if (this.canCreateNewLot && !this.useExistingLots) {\n            return false;\n        }\n        return this.record.picking_type_id.show_reserved_sns;\n    }\n\n    get canPutInPack() {\n        if (this.config.restrict_scan_product) {\n            return this.pageLines.some((line) => line.qty_done && !line.result_package_id);\n        }\n        return true;\n    }\n\n    get canScrap() {\n        const { picking_type_code, state } = this.record;\n        return (\n            (picking_type_code === \"incoming\" && state === \"done\") ||\n            (picking_type_code === \"outgoing\" && state !== \"done\") ||\n            picking_type_code === \"internal\"\n        );\n    }\n\n    get scrapContext() {\n        const context = this._getNewLineDefaultContext();\n        delete context.force_fullfil_quantity;\n        const moves = this.record.move_ids.map((id) => this.cache.getRecord(\"stock.move\", id));\n        context[\"product_ids\"] = moves.map((move) => move.product_id);\n        return context;\n    }\n\n    get canSelectLocation() {\n        return !(\n            this.config.restrict_scan_source_location ||\n            this.config.restrict_scan_dest_location != \"optional\"\n        );\n    }\n\n    shouldSplitLine(line) {\n        if (!line.qty_done || !line.reserved_uom_qty || line.qty_done >= line.reserved_uom_qty) {\n            return false; // No need to split a completed line or a line with no reservation.\n        }\n        line = this._getParentLine(line) || line;\n        return line.qty_done && line.reserved_uom_qty && line.qty_done < line.reserved_uom_qty;\n    }\n\n    /**\n     * Splits a line if its qty done is less than reserved.\n     * In case of a grouped line, if there's is a lot id or product tracking is serial,\n     * the new line doesn't need to be splitted since there is an existing line\n     * that will be grouped seperately after location is changed.\n     *\n     * @returns {Boolean|Object} Returns the new splitted line or false if line can't be split.\n     */\n    async splitLine(line) {\n        if (!this.shouldSplitLine(line)) {\n            return false;\n        }\n        // Use line's locations otherwise the picking's locations are used as default locations.\n        const fieldsParams = {\n            location_id: line.location_id.id,\n            location_dest_id: line.location_dest_id.id,\n            package_id: line.package_id?.id,\n        };\n        const newLine = await this._createNewLine({ copyOf: line, fieldsParams });\n        delete newLine.parentLine;\n        // Update the reservation of the both old and new lines.\n        newLine.reserved_uom_qty = line.reserved_uom_qty - line.qty_done;\n        line.reserved_uom_qty = line.qty_done;\n        // Be sure the new line has no lot by default.\n        newLine.lot_id = false;\n        newLine.lot_name = false;\n\n        return newLine;\n    }\n\n    /**\n     * The line's destination is changed to the given location, and if the line's reservation isn't\n     * fulfilled, the remaining qties are moved to a new line with the original destination location.\n     *\n     * @param {int} id location's id\n     */\n    async changeDestinationLocation(id, selectedLine) {\n        if (selectedLine.lines && !selectedLine.isPackageLine) {\n            this._clearScanData();\n            return false;\n        }\n        if (!selectedLine.lot_id) {\n            await this.splitLine(selectedLine);\n        }\n        // If the line has no reservation and is grouped with sibling lines,\n        // checks if we can assign to it a part of the reservation.\n        const parentLine = this._getParentLine(selectedLine);\n        if (\n            selectedLine.product_id.tracking === \"lot\" &&\n            parentLine &&\n            selectedLine.qty_done &&\n            !selectedLine.reserved_uom_qty\n        ) {\n            // Searches for a line with uncomplete reservation.\n            const uncompletedLine = parentLine.lines.find(\n                (line) => line.reserved_uom_qty && line.qty_done < line.reserved_uom_qty\n            );\n            if (uncompletedLine) {\n                // Checks if a portion of the reservation can be assign to the current line.\n                const remainingQty = Math.max(\n                    0,\n                    uncompletedLine.reserved_uom_qty - uncompletedLine.qty_done\n                );\n                const stolenReservation = Math.min(remainingQty, selectedLine.qty_done);\n                if (stolenReservation) {\n                    // Assigns the reservation on the current line.\n                    uncompletedLine.reserved_uom_qty -= stolenReservation;\n                    selectedLine.reserved_uom_qty = stolenReservation;\n                }\n            }\n        }\n        selectedLine.location_dest_id = this.cache.getRecord(\"stock.location\", id);\n        this._markLineAsDirty(selectedLine);\n        this._clearScanData();\n        return true;\n    }\n\n    _clearScanData() {\n        this.selectedLineVirtualId = false;\n        this.location = false;\n        this.lastScanned.packageId = false;\n        this.lastScanned.product = false;\n        this.scannedLinesVirtualId = [];\n    }\n\n    get considerPackageLines() {\n        return this._moveEntirePackage() && this.packageLines.length;\n    }\n\n    get displayAddProductButton() {\n        return !this._useReservation || this.config.barcode_allow_extra_product;\n    }\n\n    get displayCancelButton() {\n        return ![\"done\", \"cancel\"].includes(this.record.state);\n    }\n\n    get displayDestinationLocation() {\n        return (\n            this.groups.group_stock_multi_locations &&\n            [\"incoming\", \"internal\"].includes(this.record.picking_type_code)\n        );\n    }\n\n    get displayPutInPackButton() {\n        return this.groups.group_tracking_lot && this.config.restrict_put_in_pack != \"no\";\n    }\n\n    get displayResultPackage() {\n        return true;\n    }\n\n    get displaySourceLocation() {\n        return (\n            super.displaySourceLocation &&\n            [\"internal\", \"outgoing\"].includes(this.record.picking_type_code)\n        );\n    }\n\n    get displayReturnButton() {\n        return this.resModel === \"stock.picking\" && this.isDone;\n    }\n\n    get useScanSourceLocation() {\n        return super.useScanSourceLocation && this.config.restrict_scan_source_location;\n    }\n\n    get useScanDestinationLocation() {\n        return super.useScanDestinationLocation && this.config.restrict_scan_dest_location != \"no\";\n    }\n\n    get displayValidateButton() {\n        return true;\n    }\n\n    get highlightValidateButton() {\n        if (!this.pageLines.length && !this.packageLines.length) {\n            return false;\n        }\n        if (\n            this.config.lines_need_destination_location &&\n            !this.lastScanned.destLocation &&\n            (this.selectedLine || this.lastScanned.packageId)\n        ) {\n            return false;\n        }\n        for (let line of this.pageLines) {\n            line = this._getParentLine(line) || line;\n            if (this._lineIsNotComplete(line)) {\n                return false;\n            }\n        }\n        for (const packageLine of this.packageLines) {\n            if (this._lineIsNotComplete(packageLine)) {\n                return false;\n            }\n        }\n        return Boolean([...this.pageLines, ...this.packageLines].length);\n    }\n\n    get isDone() {\n        return this.record.state === \"done\";\n    }\n\n    get isCancelled() {\n        return this.record.state === \"cancel\";\n    }\n\n    lineIsFaulty(line) {\n        return (\n            this._useReservation &&\n            line.qty_done > line.reserved_uom_qty &&\n            (this.showReservedSns || !this.lineIsTracked(line))\n        );\n    }\n\n    get moveIds() {\n        return this.record.move_ids;\n    }\n\n    get packageLines() {\n        if (!this._moveEntirePackage() || !this.currentState.lines.length) {\n            return [];\n        }\n        return this._getPackageLines();\n    }\n\n    get pageIsDone() {\n        for (const line of this.groupedLines) {\n            if (\n                this._lineIsNotComplete(line) ||\n                this._lineNeedsToBePacked(line) ||\n                (line.product_id.tracking != \"none\" && !(line.lot_id || line.lot_name))\n            ) {\n                return false;\n            }\n        }\n        for (const line of this.packageLines) {\n            if (this._lineIsNotComplete(line)) {\n                return false;\n            }\n        }\n        return Boolean([...this.groupedLines, ...this.packageLines].length);\n    }\n\n    /**\n     * Returns only the lines.\n     * @returns {Array<Object>}\n     */\n    get pageLines() {\n        let lines = super.pageLines;\n        if (this._moveEntirePackage()) {\n            lines = lines.filter(\n                (line) => !(line.package_id && line.result_package_id && line.is_entire_pack)\n            );\n        }\n        return this._sortLine(lines);\n    }\n\n    get previousScannedLinesByPackage() {\n        if (this.lastScanned.packageId) {\n            return this.currentState.lines.filter(\n                (l) => l.result_package_id.id === this.lastScanned.packageId\n            );\n        }\n        return [];\n    }\n\n    get printButtons() {\n        const buttons = [\n            {\n                name: _t(\"Print Picking Operations\"),\n                class: \"o_print_picking\",\n                method: \"do_print_picking\",\n            },\n            {\n                name: _t(\"Print Delivery Slip\"),\n                class: \"o_print_delivery_slip\",\n                method: \"action_print_delivery_slip\",\n            },\n            {\n                name: _t(\"Print Barcodes\"),\n                class: \"o_print_barcodes\",\n                method: \"action_print_barcode\",\n            },\n        ];\n        if (this.groups.group_tracking_lot) {\n            buttons.push({\n                name: _t(\"Print Packages\"),\n                class: \"o_print_packages\",\n                method: \"action_print_packges\",\n            });\n        }\n\n        return buttons;\n    }\n\n    get reloadingMoveLines() {\n        return this.currentState !== undefined;\n    }\n\n    async save() {\n        if (this.linesToSave.length > 0) {\n            await this._setUser();\n        }\n        return super.save();\n    }\n\n    /**\n     * Return the last scanned package line (only relevant for \"move entire package\" operations.)\n     */\n    get selectedPackageLine() {\n        return (\n            this.lastScanned.packageId &&\n            this.packageLines.find((pl) => pl.result_package_id.id == this.lastScanned.packageId)\n        );\n    }\n\n    /**\n     * Return previously scanned packages.\n     */\n    get lastScannedPackages() {\n        const packages = [];\n        for (let barcodeIndex = 0; barcodeIndex < this.scanHistory.length; barcodeIndex++) {\n            const barcodeData = this.scanHistory[barcodeIndex];\n            if (barcodeData.package) {\n                packages.push(barcodeData.package);\n            } else if (barcodeData.packageType && barcodeIndex === 0) {\n                // Special case: if the last scanned barcode is a package type,\n                // we skip it without breaking the loop because it can mean we want\n                // to put in pack scanned package(s) into a new package of this type.\n                continue;\n            } else {\n                break;\n            }\n        }\n        return packages;\n    }\n\n    get useExistingLots() {\n        return this.record.use_existing_lots;\n    }\n\n    async uploadSignature({ signatureImage }) {\n        const file = signatureImage.split(\",\")[1];\n\n        this.ui.block();\n        await this.orm.write(this.resModel, [this.resId], {\n            signature: file,\n        });\n        this.ui.unblock();\n        await this.save();\n        this.trigger(\"refresh\");\n    }\n\n    openSignatureDialog(validateAfterSignature = false) {\n        const nameAndSignatureProps = {\n            mode: \"draw\",\n            displaySignatureRatio: 3,\n            signatureType: \"signature\",\n            noInputName: true,\n        };\n        const defaultName = this.record.partner_id?.display_name;\n\n        const dialogProps = {\n            defaultName,\n            nameAndSignatureProps,\n            uploadSignature: async (data) => {\n                await this.uploadSignature(data);\n                if (validateAfterSignature) {\n                    await super.validate();\n                }\n            },\n        };\n        this.dialogService.add(SignatureDialog, dialogProps);\n    }\n\n    get shouldOpenSignatureModal() {\n        const { picking_type_code: pickingTypeCode, signature } = this.record;\n        return (\n            pickingTypeCode === \"outgoing\" && !signature && this.groups.group_stock_sign_delivery\n        );\n    }\n\n    async validate() {\n        if (\n            this.config.lines_need_destination_location &&\n            !this.lastScanned.destLocation &&\n            (this.selectedLine || this.lastScanned.packageId)\n        ) {\n            return this.notification(_t(\"Destination location must be scanned\"), {\n                type: \"danger\",\n            });\n        }\n        if (\n            this.config.lines_need_to_be_packed &&\n            this.currentState.lines.some((line) => this._lineNeedsToBePacked(line))\n        ) {\n            return this.notification(_t(\"All products need to be packed\"), { type: \"danger\" });\n        }\n        await this._setUser();\n        if (this.config.create_backorder === \"ask\") {\n            // If there are some uncompleted lines, displays the backorder dialog.\n            const uncompletedLines = [];\n            const alreadyChecked = [];\n            let atLeastOneLinePartiallyProcessed = false;\n            for (let line of this.currentState.lines) {\n                line = this._getParentLine(line) || line;\n                if (alreadyChecked.includes(line.virtual_id)) {\n                    continue;\n                }\n                // Keeps track of already checked lines to avoid to check multiple times grouped lines.\n                alreadyChecked.push(line.virtual_id);\n                let qtyDone = line.qty_done;\n                if (qtyDone < line.reserved_uom_qty) {\n                    // Checks if another move line shares the same move id and adds its quantity done in that case.\n                    qtyDone += this.currentState.lines.reduce(\n                        (additionalQtyDone, otherLine) =>\n                            otherLine.product_id.id === line.product_id.id &&\n                            otherLine.move_id === line.move_id &&\n                            !otherLine.reserved_uom_qty\n                                ? additionalQtyDone + otherLine.qty_done\n                                : additionalQtyDone,\n                        0\n                    );\n                    if (qtyDone < line.reserved_uom_qty) {\n                        // Quantity done still insufficient.\n                        uncompletedLines.push(line);\n                    }\n                }\n                atLeastOneLinePartiallyProcessed = atLeastOneLinePartiallyProcessed || qtyDone > 0;\n            }\n            if (\n                this.showBackOrderDialog &&\n                atLeastOneLinePartiallyProcessed &&\n                uncompletedLines.length\n            ) {\n                this.trigger(\"playSound\");\n                return this.dialogService.add(BackorderDialog, {\n                    displayUoM: this.groups.group_uom,\n                    uncompletedLines,\n                    onApply: () => super.validate(),\n                });\n            }\n        }\n        if (this.record.return_id) {\n            this.validateContext = {\n                ...this.validateContext,\n                picking_ids_not_to_backorder: this.resId,\n            };\n        }\n        if (this.shouldOpenSignatureModal) {\n            this.openSignatureDialog(true);\n            return;\n        }\n        return await super.validate();\n    }\n\n    // -------------------------------------------------------------------------\n    // Private\n    // -------------------------------------------------------------------------\n\n    async _assignEmptyPackage(line, resultPackage) {\n        const fieldsParams = this._convertDataToFieldsParams({ resultPackage });\n        fieldsParams.dontUpdateSourceLocation = true;\n        const parentLine = this._getParentLine(line);\n        const targetLines = parentLine ? parentLine.lines : [line];\n        for (const subline of targetLines) {\n            // Assigns the result package on all sibling lines\n            if (subline === line || (subline.qty_done && !subline.result_package_id)) {\n                if (this.shouldSplitLine(subline)) {\n                    // Subline has no package already and is only partially full,\n                    // so we split off the remaining amount into a new move line\n                    const newLine = await this.splitLine(subline);\n                    [newLine.sortIndex, subline.sortIndex] = [subline.sortIndex, newLine.sortIndex];\n                    if (subline === line) {\n                        this.selectLine(newLine);\n                    }\n                }\n                await this.updateLine(subline, fieldsParams);\n            }\n        }\n    }\n\n    _getNewLineDefaultContext() {\n        return {\n            default_company_id: this.record.company_id,\n            default_location_id: this.lastScanned.sourceLocation.id || this._defaultLocation().id,\n            default_location_dest_id: this._defaultDestLocation().id,\n            default_picking_id: this.resId,\n            default_qty_done: 1,\n            display_default_code: false,\n            hide_unlink_button: Boolean(!this.selectedLine || this.selectedLine.reserved_uom_qty),\n            force_fullfil_quantity: this.selectedLine && this.selectedLine.reserved_uom_qty,\n        };\n    }\n\n    async _cancel() {\n        await this.save();\n        await this.orm.call(this.resModel, \"action_cancel\", [[this.resId]]);\n        this._cancelNotification();\n        this.trigger(\"history-back\");\n    }\n\n    _cancelNotification() {\n        this.notification(_t(\"The transfer has been cancelled\"));\n    }\n\n    _checkBarcode(barcodeData) {\n        const check = { title: _t(\"Not the expected scan\") };\n        const { location, lot, product, destLocation, packageType } = barcodeData;\n        const resultPackage = barcodeData.package;\n\n        if (this.config.restrict_scan_source_location && !barcodeData.location) {\n            // Special case where the user can not scan a destination but a source was already scanned.\n            // That means what is supposed to be a destination is in this case a source.\n            if (\n                this.lastScanned.sourceLocation &&\n                barcodeData.destLocation &&\n                this.config.restrict_scan_dest_location == \"no\"\n            ) {\n                barcodeData.location = barcodeData.destLocation;\n                delete barcodeData.destLocation;\n            }\n            // Special case where the source is mandatory and the app's waiting for but none was\n            // scanned, get the previous scanned one if possible.\n            if (!this.lastScanned.sourceLocation && this._currentLocation) {\n                this.lastScanned.sourceLocation = this._currentLocation;\n            }\n        }\n\n        if (\n            this.config.restrict_scan_source_location &&\n            !this._currentLocation &&\n            !this.selectedLine\n        ) {\n            // Source Location.\n            if (!location) {\n                check.title = _t(\"Mandatory Source Location\");\n                check.message = _t(\n                    \"You are supposed to scan %s or another source location\",\n                    this.location.display_name\n                );\n            }\n        } else if (this._mustScanProductFirst(barcodeData)) {\n            check.message = lot\n                ? _t(\"Scan a product before scanning a tracking number\")\n                : _t(\"You must scan a product\");\n        } else if (\n            this.config.restrict_put_in_pack == \"mandatory\" &&\n            !(resultPackage || packageType) &&\n            this.selectedLine &&\n            !this.qty_done &&\n            !this.selectedLine.result_package_id &&\n            ((product && product.id != this.selectedLine.product_id.id) || location || destLocation)\n        ) {\n            // Package.\n            check.message = _t(\"You must scan a package or put in pack\");\n        } else if (\n            this.config.restrict_scan_dest_location == \"mandatory\" &&\n            !this.lastScanned.destLocation\n        ) {\n            // Destination Location.\n            if (destLocation) {\n                this.lastScanned.destLocation = destLocation;\n            } else if (\n                product &&\n                this.selectedLine &&\n                this.selectedLine.product_id.id != product.id\n            ) {\n                // Cannot scan another product before a destination was scanned.\n                check.title = _t(\"Mandatory Destination Location\");\n                check.message = _t(\n                    \"Please scan destination location for %s before scanning other product\",\n                    this.selectedLine.product_id.display_name\n                );\n            }\n        }\n        check.error = Boolean(check.message);\n        return check;\n    }\n\n    _mustScanProductFirst(barcodeData) {\n        const { location, product } = barcodeData;\n        const packageWithQuant = barcodeData.package?.contained_quant_ids?.length;\n        return (\n            this.config.restrict_scan_product && // Restriction on product.\n            !(product || packageWithQuant || this.selectedLine) && // A product/package was scanned.\n            !(this.config.restrict_scan_source_location && location && !this.selectedLine) // Maybe the user scanned the wrong location and trying to scan the right one\n        );\n    }\n\n    async _closeValidate(ev) {\n        const record = await this.orm.read(this.resModel, [this.record.id], [\"state\"]);\n        if (record[0].state === \"done\") {\n            // Checks if the picking generated a backorder. Updates the picking's data if it's the case.\n            const backorders = await this.orm.searchRead(\n                this.backorderModel,\n                this.backordersDomain,\n                [\"display_name\", \"id\"]\n            );\n            const buttons = backorders.map((bo) => {\n                const additionalContext = { active_id: bo.id };\n                return {\n                    name: bo.display_name,\n                    onClick: () => {\n                        this.action.doAction(this.actionName, { additionalContext });\n                    },\n                };\n            });\n            if (backorders.length) {\n                const phrase =\n                    backorders.length === 1\n                        ? _t(\"Following backorder was created:\")\n                        : _t(\"Following backorders were created:\");\n                this.validateMessage = markup`<div>\n                    <p>${this.validateMessage}<br>${phrase}</p>\n                </div>`;\n            }\n            // If all is OK, displays a notification and goes back to the previous page.\n            this.notification(this.validateMessage, { type: \"success\", buttons });\n            this.trigger(\"history-back\");\n        }\n    }\n\n    _convertDataToFieldsParams(args) {\n        const params = {\n            lot_name: args.lotName,\n            product_id: args.product,\n            qty_done: args.quantity,\n        };\n        if (args.lot) {\n            params.lot_id = args.lot;\n        }\n        if (args.package) {\n            params.package_id = args.package;\n        }\n        if (\n            args.packaging &&\n            args.product.tracking === \"serial\" &&\n            (this.useExistingLots || this.canCreateNewLot)\n        ) {\n            params.packaging = args.packaging;\n            params.qty_done = 0;\n        }\n        if (args.resultPackage) {\n            params.result_package_id = args.resultPackage;\n        }\n        if (args.owner) {\n            params.owner_id = args.owner;\n        }\n        if (args.destLocation) {\n            params.location_dest_id = args.destLocation.id;\n        }\n        if (args.srcLocation) {\n            params.location_id = args.srcLocation;\n        }\n        if (args.isEntirePack) {\n            params.is_entire_pack = args.isEntirePack;\n        }\n        return params;\n    }\n\n    _createCommandVals(line) {\n        const values = {\n            dummy_id: line.virtual_id,\n            is_entire_pack: line.is_entire_pack,\n            location_id: line.location_id,\n            location_dest_id: line.location_dest_id,\n            lot_name: line.lot_name,\n            lot_id: line.lot_id,\n            package_id: line.package_id,\n            picking_id: line.picking_id,\n            picked: true,\n            product_id: line.product_id,\n            product_uom_id: line.product_uom_id,\n            owner_id: line.owner_id,\n            quantity: line.qty_done,\n            result_package_id: line.result_package_id,\n            state: \"assigned\",\n        };\n        for (const [key, value] of Object.entries(values)) {\n            values[key] = this._fieldToValue(value);\n        }\n        return values;\n    }\n\n    _getMoveLineData(id) {\n        const smlData = this.cache.getRecord(\"stock.move.line\", id);\n        smlData.dummy_id = smlData.dummy_id && Number(smlData.dummy_id);\n        // Checks if this line is already in the picking's state to get back\n        // its `virtual_id` (and so, avoid to set a new `virtual_id`).\n        let prevLine = this.currentState?.lines.find((line) => line.id === id);\n        if (!prevLine && smlData.dummy_id) {\n            prevLine = this.currentState?.lines.find(\n                (line) => line.virtual_id === smlData.dummy_id\n            );\n        }\n        const previousVirtualId = prevLine && prevLine.virtual_id;\n        smlData.virtual_id = smlData.dummy_id || previousVirtualId || this._uniqueVirtualId;\n        smlData.product_id = this.cache.getRecord(\"product.product\", smlData.product_id);\n        smlData.product_uom_id = this.cache.getRecord(\"uom.uom\", smlData.product_uom_id);\n        smlData.packaging_uom_id =\n            smlData.packaging_uom_id && this.cache.getRecord(\"uom.uom\", smlData.packaging_uom_id);\n        smlData.location_id = this.cache.getRecord(\"stock.location\", smlData.location_id);\n        smlData.location_dest_id = this.cache.getRecord(\"stock.location\", smlData.location_dest_id);\n        smlData.lot_id = smlData.lot_id && this.cache.getRecord(\"stock.lot\", smlData.lot_id);\n        smlData.owner_id =\n            smlData.owner_id && this.cache.getRecord(\"res.partner\", smlData.owner_id);\n        smlData.package_id =\n            smlData.package_id && this.cache.getRecord(\"stock.package\", smlData.package_id);\n        smlData.outermost_result_package_id =\n            smlData.outermost_result_package_id &&\n            this.cache.getRecord(\"stock.package\", smlData.outermost_result_package_id);\n\n        if (smlData.package_id?.parent_package_id) {\n            smlData.package_id.parent_package_id = this.cache.getRecord(\n                \"stock.package\",\n                smlData.package_id.parent_package_id\n            );\n        }\n\n        if (this.reloadingMoveLines) {\n            if (prevLine) {\n                smlData.sortIndex = prevLine.sortIndex;\n                if (smlData.quantity && !smlData.qty_done) {\n                    // The reservation likely changed.\n                    smlData.reserved_uom_qty = smlData.quantity;\n                } else {\n                    if (smlData.product_uom_id.id !== prevLine.product_uom_id.id) {\n                        // Compatible but not the same UoM => Need a conversion.\n                        const params = { digits: [false, this.precision] };\n                        const baseQty =\n                            (prevLine.reserved_uom_qty * prevLine.product_uom_id.factor) /\n                            smlData.product_uom_id.factor;\n                        smlData.reserved_uom_qty = parseFloat(formatFloat(baseQty, params));\n                    } else {\n                        // The reservation of this line is already known.\n                        smlData.reserved_uom_qty = prevLine.reserved_uom_qty;\n                    }\n                }\n            } else {\n                // This line was created in the Barcode App, so it has no reservation.\n                smlData.qty_done = smlData.quantity;\n                smlData.reserved_uom_qty = 0;\n            }\n        } else {\n            // First loading: `reserved_uom_qty` keeps in memory what is the\n            // initial reservation for this move line clientside only, this\n            // information is lost once the user closes the operation.\n            smlData.reserved_uom_qty = smlData.quantity;\n        }\n\n        const resultPackage =\n            smlData.result_package_id &&\n            this.cache.getRecord(\"stock.package\", smlData.result_package_id);\n        if (resultPackage) {\n            // Fetch the package type if needed.\n            smlData.result_package_id = resultPackage;\n            const packageType = resultPackage && resultPackage.package_type_id;\n            resultPackage.package_type_id =\n                packageType && this.cache.getRecord(\"stock.package.type\", packageType);\n        }\n        if (smlData.result_package_id?.parent_package_id) {\n            smlData.result_package_id.parent_package_id = this.cache.getRecord(\n                \"stock.package\",\n                smlData.result_package_id.parent_package_id\n            );\n        }\n        return smlData;\n    }\n\n    _createLinesState() {\n        const lines = [];\n        const picking = this.cache.getRecord(this.resModel, this.resId);\n        for (const id of picking.move_line_ids) {\n            const smlData = this._getMoveLineData(id);\n            lines.push(smlData);\n        }\n        return lines;\n    }\n\n    _defaultLocation() {\n        return this.cache.getRecord(\"stock.location\", this.record.location_id);\n    }\n\n    _defaultDestLocation() {\n        return this.cache.getRecord(\"stock.location\", this.record.location_dest_id);\n    }\n\n    _getCommands() {\n        const commands = Object.assign(super._getCommands(), {\n            OBTPRSL: this.print.bind(this, false, \"action_print_delivery_slip\"),\n            OBTPROP: this.print.bind(this, false, \"do_print_picking\"),\n            OBTSCRA: this._scrap.bind(this),\n            OBTRETU: this._returnProducts.bind(this),\n        });\n        if (!this.isDone) {\n            commands[\"OBTPACK\"] = this._putInPack.bind(this);\n            commands[\"OCDCANC\"] = this._cancel.bind(this);\n        }\n        // Dummy command to avoid error message.\n        commands.OBTUPCK = () => {};\n        return commands;\n    }\n\n    _getDefaultMessageType() {\n        if (this.useScanSourceLocation && !this.lastScanned.sourceLocation) {\n            return \"scan_src\";\n        }\n        return \"scan_product\";\n    }\n\n    _getModelRecord() {\n        const record = this.cache.getRecord(this.resModel, this.resId);\n        if (record.picking_type_id && record.state !== \"cancel\") {\n            record.picking_type_id = this.cache.getRecord(\n                \"stock.picking.type\",\n                record.picking_type_id\n            );\n        }\n        if (record.partner_id && record.state !== \"cancel\") {\n            record.partner_id = this.cache.getRecord(\"res.partner\", record.partner_id);\n        }\n        return record;\n    }\n\n    _getNewLineDefaultValues(fieldsParams) {\n        const defaultValues = super._getNewLineDefaultValues(...arguments);\n        if (\n            this.selectedLine &&\n            !fieldsParams.move_id &&\n            this.selectedLine.product_id.id === fieldsParams.product_id?.id\n        ) {\n            defaultValues.move_id = this.selectedLine.move_id;\n        }\n        const newLineDefaultVals = Object.assign(defaultValues, {\n            location_dest_id: this._defaultDestLocation(),\n            reserved_uom_qty: 0,\n            qty_done: 0,\n            picking_id: this.resId,\n            result_package_id: false,\n            is_entire_pack: false,\n        });\n        if (fieldsParams.product_id?.tracking === \"serial\" && fieldsParams.packaging) {\n            newLineDefaultVals.reserved_uom_qty = 1;\n        }\n        return newLineDefaultVals;\n    }\n\n    _getFieldToWrite() {\n        return [\n            \"is_entire_pack\",\n            \"location_id\",\n            \"location_dest_id\",\n            \"lot_id\",\n            \"lot_name\",\n            \"package_id\",\n            \"outermost_result_package_id\",\n            \"owner_id\",\n            \"qty_done\",\n            \"result_package_id\",\n        ];\n    }\n\n    _getPackageLines() {\n        const linesWithPackage = this.currentState.lines.filter(\n            (line) =>\n                line.is_entire_pack &&\n                line.package_id &&\n                line.result_package_id &&\n                line.package_id.complete_name === line.result_package_id.complete_name\n        );\n        // Groups lines by package.\n        const groupedLines = {};\n        for (const line of linesWithPackage) {\n            const packageId =\n                line.package_id.outermost_package_id ||\n                line.outermost_result_package_id?.id ||\n                line.package_id.id;\n            if (!groupedLines[packageId]) {\n                groupedLines[packageId] = [];\n            }\n            groupedLines[packageId].push(line);\n        }\n        const packageLines = [];\n        for (const key in groupedLines) {\n            // Check if the package is reserved.\n            const reservedPackage = groupedLines[key].every((line) => this.lineIsReserved(line));\n            groupedLines[key][0].reservedPackage = reservedPackage;\n            const packageLine = Object.assign({}, groupedLines[key][0], {\n                lines: groupedLines[key],\n                isPackageLine: true,\n            });\n            packageLines.push(packageLine);\n        }\n        return this._sortLine(packageLines);\n    }\n\n    _getSaveCommand() {\n        const commands = this._getSaveLineCommand();\n        if (commands.length) {\n            return {\n                route: \"/stock_barcode/save_barcode_data\",\n                params: {\n                    model: this.resModel,\n                    res_id: this.resId,\n                    write_field: \"move_line_ids\",\n                    write_vals: commands,\n                },\n            };\n        }\n        return {};\n    }\n\n    _getScanPackageMessage() {\n        return _t(\"Scan a package or put in pack\");\n    }\n\n    _groupSublines() {\n        const groupedLine = super._groupSublines(...arguments);\n        groupedLine.reserved_uom_qty = groupedLine.totalQtyDemand;\n        groupedLine.qty_done = groupedLine.totalQtyDone;\n        return groupedLine;\n    }\n\n    _incrementTrackedLine() {\n        return !(this.record.use_create_lots || this.record.use_existing_lots);\n    }\n\n    _lineCannotBeTaken(line) {\n        // A packed line without expected quantity or completed cannot be taken\n        const fullyPacked =\n            line.result_package_id && (!line.reserved_uom_qty || this._lineIsComplete(line));\n        return fullyPacked || super._lineCannotBeTaken(...arguments);\n    }\n\n    _lineIsComplete(line) {\n        const isComplete = line.reserved_uom_qty && line.qty_done >= line.reserved_uom_qty;\n        if (line.isPackageLine && !line.reserved_uom_qty && line.qty_done) {\n            return true; // For package line, considers an unreserved package as a completed line.\n        }\n        if (isComplete && line.lines) {\n            // Grouped lines/package lines have multiple sublines.\n            for (const subline of line.lines) {\n                // For tracked product, a line with `qty_done` but no tracking number is considered as not complete.\n                if (subline.product_id.tracking != \"none\") {\n                    if (subline.qty_done && !(subline.lot_id || subline.lot_name)) {\n                        return false;\n                    }\n                } else if (\n                    subline.reserved_uom_qty &&\n                    subline.qty_done < subline.reserved_uom_qty\n                ) {\n                    return false;\n                }\n            }\n        }\n        return isComplete;\n    }\n\n    _lineIsNotComplete(line) {\n        const currentLine =\n            (line.product_id.tracking !== \"none\" && this._getParentLine(line)) || line;\n        let isNotComplete =\n            currentLine.reserved_uom_qty && currentLine.qty_done < currentLine.reserved_uom_qty;\n        // if we're using the parent line we don't want to return true if the parent line is incomplete but not the line\n        if (isNotComplete && line != currentLine) {\n            isNotComplete = line.reserved_uom_qty && line.qty_done < line.reserved_uom_qty;\n        }\n        if (!isNotComplete && currentLine.lines) {\n            // Grouped lines/package lines have multiple sublines.\n            for (const subline of currentLine.lines) {\n                // For tracked product, a line with `qty_done` but no tracking number is considered as not complete.\n                if (subline.product_id.tracking != \"none\") {\n                    if (subline.qty_done && !(subline.lot_id || subline.lot_name)) {\n                        return true;\n                    }\n                } else if (\n                    subline.reserved_uom_qty &&\n                    subline.qty_done < subline.reserved_uom_qty\n                ) {\n                    return true;\n                }\n            }\n        }\n        return isNotComplete;\n    }\n\n    _lineNeedsToBePacked(line) {\n        return Boolean(\n            this.config.lines_need_to_be_packed && line.qty_done && !line.result_package_id\n        );\n    }\n\n    _moveEntirePackage() {\n        return this.record.picking_type_entire_packs;\n    }\n\n    async _processBarcode(barcode) {\n        if (this.isDone && !this.commands[barcode]) {\n            return this.notification(_t(\"This picking is already done\"), { type: \"danger\" });\n        }\n        return super._processBarcode(barcode);\n    }\n\n    async _processLocation(barcodeData) {\n        super._processLocation(...arguments);\n        if (barcodeData.destLocation) {\n            await this._processLocationDestination(barcodeData);\n            this.trigger(\"update\");\n        }\n    }\n\n    async _processLocationSource(barcodeData) {\n        // For planned transfers, check the scanned location is a part of transfer source location.\n        if (\n            this._useReservation &&\n            !this._isSublocation(barcodeData.location, this._defaultLocation())\n        ) {\n            barcodeData.stopped = true;\n            const message = _t(\"The scanned location doesn't belong to this operation's location\");\n            return this.notification(message, { type: \"danger\" });\n        }\n        super._processLocationSource(...arguments);\n        // Splits uncompleted lines to be able to add reserved products from unreserved location.\n        let currentLine = this.selectedLine || this.lastScannedLine;\n        currentLine = this._getParentLine(currentLine) || currentLine;\n        if (currentLine && currentLine.location_id.id !== barcodeData.location.id) {\n            const qtyDone = this.getQtyDone(currentLine);\n            const reservedQty = this.getQtyDemand(currentLine);\n            const remainingQty = reservedQty - qtyDone;\n            if (this.shouldSplitLine(currentLine)) {\n                const fieldsParams = this._convertDataToFieldsParams(barcodeData);\n                let newLine;\n                if (currentLine.lines) {\n                    for (const line of currentLine.lines) {\n                        if (!line.reserved_uom_qty) {\n                            line.reserved_uom_qty = line.qty_done;\n                        }\n                        if (this.shouldSplitLine(line) && !newLine) {\n                            newLine = await this._createNewLine({\n                                copyOf: line,\n                                fieldsParams,\n                            });\n                            delete newLine.parentLine;\n                            line.reserved_uom_qty = line.qty_done;\n                        }\n                    }\n                } else {\n                    newLine = await this._createNewLine({\n                        copyOf: currentLine,\n                        fieldsParams,\n                    });\n                }\n                currentLine.reserved_uom_qty = qtyDone;\n                if (newLine) {\n                    newLine.reserved_uom_qty = remainingQty;\n                    newLine.lot_id = false;\n                    this._markLineAsDirty(newLine);\n                }\n                this._markLineAsDirty(currentLine);\n            }\n        }\n    }\n\n    /**\n     * Returns true if the first given location is a sublocation of the second given location.\n     * @param {Object} childLocation\n     * @param {Object} parentLocation\n     * @returns {boolean}\n     */\n    _isSublocation(childLocation, parentLocation) {\n        return childLocation.parent_path.includes(parentLocation.parent_path);\n    }\n\n    _getLinesToMove() {\n        const configScanDest = this.config.restrict_scan_dest_location;\n        // Usually, assign the destination to the selected line or to the selected package's lines.\n        let lines = this.selectedPackageLine?.lines || this.selectedLine ? [this.selectedLine] : [];\n        if (configScanDest === \"mandatory\" && this.selectedLine?.product_id?.tracking !== \"none\") {\n            // When we assign the location to only the last scanned line, if the selected line is\n            // tracked, we want to assign the destination to its scanned sibling lines too.\n            const parentLine = this._getParentLine(this.selectedLine);\n            if (parentLine) {\n                lines = this.previousScannedLines.filter((line) =>\n                    parentLine.virtual_ids.includes(line.virtual_id)\n                );\n            }\n        } else if (configScanDest === \"optional\" && this.previousScannedLines?.length) {\n            // If config is \"After group of Products\", get all previously scanned lines.\n            for (const line of this.previousScannedLines) {\n                if (!lines.find((l) => l.virtual_id === line.virtual_id)) {\n                    lines.push(line);\n                }\n            }\n        }\n        if (this.previousScannedLinesByPackage?.length) {\n            // In case some lines were added by scanning a package, get those lines.\n            lines = this.previousScannedLinesByPackage;\n        }\n\n        return Array.from(new Set(lines));\n    }\n\n    _getLineMoveId(line) {\n        return line.move_id;\n    }\n\n    _onExit() {\n        if ([\"done\", \"cancel\"].includes(this.record.state) || this.moveIds?.length === 0) {\n            // No need to all post process if operation is closed or have no move.\n            return;\n        }\n        const quantitiesByMove = this.initialState.lines.reduce((res, line) => {\n            const moveId = this._getLineMoveId(line);\n            if (res[moveId]) {\n                res[moveId].quantity_done += line.qty_done;\n                res[moveId].reserved_uom_qty += line.reserved_uom_qty;\n            } else {\n                res[moveId] = {\n                    quantity_done: line.qty_done,\n                    reserved_uom_qty: line.reserved_uom_qty,\n                };\n            }\n            return res;\n        }, {});\n        return this.orm.call(\"stock.move\", \"post_barcode_process\", [\n            this.moveIds,\n            quantitiesByMove,\n        ]);\n    }\n\n    async _processLocationDestination(barcodeData) {\n        const configScanDest = this.config.restrict_scan_dest_location;\n        if (configScanDest == \"no\") {\n            return;\n        }\n        // For planned transfers, check the scanned location is a part of transfer destination.\n        if (\n            this._useReservation &&\n            !this._isSublocation(barcodeData.destLocation, this._defaultDestLocation())\n        ) {\n            barcodeData.stopped = true;\n            const message = _t(\n                \"The scanned location doesn't belong to this operation's destination\"\n            );\n            return this.notification(message, { type: \"danger\" });\n        }\n\n        // Change the destination of all concerned lines.\n        const lines = this._getLinesToMove();\n        for (const line of lines) {\n            await this.changeDestinationLocation(barcodeData.destLocation.id, line);\n        }\n        barcodeData.stopped = true;\n    }\n\n    async _processPackage(barcodeData) {\n        const { packageName } = barcodeData;\n        const recPackage = barcodeData.package;\n        if (barcodeData.packageType && !recPackage) {\n            // Scanned a package type and no existing package: make a put in pack (forced package type).\n            barcodeData.stopped = true;\n            return await this._processPackageType(barcodeData);\n        } else if (packageName && !recPackage) {\n            // Scanned a non-existing package: make a put in pack.\n            this.lastScanned.packageId = false;\n            barcodeData.stopped = true;\n            return await this._putInPack({ default_name: packageName });\n        } else if (!recPackage) {\n            return; // No package, package's type or package's name => Nothing to do.\n        }\n        const packLocation = recPackage.location_id\n            ? this.cache.dbIdCache[\"stock.location\"][recPackage.location_id]\n            : false;\n        if (recPackage.location_id && !packLocation) {\n            // The package is in a location but the location was not found in the cache,\n            // surely because this location is not related to this picking.\n            return;\n        }\n        if (\n            packLocation &&\n            packLocation.id !== this._defaultDestLocation().id &&\n            ((this.config.restrict_scan_source_location && packLocation.id !== this.location.id) ||\n                (!this.config.restrict_scan_source_location &&\n                    !this._isSublocation(packLocation, this.location)))\n        ) {\n            // Package is not located at the destination (result package) and is not located at the\n            // scanned source location (or one of its sublocations) neither.\n            return;\n        }\n\n        let alreadyDonePackId;\n        let scannedPackages = false;\n        for (const packageLine of this.packageLines) {\n            if (!this._isPackageInPackage(packageLine.package_id, recPackage)) {\n                continue;\n            }\n            // Scanned package is either a parent of a line package or the package itself,\n            // thus need to validate all relevant lines.\n            if (packageLine.qty_done) {\n                alreadyDonePackId = recPackage.id;\n                continue;\n            }\n            for (const line of packageLine.lines) {\n                this.selectedLineVirtualId = line.virtual_id;\n                await this._updateLineQty(line, { qty_done: line.reserved_uom_qty });\n                this._markLineAsDirty(line);\n                scannedPackages = true;\n            }\n        }\n        if (alreadyDonePackId) {\n            this.lastScanned.packageId = alreadyDonePackId;\n            this.notification(_t(\"This package is already scanned.\"), { type: \"danger\" });\n        }\n        if (scannedPackages || alreadyDonePackId) {\n            this.lastScanned.packageId = recPackage.id;\n            barcodeData.stopped = true;\n            return this.trigger(\"update\");\n        }\n\n        // Scanned a package: fetches package's quant and creates a line for\n        // each of them, except if the package is already scanned.\n        // TODO: can check if quants already in cache to avoid to make a RPC if\n        // there is all in it (or make the RPC only on missing quants).\n        this.lastScanned.packageId = false;\n        const res = await this.orm.call(\"stock.quant\", \"get_stock_barcode_data_records\", [\n            recPackage.contained_quant_ids,\n        ]);\n        this.cache.setCache(res.records);\n        const quants = res.records[\"stock.quant\"];\n        // Do not allow extra products if they are not allowed\n        if (!this.config.barcode_allow_extra_product) {\n            const allowedProductIds = new Set(\n                this.currentState.lines.map((line) => line.product_id.id)\n            );\n            if (quants.some((quant) => !allowedProductIds.has(quant.product_id))) {\n                barcodeData.error = _t(\n                    \"This package contains extra products and extra products are not allowed on this operation.\"\n                );\n                return;\n            }\n        }\n        // If the package is empty or is already at the destination location,\n        // assign it to the last scanned line.\n        const currentLine = this.selectedLine || this.lastScannedLine;\n        if (\n            currentLine &&\n            (!quants.length || recPackage.location_id === currentLine.location_dest_id.id)\n        ) {\n            const linesToUpdate = [currentLine];\n            if (this.config.restrict_put_in_pack === \"optional\") {\n                // If the current line is not packed yet, we want to pack only unpacked lines,\n                // but if the current line is packed, we want to pack only the lines who are\n                // already packed but where the package isn't itself in another package.\n                const filterFunction = !currentLine.result_package_id\n                    ? (line) => !line.result_package_id\n                    : (line) => line.result_package_id && !line.result_package_id.package_dest_id;\n                linesToUpdate.push(\n                    ...this.previousScannedLines.filter(\n                        (line) =>\n                            line.qty_done &&\n                            line.virtual_id !== currentLine.virtual_id &&\n                            filterFunction(line)\n                    )\n                );\n            }\n            if (!currentLine.result_package_id) {\n                for (const line of linesToUpdate) {\n                    await this._assignEmptyPackage(line, recPackage);\n                }\n            } else {\n                const packageIds = linesToUpdate.map((l) => l.result_package_id?.id);\n                await this._putPackInPack(packageIds, {\n                    default_package_id: recPackage.id,\n                });\n            }\n            barcodeData.stopped = true;\n            this.lastScanned.packageId = recPackage.id;\n            this.trigger(\"update\");\n            return;\n        }\n\n        if (this.location && (!packLocation || !this._isSublocation(packLocation, this.location))) {\n            // Package not at the source location: can't add its content.\n            return;\n        }\n        // Checks if the package is already scanned.\n        let alreadyExisting = 0;\n        for (const line of this.pageLines) {\n            if (\n                line.package_id &&\n                line.package_id.id === recPackage.id &&\n                this.getQtyDone(line) > 0\n            ) {\n                alreadyExisting++;\n            }\n        }\n        if (alreadyExisting >= quants.length) {\n            barcodeData.error = _t(\"This package is already scanned.\");\n            return;\n        }\n\n        if (alreadyExisting) {\n            const userConfirmation = new Deferred();\n            this.dialogService.add(ConfirmationDialog, {\n                body: _t(\n                    \"You have already scanned %s items of this package. Do you want to scan the whole package?\",\n                    alreadyExisting\n                ),\n                title: _t(\"Scanning package\"),\n                cancel: () => userConfirmation.resolve(false),\n                confirm: () => userConfirmation.resolve(true),\n                close: () => userConfirmation.resolve(false),\n            });\n            if (!(await userConfirmation)) {\n                barcodeData.stopped = true;\n                return;\n            }\n        }\n\n        // For each quants, creates or increments a barcode line.\n        for (const quant of quants) {\n            const quantUoM = this.cache.getRecord(\"uom.uom\", quant.product_uom_id);\n            const product = this.cache.getRecord(\"product.product\", quant.product_id);\n            const quantPackage = this.cache.getRecord(\"stock.package\", quant.package_id);\n            const searchLineParams = Object.assign({}, barcodeData, { product, quantPackage });\n            let remaining_qty = quant.quantity;\n            let qty_used = 0;\n            while (remaining_qty > 0) {\n                const currentLine = this._findLine(searchLineParams);\n                if (currentLine) {\n                    // Updates an existing line.\n                    const uomFactor = quantUoM.factor / currentLine.product_uom_id.factor;\n                    const lineQtyDiff = currentLine.reserved_uom_qty - currentLine.qty_done;\n                    const qtyNeeded = Math.max(lineQtyDiff, 0) / uomFactor;\n                    qty_used = qtyNeeded ? Math.min(qtyNeeded, remaining_qty) : remaining_qty;\n                    const fieldsParams = this._convertDataToFieldsParams({\n                        quantity: qty_used * uomFactor,\n                        lotName: barcodeData.lotName,\n                        lot: barcodeData.lot,\n                        package: quant.package_id,\n                        owner: barcodeData.owner,\n                    });\n                    await this.updateLine(currentLine, fieldsParams);\n                } else {\n                    // Creates a new line.\n                    qty_used = remaining_qty;\n                    const isEntirePack = qty_used === quant.quantity;\n                    const fieldsParams = this._convertDataToFieldsParams({\n                        product,\n                        quantity: qty_used,\n                        lot: quant.lot_id,\n                        package: quant.package_id,\n                        resultPackage: quant.package_id,\n                        owner: quant.owner_id,\n                        srcLocation: quant.location_id,\n                        isEntirePack,\n                    });\n                    if (quant.package_id !== recPackage.id) {\n                        fieldsParams.outermost_result_package_id = recPackage.id;\n                    }\n                    const newLine = await this._createNewLine({ fieldsParams });\n                    if (isEntirePack) {\n                        // Keep in memory what was the initial package line's quantity.\n                        newLine.packedQuantity = qty_used;\n                    }\n                }\n                remaining_qty -= qty_used;\n            }\n        }\n        barcodeData.stopped = true;\n        this.selectedLineVirtualId = false;\n        this.lastScanned.packageId = recPackage.id;\n        this.trigger(\"update\");\n    }\n\n    async _processPackageType(barcodeData) {\n        const { packageType } = barcodeData;\n        if (!this.selectedLine && this.lastScannedPackages.length && packageType) {\n            // One or multiple packages were previously scanned and a package\n            // type is now scanned => Put the scanned package(s) in a new package.\n            const packageIds = this.lastScannedPackages.map((pack) => pack.id);\n            return await this._putPackInPack(packageIds, {\n                default_package_type_id: packageType.id,\n                default_name: barcodeData?.packageName,\n            });\n        }\n        if (!this.selectedLine || !this.selectedLine.qty_done) {\n            barcodeData.stopped = true;\n            const message = _t(\n                \"You can't apply a package type. First, scan product or select a line\"\n            );\n            return this.notification(message, { type: \"warning\" });\n        }\n        const resultPackage = this.selectedLine.result_package_id;\n        if (!resultPackage) {\n            // No package on the line => Do a put in pack.\n            const additionalParams = { default_package_type_id: packageType.id };\n            if (barcodeData.packageName) {\n                additionalParams.default_name = barcodeData.packageName;\n            }\n            await this._putInPack(additionalParams);\n        } else if (!resultPackage.package_type_id) {\n            // Changes the package type for the scanned one.\n            await this.save();\n            await this.orm.write(\"stock.package\", [resultPackage.id], {\n                package_type_id: packageType.id,\n            });\n            const message = _t(\"Package type %(type)s applied to the package %(package)s\", {\n                type: packageType.name,\n                package: resultPackage.name,\n            });\n            this.notification(message, { type: \"success\" });\n            this.trigger(\"refresh\");\n        } else {\n            // Put package(s) inside a new one of the scanned package type.\n            const packageToPackIds = this.getPackageToPackIds();\n            return this._putPackInPack(packageToPackIds, {\n                default_package_type_id: packageType.id,\n                default_name: barcodeData?.packageName,\n            });\n        }\n    }\n\n    async _putInPack(additionalParams = {}) {\n        const context = { barcode_view: true };\n        if (this.selectedLine?.result_package_id) {\n            // If selected line is already packed, we do a \"pack in pack\" instead of a \"put in pack\"\n            const packageToPackIds = this.getPackageToPackIds();\n            return this._putPackInPack(packageToPackIds, context);\n        }\n        if (!this.groups.group_tracking_lot) {\n            return this.notification(_t(\"To use packages, enable 'Packages' in the settings\"), {\n                type: \"danger\",\n            });\n        }\n        // Before the put in pack, create a new empty move line with the remaining\n        // quantity for each uncompleted move line who will be packaged.\n        const lines = [...this.pageLines];\n        for (const line of lines) {\n            if (line.result_package_id || !this.shouldSplitLine(line)) {\n                continue; // Line is already in a package or no quantity to process.\n            }\n            await this.splitLine(line);\n        }\n        await this.save();\n        const result = await this.orm.call(this.resModel, \"action_put_in_pack\", [[this.resId]], {\n            package_type_id: additionalParams.default_package_type_id,\n            package_name: additionalParams.default_name,\n            context,\n        });\n        if (typeof result === \"object\" && result.type) {\n            return this.trigger(\"process-action\", result);\n        }\n        this.trigger(\"refresh\");\n    }\n\n    /**\n     * Define which line must be packed, depending of the selected line.\n     * If the selected line is already packed, pack other already packed lines.\n     * If selected line is not packed, pack other no packed lines.\n     * If selected line has some quantity, pack only lines with quantity.\n     * @returns {Array<number>} list of packages' id\n     */\n    getPackageToPackIds() {\n        const packageIds = [];\n        // Define what level of packs need to be packed.\n        const mustPackAlreadyPacked = Boolean(this.selectedLine.result_package_id.package_dest_id);\n        const mustPackWithQuantity = Boolean(this.getQtyDone(this.selectedLine));\n        for (const line of this.previousScannedLines) {\n            if (mustPackAlreadyPacked && !line.result_package_id.package_dest_id) {\n                continue;\n            } else if (\n                !mustPackAlreadyPacked &&\n                (!line.result_package_id || line.result_package_id.package_dest_id)\n            ) {\n                continue;\n            } else if (mustPackWithQuantity && !this.getQtyDone(line)) {\n                continue;\n            }\n            packageIds.push(line.result_package_id.id);\n        }\n        return packageIds;\n    }\n\n    async _putPackInPack(packageIds, additionalParams = {}) {\n        const context = { barcode_view: true };\n        if (!this.groups.group_tracking_lot) {\n            return this.notification(_t(\"To use packages, enable 'Packages' in the settings\"), {\n                type: \"danger\",\n            });\n        }\n        if (!packageIds?.length) {\n            // Nothing to do in this case\n            return;\n        }\n        await this.save();\n        const result = await this.orm.call(\"stock.package\", \"action_put_in_pack\", [packageIds], {\n            package_id: additionalParams.default_package_id,\n            package_type_id: additionalParams.default_package_type_id,\n            package_name: additionalParams.default_name,\n            context,\n        });\n        if (typeof result === \"object\" && result.type) {\n            this.trigger(\"process-action\", result);\n        } else {\n            this.trigger(\"refresh\");\n        }\n    }\n\n    async unpack(linesToUnpack) {\n        for (const line of linesToUnpack) {\n            if (line.outermost_result_package_id) {\n                await this.updateLine(line, { outermost_result_package_id: false });\n            } else {\n                await this.updateLine(line, { result_package_id: false });\n            }\n        }\n        await this.save();\n        this.trigger(\"update\");\n    }\n\n    async _returnProducts() {\n        const action = await this.orm.call(this.resModel, \"action_create_return_picking\", [\n            [this.resId],\n        ]);\n        return this.action.doAction(action, { stackPosition: \"replaceCurrentAction\" });\n    }\n\n    async _scrap() {\n        if (!this.canScrap) {\n            const message = _t(\"You can't register scrap at this state of the operation\");\n            return this.notification(message, { type: \"warning\" });\n        }\n        await this.newScrapProduct();\n    }\n\n    /**\n     * Set the pickings's responsible to the active user.\n     */\n    async _setUser() {\n        if (this.record.id && this.record.user_id != user.userId) {\n            this.record.user_id = user.userId;\n            await this.orm.write(this.resModel, [this.record.id], { user_id: user.userId });\n        }\n    }\n\n    _setLocationFromBarcode(result, location) {\n        if (this.record.picking_type_code === \"outgoing\") {\n            result.location = location;\n        } else if (this.record.picking_type_code === \"incoming\") {\n            result.destLocation = location;\n        } else if (this.previousScannedLines.length || this.previousScannedLinesByPackage.length) {\n            if (\n                this.config.restrict_scan_source_location &&\n                this.config.restrict_scan_dest_location === \"no\" &&\n                this.barcodeInfo.class != \"scan_dest\"\n            ) {\n                result.location = location;\n            } else {\n                result.destLocation = location;\n            }\n        } else if ([\"scan_product_or_dest\", \"scan_dest\"].includes(this.barcodeInfo.class)) {\n            result.destLocation = location;\n        } else {\n            result.location = location;\n        }\n        return result;\n    }\n\n    _sortingMethod(l1, l2) {\n        const l1IsCompleted = this._lineIsComplete(l1);\n        const l2IsCompleted = this._lineIsComplete(l2);\n        // Complete lines always on the bottom.\n        if (!l1IsCompleted && l2IsCompleted) {\n            return -1;\n        } else if (l1IsCompleted && !l2IsCompleted) {\n            return 1;\n        }\n        return super._sortingMethod(...arguments);\n    }\n\n    _updateLineQty(line, args) {\n        if (args.qty_done) {\n            if (line.product_id.tracking === \"serial\") {\n                const nextQty = line.qty_done + args.qty_done;\n                if (nextQty > 1 && (this.record.use_create_lots || this.record.use_existing_lots)) {\n                    return; // Can't have more than 1 qty by serial number.\n                }\n            }\n            line.qty_done += args.qty_done;\n            this._setUser();\n        }\n    }\n\n    _updateLotName(line, lotName) {\n        line.lot_name = lotName;\n    }\n\n    _canOverrideTrackingNumber(line, newLotName) {\n        return (\n            super._canOverrideTrackingNumber(...arguments) ||\n            (this.location.id === line.location_id.id &&\n                !line.package_id &&\n                !line.owner_id &&\n                this.getQtyDone(line) === 0)\n        );\n    }\n\n    async _processGs1Data(data) {\n        const result = await super._processGs1Data(...arguments);\n        const { rule } = data;\n        if (\n            result.location &&\n            (rule.type === \"location_dest\" || this.barcodeInfo.class === \"scan_product_or_dest\")\n        ) {\n            result.destLocation = result.location;\n            result.location = undefined;\n        }\n        return result;\n    }\n\n    _getCompanyId() {\n        return this.record.company_id;\n    }\n}\n", "import { ApplyQuantDialog } from \"@stock_barcode/components/apply_quant_dialog\";\nimport { ConfirmQuantDialog } from \"@stock_barcode/components/confirm_quant_dialog\";\nimport BarcodeModel from \"@stock_barcode/models/barcode_model\";\nimport { _t } from \"@web/core/l10n/translation\";\n\nexport default class BarcodeQuantModel extends BarcodeModel {\n    constructor(params) {\n        super(...arguments);\n        this.lineModel = this.resModel;\n        this.validateMessage = _t(\"The inventory count has been updated\");\n        this.validateMethod = \"action_validate\";\n        this.deleteLineMethod = this.validateMethod;\n    }\n\n    async validate() {\n        return this.apply({ shouldConfirm: true });\n    }\n\n    async waitReview() {\n        return this.apply({ shouldWaitReview: true });\n    }\n\n    /**\n     * Check if the Inventory Adjustment can be applied and apply it only if it can be.\n     * @returns {Promise}\n     */\n    apply(options = {}) {\n        if (this.checkBeforeApply()) {\n            const { shouldConfirm = false, shouldWaitReview = false } = options;\n            const confirm = (context) => this._apply(context);\n            const waitReview = () => {\n                this.save();\n                this.trigger(\"history-back\");\n                this.trigger(\"update\");\n            };\n            if (shouldConfirm) {\n                return confirm();\n            } else if (shouldWaitReview) {\n                return waitReview();\n            } else {\n                this.dialogService.add(ConfirmQuantDialog, {\n                    onConfirm: confirm,\n                    onWaitReview: waitReview,\n                });\n            }\n        }\n    }\n\n    /**\n     * Makes some checks and returns true if the Inventory Adjustment can be\n     * applied or display a notification if it can not.\n     * @returns {Boolean}\n     */\n    checkBeforeApply() {\n        if (this.applyOn === 0) {\n            const message = _t(\"There is nothing to apply in this page.\");\n            this.notification(message, { type: \"warning\" });\n            return false;\n        }\n        // Checks if there are not counted serial numbers in the same location than counted quants.\n        const countedSerialNumbers = this.groupedLines.filter(\n            (gl) => gl.lines && gl.inventory_quantity_set && gl.product_id.tracking === \"serial\"\n        );\n        const notCountedSiblingSerialNumbers = [];\n        for (const groupedLine of countedSerialNumbers) {\n            for (const line of groupedLine.lines) {\n                if (!line.inventory_quantity_set) {\n                    notCountedSiblingSerialNumbers.push(line);\n                }\n            }\n        }\n        // In case there is not counted SN, asks the user if they want to count them as missing.\n        if (notCountedSiblingSerialNumbers.length) {\n            this.dialogService.add(ApplyQuantDialog, {\n                onApply: this._apply.bind(this),\n                onApplyAll: () => {\n                    // Set not counted SN as counted before to apply.\n                    for (const line of notCountedSiblingSerialNumbers) {\n                        this.toggleAsCounted(line);\n                    }\n                    this._apply();\n                },\n            });\n            return false;\n        }\n        return true;\n    }\n\n    /**\n     * Apply quantity set on counted quants.\n     * @returns {Promise}\n     */\n    async _apply(context = {}) {\n        await this.save();\n        const quantIds = this.pageLines.map((quant) => quant.id);\n        const action = await this.orm.call(\"stock.quant\", \"action_validate\", [quantIds]);\n        const notifyAndGoAhead = (res) => {\n            if (res && res.special) {\n                // Do nothing if come from a discarded wizard.\n                return this.trigger(\"refresh\");\n            }\n            this.notification(this.validateMessage, { type: \"success\" });\n            this.trigger(\"history-back\");\n        };\n        if (action && action.res_model) {\n            return this.action.doAction(action, { onClose: notifyAndGoAhead });\n        }\n        notifyAndGoAhead();\n    }\n\n    get applyOn() {\n        return this.pageLines.filter((line) => line.inventory_quantity_set).length;\n    }\n\n    get barcodeInfo() {\n        // Takes the parent line if the current line is part of a group.\n        let line = this._getParentLine(this.selectedLine) || this.selectedLine;\n        if (!line && this.lastScanned.packageId) {\n            line = this.pageLines.find(\n                (l) => l.package_id && l.package_id.id === this.lastScanned.packageId\n            );\n        }\n        // Defines some messages who can appear in multiple cases.\n        const messages = {\n            scanProduct: {\n                class: \"scan_product\",\n                message: _t(\"Scan a product\"),\n                icon: \"tags\",\n            },\n            scanLot: {\n                class: \"scan_lot\",\n                message: _t(\n                    \"Scan lot numbers for product %s to change their quantity\",\n                    line ? line.product_id.display_name : \"\"\n                ),\n                icon: \"barcode\",\n            },\n            scanSerial: {\n                class: \"scan_serial\",\n                message: _t(\n                    \"Scan serial numbers for product %s to change their quantity\",\n                    line ? line.product_id.display_name : \"\"\n                ),\n                icon: \"barcode\",\n            },\n        };\n\n        if (line) {\n            // Message depends of the selected line's state.\n            const { tracking } = line.product_id;\n            const trackingNumber = this.getlotName(line);\n            if (this._lineIsNotComplete(line)) {\n                if (tracking !== \"none\") {\n                    return tracking === \"lot\" ? messages.scanLot : messages.scanSerial;\n                }\n                return messages.scanProduct;\n            } else if (tracking !== \"none\" && !trackingNumber) {\n                // Line's quantity is fulfilled but still waiting a tracking number.\n                return tracking === \"lot\" ? messages.scanLot : messages.scanSerial;\n            } else {\n                // Line's quantity is fulfilled.\n                if (\n                    this.groups.group_stock_multi_locations &&\n                    line.location_id.id === this.location.id\n                ) {\n                    return {\n                        class: \"scan_product_or_src\",\n                        message: _t(\n                            \"Scan more products in %s or scan another location\",\n                            this.location.display_name\n                        ),\n                    };\n                }\n                return messages.scanProduct;\n            }\n        }\n        // No line selected, returns default message (depends if multilocation is enabled).\n        if (this.groups.group_stock_multi_locations) {\n            if (!this.lastScanned.sourceLocation) {\n                return {\n                    class: \"scan_src\",\n                    message: _t(\"Scan a location\"),\n                    icon: \"sign-out\",\n                };\n            }\n            return {\n                class: \"scan_product_or_src\",\n                message: _t(\n                    \"Scan a product in %s or scan another location\",\n                    this.location.display_name\n                ),\n            };\n        }\n        return messages.scanProduct;\n    }\n\n    get displayByUnitButton() {\n        return true;\n    }\n\n    displaySetButton(line) {\n        const isSelected = this.selectedLineVirtualId === line.virtual_id;\n        return (\n            isSelected &&\n            (this.showQuantityCount ||\n                (line.product_id.tracking === \"serial\" && this.getlotName(line)))\n        );\n    }\n\n    setData(data) {\n        this.userId = data.data.user_id;\n        this.showQuantityCount = data.data.show_quantity_count;\n        this.countEntireLocation = false;\n        super.setData(...arguments);\n        const companies = data.data.records[\"res.company\"];\n        this.companyIds = companies.map((company) => company.id);\n        this.lineFormViewId = data.data.line_view_id;\n    }\n\n    get displayApplyButton() {\n        return true;\n    }\n\n    getQtyDone(line) {\n        return line.inventory_quantity_set ? line.inventory_quantity : 0;\n    }\n\n    getQtyDemand(line) {\n        return this.showQuantityCount ? line.quantity : 0;\n    }\n\n    getActionRefresh(newId) {\n        const action = super.getActionRefresh(newId);\n        action.params.res_id = this.currentState.lines.map((l) => l.id);\n        if (newId) {\n            action.params.res_id.push(newId);\n        }\n        return action;\n    }\n\n    get highlightValidateButton() {\n        return this.applyOn > 0 && this.applyOn === this.pageLines.length;\n    }\n\n    IsNotSet(line) {\n        return !line.inventory_quantity_set;\n    }\n\n    lineCanBeDeleted(line) {\n        return line.inventory_quantity_set && this.getQtyDone(line) === 0;\n    }\n\n    lineIsFaulty(line) {\n        if (this.showQuantityCount) {\n            return line.inventory_quantity_set && line.inventory_quantity !== line.quantity;\n        }\n        return false; // Never show a line as faulty if we don't display the expected quantity.\n    }\n\n    lineIsTracked(line) {\n        const lineIsTracked = super.lineIsTracked(...arguments);\n        if (lineIsTracked && line.product_id.tracking === \"serial\") {\n            // Count quants tracked by SN as untracked if they have no SN and multiple quantity.\n            return (\n                this.getlotName(line) ||\n                (this.getQtyDone(line) <= 1 && this.getQtyDemand(line) <= 1)\n            );\n        }\n        return lineIsTracked;\n    }\n\n    get printButtons() {\n        return [\n            {\n                name: _t(\"Print Inventory\"),\n                class: \"o_print_inventory\",\n                action: \"stock.action_report_inventory\",\n            },\n        ];\n    }\n\n    get recordIds() {\n        return this.currentState.lines.map((l) => l.id);\n    }\n\n    /**\n     * Marks or unmarks the line as counted and set its inventory quantity to zero.\n     *\n     * @param {Object} line\n     */\n    toggleAsCounted(line) {\n        line.inventory_quantity = 0;\n        line.inventory_quantity_set = !line.inventory_quantity_set;\n        this._markLineAsDirty(line);\n        this.trigger(\"update\");\n    }\n\n    updateLineQty(virtualId, qty = 1) {\n        this.actionMutex.exec(() => {\n            const line = this.pageLines.find((l) => l.virtual_id === virtualId);\n            this.updateLine(line, { inventory_quantity: qty });\n            this.trigger(\"update\");\n        });\n    }\n\n    // --------------------------------------------------------------------------\n    // Private\n    // --------------------------------------------------------------------------\n\n    _getCommands() {\n        return Object.assign(super._getCommands(), {\n            OBTAPPLY: this.apply.bind(this),\n            OBTWREV: this.waitReview.bind(this),\n        });\n    }\n\n    _getMissingRecordsParams() {\n        const params = super._getMissingRecordsParams();\n        params.fetch_quants = true;\n        return params;\n    }\n\n    _getNewLineDefaultContext() {\n        return {\n            default_company_id: this.companyIds[0],\n            default_location_id: this._defaultLocation().id,\n            default_inventory_quantity: 1,\n            default_user_id: this.userId,\n            inventory_mode: true,\n            display_default_code: false,\n            hide_qty_to_count: !this.showQuantityCount,\n        };\n    }\n\n    _createCommandVals(line) {\n        const values = {\n            dummy_id: line.virtual_id,\n            inventory_date: line.inventory_date,\n            inventory_quantity: line.inventory_quantity,\n            inventory_quantity_set: line.inventory_quantity_set,\n            location_id: line.location_id,\n            lot_id: line.lot_id,\n            lot_name: line.lot_name,\n            package_id: line.package_id,\n            product_id: line.product_id,\n            owner_id: line.owner_id,\n            user_id: this.userId,\n        };\n        for (const [key, value] of Object.entries(values)) {\n            values[key] = this._fieldToValue(value);\n        }\n        return values;\n    }\n\n    async _createNewLine(params) {\n        // When creating a new line, we need to know if a quant already exists\n        // for this line, and in this case, update the new line fields.\n        const product = params.fieldsParams.product_id;\n        if (!product.is_storable) {\n            const productName =\n                (product.default_code ? `[${product.default_code}] ` : \"\") + product.display_name;\n            const message = _t(\n                \"%s can't be inventoried. Only storable products can be inventoried.\",\n                productName\n            );\n            this.notification(message, { type: \"warning\" });\n            return false;\n        }\n        const location_id = this.location.id;\n        const { lot_id, lot_name, owner_id, package_id } = params.fieldsParams;\n        let quants = [];\n        if (!params.fieldsParams.packaging || product.tracking === \"none\") {\n            if (!lot_id && !lot_name && !package_id && !owner_id) {\n                quants = await this.cache.getQuants(product, location_id);\n            } else {\n                const quantParams = { lot_id, lot_name, owner_id, package_id };\n                quants = await this.cache.getQuants(product, location_id, quantParams);\n            }\n        }\n        if (\n            quants.length === 1 &&\n            (product.tracking === \"none\" ||\n                params.fieldsParams.lot_name ||\n                params.fieldsParams.lot_id)\n        ) {\n            const inventory_quantity =\n                product.tracking === \"lot\"\n                    ? quants[0].quantity\n                    : params.fieldsParams.inventory_quantity || 1;\n            params.fieldsParams = Object.assign({}, params.fieldsParams, { inventory_quantity });\n        }\n        let newLine = false;\n        if (quants.length) {\n            // Found existing quants: create a line for each one.\n            const lineIds = this.currentState.lines.map((l) => l.id);\n            for (const quant of quants) {\n                if (lineIds.includes(quant.id)) {\n                    continue; // Don't create line for quant if there is already a line for it.\n                }\n                const lineParams = {\n                    fieldsParams: Object.assign({}, quant, params.fieldsParams),\n                };\n                const newlyCreatedLine = await super._createNewLine(lineParams);\n                this.selectedLineVirtualId = newlyCreatedLine.virtual_id;\n                // Keeps the first created line so that the one who will be selected.\n                newLine = newLine || newlyCreatedLine;\n                // If the quant already exits, we add it into the `initialState` to\n                // avoid comparison issue with the `currentState` when the save occurs.\n                const lineWithOriginalQuantValues = Object.assign({}, newlyCreatedLine, {\n                    inventory_date: quant.inventory_date,\n                    inventory_quantity: quant.inventory_quantity,\n                    inventory_quantity_set: quant.inventory_quantity_set,\n                    quantity: quant.quantity,\n                    user_id: quant.user_id,\n                });\n                this.initialState.lines.push(lineWithOriginalQuantValues);\n            }\n        } else {\n            // No existing quant: creates an empty new line.\n            newLine = await super._createNewLine(params);\n        }\n        return newLine;\n    }\n\n    _convertDataToFieldsParams(args) {\n        const params = {};\n        // Set the fields in `params` only if they are in `args`.\n        if (args.packaging && args.product.tracking === \"serial\") {\n            params.inventory_quantity = 1;\n        } else if (args.quantity) {\n            params.inventory_quantity = args.quantity;\n        }\n        args.lot && (params.lot_id = args.lot);\n        args.lotName && (params.lot_name = args.lotName);\n        args.owner && (params.owner_id = args.owner);\n        args.package && (params.package_id = args.package);\n        args.product && (params.product_id = args.product);\n        args.product && args.product.uom_id && (params.product_uom_id = args.product.uom_id);\n        args.packaging && (params.packaging = args.packaging);\n        return params;\n    }\n\n    _getNewLineDefaultValues(fieldsParams) {\n        const defaultValues = super._getNewLineDefaultValues(...arguments);\n        Object.assign(defaultValues, {\n            inventory_date: new Date().toISOString().slice(0, 10),\n            inventory_quantity: 0,\n            quantity: (fieldsParams && fieldsParams.quantity) || 0,\n            user_id: this.userId,\n        });\n        // Marks the new line's quantity as set only if it's not an existing quant (no `quantity`)\n        // or if it already has a counted quantity. It's to avoid tragedy if the user applies by\n        // mistake the inventory adjustment after scanned a product with multiple serial/lot numbers\n        if (fieldsParams.quantity === undefined || fieldsParams.inventory_quantity) {\n            defaultValues.inventory_quantity_set = true;\n        }\n        return defaultValues;\n    }\n\n    _getFieldToWrite() {\n        return [\n            \"inventory_date\",\n            \"inventory_quantity\",\n            \"inventory_quantity_set\",\n            \"user_id\",\n            \"location_id\",\n            \"lot_name\",\n            \"lot_id\",\n            \"package_id\",\n            \"owner_id\",\n        ];\n    }\n\n    _getSaveCommand() {\n        const commands = this._getSaveLineCommand();\n        if (commands.length) {\n            return {\n                route: \"/stock_barcode/save_barcode_data\",\n                params: {\n                    model: this.resModel,\n                    res_id: false,\n                    write_field: false,\n                    write_vals: commands,\n                },\n            };\n        }\n        return {};\n    }\n\n    _groupSublines() {\n        const groupedLine = super._groupSublines(...arguments);\n        const hasAtLeastOneSetSubline = groupedLine.lines.find((l) => l.inventory_quantity_set);\n        groupedLine.inventory_quantity = groupedLine.totalQtyDone;\n        groupedLine.quantity = groupedLine.totalQtyDemand;\n        groupedLine.inventory_quantity_set = hasAtLeastOneSetSubline;\n        return groupedLine;\n    }\n\n    _lineIsNotComplete(line) {\n        return line.inventory_quantity === 0;\n    }\n\n    async _processPackage(barcodeData) {\n        const { packageType, packageName } = barcodeData;\n        let recPackage = barcodeData.package;\n        this.lastScanned.packageId = false;\n        if (!recPackage && !packageType && !packageName) {\n            return; // No Package data to process.\n        }\n        // Scan a new package and/or a package type -> Create a new package with those parameters.\n        const currentLine = this.selectedLine || this.lastScannedLine;\n        if (\n            currentLine.package_id &&\n            packageType &&\n            !recPackage &&\n            !packageName &&\n            currentLine.package_id.id !== packageType\n        ) {\n            // Changes the package type for the scanned one.\n            await this.orm.write(\"stock.package\", [currentLine.package_id.id], {\n                package_type_id: packageType.id,\n            });\n            const message = _t(\"Package type %(type)s applied to the package %(package)s\", {\n                type: packageType.name,\n                package: currentLine.package_id.name,\n            });\n            barcodeData.stopped = true;\n            return this.notification(message, { type: \"success\" });\n        }\n        if (!recPackage) {\n            if (currentLine && !currentLine.package_id) {\n                const valueList = {};\n                if (packageName) {\n                    valueList.name = packageName;\n                }\n                if (packageType) {\n                    valueList.package_type_id = packageType.id;\n                }\n                const newPackageData = await this.orm.call(\n                    \"stock.package\",\n                    \"action_create_from_barcode\",\n                    [valueList]\n                );\n                this.cache.setCache(newPackageData);\n                recPackage = newPackageData[\"stock.package\"][0];\n            }\n        }\n        if (!recPackage && packageName) {\n            const currentLine = this.selectedLine || this.lastScannedLine;\n            if (currentLine && !currentLine.package_id) {\n                const newPackageData = await this.orm.call(\n                    \"stock.package\",\n                    \"action_create_from_barcode\",\n                    [{ name: packageName }]\n                );\n                this.cache.setCache(newPackageData);\n                recPackage = newPackageData[\"stock.package\"][0];\n            }\n        }\n        if (!recPackage || (recPackage.location_id && recPackage.location_id != this.location.id)) {\n            return;\n        }\n        // TODO: can check if quants already in cache to avoid to make a RPC if\n        // there is all in it (or make the RPC only on missing quants).\n        const res = await this.orm.call(\"stock.quant\", \"get_stock_barcode_data_records\", [\n            recPackage.contained_quant_ids,\n        ]);\n        const quants = res.records[\"stock.quant\"];\n        if (!quants.length) {\n            // Empty package => Assigns it to the last scanned line.\n            const currentLine = this.selectedLine || this.lastScannedLine;\n            if (currentLine && !currentLine.package_id) {\n                const fieldsParams = this._convertDataToFieldsParams({\n                    package: recPackage,\n                });\n                await this.updateLine(currentLine, fieldsParams);\n                barcodeData.stopped = true;\n                this.selectedLineVirtualId = false;\n                this.lastScanned.packageId = recPackage.id;\n                this.trigger(\"update\");\n            }\n            return;\n        }\n        this.cache.setCache(res.records);\n\n        // Checks if the package is already scanned.\n        let alreadyExisting = 0;\n        for (const line of this.pageLines) {\n            if (\n                line.package_id &&\n                line.package_id.id === recPackage.id &&\n                this.getQtyDone(line) > 0\n            ) {\n                alreadyExisting++;\n            }\n        }\n        if (alreadyExisting === quants.length) {\n            barcodeData.error = _t(\"This package is already scanned.\");\n            return;\n        }\n        // For each quants, creates or increments a barcode line.\n        for (const quant of quants) {\n            const product = this.cache.getRecord(\"product.product\", quant.product_id);\n            const searchLineParams = Object.assign({}, barcodeData, { product });\n            const currentLine = this._findLine(searchLineParams);\n            if (currentLine) {\n                // Updates an existing line.\n                const fieldsParams = this._convertDataToFieldsParams({\n                    quantity: quant.quantity,\n                    lotName: barcodeData.lotName,\n                    lot: barcodeData.lot,\n                    package: recPackage,\n                    owner: barcodeData.owner,\n                });\n                await this.updateLine(currentLine, fieldsParams);\n            } else {\n                // Creates a new line.\n                const fieldsParams = this._convertDataToFieldsParams({\n                    product,\n                    quantity: quant.quantity,\n                    lot: quant.lot_id,\n                    package: quant.package_id,\n                    owner: quant.owner_id,\n                });\n                const newLine = await this._createNewLine({ fieldsParams });\n                newLine.inventory_quantity = quant.quantity;\n            }\n        }\n        barcodeData.stopped = true;\n        this.selectedLineVirtualId = false;\n        this.lastScanned.packageId = recPackage.id;\n        this.trigger(\"update\");\n    }\n\n    async _processLocation(barcodeData) {\n        super._processLocation(barcodeData);\n        if (barcodeData.location && this.countEntireLocation) {\n            await this.loadQuantsForLocation(barcodeData);\n        }\n    }\n\n    async loadQuantsForLocation(barcodeData) {\n        const res = await this.orm.call(\"stock.location\", \"get_counted_quant_data_records\", [\n            barcodeData.location.id,\n        ]);\n        this.cache.setCache(res.records);\n\n        const quants = res.records[\"stock.quant\"];\n        for (const quant of quants) {\n            const product = this.cache.getRecord(\"product.product\", quant.product_id);\n            const lot = quant.lot_id && this.cache.getRecord(\"stock.lot\", quant.lot_id);\n            const quant_package =\n                quant.package_id && this.cache.getRecord(\"stock.package\", quant.package_id);\n            const searchLineParams = Object.assign({}, barcodeData, { product, lot });\n            searchLineParams[\"package\"] = quant_package;\n            const currentLine = this._findLine(searchLineParams);\n            if (!currentLine) {\n                const fieldsParams = this._convertDataToFieldsParams({\n                    product,\n                    quantity: quant.quantity,\n                    lot: quant.lot_id,\n                    package: quant.package_id,\n                    owner: quant.owner_id,\n                });\n                const newLine = await this._createNewLine({ fieldsParams });\n                if (newLine) {\n                    newLine.inventory_quantity = quant.inventory_quantity;\n                    newLine.inventory_quantity_set = false;\n                }\n            }\n        }\n        barcodeData.stopped = true;\n        this.selectedLineVirtualId = false;\n        this.trigger(\"update\");\n    }\n\n    _updateLineQty(line, args) {\n        if (args.quantity) {\n            // Set stock quantity.\n            line.quantity = args.quantity;\n        }\n        if (args.inventory_quantity) {\n            if (args.uom) {\n                const lineUOM = line.product_uom_id;\n                if (args.uom.factor !== lineUOM.factor) {\n                    // Convert the scanned qty into the product UoM.\n                    const factor = args.uom.factor / lineUOM.factor;\n                    args.inventory_quantity = args.inventory_quantity * factor;\n                    args.uom = lineUOM;\n                }\n            }\n            line.inventory_quantity += args.inventory_quantity;\n            if (line.inventory_quantity > 0) {\n                args.inventory_quantity_set = true;\n            }\n            line.inventory_quantity_set = this.countEntireLocation\n                ? args.inventory_quantity_set\n                : true;\n            if (line.product_id.tracking === \"serial\" && (line.lot_name || line.lot_id)) {\n                line.inventory_quantity = Math.max(0, Math.min(1, line.inventory_quantity));\n            }\n        }\n    }\n\n    async _updateLotName(line, lotName) {\n        if (line.lot_name === lotName) {\n            // No need to update the line's tracking number if it's already set.\n            return Promise.resolve();\n        }\n        line.lot_name = lotName;\n        const owner_id = line.owner_id ? line.owner_id : false;\n        const package_id = line.package_id && line.package_id;\n        const existingQuant = await this.cache.getQuants(line.product_id, line.location_id.id, {\n            lot_id: line.lot_id,\n            lot_name: lotName,\n            owner_id,\n            package_id,\n        });\n        if (existingQuant && existingQuant.length) {\n            Object.assign(line, existingQuant[0]);\n            if (line.lot_id) {\n                line.lot_id = await this.cache.getRecordByBarcode(lotName, \"stock.lot\");\n            }\n        }\n    }\n\n    _canOverrideTrackingNumber(line, newLotName) {\n        return super._canOverrideTrackingNumber(...arguments) && (!line.id || line.lot_id);\n    }\n\n    _createLinesState() {\n        const today = new Date().toISOString().slice(0, 10);\n        const lines = [];\n        for (const id of Object.keys(this.cache.dbIdCache[\"stock.quant\"]).map((id) => Number(id))) {\n            const quant = this.cache.getRecord(\"stock.quant\", id);\n            if ((quant.user_id && quant.user_id !== this.userId) || quant.inventory_date > today) {\n                // Doesn't take quants who must be counted by another user or in the future.\n                continue;\n            }\n            // Checks if this line is already in the quant state to get back\n            // its `virtual_id` (and so, avoid to set a new `virtual_id`).\n            const prevLine = this.currentState && this.currentState.lines.find((l) => l.id === id);\n            const previousVirtualId = prevLine && prevLine.virtual_id;\n            quant.dummy_id = quant.dummy_id && Number(quant.dummy_id);\n            quant.virtual_id = quant.dummy_id || previousVirtualId || this._uniqueVirtualId;\n            quant.product_id = this.cache.getRecord(\"product.product\", quant.product_id);\n            quant.product_uom_id = this.cache.getRecord(\"uom.uom\", quant.product_uom_id);\n            quant.location_id = this.cache.getRecord(\"stock.location\", quant.location_id);\n            quant.lot_id = quant.lot_id && this.cache.getRecord(\"stock.lot\", quant.lot_id);\n            quant.package_id =\n                quant.package_id && this.cache.getRecord(\"stock.package\", quant.package_id);\n            quant.owner_id = quant.owner_id && this.cache.getRecord(\"res.partner\", quant.owner_id);\n            lines.push(Object.assign({}, quant));\n        }\n        return lines;\n    }\n\n    _getName() {\n        return _t(\"Physical Inventory\");\n    }\n\n    _getPrintOptions() {\n        const options = super._getPrintOptions();\n        const quantsToPrint = this.pageLines.filter((quant) => quant.inventory_quantity_set);\n        if (quantsToPrint.length === 0) {\n            return { warning: _t(\"There is nothing to print in this page.\") };\n        }\n        options.additionalContext = { active_ids: quantsToPrint.map((quant) => quant.id) };\n        return options;\n    }\n\n    _selectLine(line) {\n        if (this.selectedLineVirtualId !== line.virtual_id) {\n            // Unfolds the group where the line is, folds other lines' group.\n            this.unfoldLineKey = this.groupKey(line);\n        }\n        super._selectLine(...arguments);\n    }\n\n    zeroQtyClass(line) {\n        return this.IsNotSet(line) ? super.zeroQtyClass(...arguments) : \"text-danger\";\n    }\n\n    _getCompanyId() {\n        return this.companyIds[0];\n    }\n\n    _shouldBeExpressedInPackagingUom() {\n        return false;\n    }\n}\n", "import { registry } from \"@web/core/registry\";\nimport { useRecordObserver } from \"@web/model/relational_model/utils\";\nimport { formatFloat } from \"@web/core/utils/numbers\";\nimport { standardWidgetProps } from \"@web/views/widgets/standard_widget_props\";\nimport { user } from \"@web/core/user\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { Component, onWillStart, useState } from \"@odoo/owl\";\n\nexport class Digipad extends Component {\n    static template = \"stock_barcode.DigipadTemplate\";\n    static props = {\n        ...standardWidgetProps,\n        fieldToEdit: { type: String },\n        fulfilledAt: { type: String, optional: true },\n    };\n\n    setup() {\n        this.orm = useService(\"orm\");\n        const { data } = this.props.record;\n        const context = this.props.record.evalContext.context;\n        this.quantity = data[this.props.fieldToEdit];\n        this.value = String(this.quantity);\n        this.fulfillQuantity =\n            this.props.fulfilledAt && !context.hide_qty_to_count ? data[this.props.fulfilledAt] : 0;\n        if (context.force_fullfil_quantity) {\n            this.fulfillQuantity = context.force_fullfil_quantity;\n        }\n        const field = this.props.record.model.config.fields[this.props.fieldToEdit];\n        this.precision = field.digits[1];\n        this.productId = this.props.record.data.product_id.id;\n        this.state = useState({\n            packagingButtons: [],\n        });\n        useRecordObserver(async (record) => {\n            if (this.productId != record.data.product_id.id) {\n                this.productId = record.data.product_id.id;\n                await this._fetchPackagingButtons();\n            }\n        });\n        onWillStart(async () => {\n            this.displayUOM = await user.hasGroup(\"uom.group_uom\");\n            await this._fetchPackagingButtons();\n        });\n    }\n\n    get changes() {\n        return { [this.props.fieldToEdit]: Number(this.value) };\n    }\n\n    get quantityToFulfill() {\n        if (!this.fulfillQuantity) {\n            return 0;\n        }\n        const record = this.props.record.data;\n        const currentQty = record[this.props.fieldToEdit];\n\n        const quantityToFulfill = this.fulfillQuantity - currentQty;\n        const params = { digits: [false, this.precision], thousandsSep: \"\", decimalPoint: \".\" };\n        return parseFloat(formatFloat(quantityToFulfill, params));\n    }\n\n    get buttonContainerClass() {\n        return this.fulfillQuantity ? \"col-3\" : \"col-4\";\n    }\n\n    get buttonFulfillClass() {\n        if (this.quantityToFulfill > 0) {\n            return \"btn-success\";\n        } else if (this.quantityToFulfill < 0) {\n            return \"btn-warning\";\n        }\n        return \"btn-secondary text-success\";\n    }\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * Copies the input value if digipad value is not set yet, or overrides it if there is a\n     * difference between the two values (in case user has manualy edited the input value).\n     * @private\n     */\n    _checkInputValue() {\n        const input = document.querySelector(`div[name=\"${this.props.fieldToEdit}\"] input`);\n        const inputValue = input.value;\n        if (Number(this.value) != Number(inputValue)) {\n            this.value = inputValue;\n            this.quantity = Number(this.value || 0);\n        }\n    }\n\n    /**\n     * Increments the field value by the interval amount (1 by default).\n     * @private\n     * @param {integer} [interval=1]\n     */\n    async _increment(interval = 1, enforceQuantity = false) {\n        if (enforceQuantity) {\n            this.quantity = interval;\n        } else {\n            this._checkInputValue();\n            this.quantity = Math.max(this.quantity + interval, 0);\n        }\n        this.value = this.quantity.toFixed(this.precision);\n        if (parseFloat(this.value) % 1 == 0) {\n            this.value = String(Math.floor(parseFloat(this.value)));\n        }\n        await this.props.record.update(this.changes);\n    }\n\n    /**\n     * Search for the product's packaging buttons.\n     * @private\n     * @returns {Promise}\n     */\n    async _fetchPackagingButtons() {\n        const record = this.props.record.data;\n        this.productUom = (\n            await this.orm.searchRead(\n                \"uom.uom\",\n                [[\"id\", \"=\", record.product_uom_id?.id]],\n                [\"factor\"]\n            )\n        )?.[0];\n        if (record.product_id.id) {\n            let domain = [[\"id\", \"=\", record.product_id.id]];\n            const product_uoms = await this.orm.searchRead(\"product.product\", domain, [\"uom_ids\"]);\n            if (product_uoms.length === 0) {\n                return;\n            }\n            domain = [[\"id\", \"in\", product_uoms[0].uom_ids]];\n            if (this.quantityToFulfill) {\n                // Doesn't fetch packaging with a too high quantity.\n                domain.push([\"factor\", \"<=\", this.quantityToFulfill]);\n            }\n            this.state.packagingButtons = await this.orm.searchRead(\n                \"uom.uom\",\n                domain,\n                [\"name\", \"factor\", \"package_type_id\"],\n                { limit: 2 }\n            );\n        } else {\n            this.state.packagingButtons = [];\n        }\n    }\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n     * Handles the click on one of the digipad's button and updates the value..\n     * @private\n     * @param {String} button\n     */\n    erase() {\n        this._checkInputValue();\n        this.quantity = 0;\n        this.value = String(this.quantity);\n        this.props.record.update(this.changes);\n    }\n\n    increment() {\n        this._increment();\n    }\n\n    decrement() {\n        this._increment(-1);\n    }\n\n    /**\n     * Handles the click on one of the digipad's button and updates the value..\n     * @private\n     * @param {String} button\n     */\n    fulfill() {\n        this._checkInputValue();\n        this.quantity = this.fulfillQuantity;\n        this.value = String(this.quantity);\n        this.props.record.update(this.changes);\n    }\n}\n\nexport const digipad = {\n    component: Digipad,\n    extractProps: ({ attrs }) => ({\n        fieldToEdit: attrs.field_to_edit,\n        fulfilledAt: attrs.fulfilled_at,\n    }),\n};\nregistry.category(\"view_widgets\").add(\"digipad\", digipad);\n", "import { registry } from \"@web/core/registry\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { ImageField } from \"@web/views/fields/image/image_field\";\nimport { Component } from \"@odoo/owl\";\n\nexport class FullScreenImage extends Component {\n    static template = \"stock_barcode.FullScreenImage\";\n    static props = {\n        src: { type: String },\n        close: Function,\n    };\n}\n\nexport class ImagePreviewField extends ImageField {\n    static template = \"stock_barcode.ImagePreviewField\";\n\n    setup() {\n        super.setup();\n        this.dialog = useService(\"dialog\");\n    }\n\n    openImageFullScreen() {\n        this.dialog.add(FullScreenImage, {\n            src: this.getUrl(this.props.name),\n        });\n    }\n}\n\nexport const imageClickEnlarge = {\n    component: ImagePreviewField,\n};\n\nregistry.category(\"fields\").add(\"image_preview\", imageClickEnlarge);\n", "import { registry } from \"@web/core/registry\";\nimport { user } from \"@web/core/user\";\nimport { X2ManyField, x2ManyField } from \"@web/views/fields/x2many/x2many_field\";\nimport { onWillStart, onWillUpdateProps, useState, useEffect } from \"@odoo/owl\";\n\nexport class StockBarcodeQuantOne2ManyField extends X2ManyField {\n    setup() {\n        super.setup();\n        this.state = useState({ selectedQuantId: null });\n        this.moveLineData = this.props.record.data;\n        this.quantRecords = this.moveLineData.product_stock_quant_ids.records;\n\n        onWillStart(async () => {\n            this.isQuantSelectable = false;\n            const allowedOperationCodes = [\"internal\", \"outgoing\", \"mrp_operation\"];\n            const validOperation = allowedOperationCodes.includes(this.moveLineData.picking_code);\n            if (validOperation) {\n                this.isQuantSelectable =\n                    (await user.hasGroup(\"stock.group_production_lot\")) ||\n                    (await user.hasGroup(\"stock.group_stock_multi_locations\")) ||\n                    (await user.hasGroup(\"stock.group_tracking_lot\")) ||\n                    (await user.hasGroup(\"stock.group_tracking_owner\"));\n            }\n\n            this._findMatchingQuant();\n        });\n\n        onWillUpdateProps(() => this._findMatchingQuant());\n\n        useEffect(\n            () => this._highlightSelectedQuant(),\n            () => [this.state.selectedQuantId]\n        );\n    }\n\n    async openRecord(record) {\n        if (this.isQuantSelectable) {\n            const vals = {\n                location_id: record.data.location_id,\n                lot_id: record.data.lot_id,\n                package_id: record.data.package_id,\n                owner_id: record.data.owner_id,\n            };\n            this.state.selectedQuantId = record.data.id;\n            return await this.props.record.update(vals);\n        }\n        return;\n    }\n\n    /**\n     * Finds the quant matching the current move line data.\n     */\n    _findMatchingQuant() {\n        const matchedQuantRecord = this.quantRecords.find(({ data: quant }) => {\n            const { location_id, lot_id, package_id, owner_id } = quant;\n            return (\n                location_id?.id === this.moveLineData.location_id?.id &&\n                lot_id?.id === this.moveLineData.lot_id?.id &&\n                package_id?.id === this.moveLineData.package_id?.id &&\n                owner_id?.id === this.moveLineData.owner_id?.id\n            );\n        });\n        this.state.selectedQuantId = matchedQuantRecord?.data.id || null;\n    }\n\n    _highlightSelectedQuant() {\n        this.quantRecords.forEach((quant) => {\n            quant.selected = quant.data.id === this.state.selectedQuantId;\n        });\n    }\n}\n\nexport const stockBarcodeQuantOne2ManyField = {\n    ...x2ManyField,\n    component: StockBarcodeQuantOne2ManyField,\n};\n\nregistry.category(\"fields\").add(\"stock_barcode_quant_one2many\", stockBarcodeQuantOne2ManyField);\n", "import { registry } from \"@web/core/registry\";\nimport { standardFieldProps } from \"@web/views/fields/standard_field_props\";\nimport { user } from \"@web/core/user\";\nimport { Component, onWillStart } from \"@odoo/owl\";\n\nexport class SetReservedQuantityButton extends Component {\n    static props = {\n        ...standardFieldProps,\n        fieldToSet: { type: String },\n    };\n    static template = \"stock_barcode.SetReservedQuantityButtonTemplate\";\n\n    setup() {\n        onWillStart(async () => {\n            this.displayUOM = await user.hasGroup(\"uom.group_uom\");\n        });\n    }\n\n    get uom() {\n        const { id, display_name: name } = this.props.record.data.product_uom_id || {};\n        return { id, name };\n    }\n\n    _setQuantity(ev) {\n        ev.stopPropagation();\n        this.props.record.update({\n            [this.props.fieldToSet]: this.props.record.data[this.props.name],\n        });\n    }\n}\n\nexport const setReservedQuantityButton = {\n    component: SetReservedQuantityButton,\n    extractProps: ({ attrs }) => {\n        if (attrs.field_to_set) {\n            return { fieldToSet: attrs.field_to_set };\n        }\n        return {};\n    },\n};\n\nregistry.category(\"fields\").add(\"set_reserved_qty_button\", setReservedQuantityButton);\n", "import { registry } from \"@web/core/registry\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { Component, onWillStart, onWillUpdateProps, useState } from \"@odoo/owl\";\n\nexport class SendcloudFunctionalitiesField extends Component {\n    static template = \"delivery_sendcloud.functionalities\";\n    static props = {\n        \"*\": true,\n    };\n    setup() {\n        const { data } = this.props.record;\n        this.actionService = useService(\"action\");\n        this.orm = useService(\"orm\");\n        this.resId = data.id;\n        this.sendcloudProductId = data.sendcloud_shipping_id.id;\n        this.filterableFunc = useState({});\n        this.currentFilters = {...data.sendcloud_product_functionalities};\n        onWillStart(this._fetchFunctionalities);\n        onWillUpdateProps((nextProps) => this._onWillUpdateProps(nextProps));\n    }\n\n    async _fetchFunctionalities(){\n        const res = await this.orm.read(\n            \"sendcloud.shipping.product\",\n            [this.sendcloudProductId],\n            ['functionalities']\n        );\n        const productFunctionalities = res[0].functionalities.customizable;\n        this.filterableFunc = {...productFunctionalities};\n        for (const func in this.filterableFunc){\n            for (let i = 0; i < this.filterableFunc[func].length; i++){\n                if (this.filterableFunc[func][i] === null){\n                    this.filterableFunc[func][i] = \"None\";\n                }\n            }\n        }\n    }\n\n    async _onWillUpdateProps(nextProps){\n        const { data } = nextProps.record;\n        if (typeof data.sendcloud_shipping_id === \"undefined\"){\n            return false;\n        }\n        const newProductId = data.sendcloud_shipping_id[0];\n        if (newProductId === this.sendcloudProductId ){\n            return false;\n        }\n        this.sendcloudProductId = newProductId;\n        await this._fetchFunctionalities();\n        this.currentFilters = data.sendcloud_product_functionalities;\n    }\n\n    _humanizeFunctionality(technicalName){\n        technicalName = new String(technicalName);\n        return technicalName.substring(0, 1).toUpperCase().concat(technicalName.substring(1).replaceAll(\"_\", \" \")).toString();\n    }\n\n    _isChecked(func, option){\n        return this.currentFilters[func]?.includes(option);\n    }\n\n    _onChecked(func, option){\n        if (!this.currentFilters){\n            this.currentFilters = {};\n        }\n        if (func in this.currentFilters){\n            const i = this.currentFilters[func].indexOf(option);\n            if (i >= 0){\n                this.currentFilters[func].splice(i,1);\n                if (this.currentFilters[func].length == 0){\n                    delete this.currentFilters[func];\n                }\n            }\n            else{\n                this.currentFilters[func].push(option);\n            }\n        }else{\n            this.currentFilters[func] = [option];\n        }\n        this.props.record.update({['sendcloud_product_functionalities']: this.currentFilters});\n    }\n\n}\n\nexport const sendcloudFunctionalitiesField = {\n    component: SendcloudFunctionalitiesField,\n};\n\nregistry.category(\"fields\").add(\"sendcloud_functionalities\", sendcloudFunctionalitiesField);\n", "import { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\nimport { formatFloat } from \"@web/views/fields/formatters\";\nimport { registry } from \"@web/core/registry\";\nimport { Component, useState } from \"@odoo/owl\";\n\nexport class SendcloudProductSelectionWidget extends Component {\n    static template = \"delivery_sendcloud.SendcloudProductWidget\";\n    static components = {\n        Dropdown,\n        DropdownItem,\n    };\n    static props = {\n        \"*\": true,\n    };\n    setup() {\n        const { data } = this.props.record;\n        this.isReturn = this.props.name == \"return_products\";\n        this.products = data[this.props.name];\n        if (this.isReturn || this.products.length == 0){\n            this.products.unshift({name : \"None\", code : false});\n        }\n        this.activeCodeKey = this.isReturn ? \"return\" : \"shipping\";\n        this.state = useState({\n            'code': this.activeCode,\n        });\n    }\n\n    get activeProduct(){\n        let product = this.products.find(pr => pr.code == this.state.code);\n        // Product may be undefined if the user removed the carrier from his sendcloud account\n        if (product === undefined){\n            product = this.products[0];\n        }\n        if (!(\"local_cache\" in product)){\n            product['local_cache'] = {};\n        }\n        return product;\n    }\n\n    get functionalities(){\n        if(! this.activeProduct.local_cache.functionalities){\n            this.activeProduct.local_cache['functionalities'] = {\"bool_func\" : [], \"detail_func\" : {}, \"customizable\" : {}};\n            this._cleanFunctionalities();\n        }\n        return this.activeProduct.local_cache.functionalities;\n    }\n\n    get boolFunctionalities(){\n        return this.functionalities.bool_func; //array of string\n    }\n\n    get detailFunctionalities(){\n        return this.functionalities.detail_func; //dict of string\n    }\n\n    get maxHeight(){\n        return this._maxDimension(\"height\");\n    }\n\n    get maxLength(){\n        return this._maxDimension(\"length\");\n    }\n\n    get maxWidth(){\n        return this._maxDimension(\"width\");\n    }\n\n    get weightRange(){\n        return this.activeProduct.weight_range['min_weight'] + \" - \" + this.activeProduct.weight_range['max_weight'];\n    }\n\n    get activeCode(){\n        return this.props.record.data.sendcloud_products_code[this.activeCodeKey];\n    }\n\n    set activeCode(value){\n        this.props.record.data.sendcloud_products_code[this.activeCodeKey] = value;\n    }\n\n    _cleanFunctionalities(){\n        const product = this.activeProduct;\n        const func_cache = product.local_cache.functionalities;\n        const keys = Object.keys(product.available_functionalities).sort();\n        for(const key of keys){\n            const value = product.available_functionalities[key];\n            const name = this._humanizeFunctionality(key)\n            if (value.some(v => v === true)){\n                func_cache.bool_func.push(name);\n            }else{\n                for(const v in value){\n                    if (value[v] !== false && (value.length > 1 || value[v] !== null)){\n                        const humanizedValue = value[v] === null ? 'None' : this._humanizeFunctionality(value[v]);\n                         if (name in func_cache.detail_func){\n                            func_cache.detail_func[name].push(humanizedValue);\n                         }\n                         else{\n                            func_cache.detail_func[name] = [humanizedValue];\n\n                         }\n                    }\n                }\n                if (name in func_cache.detail_func){\n                    func_cache.detail_func[name] = func_cache.detail_func[name].join(\", \");\n                }\n            }\n            // When multiple parameters are available for the same functionality,\n            //  add the 'technical description' appart for later user-customization\n            if (value.length > 1){\n                func_cache.customizable[key] = value;\n            }\n        }\n    }\n\n    _humanizeFunctionality(technicalName){\n        technicalName = new String(technicalName);\n        return technicalName.substring(0, 1).toUpperCase().concat(technicalName.substring(1).replaceAll(\"_\", \" \")).toString();\n    }\n\n    _maxDimension(name){\n        if (name != 'length' && name != 'width' && name != 'height'){\n            return 0;\n        }\n        if (!(\"max_\"+name in this.activeProduct.local_cache)){\n            // Ensure normalization in cm\n            // [method].properties.max_dimensions.unit is in {millimeter, centimeter, meter}\n            var size = this.activeProduct.methods.map((x) => {\n                let val = parseInt(x.properties.max_dimensions[name]);\n                let factor = 1.0; // centimeter per default\n                let unit = x.properties.max_dimensions['unit'];\n                if (unit == 'millimeter'){\n                    factor = 0.1;\n                }else if (unit == 'meter'){\n                    factor = 100;\n                }\n                return formatFloat(val * factor);\n            });\n            this.activeProduct.local_cache[\"max_\"+name] = Math.max.apply(null, size);\n        }\n        return this.activeProduct.local_cache[\"max_\"+name];\n    }\n\n    _onSelected(code){\n        this.state.code = code;\n        this.activeCode = code;\n    }\n}\n\nexport const sendcloudProductSelectionWidget = {\n    component: SendcloudProductSelectionWidget,\n};\n\nregistry.category(\"fields\").add(\"sendcloud_product_selection\", sendcloudProductSelectionWidget);\n\n", "import { patch } from \"@web/core/utils/patch\";\nimport { Message } from \"@mail/core/common/message\";\n\npatch(Message, {\n    SHADOW_LINK_COLOR: \"#017e84\",\n    SHADOW_LINK_HOVER_COLOR: \"#016b70\",\n});\n", "import { FileViewer } from \"@web/core/file_viewer/file_viewer\";\nimport { patch } from \"@web/core/utils/patch\";\n\nimport { useBackButton } from \"@web_mobile/js/core/hooks\";\n\npatch(FileViewer.prototype, {\n    setup() {\n        super.setup();\n        useBackButton(() => this.close());\n    },\n});\n", "import { ChatWindow } from \"@mail/core/common/chat_window\";\n\nimport { useService } from \"@web/core/utils/hooks\";\nimport { patch } from \"@web/core/utils/patch\";\n\nimport { useBackButton } from \"@web_mobile/js/core/hooks\";\n\npatch(ChatWindow.prototype, {\n    setup() {\n        super.setup();\n        useBackButton(() => this.props.chatWindow.close());\n        this.homeMenuService = useService(\"home_menu\");\n    },\n});\n", "import { MessagingMenu } from \"@mail/core/public_web/messaging_menu\";\n\nimport { patch } from \"@web/core/utils/patch\";\n\nimport { useBackButton } from \"@web_mobile/js/core/hooks\";\n\npatch(MessagingMenu.prototype, {\n    setup() {\n        super.setup();\n        useBackButton(\n            () => this.dropdown.close(),\n            () => this.dropdown.isOpen\n        );\n    },\n});\n", "    import { _t } from \"@web/core/l10n/translation\";\n    import { registry } from \"@web/core/registry\";\n    import { patch } from \"@web/core/utils/patch\";\n    import { markup } from \"@odoo/owl\";\n    import { accountTourSteps } from \"@account/js/tours/account\";\n\n    patch(accountTourSteps, {\n        draftBillSelector:\n            \":has(.o_radio_input[data-value=in_invoice]:checked):has(.o_arrow_button_current[data-value=draft])\",\n        newInvoice() {\n            return [\n                {\n                    trigger: \"button[name=action_create_new]\",\n                    content: _t(\"Now, we'll create your first invoice (accountant)\"),\n                    run: \"click\",\n                }\n            ]\n        },\n        endSteps() {\n            return [\n                {\n                    trigger: \".dropdown-item[data-menu-xmlid='account.menu_board_journal_1']\",\n                    content: _t(\"You can now go back to the dashboard.\"),\n                    tooltipPosition: \"bottom\",\n                    run: \"click\",\n                },\n            ];\n        },\n    });\n\n\n    registry.category(\"web_tour.tours\").add('account_accountant_tour', {\n            url: \"/odoo\",\n            steps: () => [\n            ...accountTourSteps.goToAccountMenu().map(step => ({\n                ...step,\n                // A little hack since the user will come from the account tour which we make sure to make him go to the\n                // dashboard in endSteps(), so we want to resume from there.\n                isActive: ['auto'].concat(step.isActive || []),\n            })),\n            // The tour will stop here if there is at least 1 vendor bill in the database.\n            // While not ideal, it is ok, since that means the user obviously knows how to create a vendor bill...\n            {\n                trigger: 'a[name=\"action_create_vendor_bill\"]',\n                content: markup(_t('Create your first vendor bill.<br/><br/><i>Tip: If you don\u2019t have one on hand, use our sample bill.</i>')),\n                tooltipPosition: 'bottom',\n                run: \"click\",\n            }, {\n                trigger: `.o_form_view_container${accountTourSteps.draftBillSelector} button.btn-primary[name='action_post']`,\n                content: _t('After the data extraction, check and validate the bill. If no vendor has been found, add one before validating.'),\n                tooltipPosition: 'bottom',\n                run: \"click\",\n            }, {\n                trigger: '.dropdown-item[data-menu-xmlid=\"account.menu_board_journal_1\"]',\n                content: _t('Let\u2019s go back to the dashboard.'),\n                tooltipPosition: 'bottom',\n                run: \"click\",\n            }, {\n                trigger: 'a[name=\"open_action\"] span:contains(bank)',\n                content: _t('Connect your bank and get your latest transactions.'),\n                tooltipPosition: 'bottom',\n                run: \"click\",\n            }, {\n                isActive: [\"auto\"],\n                trigger: \".o_bank_reconciliation_container\",\n            }, {\n                trigger: \".o_bank_rec_widget_kanban_view button.o-kanban-button-new\",\n                content: _t('Create a new transaction.'),\n                tooltipPosition: \"bottom\",\n                run: \"click\",\n            }, {\n                trigger: \".o_bank_rec_widget_kanban_view div[name=amount] input\",\n                content: _t(\"Set an amount.\"),\n                tooltipPosition: \"bottom\",\n                run: \"edit -19250.00\",\n            }, {\n                trigger: \".o_bank_rec_widget_kanban_view div[name=payment_ref] input[id=payment_ref_0]\",\n                content: _t(\"Set the payment reference.\"),\n                tooltipPosition: \"bottom\",\n                run: \"edit Payment Deco Adict\",\n            }, {\n                trigger: \".o_bank_rec_widget_kanban_view button.o_kanban_edit\",\n                content: _t(\"Confirm the transaction.\"),\n                tooltipPosition: \"bottom\",\n                run: \"click\",\n            }, {\n                isActive: [\"auto\"],\n                trigger: '.o_kanban_renderer:not(:has(.o_bank_reconciliation_quick_create))',\n            }, {\n                isActive: [\"auto\"],\n                trigger: \".o_bank_reconciliation_container\",\n            }, {\n                trigger: \".o_bank_rec_widget_kanban_view div[name='bank_statement_line']:first\",\n                content: _t(\"Click on the statement to unfold it.\"),\n                tooltipPosition: \"bottom\",\n                run: \"click\",\n            }, {\n                isActive: [\"auto\"],\n                trigger: \"div.o_button_line\",\n            }, {\n                trigger: '.dropdown-item[data-menu-xmlid=\"account.menu_board_journal_1\"]',\n                content: _t('Let\u2019s go back to the dashboard.'),\n                run: \"click\",\n            }\n        ]\n    });\n", "import { AttachmentView } from \"@mail/core/common/attachment_view\";\nimport { onMounted } from \"@odoo/owl\";\nimport { useBus } from \"@web/core/utils/hooks\";\n\nexport class AccountAttachmentView extends AttachmentView {\n    static props = [...AttachmentView.props, \"openInPopout\"];\n    static components = { AttachmentView };\n\n    setup() {\n        super.setup();\n        if (this.props.openInPopout) {\n            onMounted(this.onClickPopout);\n        }\n        // In the mail_popout_service.js the function pollClosedWindow will trigger a resize event on the main window\n        // when the external window is closed. It allow us to know when to hide the preview for small screens.\n        useBus(this.uiService.bus, \"resize\", () => this.env.setPopout(false));\n    }\n}\n", "import { AccountAttachmentView } from \"./account_attachment_view\";\n\nimport { registry } from \"@web/core/registry\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { listView } from \"@web/views/list/list_view\";\nimport { ListRenderer } from \"@web/views/list/list_renderer\";\nimport { ListController } from \"@web/views/list/list_controller\";\nimport { SIZES } from \"@web/core/ui/ui_service\";\nimport { useChildSubEnv, useState } from \"@odoo/owl\";\n\nexport class AttachmentPreviewListController extends ListController {\n    static template = \"account_accountant.AttachmentPreviewListView\";\n    static components = {\n        ...ListController.components,\n        AccountAttachmentView,\n    };\n    setup() {\n        super.setup();\n        /** @type {import(\"@mail/core/common/store_service\").Store} */\n        this.store = useService(\"mail.store\");\n        this.ui = useService(\"ui\");\n        this.mailPopoutService = useService(\"mail.popout\");\n        this.attachmentPreviewState = useState({\n            displayAttachment: localStorage.getItem(this.previewerStorageKey) !== \"false\",\n            selectedRecord: false,\n            thread: null,\n        });\n        this.popout = useState({ active: false });\n\n        useChildSubEnv({\n            setPopout: this.setPopout.bind(this),\n        });\n    }\n\n    get previewEnabled() {\n        return (\n            !this.env.searchModel.context.disable_preview &&\n            (this.ui.size >= SIZES.XXL || this.mailPopoutService.externalWindow)\n        );\n    }\n\n    togglePreview() {\n        this.attachmentPreviewState.displayAttachment =\n            !this.attachmentPreviewState.displayAttachment;\n        localStorage.setItem(\n            this.previewerStorageKey,\n            this.attachmentPreviewState.displayAttachment\n        );\n    }\n\n    setPopout(value) {\n        /**\n         * This function will set the popout value to false or true depending on the situation.\n         * We set popout to True when clicking on a line that has an attachment and then clicking on the popout button.\n         * Once the external page is closed, the popout is set to false again.\n         */\n        if (this.attachmentPreviewState.thread?.attachmentsInWebClientView.length) {\n            this.popout.active = value;\n        }\n    }\n\n    async setThread(lineData, attachmentField, modelField) {\n        const attachments = lineData?.data[attachmentField]?.records || [];\n        if (!lineData || !attachments.length) {\n            this.attachmentPreviewState.thread = null;\n            return;\n        }\n        const thread = this.store.Thread.insert({\n            attachments: attachments.map((attachment) => ({\n                id: attachment.resId,\n                mimetype: attachment.data.mimetype,\n            })),\n            id: lineData.data[modelField].id,\n            model: lineData.fields[modelField].relation,\n        });\n        if (!thread.message_main_attachment_id && thread.attachmentsInWebClientView.length > 0) {\n            thread.update({ message_main_attachment_id: thread.attachmentsInWebClientView[0] });\n        }\n        this.attachmentPreviewState.thread = thread;\n    }\n}\n\nexport class AttachmentPreviewListRenderer extends ListRenderer {\n    static props = [...ListRenderer.props, \"setSelectedRecord\"];\n    async onCellClicked(record, column, ev) {\n        this.props.setSelectedRecord(record);\n        await super.onCellClicked(record, column, ev);\n    }\n\n    findFocusFutureCell(cell, cellIsInGroupRow, direction) {\n        const futureCell = super.findFocusFutureCell(cell, cellIsInGroupRow, direction);\n        if (futureCell) {\n            const dataPointId = futureCell.closest(\"tr\").dataset.id;\n            const record = this.props.list.records.filter((x) => x.id === dataPointId)[0];\n            this.props.setSelectedRecord(record);\n        }\n        return futureCell;\n    }\n}\nexport const AccountAttachmentListView = {\n    ...listView,\n    Renderer: AttachmentPreviewListRenderer,\n    Controller: AttachmentPreviewListController,\n};\n\nregistry.category(\"views\").add(\"account_attachment_list\", AccountAttachmentListView);\n", "import { Component } from \"@odoo/owl\";\nimport { standardFieldProps } from \"@web/views/fields/standard_field_props\";\nimport { registry } from \"@web/core/registry\";\nimport { useService } from \"@web/core/utils/hooks\";\n\nclass BankRecWidgetApplyAmountHtmlField extends Component {\n    static props = standardFieldProps;\n    static template = \"account_accountant.BankRecWidgetApplyAmountHtmlField\";\n\n    setup() {\n        this.action = useService(\"action\");\n        this.orm = useService(\"orm\");\n    }\n\n    get value() {\n        return this.props.record.data[this.props.name];\n    }\n\n    async switchApplyAmount(ev) {\n        const root = this.env.model.root;\n        const fetchReconciledLines = async (fields = []) => {\n            return await this.orm.searchRead(\n                \"account.move.line\",\n                [\n                    [\n                        \"id\",\n                        \"in\",\n                        ...root.data.reconciled_lines_excluding_exchange_diff_ids._currentIds,\n                    ],\n                ],\n                fields\n            );\n        };\n\n        const fetchStatementLines = async (fields = []) => {\n            return await this.orm.searchRead(\n                \"account.move.line\",\n                [[\"move_id\", \"=\", root.data.move_id.id]],\n                fields\n            );\n        };\n\n        if (ev.target.attributes.name?.value === \"action_redirect_to_move\") {\n            const [line] = await fetchReconciledLines([\"amount_currency\", \"balance\", \"move_id\"]);\n            await this.openMove(line.move_id[0]);\n        } else if (ev.target.attributes.name?.value === \"apply_full_amount\") {\n            const [line] = await fetchReconciledLines([\"amount_currency\", \"balance\"]);\n            await root.update({\n                balance: -line.balance,\n                amount_currency: -line.amount_currency,\n            });\n        } else if (ev.target.attributes.name?.value === \"apply_partial_amount\") {\n            const lines = await fetchStatementLines([\"amount_currency\", \"balance\"]);\n            // We have all the lines of the entry, we want the amount of the suspense line\n            await root.update({\n                balance: lines.at(-1).balance,\n                amount_currency: lines.at(-1).amount_currency,\n            });\n        }\n    }\n\n    openMove(moveId) {\n        this.action.doAction({\n            type: \"ir.actions.act_window\",\n            res_model: \"account.move\",\n            res_id: moveId,\n            views: [[false, \"form\"]],\n            target: \"current\",\n        });\n    }\n}\n\nconst bankRecWidgetApplyAmountHtmlField = { component: BankRecWidgetApplyAmountHtmlField };\n\nregistry.category(\"fields\").add(\"apply_amount_html\", bankRecWidgetApplyAmountHtmlField);\n", "import { EventBus, reactive, useState } from \"@odoo/owl\";\nimport { browser } from \"@web/core/browser/browser\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { registry } from \"@web/core/registry\";\n\nexport class BankReconciliationService {\n    constructor(env, services) {\n        this.env = env;\n        this.setup(env, services);\n    }\n\n    setup(env, services) {\n        this.bus = new EventBus();\n        this.orm = services[\"orm\"];\n\n        this.chatterState = reactive({\n            visible:\n                JSON.parse(\n                    browser.sessionStorage.getItem(\"isBankReconciliationWidgetChatterOpened\")\n                ) ?? false,\n            statementLine: null,\n        });\n        this.reconcileCountPerPartnerId = reactive({});\n        this.reconcileModelPerStatementLineId = reactive({});\n    }\n\n    toggleChatter() {\n        this.chatterState.visible = !this.chatterState.visible;\n        browser.sessionStorage.setItem(\n            \"isBankReconciliationWidgetChatterOpened\",\n            this.chatterState.visible\n        );\n    }\n\n    /**\n     * Specific function to open the chatter.\n     * For a particular case, where the customer clicks on\n     * the chatter icon directly on the bank statement line,\n     * we want to open the chatter but not close it.\n     */\n    openChatter() {\n        this.chatterState.visible = true;\n    }\n\n    selectStatementLine(statementLine) {\n        this.chatterState.statementLine = statementLine;\n    }\n\n    reloadChatter() {\n        this.bus.trigger(\"MAIL:RELOAD-THREAD\", {\n            model: \"account.move\",\n            id: this.statementLineMoveId,\n        });\n    }\n\n    async computeReconcileLineCountPerPartnerId(records) {\n        const groups = await this.orm.formattedReadGroup(\n            \"account.move.line\",\n            [\n                [\"parent_state\", \"in\", [\"draft\", \"posted\"]],\n                [\n                    \"partner_id\",\n                    \"in\",\n                    records\n                        .filter((record) => !!record.data.partner_id.id)\n                        .map((record) => record.data.partner_id.id),\n                ],\n                [\"company_id\", \"child_of\", records.map((record) => record.data.company_id.id)],\n                [\"search_account_id.reconcile\", \"=\", true],\n                [\"display_type\", \"not in\", [\"line_section\", \"line_note\"]],\n                [\"reconciled\", \"=\", false],\n                \"|\",\n                [\"search_account_id.account_type\", \"not in\", [\"asset_receivable\", \"liability_payable\"]],\n                [\"payment_id\", \"=\", false],\n                [\"statement_line_id\", \"not in\", records.map((record) => record.data.id)],\n            ],\n            [\"partner_id\"],\n            [\"id:count\"]\n        );\n\n        this.reconcileCountPerPartnerId = {};\n        groups.forEach((group) => {\n            this.reconcileCountPerPartnerId[group.partner_id[0]] = group[\"id:count\"];\n        });\n    }\n\n    async computeAvailableReconcileModels(records) {\n        this.reconcileModelPerStatementLineId =\n            Object.keys(records).length === 0\n                ? {}\n                : await this.orm.call(\n                      \"account.reconcile.model\",\n                      \"get_available_reconcile_model_per_statement_line\",\n                      [records.map((record) => record.data.id)]\n                  );\n    }\n\n    async updateAvailableReconcileModels(recordId) {\n        const result = await this.orm.call(\n            \"account.reconcile.model\",\n            \"get_available_reconcile_model_per_statement_line\",\n            [[recordId]]\n        );\n        this.reconcileModelPerStatementLineId[recordId] = result[recordId];\n    }\n\n    async reloadRecords(records) {\n        await Promise.all([...records.map((record) => record.load())]);\n    }\n\n    get statementLineMove() {\n        return this.chatterState.statementLine?.data.move_id;\n    }\n\n    get statementLineMoveId() {\n        return this.statementLineMove?.id;\n    }\n\n    get statementLine() {\n        return this.chatterState.statementLine;\n    }\n\n    get statementLineId() {\n        return this.statementLine?.data?.id;\n    }\n}\n\nconst bankReconciliationService = {\n    dependencies: [\"orm\"],\n    start(env, services) {\n        return new BankReconciliationService(env, services);\n    },\n};\n\nregistry.category(\"services\").add(\"bankReconciliation\", bankReconciliationService);\n\nexport function useBankReconciliation() {\n    return useState(useService(\"bankReconciliation\"));\n}\n", "import { FormController } from \"@web/views/form/form_controller\";\nimport { FormViewDialog } from \"@web/views/view_dialogs/form_view_dialog\";\nimport { formView } from \"@web/views/form/form_view\";\nimport { onWillStart } from \"@odoo/owl\";\nimport { registry } from \"@web/core/registry\";\nimport { user } from \"@web/core/user\";\n\nexport class BankRecFormDialog extends FormViewDialog {\n    setup() {\n        super.setup();\n        Object.assign(this.viewProps, {\n            buttonTemplate: 'accountant.BankRecFormDialog.buttons',\n        });\n    }\n}\n\nexport class BankRecEditLineFormController extends FormController {\n    setup() {\n        super.setup();\n        this.isReviewed = this.props.context.is_reviewed;\n        onWillStart(async () => {\n            this.userCanReview = await user.hasGroup(\"account.group_account_user\");\n        })\n    }\n\n    async toReviewButtonClicked (params = {}) {\n        await this.orm.call(\"account.move\", \"set_moves_checked\", [\n            this.model.root.data.move_id.id,\n            false,\n        ]);\n        return this.saveButtonClicked(params);\n    }\n}\n\nexport const bankRecEditLineFormController = {\n    ...formView,\n    Controller: BankRecEditLineFormController,\n};\n\nregistry.category(\"views\").add(\"bankrec_edit_line\", bankRecEditLineFormController);\n", "import { Component } from \"@odoo/owl\";\nimport { useService } from \"@web/core/utils/hooks\";\n\nexport class BankRecButton extends Component {\n    static template = \"account_accountant.BankRecButton\";\n    static props = {\n        label: { type: String, optional: true },\n        action: { type: Function, optional: true },\n        count: { type: [Number, { value: null }], optional: true },\n        primary: { type: Boolean, optional: true },\n        toReview: { type: Boolean, optional: true },\n        classes: { type: String, optional: true },\n    };\n    static defaultProps = {\n        primary: false,\n        classes: \"\",\n    };\n\n    setup() {\n        this.ui = useService(\"ui\");\n    }\n}\n", "import { BankRecButton } from \"../button/button\";\nimport { BankRecFileUploader } from \"../file_uploader/file_uploader\";\nimport { Component } from \"@odoo/owl\";\nimport { ConfirmationDialog } from \"@web/core/confirmation_dialog/confirmation_dialog\";\nimport { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\nimport { SelectCreateDialog } from \"@web/views/view_dialogs/select_create_dialog\";\nimport { BankRecSelectCreateDialog } from \"../search_dialog/search_dialog\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { getCurrency } from \"@web/core/currency\";\nimport { useOwnedDialogs, useService } from \"@web/core/utils/hooks\";\nimport { useBankReconciliation } from \"../bank_reconciliation_service\";\nimport { useHotkey } from \"@web/core/hotkeys/hotkey_hook\";\n\nexport class BankRecButtonList extends Component {\n    static template = \"account_accountant.BankRecButtonList\";\n    static components = {\n        Dropdown,\n        DropdownItem,\n        BankRecButton,\n        BankRecFileUploader,\n    };\n    static props = {\n        statementLineRootRef: { type: Object },\n        statementLine: { type: Object },\n        suspenseAccountLine: { type: Object, optional: true },\n        reconcileLineCount: { type: [Number, { value: null }], optional: true },\n        reconcileModels: Array,\n        preSelectedReconciliationModel: { type: Object, optional: true },\n    };\n    static defaultProps = {\n        reconcileLineCount: 0,\n    };\n\n    setup() {\n        this.action = useService(\"action\");\n        this.ui = useService(\"ui\");\n        this.orm = useService(\"orm\");\n\n        this.addDialog = useOwnedDialogs();\n        this.currencyDigits = getCurrency(this.statementLineData.currency_id.id)?.digits || 2;\n        this.bankReconciliation = useBankReconciliation();\n\n        this.registerHotkeys();\n    }\n\n    restoreFocus() {\n        if (this.isLineSelected) {\n            this.props.statementLineRootRef.el.focus();\n        }\n    }\n\n    /**\n     * Displays a search dialog (no create option) for selecting a `res.partner` record.\n     */\n    setPartnerOnReconcileLine() {\n        this.addDialog(\n            SelectCreateDialog,\n            {\n                title: _t(\"Search: Partner\"),\n                noCreate: false,\n                multiSelect: false,\n                resModel: \"res.partner\",\n                context: { default_name: this.statementLineData.partner_name },\n                onSelected: async (partner) => {\n                    await this.orm.call(\n                        \"account.bank.statement.line\",\n                        \"set_partner_bank_statement_line\",\n                        [this.statementLineData.id, partner[0]]\n                    );\n                    const recordsToLoad = [];\n                    if (this.statementLineData.partner_name) {\n                        // Reload all impacted statement lines if we have a partner_name\n                        recordsToLoad.push(\n                            ...this.env.model.root.records.filter(\n                                (record) =>\n                                    record.data.partner_name === this.statementLineData.partner_name\n                            )\n                        );\n                    } else {\n                        recordsToLoad.push(this.props.statementLine);\n                    }\n                    await this.bankReconciliation.reloadRecords(recordsToLoad);\n                    await this.bankReconciliation.computeReconcileLineCountPerPartnerId(\n                        this.env.model.root.records\n                    );\n                    this.bankReconciliation.reloadChatter();\n                    this.restoreFocus();\n                },\n            },\n            {\n                onClose: () => {\n                    this.restoreFocus();\n                },\n            }\n        );\n    }\n\n    /**\n     * Opens a dialog to select an account and assigns it to the current reconcile line.\n     */\n    setAccountOnReconcileLine() {\n        const context = {\n            list_view_ref: \"account_accountant.view_account_list_bank_rec_widget\",\n            search_view_ref: \"account_accountant.view_account_search_bank_rec_widget\",\n            ...(this.statementLineData.amount > 0\n                ? { preferred_account_type: \"income\" }\n                : { preferred_account_type: \"expense\" }),\n        };\n\n        this.addDialog(\n            SelectCreateDialog,\n            {\n                title: _t(\"Search: Account\"),\n                noCreate: true,\n                multiSelect: false,\n                domain: [\n                    [\n                        \"id\",\n                        \"not in\",\n                        [\n                            this.statementLineData.journal_id.suspense_account_id.id,\n                            this.statementLineData.journal_id.default_account_id.id,\n                        ],\n                    ],\n                ],\n                context: context,\n                resModel: \"account.account\",\n                onSelected: async (account) => {\n                    // After setting an account on a line, a new reconciliation model may be automatically created. If so,\n                    // we need to reload the records that will use this model to make sure the new model is displayed.\n                    const linesToLoad = await this._setAccountOnReconcileLine(\n                        this.lastAccountMoveLine.data.id,\n                        account[0],\n                        { context: { account_default_taxes: true } }\n                    );\n                    const recordsToLoad = [\n                        ...this.env.model.root.records.filter((record) =>\n                            linesToLoad.includes(record.data.id)\n                        ),\n                        this.props.statementLine,\n                    ];\n                    await this.bankReconciliation.reloadRecords(recordsToLoad);\n                    this.bankReconciliation.reloadChatter();\n                    this.restoreFocus();\n                },\n            },\n            {\n                onClose: () => {\n                    this.restoreFocus();\n                },\n            }\n        );\n    }\n\n    /**\n     * Assigns the given account to a specific account move line within the current bank statement line.\n     *\n     * @param {number} amlId - ID of the account move line to update.\n     * @param {number} accountId - ID of the selected account to assign.\n     * @param {Object} context - the context to use for adding default tax of account\n     *\n     * @returns {Promise<list>} - The list of IDs of lines to reload in case of auto-rule creation.\n     */\n    async _setAccountOnReconcileLine(amlId, accountId, context = {}) {\n        return await this.orm.call(\n            \"account.bank.statement.line\",\n            \"set_account_bank_statement_line\",\n            [this.statementLineData.id, amlId, accountId],\n            context\n        );\n    }\n\n    /**\n     * Sets the account receivable on the current reconcile line.\n     */\n    async setAccountReceivableOnReconcileLine() {\n        let accountId;\n        if (this.statementLineData.partner_id.property_account_receivable_id.id) {\n            accountId = this.statementLineData.partner_id.property_account_receivable_id.id;\n        } else {\n            accountId = await this.orm.webSearchRead(\"account.account\", [\n                [\"account_type\", \"=\", \"asset_receivable\"],\n            ]);\n        }\n        await this._setAccountOnReconcileLine(this.lastAccountMoveLine.data.id, accountId);\n        this.props.statementLine.load();\n        this.bankReconciliation.reloadChatter();\n    }\n\n    /**\n     * Sets the account payable on the current reconcile line..\n     */\n    async setAccountPayableOnReconcileLine() {\n        let accountId;\n        if (this.statementLineData.partner_id.property_account_payable_id.id) {\n            accountId = this.statementLineData.partner_id.property_account_payable_id.id;\n        } else {\n            accountId = await this.orm.webSearchRead(\"account.account\", [\n                [\"account_type\", \"=\", \"liability_payable\"],\n            ]);\n        }\n        await this._setAccountOnReconcileLine(this.lastAccountMoveLine.data.id, accountId);\n        this.props.statementLine.load();\n        this.bankReconciliation.reloadChatter();\n    }\n\n    /**\n     * Opens a dialog to search and select journal items to reconcile with the current bank statement line.\n     */\n    reconcileOnReconcileLine() {\n        const context = {\n            list_view_ref: \"account_accountant.view_account_move_line_list_bank_rec_widget\",\n            search_view_ref: \"account_accountant.view_account_move_line_search_bank_rec_widget\",\n            preferred_aml_value: -this.props.suspenseAccountLine.amount_currency,\n            preferred_aml_currency_id: this.props.suspenseAccountLine.currency_id.id,\n            ...(this.statementLineData.partner_id\n                ? { search_default_partner_id: this.statementLineData.partner_id.id }\n                : { search_default_posted: 1 }),\n        };\n\n        this.addDialog(\n            BankRecSelectCreateDialog,\n            {\n                title: _t(\"Search: Journal Items to Match\"),\n                noCreate: true,\n                domain: this.getReconcileButtonDomain(),\n                resModel: \"account.move.line\",\n                size: \"xl\",\n                context: context,\n                onSelected: async (moveLines) => {\n                    await this.orm.call(\n                        \"account.bank.statement.line\",\n                        \"set_line_bank_statement_line\",\n                        [this.statementLineData.id, moveLines]\n                    );\n                    await this.bankReconciliation.computeReconcileLineCountPerPartnerId(\n                        this.env.model.root.records\n                    );\n                    this.props.statementLine.load();\n                    this.bankReconciliation.reloadChatter();\n                    this.restoreFocus();\n                },\n                suspenseAccountLine: this.props.suspenseAccountLine,\n                reference: this.statementLineData.payment_ref,\n                date: this.statementLineData.date,\n            },\n            {\n                onClose: () => {\n                    this.restoreFocus();\n                },\n            }\n        );\n    }\n\n    getReconcileButtonDomain() {\n        return [\n            [\"parent_state\", \"in\", [\"draft\", \"posted\"]],\n            [\"company_id\", \"child_of\", this.statementLineData.company_id.id],\n            [\"search_account_id.reconcile\", \"=\", true],\n            [\"display_type\", \"not in\", [\"line_section\", \"line_note\"]],\n            [\"reconciled\", \"=\", false],\n            \"|\",\n            [\"search_account_id.account_type\", \"not in\", [\"asset_receivable\", \"liability_payable\"]],\n            [\"payment_id\", \"=\", false],\n            [\"statement_line_id\", \"!=\", this.statementLineData.id],\n        ];\n    }\n\n    /**\n     * Deletes the current bank statement line.\n     */\n    async deleteTransaction() {\n        this.addDialog(ConfirmationDialog, {\n            body: _t(\"Are you sure you want to delete this statement line?\"),\n            confirm: async () => {\n                await this.orm.unlink(\"account.bank.statement.line\", [this.statementLineData.id]);\n                this.env.model.load();\n            },\n            cancel: () => {},\n        });\n    }\n\n    /**\n     * Set the move of the statement line as to check\n     */\n    async setStatementLineAsReviewed() {\n        await this.orm.call(\"account.move\", \"set_moves_checked\", [\n            this.statementLineData.move_id.id,\n        ]);\n        this.props.statementLine.load();\n        this.bankReconciliation.reloadChatter();\n    }\n\n    // -----------------------------------------------------------------------------\n    // Reconciliation Model\n    // -----------------------------------------------------------------------------\n    /**\n     * Applies a reconciliation model to the current bank statement line.\n     *\n     * @param {number} reconciliationModelId - The ID of the reconciliation model to apply.\n     */\n    async triggerReconciliationModel(reconciliationModelId) {\n        await this.orm.call(\"account.reconcile.model\", \"trigger_reconciliation_model\", [\n            reconciliationModelId,\n            this.statementLineData.id,\n        ]);\n        await this.bankReconciliation.computeReconcileLineCountPerPartnerId(\n            this.env.model.root.records\n        );\n        this.props.statementLine.load();\n        this.bankReconciliation.reloadChatter();\n    }\n\n    /**\n     * Retrieves the corresponding action, condition, and button element for a given key.\n     * This function is part of a keydown event handler that maps specific key presses to actions\n     * on the reconciliation line, such as setting a partner or reconciling an account.\n     * It checks if a line is selected and if the relevant button exists before returning the action details.\n     *\n     * @param {string|number} key - The key pressed.\n     * @returns {Object|undefined} An object containing the action, condition, and button element, or undefined if no action is found.\n     */\n    getKeyAction(key) {\n        const keyActions = {\n            1: {\n                condition:\n                    this.props.statementLineRootRef.el.querySelector(\".set-partner-btn\") &&\n                    this.isLineSelected,\n                action: async () => this.setPartnerOnReconcileLine(),\n                buttonElement: this.props.statementLineRootRef.el.querySelector(\".set-partner-btn\"),\n            },\n            2: {\n                condition:\n                    this.props.statementLineRootRef.el.querySelector(\".reconcile-btn\") &&\n                    this.isLineSelected,\n                action: async () => this.reconcileOnReconcileLine(),\n                buttonElement: this.props.statementLineRootRef.el.querySelector(\".reconcile-btn\"),\n            },\n            3: {\n                condition:\n                    this.props.statementLineRootRef.el.querySelector(\".set-account-btn\") &&\n                    this.isLineSelected,\n                action: () => this.setAccountOnReconcileLine(),\n                buttonElement: this.props.statementLineRootRef.el.querySelector(\".set-account-btn\"),\n            },\n            4: {\n                condition:\n                    this.props.statementLineRootRef.el.querySelector(\".set-payable-btn\") &&\n                    this.isLineSelected,\n                action: () => this.setAccountPayableOnReconcileLine(),\n                buttonElement: this.props.statementLineRootRef.el.querySelector(\".set-payable-btn\"),\n            },\n            5: {\n                condition:\n                    this.props.statementLineRootRef.el.querySelector(\".set-receivable-btn\") &&\n                    this.isLineSelected,\n                action: () => this.setAccountReceivableOnReconcileLine(),\n                buttonElement:\n                    this.props.statementLineRootRef.el.querySelector(\".set-receivable-btn\"),\n            },\n            6: {\n                condition:\n                    this.props.statementLineRootRef.el.querySelector(\n                        \".reconciliation-model-btn-0\"\n                    ) && this.isLineSelected,\n                action: () => {\n                    const buttonElement = this.props.statementLineRootRef.el.querySelector(\n                        \".reconciliation-model-btn-0\"\n                    );\n                    if (buttonElement) {\n                        buttonElement.click();\n                    }\n                },\n                buttonElement: this.props.statementLineRootRef.el.querySelector(\n                    \".reconciliation-model-btn-0\"\n                ),\n            },\n            7: {\n                condition:\n                    this.props.statementLineRootRef.el.querySelector(\n                        \".reconciliation-model-btn-1\"\n                    ) && this.isLineSelected,\n                action: () => {\n                    const buttonElement = this.props.statementLineRootRef.el.querySelector(\n                        \".reconciliation-model-btn-1\"\n                    );\n                    if (buttonElement) {\n                        buttonElement.click();\n                    }\n                },\n                buttonElement: this.props.statementLineRootRef.el.querySelector(\n                    \".reconciliation-model-btn-1\"\n                ),\n            },\n            8: {\n                condition:\n                    this.props.statementLineRootRef.el.querySelector(\n                        \".reconciliation-model-btn-2\"\n                    ) && this.isLineSelected,\n                action: () => {\n                    const buttonElement = this.props.statementLineRootRef.el.querySelector(\n                        \".reconciliation-model-btn-2\"\n                    );\n                    if (buttonElement) {\n                        buttonElement.click();\n                    }\n                },\n                buttonElement: this.props.statementLineRootRef.el.querySelector(\n                    \".reconciliation-model-btn-2\"\n                ),\n            },\n            Enter: {\n                condition:\n                    this.props.statementLineRootRef.el.querySelector(\".btn-primary\") &&\n                    this.isLineSelected,\n                action: () => {\n                    const primaryButtons = this.props.statementLineRootRef.el.querySelectorAll(\".btn-primary\");\n                    if (primaryButtons.length > 0) {\n                        primaryButtons[0].click();\n                    }\n                },\n                buttonElement: this.props.statementLineRootRef.el.querySelector(\".btn-primary\"),\n            },\n        };\n        return keyActions[key];\n    }\n\n    /**\n     * Registers hotkeys for the reconciliation buttons.\n     */\n    registerHotkeys() {\n        const hotkeyConfigs = [\n            { key: \"1\", trigger: \"alt+shift+1\" },\n            { key: \"2\", trigger: \"alt+shift+2\" },\n            { key: \"3\", trigger: \"alt+shift+3\" },\n            { key: \"4\", trigger: \"alt+shift+4\" },\n            { key: \"5\", trigger: \"alt+shift+5\" },\n            { key: \"6\", trigger: \"alt+shift+6\" },\n            { key: \"7\", trigger: \"alt+shift+7\" },\n            { key: \"8\", trigger: \"alt+shift+8\" },\n            { key: \"Enter\", trigger: \"alt+shift+enter\" },\n        ];\n        hotkeyConfigs.forEach(({ key, trigger }) => {\n            useHotkey(\n                trigger,\n                ({ target }) => {\n                    const { condition, action } = this.getKeyAction(key);\n                    if (condition) {\n                        action();\n                    }\n                },\n                {\n                    area: () => this.props.statementLineRootRef.el.parentElement,\n                    withOverlay: () => {\n                        const { buttonElement, condition } = this.getKeyAction(key);\n                        return condition ? buttonElement : null;\n                    },\n                    isAvailable: () => {\n                        const { condition } = this.getKeyAction(key);\n                        return condition;\n                    },\n                }\n            );\n        });\n    }\n\n    // -----------------------------------------------------------------------------\n    // File Uploader\n    // -----------------------------------------------------------------------------\n    get bankRecFileUploaderRecord() {\n        return {\n            statementLineId: this.statementLineData.id,\n        };\n    }\n\n    // -----------------------------------------------------------------------------\n    // ACTION\n    // -----------------------------------------------------------------------------\n    actionViewRecoModels() {\n        return this.action.doAction(\"account.action_account_reconcile_model\");\n    }\n\n    // -----------------------------------------------------------------------------\n    // GETTER\n    // -----------------------------------------------------------------------------\n    get statementLineData() {\n        return this.props.statementLine.data;\n    }\n\n    get isLineSelected() {\n        return this.statementLineData.id === this.bankReconciliation.statementLine?.data.id;\n    }\n\n    get lastAccountMoveLine() {\n        return this.statementLineData.line_ids.records.at(-1);\n    }\n\n    get isCustomerRankHigher() {\n        return (\n            this.statementLineData.partner_id.customer_rank >\n            this.statementLineData.partner_id.supplier_rank\n        );\n    }\n\n    get isSetPartnerButtonShown() {\n        return !this.statementLineData.partner_id;\n    }\n\n    get isSetAccountButtonShown() {\n        return !this.statementLineData.account_id;\n    }\n\n    get isSetReceivableButtonShown() {\n        return (\n            !this.isSetPartnerButtonShown &&\n            ((this.statementLineData.partner_id.customer_rank && this.isCustomerRankHigher) ||\n                this.statementLineData.amount > 0)\n        );\n    }\n\n    get isSetPayableButtonShown() {\n        return (\n            !this.isSetPartnerButtonShown &&\n            ((this.statementLineData.partner_id.supplier_rank && !this.isCustomerRankHigher) ||\n                this.statementLineData.amount < 0)\n        );\n    }\n\n    get isReconcileButtonShown() {\n        // Show the button if we have more than one reconciliable line\n        // or if we didn't compute it yet\n        return this.props.reconcileLineCount === null || this.props.reconcileLineCount;\n    }\n\n    get reconcileModelsInDropdown() {\n        if (this.ui.isSmall) {\n            return this.props.reconcileModels;\n        }\n        return this.props.reconcileModels.filter(\n            (model) => model.id !== this.props?.preSelectedReconciliationModel?.id\n        );\n    }\n\n    /**\n     * Dynamically builds the list of action buttons to be shown in the reconciliation interface.\n     *\n     * @returns {Object} buttonsToDisplay - A dictionary of buttons to render in the UI.\n     */\n    get buttons() {\n        const buttonsToDisplay = {};\n        if (this.isSetPartnerButtonShown) {\n            buttonsToDisplay.partner = {\n                label: _t(\"Set Partner\"),\n                action: this.setPartnerOnReconcileLine.bind(this),\n                classes: \"set-partner-btn\",\n            };\n        } else {\n            buttonsToDisplay.receivable = {\n                label: _t(\"Receivable\"),\n                action: this.setAccountReceivableOnReconcileLine.bind(this),\n                classes: \"set-receivable-btn\",\n            };\n            buttonsToDisplay.payable = {\n                label: _t(\"Payable\"),\n                action: this.setAccountPayableOnReconcileLine.bind(this),\n                classes: \"set-payable-btn\",\n            };\n        }\n\n        if (this.isReconcileButtonShown) {\n            buttonsToDisplay.reconcile = {\n                label: _t(\"Reconcile\"),\n                action: this.reconcileOnReconcileLine.bind(this),\n                count: this.props.reconcileLineCount,\n                classes: \"reconcile-btn\",\n            };\n        }\n\n        if (this.isSetAccountButtonShown) {\n            buttonsToDisplay.account = {\n                label: _t(\"Set Account\"),\n                action: this.setAccountOnReconcileLine.bind(this),\n                classes: \"set-account-btn\",\n            };\n        }\n\n        if (this.statementLineData.is_reconciled && !this.statementLineData.checked) {\n            buttonsToDisplay.toReview = {\n                label: _t(\"Reviewed\"),\n                action: this.setStatementLineAsReviewed.bind(this),\n                toReview: true,\n            };\n        }\n\n        return buttonsToDisplay;\n    }\n\n    /**\n     * Prioritizing which buttons are shown and which one is marked as \"primary\".\n     *\n     * @returns {Array<Object>} An array of button objects, each with label, action, and optionally `primary`.\n     */\n    get buttonsToDisplay() {\n        const buttons = this.buttons || {};\n\n        let primaryButtonKeys = [];\n        let secondaryButtonKeys = [];\n        if (buttons?.partner && buttons?.account) {\n            primaryButtonKeys = [\"partner\", \"account\"];\n        } else if (buttons?.reconcile && !!buttons.reconcile?.count) {\n            primaryButtonKeys = [\"reconcile\"];\n            if (this.isSetReceivableButtonShown) {\n                secondaryButtonKeys = [\"receivable\"];\n            } else {\n                secondaryButtonKeys = [\"payable\"];\n            }\n        } else if (this.isSetReceivableButtonShown) {\n            primaryButtonKeys = [\"receivable\"];\n        } else if (this.isSetPayableButtonShown) {\n            primaryButtonKeys = [\"payable\"];\n        }\n\n        return [\n            ...primaryButtonKeys.map((key) => ({ ...buttons[key], primary: true })),\n            ...secondaryButtonKeys.map((key) => ({ ...buttons[key] })),\n        ];\n    }\n\n    get buttonsInDropdown() {\n        const buttons = this.buttons || {};\n        if (this.props.preSelectedReconciliationModel) {\n            return Object.values(buttons);\n        }\n        const buttonToDisplayClasses = this.buttonsToDisplay.map((button) => button.classes) || [];\n        // Get all other buttons excluding primary ones\n        return Object.values(buttons).filter(\n            (button) => !buttonToDisplayClasses.includes(button.classes)\n        );\n    }\n}\n", "import { Chatter } from \"@mail/chatter/web_portal/chatter\";\n\nexport class BankRecChatter extends Chatter {\n    static props = [...Chatter.props, \"statementLine?\"];\n\n    async reloadParentView() {\n        await this.props.statementLine?.load();\n    }\n}\n", "import { DocumentFileUploader } from \"@account/components/document_file_uploader/document_file_uploader\";\n\nexport class BankRecFileUploader extends DocumentFileUploader {\n\n    /**\n     * This method extends the DocumentFileUploader `getExtraContext` to add the `statement_line_id` to the context,\n     * which is useful for passing the current statement line ID to other components or services. This will be useful\n     * for the 'create_document_from_attachment' method (in account_bank_statement.py).\n     *\n     * @returns {Object} The extended context object with the `statement_line_id`.\n     */\n    getExtraContext() {\n        const extraContext = super.getExtraContext();\n        return {\n            ...extraContext,\n            statement_line_id: this.props.record.statementLineId,\n        };\n    }\n\n    getResModel() {\n        return \"account.bank.statement.line\";\n    }\n}\n", "import { useSubEnv, onWillRender, onWillDestroy } from \"@odoo/owl\";\nimport { KanbanController } from \"@web/views/kanban/kanban_controller\";\nimport { makeActiveField } from \"@web/model/relational_model/utils\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { useBankReconciliation } from \"./bank_reconciliation_service\";\nimport { useHotkey } from \"@web/core/hotkeys/hotkey_hook\";\nimport { user } from \"@web/core/user\";\n\nexport class BankRecKanbanController extends KanbanController {\n    static template = \"account_accountant.BankRecoKanbanController\";\n\n    async setup() {\n        super.setup();\n        this.orm = useService(\"orm\");\n        this.bankReconciliation = useBankReconciliation();\n        useSubEnv({\n            bus: this.bankReconciliation.bus,\n        });\n        useHotkey(\"alt+shift+c\", () => this.bankReconciliation.toggleChatter(), {\n            bypassEditableProtection: true,\n            withOverlay: () => this.rootRef.el.querySelector(\".bank-chatter-btn\"),\n        });\n        onWillRender(() => { user.updateContext({ from_bank_reco : true }) });\n        onWillDestroy(() => { user.updateContext({ from_bank_reco : false }) });\n    }\n\n    async createRecord() {\n        this.env.bus.trigger(\"createRecordQuickCreate\");\n    }\n\n    getCheckedField() {\n        return {\n            fields: {\n                checked: { name: \"checked\", type: \"char\", }\n            },\n            activeFields : {\n                checked: makeActiveField(),\n            }\n        }\n    }\n\n    get modelParams() {\n        const params = super.modelParams;\n        params.config.activeFields.move_id = makeActiveField();\n        params.config.activeFields.move_id.related = {\n            fields: {\n                id: { name: \"id\", type: \"int\" },\n                display_name: { name: \"display_name\", type: \"char\" },\n                attachment_ids: { name: \"attachment_ids\", type: \"one2many\" },\n                checked: { name: \"checked\", type: \"char\" },\n            },\n            activeFields: {\n                attachment_ids: makeActiveField(),\n                checked: makeActiveField(),\n            },\n        };\n        params.config.activeFields.bank_statement_attachment_ids = makeActiveField();\n        params.config.activeFields.bank_statement_attachment_ids.related = {\n            fields: {\n                id: { name: \"id\", type: \"int\" },\n                display_name: { name: \"display_name\", type: \"char\" },\n            },\n            activeFields: {\n                id: makeActiveField(),\n                display_name: makeActiveField(),\n            },\n        };\n        params.config.activeFields.attachment_ids = makeActiveField();\n        params.config.activeFields.partner_id = makeActiveField();\n        params.config.activeFields.partner_id.related = {\n            fields: {\n                id: { name: \"id\", type: \"int\" },\n                display_name: { name: \"display_name\", type: \"char\" },\n                property_account_receivable_id: {\n                    name: \"property_account_receivable_id\",\n                    type: \"many2one\",\n                },\n                property_account_payable_id: {\n                    name: \"property_account_payable_id\",\n                    type: \"many2one\",\n                },\n                customer_rank: { name: \"customer_rank\", type: \"int\" },\n                supplier_rank: { name: \"supplier_rank\", type: \"int\" },\n            },\n            activeFields: {\n                id: makeActiveField(),\n                display_name: makeActiveField(),\n                property_account_receivable_id: makeActiveField(),\n                property_account_payable_id: makeActiveField(),\n                customer_rank: makeActiveField(),\n                supplier_rank: makeActiveField(),\n            },\n        };\n\n        params.config.activeFields.line_ids = makeActiveField();\n        params.config.activeFields.line_ids.related = {\n            fields: {\n                id: { name: \"id\", type: \"int\" },\n                display_name: { name: \"display_name\", type: \"char\" },\n                name: { name: \"name\", type: \"char\" },\n                balance: { name: \"balance\", type: \"monetary\" },\n                amount_currency: { name: \"amount_currency\", type: \"monetary\" },\n                currency_id: { name: \"currency_id\", type: \"many2one\" },\n                currency_rate: { name: \"currency_rate\", type: \"float\" },\n                is_same_currency: { name: \"is_same_currency\", type: \"boolean\" },\n                company_currency_id: { name: \"company_currency_id\", type: \"many2one\" },\n                account_id: { name: \"account_id\", type: \"many2one\" },\n                partner_id: { name: \"partner_id\", type: \"many2one\" },\n                move_id: { name: \"move_id\", type: \"many2one\" },\n                move_attachment_ids: { name: \"move_attachment_ids\", type: \"one2many\" },\n                reconciled_lines_ids: { name: \"reconciled_lines_ids\", type: \"many2many\" },\n                reconciled_lines_excluding_exchange_diff_ids: {\n                    name: \"reconciled_lines_excluding_exchange_diff_ids\",\n                    type: \"many2many\",\n                },\n                matched_debit_ids: { name: \"matched_debit_ids\", type: \"one2many\" },\n                matched_credit_ids: { name: \"matched_credit_ids\", type: \"one2many\" },\n                reconcile_model_id: { name: \"reconcile_model_id\", type: \"many2one\" },\n                has_invalid_analytics: { name: \"has_invalid_analytics\", type: \"boolean\" },\n                tax_line_id: { name: \"tax_line_id\", type: \"many2one\" },\n                tax_ids: { name: \"tax_ids\", type: \"many2many\" },\n            },\n            activeFields: {\n                id: makeActiveField(),\n                display_name: makeActiveField(),\n                name: makeActiveField(),\n                balance: makeActiveField(),\n                amount_currency: makeActiveField(),\n                currency_id: makeActiveField(),\n                currency_rate: makeActiveField(),\n                is_same_currency: makeActiveField(),\n                company_currency_id: makeActiveField(),\n                account_id: makeActiveField(),\n                partner_id: makeActiveField(),\n                move_id: makeActiveField(),\n                move_attachment_ids: makeActiveField(),\n                reconciled_lines_ids: makeActiveField(),\n                reconciled_lines_excluding_exchange_diff_ids: makeActiveField(),\n                matched_debit_ids: makeActiveField(),\n                matched_credit_ids: makeActiveField(),\n                reconcile_model_id: makeActiveField(),\n                has_invalid_analytics: makeActiveField(),\n                tax_line_id: makeActiveField(),\n                tax_ids: makeActiveField(),\n            },\n        };\n        params.config.activeFields.line_ids.related.activeFields.move_attachment_ids.related = {\n            fields: {\n                id: { name: \"id\", type: \"int\" },\n                display_name: { name: \"display_name\", type: \"char\" },\n            },\n            activeFields: {\n                id: makeActiveField(),\n                display_name: makeActiveField(),\n            },\n        };\n        params.config.activeFields.line_ids.related.activeFields.matched_debit_ids.related = {\n            fields: {\n                id: { name: \"id\", type: \"int\" },\n                display_name: { name: \"display_name\", type: \"char\" },\n                exchange_move_id: { name: \"exchange_move_id\", type: \"many2one\" },\n            },\n            activeFields: {\n                id: makeActiveField(),\n                display_name: makeActiveField(),\n                exchange_move_id: makeActiveField(),\n            },\n        };\n        params.config.activeFields.line_ids.related.activeFields.matched_debit_ids.related.activeFields.exchange_move_id.related =\n            {\n                fields: {\n                    id: { name: \"id\", type: \"int\" },\n                    display_name: { name: \"display_name\", type: \"char\" },\n                    line_ids: { name: \"line_ids\", type: \"one2many\" },\n                },\n                activeFields: {\n                    id: makeActiveField(),\n                    display_name: makeActiveField(),\n                    line_ids: makeActiveField(),\n                },\n            };\n        params.config.activeFields.line_ids.related.activeFields.matched_debit_ids.related.activeFields.exchange_move_id.related.activeFields.line_ids.related =\n            {\n                fields: {\n                    id: { name: \"id\", type: \"int\" },\n                    display_name: { name: \"display_name\", type: \"char\" },\n                    balance: { name: \"balance\", type: \"monetary\" },\n                },\n                activeFields: {\n                    id: makeActiveField(),\n                    display_name: makeActiveField(),\n                    balance: makeActiveField(),\n                },\n            };\n        params.config.activeFields.line_ids.related.activeFields.matched_credit_ids.related = {\n            fields: {\n                id: { name: \"id\", type: \"int\" },\n                display_name: { name: \"display_name\", type: \"char\" },\n                exchange_move_id: { name: \"exchange_move_id\", type: \"many2one\" },\n            },\n            activeFields: {\n                id: makeActiveField(),\n                display_name: makeActiveField(),\n                exchange_move_id: makeActiveField(),\n            },\n        };\n        params.config.activeFields.line_ids.related.activeFields.matched_credit_ids.related.activeFields.exchange_move_id.related =\n            {\n                fields: {\n                    id: { name: \"id\", type: \"int\" },\n                    display_name: { name: \"display_name\", type: \"char\" },\n                    line_ids: { name: \"line_ids\", type: \"one2many\" },\n                },\n                activeFields: {\n                    id: makeActiveField(),\n                    display_name: makeActiveField(),\n                    line_ids: makeActiveField(),\n                },\n            };\n        params.config.activeFields.line_ids.related.activeFields.matched_credit_ids.related.activeFields.exchange_move_id.related.activeFields.line_ids.related =\n            {\n                fields: {\n                    id: { name: \"id\", type: \"int\" },\n                    display_name: { name: \"display_name\", type: \"char\" },\n                    balance: { name: \"balance\", type: \"monetary\" },\n                },\n                activeFields: {\n                    id: makeActiveField(),\n                    display_name: makeActiveField(),\n                    balance: makeActiveField(),\n                },\n            };\n        params.config.activeFields.line_ids.related.activeFields.reconciled_lines_ids.related = {\n            fields: {\n                id: { name: \"id\", type: \"int\" },\n                display_name: { name: \"display_name\", type: \"char\" },\n                move_name: { name: \"move_name\", type: \"char\" },\n                move_id: { name: \"move_id\", type: \"many2one\" },\n                amount_currency: { name: \"amount_currency\", type: \"monetary\" },\n                full_reconcile_id: { name: \"full_reconcile_id\", type: \"many2one\" },\n                currency_id: { name: \"currency_id\", type: \"many2one\" },\n                move_attachment_ids: { name: \"move_attachment_ids\", type: \"one2many\" },\n            },\n            activeFields: {\n                id: makeActiveField(),\n                display_name: makeActiveField(),\n                move_name: makeActiveField(),\n                move_id: makeActiveField(),\n                amount_currency: makeActiveField(),\n                full_reconcile_id: makeActiveField(),\n                currency_id: makeActiveField(),\n                move_attachment_ids: makeActiveField(),\n            },\n        };\n        params.config.activeFields.line_ids.related.activeFields.reconciled_lines_excluding_exchange_diff_ids.related =\n            {\n                fields: {\n                    id: { name: \"id\", type: \"int\" },\n                    move_name: { name: \"move_name\", type: \"char\" },\n                    move_id: { name: \"move_id\", type: \"many2one\" },\n                },\n                activeFields: {\n                    id: makeActiveField(),\n                    move_name: makeActiveField(),\n                    move_id: makeActiveField(),\n                },\n            };\n        params.config.activeFields.line_ids.related.activeFields.reconciled_lines_ids.related.activeFields.move_id.related =\n            this.getCheckedField();\n        params.config.activeFields.line_ids.related.activeFields.reconciled_lines_excluding_exchange_diff_ids.related.activeFields.move_id.related =\n            this.getCheckedField();\n        params.config.activeFields.line_ids.related.activeFields.move_id.related =\n            this.getCheckedField();\n        params.config.activeFields.line_ids.related.activeFields.tax_ids.related = {\n            fields: {\n                id: { name: \"id\", type: \"int\" },\n                display_name: { name: \"display_name\", type: \"char\" },\n            },\n            activeFields: {\n                id: makeActiveField(),\n                display_name: makeActiveField(),\n            },\n        };\n        params.config.activeFields.line_ids.related.activeFields.partner_id.related = {\n            fields: {\n                id: { name: \"id\", type: \"int\" },\n                display_name: { name: \"display_name\", type: \"char\" },\n                property_account_receivable_id: {\n                    name: \"property_account_receivable_id\",\n                    type: \"many2one\",\n                },\n                property_account_payable_id: {\n                    name: \"property_account_payable_id\",\n                    type: \"many2one\",\n                },\n            },\n            activeFields: {\n                id: makeActiveField(),\n                display_name: makeActiveField(),\n                property_account_receivable_id: makeActiveField(),\n                property_account_payable_id: makeActiveField(),\n            },\n        };\n        params.config.activeFields.line_ids.related.activeFields.account_id.related = {\n            fields: {\n                id: { name: \"id\", type: \"int\" },\n                display_name: { name: \"display_name\", type: \"char\" },\n                account_type: { name: \"account_type\", type: \"char\" },\n            },\n            activeFields: {\n                id: makeActiveField(),\n                display_name: makeActiveField(),\n                account_type: makeActiveField(),\n            },\n        };\n        params.config.activeFields.journal_id = makeActiveField();\n        params.config.activeFields.journal_id.related = {\n            fields: {\n                id: { name: \"id\", type: \"int\" },\n                suspense_account_id: { name: \"suspense_account_id\", type: \"many2one\" },\n                default_account_id: { name: \"default_account_id\", type: \"many2one\" },\n                currency_id: { name: \"currency_id\", type: \"many2one\" },\n            },\n            activeFields: {\n                id: makeActiveField(),\n                suspense_account_id: makeActiveField(),\n                default_account_id: makeActiveField(),\n                currency_id: makeActiveField(),\n            },\n        };\n        params.config.activeFields.company_id = makeActiveField();\n        params.config.activeFields.company_id.related = {\n            fields: {\n                id: { name: \"id\", type: \"int\" },\n                currency_id: { name: \"currency_id\", type: \"many2one\" },\n            },\n            activeFields: {\n                id: makeActiveField(),\n                currency_id: makeActiveField(),\n            },\n        };\n        return params;\n    }\n}\n", "import { BankRecChatter } from \"./chatter/chatter\";\nimport { BankRecQuickCreate } from \"./quick_create/quick_create\";\nimport { BankRecKanbanController } from \"./kanban_controller\";\nimport { BankRecStatementLine } from \"./statement_line/statement_line\";\nimport { BankRecStatementSummary } from \"./statement_summary/statement_summary\";\nimport { browser } from \"@web/core/browser/browser\";\nimport { KanbanRenderer } from \"@web/views/kanban/kanban_renderer\";\nimport { kanbanView } from \"@web/views/kanban/kanban_view\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { formatMonetary } from \"@web/views/fields/formatters\";\nimport { useState, onWillStart, onWillDestroy } from \"@odoo/owl\";\nimport { registry } from \"@web/core/registry\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { useBankReconciliation } from \"./bank_reconciliation_service\";\n\nexport class BankRecKanbanRenderer extends KanbanRenderer {\n    static template = \"account_accountant.BankRecKanbanRenderer\";\n    static components = {\n        ...KanbanRenderer.components,\n        BankRecQuickCreate,\n        BankRecStatementSummary,\n        BankRecStatementLine,\n        BankRecChatter,\n    };\n\n    setup() {\n        super.setup();\n        this.action = useService(\"action\");\n        this.orm = useService(\"orm\");\n        this.ui = useService(\"ui\");\n        this.bankReconciliation = useBankReconciliation();\n        this.globalState = useState({\n            resModel: this.env.model.config.resModel,\n            context: this.env.model.config.context,\n            quickCreate: {\n                isVisible: false,\n                quickCreateView: this.props.archInfo.quickCreateView,\n            },\n            journalId:\n                this.env.model.config.context.default_journal_id ||\n                this.env.model.config.context.active_id,\n            totalJournalAmount: \"\",\n        });\n\n        this.env.model.hooks.onRootLoaded = async (newRoot) => {\n            await this.prepareInitialState(newRoot.records);\n        }\n\n        this.env.bus.addEventListener(\"createRecordQuickCreate\", () => {\n            this.globalState.quickCreate.isVisible = true;\n        });\n\n        onWillStart(async () => {\n            await this.prepareInitialState(this.env.model.root.records);\n        });\n\n        onWillDestroy(() => {\n            browser.sessionStorage.setItem(\n                \"isBankReconciliationWidgetChatterOpened\",\n                this.bankReconciliation.chatterState.visible\n            );\n            browser.sessionStorage.setItem(\n                \"bankReconciliationStatementLineId\",\n                this.bankReconciliation.chatterState.statementLine?.data.id\n            );\n            this.bankReconciliation.chatterState.statementLine = null;\n        });\n    }\n\n    /**\n     * Prepare the initial bank reconciliation widget info on load or when records changes\n     *\n     * @param {Array<Object>} records - Bank statement line records\n     * @returns {Promise<void>} Resolves when all computations are done\n     */\n    async prepareInitialState(records){\n        await Promise.all([\n            this.getJournalTotalAmount(),\n            this.bankReconciliation.computeReconcileLineCountPerPartnerId(records),\n            this.bankReconciliation.computeAvailableReconcileModels(records),\n        ]);\n        const statementLineId =\n            parseInt(browser.sessionStorage.getItem(\"bankReconciliationStatementLineId\")) ||\n            records[0]?.data.id;\n        const statementLine =\n            records.find((record) => record.data.id === statementLineId) ?? records[0];\n        this.bankReconciliation.selectStatementLine(statementLine);\n    }\n\n    /**\n        Override.\n    **/\n    cancelQuickCreate() {\n        this.globalState.quickCreate.isVisible = false;\n    }\n\n    /**\n        Override.\n    **/\n    async validateQuickCreate(recordId, mode) {\n        // When adding a record, some information needs to be recomputed\n        await this.bankReconciliation.updateAvailableReconcileModels(recordId);\n        await this.env.model.load();\n        await this.getJournalTotalAmount();\n        await this.bankReconciliation.computeReconcileLineCountPerPartnerId(\n            this.env.model.root.records\n        );\n\n        if (mode === \"add_close\") {\n            this.globalState.quickCreate.isVisible = false;\n        }\n    }\n\n    async getJournalTotalAmount() {\n        const value = await this.orm.call(\"account.journal\", \"get_total_journal_amount\", [\n            this.globalState.journalId,\n        ]);\n        this.globalState.totalJournalAmount = value.balance_amount;\n        this.globalState.totalIsInvalid = value.has_invalid_statements;\n        return value;\n    }\n\n    // -----------------------------------------------------------------------------\n    // ACTION\n    // -----------------------------------------------------------------------------\n    async actionOpenBankGL() {\n        const actionData = await this.orm.call(\n            \"account.journal\",\n            \"action_open_bank_balance_in_gl\",\n            [\n                this.env.model.config.context.default_journal_id ||\n                    this.env.model.config.context.active_id,\n            ],\n        );\n        this.action.doAction(actionData);\n    }\n\n    actionOpenStatement(statementId) {\n        const action = {\n            type: \"ir.actions.act_window\",\n            res_model: \"account.bank.statement\",\n            res_id: statementId,\n            views: [[false, \"form\"]],\n            target: \"current\",\n            context: {\n                form_view_ref: \"account_accountant.view_bank_statement_form_bank_rec_widget\",\n            },\n        };\n\n        this.action.doAction(action);\n    }\n\n    // -----------------------------------------------------------------------------\n    // GETTER\n    // -----------------------------------------------------------------------------\n\n    get quickCreateContext() {\n        return {\n            ...this.globalState.context,\n        };\n    }\n\n    // TODO: remove in master\n    get hideCurrentBalance() {\n        return false;\n    }\n\n    get hasStatementLine() {\n        return this.env.model.root.count;\n    }\n\n    get totalJournalLabel() {\n        return _t(\"Current Balance\");\n    }\n\n    /**\n    Prepares a list of statements based on the statement_id of the bank statement line records.\n    Statements are only displayed above the first line of the statement (all lines might not be visible in the kanban)\n    **/\n    get statementGroups() {\n        const statementGroups = {};\n        let lastStatementId = null;\n        for (const record of this.env.model.root.records) {\n            const statementId = record.data.statement_id?.id;\n            if (statementId && statementId !== lastStatementId) {\n                // Add the statement group information to the statementGroups object\n                statementGroups[record.data.id] = {\n                    statementId: statementId,\n                    name: record.data.statement_name,\n                    balance: formatMonetary(record.data.statement_balance_end_real, {\n                        currencyId: record.data.currency_id.id,\n                    }),\n                    isValid: record.data.statement_complete && record.data.statement_valid,\n                };\n                lastStatementId = statementId;\n            } else {\n                if (\n                    Object.keys(statementGroups).length &&\n                    !statementId &&\n                    typeof lastStatementId !== \"string\"\n                ) {\n                    statementGroups[record.data.id] = {\n                        name: _t(\"No Bank Statement\"),\n                        isValid: true,\n                    };\n                    lastStatementId = \"no_bank_statement\";\n                }\n            }\n        }\n        return statementGroups;\n    }\n\n    get isQuickCreateVisible() {\n        return this.globalState.quickCreate.isVisible;\n    }\n}\n\nexport const BankRecKanbanView = {\n    ...kanbanView,\n    Controller: BankRecKanbanController,\n    Renderer: BankRecKanbanRenderer,\n    searchMenuTypes: [\"filter\", \"favorite\"],\n};\n\nregistry.category(\"views\").add(\"bank_rec_widget_kanban\", BankRecKanbanView);\n", "import { Component } from \"@odoo/owl\";\nimport { formatMonetary } from \"@web/views/fields/formatters\";\nimport { useService } from \"@web/core/utils/hooks\";\n\nexport class BankRecLineInfoPopOver extends Component {\n    static template = \"account_accountant.BankRecLineInfoPopOver\";\n    static props = {\n        lineData: { type: Object, optional: true },\n        statementLineData: { type: Object, optional: true },\n        exchangeMove: { type: Object, optional: true },\n        isPartiallyReconciled: { type: Boolean, optional: true },\n        close: { type: Function, optional: true },\n    };\n\n    setup() {\n        this.action = useService(\"action\");\n    }\n\n    openExchangeMove() {\n        this.action.doAction({\n            type: \"ir.actions.act_window\",\n            res_model: \"account.move\",\n            res_id: this.props.exchangeMove.id,\n            views: [[false, \"form\"]],\n            target: \"current\",\n        });\n    }\n\n    openReconciledMove() {\n        this.action.doAction({\n            type: \"ir.actions.act_window\",\n            res_model: \"account.move\",\n            res_id: this.reconciledLineData.move_id.id,\n            views: [[false, \"form\"]],\n            target: \"current\",\n        });\n    }\n\n    get reconciledMoveName() {\n        return this.reconciledLineData.move_name;\n    }\n\n    get formattedReconciledMoveAmountCurrency() {\n        return formatMonetary(this.reconciledLineData.amount_currency, {\n            currencyId: this.reconciledLineData.currency_id.id,\n        });\n    }\n\n    get reconciledLineData() {\n        return this.props.lineData.reconciled_lines_ids.records[0].data;\n    }\n\n    get formattedLineDataAmountCurrency() {\n        return formatMonetary(this.props.lineData.amount_currency, {\n            currencyId: this.props.lineData.currency_id.id,\n        });\n    }\n\n    get exchangeDiffMoveName() {\n        return this.props.exchangeMove.display_name;\n    }\n\n    get exchangeMoveBalance() {\n        return this.props.exchangeMove.line_ids[0].balance;\n    }\n\n    get formattedExchangeMoveBalance() {\n        return formatMonetary(this.exchangeMoveBalance, {\n            currencyId: this.props.statementLineData.company_id.currency_id?.id,\n        });\n    }\n}\n", "import { Component, useRef } from \"@odoo/owl\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { formatMonetary } from \"@web/views/fields/formatters\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { useBankReconciliation } from \"../bank_reconciliation_service\";\nimport { usePopover } from \"@web/core/popover/popover_hook\";\nimport { BankRecFormDialog } from \"../bankrec_form_dialog/bankrec_form_dialog\";\nimport { BankRecLineInfoPopOver } from \"../line_info_pop_over/line_info_pop_over\";\nimport { x2ManyCommands } from \"@web/core/orm_service\";\n\nexport class BankRecLineToReconcile extends Component {\n    static template = \"account_accountant.BankRecLineToReconcile\";\n\n    static props = {\n        line: Object,\n        statementLine: Object,\n    };\n\n    setup() {\n        this.action = useService(\"action\");\n        this.orm = useService(\"orm\");\n        this.dialogService = useService(\"dialog\");\n        this.ui = useService(\"ui\");\n        this.bankReconciliation = useBankReconciliation();\n\n        this.lineInfoRef = useRef(\"line-info-ref\");\n        this.lineInfoPopOver = usePopover(BankRecLineInfoPopOver, {\n            position: \"left\",\n            closeOnClickAway: true,\n        });\n    }\n\n    onClickLine() {\n        if (this.ui.isSmall) {\n            this.toggleEditLine();\n        }\n    }\n\n    /**\n     * Opens a dialog to edit a bank statement line and saves any changes.\n     *\n     * This method:\n     * - Opens a dialog (`FormViewDialog`) to allow the user to edit the current `account.move.line`.\n     * - On saving, the dialog triggers the `onRecordSave` callback, which:\n     *   - Calls `edit_bank_statement_line` on the ORM to update the bank statement line.\n     *   - Reloads the statement line data.\n     *   - Updates the chatter on the related journal entry.\n     */\n    toggleEditLine() {\n        this.dialogService.add(BankRecFormDialog, {\n            title: _t(\"Edit Line\"),\n            resModel: \"account.move.line\",\n            resId: this.lineData.id,\n            context: {\n                form_view_ref: \"account_accountant.view_bank_rec_edit_line\",\n                is_reviewed: this.lineData.move_id.checked,\n            },\n            onRecordSave: async (record) => {\n                await this.orm.call(\"account.bank.statement.line\", \"edit_reconcile_line\", [\n                    this.statementLineData.id,\n                    this.lineData.id,\n                    await record.getChanges(),\n                ]);\n                this.props.statementLine.load();\n                this.bankReconciliation.reloadChatter();\n                return true;\n            },\n        });\n    }\n\n    /**\n     * Deletes a line to reconcile.\n     *\n     * This method:\n     * - Calls `delete_reconciled_line` on the ORM to delete the line.\n     * - Reloads the statement line data after deletion.\n     * - Updates the chatter on the related journal entry.\n     */\n    async deleteLine() {\n        await this.orm.call(\"account.bank.statement.line\", \"delete_reconciled_line\", [\n            this.statementLineData.id,\n            this.lineData.id,\n        ]);\n        if (this.lineData.reconciled_lines_ids.records.length) {\n            // Only update the line count per partner if we delete\n            // a line which is reconciled to another move line\n            // We don't use await here as it could be reloaded asynchronously.\n            this.bankReconciliation.computeReconcileLineCountPerPartnerId(\n                this.env.model.root.records\n            );\n        }\n        this.props.statementLine.load();\n        this.bankReconciliation.reloadChatter();\n    }\n\n    // -----------------------------------------------------------------------------\n    // ACTION\n    // -----------------------------------------------------------------------------\n    openMove() {\n        this.action.doAction({\n            type: \"ir.actions.act_window\",\n            res_model: \"account.move\",\n            res_id: this.moveData.id,\n            views: [[false, \"form\"]],\n            target: \"current\",\n        });\n    }\n\n    openPartner() {\n        this.action.doAction({\n            type: \"ir.actions.act_window\",\n            res_model: \"res.partner\",\n            res_id: this.lineData.partner_id.id,\n            views: [[false, \"form\"]],\n            target: \"current\",\n        });\n    }\n\n    openLineInfoPopOver() {\n        if (this.lineInfoPopOver.isOpen || !this.showLineInfo) {\n            this.lineInfoPopOver.close();\n        } else {\n            this.lineInfoPopOver.open(this.lineInfoRef.el, {\n                statementLineData: this.statementLineData,\n                lineData: this.lineData,\n                exchangeMove: this.exchangeMove,\n                isPartiallyReconciled: this.isPartiallyReconciled,\n            });\n        }\n    }\n\n    async deleteTax(taxIndex) {\n        const taxChanged = this.lineDataTaxIds[taxIndex];\n        await this.orm.call(\"account.bank.statement.line\", \"edit_reconcile_line\", [\n            this.statementLineData.id,\n            this.lineData.id,\n            { tax_ids: [[x2ManyCommands.UNLINK, taxChanged.data.id]] },\n        ]);\n        this.props.statementLine.load();\n        this.bankReconciliation.reloadChatter();\n    }\n\n    // -----------------------------------------------------------------------------\n    // GETTER\n    // -----------------------------------------------------------------------------\n    get statementLineData() {\n        return this.props.statementLine.data;\n    }\n\n    get lineData() {\n        return this.props.line;\n    }\n\n    get reconciledLineId() {\n        return this.lineData.reconciled_lines_ids.records.length === 1\n            ? this.lineData.reconciled_lines_ids.records[0].data\n            : null;\n    }\n\n    get reconciledLineExcludingExchangeDiffId() {\n        return this.lineData.reconciled_lines_excluding_exchange_diff_ids.records.length === 1\n            ? this.lineData.reconciled_lines_excluding_exchange_diff_ids.records[0].data\n            : null;\n    }\n\n    get moveData() {\n        return (\n            this.reconciledLineId?.move_id ||\n            this.reconciledLineExcludingExchangeDiffId?.move_id ||\n            this.lineData.move_id\n        );\n    }\n\n    get isPartiallyReconciled() {\n        if (!this.reconciledLineId) {\n            return false;\n        }\n        return !this.reconciledLineId.full_reconcile_id?.id;\n    }\n\n    get hasDifferentCurrencies() {\n        return this.lineData.currency_id.id !== this.statementLineData.currency_id.id;\n    }\n\n    get formattedAmountCurrencyOfLine() {\n        return formatMonetary(this.lineData.amount_currency, {\n            currencyId: this.lineData.currency_id.id,\n        });\n    }\n\n    get formattedAmountCurrencyOfStatementLine() {\n        return formatMonetary(this.lineData.amount_currency, {\n            currencyId: this.statementLineData.currency_id.id,\n        });\n    }\n\n    get exchangeMove() {\n        return (\n            this.lineData.matched_debit_ids.records[0]?.data.exchange_move_id ||\n            this.lineData.matched_credit_ids.records[0]?.data.exchange_move_id\n        );\n    }\n\n    get showLineInfo() {\n        return this.isPartiallyReconciled || this.exchangeMove?.id;\n    }\n\n    get isTaxLine() {\n        return this.lineData.tax_line_id;\n    }\n\n    get lineDataTaxIds() {\n        return this.lineData.tax_ids.records;\n    }\n}\n", "import {\n    AttachmentPreviewListController,\n    AttachmentPreviewListRenderer,\n} from \"../../attachment_preview_list_view/attachment_preview_list_view\";\nimport { registry } from \"@web/core/registry\";\nimport { listView } from \"@web/views/list/list_view\";\nimport { useChildSubEnv } from \"@odoo/owl\";\nimport { makeActiveField } from \"@web/model/relational_model/utils\";\n\nexport class BankRecListController extends AttachmentPreviewListController {\n    setup() {\n        super.setup(...arguments);\n\n        this.skipKanbanRestore = {};\n\n        useChildSubEnv({\n            skipKanbanRestoreNeeded: (stLineId) => this.skipKanbanRestore[stLineId],\n        });\n    }\n\n    /**\n     * Override\n     * Don't allow bank_rec_form to be restored with previous values since the statement line has changed.\n     */\n    async onRecordSaved(record) {\n        this.skipKanbanRestore[record.resId] = true;\n        return super.onRecordSaved(...arguments);\n    }\n\n    get previewerStorageKey() {\n        return \"account.statement_line_pdf_previewer_hidden\";\n    }\n\n    get modelParams() {\n        const params = super.modelParams;\n        params.config.activeFields.bank_statement_attachment_ids = makeActiveField();\n        params.config.activeFields.bank_statement_attachment_ids.related = {\n            fields: {\n                mimetype: { name: \"mimetype\", type: \"char\" },\n            },\n            activeFields: {\n                mimetype: makeActiveField(),\n            },\n        };\n        params.config.activeFields.attachment_ids = makeActiveField();\n        params.config.activeFields.attachment_ids.related = {\n            fields: {\n                mimetype: { name: \"mimetype\", type: \"char\" },\n            },\n            activeFields: {\n                mimetype: makeActiveField(),\n            },\n        };\n        return params;\n    }\n\n    async setSelectedRecord(accountBankStatementLineData) {\n        this.attachmentPreviewState.selectedRecord = accountBankStatementLineData;\n        if (accountBankStatementLineData.data?.attachment_ids.count) {\n            await this.setThread(accountBankStatementLineData, \"attachment_ids\", \"move_id\");\n        } else {\n            await this.setThread(\n                accountBankStatementLineData,\n                \"bank_statement_attachment_ids\",\n                \"statement_id\"\n            );\n        }\n    }\n}\n\nexport class BankRecListRenderer extends AttachmentPreviewListRenderer {}\n\nexport const bankRecListView = {\n    ...listView,\n    Controller: BankRecListController,\n    Renderer: BankRecListRenderer,\n};\n\nregistry.category(\"views\").add(\"bank_rec_list\", bankRecListView);\n", "import { Component } from \"@odoo/owl\";\nimport { registry } from \"@web/core/registry\";\nimport { computeM2OProps, Many2One } from \"@web/views/fields/many2one/many2one\";\nimport { buildM2OFieldDescription, Many2OneField } from \"@web/views/fields/many2one/many2one_field\";\n\nexport class BankRecMany2OneMultiID extends Component {\n    static template = \"account_accountant.BankRecMany2OneMultiID\";\n    static components = { Many2One };\n    static props = { ...Many2OneField.props };\n\n    get m2oProps() {\n        const props = computeM2OProps(this.props);\n        if (this.props.record.selected && this.props.record.model.multiEdit) {\n            props.context.active_ids = this.env.model.root.selection.map((r) => r.resId);\n        }\n        return props;\n    }\n}\n\nregistry.category(\"fields\").add(\"bank_rec_list_many2one_multi_id\", {\n    ...buildM2OFieldDescription(BankRecMany2OneMultiID),\n});\n", "import {\n    KanbanRecordQuickCreate,\n    KanbanQuickCreateController,\n} from \"@web/views/kanban/kanban_record_quick_create\";\n\nexport class BankRecQuickCreateController extends KanbanQuickCreateController {\n    static template = \"account_accountant.BankRecQuickCreateController\";\n}\n\nexport class BankRecQuickCreate extends KanbanRecordQuickCreate {\n    static template = \"account_accountant.BankRecQuickCreate\";\n    static props = {\n        ...KanbanRecordQuickCreate.props,\n        resModel: { type: String },\n        context: { type: Object },\n        group: { type: Object, optional: true },\n    };\n    static components = { BankRecQuickCreateController };\n\n    /**\n    Overriden.\n    **/\n    async getQuickCreateProps(props) {\n        await super.getQuickCreateProps({\n            ...props,\n            group: {\n                resModel: props.resModel,\n                context: props.context,\n            },\n        });\n    }\n}\n", "import { Component } from \"@odoo/owl\";\nimport { useBankReconciliation } from \"../bank_reconciliation_service\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { x2ManyCommands } from \"@web/core/orm_service\";\n\nexport class BankRecReconciledLineName extends Component {\n    static template = \"account_accountant.BankRecReconciledLineName\";\n    static props = {\n        statementLine: { type: Object },\n        linesToReconcile: { type: Object },\n        moveLineId: { type: String },\n        valueToDisplay: { type: Object },\n    };\n\n    setup() {\n        this.orm = useService(\"orm\");\n        this.bankReconciliation = useBankReconciliation();\n    }\n\n    async deleteTax(lineId, taxChanged) {\n        const lineData = this.props.linesToReconcile.filter((line) => {\n            return line.id === parseInt(lineId);\n        })[0];\n        await this.orm.call(\"account.bank.statement.line\", \"edit_reconcile_line\", [\n            this.props.statementLine.data.id,\n            lineData.id,\n            { tax_ids: [[x2ManyCommands.UNLINK, taxChanged.data.id]] },\n        ]);\n        this.props.statementLine.load();\n        this.bankReconciliation.reloadChatter();\n    }\n}\n", "import { SelectCreateDialog } from \"@web/views/view_dialogs/select_create_dialog\";\nimport { formatMonetary } from \"@web/views/fields/formatters\";\nimport { useService } from \"@web/core/utils/hooks\";\n\nconst { DateTime } = luxon;\n\nexport class BankRecSelectCreateDialog extends SelectCreateDialog {\n    static template = \"account_accountant.BankRecSelectCreateDialog\";\n    static props = {\n        ...SelectCreateDialog.props,\n        suspenseAccountLine: Object,\n        reference: String,\n        date: DateTime,\n        size: { type: String, optional: true },\n    };\n\n    static defaultProps = {\n        ...SelectCreateDialog.defaultProps,\n        size: \"lg\",\n    };\n\n    setup() {\n        super.setup();\n        this.orm = useService(\"orm\");\n        this.ui = useService(\"ui\");\n        this.state.remainingAmount = this.suspenseAccountLine.amount_currency;\n        this.state.hideRemainingAmount = false;\n\n        this.baseViewProps.onSelectionChanged = (resIds, selectedLines) => {\n            this.state.resIds = resIds;\n            this.changeInSelectedMoveLine(selectedLines);\n        };\n    }\n\n    async changeInSelectedMoveLine(selectedLines) {\n        if (!selectedLines?.length) {\n            this.state.remainingAmount = this.suspenseAccountLine.amount_currency;\n            return;\n        }\n\n        let selectedLinesSum = 0;\n        this.state.hideRemainingAmount = false;\n        // When the suspense currency is different from the company one, we cannot compute the remaining amount correctly\n        // due to the currency rates. So in this case, when the user select multiple currencies we add the remaining amount\n        if (\n            this.suspenseAccountLine.currency_id.id !==\n            this.suspenseAccountLine.company_currency_id.id\n        ) {\n            const selectedLineCurrencies = selectedLines.map((line) => line.currency_id);\n\n            if (\n                selectedLineCurrencies.length !== 1 ||\n                (selectedLineCurrencies.length === 1 &&\n                    selectedLineCurrencies[0] !== this.suspenseAccountLine.currency_id.id)\n            ) {\n                this.state.hideRemainingAmount = true;\n                return;\n            } else {\n                selectedLinesSum = selectedLines.reduce((sum, line) => {\n                    return sum + line.amount_residual_currency;\n                }, 0);\n            }\n        } else {\n            selectedLinesSum = selectedLines.reduce((sum, line) => {\n                return sum + line.amount_residual;\n            }, 0);\n        }\n        this.state.remainingAmount = this.suspenseAccountLine.amount_currency + selectedLinesSum;\n    }\n\n    get suspenseAccountLine() {\n        return this.props?.suspenseAccountLine;\n    }\n\n    get remainingAmountFormatted() {\n        return formatMonetary(this.state.remainingAmount, {\n            currencyId: this.suspenseAccountLine.currency_id.id,\n        });\n    }\n\n    get formattedStatementLineDate() {\n        return this.props.date?.toLocaleString();\n    }\n}\n", "import { ListController } from \"@web/views/list/list_controller\";\nimport { ListRenderer } from \"@web/views/list/list_renderer\";\nimport { listView } from \"@web/views/list/list_view\";\nimport { registry } from \"@web/core/registry\";\nimport { useService } from \"@web/core/utils/hooks\";\n\nexport class BankRecReconcileDialogListController extends ListController {\n    setup() {\n        super.setup();\n        this.orm = useService(\"orm\");\n    }\n\n    async onSelectionChanged() {\n        const resIds = await this.model.root.getResIds(true);\n        if (!resIds.length) {\n            this.props.onSelectionChanged(resIds, []);\n        }\n\n        let selectedLines;\n        // When being in the list view with more element than the limit and doing a select all, the user has the\n        // possibility to select more element than the limit. In this case the isDomainSelected is True\n        if (this.isDomainSelected) {\n            const { resModel, context } = this.model.root._config;\n            selectedLines = await this.orm.read(\n                resModel,\n                resIds,\n                [\"amount_residual\", \"amount_residual_currency\", \"currency_id\"],\n                { context }\n            );\n        } else {\n            selectedLines = Object.values(this.model.root.records)\n                .filter((record) => resIds.includes(record._config.resId))\n                .map((record) => {\n                    const data = record.data;\n                    return {\n                        amount_residual: data.amount_residual,\n                        amount_residual_currency: data.amount_residual_currency,\n                        currency_id: data.currency_id.id,\n                    };\n                });\n        }\n        this.props.onSelectionChanged(resIds, selectedLines);\n    }\n}\n\nexport class BankRecReconcileDialogListRenderer extends ListRenderer {\n    static template = \"account_accountant.BankRecReconcileDialogListRenderer\";\n    static recordRowTemplate = \"account_accountant.BankRecReconcileDialogListRenderer.RecordRow\";\n\n    async openMoveView(record) {\n        this.env.services.action.doAction({\n            type: \"ir.actions.act_window\",\n            res_model: \"account.move\",\n            res_id: record.data.move_id.id,\n            views: [[false, \"form\"]],\n            target: \"current\",\n        });\n    }\n}\n\nexport const bankRecReconcileDialogListRenderer = {\n    ...listView,\n    Renderer: BankRecReconcileDialogListRenderer,\n    Controller: BankRecReconcileDialogListController,\n};\n\nregistry.category(\"views\").add(\"bank_rec_dialog_list\", bankRecReconcileDialogListRenderer);\n", "import { BankRecButtonList } from \"../button_list/button_list\";\nimport { BankRecLineToReconcile } from \"../line_to_reconcile/line_to_reconcile\";\nimport { BankRecReconciledLineName } from \"../reconciled_line_name/reconciled_line_name\";\nimport { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\nimport { formatMonetary } from \"@web/views/fields/formatters\";\nimport { KanbanRecord } from \"@web/views/kanban/kanban_record\";\nimport { user } from \"@web/core/user\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { onWillStart, useState, useRef } from \"@odoo/owl\";\nimport { useBankReconciliation } from \"../bank_reconciliation_service\";\n\nexport class BankRecStatementLine extends KanbanRecord {\n    static template = \"account_accountant.BankRecStatementLine\";\n    static components = {\n        BankRecLineToReconcile,\n        BankRecButtonList,\n        DropdownItem,\n        BankRecReconciledLineName,\n    };\n    static props = [...KanbanRecord.props];\n\n    setup() {\n        super.setup();\n        this.orm = useService(\"orm\");\n        this.ui = useService(\"ui\");\n        this.bankReconciliation = useBankReconciliation();\n        this.state = useState({\n            isUnfolded: false,\n        });\n        this.statementLineRootRef = useRef(\"root\");\n        if (this.env.model.config.context?.default_st_line_id === this.props.record.resId) {\n            this.state.isUnfolded = true;\n            this.bankReconciliation.selectStatementLine(this.props.record);\n        }\n        onWillStart(async () => {\n            this.userCanReview = await user.hasGroup(\"account.group_account_user\");\n        });\n    }\n\n    getRecordClasses() {\n        let classes = super.getRecordClasses();\n        if (this.hasStatementLine === 1) {\n            classes += \" mt-3\";\n        }\n        return classes;\n    }\n\n    // -----------------------------------------------------------------------------\n    // ACTION\n    // -----------------------------------------------------------------------------\n\n    openStatementCreate() {\n        this.action.doAction(\"account_accountant.action_bank_statement_form_bank_rec_widget\", {\n            additionalContext: {\n                split_line_id: this.recordData.id,\n                default_journal_id: this.recordData.journal_id.id,\n            },\n            onClose: async () => {\n                this.env.model.load();\n            },\n        });\n    }\n\n    openPartner() {\n        this.action.doAction({\n            type: \"ir.actions.act_window\",\n            res_model: \"res.partner\",\n            res_id: this.partner.id,\n            views: [[false, \"form\"]],\n            target: \"current\",\n        });\n    }\n\n    async removePartner() {\n        await this.orm.write(\"account.bank.statement.line\", [this.recordData.id], {\n            partner_id: false,\n        });\n        this.record.load();\n    }\n\n    // -----------------------------------------------------------------------------\n    // HELPER\n    // -----------------------------------------------------------------------------\n    get reconciledLineName() {\n        const reconciledLine = {};\n        for (const line of this.linesToReconcile) {\n            if (\n                line.reconciled_lines_excluding_exchange_diff_ids.records.length === 1 &&\n                line.reconciled_lines_excluding_exchange_diff_ids.records[0].data.move_name\n            ) {\n                reconciledLine[line.id] = {\n                    move: line.reconciled_lines_excluding_exchange_diff_ids.records[0].data\n                        .move_name,\n                };\n            } else if (line.tax_ids.count) {\n                reconciledLine[line.id] = { tax: line.tax_ids.records };\n            } else {\n                reconciledLine[line.id] = { account: line.account_id.display_name };\n            }\n        }\n        return reconciledLine;\n    }\n\n    get record() {\n        return this.props.record;\n    }\n\n    get recordData() {\n        return this.props.record.data;\n    }\n\n    fold() {\n        if (this.state.isUnfolded) {\n            this.toggleUnfold();\n        }\n        this.selectStatementLine();\n    }\n\n    unfold() {\n        if (!this.state.isUnfolded) {\n            this.toggleUnfold();\n        }\n        this.selectStatementLine();\n    }\n\n    toggleUnfold() {\n        this.state.isUnfolded = !this.isUnfolded;\n        this.selectStatementLine();\n    }\n\n    selectStatementLine() {\n        // Update the chatter with the last selected element\n        this.bankReconciliation.selectStatementLine(this.record);\n    }\n\n    openChatter() {\n        this.selectStatementLine();\n        this.bankReconciliation.openChatter();\n    }\n\n    get hasInvalidAnalytics() {\n        return this.linesToReconcile.some((line) => line.has_invalid_analytics);\n    }\n\n    get isUnfolded() {\n        return this.state.isUnfolded;\n    }\n\n    get hasStatementLine() {\n        return this.env.model.root.count;\n    }\n\n    get formattedAmount() {\n        return formatMonetary(this.recordData.amount, {\n            currencyId: this.recordData.currency_id.id,\n        });\n    }\n\n    get formattedDate() {\n        return this.recordData.date.toLocaleString({\n            month: \"short\",\n            day: \"2-digit\",\n        });\n    }\n\n    get formattedFullDate() {\n        return this.recordData.date.toLocaleString({\n            month: \"long\",\n            day: \"numeric\",\n            year: \"numeric\",\n        });\n    }\n\n    get partner() {\n        return this.recordData.partner_id;\n    }\n\n    get linesToReconcile() {\n        return this.accountMoveLines.filter((line) => {\n            return (\n                line.account_id.id !== this.recordData.journal_id?.suspense_account_id.id &&\n                line.account_id.id !== this.recordData.journal_id?.default_account_id.id\n            );\n        });\n    }\n\n    get suspenseAccountLine() {\n        return this.accountMoveLines.filter((line) => {\n            return line.account_id.id === this.recordData.journal_id.suspense_account_id.id;\n        })?.[0];\n    }\n\n    get accountMoveLines() {\n        return [...this.recordData.line_ids.records.map((line) => line.data)];\n    }\n\n    get hasForeignCurrencyAndSameCurrencyForAllLines() {\n        return (\n            this.recordData.foreign_currency_id &&\n            this.linesToReconcile &&\n            this.linesToReconcile.filter((line) => {\n                return line.currency_id.id !== this.recordData.foreign_currency_id.id;\n            }).length === 0\n        );\n    }\n\n    get suspenseAccountLineFormattedAmount() {\n        return formatMonetary(this.suspenseAccountLine.amount_currency, {\n            currencyId: this.suspenseAccountLine?.currency_id.id,\n        });\n    }\n\n    get activityNumber() {\n        return this.recordData.activity_ids.count;\n    }\n\n    /**\n     * Checks if there is at least one attachment associated with the bank statement line or its related records.\n     *\n     * This getter aggregates attachment counts from:\n     * - The bank statement line record itself or the related move since attachment_ids is a related to move_id.attachment_ids.\n     * - The related move lines themselves, if they have attachments directly associated (`line.move_attachment_ids`)\n     *   except the attachments link to the statement.\n     * - The lines reconciled with the related move lines, specifically checking for attachments on the\n     *   move associated with those reconciled lines.\n     *\n     * This check ensures that all attachments from associated invoices, bills, and other related documents are considered.\n     *\n     * @returns {number} The total number of attachments found. A return value greater than 0 indicates the presence of attachments.\n     */\n    get hasAttachment() {\n        const statementAttachment = this.recordData.bank_statement_attachment_ids.records.map(\n            (attachment) => attachment.data.id\n        );\n\n        return (\n            this.recordData.attachment_ids.records.length +\n            this.linesToReconcile\n                .flatMap((line) => line.reconciled_lines_ids.records)\n                .filter((line) => line.data.move_attachment_ids?.count)\n                .reduce(\n                    (accumulator, line) =>\n                        parseInt(accumulator) + parseInt(line.data.move_attachment_ids.count),\n                    0\n                ) +\n            this.linesToReconcile\n                .filter(\n                    (line) =>\n                        line.move_attachment_ids?.count &&\n                        !line.move_attachment_ids.records\n                            .map((attachment) => attachment.data.id)\n                            .every((id) => statementAttachment.includes(id))\n                )\n                .reduce(\n                    (accumulator, line) =>\n                        parseInt(accumulator) + parseInt(line.move_attachment_ids.count),\n                    0\n                )\n        );\n    }\n\n    get amountClasses() {\n        const classes = this.recordData.foreign_currency_id ? \"w-50\" : \"w-100\";\n        if (this.recordData.amount > 0) {\n            return `${classes} fw-bold`;\n        }\n        if (this.recordData.amount < 0) {\n            return `${classes} text-danger fw-bold`;\n        }\n        return `${classes} text-secondary`;\n    }\n\n    get buttonListProps() {\n        return {\n            statementLineRootRef: this.statementLineRootRef,\n            statementLine: this.record,\n            reconcileLineCount:\n                this.bankReconciliation.reconcileCountPerPartnerId[this.recordData.partner_id.id] ??\n                null,\n            reconcileModels:\n                this.bankReconciliation.reconcileModelPerStatementLineId[this.recordData.id] ?? [],\n            preSelectedReconciliationModel: this.accountMoveLines\n                .filter((line) => line.reconcile_model_id.id)\n                .map((line) => line.reconcile_model_id)?.[0],\n        };\n    }\n\n    get formattedAmountCurrencyInForeign() {\n        return formatMonetary(this.recordData.amount_currency, {\n            currencyId: this.recordData.foreign_currency_id.id,\n        });\n    }\n\n    get isSelected() {\n        return this.recordData.move_id.id === this.bankReconciliation.statementLineMoveId;\n    }\n\n    get isChatterOpen() {\n        return this.bankReconciliation.chatterState.visible;\n    }\n}\n", "import { Component } from \"@odoo/owl\";\n\nexport class BankRecStatementSummary extends Component {\n    static template = \"account_accountant.BankRecStatementSummary\";\n\n    static props = {\n        label: { type: String },\n        amount: { type: String, optional: true },\n        action: { type: Function },\n        journalId: { type: Number, optional: true },\n        isValid: { type: Boolean, optional: true },\n        journalIsInvalid: { type: Boolean, optional: true },\n    };\n    static defaultProps = {\n        isValid: true,\n    };\n\n    actionApplyInvalidStatement() {\n        const facets = this.env.searchModel.facets;\n        const searchItems = this.env.searchModel.searchItems;\n        const invalidStatementFilter = Object.values(searchItems).find(\n            (i) => i.name == \"invalid_statement\"\n        );\n        const invalidStatementFacet = facets.filter(\n            (i) => i.groupId == invalidStatementFilter.groupId\n        );\n        if (\n            invalidStatementFacet.length == 0 || \n            !invalidStatementFacet[0].values.includes(invalidStatementFilter.description)\n        ){\n            this.env.searchModel.toggleSearchItem(invalidStatementFilter.id);\n        }\n    }\n}\n", "/** @odoo-module **/\n\nimport { patch } from \"@web/core/utils/patch\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { ExportDataDialog } from \"@web/views/view_dialogs/export_data_dialog\";\n\npatch(ExportDataDialog.prototype, {\n    async fetchFields(value) {\n        await super.fetchFields(value);\n\n        const analyticLineIdsField = this.knownFields['analytic_line_ids'];\n        if (analyticLineIdsField) {\n            // If analytic_distribution field is here, we remove it to replace it by the new fields\n            this.state.exportList = this.state.exportList.filter(\n                (field) => field.id !== 'analytic_distribution'\n            );\n            const analyticLineFields = await rpc(\"/web/export/get_fields\", {\n                model: analyticLineIdsField.params.model,\n                prefix: analyticLineIdsField.params.prefix,\n                parent_name: analyticLineIdsField.params.parent_field.string,\n                import_compat: analyticLineIdsField.default_export,\n                parent_field_type: analyticLineIdsField.params.parent_field.type,\n                domain:[],\n            });\n            // We exclude auto_account_id as it's a magic field who doesn't need to be exported\n            const filteredAnalyticLineFields = analyticLineFields.filter(\n                (field) => field.params?.model === 'account.analytic.account'\n                    && !field.id.includes('auto_account_id')\n                    || field.id === 'analytic_line_ids/amount'\n            );\n\n            this.state.exportList.push(...filteredAnalyticLineFields);\n        }\n    },\n});\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { Component, onWillStart } from \"@odoo/owl\";\nimport { registry } from \"@web/core/registry\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { standardActionServiceProps } from \"@web/webclient/actions/action_service\";\nimport { user } from \"@web/core/user\";\n\nclass JournalCreateWizardCard extends Component {\n    static template = \"account.JournalCreateWizardCard\";\n    static props = [\"images\", \"title\", \"text\"];\n}\n\nexport class JournalCreateWizard extends Component {\n    static template = \"account.JournalCreateWizard\";\n    static props = { ...standardActionServiceProps };\n    static components = { JournalCreateWizardCard };\n\n    setup() {\n        super.setup();\n        this.orm = useService(\"orm\");\n        this.action = useService(\"action\");\n\n        onWillStart(async () => {\n            this.hasGroupAccountUser = await user.hasGroup(\"account.group_account_user\");\n        });\n    }\n\n    async openAccountWizard(type) {\n        const addBankAction = await this.orm.call(\n            \"res.company\",\n            `setting_init_${type}_account_action`,\n            user.activeCompany.ids\n        );\n        this.action.doAction(addBankAction);\n        this.env.dialogData.close();\n    }\n\n    openCreateJournalForm(type) {\n        this.action.doAction({\n            type: \"ir.actions.act_window\",\n            res_model: \"account.journal\",\n            views: [[false, \"form\"]],\n            target: \"current\",\n            context: { default_type: type },\n        });\n    }\n\n    cardImages(cardType) {\n        const result = cardType === \"card\" ? [\"logo_visa\", \"logo_mastercard\"] : [cardType];\n        return result.map((imageName) => `/account_accountant/static/src/img/journal_create_wizard/${imageName}.svg`);\n    }\n\n    get cardsData() {\n        const data = [\n            {\n                images: this.cardImages(\"bank\"),\n                title: _t(\"Bank\"),\n                text: _t(\"Connect your bank and payment gateways (Paypal, Stripe, ...) or record your transactions manually\"),\n                onClick: () => this.openAccountWizard(\"bank\"),\n            },\n            {\n                images: this.cardImages(\"card\"),\n                title: _t(\"Card\"),\n                text: _t(\"Connect your credit card accounts and manage your payouts\"),\n                onClick: () => this.openAccountWizard(\"credit_card\"),\n            },\n            {\n                images: this.cardImages(\"cash\"),\n                title: _t(\"Cash\"),\n                text: _t(\"Record your cash movements and transfers\"),\n                onClick: () => this.openCreateJournalForm(\"cash\"),\n            },\n        ];\n\n        if (this.hasGroupAccountUser) {\n            data.push(\n                {\n                    images: this.cardImages(\"general\"),\n                    title: _t(\"Miscellaneous Journal\"),\n                    text: _t(\"Payroll, depreciation, closing entries, deferred revenues, ...etc\"),\n                    onClick: () => this.openCreateJournalForm(\"general\"),\n                },\n                {\n                    images: this.cardImages(\"sale\"),\n                    title: _t(\"Sales Journal\"),\n                    text: _t(\"Create a separate journal for specific sales activities\"),\n                    onClick: () => this.openCreateJournalForm(\"sale\"),\n                },\n                {\n                    images: this.cardImages(\"purchase\"),\n                    title: _t(\"Purchases Journal\"),\n                    text: _t(\"Create a separate journal to organize vendor bills\"),\n                    onClick: () => this.openCreateJournalForm(\"purchase\"),\n                }\n            );\n        }\n\n        return data;\n    }\n}\n\nregistry.category(\"actions\").add(\"journal_create_wizard\", JournalCreateWizard);\n", "import { registry } from \"@web/core/registry\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { standardFieldProps } from \"@web/views/fields/standard_field_props\";\nimport { Component } from \"@odoo/owl\";\n\nclass MatchingLink extends Component {\n    static props = { ...standardFieldProps };\n    static template = \"account_accountant.MatchingLink\";\n\n    setup() {\n        this.orm = useService(\"orm\");\n        this.action = useService(\"action\");\n    }\n\n    async reconcile() {\n        this.action.doAction(\"account_accountant.action_move_line_posted_unreconciled\", {\n            additionalContext: {\n                search_default_partner_id: this.props.record.data.partner_id.id,\n                search_default_account_id: this.props.record.data.account_id.id,\n            },\n        });\n    }\n\n    async viewMatch() {\n        const action = await this.orm.call(\"account.move.line\", \"open_reconcile_view\", [this.props.record.resId], {});\n        this.action.doAction(action, { additionalContext: { is_matched_view: true } });\n    }\n\n    get colorCode() {\n        const matchValue = this.props.record.data[this.props.name];\n        const matchColorValue = matchValue.replace('P', '');\n        if (matchColorValue === '*') {\n            // reserve color code 0 for multi partial matches\n            return 0;\n        } else {\n            // there is 12 available color palette for 'o_tag_color_*'\n            // since the color code 0 has been reserved by 'P*', we can only use color codes between 1 and 11\n            return parseInt(matchColorValue) % 11 + 1;\n        }\n    }\n}\n\nregistry.category(\"fields\").add(\"matching_link_widget\", {\n    component: MatchingLink,\n});\n", "import { AttachmentPreviewListController, AttachmentPreviewListRenderer } from \"../attachment_preview_list_view/attachment_preview_list_view\";\n\nimport { registry } from \"@web/core/registry\";\nimport { listView } from \"@web/views/list/list_view\";\nimport { makeActiveField } from \"@web/model/relational_model/utils\";\n\nexport class AccountMoveLineListController extends AttachmentPreviewListController {\n    get previewerStorageKey() {\n        return \"account.move_line_pdf_previewer_hidden\";\n    }\n\n    get modelParams() {\n        const params = super.modelParams;\n        params.config.activeFields.move_attachment_ids = makeActiveField();\n        params.config.activeFields.move_attachment_ids.related = {\n            fields: {\n                mimetype: { name: \"mimetype\", type: \"char\" },\n            },\n            activeFields: {\n                mimetype: makeActiveField(),\n            },\n        };\n        return params;\n    }\n\n    async setSelectedRecord(accountMoveLineData) {\n        this.attachmentPreviewState.selectedRecord = accountMoveLineData;\n        await this.setThread(accountMoveLineData, \"move_attachment_ids\", \"move_id\");\n    }\n}\n\nexport class AccountMoveLineListRenderer extends AttachmentPreviewListRenderer {}\n\nexport const AccountMoveLineListView = {\n    ...listView,\n    Renderer: AccountMoveLineListRenderer,\n    Controller: AccountMoveLineListController,\n};\n\nregistry.category(\"views\").add(\"account_move_line_list\", AccountMoveLineListView);\n", "import { registry } from \"@web/core/registry\";\nimport { useSubEnv } from \"@odoo/owl\";\nimport { AccountMoveLineListController, AccountMoveLineListRenderer, AccountMoveLineListView } from \"../move_line_list/move_line_list\";\n\n\nexport class AccountMoveLineReconcileListController extends AccountMoveLineListController {\n\n    setup() {\n        super.setup();\n        useSubEnv({\n            callAutoReconcileAction: this.openAutoReconcileWizard.bind(this),\n        });\n    }\n\n    openAutoReconcileWizard(group=null) {\n        if (group) {\n            return this.actionService.doAction(\"account_accountant.action_open_auto_reconcile_wizard\", {\n                additionalContext: {\n                    domain: group.list.domain,\n                }\n            });\n        } else {\n            return this.actionService.doAction(\"account_accountant.action_open_auto_reconcile_wizard\");\n        }\n    }\n}\n\nexport class AccountMoveLineReconcileListRenderer extends AccountMoveLineListRenderer {\n\n    static groupRowTemplate = \"account_accountant.AccountMoveLineReconcileGroupRow\";\n\n    setup() {\n        super.setup();\n        this.props.list.groups?.map(group => this.toggleGroup(group));  // unfold the first groups (account_id)\n    }\n\n}\n\nexport const AccountMoveLineReconcileLineListView = {\n    ...AccountMoveLineListView,\n    Controller: AccountMoveLineReconcileListController,\n    Renderer: AccountMoveLineReconcileListRenderer,\n    buttonTemplate: \"account_accountant.ListViewReconcile.Buttons\",\n}\n\nregistry.category(\"views\").add(\"account_move_line_reconcile_list\", AccountMoveLineReconcileLineListView);\n", "import { registry } from \"@web/core/registry\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { standardWidgetProps } from \"@web/views/widgets/standard_widget_props\";\nimport { Component } from \"@odoo/owl\";\n\nclass IAPActionButtonsWidget extends Component {\n    static template = \"iap.ActionButtonsWidget\";\n    static props = {\n        ...standardWidgetProps,\n        serviceName: String,\n        showServiceButtons: Boolean,\n    };\n\n    setup() {\n        this.orm = useService(\"orm\");\n        this.action = useService(\"action\");\n    }\n\n    async onViewServicesClicked() {\n        this.action.doAction(\"iap.iap_account_action\");\n    }\n\n    async onManageServiceLinkClicked() {\n        const account_id = await this.orm.silent.call(\"iap.account\", \"get_account_id\", [this.props.serviceName]);\n        this.action.doAction({\n            type: \"ir.actions.act_window\",\n            res_model: \"iap.account\",\n            res_id: account_id,\n            views: [[false, \"form\"]],\n        });\n    }\n}\n\nexport const iapActionButtonsWidget = {\n    component: IAPActionButtonsWidget,\n    extractProps: ({ attrs }) => {\n        return {\n            serviceName: attrs.service_name,\n            showServiceButtons: !Boolean(attrs.hide_service),\n        };\n    },\n};\nregistry.category(\"view_widgets\").add(\"iap_buy_more_credits\", iapActionButtonsWidget);\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\n\nimport { markup } from \"@odoo/owl\";\n\nexport const iapNotificationService = {\n    dependencies: [\"bus_service\", \"notification\"],\n\n    start(env, { bus_service, notification }) {\n        bus_service.subscribe(\"iap_notification\", (params) => {\n            if (params.type == \"no_credit\") {\n                displayCreditErrorNotification(params);\n            } else {\n                displayNotification(params);\n            }\n        });\n        bus_service.start();\n\n        function displayNotification(params) {\n            notification.add(params.message, {\n                title: params.title,\n                type: params.type,\n            });\n        }\n\n        function displayCreditErrorNotification(params) {\n            const message = markup`\n                <a class='btn btn-link' href='${params.get_credits_url}' target='_blank'>\n                    <i class='oi oi-arrow-right'></i>\n                    ${_t(\"Buy more credits\")}\n                </a>`;\n            notification.add(message, {\n                title: params.title,\n                type: \"danger\",\n            });\n        }\n    },\n};\n\nregistry.category(\"services\").add(\"iapNotification\", iapNotificationService);\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { patch } from \"@web/core/utils/patch\";\nimport { PhoneField, phoneField, formPhoneField } from \"@web/views/fields/phone/phone_field\";\nimport { SendSMSButton } from '@sms/components/sms_button/sms_button';\n\npatch(PhoneField, {\n    components: {\n        ...PhoneField.components,\n        SendSMSButton\n    },\n    defaultProps: {\n        ...PhoneField.defaultProps,\n        enableButton: true,\n    },\n    props: {\n        ...PhoneField.props,\n        enableButton: { type: Boolean, optional: true },\n    },\n});\n\nconst patchDescr = () => ({\n    extractProps({ options }) {\n        const props = super.extractProps(...arguments);\n        props.enableButton = options.enable_sms;\n        return props;\n    },\n    supportedOptions: [{\n        label: _t(\"Enable SMS\"),\n        name: \"enable_sms\",\n        type: \"boolean\",\n        default: true,\n    }],\n});\n\npatch(phoneField, patchDescr());\npatch(formPhoneField, patchDescr());\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { user } from \"@web/core/user\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { Component, status } from \"@odoo/owl\";\n\nexport class SendSMSButton extends Component {\n    static template = \"sms.SendSMSButton\";\n    static props = [\"*\"];\n    setup() {\n        this.action = useService(\"action\");\n        this.title = _t(\"Send SMS\");\n    }\n    get phoneHref() {\n        return \"sms:\" + this.props.record.data[this.props.name].replace(/\\s+/g, \"\");\n    }\n    async onClick() {\n        await this.props.record.save();\n        this.action.doAction(\n            {\n                type: \"ir.actions.act_window\",\n                target: \"new\",\n                name: this.title,\n                res_model: \"sms.composer\",\n                views: [[false, \"form\"]],\n                context: {\n                    ...user.context,\n                    default_res_model: this.props.record.resModel,\n                    default_res_id: this.props.record.resId,\n                    default_number_field_name: this.props.name,\n                    default_composition_mode: \"comment\",\n                    dialog_size: \"medium\",\n                },\n            },\n            {\n                onClose: () => {\n                    if (status(this) === \"destroyed\") {\n                        return;\n                    }\n                    this.props.record.load();\n                },\n            }\n        );\n    }\n}\n", "import { _t } from \"@web/core/l10n/translation\";\nimport {\n    EmojisTextField,\n    emojisTextField,\n} from \"@mail/views/web/fields/emojis_text_field/emojis_text_field\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { registry } from \"@web/core/registry\";\n\n/**\n * SmsWidget is a widget to display a textarea (the body) and a text representing\n * the number of SMS and the number of characters. This text is computed every\n * time the user changes the body.\n */\nexport class SmsWidget extends EmojisTextField {\n    static template = \"sms.SmsWidget\";\n    setup() {\n        super.setup();\n        this._emojiAdded = () => this.props.record.update({ [this.props.name]: this.targetEditElement.el.value });\n        this.notification = useService('notification');\n    }\n\n    get encoding() {\n        return this._extractEncoding(this.props.record.data[this.props.name] || '');\n    }\n    get nbrChar() {\n        const content = this._getValueForSmsCounts(this.props.record.data[this.props.name] || \"\");\n        return content.length + (content.match(/\\n/g) || []).length;\n    }\n    get nbrCharExplanation() {\n        return \"\";\n    }\n    get nbrSMS() {\n        return this._countSMS(this.nbrChar, this.encoding);\n    }\n\n    //--------------------------------------------------------------------------\n    // Private: SMS\n    //--------------------------------------------------------------------------\n\n    /**\n     * Count the number of SMS of the content\n     * @private\n     * @returns {integer} Number of SMS\n     */\n    _countSMS(nbrChar, encoding) {\n        if (nbrChar === 0) {\n            return 0;\n        }\n        if (encoding === 'UNICODE') {\n            if (nbrChar <= 70) {\n                return 1;\n            }\n            return Math.ceil(nbrChar / 67);\n        }\n        if (nbrChar <= 160) {\n            return 1;\n        }\n        return Math.ceil(nbrChar / 153);\n    }\n\n    /**\n     * Extract the encoding depending on the characters in the content\n     * @private\n     * @param {String} content Content of the SMS\n     * @returns {String} Encoding of the content (GSM7 or UNICODE)\n     */\n    _extractEncoding(content) {\n        if (String(content).match(RegExp(\"^[@\u00a3$\u00a5\u00e8\u00e9\u00f9\u00ec\u00f2\u00c7\\\\n\u00d8\u00f8\\\\r\u00c5\u00e5\u0394_\u03a6\u0393\u039b\u03a9\u03a0\u03a8\u03a3\u0398\u039e\u00c6\u00e6\u00df\u00c9 !\\\\\\\"#\u00a4%&'()*+,-./0123456789:;<=>?\u00a1ABCDEFGHIJKLMNOPQRSTUVWXYZ\u00c4\u00d6\u00d1\u00dc\u00a7\u00bfabcdefghijklmnopqrstuvwxyz\u00e4\u00f6\u00f1\u00fc\u00e0]*$\"))) {\n            return 'GSM7';\n        }\n        return 'UNICODE';\n    }\n\n    /**\n     * Implement if more characters are going to be sent then those appearing in\n     * value, if that value is processed before being sent.\n     * E.g., links are converted to trackers in mass_mailing_sms.\n     *\n     * Note: goes with an explanation in nbrCharExplanation\n     *\n     * @param {String} value content to be parsed for counting extra characters\n     * @return string length-corrected value placeholder for the post-processed\n     * state\n     */\n    _getValueForSmsCounts(value) {\n        return value;\n    }\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n     * @override\n     * @private\n     */\n    async onBlur() {\n        await super.onBlur();\n        var content = this.props.record.data[this.props.name] || '';\n        if( !content.trim().length && content.length > 0) {\n            this.notification.add(\n                _t(\"Your SMS Text Message must include at least one non-whitespace character\"),\n                { type: 'danger' },\n            )\n            await this.props.record.update({ [this.props.name]: content.trim() });\n        }\n    }\n\n    /**\n     * @override\n     * @private\n     */\n    async onInput(ev) {\n        super.onInput(...arguments);\n        await this.props.record.update({ [this.props.name]: this.targetEditElement.el.value });\n    }\n}\n\nexport const smsWidget = {\n    ...emojisTextField,\n    component: SmsWidget,\n    additionalClasses: [\n        ...(emojisTextField.additionalClasses || []),\n        \"o_field_text\",\n        \"o_field_text_emojis\",\n    ],\n};\n\nregistry.category(\"fields\").add(\"sms_widget\", smsWidget);\n", "import { Failure } from \"@mail/core/common/failure_model\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { patch } from \"@web/core/utils/patch\";\n\npatch(Failure.prototype, {\n    get iconSrc() {\n        if (this.type === \"sms\") {\n            return \"/sms/static/img/sms_failure.svg\";\n        }\n        return super.iconSrc;\n    },\n    get body() {\n        if (this.type === \"sms\") {\n            if (this.notifications.length === 1 && this.lastMessage?.thread) {\n                return _t(\"An error occurred when sending an SMS on \u201c%(record_name)s\u201d\", {\n                    record_name: this.lastMessage.thread.display_name,\n                });\n            }\n            return _t(\"An error occurred when sending an SMS\");\n        }\n        return super.body;\n    },\n});\n", "import { Notification } from \"@mail/core/common/notification_model\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { patch } from \"@web/core/utils/patch\";\n\n/** @type {import(\"models\").Notification} */\nconst notificationPatch = {\n    get failureMessage() {\n        switch (this.failure_type) {\n            case \"sms_number_missing\":\n                return _t(\"Missing Number\");\n            case \"sms_number_format\":\n                return _t(\"Wrong Number Format\");\n            case \"sms_credit\":\n                return _t(\"Insufficient Credit\");\n            case \"sms_country_not_supported\":\n                return _t(\"Country Not Supported\");\n            case \"sms_registration_needed\":\n                return _t(\"Country-specific Registration Required\");\n            case \"sms_server\":\n                return _t(\"Server Error\");\n            case \"sms_acc\":\n                return _t(\"Unregistered Account\");\n            case \"sms_expired\":\n                return _t(\"Expired\");\n            case \"sms_invalid_destination\":\n                return _t(\"Invalid Destination\");\n            case \"sms_not_allowed\":\n                return _t(\"Not Allowed\");\n            case \"sms_not_delivered\":\n                return _t(\"Not Delivered\");\n            case \"sms_rejected\":\n                return _t(\"Rejected\");\n            default:\n                return super.failureMessage;\n        }\n    },\n    get icon() {\n        if (this.notification_type === \"sms\") {\n            return \"fa fa-mobile\";\n        }\n        return super.icon;\n    },\n    get label() {\n        if (this.notification_type === \"sms\") {\n            return _t(\"SMS\");\n        }\n        return super.label;\n    },\n};\npatch(Notification.prototype, notificationPatch);\n", "import { MessagingMenu } from \"@mail/core/public_web/messaging_menu\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { patch } from \"@web/core/utils/patch\";\n\npatch(MessagingMenu.prototype, {\n    openFailureView(failure) {\n        if (failure.type === \"email\") {\n            return super.openFailureView(failure);\n        }\n        this.env.services.action.doAction({\n            name: _t(\"SMS Failures\"),\n            type: \"ir.actions.act_window\",\n            view_mode: \"kanban,list,form\",\n            views: [\n                [false, \"kanban\"],\n                [false, \"list\"],\n                [false, \"form\"],\n            ],\n            target: \"current\",\n            res_model: failure.resModel,\n            domain: [[\"message_has_sms_error\", \"=\", true]],\n            context: { create: false },\n        });\n        this.dropdown.close();\n    },\n    getFailureNotificationName(failure) {\n        if (failure.type === \"sms\") {\n            return _t(\"SMS Failure: %(modelName)s\", { modelName: failure.modelName });\n        }\n        return super.getFailureNotificationName(...arguments);\n    },\n});\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { user } from \"@web/core/user\";\nimport { Message } from \"@mail/core/common/message\";\n\nimport { patch } from \"@web/core/utils/patch\";\n\npatch(Message.prototype, {\n    async onClickNotification(ev) {\n        const hasAccountFailure = this.message.notification_ids.some(\n            (notification) => notification.isFailure && notification.failure_type === \"sms_acc\"\n        );\n        if (\n            this.message.message_type === \"sms\" &&\n            hasAccountFailure &&\n            (await user.hasGroup(\"base.group_system\"))\n        ) {\n            const [accountId] = await this.env.services.orm.call(\"iap.account\", \"get\", [], {\n                service_name: \"sms\",\n                force_create: false,\n            });\n            if (accountId) {\n                this.env.services.action.doAction({\n                    type: \"ir.actions.act_window\",\n                    name: _t(\"SMS Account\"),\n                    target: \"current\",\n                    res_model: \"iap.account\",\n                    res_id: accountId,\n                    views: [[false, \"form\"]],\n                });\n                return;\n            }\n        }\n\n        super.onClickNotification(ev);\n    },\n});\n", "import { BankReconciliationService } from \"@account_accountant/components/bank_reconciliation/bank_reconciliation_service\";\nimport { patch } from \"@web/core/utils/patch\";\nimport { reactive } from \"@odoo/owl\";\n\npatch(BankReconciliationService.prototype, {\n    setup(env, services) {\n        super.setup(env, services);\n        this.hasAvailableBatchPayments = reactive({ value: false });\n    },\n\n    async updateHasAvailableBatchPayments(journalId) {\n        this.hasAvailableBatchPayments.value = !!(await this.orm.searchCount(\n            \"account.batch.payment\",\n            [\n                [\"state\", \"!=\", \"reconciled\"],\n                [\"journal_id\", \"=\", journalId],\n            ],\n            {\n                limit: 1,\n            }\n        ));\n    },\n});\n", "import { BankRecButtonList } from \"@account_accountant/components/bank_reconciliation/button_list/button_list\";\nimport { SelectCreateDialog } from \"@web/views/view_dialogs/select_create_dialog\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { patch } from \"@web/core/utils/patch\";\n\npatch(BankRecButtonList.prototype, {\n    selectBatchPayment() {\n        this.addDialog(SelectCreateDialog, {\n            title: _t(\"Search: Batch Payment\"),\n            noCreate: true,\n            multiSelect: false,\n            resModel: \"account.batch.payment\",\n            context: { search_default_journal_id: this.statementLineData.journal_id.id },\n            domain: [[\"state\", \"!=\", \"reconciled\"]],\n            onSelected: async (batch) => {\n                await this.onSelectBatchPayment(batch[0]);\n            },\n        });\n    },\n\n    async onSelectBatchPayment(batchPaymentId) {\n        await this.orm.call(\n            \"account.bank.statement.line\",\n            \"set_batch_payment_bank_statement_line\",\n            [this.statementLineData.id, batchPaymentId]\n        );\n        await this.bankReconciliation.updateHasAvailableBatchPayments(\n            this.statementLineData.journal_id.id\n        );\n        this.props.statementLine.load();\n        this.bankReconciliation.reloadChatter();\n    },\n\n    get buttons() {\n        const buttonsToDisplay = super.buttons;\n        if (this.isBatchPaymentsButtonShown) {\n            buttonsToDisplay.batch = {\n                label: _t(\"Batches\"),\n                action: this.selectBatchPayment.bind(this),\n                classes: \"batches-btn\",\n            };\n        }\n        return buttonsToDisplay;\n    },\n\n    get isBatchPaymentsButtonShown() {\n        return this.bankReconciliation.hasAvailableBatchPayments.value;\n    },\n});\n", "import { BankRecKanbanController } from \"@account_accountant/components/bank_reconciliation/kanban_controller\";\nimport { patch } from \"@web/core/utils/patch\";\nimport { makeActiveField } from \"@web/model/relational_model/utils\";\n\npatch(BankRecKanbanController.prototype, {\n    get modelParams() {\n        const params = super.modelParams;\n        params.config.activeFields.line_ids.related.fields.payment_lines_ids = {\n            name: \"payment_lines_ids\",\n            type: \"many2many\",\n        };\n        params.config.activeFields.line_ids.related.activeFields.payment_lines_ids =\n            makeActiveField();\n        params.config.activeFields.line_ids.related.activeFields.payment_lines_ids.related = {\n            fields: {\n                id: { name: \"id\", type: \"int\" },\n                display_name: { name: \"display_name\", type: \"char\" },\n                batch_payment_id: { name: \"batch_payment_id\", type: \"many2one\" },\n            },\n            activeFields: {\n                id: makeActiveField(),\n                display_name: makeActiveField(),\n                batch_payment_id: makeActiveField(),\n            },\n        };\n        return params;\n    },\n});\n", "import { BankRecKanbanRenderer } from \"@account_accountant/components/bank_reconciliation/kanban_renderer\";\nimport { patch } from \"@web/core/utils/patch\";\nimport { onWillStart } from \"@odoo/owl\";\n\npatch(BankRecKanbanRenderer.prototype, {\n    setup() {\n        super.setup();\n\n        onWillStart(async () => {\n            await this.bankReconciliation.updateHasAvailableBatchPayments(\n                this.globalState.journalId\n            );\n        });\n    },\n});\n", "import { BankRecLineToReconcile } from \"@account_accountant/components/bank_reconciliation/line_to_reconcile/line_to_reconcile\";\nimport { patch } from \"@web/core/utils/patch\";\n\npatch(BankRecLineToReconcile.prototype, {\n    openMove() {\n        if (\n            this.paymentLinesId &&\n            (!this.reconciledLineId?.move_id ||\n                !this.reconciledLineExcludingExchangeDiffId?.move_id)\n        ) {\n            return this.action.doAction({\n                type: \"ir.actions.act_window\",\n                res_model: \"account.payment\",\n                res_id: this.paymentLinesId.id,\n                views: [[false, \"form\"]],\n                target: \"current\",\n            });\n        }\n        super.openMove();\n    },\n\n    async deleteLine() {\n        await super.deleteLine();\n        const batchPaymentIds = [\n            ...this.lineData.payment_lines_ids.records.map(\n                (payment) => payment.batch_payment_id?.id\n            ),\n        ];\n        if (batchPaymentIds.length) {\n            await this.bankReconciliation.updateHasAvailableBatchPayments(\n                this.statementLineData.journal_id.id\n            );\n            this.props.statementLine.load();\n        }\n    },\n\n    get paymentLinesId() {\n        return this.lineData.payment_lines_ids.records.length === 1\n            ? this.lineData.payment_lines_ids.records[0].data\n            : null;\n    },\n\n    get moveData() {\n        return (\n            this.reconciledLineId?.move_id ||\n            this.reconciledLineExcludingExchangeDiffId?.move_id ||\n            this.paymentLinesId ||\n            this.lineData.move_id\n        );\n    },\n});\n", "import { Mutex } from \"@web/core/utils/concurrency\";\nimport { checkFileSize } from \"@web/core/utils/files\";\n\nexport class BinaryFileManager {\n    constructor(resModel, fields, parameters, context, orm, notificationService) {\n        this.resModel = resModel;\n        this.fields = [\".id\", ...fields];\n        this.parameters = parameters;\n\n        this.context = context;\n        this.orm = orm;\n        this.notificationService = notificationService;\n\n        this.maxBatchSize = this.parameters.maxBatchSize * 0.95; // 0.95 for not calculated payload overhead\n        this.delayAfterEachBatch = this.parameters.delayAfterEachBatch * 1000;\n\n        this.dataToSend = {};\n\n        this.mutex = new Mutex();\n    }\n\n    async addFile(id, field, file) {\n        let data = await this._readFile(file);\n        if (typeof data === \"string\" && data.startsWith(\"data:\")) {\n            // Remove data:image/*;base64,\n            data = data.split(\",\")[1];\n        }\n        const dataSize = data.length;\n        if (!checkFileSize(dataSize, this.notificationService)) {\n            return;\n        }\n\n        if (this.getCurrentSize() + dataSize >= this.maxBatchSize) {\n            await this.mutex.exec(async () => await this._send());\n        }\n        if (!(id in this.dataToSend)) {\n            this.dataToSend[id] = Array(this.fields.length);\n            this.dataToSend[id][0] = id;\n        }\n        const indexOfField = this.fields.indexOf(field, 1);\n        this.dataToSend[id][indexOfField] = data;\n    }\n\n    async sendLastPayload() {\n        if (Object.keys(this.dataToSend).length > 0) {\n            await this.mutex.exec(async () => await this._send());\n        }\n    }\n\n    async _send() {\n        await new Promise((resolve) => {\n            setTimeout(resolve, this.delayAfterEachBatch);\n        });\n        const data = Object.values(this.dataToSend);\n        this.dataToSend = {};\n        const context = {\n            ...this.context,\n            import_file: true,\n            tracking_disable: this.parameters.tracking_disable,\n            name_create_enabled_fields: this.parameters.name_create_enabled_fields || {},\n            import_set_empty_fields: this.parameters.import_set_empty_fields || [],\n            import_skip_records: this.parameters.import_skip_records || [],\n        };\n        let res;\n        try {\n            res = await this.orm.call(this.resModel, \"load\", [], {\n                fields: this.fields,\n                data,\n                context,\n            });\n        } catch (error) {\n            console.error(error);\n            return { error };\n        }\n        return res;\n    }\n\n    _readFile(file) {\n        return new Promise((resolve, reject) => {\n            const reader = new FileReader();\n            reader.onerror = (event) => reject(event);\n            reader.onabort = (event) => reject(event);\n            reader.onload = (event) => resolve(event.target.result);\n            reader.readAsDataURL(file);\n        });\n    }\n\n    getCurrentSize() {\n        return JSON.stringify(this.dataToSend).length;\n    }\n}\n", "import { Component, onWillStart, useRef, useState } from \"@odoo/owl\";\nimport { useDropzone } from \"@web/core/dropzone/dropzone_hook\";\nimport { FileInput } from \"@web/core/file_input/file_input\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { useFileUploader } from \"@web/core/utils/files\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { Layout } from \"@web/search/layout\";\nimport { DocumentationLink } from \"@web/views/widgets/documentation_link/documentation_link\";\nimport { standardActionServiceProps } from \"@web/webclient/actions/action_service\";\nimport { ImportDataContent } from \"../import_data_content/import_data_content\";\nimport { ImportDataProgress } from \"../import_data_progress/import_data_progress\";\nimport { ImportDataSidepanel } from \"../import_data_sidepanel/import_data_sidepanel\";\nimport { useImportModel } from \"../import_model\";\n\nexport class ImportAction extends Component {\n    static template = \"ImportAction\";\n    static nextId = 1;\n    static components = {\n        FileInput,\n        ImportDataContent,\n        ImportDataSidepanel,\n        Layout,\n        DocumentationLink,\n    };\n    static props = { ...standardActionServiceProps };\n    static path = \"import\";\n    static displayName = _t(\"Import a File\");\n\n    setup() {\n        this.actionService = useService(\"action\");\n        this.notification = useService(\"notification\");\n        this.env.config.setDisplayName(this.props.action.name || _t(\"Import a File\"));\n        this.model = useImportModel({\n            env: this.env,\n            context: this.props.action.params?.context || {},\n        });\n\n        this.state = useState({\n            filename: undefined,\n            numRows: 0,\n            importMessages: [],\n            importProgress: {\n                value: 0,\n                step: 1,\n            },\n            isPaused: false,\n            isTested: false,\n            previewError: \"\",\n        });\n\n        this.uploadFiles = useFileUploader();\n        useDropzone(useRef(\"root\"), async (event) => {\n            const { files } = event.dataTransfer;\n            if (files.length === 0) {\n                this.notification.add(\n                    _t(\"Please upload an Excel (.xls or .xlsx) or .csv file to import.\"),\n                    {\n                        type: \"danger\",\n                    }\n                );\n            } else if (files.length > 1) {\n                this.notification.add(_t(\"Please upload a single file.\"), {\n                    type: \"danger\",\n                });\n            } else {\n                const file = files[0];\n                const isValidFile =\n                    file.name.endsWith(\".csv\") ||\n                    file.name.endsWith(\".xls\") ||\n                    file.name.endsWith(\".xlsx\");\n                if (!isValidFile) {\n                    this.notification.add(\n                        _t(\"Please upload an Excel (.xls or .xlsx) or .csv file to import.\"),\n                        {\n                            type: \"danger\",\n                        }\n                    );\n                } else {\n                    await this.uploadFiles(this.uploadFilesRoute, {\n                        csrf_token: odoo.csrf_token,\n                        ufile: [file],\n                        model: this.resModel,\n                        id: this.model.id,\n                    });\n                    this.handleFilesUpload([file]);\n                }\n            }\n        });\n\n        onWillStart(this.onWillStart);\n    }\n\n    async onWillStart() {\n        const action = await this.actionService.currentAction;\n        const activeModel = this.props.action.params?.active_model;\n        if (activeModel) {\n            this.resModel = activeModel;\n            if (action?.type === \"ir.actions.act_window\" && action?.res_model === this.resModel) {\n                this.action = action;\n            } else {\n                this.props.updateActionState({ active_model: this.resModel });\n            }\n        } else {\n            if (!action) {\n                return this.env.config.historyBack();\n            }\n            if (action.type !== \"ir.actions.act_window\") {\n                return this.actionService.restore(this.actionService.currentController.jsId);\n            }\n            this.action = action;\n            this.resModel = this.action.res_model;\n        }\n        this.model.setResModel(this.resModel);\n        return this.model.init();\n    }\n\n    cancel() {\n        this.env.config.historyBack();\n    }\n\n    openRecords(resIds) {\n        this.actionService.doAction({\n            type: \"ir.actions.act_window\",\n            name: _t(\"Imported records\"),\n            res_model: this.model.resModel,\n            view_mode: this.action?.view_mode || \"list,form\",\n            views: this.action?.views || [\n                [false, \"list\"],\n                [false, \"form\"],\n            ],\n            domain: [[\"id\", \"in\", resIds]],\n            target: \"current\",\n            path: \"imported-records\",\n        });\n    }\n\n    get display() {\n        return {\n            controlPanel: {},\n        };\n    }\n\n    get importTemplates() {\n        return this.model.importTemplates;\n    }\n\n    get uploadFilesRoute() {\n        return \"/base_import/set_file\";\n    }\n\n    //--------------------------------------------------------------------------\n    // Options\n    //--------------------------------------------------------------------------\n\n    get formattingOptions() {\n        return this.model.formattingOptions;\n    }\n\n    get totalToImport() {\n        return this.state.numRows - parseInt(this.importOptions.skip);\n    }\n\n    get totalSteps() {\n        return this.isBatched ? Math.ceil(this.totalToImport / this.importOptions.limit) : 1;\n    }\n\n    get importOptions() {\n        return this.model.importOptions;\n    }\n\n    get isPreviewing() {\n        return this.state.filename !== undefined;\n    }\n\n    // Activate the batch configuration panel only if the number of rows > 100. (In order to let the user choose\n    // the batch size even for medium size file. Could be useful to reduce the batch size for complex models).\n    get isBatched() {\n        return this.state.numRows > 100;\n    }\n\n    async onOptionChanged(name, value, fieldName = null) {\n        this.model.block();\n        const result = await this.model.setOption(name, value, fieldName);\n        if (result) {\n            const { res, error } = result;\n            if (!error && res.num_rows) {\n                this.state.numRows = res.num_rows;\n                this.state.previewError = undefined;\n            } else {\n                this.state.previewError = error;\n            }\n        }\n        this.model.unblock();\n    }\n\n    async reload() {\n        this.model.block();\n        await this.model.updateData();\n        this.model.unblock();\n    }\n\n    //--------------------------------------------------------------------------\n    // File\n    //--------------------------------------------------------------------------\n\n    async handleFilesUpload(files) {\n        if (!files || files.length <= 0) {\n            return;\n        }\n\n        this.state.filename = files[0].name;\n        this.state.importMessages = [];\n\n        this.model.block(_t(\"Loading file...\"));\n        const { res, error } = await this.model.updateData(true);\n\n        if (error) {\n            this.state.previewError = error;\n        } else {\n            this.state.numRows = res.num_rows;\n            this.state.previewError = undefined;\n        }\n        this.state.isTested = false;\n        this.model.unblock();\n    }\n\n    async handleImport(isTest = true) {\n        const message = isTest ? _t(\"Testing\") : _t(\"Importing\");\n\n        let blockComponent;\n        if (this.isBatched) {\n            blockComponent = {\n                class: ImportDataProgress,\n                props: {\n                    stopImport: () => this.stopImport(),\n                    totalSteps: this.totalSteps,\n                    importProgress: this.state.importProgress,\n                },\n            };\n        }\n\n        this.model.block(message, blockComponent);\n\n        let res = { ids: [] };\n        try {\n            const data = await this.model.executeImport(\n                isTest,\n                this.totalSteps,\n                this.state.importProgress\n            );\n            res = data.res;\n        } finally {\n            this.model.unblock();\n        }\n\n        if (!isTest && res.nextrow) {\n            this.state.isPaused = true;\n        }\n\n        if (res.ids.length) {\n            if (!isTest) {\n                this.notification.add(_t(\"%s records successfully imported\", res.ids.length), {\n                    type: \"success\",\n                });\n                if (!this.state.isPaused) {\n                    this.openRecords(res.ids);\n                }\n            } else {\n                this.state.isTested = true;\n            }\n        }\n    }\n\n    stopImport() {\n        this.model.stopImport();\n    }\n\n    //--------------------------------------------------------------------------\n    // Fields\n    //--------------------------------------------------------------------------\n\n    onFieldChanged(column, fieldInfo) {\n        this.model.setColumnField(column, fieldInfo);\n    }\n\n    isFieldSet(column) {\n        return column.fieldInfo != null;\n    }\n\n    get hasBinaryFields() {\n        return this.model.columns.some((column) => column.fieldInfo?.type === \"binary\");\n    }\n}\n\nregistry.category(\"actions\").add(\"import\", ImportAction);\n", "import { Component } from \"@odoo/owl\";\n\nexport class ImportBlockUI extends Component {\n    static props = {\n        message: { type: String, optional: true },\n        blockComponent: { type: Object, optional: true },\n    };\n    static template = \"base_import.BlockUI\";\n}\n", "import { Component, useState } from \"@odoo/owl\";\nimport { useService } from \"@web/core/utils/hooks\";\n\nexport class ImportDataColumnError extends Component {\n    static template = \"ImportDataColumnError\";\n    static props = {\n        errors: { type: Array },\n        fieldInfo: { type: Object },\n        resultNames: { type: Array },\n    };\n\n    setup() {\n        this.action = useService(\"action\");\n        this.orm = useService(\"orm\");\n        this.state = useState({\n            isExpanded: false,\n            moreInfoContent: undefined,\n        });\n    }\n    get moreInfo() {\n        const moreInfoObjects = this.props.errors.map((error) => error.moreinfo);\n        return moreInfoObjects.length && moreInfoObjects[0];\n    }\n    isErrorVisible(index) {\n        return this.state.isExpanded || index < 3;\n    }\n    onMoreInfoClicked() {\n        const moreInfo = this.moreInfo;\n        if (this.state.moreInfoContent) {\n            this.state.moreInfoContent = undefined;\n        } else if (moreInfo instanceof Array) {\n            this.state.moreInfoContent = moreInfo;\n        } else {\n            this.action.doAction(moreInfo);\n        }\n    }\n}\n", "import { Component } from \"@odoo/owl\";\nimport { SelectMenu } from \"@web/core/select_menu/select_menu\";\nimport { ImportDataColumnError } from \"../import_data_column_error/import_data_column_error\";\nimport { ImportDataOptions } from \"../import_data_options/import_data_options\";\nimport { _t } from \"@web/core/l10n/translation\";\n\nexport class ImportDataContent extends Component {\n    static template = \"ImportDataContent\";\n    static components = {\n        ImportDataColumnError,\n        ImportDataOptions,\n        SelectMenu,\n    };\n    static props = {\n        columns: { type: Array },\n        isFieldSet: { type: Function },\n        onOptionChanged: { type: Function },\n        onFieldChanged: { type: Function },\n        options: { type: Object },\n        importMessages: { type: Object },\n        previewError: { type: String, optional: true },\n    };\n\n    getGroups(column) {\n        const groups = [\n            { choices: this.makeChoices(column.fields.basic) },\n            { choices: this.makeChoices(column.fields.suggested), label: _t(\"Suggested Fields\") },\n            {\n                choices: this.makeChoices(column.fields.additional),\n                label:\n                    column.fields.suggested.length > 0\n                        ? _t(\"Additional Fields\")\n                        : _t(\"Standard Fields\"),\n            },\n            { choices: this.makeChoices(column.fields.relational), label: _t(\"Relation Fields\") },\n        ];\n        return groups;\n    }\n\n    makeChoices(fields) {\n        return fields.map((field) => ({\n            label: field.label,\n            value: field.fieldPath,\n            iconClass: `o_import_field_icon_${field.type}`,\n        }));\n    }\n\n    getTooltipDetails(field) {\n        return JSON.stringify({\n            resModel: field.model_name,\n            debug: true,\n            field: {\n                name: field.name,\n                label: field.string,\n                type: field.type,\n            },\n        });\n    }\n\n    getTooltip(column) {\n        const displayCount = 5;\n        if (column.previews.length > displayCount) {\n            return JSON.stringify({\n                lines: [\n                    ...column.previews.slice(0, displayCount - 1),\n                    `(+${column.previews.length - displayCount + 1})`,\n                ],\n            });\n        } else {\n            return JSON.stringify({ lines: column.previews.slice(0, displayCount) });\n        }\n    }\n\n    getErrorMessageClass(messages, type, index) {\n        return `alert alert-${type} m-0 p-2 ${index === messages.length - 1 ? \"\" : \"mb-2\"}`;\n    }\n\n    getCommentClass(column, comment, index) {\n        return `alert-${comment.type} ${index < column.comments.length - 1 ? \"mb-2\" : \"mb-0\"}`;\n    }\n\n    onFieldChanged(column, fieldPath) {\n        const fields = [\n            ...column.fields.basic,\n            ...column.fields.suggested,\n            ...column.fields.additional,\n            ...column.fields.relational,\n        ];\n        const fieldInfo = fields.find((f) => f.fieldPath === fieldPath);\n        this.props.onFieldChanged(column, fieldInfo);\n    }\n}\n", "import { Component, useState, onWillStart } from \"@odoo/owl\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { useService } from \"@web/core/utils/hooks\";\n\nexport class ImportDataOptions extends Component {\n    static template = \"ImportDataOptions\";\n    static props = {\n        importOptions: { type: Object, optional: true },\n        fieldInfo: { type: Object },\n        onOptionChanged: { type: Function },\n    };\n\n    setup() {\n        this.orm = useService(\"orm\");\n        this.state = useState({\n            options: [],\n        });\n        this.currentModel = this.props.fieldInfo.comodel_name || this.props.fieldInfo.model_name;\n        onWillStart(async () => {\n            this.state.options = await this.loadOptions();\n        });\n    }\n    get isVisible() {\n        return [\"many2one\", \"many2many\", \"selection\", \"boolean\"].includes(\n            this.props.fieldInfo.type\n        );\n    }\n    async loadOptions() {\n        const options = [[\"prevent\", _t(\"Prevent import\")]];\n        if (this.props.fieldInfo.type === \"boolean\") {\n            options.push([\"false\", _t(\"Set to: False\")]);\n            options.push([\"true\", _t(\"Set to: True\")]);\n            !this.props.fieldInfo.required &&\n                options.push([\"import_skip_records\", _t(\"Skip record\")]);\n        }\n        if ([\"many2one\", \"many2many\", \"selection\"].includes(this.props.fieldInfo.type)) {\n            if (!this.props.fieldInfo.required) {\n                options.push([\"import_set_empty_fields\", _t(\"Set value as empty\")]);\n                options.push([\"import_skip_records\", _t(\"Skip record\")]);\n            }\n            if (this.props.fieldInfo.type === \"selection\") {\n                const fields = await this.orm.call(this.currentModel, \"fields_get\");\n                const selection = fields[this.props.fieldInfo.name].selection.map((opt) => [\n                    opt[0],\n                    _t(\"Set to: %s\", opt[1]),\n                ]);\n                options.push(...selection);\n            } else {\n                options.push([\"name_create_enabled_fields\", _t(\"Create new values\")]);\n            }\n        }\n        return options;\n    }\n    onSelectionChanged(ev) {\n        if (\n            [\n                \"name_create_enabled_fields\",\n                \"import_set_empty_fields\",\n                \"import_skip_records\",\n            ].includes(ev.target.value)\n        ) {\n            this.props.onOptionChanged(\n                ev.target.value,\n                ev.target.value,\n                this.props.fieldInfo.fieldPath\n            );\n        } else {\n            const value = {\n                fallback_value: ev.target.value,\n                field_model: this.currentModel,\n                field_type: this.props.fieldInfo.type,\n            };\n            this.props.onOptionChanged(\"fallback_values\", value, this.props.fieldInfo.fieldPath);\n        }\n    }\n}\n", "import { Component, useEffect, useState } from \"@odoo/owl\";\n\nexport class ImportDataProgress extends Component {\n    static template = \"ImportDataProgress\";\n    static props = {\n        importProgress: { type: Object },\n        stopImport: { type: Function },\n        totalSteps: { type: Number },\n    };\n\n    setup() {\n        this.timer = undefined;\n        this.timeStart = Date.now();\n        this.state = useState({\n            isInterrupted: false,\n            timeLeft: null,\n        });\n\n        useEffect(\n            () => {\n                this.updateTimer();\n                return () => {\n                    clearInterval(this.timer);\n                };\n            },\n            () => []\n        );\n    }\n\n    get minutesLeft() {\n        return this.state.timeLeft.toFixed(2);\n    }\n\n    get secondsLeft() {\n        return Math.round(this.state.timeLeft * 60);\n    }\n\n    interrupt() {\n        this.state.isInterrupted = true;\n        this.props.stopImport();\n    }\n\n    updateTimer() {\n        if (this.timer) {\n            clearInterval(this.timer);\n        }\n        this.state.timeLeft =\n            ((Date.now() - this.timeStart) *\n                ((100 - this.props.importProgress.value) / this.props.importProgress.value)) /\n            60000;\n        this.timer = setInterval(() => this.updateTimer(), 1000);\n    }\n}\n", "import { Component } from \"@odoo/owl\";\nimport { CheckBox } from \"@web/core/checkbox/checkbox\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { DocumentationLink } from \"@web/views/widgets/documentation_link/documentation_link\";\n\nexport class ImportDataSidepanel extends Component {\n    static template = \"ImportDataSidepanel\";\n    static components = { CheckBox, DocumentationLink };\n    static props = {\n        filename: { type: String },\n        formattingOptions: { type: Object, optional: true },\n        options: { type: Object },\n        importTemplates: { type: Array, optional: true },\n        isBatched: { type: Boolean, optional: true },\n        onOptionChanged: { type: Function },\n        onReload: { type: Function },\n        hasBinaryFields: { type: Boolean },\n        binaryFilesParams: { type: Object },\n        onBinaryFilesParamsChanged: { type: Function },\n    };\n\n    get fileName() {\n        return this.props.filename.split(\".\")[0];\n    }\n\n    get fileExtension() {\n        return \".\" + this.props.filename.split(\".\").pop();\n    }\n\n    getOptionValue(name) {\n        if (name === \"skip\") {\n            return (this.props.options.skip + 1).toString();\n        }\n        return this.props.options[name].toString();\n    }\n\n    setOptionValue(name, value) {\n        this.props.onOptionChanged(name, isNaN(parseFloat(value)) ? value : Number(value));\n    }\n\n    // Start at row 1 = skip 0 lines\n    onLimitChange(ev) {\n        this.props.onOptionChanged(\"skip\", ev.target.value ? ev.target.value - 1 : 0);\n    }\n\n    get binaryFilesLabel() {\n        const files = this.props.binaryFilesParams.binaryFiles.value;\n        const number = Object.keys(files).length;\n        if (number > 0) {\n            return _t(\"%(number)s file(s) selected\", { number });\n        }\n        return _t(\"No file selected\");\n    }\n}\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { checkFileSize, DEFAULT_MAX_FILE_SIZE } from \"@web/core/utils/files\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { pick } from \"@web/core/utils/objects\";\nimport { groupBy, sortBy } from \"@web/core/utils/arrays\";\nimport { memoize } from \"@web/core/utils/functions\";\nimport { session } from \"@web/session\";\nimport { useState } from \"@odoo/owl\";\nimport { ImportBlockUI } from \"./import_block_ui\";\nimport { BinaryFileManager } from \"./binary_file_manager\";\n\nconst mainComponentRegistry = registry.category(\"main_components\");\n\nconst strftimeFormatTable = {\n    d: \"w\",\n    DD: \"d\",\n    ddd: \"a\",\n    dddd: \"A\",\n    DDDD: \"j\",\n    ww: \"U\",\n    WW: \"W\",\n    mm: \"M\",\n    MM: \"m\",\n    MMM: \"b\",\n    MMMM: \"B\",\n    YYYY: \"Y\",\n    YY: \"y\",\n    ss: \"S\",\n    hh: \"h\",\n    HH: \"H\",\n    A: \"p\",\n};\n\n/**\n * Convert a human readable format to Python strftime format. In case\n * no corresponding format is supported, a similar fallback is given\n * from the list of other supported formatting value.\n *\n * @param {string} value original Luxon format\n * @returns {string} valid strftime format\n */\nconst humanToStrftimeFormat = memoize(function humanToStrftimeFormat(value) {\n    const regex = /(dddd|ddd|dd|d|mmmm|mmm|mm|ww|yyyy|yy|hh|ss|a)/gi;\n    return value.replace(regex, (value) => {\n        if (strftimeFormatTable[value]) {\n            return \"%\" + strftimeFormatTable[value];\n        }\n        return (\n            \"%\" +\n            (strftimeFormatTable[value.toLowerCase()] || strftimeFormatTable[value.toUpperCase()])\n        );\n    });\n});\n\nconst strftimeToHumanFormat = memoize(function strftimeToHumanFormat(value) {\n    Object.entries(strftimeFormatTable).forEach(([k, v]) => {\n        value = value.replace(`%${v}`, k);\n    });\n    return value;\n});\n\n/**\n * -------------------------------------------------------------------------\n * Base Import Business Logic\n * -------------------------------------------------------------------------\n *\n * Handles mapping and updating the preview data of the csv/excel files to be\n * used in the different base_import components.\n *\n * When uploading a file some \"preview data\" is returned by the backend, this\n * data consist of the different columns of the file and the odoo fields which\n * these columns can be mapped to.\n *\n * Only a small selection of the lines are returned so the user can get an idea\n * of how to correctly map the columns. *(this is why it is refered as \"preview\n * data\")*\n *\n */\nexport class BaseImportModel {\n    constructor({ env, context, orm }) {\n        this.id = 1;\n        this.env = env;\n        this.orm = orm;\n        this.handleInterruption = false;\n\n        this.context = context || {};\n\n        this.fields = [];\n        this.columns = [];\n        this.importMessages = [];\n        this._importOptions = {};\n\n        this.importTemplates = [];\n\n        this.formattingOptionsValues = this._getCSVFormattingOptions();\n\n        this.importOptionsValues = {\n            ...this.formattingOptionsValues,\n            advanced: {\n                reloadParse: true,\n                value: true,\n            },\n            has_headers: {\n                reloadParse: true,\n                value: true,\n            },\n            keep_matches: {\n                value: false,\n            },\n            limit: {\n                value: 2000,\n            },\n            sheets: {\n                value: [],\n            },\n            sheet: {\n                label: _t(\"Selected Sheet:\"),\n                reloadParse: true,\n                value: \"\",\n            },\n            skip: {\n                value: 0,\n            },\n            tracking_disable: {\n                value: true,\n            },\n        };\n\n        const maxUploadSize = session.max_file_upload_size || DEFAULT_MAX_FILE_SIZE;\n        this.binaryFilesParams = {\n            binaryFiles: {\n                value: {},\n            },\n            maxSizePerBatch: {\n                help: _t(\"Defines how many megabytes can be imported in each batch import\"),\n                value: 10,\n                max: Math.round(maxUploadSize / 1024 / 1024),\n                min: 0,\n            },\n            delayAfterEachBatch: {\n                help: _t(\n                    \"After each batch import, this delay is applied to avoid unthrottled calls\"\n                ),\n                value: 1,\n                min: 1,\n            },\n        };\n\n        this.fieldsToHandle = {};\n\n        this.notificationService = useService(\"notification\");\n    }\n\n    //--------------------------------------------------------------------------\n    // Public\n    //--------------------------------------------------------------------------\n\n    get formattingOptions() {\n        return pick(this.importOptionsValues, ...Object.keys(this.formattingOptionsValues));\n    }\n\n    /**\n     * This getter returns the current values pf the options, formatted to match the\n     * server API (date and datetime options should be Python strftime formatted)\n     */\n    get formattedImportOptions() {\n        const options = this.importOptions;\n        options.date_format = humanToStrftimeFormat(options.date_format);\n        options.datetime_format = humanToStrftimeFormat(options.datetime_format);\n        return options;\n    }\n\n    get importOptions() {\n        const tempImportOptions = {\n            import_skip_records: [],\n            import_set_empty_fields: [],\n            fallback_values: {},\n            name_create_enabled_fields: {},\n        };\n        for (const [name, option] of Object.entries(this.importOptionsValues)) {\n            tempImportOptions[name] = option.value;\n        }\n\n        for (const key in this.fieldsToHandle) {\n            const value = this.fieldsToHandle[key];\n            if (value) {\n                if (value.optionName === \"import_skip_records\") {\n                    tempImportOptions.import_skip_records.push(key);\n                } else if (value.optionName === \"import_set_empty_fields\") {\n                    tempImportOptions.import_set_empty_fields.push(key);\n                } else if (value.optionName === \"name_create_enabled_fields\") {\n                    tempImportOptions.name_create_enabled_fields[key] = true;\n                } else if (value.optionName === \"fallback_values\") {\n                    tempImportOptions.fallback_values[key] = value.value;\n                }\n            }\n        }\n\n        this._importOptions = tempImportOptions;\n        return tempImportOptions;\n    }\n\n    set importOptions(options) {\n        for (const key in options) {\n            this.importOptionsValues[key].value = options[key];\n        }\n    }\n\n    /**\n     * A custom BlockUI is required to add the progress bar or text when blocking\n     * the UI, without modifying the core ui service to handle a generic use case\n     */\n    block(message, blockComponent) {\n        mainComponentRegistry.add(\n            \"ImportBlockUI\",\n            {\n                Component: ImportBlockUI,\n                props: {\n                    blockComponent,\n                    message,\n                },\n            },\n            { force: true }\n        );\n    }\n\n    unblock() {\n        mainComponentRegistry.remove(\"ImportBlockUI\");\n    }\n\n    setResModel(resModel) {\n        this.resModel = resModel;\n    }\n\n    async init() {\n        [this.importTemplates, this.id] = await Promise.all([\n            this.orm.call(this.resModel, \"get_import_templates\", [], {\n                context: this.context,\n            }),\n            this.orm.call(\"base_import.import\", \"create\", [{ res_model: this.resModel }]),\n        ]);\n    }\n\n    async executeImport(isTest = false, totalSteps, importProgress) {\n        this.handleInterruption = false;\n        this._updateComments();\n        this.importMessages = [];\n\n        const startRow = this.importOptions.skip;\n        const importRes = {\n            ids: [],\n            fields: this.columns.map((e) => Boolean(e.fieldInfo) && e.fieldInfo.fieldPath),\n            columns: this.columns.map((e) => e.name.trim().toLowerCase()),\n            hasError: false,\n        };\n\n        for (let i = 1; i <= totalSteps; i++) {\n            if (this.handleInterruption) {\n                if (importRes.hasError || isTest) {\n                    importRes.nextrow = startRow;\n                    this.setOption(\"skip\", startRow);\n                }\n                break;\n            }\n\n            const error = await this._executeImportStep(isTest, importRes);\n            if (error) {\n                const errorData = error.data || {};\n                const message =\n                    (errorData.arguments && (errorData.arguments[1] || errorData.arguments[0])) ||\n                    _t(\n                        \"An unknown issue occurred during import (possibly lost connection, data limit exceeded or memory limits exceeded). Please retry in case the issue is transient. If the issue still occurs, try to split the file rather than import it at once.\"\n                    );\n\n                if (error.message) {\n                    this._addMessage(\"danger\", [error.message, message]);\n                } else {\n                    this._addMessage(\"danger\", [message]);\n                }\n\n                importRes.hasError = true;\n                break;\n            }\n\n            if (importProgress) {\n                importProgress.step = i;\n                importProgress.value = Math.round((100 * (i - 1)) / totalSteps);\n            }\n        }\n\n        if (!importRes.hasError) {\n            if (!isTest && importRes.nextrow) {\n                this._addMessage(\"warning\", [\n                    _t(\n                        \"Click 'Resume' to proceed with the import, resuming at line %s.\",\n                        importRes.nextrow + 1\n                    ),\n                    _t(\"You can test or reload your file before resuming the import.\"),\n                ]);\n            }\n            if (isTest) {\n                this._addMessage(\"info\", [_t(\"Everything seems valid.\")]);\n                this.setOption(\"skip\", 0);\n            }\n        } else {\n            importRes.nextrow = startRow;\n        }\n        return { res: importRes };\n    }\n\n    /**\n     * Ask the server for the parsing preview\n     * and update the data accordingly.\n     */\n    async updateData(fileChanged = false) {\n        if (fileChanged) {\n            this.importOptionsValues.sheet.value = \"\";\n        }\n        this.importMessages = [];\n\n        const res = await this.orm.call(\"base_import.import\", \"parse_preview\", [\n            this.id,\n            this.formattedImportOptions,\n        ]);\n\n        if (!res.error) {\n            res.options.date_format = strftimeToHumanFormat(res.options.date_format);\n            res.options.datetime_format = strftimeToHumanFormat(res.options.datetime_format);\n            this._onLoadSuccess(res);\n        } else {\n            this._onLoadError();\n        }\n        return { res, error: res.error };\n    }\n\n    async setOption(optionName, value, fieldName) {\n        if (fieldName) {\n            this.fieldsToHandle[fieldName] = {\n                optionName,\n                value,\n            };\n            return;\n        }\n        this.importOptionsValues[optionName].value = value;\n        if (this.importOptionsValues[optionName].reloadParse) {\n            return this.updateData();\n        }\n    }\n\n    onBinaryFilesParamsChanged(parameterName, value) {\n        if (parameterName === \"binaryFiles\") {\n            const files = {};\n            for (const file of value) {\n                if (checkFileSize(file.size, this.notificationService)) {\n                    files[file.name] = file;\n                }\n            }\n            value = files;\n        }\n        this.binaryFilesParams[parameterName].value = value;\n    }\n\n    setColumnField(column, fieldInfo) {\n        column.fieldInfo = fieldInfo;\n        this._updateComments(column);\n    }\n\n    isColumnFieldSet(column) {\n        return column.fieldInfo != null;\n    }\n\n    /*\n     * We must wait the current iteration of execute_import to conclude and it\n     * will stop at the start of the next batch with handleInterruption\n     */\n    stopImport() {\n        this.handleInterruption = true;\n    }\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    _addMessage(type, lines) {\n        const importMsgs = this.importMessages;\n        importMsgs.push({\n            type: type.replace(\"error\", \"danger\"),\n            lines,\n        });\n        this.importMessages = importMsgs;\n    }\n\n    async _executeImportStep(isTest, importRes) {\n        const importArgs = [\n            this.id,\n            importRes.fields,\n            importRes.columns,\n            this.formattedImportOptions,\n        ];\n        const { ids, messages, nextrow, name, error, binary_filenames } = await this._callImport(\n            isTest,\n            importArgs\n        );\n\n        // Handle server errors\n        if (error) {\n            return error;\n        }\n\n        if (ids) {\n            importRes.ids = importRes.ids.concat(ids);\n        }\n\n        // Handle import errors\n        if (messages && messages.length) {\n            importRes.hasError = true;\n            this.stopImport();\n            if (this._handleImportErrors(messages, name)) {\n                return false;\n            }\n        }\n\n        // Push local image to records\n        await this._pushLocalImageToRecords(ids, binary_filenames, isTest);\n\n        // Check if we should continue\n        if (nextrow) {\n            this.setOption(\"skip\", nextrow);\n            importRes.nextrow = nextrow;\n        } else {\n            // Falsy `nextrow` signals there's nothing left to import\n            this.stopImport();\n        }\n        return false;\n    }\n\n    async _pushLocalImageToRecords(ids, binaryFilenames, isTest) {\n        if (typeof binaryFilenames === \"object\") {\n            const parameters = {\n                tracking_disable: this.importOptions.tracking_disable,\n                delayAfterEachBatch: this.binaryFilesParams.delayAfterEachBatch.value,\n                maxBatchSize: this.binaryFilesParams.maxSizePerBatch.value * 1024 * 1024,\n            };\n\n            if (!this.binaryFilesParams.binaryFiles) {\n                return;\n            }\n            const binaryFiles = this.binaryFilesParams.binaryFiles.value;\n            const fields = Object.keys(binaryFilenames);\n            const binaryFileManager = new BinaryFileManager(\n                this.resModel,\n                fields,\n                parameters,\n                this.context,\n                this.orm,\n                this.notificationService\n            );\n            for (let rowIndex = 0; rowIndex < ids.length; rowIndex++) {\n                const id = ids[rowIndex];\n                for (const field of fields) {\n                    const fileName = binaryFilenames[field][rowIndex];\n                    if (!fileName) {\n                        continue;\n                    }\n                    if (fileName in binaryFiles) {\n                        const file = binaryFiles[fileName];\n                        if (!file || isTest) {\n                            continue;\n                        }\n                        await binaryFileManager.addFile(id, field, file);\n                    }\n                }\n            }\n            if (!isTest) {\n                await binaryFileManager.sendLastPayload();\n            }\n        }\n    }\n\n    async _callImport(dryrun, args) {\n        try {\n            const res = await this.orm.silent.call(\"base_import.import\", \"execute_import\", args, {\n                dryrun,\n                context: {\n                    ...this.context,\n                    tracking_disable: this.importOptions.tracking_disable,\n                },\n            });\n            return res;\n        } catch (error) {\n            // This pattern isn't optimal but it is need to have\n            // similar behaviours as in legacy. That is, catching\n            // all import errors and showing them inside the top\n            // \"messages\" area.\n            return { error };\n        }\n    }\n\n    _handleImportErrors(messages, name) {\n        if (messages[0].not_matching_error) {\n            this._addMessage(messages[0].type, [messages[0].message]);\n            return true;\n        }\n\n        const sortedMessages = this._groupErrorsByField(messages);\n        if (sortedMessages[0]) {\n            this._addMessage(sortedMessages[0].type, [sortedMessages[0].message]);\n            delete sortedMessages[0];\n        } else {\n            this._addMessage(\"danger\", [_t(\"The file contains blocking errors (see below)\")]);\n        }\n\n        for (const [columnFieldId, errors] of Object.entries(sortedMessages)) {\n            // Handle errors regarding specific colums.\n            const column = this.columns.find(\n                (e) => e.fieldInfo && e.fieldInfo.fieldPath === columnFieldId\n            );\n            if (column) {\n                column.resultNames = name;\n                column.errors = errors;\n            } else {\n                for (const error of errors) {\n                    // Handle errors regarding specific records.\n                    if (error.record !== undefined) {\n                        this._addMessage(\"danger\", [\n                            error.rows.from === error.rows.to\n                                ? _t('Error at row %(row)s: \"%(error)s\"', {\n                                      row: error.record,\n                                      error: error.message,\n                                  })\n                                : _t(\"%s at multiple rows\", error.message),\n                        ]);\n                    }\n                    // Handle global errors.\n                    else {\n                        this._addMessage(\"danger\", [error.message]);\n                    }\n                }\n            }\n        }\n    }\n\n    _groupErrorsByField(messages) {\n        const groupedErrors = {};\n        const errorsByMessage = groupBy(this._sortErrors(messages), (f) => f.message || \"0\");\n        for (const [message, errors] of Object.entries(errorsByMessage)) {\n            if (!message.record) {\n                const foundError = errors.find((e) => e.record === undefined);\n                if (foundError) {\n                    groupedErrors[0] = foundError;\n                    continue;\n                }\n            }\n\n            errors[0].rows.to = errors[errors.length - 1].rows.to;\n            const fieldId = errors[0].field_path ? errors[0].field_path.join(\"/\") : errors[0].field;\n            if (groupedErrors[fieldId]) {\n                groupedErrors[fieldId].push(errors[0]);\n            } else {\n                groupedErrors[fieldId] = [errors[0]];\n            }\n        }\n        return groupedErrors;\n    }\n\n    _sortErrors(messages) {\n        return sortBy(messages, (e) => [\"error\", \"warning\", \"info\"].indexOf(e.priority));\n    }\n\n    /**\n     * On the preview data succesfuly loaded, update the\n     * import options, columns and messages.\n     * @param {*} res\n     */\n    _onLoadSuccess(res) {\n        // Set options\n        for (const key in res.options) {\n            if (this.importOptionsValues[key]) {\n                this.importOptionsValues[key].value = res.options[key];\n            }\n        }\n\n        if (!res.fields.length) {\n            this.importOptionsValues.advanced.value = res.advanced_mode;\n        }\n\n        this.fields = res.fields;\n        this.columns = this._getColumns(res);\n\n        // Set import messages\n        if (res.headers.length === 1) {\n            this._addMessage(\"warning\", [\n                _t(\n                    \"A single column was found in the file, this often means the file separator is incorrect.\"\n                ),\n            ]);\n        }\n\n        this._updateComments();\n    }\n\n    _onLoadError() {\n        this.columns = [];\n        this.importMessages = [];\n    }\n\n    _getColumns(res) {\n        function getId(res, index) {\n            return res.matches && index in res.matches && res.matches[index].length > 0\n                ? res.matches[index].join(\"/\")\n                : undefined;\n        }\n\n        if (this.importOptions.has_headers && res.headers && res.preview.length > 0) {\n            return res.headers.flatMap((header, index) =>\n                this._createColumn(\n                    res,\n                    getId(res, index),\n                    header,\n                    index,\n                    res.preview[index],\n                    res.preview[index][0]\n                )\n            );\n        } else if (res.preview && res.preview.length >= 2) {\n            return res.preview.flatMap((preview, index) =>\n                this._createColumn(\n                    res,\n                    preview[0],\n                    this.importOptions.has_headers ? preview[0] : preview.join(\", \"),\n                    index,\n                    preview,\n                    preview[1]\n                )\n            );\n        }\n        return [];\n    }\n\n    _createColumn(res, id, name, index, previews, preview) {\n        const fields = this._getFields(res, index);\n        return {\n            id,\n            name,\n            preview,\n            previews,\n            fields,\n            fieldInfo: this._findField(fields, id),\n            comments: [],\n            errors: [],\n        };\n    }\n\n    _findField(fields, id) {\n        return Object.entries(fields)\n            .flatMap((e) => e[1])\n            .find((field) => field.fieldPath === id);\n    }\n\n    /**\n     * Sort fields into their respective categories, namely:\n     * - Basic => Only the ID field\n     * - Suggested => Non-relational fields from the header\"s types\n     * - Additional => Non-relational fields of any other type\n     * - Relational => Relational fields\n     * @param {*} res\n     */\n    _getFields(res, index) {\n        const advanced = this.importOptionsValues.advanced.value;\n        const fields = {\n            basic: [],\n            suggested: [],\n            additional: [],\n            relational: [],\n        };\n\n        function isRegular(subfields) {\n            return (\n                !subfields ||\n                subfields.length === 0 ||\n                (subfields.length === 2 &&\n                    subfields[0].name === \"id\" &&\n                    subfields[1].name === \".id\")\n            );\n        }\n\n        function hasType(types, field) {\n            return types && types.indexOf(field.type) !== -1;\n        }\n\n        const sortSingleField = (field, ancestors, collection, types) => {\n            ancestors.push(field);\n            field.fieldPath = ancestors.map((f) => f.name).join(\"/\");\n            field.label = ancestors.map((f) => f.string).join(\" / \");\n\n            // Get field respective category\n            if (!collection) {\n                if (field.name === \"id\") {\n                    collection = fields.basic;\n                } else if (isRegular(field.fields)) {\n                    collection = hasType(types, field) ? fields.suggested : fields.additional;\n                } else {\n                    collection = fields.relational;\n                }\n            }\n\n            // Add field to found category\n            collection.push(field);\n\n            if (advanced) {\n                for (const subfield of field.fields) {\n                    sortSingleField(subfield, [...ancestors], collection, types);\n                }\n            }\n        };\n\n        // Sort fields in their respective categories\n        for (const field of this.fields) {\n            if (!field.isRelation) {\n                if (advanced) {\n                    sortSingleField(field, [], undefined, [\"all\"]);\n                } else {\n                    const acceptedTypes = res.header_types[index];\n                    sortSingleField(field, [], undefined, acceptedTypes);\n                }\n            }\n        }\n\n        return fields;\n    }\n\n    _updateComments(updatedColumn) {\n        for (const column of this.columns) {\n            column.comments = [];\n            column.errors = [];\n            column.resultNames = [];\n            column.importOptions =\n                column.fieldInfo && this.fieldsToHandle[column.fieldInfo.fieldPath];\n\n            if (!column.fieldInfo) {\n                continue;\n            }\n\n            // Fields of type \"char\", \"text\" or \"many2many\" can be specified multiple\n            // times and they will be concatenated, fields of other types must be unique.\n            if ([\"char\", \"text\", \"html\", \"many2many\"].includes(column.fieldInfo.type)) {\n                if (column.fieldInfo.type === \"many2many\") {\n                    column.comments.push({\n                        type: \"info\",\n                        content: _t(\"To import multiple values, separate them by a comma.\"),\n                    });\n                }\n\n                // If multiple columns are mapped on the same field, inform\n                // the user that they will be concatenated.\n                const samefieldColumns = this.columns.filter(\n                    (col) => col.fieldInfo && col.fieldInfo.fieldPath === column.fieldInfo.fieldPath\n                );\n                if (samefieldColumns.length >= 2) {\n                    column.comments.push({\n                        type: \"info\",\n                        content: _t(\"This column will be concatenated in field\"),\n                        fieldName: column.fieldInfo.string,\n                    });\n                }\n            } else if (updatedColumn && column.id !== updatedColumn.id && updatedColumn.fieldInfo) {\n                // If column is mapped on an already mapped field, remove that field\n                // from the old column to keep it unique.\n                if (updatedColumn.fieldInfo.fieldPath === column.fieldInfo.fieldPath) {\n                    column.fieldInfo = null;\n                }\n            }\n        }\n    }\n\n    _getCSVFormattingOptions() {\n        return {\n            encoding: {\n                label: _t(\"Encoding:\"),\n                type: \"select\",\n                value: \"\",\n                options: [\n                    \"utf-8\",\n                    \"utf-16\",\n                    \"windows-1252\",\n                    \"latin1\",\n                    \"latin2\",\n                    \"big5\",\n                    \"gb18030\",\n                    \"shift_jis\",\n                    \"windows-1251\",\n                    \"koi8_r\",\n                ],\n            },\n            separator: {\n                label: _t(\"Separator:\"),\n                type: \"select\",\n                value: \"\",\n                options: [\n                    { value: \",\", label: _t(\"Comma\") },\n                    { value: \";\", label: _t(\"Semicolon\") },\n                    { value: \"\\t\", label: _t(\"Tab\") },\n                    { value: \" \", label: _t(\"Space\") },\n                ],\n            },\n            quoting: {\n                label: _t(\"Text Delimiter:\"),\n                type: \"input\",\n                value: '\"',\n            },\n            date_format: {\n                help: _t(\n                    \"Use YYYY to represent the year, MM for the month and DD for the day. Include separators such as a dot, forward slash or dash. You can use a custom format in addition to the suggestions provided. Leave empty to let Odoo guess the format (recommended)\"\n                ),\n                label: _t(\"Date Format:\"),\n                type: \"input\",\n                value: \"\",\n                options: [\n                    \"YYYY-MM-DD\",\n                    \"YYYY/MM/DD\",\n                    \"DD/MM/YYYY\",\n                    \"DDMMYYYY\",\n                    \"MM/DD/YYYY\",\n                    \"MMDDYYYY\",\n                ],\n            },\n            datetime_format: {\n                help: _t(\n                    \"Use HH for hours in a 24h system, use II in conjonction with 'p' for a 12h system. You can use a custom format in addition to the suggestions provided. Leave empty to let Odoo guess the format (recommended)\"\n                ),\n                label: _t(\"Datetime Format:\"),\n                type: \"input\",\n                value: \"\",\n                options: [\n                    \"YYYY-MM-DD HH:mm:SS\",\n                    \"YYYY/MM/DD HH:mm:SS\",\n                    \"DD/MM/YYYY HH:mm:SS\",\n                    \"DDMMYYYY HH:mm:SS\",\n                    \"MM/DD/YYYY II:mm:SS p\",\n                    \"MMDDYYYY II:mm:SS p\",\n                ],\n            },\n            float_thousand_separator: {\n                label: _t(\"Thousands Separator:\"),\n                type: \"select\",\n                value: \",\",\n                options: [\n                    { value: \",\", label: _t(\"Comma\") },\n                    { value: \".\", label: _t(\"Dot\") },\n                    { value: \"\", label: _t(\"No Separator\") },\n                ],\n            },\n            float_decimal_separator: {\n                label: _t(\"Decimals Separator:\"),\n                type: \"select\",\n                value: \".\",\n                options: [\n                    { value: \",\", label: _t(\"Comma\") },\n                    { value: \".\", label: _t(\"Dot\") },\n                ],\n            },\n        };\n    }\n}\n\n/**\n * @returns {BaseImportModel}columns\n */\nexport function useImportModel({ env, context }) {\n    const orm = useService(\"orm\");\n    return useState(new BaseImportModel({ env, context, orm }));\n}\n", "import { Component } from \"@odoo/owl\";\nimport { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\nimport { registry } from \"@web/core/registry\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { exprToBoolean } from \"@web/core/utils/strings\";\nimport { STATIC_ACTIONS_GROUP_NUMBER } from \"@web/search/action_menus/action_menus\";\n\nconst cogMenuRegistry = registry.category(\"cogMenu\");\n\n/**\n * 'Import records' menu\n *\n * This component is used to import the records for particular model.\n * @extends Component\n */\nexport class ImportRecords extends Component {\n    static template = \"base_import.ImportRecords\";\n    static components = { DropdownItem };\n    static props = {};\n\n    setup() {\n        this.action = useService(\"action\");\n    }\n\n    //---------------------------------------------------------------------\n    // Protected\n    //---------------------------------------------------------------------\n\n    importRecords() {\n        const { context, resModel } = this.env.searchModel;\n        this.action.doAction({\n            type: \"ir.actions.client\",\n            tag: \"import\",\n            params: { active_model: resModel, context },\n        });\n    }\n}\n\nexport const importRecordsItem = {\n    Component: ImportRecords,\n    groupNumber: STATIC_ACTIONS_GROUP_NUMBER,\n    isDisplayed: ({ config, isSmall }) =>\n        !isSmall &&\n        config.actionType === \"ir.actions.act_window\" &&\n        [\"kanban\", \"list\"].includes(config.viewType) &&\n        exprToBoolean(config.viewArch.getAttribute(\"import\"), true) &&\n        exprToBoolean(config.viewArch.getAttribute(\"create\"), true),\n};\n\ncogMenuRegistry.add(\"import-menu\", importRecordsItem, { sequence: 1 });\n", "import { BaseImportModel } from \"@base_import/import_model\";\nimport { patch } from \"@web/core/utils/patch\";\nimport { _t } from \"@web/core/l10n/translation\";\n\npatch(BaseImportModel.prototype, {\n    async init() {\n        await super.init(...arguments);\n\n        if (this.resModel === \"account.bank.statement\") {\n            this.importTemplates.push({\n                label: _t(\"Import Template for Bank Statements\"),\n                template: \"/account_bank_statement_import/static/csv/account.bank.statement.csv\",\n            });\n        }\n    },\n});\n", "import { AccountFileUploader } from \"@account/components/account_file_uploader/account_file_uploader\";\nimport { BankRecKanbanController } from \"@account_accountant/components/bank_reconciliation/kanban_controller\";\nimport {\n    BankRecKanbanRenderer,\n    BankRecKanbanView,\n} from \"@account_accountant/components/bank_reconciliation/kanban_renderer\";\nimport { UploadDropZone } from \"@account/components/upload_drop_zone/upload_drop_zone\";\nimport { registry } from \"@web/core/registry\";\nimport { useState } from \"@odoo/owl\";\n\nconst synchronizedModes = [\"online_sync\", \"l10n_be_codabox\"]\n\nexport class BankRecKanbanUploadController extends BankRecKanbanController {\n    static components = {\n        ...BankRecKanbanController.components,\n        AccountFileUploader,\n    };\n\n    get showUploadButton() {\n        return !synchronizedModes.includes(this.props.context?.bank_statements_source);\n    }\n}\n\nexport class BankRecKanbanUploadRenderer extends BankRecKanbanRenderer {\n    static components = {\n        ...BankRecKanbanRenderer.components,\n        UploadDropZone,\n    };\n\n    setup() {\n        super.setup();\n        this.dropzoneState = useState({ visible: false });\n    }\n\n    onDragStart(ev) {\n        if (ev.dataTransfer.types.includes(\"Files\")) {\n            this.dropzoneState.visible = true;\n        }\n    }\n\n    get showUploadButton() {\n        return !synchronizedModes.includes(this.env.model.config.context?.bank_statements_source);\n    }\n}\n\nexport const bankRecKanbanUploadView = {\n    ...BankRecKanbanView,\n    Controller: BankRecKanbanUploadController,\n    Renderer: BankRecKanbanUploadRenderer,\n    buttonTemplate: \"account_accountant.BankRecoKanbanUploadButton\",\n};\n\nregistry.category(\"views\").add(\"bank_rec_widget_kanban\", bankRecKanbanUploadView, { force: true });\n", "import { registry } from \"@web/core/registry\";\nimport { ListRenderer } from \"@web/views/list/list_renderer\";\nimport { AccountFileUploader } from \"@account/components/account_file_uploader/account_file_uploader\";\nimport { UploadDropZone } from \"@account/components/upload_drop_zone/upload_drop_zone\";\nimport {\n    bankRecListView,\n    BankRecListController,\n    BankRecListRenderer,\n} from \"@account_accountant/components/bank_reconciliation/list_view/list\";\nimport { useState } from \"@odoo/owl\";\n\nconst synchronizedModes = [\"online_sync\", \"l10n_be_codabox\"]\n\nexport class BankRecListUploadController extends BankRecListController {\n    static components = {\n        ...BankRecListController.components,\n        AccountFileUploader,\n    };\n\n    get showUploadButton() {\n        return !synchronizedModes.includes(this.props.context?.bank_statements_source);\n    }\n}\n\nexport class BankRecListUploadRenderer extends BankRecListRenderer {\n    static template = \"account.BankRecListUploadRenderer\";\n    static components = {\n        ...ListRenderer.components,\n        UploadDropZone,\n    };\n\n    setup() {\n        super.setup();\n        this.dropzoneState = useState({ visible: false });\n    }\n\n    onDragStart(ev) {\n        if (ev.dataTransfer.types.includes(\"Files\")) {\n            this.dropzoneState.visible = true;\n        }\n    }\n\n    get showUploadButton() {\n        return !synchronizedModes.includes(this.env.model.config.context?.bank_statements_source);\n    }\n}\n\nexport const bankRecListUploadView = {\n    ...bankRecListView,\n    Controller: BankRecListUploadController,\n    Renderer: BankRecListUploadRenderer,\n    buttonTemplate: \"account.BankRecListUploadButtons\",\n};\n\nregistry.category(\"views\").add(\"bank_rec_list\", bankRecListUploadView, { force: true });\n", "import { onWillStart } from \"@odoo/owl\";\nimport { registry } from \"@web/core/registry\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { ImportAction } from \"@base_import/import_action/import_action\";\nimport { useBankStatementCSVImportModel } from \"./bank_statement_csv_import_model\";\nimport { x2ManyCommands } from \"@web/core/orm_service\";\n\nexport class BankStatementImportAction extends ImportAction {\n    setup() {\n        super.setup();\n\n        this.orm = useService(\"orm\");\n\n        this.model = useBankStatementCSVImportModel({\n            env: this.env,\n            context: this.props.action.params.context || {},\n        });\n\n        this.env.config.setDisplayName(_t(\"Import Bank Statement\")); // Displayed in the breadcrumbs\n        this.state.filename = this.props.action.params.filename || undefined;\n\n        onWillStart(async () => {\n            if (this.props.action.params.context) {\n                this.model.id = this.props.action.params.context.wizard_id;\n                await this.model.init();\n                await super.handleFilesUpload([{ name: this.state.filename }])\n            }\n        });\n    }\n\n    async openRecords(resIds) {\n        if (this.model.statement_id) {\n            const res = await this.orm.call(\n                \"account.bank.statement\",\n                \"action_open_bank_reconcile_widget\",\n                [this.model.statement_id]\n            );\n            return this.actionService.doAction(res);\n        }\n        const statementLines = await this.orm.searchRead(\n            \"account.bank.statement.line\",\n            [[\"id\", \"in\", resIds]],\n            [\"statement_id\"]\n        );\n        const statementIds = Array.from(\n            new Set(statementLines.map((statementLine) => statementLine.statement_id[0]))\n        );\n        await this.orm.write(\"account.bank.statement\", statementIds, {\n            attachment_ids: this.props.action.params.context.attachment_ids.map((attachment) =>\n                x2ManyCommands.link(attachment)\n            ),\n        });\n        super.openRecords(resIds);\n    }\n}\n\nregistry.category(\"actions\").add(\"import_bank_stmt\", BankStatementImportAction);\n", "import { useState } from \"@odoo/owl\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { BaseImportModel } from \"@base_import/import_model\";\n\nclass BankStatementCSVImportModel extends BaseImportModel {\n    async init() {\n        this.importOptionsValues.bank_stmt_import = {\n            value: true,\n        };\n        return Promise.resolve();\n    }\n\n    async _onLoadSuccess(res) {\n        super._onLoadSuccess(res);\n\n        if (!res.messages || res.messages.length === 0 || res.messages.length > 1) {\n            return;\n        }\n\n        const message = res.messages[0];\n        if (message.ids) {\n            this.statement_line_ids = message.ids\n        }\n\n        if (message.messages && message.messages.length > 0) {\n            this.statement_id = message.messages[0].statement_id\n        }\n    }\n}\n\n/**\n * @returns {BankStatementCSVImportModel}\n */\nexport function useBankStatementCSVImportModel({ env, context }) {\n    const orm = useService(\"orm\");\n    return useState(new BankStatementCSVImportModel({ env, context, orm }));\n}\n", "import { Component, useState } from \"@odoo/owl\";\n\nexport class Box extends Component {\n    static template = \"iap_extract.Box\";\n    static props = {\n        box: Object,\n        pageWidth: String,\n        pageHeight: String,\n        onClickBoxCallback: Function,\n    };\n    /**\n     * @override\n     */\n    setup() {\n        this.state = useState(this.props.box);\n    }\n\n    //--------------------------------------------------------------------------\n    // Public\n    //--------------------------------------------------------------------------\n\n    get style() {\n        const style = [\n            `left: calc(${this.state.midX} * ${this.props.pageWidth})`,\n            `top: calc(${this.state.midY} * ${this.props.pageHeight})`,\n            `width: calc(${this.state.width} * ${this.props.pageWidth})`,\n            `height: calc(${this.state.height} * ${this.props.pageHeight})`,\n            `transform: translate(-50%, -50%) rotate(${this.state.angle}deg)`,\n            `-ms-transform: translate(-50%, -50%) rotate(${this.state.angle}deg)`,\n            `-webkit-transform: translate(-50%, -50%) rotate(${this.state.angle}deg)`,\n        ].join('; ');\n        return style;\n    }\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    onClick() {\n        this.props.onClickBoxCallback(this.state.id, this.state.page);\n    }\n};\n", "import { Box } from '@iap_extract/components/manual_correction/box';\nimport { Component, useExternalListener, useRef, useState } from \"@odoo/owl\";\n\nexport class BoxLayer extends Component {\n    static components = { Box };\n    static template = \"iap_extract.BoxLayer\";\n    static props = {\n        boxes: Array,\n        pageLayer: {\n            validate: (pageLayer) => {\n                // target may be inside an iframe, so get the Element constructor\n                // to test against from its owner document's default view\n                const Element = pageLayer?.ownerDocument?.defaultView?.Element;\n                return (\n                    (Boolean(Element) &&\n                        (pageLayer instanceof Element || pageLayer instanceof window.Element)) ||\n                    (typeof pageLayer === \"object\" && pageLayer?.constructor?.name?.endsWith(\"Element\"))\n                );\n            },\n        },\n        onClickBoxCallback: Function,\n        onBoxesSelectionCallback: Function,\n        mode: String,\n    };\n    /**\n     * @override\n     */\n    setup() {\n        this.state = useState({\n            boxes: this.props.boxes,\n            isSelecting: false,\n            selectionStart: { x: 0, y: 0 },\n            selectionEnd: { x: 0, y: 0 },\n        });\n        this.boxLayerRef = useRef(\"boxLayer\");\n\n        // Used to define the style of the contained boxes\n        if (this.isOnPDF) {\n            this.pageWidth = this.props.pageLayer.style.width;\n            this.pageHeight = this.props.pageLayer.style.height;\n\n            // Get the scrollable element of the PDF viewer to listen to scroll events\n            this.viewerScrollableEl = this.props.pageLayer.ownerDocument.getElementById(\"viewerContainer\");\n            useExternalListener(this.viewerScrollableEl, \"scroll\", this.onScroll);\n        } else if (this.isOnImg) {\n            this.viewerScrollableEl = this.props.pageLayer.parentElement;\n            useExternalListener(this.viewerScrollableEl, \"scroll\", this.onScroll);\n            this.pageWidth = `${this.props.pageLayer.clientWidth}px`;\n            this.pageHeight = `${this.props.pageLayer.clientHeight}px`;\n        }\n    }\n\n    checkAndHighlightBoxes() {\n        const { selectionStart, selectionEnd } = this.state;\n        const selectionRect = {\n            left: Math.min(selectionStart.x, selectionEnd.x),\n            top: Math.min(selectionStart.y, selectionEnd.y),\n            right: Math.max(selectionStart.x, selectionEnd.x),\n            bottom: Math.max(selectionStart.y, selectionEnd.y),\n        };\n\n        const boxElements = this.boxLayerRef.el.querySelectorAll('.o_extract_mixin_box');\n\n        boxElements.forEach(boxEl => {\n            const boxId = parseInt(boxEl.dataset.id, 10);\n            const matchedBox = this.props.boxes.find(box => box.id === boxId);\n            const boxRect = boxEl.getBoundingClientRect();\n\n            const isOverlapping = !(\n                boxRect.right < selectionRect.left ||\n                boxRect.left > selectionRect.right ||\n                boxRect.bottom < selectionRect.top ||\n                boxRect.top > selectionRect.bottom\n            );\n            matchedBox.isHighlighted = isOverlapping;\n        });\n    }\n\n    //--------------------------------------------------------------------------\n    // Public\n    //--------------------------------------------------------------------------\n\n    get style() {\n        if (this.isOnPDF) {\n            return 'width: ' + this.props.pageLayer.style.width + '; ' +\n                   'height: ' + this.props.pageLayer.style.height + ';';\n        } else if (this.isOnImg) {\n            return 'width: ' + this.props.pageLayer.clientWidth + 'px; ' +\n                   'height: ' + this.props.pageLayer.clientHeight + 'px; ' +\n                   'left: ' + this.props.pageLayer.offsetLeft + 'px; ' +\n                   'top: ' + this.props.pageLayer.offsetTop + 'px;';\n        }\n    }\n\n    get selectionStyle() {\n        const { selectionStart, selectionEnd } = this.state;\n        const x1 = Math.min(selectionStart.x, selectionEnd.x);\n        const y1 = Math.min(selectionStart.y, selectionEnd.y);\n        const x2 = Math.max(selectionStart.x, selectionEnd.x);\n        const y2 = Math.max(selectionStart.y, selectionEnd.y);\n\n        return `\n            left: ${x1}px;\n            top: ${y1}px;\n            width: ${x2 - x1}px;\n            height: ${y2 - y1}px;\n        `;\n    }\n\n    get isOnImg() {\n        return this.props.mode === 'img';\n    }\n\n    get isOnPDF() {\n        return this.props.mode === 'pdf';\n    }\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    onMouseDown(event) {\n        this.state.isSelecting = true;\n        this.state.selectionStart = { x: event.clientX, y: event.clientY};\n        this.state.selectionEnd = { x: event.clientX, y: event.clientY};\n        if (this.viewerScrollableEl) {\n            this.scrollX = this.viewerScrollableEl.scrollLeft;\n            this.scrollY = this.viewerScrollableEl.scrollTop;\n        }\n    }\n\n    onMouseUp(event) {\n        if (!this.state.isSelecting) {\n            return;\n        }\n\n        this.state.isSelecting = false;\n\n        this.checkAndHighlightBoxes();\n\n        const selectedBoxes = this.props.boxes.filter(box => box.isHighlighted);\n        this.props.onBoxesSelectionCallback(selectedBoxes);\n\n        this.props.boxes.forEach(box => box.isHighlighted = false);\n        this.state.selectionStart = { x: 0, y: 0 };\n        this.state.selectionEnd = { x: 0, y: 0 };\n    }\n\n    onMouseMove(event) {\n        if (!this.state.isSelecting) {\n            return;\n        }\n        this.state.selectionEnd = { x: event.clientX, y: event.clientY };\n        this.checkAndHighlightBoxes();\n    }\n\n    onScroll(event) {\n        if (this.state.isSelecting) {\n            // Adjust the selection on scroll\n            const scrollX = this.viewerScrollableEl.scrollLeft;\n            const scrollY = this.viewerScrollableEl.scrollTop;\n\n            this.state.selectionStart.x += this.scrollX - scrollX;\n            this.state.selectionStart.y += this.scrollY - scrollY;\n\n            this.scrollX = scrollX;\n            this.scrollY = scrollY;\n        }\n    }\n};\n", "import { appTranslateFn } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { getTemplate } from \"@web/core/templates\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { App, onWillUnmount, reactive, useExternalListener, useState } from \"@odoo/owl\";\n\nimport { BoxLayer } from '@iap_extract/components/manual_correction/box_layer';\n\n\nconst BOX_TYPES = ['word', 'number', 'date'];\nconst MIN_OVERLAP_RATIO = 0.25;  // The minimum overlap ratio required for two boxes to be considered aligned\nconst FIELD_TO_BOX_TYPE_MAPPING = {\n    'char': 'word',\n    'text': 'word',\n    'html': 'word',\n    'many2one': 'word',\n    'integer': 'number',\n    'float': 'number',\n    'monetary': 'number',\n    'date': 'date',\n    'datetime': 'date',\n};\n\n/**\n * This is the renderer mixin of the subview that adds OCR features on the attachment\n * preview. It displays boxes that have been generated by the OCR. The user can manually\n * select boxes to fill in the fields of the record.\n * T should be a subclass of FormRenderer (e.g. invoice_extract_form.js or bank_statement_extract_form.js)\n */\nexport const ExtractMixinFormRenderer = (T) => class extends T {\n    /**\n     * @override\n     */\n    setup() {\n        super.setup();\n\n        /** @type {import(\"@mail/core/common/store_service\").Store} */\n        this.store = useService(\"mail.store\");\n        this.dialog = useService(\"dialog\");\n        this.orm = useService(\"orm\");\n        this.mailPopoutService = useService(\"mail.popout\");\n\n        this.recordModel = null; // to override in the subclass\n\n        // This contain the record id of the fetched data.\n        // It needs to be tracked as, if another record is loaded, we should fetch the data of the new record.\n        this.recordId = -1;\n\n        this.boxLayerApps = [];\n        this.activeField;\n        this.activeFieldEl;\n        this.activeBoxType;\n        this.boxes = {};\n        this.skewAngles = {};\n        this.isShiftPressed = false;\n\n        // When a x2many line is created, keep it in memory so that, on the next selection of multiple boxes\n        // on one of the x2many field, we can match which box belongs to which line.\n        this.x2ManyLines = {};\n\n        this.state = useState({\n            visibleBoxes: {},\n        });\n\n        useExternalListener(window, \"focusin\", (event) => {\n            const fieldWidget = event.target.closest(\".o_field_widget,.o_field_cell\");\n            if (fieldWidget){\n                this.onFocusFieldWidget(fieldWidget);\n            }\n        });\n\n        useExternalListener(window, \"focusout\", (event) => {\n            if (event.target.closest(\".o_field_widget\") && !this.mailPopoutService.externalWindow){\n                this.onBlurFieldWidget();\n            }\n        });\n\n        useExternalListener(window, \"keydown\", (event) => {\n            if (event.key === 'Shift') {\n                this.isShiftPressed = true;\n            }\n        });\n\n        useExternalListener(window, \"keyup\", (event) => {\n            if (event.key === 'Shift') {\n                this.isShiftPressed = false;\n            }\n        });\n\n        useExternalListener(window, \"pointerdown\", (event) => {\n            // For date fields, a calendar popup appear and listens on the pointerdown event to hide itself when a\n            // click occurs outside of it. This causes the loss of focus of the field and thus hides the boxes.\n            if (this.activeField && event.target.closest('.o-mail-Attachment')) {\n                event.stopImmediatePropagation();\n            }\n        }, { capture: true });\n\n        onWillUnmount (() => {\n            this.destroyBoxLayers();\n        });\n    }\n\n    /**\n     * Launch an Owl App with the box layer as root component.\n     */\n    createBoxLayerApp(props) {\n        props.onClickBoxCallback = this.onClickBox.bind(this);\n        props.onBoxesSelectionCallback = this.onBoxesSelection.bind(this);\n        return new App(BoxLayer, {\n            env: this.env,\n            dev: this.env.debug,\n            getTemplate,\n            props,\n            translatableAttributes: [\"data-tooltip\"],\n            translateFn: appTranslateFn,\n        });\n    }\n\n    /**\n     * Renders the box layers on @element.\n     * If a box layer already exists, it is re-used.\n     */\n    renderBoxLayers(element) {\n        const proms = [];\n        // In case of img\n        if (element.classList.contains('img-fluid')) {\n            this.destroyBoxLayers();\n            const boxLayerApp = this.createBoxLayerApp({\n                boxes: this.state.visibleBoxes[0] || [],\n                mode: 'img',\n                pageLayer: element,\n            });\n            proms.push(boxLayerApp.mount(element.parentElement));\n            this.boxLayerApps = [boxLayerApp];\n        }\n        // In case of pdf\n        if (element.tagName === 'IFRAME') {\n            // Dynamically add css on the pdf viewer\n            const pdfDocument = element.contentDocument;\n            if (!pdfDocument.querySelector('head link#box_layer')) {\n                const win = this.mailPopoutService.externalWindow || window;\n                const boxLayerStylesheet = win.document.createElement('link');\n                boxLayerStylesheet.setAttribute('id', 'box_layer');\n                boxLayerStylesheet.setAttribute('rel', 'stylesheet');\n                boxLayerStylesheet.setAttribute('type', 'text/css');\n                boxLayerStylesheet.setAttribute('href', '/iap_extract/static/src/components/manual_correction/box_layer.css');\n                pdfDocument.querySelector('head').append(boxLayerStylesheet);\n            }\n            const pageLayers = pdfDocument.querySelectorAll('.page');\n            for (const pageLayer of pageLayers) {\n                const pageNum = pageLayer.dataset['pageNumber'] - 1;\n                const boxLayerApp = this.createBoxLayerApp({\n                    boxes: this.state.visibleBoxes[pageNum] || [],\n                    mode: 'pdf',\n                    pageLayer: pageLayer,\n                });\n                proms.push(boxLayerApp.mount(pageLayer));\n                this.boxLayerApps.push(boxLayerApp);\n            }\n        }\n        return Promise.all(proms);\n    }\n\n    shouldRenderBoxes() {\n        const thread = this.store.Thread.insert({\n            id: this.props.record.resId,\n            model: this.props.record.resModel,\n        });\n        return (\n            [\"waiting_validation\", \"validation_to_send\"].includes(\n                this.props.record.data.extract_state\n            ) &&\n            this.props.record.data.extract_attachment_id &&\n            thread.message_main_attachment_id.id === this.props.record.data.extract_attachment_id.id\n        );\n    }\n\n    /**\n     * Renders the boxes on @attachment.\n     * It also determines which boxes should be visible according to the current active field.\n     */\n    renderExtract(attachment) {\n        if (this.shouldRenderBoxes()) {\n            if (this.activeField !== undefined) {\n                const dataToFetch = this.recordId !== this.props.record.resId;\n                if (dataToFetch) {\n                    this.orm.call(this.recordModel, 'get_boxes', [this.props.record.resId]).then((boxes) => {\n                        this.recordId = this.props.record.resId;\n                        this.boxes = reactive(boxes);\n\n                        // Compute the skew angle of each page, it will be used to compute box alignment\n                        const boxesPerPage = {};\n                        for (const boxType of BOX_TYPES) {\n                            for (const [pageNumber, pageBoxes] of Object.entries(boxes[boxType])) {\n                                if (!boxesPerPage[pageNumber]) {\n                                    boxesPerPage[pageNumber] = [];\n                                }\n                                boxesPerPage[pageNumber].push(...pageBoxes);\n                            }\n                        }\n                        for (const [pageNumber, pageBoxes] of Object.entries(boxesPerPage)) {\n                            this.skewAngles[pageNumber] = this.median(pageBoxes.map(box => box.angle));\n                        }\n\n                        this.state.visibleBoxes = this.boxes[this.activeBoxType] || {};\n                        this.renderBoxLayers(attachment);\n                    });\n                }\n                else {\n                    this.state.visibleBoxes = this.boxes[this.activeBoxType] || {};\n                    this.renderBoxLayers(attachment);\n                }\n            }\n        }\n    }\n\n    /**\n     * Determines the DOM element on which the boxes must be rendered, then render them.\n     */\n    showBoxes() {\n        // Case pdf (iframe)\n        const win = this.mailPopoutService.externalWindow || window;\n        const iframe = win.document.querySelector('.o-mail-Attachment iframe');\n        if (iframe) {\n            const iframeDoc = iframe.contentDocument;\n            if (iframeDoc) {\n                this.renderExtract(iframe);\n                return;\n            }\n        }\n        // Case img\n        const attachment = win.document.getElementById('attachment_img');\n        if (attachment && attachment.complete) {\n            this.renderExtract(attachment);\n            return;\n        }\n    }\n\n    resetActiveField() {\n        this.activeField = undefined;\n        this.activeBoxType = undefined;\n        this.activeFieldEl = undefined;\n        this.destroyBoxLayers();\n    }\n\n    destroyBoxLayers() {\n        for (const boxLayerApp of this.boxLayerApps) {\n            try {\n                boxLayerApp.destroy();\n            } catch {}\n        }\n        this.boxLayerApps = [];\n    }\n\n    median(arr) {\n        const sortedArr = arr.slice().sort((a, b) => a - b);\n        const middle = Math.floor(sortedArr.length / 2);\n        if (sortedArr.length % 2 === 0) {\n            return (sortedArr[middle - 1] + sortedArr[middle]) / 2;\n        } else {\n            return sortedArr[middle];\n        }\n    }\n\n    unskewBoxes(boxes, skewAngle) {\n        const unskewedBoxes = JSON.parse(JSON.stringify(boxes));\n\n        const angleInRadians = -skewAngle * (Math.PI / 180);\n\n        const cosAngle = Math.cos(angleInRadians);\n        const sinAngle = Math.sin(angleInRadians);\n\n        // The coordinates of the boxes are normalized within [0, 1], 0.5 represents the center of the axis\n        const centerX = 0.5;\n        const centerY = 0.5;\n\n        for (const box of unskewedBoxes) {\n            // Translate the box's midpoint to be relative to the page's center\n            const translatedX = box.midX - centerX;\n            const translatedY = box.midY - centerY;\n\n            // Apply the rotation formula\n            const rotatedX = translatedX * cosAngle - translatedY * sinAngle;\n            const rotatedY = translatedX * sinAngle + translatedY * cosAngle;\n\n            // Translate the coordinates back to the original system\n            const unskewedMidX = rotatedX + centerX;\n            const unskewedMidY = rotatedY + centerY;\n\n            box.minX = unskewedMidX - box.width / 2;\n            box.midX = unskewedMidX;\n            box.maxX = unskewedMidX + box.width / 2;\n\n            box.minY = unskewedMidY - box.height / 2;\n            box.midY = unskewedMidY;\n            box.maxY = unskewedMidY + box.height / 2;\n        }\n        return unskewedBoxes;\n    }\n\n    getBoxType(fullFieldName) {\n        if (!fullFieldName) {\n            return false;\n        }\n        let modelFieldType;\n        if (fullFieldName.includes('.')) {\n            const [parentField, fieldName] = fullFieldName.split('.');\n            modelFieldType = this.props.record.data[parentField]?._config.fields[fieldName]?.type;\n        }\n        else {\n            modelFieldType = this.props.record.fields[fullFieldName]?.type;\n        }\n        return FIELD_TO_BOX_TYPE_MAPPING[modelFieldType];\n    }\n\n    /**\n     * Updates the field's value according to @newFieldValue.\n     */\n    async handleFieldChanged(field, newFieldValue) {\n        if (field.type === 'many2one') {\n            // Use dedicated function for currencies\n            if (field.relation === 'res.currency') {\n                const currencyId = await this.orm.call(\n                    this.recordModel,\n                    'get_currency_from_text',\n                    [this.props.record.resId, newFieldValue],\n                );\n                if (currencyId) {\n                    return { id: currencyId };\n                }\n                return;\n            }\n\n            // General case, perform a name search\n            const results = await this.orm.call(field.relation, \"name_search\", [], {\n                name: newFieldValue,\n                operator: \"ilike\",\n                limit: 2,\n            });\n            if (results.length === 1) {\n                return { id: results[0][0] };\n            }\n            return;\n        }\n        return newFieldValue;\n    }\n\n    async getNewRecordValues(record, line, field, boxType) {\n        const value = await this.handleFieldChanged(\n            record.fields[field],\n            this.getValueFromBoxes(line.boxes, boxType),\n        );\n        return { [field]: value };\n    }\n\n    getFullFieldName(fieldEl) {\n        let fullFieldName = fieldEl.getAttribute('name');\n\n        const parentField = fieldEl.parentElement.closest('.o_field_widget');\n        if (parentField) {\n            fullFieldName = `${parentField.getAttribute('name')}.${fullFieldName}`;\n        }\n        return fullFieldName;\n    }\n\n    getValueFromBoxes(boxes, type) {\n        let newValue = boxes.map(box => box.text).join(\" \");\n        if (type === 'word') {\n            // The words are joined with a space, but characters such as ',', '.', etc shouldn't have spaces around them\n            const toReplace = {\n                \" .\": \".\",\n                \" ,\": \",\",\n                \" :\": \":\",\n                \"( \": \"(\",\n                \" )\": \")\",\n                \"[ \": \"[\",\n                \" ]\": \"]\",\n                \" -\": \"-\",\n                \"- \": \"-\",\n            }\n            for (const [pattern, replacement] of Object.entries(toReplace)) {\n                newValue = newValue.replaceAll(pattern, replacement);\n            }\n        }\n        else if (type === 'date') {\n            newValue = registry.category(\"parsers\").get(\"date\")(newValue.split(' ')[0]);\n        }\n        else if (type === 'number') {\n            newValue = registry.category(\"parsers\").get(\"float\")(newValue.split(' ')[0]);\n        }\n        return newValue;\n    }\n\n    isPartOfLine(line, element) {\n        return this.isPartOfAxis(line, element, 'Y');\n    }\n\n    isPartOfAxis(axis, element, orientation) {\n        if (axis.page != element.page) {\n            return false;\n        }\n\n        const min = 'min' + orientation;\n        const max = 'max' + orientation;\n\n        const lengthAxis = axis[max] - axis[min];\n        const lengthBox = element[max] - element[min];\n\n        const overlap = Math.max(0, Math.min(axis[max], element[max]) - Math.max(axis[min], element[min]));\n        return overlap / Math.min(lengthAxis, lengthBox) >= MIN_OVERLAP_RATIO;\n    }\n\n    groupBoxesByLine(boxes) {\n        return this.groupBoxesByAxis(boxes, 'Y');\n    }\n\n    groupBoxesByColumn(boxes) {\n        return this.groupBoxesByAxis(boxes, 'X');\n    }\n\n    groupBoxesByAxis(boxes, orientation) {\n        if (!boxes.length) {\n            return [];\n        }\n\n        const boxesCopy = JSON.parse(JSON.stringify(boxes));\n\n        const min = 'min' + orientation;\n        const max = 'max' + orientation;\n\n        boxesCopy.sort((a, b) => a[min] - b[min]);\n\n        let axes = []\n        let currentAxis = {\n            'boxes': [boxesCopy[0]],\n            [min]: boxesCopy[0][min],\n            [max]: boxesCopy[0][max],\n            'page': boxesCopy[0].page,\n        }\n\n        boxesCopy.slice(1, boxesCopy.length).forEach((box) => {\n            if (this.isPartOfAxis(currentAxis, box, orientation)) {\n                currentAxis.boxes.push(box);\n                currentAxis[min] = Math.min(currentAxis[min], box[min]);\n                currentAxis[max] = Math.max(currentAxis[max], box[max]);\n            } else {\n                axes.push(currentAxis);\n                currentAxis = {\n                    'boxes': [box],\n                    [min]: box[min],\n                    [max]: box[max],\n                    'page': box.page,\n                }\n            }\n        });\n        axes.push(currentAxis);\n\n        const otherOrientation = orientation === 'X' ? 'Y' : 'X';\n        axes.forEach((axis) => axis.boxes.sort((a, b) => a['min' + otherOrientation] - b['min' + otherOrientation]));\n\n        return axes;\n    }\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n     * Called when a field widget gains focus.\n     * It serves as the entry point to render the boxes of the focused field.\n     */\n    onFocusFieldWidget(fieldWidget) {\n        const fullFieldName = this.getFullFieldName(fieldWidget);\n        const fieldType = this.getBoxType(fullFieldName);\n        if (!fieldType) {\n            this.resetActiveField();\n            return;\n        }\n\n        this.activeField = fullFieldName;\n        this.activeBoxType = fieldType;\n        this.activeFieldEl = fieldWidget;\n\n        this.showBoxes();\n    }\n\n    /**\n     * Called when a field widget loses focus.\n     * It hides all boxes.\n     */\n    onBlurFieldWidget() {\n        this.resetActiveField();\n    }\n\n    async onClickBox(boxId, boxPage) {\n        const box = this.boxes[this.activeBoxType][boxPage].find(box => box.id === boxId);\n        return this.onBoxesSelection([box]);\n    }\n\n    async onBoxesSelection(boxes) {\n        if (!boxes.length) {\n            return;\n        }\n\n        const pageNumber = boxes[0].page;\n        const unskewedBoxes = this.unskewBoxes(boxes, this.skewAngles[pageNumber]);\n\n        let recordToUpdate;\n        let fieldToUpdate;\n        let newValue;\n\n        // Used if the field is a x2many\n        let parentField;\n        let rowIndex;\n\n        if (this.activeField.includes('.')) {\n            [parentField, fieldToUpdate] = this.activeField.split('.');\n\n            // Find the index of the row that is being edited\n            const parentEl = this.activeFieldEl.closest('tbody');\n            const childrenArray = Array.from(parentEl.children);\n            rowIndex = childrenArray.indexOf(this.activeFieldEl.closest('tr'));\n\n            recordToUpdate = this.props.record.data[parentField].records[rowIndex];\n        }\n        else {\n            recordToUpdate = this.props.record;\n            fieldToUpdate = this.activeField;\n        }\n\n        let lines = this.groupBoxesByLine(unskewedBoxes);\n        let linesHandled = false;\n\n        // If x2many field and multiple boxes selected\n        if (parentField && lines.length > 1) {\n            // For dates and numbers, it doesn't make sense to have multiple boxes selected on the same line.\n            // So, group them by columns and use the boxes of the column that has most of them.\n            if (['date', 'number'].includes(this.activeBoxType)) {\n                const columns = this.groupBoxesByColumn(unskewedBoxes);\n                const columnWithMostBoxes = columns.reduce((prev, current) => {\n                    return (prev && prev.boxes.length > current.boxes.length) ? prev : current;\n                })\n                if (columnWithMostBoxes.boxes.length < boxes.length / 2) {\n                    // The column should contain at least half of the selected boxes, otherwise, do nothing\n                    return;\n                }\n                lines = this.groupBoxesByLine(columnWithMostBoxes.boxes);\n\n                // Ensure each line contains a single box (should be the case, unless the boxes overlap)\n                lines.forEach((line) => {\n                    line.boxes = [line.boxes[0]];\n                })\n            }\n\n            // If there are lines created by using this tool, try to match the selected boxes by checking for aligned content\n            if (this.x2ManyLines[parentField]) {\n                // Synchronize x2ManyLines by only keeping the lines that are still present (some could have been deleted by the user)\n                this.x2ManyLines[parentField] = this.x2ManyLines[parentField].filter((x2ManyLine) => {\n                    return this.props.record.data[parentField].records.some((record) => record.id === x2ManyLine.record.id);\n                });\n\n                const existingLines = this.x2ManyLines[parentField].filter((x2ManyLine) => x2ManyLine.line.page === pageNumber);\n                existingLines.sort((a, b) => a.line.minY - b.line.minY);\n                let i = 0;\n                const updates = {};\n                lines.forEach(async (line) => {\n                    while (existingLines[i] && existingLines[i].line.maxY < line.minY) {\n                        i++;\n                    }\n                    const existingLine = existingLines[i];\n                    if (existingLine && this.isPartOfLine(existingLine.line, line)) {\n                        linesHandled = true;\n                        updates[i] = this.getValueFromBoxes(line.boxes, this.activeBoxType);\n                        i++;\n                    }\n                    else if (this.activeBoxType === 'word' && updates[i - 1]) {\n                        // Include it in the previous line, if something was matched.\n                        // This is meant to handle description spanning multiple lines.\n                        updates[i - 1] += \" \" + this.getValueFromBoxes(line.boxes, this.activeBoxType);\n                    }\n                });\n                for (const [j, value] of Object.entries(updates)) {\n                    this.handleFieldChanged(\n                        existingLines[j].record.fields[fieldToUpdate],\n                        value,\n                    ).then((newValue) => {\n                        existingLines[j].record.update({ [fieldToUpdate]: value });\n                    })\n                }\n            }\n\n            // For dates and numbers, if multiple boxes are selected, we create one line for each of them\n            if (!linesHandled && ['date', 'number'].includes(this.activeBoxType)) {\n                linesHandled = true;\n\n                const boxType = this.activeBoxType;\n                // Update the current record\n                this.getNewRecordValues(recordToUpdate, lines[0], fieldToUpdate, boxType).then((recordValues) => {\n                    recordToUpdate.update(recordValues);\n                });\n                if (!this.x2ManyLines[parentField]) {\n                    this.x2ManyLines[parentField] = [];\n                }\n                this.x2ManyLines[parentField].push({\n                    'record': recordToUpdate,\n                    'line': lines[0],\n                });\n\n                // Create a new record for each additional line\n                lines.slice(1, lines.length).forEach((line) => {\n                    this.props.record.data[parentField].addNewRecord({ mode: 'readonly', position: 'bottom' }).then(async (newRecord) => {\n                        const recordValues = await this.getNewRecordValues(newRecord, line, fieldToUpdate, boxType);\n                        newRecord.update(recordValues);\n                        this.x2ManyLines[parentField].push({\n                            'record': newRecord,\n                            'line': line,\n                        });\n                    });\n                });\n            }\n        }\n\n        if (!linesHandled) {\n            if (boxes.length > 1 && ['date', 'number'].includes(this.activeBoxType)) {\n                // Selection of multiple date/number boxes is only supported for x2many fields\n                return;\n            }\n            newValue = this.getValueFromBoxes(boxes, this.activeBoxType);\n\n            if (this.activeBoxType === 'word') {\n                if (this.isShiftPressed) {\n                    // When the shift key is pressed, append the value instead of overwriting\n                    const currentValue = recordToUpdate.data[fieldToUpdate];\n                    newValue = `${currentValue} ${newValue}`;\n                }\n            }\n            newValue = await this.handleFieldChanged(\n                recordToUpdate.fields[fieldToUpdate],\n                newValue,\n            );\n            if (newValue) {\n                recordToUpdate.update({ [fieldToUpdate]: newValue });\n            }\n        }\n\n        if (this.activeBoxType === 'date') {\n            // For the date fields, we want to hide the calendar tooltip\n            // This is achieved by simulating an 'ESC' keypress\n            this.activeFieldEl.querySelector('input').dispatchEvent(new KeyboardEvent('keydown', {\n                key: 'Escape',\n            }));\n        }\n\n        // Set a background that will slowly fade out to let the user know the boxes have been selected\n        boxes.forEach((box) => box.backgroundFadeout = true);\n        setTimeout(() => {\n            boxes.forEach((box) => box.backgroundFadeout = false);\n        }, 10);\n\n    }\n};\n", "import { Component, onWillDestroy, onWillStart, onWillUpdateProps, useState } from \"@odoo/owl\";\nimport { useRecordObserver } from \"@web/model/relational_model/utils\";\nimport { standardFieldProps } from \"@web/views/fields/standard_field_props\";\n\nimport { useService } from \"@web/core/utils/hooks\";\nimport { registry } from \"@web/core/registry\";\n\nconst CHECK_OCR_WAIT_DELAY = 5*1000;\n\nexport class StatusHeader extends Component {\n    static template = \"account_invoice_extract.Status\";\n    static props = standardFieldProps;\n\n    setup() {\n        this.state = useState({\n            status: this.props.record.data.extract_state,\n            errorMessage: this.props.record.data.extract_error_message,\n            retryLoading: false,\n            checkStatusLoading: false,\n        });\n        this.orm = useService(\"orm\");\n        this.action = useService(\"action\");\n        this.busService = this.env.services.bus_service;\n\n        useRecordObserver(async (record) => {\n            const extract_status = record.data.extract_state;\n            if (extract_status !== this.state.status) {\n                this.state.status = extract_status;\n                this.state.errorMessage = record.data.extract_error_message;\n            }\n            const extract_document_uuid = record.data.extract_document_uuid;\n            if (extract_document_uuid && !this.channelName) {\n                this.subscribeToChannel(extract_document_uuid);\n                this.enableTimeout();\n            }\n        });\n\n        onWillStart(() => {\n            // When a new document is uploaded (via the Upload button, or by attaching a file),\n            // the server will send a `extract_mixin_new_document` message on the bus with the\n            // `extract_document_uuid` created upon submitting the document to the OCR server.\n            // We can then subscribe for state changes event of this document uuid\n            // (typically when OCR has finished processing it)\n            this.busService.subscribe(\"extract_mixin_new_document\", (params) => {\n                this.state.status = params.status;\n                this.state.errorMessage = params.error_message;\n                this.subscribeToChannel(params.extract_document_uuid);\n            });\n            this.busService.subscribe(\"state_change\", ({status, error_message})=> {\n                this.state.status = status;\n                this.state.errorMessage = error_message;\n            });\n            this.enableTimeout();\n        });\n\n        onWillDestroy(() => {\n            this.busService.deleteChannel(this.channelName);\n            this.state.status = 'no_extract_requested';\n            clearTimeout(this.timeoutId);\n        });\n\n        onWillUpdateProps((nextProps) => {\n            if (nextProps.record.id !== this.props.record.id) {\n                this.state.errorMessage = nextProps.record.data.extract_error_message;\n                this.state.status = nextProps.record.data.extract_state;\n                this.subscribeToChannel(nextProps.record.data.extract_document_uuid);\n                this.enableTimeout();\n            }\n        });\n    }\n\n    subscribeToChannel(documentUUID) {\n        if (!documentUUID) {\n            return;\n        }\n        this.busService.deleteChannel(this.channelName);\n        this.channelName = `extract.mixin.status#${documentUUID}`;\n        this.busService.addChannel(this.channelName);\n    }\n\n    enableTimeout () {\n        if (!['waiting_extraction', 'extract_not_ready'].includes(this.state.status)) {\n            return;\n        }\n\n        clearTimeout(this.timeoutId);\n\n        this.timeoutId = setTimeout(async () => {\n            if (['waiting_extraction', 'extract_not_ready'].includes(this.state.status)) {\n                const [status, errorMessage] = (await this.orm.call(\n                    this.props.record.resModel,\n                    \"check_ocr_status\",\n                    [this.props.record.resId],\n                    {}\n                ))[0];\n                this.state.status = status;\n                this.state.errorMessage = errorMessage;\n            }\n        }, CHECK_OCR_WAIT_DELAY);\n    }\n\n    async checkOcrStatus() {\n        this.state.checkStatusLoading = true;\n        const [status, errorMessage] = (await this.orm.call(\n            this.props.record.resModel,\n            \"check_ocr_status\",\n            [this.props.record.resId],\n            {}\n        ))[0];\n        if (status === \"waiting_validation\") {\n            await this.refreshPage();\n            return;\n        }\n        this.state.status = status;\n        this.state.errorMessage = errorMessage;\n        this.state.checkStatusLoading = false;\n    }\n\n    async refreshPage() {\n        return this.props.record.model.load()\n    }\n\n    async buyCredits() {\n        const actionData = await this.orm.call(this.props.record.resModel, \"buy_credits\", [this.props.record.resId], {});\n        this.action.doAction(actionData);\n    }\n\n    async retryDigitalization() {\n        this.state.retryLoading = true;\n        const [status, errorMessage, documentUUID] = await this.orm.call(this.props.record.resModel, \"action_manual_send_for_digitization\", [this.props.record.resId], {});\n        this.subscribeToChannel(documentUUID);\n        this.state.status = status;\n        this.state.errorMessage = errorMessage;\n        this.state.retryLoading = false;\n        this.enableTimeout();\n    }\n}\n\nregistry.category(\"fields\").add(\"extract_state_header\", {component: StatusHeader});\n", "import { registry } from \"@web/core/registry\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { ControlPanel } from \"@web/search/control_panel/control_panel\";\n\nimport { Component, onWillStart, useRef, useState, useSubEnv } from \"@odoo/owl\";\n\nimport { AccountReportController } from \"@account_reports/components/account_report/controller\";\nimport { AccountReportButtonsBar } from \"@account_reports/components/account_report/buttons_bar/buttons_bar\";\nimport { AccountReportCogMenu } from \"@account_reports/components/account_report/cog_menu/cog_menu\";\nimport { AccountReportEllipsis } from \"@account_reports/components/account_report/ellipsis/ellipsis\";\nimport { AccountReportFilters } from \"@account_reports/components/account_report/filters/filters\";\nimport { AccountReportHeader } from \"@account_reports/components/account_report/header/header\";\nimport { AccountReportLine } from \"@account_reports/components/account_report/line/line\";\nimport { AccountReportLineCell } from \"@account_reports/components/account_report/line_cell/line_cell\";\nimport { AccountReportLineName } from \"@account_reports/components/account_report/line_name/line_name\";\nimport { AccountReportSearchBar } from \"@account_reports/components/account_report/search_bar/search_bar\";\nimport { AccountReportChatter } from \"@account_reports/components/mail/chatter\";\nimport { standardActionServiceProps } from \"@web/webclient/actions/action_service\";\nimport { useSetupAction } from \"@web/search/action_hook\";\n\n\nexport class AccountReport extends Component {\n    static template = \"account_reports.AccountReport\";\n    static props = { ...standardActionServiceProps };\n    static components = {\n        ControlPanel,\n        AccountReportButtonsBar,\n        AccountReportCogMenu,\n        AccountReportSearchBar,\n        AccountReportChatter,\n    };\n\n    static customizableComponents = [\n        AccountReportEllipsis,\n        AccountReportFilters,\n        AccountReportHeader,\n        AccountReportLine,\n        AccountReportLineCell,\n        AccountReportLineName,\n    ];\n    static defaultComponentsMap = [];\n\n    setup() {\n        this.rootRef = useRef(\"root\");\n        useSetupAction({\n            rootRef: this.rootRef,\n            getLocalState: () => {\n                return {\n                    keep_journal_groups_options: true,  // used when using the breadcrumb\n                };\n            }\n        })\n        if (this.props?.state?.keep_journal_groups_options !== undefined) {\n            this.props.action.keep_journal_groups_options = true;\n        }\n\n        // Can not use 'control-panel-bottom-right' slot without this, as viewSwitcherEntries doesn't exist here.\n        this.env.config.viewSwitcherEntries = [];\n\n        this.orm = useService(\"orm\");\n        this.actionService = useService(\"action\");\n        this.ui = useService(\"ui\");\n        this.controller = useState(new AccountReportController(this.props.action));\n        this.initialQuery = this.props.action.context.default_filter_accounts || '';\n\n        for (const customizableComponent of AccountReport.customizableComponents)\n            AccountReport.defaultComponentsMap[customizableComponent.name] = customizableComponent;\n\n        onWillStart(async () => {\n            await this.controller.load(this.env);\n        });\n\n        useSubEnv({\n            controller: this.controller,\n            component: this.getComponent.bind(this),\n            template: this.getTemplate.bind(this),\n        });\n    }\n\n    // -----------------------------------------------------------------------------------------------------------------\n    // Custom overrides\n    // -----------------------------------------------------------------------------------------------------------------\n    static registerCustomComponent(customComponent) {\n        registry.category(\"account_reports_custom_components\").add(customComponent.name, customComponent);\n    }\n\n    get cssCustomClass() {\n        return this.controller.options.custom_display_config.css_custom_class || \"\";\n    }\n\n    getComponent(name) {\n        const customComponents = this.controller.options.custom_display_config.components;\n\n        if (customComponents && customComponents[name])\n            return registry.category(\"account_reports_custom_components\").get(customComponents[name]);\n\n        return AccountReport.defaultComponentsMap[name];\n    }\n\n    getTemplate(name) {\n        const customTemplates = this.controller.options.custom_display_config.templates;\n\n        if (customTemplates && customTemplates[name])\n            return customTemplates[name];\n\n        return `account_reports.${ name }Customizable`;\n    }\n\n    // -----------------------------------------------------------------------------------------------------------------\n    // Table\n    // -----------------------------------------------------------------------------------------------------------------\n    get tableClasses() {\n        let classes = \"\";\n\n        if (this.controller.options.columns.length > 1) {\n            classes += \" striped\";\n        }\n\n        if (this.controller.options['horizontal_split'])\n            classes += \" w-50 mx-2\";\n\n        return classes;\n    }\n\n    // -----------------------------------------------------------------------------------------------------------------\n    // Chatter\n    // -----------------------------------------------------------------------------------------------------------------\n    /**\n     * @param {KeyboardEvent} ev\n     */\n    onKeydown(ev) {\n        if (ev.key === \"Escape\") {\n            this.controller.closeChatter();\n        }\n    }\n}\n\nregistry.category(\"actions\").add(\"account_report\", AccountReport);\n", "import { Component, useState } from \"@odoo/owl\";\n\nexport class AccountReportButtonsBar extends Component {\n    static template = \"account_reports.AccountReportButtonsBar\";\n    static props = {};\n\n    setup() {\n        this.controller = useState(this.env.controller);\n    }\n\n    //------------------------------------------------------------------------------------------------------------------\n    // Buttons\n    //------------------------------------------------------------------------------------------------------------------\n    get barButtons() {\n        const buttons = [];\n\n        for (const button of this.controller.buttons) {\n            if (button.always_show) {\n                buttons.push(button);\n            }\n        }\n\n        return buttons;\n    }\n}\n", "import {Component, useState} from \"@odoo/owl\";\nimport { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\n\n\nexport class AccountReportCogMenu extends Component {\n    static template = \"account_reports.AccountReportCogMenu\";\n    static components = {Dropdown, DropdownItem};\n    static props = {};\n\n    setup() {\n        this.controller = useState(this.env.controller);\n    }\n\n    //------------------------------------------------------------------------------------------------------------------\n    // Buttons\n    //------------------------------------------------------------------------------------------------------------------\n    get cogButtons() {\n        const buttons = [];\n\n        for (const button of this.controller.buttons) {\n            if (!button.always_show) {\n                buttons.push({\n                    ...button,\n                    onClick: (ev) => this.controller.buttonAction(ev, button),\n                });\n            }\n        }\n\n        return buttons;\n    }\n}\n", "/* global owl:readonly */\n\nimport { browser } from \"@web/core/browser/browser\";\nimport { user } from \"@web/core/user\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { useState, markRaw } from \"@odoo/owl\";\n\nexport class AccountReportController {\n    constructor(action) {\n        this.action = action;\n        this.actionService = useService(\"action\");\n        this.dialog = useService(\"dialog\");\n        this.orm = useService(\"orm\");\n        this.chatterState = useState({\n            model: undefined,\n            id: undefined,\n            lineId: undefined, // To identify the line when editing / deleting a message\n        });\n    }\n\n    async load(env) {\n        this.env = env;\n        this.reportOptionsMap = markRaw({});\n        this.reportInformationMap = {};\n        this.lastOpenedSectionByReport = {};\n        this.loadingCallNumberByCacheKey = markRaw(new Proxy(\n            {},\n            {\n                get(target, name) {\n                    return name in target ? target[name] : 0;\n                },\n                set(target, name, newValue) {\n                    target[name] = newValue;\n                    return true;\n                },\n            }\n        ));\n        this.actionReportId = this.action.context.report_id;\n        const isOpeningReport = !this.action?.keep_journal_groups_options  // true when opening the report, except when coming from the breadcrumb\n        const mainReportOptions = await this.loadReportOptions(this.actionReportId, false, this.action.params?.ignore_session, isOpeningReport);\n        const cacheKey = this.getCacheKey(mainReportOptions['sections_source_id'], mainReportOptions['report_id']);\n\n        // We need the options to be set and saved in order for the loading to work properly\n        this.options = mainReportOptions;\n        this.reportOptionsMap[cacheKey] = mainReportOptions;\n        this.incrementCallNumber(cacheKey);\n        this.options[\"loading_call_number\"] = this.loadingCallNumberByCacheKey[cacheKey];\n        this.cachedFilterOptions = this.options;\n        this.saveSessionOptions(mainReportOptions);\n\n        this.reportLoadingPromise = this.displayReport(mainReportOptions['report_id']);\n        this.preLoadClosedSections();\n\n        const chatterState = JSON.parse(\n            browser.sessionStorage.getItem(this.sessionChatterStateID())\n        );\n        this.chatterState.model = chatterState?.model;\n        this.chatterState.id = chatterState?.id;\n        this.chatterState.lineId = chatterState?.lineId;\n    }\n\n    getCacheKey(sectionsSourceId, reportId) {\n        return `${sectionsSourceId}_${reportId}`\n    }\n\n    incrementCallNumber(cacheKey = null) {\n        if (!cacheKey) {\n            cacheKey = this.getCacheKey(this.options['sections_source_id'], this.options['report_id']);\n        }\n        this.loadingCallNumberByCacheKey[cacheKey] += 1;\n    }\n\n    async displayReport(reportId) {\n        const cacheKey = await this.loadReport(reportId);\n        const options = await this.reportOptionsMap[cacheKey];\n        if (this.serverCallResultCanBeSetAsActive(options, options, cacheKey)) {\n            this.cachedFilterOptions = options;\n        }\n\n        return this.loadInformationMap(options, cacheKey);\n    }\n\n    serverCallResultCanBeSetAsActive(callResult, options, cacheKey) {\n        return callResult !== undefined\n            && this.loadingCallNumberByCacheKey[cacheKey] === options[\"loading_call_number\"]\n            && (this.lastOpenedSectionByReport === {} || this.lastOpenedSectionByReport[options['selected_variant_id']] === options['selected_section_id']);\n    }\n\n    async loadInformationMap(options, cacheKey) {\n        this.loadingData = true;\n        this.displayLoadingSymbolWhenTakingTooLong(options['report_id'] === this.options['report_id']);\n\n        const informationMap = await this.reportInformationMap[cacheKey];\n\n        if (this.serverCallResultCanBeSetAsActive(informationMap, options, cacheKey)) {\n            this.loadingData = false;\n            this.options = options;\n            this.data = informationMap;\n\n            // If there is a specific order for lines in the options, we want to use it by default\n            if (this.areLinesOrdered()) {\n                await this.sortLines();\n            }\n            this.setLineVisibility(this.lines);\n            this.refreshVisibleAnnotations();\n            this.saveSessionOptions(this.options);\n        }\n    }\n\n    async displayLoadingSymbolWhenTakingTooLong(longWaitBeforeLoadAnimation) {\n        // Wait for 200 ms at least to prevent the loading animation from flickering if the report loads quickly.\n        const waitingTime = longWaitBeforeLoadAnimation ? 500 : 200;\n        await new Promise((resolve) => setTimeout(resolve, waitingTime));\n\n        if (this.loadingData) {\n            this.data = undefined;\n        }\n    }\n\n    async reload(optionPath, newOptions) {\n        const rootOptionKey = optionPath ? optionPath.split(\".\")[0] : \"\";\n\n        /*\n        When reloading the UI after setting an option filter, invalidate the cached options and data of all sections supporting this filter.\n        This way, those sections will be reloaded (either synchronously when the user tries to access them or asynchronously via the preloading\n        feature), and will then use the new filter value. This ensures the filters are always applied consistently to all sections.\n        */\n        for (const [cacheKey, cachedOptionsPromise] of Object.entries(this.reportOptionsMap)) {\n            let cachedOptions = await cachedOptionsPromise;\n\n            if (rootOptionKey === \"\" || cachedOptions.hasOwnProperty(rootOptionKey)) {\n                delete this.reportOptionsMap[cacheKey];\n                delete this.reportInformationMap[cacheKey];\n            }\n        }\n\n        this.saveSessionOptions(newOptions); // The new options will be loaded from the session. Saving them now ensures the new filter is taken into account.\n        await this.displayReport(newOptions['report_id']);\n    }\n\n    async preLoadClosedSections() {\n        let sectionLoaded = false;\n        for (const section of this.options['sections']) {\n            // Preload the first non-loaded section we find amongst this report's sections.\n            const cacheKey = this.getCacheKey(this.options['sections_source_id'], section.id);\n            if (section.id != this.options['report_id'] && !this.reportInformationMap[cacheKey]) {\n                const reportCacheKey = await this.loadReport(section.id, true);\n                await this.reportInformationMap[reportCacheKey];\n\n                sectionLoaded = true;\n                // Stop iterating and schedule next call. We don't go on in the loop in case the cache is reset and we need to restart preloading.\n                break;\n            }\n        }\n\n        let nextCallDelay = (sectionLoaded) ? 100 : 1000;\n\n        const self = this;\n        setTimeout(() => self.preLoadClosedSections(), nextCallDelay);\n    }\n\n    async loadReport(reportId, preloading=false) {\n        const options = await this.loadReportOptions(reportId, preloading, false); // This also sets the promise in the cache\n        const reportToDisplayId = options['report_id']; // Might be different from reportId, in case the report to open uses sections\n\n        const cacheKey = this.getCacheKey(options['sections_source_id'], reportToDisplayId)\n        if (!this.reportInformationMap[cacheKey]) {\n            this.reportInformationMap[cacheKey] = this.orm.call(\n                \"account.report\",\n                options.readonly_query ? \"get_report_information_readonly\" : \"get_report_information\",\n                [\n                    reportToDisplayId,\n                    options,\n                ],\n                {\n                    context: this.action.context,\n                },\n            );\n        }\n\n        if (!preloading) {\n            if (options['sections'].length)\n                this.lastOpenedSectionByReport[options['sections_source_id']] = options['selected_section_id'];\n        }\n\n        return cacheKey;\n    }\n\n    async loadReportOptions(reportId, preloading=false, ignore_session=false, isOpeningReport=false) {\n        const loadOptions = (ignore_session || !this.hasSessionOptions()) ? (this.action.params?.options || {}) : this.sessionOptions();\n        const cacheKey = this.getCacheKey(loadOptions['sections_source_id'] || reportId, reportId);\n\n        if (!(cacheKey in this.loadingCallNumberByCacheKey)) {\n            this.incrementCallNumber(cacheKey);\n        }\n        loadOptions[\"loading_call_number\"] = this.loadingCallNumberByCacheKey[cacheKey];\n\n        loadOptions[\"is_opening_report\"] = isOpeningReport;\n\n        if (!this.reportOptionsMap[cacheKey]) {\n            // The options for this section are not loaded nor loading. Let's load them !\n\n            if (preloading)\n                loadOptions['selected_section_id'] = reportId;\n            else {\n                /* Reopen the last opened section by default (cannot be done through regular caching, because composite reports' options are not\n                cached (since they always reroute). */\n                if (this.lastOpenedSectionByReport[reportId])\n                    loadOptions['selected_section_id'] = this.lastOpenedSectionByReport[reportId];\n            }\n\n            this.reportOptionsMap[cacheKey] = this.orm.call(\n                \"account.report\",\n                \"get_options\",\n                [\n                   reportId,\n                   loadOptions,\n                ],\n                {\n                   context: this.action.context,\n                },\n            );\n\n            // Wait for the result, and check the report hasn't been rerouted to a section or variant; fix the cache if it has\n            let reportOptions = await this.reportOptionsMap[cacheKey];\n\n            // In case of a reroute, also set the cached options into the reroute target's key\n            const loadedOptionsCacheKey = this.getCacheKey(reportOptions['sections_source_id'], reportOptions['report_id']);\n            if (loadedOptionsCacheKey !== cacheKey) {\n                /* We delete the rerouting report from the cache, to avoid redoing this reroute when reloading the cached options, as it would mean\n                route reports can never be opened directly if they open some variant by default.*/\n                delete this.reportOptionsMap[cacheKey];\n                this.reportOptionsMap[loadedOptionsCacheKey] = reportOptions;\n\n                this.loadingCallNumberByCacheKey[loadedOptionsCacheKey] = 1;\n                delete this.loadingCallNumberByCacheKey[cacheKey];\n                return reportOptions;\n            }\n        }\n\n        return this.reportOptionsMap[cacheKey];\n    }\n\n    //------------------------------------------------------------------------------------------------------------------\n    // Generic data getters\n    //------------------------------------------------------------------------------------------------------------------\n    get buttons() {\n        return this.cachedFilterOptions.buttons;\n    }\n\n    get caretOptions() {\n        return this.data.caret_options;\n    }\n\n    get columnHeadersRenderData() {\n        return this.data.column_headers_render_data;\n    }\n\n    get columnGroupsTotals() {\n        return this.data.column_groups_totals;\n    }\n\n    get context() {\n        return this.data.context;\n    }\n\n    get filters() {\n        return this.cachedFilterOptions.filters;\n    }\n\n    get annotations() {\n        return this.data.annotations;\n    }\n\n    get userGroups() {\n        return this.options.user_groups;\n    }\n\n    get cachedUserGroups() {\n        return this.cachedFilterOptions.user_groups;\n    }\n\n    get lines() {\n        return this.data.lines;\n    }\n\n    get warnings() {\n        return this.data.warnings;\n    }\n\n    get linesOrder() {\n        return this.data.lines_order;\n    }\n\n    get report() {\n        return this.data.report;\n    }\n\n    //------------------------------------------------------------------------------------------------------------------\n    // Generic data setters\n    //------------------------------------------------------------------------------------------------------------------\n    set annotations(value) {\n        this.data.annotations = value;\n    }\n\n    set columnGroupsTotals(value) {\n        this.data.column_groups_totals = value;\n    }\n\n    set lines(value) {\n        this.data.lines = value;\n        this.setLineVisibility(this.lines);\n    }\n\n    set linesOrder(value) {\n        this.data.lines_order = value;\n    }\n\n    //------------------------------------------------------------------------------------------------------------------\n    // Helpers\n    //------------------------------------------------------------------------------------------------------------------\n    get needsColumnPercentComparison() {\n        return this.options.column_percent_comparison === \"growth\";\n    }\n\n    get hasCustomSubheaders() {\n        return this.columnHeadersRenderData.custom_subheaders.length > 0;\n    }\n\n    get hasDebugColumn() {\n        return Boolean(this.options.show_debug_column);\n    }\n\n    //------------------------------------------------------------------------------------------------------------------\n    // Options\n    //------------------------------------------------------------------------------------------------------------------\n    async _updateOption(operationType, optionPath, optionValue=null, reloadUI=false) {\n        const optionKeys = optionPath.split(\".\");\n\n        let currentOptionKey = null;\n        let option = this.cachedFilterOptions;\n\n        while (optionKeys.length > 1) {\n            currentOptionKey = optionKeys.shift();\n            option = option[currentOptionKey];\n\n            if (option === undefined)\n                throw new Error(`Invalid option key in _updateOption(): ${ currentOptionKey } (${ optionPath })`);\n        }\n\n        switch (operationType) {\n            case \"update\":\n                option[optionKeys[0]] = optionValue;\n                break;\n            case \"delete\":\n                delete option[optionKeys[0]];\n                break;\n            case \"toggle\":\n                option[optionKeys[0]] = !option[optionKeys[0]];\n                break;\n            default:\n                throw new Error(`Invalid operation type in _updateOption(): ${ operationType }`);\n        }\n\n        if (reloadUI) {\n            this.incrementCallNumber();\n            await this.reload(optionPath, this.cachedFilterOptions);\n        }\n    }\n\n    async updateOption(optionPath, optionValue, reloadUI=false) {\n        await this._updateOption('update', optionPath, optionValue, reloadUI);\n    }\n\n    async deleteOption(optionPath, reloadUI=false) {\n        await this._updateOption('delete', optionPath, null, reloadUI);\n    }\n\n    async toggleOption(optionPath, reloadUI=false) {\n        await this._updateOption('toggle', optionPath, null, reloadUI);\n    }\n\n    async switchToSection(reportId) {\n        this.saveSessionOptions({...this.cachedFilterOptions, 'selected_section_id': reportId});\n        this.displayReport(reportId);\n    }\n\n    //------------------------------------------------------------------------------------------------------------------\n    // Session options\n    //------------------------------------------------------------------------------------------------------------------\n    sessionOptionsID() {\n        /* Options are stored by action report (so, the report that was targetted by the original action triggering this flow).\n        This allows a more intelligent reloading of the previous options during user navigation (especially concerning sections and variants;\n        you expect your report to open by default the same section as last time you opened it in this http session).\n        */\n        return `account.report:${ this.actionReportId }:${ user.defaultCompany.id }`;\n    }\n\n    hasSessionOptions() {\n        return Boolean(browser.sessionStorage.getItem(this.sessionOptionsID()))\n    }\n\n    saveSessionOptions(options) {\n        browser.sessionStorage.setItem(this.sessionOptionsID(), JSON.stringify(options));\n    }\n\n    sessionOptions() {\n        return JSON.parse(browser.sessionStorage.getItem(this.sessionOptionsID()));\n    }\n\n    //------------------------------------------------------------------------------------------------------------------\n    // Lines\n    //------------------------------------------------------------------------------------------------------------------\n    lineHasDebugData(lineIndex) {\n        return 'debug_popup_data' in this.lines[lineIndex];\n    }\n\n    lineHasGrowthComparisonData(lineIndex) {\n        return Boolean(this.lines[lineIndex].column_percent_comparison_data);\n    }\n\n    isLineAncestorOf(ancestorLineId, lineId) {\n        return lineId.startsWith(`${ancestorLineId}|`);\n    }\n\n    isLineChildOf(childLineId, lineId) {\n        return childLineId.startsWith(`${lineId}|`);\n    }\n\n    isLineRelatedTo(relatedLineId, lineId) {\n        return this.isLineAncestorOf(relatedLineId, lineId) || this.isLineChildOf(relatedLineId, lineId);\n    }\n\n    isNextLineChild(index, lineId) {\n        return index < this.lines.length && this.lines[index].id.startsWith(`${lineId}|`);\n    }\n\n    isNextLineDirectChild(index, lineId) {\n        return index < this.lines.length && this.lines[index].parent_id === lineId;\n    }\n\n    isTotalLine(lineIndex) {\n        return this.lines[lineIndex].id.includes(\"|total~~\");\n    }\n\n    isLoadMoreLine(lineIndex) {\n        return this.lines[lineIndex].id.includes(\"|load_more~~\");\n    }\n\n    isLoadedLine(lineIndex) {\n        const lineID = this.lines[lineIndex].id;\n        const nextLineIndex = lineIndex + 1;\n\n        return this.isNextLineChild(nextLineIndex, lineID) && !this.isTotalLine(nextLineIndex) && !this.isLoadMoreLine(nextLineIndex);\n    }\n\n    async replaceLineWith(replaceIndex, newLines) {\n        await this.insertLines(replaceIndex, 1, newLines);\n    }\n\n    async insertLinesAfter(insertIndex, newLines) {\n        await this.insertLines(insertIndex + 1, 0, newLines);\n    }\n\n    async insertLines(lineIndex, deleteCount, newLines) {\n        this.lines.splice(lineIndex, deleteCount, ...newLines);\n    }\n\n    updateLines(lineIds, key, value) {\n        for (const lineId of lineIds) {\n            const lineIndex = this.lines.findIndex((line) => line.id === lineId);\n            this.lines.splice(lineIndex, 1, { ...this.lines[lineIndex], [key]: value });\n        }\n    }\n\n    //------------------------------------------------------------------------------------------------------------------\n    // Unfolded/Folded lines\n    //------------------------------------------------------------------------------------------------------------------\n    async unfoldLoadedLine(lineIndex) {\n        const lineId = this.lines[lineIndex].id;\n        let nextLineIndex = lineIndex + 1;\n\n        while (this.isNextLineChild(nextLineIndex, lineId)) {\n            if (this.isNextLineDirectChild(nextLineIndex, lineId)) {\n                const nextLine = this.lines[nextLineIndex];\n                nextLine.visible = true;\n                if (!nextLine.unfoldable && this.isNextLineChild(nextLineIndex + 1, nextLine.id)) {\n                    await this.unfoldLine(nextLineIndex);\n                }\n            }\n            nextLineIndex += 1;\n        }\n        return nextLineIndex;\n    }\n\n    async unfoldNewLine(lineIndex) {\n        const options = await this.options;\n        const newLines = await this.orm.call(\n            \"account.report\",\n            options.readonly_query ? \"get_expanded_lines_readonly\" : \"get_expanded_lines\",\n            [\n                this.options['report_id'],\n                this.options,\n                this.lines[lineIndex].id,\n                this.lines[lineIndex].groupby,\n                this.lines[lineIndex].expand_function,\n                this.lines[lineIndex].progress,\n                0,\n                this.lines[lineIndex].horizontal_split_side,\n            ],\n        );\n\n        if (this.areLinesOrdered()) {\n            this.updateLinesOrderIndexes(lineIndex, newLines, false)\n        }\n        this.insertLinesAfter(lineIndex, newLines);\n\n        const totalIndex = lineIndex + newLines.length + 1;\n\n        if (this.filters.show_totals && this.lines[totalIndex] && this.isTotalLine(totalIndex))\n            this.lines[totalIndex].visible = true;\n\n        // Update options\n        this.options.unfolded_lines.push(\n            ...newLines.filter(line => line.unfolded).map(({ id }) => id)\n        );\n\n        this.saveSessionOptions(this.options);\n\n        return totalIndex\n    }\n\n    /**\n     * When unfolding a line of a sorted report, we need to update the linesOrder array by adding the new lines,\n     * which will require subsequent updates on the array.\n     *\n     * - lineOrderValue represents the line index before sorting the report.\n     * @param {Integer} lineIndex: Index of the current line\n     * @param {Array} newLines: Array of lines to be added\n     * @param {Boolean} replaceLine: Useful for the splice of the linesOrder array in case we want to replace some line\n     *                               example: With the load more, we want to replace the line with others\n     **/\n    updateLinesOrderIndexes(lineIndex, newLines, replaceLine) {\n        let unfoldedLineIndex;\n        // The offset is useful because in case we use 'replaceLineWith' we want to replace the line at index\n        // unfoldedLineIndex with the new lines.\n        const offset = replaceLine ? 0 : 1;\n        for (const [lineOrderIndex, lineOrderValue] of Object.entries(this.linesOrder)) {\n            // Since we will have to add new lines into the linesOrder array, we have to update the index of the lines\n            // having a bigger index than the one we will unfold.\n            // deleteCount of 1 means that a line need to be replaced so the index need to be increase by 1 less than usual\n            if (lineOrderValue > lineIndex) {\n                this.linesOrder[lineOrderIndex] += newLines.length - replaceLine;\n            }\n            // The unfolded line is found, providing a reference for adding children in the 'linesOrder' array.\n            if (lineOrderValue === lineIndex) {\n                unfoldedLineIndex = parseInt(lineOrderIndex)\n            }\n        }\n\n        const arrayOfNewIndex = Array.from({ length: newLines.length }, (dummy, index) => this.linesOrder[unfoldedLineIndex] + index + offset);\n        this.linesOrder.splice(unfoldedLineIndex + offset, replaceLine, ...arrayOfNewIndex);\n    }\n\n    async unfoldLine(lineIndex) {\n        const targetLine = this.lines[lineIndex];\n        let lastLineIndex = lineIndex + 1;\n\n        const isLoadedLine = this.isLoadedLine(lineIndex);\n        if (isLoadedLine) {\n            lastLineIndex = await this.unfoldLoadedLine(lineIndex);\n        } else if (targetLine.expand_function) {\n            lastLineIndex = await this.unfoldNewLine(lineIndex);\n            this.loadAnnotations(lineIndex + 1, lastLineIndex);\n        }\n\n        this.setLineVisibility(this.lines.slice(lineIndex + 1, lastLineIndex));\n        targetLine.unfolded = true;\n\n        // Update options\n        if (!this.options.unfolded_lines.includes(targetLine.id))\n            this.options.unfolded_lines.push(targetLine.id);\n\n        this.saveSessionOptions(this.options);\n    }\n\n    foldLine(lineIndex) {\n        const targetLine = this.lines[lineIndex];\n\n        let foldedLinesIDs = new Set([targetLine.id]);\n        let nextLineIndex = lineIndex + 1;\n\n        while (this.isNextLineChild(nextLineIndex, targetLine.id)) {\n            this.lines[nextLineIndex].unfolded = false;\n            this.lines[nextLineIndex].visible = false;\n\n            foldedLinesIDs.add(this.lines[nextLineIndex].id);\n\n            nextLineIndex += 1;\n        }\n\n        targetLine.unfolded = false;\n\n        // Update options\n        this.options.unfolded_lines = this.options.unfolded_lines.filter(\n            unfoldedLineID => !foldedLinesIDs.has(unfoldedLineID)\n        );\n\n        this.saveSessionOptions(this.options);\n    }\n\n    //------------------------------------------------------------------------------------------------------------------\n    // Ordered lines\n    //------------------------------------------------------------------------------------------------------------------\n    linesCurrentOrderByColumn(columnIndex) {\n        if (this.areLinesOrderedByColumn(columnIndex))\n            return this.options.order_column.direction;\n\n        return \"default\";\n    }\n\n    areLinesOrdered() {\n        return this.linesOrder != null && this.options.order_column != null;\n    }\n\n    areLinesOrderedByColumn(columnIndex) {\n        return this.areLinesOrdered() && this.options.order_column.expression_label === this.options.columns[columnIndex].expression_label;\n    }\n\n    async sortLinesByColumnAsc(columnIndex) {\n        this.options.order_column = {\n            expression_label: this.options.columns[columnIndex].expression_label,\n            direction: \"ASC\",\n        };\n\n        await this.sortLines();\n        this.saveSessionOptions(this.options);\n    }\n\n    async sortLinesByColumnDesc(columnIndex) {\n        this.options.order_column = {\n            expression_label: this.options.columns[columnIndex].expression_label,\n            direction: \"DESC\",\n        };\n\n        await this.sortLines();\n        this.saveSessionOptions(this.options);\n    }\n\n    sortLinesByDefault() {\n        delete this.options.order_column;\n        delete this.data.lines_order;\n\n        this.saveSessionOptions(this.options);\n    }\n\n    async sortLines() {\n        this.linesOrder = await this.orm.call(\n            \"account.report\",\n            \"sort_lines\",\n            [\n                this.lines,\n                this.options,\n                true,\n            ],\n            {\n                context: this.action.context,\n            },\n        );\n    }\n\n    //------------------------------------------------------------------------------------------------------------------\n    // Chatter\n    //------------------------------------------------------------------------------------------------------------------\n    sessionChatterStateID() {\n        return this.sessionOptionsID() + user.activeCompany.id.toString() + \".chatter\";\n    }\n\n    async loadAnnotations(lineStartIndex = 0, lineEndIndex = this.lines.length) {\n        const new_annotations = await this.orm.call(\"account.report\", \"get_annotations\", [\n            this.action.context.report_id,\n            this.options,\n            this.lines.slice(lineStartIndex, lineEndIndex),\n        ]);\n        for (const [key, value] of Object.entries(new_annotations)) {\n            this.annotations[key] = value;\n        }\n\n        this.refreshVisibleAnnotations(lineStartIndex, lineEndIndex);\n    }\n\n    addAnnotation(messageId, resModel, resId, body) {\n        this.lines.forEach((line) => {\n            if (line.chatter?.model === resModel && line.chatter?.id === resId) {\n                this.annotations[line.id] = this.annotations[line.id] || [];\n                this.annotations[line.id].push({\n                    id: messageId,\n                    model: resModel,\n                    res_id: resId,\n                    body: body,\n                });\n                line.visible_annotations = true;\n            }\n        });\n    }\n\n    removeAnnotation(messageId) {\n        this.lines.forEach((line) => {\n            this.annotations[line.id] = (this.annotations[line.id] || []).filter((annotation) => annotation.id !== messageId);\n        });\n        this.refreshVisibleAnnotations();\n    }\n\n    async toggleLineChatter(annotation) {\n        if (\n            this.chatterState.model === annotation.resModel &&\n            this.chatterState.id === annotation.resId &&\n            this.chatterState.lineId === annotation.line_id\n        ) {\n            this.closeChatter();\n        } else {\n            this.chatterState.model = annotation.resModel;\n            this.chatterState.id = annotation.resId;\n            this.chatterState.lineId = annotation.line_id;\n            browser.sessionStorage.setItem(\n                this.sessionChatterStateID(),\n                JSON.stringify({\n                    model: this.chatterState.model,\n                    id: this.chatterState.id,\n                    lineId: this.chatterState.lineId,\n                })\n            );\n        }\n    }\n\n    closeChatter() {\n        this.chatterState.model = undefined;\n        this.chatterState.id = undefined;\n        this.chatterState.lineId = undefined;\n        browser.sessionStorage.setItem(\n            this.sessionChatterStateID(),\n            JSON.stringify({\n                model: this.chatterState.model,\n                id: this.chatterState.id,\n                lineId: this.chatterState.lineId,\n            })\n        );\n    }\n\n    //------------------------------------------------------------------------------------------------------------------\n    // Visibility\n    //------------------------------------------------------------------------------------------------------------------\n\n    refreshVisibleAnnotations(lineStartIndex = 0, lineEndIndex = this.lines.length) {\n        this.lines.slice(lineStartIndex, lineEndIndex).forEach((line) => {\n            line.visible_annotations = this.annotations[line.id] && this.annotations[line.id].length > 0;\n        });\n    }\n\n    /**\n        Defines which lines should be visible in the provided list of lines (depending on what is folded).\n    **/\n    setLineVisibility(linesToAssign) {\n        let needHidingChildren = new Set();\n\n        linesToAssign.forEach((line) => {\n            line.visible = !needHidingChildren.has(line.parent_id);\n\n            if (!line.visible || (line.unfoldable &! line.unfolded))\n                needHidingChildren.add(line.id);\n        });\n\n        // If the hide 0 lines is activated we will go through the lines to set the visibility.\n        if (this.options.hide_0_lines) {\n            this.hideZeroLines(linesToAssign);\n        }\n    }\n\n    /**\n     * Defines whether the line should be visible depending on its value and the ones of its children.\n     * For parent lines, it's visible if there is at least one child with a value different from zero\n     * or if a child is visible, indicating it's a parent line.\n     * For leaf nodes, it's visible if the value is different from zero.\n     *\n     * By traversing the 'lines' array in reverse, we can set the visibility of the lines easily by keeping\n     * a dict of visible lines for each parent.\n     *\n     * @param {Object} lines - The lines for which we want to determine visibility.\n     */\n    hideZeroLines(lines) {\n        const hasVisibleChildren = new Set();\n        const reversed_lines = [...lines].reverse()\n\n        const number_figure_types = ['integer', 'float', 'monetary', 'percentage'];\n        reversed_lines.forEach((line) => {\n            const isZero = line.columns.every(column => !number_figure_types.includes(column.figure_type) || column.is_zero);\n\n            // If the line has no visible children and all the columns are equals to zero then the line needs to be hidden\n            if (!hasVisibleChildren.has(line.id) && isZero) {\n                line.visible = false;\n            }\n\n            // If the line has a parent_id and is not hidden then we fill the set 'hasVisibleChildren'. Each parent\n            // will have an array of his visible children\n            if (line.parent_id && line.visible) {\n                // This line allows the initialization of that list.\n                hasVisibleChildren.add(line.parent_id);\n            }\n        })\n    }\n\n    //------------------------------------------------------------------------------------------------------------------\n    // Server calls\n    //------------------------------------------------------------------------------------------------------------------\n    buttonAction(ev, button) {\n        // Might be overidden to add specific functionality to button\n        // For instance adding context to a call ...\n        this.reportAction(ev, button.error_action || button.action, button.action_param, true);\n    }\n\n    async reportAction(ev, action, actionParam = null, callOnSectionsSource = false, actionContext=null) {\n        // 'ev' might be 'undefined' if event is not triggered from a button/anchor\n        ev?.preventDefault();\n        ev?.stopPropagation();\n\n        let actionOptions = this.cachedFilterOptions;\n        if (callOnSectionsSource) {\n            // When calling the sections source, we want to keep track of all unfolded lines of all sections\n            const allUnfoldedLines =  this.cachedFilterOptions.sections.length ? [] : [...this.cachedFilterOptions['unfolded_lines']]\n\n            for (const sectionData of this.cachedFilterOptions['sections']) {\n                const cacheKey = this.getCacheKey(this.cachedFilterOptions['sections_source_id'], sectionData['id']);\n                const sectionOptions = await this.reportOptionsMap[cacheKey];\n                if (sectionOptions)\n                    allUnfoldedLines.push(...sectionOptions['unfolded_lines']);\n            }\n\n            actionOptions = {...this.cachedFilterOptions, unfolded_lines: allUnfoldedLines};\n        }\n\n        const dispatchReportAction = await this.orm.call(\n            \"account.report\",\n            \"dispatch_report_action\",\n            [\n                this.cachedFilterOptions['report_id'],\n                actionOptions,\n                action,\n                actionParam,\n                callOnSectionsSource,\n            ],\n            {\n                context: Object.assign({}, this.context, actionContext)\n            }\n        );\n        if (dispatchReportAction?.help) {\n            dispatchReportAction.help = owl.markup(dispatchReportAction.help)\n        }\n\n        return dispatchReportAction ? this.actionService.doAction(dispatchReportAction) : null;\n    }\n\n    // -----------------------------------------------------------------------------------------------------------------\n    // Budget\n    // -----------------------------------------------------------------------------------------------------------------\n\n    async openBudget(budget) {\n        this.actionService.doAction({\n            type: \"ir.actions.act_window\",\n            res_model: \"account.report.budget\",\n            res_id: budget.id,\n            views: [[false, \"form\"]],\n        });\n    }\n}\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { localization } from \"@web/core/l10n/localization\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { Component, useState } from \"@odoo/owl\";\n\nimport { AccountReportEllipsisPopover } from \"@account_reports/components/account_report/ellipsis/popover/ellipsis_popover\";\n\nexport class AccountReportEllipsis extends Component {\n    static template = \"account_reports.AccountReportEllipsis\";\n    static props = {\n        name: { type: String, optional: true },\n        no_format: { optional: true },\n        type: { type: String, optional: true },\n        maxCharacters: Number,\n    };\n\n    setup() {\n        this.popover = useService(\"popover\");\n        this.notification = useService(\"notification\");\n        this.controller = useState(this.env.controller);\n    }\n\n    //------------------------------------------------------------------------------------------------------------------\n    // Ellipsis\n    //------------------------------------------------------------------------------------------------------------------\n    get triggersEllipsis() {\n        if (this.props.name)\n            return this.props.name.length > this.props.maxCharacters;\n\n        return false;\n    }\n\n    copyEllipsisText() {\n        navigator.clipboard.writeText(this.props.name);\n        this.notification.add(_t(\"Text copied\"), { type: 'success' });\n        this.popoverCloseFn();\n        this.popoverCloseFn = null;\n    }\n\n    showEllipsisPopover(ev) {\n        ev.preventDefault();\n        ev.stopPropagation();\n\n        if (this.popoverCloseFn) {\n            this.popoverCloseFn();\n            this.popoverCloseFn = null;\n        }\n\n        this.popoverCloseFn = this.popover.add(\n            ev.currentTarget,\n            AccountReportEllipsisPopover,\n            {\n                name: this.props.name,\n                copyEllipsisText: this.copyEllipsisText.bind(this),\n            },\n            {\n                closeOnClickAway: true,\n                position: localization.direction === \"rtl\" ? \"left\" : \"right\",\n            },\n        );\n    }\n}\n", "import { Component } from \"@odoo/owl\";\n\nexport class AccountReportEllipsisPopover extends Component {\n    static template = \"account_reports.AccountReportEllipsisPopover\";\n    static props = {\n        close: Function,\n        name: String,\n        copyEllipsisText: Function,\n    };\n}\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { status, Component, useState } from \"@odoo/owl\";\n\nimport { useService } from \"@web/core/utils/hooks\";\nimport { WarningDialog } from \"@web/core/errors/error_dialogs\";\n\nimport { DateTimeInput } from '@web/core/datetime/datetime_input';\nimport { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\nimport { MultiRecordSelector } from \"@web/core/record_selectors/multi_record_selector\";\nimport { formatDate, parseDate } from \"@web/core/l10n/dates\";\nconst { DateTime } = luxon;\nimport { user } from \"@web/core/user\";\n\nexport class AccountReportFilters extends Component {\n    static template = \"account_reports.AccountReportFilters\";\n    static props = {};\n    static components = {\n        DateTimeInput,\n        Dropdown,\n        DropdownItem,\n        MultiRecordSelector,\n    };\n\n    setup() {\n        this.dialog = useService(\"dialog\");\n        this.orm = useService(\"orm\");\n        this.notification = useService(\"notification\");\n        this.controller = useState(this.env.controller);\n        if (this.env.controller.cachedFilterOptions.date) {\n            this.dateFilter = useState(this.initDateFilters());\n        }\n        this.budgetName = useState({\n            value: \"\",\n            invalid: false,\n        });\n        this.timeout = null;\n    }\n\n    focusInnerInput(selectedItem) {\n        selectedItem.el.querySelector(\":scope input\")?.focus();\n    }\n\n    //------------------------------------------------------------------------------------------------------------------\n    // Getters\n    //------------------------------------------------------------------------------------------------------------------\n    get filterExtraOptionsData() {\n        return {\n            'all_entries': {\n                'name': _t(\"Draft Entries\"),\n                'group': 'account_readonly',\n                'show': this.controller.filters.show_draft,\n            },\n            'include_analytic_without_aml': {\n                'name': _t(\"Analytic Simulations\"),\n                'group': 'account_readonly',\n            },\n            'hierarchy': {\n                'name': _t(\"Hierarchy and Subtotals\"),\n                'show': this.controller.cachedFilterOptions.display_hierarchy_filter,\n            },\n            'unreconciled': {\n                'name': _t(\"Unreconciled Entries\"),\n                'show': this.controller.filters.show_unreconciled,\n            },\n            'unfold_all': {\n                'name': _t(\"Unfold All\"),\n                'show': this.controller.filters.show_all,\n            },\n            'integer_rounding_enabled': {\n                'name': _t(\"Integer Rounding\"),\n            },\n            'consolidation': {\n                'name': _t(\"Consolidation\"),\n                'show': this.controller.cachedFilterOptions.show_consolidation,\n            },\n            'hide_0_lines': {\n                'name': _t(\"Hide lines at 0\"),\n                'ui_filter': true,\n                'onSelect': () => this.toggleHideZeroLines(),\n                'show': this.controller.filters.show_hide_0_lines !== \"never\",\n            },\n            'horizontal_split': {\n                'name': _t(\"Split Horizontally\"),\n                'ui_filter': true,\n                'onSelect': () => this.toggleHorizontalSplit(),\n            },\n        }\n    }\n\n    get selectedHorizontalGroupName() {\n        for (const horizontalGroup of this.controller.cachedFilterOptions.available_horizontal_groups) {\n            if (horizontalGroup.id === this.controller.cachedFilterOptions.selected_horizontal_group_id) {\n                return horizontalGroup.name;\n            }\n        }\n        return _t(\"None\");\n    }\n\n    get isHorizontalGroupSelected() {\n        return this.controller.cachedFilterOptions.available_horizontal_groups.some((group) => {\n            return group.id === this.controller.cachedFilterOptions.selected_horizontal_group_id;\n        });\n    }\n\n    get selectedTaxUnitName() {\n        for (const taxUnit of this.controller.cachedFilterOptions.available_tax_units) {\n            if (taxUnit.id === this.controller.cachedFilterOptions.tax_unit) {\n                return taxUnit.name;\n            }\n        }\n        return _t(\"Company Only\");\n    }\n\n    get selectedVariantName() {\n        for (const variant of this.controller.cachedFilterOptions.available_variants) {\n            if (variant.id === this.controller.cachedFilterOptions.selected_variant_id) {\n                return variant.name;\n            }\n        }\n        return _t(\"None\");\n    }\n\n    get selectedSectionName() {\n        for (const section of this.controller.cachedFilterOptions.sections)\n            if (section.id === this.controller.cachedFilterOptions.selected_section_id)\n                return section.name;\n    }\n\n    get selectedAccountType() {\n        let selectedAccountType = this.controller.cachedFilterOptions.account_type.filter(\n            (accountType) => accountType.selected,\n        );\n        if (\n            !selectedAccountType.length ||\n            selectedAccountType.length === this.controller.cachedFilterOptions.account_type.length\n        ) {\n            return _t(\"All\");\n        }\n\n        const accountTypeMappings = [\n            { list: [\"trade_receivable\", \"non_trade_receivable\"], name: _t(\"All Receivable\") },\n            { list: [\"trade_payable\", \"non_trade_payable\"], name: _t(\"All Payable\") },\n            { list: [\"trade_receivable\", \"trade_payable\"], name: _t(\"Trade Partners\") },\n            { list: [\"non_trade_receivable\", \"non_trade_payable\"], name: _t(\"Non Trade Partners\") },\n        ];\n\n        const listToDisplay = [];\n        for (const mapping of accountTypeMappings) {\n            if (\n                mapping.list.every((accountType) =>\n                    selectedAccountType.map((accountType) => accountType.id).includes(accountType),\n                )\n            ) {\n                listToDisplay.push(mapping.name);\n                // Delete already checked id\n                selectedAccountType = selectedAccountType.filter(\n                    (accountType) => !mapping.list.includes(accountType.id),\n                );\n            }\n        }\n\n        return listToDisplay\n            .concat(selectedAccountType.map((accountType) => accountType.name))\n            .join(\", \");\n    }\n\n    get selectedAmlIrFilters() {\n        const selectedFilters = this.controller.cachedFilterOptions.aml_ir_filters.filter(\n            (irFilter) => irFilter.selected,\n        );\n\n        if (selectedFilters.length === 1) {\n            return selectedFilters[0].name;\n        } else if (selectedFilters.length > 1) {\n            return _t(\"%s selected\", selectedFilters.length);\n        } else {\n            return _t(\"None\");\n        }\n    }\n\n    get availablePeriodOrder() {\n        return { descending: _t(\"Descending\"), ascending: _t(\"Ascending\") };\n    }\n\n    get periodOrder() {\n        return this.controller.cachedFilterOptions.comparison.period_order === \"descending\"\n            ? _t(\"Descending\")\n            : _t(\"Ascending\");\n    }\n\n    get selectedExtraOptions() {\n        const selectedExtraOptions = [];\n\n        if (this.controller.cachedUserGroups.account_readonly && this.controller.filters.show_draft) {\n            selectedExtraOptions.push(\n                this.controller.cachedFilterOptions.all_entries\n                    ? _t(\"With Draft Entries\")\n                    : _t(\"Posted Entries\"),\n            );\n        }\n        if (this.controller.filters.show_unreconciled && this.controller.cachedFilterOptions.unreconciled) {\n            selectedExtraOptions.push(_t(\"Unreconciled Entries\"));\n        }\n        if (this.controller.cachedFilterOptions.include_analytic_without_aml) {\n            selectedExtraOptions.push(_t(\"Including Analytic Simulations\"));\n        }\n        return selectedExtraOptions.join(\", \");\n    }\n\n    get dropdownProps() {\n        return {\n            shouldFocusChildInput: false,\n            hotkeys: {\n                arrowright: (navigator) => this.focusInnerInput(navigator.activeItem),\n            },\n        };\n    }\n\n    get dateNavigationOptions() {\n        /**\n         * Returns custom navigation options to fully navigate the date options with your keyboard.\n         */\n        const findNearestDropdownItem = (navigator) => {\n            for (let i = navigator.activeItemIndex; i >= 0; i--) {\n                if (navigator.items[i].target.classList.contains(\"o-dropdown-item\")) {\n                    return navigator.items[i];\n                }\n            }\n        };\n\n        return {\n            hotkeys: {\n                arrowleft: (navigator) => {\n                    if (!navigator.activeItem) {\n                        return;\n                    }\n                    const periodType = findNearestDropdownItem(navigator)?.target.dataset.periodType;\n                    if (Object.prototype.hasOwnProperty.call(this.dateFilter, periodType)) {\n                        this.selectPreviousPeriod(periodType);\n                    }\n                },\n                arrowright: (navigator) => {\n                    if (!navigator.activeItem) {\n                        return;\n                    }\n                    const periodType = findNearestDropdownItem(navigator)?.target.dataset.periodType;\n                    if (Object.prototype.hasOwnProperty.call(this.dateFilter, periodType)) {\n                        this.selectNextPeriod(periodType);\n                    }\n                },\n                enter: {\n                    callback: (navigator) => {\n                        if (!navigator.activeItem) {\n                            return;\n                        }\n\n                        /**\n                         * Workaround for when we're editing a date field, but meanwhile hovering another dropdown item.\n                         * In that case, the active item in the navigator is the hovered one (not necessarily the one\n                         * we're editing). We check here if the current focused element on the page is an input (the one\n                         * we're editing) and in that case find the encompassing dropdown item and select it.\n                         */\n                        const focusedElement = document.activeElement;\n                        if (focusedElement.nodeName === \"INPUT\") {\n                            for (const navigatorItem of navigator.items) {\n                                if (navigatorItem.target.contains(focusedElement)) {\n                                    navigatorItem.setActive();\n                                    break;\n                                }\n                            }\n                        }\n\n                        const dropdownItem = findNearestDropdownItem(navigator);\n                        const isSelected = dropdownItem?.target.classList.contains(\"selected\");\n                        const periodType = dropdownItem?.target.dataset.periodType;\n                        const mode = dropdownItem?.target.dataset.mode;\n                        const inputField =\n                            navigator.activeItem.target.nodeName === \"INPUT\"\n                                ? navigator.activeItem.target\n                                : dropdownItem?.target.querySelector(\"input.o_input\");\n                        if (mode === \"view\" && periodType) {\n                            dropdownItem?.setActive();\n                            if (!isSelected) {\n                                // Select the period type on first enter.\n                                this.dateFilter.editing = false;\n                                this.filterClicked({\n                                    optionKey: \"date.filter\",\n                                    optionValue: periodType,\n                                    reload: true,\n                                });\n                            } else {\n                                // Make the input editable on second enter.\n                                this.editDateFilter(periodType, inputField);\n                            }\n                        } else if (mode === \"edit\" && periodType) {\n                            // Save the edited period and return focus to the dropdown item.\n                            this.saveDateFilter(periodType, inputField);\n                            dropdownItem?.setActive();\n                        } else if (periodType) {\n                            // Select the period type and potentially blur an input date field to trigger a save.\n                            inputField?.blur();\n                            this.selectDateFilter(periodType, true);\n                            dropdownItem?.setActive();\n                        }\n                    },\n                    bypassEditableProtection: true,\n                },\n            },\n            shouldFocusChildInput: false,\n        };\n    }\n\n    get periodLabel() {\n        return this.controller.cachedFilterOptions.comparison.number_period > 1 ? _t(\"Periods\") : _t(\"Period\");\n    }\n    //------------------------------------------------------------------------------------------------------------------\n    // Helpers\n    //------------------------------------------------------------------------------------------------------------------\n    get hasAnalyticGroupbyFilter() {\n        return Boolean(this.controller.cachedUserGroups.analytic_accounting) && (Boolean(this.controller.filters.show_analytic_groupby) || Boolean(this.controller.filters.show_analytic_plan_groupby));\n    }\n\n    get hasCodesFilter() {\n        return Boolean(this.controller.cachedFilterOptions.sales_report_taxes?.operation_category?.goods);\n    }\n\n    isExtraOptionFilterShown(option) {\n        let data = this.filterExtraOptionsData[option];\n        return (\n            option in this.controller.cachedFilterOptions &&\n            option in this.filterExtraOptionsData &&\n            data.show !== false &&\n            (data.group === undefined || this.controller.cachedUserGroups[data.group])\n        );\n    }\n\n    get hasExtraOptionsFilter() {\n        return Object.keys(this.filterExtraOptionsData)\n                     .some(option => this.isExtraOptionFilterShown(option));\n    }\n\n    get hasUIFilter() {\n        return Object.entries(this.filterExtraOptionsData)\n                     .some(([option, data]) => data.ui_filter && this.isExtraOptionFilterShown(option));\n    }\n\n    get isBudgetSelected() {\n        return this.controller.cachedFilterOptions.budgets?.some((budget) => {\n            return budget.selected;\n        });\n    }\n\n    //------------------------------------------------------------------------------------------------------------------\n    // Dates\n    //------------------------------------------------------------------------------------------------------------------\n    // Getters\n    dateFrom(optionKey) {\n        return DateTime.fromISO(this.controller.cachedFilterOptions[optionKey].date_from);\n    }\n\n    dateTo(optionKey) {\n        return DateTime.fromISO(this.controller.cachedFilterOptions[optionKey].date_to);\n    }\n\n    // Setters\n    setDate(optionKey, type, date) {\n        if (date) {\n            this.controller.cachedFilterOptions[optionKey][`date_${type}`] = date;\n            this.applyFilters(optionKey);\n        }\n        else {\n            this.dialog.add(WarningDialog, {\n                title: _t(\"Odoo Warning\"),\n                message: _t(\"Date cannot be empty\"),\n            });\n        }\n    }\n\n    setDateFrom(optionKey, dateFrom) {\n        this.setDate(optionKey, 'from', dateFrom);\n    }\n\n    setDateTo(optionKey, dateTo) {\n        this.setDate(optionKey, 'to', dateTo);\n    }\n\n    dateFilters(mode) {\n        switch (mode) {\n            case \"single\":\n                return [\n                    {\n                        name: _t(\"End of Month\"),\n                        period: \"month\",\n                        mode: this.dateFilter.editing === \"month\" ? \"edit\" : \"view\",\n                    },\n                    {\n                        name: _t(\"End of Quarter\"),\n                        period: \"quarter\",\n                        mode: this.dateFilter.editing === \"quarter\" ? \"edit\" : \"view\",\n                    },\n                    {\n                        name: _t(\"End of Year\"),\n                        period: \"year\",\n                        mode: this.dateFilter.editing === \"year\" ? \"edit\" : \"view\",\n                    },\n                ];\n            case \"range\":\n                return [\n                    {\n                        name: _t(\"Month\"),\n                        period: \"month\",\n                        mode: this.dateFilter.editing === \"month\" ? \"edit\" : \"view\",\n                    },\n                    {\n                        name: _t(\"Quarter\"),\n                        period: \"quarter\",\n                        mode: this.dateFilter.editing === \"quarter\" ? \"edit\" : \"view\",\n                    },\n                    {\n                        name: _t(\"Year\"),\n                        period: \"year\",\n                        mode: this.dateFilter.editing === \"year\" ? \"edit\" : \"view\",\n                    },\n                ];\n            default:\n                throw new Error(`Invalid mode in dateFilters(): ${mode}`);\n        }\n    }\n\n    initDateFilters() {\n        const filters = {\n            month: 0,\n            quarter: 0,\n            year: 0,\n            return_period: 0,\n            editing: false,\n        };\n\n        const specifier = this.controller.cachedFilterOptions.date.filter.split('_')[0];\n        const periodType = this.controller.cachedFilterOptions.date.period_type;\n        // In case the period is fiscalyear it will be computed exactly like a year period.\n        const period = periodType === \"fiscalyear\" ? \"year\" : periodType;\n        // Set the filter value based on the specifier.\n        if (Object.prototype.hasOwnProperty.call(filters, period)) {\n            filters[period] = this.controller.cachedFilterOptions.date.period || (specifier === 'previous' ? -1 : specifier === 'next' ? 1 : 0);\n        }\n\n        return filters;\n    }\n\n    getDateFilter(periodType) {\n        if (this.dateFilter[periodType] > 0) {\n            return `next_${periodType}`;\n        } else if (this.dateFilter[periodType] === 0) {\n            return `this_${periodType}`;\n        } else if (this.dateFilter[periodType] < 0) {\n            return `previous_${periodType}`;\n        } else {\n            return periodType;\n        }\n    }\n\n    selectDateFilter(periodType, reload = false) {\n        if (this.isPeriodSelected(periodType)) {\n            return;\n        }\n        this.dateFilter.editing = false;\n        const offsetPeriod = Object.prototype.hasOwnProperty.call(this.dateFilter, periodType);\n        this.filterClicked({\n            optionKey: \"date.filter\",\n            optionValue: this.getDateFilter(periodType),\n            reload: !offsetPeriod && reload,\n        });\n        if (offsetPeriod) {\n            this.filterClicked({\n                optionKey: \"date.period\",\n                optionValue: this.dateFilter[periodType],\n                reload: reload,\n            });\n        }\n    }\n\n    editDateFilter(periodType, inputField) {\n        inputField?.select();\n        this.dateFilter.editing = periodType;\n    }\n\n    saveDateFilter(periodType, inputField) {\n        if (!this.dateFilter.editing) {\n            return;\n        }\n        const enteredValue = inputField?.value;\n        let dateFilterOffset = false;\n        if (periodType === \"month\") {\n            dateFilterOffset = this._parseMonthOffset(enteredValue);\n        } else if (periodType === \"quarter\") {\n            dateFilterOffset = this._parseQuarterOffset(enteredValue);\n        } else if (periodType === \"year\") {\n            dateFilterOffset = this._parseYearOffset(enteredValue);\n        } else if (periodType === \"return_period\") {\n            dateFilterOffset = this._parseReturnPeriodOffset(enteredValue);\n        }\n        if (dateFilterOffset !== false) {\n            dateFilterOffset -= this.dateFilter[periodType];\n            this._changePeriod(periodType, dateFilterOffset);\n        }\n        inputField?.setSelectionRange(enteredValue.length, enteredValue.length);\n        this.dateFilter.editing = false;\n    }\n\n    _parseMonthOffset(input) {\n        try {\n            const monthTo = parseDate(input.trim(), { format: \"MMMM yyyy\" });\n            if (!monthTo.isValid) {\n                return false;\n            }\n            const compareDate = DateTime.now().startOf(\"month\");\n            return monthTo.startOf(\"month\").diff(compareDate, \"months\").months;\n        } catch {\n            return false;\n        }\n    }\n\n    _parseQuarterOffset(input) {\n        try {\n            const quarterTo = parseDate(input.split(\"-\").pop().trim(), { format: \"MMM yyyy\" });\n            if (!quarterTo.isValid) {\n                return false;\n            }\n            const compareDate = DateTime.now().startOf(\"quarter\");\n            return quarterTo.startOf(\"quarter\").diff(compareDate, \"quarters\").quarters;\n        } catch {\n            return false;\n        }\n    }\n\n    _parseYearOffset(input) {\n        try {\n            const yearTo = parseDate(input, { format: \"yyyy\" });\n            if (!yearTo.isValid) {\n                return false;\n            }\n            const compareDate = DateTime.now().startOf(\"year\");\n            return yearTo.startOf(\"year\").diff(compareDate, \"years\").years;\n        } catch {\n            return false;\n        }\n    }\n\n    _parseReturnPeriodOffset(input) {\n        try {\n            const dateTo = parseDate(input.split(\"-\").pop().trim());\n            if (!dateTo.isValid) {\n                return false;\n            }\n            const periodicitySettings = this.controller.cachedFilterOptions.return_periodicity;\n            const [, compareTo] = this._computeReturnPeriodDates(periodicitySettings, DateTime.now());\n            const [, taxPeriodTo] = this._computeReturnPeriodDates(periodicitySettings, dateTo);\n            return (\n                taxPeriodTo.startOf(\"month\").diff(compareTo.startOf(\"month\"), \"months\").months /\n                periodicitySettings.months_per_period\n            );\n        } catch {\n            return false;\n        }\n    }\n\n    selectPreviousPeriod(periodType) {\n        this._changePeriod(periodType, -1);\n    }\n\n    selectNextPeriod(periodType) {\n        this._changePeriod(periodType, 1);\n    }\n\n    _changePeriod(periodType, increment) {\n        this.dateFilter[periodType] = this.dateFilter[periodType] + increment;\n\n        this.controller.updateOption(\"date.filter\", this.getDateFilter(periodType));\n        this.controller.updateOption(\"date.period\", this.dateFilter[periodType]);\n\n        this.applyFilters(\"date.period\");\n    }\n\n    isPeriodSelected(periodType) {\n        return this.controller.cachedFilterOptions.date.filter.endsWith(periodType)\n    }\n\n    get shouldDisplayReturnPeriod() {\n        const periodicitySettings = this.controller.cachedFilterOptions.return_periodicity;\n        if (periodicitySettings) {\n            return periodicitySettings.start_day !== 1 || periodicitySettings.start_month !== 1 || ![1, 3, 12].includes(periodicitySettings.months_per_period);\n        }\n\n        return false;\n    }\n\n    displayPeriod(periodType) {\n        const dateTo = DateTime.now();\n\n        if (periodType === \"return_period\" && !this.controller.cachedFilterOptions.return_periodicity)\n            periodType = \"month\";\n\n        switch (periodType) {\n            case \"month\":\n                return this._displayMonth(dateTo);\n            case \"quarter\":\n                return this._displayQuarter(dateTo);\n            case \"year\":\n                return this._displayYear(dateTo);\n            case \"return_period\":\n                return this._displayReturnPeriod(dateTo);\n            default:\n                throw new Error(`Invalid period type in displayPeriod(): ${ periodType }`);\n        }\n    }\n\n    _displayMonth(dateTo) {\n        return dateTo.plus({ months: this.dateFilter.month }).toFormat(\"MMMM yyyy\");\n    }\n\n    _displayQuarter(dateTo) {\n        const quarterMonths = {\n            1: { 'start': 1, 'end': 3 },\n            2: { 'start': 4, 'end': 6 },\n            3: { 'start': 7, 'end': 9 },\n            4: { 'start': 10, 'end': 12 },\n        }\n\n        dateTo = dateTo.plus({ months: this.dateFilter.quarter * 3 });\n\n        const quarterDateFrom = DateTime.utc(dateTo.year, quarterMonths[dateTo.quarter]['start'], 1)\n        const quarterDateTo = DateTime.utc(dateTo.year, quarterMonths[dateTo.quarter]['end'], 1)\n\n        return `${ formatDate(quarterDateFrom, {format: \"MMM\"}) } - ${ formatDate(quarterDateTo, {format: \"MMM yyyy\"}) }`;\n    }\n\n    _displayYear(dateTo) {\n        return dateTo.plus({ years: this.dateFilter.year }).toFormat(\"yyyy\");\n    }\n\n    _displayReturnPeriod(dateTo) {\n        const periodicitySettings = this.controller.cachedFilterOptions.return_periodicity;\n        const targetDateInPeriod = dateTo.plus({months: periodicitySettings.months_per_period * this.dateFilter['return_period']})\n        const [start, end] = this._computeReturnPeriodDates(periodicitySettings, targetDateInPeriod);\n        return formatDate(start) + ' - ' + formatDate(end);\n    }\n\n    _computeReturnPeriodDates(periodicitySettings, dateInsideTargettesPeriod) {\n        /**\n         * This function need to stay consitent with the one inside account_return_type from module account_reports.\n         * function_name = _get_period_boundaries\n         */\n        const startMonth = periodicitySettings.start_month;\n        const startDay = periodicitySettings.start_day\n        const monthsPerPeriod = periodicitySettings.months_per_period;\n        const aligned_date = dateInsideTargettesPeriod.minus({days: startDay - 1})\n        let year = aligned_date.year;\n        const monthOffset = aligned_date.month - startMonth;\n\n        let periodNumber = Math.floor(monthOffset / monthsPerPeriod) + 1;\n\n        if (dateInsideTargettesPeriod < DateTime.now().set({year: year, month: startMonth, day: startDay})) {\n            year -= 1;\n            periodNumber = Math.floor((12 + monthOffset) / monthsPerPeriod) + 1;\n        }\n\n        let deltaMonth = periodNumber * monthsPerPeriod;\n\n        const endDate = DateTime.utc(year, startMonth, 1).plus({ months: deltaMonth, days: startDay-2})\n        const startDate = DateTime.utc(year, startMonth, 1).plus({ months: deltaMonth-monthsPerPeriod }).set({ day: startDay})\n        return [startDate, endDate];\n    }\n\n    //------------------------------------------------------------------------------------------------------------------\n    // Number of periods\n    //------------------------------------------------------------------------------------------------------------------\n    setNumberPeriods(ev) {\n        const numberPeriods = ev.target.value;\n\n        if (numberPeriods >= 1)\n            this.controller.cachedFilterOptions.comparison.number_period = parseInt(numberPeriods);\n        else\n            this.dialog.add(WarningDialog, {\n                title: _t(\"Odoo Warning\"),\n                message: _t(\"Number of periods cannot be smaller than 1\"),\n            });\n    }\n\n    //------------------------------------------------------------------------------------------------------------------\n    // Records\n    //------------------------------------------------------------------------------------------------------------------\n    getMultiRecordSelectorProps(resModel, optionKey) {\n        return {\n            resModel,\n            resIds: this.controller.cachedFilterOptions[optionKey],\n            update: (resIds) => {\n                this.filterClicked({ optionKey: optionKey, optionValue: resIds, reload: true});\n            },\n        };\n    }\n\n    //------------------------------------------------------------------------------------------------------------------\n    // Rounding unit\n    //------------------------------------------------------------------------------------------------------------------\n    roundingUnitName(roundingUnit) {\n        return _t(\"In %s\", this.controller.cachedFilterOptions[\"rounding_unit_names\"][roundingUnit][0]);\n    }\n\n    //------------------------------------------------------------------------------------------------------------------\n    // Generic filters\n    //------------------------------------------------------------------------------------------------------------------\n    async filterClicked({ optionKey, optionValue = undefined, reload = false}) {\n        if (optionValue !== undefined) {\n            await this.controller.updateOption(optionKey, optionValue);\n        } else {\n            await this.controller.toggleOption(optionKey);\n        }\n\n        if (reload) {\n            await this.applyFilters(optionKey);\n        }\n    }\n\n    async applyFilters(optionKey = null, delay = 500) {\n        // We only call the reload after the delay is finished, to avoid doing 5 calls if you want to click on 5 journals\n        if (this.timeout) {\n            clearTimeout(this.timeout);\n        }\n\n        this.controller.incrementCallNumber();\n\n        this.timeout = setTimeout(async () => {\n            if (status(this) !== \"destroyed\")\n                await this.controller.reload(optionKey, this.controller.cachedFilterOptions);\n        }, delay);\n    }\n\n    //------------------------------------------------------------------------------------------------------------------\n    // Custom filters\n    //------------------------------------------------------------------------------------------------------------------\n    selectJournal(journal) {\n        if (journal.model === \"account.journal.group\") {\n            const wasSelected = journal.selected;\n            this.ToggleSelectedJournal(journal);\n            this.controller.cachedFilterOptions.__journal_group_action = {\n                action: wasSelected ? \"remove\" : \"add\",\n                id: parseInt(journal.id),\n            };\n            // Toggle the selected status after the action is set\n            journal.selected = !wasSelected;\n        } else {\n            journal.selected = !journal.selected;\n        }\n        this.applyFilters(\"journals\");\n    }\n\n    ToggleSelectedJournal(selectedJournal) {\n        if (selectedJournal.selected) {\n            this.controller.cachedFilterOptions.journals.forEach((journal) => {\n                journal.selected = false;\n            });\n        } else {\n            this.controller.cachedFilterOptions.journals.forEach((journal) => {\n                journal.selected = selectedJournal.journals.includes(journal.id) && journal.model === \"account.journal\";\n            });\n        }\n    }\n\n    unfoldCompanyJournals(selectedCompany) {\n        let inSelectedCompanySection = false;\n        for (const journal of this.controller.cachedFilterOptions.journals) {\n            if (journal.id === \"divider\" && journal.model === \"res.company\") {\n                if (journal.name === selectedCompany.name) {\n                    journal.unfolded = !journal.unfolded;\n                    inSelectedCompanySection = true;\n                } else if (inSelectedCompanySection) {\n                    break;  // Reached another company divider, exit the loop\n                }\n            }\n            if (inSelectedCompanySection && journal.model === \"account.journal\") {\n                journal.visible = !journal.visible;\n            }\n        }\n    }\n\n    async filterVariant(reportId) {\n        this.controller.saveSessionOptions({\n            ...this.controller.cachedFilterOptions,\n            selected_variant_id: reportId,\n            sections_source_id: reportId,\n        });\n        const cacheKey = this.controller.getCacheKey(reportId, reportId);\n        // if the variant hasn't been loaded yet, set up the call number\n        if (!(cacheKey in this.controller.loadingCallNumberByCacheKey)) {\n            this.controller.incrementCallNumber(cacheKey);\n        }\n        await this.controller.displayReport(reportId);\n    }\n\n    async filterTaxUnit(taxUnit) {\n        await this.filterClicked({ optionKey: \"tax_unit\", optionValue: taxUnit.id});\n        this.controller.saveSessionOptions(this.controller.cachedFilterOptions);\n\n        // force the company to those impacted by the tax units, the reload will be force by this function\n        user.activateCompanies(taxUnit.company_ids);\n    }\n\n    async toggleHideZeroLines() {\n        // Avoid calling the database when this filter is toggled; as the exact same lines would be returned; just reassign visibility.\n        await this.controller.toggleOption(\"hide_0_lines\", false);\n\n        this.controller.saveSessionOptions(this.controller.cachedFilterOptions);\n        this.controller.setLineVisibility(this.controller.lines);\n    }\n\n    async toggleHorizontalSplit() {\n        await this.controller.toggleOption(\"horizontal_split\", false);\n        this.controller.saveSessionOptions(this.controller.cachedFilterOptions);\n    }\n\n    async filterRoundingUnit(rounding) {\n        await this.controller.updateOption('rounding_unit', rounding, false);\n\n        this.controller.saveSessionOptions(this.controller.cachedFilterOptions);\n\n        this.controller.lines = await this.controller.orm.call(\n            \"account.report\",\n            \"dispatch_report_action\",\n            [\n                this.controller.cachedFilterOptions.report_id,\n                this.controller.cachedFilterOptions,\n                \"format_column_values_from_client\",\n                this.controller.lines,\n            ],\n            {\n                context: this.controller.context,\n            }\n        );\n    }\n\n    async selectHorizontalGroup(horizontalGroupId) {\n        if (horizontalGroupId === this.controller.cachedFilterOptions.selected_horizontal_group_id) {\n            return;\n        }\n        await this.filterClicked({ optionKey: \"selected_horizontal_group_id\", optionValue: horizontalGroupId, reload: true});\n    }\n\n    selectBudget(budget) {\n        budget.selected = !budget.selected;\n        this.applyFilters( 'budgets')\n    }\n\n    async createBudget() {\n        const budgetName = this.budgetName.value.trim();\n        if (!budgetName.length) {\n            this.budgetName.invalid = true;\n            this.notification.add(_t(\"Please enter a valid budget name.\"), {\n                type: \"danger\",\n            });\n            return;\n        }\n        const createdId = await this.orm.call(\"account.report.budget\", \"create\", [\n            { name: budgetName },\n        ]);\n        this.budgetName.value = \"\";\n        this.budgetName.invalid = false;\n        const cachedFilterOptions = this.controller.cachedFilterOptions;\n        this.controller.reload(\"budgets\", {\n            ...cachedFilterOptions,\n            budgets: [\n                ...cachedFilterOptions.budgets,\n                // Selected by default if we don't have any horizontal group selected\n                { id: createdId, selected: !this.isHorizontalGroupSelected },\n            ],\n        });\n    }\n}\n", "import { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\nimport { useService } from \"@web/core/utils/hooks\"\n\nimport { Component, useState } from \"@odoo/owl\";\n\nexport class AccountReportHeader extends Component {\n    static template = \"account_reports.AccountReportHeader\";\n    static props = {};\n    static components = {\n        Dropdown,\n        DropdownItem,\n    }\n\n    setup() {\n        this.orm = useService(\"orm\");\n        this.controller = useState(this.env.controller);\n    }\n    // -----------------------------------------------------------------------------------------------------------------\n    // Headers\n    // -----------------------------------------------------------------------------------------------------------------\n    get columnHeaders() {\n        let columnHeaders = [];\n\n        this.controller.options.column_headers.forEach((columnHeader, columnHeaderIndex) => {\n            let columnHeadersRow = [];\n\n            for (let i = 0; i < this.controller.columnHeadersRenderData.level_repetitions[columnHeaderIndex]; i++) {\n                columnHeadersRow = [ ...columnHeadersRow, ...columnHeader];\n            }\n\n            columnHeaders.push(columnHeadersRow);\n        });\n\n        return columnHeaders;\n    }\n\n    columnHeadersColspan(column_index, header, compactOffset = 0) {\n        let colspan = header.colspan || this.controller.columnHeadersRenderData.level_colspan[column_index]\n        // In case of we need the total column for horizontal we need to increase the colspan of the first row\n        if(this.controller.options.show_horizontal_group_total && column_index === 0) {\n           colspan += 1;\n        }\n        return colspan;\n    }\n\n    columnHeadersRowspan(header) {\n        return header?.forced_options?.no_subheader_division\n            ? this.controller.options.column_headers.length - 1\n            : 1;\n    }\n\n    //------------------------------------------------------------------------------------------------------------------\n    // Subheaders\n    //------------------------------------------------------------------------------------------------------------------\n    get subheaders() {\n        const columns = JSON.parse(JSON.stringify(this.controller.options.columns));\n        const columnsPerGroupKey = {};\n\n        columns.forEach((column) => {\n            columnsPerGroupKey[`${column.column_group_key}_${column.expression_label}`] = column;\n        });\n\n        return this.controller.lines[0].columns.map((column) => {\n            if (columnsPerGroupKey[`${column.column_group_key}_${column.expression_label}`]) {\n                return columnsPerGroupKey[`${column.column_group_key}_${column.expression_label}`];\n            } else {\n                return {\n                    expression_label: \"\",\n                    sortable: false,\n                    name: \"\",\n                    colspan: 1,\n                };\n            }\n        });\n    }\n\n    // -----------------------------------------------------------------------------------------------------------------\n    // Custom subheaders\n    // -----------------------------------------------------------------------------------------------------------------\n    get customSubheaders() {\n        let customSubheaders = [];\n\n        this.controller.columnHeadersRenderData.custom_subheaders.forEach(customSubheader => {\n            customSubheaders.push(customSubheader);\n        });\n\n        return customSubheaders;\n    }\n\n    // -----------------------------------------------------------------------------------------------------------------\n    // Sortable\n    // -----------------------------------------------------------------------------------------------------------------\n    sortableClasses(columIndex) {\n        switch (this.controller.linesCurrentOrderByColumn(columIndex)) {\n            case \"ASC\":\n                return \"fa fa-long-arrow-up\";\n            case \"DESC\":\n                return \"fa fa-long-arrow-down\";\n            default:\n                return \"fa fa-arrows-v\";\n        }\n    }\n\n    async sortLinesByColumn(columnIndex, column) {\n        if (column.sortable) {\n            switch (this.controller.linesCurrentOrderByColumn(columnIndex)) {\n                case \"default\":\n                    await this.controller.sortLinesByColumnAsc(columnIndex);\n                    break;\n                case \"ASC\":\n                    await this.controller.sortLinesByColumnDesc(columnIndex);\n                    break;\n                case \"DESC\":\n                    this.controller.sortLinesByDefault();\n                    break;\n                default:\n                    throw new Error(`Invalid value: ${ this.controller.linesCurrentOrderByColumn(columnIndex) }`);\n            }\n        }\n    }\n}\n", "import { localization } from \"@web/core/l10n/localization\";\n\nimport { useService } from \"@web/core/utils/hooks\";\nimport { Component, useState } from \"@odoo/owl\";\n\nimport { AccountReportDebugPopover } from \"@account_reports/components/account_report/line/popover/debug_popover\";\nimport { AccountReportLineCellEditable } from \"@account_reports/components/account_report/line_cell_editable/line_cell_editable\";\n\nexport class AccountReportLine extends Component {\n    static template = \"account_reports.AccountReportLine\";\n    static props = {\n        lineIndex: Number,\n        line: Object,\n    };\n    static components = {\n        AccountReportLineCellEditable,\n    };\n\n    setup() {\n        this.popover = useService(\"popover\");\n        this.controller = useState(this.env.controller);\n    }\n\n    // -----------------------------------------------------------------------------------------------------------------\n    // Line\n    // -----------------------------------------------------------------------------------------------------------------\n    get lineClasses() {\n        let classes = ('level' in this.props.line) ? `line_level_${this.props.line.level}` : 'line_level_default';\n\n        if (!this.props.line.visible || this.isHiddenBySearchFilter())\n            classes += \" d-none\";\n\n        if (this.props.line.unfolded && this.hasVisibleChild())\n            classes += \" unfolded\";\n\n        if (this.controller.isTotalLine(this.props.lineIndex))\n            classes += \" total\";\n\n        if (this.props.line.class)\n            classes += ` ${this.props.line.class}`;\n\n        return classes;\n    }\n\n    hasVisibleChild() {\n        let nextLineIndex = this.props.lineIndex + 1;\n\n        while (this.controller.isNextLineChild(nextLineIndex, this.props.line['id'])) {\n            if (this.controller.lines[nextLineIndex].visible && !this.isHiddenBySearchFilter(this.controller.lines[nextLineIndex].id))\n                return true;\n\n            nextLineIndex += 1;\n        }\n        return false;\n    }\n\n    // -----------------------------------------------------------------------------------------------------------------\n    // Growth comparison\n    // -----------------------------------------------------------------------------------------------------------------\n    get growthComparisonClasses() {\n        let classes = \"text-end\";\n\n        switch(this.props.line.column_percent_comparison_data.mode) {\n            case \"green\":\n                classes += \" text-success\";\n                break;\n            case \"muted\":\n                classes += \" muted\";\n                break;\n            case \"red\":\n                classes += \" text-danger\";\n                break;\n        }\n\n        return classes;\n    }\n\n    // -----------------------------------------------------------------------------------------------------------------\n    // Total Horizontal Group\n    // -----------------------------------------------------------------------------------------------------------------\n    get HorizontalGroupTotalClasses() {\n        let classes = \"text-end\";\n        switch(Math.sign(this.props.line.horizontal_group_total_data?.no_format)) {\n            case 1:\n                break;\n            case 0:\n                classes += \" muted\";\n                break;\n            case -1:\n                classes += \" text-danger\";\n                break;\n        }\n\n        return classes;\n    }\n\n\n    //------------------------------------------------------------------------------------------------------------------\n    // Search\n    //------------------------------------------------------------------------------------------------------------------\n    isHiddenBySearchFilter(lineId = null) {\n        // If no lineId is provided, this will execute on the current line object\n        // Otherwise, it will execute on the given lineId\n        lineId ||= this.props.line.id;\n\n        if (!(\"lines_searched\" in this.controller))\n            return false;\n\n        for (let searchLineId of this.controller.lines_searched)\n            if (this.controller.isLineRelatedTo(searchLineId, lineId) || lineId === searchLineId)\n                return false;\n\n        return true;\n    }\n\n    //------------------------------------------------------------------------------------------------------------------\n    // Debug popover\n    //------------------------------------------------------------------------------------------------------------------\n    showDebugPopover(ev) {\n        const close = () => {\n            this.popoverCloseFn();\n            this.popoverCloseFn = null;\n        }\n\n        if (this.popoverCloseFn)\n            close();\n\n        this.popoverCloseFn = this.popover.add(\n            ev.currentTarget,\n            AccountReportDebugPopover,\n            {\n                expressionsDetail: JSON.parse(this.props.line.debug_popup_data).expressions_detail,\n                onClose: close,\n            },\n            {\n                closeOnClickAway: true,\n                position: localization.direction === \"rtl\" ? \"left\" : \"right\",\n            },\n        );\n    }\n}\n", "import { Component } from \"@odoo/owl\";\n\nexport class AccountReportDebugPopover extends Component {\n    static template = \"account_reports.AccountReportDebugPopover\";\n    static props = {\n        close: Function,\n        expressionsDetail: Array,\n        onClose: Function,\n    };\n}\n", "import { localization } from \"@web/core/l10n/localization\";\nimport { useService } from \"@web/core/utils/hooks\";\n\nimport { AccountReportCarryoverPopover } from \"@account_reports/components/account_report/line_cell/popover/carryover_popover\";\nimport { AccountReportEditPopover } from \"@account_reports/components/account_report/line_cell/popover/edit_popover\";\n\nimport { Component, markup, useState } from \"@odoo/owl\";\n\nexport class AccountReportLineCell extends Component {\n    static template = \"account_reports.AccountReportLineCell\";\n    static props = {\n        line: {\n            type: Object,\n            optional: true,\n        },\n        cell: Object,\n    };\n\n    setup() {\n        this.action = useService(\"action\");\n        this.orm = useService(\"orm\");\n        this.popover = useService(\"popover\");\n        this.controller = useState(this.env.controller);\n    }\n\n    // -----------------------------------------------------------------------------------------------------------------\n    // Helpers\n    // -----------------------------------------------------------------------------------------------------------------\n    isNumeric(type) {\n        return ['float', 'integer', 'monetary', 'percentage'].includes(type);\n    }\n\n    // -----------------------------------------------------------------------------------------------------------------\n    // Attributes\n    // -----------------------------------------------------------------------------------------------------------------\n    get cellClasses() {\n        if (this.props.cell.comparison_mode) {\n            return this.comparisonClasses;\n        }\n\n        let classes = \"\";\n\n        if (this.props.cell.auditable)\n            classes += \" auditable\";\n\n        if (this.props.cell.figure_type === 'date')\n            classes += \" date\";\n\n        if (this.props.cell.figure_type === 'string')\n            classes += \" text\";\n\n        if (this.isNumeric(this.props.cell.figure_type)) {\n            classes += \" numeric text-end\";\n\n            if (this.props.cell.no_format !== undefined)\n                switch (Math.sign(this.props.cell.no_format)) {\n                    case 1:\n                        break;\n                    case 0:\n                    case -0:\n                        classes += \" muted\";\n                        break;\n                    case -1:\n                        classes += \" text-danger\";\n                        break;\n                }\n        }\n\n        if (this.props.cell.class)\n            classes += ` ${this.props.cell.class}`;\n\n        return classes;\n    }\n\n    // -----------------------------------------------------------------------------------------------------------------\n    // Audit\n    // -----------------------------------------------------------------------------------------------------------------\n    async audit() {\n        const auditAction = await this.orm.call(\n            \"account.report\",\n            \"dispatch_report_action\",\n            [\n                this.controller.options.report_id,\n                this.controller.options,\n                \"action_audit_cell\",\n                {\n                    report_line_id: this.props.cell.report_line_id,\n                    expression_label: this.props.cell.expression_label,\n                    calling_line_dict_id: this.props.line.id,\n                    column_group_key: this.props.cell.column_group_key,\n                },\n            ],\n            {\n                context: this.controller.context,\n            }\n        );\n        if (auditAction.help) {\n            auditAction.help = markup(auditAction.help);\n        }\n\n        return this.action.doAction(auditAction);\n    }\n\n    // -----------------------------------------------------------------------------------------------------------------\n    // Edit Popover\n    // -----------------------------------------------------------------------------------------------------------------\n    editPopover(ev) {\n        const close = () => {\n            this.popoverCloseFn();\n            this.popoverCloseFn = null;\n        }\n\n        if (this.popoverCloseFn)\n            close();\n\n        this.popoverCloseFn = this.popover.add(\n            ev.currentTarget,\n            AccountReportEditPopover,\n            {\n                line_id: this.props.line.id,\n                cell: this.props.cell,\n                controller: this.controller,\n                onClose: close,\n            },\n            {\n                closeOnClickAway: true,\n                position: localization.direction === \"rtl\" ? \"bottom\" : \"left\",\n            },\n        );\n    }\n\n    //------------------------------------------------------------------------------------------------------------------\n    // Carryover popover\n    //------------------------------------------------------------------------------------------------------------------\n    carryoverPopover(ev) {\n        if (this.popoverCloseFn) {\n            this.popoverCloseFn();\n            this.popoverCloseFn = null;\n        }\n\n        this.popoverCloseFn = this.popover.add(\n            ev.currentTarget,\n            AccountReportCarryoverPopover,\n            {\n                carryoverData: JSON.parse(this.props.cell.info_popup_data),\n                options: this.controller.options,\n                context: this.controller.context,\n            },\n            {\n                closeOnClickAway: true,\n                position: localization.direction === \"rtl\" ? \"bottom\" : \"right\",\n            },\n        );\n    }\n\n    // -----------------------------------------------------------------------------------------------------------------\n    // Comparison cell\n    // -----------------------------------------------------------------------------------------------------------------\n    get comparisonClasses() {\n        let classes = \"text-end\";\n\n        switch (this.props.cell.comparison_mode) {\n            case \"green\":\n                classes += \" text-success\";\n                break;\n            case \"muted\":\n                classes += \" muted\";\n                break;\n            case \"red\":\n                classes += \" text-danger\";\n                break;\n        }\n\n        return classes;\n    }\n}\n", "import { useService } from \"@web/core/utils/hooks\";\nimport { Component } from \"@odoo/owl\";\n\nexport class AccountReportCarryoverPopover extends Component {\n    static template = \"account_reports.AccountReportCarryoverPopover\";\n    static props = {\n        close: Function,\n        carryoverData: Object,\n        options: Object,\n        context: Object,\n    };\n\n    setup() {\n        this.actionService = useService(\"action\");\n        this.orm = useService(\"orm\");\n    }\n\n    //------------------------------------------------------------------------------------------------------------------\n    //\n    //------------------------------------------------------------------------------------------------------------------\n    async viewCarryoverLinesAction(expressionId, columnGroupKey) {\n        const viewCarryoverLinesAction = await this.orm.call(\n            \"account.report.expression\",\n            \"action_view_carryover_lines\",\n            [\n                expressionId,\n                this.props.options,\n                columnGroupKey,\n            ],\n            {\n                context: this.props.context,\n            }\n        );\n        this.props.close()\n        return this.actionService.doAction(viewCarryoverLinesAction);\n    }\n}\n", "import { useAutofocus, useService } from \"@web/core/utils/hooks\";\n\nimport { Component, useRef } from \"@odoo/owl\";\n\nexport class AccountReportEditPopover extends Component {\n    static template = \"account_reports.AccountReportEditPopover\";\n    static props = {\n        line_id: String,\n        cell: Object,\n        controller: Object,\n        onClose: Function,\n        close: Function,\n    };\n\n    setup() {\n        this.orm = useService(\"orm\");\n\n        if (this.props.cell.figure_type === 'boolean') {\n            this.booleanTrue = useRef(\"booleanTrue\");\n            this.booleanFalse = useRef(\"booleanFalse\");\n        } else {\n            this.input = useRef(\"input\");\n            useAutofocus({ refName: \"input\" });\n        }\n    }\n\n    // -----------------------------------------------------------------------------------------------------------------\n    // Edit\n    // -----------------------------------------------------------------------------------------------------------------\n    async edit() {\n        let editValue;\n        const editPopupData = JSON.parse(this.props.cell.edit_popup_data);\n\n        if (this.props.cell.figure_type === 'boolean')\n            editValue = Number(this.booleanTrue.el.checked && !this.booleanFalse.el.checked);\n        else\n            editValue = this.input.el.value;\n\n        const res = await this.orm.call(\n            \"account.report\",\n            \"action_modify_manual_value\",\n            [\n                this.props.controller.options.report_id,\n                this.props.line_id,\n                this.props.controller.options,\n                editPopupData.column_group_key,\n                editValue,\n                editPopupData.target_expression_id,\n                editPopupData.rounding,\n                this.props.controller.columnGroupsTotals,\n            ],\n            {\n                context: this.props.controller.context,\n            }\n        );\n\n        this.props.controller.lines = res.lines;\n        this.props.controller.columnGroupsTotals = res.column_groups_totals;\n\n        this.props.onClose();\n    }\n}\n", "import { useRef, useState } from \"@odoo/owl\";\nimport { useHotkey } from \"@web/core/hotkeys/hotkey_hook\";\n\nimport { AccountReportLineCell } from \"@account_reports/components/account_report/line_cell/line_cell\";\n\nexport class AccountReportLineCellEditable extends AccountReportLineCell {\n    static template = \"account_reports.AccountReportLineCellEditable\";\n\n    setup() {\n        super.setup();\n        this.input = useRef(\"input\");\n        this.focused = useState({ value: false });\n\n        useHotkey(\n            \"Enter\",\n            (ev) => {\n                ev.target.blur();\n            },\n            { bypassEditableProtection: true }\n        );\n    }\n\n    get cellClasses() {\n        let classes = super.cellClasses;\n        if (this.hasEditPopupData) {\n            classes += \" editable-cell\";\n        }\n        return classes;\n    }\n\n    get hasEditPopupData() {\n        return Boolean(this.props.cell?.edit_popup_data);\n    }\n\n    async onChange() {\n        if (!this.input.el.value.trim()) {\n            return;\n        }\n        const editValue = this.input.el.value;\n        const cellEditData = this.hasEditPopupData\n            ? JSON.parse(this.props.cell.edit_popup_data)\n            : {};\n\n        const res = await this.orm.call(\n            \"account.report\",\n            \"action_modify_manual_value\",\n            [\n                this.controller.options.report_id,\n                this.props.line.id,\n                this.controller.options,\n                cellEditData.column_group_key,\n                editValue,\n                cellEditData.target_expression_id,\n                cellEditData.rounding,\n                this.controller.columnGroupsTotals,\n            ],\n            {\n                context: this.controller.context,\n            }\n        );\n        this.controller.lines = res.lines;\n        this.controller.columnGroupsTotals = res.column_groups_totals;\n        this.focused.value = false;\n    }\n\n    get inputValue() {\n        return this.focused.value ? this.props.cell.no_format : this.props.cell.name;\n    }\n\n    onFocus() {\n        this.focused.value = true;\n    }\n\n    onBlur() {\n        if (JSON.stringify(this.props.cell.no_format) === this.input.el.value) {\n            this.focused.value = false;\n        }\n    }\n}\n", "import { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { Component, useState, useRef, useEffect } from \"@odoo/owl\";\n\nimport { parseLineId } from \"@account_reports/js/util\";\n\nimport { RelationalModel } from \"@web/model/relational_model/relational_model\";\n\nimport { AccountReturnSelectionBadge } from \"../../account_return/widgets/account_return_selection_badge\";\n\nexport class AccountReportLineName extends Component {\n    static template = \"account_reports.AccountReportLineName\";\n    static props = {\n        lineIndex: Number,\n        line: Object,\n    };\n    static components = {\n        Dropdown,\n        DropdownItem,\n        AccountReturnSelectionBadge,\n    }\n\n    setup() {\n        this.action = useService(\"action\");\n        this.orm = useService(\"orm\");\n        this.ui = useService(\"ui\");\n        this.controller = useState(this.env.controller);\n\n        this.lineNameCell = useRef(\"lineNameCell\");\n\n        this.accountStatus = useState({ record: false });\n        useEffect(()=> {\n            this.loadAuditStatus();\n        }, () => [this.props.line]);\n    }\n\n    async loadAuditStatus() {\n        if (this.props.line.account_status) {\n            if (this.accountStatus.record && this.props.line.account_status.id === this.accountStatus.record.resId)\n                return;\n\n            const fields = {\n                status: {\n                    selection: [\n                        ['todo', 'To Review'],\n                        ['reviewed', 'Reviewed'],\n                        ['supervised', 'Supervised'],\n                        ['anomaly', 'Anomaly'],\n                    ],\n                    required: false,\n                }\n            }\n\n\n            const model = new RelationalModel(\n                this.env,\n                {\n                    config: {\n                        resModel: 'account.audit.account.status',\n                        fields: fields,\n                        activeFields: fields,\n                        openGroupsByDefault: true,\n                        isMonoRecord: true\n                    },\n                    groupsLimit: Number.MAX_SAFE_INTEGER,\n                    limit: 1,\n                    countLimit: 1,\n                },\n                {orm: this.orm}\n            );\n\n            this.accountStatus.record = new model.constructor.Record(\n                model,\n                {\n                    context: this.env.controller.context,\n                    activeFields: fields,\n                    fields: fields,\n                    resModel: 'account.audit.account.status',\n                    resId: this.props.line.account_status.id,\n                    resIds: [this.props.line.account_status.id],\n                    isMonoRecord: true,\n                    mode: 'readonly',\n                },\n                this.props.line.account_status,\n                { manuallyAdded: !this.props.line.account_status.id }\n            )\n        }\n        else if (this.accountStatus.record) {\n            this.accountStatus.record = false\n        }\n    }\n\n    get accountStatusBadgeOptions() {\n        return {\n            todo: { decoration: \"muted\" },\n            reviewed: { decoration: \"info\" },\n            supervised: { decoration: \"success\" },\n            anomaly: { decoration: \"danger\" },\n        };\n    }\n\n    get modelName() {\n        return parseLineId(this.props.line.id).at(-1)[1];\n    }\n\n    //------------------------------------------------------------------------------------------------------------------\n    // Caret options\n    //------------------------------------------------------------------------------------------------------------------\n    get caretOptions() {\n        return this.controller.caretOptions[this.props.line.caret_options];\n    }\n\n    get hasCaretOptions() {\n        return this.caretOptions?.length > 0;\n    }\n\n    async caretAction(caretOption) {\n        const res = await this.orm.call(\n            \"account.report\",\n            \"dispatch_report_action\",\n            [\n                this.controller.options.report_id,\n                this.controller.options,\n                caretOption.action,\n                {\n                    line_id: this.props.line.id,\n                    action_param: caretOption.action_param,\n                },\n            ],\n            {\n                context: this.controller.context,\n            }\n        );\n\n        return this.action.doAction(res);\n    }\n\n    // -----------------------------------------------------------------------------------------------------------------\n    // Classes\n    // -----------------------------------------------------------------------------------------------------------------\n    get lineNameClasses() {\n        let classes = \"text\";\n\n        if (this.props.line.unfoldable)\n            classes += \" unfoldable\";\n\n        if (this.props.line.is_draft)\n            classes += \" draft\";\n\n        if (this.props.line.class)\n            classes += ` ${ this.props.line.class }`;\n\n        return classes;\n    }\n\n    // -----------------------------------------------------------------------------------------------------------------\n    // Action\n    // -----------------------------------------------------------------------------------------------------------------\n    async triggerAction() {\n        const res = await this.orm.call(\n            \"account.report\",\n            \"execute_action\",\n            [\n                this.controller.options.report_id,\n                this.controller.options,\n                {\n                    id: this.props.line.id,\n                    actionId: this.props.line.action_id,\n                },\n            ],\n            {\n                context: this.controller.context,\n            }\n        );\n\n        return this.action.doAction(res);\n    }\n\n    // -----------------------------------------------------------------------------------------------------------------\n    // Load more\n    // -----------------------------------------------------------------------------------------------------------------\n    async loadMore() {\n        const newLines = await this.orm.call(\n            \"account.report\",\n            \"get_expanded_lines\",\n            [\n                this.controller.options.report_id,\n                this.controller.options,\n                this.props.line.parent_id,\n                this.props.line.groupby,\n                this.props.line.expand_function,\n                this.props.line.progress,\n                this.props.line.offset,\n                this.props.line.horizontal_split_side,\n            ],\n        );\n\n        this.controller.setLineVisibility(newLines)\n        if (this.controller.areLinesOrdered()) {\n            this.controller.updateLinesOrderIndexes(this.props.lineIndex, newLines, true)\n        }\n        await this.controller.replaceLineWith(this.props.lineIndex, newLines);\n    }\n\n    // -----------------------------------------------------------------------------------------------------------------\n    // Fold / Unfold\n    // -----------------------------------------------------------------------------------------------------------------\n    toggleFoldable() {\n        if (this.props.line.unfoldable)\n            if (this.props.line.unfolded)\n                this.controller.foldLine(this.props.lineIndex);\n            else\n                this.controller.unfoldLine(this.props.lineIndex);\n    }\n\n    // -----------------------------------------------------------------------------------------------------------------\n    // Chatter\n    // -----------------------------------------------------------------------------------------------------------------\n    get isChatterAnnotated() {\n        return this.props.line.visible_annotations;\n    }\n\n    get isChatterSelected() {\n        return this.controller.chatterState.lineId === this.props.line.id;\n    }\n\n    async openChatter() {\n        if (!this.props.line.chatter) {\n            return;\n        }\n\n        this.controller.toggleLineChatter({\n            resModel: this.props.line.chatter.model,\n            resId: this.props.line.chatter.id,\n            line_id: this.props.line.id,\n        });\n    }\n}\n", "import { Component, useRef, useState, onMounted } from \"@odoo/owl\";\n\nexport class AccountReportSearchBar extends Component {\n    static template = \"account_reports.AccountReportSearchBar\";\n    static props = {\n        initialQuery: { type: String, optional: true },\n    };\n\n    setup() {\n        this.searchText = useRef(\"search_bar_input\");\n        this.controller = useState(this.env.controller);\n\n        onMounted(() => {\n            if (this.props.initialQuery) {\n                this.searchText.el.value = this.props.initialQuery;\n                this.search();\n            }\n        });\n    }\n\n    //------------------------------------------------------------------------------------------------------------------\n    // Search\n    //------------------------------------------------------------------------------------------------------------------\n    async search() {\n        const inputText = this.searchText.el.value.trim();\n        const query = inputText.toLowerCase();\n        const linesIDsMatched = [];\n\n        // Since the search bar is loaded before the report data, we need to wait for it\n        await this.controller.reportLoadingPromise;\n\n        if (query.length) {\n            for (const line of this.controller.lines) {\n                if (!line.name) continue;\n\n                const lineName = line.name.trim().toLowerCase();\n                const match = (lineName.indexOf(query) !== -1);\n\n                if (match) {\n                    linesIDsMatched.push(line.id);\n                }\n            }\n            this.controller.lines_searched = linesIDsMatched;\n            this.controller.updateOption(\"filter_search_bar\", inputText);\n        } else {\n            delete this.controller.lines_searched;\n            this.controller.deleteOption(\"filter_search_bar\");\n        }\n    }\n}\n", "import { registry } from \"@web/core/registry\"\n\n\nexport async function AccountReturnRefreshHandler(env, action) {\n    const params = action.params || {};\n    env.bus.trigger(\"return_reload_model\", {resIds: params.return_ids});\n    return params.next_action;\n}\n\nregistry.category(\"actions\").add(\"action_return_refresh\", AccountReturnRefreshHandler)\n", "import { registry } from \"@web/core/registry\"\n\n\nexport async function AccountReturnCloseWizard(env, action) {\n    const params = action.params || {};\n    env.services.action.doAction({\n        type: 'ir.actions.act_window_close'\n    });\n    return params.next_action;\n}\n\nregistry.category(\"actions\").add(\"action_return_close_wizard\", AccountReturnCloseWizard)\n", "import { reactive, useState } from \"@odoo/owl\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { registry } from \"@web/core/registry\";\n\nclass AuditBalanceListChatterService {\n    constructor() {\n        this.chatterState = reactive({ res_id: undefined, date_to: undefined });\n    }\n\n    closeChatter() {\n        this.chatterState.res_id = undefined;\n    }\n\n    openChatter(resId) {\n        this.chatterState.res_id = resId;\n    }\n}\n\nconst auditBalanceListChatterService = {\n    start() {\n        return new AuditBalanceListChatterService();\n    },\n};\n\nregistry.category(\"services\").add(\"auditBalanceListChatterService\", auditBalanceListChatterService);\n\nexport function useAuditBalanceListChatterService() {\n    return useState(useService(\"auditBalanceListChatterService\"));\n}\n", "import { useService } from \"@web/core/utils/hooks\";\nimport { ListController } from \"@web/views/list/list_controller\";\nimport { useAuditBalanceListChatterService } from \"./account_audit_balance_list_chatter_service\";\nimport { onWillStart } from \"@odoo/owl\";\n\nexport class AccountAuditBalanceListController extends ListController {\n    static template = \"account_reports.account_audit_balance_list_controller\";\n\n    setup() {\n        super.setup();\n        this.orm = useService(\"orm\");\n        this.chatterService = useAuditBalanceListChatterService();\n\n        onWillStart(async () => {\n            const working_file_id = this.props.context?.working_file_id;\n            if (!working_file_id) {\n                return;\n            }\n\n            const working_file = await this.orm.searchRead(\n                \"account.return\",\n                [[\"id\", \"=\", working_file_id]],\n                [\"date_to\"],\n                { limit: 1 }\n            );\n            if (working_file.length === 1) {\n                this.chatterService.chatterState.date_to = working_file[0]?.date_to;\n            }\n        });\n    }\n\n    openJournalItems() {\n        this.actionService.doActionButton({\n            context: this.model.root.context,\n            resModel: this.model.root.resModel,\n            name: \"action_audit_account\",\n            type: \"object\",\n            resIds: this.model.root.selection.map((record) => record.data.code),\n        });\n    }\n}\n", "import { useService } from \"@web/core/utils/hooks\";\n\nimport { ListRenderer } from \"@web/views/list/list_renderer\";\nimport { AccountReportChatter } from \"@account_reports/components/mail/chatter\";\nimport { useAuditBalanceListChatterService } from \"./account_audit_balance_list_chatter_service\";\nimport { useEffect, useRef } from \"@odoo/owl\";\n\nexport class AccountAuditBalanceListRenderer extends ListRenderer {\n    static template = \"account_reports.account_audit_balance_list_renderer\";\n\n    static components = {\n        ...ListRenderer.components,\n        AccountReportChatter,\n    };\n\n    setup() {\n        super.setup();\n        this.ui = useService(\"ui\");\n        this.chatterService = useAuditBalanceListChatterService();\n\n        this.chatterRef = useRef(\"AuditChatter\");\n\n        useEffect(() => {\n            if (this.props.list.editedRecord) {\n                this.chatterService.openChatter(this.props.list.editedRecord.evalContext.id);\n            } else {\n                this.chatterService.closeChatter();\n            }\n        }, () => [this.props.list.editedRecord]);\n    }\n\n    onGlobalClick(event) {\n        if (this.chatterRef.el.contains(event.target)) {\n            // ignore clicks inside the chatter as we dont want it to close when clicking inside it\n            return;\n        }\n        super.onGlobalClick(event);\n    }\n\n    getCellClass(column, record) {\n        const classNames = super.getCellClass(column, record);\n        if (column.name === 'audit_balance' && record.data.audit_balance_show_warning\n            || column.name === 'audit_previous_balance' && record.data.audit_previous_balance_show_warning) {\n            return `${classNames} table-warning`;\n        }\n        return classNames;\n    }\n}\n", "import { registry } from \"@web/core/registry\";\nimport { listView } from \"@web/views/list/list_view\";\nimport { AccountAuditBalanceListRenderer } from \"./account_audit_balance_list_renderer\";\nimport { AccountAuditBalanceListController } from \"./account_audit_balance_list_controller\";\n\nexport const accountAuditBalanceList = {\n    ...listView,\n    Renderer: AccountAuditBalanceListRenderer,\n    Controller: AccountAuditBalanceListController,\n};\n\nregistry.category(\"views\").add(\"account_audit_balance_list\", accountAuditBalanceList);\n", "import { registry } from \"@web/core/registry\";\nimport { kanbanView } from \"@web/views/kanban/kanban_view\";\nimport { AccountReturnBaseKanbanRenderer } from \"./account_return_base_kanban_renderer\";\n\nexport const accountReturnAuditKanbanView = {\n    ...kanbanView,\n    Renderer: AccountReturnBaseKanbanRenderer,\n};\n\nregistry.category(\"views\").add(\"account_return_audit_kanban\", accountReturnAuditKanbanView);\n", "import { useEffect } from \"@odoo/owl\";\nimport { evaluateExpr } from \"@web/core/py_js/py\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { KanbanRenderer } from \"@web/views/kanban/kanban_renderer\";\n\nconst { DateTime } = luxon;\n\n\nexport class AccountReturnBaseKanbanRenderer extends KanbanRenderer\n{\n    setup() {\n        super.setup();\n        this.orm = useService(\"orm\");\n\n        // Retrieve the allowed categories from xml arch\n        const optionsArch = this.props.archInfo.xmlDoc.getAttribute('options');\n        this.allowedCategories = ['account_return']\n        if (optionsArch) {\n            const options = evaluateExpr(optionsArch);\n            this.allowedCategories = options.allowed_categories;\n        }\n\n        useEffect(() => {this.runAllReturnChecks()}, () => []);\n    }\n\n    async runAllReturnChecks() {\n        const additionalDomain = [\n            ['date_from', '<=', DateTime.now().endOf(\"month\").toISODate()],\n            ['return_type_category', 'in', this.allowedCategories],\n        ]\n\n        const returnIds = await this.orm.call(\n            'account.return',\n            'get_next_returns_ids',\n            [\n                null,\n                additionalDomain,\n                true, //allow_multiple_by_types\n            ],\n        );\n\n        await this.orm.call(\n            'account.return',\n            'refresh_checks',\n            [returnIds]\n        );\n\n        // reload records\n        await this.props.list.model.load();\n    }\n}\n", "import { useSubEnv, useEffect } from \"@odoo/owl\";\nimport { useSetupAction } from \"@web/search/action_hook\";\nimport { KanbanController } from \"@web/views/kanban/kanban_controller\";\n\nexport class AccountReturnCheckKanbanController extends KanbanController {\n    setup() {\n        super.setup();\n\n        useSubEnv({\n            reload: () => this.model.load(),\n        })\n\n        useSetupAction({\n            rootRef: this.rootRef,\n            getLocalState: () => {\n                const renderer = this.rootRef.el.querySelector(\".kanban_return_and_checks_cards\");\n                return {\n                    rendererScrollPositions: {\n                        top: renderer?.scrollTop || 0,\n                    },\n                };\n            },\n        });\n\n        let { rendererScrollPositions } = this.props.state || {};\n        useEffect(() => {\n            if (rendererScrollPositions) {\n                const renderer = this.rootRef.el.querySelector(\".kanban_return_and_checks_cards\");\n                if (renderer) {\n                    renderer.scrollTop = rendererScrollPositions.top;\n                    rendererScrollPositions = null;\n                }\n            }\n        });\n    }\n}\n", "import { KanbanRecord } from \"@web/views/kanban/kanban_record\";\n\nexport class AccountReturnCheckKanbanRecord extends KanbanRecord {\n    onGlobalClick(ev, newWindow) {\n        if (this.props.record.data.action)\n            super.onGlobalClick(ev, newWindow);\n    }\n\n    getRecordClasses() {\n        const { archInfo, forceGlobalClick } = this.props;\n        let classes = super.getRecordClasses();\n\n        const shouldHaveCursorPointer = (forceGlobalClick || archInfo.openAction || archInfo.canOpenRecords) && this.props.record.data.action;\n        classes = classes.replace(/\\bcursor-pointer\\b/, \"\");\n\n        if (shouldHaveCursorPointer) {\n            classes += \" cursor-pointer\";\n        }\n\n        return classes.trim();\n    }\n} \n", "import {\n    AccountReturnCheckKanbanRecord\n} from \"@account_reports/components/account_return/views/account_return_check_kanban_record\";\nimport {KanbanRenderer} from \"@web/views/kanban/kanban_renderer\";\nimport {onWillStart, onWillDestroy} from \"@odoo/owl\";\nimport {useService} from \"@web/core/utils/hooks\";\nimport {registry} from \"@web/core/registry\";\nimport {parseXML} from \"@web/core/utils/xml\";\nimport {extractFieldsFromArchInfo, getFieldsSpec} from \"@web/model/relational_model/utils\";\nimport {RelationalModel} from \"@web/model/relational_model/relational_model\";\nimport {isNull} from \"@web/views/utils\";\nimport {AccountReturnKanbanRecord} from \"./account_return_kanban_record\";\nimport {Chatter} from \"@mail/chatter/web_portal/chatter\";\n\n\nconst viewRegistry = registry.category(\"views\");\n\n\nexport class AccountReturnCheckKanbanRenderer extends KanbanRenderer {\n    static template = \"account_reports.account_return_check_kanban_renderer\";\n\n    static components = {\n        ...KanbanRenderer.components,\n        AccountReturnCheckKanbanRecord,\n        AccountReturnKanbanRecord,\n        Chatter,\n    };\n\n    setup() {\n        super.setup();\n        this.orm = useService(\"orm\");\n        this.action = useService(\"action\");\n        this.viewService = useService(\"view\");\n        const context = this.props.list.context;\n        this.destroyed = false\n        this.originalListLoad = this.props.list.model.load.bind(this.props.list.model);\n\n        onWillDestroy(async () => {\n            this.destroyed = true;\n        });\n\n        if (context.active_model === \"account.return\") {\n            this.currentReturnId = context.active_id\n\n            onWillStart(async () => {\n                const { fields, relatedModels, views } = await this.viewService.loadViews({\n                    resModel: \"account.return\",\n                    context: context,\n                    views: [[context.account_return_view_id, \"kanban\"]],\n                });\n                const { ArchParser } = viewRegistry.get(\"kanban\");\n                const xmlDoc = parseXML(views[\"kanban\"].arch);\n                this.returnArchInfo = new ArchParser().parse(xmlDoc, relatedModels, 'account.return');\n\n                const extractedFields = extractFieldsFromArchInfo(this.returnArchInfo, fields);\n\n                const accountReturnId = this.currentReturnId;\n                if (!accountReturnId) return;\n                this.specification = getFieldsSpec(extractedFields.activeFields, extractedFields.fields, context)\n\n                const returnData = await this.orm.webRead(\n                    'account.return',\n                    [accountReturnId],\n                    { specification: this.specification }\n                );\n\n                const modelParams = this.getModelParams(extractedFields.activeFields, extractedFields.fields);\n                const model = new RelationalModel(this.env, modelParams, {orm: this.orm});\n\n                this.returnRecord = new model.constructor.Record(\n                    model,\n                    {\n                        context: { ...context, in_checks_view: true },\n                        activeFields: extractedFields.activeFields,\n                        resModel: 'account.return',\n                        fields: extractedFields.fields,\n                        resId: accountReturnId,\n                        resIds: [accountReturnId],\n                        isMonoRecord: true,\n                        mode: 'readonly',\n                    },\n                    returnData[0],\n                    { manuallyAdded: !returnData.id }\n                )\n\n                this.props.list.model.load = async (params) => {\n                    // Reload return card\n                    const result = await this.originalListLoad(params);\n                    if (this.destroyed) return result;\n                    const returnData = await this.orm.webRead(\n                        'account.return',\n                        [accountReturnId],\n                        { specification: this.specification }\n                    );\n                    if (this.destroyed) return result;\n                    this.returnRecord._setData(returnData[0]);\n\n                    // Reload chatter messages\n                    this.env.bus.trigger(\"MAIL:RELOAD-THREAD\", {\n                        model: \"account.return\",\n                        id: accountReturnId,\n                    });\n\n                    return result;\n                };\n\n                // Update records checks\n                const records = this.props.list.records;\n                if (records.length > 0) {\n                    const checkResults = this.orm.call(\"account.return\", \"refresh_checks\", [this.currentReturnId])\n                    checkResults.then(async () => {\n                        if (!this.destroyed) {\n                            await this.props.list.model.load();\n                        }\n                    });\n                }\n            });\n        }\n    }\n\n    getModelParams(activeFields, fields) {\n        const modelConfig = {\n            resModel: 'account.return',\n            fields,\n            activeFields,\n            openGroupsByDefault: true,\n        };\n\n        return {\n            config: modelConfig,\n            groupsLimit: Number.MAX_SAFE_INTEGER,\n            limit: 1,\n            countLimit: 1,\n        };\n    }\n\n    get groups() {\n        const { list } = this.props;\n        if (!list.isGrouped) {\n            return false;\n        }\n        return list.groups.map((group, index) => ({\n            ...group,\n            key: isNull(group.value) ? `group_key_${index}` : String(group.value),\n        }));\n    }\n\n    async openRecord(record, params) {\n        const recordId = record.resId;\n        if (record.resModel === \"account.return.check\") {\n            const result = await this.orm.call(\n                record.resModel,\n                \"action_review\",\n                [recordId]\n            );\n\n            if (result) {\n                this.action.doAction(result);\n            }\n        }\n    }\n}\n", "import { registry } from \"@web/core/registry\";\n\nimport { kanbanView } from \"@web/views/kanban/kanban_view\";\nimport { AccountReturnCheckKanbanRenderer } from \"./account_return_check_kanban_renderer\";\nimport { AccountReturnCheckKanbanController } from \"./account_return_check_kanban_controller\";\n\nexport const accountReturnCheckKanbanView = {\n    ...kanbanView,\n    Renderer: AccountReturnCheckKanbanRenderer,\n    Controller: AccountReturnCheckKanbanController,\n};\n\nregistry.category(\"views\").add(\"account_return_check_kanban\", accountReturnCheckKanbanView);\n", "import { KanbanRecord } from \"@web/views/kanban/kanban_record\";\n\nexport class AccountReturnKanbanRecord extends KanbanRecord {\n    setup() {\n        super.setup();\n        if (this.props.record.data.activity_type_icon === 'fa-check') {\n            this.props.record.data.activity_type_icon = 'fa-tasks';\n        }\n    }\n\n    getRecordClasses() {\n        let classes = super.getRecordClasses();\n        // Remove the cursor on the card of the return when being on the checks kanban view\n        if (this.props.record.context?.in_checks_view) {\n            classes = classes.replace(/\\bcursor-pointer\\b/, \"\");\n        }\n        return classes;\n    }\n}\n", "import { useService, useBus } from \"@web/core/utils/hooks\";\nimport { isNull } from \"@web/views/utils\";\nimport { KanbanRenderer } from \"@web/views/kanban/kanban_renderer\";\nimport {AccountReturnKanbanRecord} from \"./account_return_kanban_record\";\nimport { AccountReturnBaseKanbanRenderer } from \"./account_return_base_kanban_renderer\";\n\n\nexport class AccountReturnKanbanRenderer extends AccountReturnBaseKanbanRenderer {\n    static template=\"account_reports.account_return_kanban_renderer\";\n\n    static props = [\n        ...KanbanRenderer.props,\n    ]\n\n    static components = {\n        ...KanbanRenderer.components,\n        AccountReturnKanbanRecord\n    };\n\n    setup() {\n        super.setup();\n        this.orm = useService(\"orm\");\n        this.actionService = useService(\"action\");\n\n        useBus(this.env.bus, \"return_reload_model\", (ev) => {\n            const recordIds = ev.detail.resIds;\n            let recordToReload = this.records.filter((record) => recordIds.includes(record.resId));\n            for (let record of recordToReload) {\n                record.model.load();\n            }\n        });\n    }\n\n    async openRecord(record, params) {\n        if (record.context?.in_checks_view) {\n            return\n        }\n        const action = await this.orm.call(\"account.return\", \"action_open_account_return\", [record.resIds]);\n        if (!action)\n            return\n        return this.actionService.doAction(action);\n    }\n\n    get records() {\n        const { list } = this.props;\n        if (list.isGrouped) {\n            return list.groups.flatMap((group) => group.list.records);\n        }\n        else {\n            return list.records;\n        }\n    }\n\n    get groups() {\n        const { list } = this.props;\n        if (!list.isGrouped) {\n            return false;\n        }\n\n        return list.groups.map((group, i) => ({\n            ...group,\n            key: isNull(group.value) ? `group_key_${i}` : String(group.value),\n        }));\n    }\n}\n", "import { registry } from \"@web/core/registry\";\n\nimport { kanbanView } from \"@web/views/kanban/kanban_view\";\nimport { AccountReturnKanbanRenderer } from \"./account_return_kanban_renderer\";\n\nexport const accountReturnKanbanView = {\n    ...kanbanView,\n    Renderer: AccountReturnKanbanRenderer\n};\n\nregistry.category(\"views\").add(\"account_return_kanban\", accountReturnKanbanView);\n", "import { ActivityMenu } from \"@mail/core/web/activity_menu\";\nimport { patch } from \"@web/core/utils/patch\";\n\n\npatch(ActivityMenu.prototype, {\n    availableViews(group) {\n        if (group.model !== \"account.return\") {\n            return super.availableViews(...arguments);\n        }\n        return [[false, \"kanban\"]];\n    },\n});\n", "import { Component } from \"@odoo/owl\";\nimport { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\nimport { registry } from \"@web/core/registry\";\n\nconst cogMenuRegistry = registry.category(\"cogMenu\");\n\n\nexport class RefreshAccountReturns extends Component {\n    static template = \"account_reports.RefreshAccountReturns\";\n    static components = { DropdownItem };\n    static props = {};\n\n    async refresh_all_account_returns() {\n        await this.env.services.orm.call(\"account.return\", \"action_refresh_all_returns\");\n        await this.env.model.load();\n    }\n}\n\nexport const refreshAccountReturns = {\n    Component: RefreshAccountReturns,\n    groupNumber: 5,\n    isDisplayed: ({ config }) => {\n        return config.actionType === \"ir.actions.act_window\" &&\n        [\"kanban\"].includes(config.viewType) &&\n        [\"account_return_kanban\"].includes(config.viewSubType);\n    },\n};\n\ncogMenuRegistry.add(\"refresh-account-returns-menu\", refreshAccountReturns, { sequence: 10 });\n", "import { registry } from \"@web/core/registry\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { CharField, charField } from \"@web/views/fields/char/char_field\";\nimport { exprToBoolean } from \"@web/core/utils/strings\";\n\nexport class AccountAuditClickableCharField extends CharField {\n    static template = \"account_reports.AccountAuditClickableCharField\";\n\n    static props = {\n        ...CharField.props,\n        onclick: { type: String },\n    };\n\n    setup() {\n        super.setup();\n        this.actionService = useService(\"action\");\n    }\n\n    async onClickField() {\n        const test = {\n            context: this.props.record.context,\n            resModel: this.props.record.resModel,\n            name: this.props.onclick,\n            type: \"object\",\n            resId: this.props.record.resId,\n        };\n        this.actionService.doActionButton(test);\n    }\n}\n\nexport const accountAuditClickableCharField = {\n    ...charField,\n    component: AccountAuditClickableCharField,\n    supportedOptions: [\n        {\n            label: _t(\"Dynamic Placeholder\"),\n            name: \"placeholder_field\",\n            type: \"field\",\n            availableTypes: [\"char\", \"text\"],\n            help: _t(\n                \"Displays the value of the selected field as a textual hint. If the selected field is empty, the static placeholder attribute is displayed instead.\"\n            ),\n        },\n        {\n            label: _t(\"OnClick Handler\"),\n            name: \"onclick\",\n            type: \"function\",\n            availableTypes: [\"char\", \"text\"],\n            help: _t(\n                \"Name of the function to call when the cell value is clicked.\"\n            ),\n        },\n    ],\n    extractProps: ({ attrs, options, placeholder }) => ({\n        isPassword: exprToBoolean(attrs.password),\n        dynamicPlaceholder: options.dynamic_placeholder || false,\n        dynamicPlaceholderModelReferenceField:\n            options.dynamic_placeholder_model_reference_field || \"\",\n        autocomplete: attrs.autocomplete,\n        placeholder,\n        onclick: options.onclick,\n    }),\n};\n\nregistry.category(\"fields\").add(\"accountAuditClickableCharField\", accountAuditClickableCharField);\n", "import { ProgressBarField, progressBarField } from \"@web/views/fields/progress_bar/progress_bar_field\"\nimport { registry } from \"@web/core/registry\";\nexport class AccountAuditProgressbar extends ProgressBarField {\n    static template = \"account_reports.AccountAuditProgresbar\";\n\n    get progressBarColorClass() {\n        if (this.maxValue == 0) {\n            return \"\";\n        }\n        return this.currentValue > this.maxValue ? this.props.overflowClass : \"bg-success\";\n    }\n\n    get maxValue() {\n        return this.props.record.data[this.maxValueField];\n    }\n\n    get hasMaxValue() {\n        return this.props.record.data.hasOwnProperty(this.maxValueField)\n    }\n}\n\nexport const AccountAuditProgressBarField = {\n    ...progressBarField,\n    component: AccountAuditProgressbar,\n};\n\nregistry.category(\"fields\").add(\"account_audit_progressbar\", AccountAuditProgressBarField);\n\n", "import { FileModel } from \"@web/core/file_viewer/file_model\";\n\nexport class CheckAttachment extends FileModel {\n    constructor(fileData) {\n        super();\n        this.data = fileData;\n        for (const property of [\n            \"access_token\",\n            \"checksum\",\n            \"extension\",\n            \"filename\",\n            \"id\",\n            \"mimetype\",\n            \"name\",\n            \"type\",\n            \"tmpUrl\",\n            \"url\",\n            \"uploading\",\n        ]) {\n            this[property] = this.data[property];\n        }\n    }\n}\n", "import { registry } from \"@web/core/registry\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { useFileViewer } from \"@web/core/file_viewer/file_viewer_hook\";\nimport { Component } from \"@odoo/owl\";\nimport { standardWidgetProps } from \"@web/views/widgets/standard_widget_props\";\nimport { CheckAttachment } from \"./account_return_check_attachment_model\";\nimport { downloadFile } from \"@web/core/network/download\";\n\nclass AccountReturnCheckAttachmentViewer extends Component {\n    static template = \"account_reports.AccountReturnCheckAttachmentViewer\";\n    static components = {\n\n    };\n    static props = {\n        ...standardWidgetProps,\n    };\n\n    setup() {\n        super.setup();\n        this.orm = useService(\"orm\");\n        this.fileViewer = useFileViewer();\n    }\n\n    async openAttachments() {\n        let result = await this.orm.read(\n            this.props.record.resModel,\n            [this.props.record.resId],\n            [\"attachment_ids\"]\n        );\n        if (result) {\n            const attachmentIds = result[0].attachment_ids;\n            const attachmentsData = await this.orm.read(\n                'ir.attachment',\n                attachmentIds,\n                [\n                    \"access_token\",\n                    \"checksum\",\n                    \"id\",\n                    \"name\",\n                    \"store_fname\",\n                    \"type\",\n                    \"url\",\n                    \"mimetype\",\n                ]\n            );\n            let attachments = [];\n            for (let attachmentData of attachmentsData) {\n                const splittedName = attachmentData.name.split('.');\n                const extension = splittedName[splittedName.length - 1];\n                attachments.push(new CheckAttachment(\n                    {\n                        ...attachmentData,\n                        filename: attachmentData.name,\n                        extension: extension,\n                    }\n                ))\n            }\n            const viewableFiles = attachments.filter((file) => file.isViewable);\n            const unviewableFiles = attachments.filter((file) => !file.isViewable);\n            for (let unviewableFile of unviewableFiles) {\n                downloadFile(unviewableFile.downloadUrl);\n            }\n            if (viewableFiles.length) {\n                this.fileViewer.open(viewableFiles[0], viewableFiles);\n            }\n        }\n    }\n}\n\nexport const accountReturnCheckAttachmentViewer = {\n    component: AccountReturnCheckAttachmentViewer,\n}\n\nregistry.category(\"view_widgets\").add(\"account_return_check_attachment_viewer\", accountReturnCheckAttachmentViewer)\n", "import { registry } from \"@web/core/registry\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { Component } from \"@odoo/owl\";\nimport { standardWidgetProps } from \"@web/views/widgets/standard_widget_props\";\nimport { FileUploader } from \"@web/views/fields/file_handler\";\n\nclass CustomAccountReturnFileUploaderComponent extends FileUploader {\n    static template = \"account_reports.CustomAccountReturnFileUploaderComponent\"\n}\n\nclass AccountReturnCheckFileUploader extends Component {\n    static template = \"account_reports.AccountReturnCheckFileUploader\";\n    static components = {\n        FileUploader: CustomAccountReturnFileUploaderComponent\n    };\n    static props = {\n        ...standardWidgetProps,\n    };\n\n    setup() {\n        super.setup();\n        this.orm = useService(\"orm\");\n        this.attachmentIds = [];\n    }\n\n    async onFileUploaded(file) {\n        const att_data = {\n            name: file.name,\n            mimetype: file.type,\n            datas: file.data,\n            res_model: this.props.record.resModel,\n            res_id: this.props.record.resId,\n        };\n        const [att_id] = await this.orm.create(\n            \"ir.attachment\",\n            [att_data],\n            { context: this.props.record.context}\n        );\n        this.attachmentIds.push(att_id);\n    }\n\n    async onUploadCompleted() {\n        await this.orm.write(this.props.record.resModel, [this.props.record.resId], { attachment_ids: this.attachmentIds });\n        this.attachmentIds = [];\n        this.props.record.model.load();\n    }\n\n    async onClick(event) {\n        console.log(\"uploader\")\n        console.log(event.target);\n        event.stopPropagation();\n    }\n}\n\nexport const accountReturnCheckFileUploader = {\n    component: AccountReturnCheckFileUploader,\n}\n\nregistry.category(\"view_widgets\").add(\"account_return_check_file_uploader\", accountReturnCheckFileUploader)\n", "import { registry } from \"@web/core/registry\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { formatDate, parseDate } from \"@web/core/l10n/dates\";\nimport { Component, useState, onWillStart } from \"@odoo/owl\";\nimport { standardWidgetProps } from \"@web/views/widgets/standard_widget_props\";\nimport { _t } from \"@web/core/l10n/translation\";\nconst { DateTime } = luxon;\n\n\nexport class AccountReturnDashboardList extends Component {\n    static template = \"account_reports.account_return_dashboard_list\";\n    static props = {\n        ...standardWidgetProps,\n    };\n\n    setup() {\n        this.orm = useService(\"orm\");\n        this.action = useService(\"action\");\n        this.accountReturns = useState([]);\n        onWillStart(this.fetchNextReturns);\n    }\n\n    formatReturn(accountReturn) {\n        const deadlineDate = parseDate(accountReturn.date_deadline);\n        const now = DateTime.now().startOf('day');\n        const daysDiff = deadlineDate.diff(now, 'days').days;\n\n        let deadlineDisplay;\n        let deadlineClass = '';\n\n        if (daysDiff < -1) {\n            deadlineDisplay = formatDate(deadlineDate);\n            deadlineClass = 'text-danger';\n        } else if (daysDiff === -1) {\n            deadlineDisplay = _t(\"Yesterday\");\n            deadlineClass = 'text-danger';\n        } else if (daysDiff === 0) {\n            deadlineDisplay = _t(\"Today\");\n            deadlineClass = 'text-warning';\n        } else if (daysDiff === 1) {\n            deadlineDisplay = _t(\"Tomorrow\");\n            deadlineClass = 'text-warning';\n        } else if (daysDiff <= 5) {\n            deadlineDisplay = _t(\"In %s days\", Math.round(daysDiff));\n            deadlineClass = 'text-warning';\n        } else {\n            deadlineDisplay = _t(\"In %s days\", Math.round(daysDiff));\n        }\n\n        return {\n            id: accountReturn.id,\n            name: accountReturn.name,\n            type_id: accountReturn.type_id,\n            deadline: formatDate(deadlineDate),\n            deadlineDisplay,\n            deadlineClass,\n        };\n    }\n\n    async fetchNextReturns() {\n        const returns = await this.orm.call(\n            'account.return',\n            'get_next_return_for_dashboard',\n            [this.props.record.resId], //allow_multiple_by_types\n        );\n\n        this.accountReturns = returns.map(this.formatReturn);\n    }\n\n    /**\n     * Opens the Tax Return view filtered by return type.\n     * @param {Object} accountReturn - The account return object.\n     */\n    async openTaxReturn(accountReturn) {\n        const returnTypeId = accountReturn?.type_id || '';\n        const [viewId, searchViewId] = await this.orm.call(\"account.return\", \"get_kanban_view_and_search_view_id\", [[accountReturn.id]]);\n\n        // Open the filtered Tax Return view\n        this.action.doAction({\n            name: _t(\"Tax Return\"),\n            type: 'ir.actions.act_window',\n            res_model: 'account.return',\n            views: [\n                [viewId || false, 'kanban'],\n                [false, 'calendar'],\n            ],\n            search_view_id: [searchViewId || false],\n            context: {\n                'search_default_groupby_deadline': 1,\n                'search_default_todo_returns': 1,\n                // Apply name filter using return type\n                'search_default_type_id': returnTypeId,\n            },\n            domain: [['return_type_category', '=', 'account_return']],\n        });\n    }\n}\n\n\nexport const accountReturnDashboardList = {\n    component: AccountReturnDashboardList,\n}\n\nregistry.category(\"view_widgets\").add(\"account_return_dashboard_list\", accountReturnDashboardList);\n", "import { registry } from \"@web/core/registry\";\nimport { Component } from \"@odoo/owl\";\nimport { standardFieldProps } from \"@web/views/fields/standard_field_props\";\n\nexport class AccountReturnNameBadge extends Component {\n    static template = \"account_reports.AccountReturnNameBadgeField\";\n    static props = {\n        ...standardFieldProps,\n        options: { type: Object, optional: true },\n    };\n\n    updateName(event) {\n        const newName = event.target.value;\n        this.props.record.update({ name: newName });\n        this.props.record.save();\n    }\n}\n\nexport const accountReturnNameBadge = {\n    supportedTypes: [\"char\"],\n    component: AccountReturnNameBadge,\n    extractProps: ({ options }) => ({ options }),\n};\n\nregistry.category(\"fields\").add(\"account_return_name_badge\", accountReturnNameBadge);\n", "import { registry } from \"@web/core/registry\";\nimport { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { Component, onWillStart } from \"@odoo/owl\";\nimport { standardFieldProps } from \"@web/views/fields/standard_field_props\";\nimport { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\nimport { cookie } from \"@web/core/browser/cookie\";\nimport { user } from \"@web/core/user\";\n\nexport class AccountReturnSelectionBadge extends Component {\n    static template = \"account_reports.AccountReturnSelectionBadgeField\";\n    static props = {\n        ...standardFieldProps,\n        decorations: { type: Object, optional: true },\n        options: { type: Object, optional: true },\n        class: { type: String, optional: true },\n        size: { type: String, optional: true },\n    };\n\n    setup() {\n        onWillStart(async () => {\n            this.editableOptions = await this.getEditableOptions();\n        });\n    }\n\n    static defaultProps = {\n        size: \"normal\"\n    };\n\n    static components = {\n        Dropdown,\n        DropdownItem,\n    }\n\n    get options() {\n        return this.props.record.fields[this.props.name].selection;\n    }\n\n    get value() {\n        return this.props.record.data[this.props.name];\n    }\n\n    get required() {\n        return this.props.record.fields[this.props.name].required;\n    }\n\n    get display() {\n        const result = this.options.filter((val) => val[0] == this.value)[0];\n        if(result) {\n            return result[1];\n        }\n        return null;\n    }\n\n    async getEditableOptions () {\n        const editableOptions = [false]\n\n        for (let [key, value] of Object.entries(this.props.options)) {\n            if ([true, undefined].includes(value.can_edit) || typeof value.can_edit == 'string' && await user.hasGroup(value.can_edit)) {\n                editableOptions.push(key);\n            }\n        }\n\n        return editableOptions;\n    }\n\n    decorationForValue(value, isDropdownItem=false) {\n        const colorScheme = cookie.get(\"color_scheme\");\n        const defaultStyle = isDropdownItem && colorScheme == 'dark' ? \"text-bg-200\" : \"text-bg-300\";\n        const decoration = this.props.options[value]?.decoration;\n        if (decoration) {\n            if (decoration === \"muted\") {\n                return defaultStyle;\n            }\n            return `text-bg-${this.props.options[value].decoration}`;\n        }\n        return isDropdownItem && colorScheme == 'dark' ? \"text-bg-200\" : \"text-bg-100\";\n    }\n\n    get additionalClassName() {\n        return this.props.class || \"\";\n    }\n\n    get capsuleStyle() {\n        if (this.props.size === 'normal') {\n            return \"min-width: 70px; height:21px;\";\n        }\n        else {\n            return \"\";\n        }\n    }\n\n    async onChange(value) {\n        await this.props.record.update(\n            { [this.props.name]: value },\n            { save: true }\n        );\n        this.env.reload?.()\n    }\n}\n\nexport const accountReturnSelectionBadge = {\n    supportedTypes: [\"selection\"],\n    component: AccountReturnSelectionBadge,\n    extractProps: ({options}) => {\n        return { options };\n    },\n}\n\nregistry.category(\"fields\").add(\"account_return_selection_badge\", accountReturnSelectionBadge)\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { WarningDialog } from \"@web/core/errors/error_dialogs\";\nimport { AccountReport } from \"@account_reports/components/account_report/account_report\";\nimport { AccountReportFilters } from \"@account_reports/components/account_report/filters/filters\";\n\nexport class AgedPartnerBalanceFilters extends AccountReportFilters {\n    static template = \"account_reports.AgedPartnerBalanceFilters\";\n\n    //------------------------------------------------------------------------------------------------------------------\n    // Aging Interval\n    //------------------------------------------------------------------------------------------------------------------\n    async setAgingInterval(ev) {\n        const agingInterval = parseInt(ev.target.value);\n        if (agingInterval < 1) {\n            this.dialog.add(WarningDialog, {\n                title: _t(\"Odoo Warning\"),\n                message: _t(\"Intervals cannot be smaller than 1\"),\n            });\n            return;\n        }\n\n        await this.filterClicked({ optionKey:\"aging_interval\", optionValue: agingInterval, reload: true });\n    }\n\n    get filterExtraOptionsData() {\n        return { \n            ...super.filterExtraOptionsData,\n            'show_currency': {\n                'name': _t(\"Show Currency\"),\n                'show': this.controller.cachedFilterOptions.multi_currency,\n            },\n            'show_account': {\n                'name': _t(\"Show Account\"),\n            },\n        };\n    }\n}\n\nAccountReport.registerCustomComponent(AgedPartnerBalanceFilters);\n", "import { _t } from \"@web/core/l10n/translation\";\n\nimport { AccountReport } from \"@account_reports/components/account_report/account_report\";\nimport { AccountReportFilters } from \"@account_reports/components/account_report/filters/filters\";\n\nexport class JournalReportFilters extends AccountReportFilters {\n    get filterExtraOptionsData() {\n        return {\n            ...super.filterExtraOptionsData,\n            'show_payment_lines': {\n                'name': _t(\"Include Payments\"),\n            },\n        };\n    }\n};\n\nAccountReport.registerCustomComponent(JournalReportFilters);\n", "import { AccountReport } from \"@account_reports/components/account_report/account_report\";\nimport { AccountReportLine } from \"@account_reports/components/account_report/line/line\";\n\nexport class JournalReportLine extends AccountReportLine {\n    static template = \"account_reports.JournalReportLine\";\n\n    // -----------------------------------------------------------------------------------------------------------------\n    // Classes\n    // -----------------------------------------------------------------------------------------------------------------\n    get lineClasses() {\n        let classes = super.lineClasses;\n\n        if (this.props.line.id.includes(\"|headers~~\"))\n            classes += ' accent_header';\n\n        if (this.props.line.move_id)\n            classes += ' accent_line';\n\n        return classes;\n    }\n\n    // -----------------------------------------------------------------------------------------------------------------\n    // Actions\n    // -----------------------------------------------------------------------------------------------------------------\n    async openTaxJournalItems(ev, name, taxType) {\n        return this.controller.reportAction(ev, \"journal_report_action_open_tax_journal_items\", {\n            name: name,\n            tax_type: taxType,\n            journal_id: this.props.line.journal_id,\n            journal_type: this.props.line.journal_type,\n            date_form: this.props.line.date_from,\n            date_to: this.props.line.date_to,\n        })\n    }\n}\n\nAccountReport.registerCustomComponent(JournalReportLine);\n", "import { registry } from \"@web/core/registry\";\nimport { AccountMoveLineListRenderer, AccountMoveLineListView } from \"@account_accountant/components/move_line_list/move_line_list\";\n\n\nexport class JournalReportAccountMoveLineReconcileListRenderer extends AccountMoveLineListRenderer {\n    setup() {\n        super.setup();\n        this.props.list.groups?.forEach(group => {\n            group.list?.groups?.forEach(innerGroup => {\n                this.toggleGroup(innerGroup);\n            });\n        });\n    }\n}\n\nexport const JournalReportAccountMoveLineReconcileLineListView = {\n    ...AccountMoveLineListView,\n    Renderer: JournalReportAccountMoveLineReconcileListRenderer,\n};\n\nregistry.category(\"views\").add(\"account_move_line_journal_report_list\", JournalReportAccountMoveLineReconcileLineListView);\n", "import { Chatter } from \"@mail/chatter/web_portal/chatter\";\nimport { AccountReportComposer } from \"./composer\";\nimport { AccountReportThread } from \"./thread\";\n\nexport class AccountReportChatter extends Chatter {\n    static template = \"account_reports.Chatter\";\n    static props = [...Chatter.props, \"reportController?\", \"date_to\", \"list?\"];\n    static components = {\n        ...Chatter.components,\n        Composer: AccountReportComposer,\n        Thread: AccountReportThread,\n    };\n}\n", "import { Composer } from \"@mail/core/common/composer\";\nimport { serializeDate } from \"@web/core/l10n/dates\";\nconst { DateTime } = luxon;\n\nexport class AccountReportComposer extends Composer {\n    static props = [...Composer.props, \"reportController?\", \"date_to\", \"list?\"];\n\n    get postData() {\n        return {\n            ...super.postData,\n            account_reports_annotation_date: serializeDate(DateTime.fromISO(this.props.date_to)),\n        };\n    }\n\n    async _sendMessage(value, postData, extraData) {\n        const message = await super._sendMessage(value, postData, extraData);\n        this.props.reportController?.addAnnotation(\n            message.id,\n            message.model,\n            message.res_id,\n            message.body\n        );\n        this.props.list?.records.forEach((record) => {\n            if (record.isInEdition) {\n                record.load();\n            }\n            return record;\n        });\n        return message;\n    }\n\n    get fullComposerAdditionalContext() {\n        return {\n            ...super.fullComposerAdditionalContext,\n            default_account_reports_annotation_date: serializeDate(DateTime.fromISO(this.props.date_to)),\n        };\n    }\n}\n", "import { Message } from \"@mail/core/common/message\";\n\nexport class AccountReportMessage extends Message {\n    static props = [...Message.props, \"reportController?\"];\n}\n", "import { messageActionsRegistry } from \"@mail/core/common/message_actions\";\n\nimport { AccountReportMessage } from \"./message\";\nimport { patch } from \"@web/core/utils/patch\";\n\nconst deleteAction = messageActionsRegistry.get(\"delete\");\n\npatch(deleteAction, {\n    onSelected({ owner }) {\n        const res = super.onSelected(...arguments);\n        if (!(owner instanceof AccountReportMessage)) {\n            return res;\n        }\n        res.then((value) => {\n            if (!value || !owner.props.reportController) {\n                return;\n            }\n            owner.props.reportController.removeAnnotation(owner.message.id);\n        });\n        return res;\n    },\n});\n", "import { Message } from \"@mail/core/common/message\";\nimport { patch } from \"@web/core/utils/patch\";\nimport { formatDate } from \"@web/core/l10n/dates\";\n\nconst { DateTime } = luxon;\n\npatch(Message.prototype, {\n    formatAccountReportsAnnotationDate(date) {\n        return formatDate(DateTime.fromISO(date));\n    },\n});\n", "import { Store } from \"@mail/core/common/store_service\";\n\nimport { patch } from \"@web/core/utils/patch\";\n\npatch(Store.prototype, {\n    async getMessagePostParams({ postData }) {\n        const params = await super.getMessagePostParams(...arguments);\n        if (postData.account_reports_annotation_date) {\n            params.post_data.account_reports_annotation_date = postData.account_reports_annotation_date;\n        }\n        return params;\n    },\n});\n", "import { Thread } from \"@mail/core/common/thread\";\nimport { AccountReportMessage } from \"./message\";\n\nexport class AccountReportThread extends Thread {\n    static template = \"account_reports.Thread\";\n    static props = [...Thread.props, \"reportController?\"];\n    static components = { ...Thread.components, Message: AccountReportMessage };\n}\n", "import { AccountReport } from \"@account_reports/components/account_report/account_report\";\nimport { AccountReportFilters } from \"@account_reports/components/account_report/filters/filters\";\nimport { parseFloat } from \"@web/views/fields/parsers\";\nimport { _t } from \"@web/core/l10n/translation\";\n\nexport class MulticurrencyRevaluationReportFilters extends AccountReportFilters {\n    static template = \"account_reports.MulticurrencyRevaluationReportFilters\";\n\n    //------------------------------------------------------------------------------------------------------------------\n    // Custom filters\n    //------------------------------------------------------------------------------------------------------------------\n    async filterExchangeRate(ev, currencyId) {\n        try {\n            this.controller.cachedFilterOptions.currency_rates[currencyId].rate = Math.abs(parseFloat(ev.currentTarget.value));\n        } catch {\n            this.notification.add(_t(\"Please enter a valid number.\"), {\n                type: \"danger\",\n            });\n        }\n    }\n}\n\nAccountReport.registerCustomComponent(MulticurrencyRevaluationReportFilters);\n", "import { AccountReport } from \"@account_reports/components/account_report/account_report\";\nimport { AccountReportLineCell } from \"@account_reports/components/account_report/line_cell/line_cell\";\nconst { DateTime } = luxon;\n\n\nexport class PartnerLedgerLineCell extends AccountReportLineCell {\n    get cellClasses() {\n        let superCellClasses = super.cellClasses;\n        const cell = this.props.cell;\n        if (\n            cell.figure_type === 'date'\n            && cell.expression_label == 'date_maturity'\n            && cell.no_format\n            && DateTime.fromISO(cell.no_format).startOf('day') < DateTime.now().startOf('day')\n        ) {\n            superCellClasses += ' text-danger';\n        }\n        return superCellClasses;\n    }\n}\n\nAccountReport.registerCustomComponent(PartnerLedgerLineCell);\n", "import { AccountReport } from \"@account_reports/components/account_report/account_report\";\nimport { AccountReportLine } from \"@account_reports/components/account_report/line/line\";\nimport { PartnerLedgerFollowupLineCell } from \"@account_reports/components/partner_ledger_followup/line_cell/line_cell\";\n\nexport class PartnerLedgerFollowupLine extends AccountReportLine {\n    static template = \"account_reports.PartnerLedgerFollowupLine\";\n    static components = {\n        ...AccountReportLine.components,\n        PartnerLedgerFollowupLineCell,\n    };\n}\nAccountReport.registerCustomComponent(PartnerLedgerFollowupLine);\n", "import { AccountReportLineCell } from \"@account_reports/components/account_report/line_cell/line_cell\";\n\nexport class PartnerLedgerFollowupLineCell extends AccountReportLineCell {\n    static template = \"account_reports.PartnerLedgerFollowupLineCell\";\n\n    async toggleNoFollowup(ev) {\n        const res = await this.orm.call(\n            \"account.partner.ledger.report.handler\",\n            \"action_toggle_no_followup\",\n            [this.props.line.id, this.controller.lines.map((line) => line.id)]\n        );\n        this.controller.updateLines(res.updated_line_ids, \"no_followup\", res.updated_value);\n    }\n}\n", "import { _t } from \"@web/core/l10n/translation\";\n\nimport { AccountReport } from \"@account_reports/components/account_report/account_report\";\nimport { AccountReportFilters } from \"@account_reports/components/account_report/filters/filters\";\n\nexport class SalesReportFilters extends AccountReportFilters {\n    static template = \"account_reports.SalesReportFilters\";\n\n    //------------------------------------------------------------------------------------------------------------------\n    // Getters\n    //------------------------------------------------------------------------------------------------------------------\n    get selectedEcTaxName() {\n        const selected = this.controller.cachedFilterOptions.ec_tax_filter_selection.filter(\n            (ecTax) => ecTax.selected,\n        );\n\n        switch (selected.length) {\n            case this.controller.cachedFilterOptions.ec_tax_filter_selection.length:\n                return _t(\"All\");\n            case 0:\n                return _t(\"None\");\n            default:\n                return selected.map((s) => s.name.substring(0, 1)).join(\", \");\n        }\n    }\n}\n\nAccountReport.registerCustomComponent(SalesReportFilters);\n", "import { registry } from \"@web/core/registry\";\nimport { download } from \"@web/core/network/download\";\n\nasync function executeAccountReportDownload({ env, action }) {\n    env.services.ui.block();\n\n    const url = \"/account_reports\";\n    const data = action.data;\n\n    try {\n        await download({ url, data });\n        if (!data.no_closing_after_download ?? true)\n            if (data.next_action) {\n                env.services.action.doAction(data.next_action);\n            } else {\n                env.services.action.doAction({type: 'ir.actions.act_window_close'});\n            }\n    } catch (e) {\n        if (e.exceptionName === 'AccountReportFileDownloadException') {\n            const reportOptions = JSON.parse(data.options);\n            const reportAction = await env.services.orm.call(\n                'account.report',\n                'open_account_report_file_download_error_wizard',\n                [reportOptions.report_id, e.data.arguments[0], e.data.arguments[1]],\n            );\n            env.services.action.doAction(reportAction);\n        } else {\n            throw e;\n        }\n    } finally {\n        env.services.ui.unblock();\n    }\n}\n\nregistry\n    .category(\"action_handlers\")\n    .add('ir_actions_account_report_download', executeAccountReportDownload);\n", "const LINE_ID_HIERARCHY_DELIMITER = \"|\";\nconst LINE_ID_GROUP_DELIMITER = \"~\";\n\nexport function buildLineId(current) {\n    const convertNull = (value) => {\n        if (value === null || value === undefined || value === false) {\n            return \"\";\n        } else if (typeof value === \"object\") {\n            // We're using the replaceAll to follow the python\n            // behavior where we're stringify a dict.\n            return JSON.stringify(value).replaceAll('\":', '\": ');\n        }\n        return value;\n    };\n\n    return current\n        .map(([markup, model, value]) => {\n            const lineValues = [convertNull(markup), convertNull(model), convertNull(value)];\n            return lineValues.join(LINE_ID_GROUP_DELIMITER);\n        })\n        .join(LINE_ID_HIERARCHY_DELIMITER);\n}\n\nexport function parseLineId(lineID, markupAsString = false) {\n    const parseMarkup = (markup) => {\n        if (!markup) {\n            return markup;\n        }\n        try {\n            const result = JSON.parse(markup);\n            return typeof result === \"object\" ? result : markup;\n        } catch {\n            return markup;\n        }\n    };\n\n    if (!lineID) {\n        return [];\n    }\n    return lineID.split(LINE_ID_HIERARCHY_DELIMITER).map((key) => {\n        const [markup, model, value] = key.split(LINE_ID_GROUP_DELIMITER);\n        return [\n            (markupAsString ? markup : parseMarkup(markup)) || null,\n            model || null,\n            model && value ? parseInt(value) : value || null,\n        ];\n    });\n}\n", "import { reactive, useEnv, useRef } from \"@odoo/owl\";\nimport { useDateTimePicker } from \"@web/core/datetime/datetime_picker_hook\";\nimport { serializeDate } from \"@web/core/l10n/dates\";\nimport { ListController } from \"@web/views/list/list_controller\";\n\nconst { DateTime } = luxon;\n\n\nexport class AccrualListController extends ListController {\n    setup() {\n        super.setup();\n        this.accrualContext = useEnv().accrualContext;\n        this.state = reactive({\n            date: DateTime.now(),\n        });\n        this.dateAsString = serializeDate(this.state.date);\n        if (this.model.config.resModel === \"purchase.order.line\") {\n            this.model.config.fields.qty_received_at_date.aggregator = \"sum\";\n        } else {\n            this.model.config.fields.qty_delivered_at_date.aggregator = \"sum\";\n        }\n            this.model.config.fields.qty_invoiced_at_date.aggregator = \"sum\";\n        this.model.config.fields.amount_to_invoice_at_date.aggregator = \"sum\";\n        this.dateFilterRef = useRef(\"filterDate\");\n        const getPickerProps = () => {\n            const pickerProps = {\n                value: this.state.date,\n                type: \"date\",\n            };\n            return pickerProps;\n        };\n        this.dateTimePicker = useDateTimePicker({\n            target: \"filterDate\",\n            get pickerProps() {\n                return getPickerProps();\n            },\n            onApply: (newDate) => {\n                if (newDate) {\n                    this.setDate(newDate);\n                    this.render();\n                }\n            },\n        });\n    }\n\n    async openRecord(record) {\n        // Instead of opening the record itself, open the parent order.\n        const res_model = record.model.config.fields.order_id.relation;\n        this.actionService.doAction(\n            {\n                name: record.data.order_id.display_name,\n                type: \"ir.actions.act_window\",\n                res_model,\n                res_id: record.data.order_id.id,\n                views: [[false, 'form']],\n                target: \"current\",\n            },\n        );\n    }\n\n    async setDate(date) {\n        this.dateAsString = serializeDate(date);\n        this.model.config.context.accrual_entry_date = this.dateAsString;\n        this.accrualContext.accrual_entry_date = this.dateAsString;\n        this.state.date = date;\n\n        if (this.model.config.groups) {\n            // If records are grouped, update context of grouped lines too.\n            for (const group of Object.values(this.model.config.groups)) {\n                group.context.accrual_entry_date = this.dateAsString;\n            }\n        }\n\n        await this.model.root.load();\n        this.model.notify();\n    }\n\n    get date() {\n        return this.state.date.toLocaleString();\n    }\n\n    onDateClick() {\n        this.dateTimePicker.open();\n    }\n\n    async beforeExecuteActionButton(clickParams) {\n        if (this.dateAsString) {\n            // If a date was selected, use it as the default date for the wizard.\n            clickParams.buttonContext.default_date = this.dateAsString;\n        }\n        return super.beforeExecuteActionButton(...arguments);\n    }\n}\n", "import { useSubEnv } from \"@odoo/owl\";\nimport { SearchModel } from \"@web/search/search_model\";\n\nexport class AccrualListSearchModel extends SearchModel {\n    setup(services) {\n        super.setup(services);\n        this.accrualContext = {};\n        useSubEnv({ accrualContext: this.accrualContext });\n    }\n\n    _getContext() {\n        const context = super._getContext();\n        Object.assign(context, this.accrualContext);\n        return context;\n    }\n}\n", "import { registry } from \"@web/core/registry\";\nimport { AccrualListController } from \"./accrual_list_controller\";\nimport { AccrualListSearchModel } from \"./accrual_list_search_model\";\nimport { listView } from \"@web/views/list/list_view\";\n\n\nexport const accrualListView = {\n    ...listView,\n    buttonTemplate: \"account_reports.AccrualListView.Buttons\",\n    Controller: AccrualListController,\n    SearchModel: AccrualListSearchModel,\n};\n\nregistry.category(\"views\").add(\"accrual_list_view\", accrualListView);\n", "import { useRef, onWillRender } from \"@odoo/owl\";\nimport { ConfirmationDialog } from \"@web/core/confirmation_dialog/confirmation_dialog\";\nimport { WarningDialog } from \"@web/core/errors/error_dialogs\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { useNestedSortable } from \"@web/core/utils/nested_sortable\";\nimport { registry } from \"@web/core/registry\";\nimport { ListRenderer } from \"@web/views/list/list_renderer\";\nimport { X2ManyField, x2ManyField } from \"@web/views/fields/x2many/x2many_field\";\nimport { useOpenX2ManyRecord, useX2ManyCrud } from \"@web/views/fields/relational_utils\";\n\nexport class AccountReportListRenderer extends ListRenderer {\n    static template = \"account_reports.AccountReportList\";\n\n    setup() {\n        this.dialog = useService(\"dialog\");\n        this.orm = useService(\"orm\");\n\n        // From ListRenderer\n        // We can't really use `super.setup()` because it expects to be used on a html table.\n        this.allColumns = this.processAllColumn(this.props.archInfo.columns, this.props.list);\n        this.keyOptionalFields = `optional_fields,${this.createViewKey()}`;\n        this.optionalActiveFields = this.computeOptionalActiveFields();\n        this.columns = this.getActiveColumns();\n\n        this.props.list._config.orderBy = [\n            {\n                \"name\": \"sequence\",\n                \"asc\": true\n            },\n            {\n                \"name\": \"id\",\n                \"asc\": true\n            }\n        ]\n\n        useNestedSortable({\n            ref: useRef(\"root\"),\n            elements: \"li.draggable\",\n            nest: true,\n            nestInterval: 10,\n            onDragStart: (ctx) => this.onDragStart(ctx),\n            onDragEnd: () => this.onDragEnd(),\n            onDrop: (ctx) => this.onDrop(ctx),\n        });\n\n        onWillRender(() => {\n            this.allColumns = this.processAllColumn(this.props.archInfo.columns, this.props.list);\n            this.columns = this.getActiveColumns();\n        });\n    }\n\n    //------------------------------------------------------------------------------------------------------------------\n    // Records\n    //------------------------------------------------------------------------------------------------------------------\n    recordsDataDeepCopy(records) {\n        const fields = this.allColumns.map((column) => column.name);\n\n        return records.map((record) => {\n            let recordData = {};\n\n            for (const field of fields)\n                recordData[field] = record.data[field];\n\n            return recordData;\n        });\n    }\n\n    //------------------------------------------------------------------------------------------------------------------\n    // Format\n    //------------------------------------------------------------------------------------------------------------------\n    formatData() {\n        let idToIndexMap = {};\n        let tree = [];\n\n        let lines = this.recordsDataDeepCopy(this.props.list.records);\n\n        for (const [index, line] of lines.entries()) {\n            line.index = index;\n            line.children = [];\n            line.descendants_count = 0;\n\n            idToIndexMap[line.id] = index;\n\n            if (line.parent_id?.id) {\n                let parentLine = lines[idToIndexMap[line.parent_id.id]];\n\n                if (parentLine) {\n                    parentLine?.children.push(line);\n\n                    while (parentLine) {\n                        parentLine.descendants_count += 1;\n                        parentLine = lines[idToIndexMap[parentLine.parent_id?.id]];\n                    }\n                } else {\n                    // Since the parentLine doesn't exist yet. It means that this line is out of sequence.\n                    line.out_of_sequence_error = true;\n\n                    tree.push(line);\n                }\n            } else {\n                tree.push(line);\n            }\n        }\n\n        return tree;\n    }\n\n    //------------------------------------------------------------------------------------------------------------------\n    // Placeholder\n    //------------------------------------------------------------------------------------------------------------------\n    onDragStart(ctx) {\n        function sanitize(element) {\n            if (element.nodeName === 'LI') {\n                [\"data-record_index\", \"data-record_id\", \"data-descendants_count\"].forEach((attribute) => {\n                    element.removeAttribute(attribute);\n                });\n\n                element.classList.remove(\"draggable\");\n            }\n\n            Array.from(element.childNodes).forEach((child) => {\n                sanitize(child);\n            });\n        }\n\n        const placeholder = ctx.element.cloneNode(true);\n\n        placeholder.removeAttribute(\"style\");\n        placeholder.classList.replace(\"o_dragged\", \"o_dragged_placeholder\");\n\n        sanitize(placeholder);\n\n        document.querySelector(\".o_nested_sortable_placeholder\").after(placeholder);\n    }\n\n    onDragEnd() {\n        // Clear placeholder\n        document.querySelector(\".o_dragged_placeholder\").remove();\n    }\n\n    //------------------------------------------------------------------------------------------------------------------\n    // Nested sorting\n    //------------------------------------------------------------------------------------------------------------------\n    async setRecordParent(currentElement, parentElement) {\n        const currentRecordIndex = currentElement.dataset.record_index;\n        const parentRecordIndex = parentElement?.dataset.record_index;\n\n        // Default root element\n        let parent = false;\n\n        // parentRecordIndex is a string. It should be true with '0'.\n        if (parentRecordIndex) {\n            parent = {\n                id: this.props.list.records[parentRecordIndex].data.id,\n                display_name: this.props.list.records[parentRecordIndex].data.name,\n            };\n        }\n\n        await this.props.list.records[currentRecordIndex].update({'parent_id': parent});\n    }\n\n    async setRecordHierarchy(currentElement, parentElement) {\n        const currentRecordIndex = parseInt(currentElement.dataset.record_index);\n        const parentRecordIndex = parentElement?.dataset.record_index;\n        const parentRecord = (parentRecordIndex) ? this.props.list.records[parentRecordIndex].data : null;\n\n        const hierarchyLevels = {};\n\n        if (parentRecord)\n            hierarchyLevels[parentRecord.id] = parentRecord.hierarchy_level;\n\n        const ancestors = new Set();\n\n        for (let index = currentRecordIndex; index < this.props.list.records.length; index++) {\n            const record = this.props.list.records[index];\n            const parentId = (record.data.parent_id) ? record.data.parent_id.id : false;\n\n            if (ancestors.size && !ancestors.has(parentId))\n                break;\n\n            let parentHierarchyLevel = (record.data.parent_id) ? hierarchyLevels[record.data.parent_id.id] : null;\n\n            if (parentHierarchyLevel != null) {\n                parentHierarchyLevel = (parentHierarchyLevel === 0) ? 1 : parentHierarchyLevel;\n                await record.update({'hierarchy_level': parentHierarchyLevel + 2});\n            } else {\n                await record.update({'hierarchy_level': 1});\n            }\n\n            ancestors.add(record.data.id);\n            hierarchyLevels[record.data.id] = record.data.hierarchy_level;\n        }\n    }\n\n    async setRecordSequence(currentElement, parentElement, previousElement, previousElementDescendantCount, nextElement) {\n        const currentRecordIndex = currentElement.dataset.record_index;\n        const currentRecordDescendantsCount = parseInt(currentElement.dataset.descendants_count);\n        const lastDescendantIndex = parseInt(currentRecordIndex) + currentRecordDescendantsCount\n        const recordsToMove = this.props.list.records.slice(currentRecordIndex, lastDescendantIndex + 1);\n\n        // We remove the element(s) we are moving\n        this.props.list.records.splice(currentRecordIndex, recordsToMove.length);\n\n        const previousRecordIndex = previousElement?.dataset.record_index;\n        const nextRecordIndex = nextElement?.dataset.record_index;\n\n        let newCurrentRecordIndex;\n\n        if (previousRecordIndex) {\n            newCurrentRecordIndex = parseInt(previousRecordIndex) + 1 + parseInt(previousElementDescendantCount);\n        } else if (nextRecordIndex) {\n            // We add the element in the first position\n            newCurrentRecordIndex = parseInt(nextRecordIndex);\n        } else {\n            // We add the element as a first child\n            newCurrentRecordIndex = parseInt(parentElement?.dataset.record_index) + 1;\n        }\n\n        // If the original position of the line we want to move is before the position we want to drop it, then we need\n        // to adjust the index because all the indexes of the lines after it have changed (we removed lines).\n        if (currentRecordIndex < newCurrentRecordIndex) {\n            newCurrentRecordIndex -= (1 + currentRecordDescendantsCount);\n        }\n\n        // We add the element(s) we are moving into the new position\n        this.props.list.records.splice(newCurrentRecordIndex, 0, ...recordsToMove);\n\n        for (const [index, record] of this.props.list.records.entries())\n            await record.update({'sequence': index + 1});\n    }\n\n    async onDrop(ctx) {\n        const parentRecordIndex = ctx.parent?.dataset.record_index;\n\n        // We can't drop a line if it's parent has a 'user_groupby`\n        if (this.props.list.records[parentRecordIndex]?.data.user_groupby)\n            return this.dialog.add(WarningDialog, {\n                message: _t(\"A line with a 'Group By' value cannot have children.\"),\n            });\n\n        // We need to save it before as it's value might change during calculations below\n        const previousElementDescendantCount = ctx.previous?.dataset.descendants_count\n\n        await this.setRecordParent(ctx.element, ctx.parent);\n        await this.setRecordHierarchy(ctx.element, ctx.parent);\n        await this.setRecordSequence(ctx.element, ctx.parent, ctx.previous, previousElementDescendantCount, ctx.next);\n    }\n\n    //------------------------------------------------------------------------------------------------------------------\n    // Delete\n    //------------------------------------------------------------------------------------------------------------------\n    onDeleteRecord(recordIndex) {\n        const currentRecordId = this.props.list.records[recordIndex].data.id\n        const nextRecordParentId = this.props.list.records[recordIndex + 1]?.data.parent_id.id\n\n        // We check if the next line is a children of the current one\n        if (nextRecordParentId === currentRecordId) {\n            return new Promise((resolve) => {\n                this.dialog.add(ConfirmationDialog, {\n                    body: _t(\"This line and all its children will be deleted. Are you sure you want to proceed?\"),\n                    confirmLabel: \"Delete\",\n                    confirm: () => {\n                        this.deleteRecord(recordIndex);\n                        resolve();\n                    },\n                    cancel: resolve,\n                });\n            });\n        }\n        this.deleteRecord(recordIndex);\n    }\n\n    deleteRecord(recordIndex) {\n        const recordToDelete = this.props.list.records[recordIndex];\n\n        const recordsToDelete = [recordToDelete];\n        const ancestors = new Set([recordToDelete.data.id]);\n\n        // We get all the children of the line we are deleting\n        for (let index = recordIndex + 1; index < this.props.list.records.length; index++) {\n            const record = this.props.list.records[index];\n\n            if (!ancestors.has(record.data.parent_id.id))\n                break;\n\n            recordsToDelete.push(record);\n            ancestors.add(record.data.id);\n        }\n\n        for (const record of recordsToDelete)\n            this.props.list.delete(record);\n    }\n}\n\nexport class AccountReportsLinesListX2ManyField extends X2ManyField {\n    static components = {\n        ...X2ManyField.components,\n        ListRenderer: AccountReportListRenderer,\n    }\n\n    // Overrides the \"openRecord\" method to overload the save. This will force the record to be saved in the database.\n    setup() {\n        super.setup();\n\n        const { saveRecord, updateRecord } = useX2ManyCrud(\n            () => this.list,\n            this.isMany2Many\n        );\n\n        const openRecord = useOpenX2ManyRecord({\n            resModel: this.list.resModel,\n            activeField: this.activeField,\n            activeActions: this.activeActions,\n            getList: () => this.list,\n            saveRecord: async (record) => {\n                for (const [index, record] of this.props.record.data.line_ids.records.entries())\n                    record.update({'sequence': index + 1});\n\n                record.update({\n                    'sequence': this.props.record.data.line_ids.records.length,\n                });\n\n                await saveRecord(record);\n                await this.props.record.save();\n            },\n            updateRecord: updateRecord,\n            isMany2Many: this.isMany2Many,\n        });\n\n        this._openRecord = (params) => {\n            const activeElement = document.activeElement;\n\n            openRecord({\n                ...params,\n                onClose: () => {\n                    if (activeElement) {\n                        activeElement.focus();\n                    }\n                },\n            });\n        };\n    }\n}\n\nregistry.category(\"fields\").add(\"account_report_lines_list_x2many\", {\n    ...x2ManyField,\n    component: AccountReportsLinesListX2ManyField,\n});\n", "import { registry } from \"@web/core/registry\";\nimport {\n    KanbanMany2ManyAvatarUserTagsList,\n    KanbanMany2ManyTagsAvatarUserField,\n    kanbanMany2ManyTagsAvatarUserField,\n} from \"@mail/views/web/fields/many2many_avatar_user_field/many2many_avatar_user_field\";\n\n\nexport class Many2ManyAvatarUserApproverTagsList extends KanbanMany2ManyAvatarUserTagsList {\n    static template = \"account_reports.KanbanMany2ManyAvatarUserTagsList\";\n}\n\n\nexport class Many2ManyAvatarUserApprover extends KanbanMany2ManyTagsAvatarUserField {\n    static components = {\n        ...KanbanMany2ManyTagsAvatarUserField.components,\n        TagsList: Many2ManyAvatarUserApproverTagsList,\n    }\n}\n\nexport const many2ManyAvatarUserApprover = {\n    ...kanbanMany2ManyTagsAvatarUserField,\n    component: Many2ManyAvatarUserApprover,\n};\n\nregistry.category(\"fields\").add(\"kanban.many2many_avatar_user_approver\", many2ManyAvatarUserApprover);\n", "import { registry } from \"@web/core/registry\";\nimport { usePopover } from \"@web/core/popover/popover_hook\";\nimport { localization } from \"@web/core/l10n/localization\";\nimport { Component } from \"@odoo/owl\";\nimport { standardFieldProps } from \"@web/views/fields/standard_field_props\";\n\nclass FollowupTrustPopOver extends Component {\n    static template = \"account_followup.FollowupTrustPopOver\";\n    static props = {\n        record: Object,\n        widget: Object,\n        close: { optional: true, type: Function },\n    };\n}\n\nclass FollowupTrustWidget extends Component {\n    static template = \"account_followup.FollowupTrustWidget\";\n    static props = {...standardFieldProps};\n    setup() {\n        super.setup();\n        const position = localization.direction === \"rtl\" ? \"bottom\" : \"right\";\n        this.popover = usePopover(FollowupTrustPopOver, { position });\n    }\n\n    displayTrust() {\n        var selections = this.props.record.fields.trust.selection;\n        var trust = this.props.record.data[this.props.name];\n        for (var i=0; i < selections.length; i++) {\n            if (selections[i][0] == trust) {\n                return selections[i][1];\n            }\n        }\n    }\n\n    onTrustClick(ev) {\n        this.popover.open(ev.currentTarget, {\n            record: this.props.record,\n            widget: this,\n        });\n    }\n\n    async setTrust(trust) {\n        this.props.record.update({ [this.props.name]: trust });\n        this.popover.close();\n    }\n}\n\nregistry.category(\"fields\").add(\"followup_trust_widget\", {\n    component: FollowupTrustWidget,\n});\n", "import { browser } from \"@web/core/browser/browser\";\nimport { registry } from \"@web/core/registry\";\n\nexport async function downloadAndCloseFollowupWizardAction(env, action) {\n    const url = action.params.url\n    try {\n        browser.open(url);\n    }\n    finally {\n        return { type: \"ir.actions.act_window_close\" };\n    }\n}\n\nregistry.category(\"actions\").add(\"close_followup_wizard\", downloadAndCloseFollowupWizardAction);\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { FormViewDialog } from \"@web/views/view_dialogs/form_view_dialog\";\n\nimport { AccountMoveFormView, AccountMoveFormRenderer } from '@account/components/account_move_form/account_move_form';\nimport { ExtractMixinFormRenderer } from '@iap_extract/components/manual_correction/form_renderer';\n\nexport class InvoiceExtractFormRenderer extends ExtractMixinFormRenderer(AccountMoveFormRenderer) {\n    setup() {\n        super.setup();\n        this.recordModel = 'account.move';\n    }\n\n    /**\n     * @override ExtractMixinFormRenderer\n     */\n    shouldRenderBoxes() {\n        return (\n            super.shouldRenderBoxes() &&\n            this.props.record.data.state === 'draft' &&\n            ['in_invoice', 'in_refund', 'in_receipt', 'out_invoice', 'out_refund', 'out_receipt'].includes(this.props.record.data.move_type)\n        )\n    }\n\n    async openCreatePartnerDialog(text) {\n        const ctxFromDb = await this.orm.call('account.move', 'get_partner_create_data', [[this.props.record.resId], text]);\n        this.dialog.add(\n            FormViewDialog,\n            {\n                resModel: 'res.partner',\n                context: ctxFromDb,\n                title: _t(\"Create\"),\n                onRecordSaved: (record) => {\n                    this.props.record.update({ partner_id: { id: record.resId } });\n                },\n            }\n        );\n    }\n\n    /**\n     * @override ExtractMixinFormRenderer\n     */\n    async handleFieldChanged(field, newFieldValue) {\n        if (field.name === 'partner_id') {\n            const partnerId = await this.orm.call(\n                this.recordModel,\n                'get_partner_from_text',\n                [this.props.record.resId, newFieldValue],\n            );\n            if (!partnerId) {\n                await this.openCreatePartnerDialog(newFieldValue);\n            }\n            this.activeFieldEl.querySelector('.o-autocomplete--dropdown-menu')?.classList.remove('show');\n            if (partnerId) {\n                return { id: partnerId };\n            }\n            return;\n        }\n        return super.handleFieldChanged(...arguments);\n    }\n};\n\nconst AccountMoveFormViewExtract = {\n    ...AccountMoveFormView,\n    Renderer: InvoiceExtractFormRenderer,\n};\n\nregistry.category(\"views\").add(\"account_move_form\", AccountMoveFormViewExtract, { force: true });\n", "import { formView } from \"@web/views/form/form_view\";\nimport { FormController } from \"@web/views/form/form_controller\";\nimport { registry } from \"@web/core/registry\";\nimport { useCheckDuplicateService } from \"./account_duplicate_transaction_hook\";\n\nexport class AccountDuplicateTransactionsFormController extends FormController {\n    setup() {\n        super.setup();\n        this.duplicateCheckService = useCheckDuplicateService();\n    }\n\n    async beforeExecuteActionButton(clickParams) {\n        if (clickParams.name === \"delete_selected_transactions\") {\n            const selected = this.duplicateCheckService.selectedLines;\n            if (selected.size) {\n                await this.orm.call(\n                    \"account.bank.statement.line\",\n                    \"unlink\",\n                    [Array.from(selected)],\n                );\n                this.env.services.action.doAction({type: 'ir.actions.client', tag: 'reload'});\n            }\n            return false;\n        }\n        return super.beforeExecuteActionButton(...arguments);\n    }\n\n    get cogMenuProps() {\n        const props = super.cogMenuProps;\n        props.items.action = [];\n        return props;\n    }\n}\n\nexport const form = { ...formView, Controller: AccountDuplicateTransactionsFormController };\n\nregistry.category(\"views\").add(\"account_duplicate_transactions_form\", form);\n", "import { useService } from \"@web/core/utils/hooks\";\n\nexport function useCheckDuplicateService() {\n    return useService(\"account_online_synchronization.duplicate_check_service\");\n}\n", "import { registry } from \"@web/core/registry\";\n\nclass AccountDuplicateTransactionsServiceModel {\n    constructor() {\n        this.selectedLines = new Set();\n    }\n\n    updateLIne(selected, id) {\n        this.selectedLines[selected ? \"add\" : \"delete\"](id);\n    }\n}\n\nconst duplicateCheckService = {\n    start(env, services) {\n        return new AccountDuplicateTransactionsServiceModel();\n    },\n};\n\nregistry\n    .category(\"services\")\n    .add(\"account_online_synchronization.duplicate_check_service\", duplicateCheckService);\n", "import { onMounted } from \"@odoo/owl\";\nimport { registry } from \"@web/core/registry\";\nimport { ListRenderer } from \"@web/views/list/list_renderer\";\nimport { X2ManyField, x2ManyField } from \"@web/views/fields/x2many/x2many_field\";\nimport { useCheckDuplicateService } from \"./account_duplicate_transaction_hook\";\n\nexport class AccountDuplicateTransactionsListRenderer extends ListRenderer {\n    static template = \"account_online_synchronization.AccountDuplicateTransactionsListRenderer\";\n    static recordRowTemplate = \"account_online_synchronization.AccountDuplicateTransactionsRecordRow\";\n\n    setup() {\n        super.setup();\n        this.duplicateCheckService = useCheckDuplicateService();\n\n        onMounted(() => {\n            this.deleteButton = document.querySelector('button[name=\"delete_selected_transactions\"]');\n            this.deleteButton.disabled = true;\n        });\n    }\n\n    toggleRecordSelection(selected, record) {\n        this.duplicateCheckService.updateLIne(selected, record.data.id);\n        this.deleteButton.disabled = this.duplicateCheckService.selectedLines.size === 0;\n    }\n\n    get hasSelectors() {\n        return true;\n    }\n\n    getRowClass(record) {\n        let classes = super.getRowClass(record);\n        const firstIdsInGroup = this.env.model.root.data.first_ids_in_group;\n        if (firstIdsInGroup instanceof Array && firstIdsInGroup.includes(record.data.id)) {\n            classes += \" account_duplicate_transactions_lines_list_x2many_group_line\";\n        }\n        return classes;\n    }\n}\n\nexport class AccountDuplicateTransactionsLinesListX2ManyField extends X2ManyField {\n    static components = {\n        ...X2ManyField.components,\n        ListRenderer: AccountDuplicateTransactionsListRenderer,\n    };\n}\n\nregistry.category(\"fields\").add(\"account_duplicate_transactions_lines_list_x2many\", {\n    ...x2ManyField,\n    component: AccountDuplicateTransactionsLinesListX2ManyField,\n});\n", "import { registry } from \"@web/core/registry\";\nimport { standardWidgetProps } from \"@web/views/widgets/standard_widget_props\";\nimport { useService, useBus } from \"@web/core/utils/hooks\";\nimport { SIZES } from \"@web/core/ui/ui_service\";\nimport { Component, useState, useRef, onWillStart } from \"@odoo/owl\";\nimport { useBankInstitutions } from \"@account_online_synchronization/hooks/bank_institutions_hook\";\n\nclass BankConfigureWidget extends Component {\n    static template = \"account.BankConfigureWidget\";\n    static props = {\n        ...standardWidgetProps,\n    }\n    setup() {\n        this.container = useRef(\"container\");\n        this.allInstitutions = [];\n        this.state = useState({\n            isLoading: true,\n            institutions: [],\n            gridStyle: \"grid-template-columns: repeat(5, minmax(90px, 1fr));\"\n        });\n        this.orm = useService(\"orm\");\n        this.action = useService(\"action\");\n        this.ui = useService(\"ui\");\n        this.bankInstitutions = useBankInstitutions();\n        onWillStart(this.fetchInstitutions);\n        useBus(this.ui.bus, \"resize\", this.computeGrid);\n    }\n\n    computeGrid() {\n        if (this.allInstitutions.length > 4) {\n            let containerWidth = this.container.el ? this.container.el.offsetWidth - 32 : 0;\n            // when the container width can't be computed, use the screen size and number of journals.\n            if (!containerWidth) {\n                if (this.ui.size >= SIZES.XXL) {\n                    containerWidth = window.innerWidth / (this.props.record.model.root.count < 6 ? 2 : 3);\n                } else {\n                    containerWidth = Math.max(this.ui.size * 100, 400);\n                }\n            }\n            const canFit = Math.floor(containerWidth / 100);\n            const numberOfRows = (Math.floor((this.allInstitutions.length + 1) / 2) >= canFit) + 1;\n            this.state.gridStyle = `grid-template-columns: repeat(${canFit}, minmax(90px, 1fr));\n                                    grid-template-rows: repeat(${numberOfRows}, 1fr);\n                                    grid-auto-rows: 0px;\n                                   `;\n        }\n        this.state.institutions = this.allInstitutions;\n    }\n\n    async fetchInstitutions() {\n        this.bankInstitutions.fetch(this.props.record.resId)\n        .then((response) => {\n            this.allInstitutions = response;\n        })\n        .finally(() => {\n            this.state.isLoading = false;\n            this.computeGrid();\n        });\n    }\n\n    async connectBank(institutionId=null) {\n        const action = await this.orm.call(\"account.online.link\", \"action_new_synchronization\", [[]], {\n            preferred_inst: institutionId,\n            journal_id: this.props.record.resId,\n        })\n        this.action.doAction(action);\n    }\n\n    async fallbackConnectBank() {\n        const action = await this.orm.call(\n            'account.online.link',\n            'create_new_bank_account_action',\n            [this.props.record.resId, { journal_type: this.props.record.data.type }], {\n            context: {\n                active_model: 'account.journal',\n                active_id: this.props.record.resId,\n            }\n        });\n        this.action.doAction(action);\n    }\n}\n\nexport const bankConfigureWidget = {\n    component: BankConfigureWidget,\n}\n\nregistry.category(\"view_widgets\").add(\"bank_configure\", bankConfigureWidget);\n", "import { Component } from \"@odoo/owl\";\nimport { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\nimport { registry } from \"@web/core/registry\";\nimport { useService } from \"@web/core/utils/hooks\";\n\nconst cogMenuRegistry = registry.category(\"cogMenu\");\n\n/**\n * 'Fetch Missing Transactions' menu\n *\n * This component is used to open a wizard allowing the user to fetch their missing/pending\n * transaction since a specific date.\n * It's only available in the bank reconciliation widget.\n * By default, if there is only one selected journal, this journal is directly selected.\n * In case there is no selected journal or more than one, we let the user choose which\n * journal he/she wants. This part is handled by the server.\n * @extends Component\n */\nexport class FetchMissingTransactions extends Component {\n    static template = \"account_online_synchronization.FetchMissingTransactions\";\n    static components = { DropdownItem };\n    static props = {};\n\n    setup() {\n        this.action = useService(\"action\");\n    }\n\n    //---------------------------------------------------------------------\n    // Protected\n    //---------------------------------------------------------------------\n\n    async openFetchMissingTransactionsWizard() {\n        const { context } = this.env.searchModel;\n        const activeModel = context.active_model;\n        let activeIds = [];\n        if (activeModel === \"account.journal\") {\n            activeIds = context.active_ids;\n        } else if (!!context.default_journal_id) {\n            activeIds = context.default_journal_id;\n        }\n        // We have to use this.env.services.orm.call instead of using useService\n        // for a specific reason. useService implies that function calls with\n        // are \"protected\", it means that if the component is closed the\n        // response will be pending and the code stop their execution.\n        // By passing directly from the env, this protection is not activated.\n        const action = await this.env.services.orm.call(\n            \"account.journal\",\n            \"action_open_missing_transaction_wizard\",\n            [activeIds]\n        );\n        return this.action.doAction(action);\n    }\n}\n\nexport const fetchMissingTransactionItem = {\n    Component: FetchMissingTransactions,\n    groupNumber: 5,\n    isDisplayed: ({ config, isSmall }) => {\n        return !isSmall &&\n        config.actionType === \"ir.actions.act_window\" &&\n        [\"kanban\", \"list\"].includes(config.viewType) &&\n        [\"bank_rec_widget_kanban\", \"bank_rec_list\"].includes(config.viewSubType);\n    },\n};\n\ncogMenuRegistry.add(\"fetch-missing-transaction-menu\", fetchMissingTransactionItem, { sequence: 1 });\n", "import { Component } from \"@odoo/owl\";\nimport { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\nimport { registry } from \"@web/core/registry\";\nimport { useService } from \"@web/core/utils/hooks\";\n\n/**\n * 'Find Duplicate Transactions' menu\n *\n * This component is used to open a wizard allowing the user to find duplicate\n * transactions since a specific date.\n * It's only available in the bank reconciliation widget.\n * By default, if there is only one selected journal, this journal is directly selected.\n * In case there is no selected journal or more than one, we let the user choose.\n * @extends Component\n */\nexport class FindDuplicateTransactions extends Component {\n    static template = \"account_online_synchronization.FindDuplicateTransactions\";\n    static components = { DropdownItem };\n    static props = {};\n\n    setup() {\n        this.action = useService(\"action\");\n    }\n\n    //---------------------------------------------------------------------\n    // Protected\n    //---------------------------------------------------------------------\n\n    async openFindDuplicateTransactionsWizard() {\n        const { context } = this.env.searchModel;\n        const activeModel = context.active_model;\n        let activeIds = [];\n        if (activeModel === \"account.journal\") {\n            activeIds = context.active_ids;\n        } else if (context.default_journal_id) {\n            activeIds = context.default_journal_id;\n        }\n        return this.action.doActionButton({\n            type: \"object\",\n            resModel: \"account.journal\",\n            name:\"action_open_duplicate_transaction_wizard\",\n            resIds: activeIds,\n        })\n    }\n}\n\nexport const findDuplicateTransactionItem = {\n    Component: FindDuplicateTransactions,\n    groupNumber: 5, // same group as fetch missing transactions\n    isDisplayed: ({ config, isSmall }) => {\n        return (\n            !isSmall &&\n            config.actionType === \"ir.actions.act_window\" &&\n            [\"kanban\", \"list\"].includes(config.viewType) &&\n            [\"bank_rec_widget_kanban\", \"bank_rec_list\"].includes(config.viewSubType)\n        )\n    },\n};\n\nregistry.category(\"cogMenu\").add(\n    \"find-duplicate-transaction-menu\",\n    findDuplicateTransactionItem,\n    { sequence: 3 }, // after fetch missing transactions\n);\n", "import { patch } from \"@web/core/utils/patch\";\nimport { BankRecKanbanRenderer } from \"@account_accountant/components/bank_reconciliation/kanban_renderer\";\n\npatch(BankRecKanbanRenderer.prototype, {\n    async getJournalTotalAmount() {\n        const values = await super.getJournalTotalAmount();\n        this.globalState.journalAvailableBalanceAmount = values.available_balance_amount || \"\";\n    },\n});\n", "import { patch } from \"@web/core/utils/patch\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { BankRecStatementSummary } from \"@account_accountant/components/bank_reconciliation/statement_summary/statement_summary\";\n\npatch(BankRecStatementSummary, {\n    props: {\n        ...BankRecStatementSummary.props,\n        journalAvailableBalanceAmount: { type: String, optional: true },\n    },\n});\n\npatch(BankRecStatementSummary.prototype, {\n    setup() {\n        super.setup();\n        this.orm = useService(\"orm\");\n        this.action = useService(\"action\");\n    },\n\n    async actionOpenPendingBankStatementLines() {\n        this.action.doActionButton({\n            type: \"object\",\n            resId: this.props.journalId,\n            name: \"action_open_pending_bank_statement_lines\",\n            resModel: \"account.journal\",\n        });\n    },\n});\n", "import { registry } from \"@web/core/registry\";\nimport { standardWidgetProps } from \"@web/views/widgets/standard_widget_props\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { Component, useState } from \"@odoo/owl\";\n\nclass ConnectedUntil extends Component {\n    static template = \"account_online_synchronization.ConnectedUntil\";\n    static props = { ...standardWidgetProps };\n\n    setup() {\n        this.state = useState({\n            isHovered: false,\n            displayReconnectButton: false,\n        });\n\n        if (this.isConnectionExpiredIn(0)) {\n            this.state.displayReconnectButton = true;\n        }\n\n        this.action = useService(\"action\");\n        this.orm = useService(\"orm\");\n    }\n\n    get cssClasses() {\n        let cssClasses = \"text-nowrap w-100\";\n        if (this.isConnectionExpiredIn(7)) {\n            cssClasses += this.isConnectionExpiredIn(3) ? \" text-danger\" : \" text-warning\";\n        }\n        return cssClasses;\n    }\n\n    onMouseEnter() {\n        this.state.isHovered = true;\n    }\n\n    onMouseLeave() {\n        this.state.isHovered = false;\n    }\n\n    isConnectionExpiredIn(nbDays) {\n        return this.props.record.data.expiring_synchronization_due_day <= nbDays;\n    }\n\n    async extendConnection() {\n        const action = await this.orm.call(\n            \"account.journal\",\n            \"action_extend_consent\",\n            [this.props.record.resId],\n            {}\n        );\n        this.action.doAction(action);\n    }\n}\n\nexport const connectedUntil = {\n    component: ConnectedUntil,\n};\n\nregistry.category(\"view_widgets\").add(\"connected_until_widget\", connectedUntil);\n", "import { onWillStart } from \"@odoo/owl\";\nimport { patch } from \"@web/core/utils/patch\";\nimport { JournalCreateWizard } from \"@account_accountant/components/journal_create_wizard/journal_create_wizard\";\nimport { useBankInstitutions } from \"@account_online_synchronization/hooks/bank_institutions_hook\";\n\npatch(JournalCreateWizard.prototype, {\n    setup() {\n        super.setup();\n        this.bankInstitutions = useBankInstitutions();\n        onWillStart(async () => {\n            this.institutionsPictures = (await this.bankInstitutions.fetch())\n                .slice(0, 2)\n                .map((institution) => institution.picture);\n        });\n    },\n    cardImages(cardType) {\n        return cardType !== \"bank\" || this.institutionsPictures.length !== 2\n            ? super.cardImages(cardType)\n            : this.institutionsPictures;\n    },\n});\n", "import { onMounted, useState } from \"@odoo/owl\";\nimport { registry } from \"@web/core/registry\";\nimport { RadioField, radioField } from \"@web/views/fields/radio/radio_field\";\nimport { useService } from '@web/core/utils/hooks';\n\n\nclass OnlineAccountRadio extends RadioField {\n    static template = \"account_online_synchronization.OnlineAccountRadio\";\n    setup() {\n        super.setup();\n        this.orm = useService(\"orm\");\n        this.state = useState({balances: {}});\n\n        onMounted(async () => {\n            this.state.balances = await this.loadData();\n            // Make sure the first option is selected by default.\n            this.onChange(this.items[0]);\n        });\n    }\n\n    async loadData() {\n        const ids = this.items.map(i => i[0]);\n        return await this.orm.call(\"account.online.account\", \"get_formatted_balances\", [ids]);\n    }\n\n    getBalanceName(itemID) {\n        return this.state.balances?.[itemID]?.[0] ?? \"Loading ...\";\n    }\n\n    isNegativeAmount(itemID) {\n        // In case of the value is undefined, it will return false as intended.\n        return this.state.balances?.[itemID]?.[1] < 0;\n    }\n}\n\nregistry.category(\"fields\").add(\"online_account_radio\", {\n    ...radioField,\n    component: OnlineAccountRadio,\n});\n", "import { registry } from \"@web/core/registry\";\nimport { standardWidgetProps } from \"@web/views/widgets/standard_widget_props\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { Component, useState, onWillStart, markup } from \"@odoo/owl\";\n\nclass RefreshSpin extends Component {\n    static template = \"account_online_synchronization.RefreshSpin\";\n    static props = { ...standardWidgetProps };\n\n    setup() {\n        this.state = useState({\n            isHovered: false,\n            fetchingStatus: false,\n            connectionStateDetails: null,\n        });\n\n        this.actionService = useService(\"action\");\n        this.busService = this.env.services.bus_service;\n        this.orm = useService(\"orm\");\n        this.state.fetchingStatus = this.props.record.data.online_sync_fetching_status;\n\n        this.busService.subscribe(\"online_sync\", (notification) => {\n            if (notification?.id === this.recordId && notification?.connection_state_details) {\n                this.state.connectionStateDetails = notification.connection_state_details;\n            }\n        });\n\n        onWillStart(() => {\n            this._initConnectionStateDetails();\n        });\n    }\n\n    refresh() {\n        this.actionService.restore(this.actionService.currentController.jsId);\n    }\n\n    onMouseEnter() {\n        this.state.isHovered = true;\n    }\n\n    onMouseLeave() {\n        this.state.isHovered = false;\n    }\n\n    async openAction() {\n        /**\n         * This function is used to open the action that the asynchronous process saved\n         * on the databsase. It allows users to call the action when they want and not when\n         * the process is over.\n         */\n        const action = await this.orm.call(\n            \"account.journal\",\n            \"action_open_dashboard_asynchronous_action\",\n            [this.recordId],\n        );\n        this.actionService.doAction(action);\n        this.state.connectionStateDetails = null;\n    }\n\n    async fetchTransactions() {\n        /**\n         * This function call the function to fetch transactions.\n         * In the main case, we don't do anything after calling the function.\n         * The idea is that websockets will update the status by themselves.\n         * In one specific case, we have to return an action to the user to open\n         * the Odoo Fin iframe to refresh the connection.\n         */\n        this.state.connectionStateDetails = { status: \"fetching\" };\n        const action = await this.orm.call(\"account.journal\", \"manual_sync\", [this.recordId]);\n        if (action) {\n            action.help = markup(action.help);\n            this.actionService.doAction(action);\n        }\n    }\n\n    _initConnectionStateDetails() {\n        /**\n         * This function is used to get the last state of the connection (if there is one)\n         */\n        const kanbanDashboardData = JSON.parse(this.props.record.data.kanban_dashboard);\n        this.state.connectionStateDetails = kanbanDashboardData?.connection_state_details;\n    }\n\n    get recordId() {\n        return this.props.record.data.id;\n    }\n\n    get connectionStatus() {\n        return this.state.connectionStateDetails?.status;\n    }\n}\n\nexport const refreshSpin = {\n    component: RefreshSpin,\n};\n\nregistry.category(\"view_widgets\").add(\"refresh_spin_widget\", refreshSpin);\n", "import { ListRenderer } from \"@web/views/list/list_renderer\";\nimport { ListController } from \"@web/views/list/list_controller\";\nimport { registry } from \"@web/core/registry\";\nimport { listView } from \"@web/views/list/list_view\";\nimport { useService } from \"@web/core/utils/hooks\";\n\nexport class TransientBankStatementLineListController extends ListController {\n\n    setup() {\n        super.setup();\n        this.orm = useService(\"orm\");\n        this.action = useService(\"action\");\n    }\n\n    async onClickImportTransactions() {\n        const resIds = await this.model.root.getResIds(true);\n        const resultAction = await this.orm.call(\"account.bank.statement.line.transient\", \"action_import_transactions\", [resIds]);\n        this.action.doAction(resultAction);\n    }\n\n    get allowImportTransaction() {\n        return !this.props.context.disable_import;\n    }\n}\n\nexport class TransientBankStatementLineListRenderer extends ListRenderer {\n\n    static template = \"account_online_synchronization.TransientBankStatementLineRenderer\";\n\n    setup() {\n        super.setup();\n        this.orm = useService(\"orm\");\n        this.action = useService(\"action\");\n    }\n\n    async openManualEntries() {\n        if (this.env.searchModel.context.active_model === \"account.missing.transaction.wizard\" && this.env.searchModel.context.active_ids) {\n            const activeIds = this.env.searchModel.context.active_ids;\n            const action = await this.orm.call(\"account.missing.transaction.wizard\", \"action_open_manual_bank_statement_lines\", activeIds);\n            this.action.doAction(action);\n        }\n    }\n\n    async openCancelledEntries() {\n        if (\n            this.env.searchModel.context.active_model === \"account.missing.transaction.wizard\" &&\n            this.env.searchModel.context.active_ids\n        ) {\n            return await this.action.doActionButton({\n                name: \"action_open_cancelled_bank_statement_lines\",\n                type: \"object\",\n                resModel: \"account.missing.transaction.wizard\",\n                resIds: this.env.searchModel.context.active_ids,\n            });\n        }\n    }\n}\n\nexport const TransientBankStatementLineListView = {\n    ...listView,\n    Renderer: TransientBankStatementLineListRenderer,\n    Controller: TransientBankStatementLineListController,\n    buttonTemplate: \"TransientBankStatementLineButtonTemplate\",\n}\n\nregistry.category(\"views\").add(\"transient_bank_statement_line_list_view\", TransientBankStatementLineListView);\n", "import { patch } from \"@web/core/utils/patch\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { BankRecKanbanController } from \"@account_accountant/components/bank_reconciliation/kanban_controller\";\n\npatch(BankRecKanbanController.prototype, {\n    setup() {\n        super.setup();\n        this.action = useService(\"action\");\n        this.displayDuplicateWarning = !!this.props.context.duplicates_from_date;\n    },\n\n    async onWarningClick() {\n        const { context } = this.env.searchModel;\n        return this.action.doActionButton({\n            type: \"object\",\n            resModel: \"account.journal\",\n            name: \"action_open_duplicate_transaction_wizard\",\n            resId: context.default_journal_id || context.active_id,\n            args: JSON.stringify([context.duplicates_from_date]),\n        });\n    },\n});\n", "import { useService } from \"@web/core/utils/hooks\";\n\nexport function useBankInstitutions() {\n    const orm = useService(\"orm\");\n    function fetch(journalId) {\n        return new Promise((resolve, reject) => {\n            orm.silent\n                .call(\"account.journal\", \"fetch_online_sync_favorite_institutions\", [journalId])\n                .then((response) => {\n                    resolve(response);\n                })\n                .catch((error) => {\n                    reject(error);\n                });\n        });\n    }\n    return {\n        fetch,\n    };\n}\n", "import { registry } from \"@web/core/registry\";\nimport { loadJS } from \"@web/core/assets\";\nimport { cookie } from \"@web/core/browser/cookie\";\nimport { markup } from \"@odoo/owl\";\nconst actionRegistry = registry.category('actions');\n/* global OdooFin */\n\nfunction OdooFinConnector(parent, action) {\n    const orm = parent.services.orm;\n    const currency = parent.services.currency;\n    const actionService = parent.services.action;\n    const notificationService = parent.services.notification;\n    const debugMode = parent.debug;\n\n    const id = action.id;\n    action.params.colorScheme = cookie.get(\"color_scheme\");\n    let mode = action.params.mode || 'link';\n    // Ensure that the proxyMode is valid\n    const modeRegexp = /^[a-z0-9-_]+$/;\n    const runbotRegexp = /^https:\\/\\/[a-z0-9-_]+\\.[a-z0-9-_]+\\.odoo\\.com$/;\n    if (!modeRegexp.test(action.params.proxyMode) && !runbotRegexp.test(action.params.proxyMode)) {\n        return;\n    }\n    let url = 'https://' + action.params.proxyMode + '.odoofin.com/proxy/v1/odoofin_link';\n    if (runbotRegexp.test(action.params.proxyMode)) {\n        url = action.params.proxyMode + '/proxy/v1/odoofin_link';\n    }\n    let actionResult = false;\n\n    loadJS(url)\n        .then(function () {\n            // Create and open the iframe\n            const params = {\n                data: action.params,\n                proxyMode: action.params.proxyMode,\n                onEvent: async function (event, data) {\n                    switch (event) {\n                        case 'close':\n                            return;\n                        case 'reload':\n                            return actionService.doAction({type: 'ir.actions.client', tag: 'reload'});\n                        case 'notification':\n                            notificationService.add(data.message, data);\n                            break;\n                        case 'exchange_token':\n                            await orm.call('account.online.link', 'exchange_token',\n                                [[id], data], {context: action.context});\n                            break;\n                        case 'success':\n                            mode = data.mode || mode;\n                            actionResult = await orm.call('account.online.link', 'success', [[id], mode, data], {context: action.context});\n                            actionResult.help = markup(actionResult.help)\n                            // Reload the currency otherwise in might not be in the session for the getCurrency\n                            await currency.reloadCurrencies();\n                            return actionService.doAction(actionResult);\n                        case 'connect_existing_account':\n                            actionResult = await orm.call('account.online.link', 'connect_existing_account', [data], {context: action.context});\n                            actionResult.help = markup(actionResult.help)\n                            return actionService.doAction(actionResult);\n                        default:\n                            return;\n                    }\n                },\n                onAddBank: async function (data) {\n                    actionResult = await orm.call(\n                        \"account.online.link\",\n                        \"create_new_bank_account_action\",\n                        [[id], data],\n                        { context: action.context }\n                    );\n                    return actionService.doAction(actionResult);\n                }\n            };\n            // propagate parent debug mode to iframe\n            if (typeof debugMode !== \"undefined\" && debugMode) {\n                params.data[\"debug\"] = debugMode;\n            }\n            OdooFin.create(params);\n            OdooFin.open();\n        });\n    return;\n}\n\nactionRegistry.add('odoo_fin_connector', OdooFinConnector);\n\nexport default OdooFinConnector;\n", "/** @odoo-module **/\nimport { Component } from \"@odoo/owl\";\nimport { registry } from \"@web/core/registry\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport {_t} from \"@web/core/l10n/translation\";\nimport { standardActionServiceProps } from \"@web/webclient/actions/action_service\";\n\n\nclass WhatIsPeppol extends Component {\n    static props = { ...standardActionServiceProps };\n    static template = \"account_peppol.WhatIsPeppol\";\n\n    setup() {\n        super.setup();\n        this.actionService = useService(\"action\");\n    }\n\n    closeButtonLabel() {\n        if (this.props.action.context.action_on_activate.res_model === \"peppol.registration\") {\n            return _t(\"Activate\")\n        } else {\n            return _t(\"Got it !\")\n        }\n    }\n\n    activate() {\n        const action = this.props.action.context.action_on_activate;\n        this.actionService.doAction({\n            name: action.name,\n            type: action.type,\n            res_model: action.res_model,\n            views: [[false, action.view_mode]],\n            target: action.target,\n            context: action.context,\n        });\n    }\n}\n\nregistry.category(\"actions\").add(\"account_peppol.what_is_peppol\", WhatIsPeppol);\n", "import { registry } from \"@web/core/registry\";\nimport { CharField, charField } from \"@web/views/fields/char/char_field\";\nimport { useDebounced } from \"@web/core/utils/timing\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { useState } from \"@odoo/owl\";\n\nexport const DELAY = 400;\n\nexport class IbanWidget extends CharField {\n    static template = \"base_iban.iban\";\n    setup() {\n        super.setup();\n        this.state = useState({ isValidIBAN: null });\n        this.orm = useService(\"orm\");\n        this.validateIbanDebounced = useDebounced(async (ev) => {\n            const iban = ev.target.value;\n            if (!iban) {\n                this.state.isValidIBAN = null;\n            } else if (!/[A-Za-z]{2}.{3,}/.test(iban)) {\n                this.state.isValidIBAN = false;\n            } else {\n                this.state.isValidIBAN = await this.orm.call(\"res.partner.bank\", \"check_iban\", [[], iban]);\n            }\n        }, DELAY);\n    }\n}\n\nexport const ibanWidget = {\n    ...charField,\n    component: IbanWidget,\n};\n\nregistry.category(\"fields\").add(\"iban\", ibanWidget);\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { formatDate, formatDateTime } from \"@web/core/l10n/dates\";\nimport { browser } from \"@web/core/browser/browser\";\nimport { rpc } from \"@web/core/network/rpc\";\n\nexport const aiChatLauncherService = {\n    dependencies: [\"mail.store\", \"orm\", \"action\"],\n    start(env, services) {\n        const actionService = services[\"action\"];\n        const mailStore = services[\"mail.store\"];\n\n        async function openFullComposer(msgType, resModel, resId, content) {\n            let allRecipients = [];\n            const thread = await mailStore.Thread.getOrFetch({ model: resModel, id: resId });\n            if (msgType === \"message\") {\n                allRecipients = [...thread.suggestedRecipients, ...thread.additionalRecipients];\n                // auto-create partner\n                const newPartners = allRecipients.filter((recipient) => !recipient.partner_id);\n                if (newPartners.length !== 0) {\n                    const recipientEmails = [];\n                    newPartners.forEach((recipient) => {\n                        recipientEmails.push(recipient.email);\n                    });\n                    const partners = await rpc(\"/mail/partner/from_email\", {\n                        thread_model: thread.model,\n                        thread_id: thread.id,\n                        emails: recipientEmails,\n                    });\n                    for (const index in partners) {\n                        const partnerData = partners[index];\n                        const partner = mailStore[\"res.partner\"].insert(partnerData);\n                        const email = recipientEmails[index];\n                        const recipient = allRecipients.find(\n                            (recipient) => recipient.email === email,\n                        );\n                        recipient.partner_id = partner.id;\n                    }\n                }\n            }\n            actionService.doAction(\n                {\n                    name: msgType === \"message\" ? _t(\"Send Message\") : _t(\"Log Note\"),\n                    res_model: \"mail.compose.message\",\n                    target: \"new\",\n                    type: \"ir.actions.act_window\",\n                    view_id: false,\n                    view_mode: \"form\",\n                    views: [[false, \"form\"]],\n                    context: {\n                        clicked_on_full_composer: true,\n                        default_body: content,\n                        default_model: resModel,\n                        default_partner_ids: allRecipients.map((recipient) => recipient.partner_id),\n                        default_res_ids: [resId],\n                        default_subtype_xmlid:\n                            msgType === \"message\" ? \"mail.mt_comment\" : \"mail.mt_note\",\n                    },\n                },\n                { onClose: () => thread?.fetchNewMessages() },\n            );\n        }\n\n        return {\n            async launchAIChat({\n                callerComponentName,\n                recordModel,\n                recordId,\n                channelTitle,\n                aiSpecialActions,\n                aiChatSourceId,\n                originalRecordData = null,\n                originalRecordFields = null,\n                textSelection = null,\n            }) {\n                let frontEndRecordInfo;\n                // if the component calling the AI has access to record info, we pass it straight to the AI\n                if (['html_field_record', 'html_field_text_select', 'chatter_ai_button'].includes(callerComponentName)) {\n                    frontEndRecordInfo = this.recordDataToContextJSON(originalRecordData, originalRecordFields);\n                }\n                // make the insert button target the component that called the AI\n                services['mail.store'].aiInsertButtonTarget = aiChatSourceId;\n                \n                const { ai_channel_id, data, prompts, model_has_thread } = await services.orm.call(\n                    'discuss.channel',\n                    'create_ai_draft_channel',\n                    [\n                        callerComponentName,\n                        channelTitle,\n                        recordModel,\n                        recordId,\n                        frontEndRecordInfo,\n                        textSelection,\n                    ],\n                );\n\n                services['mail.store'].insert(data);\n                const thread = await services['mail.store'].Thread.getOrFetch({\n                    model: \"discuss.channel\",\n                    id: Number(ai_channel_id),\n                });\n                browser.localStorage.setItem(\"ai.thread.prompt_buttons.\".concat(thread.id), JSON.stringify(prompts));\n\n                // add sendMessage and logNote only if the model inherits from mail.thread\n                if (callerComponentName === \"chatter_ai_button\" && model_has_thread) {\n                    aiSpecialActions = {\n                        ...(aiSpecialActions || {}),\n                        sendMessage: (content) =>\n                            openFullComposer(\"message\", recordModel, recordId, content),\n                        logNote: (content) =>\n                            openFullComposer(\"note\", recordModel, recordId, content),\n                    };\n                }\n                thread.ai_prompt_buttons = prompts;\n                thread.aiSpecialActions = aiSpecialActions;\n                thread.aiChatSource = aiChatSourceId;\n                thread.openChatWindow({ focus: true });\n            },\n            /**\n             * Converts record data to JSON, so we can pass them to the AI record's context\n             * @returns {String} String JSON representation of the record\n             */\n            recordDataToContextJSON(recordData, fieldsInfo) {\n                const result = {};\n\n                for (const fieldName in recordData) {\n                    if (!recordData.hasOwnProperty(fieldName)) continue;\n                    const fieldValue = recordData[fieldName];\n                    const fieldInfo = fieldsInfo[fieldName] || {};\n                    // Skip binary fields entirely - there is no easy way of placing them in the context\n                    if (fieldInfo.type === 'binary') {\n                        continue;\n                    }\n                    // Handle relational fields\n                    if (['many2one', 'many2many', 'one2many'].includes(fieldInfo.type)) {\n                        // Skip abnormally large relational fields which can floud the AI context\n                        if (fieldValue && fieldValue.records && fieldValue.records.length > 50) {\n                            continue;\n                        }\n                        switch (fieldInfo.type) {\n                            case 'many2one':\n                                result[fieldName] = fieldValue ? fieldValue.display_name || fieldValue.name : null;\n                                break;\n                            case 'many2many':\n                            case 'one2many':\n                                if (fieldValue && fieldValue.records) {\n                                    result[fieldName] = fieldValue.records.map(record => \n                                        record.data.display_name || record.data.name\n                                    );\n                                } else {\n                                    result[fieldName] = [];\n                                }\n                                break;\n                        }\n                    } else if (fieldInfo.type === 'date' && fieldValue) {  // handle date fields\n                        const date = luxon.DateTime.fromISO(fieldValue);\n                        result[fieldName] = date.isValid ? formatDate(date) : fieldValue;\n                    } else if (fieldInfo.type === 'datetime' && fieldValue) {  // handle datetime fields\n                        const datetime = luxon.DateTime.fromISO(fieldValue);\n                        result[fieldName] = datetime.isValid ? formatDateTime(datetime) : fieldValue;\n                    } else {  // handle all other types of fields\n                        result[fieldName] = fieldValue;\n                    }\n                }\n                return result;\n            },\n        };\n    },\n};\n\nregistry.category(\"services\").add(\"aiChatLauncher\", aiChatLauncherService);\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { Component, useRef, useState } from \"@odoo/owl\";\nimport { registry } from \"@web/core/registry\";\nimport { standardFieldProps } from \"@web/views/fields/standard_field_props\";\nimport { useAutoresize } from \"@web/core/utils/autoresize\";\nimport { useService } from \"@web/core/utils/hooks\";\n\n/**\n * Widget to edit the JSON schema sent to the LLM provider.\n *\n * See https://json-schema.org/understanding-json-schema/reference\n */\nexport class AiJsonSchema extends Component {\n    static template = \"ai.AiJsonSchema\";\n    static props = { ...standardFieldProps };\n\n    setup() {\n        this.tableEl = useRef(\"table\");\n        this.textareaEl = useRef(\"textarea\");\n        this.notification = useService(\"notification\");\n        this.state = useState({\n            technical_mode: false,\n            // There's no order in the arguments, so we try to not move them when editing\n            orderArguments: Object.keys(this.value?.properties || {}).sort(),\n        });\n        useAutoresize(this.textareaEl);\n    }\n\n    get types() {\n        return [\n            [\"boolean\", \"Boolean\"],\n            [\"integer\", \"Integer\"],\n            [\"number\", \"Number\"],\n            [\"string\", \"String\"],\n            [\"enum_integer\", \"Integer Enum\"],\n            [\"enum_number\", \"Number Enum\"],\n            [\"enum_string\", \"String Enum\"],\n            [\"regex\", \"Regex\"],\n            [\"array_boolean\", \"Array of Boolean\"],\n            [\"array_integer\", \"Array of Integer\"],\n            [\"array_number\", \"Array of Number\"],\n            [\"array_string\", \"Array of String\"],\n        ];\n    }\n\n    get value() {\n        let value;\n        try {\n            value = JSON.parse(this.props.record.data[this.props.name] || \"{}\");\n        } catch {\n            value = {};\n        }\n        const defaultKeys = {\n            properties: {},\n            required: [],\n            type: \"object\",\n        };\n        return { ...defaultKeys, ...value };\n    }\n\n    get arguments() {\n        const value = this.value;\n        if (!value) {\n            return [];\n        }\n        const required = value.required || [];\n        const keys = Object.keys(value.properties).sort(\n            (a, b) => this.state.orderArguments.indexOf(a) - this.state.orderArguments.indexOf(b)\n        );\n        return keys.map((k) => [\n            k,\n            value.properties[k],\n            required.includes(k),\n            this.getEffectiveType(value.properties[k]),\n        ]);\n    }\n\n    getEffectiveType(properties) {\n        if (\n            properties.enum !== undefined &&\n            [\"integer\", \"number\", \"string\"].includes(properties.type)\n        ) {\n            return `enum_${properties.type}`;\n        }\n        if (properties.pattern !== undefined) {\n            return \"regex\";\n        }\n        if (\n            properties.type === \"array\" &&\n            [\"boolean\", \"integer\", \"number\", \"string\"].includes(properties.items.type)\n        ) {\n            return `array_${properties.items.type}`;\n        }\n        return properties.type;\n    }\n\n    onNameChange(newName, oldName, target) {\n        const recordValue = this.value;\n        if (recordValue.properties[newName]) {\n            this.notification.add(_t(\"The name should be unique\"), { type: \"warning\" });\n            target.value = oldName;\n            return;\n        }\n        recordValue.properties[newName] = recordValue.properties[oldName];\n        delete recordValue.properties[oldName];\n        recordValue.required = recordValue.required.map((n) => (n === oldName ? newName : n));\n        this.state.orderArguments = this.state.orderArguments.map((n) =>\n            n === oldName ? newName : n\n        );\n        this.props.record.update({ [this.props.name]: JSON.stringify(recordValue) });\n    }\n\n    onDescriptionChange(value, name) {\n        const recordValue = this.value;\n        recordValue.properties[name].description = value;\n        this.props.record.update({ [this.props.name]: JSON.stringify(recordValue) });\n    }\n\n    onTypeChange(value, name) {\n        const recordValue = this.value;\n\n        delete recordValue.properties[name].enum;\n        delete recordValue.properties[name].pattern;\n        delete recordValue.properties[name].items;\n        delete recordValue.properties[name].maxLength;\n\n        if (value.startsWith(\"enum_\")) {\n            recordValue.properties[name].type = value.split(\"_\").at(-1);\n            recordValue.properties[name].enum = [];\n        } else if (value === \"regex\") {\n            recordValue.properties[name].type = \"string\";\n            recordValue.properties[name].pattern = \"\";\n        } else if (value.startsWith(\"array_\")) {\n            recordValue.properties[name].type = \"array\";\n            recordValue.properties[name].items = { type: value.split(\"_\").at(-1) };\n        } else {\n            recordValue.properties[name].type = value;\n        }\n\n        if (value === \"string\") {\n            recordValue.properties[name].maxLength = 60;\n        }\n        this.props.record.update({ [this.props.name]: JSON.stringify(recordValue) });\n    }\n\n    onEnumChange(value, name) {\n        const recordValue = this.value;\n        const cast =\n            {\n                integer: (v) => parseInt(v) || 0,\n                number: (v) => parseFloat(v) || 0.0,\n            }[recordValue.properties[name].type] || ((v) => v);\n        recordValue.properties[name].enum = (value || \"\").split(\",\").map((v) => cast(v.trim()));\n        this.props.record.update({ [this.props.name]: JSON.stringify(recordValue) });\n    }\n\n    onRegexChange(value, name) {\n        const recordValue = this.value;\n        recordValue.properties[name].type = \"string\";\n        recordValue.properties[name].pattern = value;\n        this.props.record.update({ [this.props.name]: JSON.stringify(recordValue) });\n    }\n\n    onMaxLengthChange(maxLength, name) {\n        maxLength = parseInt(maxLength);\n        const recordValue = this.value;\n        if (!maxLength) {\n            delete recordValue.properties[name].maxLength;\n        } else {\n            recordValue.properties[name].maxLength = maxLength;\n        }\n        this.props.record.update({ [this.props.name]: JSON.stringify(recordValue) });\n    }\n\n    onRequiredChange(value, name) {\n        const recordValue = this.value;\n        if (!value) {\n            recordValue.required = recordValue.required.filter((n) => n !== name);\n        } else if (!recordValue.required.includes(name)) {\n            recordValue.required.push(name);\n        }\n        this.props.record.update({ [this.props.name]: JSON.stringify(recordValue) });\n    }\n\n    onDelete(name) {\n        const recordValue = this.value;\n        recordValue.required = recordValue.required.filter((n) => n !== name);\n        this.state.orderArguments = this.state.orderArguments.filter((n) => n !== name);\n        delete recordValue.properties[name];\n        this.props.record.update({\n            [this.props.name]: Object.keys(recordValue.properties).length\n                ? JSON.stringify(recordValue)\n                : false,\n        });\n    }\n\n    onNewArgument() {\n        const recordValue = this.value;\n        if (recordValue.properties[\"\"]) {\n            this.tableEl.el.querySelector(\"td:first-child input:placeholder-shown\").focus();\n            return;\n        }\n        recordValue.properties[\"\"] = { type: \"string\", maxLength: 60 };\n        if (!recordValue.required.includes(\"\")) {\n            recordValue.required.push(\"\");\n        }\n        this.state.orderArguments.push(\"\");\n        this.props.record.update({ [this.props.name]: JSON.stringify(recordValue) });\n    }\n\n    onBlur(ev) {\n        const value = ev.target.value;\n        this.props.record.update({ [this.props.name]: value });\n    }\n}\n\nconst aiJsonSchema = {\n    component: AiJsonSchema,\n};\n\nregistry.category(\"fields\").add(\"ai_json_schema\", aiJsonSchema);\n", "import { ModelFieldSelectorPopover } from \"@web/core/model_field_selector/model_field_selector_popover\";\nimport { user } from \"@web/core/user\";\nimport { useService } from \"@web/core/utils/hooks\";\n\nimport { useState, onWillStart } from \"@odoo/owl\";\n\nexport class AiModelFieldSelectorPopover extends ModelFieldSelectorPopover {\n    static template = \"ai.AiModelFieldSelectorPopover\";\n    static props = {\n        ...ModelFieldSelectorPopover.props,\n        updateBatch: Function,\n        fieldsPath: { type: Array, optional: true },\n        aiFieldPath: { type: String, optional: true },\n    };\n    static defaultProps = {\n        ...ModelFieldSelectorPopover.defaultProps,\n        readProperty: true,\n    };\n\n    setup() {\n        this.aiFieldsState = useState({ selected: [] });\n\n        this.orm = useService(\"orm\");\n\n        onWillStart(async () => {\n            if (this.props.fieldsPath) {\n                await this._loadFields(this.props.fieldsPath);\n            }\n        });\n        this.isTemplateEditor = null;\n        this.allowedQwebExpressions = null;\n        return super.setup();\n    }\n\n    get fieldNames() {\n        // Can not use `page.path`, because properties have custom code\n        const currentPath = [];\n        let page = this.state.page.previousPage;\n        while (page) {\n            currentPath.push(page.selectedName);\n            page = page.previousPage;\n        }\n        const currentPathStr = currentPath.reverse().join(\".\");\n\n        return super.fieldNames.filter((f) => {\n            const path = currentPathStr ? `${currentPathStr}.${f}` : f;\n            if (this.props.aiFieldPath && this.props.aiFieldPath === path) {\n                return false;\n            }\n            return true;\n        });\n    }\n\n    async selectField(field) {\n        if (field.type === \"properties\") {\n            return this.followRelation(field);\n        }\n        this.state.page.selectedName = field.name;\n        // Check if this exact field chain already exists in selected fields\n        if (this.isFieldSelected(field)) {\n            return;\n        }\n\n        this.keepLast.add(Promise.resolve());\n\n        this.aiFieldsState.selected.push([...this.fieldsChain, field]);\n\n        if (!(await user.hasGroup(\"mail.group_mail_template_editor\"))) {\n            // If the user is not template editor, he won't be able to select many fields\n            this.onInsert();\n        }\n    }\n\n    get fieldsChain() {\n        let page = this.state.page.previousPage;\n        const fieldsChain = [];\n        while (page) {\n            fieldsChain.push(page.fieldDefs[page.selectedName]);\n            page = page.previousPage;\n        }\n        return fieldsChain.reverse();\n    }\n\n    onInsert() {\n        this.props.updateBatch(this.aiFieldsState.selected);\n        this.props.close();\n    }\n\n    onRemoveField(path) {\n        this.aiFieldsState.selected = this.aiFieldsState.selected.filter((f) => f[0] !== path);\n    }\n\n    async beforeFilter() {\n        if (this.isTemplateEditor === null) {\n            const getAllowedQwebExpressions = this.env.services[\"allowed_qweb_expressions\"];\n            this.isTemplateEditor = await user.hasGroup(\"mail.group_mail_template_editor\");\n            this.allowedQwebExpressions = await getAllowedQwebExpressions(this.props.resModel);\n        }\n    }\n\n    async followRelations() {\n        await this.beforeFilter();\n        return super.followRelations(...arguments);\n    }\n    async loadPages() {\n        await this.beforeFilter();\n        return super.loadPages(...arguments);\n    }\n\n    filter(fieldDefs, path) {\n        fieldDefs = super.filter(fieldDefs, path);\n        const filteredKeys = Object.keys(fieldDefs).filter((key) => {\n            const fullPath = `object${path ? `.${path}` : \"\"}.${fieldDefs[key].name}`;\n            if (!this.isTemplateEditor && !this.allowedQwebExpressions?.includes(fullPath)) {\n                return false;\n            }\n            if (fieldDefs[key].type === \"separator\") {\n                // Don't show separator property\n                return false;\n            }\n            return fieldDefs[key].searchable;\n        });\n        return Object.fromEntries(filteredKeys.map((k) => [k, fieldDefs[k]]));\n    }\n\n    /**\n     * Load the list of field path in the popover state.\n     */\n    async _loadFields(fieldsPath) {\n        const fieldsInfos = Object.fromEntries(\n            await Promise.all(\n                fieldsPath.map(async (path) => [\n                    path,\n                    await this.fieldService.loadPath(this.props.resModel, path, true),\n                ])\n            )\n        );\n\n        const selected = [];\n        for (const path of fieldsPath) {\n            const { names, modelsInfo } = await fieldsInfos[path];\n            const fieldsInfo = [];\n            for (let i = 0; i < names.length; i++) {\n                const f = modelsInfo[i].fieldDefs[names[i]];\n                fieldsInfo.push(f);\n            }\n            selected.push(fieldsInfo);\n        }\n        this.aiFieldsState.selected = selected;\n    }\n    \n    isFieldSelected(fieldDef) {\n    const fullFieldPath = [...this.fieldsChain, fieldDef].map(f => f.name).join('.');\n    \n    // Check if this path exists in selected fields\n    return this.aiFieldsState.selected.some(selectedChain => \n        selectedChain.map(f => f.name).join('.') === fullFieldPath\n    );\n}\n}\n", "import { registry } from \"@web/core/registry\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { AlertDialog } from \"@web/core/confirmation_dialog/confirmation_dialog\";\nimport { session } from \"@web/session\";\n\nexport const aiNaturalLanguageService = {\n    dependencies: [\"bus_service\", \"action\", \"menu\", \"dialog\"],\n    start(env, { bus_service, action: actionService, menu: menuService, dialog: dialogService }) {\n        bus_service.subscribe(\n            \"AI_OPEN_MENU_LIST\",\n            async ({\n                menuID,\n                selectedFilters,\n                selectedGroupBys,\n                search,\n                customDomain,\n                aiSessionIdentifier,\n            }) => {\n                if (aiSessionIdentifier !== session.ai_session_identifier) {\n                    return;\n                }\n                const menu = await menuService.getMenu(menuID);\n                if (!menu.actionID) {\n                    return;\n                }\n                const aiProps = { selectedFilters, selectedGroupBys, search };\n                if (customDomain) {\n                    aiProps.customDomain = customDomain;\n                }\n                await actionService.doAction(menu.actionID, {\n                    props: { ai: aiProps },\n                    viewType: \"list\",\n                });\n            }\n        );\n        bus_service.subscribe(\n            \"AI_OPEN_MENU_KANBAN\",\n            async ({\n                menuID,\n                selectedFilters,\n                selectedGroupBys,\n                search,\n                customDomain,\n                aiSessionIdentifier,\n            }) => {\n                if (aiSessionIdentifier !== session.ai_session_identifier) {\n                    return;\n                }\n                const menu = await menuService.getMenu(menuID);\n                if (!menu.actionID) {\n                    return;\n                }\n                const aiProps = { selectedFilters, selectedGroupBys, search };\n                if (customDomain) {\n                    aiProps.customDomain = customDomain;\n                }\n                await actionService.doAction(menu.actionID, {\n                    props: { ai: aiProps },\n                    viewType: \"kanban\",\n                });\n            }\n        );\n        bus_service.subscribe(\n            \"AI_OPEN_MENU_PIVOT\",\n            async ({\n                menuID,\n                selectedFilters,\n                rowGroupBys,\n                colGroupBys,\n                measures,\n                search,\n                sortedColumn,\n                customDomain,\n                aiSessionIdentifier,\n            }) => {\n                if (aiSessionIdentifier !== session.ai_session_identifier) {\n                    return;\n                }\n                const menu = await menuService.getMenu(menuID);\n                if (!menu.actionID) {\n                    return;\n                }\n                const aiProps = {\n                    selectedFilters,\n                    selectedGroupBys: rowGroupBys,\n                    colGroupBys,\n                    measures,\n                    search,\n                };\n                if (sortedColumn) {\n                    aiProps.sortedColumn = sortedColumn;\n                }\n                if (customDomain) {\n                    aiProps.customDomain = customDomain;\n                }\n                await actionService.doAction(menu.actionID, {\n                    props: { ai: aiProps },\n                    viewType: \"pivot\",\n                });\n            }\n        );\n        bus_service.subscribe(\n            \"AI_OPEN_MENU_GRAPH\",\n            async ({\n                menuID,\n                selectedFilters,\n                groupBys,\n                measure,\n                mode,\n                order,\n                stacked,\n                cumulated,\n                search,\n                customDomain,\n                aiSessionIdentifier,\n            }) => {\n                if (aiSessionIdentifier !== session.ai_session_identifier) {\n                    return;\n                }\n                const menu = await menuService.getMenu(menuID);\n                if (!menu.actionID) {\n                    return;\n                }\n                const aiProps = {\n                    selectedFilters,\n                    groupBys,\n                    measure,\n                    mode,\n                    order,\n                    stacked,\n                    cumulated,\n                    search,\n                };\n                if (customDomain) {\n                    aiProps.customDomain = customDomain;\n                }\n                await actionService.doAction(menu.actionID, {\n                    props: { ai: aiProps },\n                    viewType: \"graph\",\n                });\n            }\n        );\n        bus_service.subscribe(\n            \"AI_ADJUST_SEARCH\",\n            async ({\n                removeFacets,\n                toggleFilters,\n                toggleGroupBys,\n                applySearches,\n                measures,\n                mode,\n                order,\n                stacked,\n                cumulated,\n                switchViewType,\n                customDomain,\n            }) => {\n                async function trySwitchView() {\n                    try {\n                        if (switchViewType) {\n                            await actionService.switchView(switchViewType);\n                        }\n                    } catch (error) {\n                        dialogService.add(AlertDialog, {\n                            body: _t(\n                                \"Tried to switch to %s but the app is no longer in an action window.\",\n                                switchViewType\n                            ),\n                            title: _t(\"Unable to switch view\"),\n                            confirm: () => {},\n                            confirmLabel: _t(\"Close\"),\n                        });\n                        return error;\n                    }\n                }\n                // switch view before applying the changes to the search bar because switching view\n                // will rerender the action container with the search bar. Any search bar changes from the\n                // previous action container won't be reflected if we don't wait for the new view.\n                const error = await trySwitchView();\n                if (!error) {\n                    env.bus.trigger(\"APPLY_AI_ADJUST_SEARCH\", {\n                        removeFacets,\n                        toggleFilters,\n                        toggleGroupBys,\n                        applySearches,\n                        measures,\n                        mode,\n                        order,\n                        stacked,\n                        cumulated,\n                        customDomain,\n                    });\n                }\n            }\n        );\n        bus_service.start();\n    },\n};\n\nregistry.category(\"services\").add(\"ai_natural_language_service\", aiNaturalLanguageService);\n", "import { AiModelFieldSelectorPopover } from \"@ai/ai_model_field_selector/ai_model_field_selector_popover\";\nimport { Plugin } from \"@html_editor/plugin\";\nimport { withSequence } from \"@html_editor/utils/resource\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { isHtmlContentSupported } from \"@html_editor/core/selection_plugin\";\n\nexport const AI_FIELD_SELECTOR = \"span[data-ai-field]\";\n\nexport class AIFieldSelectorPlugin extends Plugin {\n    static id = \"AIFieldSelector\";\n    static dependencies = [\"overlay\", \"selection\", \"history\", \"dom\"];\n    static shared = [\"open\"];\n    resources = {\n        user_commands: [\n            {\n                id: \"openAIFieldSelector\",\n                title: _t(\"Field Selector\"),\n                description: _t(\"Insert a field\"),\n                icon: _t(\"fa-hashtag\"),\n                run: () => this.open(),\n                isAvailable: isHtmlContentSupported,\n            },\n        ],\n        normalize_handlers: withSequence(-1, this.normalize.bind(this)),\n        powerbox_categories: { id: \"ai_prompt_tools\", name: _t(\"AI Prompt Tools\") },\n        powerbox_items: { categoryId: \"ai_prompt_tools\", commandId: \"openAIFieldSelector\" },\n        selectors_for_feff_providers: () => AI_FIELD_SELECTOR,\n    };\n\n    setup() {\n        /** @type {import(\"@html_editor/core/overlay_plugin\").Overlay} */\n        this.overlay = this.dependencies.overlay.createOverlay(AiModelFieldSelectorPopover, {\n            hasAutofocus: true,\n            className: \"popover\",\n        });\n    }\n\n    normalize(element) {\n        // make sure fields are always protected (could be added without this plugin)\n        for (const fieldEl of element.querySelectorAll(AI_FIELD_SELECTOR)) {\n            fieldEl.dataset.oeProtected = true;\n        }\n    }\n\n    open(fieldsPath) {\n        this.overlay.open({\n            props: {\n                close: this.close.bind(this),\n                resModel: this.config.fieldSelectorResModel,\n                aiFieldPath: this.config.aiFieldPath,\n                followRelations: true,\n                isDebugMode: this.config.debug,\n                showSearchInput: true,\n                update: (path, field) => {},\n                updateBatch: (fieldsInfo) => this.insert(fieldsInfo),\n                fieldsPath: fieldsPath,\n            },\n        });\n    }\n\n    close() {\n        this.overlay.close();\n        this.dependencies.selection.focusEditable();\n    }\n\n    /**\n     * Insert the given fields as <span data-ai-field=\"path\">name</span>\n     *\n     * @param {Array} fieldsInfo: list of field to insert\n     */\n    insert(fieldsInfo) {\n        if (!fieldsInfo) {\n            return;\n        }\n\n        for (const fieldInfo of fieldsInfo) {\n            const span = document.createElement(\"span\");\n            const fieldChain = fieldInfo.map((field) => field.name).join(\".\");\n            span.dataset.aiField = fieldChain;\n            span.innerText = fieldInfo.map((field) => field.string).join(\" > \");\n            this.dependencies.dom.insert(span);\n            this.dependencies.dom.insert(fieldInfo === fieldsInfo.at(-1) ? \"\\u00A0\" : \", \");\n        }\n        this.dependencies.history.addStep();\n    }\n}\n", "import { AI_FIELD_SELECTOR, AIFieldSelectorPlugin } from \"@ai/ai_prompt/ai_field_selector_plugin\";\nimport {\n    AI_RECORD_SELECTOR,\n    AIRecordsSelectorPlugin,\n} from \"@ai/ai_prompt/ai_records_selector_plugin\";\nimport { FeffPlugin } from \"@html_editor/main/feff_plugin\";\nimport { HintPlugin } from \"@html_editor/main/hint_plugin\";\nimport { PlaceholderPlugin } from \"@html_editor/main/placeholder_plugin\";\nimport { PowerboxPlugin } from \"@html_editor/main/powerbox/powerbox_plugin\";\nimport { SearchPowerboxPlugin } from \"@html_editor/main/powerbox/search_powerbox_plugin\";\nimport { CORE_PLUGINS } from \"@html_editor/plugin_sets\";\nimport { childNodeIndex } from \"@html_editor/utils/position\";\nimport { withSequence } from \"@html_editor/utils/resource\";\nimport { Wysiwyg } from \"@html_editor/wysiwyg\";\nimport { Dialog } from \"@web/core/dialog/dialog\";\nimport { localization } from \"@web/core/l10n/localization\";\nimport { isHtmlEmpty } from \"@web/core/utils/html\";\n\nimport { Component, markup, onWillUpdateProps, useState } from \"@odoo/owl\";\n\nexport class AiPrompt extends Component {\n    static template = \"ai.AiPrompt\";\n    static components = { Wysiwyg };\n    static props = {\n        comodel: { type: String, optional: true },\n        domain: { type: String, optional: true },\n        model: { type: String, optional: true },\n        onChange: { type: Function, optional: true },\n        placeholder: { type: String, optional: true },\n        prompt: { type: String },\n        readonly: { type: Boolean, optional: true },\n        aiFieldPath: { type: String, optional: true },\n        missingRecordsWarning: { type: String, optional: true },\n        updatePrompt: { type: Function, optional: true },\n    };\n\n    setup() {\n        super.setup();\n        this.state = useState({\n            key: 0,\n            hasRecords: this.props.prompt.includes(\"data-ai-record\"),\n        });\n        this.lastValue = this.props.prompt;\n\n        onWillUpdateProps((newProps) => {\n            if ((newProps.prompt || \"\").toString() !== (this.lastValue || \"\").toString()) {\n                this.lastValue = newProps.prompt;\n                this.state.key++;\n            } else if (\n                newProps.comodel !== this.props.comodel ||\n                newProps.domain !== this.props.domain\n            ) {\n                this.state.key++;\n            }\n        });\n    }\n\n    get content() {\n        const elContent = this.editor.getElContent();\n        if (isHtmlEmpty(elContent.innerText)) {\n            return \"\";\n        }\n        return elContent.innerHTML;\n    }\n\n    get hasRecords() {\n        return Boolean(this.editor.getElContent().querySelector(AI_RECORD_SELECTOR));\n    }\n\n    get missingRecordsWarning() {\n        return this.props.comodel && !this.state.hasRecords && this.props.missingRecordsWarning;\n    }\n\n    get value() {\n        return markup(this.props.prompt || \"<p><br></p>\");\n    }\n\n    getConfig() {\n        return {\n            content: this.value,\n            debug: !!this.env.debug,\n            direction: localization.direction || \"ltr\",\n            aiFieldPath: this.props.aiFieldPath,\n            fieldSelectorResModel: this.props.model,\n            getRecordInfo: () => {\n                const { resModel, resId } = this.props.record;\n                return { resModel, resId };\n            },\n            onChange: () => this.onChange(),\n            onEditorReady: () => (this.state.hasRecords = this.hasRecords),\n            placeholder: this.props.placeholder,\n            Plugins: [\n                ...CORE_PLUGINS,\n                AIFieldSelectorPlugin,\n                AIRecordsSelectorPlugin,\n                FeffPlugin,\n                HintPlugin,\n                PlaceholderPlugin,\n                PowerboxPlugin,\n                SearchPowerboxPlugin,\n            ],\n            baseContainers: [\"DIV\"],\n            recordsSelectorDomain: this.props.domain,\n            recordsSelectorResModel: this.props.comodel,\n            // small hack to continue to show the placeholder when the widget is focused but empty\n            resources: {\n                hints: [\n                    withSequence(20, {\n                        selector: \".odoo-editor-editable > p:only-child\",\n                        text: this.props.placeholder,\n                    }),\n                ],\n            },\n        };\n    }\n\n    onBlur() {\n        const content = this.content;\n        if (content !== this.lastValue) {\n            this.props.updatePrompt(this.content);\n            this.lastValue = content;\n        }\n    }\n\n    onChange() {\n        this.state.hasRecords = this.hasRecords;\n        this.props.onChange?.();\n    }\n\n    onClick(ev) {\n        ev.preventDefault();\n        ev.stopPropagation();\n        const target = ev.target?.closest(`${AI_FIELD_SELECTOR}, ${AI_RECORD_SELECTOR}`);\n        if (!target) {\n            return;\n        }\n        // select the target to remove it when we will insert\n        this.editor.shared.selection.setSelection({\n            anchorNode: target.parentElement,\n            anchorOffset: childNodeIndex(target),\n            focusOffset: childNodeIndex(target) + 1,\n        });\n        if (target.matches(AI_FIELD_SELECTOR)) {\n            this.editor.shared.AIFieldSelector.open([target.dataset.aiField]);\n        } else if (this.props.comodel) {\n            this.editor.shared.AIRecordsSelector.open([Number(target.dataset.aiRecordId)]);\n        }\n    }\n\n    onEditorLoad(editor) {\n        this.editor = editor;\n    }\n}\n\nexport class AiPromptDialog extends Component {\n    static template = \"ai.AiPromptDialog\";\n    static components = { Dialog, AiPrompt };\n    static props = {\n        aiPromptProps: { type: Object },\n        close: { type: Function },\n        confirm: { type: Function },\n    };\n\n    setup() {\n        super.setup();\n        this.confirmVals = { prompt: this.props.aiPromptProps.prompt };\n    }\n\n    get aiPromptProps() {\n        return {\n            ...this.props.aiPromptProps,\n            updatePrompt: (prompt) => (this.confirmVals.prompt = prompt),\n        };\n    }\n\n    confirm() {\n        this.props.confirm(this.confirmVals.prompt);\n        this.props.close();\n    }\n}\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { AiPrompt } from \"./ai_prompt\";\nimport { standardFieldProps } from \"@web/views/fields/standard_field_props\";\nimport { Component } from \"@odoo/owl\";\n\nexport class AiPromptField extends Component {\n    static template = \"ai.AiPromptField\";\n    static props = {\n        ...standardFieldProps,\n        // Field containing the model name that will be updated\n        modelReferenceField: { type: String },\n        // Field containing the field name that will be updated\n        fieldReferenceField: { type: String, optional: true },\n\n        placeholder: { type: String, optional: true },\n\n        // If we update a relational field, field containing the model for which we will choose candidate values\n        recordSelectorRelationField: { type: String, optional: true },\n        recordSelectorDomain: { type: String, optional: true },\n\n        showMissingRecordsWarning: { Boolean: String, optional: true },\n    };\n    static components = { AiPrompt };\n\n    get updatedFieldName() {\n        return this.props.record.data[this.props.fieldReferenceField];\n    }\n\n    get recordSelectorRelation() {\n        return this.props.record.data[this.props.recordSelectorRelationField];\n    }\n\n    get missingRecordsMessage() {\n        return _t(\"Insert records the AI can use with the '/record' command\");\n    }\n\n    get modelName() {\n        return this.props.record.data[this.props.modelReferenceField];\n    }\n\n    get prompt() {\n        return this.props.record.data[this.props.name] || \"\";\n    }\n\n    onChange() {\n        this.props.record.model.bus.trigger(\"FIELD_IS_DIRTY\", true);\n    }\n\n    updatePrompt(value) {\n        if (value !== this.prompt) {\n            this.props.record.update({ [this.props.name]: value });\n        }\n    }\n}\n\nexport const aiPrompt = {\n    component: AiPromptField,\n    supportedTypes: [\"html\"],\n    additionalClasses: [\"d-inline\"],\n\n    extractProps: ({ attrs, options }) => {\n        return {\n            placeholder: attrs.placeholder,\n            modelReferenceField: options.model_reference_field,\n            fieldReferenceField: options.field_reference_field,\n            recordSelectorRelationField: options.record_selector_relation_field,\n            recordSelectorDomain: options.record_selector_domain,\n            showMissingRecordsWarning: Boolean(options.show_missing_records_warning),\n        };\n    },\n};\n\nregistry.category(\"fields\").add(\"ai_prompt\", aiPrompt);\n", "import { RecordsSelectorPopover } from \"@ai/records_selector_popover/records_selector_popover\";\nimport { Plugin } from \"@html_editor/plugin\";\nimport { withSequence } from \"@html_editor/utils/resource\";\nimport { Domain } from \"@web/core/domain\";\nimport { ERROR_INACCESSIBLE_OR_MISSING } from \"@web/core/name_service\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { isHtmlContentSupported } from \"@html_editor/core/selection_plugin\";\n\nexport const AI_RECORD_SELECTOR = \"span[data-ai-record-id]\";\n\nexport class AIRecordsSelectorPlugin extends Plugin {\n    static id = \"AIRecordsSelector\";\n    static dependencies = [\"overlay\", \"selection\", \"history\", \"dom\"];\n    static shared = [\"open\", \"updateDisplayNames\"];\n    resources = {\n        user_commands: [\n            {\n                id: \"openAIRecordsSelector\",\n                title: _t(\"Records Selector\"),\n                description: _t(\"Insert records\"),\n                icon: \"fa-tasks\",\n                run: () => this.open(),\n                isAvailable: (selection) =>\n                    !!this.config.recordsSelectorResModel && isHtmlContentSupported(selection),\n            },\n        ],\n        normalize_handlers: withSequence(-1, this.normalize.bind(this)),\n        powerbox_items: { categoryId: \"ai_prompt_tools\", commandId: \"openAIRecordsSelector\" },\n        selectors_for_feff_providers: () => AI_RECORD_SELECTOR,\n        start_edition_handlers: this.updateDisplayNames.bind(this),\n    };\n\n    setup() {\n        /** @type {import(\"@html_editor/core/overlay_plugin\").Overlay} */\n        this.overlay = this.dependencies.overlay.createOverlay(RecordsSelectorPopover, {\n            hasAutofocus: true,\n            className: \"popover\",\n        });\n    }\n\n    normalize(element) {\n        // make sure records are always protected (could be added without this plugin)\n        for (const recordEl of element.querySelectorAll(AI_RECORD_SELECTOR)) {\n            recordEl.dataset.oeProtected = true;\n        }\n    }\n\n    async updateDisplayNames() {\n        const recordEls = this.editable.querySelectorAll(AI_RECORD_SELECTOR);\n        if (recordEls.length === 0) {\n            return;\n        }\n        if (!this.config.recordsSelectorResModel) {\n            for (const recordEl of recordEls) {\n                recordEl.innerText = _t(\"Invalid Record\");\n            }\n            return;\n        }\n        // display names might have been updated, making the prompt incoherent (because references\n        // to these records in the prompt were not updated). They are therefore updated so that the\n        // user can observe that the prompt needs to be reworked.\n        const recordIds = [...recordEls].map((el) => Number(el.dataset.aiRecordId));\n        const displayNames = await this.services.name.loadDisplayNames(\n            this.config.recordsSelectorResModel,\n            recordIds,\n        );\n        for (const recordEl of recordEls) {\n            if (displayNames[recordEl.dataset.aiRecordId] === ERROR_INACCESSIBLE_OR_MISSING) {\n                recordEl.innerText = _t(\"Invalid Record\");\n            } else if (recordEl.innerText !== displayNames[recordEl.dataset.aiRecordId]) {\n                recordEl.innerText = displayNames[recordEl.dataset.aiRecordId];\n            }\n        }\n    }\n\n    open(resIds = []) {\n        this.overlay.open({\n            props: {\n                close: this.close.bind(this),\n                domain: new Domain(this.config.recordsSelectorDomain || \"[]\").toList(),\n                resModel: this.config.recordsSelectorResModel,\n                validate: (resIds) => this.insert(resIds),\n                resIds: resIds,\n            },\n        });\n    }\n\n    close() {\n        this.overlay.close();\n        this.dependencies.selection.focusEditable();\n    }\n\n    async insert(resIds) {\n        if (!resIds?.length) {\n            return;\n        }\n        const displayNames = await this.services.name.loadDisplayNames(\n            this.config.recordsSelectorResModel,\n            resIds,\n        );\n\n        for (const resId of resIds) {\n            const span = document.createElement(\"span\");\n            span.dataset.aiRecordId = resId;\n            span.innerText = displayNames[resId];\n            this.dependencies.dom.insert(span);\n            this.dependencies.dom.insert(resId === resIds.at(-1) ? \"\\u00A0\" : \", \");\n        }\n        this.dependencies.history.addStep();\n    }\n}\n", "/** @odoo-module **/\n\nimport { Component, useRef, useState } from \"@odoo/owl\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { Dialog } from \"@web/core/dialog/dialog\";\nimport { getDataURLFromFile } from \"@web/core/utils/urls\";\n\nimport { registry } from \"@web/core/registry\";\nimport { _t } from \"@web/core/l10n/translation\";\n\n\nclass AgentSourceCard extends Component {\n    static template = \"ai.AgentSourceCard\";\n    static props = {\n        icon: String,\n        image: { type: String, optional: true },\n        title: String,\n        onClick:  { type: Function },\n    };\n}\n\n\nclass AgentSourceURLDialog extends Component {\n    static template = \"ai.AgentSourceURLDialog\";\n    static components = {\n        Dialog,\n    };\n\n    static props = {\n        addURLSources:  { type: Function },\n        close:  { type: Function },\n    };\n\n    setup() {\n        super.setup();\n        this.state = useState({\n            urls: \"\",\n        });\n    }\n\n    onConfirm() {\n        this.props.addURLSources(this.state.urls);\n        this.props.close();\n    }\n}\n\n\nexport class AgentSourceAddDialog extends Component {\n    static template = \"ai.AgentSourceAddDialog\";\n    static props = {\n        agentId: Number,\n        close:  { type: Function },\n    };\n    static components = { AgentSourceCard, Dialog };\n\n    setup() {\n        this.agentId = this.props.agentId;\n        this.orm = useService(\"orm\");\n        this.notification = useService(\"notification\");\n        this.fileInputRef = useRef(\"fileInput\");\n        this.actionService = useService(\"action\");\n        this.dialog = useService(\"dialog\");\n        this.state = useState({\n            loading: false,\n        })\n    }\n\n    onAddFileSourceClick() {\n        this.fileInputRef.el.click();\n    }\n\n    validateFileTypes(files) {\n        const allowedExtensions = ['.pdf', '.docx', '.doc', '.pptx', '.ppt', '.xlsx', '.xls', '.odt', '.ods', '.txt', '.csv'];\n        const validFiles = [];\n        const invalidFiles = [];\n        for (const file of files) {\n            const fileExtension = '.' + file.name.split('.').pop().toLowerCase();\n            if (allowedExtensions.includes(fileExtension)) {\n                validFiles.push(file);\n            } else {\n                invalidFiles.push(file);\n            }\n        }\n\n        return { validFiles, invalidFiles };\n    }\n\n    async addAttachmentSources(ev) {\n        const files = ev.target.files;\n        if (!files || !files.length) {\n            return;\n        }\n\n        // Validate file types\n        const { validFiles, invalidFiles } = this.validateFileTypes(Array.from(files));\n        if (invalidFiles.length > 0 && validFiles.length > 0) {\n            const invalidFileNames = invalidFiles.map(file => file.name).join(', ');\n            this.notification.add(\n                _t(\"Some files have invalid formats and were skipped: %s. Only PDF, Word, PowerPoint, Excel, Text, CSV, and OpenDocument files are allowed.\", invalidFileNames),\n                {\n                    type: \"warning\",\n                }\n            );\n        }\n\n        if (validFiles.length === 0) {\n            this.notification.add(\n                _t(\"No valid files to upload. Only PDF, Word, PowerPoint, Excel, Text, CSV, and OpenDocument files are allowed.\"),\n                {\n                    type: \"danger\",\n                }\n            );\n\n            ev.target.value = '';\n            return;\n        }\n\n        this.state.loading = true;\n        const files_list = await Promise.all(\n            validFiles.map(async (file) => ({\n                name: file.name,\n                datas: (await getDataURLFromFile(file)).split(\",\")[1],\n            }))\n        );\n        const files_datas = []\n        for (const attachment_data of files_list) {\n            files_datas.push({\n                name: attachment_data.name,\n                datas: attachment_data.datas,\n            });\n        }\n        await this.orm.call(\"ai.agent.source\", \"create_from_binary_files\", [files_datas, this.agentId]);\n        this.state.loading = false;\n        ev.target.value = '';\n        return this.actionService.doAction({type: \"ir.actions.client\", tag: \"soft_reload\"});\n    }\n\n    onAddLinkSourceClick() {\n        this.dialog.add(AgentSourceURLDialog, {\n            addURLSources: async (urls_string) => await this.addURLSources(urls_string),\n        });\n    }\n\n    validateAndFilterURLs(urls_list) {\n        const validUrls = [];\n        const invalidUrls = [];\n        for (const url of urls_list) {\n            if (url && (url.startsWith('https://') || url.startsWith('http://') || url.startsWith('ftp://'))) {\n                validUrls.push(url);\n            } else {\n                invalidUrls.push(url);\n            }\n        }\n        return { validUrls, invalidUrls };\n    }\n\n    async addURLSources(urls_string) {\n        if (!urls_string) {\n            return;\n        }\n        const urls_list = [...new Set(\n            urls_string\n            .split(\"\\n\")\n            .map(url => url.trim())\n            .filter(url => url !== \"\")\n        )];\n\n        if (urls_list.length > 0) {\n            const { validUrls, invalidUrls } = this.validateAndFilterURLs(urls_list);\n            if (validUrls.length > 0) {\n                this.state.loading = true;\n                await this.orm.call(\"ai.agent.source\", \"create_from_urls\", [validUrls, this.agentId]);\n                if (invalidUrls.length > 0) {\n                    this.notification.add(\n                        _t(\"Some URLs are invalid and were skipped. URLs must start with http://, https://, or ftp://\"),\n                        {\n                            type: \"warning\",\n                        }\n                    );\n                }\n            } else {\n                this.notification.add(\n                    _t(\"No valid URLs found to process.\"),\n                    {\n                        type: \"danger\",\n                    }\n                );\n            }\n\n            this.state.loading = false;\n            return this.actionService.doAction({type: \"ir.actions.client\", tag: \"soft_reload\"});\n        }\n    }\n\n    get cardsData() {\n        return [\n            {\n                icon: \"fa-upload\",\n                title: _t(\"Upload a File\"),\n                onClick: () => this.onAddFileSourceClick(),\n            },\n            {\n                icon: \"fa-link\",\n                title: _t(\"Add a Link\"),\n                onClick: () => this.onAddLinkSourceClick(),\n            },\n        ];\n    }\n}\n\nregistry.category(\"actions\").add(\"ai_open_sources_dialog\", (env, action) => {\n    const params = action.params || {};\n    env.services.dialog.add(AgentSourceAddDialog,{\n        agentId: params.agent_id,\n    });\n});\n", "import { BannerPlugin } from \"@html_editor/main/banner_plugin\";\nimport { closestElement } from \"@html_editor/utils/dom_traversal\";\nimport { patch } from \"@web/core/utils/patch\";\n\npatch(BannerPlugin.prototype, {\n    onBannerEmojiChange(iconElement) {\n        if (closestElement(iconElement, \".o_editor_prompt\")) {\n            return;\n        }\n        return super.onBannerEmojiChange(iconElement);\n    },\n});\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { Plugin } from \"@html_editor/plugin\";\nimport { DYNAMIC_PLACEHOLDER_PLUGINS } from \"@html_editor/backend/plugin_sets\";\nimport { isHtmlContentSupported } from \"@html_editor/core/selection_plugin\";\n\nexport class PromptPlugin extends Plugin {\n    static id = \"prompt\";\n    static dependencies = [\"banner\"];\n    resources = {\n        user_commands: [\n            {\n                id: \"prompt\",\n                title: _t(\"Prompt\"),\n                description: _t(\"Insert an AI prompt\"),\n                icon: \"fa-bolt\",\n                run: () => {\n                    this.dependencies.banner.insertBanner(\n                        _t(\"Prompt\"),\n                        \"\u26a1\",\n                        \"primary\",\n                        \"o_editor_prompt\",\n                        \"o_editor_prompt_content\"\n                    );\n                },\n                isAvailable: isHtmlContentSupported,\n            },\n        ],\n        powerbox_items: [\n            {\n                commandId: \"prompt\",\n                categoryId: \"ai\",\n            },\n        ],\n    };\n}\n\nDYNAMIC_PLACEHOLDER_PLUGINS.push(PromptPlugin);\n", "import { UserCommandPlugin } from \"@html_editor/core/user_command_plugin\";\nimport { closestElement } from \"@html_editor/utils/dom_traversal\";\nimport { patch } from \"@web/core/utils/patch\";\n\npatch(UserCommandPlugin.prototype, {\n    getCommand(commandId) {\n        const command = super.getCommand(commandId);\n        return {\n            ...command,\n            isAvailable: (selection) => {\n                if (closestElement(selection.anchorNode, \".o_editor_prompt\")) {\n                    // Only the \"openDynamicPlaceholder\" command is available inside a prompt.\n                    return commandId === \"openDynamicPlaceholder\";\n                }\n                return !command.isAvailable || command.isAvailable(selection);\n            },\n        };\n    },\n});\n", "import { Component, onWillStart } from \"@odoo/owl\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\n\nexport class AskAIButton extends Component {\n    static template = \"ai.AskAIButton\";\n    static components = { DropdownItem };\n    static props = {};\n\n    setup() {\n        this.orm = useService(\"orm\");\n        this.action = useService(\"action\");\n        onWillStart(async () => {\n            const agent = await this.orm.cache().call(\"ai.agent\", \"get_ask_ai_agent\", []);\n            this.hasAIAgent = Boolean(agent);\n        });\n    }\n    async onAskAIClick() {\n        const action = await this.env.services.orm.call(\"ai.agent\", \"action_ask_ai\", [\"\"]);\n        if (action) {\n            // Don't await so that the command palette can close immediately\n            this.action.doAction(action);\n        }\n    }\n}\n", "import { Component, markRaw } from \"@odoo/owl\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { imageUrl } from \"@web/core/utils/urls\";\nimport { DefaultCommandItem, CommandPalette } from \"@web/core/commands/command_palette\";\nimport { patch } from \"@web/core/utils/patch\";\nimport { highlightText } from \"@web/core/utils/html\";\n\nconst commandProviderRegistry = registry.category(\"command_provider\");\n\nclass AskAICommand extends Component {\n    static template = \"ai.AskAICommand\";\n    static props = {\n        imgUrl: String,\n        ...DefaultCommandItem.props,\n    };\n}\n\nasync function askAIProvide(env, options) {\n    const orm = env.services.orm;\n    const actions = env.services.action;\n    const agent = await orm.cache().call(\"ai.agent\", \"get_ask_ai_agent\", []);\n    if (!agent) {\n        return [];\n    }\n    return [\n        {\n            action: async () => {\n                const action = await orm.call(\"ai.agent\", \"action_ask_ai\", [options.searchValue]);\n                if (action) {\n                    // Don't await so that the command palette can close immediately\n                    actions.doAction(action);\n                }\n            },\n            category: \"app\",\n            Component: AskAICommand,\n            props: {\n                imgUrl: agent\n                    ? imageUrl(\"ai.agent\", agent.id, \"image_128\")\n                    : \"/ai/static/description/icon.png\",\n            },\n            name: _t(\"Ask AI\"),\n        },\n    ];\n}\n\ncommandProviderRegistry.add(\"ask_ai\", {\n    namespace: \"/\",\n    async provide(env, options) {\n        return askAIProvide(env, options);\n    },\n});\n\n// TODO: Add a unit test for this. The Ask AI command should be available in the default\n// namespace (CTRL+K) when no commands are found.\npatch(CommandPalette.prototype, {\n    async setCommands(namespace, options = {}) {\n        const [askAICommand] = await askAIProvide(this.env, options);\n        const result = await super.setCommands(namespace, options);\n        if (\n            askAICommand &&\n            namespace === \"default\" &&\n            this.state.commands.length === 0 &&\n            options.searchValue\n        ) {\n            this.state.commands = markRaw([\n                {\n                    ...askAICommand,\n                    keyId: this.keyId++,\n                    text: highlightText(\n                        options.searchValue,\n                        askAICommand.name,\n                        \"fw-bolder text-primary\"\n                    ),\n                },\n            ]);\n            this.selectCommand(this.state.commands.length ? 0 : -1);\n        }\n        return result;\n    },\n});\n", "import { patch } from \"@web/core/utils/patch\";\nimport { SearchBarMenu } from \"@web/search/search_bar_menu/search_bar_menu\";\nimport { AskAIButton } from \"@ai/core/web/ask_ai_button\";\n\npatch(SearchBarMenu, {\n    components: { ...SearchBarMenu.components, AskAIButton },\n});\n", "import { SearchBar } from \"@web/search/search_bar/search_bar\";\nimport { patch } from \"@web/core/utils/patch\";\nimport { useBus } from \"@web/core/utils/hooks\";\n\nfunction isDefined(value) {\n    return value !== undefined && value !== null;\n}\n\npatch(SearchBar.prototype, {\n    setup() {\n        super.setup(...arguments);\n        useBus(this.env.bus, \"APPLY_AI_ADJUST_SEARCH\", async ({ detail }) => {\n            const searchModel = this.env.searchModel;\n            for (const facetGroupId of detail.removeFacets) {\n                searchModel.deactivateGroup(facetGroupId);\n            }\n            const {\n                toggleFilters,\n                toggleGroupBys,\n                applySearches,\n                customDomain,\n                measures,\n                mode,\n                order,\n                stacked,\n                cumulated,\n            } = detail;\n            await searchModel.applyAISearch({\n                filters: toggleFilters,\n                groupBys: toggleGroupBys,\n                fieldSearches: applySearches,\n                customDomain,\n            });\n            if (\n                (measures && measures.length) ||\n                [mode, order, stacked, cumulated].some(isDefined)\n            ) {\n                this.env.bus.trigger(\"APPLY_AI_ADJUST_MODEL\", {\n                    measures: measures ?? [],\n                    mode,\n                    order,\n                    stacked,\n                    cumulated,\n                });\n            }\n            const chatboxInput = document.querySelector(\".o-mail-Composer-input\");\n            if (chatboxInput) {\n                chatboxInput.focus();\n            }\n        });\n    },\n});\n", "import { SearchModel } from \"@web/search/search_model\";\nimport { patch } from \"@web/core/utils/patch\";\nimport { GROUPABLE_TYPES } from \"@web/search/utils/misc\";\n\nconst CHAR_FIELDS = [\"char\", \"html\", \"many2many\", \"many2one\", \"one2many\", \"text\", \"properties\"];\n\npatch(SearchModel.prototype, {\n    validateField(fieldName, field) {\n        const { groupable, type } = field;\n        return groupable && fieldName !== \"id\" && GROUPABLE_TYPES.includes(type);\n    },\n    async applyAISearch({ filters, groupBys, fieldSearches, customDomain }) {\n        // TODO JCB: support dateGroupBy and fieldProperty\n        // TODO JCB: name vs fieldName, which one to use?\n        for (const filter of filters || []) {\n            const [searchItem] = this.getSearchItems(\n                (i) => i.type === \"filter\" && [i.name, i.fieldName].includes(filter)\n            );\n            if (searchItem && !searchItem?.isActive) {\n                this.toggleSearchItem(searchItem.id);\n            }\n        }\n        for (const groupBy of groupBys || []) {\n            const [searchItem] = this.getSearchItems(\n                (i) => i.type === \"groupBy\" && [i.name, i.fieldName].includes(groupBy)\n            );\n\n            if (searchItem && !searchItem?.isActive) {\n                this.toggleSearchItem(searchItem.id);\n            } else if (!searchItem) {\n                const field = this.searchViewFields[groupBy];\n                if (!field || !this.validateField(groupBy, field)) {\n                    continue;\n                }\n                this.createNewGroupBy(groupBy);\n            }\n        }\n        for (const searchString of fieldSearches || []) {\n            const separatorIndex = searchString.indexOf(\"=\");\n            if (separatorIndex === -1) {\n                console.warn(\n                    `Invalid search format: \"${searchString}\". Expected format: \"field=text\"`\n                );\n                continue;\n            }\n            const fieldName = searchString.substring(0, separatorIndex);\n            const value = searchString.substring(separatorIndex + 1);\n\n            const [searchItem] = this.getSearchItems(\n                (i) => i.type === \"field\" && i.fieldName === fieldName\n            );\n            if (searchItem) {\n                this.addAutoCompletionValues(searchItem.id, {\n                    value,\n                    label: value,\n                    operator:\n                        searchItem.operator ||\n                        (CHAR_FIELDS.includes(searchItem.fieldType) ? \"ilike\" : \"=\"),\n                });\n            }\n        }\n        if (customDomain && customDomain.length) {\n            await this.splitAndAddDomain(customDomain);\n        }\n    },\n    async load(config) {\n        const result = await super.load(config);\n        if (config.ai) {\n            await this.applyAISearch({\n                filters: config.ai.selectedFilters,\n                groupBys: config.ai.selectedGroupBys,\n                fieldSearches: config.ai.search,\n                customDomain: config.ai.customDomain,\n            });\n        }\n        return result;\n    },\n});\n", "import { patch } from \"@web/core/utils/patch\";\nimport { View } from \"@web/views/view\";\nimport { useSubEnv } from \"@odoo/owl\";\n\npatch(View.prototype, {\n    setup(...args) {\n        super.setup(...args);\n        useSubEnv({\n            config: {\n                ...this.env.config,\n                disableSearchBarAutofocus: this.props.ai\n                    ? true\n                    : this.env.config.disableSearchBarAutofocus,\n            },\n        });\n    },\n    async loadView(props) {\n        await super.loadView(props);\n        if (\"ai\" in this.componentProps) {\n            const aiProps = this.componentProps.ai;\n            if (this.props.type === \"pivot\") {\n                if (aiProps.measures && aiProps.measures.length > 0) {\n                    this.componentProps.modelParams.metaData.activeMeasures = aiProps.measures;\n                }\n                if (aiProps.colGroupBys && aiProps.colGroupBys.length > 0) {\n                    this.componentProps.modelParams.metaData.colGroupBys = aiProps.colGroupBys;\n                }\n                if (aiProps.sortedColumn) {\n                    // Set up sorted column with empty groupId for total column sorting\n                    this.componentProps.modelParams.metaData.sortedColumn = {\n                        groupId: [[], []], // Empty groupId means sort by total column\n                        measure: aiProps.sortedColumn.measure,\n                        order: aiProps.sortedColumn.order,\n                    };\n                }\n            } else if (this.props.type === \"graph\") {\n                if (aiProps.measure) {\n                    this.componentProps.modelParams.measure = aiProps.measure;\n                }\n                if (aiProps.mode) {\n                    this.componentProps.modelParams.mode = aiProps.mode;\n                }\n                if (aiProps.order) {\n                    this.componentProps.modelParams.order = aiProps.order;\n                }\n                if (aiProps.stacked !== undefined) {\n                    this.componentProps.modelParams.stacked = aiProps.stacked;\n                }\n                if (aiProps.cumulated !== undefined) {\n                    this.componentProps.modelParams.cumulated = aiProps.cumulated;\n                }\n                if (aiProps.groupBys && aiProps.groupBys.length > 0) {\n                    this.componentProps.modelParams.groupBy = aiProps.groupBys;\n                }\n            }\n            // Remove ai from componentProps to avoid passing it to the component\n            delete this.componentProps.ai;\n        }\n    },\n});\n", "import { WithSearch } from \"@web/search/with_search/with_search\";\nimport { patch } from \"@web/core/utils/patch\";\nimport { useViewDetailsGetter } from \"@ai/discuss/core/common/view_details\";\n\npatch(WithSearch, {\n    props: {\n        ...WithSearch.props,\n        ai: { type: Object, optional: true },\n    },\n});\n\npatch(WithSearch.prototype, {\n    setup() {\n        super.setup(...arguments);\n        useViewDetailsGetter(() => this.getCurrentViewInfo());\n    },\n    async getCurrentViewInfo() {\n        const config = this.env.config;\n        const searchModel = this.env.searchModel;\n        const result = {};\n        // if in form view, no need to return anything\n        if (config.viewType === \"form\") {\n            return;\n        }\n        result.action_id = config.actionId;\n        result.view_id = config.viewId;\n        result.model = searchModel.resModel;\n        result.available_view_types = config.viewSwitcherEntries.map((v) => v.type);\n        result.view_type = config.viewType;\n        result.order_by = searchModel.orderBy;\n        result.facets = searchModel.facets;\n        return result;\n    },\n});\n", "import { fields, Record } from \"@mail/core/common/record\";\n\nexport class AIPromptButton extends Record {\n    static _name = \"ai.prompt.button\";\n    static id = \"id\";\n\n    thread_id = fields.One(\"Thread\", { inverse: \"ai_prompt_buttons\" });\n}\n\nAIPromptButton.register();\n", "import { ChatWindow } from \"@mail/core/common/chat_window\";\nimport { patch } from \"@web/core/utils/patch\";\n\n\npatch(ChatWindow.prototype, {\n    get attClass() {\n        return {\n            ...super.attClass,\n            \"o-isAiComposer\": this.thread?.channel_type === \"ai_chat\",\n        };\n    },\n});\n", "import { patch } from \"@web/core/utils/patch\";\nimport { ComposerAction } from \"@mail/core/common/composer_actions\";\n\npatch(ComposerAction.prototype, {\n    _condition({ composer }) {\n        const requiredActions = [\"send-message\"];\n        if (\n            composer.targetThread?.correspondent?.persona.im_status === \"agent\" &&\n            !requiredActions.includes(this.id)\n        ) {\n            return false;\n        }\n        return super._condition(...arguments);\n    },\n});\n", "import { Composer } from \"@mail/core/common/composer\";\n\nimport { patch } from \"@web/core/utils/patch\";\n\npatch(Composer.prototype, {\n    saveContent() {\n        if (this.thread?.channel_type === \"ai_chat\") {\n            return;  // no point in saving the content in an AI chat since chats are independent \n        }\n        super.saveContent();\n    },\n    onFocusin(ev) {\n        super.onFocusin();\n        if (this.thread?.channel_type === \"ai_chat\") {\n            ev.target.select();\n        }\n    },\n    get wysiwygConfig() {\n        const config = super.wysiwygConfig;\n        return {\n            ...config,\n            getRecordInfo: () => {\n                return {\n                    resModel: this.thread?.model,\n                    resId: this.thread?.id,\n                };\n            },\n        };\n    }\n});\n", "import { rpc } from \"@web/core/network/rpc\";\nimport { patch } from \"@web/core/utils/patch\";\nimport { ChatWindow } from \"@mail/core/common/chat_window_model\";\n\npatch(ChatWindow.prototype, {\n    computeCanShow() {\n        if (this.store.aiInsertButtonTarget && this.store.discuss.isActive) {\n            return this.thread?.channel_type === \"ai_chat\";\n        }\n        return super.computeCanShow();\n    },\n    async _onClose() {\n        const thread = this.thread;\n        if (thread?.ai_agent_id) {\n            await rpc(\"/ai/close_ai_chat\", { channel_id: thread.id });\n        }\n        await super._onClose(...arguments);\n    },\n});\n", "import { Thread } from \"@mail/core/common/thread_model\";\nimport { patch } from \"@web/core/utils/patch\";\nimport { rpc, RPCError } from \"@web/core/network/rpc\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { getCurrentViewInfo } from \"@ai/discuss/core/common/view_details\";\nimport { session } from \"@web/session\";\n\npatch(Thread.prototype, {\n    async post(body, postData = {}, extraData = {}) {\n        const message = await super.post(body, postData, extraData);\n        const aiMember = this.channel_member_ids?.find(\n            (member) => member.partner_id?.im_status == \"agent\"\n        );\n        // message could be undefined if it is a command, for example /help.\n        if (message?.thread?.ai_agent_id) {\n            try {\n                if (aiMember) {\n                    aiMember.isTyping = true;\n                }\n                await rpc(\"/ai/generate_response\", {\n                    mail_message_id: message.id,\n                    channel_id: this.id,\n                    current_view_info: await getCurrentViewInfo(this.store.env.bus),\n                    ai_session_identifier: session.ai_session_identifier,\n                });\n            } catch (error) {\n                if (error instanceof RPCError) {\n                    await rpc(\"/ai/post_error_message\", {\n                        error_message:\n                            error.data?.message ||\n                            _t(\"An error occurred while generating the AI response.\"),\n                        channel_id: this.id,\n                    });\n                } else {\n                    throw error;\n                }\n            } finally {\n                if (aiMember) {\n                    aiMember.isTyping = false;\n                }\n            }\n        }\n        return message;\n    },\n});\n", "import { useComponent } from \"@odoo/owl\";\nimport { useBus } from \"@web/core/utils/hooks\";\n\nexport async function getCurrentViewInfo(bus) {\n    return new Promise((resolve) => {\n        const listener = ({ detail }) => {\n            bus.removeEventListener(\"SEND_VIEW_DETAILS!\", listener);\n            clearTimeout(timeout);\n            resolve(detail);\n        };\n        const timeout = setTimeout(() => {\n            bus.removeEventListener(\"SEND_VIEW_DETAILS!\", listener);\n            resolve(null);\n        }, 100); // Timeout to avoid waiting indefinitely\n        bus.addEventListener(\"SEND_VIEW_DETAILS!\", listener);\n        bus.trigger(\"REQUEST_VIEW_DETAILS?\");\n    });\n}\n\n/**\n * The component that can provide the view details should call this hook provided with the callback that can give the view details.\n * Once called, the `getCurrentViewInfo` can then return the current view information which is provided by the callback and is transmitted via the bus.\n */\nexport function useViewDetailsGetter(getter) {\n    const component = useComponent();\n    const bus = component.env.bus;\n    if (!bus) {\n        throw new Error(\"Bus is not available in the current environment.\");\n    }\n    useBus(bus, \"REQUEST_VIEW_DETAILS?\", async () => {\n        bus.trigger(\"SEND_VIEW_DETAILS!\", await getter());\n    });\n}\n", "import { patch } from \"@web/core/utils/patch\";\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registerMessageAction, MessageAction } from \"@mail/core/common/message_actions\";\nimport { unwrapContents } from \"@html_editor/utils/dom\";\nimport { setElementContent } from \"@web/core/utils/html\";\n\nregisterMessageAction(\"insertToComposer\", {\n    condition: ({ message, store, thread }) =>\n        !!thread?.aiSpecialActions?.insert &&\n        store.aiInsertButtonTarget && // after a reload both parts of the below conditions are undefined and but we don't want to button to appear\n        (store.aiInsertButtonTarget === thread.aiChatSource || store.env.isSmall) &&\n        !message.isSelfAuthored,\n    name: _t(\"Use this\"),\n    onSelected: ({ message, store, thread }) => {\n        const fragment = document.createDocumentFragment();\n        const content_root = document.createElement(\"span\");\n        content_root.setAttribute(\"InsertorId\", \"AIInsertion\");\n        setElementContent(content_root, message.body);\n        // check if the content is enclosed in a <p> element, if so, unwrap it\n        const paragraphElements = content_root.querySelectorAll(\"p\");\n        if (paragraphElements.length === 1) {\n            unwrapContents(paragraphElements[0]);\n        }\n        fragment.appendChild(content_root);\n        thread.aiSpecialActions.insert(fragment);\n        if (store.env.isSmall) {\n            thread.closeChatWindow();\n        }\n    },\n    sequence: 10,\n});\nregisterMessageAction(\"send-message-direct\", {\n    condition: ({ message, thread }) =>\n        !!thread?.aiSpecialActions?.sendMessage && !message.isSelfAuthored, // don't show the buttons for the user's messages,\n    name: _t(\"Send as Message\"),\n    onSelected: ({ message, thread }) => thread.aiSpecialActions.sendMessage(message.body),\n    sequence: 20,\n});\nregisterMessageAction(\"log-note-direct\", {\n    condition: ({ message, thread }) =>\n        !!thread?.aiSpecialActions?.logNote && !message.isSelfAuthored, // don't show the buttons for the user's messages\n    name: _t(\"Log as Note\"),\n    onSelected: ({ message, thread }) => thread.aiSpecialActions.logNote(message.body),\n    sequence: 25,\n});\n\npatch(MessageAction.prototype, {\n    _condition({ thread }) {\n        const requiredActions = [\n            \"insertToComposer\",\n            \"copy-message\",\n            \"send-message-direct\",\n            \"log-note-direct\",\n        ];\n        if (thread?.channel_type === \"ai_chat\") {\n            if (!requiredActions.includes(this.id)) {\n                return false;\n            }\n            if (this.id === \"copy-message\") {\n                return true;\n            }\n        }\n        return super._condition(...arguments);\n    },\n    _sequence({ thread }) {\n        if (this.id === \"copy-message\" && thread?.channel_type === \"ai_chat\") {\n            return 50;\n        }\n        return super._sequence(...arguments);\n    },\n});\n", "import { Message } from \"@mail/core/common/message\";\nimport { patch } from \"@web/core/utils/patch\";\n\npatch(Message.prototype, {\n    get quickActionCount() {\n        return this.props.thread?.channel_type === \"ai_chat\" ? 3 : super.quickActionCount;\n    },\n});\n", "import { SuggestionService } from \"@mail/core/common/suggestion_service\";\n\nimport { patch } from \"@web/core/utils/patch\";\n\npatch(SuggestionService.prototype, {\n    /** @override */\n    searchSuggestions({ delimiter, term }, { thread } = {}) {\n        if (\n            [\"ai_composer\", \"ai_chat\"].includes(thread?.channel_type) &&\n            [\"#\", \"@\"].includes(delimiter)\n        ) {\n            return {\n                type: undefined,\n                suggestions: [],\n            };\n        }\n        return super.searchSuggestions(...arguments);\n    },\n});\n", "import { patch } from \"@web/core/utils/patch\";\nimport { ThreadAction } from \"@mail/core/common/thread_actions\";\n\npatch(ThreadAction.prototype, {\n    _condition({ action, thread }) {\n        const requiredActions = [\"close\", \"fold-chat-window\", \"expand-discuss\"];\n        if (thread?.channel_type === \"ai_chat\" && !requiredActions.includes(action.id)) {\n            return false;\n        }\n        return super._condition(...arguments);\n    },\n});\n", "import { Thread } from \"@mail/core/common/thread_model\";\nimport { patch } from \"@web/core/utils/patch\";\nimport { fields } from \"@mail/core/common/record\";\nimport { browser } from \"@web/core/browser/browser\";\n\nconst AI_PROMPT_BUTTONS = \"ai.thread.prompt_buttons.\";\n\npatch(Thread.prototype, {\n    setup() {\n        super.setup();\n        this.ai_prompt_buttons = fields.Many(\"ai.prompt.button\", {\n            inverse: \"thread_id\",\n            compute() {\n                return JSON.parse(browser.localStorage.getItem(AI_PROMPT_BUTTONS.concat(this.id)));\n            },\n        });\n    },\n    async closeChatWindow(options = {}) {\n        await super.closeChatWindow(options);\n        browser.localStorage.removeItem(AI_PROMPT_BUTTONS.concat(this.id));\n    },\n    get avatarUrl() { \n        if (this.channel_type === \"ai_chat\" && this.correspondent) {\n            return this.correspondent.avatarUrl;\n        }\n\n        return super.avatarUrl;\n    },\n});\n", "import { Thread } from \"@mail/core/common/thread\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { patch } from \"@web/core/utils/patch\";\n\npatch(Thread.prototype, {\n    get showStartMessage() {\n        return super.showStartMessage || (this.props.thread.channel_type === \"ai_chat\");\n    },\n    get startMessageSubtitle() {\n        if (this.props.thread.channel_type === \"ai_chat\") {\n            return _t(\"Hello, what can I help you with?\");\n        } else {\n            return super.startMessageSubtitle;\n        }\n    },\n    onClickPromptButton(button) {\n        this.props.thread.post(button);\n    },\n    get showPromptButtons() {\n        return this.props.thread.messages.length === 0;\n    },\n});\n", "import { patch } from \"@web/core/utils/patch\";\nimport { Typing } from \"@mail/discuss/typing/common/typing\";\nimport { _t } from \"@web/core/l10n/translation\";\n\npatch(Typing.prototype, {\n    get text() {\n        const channel = this.props.channel;\n        const typingMembers = channel?.typingMembers || [];\n        if (typingMembers.length === 1 && typingMembers[0].partner_id?.im_status === \"agent\") {\n            return _t(\"AI is thinking...\");\n        }\n        return super.text;\n    },\n});\n", "import {\n    getEditableDescendants,\n    getEmbeddedProps,\n    useEditableDescendants,\n} from \"@html_editor/others/embedded_component_utils\";\nimport { Component, onMounted, onWillStart, useState } from \"@odoo/owl\";\n\nexport class ReadonlyVoiceTranscription extends Component {\n    static template = \"ai.ReadonlyVoiceTranscription\";\n    static components = {};\n    static props = {\n        host: { type: Object },\n    };\n\n    setup() {\n        this.descendants = useEditableDescendants(this.props.host);\n        this.supportedLanguages = [];\n        this.state = useState({\n            isOpened: true,\n            currentTab: \"notes\",\n        });\n\n        onWillStart(() => {\n            if (this.props.hasSummary) {\n                this.state.currentTab = \"summary\";\n            }\n        });\n\n        onMounted(() => {\n            const firstRecordingDate = this.props.host.querySelector(\"#transcript-content b\");\n            this.state.firstRecordingDate = firstRecordingDate?.textContent ?? \"\";\n        });\n    }\n\n    setCurrentTab(tabName) {\n        this.state[\"currentTab\"] = tabName;\n    }\n}\n\nexport const aiReadonlyVoiceTranscriptionEmbeddedComponent = {\n    name: \"voice-transcription\",\n    Component: ReadonlyVoiceTranscription,\n    getEditableDescendants,\n    getProps: (host) => ({ host, ...getEmbeddedProps(host) }),\n};\n", "import {\n    getEditableDescendants,\n    getEmbeddedProps,\n    StateChangeManager,\n    useEditableDescendants,\n    useEmbeddedState,\n} from \"@html_editor/others/embedded_component_utils\";\nimport { RPCError } from \"@web/core/network/rpc\";\nimport { Component, onMounted, onWillStart, useState } from \"@odoo/owl\";\nimport { user } from \"@web/core/user\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport VADAudioRecorder from \"@ai/vad_audio_recorder\";\nimport { _t } from \"@web/core/l10n/translation\";\n\nexport class VoiceTranscription extends Component {\n    static template = \"ai.VoiceTranscription\";\n    static components = {};\n    static props = {\n        host: { type: Object },\n        resModel: { type: String },\n        resId: { type: Number, optional: true },\n        firstRecordingDate: { type: Function },\n        getTabContent: { type: Function },\n        getTranscriptContent: { type: Function },\n        onTranscriptionStarted: { type: Function },\n        onTranscriptionUpdated: { type: Function },\n        onRecorderStopped: { type: Function },\n    };\n\n    static defaultProps = {\n        resId: null,\n    };\n\n    setup() {\n        this.descendants = useEditableDescendants(this.props.host);\n        this.embeddedState = useEmbeddedState(this.props.host);\n        this.actionService = useService(\"action\");\n        this.notificationService = useService(\"notification\");\n        this.orm = useService(\"orm\");\n        this.mailStore = useService(\"mail.store\");\n\n        this.supportedLanguages = [];\n\n        this.state = useState({\n            isOpened: true,\n            isRecording: false,\n            currentTab: \"notes\",\n            currentLanguage: user.lang.replace(\"-\", \"_\"),\n            status: \"idle\",\n        });\n\n        const onMessage = (data) => {\n            const eventType = data.type;\n            if (eventType === \"conversation.item.input_audio_transcription.delta\") {\n                const result = this.props.onTranscriptionUpdated(\n                    \"delta\",\n                    this.embeddedState.id,\n                    data.item_id,\n                    data.delta\n                );\n\n                if (result === null) {\n                    this.audioRecorder.stopRecording();\n                    return;\n                }\n            } else if (eventType === \"conversation.item.input_audio_transcription.completed\") {\n                const result = this.props.onTranscriptionUpdated(\n                    \"completed\",\n                    this.embeddedState.id,\n                    data.item_id,\n                    data.transcript\n                );\n                if (result === null) {\n                    this.audioRecorder.stopRecording();\n                    return;\n                }\n                this.props.onTranscriptionUpdated(\n                    \"listening\",\n                    this.embeddedState.id,\n                    null,\n                    _t(\"AI is listening...\")\n                );\n            }\n        };\n        this.audioRecorder = new VADAudioRecorder(onMessage);\n\n        onWillStart(async () => {\n            const languages = await this.orm.call(\"res.lang\", \"get_installed\", []);\n            this.supportedLanguages = languages.map(([code]) => ({\n                shortCode: code.split(\"_\")[0],\n                code,\n            }));\n\n            if (this.audioRecorder.state === \"recording\") {\n                this.embeddedState.status = \"recording\";\n            }\n\n            this.composerPrompts = (\n                await this.orm.webSearchRead(\n                    \"ai.composer\",\n                    [[\"interface_key\", \"=\", \"voice_transcription_component\"]],\n                    {\n                        specification: {\n                            ai_agent: {},\n                            default_prompt: {},\n                            available_prompts: {\n                                fields: {\n                                    name: {},\n                                },\n                            },\n                        },\n                    }\n                )\n            ).records[0];\n            if (this.embeddedState.hasSummary) {\n                this.state.currentTab = \"summary\";\n            }\n        });\n\n        onMounted(() => {\n            this.state.firstRecordingDate = this.props.firstRecordingDate(this.embeddedState.id);\n        });\n    }\n\n    setCurrentTab(tabName) {\n        if (tabName === \"summary\") {\n            const summaryContent = this.props.getTabContent(this.embeddedState.id, \"summary\");\n\n            if (summaryContent && summaryContent.innerText.trim() === \"\") {\n                this.updateSummary();\n            }\n        }\n\n        this.state[\"currentTab\"] = tabName;\n    }\n\n    onLanguageChange(event) {\n        this.state.currentLanguage = event.target.value;\n    }\n\n    async toggleRecording() {\n        this.state.isRecording = !this.state.isRecording;\n        if (this.embeddedState.status === \"idle\") {\n            const transcriptPrompt = this.props.getTabContent(this.embeddedState.id, \"notes\");\n            try {\n                this.embeddedState.status = \"waiting\";\n                await this.audioRecorder.startRecording(\n                    this.state.currentLanguage.split(\"_\")[0],\n                    transcriptPrompt?.innerText.trim()\n                );\n                this.embeddedState.status = \"recording\";\n                this.props.onTranscriptionStarted(\n                    this.embeddedState.id,\n                    this.state.currentLanguage.replace(\"_\", \"-\")\n                );\n                this.props.onTranscriptionUpdated(\n                    \"listening\",\n                    this.embeddedState.id,\n                    null,\n                    _t(\"AI is listening...\")\n                );\n                this.setCurrentTab(\"transcript\");\n            } catch (error) {\n                this.embeddedState.status = \"idle\";\n                this.state.isRecording = false;\n                if (error instanceof DOMException && error.name === \"NotAllowedError\") {\n                    this.notificationService.add(\n                        _t(\n                            \"You must allow the access to your microphone to start the recording. Try refreshing the page and start again.\"\n                        ),\n                        {\n                            title: _t(\"Access error\"),\n                            type: \"danger\",\n                        }\n                    );\n                } else if (\n                    error instanceof RPCError &&\n                    error.data.name === \"odoo.exceptions.UserError\"\n                ) {\n                    this.notificationService.add(error.data.message, {\n                        title: _t(\"User error\"),\n                        type: \"danger\",\n                    });\n                } else {\n                    this.notificationService.add(_t(\"Unable to start the recording.\"), {\n                        title: _t(\"An error occured\"),\n                        type: \"danger\",\n                    });\n                }\n            }\n        } else if (this.embeddedState.status === \"recording\") {\n            this.props.onTranscriptionUpdated(\"stopped\", this.embeddedState.id);\n            this.audioRecorder.stopRecording();\n            this.updateSummary();\n        }\n    }\n\n    async updateSummary(prompt = \"\") {\n        this.embeddedState.status = \"summarizing\";\n        const summary = await this.getSummary(prompt);\n        if (summary) {\n            this.props.onRecorderStopped(this.embeddedState.id, summary);\n            this.embeddedState.hasSummary = true;\n            this.setCurrentTab(\"summary\");\n        }\n        this.embeddedState.status = \"idle\";\n    }\n\n    async getSummary(prompt = \"\") {\n        const textToSummarize = this.props.getTranscriptContent(this.embeddedState.id);\n        if (!this.composerPrompts || !textToSummarize || textToSummarize.trim() === \"\") {\n            return null;\n        }\n\n        const summaryLanguage = `You MUST provide the summary in the following language: ${this.state.currentLanguage}`;\n        const summary = await this.orm.call(\n            \"ai.agent\",\n            \"get_direct_response\",\n            [this.composerPrompts.ai_agent],\n            {\n                prompt: `${this.composerPrompts.default_prompt}\\n${prompt}\\n${summaryLanguage}\\n${textToSummarize}`,\n                enable_html_response: true,\n            }\n        );\n        return String(summary);\n    }\n\n    async openComposer() {\n        this.actionService.doAction(\n            {\n                type: \"ir.actions.act_window\",\n                name: _t(\"Share transcript summary\"),\n                view_mode: \"form\",\n                res_model: \"mail.compose.message\",\n                views: [[false, \"form\"]],\n                target: \"new\",\n                view_id: false,\n                context: {\n                    default_model: this.props.resModel,\n                    default_res_ids: [this.props.resId],\n                    default_subject: _t(\"Share transcript summary\"),\n                    default_body:\n                        this.props.getTabContent(this.embeddedState.id, \"summary\").innerHTML ?? \"\",\n                    clicked_on_full_composer: true,\n                },\n            },\n            {\n                onClose: async () => {\n                    const thread = this.mailStore.Thread.get({\n                        model: this.props.resModel,\n                        id: this.props.resId,\n                    });\n                    thread?.fetchNewMessages();\n                },\n            }\n        );\n    }\n}\n\nexport const aiVoiceTranscriptionEmbeddedComponent = {\n    name: \"voice-transcription\",\n    Component: VoiceTranscription,\n    getEditableDescendants: getEditableDescendants,\n    getProps: (host) => ({ host }),\n    getStateChangeManager: (config) =>\n        new StateChangeManager(\n            Object.assign(config, {\n                getEmbeddedState: (host) => {\n                    const props = getEmbeddedProps(host);\n                    if (host.dataset.embeddedState) {\n                        const currentState = JSON.parse(host.dataset.embeddedState);\n                        props.status = currentState.next?.status;\n                    }\n                    props.status ??= \"idle\";\n                    return props;\n                },\n                stateToEmbeddedProps: (host, state) => {\n                    const props = getEmbeddedProps(host);\n                    props.id = state.id;\n                    props.hasSummary = state.hasSummary;\n                    return props;\n                },\n            })\n        ),\n};\n", "import {\n    MAIN_EMBEDDINGS,\n    READONLY_MAIN_EMBEDDINGS,\n} from \"@html_editor/others/embedded_components/embedding_sets\";\nimport { aiVoiceTranscriptionEmbeddedComponent } from \"./core/voice_transcription\";\nimport { aiReadonlyVoiceTranscriptionEmbeddedComponent } from \"./core/readonly_voice_transcription\";\n\nMAIN_EMBEDDINGS.push(aiVoiceTranscriptionEmbeddedComponent);\nREADONLY_MAIN_EMBEDDINGS.push(aiReadonlyVoiceTranscriptionEmbeddedComponent);\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { EMBEDDED_COMPONENT_PLUGINS } from \"@html_editor/plugin_sets\";\nimport { selectElements } from \"@html_editor/utils/dom_traversal\";\nimport { parseHTML } from \"@html_editor/utils/html\";\nimport { withSequence } from \"@html_editor/utils/resource\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { renderToElement } from \"@web/core/utils/render\";\nimport { uuid } from \"@web/core/utils/strings\";\n\nconst componentSelector = (id) => `#transcriber-${id}`;\n\nconst RECORDER_SELECTOR = \"[data-embedded='voice-transcription']\";\nconst NOTES_CONTENT_SELECTOR = \"[data-embedded-editable='notesContent']\";\nconst TRANSCRIPT_CONTENT_SELECTOR = \"[data-embedded-editable='transcriptContent']\";\n\nexport class TranscriptionPlugin extends Plugin {\n    static id = \"voice-transcription\";\n    static dependencies = [\"baseContainer\", \"dom\", \"history\", \"selection\", \"embeddedComponents\"];\n\n    resources = {\n        hints: [\n            {\n                selector: `${RECORDER_SELECTOR} ${NOTES_CONTENT_SELECTOR}:not(:focus) > *:only-child`,\n                text: _t(\"Add notes about your upcoming meeting\"),\n            },\n            {\n                selector: `${RECORDER_SELECTOR} ${TRANSCRIPT_CONTENT_SELECTOR}:not(:focus) > *:only-child`,\n                text: _t(\"Start recording to get a real-time transcript of the conversation\"),\n            },\n        ],\n        hint_targets_providers: (selectionData, editable) => [\n            ...editable.querySelectorAll(\n                `${RECORDER_SELECTOR} ${NOTES_CONTENT_SELECTOR}:not(:focus) > *:only-child, ${RECORDER_SELECTOR} ${TRANSCRIPT_CONTENT_SELECTOR}:not(:focus) > *:only-child`\n            ),\n        ],\n        user_commands: [\n            {\n                id: \"openTranscriptDialog\",\n                title: _t(\"Voice Transcript\"),\n                description: _t(\"Dictate text or record a meeting\"),\n                run: this.insertTranscriptionComponent.bind(this),\n            },\n        ],\n        powerbox_items: [\n            {\n                keywords: [_t(\"AI\")],\n                categoryId: \"ai\",\n                commandId: \"openTranscriptDialog\",\n                icon: \"fa-microphone\",\n            },\n        ],\n        mount_component_handlers: this.setupTranscriptionComponent.bind(this),\n        normalize_handlers: withSequence(Infinity, this.normalize.bind(this)),\n    };\n\n    insertTranscriptionComponent(params = {}) {\n        const transcriptBlock = renderToElement(\"ai.VoiceTranscriptionBlueprint\", {\n            embeddedProps: JSON.stringify({\n                id: uuid(),\n            }),\n        });\n        this.dependencies.dom.insert(transcriptBlock);\n        this.dependencies.history.addStep();\n    }\n\n    setupTranscriptionComponent({ name, props }) {\n        if (name === \"voice-transcription\") {\n            const { resModel, resId } = this.config.getRecordInfo();\n            Object.assign(props, {\n                resModel,\n                resId,\n                firstRecordingDate: (id) => this.getFirstRecordingDate(id),\n                getTabContent: (id, tabName) => this.getTabContent(id, tabName),\n                getTranscriptContent: (id) => this.getTranscriptContent(id),\n                onTranscriptionStarted: (id, currentLanguage) =>\n                    this.startTranscription(id, currentLanguage),\n                onTranscriptionUpdated: (state, componentId, chunkId, textContent) =>\n                    this.updateTranscription(state, componentId, chunkId, textContent),\n                onRecorderStopped: (id, transcript) => this.updateSummary(id, transcript),\n            });\n        }\n    }\n\n    /**\n     * Retrieves the dom content of a tab of the transcription component\n     * @param {string} id - the id of the transcription component\n     * @param {string} tabName - the name of the tab\n     * @returns {HtmlElement} the content of the tab\n     */\n    getTabContent(id, tabName) {\n        return this.editable.querySelector(\n            `${componentSelector(id)} #${tabName}-content>[data-embedded-editable]`\n        );\n    }\n\n    getTranscriptContent(id) {\n        const transcriptElements = this.editable.querySelectorAll(\n            `${componentSelector(id)} ${TRANSCRIPT_CONTENT_SELECTOR} > :not(:has(b:only-child))`\n        );\n        const transcript = [];\n        transcriptElements.forEach((element) => {\n            const innerText = element.innerText.trim();\n            if (innerText !== \"\") {\n                transcript.push(innerText);\n            }\n        });\n        return transcript.join(\"\\n\");\n    }\n\n    /**\n     * Retrieves the date of the first recording form the dom\n     * @param {string} id - the id of the transcription component\n     * @returns {string} the date of the first recording\n     */\n    getFirstRecordingDate(id) {\n        const firstDateElement = this.editable.querySelector(\n            `${componentSelector(id)} #transcript-content b`\n        );\n        return firstDateElement?.textContent;\n    }\n\n    startTranscription(id, currentLanguage) {\n        const today = new Date();\n        const timeString = today.toLocaleTimeString([currentLanguage], {\n            hour: \"2-digit\",\n            minute: \"2-digit\",\n        });\n\n        const anchorNode = this.editable.querySelector(\n            `${componentSelector(id)} #transcript-content>div`\n        );\n        const textElement = document.createElement(\"p\");\n        const boldElement = document.createElement(\"b\");\n\n        boldElement.textContent = `${today.toLocaleDateString()} - ${timeString}`;\n        textElement.appendChild(boldElement);\n        anchorNode.appendChild(textElement);\n        this.dependencies.history.addStep();\n    }\n\n    updateTranscription(state, componentId, chunkId = \"\", textContent = \"\") {\n        switch (state) {\n            case \"listening\": {\n                const anchorNode = this.getTabContent(componentId, \"transcript\");\n                const textElement = document.createElement(\"p\");\n                textElement.classList.add(\n                    \"o-ai-transcription-listening\",\n                    \"ps-2\",\n                    \"border-start\",\n                    \"border-2\",\n                    \"border-muted\"\n                );\n                textElement.textContent = _t(\"AI is listening...\");\n                anchorNode.appendChild(textElement);\n                this.dependencies.history.addStep();\n                break;\n            }\n            case \"delta\":\n                return this.updateDelta(componentId, chunkId, textContent);\n            case \"completed\":\n                return this.commitTranscription(componentId, chunkId, textContent);\n            case \"stopped\": {\n                const listeningNode = this.editable.querySelector(\n                    `${componentSelector(componentId)} .o-ai-transcription-listening`\n                );\n                listeningNode?.remove();\n                this.dependencies.history.addStep();\n                break;\n            }\n        }\n    }\n\n    updateDelta(componentId, chunkId, textContent) {\n        let textElement = this.editable.querySelector(\n            `${componentSelector(componentId)} #current-transcript-${chunkId}`\n        );\n        if (!textElement) {\n            const anchorNode = this.getTabContent(componentId, \"transcript\");\n            const listeningNode = this.editable.querySelector(\n                `${componentSelector(componentId)} .o-ai-transcription-listening`\n            );\n            if (!anchorNode) {\n                return null;\n            }\n            textElement = document.createElement(\"p\");\n            textElement.setAttribute(\"id\", `current-transcript-${chunkId}`);\n            textElement.classList.add(\n                \"text-muted\",\n                \"ps-2\",\n                \"border-start\",\n                \"border-2\",\n                \"border-muted\"\n            );\n            if (listeningNode) {\n                anchorNode.replaceChild(textElement, listeningNode);\n            } else {\n                anchorNode.appendChild(textElement);\n            }\n            this.dependencies.history.addStep();\n        }\n        textElement.textContent = textElement.textContent + textContent;\n        return textElement;\n    }\n\n    commitTranscription(componenentId, chunkId, textContent) {\n        let currentTranscript = this.editable.querySelector(\n            `${componentSelector(componenentId)} #current-transcript-${chunkId}`\n        );\n        if (!currentTranscript) {\n            const anchorNode = this.editable.querySelector(\n                `${componentSelector(componenentId)} #transcript-content>div`\n            );\n            if (!anchorNode) {\n                return null;\n            }\n            currentTranscript = document.createElement(\"p\");\n            anchorNode.appendChild(currentTranscript);\n        } else {\n            currentTranscript.removeAttribute(\"id\");\n            currentTranscript.removeAttribute(\"class\");\n        }\n        currentTranscript.textContent = textContent;\n        this.dependencies.history.addStep();\n        return currentTranscript;\n    }\n\n    updateSummary(componentId, transcript) {\n        /** @type {HTMLElement} */\n        const anchorNode = this.editable.querySelector(\n            `${componentSelector(componentId)} #summary-content>div`\n        );\n\n        const existingSummary = anchorNode.querySelector(\"section\");\n        const htmlTranscript = parseHTML(document, transcript);\n        const summarySection = document.createElement(\"section\");\n        summarySection.appendChild(htmlTranscript);\n\n        if (existingSummary === null) {\n            anchorNode.appendChild(summarySection);\n        } else {\n            anchorNode.replaceChild(summarySection, existingSummary);\n        }\n        this.dependencies.history.addStep();\n    }\n\n    normalize(element) {\n        for (const emptyRecorderNode of selectElements(\n            element,\n            `${RECORDER_SELECTOR} [data-embedded-editable]:empty`\n        )) {\n            const baseContainer = this.dependencies.baseContainer.createBaseContainer();\n            baseContainer.appendChild(this.document.createElement(\"br\"));\n            emptyRecorderNode.replaceChildren(baseContainer);\n        }\n    }\n}\n\nEMBEDDED_COMPONENT_PLUGINS.push(TranscriptionPlugin);\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { Plugin } from \"@html_editor/plugin\";\nimport { closestElement } from \"@html_editor/utils/dom_traversal\";\nimport { withSequence } from \"@html_editor/utils/resource\";\nimport { isContentEditable } from \"@html_editor/utils/dom_info\";\nimport { unwrapContents } from \"@html_editor/utils/dom\";\nimport { MAIN_PLUGINS } from \"@html_editor/plugin_sets\";\nimport { user } from \"@web/core/user\";\nimport { MAIL_CORE_PLUGINS } from \"@mail/core/common/plugin/plugin_sets\";\n\nexport class ChatGPTPlugin extends Plugin {\n    static id = \"chatgpt\";\n    static dependencies = [\n        \"baseContainer\",\n        \"selection\",\n        \"history\",\n        \"dom\",\n        \"sanitize\",\n        \"dialog\",\n        \"split\",\n    ];\n    resources = {\n        user_commands: [\n            {\n                id: \"openChatGPTDialog\",\n                title: _t(\"AI\"),\n                description: _t(\"Generate or transform content with AI\"),\n                run: this.openDialog.bind(this),\n                isAvailable: () => user.isInternalUser,\n            },\n        ],\n        toolbar_items: [\n            {\n                id: \"chatgpt\",\n                groupId: \"ai\",\n                commandId: \"openChatGPTDialog\",\n                namespaces: [\"compact\", \"expanded\"],\n                icon: \"ai-logo-icon\",\n                isDisabled: this.isNotReplaceableByAI.bind(this),\n            },\n        ],\n\n        powerbox_categories: withSequence(70, { id: \"ai\", name: _t(\"AI Tools\") }),\n        powerbox_items: {\n            keywords: [_t(\"AI\")],\n            categoryId: \"ai\",\n            commandId: \"openChatGPTDialog\",\n            icon: \"fa-magic\",\n        },\n\n        power_buttons: withSequence(20, {\n            commandId: \"openChatGPTDialog\",\n            icon: \"ai-logo-icon\",\n        }),\n    };\n\n    isNotReplaceableByAI(selection = this.dependencies.selection.getEditableSelection()) {\n        if (selection.isCollapsed) {\n            return false;\n        }\n        const isEmpty = !selection.textContent().replace(/\\s+/g, \"\");\n        const cannotReplace = this.dependencies.selection\n            .getTargetedNodes()\n            .find((el) => this.dependencies.split.isUnsplittable(el) || !isContentEditable(el));\n        return cannotReplace || isEmpty;\n    }\n\n    async openDialog(params = {}) {\n        const selection = this.dependencies.selection.getEditableSelection();\n        const dialogParams = {\n            insert: (content) => {\n                const insertedNodes = this.dependencies.dom.insert(content);\n                this.dependencies.history.addStep();\n                // Add a frame around the inserted content to highlight it for 2\n                // seconds.\n                const start = insertedNodes?.length && closestElement(insertedNodes[0]);\n                const end =\n                    insertedNodes?.length &&\n                    closestElement(insertedNodes[insertedNodes.length - 1]);\n                if (start && end) {\n                    const divContainer = this.editable.parentElement;\n                    let [parent, left, top] = [\n                        start.offsetParent,\n                        start.offsetLeft,\n                        start.offsetTop - start.scrollTop,\n                    ];\n                    while (parent && !parent.contains(divContainer)) {\n                        left += parent.offsetLeft;\n                        top += parent.offsetTop - parent.scrollTop;\n                        parent = parent.offsetParent;\n                    }\n                    let [endParent, endTop] = [end.offsetParent, end.offsetTop - end.scrollTop];\n                    while (endParent && !endParent.contains(divContainer)) {\n                        endTop += endParent.offsetTop - endParent.scrollTop;\n                        endParent = endParent.offsetParent;\n                    }\n                    const div = document.createElement(\"div\");\n                    div.classList.add(\"o-chatgpt-content\");\n                    const FRAME_PADDING = 3;\n                    div.style.left = `${left - FRAME_PADDING}px`;\n                    div.style.top = `${top - FRAME_PADDING}px`;\n                    div.style.width = `${\n                        Math.max(start.offsetWidth, end.offsetWidth) + FRAME_PADDING * 2\n                    }px`;\n                    div.style.height = `${endTop + end.offsetHeight - top + FRAME_PADDING * 2}px`;\n                    divContainer.prepend(div);\n                    setTimeout(() => div.remove(), 2000);\n                }\n                unwrapContents(insertedNodes[0]);\n            },\n            ...params,\n        };\n        dialogParams.baseContainer = this.dependencies.baseContainer.getDefaultNodeName();\n        // collapse to end\n        let callerComp,\n            recordModel,\n            recordId,\n            recordData,\n            recordFields,\n            callerId,\n            channelTitle,\n            textSelection;\n        const { resModel, resId, data, fields, id } = this.config.getRecordInfo();\n        if (selection.isCollapsed) {\n            if (resModel === \"mail.compose.message\") {\n                callerComp = \"mail_composer\";\n                recordModel = data.model;\n                recordId = Number(data.res_ids.slice(1, -1)); // resIds should look like so `[id]`, the slice and cast allows to extract the id\n                recordData = data;\n                channelTitle = data.subject;\n                callerId = id;\n            } else {\n                callerComp = \"html_field_record\";\n                recordModel = resModel;\n                recordId = resId;\n                recordData = data;\n                recordFields = fields;\n                channelTitle = data?.display_name || _t(\"Editor\");\n                callerId = resId || id;\n            }\n        } else {\n            callerComp = \"html_field_text_select\";\n            recordModel = resModel;\n            recordId = resId;\n            recordData = data;\n            recordFields = fields;\n            channelTitle = _t(\"Text Selection\");\n            callerId = resId || id;\n            textSelection = selection.textContent();\n        }\n        await this.services.aiChatLauncher.launchAIChat({\n            callerComponentName: callerComp,\n            recordModel: recordModel,\n            recordId: recordId,\n            originalRecordData: recordData,\n            originalRecordFields: recordFields,\n            aiSpecialActions: {\n                insert: dialogParams.insert,\n            },\n            channelTitle: channelTitle,\n            aiChatSourceId: callerId,\n            textSelection: textSelection,\n        });\n        if (this.services.ui.isSmall) {\n            // TODO: Find a better way and avoid modifying range\n            // HACK: In the case of opening through dropdown:\n            // - when dropdown open, it keep the element focused before the open\n            // - when opening the dialog through the dropdown, the dropdown closes\n            // - upon close, the generic code of the dropdown sets focus on the kept element (in our case, the editable)\n            // - we need to remove the range after the generic code of the dropdown is triggered so we hack it by removing the range in the next tick\n            Promise.resolve().then(() => {\n                // If the dialog is opened on a small screen, remove all selection\n                // because the selection can be seen through the dialog on some devices.\n                this.document.getSelection()?.removeAllRanges();\n            });\n        }\n    }\n\n    destroy() {\n        const { resModel } = this.config.getRecordInfo();\n        if (resModel !== \"mail.compose.message\") {\n            this.services[\"mail.store\"].aiInsertButtonTarget = false;\n        }\n        super.destroy();\n    }\n}\n\nMAIN_PLUGINS.push(ChatGPTPlugin);\nMAIL_CORE_PLUGINS.push(ChatGPTPlugin);\n", "import { registry } from \"@web/core/registry\";\n\nasync function initChat(env, action) {\n    const store = env.services[\"mail.store\"];\n\n    const thread = await store.Thread.getOrFetch({\n        model: \"discuss.channel\",\n        id: Number(action.params.channelId),\n    });\n    if (!thread) {\n        throw new Error(\"Thread not found\");\n    }\n    thread.open({ focus: true });\n    await thread.isLoadedDeferred;\n    if (action.params.user_prompt && thread.status !== \"loading\") {\n        await thread.post(action.params.user_prompt);\n    }\n}\n\nregistry.category(\"actions\").add(\"agent_chat_action\", initChat);\n", "import { registry } from \"@web/core/registry\";\nimport { standardFieldProps } from \"@web/views/fields/standard_field_props\";\n\nimport { Component, markup, onMounted, onWillUnmount } from \"@odoo/owl\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { htmlJoin } from \"@web/core/utils/html\";\n\nexport class MailComposerChatGPT extends Component {\n    static template = \"mail.MailComposerChatGPT\";\n    static props = { ...standardFieldProps };\n\n    setup() {\n        this.store = useService(\"mail.store\");\n        this.orm = useService(\"orm\");\n        this.aiChatLauncher = useService(\"aiChatLauncher\");\n        let currentDialog, previousZIndex;\n        onMounted(() => {\n            currentDialog = document.querySelector(\".o-overlay-item:has(.o_dialog\");\n            if (currentDialog) {\n                previousZIndex = currentDialog.style.zIndex;\n                // 1020 is the value of the $zindex-sticky which is the z-index value used for the `.o-mail-ChatWindow`\n                // See odoo/addons/mail/static/src/core/common/chat_window.scss\n                // We use this value to ensure that the dialog that contains this component is rendered below the `.o-mail-ChatWindow`s.\n                currentDialog.style.zIndex = \"1020\";\n            }\n        });\n        onWillUnmount(() => {\n            this.store.aiInsertButtonTarget = false;\n            if (currentDialog) {\n                currentDialog.style.zIndex = previousZIndex;\n            }\n        });\n    }\n\n    async onOpenChatGPTPromptDialogBtnClick() {\n        await this.aiChatLauncher.launchAIChat({\n            callerComponentName: \"mail_composer\",\n            recordModel: this.props.record.data.model,\n            recordId: Number(this.props.record.data.res_ids.slice(1, -1)),\n            originalRecordData: this.props.record.data,\n            aiSpecialActions: {\n                insert: (content) => {\n                    const root = document.createElement(\"div\");\n                    root.appendChild(content);\n                    const { body } = this.props.record.data;\n                    // markup: the element of which innerHTML is taken should be safely built\n                    this.props.record.update({\n                        body: htmlJoin([markup(root.innerHTML), body]),\n                    });\n                },\n            },\n            channelTitle: this.props.record.data.subject,\n            aiChatSourceId: this.props.record.id,\n        });\n    }\n}\n\nexport const mailComposerChatGPT = {\n    component: MailComposerChatGPT,\n    fieldDependencies: [{ name: \"body\", type: \"text\" }],\n};\n\nregistry.category(\"fields\").add(\"mail_composer_chatgpt\", mailComposerChatGPT);\n", "import { MultiRecordSelector } from \"@web/core/record_selectors/multi_record_selector\";\n\nimport { Component, useState } from \"@odoo/owl\";\n\nexport class RecordsSelectorPopover extends Component {\n    static components = { MultiRecordSelector };\n    static template = \"ai.RecordsSelectorPopover\";\n    static props = {\n        resModel: { type: String },\n        close: { type: Function },\n        domain: { type: Array, optional: true },\n        validate: { type: Function },\n        resIds: { type: Array, optional: true },\n    };\n\n    setup() {\n        this.state = useState({\n            resIds: this.props.resIds || [],\n        });\n    }\n\n    update(resIds) {\n        this.state.resIds = resIds;\n    }\n\n    validate() {\n        this.props.validate(this.state.resIds);\n        this.props.close();\n    }\n}\n", "import { rpc } from \"@web/core/network/rpc\";\nimport { url } from \"@web/core/utils/urls\";\n\n/**\n * @typedef {object} RealTimeSessionInfo\n * @property {string} value The client secret value for WebSocket authentication.\n * @property {number} expires_at the exiration timestamp of the session\n * @property {object} session The expiration information about the created session.\n */\n\nexport default class VADAudioRecorder {\n    static instance = null;\n\n    /**\n     * @type {WebSocket}\n     */\n    static socket = null;\n    /**\n     * @type {AudioContext}\n     */\n    static audioContext = null;\n\n    /**\n     * @type {MediaStream}\n     */\n    static audioStream = null;\n\n    static listenerCount = 0;\n\n    constructor(\n        onMessage,\n        filterOptions = {\n            type: \"bandpass\",\n            frequency: 1850,\n            Q: 4.0,\n        }\n    ) {\n        this.onMessage = onMessage;\n        this.filterOptions = filterOptions;\n        this.state = \"inactive\";\n    }\n\n    /**\n     * This method will start the recording of the audio and the realtime transcription session.\n     * @param {string} language the language to use for the transcription session\n     * @param {string} prompt the prompt to give to the transcription tool\n     */\n    async startRecording(language, prompt) {\n        /** @type{RealTimeSessionInfo} */\n        const sessionInfo = await rpc(\"/ai/transcription/session\", {\n            language,\n            prompt,\n        });\n        if (VADAudioRecorder.audioStream === null) {\n            VADAudioRecorder.audioStream = await navigator.mediaDevices.getUserMedia({\n                audio: {\n                    noiseSuppression: true,\n                    echoCancellation: true,\n                },\n            });\n        }\n\n        await this.setupTranscriptionSession(sessionInfo);\n        VADAudioRecorder.listenerCount++;\n        this.state = \"recording\";\n    }\n\n    /**\n     * This method sets up the transcription session. This includes setting up the audio pipeline\n     * to filter the audio and formatting it with the help of the PCM16AudioProcessor. The method\n     * also starts the {@link WebSocket} session with OpenAI.\n     * @param {RealTimeSessionInfo} sessionInfo the information about the session containing the ephemeral token\n     */\n    async setupTranscriptionSession(sessionInfo) {\n        if (!VADAudioRecorder.socket) {\n            VADAudioRecorder.socket = new WebSocket(\"wss://api.openai.com/v1/realtime\", [\n                \"realtime\",\n                // Auth\n                \"openai-insecure-api-key.\" + sessionInfo.value,\n            ]);\n        }\n        this.socketMessageListener = (event) => {\n            const jsonData = JSON.parse(event.data);\n            this.onMessage(jsonData);\n        };\n        VADAudioRecorder.socket.addEventListener(\"message\", this.socketMessageListener);\n\n        VADAudioRecorder.socket.onerror = (error) => {\n            this.state = \"stopped\";\n            console.error(error);\n        };\n\n        if (!VADAudioRecorder.audioContext || VADAudioRecorder.audioContext.state === \"closed\") {\n            VADAudioRecorder.audioContext = new AudioContext();\n            const audioContext = VADAudioRecorder.audioContext;\n\n            const sourceNode = audioContext.createMediaStreamSource(VADAudioRecorder.audioStream);\n            const filterNode = audioContext.createBiquadFilter();\n            filterNode.type = this.filterOptions.type;\n            filterNode.frequency.setValueAtTime(\n                this.filterOptions.frequency,\n                audioContext.currentTime\n            );\n            filterNode.Q.setValueAtTime(this.filterOptions.Q, audioContext.currentTime);\n\n            const workletUrl = url(\"/ai/static/src/worklets/pcm16_audio_processor.js\");\n            await audioContext.audioWorklet.addModule(workletUrl);\n            const pcm16AudioProcessorNode = new AudioWorkletNode(audioContext, \"pcm16-processor\");\n\n            if (audioContext.state === \"suspended\") {\n                await audioContext.resume();\n            }\n            sourceNode.connect(filterNode);\n            filterNode.connect(pcm16AudioProcessorNode);\n            pcm16AudioProcessorNode.connect(audioContext.destination);\n\n            pcm16AudioProcessorNode.port.onmessage = (event) => {\n                const socket = VADAudioRecorder.socket;\n                if (socket !== null && socket.readyState === 1) {\n                    socket.send(\n                        JSON.stringify({\n                            type: \"input_audio_buffer.append\",\n                            audio: btoa(String.fromCharCode(...new Uint8Array(event.data))),\n                        })\n                    );\n                }\n            };\n        }\n    }\n\n    /**\n     * This method stops the recording of the audio. Specifically, closing the socket, and\n     * disposing all the media resources currently in use.\n     */\n    stopRecording() {\n        VADAudioRecorder.listenerCount = Math.max(VADAudioRecorder.listenerCount - 1, 0);\n        VADAudioRecorder.socket?.removeEventListener(\"message\", this.socketMessageListener);\n\n        if (VADAudioRecorder.listenerCount === 0) {\n            VADAudioRecorder.audioContext?.close();\n            VADAudioRecorder.socket?.close();\n            VADAudioRecorder.audioStream?.getTracks().forEach((track) => track.stop());\n\n            VADAudioRecorder.socket = null;\n            VADAudioRecorder.audioContext = null;\n            VADAudioRecorder.audioStream = null;\n            this.state = \"stopped\";\n        }\n    }\n}\n", "import { Component } from \"@odoo/owl\";\nimport { registry } from \"@web/core/registry\";\nimport { standardFieldProps } from \"@web/views/fields/standard_field_props\";\n\nexport class AgentSourceTypeIcon extends Component {\n    static template = \"ai.AgentSourceTypeIcon\";\n    static props = { ...standardFieldProps };\n\n    setup() {\n        this.type = this.props.record.data.type;\n        this.mimetype = this.props.record.data.mimetype;\n    }\n\n    get iconConfig() {\n        const type = this.type;\n        const mimetype = this.mimetype;\n\n        if (type === 'url') {\n            return {\n                type: 'fontawesome',\n                class: 'fa fa-link ms-1',\n                title: 'Link'\n            };\n        } else if (type === 'binary' && mimetype) {\n            return {\n                type: 'mimetype',\n                class: 'o_agent_source_icon o_image',\n                dataMimetype: mimetype,\n                title: mimetype\n            };\n        } else {\n            return {\n                type: 'fontawesome',\n                class: 'fa fa-file-o ms-1',\n                title: 'File'\n            };\n        }\n    }\n}\n\nconst agentSourceTypeIcon = {\n    component: AgentSourceTypeIcon,\n};\n\nregistry.category(\"fields\").add(\"agent_source_type_icon\", agentSourceTypeIcon);\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { formatFloat } from \"@web/core/utils/numbers\";\nimport { IntegerField } from \"@web/views/fields/integer/integer_field\";\n\nexport class SourceSizeIntegerField extends IntegerField {\n    get formattedValue() {\n        if (!this.value) {\n            return \"\";\n        }\n        return `${formatFloat(this.value, { humanReadable: true })}B`;\n    }\n}\n\nconst sourceSizeIntegerField = {\n    component: SourceSizeIntegerField,\n    displayName: _t(\"SourceSizeIntegerField\"),\n    supportedTypes: [\"integer\"],\n};\n\nregistry.category(\"fields\").add(\"source_size\", sourceSizeIntegerField);\n", "import { registry } from \"@web/core/registry\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { Field } from \"@web/views/fields/field\";\n\nexport class SourceViewLink extends Field {\n    static template = \"ai.SourceViewLink\";\n\n    setup() {\n        super.setup();\n        this.actionService = useService(\"action\");\n    }\n\n    get isClickable() {\n        return this.props.record.data.status === 'indexed' && this.props.record.data.user_has_access;\n    }\n\n    async onClickField(ev) {\n        ev.preventDefault();\n        ev.stopPropagation();\n        if (!this.isClickable) {\n            return;\n        }\n        const action = {\n            context: this.props.record.context,\n            resModel: this.props.record.resModel,\n            name: \"action_access_source\",\n            type: \"object\",\n            resId: this.props.record.resId,\n        };\n        await this.actionService.doActionButton(action);\n    }\n}\n\nregistry.category(\"fields\").add(\"source_view_link\", {component: SourceViewLink,});\n", "import { registry } from \"@web/core/registry\";\nimport { BadgeField, badgeField } from \"@web/views/fields/badge/badge_field\";\n\nexport class StatusBadgeTooltip extends BadgeField {\n    static template = \"ai.StatusBadgeTooltip\";\n\n    get showTooltip() {\n        return this.props.record.data[this.props.name] === 'failed' &&\n            this.props.record.data.error_details;\n    }\n\n    get errorDetails() {\n        return this.props.record.data.error_details || '';\n    }\n}\n\nregistry.category(\"fields\").add(\"status_badge_tooltip\", {\n    ...badgeField,\n    component: StatusBadgeTooltip,\n});\n", "import { Component } from \"@odoo/owl\";\nimport { registry } from \"@web/core/registry\";\nimport { useService } from \"@web/core/utils/hooks\";\n\nexport default class SystrayAction extends Component {\n    static props = {};\n    static template = \"ai.SystrayAction\";\n\n    setup() {\n        super.setup();\n        this.actionService = useService(\"action\");\n        this.aiChatLauncher = useService(\"aiChatLauncher\");\n    }\n\n    async onClickLaunchAIChat() {\n        const currentController = this.actionService.currentController;\n        if (currentController?.view?.type === \"form\") {\n            this.env.bus.trigger(\"AI:OPEN_AI_CHAT\", { origin: \"chatter_ai_button\" });\n            return;\n        }\n        this.aiChatLauncher.launchAIChat({\n            callerComponentName: \"systray_ai_button\",\n        });\n    }\n}\n\nregistry\n    .category(\"systray\")\n    .add(\"ai.systray_action\", { Component: SystrayAction }, { sequence: 30 });\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { x2ManyCommands } from \"@web/core/orm_service\";\nimport { patch } from \"@web/core/utils/patch\";\nimport { Record } from \"@web/model/relational_model/record\";\nimport { markup } from \"@odoo/owl\";\n\nconst notificationTitles = [\n    _t(\"I couldn't quite make sense of that\"),\n    _t(\"Hmm... that was a bit fuzzy for me\"),\n    _t(\"Missing a few pieces of the puzzle\"),\n    _t(\"I gave it a shot, but came up empty\"),\n    _t(\"My circuits didn't quite get it\"),\n    _t(\"I couldn't connect the dots\"),\n    _t(\"That was a bit over my head\"),\n    _t(\"Felt like a riddle I couldn\u2019t solve\"),\n];\n\nfunction getRandomToasterTitle() {\n    const index = Math.floor(Math.random() * notificationTitles.length);\n    return notificationTitles[index];\n}\n\npatch(Record.prototype, {\n    computeAiField(fieldName) {\n        return this.model.mutex.exec(() => this._computeAiField(fieldName));\n    },\n\n    computeAiProperty(fullName) {\n        return this.model.mutex.exec(() => this._computeAiProperty(fullName));\n    },\n\n    async _computeAiField(fieldName) {\n        const field = this.fields[fieldName];\n        if (!field?.ai) {\n            throw new Error(\"Cannot compute a non-AI field using AI\");\n        }\n        let value;\n        try {\n            value = await this.model.orm.call(this.resModel, \"get_ai_field_value\", [\n                this.resId || [],\n                fieldName,\n                this._getChanges(),\n            ]);\n        } catch (e) {\n            if (e.exceptionName === \"odoo.addons.ai_fields.tools.UnresolvedQuery\") {\n                this.model.notification.add(e.data.message, {\n                    autocloseDelay: 7000,\n                    title: `\ud83e\udd16 ${getRandomToasterTitle()}`,\n                    type: \"warning\",\n                });\n                value = field.type === \"many2many\" && [x2ManyCommands.set([])];\n            } else {\n                throw e;\n            }\n        }\n        // allows to use the \"SET\" command\n        if (field.type === \"many2many\") {\n            await this._update({ [fieldName]: value });\n            return;\n        }\n        await this._update(\n            this._parseServerValues({ [fieldName]: value }, { currentValues: this.data }),\n        );\n    },\n\n    async _computeAiProperty(fullName) {\n        const property = this.fields[fullName];\n        if (!property?.ai) {\n            throw new Error(\"Cannot compute a non-AI property using AI\");\n        }\n        let value;\n        try {\n            value = await this.model.orm.call(this.resModel, \"get_ai_property_value\", [\n                this.resId || [],\n                fullName,\n                this._getChanges(),\n            ]);\n        } catch (e) {\n            if (e.exceptionName === \"odoo.addons.ai_fields.tools.UnresolvedQuery\") {\n                this.model.notification.add(e.data.message, {\n                    autocloseDelay: 7000,\n                    title: `\ud83e\udd16 ${getRandomToasterTitle()}`,\n                    type: \"warning\",\n                });\n                return false;\n            }\n            throw e;\n        }\n        if (property.type === \"html\") {\n            return markup(value);\n        }\n        return value;\n    },\n});\n", "import { HtmlField, htmlField } from \"@html_editor/fields/html_field\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { BooleanField, booleanField } from \"@web/views/fields/boolean/boolean_field\";\nimport { CharField, charField } from \"@web/views/fields/char/char_field\";\nimport { DateTimeField, dateField, dateTimeField } from \"@web/views/fields/datetime/datetime_field\";\nimport { FloatField, floatField } from \"@web/views/fields/float/float_field\";\nimport { IntegerField, integerField } from \"@web/views/fields/integer/integer_field\";\nimport {\n    Many2ManyTagsAvatarField,\n    many2ManyTagsAvatarField,\n} from \"@web/views/fields/many2many_tags_avatar/many2many_tags_avatar_field\";\nimport {\n    Many2ManyTagsField,\n    many2ManyTagsField,\n} from \"@web/views/fields/many2many_tags/many2many_tags_field\";\nimport {\n    Many2OneAvatarField,\n    many2OneAvatarField,\n} from \"@web/views/fields/many2one_avatar/many2one_avatar_field\";\nimport { buildM2OFieldDescription, Many2OneField } from \"@web/views/fields/many2one/many2one_field\";\nimport { MonetaryField, monetaryField } from \"@web/views/fields/monetary/monetary_field\";\nimport { SelectionField, selectionField } from \"@web/views/fields/selection/selection_field\";\nimport { TextField, textField } from \"@web/views/fields/text/text_field\";\n\nconst AiFieldMixin = (fieldClass) => {\n    return class extends fieldClass {\n        setup() {\n            super.setup();\n            this.notification = useService(\"notification\");\n        }\n\n        get isAiComputed() {\n            return this.props.record.fields[this.props.name].ai;\n        }\n\n        async onAiClick() {\n            await this.props.record.computeAiField(this.props.name);\n        }\n    };\n};\n\nclass AiBooleanField extends AiFieldMixin(BooleanField) {\n    static template = \"ai_fields.AiBooleanField\";\n}\n\nconst aiBooleanField = {\n    ...booleanField,\n    component: AiBooleanField,\n    displayName: _t(\"AI Checkbox\"),\n};\n\nregistry.category(\"fields\").add(\"ai_boolean\", aiBooleanField);\n\nclass AiCharField extends AiFieldMixin(CharField) {\n    static template = \"ai_fields.AiCharField\";\n}\n\nconst aiCharField = {\n    ...charField,\n    component: AiCharField,\n    displayName: _t(\"AI Text\"),\n};\n\nregistry.category(\"fields\").add(\"ai_char\", aiCharField);\n\nclass AiDateTimeField extends AiFieldMixin(DateTimeField) {\n    static template = \"ai_fields.AiDateTimeField\";\n}\n\nconst aiDateField = {\n    ...dateField,\n    component: AiDateTimeField,\n    displayName: _t(\"AI Date\"),\n};\n\nregistry.category(\"fields\").add(\"ai_date\", aiDateField);\n\nconst aiDateTimeField = {\n    ...dateTimeField,\n    component: AiDateTimeField,\n    displayName: _t(\"AI Date & Time\"),\n};\n\nregistry.category(\"fields\").add(\"ai_datetime\", aiDateTimeField);\n\nclass AiFloatField extends AiFieldMixin(FloatField) {\n    static template = \"ai_fields.AiFloatField\";\n}\n\nconst aiFloatField = {\n    ...floatField,\n    component: AiFloatField,\n    displayName: _t(\"AI Float\"),\n};\n\nregistry.category(\"fields\").add(\"ai_float\", aiFloatField);\n\nclass AiHtmlField extends AiFieldMixin(HtmlField) {\n    static template = \"ai_fields.AiHtmlField\";\n}\n\nconst aiHtmlField = {\n    ...htmlField,\n    additionalClasses: [\"o_field_html\"],\n    component: AiHtmlField,\n    displayName: _t(\"AI Html\"),\n};\n\nregistry.category(\"fields\").add(\"ai_html\", aiHtmlField);\n\nclass AiIntegerField extends AiFieldMixin(IntegerField) {\n    static template = \"ai_fields.AiIntegerField\";\n}\n\nconst aiIntegerField = {\n    ...integerField,\n    component: AiIntegerField,\n    displayName: _t(\"AI Integer\"),\n};\n\nregistry.category(\"fields\").add(\"ai_integer\", aiIntegerField);\n\nclass AiMany2ManyTagsAvatarField extends AiFieldMixin(Many2ManyTagsAvatarField) {\n    static template = \"ai_fields.AiMany2ManyTagsAvatarField\";\n}\n\nconst aiMany2ManyTagsAvatarField = {\n    ...many2ManyTagsAvatarField,\n    component: AiMany2ManyTagsAvatarField,\n    displayName: _t(\"AI Many2Many Tags Avatar\"),\n};\n\nregistry.category(\"fields\").add(\"ai_many2many_tags_avatar\", aiMany2ManyTagsAvatarField);\n\nclass AiMany2ManyTagsField extends AiFieldMixin(Many2ManyTagsField) {\n    static template = \"ai_fields.AiMany2ManyTagsField\";\n}\n\nconst aiMany2ManyTagsField = {\n    ...many2ManyTagsField,\n    component: AiMany2ManyTagsField,\n    displayName: _t(\"AI Many2Many Tags\"),\n};\n\nregistry.category(\"fields\").add(\"ai_many2many_tags\", aiMany2ManyTagsField);\n\nclass AiMany2OneAvatarField extends AiFieldMixin(Many2OneAvatarField) {\n    static template = \"ai_fields.AiMany2OneAvatarField\";\n}\n\nconst aiMany2OneAvatarField = {\n    ...many2OneAvatarField,\n    component: AiMany2OneAvatarField,\n    displayName: _t(\"AI Many2One\"),\n};\n\nregistry.category(\"fields\").add(\"ai_many2one_avatar\", aiMany2OneAvatarField);\n\nclass AiMany2OneField extends AiFieldMixin(Many2OneField) {\n    static template = \"ai_fields.AiMany2OneField\";\n}\n\nconst aiMany2OneField = {\n    ...buildM2OFieldDescription(AiMany2OneField),\n    displayName: _t(\"AI Many2One\"),\n};\n\nregistry.category(\"fields\").add(\"ai_many2one\", aiMany2OneField);\n\nclass AiMonetaryField extends AiFieldMixin(MonetaryField) {\n    static template = \"ai_fields.AiMonetaryField\";\n}\n\nconst aiMonetaryField = {\n    ...monetaryField,\n    component: AiMonetaryField,\n    displayName: _t(\"AI Monetary\"),\n};\n\nregistry.category(\"fields\").add(\"ai_monetary\", aiMonetaryField);\n\nclass AiSelectionField extends AiFieldMixin(SelectionField) {\n    static template = \"ai_fields.AiSelectionField\";\n}\n\nconst aiSelectionField = {\n    ...selectionField,\n    component: AiSelectionField,\n    displayName: _t(\"AI Selection\"),\n};\n\nregistry.category(\"fields\").add(\"ai_selection\", aiSelectionField);\n\nclass AiTextField extends AiFieldMixin(TextField) {\n    static template = \"ai_fields.AiTextField\";\n}\n\nconst aiTextField = {\n    ...textField,\n    additionalClasses: [\"o_field_text\"],\n    component: AiTextField,\n    displayName: _t(\"AI Text\"),\n};\n\nregistry.category(\"fields\").add(\"ai_text\", aiTextField);\n", "import { AiPrompt } from \"@ai/ai_prompt/ai_prompt\";\nimport { patch } from \"@web/core/utils/patch\";\nimport { PropertiesField } from \"@web/views/fields/properties/properties_field\";\nimport { PropertyDefinition } from \"@web/views/fields/properties/property_definition\";\n\npatch(PropertiesField.prototype, {\n    get additionalPropertyDefinitionProps() {\n        return {\n            ...super.additionalPropertyDefinitionProps,\n            propertiesModel: this.props.record.resModel,\n        };\n    },\n    async onAiClick(propertyName) {\n        const value = await this.props.record.computeAiProperty(\n            `${this.props.name}.${propertyName}`,\n        );\n        this.onPropertyValueChange(propertyName, value);\n    },\n\n    async onPropertyDefinitionChange(propertyDefinition) {\n        const propertyIndex = this._getPropertyIndex(propertyDefinition.name);\n        const oldDefinition = this.propertiesList[propertyIndex];\n\n        if (!oldDefinition.ai && propertyDefinition.ai) {\n            const lastRecord = await this.orm.call(\n                this.props.record._config.resModel,\n                \"search\",\n                [],\n                { domain: [], limit: 1, order: \"id DESC\" },\n            );\n            const resId = lastRecord[0] || 0;\n            if (this.props.record._config.resId) {\n                propertyDefinition.ai_domain = [\n                    \"|\",\n                    [\"id\", \">=\", resId - 50],\n                    [\"id\", \"=\", this.props.record._config.resId],\n                ];\n            } else {\n                propertyDefinition.ai_domain = [[\"id\", \">=\", resId - 50]];\n            }\n        }\n\n        return await super.onPropertyDefinitionChange(...arguments);\n    },\n});\n\npatch(PropertyDefinition, {\n    props: { ...PropertyDefinition.props, propertiesModel: { type: String } },\n    components: { ...PropertyDefinition.components, AiPrompt },\n});\n\npatch(PropertyDefinition.prototype, {\n    onAiChange(newValue) {\n        const propertyDefinition = {\n            ...this.state.propertyDefinition,\n            ai: newValue,\n            default: false,\n        };\n        this.props.onChange(propertyDefinition);\n        this.state.propertyDefinition = propertyDefinition;\n    },\n\n    onSystemPromptChange(newPrompt) {\n        const propertyDefinition = {\n            ...this.state.propertyDefinition,\n            system_prompt: newPrompt,\n        };\n        this.props.onChange(propertyDefinition);\n        this.state.propertyDefinition = propertyDefinition;\n    },\n\n    get comodel() {\n        if ([\"many2one\", \"many2many\"].includes(this.state.propertyDefinition.type)) {\n            return this.state.propertyDefinition.comodel || undefined;\n        }\n        return undefined;\n    },\n});\n", "/** @odoo-module **/\n\nimport { AgentSourceAddDialog } from \"@ai/components/agent_add_source_dialog/agent_add_source_dialog\";\nimport { SelectCreateDialog } from \"@web/views/view_dialogs/select_create_dialog\";\nimport { patch } from \"@web/core/utils/patch\";\nimport { _t } from \"@web/core/l10n/translation\";\n\npatch(AgentSourceAddDialog.prototype, {\n    get cardsData() {\n        return [\n            ...super.cardsData,\n            {\n                image: \"/ai_knowledge/static/img/icon.png\",\n                icon: \"fa-file-text-o\",\n                title: _t(\"Add from Knowledge\"),\n                onClick: () => this.onAddKnowledgeSourceClick(),\n            },\n        ];\n    },\n\n    onAddKnowledgeSourceClick() {\n        this.dialog.add(\n            SelectCreateDialog,\n            {\n                title: _t(\"Add from Knowledge\"),\n                noCreate: true,\n                multiSelect: true,\n                resModel: \"knowledge.article\",\n                domain: [\n                    [\"is_template\", \"=\", false],\n                    [\"is_article_item\", \"=\", false],\n                    [\"user_has_access\", \"=\", true],\n                ],\n                onSelected: async (resIds) => {\n                    if (resIds.length) {\n                        this.addKnowledgeArticlesToAgent(resIds);\n                    }\n                },\n            },\n        );\n    },\n\n    async addKnowledgeArticlesToAgent(article_ids) {\n        this.state.loading = true;\n        await this.orm.call(\"ai.agent.source\", \"create_from_articles\", [\n            article_ids,\n            this.agentId,\n        ]);\n        this.state.loading = false;\n        return this.actionService.doAction({type: \"ir.actions.client\", tag: \"soft_reload\"});\n    },\n});\n", "import { AgentSourceTypeIcon } from \"@ai/views/fields/agent_source_type_icon/agent_source_type_icon\";\nimport { patch } from \"@web/core/utils/patch\";\n\npatch(AgentSourceTypeIcon.prototype, {\n    get iconConfig() {\n        const type = this.type;\n        if (type === 'knowledge_article') {\n            return {\n                type: 'image',\n                src: '/ai_knowledge/static/img/icon.png',\n                title: 'Knowledge Article'\n            };\n        }\n        return super.iconConfig;\n    }\n});\n", "import { WysiwygArticleHelper } from \"@knowledge/components/wysiwyg_article_helper/wysiwyg_article_helper\";\n\nimport { patch } from \"@web/core/utils/patch\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { useService } from \"@web/core/utils/hooks\";\n\npatch(WysiwygArticleHelper.prototype, {\n    setup(){\n        super.setup();\n        this.aiChatLauncher = useService(\"aiChatLauncher\");\n    },\n    async onGenerateArticleClick() {\n        await this.aiChatLauncher.launchAIChat({\n            callerComponentName: \"html_field_knowledge\",\n            recordModel: \"knowledge.article\",\n            recordId: this.props.record.resId,\n            aiSpecialActions: {\n                insert: (fragment) => {\n                    const generatedContentTitle = fragment.querySelector(\"h1,h2\");\n                    const articleTitle = this.props.editor.document.createElement(\"h1\");\n                    if (generatedContentTitle && generatedContentTitle.tagName !== \"H1\") {\n                        articleTitle.innerText = generatedContentTitle.innerText;\n                        generatedContentTitle.replaceWith(articleTitle);\n                    } else if (!generatedContentTitle) {\n                        const br = this.props.editor.document.createElement(\"BR\");\n                        articleTitle.replaceChildren(br);\n                        fragment.prepend(articleTitle);\n                    }\n                    this.props.editor.editable.replaceChildren(...fragment.children);\n                    this.props.editor.shared.selection.setCursorEnd(this.props.editor.editable);\n                    this.props.editor.shared.history.addStep();\n                },\n            },\n            aiChatSourceId: this.props.record.resId,\n            channelTitle: _t(\"Knowledge Article Editor\"),\n        });\n    }\n});\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\n\nregistry.category(\"actions\").add(\"doc_api_key_wizard\", () => {\n    return {\n        type: \"ir.actions.act_window\",\n        name: _t(\"API Key Wizard\"),\n        res_model: \"res.users.apikeys.description\",\n        views: [[false, \"form\"]],\n        target: \"new\",\n    }\n});\n", "/** @odoo-module **/\n\n/*!\n  * simplewebauthn/browser@9.0.1 (https://github.com/MasterKale/SimpleWebAuthn)\n  * Copyright 2020 Matthew Miller\n  * Licensed under MIT (https://github.com/MasterKale/SimpleWebAuthn/blob/master/LICENSE.md)\n  */\n\nfunction utf8StringToBuffer(value) {\n    return new TextEncoder().encode(value);\n}\n\nfunction bufferToBase64URLString(buffer) {\n    const bytes = new Uint8Array(buffer);\n    let str = '';\n    for (const charCode of bytes) {\n        str += String.fromCharCode(charCode);\n    }\n    const base64String = btoa(str);\n    return base64String.replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=/g, '');\n}\n\nfunction base64URLStringToBuffer(base64URLString) {\n    const base64 = base64URLString.replace(/-/g, '+').replace(/_/g, '/');\n    const padLength = (4 - (base64.length % 4)) % 4;\n    const padded = base64.padEnd(base64.length + padLength, '=');\n    const binary = atob(padded);\n    const buffer = new ArrayBuffer(binary.length);\n    const bytes = new Uint8Array(buffer);\n    for (let i = 0; i < binary.length; i++) {\n        bytes[i] = binary.charCodeAt(i);\n    }\n    return buffer;\n}\n\nfunction browserSupportsWebAuthn() {\n    return (window?.PublicKeyCredential !== undefined &&\n        typeof window.PublicKeyCredential === 'function');\n}\n\nfunction toPublicKeyCredentialDescriptor(descriptor) {\n    const { id } = descriptor;\n    return {\n        ...descriptor,\n        id: base64URLStringToBuffer(id),\n        transports: descriptor.transports,\n    };\n}\n\nfunction isValidDomain(hostname) {\n    return (hostname === 'localhost' ||\n        /^([a-z0-9]+(-[a-z0-9]+)*\\.)+[a-z]{2,}$/i.test(hostname));\n}\n\nclass WebAuthnError extends Error {\n    constructor({ message, code, cause, name, }) {\n        super(message, { cause });\n        this.name = name ?? cause.name;\n        this.code = code;\n    }\n}\n\nfunction identifyRegistrationError({ error, options, }) {\n    const { publicKey } = options;\n    if (!publicKey) {\n        throw Error('options was missing required publicKey property');\n    }\n    if (error.name === 'AbortError') {\n        if (options.signal instanceof AbortSignal) {\n            return new WebAuthnError({\n                message: 'Registration ceremony was sent an abort signal',\n                code: 'ERROR_CEREMONY_ABORTED',\n                cause: error,\n            });\n        }\n    }\n    else if (error.name === 'ConstraintError') {\n        if (publicKey.authenticatorSelection?.requireResidentKey === true) {\n            return new WebAuthnError({\n                message: 'Discoverable credentials were required but no available authenticator supported it',\n                code: 'ERROR_AUTHENTICATOR_MISSING_DISCOVERABLE_CREDENTIAL_SUPPORT',\n                cause: error,\n            });\n        }\n        else if (publicKey.authenticatorSelection?.userVerification === 'required') {\n            return new WebAuthnError({\n                message: 'User verification was required but no available authenticator supported it',\n                code: 'ERROR_AUTHENTICATOR_MISSING_USER_VERIFICATION_SUPPORT',\n                cause: error,\n            });\n        }\n    }\n    else if (error.name === 'InvalidStateError') {\n        return new WebAuthnError({\n            message: 'The authenticator was previously registered',\n            code: 'ERROR_AUTHENTICATOR_PREVIOUSLY_REGISTERED',\n            cause: error,\n        });\n    }\n    else if (error.name === 'NotAllowedError') {\n        return new WebAuthnError({\n            message: error.message,\n            code: 'ERROR_PASSTHROUGH_SEE_CAUSE_PROPERTY',\n            cause: error,\n        });\n    }\n    else if (error.name === 'NotSupportedError') {\n        const validPubKeyCredParams = publicKey.pubKeyCredParams.filter((param) => param.type === 'public-key');\n        if (validPubKeyCredParams.length === 0) {\n            return new WebAuthnError({\n                message: 'No entry in pubKeyCredParams was of type \"public-key\"',\n                code: 'ERROR_MALFORMED_PUBKEYCREDPARAMS',\n                cause: error,\n            });\n        }\n        return new WebAuthnError({\n            message: 'No available authenticator supported any of the specified pubKeyCredParams algorithms',\n            code: 'ERROR_AUTHENTICATOR_NO_SUPPORTED_PUBKEYCREDPARAMS_ALG',\n            cause: error,\n        });\n    }\n    else if (error.name === 'SecurityError') {\n        const effectiveDomain = window.location.hostname;\n        if (!isValidDomain(effectiveDomain)) {\n            return new WebAuthnError({\n                message: `${window.location.hostname} is an invalid domain`,\n                code: 'ERROR_INVALID_DOMAIN',\n                cause: error,\n            });\n        }\n        else if (publicKey.rp.id !== effectiveDomain) {\n            return new WebAuthnError({\n                message: `The RP ID \"${publicKey.rp.id}\" is invalid for this domain`,\n                code: 'ERROR_INVALID_RP_ID',\n                cause: error,\n            });\n        }\n    }\n    else if (error.name === 'TypeError') {\n        if (publicKey.user.id.byteLength < 1 || publicKey.user.id.byteLength > 64) {\n            return new WebAuthnError({\n                message: 'User ID was not between 1 and 64 characters',\n                code: 'ERROR_INVALID_USER_ID_LENGTH',\n                cause: error,\n            });\n        }\n    }\n    else if (error.name === 'UnknownError') {\n        return new WebAuthnError({\n            message: 'The authenticator was unable to process the specified options, or could not create a new credential',\n            code: 'ERROR_AUTHENTICATOR_GENERAL_ERROR',\n            cause: error,\n        });\n    }\n    return error;\n}\n\nclass BaseWebAuthnAbortService {\n    createNewAbortSignal() {\n        if (this.controller) {\n            const abortError = new Error('Cancelling existing WebAuthn API call for new one');\n            abortError.name = 'AbortError';\n            this.controller.abort(abortError);\n        }\n        const newController = new AbortController();\n        this.controller = newController;\n        return newController.signal;\n    }\n    cancelCeremony() {\n        if (this.controller) {\n            const abortError = new Error('Manually cancelling existing WebAuthn API call');\n            abortError.name = 'AbortError';\n            this.controller.abort(abortError);\n            this.controller = undefined;\n        }\n    }\n}\nconst WebAuthnAbortService = new BaseWebAuthnAbortService();\n\nconst attachments = ['cross-platform', 'platform'];\nfunction toAuthenticatorAttachment(attachment) {\n    if (!attachment) {\n        return;\n    }\n    if (attachments.indexOf(attachment) < 0) {\n        return;\n    }\n    return attachment;\n}\n\nasync function startRegistration(creationOptionsJSON) {\n    if (!browserSupportsWebAuthn()) {\n        throw new Error('WebAuthn is not supported in this browser');\n    }\n    const publicKey = {\n        ...creationOptionsJSON,\n        challenge: base64URLStringToBuffer(creationOptionsJSON.challenge),\n        user: {\n            ...creationOptionsJSON.user,\n            id: utf8StringToBuffer(creationOptionsJSON.user.id),\n        },\n        excludeCredentials: creationOptionsJSON.excludeCredentials?.map(toPublicKeyCredentialDescriptor),\n    };\n    const options = { publicKey };\n    options.signal = WebAuthnAbortService.createNewAbortSignal();\n    let credential;\n    try {\n        credential = (await navigator.credentials.create(options));\n    }\n    catch (err) {\n        throw identifyRegistrationError({ error: err, options });\n    }\n    if (!credential) {\n        throw new Error('Registration was not completed');\n    }\n    const { id, rawId, response, type } = credential;\n    let transports = undefined;\n    if (typeof response.getTransports === 'function') {\n        transports = response.getTransports();\n    }\n    let responsePublicKeyAlgorithm = undefined;\n    if (typeof response.getPublicKeyAlgorithm === 'function') {\n        try {\n            responsePublicKeyAlgorithm = response.getPublicKeyAlgorithm();\n        }\n        catch (error) {\n            warnOnBrokenImplementation('getPublicKeyAlgorithm()', error);\n        }\n    }\n    let responsePublicKey = undefined;\n    if (typeof response.getPublicKey === 'function') {\n        try {\n            const _publicKey = response.getPublicKey();\n            if (_publicKey !== null) {\n                responsePublicKey = bufferToBase64URLString(_publicKey);\n            }\n        }\n        catch (error) {\n            warnOnBrokenImplementation('getPublicKey()', error);\n        }\n    }\n    let responseAuthenticatorData;\n    if (typeof response.getAuthenticatorData === 'function') {\n        try {\n            responseAuthenticatorData = bufferToBase64URLString(response.getAuthenticatorData());\n        }\n        catch (error) {\n            warnOnBrokenImplementation('getAuthenticatorData()', error);\n        }\n    }\n    return {\n        id,\n        rawId: bufferToBase64URLString(rawId),\n        response: {\n            attestationObject: bufferToBase64URLString(response.attestationObject),\n            clientDataJSON: bufferToBase64URLString(response.clientDataJSON),\n            transports,\n            publicKeyAlgorithm: responsePublicKeyAlgorithm,\n            publicKey: responsePublicKey,\n            authenticatorData: responseAuthenticatorData,\n        },\n        type,\n        clientExtensionResults: credential.getClientExtensionResults(),\n        authenticatorAttachment: toAuthenticatorAttachment(credential.authenticatorAttachment),\n    };\n}\nfunction warnOnBrokenImplementation(methodName, cause) {\n    console.warn(`The browser extension that intercepted this WebAuthn API call incorrectly implemented ${methodName}. You should report this error to them.\\n`, cause);\n}\n\nfunction bufferToUTF8String(value) {\n    return new TextDecoder('utf-8').decode(value);\n}\n\nfunction browserSupportsWebAuthnAutofill() {\n    const globalPublicKeyCredential = window\n        .PublicKeyCredential;\n    if (globalPublicKeyCredential.isConditionalMediationAvailable === undefined) {\n        return new Promise((resolve) => resolve(false));\n    }\n    return globalPublicKeyCredential.isConditionalMediationAvailable();\n}\n\nfunction identifyAuthenticationError({ error, options, }) {\n    const { publicKey } = options;\n    if (!publicKey) {\n        throw Error('options was missing required publicKey property');\n    }\n    if (error.name === 'AbortError') {\n        if (options.signal instanceof AbortSignal) {\n            return new WebAuthnError({\n                message: 'Authentication ceremony was sent an abort signal',\n                code: 'ERROR_CEREMONY_ABORTED',\n                cause: error,\n            });\n        }\n    }\n    else if (error.name === 'NotAllowedError') {\n        return new WebAuthnError({\n            message: error.message,\n            code: 'ERROR_PASSTHROUGH_SEE_CAUSE_PROPERTY',\n            cause: error,\n        });\n    }\n    else if (error.name === 'SecurityError') {\n        const effectiveDomain = window.location.hostname;\n        if (!isValidDomain(effectiveDomain)) {\n            return new WebAuthnError({\n                message: `${window.location.hostname} is an invalid domain`,\n                code: 'ERROR_INVALID_DOMAIN',\n                cause: error,\n            });\n        }\n        else if (publicKey.rpId !== effectiveDomain) {\n            return new WebAuthnError({\n                message: `The RP ID \"${publicKey.rpId}\" is invalid for this domain`,\n                code: 'ERROR_INVALID_RP_ID',\n                cause: error,\n            });\n        }\n    }\n    else if (error.name === 'UnknownError') {\n        return new WebAuthnError({\n            message: 'The authenticator was unable to process the specified options, or could not create a new assertion signature',\n            code: 'ERROR_AUTHENTICATOR_GENERAL_ERROR',\n            cause: error,\n        });\n    }\n    return error;\n}\n\nasync function startAuthentication(requestOptionsJSON, useBrowserAutofill = false) {\n    if (!browserSupportsWebAuthn()) {\n        throw new Error('WebAuthn is not supported in this browser');\n    }\n    let allowCredentials;\n    if (requestOptionsJSON.allowCredentials?.length !== 0) {\n        allowCredentials = requestOptionsJSON.allowCredentials?.map(toPublicKeyCredentialDescriptor);\n    }\n    const publicKey = {\n        ...requestOptionsJSON,\n        challenge: base64URLStringToBuffer(requestOptionsJSON.challenge),\n        allowCredentials,\n    };\n    const options = {};\n    if (useBrowserAutofill) {\n        if (!(await browserSupportsWebAuthnAutofill())) {\n            throw Error('Browser does not support WebAuthn autofill');\n        }\n        const eligibleInputs = document.querySelectorAll('input[autocomplete$=\\'webauthn\\']');\n        if (eligibleInputs.length < 1) {\n            throw Error('No <input> with \"webauthn\" as the only or last value in its `autocomplete` attribute was detected');\n        }\n        options.mediation = 'conditional';\n        publicKey.allowCredentials = [];\n    }\n    options.publicKey = publicKey;\n    options.signal = WebAuthnAbortService.createNewAbortSignal();\n    let credential;\n    try {\n        credential = (await navigator.credentials.get(options));\n    }\n    catch (err) {\n        throw identifyAuthenticationError({ error: err, options });\n    }\n    if (!credential) {\n        throw new Error('Authentication was not completed');\n    }\n    const { id, rawId, response, type } = credential;\n    let userHandle = undefined;\n    if (response.userHandle) {\n        userHandle = bufferToUTF8String(response.userHandle);\n    }\n    return {\n        id,\n        rawId: bufferToBase64URLString(rawId),\n        response: {\n            authenticatorData: bufferToBase64URLString(response.authenticatorData),\n            clientDataJSON: bufferToBase64URLString(response.clientDataJSON),\n            signature: bufferToBase64URLString(response.signature),\n            userHandle,\n        },\n        type,\n        clientExtensionResults: credential.getClientExtensionResults(),\n        authenticatorAttachment: toAuthenticatorAttachment(credential.authenticatorAttachment),\n    };\n}\n\nfunction platformAuthenticatorIsAvailable() {\n    if (!browserSupportsWebAuthn()) {\n        return new Promise((resolve) => resolve(false));\n    }\n    return PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable();\n}\n\nexport { WebAuthnAbortService, WebAuthnError, base64URLStringToBuffer, browserSupportsWebAuthn, browserSupportsWebAuthnAutofill, bufferToBase64URLString, platformAuthenticatorIsAvailable, startAuthentication, startRegistration };\n", "import { rpc } from \"@web/core/network/rpc\";\nimport { registry } from \"@web/core/registry\";\nimport { FormController } from \"@web/views/form/form_controller\";\nimport { formView } from \"@web/views/form/form_view\";\nimport * as passkeyLib from \"../../lib/simplewebauthn.js\";\n\nexport class PassKeyIdentityCheckFormController extends FormController {\n    /**\n     * @override\n     */\n    async beforeExecuteActionButton(clickParams) {\n        if (\n            clickParams.name === \"run_check\" &&\n            this.model.root.data.auth_method == \"webauthn\"\n        ) {\n            const serverOptions = await rpc(\"/auth/passkey/start-auth\");\n            const auth = await passkeyLib.startAuthentication(serverOptions).catch(e => console.log(e));\n            // In case the user cancelled the passkey browser check, just interrupt.\n            if(!auth) return false;\n            this.model.root.update({password: JSON.stringify(auth)});\n        }\n        return super.beforeExecuteActionButton(...arguments);\n    }\n}\n\nexport const PassKeyIdentityCheckFormView = {\n    ...formView,\n    Controller: PassKeyIdentityCheckFormController,\n};\n\nregistry.category(\"views\").add(\"auth_passkey_identity_check_view_form\", PassKeyIdentityCheckFormView);\n", "import { registry } from \"@web/core/registry\";\nimport { FormController } from \"@web/views/form/form_controller\";\nimport { formView } from \"@web/views/form/form_view\";\nimport * as passkeyLib from \"../../lib/simplewebauthn.js\";\n\nexport class PassKeyNameFormController extends FormController {\n    /**\n     * @override\n     */\n    async beforeExecuteActionButton(clickParams) {\n        if (clickParams.name === \"make_key\") {\n            const name = document.querySelector(\"div[name='name'].o_field_widget input\").value;\n            if(name.length > 0) {\n                const serverOptions = this.props.context.registration;\n                const registration = await passkeyLib.startRegistration(serverOptions).catch(e => console.error(e));\n                // In case the user cancelled the passkey browser check, just interrupt.\n                if(!registration) return false;\n                clickParams.args = JSON.stringify([registration]);\n            }\n        }\n        return super.beforeExecuteActionButton(...arguments);\n    }\n}\n\nexport const PassKeyNameFormView = {\n    ...formView,\n    Controller: PassKeyNameFormController,\n};\n\nregistry.category(\"views\").add(\"auth_passkey_key_create_view_form\", PassKeyNameFormView);\n", "import { ListRenderer } from \"@web/views/list/list_renderer\";\n\nexport class ImportModuleListRenderer extends ListRenderer {\n\n    get hasSelectors() {\n        return super.hasSelectors && this.props.list.records.every(record => record._values.module_type != 'industries');\n    }\n\n    async onCellClicked(record, column, ev) {\n        if (record._values.module_type && record._values.module_type !== 'official') {\n            const re_action = {\n                name: \"more_info\",\n                res_model: \"ir.module.module\",\n                res_id: -1,\n                type: \"ir.actions.act_window\",\n                views: [[false, \"form\"]],\n                context: {\n                    'module_name': record._values.name,\n                    'module_type': record._values.module_type,\n                }\n            }\n            this.env.services.action.doAction(re_action);\n        }\n        else{\n            super.onCellClicked(record, column, ev);\n        }\n    }\n}\n", "import { registry } from \"@web/core/registry\";\nimport { listView } from \"@web/views/list/list_view\";\nimport { ImportModuleListRenderer } from \"./base_import_list_renderer\";\n\n\nexport const ImportModuleListView = {\n    ...listView,\n    Renderer: ImportModuleListRenderer,\n}\n\nregistry.category(\"views\").add(\"ir_module_module_tree_view\", ImportModuleListView);\n", "import { registry } from \"@web/core/registry\";\nimport { loadJS } from \"@web/core/assets\";\n\n// temporary for OnNoResultReturned bug\nimport { ThirdPartyScriptError } from \"@web/core/errors/error_service\";\nconst errorHandlerRegistry = registry.category(\"error_handlers\");\nimport { Component, onWillRender, useEffect, useRef, useState, xml } from \"@odoo/owl\";\nimport { standardFieldProps } from \"@web/views/fields/standard_field_props\";\n\nconst MONDIALRELAY_SCRIPT_URL = \"https://widget.mondialrelay.com/parcelshop-picker/jquery.plugin.mondialrelay.parcelshoppicker.min.js\"\n\nfunction corsIgnoredErrorHandler(env, error) {\n    if (error instanceof ThirdPartyScriptError) {\n        return true;\n    }\n}\n\nexport class MondialRelayField extends Component {\n    static template = xml`<div t-if=\"enabled\" t-ref=\"root\"/>`;\n    static props = {...standardFieldProps};\n    setup() {\n        this.root = useRef(\"root\");\n        this.state = useState({\n            libLoaded: false, // Whether the library is loaded or not\n        });\n        onWillRender(() => {\n            // Do nothing if the record is not of type mondial_relay\n            if (!this.enabled || this.state.libLoaded) {\n                return;\n            }\n            loadJS(MONDIALRELAY_SCRIPT_URL).then(() => {this.state.libLoaded = true});\n        });\n\n        useEffect(\n            (el) => {\n                if (!el) {\n                    return;\n                }\n                this.insertWidget($(el));\n            },\n            () => [this.state.libLoaded && this.root.el],\n        )\n    }\n\n    get enabled() {\n        return this.props.record.data.is_mondialrelay;\n    }\n\n    insertWidget($el) {\n        const params = {\n            Target: \"\", // required but handled by OnParcelShopSelected\n            Brand: this.props.record.data.mondialrelay_brand,\n            ColLivMod: this.props.record.data.mondial_realy_colLivMod,\n            AllowedCountries: this.props.record.data.mondialrelay_allowed_countries,\n            PostCode: this.props.record.data.shipping_zip || '',\n            Country: this.props.record.data.shipping_country_code  || '',\n            Responsive: true,\n            ShowResultsOnMap: true,\n            AutoSelect: this.props.record.data.mondialrelay_last_selected_id,\n            OnParcelShopSelected: (RelaySelected) => {\n                const values = JSON.stringify({\n                    'id': RelaySelected.ID,\n                    'name': RelaySelected.Nom,\n                    'street': RelaySelected.Adresse1,\n                    'street2': RelaySelected.Adresse2,\n                    'zip': RelaySelected.CP,\n                    'city': RelaySelected.Ville,\n                    'country': RelaySelected.Pays,\n                });\n                this.props.record.update({ [this.props.name]: values });\n            },\n            OnNoResultReturned: () => {\n                // HACK while Mondial Relay fix his bug\n                // disable corsErrorHandler for 10 seconds\n                // If code postal not valid, it will crash with Cors Error:\n                // Cannot read property 'on' of undefined at u.MR_FitBounds\n                const randInt = Math.floor(Math.random() * 100);\n                errorHandlerRegistry.add(\"corsIgnoredErrorHandler\" + randInt, corsIgnoredErrorHandler, {sequence: 10});\n                setTimeout(function () {\n                    errorHandlerRegistry.remove(\"corsIgnoredErrorHandler\" + randInt);\n                }, 10000);\n            },\n        };\n        $el.show();\n        $el.MR_ParcelShopPicker(params);\n        $el.trigger(\"MR_RebindMap\");\n    }\n}\n\nexport const mondialRelayField = {\n    component: MondialRelayField,\n};\n\nregistry.category(\"fields\").add(\"mondialrelay_relay\", mondialRelayField);\n", "import { registry } from \"@web/core/registry\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { CharField, charField } from \"@web/views/fields/char/char_field\";\nimport { AutoComplete } from \"@web/core/autocomplete/autocomplete\";\nimport { googlePlacesSession } from \"../google_places_session\";\nimport { useChildRef } from \"@web/core/utils/hooks\";\nimport { useInputField } from \"@web/views/fields/input_field_hook\";\n\nconst standardAddressFields = {\n    street: {\n        label: _t(\"Street field\"),\n        type: [\"char\"]\n    },\n    street2: {\n        label: _t(\"Additional street field\"),\n        type: [\"char\"]\n    },\n    city: {\n        label: _t(\"City field\"),\n        type: [\"char\"]\n    },\n    state_id: {\n        label: _t(\"State field\"),\n        type: [\"char\", \"many2one\"]\n    },\n    zip: {\n        label: _t(\"Zip field\"),\n        type: [\"char\"]\n    },\n    country_id: {\n        label: _t(\"Country field\"),\n        type: [\"char\", \"many2one\"]\n    }\n}\n\nexport class AddressAutoComplete extends CharField {\n    static template = \"google_address_autocomplete.AddressAutoCompleteTemplate\";\n    static components = { AutoComplete, ...CharField.components };\n\n    static props = {...CharField.props,\n        addressFieldMap: {\n            type: Object,\n            optional: true,\n        }\n    }\n\n    static defaultProps = {\n        ...CharField.defaultProps,\n        addressFieldMap: {},\n    }\n\n    setup() {\n        super.setup();\n        this.input = useChildRef();\n        useInputField({\n            ref: this.input,\n            getValue: () => this.props.record.data[this.props.name] || \"\",\n            parse: (v) => this.parse(v),\n        });\n    }\n\n    get sources() {\n        return [\n            {\n                options: async (request) => {\n                    if (request.length > 5) {\n                        const suggestions = await googlePlacesSession.getAddressPropositions({\n                            partial_address: request,\n                            use_employees_key: true,\n                        });\n                        suggestions.results = suggestions.results.map((result) => ({\n                            label: result.formatted_address,\n                            onSelect: () => this.selectAddressProposition(result),\n                        }));\n                        if (suggestions.results.length) {\n                            suggestions.results.push({\n                                label: \"&#160;\",\n                                cssClass: \"pe-none o-google-credits\",\n                            });\n                        }\n                        return suggestions.results;\n                    } else {\n                        return [];\n                    }\n                },\n                optionSlot: \"option\",\n                placeholder: _t(\"Searching for addresses...\"),\n            },\n        ];\n    }\n\n    async selectAddressProposition(option) {\n        const address = await googlePlacesSession.getAddressDetails({\n            address: option.formatted_address,\n            google_place_id: option.google_place_id,\n            use_employees_key: true,\n        });\n\n        const fieldToDetail = {\n            street: \"formatted_street_number\",\n            country_id: \"country\",\n            state_id: \"state\",\n        };\n        const fieldsToUpdate = Object.keys(standardAddressFields);\n\n        const activeFields = this.props.record.activeFields;\n        const fields = this.props.record.fields;\n        const addressFieldMap = this.props.addressFieldMap;\n\n        const valuesToUpdate = {};\n        const rest = [];\n        fieldsToUpdate.forEach((fieldName) => {\n            const addressField = fieldToDetail[fieldName] || fieldName;\n            let value = address[addressField];\n\n            const recordFieldName = addressFieldMap[fieldName] || fieldName;\n            if (recordFieldName in activeFields) {\n                if (fields[recordFieldName].type === \"many2one\") {\n                    value = value && { id: value[0], display_name: value[1] };\n                } else if (Array.isArray(value)) {\n                    value = value[1];\n                }\n                valuesToUpdate[recordFieldName] = value || false;\n            } else if (!(recordFieldName in fields)) {\n                value = Array.isArray(value) ? value[1] : value;\n                rest.push(value);\n            }\n        });\n        if (!(this.props.name in valuesToUpdate) && rest.length) {\n            valuesToUpdate[this.props.name] = rest.join(\" \");\n        }\n        this.props.record.update(valuesToUpdate);\n    }\n}\n\nexport const addressAutoComplete = {\n    ...charField,\n    component: AddressAutoComplete,\n    displayName: _t(\"Address AutoComplete\"),\n    supportedTypes: [\"char\"],\n    supportedOptions: [\n        ...charField.supportedOptions,\n        ...Object.entries(standardAddressFields).map(([fname, data]) => {\n            return {\n                label: data.label,\n                placeholder: fname,\n                type : \"field\",\n                name: fname,\n                availableTypes: data.type,\n            }\n        })\n    ],\n    extractProps: (fieldInfo, dynamicInfo) => {\n        const { options } = fieldInfo;\n        const props = charField.extractProps(fieldInfo, dynamicInfo);\n        const addressFieldMap = {};\n        Object.keys(standardAddressFields).forEach((fname) => {\n            const optionValue = options[fname];\n            if (optionValue) {\n                addressFieldMap[fname] = optionValue;\n            }\n        });\n        props.addressFieldMap = addressFieldMap;\n        return props;\n    }\n};\nregistry.category(\"fields\").add(\"google_address_autocomplete\", addressAutoComplete);\n", "import { rpc } from \"@web/core/network/rpc\";\n\nfunction makeGooglePlacesSession() {\n    let current;\n\n    /**\n     * Used to generate a unique session ID for the places API.\n     * According to the API docs:\n     * \"The session begins when the user starts typing a query,\n     * and concludes when they select a place and a call to Place Details is made.\n     * Each session can have multiple queries, followed by one place selection.\n     * [...] Once a session has concluded, the token is no longer valid;\n     * your app must generate a fresh token for each session.\"\n     * https://developers.google.com/maps/documentation/places/web-service/details#session_tokens\n     */\n    function generateUUID() {\n        return \"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx\".replace(/[xy]/g, function (c) {\n            const r = (Math.random() * 16) | 0,\n                v = c == \"x\" ? r : (r & 0x3) | 0x8;\n            return v.toString(16);\n        });\n    }\n\n    function getAddressPropositions(params = {}) {\n        if (!params.session_id) {\n            current = current || generateUUID();\n            params.session_id = current;\n        }\n        return rpc(\"/autocomplete/address\", params);\n    }\n\n    async function getAddressDetails(params = {}) {\n        if (!params.session_id) {\n            current = current || generateUUID();\n            params.session_id = current;\n        }\n        current = null;\n        return rpc(\"/autocomplete/address_full\", params);\n    }\n\n    return {\n        get sessionToken() {\n            return current;\n        },\n        getAddressPropositions,\n        getAddressDetails,\n    };\n}\n\nexport const googlePlacesSession = makeGooglePlacesSession();\n", "import mobile from \"@web_mobile/js/services/core\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { session } from \"@web/session\";\n\n//Send info only if client is mobile\nif (mobile.methods.getFCMKey) {\n    var registerDevice = function (fcm_project_id) {\n        mobile.methods.getFCMKey({\n            project_id: fcm_project_id,\n            inbox_action: session.inbox_action,\n        }).then(function (response) {\n            if (response.success) {\n                rpc('/web/dataset/call_kw/res.config.settings/register_device', {\n                    model: 'res.config.settings',\n                    method: 'register_device',\n                    args: [\n                        response.data.subscription_id,\n                        response.data.device_name,\n                        response.data.fcm_token_old,\n                    ],\n                    kwargs: {},\n                }).then(function (ocn_token) {\n                    if (mobile.methods.setOCNToken) {\n                        mobile.methods.setOCNToken({ocn_token: ocn_token});\n                    }\n                });\n            }\n        }).catch(e => console.error(e));\n    };\n    if (session.fcm_project_id) {\n        registerDevice(session.fcm_project_id);\n    } else {\n        rpc('/web/dataset/call_kw/res.config.settings/get_fcm_project_id', {\n            model: 'res.config.settings',\n            method: 'get_fcm_project_id',\n            args: [],\n            kwargs: {},\n        }).then(function (response) {\n            if (response) {\n                registerDevice(response);\n            }\n        });\n    }\n}\n", "import { AutoComplete } from \"@web/core/autocomplete/autocomplete\";\n\n\nexport class PartnerAutoComplete extends AutoComplete {\n    static template = \"partner_autocomplete.PartnerAutoComplete\";\n\n    setup() {\n        super.setup();\n        this.shouldSearchWorldwide = false;\n\t}\n\n\t// Override of AutoComplete\n    loadOptions(options, request) {\n        if (typeof options === \"function\") {\n            return options(request, this.shouldSearchWorldwide);\n        } else {\n            return options;\n        }\n    }\n\n\tasync searchWorldwide(ev){\n\t\tthis.shouldSearchWorldwide = true;\n\t\tev.preventDefault();\n\t\tsuper.close();\n\t\tsuper.open(true);\n\t}\n}\n", "/* global checkVATNumber */\n\nimport { loadJS } from \"@web/core/assets\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { KeepLast } from \"@web/core/utils/concurrency\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { renderToMarkup } from \"@web/core/utils/render\";\nimport { onWillStart } from \"@odoo/owl\";\n\n/**\n * Get list of companies via Autocomplete API\n *\n * @param {string} value\n * @returns {Promise}\n * @private\n */\nexport function usePartnerAutocomplete() {\n    const keepLastOdoo = new KeepLast();\n\n    const notification = useService(\"notification\");\n    const orm = useService(\"orm\");\n\n    let lastNoResultsQuery = null;\n\n    onWillStart(async () => {\n        await loadJS(\"/partner_autocomplete/static/lib/jsvat.js\");\n    });\n\n    function sanitizeVAT(value) {\n        return value ? value.replace(/[^A-Za-z0-9]/g, '') : '';\n    }\n\n    async function isVATNumber(value) {\n        // checkVATNumber is defined in library jsvat.\n        // It validates that the input has a valid VAT number format\n        return checkVATNumber(sanitizeVAT(value));\n    }\n\n    function isGSTNumber(value) {\n        // Check if the input is a valid GST number.\n        let isGST = false;\n        if (value && value.length === 15) {\n            const allGSTinRe = [\n                /\\d{2}[a-zA-Z]{5}\\d{4}[a-zA-Z][1-9A-Za-z][Zz1-9A-Ja-j][0-9a-zA-Z]/, // Normal, Composite, Casual GSTIN\n                /\\d{4}[A-Z]{3}\\d{5}[UO]N[A-Z0-9]/, // UN/ON Body GSTIN\n                /\\d{4}[a-zA-Z]{3}\\d{5}NR[0-9a-zA-Z]/, // NRI GSTIN\n                /\\d{2}[a-zA-Z]{4}[a-zA-Z0-9]\\d{4}[a-zA-Z][1-9A-Za-z][DK][0-9a-zA-Z]/, // TDS GSTIN\n                /\\d{2}[a-zA-Z]{5}\\d{4}[a-zA-Z][1-9A-Za-z]C[0-9a-zA-Z]/ // TCS GSTIN\n            ];\n\n            isGST = allGSTinRe.some((re) => re.test(value));\n        }\n\n        return isGST;\n    }\n\n    async function autocomplete(value, queryCountryId) {\n        value = value.trim();\n        const isVAT = await isVATNumber(value);\n        if (isVAT){\n        \tvalue = sanitizeVAT(value);\n        }\n        const isGST = isGSTNumber(value);\n        return await getSuggestions(value, isVAT || isGST, queryCountryId);\n    }\n\n    /**\n     * Get enrichment data\n     *\n     * @param {Object} company\n     * @returns {Promise}\n     * @private\n     */\n    function enrichCompany(company) {\n        if (isGSTNumber(company.query)){\n            return orm.call('res.partner', 'enrich_by_gst', [company.query]);\n        }\n        return orm.call('res.partner', 'enrich_by_duns', [company.duns]);\n    }\n\n    function removeUselessFields(company, fieldsToKeep) {\n        // Delete attribute to avoid \"Field_changed\" errors (these fields will be populated in the form)\n        for (const field in company){\n            if (!fieldsToKeep.includes(field)){\n                delete company[field]\n            }\n        }\n        return company;\n    };\n\n    /**\n     * Get enriched data + logo before populating partner form\n     *\n     * @param {Object} company\n     * @returns {Promise}\n     */\n    function getCreateData(company, fieldsToKeep) {\n        return enrichCompany(company).then((companyData) => {\n            // Fetch additional company info via Autocomplete Enrichment API\n\n            if (companyData.error) {\n                if (companyData.error_message === 'Insufficient Credit') {\n                    notifyNoCredits();\n                }\n                else if (companyData.error_message === 'No Account Token') {\n                    notifyAccountToken();\n                }\n                else {\n                    notification.add(companyData.error_message);\n                }\n                companyData = {\n                    ...company,\n                    ...companyData,\n                };\n            }\n            return {\n                company: companyData,\n                logo: companyData.logo || false,\n            };\n        })\n    }\n\n    /**\n     * Use Odoo Autocomplete API to return suggestions\n     *\n     * @param {string} value\n     * @param {boolean} isVAT\n     * @returns {Promise}\n     * @private\n     */\n    async function getSuggestions(value, isVAT, queryCountryId) {\n        const method = isVAT ? 'autocomplete_by_vat' : 'autocomplete_by_name';\n\n        // Optimization: if the search query starts with the same content as a previous query for\n        // which there was no results, there won't be any results for the current query.\n        // E.g., if there is no results for query \"abc123\", there won't be any results for query \"abc1234\".\n        if (!isVAT && lastNoResultsQuery && value.startsWith(lastNoResultsQuery)) {\n            return [];\n        }\n\n        const prom = orm.silent.call(\n            'res.partner',\n            method,\n            [value, queryCountryId],\n        );\n\n        const suggestions = await keepLastOdoo.add(prom);\n\n        if (!isVAT && suggestions.length === 0) {\n            lastNoResultsQuery = value;\n        }\n\n        await Promise.all(suggestions.map(async (suggestion) => {\n            suggestion.query = value;  // Save queried value (name, VAT) for later\n            suggestion.description = '';\n            if (suggestion.city){\n                suggestion.description += suggestion.city;\n            }\n            // Show country name only if searching worldwide\n            if (queryCountryId === 0 && suggestion.country_id && suggestion.country_id.display_name) {\n                suggestion.description +=  ', ' + suggestion.country_id.display_name;\n            }\n            return suggestion;\n        }));\n        return suggestions;\n    }\n\n    /**\n     * @private\n     * @returns {Promise}\n     */\n    async function notifyNoCredits() {\n        const url = await orm.call(\n            'iap.account',\n            'get_credits_url',\n            ['partner_autocomplete'],\n        );\n        const title = _t('Not enough credits for Partner Autocomplete');\n        const content = renderToMarkup('partner_autocomplete.InsufficientCreditNotification', {\n            credits_url: url\n        });\n        notification.add(content, {\n            title,\n        });\n    }\n\n    async function notifyAccountToken() {\n        const url = await orm.call(\n            'iap.account',\n            'get_config_account_url',\n            []\n        );\n        const title = _t('IAP Account Token missing');\n        if (url) {\n            const content = renderToMarkup('partner_autocomplete.AccountTokenMissingNotification', {\n                account_url: url\n            });\n            notification.add(content, {\n                title,\n            });\n        }\n        else {\n            notification.add(title);\n        }\n    }\n    return { autocomplete, getCreateData, removeUselessFields };\n}\n", "import { useChildRef, useService } from \"@web/core/utils/hooks\";\nimport { registry } from \"@web/core/registry\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { CharField, charField } from \"@web/views/fields/char/char_field\";\nimport { useInputField } from \"@web/views/fields/input_field_hook\";\n\nimport { usePartnerAutocomplete } from \"@partner_autocomplete/js/partner_autocomplete_core\";\nimport { PartnerAutoComplete } from \"@partner_autocomplete/js/partner_autocomplete_component\";\n\nexport class PartnerAutoCompleteCharField extends CharField {\n    static template = \"partner_autocomplete.PartnerAutoCompleteCharField\";\n    static components = {\n        ...CharField.components,\n        PartnerAutoComplete,\n    };\n    setup() {\n        super.setup();\n\n        this.orm = useService(\"orm\");\n        this.partnerAutocomplete = usePartnerAutocomplete();\n\n        this.inputRef = useChildRef();\n        useInputField({ getValue: () => this.props.record.data[this.props.name] || \"\", parse: (v) => this.parse(v), ref: this.inputRef});\n    }\n\n    async validateSearchTerm(request) {\n        return request && request.length > 2;\n    }\n\n    get sources() {\n        return [\n            {\n                options: async (request, shouldSearchWorldWide) => {\n                    if (await this.validateSearchTerm(request)) {\n                        let queryCountryId = this.props.record.data?.country_id ? this.props.record.data.country_id.id : false;\n                        if (shouldSearchWorldWide){\n                        \tqueryCountryId = 0;\n                        }\n                        const suggestions = await this.partnerAutocomplete.autocomplete(request, queryCountryId);\n                        return suggestions.map((suggestion) => ({\n                            cssClass: \"partner_autocomplete_dropdown_char\",\n                            data: suggestion,\n                            label: suggestion.name,\n                            onSelect: () => this.onSelectPartnerAutocompleteOption(suggestion),\n                        }));\n                    }\n                    else {\n                        return [];\n                    }\n                },\n                optionSlot: \"partnerOption\",\n                placeholder: _t('Searching Autocomplete...'),\n            },\n        ];\n    }\n\n    async onSelectPartnerAutocompleteOption(option) {\n        let data = await this.partnerAutocomplete.getCreateData(option);\n        if (!data?.company) {\n            return;\n        }\n\n        if (data.logo) {\n            const logoField = this.props.record.resModel === 'res.partner' ? 'image_1920' : 'logo';\n            data.company[logoField] = data.logo;\n        }\n\n        // Save UNSPSC codes (tags)\n        const unspsc_codes = data.company.unspsc_codes\n\n        // Delete useless fields before updating record\n        data.company = this.partnerAutocomplete.removeUselessFields(data.company, Object.keys(this.props.record.fields));\n\n        // Update record with retrieved values\n        if (data.company.name) {\n            await this.props.record.update({name: data.company.name});  // Needed otherwise name it is not saved\n        }\n\n        // Add UNSPSC codes (tags)\n        if (this.props.record.resModel === 'res.partner' && unspsc_codes && unspsc_codes.length !== 0) {\n            // category id is fetched and then tags are created (many2many)\n            const category_id = await this.orm.call(\"res.partner\", \"iap_partner_autocomplete_get_tag_ids\", [this.props.record.resId, unspsc_codes]);\n            data.company['category_id'] = [[6, 0, category_id]];\n        }\n        await this.props.record.update(data.company);\n\n        if (this.props.setDirty) {\n            this.props.setDirty(false);\n        }\n    }\n}\n\nexport const partnerAutoCompleteCharField = {\n    ...charField,\n    component: PartnerAutoCompleteCharField,\n};\n\nregistry.category(\"fields\").add(\"field_partner_autocomplete\", partnerAutoCompleteCharField);\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { computeM2OProps, Many2One } from \"@web/views/fields/many2one/many2one\";\nimport { buildM2OFieldDescription, Many2OneField } from \"@web/views/fields/many2one/many2one_field\";\nimport { Component } from \"@odoo/owl\";\nimport { Many2XAutocomplete, useOpenMany2XRecord } from \"@web/views/fields/relational_utils\";\n\nimport { usePartnerAutocomplete } from \"@partner_autocomplete/js/partner_autocomplete_core\";\nimport { PartnerAutoComplete } from \"@partner_autocomplete/js/partner_autocomplete_component\";\n\nexport class PartnerMany2XAutocomplete extends Many2XAutocomplete {\n    static components = {\n        ...super.components,\n        AutoComplete: PartnerAutoComplete,\n    };\n}\nexport class PartnerMany2One extends Many2One {\n    static components = {\n        ...super.components,\n        Many2XAutocomplete: PartnerMany2XAutocomplete,\n    };\n}\n\nexport class PartnerAutoCompleteMany2one extends Component {\n    static template = \"partner_autocomplete.PartnerAutoCompleteMany2one\";\n    static components = { Many2One: PartnerMany2One };\n    static props = { ...Many2OneField.props };\n\n    setup() {\n        super.setup();\n        this.orm = useService(\"orm\");\n        this.partnerAutocomplete = usePartnerAutocomplete();\n        this.openRecord = useOpenMany2XRecord({\n            resModel: this.props.record.fields[this.props.name].relation,\n            activeActions: {\n                create: this.props.canCreate,\n                createEdit: this.props.canCreateEdit,\n                write: this.props.canWrite,\n            },\n            isToMany: false,\n            onRecordSaved: (record) => this.props.record.update({\n                [this.props.name]: {\n                    id: record.resId,\n                    display_name: record.data.display_name || record.data.name,\n                },\n            }),\n            onRecordDiscarded: () => this.props.record.update(false),\n            fieldString: this.props.string || this.props.record.fields[this.props.name].string,\n        });\n    }\n\n    validateSearchTerm(request) {\n        return request && request.length > 2;\n    }\n\n    get m2oProps() {\n        return {\n            ...computeM2OProps(this.props),\n            otherSources: this.sources,\n        };\n    }\n\n    get sources() {\n        if (!this.props.canCreate) {\n            return [];\n        }\n        return [\n            {\n                options: async (request, shouldSearchWorldWide) => {\n                    if (this.validateSearchTerm(request)) {\n                        let queryCountryId = false;\n                    \tif (shouldSearchWorldWide){\n\t\t\t\t\t\t\tqueryCountryId = 0;\n\t\t\t\t\t\t}\n                        const suggestions = await this.partnerAutocomplete.autocomplete(request, queryCountryId);\n                        return suggestions.map((suggestion) => ({\n                            cssClass: \"partner_autocomplete_dropdown_many2one\",\n                            data: suggestion,\n                            label: suggestion.name,\n                            onSelect: () => this.onSelectPartnerAutocompleteOption(suggestion),\n                        }));\n                    }\n                    else {\n                        return [];\n                    }\n                },\n                optionSlot: \"partnerOption\",\n                placeholder: _t(\"Searching Autocomplete...\"),\n            },\n        ];\n    }\n\n    async onSelectPartnerAutocompleteOption(option) {\n        const data = await this.partnerAutocomplete.getCreateData(option);\n\t\tif (!data?.company) {\n\t\t\treturn;\n\t\t}\n        let context = {\n            'default_is_company': true\n        };\n\n        for (const [key, val] of Object.entries(data.company)) {\n            context['default_' + key] = val && val.id ? val.id : val;\n        }\n\n        if (data.logo) {\n            context.default_image_1920 = data.logo;\n        }\n\n        const unspsc_codes = data.company.unspsc_codes;\n        if(unspsc_codes){\n            context.default_category_id = await this.orm.call(\"res.partner\", \"iap_partner_autocomplete_get_tag_ids\", [[], unspsc_codes]);\n        }\n\n        return this.openRecord({ context });\n    }\n}\n\nexport const PartnerAutoCompleteMany2oneField = {\n    ...buildM2OFieldDescription(PartnerAutoCompleteMany2one),\n};\n\nregistry.category(\"fields\").add(\"res_partner_many2one\", PartnerAutoCompleteMany2oneField);\n", "import { registry } from \"@web/core/registry\";\nimport { session } from \"@web/session\";\nimport { user } from \"@web/core/user\";\n\nexport const companyAutocompleteService = {\n    dependencies: [\"orm\"],\n\n    start(env, { orm }) {\n        if (session.iap_company_enrich) {\n            orm.silent.call(\"res.company\", \"iap_enrich_auto\", [user.activeCompany.id], {});\n        }\n    },\n};\n\nregistry\n    .category(\"services\")\n    .add(\"partner_autocomplete.companyAutocomplete\", companyAutocompleteService);\n", "import { registry } from \"@web/core/registry\";\nimport { CharField, charField } from \"@web/views/fields/char/char_field\";\nimport { scanBarcode } from \"@web/core/barcode/barcode_dialog\";\n\nexport class BarcodeScannerWidget extends CharField {\n    static template = \"product_barcodelookup.barcodeformbarcode\";\n    setup() {\n        super.setup();\n    }\n    async onBarcodeBtnClick() {\n        const barcode = await scanBarcode(this.env);\n        if (barcode) {\n            await this.props.record.update({\n                barcode: barcode,\n            });\n        }\n    }\n}\n\nexport const barcodeScannerWidget = {\n    ...charField,\n    component: BarcodeScannerWidget,\n};\n\nregistry.category(\"fields\").add(\"productScanner\", barcodeScannerWidget);\n", "import { onWillStart } from \"@odoo/owl\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { useOpenChat } from \"@mail/core/web/open_chat_hook\";\nimport { AvatarCardPopover } from \"@mail/discuss/web/avatar_card/avatar_card_popover\";\n\nexport class AvatarCardResourcePopover extends AvatarCardPopover {\n    static template = \"resource_mail.AvatarCardResourcePopover\";\n\n    static props = {\n        ...AvatarCardPopover.props,\n        model: { type: String, optional: true },\n        recordModel: {\n            type: String,\n            optional: true,\n        },\n    };\n\n    static defaultProps = {\n        ...AvatarCardPopover.defaultProps,\n        recordModel: \"resource.resource\",\n    };\n\n    setup() {\n        this.orm = useService(\"orm\");\n        this.actionService = useService(\"action\");\n        this.openChat = useOpenChat(\"res.users\");\n        onWillStart(this.onWillStart);\n    }\n\n    async onWillStart() {\n        [this.record] = await this.orm.call(this.props.recordModel, 'get_avatar_card_data', [[this.props.id], this.fieldNames], {});\n        await Promise.all(this.loadAdditionalData());\n    }\n\n    loadAdditionalData() {\n        // To use when overriden in other modules to load additional data, returns promise(s)\n        return [];\n    }\n\n    get fieldNames() {\n        return [\"email\", \"im_status\", \"name\", \"phone\", \"resource_type\", \"share\", \"user_id\"];\n    }\n\n    get name() {\n        return this.record.name;\n    }\n\n    get email() {\n        return this.record.email;\n    }\n\n    get phone() {\n        return this.record.phone;\n    }\n\n    get displayAvatar() {\n        return this.record.user_id?.length;\n    }\n\n    get showViewProfileBtn() {\n        return false;\n    }\n\n    get userId() {\n        return this.record.user_id[0];\n    }\n\n    onSendClick() {\n        this.openChat(this.userId);\n        this.props.close();\n    }\n}\n", "import { Avatar } from \"@mail/views/web/fields/avatar/avatar\";\nimport { AvatarCardResourcePopover } from \"../avatar_card_resource/avatar_card_resource_popover\";\n\nexport class AvatarResource extends Avatar {\n    static components = { ...super.components, Popover: AvatarCardResourcePopover };\n}\n", "import { registry } from \"@web/core/registry\";\nimport { usePopover } from \"@web/core/popover/popover_hook\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport {\n    KanbanMany2ManyTagsAvatarUserField,\n    ListMany2ManyTagsAvatarUserField,\n    Many2ManyTagsAvatarUserField,\n    Many2ManyAvatarUserTagsList,\n    kanbanMany2ManyTagsAvatarUserField,\n    listMany2ManyTagsAvatarUserField,\n    many2ManyTagsAvatarUserField,\n} from \"@mail/views/web/fields/many2many_avatar_user_field/many2many_avatar_user_field\";\nimport { Many2XAutocomplete } from \"@web/views/fields/relational_utils\";\nimport { AvatarCardResourcePopover } from \"@resource_mail/components/avatar_card_resource/avatar_card_resource_popover\";\nimport { Domain } from \"@web/core/domain\";\nimport { KanbanMany2ManyTagsAvatarFieldTagsList } from \"@web/views/fields/many2many_tags_avatar/many2many_tags_avatar_field\";\n\n\nexport class AvatarResourceMany2XAutocomplete extends Many2XAutocomplete {\n    /**\n     * @override\n     */\n    search(request) {\n        return this.orm.call(\n            this.props.resModel,\n            \"search_read\",\n            [this.getDomain(request), [\"id\", \"display_name\", \"resource_type\", \"color\"]],\n            {\n                context: this.props.context,\n                limit: this.props.searchLimit + 1,\n            }\n        );\n    }\n\n    /**\n     * @override\n     */\n    getDomain(request) {\n        return Domain.and([[[\"name\", \"ilike\", request]], this.props.getDomain()]).toList(\n            this.props.context\n        );\n    }\n}\n\nclass Many2ManyAvatarResourceTagsList extends Many2ManyAvatarUserTagsList {\n    static template = \"resource_mail.Many2ManyAvatarResourceTagsList\";\n}\n\nconst WithResourceFieldMixin = (T) => class ResourceFieldMixin extends T {\n    setup() {\n        super.setup(...arguments);\n        if (this.relation == \"resource.resource\") {\n            this.avatarCard = usePopover(AvatarCardResourcePopover);\n        }\n    }\n\n    static components = {\n        ...super.components,\n        Many2XAutocomplete: AvatarResourceMany2XAutocomplete,\n        TagsList: Many2ManyAvatarResourceTagsList,\n    };\n    static optionTemplate = \"resource_mail.Many2ManyAvatarResourceField.option\";\n\n    displayAvatarCard(record) {\n        return !this.env.isSmall && this.relation === \"resource.resource\" && record.data.resource_type === \"user\";\n    }\n\n    getTagProps(record) {\n        return {\n            ...super.getTagProps(...arguments),\n            icon: record.data.resource_type === \"user\" ? null : \"fa-wrench\",\n            colorIndex: record.data.color,\n            img: record.data.resource_type === \"user\"\n                ? `/web/image/${this.relation}/${record.resId}/avatar_128`\n                : null,\n        };\n    }\n};\n\nconst resourceFieldMixin = {\n    relatedFields: (fieldInfo) => {\n        return [\n            ...many2ManyTagsAvatarUserField.relatedFields(fieldInfo),\n            {\n                name: \"resource_type\",\n                type: \"selection\",\n                selection: [\n                    [\"user\", _t(\"Human\")],\n                    [\"material\", _t(\"Material\")],\n                ],\n            },\n            {\n                name: \"color\",\n                type: \"integer\",\n            },\n        ];\n    },\n};\n\nexport class Many2ManyAvatarResourceField extends WithResourceFieldMixin(Many2ManyTagsAvatarUserField) {}\nexport const many2ManyAvatarResourceField = {\n    ...many2ManyTagsAvatarUserField,\n    ...resourceFieldMixin,\n    component: Many2ManyAvatarResourceField,\n};\nregistry.category(\"fields\").add(\"many2many_avatar_resource\", many2ManyAvatarResourceField);\n\nexport class ListMany2ManyAvatarResourceField extends WithResourceFieldMixin(ListMany2ManyTagsAvatarUserField) {}\nexport const listMany2ManyAvatarResourceField = {\n    ...listMany2ManyTagsAvatarUserField,\n    ...resourceFieldMixin,\n    component: ListMany2ManyAvatarResourceField,\n};\nregistry.category(\"fields\").add(\"list.many2many_avatar_resource\", listMany2ManyAvatarResourceField);\n\nexport class KanbanMany2ManyAvatarResourceTagsList extends Many2ManyAvatarResourceTagsList {\n    static props = KanbanMany2ManyTagsAvatarFieldTagsList.props;\n}\nexport class KanbanMany2ManyAvatarResourceField extends WithResourceFieldMixin(KanbanMany2ManyTagsAvatarUserField) {\n    static components = {\n        ...super.components,\n        TagsList: KanbanMany2ManyAvatarResourceTagsList,\n    };\n\n    get tags() {\n        return super.tags.reverse();\n    }\n}\nexport const kanbanMany2ManyAvatarResourceField = {\n    ...kanbanMany2ManyTagsAvatarUserField,\n    ...resourceFieldMixin,\n    component: KanbanMany2ManyAvatarResourceField,\n};\nregistry.category(\"fields\").add(\"kanban.many2many_avatar_resource\", kanbanMany2ManyAvatarResourceField);\n", "import { Component } from \"@odoo/owl\";\nimport { AvatarResource } from \"@resource_mail/components/avatar_resource/avatar_resource\";\nimport { registry } from \"@web/core/registry\";\nimport { computeM2OProps, KanbanMany2One } from \"@web/views/fields/many2one/many2one\";\nimport { buildM2OFieldDescription, Many2OneField } from \"@web/views/fields/many2one/many2one_field\";\n\nexport class KanbanMany2OneAvatarResourceField extends Component {\n    static template = \"resource_mail.KanbanMany2OneAvatarResourceField\";\n    static components = { AvatarResource, KanbanMany2One };\n    static props = { ...Many2OneField.props };\n\n    get m2oProps() {\n        return {\n            ...computeM2OProps(this.props),\n            specification: {\n                resource_type: {},\n            },\n        };\n    }\n}\n\n/** @type {import(\"registries\").FieldsRegistryItemShape} */\nexport const kanbanMany2OneAvatarResourceField = {\n    ...buildM2OFieldDescription(KanbanMany2OneAvatarResourceField),\n    additionalClasses: [\"o_field_many2one_avatar_kanban\", \"o_field_many2one_avatar\"],\n    fieldDependencies: [\n        { name: \"display_name\", type: \"char\" },\n        // to add in model that will use this widget for m2o field related to resource.resource record (as related field is only supported for x2m)\n        { name: \"resource_type\", type: \"selection\" },\n    ],\n};\n\nregistry\n    .category(\"fields\")\n    .add(\"kanban.many2one_avatar_resource\", kanbanMany2OneAvatarResourceField);\n", "import { Component } from \"@odoo/owl\";\nimport { AvatarResource } from \"@resource_mail/components/avatar_resource/avatar_resource\";\nimport { registry } from \"@web/core/registry\";\nimport { computeM2OProps, Many2One } from \"@web/views/fields/many2one/many2one\";\nimport { buildM2OFieldDescription, extractM2OFieldProps, Many2OneField } from \"@web/views/fields/many2one/many2one_field\";\n\nexport class Many2OneAvatarResourceField extends Component {\n    static template = \"resource_mail.Many2OneAvatarResourceField\";\n    static components = { AvatarResource, Many2One };\n    static props = { ...Many2OneField.props };\n\n    get m2oProps() {\n        return {\n            ...computeM2OProps(this.props),\n            specification: {\n                resource_type: {},\n            },\n        };\n    }\n}\n\n/** @type {import(\"registries\").FieldsRegistryItemShape} */\nexport const many2OneAvatarResourceField = {\n    ...buildM2OFieldDescription(Many2OneAvatarResourceField),\n    additionalClasses: [\"o_field_many2one_avatar\"],\n    extractProps(staticInfo, dynamicInfo) {\n        return {\n            ...extractM2OFieldProps(staticInfo, dynamicInfo),\n            canOpen: \"no_open\" in staticInfo.options\n                ? !staticInfo.options.no_open\n                : staticInfo.viewType === \"form\",\n        };\n    },\n    fieldDependencies: [\n        { name: \"display_name\", type: \"char\" },\n        // to add in model that will use this widget for m2o field related to resource.resource record (as related field is only supported for x2m)\n        { name: \"resource_type\", type: \"selection\" },\n    ],\n};\n\nregistry.category(\"fields\").add(\"many2one_avatar_resource\", many2OneAvatarResourceField);\n", "(function(){\n\nfunction saas_version() {\n    // [9, 0] for 9.0\n    // [9, 11] for 9.saas~11\n    var server_version;\n    if (odoo.db_info) {\n        server_version = odoo.db_info.server_version;\n    } else if (odoo.session_info) {\n        server_version = odoo.session_info.server_version;\n    } else if (odoo.info) {\n        // Since 14.5, those data are now stored in odoo.info\n        server_version = odoo.info.server_version;\n    } else {\n        // Sometimes odoo.info is populated too late\n        let session = odoo.__session_info__;\n        if (!session && odoo.__DEBUG__ && odoo.__DEBUG__.services[\"@web/session\"]) {\n            session = odoo.__DEBUG__.services[\"@web/session\"].session;\n        } else if (!session && odoo.loader) {\n            session = odoo.loader.modules.get(\"@web/session\").session;\n        }\n        if (session) {\n            server_version = session.server_version;\n        }\n    }\n    if (server_version) {\n        var parsed = server_version.replace(/[^\\d.]/g, '').split('.').map(function(x) {return parseInt(x);});\n        return new Version(parsed[0], parsed[1]);\n    }\n    return new Version(0, 0);\n}\nfunction Version(major, minor) {\n    this.major = major;\n    this.minor = minor;\n}\nconst MIN_SUPPORTED_VERSION = 16;\nVersion.prototype.check = function check(major, minor) {\n    if (major < MIN_SUPPORTED_VERSION || (major == MIN_SUPPORTED_VERSION && minor == 0)) {\n        console.warn(`Checking version against ${major}.${minor} which is not supported any more.`);\n    }\n    return (this.major > major) ||\n        (this.major === major && this.minor >= (minor || 0));\n};\n\nvar s_version = saas_version();\n\n// TODO(JDE): import from saas_compat.js\nfunction patchCompat(patchFn, target, name, patch) {\n    if (s_version.check(17, 0)) {\n        return patchFn(target, patch);\n    } else {\n        return patchFn(target, name, patch);\n    }\n}\n\n\n// Some fields on various records trigger module installs\nvar magicModuleFields = Object.assign({}, magicModuleFields, {\n\n    // helpdesk.team fields\n    'use_website_helpdesk_form': 'website_helpdesk',\n    'use_website_helpdesk_livechat': 'website_helpdesk_livechat',\n    'use_website_helpdesk_forum': 'website_helpdesk_forum',\n    'use_website_helpdesk_slides': 'website_helpdesk_slides',\n    'use_website_helpdesk_knowledge': 'website_helpdesk_knowledge',\n    'use_helpdesk_timesheet': 'helpdesk_timesheet',\n    'use_helpdesk_sale_timesheet': 'helpdesk_sale_timesheet',\n    'use_credit_notes': 'helpdesk_account',\n    'use_product_returns': 'helpdesk_stock',\n    'use_product_repairs': 'helpdesk_repair',\n    'use_coupons': 'helpdesk_sale_loyalty',\n    'use_fsm': 'helpdesk_fsm',\n\n    // pos.config fields\n    'module_pos_restaurant': 'pos_restaurant',\n    'module_pos_avatax': 'pos_avatax',\n    'module_pos_discount': 'pos_discount',\n    'module_pos_hr': 'pos_hr',\n    'module_pos_sms': 'pos_sms',\n});\n\nfunction userIsInternalUser(data) {\n    // \"Internal User\" is the first group created during database bootstrap\n    // and it's assigned id 1 (see odoo/addons/base/data/base_data.sql),\n    // guess if user is an employee by checking if it's assigned to selection\n    // group.\n    for (const field in data) {\n        if (field.startsWith(\"sel_groups_\") && data[field] === 1) {\n            return true;\n        }\n    }\n    return false;\n}\n\nfunction resConfigSettingsModuleNames(data) {\n    // return list of modules that are checked to be installed\n    const moduleNames = [];\n    for (const field in data) {\n        if (field.startsWith(\"module_\") && data[field] === true) {\n            moduleNames.push(field.slice(7));\n        } else if (field.startsWith(\"pos_module_\") && data[field] === true) {\n            moduleNames.push(field.slice(11));\n        }\n    }\n    return moduleNames;\n}\n\nfunction knownMagicFieldModuleNames(data) {\n    // return list of fields that may trigger install of new module(s)\n    const moduleNames = [];\n    for (const field in magicModuleFields) {\n        if (Object.hasOwn(data, field) && data[field] === true) {\n            moduleNames.push(magicModuleFields[field]);\n        }\n    }\n    return moduleNames;\n}\n\n// Helper for saveRecord overrides\nfunction willEnableCustomPlan(resId, modelName, data) {\n    return (\n        !(resId) && (\n            (modelName === 'res.company') ||\n            (modelName === 'ir.model.fields' && data.compute) ||\n            (modelName === 'ir.actions.server' && data.state === 'code') ||\n            (modelName === 'ir.cron')\n        )\n    );\n}\n\nfunction willInstallModules(resId, modelName, data) {\n    return (\n        !(resId) && modelName === 'res.config.settings'\n    ) || (\n        // check both on create and write\n        (\n            ['helpdesk.team', 'pos.config'].includes(modelName) && knownMagicFieldModuleNames(data).length\n        )\n    );\n}\n\nfunction willIncreasePrice(resId, modelName, data) {\n    return (\n        !(resId) && (modelName === 'res.users' && userIsInternalUser(data))\n    );\n}\n\nfunction willUpsell(resId, modelName, data) {\n    return (\n        willEnableCustomPlan(resId, modelName, data) ||\n        willInstallModules(resId, modelName, data) ||\n        willIncreasePrice(resId, modelName, data)\n    );\n}\n\nfunction getSaveOverrideFn(fn, ConfirmModal, getResId, getModelName, getData) {\n\n    const saveOverride = async function () {\n\n        var self = this;\n        const resId = getResId(this);\n        const modelName = getModelName(this);\n        let data = getData(this);\n        let canSave = true;\n\n        if (this.askChanges !== undefined) {\n            // wait until pending changes are completed (ex. onchange() calls)\n            // to ensure data are synced with model.\n            await this.askChanges();\n            data = getData(this);\n        }\n\n        if (willUpsell(resId, modelName, data)) {\n            canSave = canSave && await new Promise(resolve => {\n                let moduleNames = [];\n                const paidFeature = modelName;\n                if (modelName === \"res.config.settings\") {\n                    moduleNames = resConfigSettingsModuleNames(data);\n                } else if ([\"helpdesk.team\", \"pos.config\"].includes(modelName)) {\n                    moduleNames = knownMagicFieldModuleNames(data);\n                }\n\n                // Use owl component.\n                self.model.dialogService.add(ConfirmModal, {\n                    moduleNames: moduleNames,\n                    paidFeature: paidFeature,\n                    confirm: () => { resolve(true); },\n                    cancel: () => { resolve(false); },\n                });\n            });\n        }\n        if (canSave) {\n            return fn.apply(this, arguments);\n        }\n        return canSave;\n    };\n    return saveOverride;\n}\n\n    /* We're forced to be explicit due to boot.js parsing the function code\n    * with a regex to enumerate and validate dependencies prior to loading them\n    */\n    var deps;\n\n    deps = {\n        HomeMenu: '@web_enterprise/webclient/home_menu/home_menu',\n        SaasExpirationPanel: '@saas_trial/owl/expiration_panel/saas_expiration_panel',\n        MailLimitPanel: '@saas_trial/owl/mail_limit_panel/mail_limit_panel',\n        Registry: \"@web/core/registry\",\n    };\n\n    odoo.define('saas_trial.AppSwitcher', Object.values(deps), function(require) {\n        const { HomeMenu } = require(deps.HomeMenu);\n        const { SaasExpirationPanel } = require(deps.SaasExpirationPanel);\n        HomeMenu.components.ExpirationPanel = SaasExpirationPanel;\n\n        /* the enterpriseSubscriptionService introduced in saas-15.3 is able\n        to block the UI based on the expiration date set in the ICPs, a behaviour we do not want.\n        Since we don't rely on that service for managing expiration warning we can remove it\n        completely */\n        const { registry } = require(deps.Registry);\n\n        // We have to create a dummy service to prevent HomeMenu crashing\n        const enterpriseSubscriptionService = {\n            name: \"enterprise_subscription\",\n            start() {return {};},\n        };\n        registry.category('services').remove('enterprise_subscription');\n        registry.category('services').add('enterprise_subscription', enterpriseSubscriptionService);\n\n        const { MailLimitPanel } = require(deps.MailLimitPanel);\n        HomeMenu.components.MailLimitPanel = MailLimitPanel;\n    });\n\n    // Fix HookConfirmModal for OWL FormView and KanbanView\n    let patchedClassPath = '@web/views/basic_relational_model';\n    if (s_version.check(17, 0)) {\n        patchedClassPath = '@web/model/relational_model/record';\n    }\n    const owlConfirmModalHookDeps = [\n        '@saas_trial/owl/confirm_modal/confirm_modal',\n        '@web/core/utils/patch',\n        patchedClassPath,\n    ];\n\n    if (!s_version.check(17, 0)) {\n    odoo.define('saas_trial.OwlConfirmModalHook', owlConfirmModalHookDeps, function (require) {\n        \"use strict\";\n\n        const { SaasConfirmationDialog } = require('@saas_trial/owl/confirm_modal/confirm_modal');\n        const { patch } = require('@web/core/utils/patch');\n\n        const PatchedClass = require(patchedClassPath).Record;\n\n        const getModelName = (rec) => rec.resModel;\n        const getResId = (rec) => rec.resId;\n        const getData = (rec) => rec.data;\n\n        // Find function to wrap according to version\n        const fn = PatchedClass.prototype.save;\n\n        const saveOverride = getSaveOverrideFn(fn, SaasConfirmationDialog, getResId, getModelName, getData);\n\n        patch(PatchedClass.prototype, 'saas_trial.RecordSave', {save: saveOverride});\n    });\n    }\n\n    const ViewButtonConfirmModalHookDeps = [\n        '@web/core/l10n/translation',\n        '@saas_trial/owl/confirm_modal/confirm_modal',\n        '@web/core/utils/hooks',\n        '@web/core/utils/patch',\n        '@web/views/view_button/view_button',\n    ];\n    odoo.define('saas_trial.ViewButtonConfirmModalHook', ViewButtonConfirmModalHookDeps, function (require) {\n        \"use strict\";\n\n        const { patch } = require('@web/core/utils/patch');\n        const { ViewButton } = require('@web/views/view_button/view_button');\n        const { SaasConfirmationDialog } = require('@saas_trial/owl/confirm_modal/confirm_modal');\n        const { _t } = require('@web/core/l10n/translation');\n\n        const { useService } = require('@web/core/utils/hooks');\n\n        ViewButton.props.push('data-application?', 'data-id?', 'data-paid-feature?');\n\n        patchCompat(patch, ViewButton.prototype, 'saas_trial.AppViewButton', {\n            setup() {\n                if (s_version.check(17, 0)) {\n                    super.setup(...arguments);\n                } else {\n                    this._super(...arguments);\n                }\n                this.dialogService = useService('dialog');\n                this.action = useService(\"action\");\n                this.orm = useService(\"orm\");\n            },\n\n            // As we remove the \"name\" attribute from the button, it gets disabled by default\n            // We therefore need to override the disabled() getter to still enable them.\n            get disabled() {\n                if ((this.props.record && this.props.record.resModel === \"ir.module.module\") ||\n                    (this.props.list && this.props.resModel === \"ir.module.module\")) {\n                    return undefined;\n                } else {\n                    const { name, type, special } = this.clickParams;\n                    return (!name && !type && !special) || this.props.disabled;\n                }\n            },\n            onClick(ev) {\n                // Module Installation\n                var self = this;\n                if (this.props.record && this.props.record.resModel === \"ir.module.module\" &&\n                    (!this.clickParams.name || this.clickParams.name == 'button_immediate_install') &&\n                    this.props.className.split(' ').includes('button_immediate_install')) {\n                    const installBtns = document.getElementsByClassName(\"button_immediate_install\");\n                    Array.from(installBtns).forEach(e => {\n                        e.setAttribute(\"disabled\", \"disabled\");\n                    });\n                    ev.target.textContent = _t('Checking...');\n                    ev.target.setAttribute(\"data-application\", this.props.record.data.application);\n\n                    const install = () => {\n                        ev.target.textContent = _t('Installing...');\n                        self.orm.call(\n                            \"ir.module.module\",\n                            \"button_immediate_install\",\n                            [[self.props.record.resId],]\n                        ).then(function (nextAction) {\n                            ev.target.textContent = _t('Installed');\n                            self.action.doAction(nextAction);\n                        });\n                    };\n                    this.dialogService.add(SaasConfirmationDialog, {\n                        btnEl: ev.target,\n                        appId: this.props.record.resId,\n                        moduleNames: [this.props.record.data.name],\n                        confirm: install,\n                    });\n                    return;\n                }\n                if (s_version.check(17, 0) &&\n                    this.props.record && this.props.record.resModel === \"base.import.module\" &&\n                    this.clickParams.name == 'import_module' && self.props.record.resId) {\n                    const importBtn = ev.currentTarget;\n                    importBtn.setAttribute(\"disabled\", \"disabled\");\n                    const install = () => {\n                        self.orm.call(\"base.import.module\",\n                            \"write\",\n                            [\n                                [self.props.record.resId],\n                                {\n                                    'with_demo': self.props.record.data.with_demo ? true: false,\n                                    'force': self.props.record.data.force ? true: false,\n                                }\n                            ]\n                        ).then(function(){\n                            self.orm.call(\n                                \"base.import.module\",\n                                \"import_module\", [[self.props.record.resId],]\n                            ).then(function (nextAction) {\n                                self.action.doAction(nextAction);\n                            });\n                        });\n                    };\n                    const cancel = () => {\n                        importBtn.removeAttribute(\"disabled\");\n                    };\n                    self.orm.call(\n                        \"base.import.module\",\n                        \"get_dependencies_to_install_names\",\n                        [self.props.record.resId],\n                    ).then(function (module_names){\n                        if (module_names.length){\n                            self.dialogService.add(SaasConfirmationDialog, {\n                                btnEl: ev.target,\n                                moduleNames: module_names,\n                                confirm: install,\n                                cancel: cancel,\n                            });\n                        }\n                        else{\n                            install();\n                        }\n                    });\n                    return;\n                    }\n                if (this._super) {\n                    return this._super(...arguments);\n                }\n                return super.onClick(...arguments);\n            }\n        });\n    });\n\n    // Hook on \"Install Odoo Studio\" promotion dialog\n    const PromoteStudioDialogModule  = s_version.check(18, 3)\n                                       ? \"@web_enterprise/webclient/promote_studio/promote_studio_dialog\"\n                                       : \"@web_enterprise/webclient/promote_studio_dialog/promote_studio_dialog\";\n    const PromoteStudioDialogOwlHookDeps = [\n        '@web/core/browser/browser',\n        '@saas_trial/owl/confirm_modal/confirm_modal',\n        PromoteStudioDialogModule,\n        '@web/core/utils/hooks',\n        '@web/core/utils/patch',\n    ];\n    odoo.define('saas_trial.PromoteStudioDialogOwlHook', PromoteStudioDialogOwlHookDeps, function (require) {\n        \"use strict\";\n\n        const { browser } = require(\"@web/core/browser/browser\");\n        const { SaasConfirmationDialog } = require('@saas_trial/owl/confirm_modal/confirm_modal');\n        const { PromoteStudioDialog } = require(PromoteStudioDialogModule);\n        const { patch } = require('@web/core/utils/patch');\n        const { useService } = require('@web/core/utils/hooks');\n\n        patchCompat(patch, PromoteStudioDialog.prototype, 'saas_trial.PromoteStudioDialogOwl', {\n            setup() {\n                if (s_version.check(17, 0)) {\n                    super.setup(...arguments);\n                } else {\n                    this._super(...arguments);\n                }\n                this.dialogService = useService('dialog');\n                this.orm = useService('orm');\n                this.ui = useService('ui');\n            },\n            async onClickInstallStudio() {\n                var studioDialog = this;\n\n                // check and set disableClick to prevent the user from\n                // triggering the confirmation modal multiple times and\n                // to avoid closing the underlying \"promote dialog\" when\n                // the user clicks on the confirmation dialog itself\n                if (studioDialog.disableClick) {\n                    return; // already waiting for the confirmation modal\n                }\n                studioDialog.disableClick = true;\n\n                const modules = await this.orm.searchRead(\n                    \"ir.module.module\",\n                    [[\"name\", \"=\", \"web_studio\"]],\n                    [\"id\"]\n                );\n                this.dialogService.add(SaasConfirmationDialog, {\n                    appId: modules[0].id,\n                    moduleNames: [\"web_studio\"],\n                    confirm: () => {\n                        studioDialog.ui.block();\n                        studioDialog.orm.call(\n                            \"ir.module.module\",\n                            \"button_immediate_install\",\n                            [[modules[0].id],]\n                        ).then(function (nextAction) {\n                            studioDialog.ui.unblock();\n                            browser.localStorage.setItem(\"openStudioOnReload\", \"main\");\n                            browser.location.reload();\n                            studioDialog.disableClick = false;\n                        });\n                     },\n                     cancel: () => {\n                        studioDialog.disableClick = false;\n                     },\n                });\n            },\n        });\n\n    });\n\n    const saas_trial_requirements = [\n        '@web/webclient/webclient',\n        '@web/core/l10n/dates',\n        '@web/core/utils/hooks',\n        '@web/core/registry',\n        '@saas_trial/owl/db_expiration_tag/db_expiration_tag',\n        '@web/session',\n        '@web/core/browser/browser',\n        '@saas_trial/owl/confirm_modal/confirm_modal',\n        '@saas_trial/owl/rolling_release/rolling_release',\n        '@web/core/utils/strings',\n        '@web/core/l10n/translation',\n        '@saas_trial/owl/resend_activation_email_modal/resend_activation_email_modal',\n        '@saas_trial/dates',\n    ];\n    odoo.define('saas_trial.saas_trial', saas_trial_requirements, function(require) {\n        'use strict';\n        var { WebClient } = require('@web/webclient/webclient');\n        const { deserializeDateTime } = require(\"@web/core/l10n/dates\");\n        var { useService } = require('@web/core/utils/hooks');\n        var { registry } = require('@web/core/registry');\n        var { DbExpirationTag } = require('@saas_trial/owl/db_expiration_tag/db_expiration_tag');\n        const { ResendActivationEmailModal } = require('@saas_trial/owl/resend_activation_email_modal/resend_activation_email_modal');\n        const { computeTimeLeft } = require('@saas_trial/dates');\n        var { session } = require(\"@web/session\");\n        var { browser } = require(\"@web/core/browser/browser\");\n\n        var { _t } = require(\"@web/core/l10n/translation\");\n        var { escape } = require(\"@web/core/utils/strings\");\n\n        // use same method as saas_session owl service\n        if (!s_version.check(17, 1)) {\n            session.isAdmin = session.is_admin || session.is_system;\n        } else {\n            const userModule = odoo.loader.modules.get(\"@web/core/user\");\n            const user = userModule.user;\n            session.isAdmin = user.isAdmin || user.isSystem;\n        }\n\n        // Use Milk-compatible template\n        if (s_version.check(16, 3)) {\n            DbExpirationTag.template = \"saas_trial.DbExpirationTagMilk\";\n        }\n\n        var community_setup = WebClient.prototype.setup;\n        WebClient.prototype.setup = function() {\n            var notificationService = useService(\"notification\");\n            var _onSaasTrialNotification = function(notifications) {\n                    notifications.forEach(function (notification) {\n                    if (notification.type === 'saas_trial.notification') {\n                        var title = notification.payload.title;\n                        var message = notification.payload.message;\n                        notificationService.add(escape(message), {\n                            title: escape(title),\n                            sticky: notification.payload.sticky,\n                        });\n                    }\n                }, this);\n            };\n            if (s_version.check(16, 1)) {\n                this.env.services.bus_service.addEventListener('notification', ({ detail: notifications }) => {\n                    _onSaasTrialNotification(notifications);\n                });\n            }\n            // Handle bus service refactor in OWL2.0\n            else {\n                var bus_service = useService(\"bus_service\");\n                bus_service.addEventListener('notification', ({ detail: notifications }) => {\n                    _onSaasTrialNotification(notifications);\n                });\n            }\n            community_setup.call(this);\n\n            // Pending Activation button\n            registry.category(\"systray\").add(\n                'saas_trial.db_expiration_tag_backend',\n                { Component: DbExpirationTag },\n                { sequence: 100 }\n            );\n\n            // Rolling Release button\n            var { RollingReleaseSystray } = require(\"@saas_trial/owl/rolling_release/rolling_release\");\n            registry.category(\"systray\").add(\n                'saas_trial.rolling_release_systray',\n                { Component: RollingReleaseSystray },\n                { sequence: 99 }\n            );\n\n            // On mobile, odoo_account is removed from registry to avoid to leave the apps\n            // we we don't need to rename it, and we don't add a new link that go out the application\n            if ('odoo_account' in registry.category(\"user_menuitems\").content) {\n                // Change /my -> /my/databases\n                const superOdooAccount = registry.category(\"user_menuitems\").get('odoo_account');\n\n                const myDatabases = function (env) {\n                    const res = superOdooAccount(env);\n                    res.description = _t('My databases');\n                    res.callback = async function () {\n                        const trial_info = env.services.saas_meta;\n                        if (trial_info.status === 'to_activate') {\n                            const expirationDate = trial_info.expiry_oe ? deserializeDateTime(trial_info.expiry_oe)\n                                : luxon.DateTime.fromISO(trial_info.expiry, {zone: 'utc'});\n                            env.services.dialog.add(ResendActivationEmailModal, {\n                                \"activationEmail\": trial_info.activation_email,\n                                \"userEmail\": trial_info.user_email,\n                                \"timeLeft\": computeTimeLeft(expirationDate),\n                            });\n                        } else {\n                            st_exports.assign_location(\"/saas_trial/link_oauth_uid\");\n                        }\n                    };\n                    return res;\n                };\n                registry.category(\"user_menuitems\").add('odoo_account', myDatabases, {force: true});\n\n                if (session.isAdmin) {\n                    // add my subscription submenu\n                    const mySubscriptionItem = function (env) {\n                        const res = superOdooAccount(env);\n                        res.description = _t('My subscription');\n                        res.callback = function () {\n\n                            const trial_info = env.services.saas_meta;\n                            if (trial_info.status === 'to_activate') {\n                                const expirationDate = trial_info.expiry_oe ? deserializeDateTime(trial_info.expiry_oe)\n                                    : luxon.DateTime.fromISO(trial_info.expiry, {zone: 'utc'});\n                                env.services.dialog.add(ResendActivationEmailModal, {\n                                    \"activationEmail\": trial_info.activation_email,\n                                    \"userEmail\": trial_info.user_email,\n                                    \"timeLeft\": computeTimeLeft(expirationDate),\n                                });\n                            }\n                            else{\n                                env.services.action.doAction({\n                                    type: 'ir.actions.client',\n                                    name: _t('My Subscription'),\n                                    tag: 'saas_trial.MyDatabase',\n                                    path: 'my-subscription',\n                                });\n                            }\n                        };\n                        return res;\n                    };\n                    registry.category(\"user_menuitems\").add('odoo_subscription', mySubscriptionItem);\n                    const separator = function (seq) {\n                        return function () {\n                            return {\n                                type: \"separator\",\n                                sequence: seq,\n                            };\n                        };\n                    };\n                    registry.category(\"user_menuitems\").add(\"separator3\", separator(62));\n                }\n            }\n\n        };\n\n        const st_exports = {\n            assign_location(href) {\n                browser.location.href = href;\n            }\n        };\n\n        return st_exports;\n    });\n\nif (s_version.check(16, 3)) {\n    // 'web_tour.tour' has been removed in saas-16.3 (with the convertion of\n    // tours to owl), add compat module so that tour can be used for all\n    // versions\n    odoo.define('web_tour.tour', [\"@web/core/registry\"], function (require) {\n        const { registry } = require(\"@web/core/registry\");\n        class TourManager {\n            register() {\n                const args = Array.prototype.slice.call(arguments);\n                const last_arg = args[args.length - 1];\n                const tour_name = args[0];\n                if (registry.category(\"web_tour.tours\").get(tour_name, undefined) !== undefined) {\n                    console.warn(\"Tour \" + tour_name + \" is already defined\");\n                    return;\n                }\n                const options = args.length === 2 ? {} : args[1];\n                const steps = last_arg instanceof Array ? last_arg : [last_arg];\n                if (s_version.check(17, 4)) {\n                    // only standard TourStep keys are now accepted, so we need\n                    // remove incompatible/custom keys that were used for\n                    // multi-versions tours.\n                    const incompatibleTourStepOptions = ['isCheck', 'plan'];\n                    if (!s_version.check(18, 4)) {\n                        incompatibleTourStepOptions.push('expectUnloadPage');\n                    }\n                    if (s_version.check(18, 0)) {\n                        incompatibleTourStepOptions.push('allowDisabled');\n                        const validTourOptions = [\n                            \"name\",\n                            \"saveAs\",\n                            \"steps\",\n                            \"url\",\n                            \"wait_for\",\n                        ];\n                        if (!s_version.check(18, 1)) {\n                            // checkDelay is only used in version <=18.0\n                            validTourOptions.push(\"checkDelay\");\n                        }\n                        for (const key in options) {\n                            if (!validTourOptions.includes(key)) {\n                                delete options[key];\n                            }\n                        }\n                    }\n                    steps.map(step => {\n                        for (const key of incompatibleTourStepOptions) {\n                            if (key == 'isCheck' && step.isCheck === true) {\n                                // remove legacy empty 'run' method for check\n                                delete step.run;\n                            }\n                            delete step[key];\n                        }\n                    });\n                }\n                if (!s_version.check(18, 0) && options.checkDelay === undefined) {\n                    // lower default checkDelay for versions before 18.0\n                    options.checkDelay = 250;\n                }\n                const tourOptionsWithSteps = Object.assign({}, options, {\n                    // >=saas~16.4: steps have to be a function that return a list (see odoo/odoo@8bfa76a84)\n                    // <=saas~16.3: steps have to be a list\n                    steps: s_version.check(16, 4) ? (() => { return steps; }) : steps,\n                });\n                registry.category(\"web_tour.tours\").add(tour_name, tourOptionsWithSteps);\n            }\n        }\n        return new TourManager();\n    });\n}\n\nif (s_version.check(17, 3) && !s_version.check(19, 0)) {\n    // Commit https://github.com/odoo/odoo/commit/29bd1ddbd4fd introduce\n    // a refactoring of tour action helpers, this re-add actions that were\n    // removed to keep compat with existing cross-versions tours\n    var tour_helpers_dep = '@web_tour/tour_service/tour_utils';\n    var tour_helpers_class = 'RunningTourActionHelper';\n    if (s_version.check(17, 4)) {\n        tour_helpers_dep = '@web_tour/tour_service/tour_helpers';\n        tour_helpers_class = 'TourHelpers';\n    }\n    odoo.define('saas_trial.web_tour_step_action_compat', [tour_helpers_dep], function (require) {\n        const TourHelpers = require(tour_helpers_dep)[tour_helpers_class];\n\n        TourHelpers.prototype.text = function(value, selector) {\n            const element = this._get_action_element(selector);\n            if (element.tagName === \"SELECT\") {\n                // try matching selection option by value, then by label\n                try {\n                    return this.select(value, selector);\n                } catch {\n                    return this.selectByLabel(value, selector);\n                }\n            } else {\n                // TODO: try autodetecting Odoo WYSIWYG editor and call \"editor()\" instead\n                return this.edit(value, selector);\n            }\n        };\n        TourHelpers.prototype.text_blur = function(value, selector) {\n            const prom = this.text(value, selector);\n            this.anchor.dispatchEvent(new Event('focusout'));\n            this.anchor.dispatchEvent(new Event('blur'));\n            return prom;\n        };\n        TourHelpers.prototype.remove_text = function (value, selector) {\n            return this.clear(value, selector);\n        };\n    });\n}\n\n})();\n", "/** @odoo-module */\nimport { _t } from \"@web/core/l10n/translation\";\nimport { sprintf } from \"@web/core/utils/strings\";\nconst { DateTime } = luxon;\n\n/**\n * Compute the human-readable duration before the expiration date of a database\n */\nexport function computeTimeLeft(expirationDate) {\n\n    const now = DateTime.local().toUTC();\n    const diff = expirationDate.diff(now, [\"days\", \"hours\", \"minutes\"]);\n\n    let timeLeft = _t(\"a few moments\");\n    if (diff.days > 0) {\n        timeLeft = sprintf(_t(\"%s days\"), diff.toFormat(\"d\"));\n    } else if (diff.hours > 0) {\n        timeLeft = sprintf(_t(\"%s hours\"), diff.toFormat(\"h\"));\n    } else if (diff.minutes > 0) {\n        timeLeft = sprintf(_t(\"%s minutes\"), diff.toFormat(\"m\"));\n    }\n    return timeLeft;\n}\n", "/** @odoo-module */\n\nimport { ConfirmationDialog } from \"@web/core/confirmation_dialog/confirmation_dialog\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { useState, onWillStart } from \"@odoo/owl\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { getHelpDeskModuleFields } from '@saas_trial/owl/saas_compat';\n\nexport class SaasConfirmationDialog extends ConfirmationDialog {\n    setup() {\n        super.setup();\n        this.orm = useService(\"orm\");\n        this.saas_meta = useService(\"saas_meta\");\n        this.saas_user_seats_service = useService(\"saas_user_seats\");\n        this.state = useState({\n            title: _t(\"You are about to modify your pricing plan\"),\n            moduleNames: this.props.moduleNames || [],\n            appId: this.props.appId,\n            paidFeature: this.props.paidFeature,\n            is_setting: this.props.is_setting,\n            is_free: this.saas_meta.mode === 'trial' && Boolean(this.saas_meta.extra_apps) === false,\n            is_edu: this.saas_meta.is_edu,\n            has_errors: false,\n            is_paid_standard_plan: this.saas_meta.mode === 'paid',  // Custom Plan dont need any confirmation check\n            has_seats: true,\n        });\n\n        var self = this;\n\n        onWillStart(async () => {\n\n            this.saas_user_seats = await this.saas_user_seats_service.fetch();\n            this.state.has_seats = this.saas_user_seats.billed === 0 || this.saas_user_seats.remaining > 0; // Free/Trial mode (unlimited) or Paid mode with seats available\n\n            if (!this.state.appId && !this.state.moduleNames.length && !this.state.paidFeature) {\n                const magicHelpdeskModuleFields = await getHelpDeskModuleFields();\n                // No app id, module names nor paid features provided, try to guess\n                // it from the target element\n                // Note: in case of helpdesk.team, $button will actually target a\n                //  form field, not a button\n                var name = this.props.btnEl.attr('name') || this.props.btnEl.parent().attr('name');\n                var isMagic = Object.hasOwn(magicHelpdeskModuleFields, name);\n                if (name) {\n                    name = isMagic ? magicHelpdeskModuleFields[name] : name.replace('module_', '');\n                    this.state.moduleNames = [name];\n                }\n            }\n\n            this.state.is_setting = this.state.moduleNames.length && !this.state.appId && !this.state.paidFeature;\n\n            var data = self.state.appId ? { module_ids: [self.state.appId] } : { module_names: self.state.moduleNames };\n            if (self.state.paidFeature &&\n                self.state.paidFeature !== 'res.config.settings' &&\n                self.state.paidFeature !== \"helpdesk.team\") {\n                data.paid_feature = self.state.paidFeature;\n            }\n\n            if (self.state.paidFeature === \"res.users\") {\n\n                if (this.saas_user_seats === null) {\n                    // Couldn't reach accounts server\n                    self.state.title = _t(\"Warning\");\n                    self.state.has_errors = true;\n                    self.result = false;\n                } else  {\n                    self.state.title = _t(\"Users Invitations\");\n                }\n\n            } else if ((self.state.is_paid_standard_plan) && (\n                    (self.state.moduleNames.includes('web_studio')) ||\n                    (self.state.paidFeature === \"res.company\") ||\n                    (self.state.paidFeature === \"ir.model.fields\") ||\n                    (self.state.paidFeature === \"ir.actions.server\") ||\n                    (self.state.paidFeature === \"ir.cron\")\n                )\n            ) {\n                self.state.title = _t(\"Pricing Upgrade\");\n            }\n\n            const result = await this.orm.call(\n                \"ir.module.module\",\n                \"get_paying_deps\",\n                [],\n                data,\n            );\n\n            if (result === false) {\n                // Means we can't determine whether the plan will change or not\n                self.state.title = _t(\"Warning\");\n                self.state.has_errors = true;\n\n            } else if (\n                // No paying dependencies, edu mode or user seats available\n                (self.state.is_edu || (result && result.length == 0)) &&\n                (self.state.paidFeature !== \"res.users\" || self.state.has_seats)\n            ) {\n                // Dont Render and resume app/feature installation\n                self.onConfirmAction();\n            }\n        });\n    }\n\n    async onConfirmAction() {\n        if (this.props.btnEl && this.props.btnEl.disabled) {\n            this.props.btnEl.textContent = _t(\"Installing...\");\n        }\n        this.props.confirm();\n        this.props.close();\n    }\n\n    async _cancel() {\n        super._cancel();\n        await this.onCancelAction();\n    }\n\n    // Needed for saas-17.2+\n    async _dismiss() {\n        super._dismiss();\n        await this.onCancelAction();\n    }\n\n    async onCancelAction() {\n        if (this.props.btnEl && this.props.btnEl.disabled) {\n            this.props.btnEl.textContent = _t(\"Install\");\n        }\n        const installBtns = document.getElementsByClassName(\"button_immediate_install\");\n        Array.from(installBtns).forEach(e => {\n            e.removeAttribute(\"disabled\");\n        });\n\n        // Call the cancel callback and close the modal.\n        if (this.props.cancel) {\n            this.props.cancel();\n        }\n        this.props.close();\n    }\n}\n\nSaasConfirmationDialog.props = {\n    ...ConfirmationDialog.props,\n    title: { type: String, optional: true },\n    body: { type: String, optional: true },\n    btnEl: { type: HTMLElement, optional: true },\n    appId: { type: Number, optional: true },\n    moduleNames: { type: Array, optional: true },\n    paidFeature: { type: String, optional: true },\n    is_setting: { type: Boolean, optional: true },\n};\nSaasConfirmationDialog.template = \"saas_trial.SaasConfirmationDialog\";\n", "/** @odoo-module **/\nimport { useService } from \"@web/core/utils/hooks\";\nimport { computeTimeLeft } from '@saas_trial/dates';\nimport { ResendActivationEmailModal }\nfrom \"@saas_trial/owl/resend_activation_email_modal/resend_activation_email_modal\";\n\n\nconst { Component, useState } = owl;\nconst { DateTime } = luxon;\n\nexport class DbExpirationTag extends Component {\n    setup() {\n        this.saas_meta = useService(\"saas_meta\");\n        this.dialog = useService(\"dialog\");\n\n        let expirationDate;\n        if (this.saas_meta.expiry_oe) {\n            expirationDate = this._parseExpirationDate(this.saas_meta.expiry_oe);\n        } else if (this.saas_meta.expiry) {\n            expirationDate = DateTime.fromISO(this.saas_meta.expiry, {zone: \"utc\"});\n        }\n\n        this.state = useState({\n            timeLeft: expirationDate ? computeTimeLeft(expirationDate) : '',\n            display: this.saas_meta.status === 'to_activate'\n        });\n\n        if (this.state.display) {\n            this._launchTimer();\n        }\n    }\n\n    async openResendModal() {\n        const props = {\n            \"activationEmail\": this.saas_meta.activation_email,\n            \"userEmail\": this.saas_meta.user_email,\n            \"timeLeft\": this.state.timeLeft\n        };\n        this.dialog.add(ResendActivationEmailModal, props);\n    }\n\n    /**\n    * @private\n    * @param {string} date\n    * @returns {luxon.DateTime}\n    * SaaS expiration dates are always localized in UTC\n    */\n     _parseExpirationDate(date) {\n        const fmt = \"yyyy-MM-dd hh:mm:ss\";\n        return DateTime.fromFormat(date, fmt, {zone: \"utc\", numberingSystem: \"latn\"});\n    }\n\n    _launchTimer() {\n        const expirationDateStr =  this.saas_meta.expiry_oe;\n        const expirationDate = this._parseExpirationDate(expirationDateStr);\n        this._refreshTimeLeft(expirationDate);\n    }\n\n    _refreshTimeLeft(expirationDate, timeout) {\n        const self = this;\n\n        if (this.state.display) {\n            setTimeout(function() {\n                self.state.timeLeft = computeTimeLeft(expirationDate);\n                self._refreshTimeLeft(expirationDate, timeout || 60 * 1000 );\n            }, timeout || 1);\n        }\n    }\n}\n\nDbExpirationTag.template = \"saas_trial.DbExpirationTag\";\nDbExpirationTag.props = {};\n", "/** @odoo-module **/\nimport { useService } from \"@web/core/utils/hooks\";\nimport { serializeDate, deserializeDateTime, parseDate, formatDate } from \"@web/core/l10n/dates\";\nimport { ExpirationPanel } from \"@web_enterprise/webclient/home_menu/expiration_panel\";\nimport { session } from \"@web/session\";\nimport {\n    ResendActivationEmailModal\n} from \"@saas_trial/owl/resend_activation_email_modal/resend_activation_email_modal\";\nimport { computeTimeLeft } from '@saas_trial/dates';\n\n\nconst { useRef, useState } = owl;\nconst { DateTime } = luxon;\n\nexport class SaasExpirationPanel extends ExpirationPanel {\n    setup() {\n        this.saas_meta = useService(\"saas_meta\");\n        this.dialog = useService(\"dialog\");\n        this.orm = useService(\"orm\");\n        this.rpc = useService(\"rpc\");\n\n        this.inputRef = useRef(\"input\");\n\n        this.state = useState({\n            displayRegisterForm: false,\n            message: false,\n            display: false,\n            isSendFail: false,\n            timer: false,\n            expirationDate: false,\n            eduEndOfLifeDate: false,\n            alreadyLinkedSendMailUrl: false,\n            mailDeliveryStatus: false,\n            mailDeliveryStatusError: false,\n        });\n\n        let expirationDate = false;\n        if (this.saas_meta.expiry_oe) {\n            expirationDate = deserializeDateTime(this.saas_meta.expiry_oe);\n        } else if (this.saas_meta.expiry) {\n            expirationDate = DateTime.fromISO(this.saas_meta.expiry, {zone: \"utc\"});\n        }\n        this.state.expirationDate = expirationDate;\n\n        if (this.saas_meta.edu_eol_date) {\n            this.state.eduEndOfLifeDate = parseDate(this.saas_meta.edu_eol_date);\n        }\n\n        // Ignore init if 1-App free\n        if (['to_activate', 'pending'].includes(this.saas_meta.status) || this.saas_meta.extra_apps) {\n            // If no expirationDate, assume no expiration\n            if (expirationDate) {\n                [this.state.timeLeft, this.state.alertType, this.state.display] = this._computeTimeLeft(expirationDate);\n\n                // We ignore the expiration date if the subscription is paid and open\n                this.state.display = (this.saas_meta.status === 'pending' && (this.saas_meta.expiry || this.saas_meta.expiry_oe)) ||\n                                     this.saas_meta.mode !== 'paid';\n            }\n        }\n        // for edu, start displaying a warning at least 2 months before expiration\n        else if (this.saas_meta.is_edu && this.saas_meta.mode == \"trial\" && this.saas_meta.status == \"activated\") {\n            if (expirationDate) {\n                [this.state.timeLeft, this.state.alertType, this.state.display] = this._computeTimeLeft(expirationDate);\n                if (this.state.eduEndOfLifeDate && this.state.eduEndOfLifeDate < expirationDate) {\n                    // if version will be EOL before the database expires, do not display\n                    // the database expiration warning\n                    this.state.display = false;\n                }\n            }\n        }\n\n        if (this.state.display) {\n            this._launchTimer();\n        }\n    }\n\n    // Prevent \"Standard\" ExpirationPanel to block the UI\n    mounted() {}\n\n    // Disable this method for the SaaS, the user shouldn't be able to close the panel\n    _onHide() {}\n\n    // Disable this standard method as it's fully replaced on our saas by `_computeTimeLeft()`\n    _computeDiffDays(date) {}\n\n\n    _redirect(url) {\n        window.location.assign(url);\n    }\n\n    /**\n     * @param {DateTime} date\n     * @returns {string}\n     */\n    formatDate(date) {\n        return formatDate(date, { format: \"DDD\", timezone: true });\n    }\n\n    _computeTimeLeft(expirationDate) {\n        const now = DateTime.local().toUTC();\n        const diff = expirationDate.diff(now, [\"days\", \"hours\", \"minutes\"]);\n\n        const display = expirationDate < now.plus({months: this.saas_meta.is_edu ? 2 : 1});\n\n        let alertType = \"info\";\n        if (diff.days <= 1) {\n            alertType = \"danger\";\n        } else if (diff.days <= 15) {\n            alertType = \"warning\";\n        }\n\n        const timeLeft = computeTimeLeft(expirationDate);\n\n        return [timeLeft, alertType, display];\n    }\n\n    _launchTimer() {\n        const expirationDateStr =  this.saas_meta.expiry_oe;\n        const expirationDate = deserializeDateTime(expirationDateStr);\n        this._refreshBanner(expirationDate);\n    }\n\n    _refreshBanner(expirationDate, timeout) {\n        const self = this;\n\n        if (self.state.display) {\n            self.state.timer = setTimeout(function() {\n                [self.state.timeLeft, self.state.alertType, self.state.display] = self._computeTimeLeft(expirationDate);\n                self._refreshBanner(expirationDate, timeout || 60 * 1000 );\n            }, timeout || 1);\n        }\n    }\n\n    /**\n     * @private\n     */\n    _getEducationLink() {\n        return session.odoo_website_url + \"/trial?edu\";\n    }\n\n    /**\n     * @private\n     */\n    async _onBuy() {\n        const limitDate = serializeDate(DateTime.utc().minus({ days: 15 }));\n        const args = [\n            [\n                [\"share\", \"=\", false],\n                [\"login_date\", \">=\", limitDate],\n            ],\n        ];\n        const nbUsers = await this.orm.call(\"res.users\", \"search_count\", args);\n        this._redirect(`/saas_trial/upgrade?num_users=${nbUsers}`);\n    }\n\n    /**\n    * @private\n    */\n    async _onRenew() {\n        this._redirect(session.odoo_website_url + '/my/subscription/');\n    }\n\n    /**\n    * @private\n    */\n    async _onResendActivationLink() {\n        const props = {\n            \"activationEmail\": this.saas_meta.activation_email,\n            \"userEmail\": this.saas_meta.user_email,\n            \"timeLeft\": this.state.timeLeft\n        };\n        this.dialog.add(ResendActivationEmailModal, props);\n    }\n\n    /**\n     * Save the registration code then triggers a ping to submit it.\n     * @private\n     */\n     async _onCodeSubmit() {\n        const input = this.inputRef.el;\n        const enterpriseCode = input.value;\n        if (!enterpriseCode) {\n            const inputTitle = input.getAttribute(\"title\");\n            input.setAttribute(\"placeholder\", inputTitle);\n            return;\n        }\n\n        const codeResponse = await this.rpc('/saas_worker/register_subscription_code',\n            {\n                code: enterpriseCode\n            });\n\n        const supportedErrors = [\n            'db_not_found',\n            'invalid_code',\n            'code_already_used',\n            'already_linked'\n        ];\n\n        if (codeResponse.success) {\n            this.state.message = \"success\";\n            this.state.displayRegisterForm = false;\n            this.state.alertType = \"success\";\n\n            // Stop the Expiration Timer\n            if (this.state.timer) {\n                clearTimeout(this.state.timer);\n            }\n\n            // Hide the banner completely after 5sec\n            await new Promise(r => setTimeout(r, 5000));\n            this.state.display = false;\n        } else {\n            if (supportedErrors.includes(codeResponse.error)) {\n                this.state.message = codeResponse.error;\n                if (codeResponse.error == 'code_already_used') {\n                    this.state.alreadyLinkedSendMailUrl = codeResponse.database_already_linked_send_mail_url;\n                }\n\n            } else {\n                this.state.message = \"unknown_error\";\n            }\n            this.state.alertType = \"danger\";\n            this.state.isSendFail = true;\n            this.state.displayRegisterForm = true;\n        }\n    }\n\n    async sendUnlinkEmail() {\n        const sendUnlinkInstructionsUrl = this.state.alreadyLinkedSendMailUrl;\n        this.state.mailDeliveryStatus = \"ongoing\";\n        const { result, reason } = await this.rpc(sendUnlinkInstructionsUrl);\n        if (result) {\n            this.state.mailDeliveryStatus = \"success\";\n        } else {\n            this.state.mailDeliveryStatus = \"fail\";\n            this.state.mailDeliveryStatusError = reason;\n        }\n    }\n\n}\n\nSaasExpirationPanel.template = \"saas_trial.SaasExpirationPanel\";\n", "/** @odoo-module */\n\nimport { resConfigInviteUsers } from \"@web/webclient/settings_form_view/widgets/res_config_invite_users\";\nimport { ConfirmationDialog } from \"@web/core/confirmation_dialog/confirmation_dialog\";\nimport { useService, } from \"@web/core/utils/hooks\";\nimport { Dialog } from \"@web/core/dialog/dialog\";\nimport { registry } from \"@web/core/registry\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { patchCompat } from \"@saas_trial/owl/saas_compat\";\nimport { onWillStart } from \"@odoo/owl\";\n\nconst ResConfigInviteUsers =\n    resConfigInviteUsers === undefined ?\n        // >= saas-16.2: class is exported as \"component\"\n        // >=16.0 && <= saas-16.1, did not export the class so get it from the registry instead\n        registry.category(\"view_widgets\").get(\"res_config_invite_users\") : resConfigInviteUsers.component;\n\n\nclass WarnDialog extends Dialog {}\nWarnDialog.template = \"saas_trial.WarnDialog\";\n\nclass WarnConfirmationDialog extends ConfirmationDialog {}\nWarnConfirmationDialog.components = {Dialog: WarnDialog};\n\npatchCompat(ResConfigInviteUsers.prototype, \"saas_trial.ResConfigInviteUsers\", {\n    setup() {\n        if (this._super) {\n            this._super(...arguments);\n        } else {\n            super.setup();\n        }\n        this.saas_meta = useService(\"saas_meta\");\n        this.saas_user_seats_service = useService(\"saas_user_seats\");\n        this.dialogService = useService(\"dialog\");\n\n        onWillStart(async () => {\n            this.saas_user_seats = await this.saas_user_seats_service.fetch();\n        });\n    },\n\n    async sendInvite() {\n        const self = this;\n        const is_free = (this.saas_meta.mode === \"trial\" && Boolean(this.saas_meta.extra_apps) === false)\n                        || this.saas_user_seats.billed === 0\n                        || this.saas_user_seats.remaining > 0;\n        const confirm = this._super ? this._super.bind(this) : async () => { await super.sendInvite(); };\n\n        if (!is_free) {\n            var dialogProps = {\n                title: _t(\"Users Invitations\"),\n                body: _t(\n                    \"Inviting user(s) in your database will increase the amount of your subscription. \" +\n                    \"Click on invite users to continue.\"\n                ),\n                confirmLabel: _t(\"Invite User(s)\"),\n                confirm: confirm,\n                cancel: () => { },\n            };\n            return new Promise((resolve) => {\n                self.dialogService.add(WarnConfirmationDialog, dialogProps, {\n                    onClose: resolve.bind(null, false),\n                });\n            });\n        } else {\n            if (this._super) {\n                return await this._super();\n            }\n            return await super.sendInvite();\n        }\n    }\n});\n", "/** @odoo-module **/\nimport { useService } from \"@web/core/utils/hooks\";\n\n\nconst { Component, useState } = owl;\n\nexport class MailLimitPanel extends Component {\n    setup() {\n        this.saas_meta = useService(\"saas_meta\");\n\n        this.state = useState({\n            display: this.saas_meta.email_daily_count >= this.saas_meta.email_daily_limit,\n        });\n    }\n}\n\nMailLimitPanel.template = \"saas_trial.MailLimitPanel\";\nMailLimitPanel.props = {};\n", "/** @odoo-module **/\n\nimport { registry } from \"@web/core/registry\";\n\nimport { Component, onWillStart, useState, onMounted } from  \"@odoo/owl\";\nimport { useService } from \"@web/core/utils/hooks\";\n\nclass MyDatabase extends Component {\n    setup() {\n        this.orm = useService(\"orm\");\n        this.consts = useService(\"saas_meta\");\n        this.state = useState({\n            db_uuid: false,\n            token: false,\n            url: this.consts.website,\n        });\n        onWillStart(this.willStart);\n\n        onMounted(() => {\n          window.addEventListener(\"message\", (event) => {\n            if (event.origin !== this.state.url) {return;}\n\n            const { action, url } = event.data;\n\n            if (action === \"redirect\" && url) {\n              window.location.href = url;\n            } else if (action === \"reload\") {\n              window.location.reload();\n            }\n          });\n        });\n    }\n    willStart() {\n        this._get_info();\n    }\n    async _get_info(){\n        const response = await fetch('/saas_worker/db/info/json', {\n            method: 'POST',\n            headers: {'Content-Type': 'application/json'},\n            body: JSON.stringify({}),\n        });\n        this.state.token = (await response.json()).result;\n    }\n}\nMyDatabase.template = \"MyDatabase\";\n\nregistry.category(\"actions\").add(\"saas_trial.MyDatabase\", MyDatabase);\n", "/** @odoo-module */\n\nimport { useService } from \"@web/core/utils/hooks\";\nimport { ConfirmationDialog } from \"@web/core/confirmation_dialog/confirmation_dialog\";\nimport { _t } from \"@web/core/l10n/translation\";\n\n\nconst { useState } = owl;\n\nexport class ResendActivationEmailModal extends ConfirmationDialog {\n    setup() {\n        super.setup();\n        this.rpc = useService(\"rpc\");\n        this.saas_session = useService(\"saas_session\");\n\n        this.state = useState({\n            timeLeft: this.props.timeLeft,\n            activationEmail: this.props.activationEmail,\n            buttonText: _t('Resend'),\n            isAdmin: this.saas_session.isAdmin,\n            isButtonDisabled: false,\n            isSendingDone: false,\n            resultMessage: '',\n            userEmail: this.props.userEmail,\n        });\n\n        this.title = _t(\"Activation Pending!\");\n    }\n\n    async _send_email () {\n\n        this.state.buttonText = _t(\"Sending...\");\n        this.state.isButtonDisabled = true;\n        const result = await this.rpc('/saas_worker/send_activation_email', {'activation_email': this.state.userEmail});\n\n        if(result.success) {\n            this.state.resultMessage = result.success;\n        } else {\n            this.state.resultMessage = result.error;\n        }\n        this.state.isSendingDone = true;\n    }\n}\n\nResendActivationEmailModal.template = \"saas_trial.ResendActivationEmailModal\";\nResendActivationEmailModal.props = Object.assign(Object.create(ConfirmationDialog.props), {\n    title: { type: String, optional: true },\n    body: { type: String, optional: true },\n    activationEmail: String,\n    userEmail: String,\n    timeLeft: String,\n});\n", "/** @odoo-module **/\n\nimport {\n    Component,\n    onWillStart,\n    useState,\n    useRef,\n    useEffect,\n    useExternalListener,\n} from \"@odoo/owl\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport {\n    RollingReleaseDialog,\n    RollingReleaseConfirmUpgradeDialog,\n} from \"./rolling_release_dialog\";\n\n\nexport class RollingReleaseSystray extends Component {\n    setup() {\n        super.setup();\n        this.rpc = useService(\"rpc\");\n        this.rr = useService(\"rr\");\n        this.dialog = useService(\"dialog\");\n        this.rootRef = useRef(\"root\");\n        this.state = useState({\n            open: false,\n            upgradeAvailable: false,\n            rr_upgrade_lock: false,\n        });\n\n        onWillStart(() => this.fetchSaasTrialRRInfo());\n\n        // Set up auto-close on user click outside on the component\n        useExternalListener(window, \"click\", this.onWindowClicked, { capture: true });\n\n        // Set up UI active element related behavior\n        this.ui = useService(\"ui\");\n        useEffect(\n            () => {\n                Promise.resolve().then(() => {\n                    this.myActiveEl = this.ui.activeElement;\n                });\n            },\n            () => []\n        );\n    }\n\n    fetchSaasTrialRRInfo() {\n        this.state.upgradeAvailable = (\n            (this.rr.state === \"rolling_release_available\") ||\n            (this.rr.state === \"rolling_release_warning\") ||\n            ((this.rr.state === \"failed\") && (!!this.rr.rr_forced_date))\n        );\n        this.state.rr_upgrade_lock = this.rr.rr_upgrade_lock;\n    }\n\n    onWindowClicked(ev) {\n        if (!this.state.open) {\n            return false;\n        }\n\n        // Return if it's a different ui active element\n        // (ex. when the upgrade confirmation modal is open)\n        if (this.ui.activeElement !== this.myActiveEl) {\n            return;\n        }\n\n        // Close if user clicked outside the component\n        const rootEl = this.rootRef.el;\n        const gotClickedInside = rootEl.contains(ev.target);\n        if (!gotClickedInside) {\n            this.state.open = false;\n        }\n    }\n\n    onToggleDropdown() {\n        this.state.open = !this.state.open;\n    }\n\n    onUpgradeNow() {\n        const self = this;\n        this.dialog.add(RollingReleaseConfirmUpgradeDialog, {\n            confirm: async () => {\n                const result = await self._rrUpgradeProduction();\n                if (result !== false) {\n                    // if upgradeNow call succeeded, auto-close the RR systray.\n                    self.state.open = false;\n                }\n            },\n            // closing the dialog or clicking on the cancel button also\n            // close the RR systray\n            cancel: () => {\n                self.state.open = false;\n            },\n        });\n    }\n\n    async onUpgradeDismiss() {\n        await this._rrUpgradeDismiss();\n        this.state.open = false;\n    }\n\n    /**\n     * Request a production upgrade for the current database\n     * @returns Boolean\n     */\n    async _rrUpgradeProduction() {\n        const result = await this.rpc('/saas_trial/rr/production_upgrade', {});\n        const successfullyRequested = result.requested === true;\n        const dialogProps = {\n            title: _t(\"Upgrade confirmation\"),\n        };\n        if (successfullyRequested) {\n            dialogProps.body = _t(\n                \"Production upgrade requested successfully! \" +\n                \"All users will be logged off. \" +\n                \"Sit back and have a cup of tea while we upgrade your database! \" +\n                \"After the upgrade, you'll be able to work with the new \" +\n                \"Odoo version! \ud83c\udf89\"\n            );\n            // hide panel after successful uupgrade request\n            this.state.upgradeAvailable = false;\n        } else {\n            dialogProps.body = result.error || _t(\"Production upgrade request failed.\");\n        }\n        this.dialog.add(RollingReleaseDialog, dialogProps);\n        return successfullyRequested;\n    }\n\n    /**\n     * Dismiss upgrade to the next version\n     * (only for databases which are currently on a LTS version)\n     * @returns Boolean\n     */\n    async _rrUpgradeDismiss() {\n        await this.rpc('/saas_trial/rr/dismiss', {});\n        // always hide panel after dismissing upgrade\n        this.state.upgradeAvailable = false;\n        return true;\n    }\n}\nRollingReleaseSystray.components = {};\nRollingReleaseSystray.props = {};\nRollingReleaseSystray.template = \"saas_trial.RollingReleaseSystray\";\n", "/** @odoo-module **/\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { ConfirmationDialog } from '@web/core/confirmation_dialog/confirmation_dialog';\n\nexport class RollingReleaseDialog extends ConfirmationDialog {}\nRollingReleaseDialog.template = 'saas_trial.RollingReleaseDialog';\n\nRollingReleaseDialog.props = Object.assign({}, ConfirmationDialog.props, {\n    db_manager_btn: { type: Boolean, optional: true },\n});\n\nRollingReleaseDialog.defaultProps = Object.assign({}, ConfirmationDialog.defaultProps, {\n    title: _t(\"Upgrade confirmation\"),\n    confirmLabel: _t(\"Confirm Upgrade\"),\n    db_manager_btn: false,\n});\n\nexport class RollingReleaseConfirmUpgradeDialog extends RollingReleaseDialog {}\nRollingReleaseConfirmUpgradeDialog.template = \"saas_trial.RollingReleaseConfirmUpgradeDialog\";\n\nRollingReleaseConfirmUpgradeDialog.props = Object.assign({}, RollingReleaseDialog.props);\ndelete RollingReleaseConfirmUpgradeDialog.props.body; // this is set in the template\n\nRollingReleaseConfirmUpgradeDialog.defaultProps = Object.assign({}, RollingReleaseDialog.defaultProps, {\n    db_manager_btn: true,\n});\n", "/** @odoo-module */\nimport { patch } from \"@web/core/utils/patch\";\nimport { session } from \"@web/session\";\n\nexport function saas_version_check(major, minor) {\n    const version_info = session.server_version_info;\n    let version_major;\n    if (typeof version_info[0] === \"string\" && version_info[0].includes(\"saas\")) {\n        version_major = parseInt(version_info[0].replace(/[^\\d.]/g, \"\"));\n    } else {\n        version_major = version_info[0];\n    }\n    const version = new Version(version_major, version_info[1]);\n    return version.check(major, minor);\n}\n\nexport function patchCompat(target, name, extension) {\n    if (saas_version_check(17, 0)) {\n        // signature changed: https://github.com/odoo/odoo/commit/04fddc19d4aedd8105e0fda5582288c2bb1833fe\n        return patch(target, extension);\n    } else {\n        return patch(target, name, extension);\n    }\n}\n\n// Some fields on helpdesk.team records trigger module installs\nexport async function getKnownModuleFields() {\n\n    let rpc;\n    if (saas_version_check(17, 2)) {\n        rpc = odoo.loader.modules.get(\"@web/core/network/rpc\").rpc;\n    } else {\n        rpc = odoo.__WOWL_DEBUG__.root.env.services.rpc;\n    }\n    const moduleFields = await rpc('/web/dataset/call_kw', {\n        model: 'ir.module.module',\n        method: 'get_all_field_modules',\n        args: [],\n        kwargs: {},\n    });\n    return moduleFields;\n}\n\n// TODO(JDE): import these functions in saas_trial.js.\nexport function userIsInternalUser(data) {\n    if (saas_version_check(18, 2)) {\n    // Since saas-18.2, the `sel_groups_` fields no longer exist on res.users,\n    // but we can rely on the `share` field instead since it's only set to true\n    // when the user is not an internal user.\n        return !data.share;\n\n    } else {\n    // \"Internal User\" is the first group created during database bootstrap\n    // and it's assigned id 1 (see odoo/addons/base/data/base_data.sql),\n    // guess if user is an employee by checking if it's assigned to selection\n    // group.\n        for (const field in data) {\n            if (field.startsWith(\"sel_groups_\") && data[field] === 1) {\n                return true;\n            }\n        }\n        return false;\n    }\n}\n\nexport function resConfigSettingsModuleNames(data) {\n    // return list of modules that are checked to be installed\n    const moduleNames = [];\n    for (const field in data) {\n        if (field.startsWith(\"module_\") && data[field] === true) {\n            moduleNames.push(field.slice(7));\n        }\n    }\n\n    // Will trigger install of the `sign` module... Blame BIB\n    if ('sign_invoice' in data && data.sign_invoice === true) {\n        moduleNames.push('sign');\n    }\n\n    // Will trigger install of the `hr` module ... Blame PIM\n    if ('pos_module_pos_hr' in data && data.pos_module_pos_hr === true) {\n        moduleNames.push('hr');\n    }\n    return moduleNames;\n}\n\nexport async function getKnownMagicFieldModuleNames(data) {\n    // return list of helpdesk.team fields that may trigger install of new module(s)\n    const moduleNames = [];\n    const magicModuleFields = await getKnownModuleFields();\n    for (const field in magicModuleFields) {\n        if ( Object.hasOwn(data, field) && data[field] === true ) {\n            moduleNames.push(magicModuleFields[field]);\n        }\n    }\n    return moduleNames;\n}\n\nfunction Version(major, minor) {\n    this.major = major;\n    this.minor = minor;\n}\nconst MIN_SUPPORTED_VERSION = 16;\nVersion.prototype.check = function check(major, minor) {\n    if (major < MIN_SUPPORTED_VERSION || (major == MIN_SUPPORTED_VERSION && minor == 0)) {\n        console.warn(`Checking version against ${major}.${minor} which is not supported any more.`);\n    }\n    return (this.major > major) ||\n        (this.major === major && this.minor >= (minor || 0));\n};\n", "/** @odoo-module */\n\n/* This file allows us to add our own components to the list, form and kanban views\nby using the t-slot='saas-display' set up in our override in controller\n*/\n\nimport { ListController } from \"@web/views/list/list_controller\";\nimport { FormController } from \"@web/views/form/form_controller\";\nimport { KanbanController } from \"@web/views/kanban/kanban_controller\";\n\nimport { SaasUserSeats } from \"@saas_trial/owl/saas_user_seats/saas_user_seats\";\n\nListController.components.SaasUserSeats = SaasUserSeats;\nFormController.components.SaasUserSeats = SaasUserSeats;\nKanbanController.components.SaasUserSeats = SaasUserSeats;\n", "/** @odoo-module **/\nimport { registry } from \"@web/core/registry\";\nimport { browser } from \"@web/core/browser/browser\";\nimport { session } from \"@web/session\";\nimport { EventBus } from \"@odoo/owl\";\nimport { saas_version_check } from \"@saas_trial/owl/saas_compat\";\n\n// Session was reworked in saas-17.1. The service hides the implementation\n// details that are version-specific.\nconst sessionService = {\n    start(env) {\n        return {\n            get isAdmin() {\n                // >= saas-17.1\n                if (saas_version_check(17, 1)) {\n                    const userModule = odoo.loader.modules.get(\"@web/core/user\");\n                    const user = userModule.user;\n                    return user.isAdmin || user.isSystem;\n                }\n                // < saas-17.1\n                return session.is_admin || session.is_system;\n            },\n        };\n    },\n};\nregistry.category(\"services\").add(\"saas_session\", sessionService);\n\n\nif (saas_version_check(17, 1)) {\n    // RPC service was removed in saas-17.1 so we re-introduce it.\n    const rpcService = {\n        start(env) {\n            const rpcModule = odoo.loader.modules.get(\"@web/core/network/rpc\");\n            return function rpc(route, params = {}, settings = {}) {\n                return rpcModule.rpc(route, params, settings);\n            };\n        },\n    };\n    registry.category(\"services\").add(\"rpc\", rpcService);\n}\n\nfunction defineUrlService(url) {\n    const service = {\n        dependencies: [\"rpc\"],\n        async start(env, {rpc}) {\n            const result = await rpc(url, {});\n            return result;\n        },\n    };\n    return service;\n}\n\nfunction cacheSaasUserSeats(rpc) {\n\n    const url = '/saas_trial/user_seats';\n    const storage_key = 'saas_user_seats';\n    const expiration = 300; // 5 minutes\n    let rpcPromise = null;\n\n    return {\n        async fetch() {\n            if (rpcPromise) {\n                return rpcPromise;\n            }\n            const localStoredItem = browser.localStorage.getItem(storage_key);\n            let parsedItem;\n            if (localStoredItem) {\n                parsedItem = JSON.parse(localStoredItem);\n            }\n\n            if (parsedItem && parsedItem.expiration > Date.now()) {\n                return parsedItem;\n            } else {\n                rpcPromise = rpc(url, {});\n                const result = await rpcPromise;\n                result.expiration = Date.now() + expiration * 1000;\n                browser.localStorage.setItem(storage_key, JSON.stringify(result));\n                rpcPromise = null;\n                return result;\n            }\n        },\n        async reload() {\n            const result = await rpc(url, {});\n            result.expiration = Date.now() + expiration * 1000;\n            browser.localStorage.setItem(storage_key, JSON.stringify(result));\n        },\n    };\n}\n\n\nfunction defineSaasUserService() {\n\n    const service = {\n        dependencies: [\"rpc\"],\n        async start(env, {rpc}) {\n\n            const bus = new EventBus();\n            const cache = cacheSaasUserSeats(rpc);\n\n            // Since 18.0 RPC uses its own bus to listen to RPC events\n            const rpcBus = (saas_version_check(18, 0)) ? odoo.loader.modules.get(\"@web/core/network/rpc\").rpcBus : env.bus;\n            rpcBus.addEventListener(\"RPC:REQUEST\", async (ev) => {\n\n                if (Object.hasOwn(ev.detail, 'data')) {\n                    const { model, method } = ev.detail.data.params;\n\n                    // arg0 is the user_id, no id means new user\n                    if ((model === 'res.users' && method === 'web_save' && ev.detail.data.params.args[0].length === 0) ||\n                        (model === 'res.config.settings' && method === 'web_create_users')) {\n                            bus.trigger(\"SAAS_USER_SEATS:NEW_USER\");\n                            await cache.reload();\n                    }\n                }\n            });\n\n            return {\n                bus,\n                fetch: cache.fetch,\n                reload: cache.reload,\n            };\n        },\n    };\n    return service;\n}\n\nregistry.category(\"services\").add(\"rr\", defineUrlService('/saas_trial/rr/info'));\nregistry.category(\"services\").add(\"saas_meta\", defineUrlService('/saas_worker/trial_info'));\nregistry.category(\"services\").add(\"saas_user_seats\", defineSaasUserService());\n", "/** @odoo-module **/\n\nimport { Component, useState, onWillStart } from \"@odoo/owl\";\nimport { session } from \"@web/session\";\nimport { registry } from \"@web/core/registry\";\nimport { standardWidgetProps } from \"@web/views/widgets/standard_widget_props\";\nimport { useBus, useService } from \"@web/core/utils/hooks\";\nimport { saas_version_check } from \"@saas_trial/owl/saas_compat\";\n\n\nexport class SaasUserSeats extends Component {\n\n    static template = \"saas_trial.SaasUserSeats\";\n    static components = {};\n    static props = {\n        model: { type: String, optional: true },\n    };\n\n    setup() {\n        super.setup();\n\n        const model = this.props.model || this.props.record?.resModel;\n        this.saas_session = useService(\"saas_session\");\n\n        this.state = useState({\n            displaySeats: undefined,\n            hasError: false,\n            nbSeatsRemaining: 0,\n            odoo_website_url: session.odoo_website_url,\n            resModel: model,\n            isAdmin: this.saas_session.isAdmin,\n        });\n\n\n        this.saas_user_seats_service = useService(\"saas_user_seats\");\n        useBus(this.saas_user_seats_service.bus, \"SAAS_USER_SEATS:NEW_USER\", () => {\n            this.state.nbSeatsRemaining -= 1;\n        });\n\n        onWillStart(async () => {\n\n            this.saas_user_seats = await this.saas_user_seats_service.fetch();\n\n            // Means service is down or odoo.com is unreachable\n            if (!this.saas_user_seats || !('billed' in this.saas_user_seats) || !('remaining' in this.saas_user_seats)) {\n                this.state.hasError = true;\n                return;\n            }\n\n            // 0 seats billed means 1-App Free or Trial\n            if (this.saas_user_seats.billed === 0) {\n               return;\n            }\n\n            if (this.saas_user_seats.billed > 0 && this.state.isAdmin && this.saas_user_seats.plan_type !== \"monthly\") {\n                this.state.nbSeatsRemaining = this.saas_user_seats.remaining;\n                this.state.displaySeats = true;\n            }\n        });\n    }\n}\n\nclass SettingsUserSeatsWidget extends SaasUserSeats {\n    static props = {\n        ...standardWidgetProps,\n    };\n}\n\nlet settingsUserSeatsWidget;\nif (saas_version_check(17, 0)) {\n    settingsUserSeatsWidget = {\n        component: SettingsUserSeatsWidget,\n    };\n} else {\n    settingsUserSeatsWidget = SettingsUserSeatsWidget;\n}\n\nregistry.category(\"view_widgets\").add(\"settings_user_seats\", settingsUserSeatsWidget);\n", "/** @odoo-module **/\n\nimport { SettingsFormCompiler } from \"@web/webclient/settings_form_view/settings_form_compiler\";\nimport { patchCompat, saas_version_check } from \"@saas_trial/owl/saas_compat\";\n\n\npatchCompat(SettingsFormCompiler.prototype, \"SaasSettingsFormCompiler\", {\n\n    /** Dynamically add a new widget to the settings form, this way we don't have\n    * to edit the views xml files.\n    */\n    compile(key, params = {}) {\n\n        let compiled = null;\n        if (saas_version_check(17, 0)) {\n            compiled = super.compile(...arguments);\n        } else {\n            compiled = this._super(...arguments);\n        }\n\n        // Get the existing user widget and clone it\n        var userSeatsWidget = compiled.querySelector('Widget[name=\"\\'res_config_invite_users\\'\"]').cloneNode(true);\n\n        // Change the name attribute so it will be automatically bonded thanks to the registry\n        userSeatsWidget.setAttribute('name', \"'settings_user_seats'\");\n\n        // Remove the not needed widgetInfo\n        userSeatsWidget.removeAttribute(\"widgetInfo\");\n\n        // Insert the new widget into the compiled DOM\n        compiled.querySelector(\n            'Widget[name=\"\\'res_config_invite_users\\'\"]'\n        ).insertAdjacentElement('beforebegin', userSeatsWidget);\n        return compiled;\n    }\n});\n", "/** @odoo-module */\n\nimport { FormController } from \"@web/views/form/form_controller\";\nimport { registry } from \"@web/core/registry\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport {\n    getKnownMagicFieldModuleNames,\n    patchCompat,\n    resConfigSettingsModuleNames,\n    saas_version_check,\n    userIsInternalUser,\n} from \"@saas_trial/owl/saas_compat\";\nimport { SaasConfirmationDialog } from \"@saas_trial/owl/confirm_modal/confirm_modal\";\n\n\n// SERVICES\nexport class PricingService {\n    constructor(env, services) {\n        this.env = env;\n    }\n\n    willEnableCustomPlan(resId, resModel, data) {\n        return (\n            (!(resId) && (\n                (resModel === \"res.company\") ||\n                (resModel === \"ir.model.fields\" && data.compute) ||\n                (resModel === \"ir.actions.server\" && data.state === \"code\") ||\n                (resModel ===\"ir.cron\")\n            )) || (\n                !!(resId) && (\n                    // Means a new child company is about to be created\n                    (resModel === \"res.company\" && data.child_ids?.count > data.all_child_ids?.count)\n                )\n            )\n        );\n    }\n\n    willInstallModules(resId, resModel, data) {\n        const potentialHelpdeskModuleFields = Object.keys(data).filter(key => key.startsWith('use_'));\n        const potentialPosModuleFields = Object.keys(data).filter(key => key.startsWith('module_'));\n        return (\n            !(resId) && (resModel === \"res.config.settings\")\n        ) || (\n            // check both on create and write\n            (resModel === \"helpdesk.team\" && potentialHelpdeskModuleFields.some(field => data[field] === true)) ||\n            (resModel === \"pos.config\" && potentialPosModuleFields.some(field => data[field] === true))\n        );\n    }\n\n    willIncreasePrice(resId, resModel, data) {\n        return (\n            !(resId) && (resModel === \"res.users\" && userIsInternalUser(data))\n        );\n    }\n\n    willUpsell(resId, resModel, data) {\n        return (\n            this.willEnableCustomPlan(resId, resModel, data) ||\n            this.willInstallModules(resId, resModel, data) ||\n            this.willIncreasePrice(resId, resModel, data)\n        );\n    }\n}\n\nexport const pricingService = {\n    dependencies: [],\n    start(env, services) {\n        return new PricingService(env, services);\n    },\n};\n\nregistry.category(\"services\").add(\"saas_trial.pricing\", pricingService);\n\n\n// PATCHES\nconst FormControllerPatch = {\n    setup() {\n        super.setup(...arguments);\n        this.pricing = useService(\"saas_trial.pricing\");\n        this.dialog = useService(\"dialog\");\n\n        // Helpdesk form controller does not respect the standard API and will never interrupt the saving process\n        // So we patch the hook directly from the setup method to ensure we are the first to be called\n        this.model.hooks.onWillSaveRecord = this.saasOnWillSaveRecord.bind(this);\n\n    },\n\n    async saasOnWillSaveRecord(record, changes) {\n        const willUpsell = await this.pricing.willUpsell(record.resId, record.resModel, record.data);\n        if (willUpsell) {\n            let moduleNames = [];\n            if (record.resModel === \"res.config.settings\") {\n                moduleNames = resConfigSettingsModuleNames(record.data);\n            } else if ([\"helpdesk.team\", \"pos.config\"].includes(record.resModel)) {\n                moduleNames = await getKnownMagicFieldModuleNames(record.data);\n            }\n\n            // Pop the confirmation dialog and wait for user choice.\n            const proceed = await new Promise((resolve) => {\n                this.dialog.add(SaasConfirmationDialog, {\n                    moduleNames: moduleNames,\n                    paidFeature: record.resModel,\n                    confirm: () => resolve(true),\n                    cancel: () => resolve(false),\n                });\n            });\n\n            // User refused the upsell, discard data.\n            if (!proceed) {\n                return false;\n            }\n        }\n\n        // Either there was no upsell or the user accepted the upsell.\n        return super.onWillSaveRecord(...arguments);\n    },\n};\n\nif (saas_version_check(17, 0)) {\n    patchCompat(FormController.prototype, \"saas_trial.form_controller\", FormControllerPatch);\n}\n", "(function () {\n\n    const deps = {\n        patch: '@web/core/utils/patch',\n        Configurator: '@website/client_actions/configurator/configurator',\n        SaasFeaturesSelectionScreen: '@saas_website/owl/website_configurator/feature_selection',\n    };\n\n    odoo.define('saas_website.WebsiteFeatureSelector', Object.values(deps), function (require) {\n        const { patch } = require(deps.patch);\n        const configurator = require(deps.Configurator);\n        const { SaasFeaturesSelectionScreen } = require(deps.SaasFeaturesSelectionScreen);\n\n        if (configurator.ROUTES) { // >= saas~18.3\n            patch(configurator.Configurator.prototype, {\n                get currentComponent() {\n                    if (this.state.currentStep === configurator.ROUTES.featuresSelectionScreen) {\n                        return SaasFeaturesSelectionScreen;\n                    }\n                    return super.currentComponent;\n                }\n            });\n        }\n        configurator.Configurator.components.FeaturesSelectionScreen = SaasFeaturesSelectionScreen;\n    });\n\n})();\n", "/** @odoo-module */\n\nimport { ConfirmationDialog } from \"@web/core/confirmation_dialog/confirmation_dialog\";\nimport { _t } from '@web/core/l10n/translation';\n\n\nexport class DnsConfirmModal extends ConfirmationDialog {\n    setup() {\n        super.setup();\n        this.title = _t(\"Want a free domain name ?\");\n    }\n}\n\nconst props = Object.assign({}, ConfirmationDialog.props);\ndelete props.body;\n\nDnsConfirmModal.props = props;\nDnsConfirmModal.template = \"saas_website.DnsConfirmModal\";\n", "\n/** @odoo-module **/\nimport { useService } from \"@web/core/utils/hooks\";\nimport { computeTimeLeft } from '@saas_trial/dates';\nimport { ResendActivationEmailModal }\nfrom \"@saas_trial/owl/resend_activation_email_modal/resend_activation_email_modal\";\nimport { DnsConfirmModal } from \"@saas_website/owl/dns_confirm_modal/dns_confirm_modal\";\nimport { _t } from '@web/core/l10n/translation';\n\n\nconst { Component, useState } = owl;\nconst { DateTime } = luxon;\n\nexport class DnsMenu extends Component {\n    setup() {\n        this.saas_meta = useService(\"saas_meta\");\n        this.dialog = useService(\"dialog\");\n        this.rpc = useService(\"rpc\");\n        this.website = useService('website');\n\n        this.title = _t(\"Domain Name\");\n\n        let expirationDate;\n        if (this.saas_meta.expiry_oe) {\n            expirationDate = this._parseExpirationDate(this.saas_meta.expiry_oe);\n        } else if (this.saas_meta.expiry) {\n            expirationDate = DateTime.fromISO(this.saas_meta.expiry, {zone: \"utc\"});\n        }\n\n        this.state = useState({\n            timeLeft: expirationDate ? computeTimeLeft(expirationDate) : '',\n            checkConfirmModal: true,\n        });\n\n        if (this.saas_meta.status === 'to_activate') {\n            this._launchTimer();\n        }\n    }\n\n    async onClickDnsMenuItem() {\n\n        if (this.saas_meta.status === \"to_activate\") {\n\n            const props = {\n                \"activationEmail\": this.saas_meta.activation_email,\n                \"userEmail\": this.saas_meta.user_email,\n                \"timeLeft\": this.state.timeLeft,\n            };\n            this.dialog.add(ResendActivationEmailModal, props);\n\n        } else if (this.saas_meta.mode === \"trial\" && this.state.checkConfirmModal) {\n\n            const result = await this.rpc('/saas_trial/domain_info', {});\n\n            if (!result.fqdn) {\n                this.dialog.add(DnsConfirmModal, {});\n            } else {\n                this.state.checkConfirmModal = false;\n                this._redirect('/saas_trial/my_dns');\n            }\n        } else if (this.saas_meta.mode === \"paid\") {\n            // No need to show the Warning Dialog in this case because\n            // a domain name is offered with the subscription without needing\n            // a review from the support team.\n            this._redirect('/saas_trial/my_dns');\n        }\n\n    }\n\n    /**\n    * @private\n    * @param {string} date\n    * @returns {luxon.DateTime}\n    * SaaS expiration dates are always localized in UTC\n    */\n    _parseExpirationDate(date) {\n        const fmt = \"yyyy-MM-dd hh:mm:ss\";\n        return DateTime.fromFormat(date, fmt, { zone: \"utc\", numberingSystem: \"latn\" });\n    }\n\n    _launchTimer() {\n        const expirationDateStr = this.saas_meta.expiry_oe;\n        const expirationDate = this._parseExpirationDate(expirationDateStr);\n        this._refreshTimeLeft(expirationDate);\n    }\n\n    _refreshTimeLeft(expirationDate, timeout) {\n        const self = this;\n\n        if (this.state.display) {\n            setTimeout(function () {\n                self.state.timeLeft = computeTimeLeft(expirationDate);\n                self._refreshTimeLeft(expirationDate, timeout || 60 * 1000);\n            }, timeout || 1);\n        }\n    }\n\n    _redirect(url) {\n        window.location.assign(url);\n    }\n}\n\nDnsMenu.template = \"saas_website.DnsMenu\";\n", "/** @odoo-module **/\nimport { useService } from \"@web/core/utils/hooks\";\n\nimport { FeaturesSelectionScreen } from \"@website/client_actions/configurator/configurator\";\n\nexport class SaasFeaturesSelectionScreen extends FeaturesSelectionScreen {\n    setup() {\n        super.setup();\n\n        // Inject saas_info into the Feature selection screen\n        this.saas_meta = useService(\"saas_meta\");\n    }\n\n    show1AppFreeWarning() {\n        return (\n            (Object.values(this.state.features).filter(\n                (feature) => feature.is_1appmax && feature.selected\n            ).length > 1) ||\n            (Object.values(this.state.features).filter(\n                (feature) => feature.is_always_paid && feature.selected\n            ).length > 0)\n        );\n    }\n}\n\nSaasFeaturesSelectionScreen.template = \"saas_website.Configurator.FeatureSelection\";\n", "import { BankRecButtonList } from \"@account_accountant/components/bank_reconciliation/button_list/button_list\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { patch } from \"@web/core/utils/patch\";\n\npatch(BankRecButtonList.prototype, {\n    actionOpenSaleOrders() {\n        this.action.doAction({\n            type: \"ir.actions.act_window\",\n            res_model: \"sale.order\",\n            target: \"current\",\n            views: [\n                [false, \"list\"],\n                [false, \"form\"],\n            ],\n            context: {\n                search_default_partner_id: this.statementLineData.partner_id.id,\n            },\n        });\n    },\n\n    get isSalesButtonShown() {\n        // This is a temporary solution\n        // Should be fixed later by task 5241035\n        return !!this.statementLineData.partner_id.id;\n    },\n\n    get buttons() {\n        const buttonsToDisplay = super.buttons;\n        if (this.isSalesButtonShown) {\n            buttonsToDisplay.sale = {\n                label: _t(\"Sales\"),\n                action: this.actionOpenSaleOrders.bind(this),\n                classes: \"sales-btn\",\n            };\n        }\n        return buttonsToDisplay;\n    },\n});\n", "import { Component, useEffect, useState } from \"@odoo/owl\";\nimport {\n    CustomFieldCard\n} from \"@sale_pdf_quote_builder/js/custom_content_kanban_like_widget/custom_field_card/custom_field_card\";\nimport { x2ManyCommands } from \"@web/core/orm_service\";\nimport { registry } from '@web/core/registry';\nimport { useService } from \"@web/core/utils/hooks\";\nimport { standardWidgetProps } from \"@web/views/widgets/standard_widget_props\";\n\nexport class CustomContentKanbanLikeWidget extends Component {\n    static components = { CustomFieldCard };\n    static template = \"sale_pdf_quote_builder.CustomContentKanbanLike\";\n    static props = {\n        ...standardWidgetProps,\n    };\n\n    setup() {\n        this.orm = useService(\"orm\");\n        this.state = useState({\n            headers: {},\n            lines: {},\n            footers: {},\n        });\n\n        // Initialize the state and update available documents when updating the quotation template.\n        useEffect((saleOrderTemplate) => {\n            this.updateState();\n        }, () => [this.props.record.data.sale_order_template_id]);\n    }\n\n    async updateState() {\n        const saved = await this.props.record.save();  // To display documents of potentially unsaved SOL.\n        if (saved) {  // do not fetch wrong form data if record was not saved.\n            const { headers, lines, footers } = await this.orm.call(\n                'sale.order', 'get_update_included_pdf_params', [this.props.record.resId]\n            )\n            this.state.headers = headers;\n            this.state.lines = lines;\n            this.state.footers = footers;\n        }\n    }\n\n    updateJson() {\n        const selectedHeaders = this.state.headers.files.filter(f => f.is_selected);\n        const selectedFooters = this.state.footers.files.filter(f => f.is_selected);\n        const value = JSON.stringify({\n            'header': Object.assign({}, ...selectedHeaders.map(header => {\n                return {\n                    [header.id]: {\n                        document_name: header.name,\n                        custom_form_fields: Object.assign({}, ...header.custom_form_fields.map(\n                            formField => ({[formField.name]: formField.value})\n                        )),\n                    }\n            }})),\n            'line': Object.assign({}, ...this.state.lines.map(line => {\n                return {\n                    [line.id]: Object.assign({}, ...line.files.filter(f => f.is_selected).map(doc => {\n                        return {\n                            [doc.id]: {\n                                document_name: doc.name,\n                                custom_form_fields: Object.assign({}, ...doc.custom_form_fields.map(\n                                    formField => ({[formField.name]: formField.value})\n                                )),\n                            }\n                    }})),\n            }})),\n            'footer': Object.assign({}, ...selectedFooters.map(footer => {\n                return {\n                    [footer.id]: {\n                        document_name: footer.name,\n                        custom_form_fields: Object.assign({}, ...footer.custom_form_fields.map(\n                            formField => ({[formField.name]: formField.value})\n                        )),\n                    }\n            }})),\n        })\n        this.props.record.update({ ['customizable_pdf_form_fields']: value });\n    }\n\n    async saveProductDocument(lineId, docId, isSelected) {\n        const sol = this.props.record.data.order_line.records.find(\n            sol => sol.resId === lineId\n        );\n        sol._noUpdateParent = true; // Ensure that no rpc will be made to save the changes\n        if (isSelected) {\n            // save is needed to ensure that no onChange call will be made\n            await sol.update({product_document_ids: [x2ManyCommands.link(docId)]}, { save: true });\n        } else {\n            // save is needed to ensure that no onChange call will be made\n            await sol.update({product_document_ids: [x2ManyCommands.unlink(docId)]}, { save: true });\n        }\n        await this.props.record.data.order_line._onUpdate({withoutOnchange: true});\n        this.updateJson();\n    };\n\n    async saveQuotationDocument(docId, isSelected) {\n        if (isSelected) {\n            await this.props.record.update({\n                quotation_document_ids: [\n                    x2ManyCommands.link(docId),\n                ],\n            });\n        } else {\n            await this.props.record.update({\n                quotation_document_ids: [\n                    x2ManyCommands.unlink(docId),\n                ],\n            });\n        }\n        this.updateJson();\n    };\n}\n\nexport const customContentKanbanLikeWidget = {\n    component: CustomContentKanbanLikeWidget,\n};\n\nregistry.category(\"view_widgets\").add(\n    \"customContentKanbanLikeWidget\", customContentKanbanLikeWidget\n);\n", "import { Component, useRef } from \"@odoo/owl\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { useAutoresize } from \"@web/core/utils/autoresize\";\n\nexport class CustomFieldCard extends Component {\n    static template = \"sale_pdf_quote_builder.customFieldCard\";\n    static props = {\n        name: String,\n        value: String,\n        onChange: Function,\n    };\n\n    setup() {\n        this.customFormFieldTextAreaRef = useRef('customFieldCardTextArea');\n        this.placeholder = _t(\"Click to write content for the PDF quote...\");\n        useAutoresize(this.customFormFieldTextAreaRef);\n    }\n\n    expandTextArea(ev) {\n        const textarea = ev.target;\n        textarea.style.height = textarea.scrollHeight+'px';\n    }\n}\n", "import { onWillRender } from \"@odoo/owl\";\nimport { UploadButton } from '@product/js/product_document_kanban/upload_button/upload_button';\nimport { KanbanController } from '@web/views/kanban/kanban_controller';\n\nexport class QuotationDocumentKanbanController extends KanbanController {\n    static components = { ...KanbanController.components, UploadButton };\n\n    setup() {\n        super.setup();\n        this.uploadRoute = '/sale_pdf_quote_builder/quotation_document/upload';\n        this.allowedMIMETypes='application/pdf';\n        onWillRender(() => {\n            this.formData = {\n                'allowed_company_ids': JSON.stringify(this.props.context.allowed_company_ids),\n            };\n        });\n    }\n}\n", "import {\n    productDocumentKanbanView\n} from '@product/js/product_document_kanban/product_document_kanban_view';\nimport {\n    QuotationDocumentKanbanController\n} from '@sale_pdf_quote_builder/js/quotation_document_kanban/quotation_document_kanban_controller';\nimport { registry } from '@web/core/registry';\n\nexport const quotationDocumentKanbanView = {\n    ...productDocumentKanbanView,\n    Controller: QuotationDocumentKanbanController,\n};\n\nregistry.category('views').add('quotation_document_kanban', quotationDocumentKanbanView);\n", "import {\n    ProductDocumentKanbanRenderer\n} from \"@product/js/product_document_kanban/product_document_kanban_renderer\";\nimport { UploadButton } from '@product/js/product_document_kanban/upload_button/upload_button';\nimport { registry } from '@web/core/registry';\nimport { X2ManyField, x2ManyField } from '@web/views/fields/x2many/x2many_field';\n\nexport class QuotationDocumentX2ManyField extends X2ManyField {\n    static template = 'sale_pdf_quote_builder.QuotationDocumentX2ManyField';\n    static components = {\n        ...X2ManyField.components,\n        UploadButton,\n        KanbanRenderer: ProductDocumentKanbanRenderer,\n    };\n\n    setup() {\n        super.setup();\n        this.uploadRoute = '/sale_pdf_quote_builder/quotation_document/upload';\n        this.formData = {\n            'sale_order_template_id': this.props.record.resId,\n        };\n        this.allowedMIMETypes='application/pdf';\n    }\n}\n\nexport const quotationDocumentX2ManyField = {\n    ...x2ManyField,\n    component: QuotationDocumentX2ManyField,\n};\n\nregistry.category('fields').add('quotation_document_many2many', quotationDocumentX2ManyField);\n", "import { Failure } from \"@mail/core/common/failure_model\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { patch } from \"@web/core/utils/patch\";\n\npatch(Failure.prototype, {\n    get iconSrc() {\n        if (this.type === \"snail\") {\n            return \"/snailmail/static/img/snailmail_failure.png\";\n        }\n        return super.iconSrc;\n    },\n    get body() {\n        if (this.type === \"snail\") {\n            if (this.notifications.length === 1 && this.lastMessage?.thread) {\n                return _t(\n                    \"An error occurred when sending a letter with Snailmail on \u201c%(record_name)s\u201d\",\n                    { record_name: this.lastMessage.thread.display_name }\n                );\n            }\n            return _t(\"An error occurred when sending a letter with Snailmail.\");\n        }\n        return super.body;\n    },\n});\n", "import { Notification } from \"@mail/core/common/notification_model\";\nimport { patch } from \"@web/core/utils/patch\";\nimport { _t } from \"@web/core/l10n/translation\";\n\n/** @type {import(\"models\").Notification} */\nconst notificationPatch = {\n    get icon() {\n        if (this.notification_type === \"snail\") {\n            return \"fa fa-paper-plane\";\n        }\n        return super.icon;\n    },\n\n    get statusIcon() {\n        if (this.notification_type === \"snail\") {\n            switch (this.notification_status) {\n                case \"sent\":\n                    return \"fa fa-check\";\n                case \"ready\":\n                    return \"fa fa-clock-o\";\n                case \"canceled\":\n                    return \"fa fa-trash-o\";\n                default:\n                    return \"fa fa-exclamation text-danger\";\n            }\n        }\n        return super.statusIcon;\n    },\n    get failureMessage() {\n        switch (this.failure_type) {\n            case \"sn_credit\":\n                return _t(\"Insufficient Credits\");\n            case \"sn_trial\":\n                return _t(\"No IAP Credits\");\n            case \"sn_price\":\n                return _t(\"Country Not Supported\");\n            case \"sn_fields\":\n                return _t(\"Missing Required Fields\");\n            case \"sn_format\":\n                return _t(\"Format Error\");\n            case \"sn_error\":\n                return _t(\"Unknown Error\");\n            default:\n                return super.failureMessage;\n        }\n    },\n    get statusTitle() {\n        if (this.notification_type === \"snail\") {\n            switch (this.notification_status) {\n                case \"sent\":\n                    return _t(\"Sent\");\n                case \"ready\":\n                    return _t(\"Awaiting Dispatch\");\n                case \"canceled\":\n                    return _t(\"Cancelled\");\n                default:\n                    return _t(\"Error\");\n            }\n        }\n        return super.statusTitle;\n    },\n};\npatch(Notification.prototype, notificationPatch);\n", "import { Message } from \"@mail/core/common/message\";\n\nimport { SnailmailNotificationPopover } from \"./snailmail_notification_popover\";\n\nMessage.components = {\n    ...Message.components,\n    Popover: SnailmailNotificationPopover,\n};\n", "import { Component } from \"@odoo/owl\";\n\nexport class SnailmailNotificationPopover extends Component {\n    static template = \"snailmail.SnailmailNotificationPopover\";\n    static props = [\"message\", \"close?\"];\n}\n", "import { MessagingMenu } from \"@mail/core/public_web/messaging_menu\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { patch } from \"@web/core/utils/patch\";\n\npatch(MessagingMenu.prototype, {\n    openFailureView(failure) {\n        if (failure.type !== \"snail\") {\n            return super.openFailureView(failure);\n        }\n        this.env.services.action.doAction({\n            name: _t(\"Snailmail Failures\"),\n            type: \"ir.actions.act_window\",\n            view_mode: \"kanban,list,form\",\n            views: [\n                [false, \"kanban\"],\n                [false, \"list\"],\n                [false, \"form\"],\n            ],\n            target: \"current\",\n            res_model: failure.resModel,\n            domain: [[\"message_ids.snailmail_error\", \"=\", true]],\n        });\n        this.dropdown.close();\n    },\n    getFailureNotificationName(failure) {\n        if (failure.type === \"snail\") {\n            return _t(\"Snailmail Failure: %(modelName)s\", { modelName: failure.modelName });\n        }\n        return super.getFailureNotificationName(...arguments);\n    },\n});\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { loadBundle } from \"@web/core/assets\";\n\nconst actionRegistry = registry.category(\"actions\");\n\n/**\n * Add a new function client action which loads the spreadsheet bundle, then\n * launch the actual action.\n * The action should be redefine in the bundle with `{ force: true }`\n * and the actual action component or function\n * @param {string} actionName\n * @param {string} [path]\n * @param {string} [displayName]\n */\nexport function addSpreadsheetActionLazyLoader(actionName, path, displayName) {\n    const actionLazyLoader = async (env, action) => {\n        // load the bundle which should redefine the action in the registry\n        await loadBundle(\"spreadsheet.o_spreadsheet\");\n\n        if (actionRegistry.get(actionName) === actionLazyLoader) {\n            // At this point, the real spreadsheet client action should be loaded and have\n            // replaced this function in the action registry. If it's not the case,\n            // it probably means that there was a crash in the bundle (e.g. syntax\n            // error). In this case, this action will remain in the registry, which\n            // will lead to an infinite loop. To prevent that, we push another action\n            // in the registry.\n            actionRegistry.add(\n                actionName,\n                () => {\n                    const msg = _t(\"%s couldn't be loaded\", actionName);\n                    env.services.notification.add(msg, { type: \"danger\" });\n                },\n                { force: true }\n            );\n        }\n        // then do the action again, with the actual definition registered\n        return action;\n    };\n    if (path) {\n        actionLazyLoader.path = path;\n    }\n    if (displayName) {\n        actionLazyLoader.displayName = displayName;\n    }\n    actionRegistry.add(actionName, actionLazyLoader);\n}\n\naddSpreadsheetActionLazyLoader(\"action_download_spreadsheet\");\n", "import { registry } from \"@web/core/registry\";\nimport { BinaryField, binaryField } from \"@web/views/fields/binary/binary_field\";\n\nexport class SpreadsheetBinaryField extends BinaryField {\n    static template = \"spreadsheet.SpreadsheetBinaryField\";\n\n    setup() {\n        super.setup();\n    }\n\n    async onFileDownload() {}\n}\n\nexport const spreadsheetBinaryField = {\n    ...binaryField,\n    component: SpreadsheetBinaryField,\n};\n\nregistry.category(\"fields\").add(\"binary_spreadsheet\", spreadsheetBinaryField);\n", "import { _t } from \"@web/core/l10n/translation\";\r\nimport { addSpreadsheetActionLazyLoader } from \"@spreadsheet/assets_backend/spreadsheet_action_loader\";\r\n\r\naddSpreadsheetActionLazyLoader(\"action_spreadsheet_dashboard\", \"dashboards\", _t(\"Dashboards\"));\r\n", "import { Component } from \"@odoo/owl\";\nimport { registry } from \"@web/core/registry\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { computeM2OProps, Many2One } from \"@web/views/fields/many2one/many2one\";\nimport { buildM2OFieldDescription, Many2OneField } from \"@web/views/fields/many2one/many2one_field\";\n\nexport class Many2OneSpreadsheetField extends Component {\n    static template = \"spreadsheet_edition.Many2OneSpreadsheetField\";\n    static components = { Many2One };\n    static props = { ...Many2OneField.props };\n\n    setup() {\n        this.action = useService(\"action\");\n        this.orm = useService(\"orm\");\n    }\n\n    get m2oProps() {\n        return {\n            ...computeM2OProps(this.props),\n            createAction: (params) => this.openSpreadsheet(params),\n        };\n    }\n\n    async openSpreadsheet({ context }) {\n        const relation = this.props.record.fields[this.props.name].relation;\n        const action = await this.orm.call(relation, \"action_open_new_spreadsheet\", [], {\n            context,\n        });\n        this.props.record.update({ [this.props.name]: { id: action.params.spreadsheet_id } });\n        await this.env.model.root.save();\n        await this.action.doAction(action);\n    }\n}\n\nregistry.category(\"fields\").add(\"many2one_spreadsheet\", {\n    ...buildM2OFieldDescription(Many2OneSpreadsheetField),\n});\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { Dialog } from \"@web/core/dialog/dialog\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { Notebook } from \"@web/core/notebook/notebook\";\n\nimport { Component, onWillStart, useState } from \"@odoo/owl\";\nimport { SpreadsheetSelectorPanel } from \"./spreadsheet_selector_panel\";\n\nconst NAME_LABELS = {\n    PIVOT: _t(\"Pivot name\"),\n    LIST: _t(\"List name\"),\n    LINK: _t(\"Link name\"),\n    GRAPH: _t(\"Graph name\"),\n};\n\n/**\n * @typedef State\n * @property {Object} spreadsheets\n * @property {string} panel\n * @property {string} name\n * @property {number|null} selectedSpreadsheetId\n * @property {string} [threshold]\n * @property {Object} pagerProps\n * @property {number} pagerProps.offset\n * @property {number} pagerProps.limit\n * @property {number} pagerProps.total\n */\n\nexport class SpreadsheetSelectorDialog extends Component {\n    static template = \"spreadsheet_edition.SpreadsheetSelectorDialog\";\n    static components = { Dialog, Notebook };\n    static props = {\n        actionOptions: Object,\n        type: String,\n        threshold: { type: Number, optional: true },\n        maxThreshold: { type: Number, optional: true },\n        name: String,\n        close: Function,\n    };\n\n    setup() {\n        /** @type {State} */\n        this.state = useState({\n            threshold: this.props.threshold,\n            name: this.props.name,\n            confirmationIsPending: false,\n        });\n        this.actionState = {\n            getOpenSpreadsheetAction: () => {},\n        };\n        this.notification = useService(\"notification\");\n        this.actionService = useService(\"action\");\n        const orm = useService(\"orm\");\n        onWillStart(async () => {\n            const spreadsheetModels = await orm.call(\n                \"spreadsheet.mixin\",\n                \"get_selector_spreadsheet_models\"\n            );\n            this.noteBookPages = spreadsheetModels.map(({ model, display_name, allow_create }) => ({\n                Component: SpreadsheetSelectorPanel,\n                id: model,\n                title: display_name,\n                props: {\n                    model,\n                    displayBlank: allow_create,\n                    onSpreadsheetSelected: this.onSpreadsheetSelected.bind(this),\n                    onSpreadsheetDblClicked: this._onInsert.bind(this),\n                },\n            }));\n        });\n    }\n\n    get nameLabel() {\n        return NAME_LABELS[this.props.type];\n    }\n\n    get title() {\n        return _t(\"Insert in Spreadsheet\");\n    }\n\n    /**\n     * @param {number|null} id\n     */\n    onSpreadsheetSelected({ getOpenSpreadsheetAction }) {\n        this.actionState = {\n            getOpenSpreadsheetAction,\n        };\n    }\n\n    async _onInsert() {\n        if (this.state.confirmationIsPending) {\n            return;\n        }\n        this.state.confirmationIsPending = true;\n        const action = await this.actionState.getOpenSpreadsheetAction();\n        if (!action) {\n            this.state.confirmationIsPending = false;\n            return;\n        }\n        const threshold = this.state.threshold ? parseInt(this.state.threshold, 10) : 0;\n        const name = this.state.name.toString();\n\n        // the action can be preceded by a notification\n        const actionOpen = action;\n        actionOpen.params = this._addToPreprocessingAction(actionOpen.params, threshold, name);\n        this.actionService.doAction(action);\n        this.props.close();\n    }\n\n    _addToPreprocessingAction(actionParams, threshold, name) {\n        return {\n            ...this.props.actionOptions,\n            preProcessingAsyncActionData: {\n                ...this.props.actionOptions.preProcessingAsyncActionData,\n                threshold,\n                name,\n            },\n            preProcessingActionData: {\n                ...this.props.actionOptions.preProcessingActionData,\n                threshold,\n                name,\n            },\n            ...actionParams,\n        };\n    }\n\n    _onDiscard() {\n        this.props.close();\n    }\n}\n", "import { browser } from \"@web/core/browser/browser\";\nimport { KeepLast } from \"@web/core/utils/concurrency\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { Pager } from \"@web/core/pager/pager\";\nimport { useHotkey } from \"@web/core/hotkeys/hotkey_hook\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { SpreadsheetSelectorGrid } from \"../spreadsheet_selector_grid/spreadsheet_selector_grid\";\n\nimport { Component, onWillStart, useState, onWillUnmount } from \"@odoo/owl\";\n\nconst DEFAULT_LIMIT = 9;\n\n/**\n * @typedef State\n * @property {Object[]} spreadsheets\n * @property {string} panel\n * @property {string} name\n * @property {number|null} selectedSpreadsheetId\n * @property {string} [threshold]\n * @property {Object} pagerProps\n * @property {number} pagerProps.offset\n * @property {number} pagerProps.limit\n * @property {number} pagerProps.total\n */\n\nexport class SpreadsheetSelectorPanel extends Component {\n    static template = \"spreadsheet_edition.SpreadsheetSelectorPanel\";\n    static components = { Pager, SpreadsheetSelectorGrid };\n    static defaultProps = {\n        displayBlank: true,\n    };\n    static props = {\n        onSpreadsheetSelected: Function,\n        onSpreadsheetDblClicked: Function,\n        model: String,\n        displayBlank: {\n            type: Boolean,\n            optional: true,\n        },\n    };\n\n    setup() {\n        /** @type {State} */\n        this.state = useState({\n            spreadsheets: [],\n            selectedSpreadsheetId: null,\n            pagerProps: {\n                offset: 0,\n                limit: this.props.displayBlank ? DEFAULT_LIMIT : DEFAULT_LIMIT + 1,\n                total: 0,\n            },\n        });\n        this.keepLast = new KeepLast();\n        this.orm = useService(\"orm\");\n        this.domain = [];\n        this.debounce = undefined;\n\n        onWillStart(async () => {\n            await this._fetchSpreadsheets();\n            const selectedItem =\n                !this.props.displayBlank && this.state.spreadsheets.length\n                    ? this.state.spreadsheets[0].id\n                    : null;\n            this._selectItem(selectedItem);\n        });\n\n        onWillUnmount(() => {\n            browser.clearTimeout(this.debounce);\n        });\n\n        useHotkey(\"Enter\", () => {\n            this.props.onSpreadsheetDblClicked();\n        });\n    }\n\n    get blankCardLabel() {\n        return _t(\"Blank spreadsheet\");\n    }\n\n    async _fetchSpreadsheets() {\n        const { offset, limit } = this.state.pagerProps;\n        const { records, total } = await this.keepLast.add(\n            this.orm.call(this.props.model, \"get_spreadsheets\", [this.domain], {\n                offset,\n                limit,\n            })\n        );\n        this.state.spreadsheets = records;\n        this.state.pagerProps.total = total;\n    }\n\n    async _getOpenSpreadsheetAction() {\n        return this.orm.call(this.props.model, \"action_open_spreadsheet\", [\n            [this.state.selectedSpreadsheetId],\n        ]);\n    }\n\n    async _getCreateAndOpenSpreadsheetAction() {\n        return this.orm.call(this.props.model, \"action_open_new_spreadsheet\");\n    }\n\n    _getActionForSelectedItem(spreadsheet) {\n        return spreadsheet\n            ? this._getOpenSpreadsheetAction\n            : this._getCreateAndOpenSpreadsheetAction;\n    }\n\n    async onSearchInput(ev) {\n        const currentSearch = ev.target.value;\n        this.domain = currentSearch !== \"\" ? [[\"name\", \"ilike\", currentSearch]] : [];\n\n        // Reset pager offset and get the total count based on the search criteria\n        this.state.pagerProps.offset = 0;\n        this._debouncedFetchSpreadsheets();\n    }\n\n    _debouncedFetchSpreadsheets() {\n        browser.clearTimeout(this.debounce);\n        this.debounce = browser.setTimeout(this._fetchSpreadsheets.bind(this), 400);\n    }\n\n    /**\n     * @param {Object} param0\n     * @param {number} param0.offset\n     * @param {number} param0.limit\n     */\n    onUpdatePager({ offset, limit }) {\n        this.state.pagerProps.offset = offset;\n        this.state.pagerProps.limit = limit;\n        this._fetchSpreadsheets();\n    }\n\n    /**\n     * @param {number|null} id\n     */\n    _selectItem(id) {\n        this.state.selectedSpreadsheetId = id;\n        const spreadsheet =\n            this.state.selectedSpreadsheetId &&\n            this.state.spreadsheets.find((s) => s.id === this.state.selectedSpreadsheetId);\n\n        this.props.onSpreadsheetSelected({\n            spreadsheet,\n            getOpenSpreadsheetAction: this._getActionForSelectedItem(spreadsheet).bind(this),\n        });\n    }\n\n    /**\n     * @param {Object} spreadsheet\n     * @returns {string} - URL for the spreadsheet thumbnail\n     */\n    getThumbnailURL(spreadsheet) {\n        if (!spreadsheet.display_thumbnail) {\n            return false;\n        }\n\n        return `data:image/jpeg;charset=utf-8;base64,${spreadsheet.display_thumbnail}`;\n    }\n}\n", "import { Component } from \"@odoo/owl\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { useHotkey } from \"@web/core/hotkeys/hotkey_hook\";\nimport { useAutofocus } from \"@web/core/utils/hooks\";\n\nconst DEFAULT_LIMIT = 9;\n\nexport class SpreadsheetSelectorGrid extends Component {\n    static template = \"spreadsheet_edition.SpreadsheetSelectorGrid\";\n    static defaultProps = {\n        displayBlank: true,\n    };\n\n    static props = {\n        spreadsheets: Array,\n        onSpreadsheetSelected: Function,\n        onSpreadsheetDblClicked: Function,\n        getThumbnailURL: Function,\n        selectedSpreadsheetId: Number | null,\n        displayBlank: { type: Boolean, optional: true },\n        blankCardLabel: { type: String, optional: true },\n    };\n\n    blankThumbnailPlaceholder = \"/spreadsheet/static/img/spreadsheet.svg\";\n\n    setup() {\n        useAutofocus();\n        useHotkey(\"ArrowRight\", () => this._onArrowKey(\"right\"), {\n            allowRepeat: true,\n        });\n        useHotkey(\"ArrowLeft\", () => this._onArrowKey(\"left\"), {\n            allowRepeat: true,\n        });\n    }\n\n    get blankTemplate() {\n        return {\n            id: null,\n            display_name: this.props.blankCardLabel ?? _t(\"Blank spreadsheet\"),\n            thumbnail: \"/spreadsheet/static/img/spreadsheet.svg\",\n        };\n    }\n\n    /**\n     * @returns {Array} - The list of spreadsheets to display in the grid.\n     */\n    get spreadsheets() {\n        if (!this.props.displayBlank) {\n            return this.props.spreadsheets;\n        }\n        return [this.blankTemplate, ...this.props.spreadsheets];\n    }\n\n    /**\n     * @returns {Number} - The number of spreadsheets to display per page.\n     */\n    get itemsPerPage() {\n        return this.props.displayBlank ? DEFAULT_LIMIT : DEFAULT_LIMIT + 1;\n    }\n\n    /**\n     * Handles the arrow key press event to navigate through spreadsheets and pages.\n     * @param {left|right} direction - The direction of the arrow key.\n     */\n    async _onArrowKey(direction) {\n        const index = this.spreadsheets.findIndex(\n            (spreadsheet) => spreadsheet.id === this.props.selectedSpreadsheetId\n        );\n\n        // Navigate to the next or previous spreadsheet\n        const navigateToSpreadsheet = (newSpreadsheetId) => {\n            if (newSpreadsheetId === this.props.selectedSpreadsheetId) {\n                return;\n            }\n            this.props.onSpreadsheetSelected(newSpreadsheetId);\n        };\n\n        switch (direction) {\n            case \"left\":\n                if (index > 0 && index < this.spreadsheets.length) {\n                    // Navigate to the previous spreadsheet\n                    navigateToSpreadsheet(this.spreadsheets[index - 1].id);\n                }\n                break;\n            case \"right\":\n                if (index < this.spreadsheets.length - 1) {\n                    // Navigate to the next spreadsheet\n                    navigateToSpreadsheet(this.spreadsheets[index + 1].id);\n                }\n                break;\n            default:\n                break;\n        }\n    }\n}\n", "import { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { SpreadsheetSelectorDialog } from \"@spreadsheet_edition/assets/components/spreadsheet_selector_dialog/spreadsheet_selector_dialog\";\n\nimport { Component } from \"@odoo/owl\";\n\n/**\n * Insert a link to a view in spreadsheet\n * @extends Component\n */\nexport class InsertViewSpreadsheet extends Component {\n    static props = {};\n    static template = \"spreadsheet_edition.InsertActionSpreadsheet\";\n    static components = { DropdownItem };\n\n    setup() {\n        this.notification = useService(\"notification\");\n        this.actionService = useService(\"action\");\n        this.dialogManager = useService(\"dialog\");\n    }\n\n    //-------------------------------------------------------------------------\n    // Handlers\n    //-------------------------------------------------------------------------\n\n    async linkInSpreadsheet() {\n        const actionToLink = await this.getViewDescription();\n        // do action with action link\n        const actionOptions = {\n            preProcessingAction: \"insertLink\",\n            preProcessingActionData: actionToLink,\n        };\n\n        this.dialogManager.add(SpreadsheetSelectorDialog, {\n            type: \"LINK\",\n            actionOptions,\n            name: this.env.config.getDisplayName(),\n        });\n    }\n\n    async getViewDescription() {\n        const { resModel } = this.env.searchModel;\n        const { views = [], actionId, viewType } = this.env.config;\n        const { xml_id } = actionId\n            ? await this.actionService.loadAction(actionId, this.env.searchModel.context)\n            : {};\n        const { context } = this.env.searchModel.getIrFilterValues();\n        const action = {\n            xmlId: xml_id,\n            domain: this.env.searchModel.domain,\n            context,\n            modelName: resModel,\n            // prevent navigation to other views as we have a dedicated domain/context\n            views: views.map(([, type]) => [false, type]),\n        };\n        return {\n            viewType,\n            action,\n        };\n    }\n}\n", "import { patch } from \"@web/core/utils/patch\";\nimport { KanbanController } from \"@web/views/kanban/kanban_controller\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { useInsertInSpreadsheet } from \"../view_hook\";\n\nexport const patchKanbanControllerExportSelection = {\n    setup() {\n        super.setup();\n        this.insertInSpreadsheet = useInsertInSpreadsheet(this.env, () =>\n            this.getExportableFields()\n        );\n    },\n\n    getStaticActionMenuItems() {\n        const root = this.model.root;\n        const isM2MGrouped = root.groupBy.some((groupBy) => {\n            const fieldName = groupBy.split(\":\")[0];\n            return root.fields[fieldName].type === \"many2many\";\n        });\n        const menuItems = super.getStaticActionMenuItems(...arguments);\n        menuItems[\"insert\"] = {\n            isAvailable: () => !isM2MGrouped,\n            sequence: 15,\n            icon: \"oi oi-view-list\",\n            description: _t(\"Insert in spreadsheet\"),\n            callback: () => this.insertInSpreadsheet(),\n        };\n        return menuItems;\n    },\n};\n\nexport const unpatchKanbanControllerExportSelection = patch(\n    KanbanController.prototype,\n    patchKanbanControllerExportSelection\n);\n", "import { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\n\nimport { Component } from \"@odoo/owl\";\n\nexport class InsertListSpreadsheetMenu extends Component {\n    static props = {};\n    static template = \"spreadsheet_edition.InsertListSpreadsheetMenu\";\n    static components = { DropdownItem };\n\n    /**\n     * @private\n     */\n    _onClick() {\n        this.env.bus.trigger(\"insert-in-spreadsheet\");\n    }\n}\n", "import { patch } from \"@web/core/utils/patch\";\nimport { ListController } from \"@web/views/list/list_controller\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { useInsertInSpreadsheet } from \"../view_hook\";\n\npatch(ListController.prototype, {\n    setup() {\n        super.setup();\n        this.insertInSpreadsheet = useInsertInSpreadsheet(this.env, () =>\n            this.getExportableFields()\n                .filter((f) => f.type !== \"properties\")\n                .filter(\n                    (f) =>\n                        Object.values(this.archInfo.fieldNodes).find((fN) => fN.name === f.name)\n                            .widget !== \"handle\"\n                )\n        );\n    },\n\n    getStaticActionMenuItems() {\n        const list = this.model.root;\n        const isM2MGrouped = list.groupBy.some((groupBy) => {\n            const fieldName = groupBy.split(\":\")[0];\n            return list.fields[fieldName].type === \"many2many\";\n        });\n        const menuItems = super.getStaticActionMenuItems(...arguments);\n        menuItems[\"insert\"] = {\n            isAvailable: () => !isM2MGrouped,\n            sequence: 15,\n            icon: \"oi oi-view-list\",\n            description: _t(\"Insert in spreadsheet\"),\n            callback: () => this.insertInSpreadsheet(),\n        };\n        return menuItems;\n    },\n});\n", "import { patch } from \"@web/core/utils/patch\";\nimport { ListRenderer } from \"@web/views/list/list_renderer\";\nimport { useService } from \"@web/core/utils/hooks\";\n\npatch(ListRenderer.prototype, {\n    /**\n     * @override\n     */\n    setup() {\n        super.setup(...arguments);\n        this.dialogService = useService(\"dialog\");\n        this.actionService = useService(\"action\");\n    },\n});\n", "import { patch } from \"@web/core/utils/patch\";\nimport { Message } from \"@mail/core/common/message\";\nimport \"@mail/core/web/message_patch\"; // dependency ordering\n\npatch(Message.prototype, {\n    /**\n     * This function overrides the original method so that when the user tries to open a the record\n     * from a starred discussion linked to a spreadsheet cell thread, it can be redirected to the corresponding\n     * spreadsheet record.\n     * @override\n     */\n    async openRecord() {\n        if (this.message.model === \"spreadsheet.cell.thread\") {\n            const action = await this.env.services.orm.call(\n                \"spreadsheet.cell.thread\",\n                \"get_spreadsheet_access_action\",\n                [this.message.thread.id]\n            );\n            this.action.doAction(action);\n            return;\n        } else {\n            super.openRecord();\n        }\n    },\n});\n", "import { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { registry } from \"@web/core/registry\";\nimport { session } from \"@web/session\";\nimport { InsertViewSpreadsheet } from \"../insert_action_link_menu/insert_action_link_menu\";\nimport { InsertListSpreadsheetMenu } from \"../list_view/insert_list_spreadsheet_menu_owl\";\n\nimport { Component } from \"@odoo/owl\";\nconst cogMenuRegistry = registry.category(\"cogMenu\");\n\nexport class SpreadsheetCogMenu extends Component {\n    static template = \"spreadsheet_edition.SpreadsheetCogMenu\";\n    static components = { Dropdown, InsertViewSpreadsheet, InsertListSpreadsheetMenu };\n    static props = {};\n}\n\ncogMenuRegistry.add(\n    \"spreadsheet-cog-menu\",\n    {\n        Component: SpreadsheetCogMenu,\n        groupNumber: 30,\n        isDisplayed: ({ config, isSmall }) =>\n            !isSmall &&\n            config.actionType === \"ir.actions.act_window\" &&\n            config.viewType !== \"form\" &&\n            session.can_insert_in_spreadsheet,\n    },\n    { sequence: 1 }\n);\n", "import { addSpreadsheetActionLazyLoader } from \"@spreadsheet/assets_backend/spreadsheet_action_loader\";\n\naddSpreadsheetActionLazyLoader(\"action_open_spreadsheet_history\");\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { useBus, useService } from \"@web/core/utils/hooks\";\nimport { SpreadsheetSelectorDialog } from \"./components/spreadsheet_selector_dialog/spreadsheet_selector_dialog\";\nimport { user } from \"@web/core/user\";\nimport { omit } from \"@web/core/utils/objects\";\n\nexport function useInsertInSpreadsheet(env, getExportableFields) {\n    const { config, bus, model, searchModel } = env;\n    useBus(bus, \"insert-in-spreadsheet\", async () => {\n        _openSpreadsheetSelectorDialog();\n    });\n    const action = useService(\"action\");\n    const _getColumnsForSpreadsheet = () => {\n        const fields = model.root.fields;\n        const columns = getExportableFields();\n        return columns\n            .filter((col) => ![\"binary\", \"json\"].includes(fields[col.name].type))\n            .map((col) => {\n                const field = fields[col.name];\n                return { name: col.name, type: field.type, string: field.string };\n            });\n    };\n\n    const _getListForSpreadsheet = async (name) => {\n        const root = model.root;\n        const { actionId } = config;\n        const { xml_id } = actionId ? await action.loadAction(actionId, model.root.context) : {};\n        const fields = root.fields;\n\n        return {\n            list: {\n                model: root.resModel,\n                domain: searchModel.domainString,\n                orderBy: root.orderBy.filter((field) => fields[field.name]),\n                context: omit(root.context, ...Object.keys(user.context)),\n                columns: _getColumnsForSpreadsheet(),\n                name,\n                actionXmlId: xml_id,\n            },\n            fields,\n        };\n    };\n\n    const _openSpreadsheetSelectorDialog = async () => {\n        const root = model.root;\n        const count = root.groups\n            ? root.groups.reduce((acc, group) => group.count + acc, 0)\n            : root.count;\n        const selection = await root.getResIds(true);\n        const threshold = selection.length > 0 ? selection.length : Math.min(count, root.limit);\n        let name = config.getDisplayName();\n        const sortBy = root.orderBy[0];\n        if (sortBy && root.fields[sortBy.name]) {\n            name = _t(\"%(field name)s by %(order)s\", {\n                \"field name\": name,\n                order: root.fields[sortBy.name].string,\n            });\n        }\n        const { list, fields } = await _getListForSpreadsheet(name);\n\n        // if some records are selected, we replace the domain with a \"id in [selection]\" clause\n        if (selection.length > 0) {\n            list.domain = [[\"id\", \"in\", selection]];\n        }\n        const actionOptions = {\n            preProcessingAsyncAction: \"insertList\",\n            preProcessingAsyncActionData: { list, threshold, fields },\n        };\n\n        const params = {\n            threshold,\n            type: \"LIST\",\n            name,\n            actionOptions,\n        };\n        root.model.dialog.add(SpreadsheetSelectorDialog, params);\n    };\n\n    return _openSpreadsheetSelectorDialog;\n}\n", "import { addSpreadsheetActionLazyLoader } from \"@spreadsheet/assets_backend/spreadsheet_action_loader\";\n\naddSpreadsheetActionLazyLoader(\"action_edit_dashboard\");\n", "import { patch } from \"@web/core/utils/patch\";\nimport { useOwnedDialogs } from \"@web/core/utils/hooks\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { FormViewDialog } from \"@web/views/view_dialogs/form_view_dialog\";\nimport { SpreadsheetSelectorPanel } from \"@spreadsheet_edition/assets/components/spreadsheet_selector_dialog/spreadsheet_selector_panel\";\n\npatch(SpreadsheetSelectorPanel.prototype, {\n    setup() {\n        super.setup();\n        this.addDialog = useOwnedDialogs();\n    },\n\n    _isDashboardModel() {\n        return this.props.model === \"spreadsheet.dashboard\";\n    },\n\n    get blankCardLabel() {\n        return this._isDashboardModel() ? _t(\"Blank dashboard\") : super.blankCardLabel;\n    },\n\n    async _getCreateAndOpenDashboardAction() {\n        return new Promise((resolve, reject) => {\n            this.addDialog(FormViewDialog, {\n                resModel: \"spreadsheet.dashboard\",\n                title: _t(\"Create a New Dashboard\"),\n                context: {\n                    form_view_ref:\n                        \"spreadsheet_dashboard_edition.spreadsheet_dashboard_creation_dialog_view_form\",\n                },\n                canExpand: false,\n                onRecordSaved: async (record) => {\n                    const action = await this.orm.call(\n                        this.props.model,\n                        \"action_open_spreadsheet\",\n                        [[record.resId]]\n                    );\n                    action.params ??= {};\n                    action.params.is_new_spreadsheet = true;\n                    resolve(action);\n                },\n                onRecordDiscarded: resolve,\n            });\n        });\n    },\n\n    _getActionForSelectedItem(spreadsheet) {\n        if (!spreadsheet && this._isDashboardModel()) {\n            return this._getCreateAndOpenDashboardAction;\n        }\n        return super._getActionForSelectedItem(spreadsheet);\n    },\n});\n", "import { addSpreadsheetActionLazyLoader } from \"@spreadsheet/assets_backend/spreadsheet_action_loader\";\n\naddSpreadsheetActionLazyLoader(\"action_sale_order_spreadsheet\", \"sale-order-spreadsheet\");\naddSpreadsheetActionLazyLoader(\n    \"action_sale_order_spreadsheet_history\",\n    \"sale-order-spreadsheet-history\"\n);\n", "import { onWillStart } from \"@odoo/owl\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { patch } from \"@web/core/utils/patch\";\nimport { ROUTES, WelcomeScreen } from \"@website/client_actions/configurator/configurator\";\n\nexport const WEBSITE_GENERATOR_ROUTE = 6;\n\npatch(WelcomeScreen.prototype, {\n    setup() {\n        super.setup(...arguments);\n        this.orm = useService(\"orm\");\n        this.ui = useService(\"ui\");\n\n        onWillStart(async () => {\n            this.showWebsiteGeneratorButton = await this.orm.call(\n                \"website\",\n                \"is_website_generator_available\",\n            );\n        });\n    },\n\n    async goToWebsiteGeneratorRequest() {\n        if (ROUTES.websiteGenerator) {\n            this.props.navigate(ROUTES.websiteGenerator);\n            return;\n        }\n\n        // install website_generator\n        this.ui.block();\n        const modules = await this.orm.searchRead(\n            \"ir.module.module\",\n            [[\"name\", \"=\", \"website_generator\"]],\n            [\"id\"],\n        );\n        await this.orm.call(\"ir.module.module\", \"button_immediate_install\", [[modules[0].id]]);\n        this.props.navigate(WEBSITE_GENERATOR_ROUTE, true);\n        this.ui.unblock();\n    },\n});\n", "import { Component, useState } from \"@odoo/owl\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { patch } from \"@web/core/utils/patch\";\nimport {\n    ROUTES,\n    Configurator,\n    SkipButton,\n} from \"@website/client_actions/configurator/configurator\";\nimport { WEBSITE_GENERATOR_ROUTE } from \"@website_enterprise/client_actions/configurator/configurator\";\n\nROUTES.websiteGenerator = WEBSITE_GENERATOR_ROUTE; // TODO: make these urls prettier?\n\nexport class WebsiteGeneratorScreen extends Component {\n    static components = { SkipButton };\n    static template = \"website_generator.Configurator.WebsiteGeneratorScreen\";\n    static props = {\n        navigate: Function,\n        skip: Function,\n    };\n\n    setup() {\n        this.action = useService(\"action\");\n        this.notification = useService(\"notification\");\n        this.orm = useService(\"orm\");\n        this.ui = useService(\"ui\");\n        this.state = useState({\n            isValidatingUrl: false,\n            isValidUrl: false,\n            url: \"\",\n            submitted: false,\n        })\n    }\n\n    async checkUrl() {\n        this.state.isValidatingUrl = true;\n        this.state.submitted = true;\n        let result;\n        try{\n            result = await this.orm.call(\"website\", \"url_check\", [this.state.url], {});\n            if (result.status === 'success') {\n                this.state.isValidatingUrl = false;\n                this.state.isValidUrl = true;\n                return true;\n            }\n            switch (result.status) {\n                case 'error_invalid_url':\n                    this.notification.add(\"Please check your URL and try again.\", {\n                        title: _t(\"The provided URL is not reachable\"),\n                        type: \"danger\",\n                    });\n                    break;\n                case 'error_banned_url':\n                    this.notification.add(\"We can't process your request.\", {\n                        title: _t(\"The provided URL is not allowed\"),\n                        type: \"danger\",\n                    });\n                    break;\n                case 'error_allowed_request_exhausted':\n                    this.notification.add(\"You have exceeded the number of requests, try again later.\", {\n                        title: _t(\"Too many requests\"),\n                        type: \"danger\",\n                    });\n                    break;\n                case 'error_url_redirection':\n                    this.notification.add(\"The requested URL redirected to another URL, try again with the final URL.\", {\n                        title: _t(\"URL redirection\"),\n                        type: \"danger\",\n                    });\n                    break;\n                default:\n                    this.notification.add(\"Something went wrong.\", {\n                        title: _t(\"Error\"),\n                    });\n                    break;\n            }\n            this.state.isValidatingUrl = false;\n            this.state.isValidUrl = false;\n            return false;\n        }\n        finally{\n            if(!result){\n                this.notification.add(\"Something went wrong.\", {\n                    title: _t(\"Error\"),\n                });\n                this.state.isValidatingUrl = false;\n                this.state.isValidUrl = false;\n                return false\n            }\n        }\n    }\n\n    async onUrlInput(ev) {\n        this.state.url = ev.target.value;\n        this.state.submitted = false;\n    }\n\n    async makeWebsiteGeneratorRequest(ev) {\n        ev.preventDefault();\n        // We have to get the form data before disabling inputs.\n        const formData = new FormData(ev.currentTarget);\n        const data = Object.fromEntries(formData.entries());\n        this.ui.block();\n        if (!await this.checkUrl()) {\n            this.ui.unblock();\n            return;\n        }\n        let result;\n        try {\n            result = await this.orm.call(\"website\", \"import_website\", [], data);\n        } finally {\n            if (!result) {\n                this.ui.unblock();\n            }\n        }\n\n        if (result) {\n            this.action.doAction({\n                type: \"ir.actions.act_url\",\n                url: \"/odoo/action-website_generator.website_generator_screen?reload=true\",\n                target: \"self\",\n            });\n        } else {\n            this.notification.add(result, {\n                title: _t(\"Something went wrong while importing your website.\"),\n            });\n        }\n    }\n}\n\npatch(Configurator, {\n    components: {\n        ...Configurator.components,\n        WebsiteGeneratorScreen,\n    },\n});\n\npatch(Configurator.prototype, {\n    get currentComponent() {\n        if (this.state.currentStep === ROUTES.websiteGenerator) {\n            return WebsiteGeneratorScreen;\n        }\n        return super.currentComponent;\n    },\n});\n", "import {registry} from \"@web/core/registry\";\nimport {\n    Component,\n    useState,\n    onMounted,\n    onWillStart,\n    onWillUnmount,\n} from \"@odoo/owl\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { useBus, useService } from \"@web/core/utils/hooks\";\nimport {redirect} from \"@web/core/utils/urls\";\nimport { standardActionServiceProps } from \"@web/webclient/actions/action_service\";\n\nclass WebsiteGenerator extends Component {\n    static template = \"website_generator.WebsiteGenerator\";\n    static props = { ...standardActionServiceProps };\n    setup() {\n        this.orm = useService(\"orm\");\n        this.dialog = useService(\"dialog\");\n        this.website = useService(\"website\");\n\n        this.canCallGetResultWaitingRequests = true;\n        this.state = useState({\n            error: \"\",\n        });\n        onWillStart(() => this._checkRequestStatus());\n        // Every 10 seconds, we ask the server to call IAP to see if the\n        // scraping result is ready.\n        // If it is ready, the server will process the IAP file and generate the\n        // website. Once it's done, a later call of this `setInterval` loop will\n        // notice it (success or error status) and act accordingly.\n        onMounted(() => {\n            this.interval = setInterval(() => this._checkRequestStatus(), 10000);\n        });\n        onWillUnmount(() => clearInterval(this.interval));\n\n        useBus(this.website.bus, \"HIDE-WEBSITE-LOADER\", () => {\n            if (!this.state.error) {\n                redirect(\"/odoo?showWebsiteGeneratorNotification=1\");\n            }\n        });\n    }\n    async _checkRequestStatus() {\n        // Get scraping request\n        const [lastScrapRequest] = await this.orm.silent.searchRead(\n            \"website_generator.request\",\n            [],\n            [\"id\", \"status\", \"status_message\", \"website_id\"],\n            { limit: 1, order: \"id DESC\" },\n        );\n        // Safety check, return to backend if no request\n        if (!lastScrapRequest) {\n            redirect(\"/odoo\");\n            return;\n        }\n        // If no real error status but not yet ready, ask server to check for\n        // results\n        if ([\"error_request_still_processing\", \"error_maintenance\", \"waiting\"]\n                .includes(lastScrapRequest.status)) {\n            if (this.canCallGetResultWaitingRequests) {\n                this.canCallGetResultWaitingRequests = false;\n                this.lastGetResultWaitingRequests = this.orm.call(\n                    \"website_generator.request\",\n                    \"get_result_waiting_requests\"\n                ).then(() => {\n                    this.canCallGetResultWaitingRequests = true;\n                });\n            }\n            this._showWebsiteLoader();\n            return;\n        }\n        // At this point, either the request is in real error or succeeded:\n        // mark it as result \"seen\"\n        this.orm.silent.write(\n            \"website_generator.request\",\n            [lastScrapRequest.id],\n            {notified: true},\n        );\n        // If it's in error, adapt screen message to show the error\n        if (lastScrapRequest.status.includes(\"error\")) {\n            this.state.error = lastScrapRequest.status_message;\n            clearInterval(this.interval);\n            this.website.hideLoader();\n            return;\n        }\n\n        this.website.prepareOutLoader();\n\n        // If it succeeded, redirect to the website\n        window.location.href = `/website/force/${lastScrapRequest.website_id[0]}`;\n    }\n\n    _showWebsiteLoader() {\n        this.website.showLoader({\n            title: _t(\"Importing your website.\"),\n            bottomMessageTemplate: \"website_generator.request_website_loader_bottom_message\",\n            showLoader: false,\n            showCloseButton: true,\n        });\n    }\n}\n\nregistry.category(\"actions\").add(\"website_generator\", WebsiteGenerator);\n", "import { _t } from \"@web/core/l10n/translation\";\nimport {registry} from \"@web/core/registry\";\nimport { user } from \"@web/core/user\";\nimport {Component, useState, onMounted, onWillStart, onWillUnmount} from \"@odoo/owl\";\nimport {useService} from \"@web/core/utils/hooks\";\nimport {session} from \"@web/session\";\n\nexport class GeneratorRequest extends Component {\n    static template = \"website_generator.GeneratorRequest\";\n    static props = {};\n    setup() {\n        this.actionService = useService(\"action\");\n        this.notification = useService(\"notification\");\n        this.orm = useService(\"orm\");\n        this.state = useState({\n            globeExtraClasses: \"\",\n        });\n\n        onWillStart(async () => {\n            this.checkRequestStatus();\n            const searchParams = new URLSearchParams(window.location.search);\n\n            if (searchParams.get(\"showWebsiteGeneratorNotification\")) {\n                // TODO: find a better way to show this notification\n                const users = await this.orm.read(\"res.partner\", [user.partnerId], [\"email\"]);\n                const userEmail = users[0].email;\n                this.notification.add(\n                    _t(\"We will notify %(email)s when everything is ready.\", { email: userEmail }),\n                    {\n                        title: _t(\"The import of your website has started!\"),\n                        type: \"info\",\n                        sticky: true,\n                    },\n                );\n            }\n        });\n        onMounted(() => {\n            this.interval = setInterval(() => this.checkRequestStatus(), 60000);\n        });\n        onWillUnmount(() => {\n            clearInterval(this.interval);\n        });\n    }\n    async goToWaitingView() {\n        await this.actionService.doAction(\"website_generator.website_generator_screen\");\n    }\n    async checkRequestStatus() {\n        const [scrapRequest] = await this.orm.silent.searchRead(\n            \"website_generator.request\",\n            [],\n            [\"status\"],\n            {limit: 1, order: \"id DESC\"},\n        );\n        if (!scrapRequest) {\n            clearInterval(this.interval);\n        }\n        if ([\"error_request_still_processing\", \"error_maintenance\", \"waiting\"]\n                .includes(scrapRequest.status)) {\n            return;\n        }\n        if (scrapRequest.status.includes(\"error\")) {\n            this.state.globeExtraClasses = \"text-danger\";\n        } else {\n            this.state.globeExtraClasses = \"text-success\";\n        }\n        clearInterval(this.interval);\n    }\n}\n\nconst systrayItem = {\n    Component: GeneratorRequest,\n    isDisplayed: () => session.show_scraper_systray,\n};\n\nregistry.category(\"website_systray\").add(\"GeneratorRequest\", systrayItem);\nregistry.category(\"systray\").add(\"GeneratorRequest\", systrayItem);\n", "import { registry } from \"@web/core/registry\";\n\nregistry.category(\"isTopWindowURL\").add(\"website_knowledge.website_builder_action\", ({ host, pathname }) =>\n    pathname && (\n        pathname.startsWith(\"/knowledge/article/\")\n        || pathname.includes(\"/knowledge/home\")\n    )\n);\n", "import { patch } from \"@web/core/utils/patch\";\n\nimport { PermissionPanel } from \"@knowledge/components/permission_panel/permission_panel\";\nimport { CopyButton } from \"@web/core/copy_button/copy_button\";\n\npatch(PermissionPanel.prototype, {\n    onWebsitePublishedClick() {\n        if (!this.userIsInternalEditor) {\n            return;\n        }\n        this.toggleWebsitePublished();\n    },\n    async toggleWebsitePublished() {\n        await this.record.update({'website_published': !this.record.data.website_published});\n    },\n});\n\nPermissionPanel.components = {\n    ...PermissionPanel.components,\n    CopyButton,\n};\n", "import { KnowledgeSidebar } from \"@knowledge/components/sidebar/sidebar\";\nimport { Domain } from \"@web/core/domain\";\nimport { patch } from \"@web/core/utils/patch\";\n\npatch(KnowledgeSidebar.prototype, {\n    setup() {\n        super.setup();\n        this.originalRootId = this.props.record.data.root_article_id.id;\n    },\n    get searchDomain() {\n        const originalDomain = super.searchDomain;\n        return Domain.or([\n            originalDomain,\n            [\"&\", [\"website_published\", \"=\", true], [\"id\", \"child_of\", this.originalRootId]],\n        ]).toList({});\n    },\n});\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { clickOnSave, registerWebsitePreviewTour } from \"@website/js/tours/tour_utils\";\n\nimport { markup } from \"@odoo/owl\";\n\nregisterWebsitePreviewTour(\n    \"blog\",\n    {\n        url: \"/\",\n    },\n    () => [\n        {\n            trigger:\n                \"body:not(:has(.o_new_content_menu_choices)) .o_new_content_container > button\",\n            content: _t(\"Click here to add new content to your website.\"),\n            tooltipPosition: \"bottom\",\n            run: \"click\",\n        },\n        {\n            trigger: 'button[data-module-xml-id=\"base.module_website_blog\"]',\n            content: _t(\"Select this menu item to create a new blog post.\"),\n            tooltipPosition: \"bottom\",\n            run: \"click\",\n        },\n        {\n            trigger: 'div[name=\"name\"] input',\n            content: _t(\"Enter your post's title\"),\n            tooltipPosition: \"bottom\",\n            run: \"edit Test\",\n        },\n        {\n            trigger: 'div.o_field_widget[name=\"blog_id\"]',\n        },\n        {\n            trigger: \"button.o_form_button_save\",\n            content: _t(\"Select the blog you want to add the post to.\"),\n            // Without demo data (and probably in most user cases) there is only\n            // one blog so this step would not be needed and would block the\n            // tour. We keep the step with \"auto: true\", so that the main python\n            // test still works but never display this to the user anymore. We\n            // suppose the user does not need guidance once that modal is\n            // opened. Note: if you run the tour via your console without demo\n            // data, the tour will thus fail as this will be considered.\n            run: \"click\",\n        },\n        {\n            trigger: \".o_builder_sidebar_open .o-snippets-menu\",\n            timeout: 15000,\n        },\n        {\n            trigger: ':iframe h1[data-oe-expression=\"blog_post.name\"]',\n            content: _t(\"Edit your title, the subtitle is optional.\"),\n            tooltipPosition: \"top\",\n            run: \"click\",\n        },\n        {\n            trigger: `:iframe #wrap h1[data-oe-expression=\"blog_post.name\"]:not(:contains(''))`,\n        },\n        {\n            trigger: \"button[data-action-id='setCoverBackground'][title='Image']\",\n            content: markup(_t(\"Set a blog post <b>cover</b>.\")),\n            tooltipPosition: \"top\",\n            run: \"click\",\n        },\n        {\n            trigger: \".o_select_media_dialog .o_we_search\",\n            content: _t('Search for an image. (eg: type \"business\")'),\n            tooltipPosition: \"top\",\n        },\n        {\n            trigger: \".o_select_media_dialog .o_existing_attachment_cell:first .o_button_area\",\n            content: _t(\"Choose an image from the library.\"),\n            tooltipPosition: \"top\",\n            run: \"click\",\n        },\n        {\n            trigger: \":iframe #o_wblog_post_content p\",\n            content: markup(\n                _t(\n                    \"<b>Write your story here.</b> Use the top toolbar to style your text: add an image or table, set bold or italic, etc. Drag and drop building blocks for more graphical blogs.\"\n                )\n            ),\n            tooltipPosition: \"top\",\n            run: \"editor Blog content\",\n        },\n        ...clickOnSave(),\n        {\n            trigger: \".o_menu_systray_item.o_mobile_preview > a\",\n            content: markup(\n                _t(\"Use this icon to preview your blog post on <b>mobile devices</b>.\")\n            ),\n            tooltipPosition: \"bottom\",\n            run: \"click\",\n        },\n        {\n            trigger: \".o_website_preview.o_is_mobile\",\n        },\n        {\n            trigger: \".o_menu_systray_item.o_mobile_preview > a\",\n            content: _t(\n                \"Once you have reviewed the content on mobile, you can switch back to the normal view by clicking here again\"\n            ),\n            tooltipPosition: \"right\",\n            run: \"click\",\n        },\n        {\n            trigger: \":iframe body:not(.editor_enable)\",\n        },\n        {\n            trigger: '.o_menu_systray_item a:contains(\"Unpublished\")',\n            tooltipPosition: \"bottom\",\n            content: markup(\n                _t(\"<b>Publish your blog post</b> to make it visible to your visitors.\")\n            ),\n            run: \"click\",\n        },\n        {\n            trigger: '.o_menu_systray_item a:contains(\"Published\")',\n        },\n    ]\n);\n", "import { _t } from \"@web/core/l10n/translation\";\nimport {\n    registerBackendAndFrontendTour,\n} from '@website/js/tours/tour_utils';\nimport { stepUtils } from \"@web_tour/tour_utils\";\n\nregisterBackendAndFrontendTour(\"question\", {\n    url: '/forum/1',\n}, () => [{\n    trigger: \".o_wforum_ask_btn\",\n    tooltipPosition: \"left\",\n    content: _t(\"Create a new post in this forum by clicking on the button.\"),\n    run: \"click\",\n    expectUnloadPage: true,\n}, {\n    trigger: \"input[name=post_name]\",\n    tooltipPosition: \"top\",\n    content: _t(\"Give your post title.\"),\n    run: \"edit Test\",\n},\n{\n    trigger: `input[name=post_name]:not(:empty)`,\n},\n{\n    trigger: \".note-editable p\",\n    content: _t(\"Put your question here.\"),\n    tooltipPosition: \"bottom\",\n    run: \"editor Test\",\n},\n{\n    trigger: `.note-editable p:not(:text(<br>))`,\n},\n{\n    trigger: \".o_select_menu_toggler\",\n    content: _t(\"Insert tags related to your question.\"),\n    tooltipPosition: \"top\",\n    run: \"click\",\n},\n...stepUtils.editSelectMenuInput(\".o_select_menu_input\", \"Test\"),\n{\n    content: \"Select found select menu item\",\n    trigger: \".o_popover.o_select_menu_menu .o_select_menu_item:contains('Test')\",\n    run: 'click',\n},\n{\n    content: \"Close search bar\",\n    trigger: \"body\",\n    run: 'click',\n},\n{\n    trigger: \"button:contains(/^Post/)\",\n    content: _t(\"Click to post your question.\"),\n    tooltipPosition: \"bottom\",\n    run: \"click\",\n    expectUnloadPage: true,\n},\n{\n    trigger: \".o_wforum_content_wrapper .h3:contains(test)\",\n},\n{\n    isActive: [\"auto\"],\n    trigger: \".modal.modal_shown.show:contains(thanks for posting!) button.btn-close\",\n    run: \"click\",\n},\n{\n    trigger: \"a:contains(Reply).collapsed\",\n    content: _t(\"Click to reply.\"),\n    tooltipPosition: \"bottom\",\n    run: \"click\",\n},\n{\n    trigger: \".note-editable p\",\n    content: _t(\"Put your answer here.\"),\n    tooltipPosition: \"bottom\",\n    run: \"editor Test\",\n},\n{\n    trigger: `.note-editable p:not(:text(<br>))`,\n},\n{\n    trigger: \"button:contains(\\\"Post Answer\\\")\",\n    content: _t(\"Click to post your answer.\"),\n    tooltipPosition: \"bottom\",\n    run: \"click\",\n    expectUnloadPage: true,\n},\n{\n    trigger: \".o_wforum_content_wrapper .h3:contains(test)\",\n},\n{\n    isActive: [\"auto\"],\n    trigger: \".modal.modal_shown.show:contains(thanks for posting!) button.btn-close\",\n    run: \"click\",\n}, {\n    trigger: \".o_wforum_validate_toggler[data-karma]:first\",\n    content: _t(\"Click here to accept this answer.\"),\n    tooltipPosition: \"right\",\n    run: \"click\",\n}, {\n    content: \"Check edit button is there\",\n    trigger: \"a:contains('Edit your answer')\",\n}]);\n", "import { startWebClient } from \"@web/start\";\nimport { WebClientEnterprise } from \"./webclient/webclient\";\n\n/**\n * This file starts the enterprise webclient. In the manifest, it replaces\n * the community main.js to load a different webclient class\n * (WebClientEnterprise instead of WebClient)\n */\nstartWebClient(WebClientEnterprise);\n", "import { mountComponent } from \"./env\";\nimport { localization } from \"@web/core/l10n/localization\";\nimport { session } from \"@web/session\";\nimport { hasTouch } from \"@web/core/browser/feature_detection\";\nimport { user } from \"@web/core/user\";\nimport { Component, whenReady } from \"@odoo/owl\";\nimport { rpc } from \"./core/network/rpc\";\nimport { RPCCache } from \"./core/network/rpc_cache\";\n\n// Chrome iOS wraps some text nodes (like measures, email...)\n// with a `<chrome_annotation>` tag, which breaks OWL rendering.\n// This meta tag allows to disable this behavior.\nconst chromeMetaTag = document.createElement(\"meta\");\nchromeMetaTag.setAttribute(\"name\", \"chrome\");\nchromeMetaTag.setAttribute(\"content\", \"nointentdetection\");\ndocument.head.appendChild(chromeMetaTag);\n\n/**\n * Function to start a webclient.\n * It is used both in community and enterprise in main.js.\n * It's meant to be webclient flexible so we can have a subclass of\n * webclient in enterprise with added features.\n *\n * @param {Component} Webclient\n */\nexport async function startWebClient(Webclient) {\n    odoo.info = {\n        db: session.db,\n        server_version: session.server_version,\n        server_version_info: session.server_version_info,\n        isEnterprise: session.server_version_info.slice(-1)[0] === \"e\",\n    };\n    odoo.isReady = false;\n\n    if (window.isSecureContext && session.browser_cache_secret) {\n        rpc.setCache(new RPCCache(\"rpc\", session.registry_hash, session.browser_cache_secret));\n    }\n\n    await whenReady();\n    const app = await mountComponent(Webclient, document.body, { name: \"Odoo Web Client\" });\n    const { env } = app;\n    Component.env = env;\n\n    const classList = document.body.classList;\n    if (localization.direction === \"rtl\") {\n        classList.add(\"o_rtl\");\n    }\n    if (user.userId === 1) {\n        classList.add(\"o_is_superuser\");\n    }\n    if (env.debug) {\n        classList.add(\"o_debug\");\n    }\n    if (hasTouch()) {\n        classList.add(\"o_touch_device\");\n    }\n    // delete odoo.debug; // FIXME: some legacy code rely on this\n    odoo.isReady = true;\n}\n"], "file": "/web/assets/edf0289/web.assets_web.js", "sourceRoot": "../../../"}